Skip to content

Commit

Permalink
feat: remove eval lua support
Browse files Browse the repository at this point in the history
  • Loading branch information
xgzlucario committed Nov 30, 2024
1 parent 53d3195 commit f9a2818
Show file tree
Hide file tree
Showing 12 changed files with 44 additions and 158 deletions.
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ This is rotom, a high performance, low latency tiny Redis Server written in Go.
3. Implements data structures such as dict, list, map, zipmap, set, zipset, and zset.
4. Supports RDB & AOF.
5. Supports 20+ commonly used commands.
6. Supports execute lua scripts.

## AELoop

Expand All @@ -37,12 +36,12 @@ In rotom, the AeLoop event loop mechanism in Redis is replicated, specifically:
## Data Structures

Rotom has made several optimizations in data structures:

s
- dict: Rotom uses `stdmap` as the db hash table, with built-in progressive rehashing.
- hash: Uses `zipmap` when the hash is small and `hashmap` when it is large.
- hash: Based on `zipmap` with higher memory efficiency.
- set: Uses `zipset` when the set is small and `mapset` when it is large.
- list: Uses a `quicklist` based on `listpack` for a doubly linked list.
- zset: Based on `hashmap` + `rbtree`.
- zset: Uses `zipzset` when small and `hash` + `skiplist` when it is large.

Notably, `zipmap` and `zipset` are space-efficient data structures based on `listpack`, which is a new compressed list proposed by Redis to replace `ziplist`, supporting both forward and reverse traversal and solving the cascading update issue in `ziplist`.

Expand Down
5 changes: 2 additions & 3 deletions README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
3. 实现了 dict, list, map, zipmap, set, zipset, zset 数据结构
4. RDB 和 AOF 持久化支持
5. 支持 20 多种常用命令
6. 支持执行 lua 脚本

## AELoop 事件循环

Expand All @@ -39,10 +38,10 @@ AeLoop(Async Event Loop) 是 Redis 的核心异步事件驱动机制,主要有
rotom 在数据结构上做了许多优化:

- dict:rotom 使用 `stdmap` 作为 db 的哈希表,自带渐进式 rehash 功能
- hash:当 hash 较小时使用 `zipmap`较大时使用 `hashmap`
- hash:基于 `zipmap`拥有更高的内存效率
- set:当 set 较小时使用 `zipset`,较大时使用 `mapset`
- list:使用基于 `listpack` 的双向链表 `quicklist`
- zset:基于 `hashmap` + `rbtree`
- zset:当 zset 较小时使用 `zipzset`,较大时使用 `hash` + `skiplist`

值得一提的是,`zipmap``zipset` 是空间紧凑的数据结构,它们都基于 `listpack`, 这是 Redis 提出的替代 `ziplist` 的新型压缩列表,支持正序及逆序遍历,解决了 `ziplist` 存在级联更新的问题。

Expand Down
4 changes: 2 additions & 2 deletions bench.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ IMAGE_NAME=$TEST_NAME

OUTPUT_FILE="output/$TEST_NAME"

COMMANDS="set,get,incr,lpush,rpush,lrange,hset,sadd,zadd"
COMMANDS="set,get,incr,lpush,rpush,hset,sadd,zadd"

PIPELINES=(1 10 50)
PIPELINES=(1 100 1000)

mkdir -p output

Expand Down
45 changes: 4 additions & 41 deletions command.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"github.com/xgzlucario/rotom/internal/hash"
"github.com/xgzlucario/rotom/internal/list"
"github.com/xgzlucario/rotom/internal/zset"
lua "github.com/yuin/gopher-lua"
)

