ledisdb/server/script.go

414 lines
7.0 KiB
Go

// +build lua
package server
import (
"encoding/hex"
"fmt"
"io"
"sync"
"github.com/siddontang/go/hack"
"github.com/siddontang/go/num"
"github.com/siddontang/ledisdb/ledis"
"github.com/siddontang/ledisdb/vendor/lua"
)
//ledis <-> lua type conversion, same as http://redis.io/commands/eval
type luaWriter struct {
l *lua.State
}
func (w *luaWriter) writeError(err error) {
panic(err)
}
func (w *luaWriter) writeStatus(status string) {
w.l.NewTable()
top := w.l.GetTop()
w.l.PushString("ok")
w.l.PushString(status)
w.l.SetTable(top)
}
func (w *luaWriter) writeInteger(n int64) {
w.l.PushInteger(n)
}
func (w *luaWriter) writeBulk(b []byte) {
if b == nil {
w.l.PushBoolean(false)
} else {
w.l.PushString(hack.String(b))
}
}
func (w *luaWriter) writeArray(lst []interface{}) {
if lst == nil {
w.l.PushBoolean(false)
return
}
w.l.CreateTable(len(lst), 0)
top := w.l.GetTop()
for i, _ := range lst {
w.l.PushInteger(int64(i) + 1)
switch v := lst[i].(type) {
case []interface{}:
w.writeArray(v)
case [][]byte:
w.writeSliceArray(v)
case []byte:
w.writeBulk(v)
case nil:
w.writeBulk(nil)
case int64:
w.writeInteger(v)
default:
panic("invalid array type")
}
w.l.SetTable(top)
}
}
func (w *luaWriter) writeSliceArray(lst [][]byte) {
if lst == nil {
w.l.PushBoolean(false)
return
}
w.l.CreateTable(len(lst), 0)
for i, v := range lst {
if v == nil {
w.l.PushBoolean(false)
//TNL: not sure what is "expected" behaviour here.
//Redis returns "false", but may be "nil" is more appropriate?
//For me both variants seems better, then empty string.
//
//w.l.PushNil()
} else {
w.l.PushString(hack.String(v))
}
w.l.RawSeti(-2, i+1)
/*
//TNL: and instead of push nil we can just skip it
if v != nil {
w.l.PushString(hack.String(v))
w.l.RawSeti(-2, i+1)
}
*/
}
}
func (w *luaWriter) writeFVPairArray(lst []ledis.FVPair) {
if lst == nil {
w.l.PushBoolean(false)
return
}
w.l.CreateTable(len(lst)*2, 0)
for i, v := range lst {
w.l.PushString(hack.String(v.Field))
w.l.RawSeti(-2, 2*i+1)
w.l.PushString(hack.String(v.Value))
w.l.RawSeti(-2, 2*i+2)
}
}
func (w *luaWriter) writeScorePairArray(lst []ledis.ScorePair, withScores bool) {
if lst == nil {
w.l.PushBoolean(false)
return
}
if withScores {
w.l.CreateTable(len(lst)*2, 0)
for i, v := range lst {
w.l.PushString(hack.String(v.Member))
w.l.RawSeti(-2, 2*i+1)
w.l.PushString(hack.String(num.FormatInt64ToSlice(v.Score)))
w.l.RawSeti(-2, 2*i+2)
}
} else {
w.l.CreateTable(len(lst), 0)
for i, v := range lst {
w.l.PushString(hack.String(v.Member))
w.l.RawSeti(-2, i+1)
}
}
}
func (w *luaWriter) writeBulkFrom(n int64, rb io.Reader) {
w.writeError(fmt.Errorf("unsupport"))
}
func (w *luaWriter) flush() {
}
type script struct {
sync.Mutex
app *App
l *lua.State
c *client
chunks map[string]struct{}
}
func (app *App) openScript() {
s := new(script)
s.app = app
s.chunks = make(map[string]struct{})
app.script = s
l := lua.NewState()
l.OpenBase()
l.OpenLibs()
l.OpenMath()
l.OpenString()
l.OpenTable()
l.OpenPackage()
l.OpenCJson()
l.OpenCMsgpack()
l.OpenStruct()
l.Register("error", luaErrorHandler)
s.l = l
s.c = newClient(app)
s.c.db = nil
w := new(luaWriter)
w.l = l
s.c.resp = w
l.NewTable()
l.PushString("call")
l.PushGoFunction(luaCall)
l.SetTable(-3)
l.PushString("pcall")
l.PushGoFunction(luaPCall)
l.SetTable(-3)
l.PushString("sha1hex")
l.PushGoFunction(luaSha1Hex)
l.SetTable(-3)
l.PushString("error_reply")
l.PushGoFunction(luaErrorReply)
l.SetTable(-3)
l.PushString("status_reply")
l.PushGoFunction(luaStatusReply)
l.SetTable(-3)
l.SetGlobal("ledis")
setMapState(l, s)
}
func (app *App) closeScript() {
app.script.l.Close()
delMapState(app.script.l)
app.script = nil
}
var mapState = map[*lua.State]*script{}
var stateLock sync.Mutex
func setMapState(l *lua.State, s *script) {
stateLock.Lock()
defer stateLock.Unlock()
mapState[l] = s
}
func getMapState(l *lua.State) *script {
stateLock.Lock()
defer stateLock.Unlock()
return mapState[l]
}
func delMapState(l *lua.State) {
stateLock.Lock()
defer stateLock.Unlock()
delete(mapState, l)
}
func luaErrorHandler(l *lua.State) int {
msg := l.ToString(1)
panic(fmt.Errorf(msg))
}
func luaCall(l *lua.State) int {
return luaCallGenericCommand(l)
}
func luaPCall(l *lua.State) (n int) {
defer func() {
if e := recover(); e != nil {
luaPushError(l, fmt.Sprintf("%v", e))
n = 1
}
return
}()
return luaCallGenericCommand(l)
}
func luaErrorReply(l *lua.State) int {
return luaReturnSingleFieldTable(l, "err")
}
func luaStatusReply(l *lua.State) int {
return luaReturnSingleFieldTable(l, "ok")
}
func luaReturnSingleFieldTable(l *lua.State, filed string) int {
if l.GetTop() != 1 || l.Type(-1) != lua.LUA_TSTRING {
luaPushError(l, "wrong number or type of arguments")
return 1
}
l.NewTable()
l.PushString(filed)
l.PushValue(-3)
l.SetTable(-3)
return 1
}
func luaSha1Hex(l *lua.State) int {
argc := l.GetTop()
if argc != 1 {
luaPushError(l, "wrong number of arguments")
return 1
}
s := l.ToString(1)
s = hex.EncodeToString(hack.Slice(s))
l.PushString(s)
return 1
}
func luaPushError(l *lua.State, msg string) {
l.NewTable()
l.PushString("err")
err := l.NewError(msg)
l.PushString(err.Error())
l.SetTable(-3)
}
func luaCallGenericCommand(l *lua.State) int {
s := getMapState(l)
if s == nil {
panic("Invalid lua call")
} else if s.c.db == nil {
panic("Invalid lua call, not prepared")
}
c := s.c
argc := l.GetTop()
if argc < 1 {
panic("Please specify at least one argument for ledis.call()")
}
c.cmd = l.ToString(1)
c.args = make([][]byte, argc-1)
for i := 2; i <= argc; i++ {
switch l.Type(i) {
case lua.LUA_TNUMBER:
c.args[i-2] = []byte(fmt.Sprintf("%.17g", l.ToNumber(i)))
case lua.LUA_TSTRING:
c.args[i-2] = []byte(l.ToString(i))
default:
panic("Lua ledis() command arguments must be strings or integers")
}
}
c.perform()
return 1
}
func luaSetGlobalArray(l *lua.State, name string, ay [][]byte) {
l.NewTable()
for i := 0; i < len(ay); i++ {
l.PushString(hack.String(ay[i]))
l.RawSeti(-2, i+1)
}
l.SetGlobal(name)
}
func luaReplyToLedisReply(l *lua.State) interface{} {
base := l.GetTop()
defer func() {
l.SetTop(base - 1)
}()
switch l.Type(-1) {
case lua.LUA_TSTRING:
return hack.Slice(l.ToString(-1))
case lua.LUA_TBOOLEAN:
if l.ToBoolean(-1) {
return int64(1)
} else {
return nil
}
case lua.LUA_TNUMBER:
return int64(l.ToInteger(-1))
case lua.LUA_TTABLE:
l.PushString("err")
l.GetTable(-2)
if l.Type(-1) == lua.LUA_TSTRING {
return fmt.Errorf("%s", l.ToString(-1))
}
l.Pop(1)
l.PushString("ok")
l.GetTable(-2)
if l.Type(-1) == lua.LUA_TSTRING {
return l.ToString(-1)
} else {
l.Pop(1)
ay := make([]interface{}, 0)
for i := 1; ; i++ {
l.PushInteger(int64(i))
l.GetTable(-2)
if l.Type(-1) == lua.LUA_TNIL {
l.Pop(1)
break
}
ay = append(ay, luaReplyToLedisReply(l))
}
return ay
}
default:
return nil
}
}