var (
Expand Down Expand Up @@ -61,7 +60,6 @@ var cmdTable = []*Command{
{"zrank", zrankCommand, 2, false},
{"zpopmin", zpopminCommand, 1, true},
{"zrange", zrangeCommand, 3, false},
{"eval", evalCommand, 2, true},
{"ping", pingCommand, 0, false},
{"hello", helloCommand, 0, false},
{"flushdb", flushdbCommand, 0, true},
Expand Down Expand Up @@ -130,7 +128,7 @@ func setCommand(writer *resp.Writer, args []resp.RESP) {

// NX
} else if equalFold(arg, NX) {
if _, ttl := db.dict.Get(key); ttl != KEY_NOT_EXIST {
if _, ttl := db.dict.Get(key); ttl != KeyNotExist {
writer.WriteNull()
return
}
Expand All @@ -149,7 +147,7 @@ func setCommand(writer *resp.Writer, args []resp.RESP) {
func incrCommand(writer *resp.Writer, args []resp.RESP) {
key := args[0].ToString()
object, ttl := db.dict.Get(key)
if ttl == KEY_NOT_EXIST {
if ttl == KeyNotExist {
object = 0
}
switch v := object.(type) {
Expand All @@ -175,7 +173,7 @@ func incrCommand(writer *resp.Writer, args []resp.RESP) {
func getCommand(writer *resp.Writer, args []resp.RESP) {
key := args[0].ToStringUnsafe()
object, ttl := db.dict.Get(key)
if ttl == KEY_NOT_EXIST {
if ttl == KeyNotExist {
writer.WriteNull()
return
}
Expand Down Expand Up @@ -601,41 +599,6 @@ func saveCommand(writer *resp.Writer, _ []resp.RESP) {
writer.WriteSString("OK")
}

func evalCommand(writer *resp.Writer, args []resp.RESP) {
L := server.lua
script := args[0].ToString()

if err := L.DoString(script); err != nil {
writer.WriteError(err)
return
}

var serialize func(isRoot bool, ret lua.LValue)
serialize = func(isRoot bool, ret lua.LValue) {
switch res := ret.(type) {
case lua.LString:
writer.WriteBulkString(res.String())

case lua.LNumber:
writer.WriteInteger(int(res))

case *lua.LTable:
writer.WriteArrayHead(res.Len())
res.ForEach(func(index, value lua.LValue) {
serialize(false, value)
})

default:
writer.WriteNull()
}

if isRoot && ret.Type() != lua.LTNil {
L.Pop(1)
}
}
serialize(true, L.Get(-1))
}

func fetchMap(key []byte, setnx ...bool) (Map, error) {
return fetch(key, func() Map { return hash.New() }, setnx...)
}
Expand All @@ -654,7 +617,7 @@ func fetchZSet(key []byte, setnx ...bool) (ZSet, error) {

func fetch[T any](key []byte, new func() T, setnx ...bool) (T, error) {
object, ttl := db.dict.Get(b2s(key))
if ttl != KEY_NOT_EXIST {
if ttl != KeyNotExist {
v, ok := object.(T)
if !ok {
return v, errWrongType
Expand Down
72 changes: 23 additions & 49 deletions command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,38 +165,39 @@ func testCommand(t *testing.T, testType string, rdb *redis.Client, sleepFn func(
})

t.Run("hash", func(t *testing.T) {
// hset
res, _ := rdb.HSet(ctx, "map", "k1", "v1").Result()
ast.Equal(res, int64(1))

res, _ = rdb.HSet(ctx, "map", "k2", "v2", "k3", "v3").Result()
ast.Equal(res, int64(2))

res, _ = rdb.HSet(ctx, "map", map[string]any{"k4": "v4", "k5": "v5"}).Result()
ast.Equal(res, int64(2))
var keys, vals []string
for i := 0; i < 1000; i++ {
keys = append(keys, fmt.Sprintf("key-%08d", i))
vals = append(vals, fmt.Sprintf("val-%08d", i))
}

res, _ = rdb.HSet(ctx, "map", map[string]any{"k4": "v4", "k5": "v5"}).Result()
ast.Equal(res, int64(0))
// hset
args := make([]string, 0, len(keys)+len(vals))
for i, k := range keys {
args = append(args, k)
args = append(args, vals[i])
}
res, err := rdb.HSet(ctx, "map", args).Result()
ast.Equal(res, int64(1000))
ast.Nil(err)

// hget
{
res, _ := rdb.HGet(ctx, "map", "k1").Result()
ast.Equal(res, "v1")

res, err := rdb.HGet(ctx, "map", "k99").Result()
ast.Equal(err, redis.Nil)
ast.Equal(res, "")
for i, k := range keys {
res, err := rdb.HGet(ctx, "map", k).Result()
ast.Equal(res, vals[i])
ast.Nil(err)
}

// hgetall
resm, _ := rdb.HGetAll(ctx, "map").Result()
ast.Equal(resm, map[string]string{"k1": "v1", "k2": "v2", "k3": "v3", "k4": "v4", "k5": "v5"})
ast.Equal(len(resm), 1000)

// hdel
res, _ = rdb.HDel(ctx, "map", "k1", "k2", "k3", "k99").Result()
ast.Equal(res, int64(3))
res, _ = rdb.HDel(ctx, "map", keys[0:10]...).Result()
ast.Equal(res, int64(10))

// error hset
_, err := rdb.HSet(ctx, "map").Result()
_, err = rdb.HSet(ctx, "map").Result()
ast.Contains(err.Error(), errWrongArguments.Error())

_, err = rdb.HSet(ctx, "map", "k1", "v1", "k2").Result()
Expand Down Expand Up @@ -424,33 +425,6 @@ func testCommand(t *testing.T, testType string, rdb *redis.Client, sleepFn func(
ast.Equal(err.Error(), errWrongType.Error())
})

t.Run("eval", func(t *testing.T) {
res, _ := rdb.Eval(ctx, "return {'key1','key2','key3'}", nil).Result()
ast.Equal(res, []any{"key1", "key2", "key3"})

res, _ = rdb.Eval(ctx, "return {1,2,3}", nil).Result()
ast.Equal(res, []any{int64(1), int64(2), int64(3)})

// set
_, err := rdb.Eval(ctx, "redis.call('set','xgz','qwe')", nil).Result()
ast.Equal(err, redis.Nil)

res, _ = rdb.Eval(ctx, "return redis.call('set','xgz','qwe')", nil).Result()
ast.Equal(res, "OK")

// get
res, _ = rdb.Eval(ctx, "return redis.call('get','xgz')", nil).Result()
ast.Equal(res, "qwe")

// get nil
_, err = rdb.Eval(ctx, "return redis.call('get','not-ex-evalkey')", nil).Result()
ast.Equal(err, redis.Nil)

// error call
_, err = rdb.Eval(ctx, "return redis.call('myfunc','key')", nil).Result()
ast.NotNil(err)
})

t.Run("flushdb", func(t *testing.T) {
rdb.Set(ctx, "test-flush", "1", 0)
res, _ := rdb.FlushDB(ctx).Result()
Expand Down
4 changes: 2 additions & 2 deletions const.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ const (
)

const (
KeepTTL = redis.KeepTTL
KEY_NOT_EXIST = -2
KeepTTL = redis.KeepTTL
KeyNotExist = -2
)

const (
Expand Down
4 changes: 2 additions & 2 deletions dict.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func (dict *Dict) Get(key string) (any, int) {
data, ok := dict.data.Get(key)
if !ok {
// key not exist
return nil, KEY_NOT_EXIST
return nil, KeyNotExist
}

ts, ok := dict.expire.Get(key)
Expand All @@ -34,7 +34,7 @@ func (dict *Dict) Get(key string) (any, int) {
now := time.Now().UnixNano()
if ts < now {
dict.delete(key)
return nil, KEY_NOT_EXIST
return nil, KeyNotExist
}

return data, int(ts-now) / int(time.Second)
Expand Down
4 changes: 2 additions & 2 deletions dict_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func TestDict(t *testing.T) {

data, ttl = dict.Get("none")
assert.Nil(data)
assert.Equal(ttl, KEY_NOT_EXIST)
assert.Equal(ttl, KeyNotExist)
})

t.Run("setTTL", func(t *testing.T) {
Expand All @@ -41,7 +41,7 @@ func TestDict(t *testing.T) {

// get expired
data, ttl = dict.Get("key")
assert.Equal(ttl, KEY_NOT_EXIST)
assert.Equal(ttl, KeyNotExist)
assert.Nil(data)

// setTTL expired
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.23

require (
github.com/alicebob/miniredis/v2 v2.33.0
github.com/bytedance/sonic v1.12.4
github.com/bytedance/sonic v1.12.5
github.com/chen3feng/stl4go v0.1.1
github.com/cockroachdb/swiss v0.0.0-20240612210725-f4de07ae6964
github.com/deckarep/golang-set/v2 v2.6.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bytedance/sonic v1.12.4 h1:9Csb3c9ZJhfUWeMtpCDCq6BUoH5ogfDFLUgQ/jG+R0k=
github.com/bytedance/sonic v1.12.4/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
github.com/bytedance/sonic v1.12.5 h1:hoZxY8uW+mT+OpkcUWw4k0fDINtOcVavEsGfzwzFU/w=
github.com/bytedance/sonic v1.12.5/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E=
github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
Expand Down
43 changes: 0 additions & 43 deletions lua.go

This file was deleted.

8 changes: 1 addition & 7 deletions rotom.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ func freeClient(client *Client) {
delete(server.clients, client.fd)
server.aeLoop.ModDetach(client.fd)
_ = net.Close(client.fd)
log.Info().Msgf("free client %d", client.fd)
}

func ProcessQueryBuf(client *Client) {
Expand Down Expand Up @@ -240,13 +241,6 @@ func initServer(config *Config) (err error) {
if err != nil {
return err
}
// init lua state
L := lua.NewState()
L.Push(L.NewFunction(OpenRedis))
L.Push(lua.LString("redis"))
L.Call(1, 0)
server.lua = L

return nil
}

Expand Down

0 comments on commit f9a2818

Please sign in to comment.