Better SET/PERSIST/TTL/STATS tests

This commit is contained in:
tidwall 2022-09-23 15:29:46 -07:00
parent 7fa2dc4419
commit 295a9c45a8
6 changed files with 206 additions and 161 deletions

View File

@ -2,7 +2,7 @@ package server
import (
"bytes"
"errors"
"math"
"strconv"
"strings"
"time"
@ -706,23 +706,22 @@ func (s *Server) cmdSET(msg *Message) (resp.Value, commandDetails, error) {
return retwerr(errInvalidArgument(args[i]))
}
}
if oobj == nil {
return retwerr(errInvalidNumberOfArguments)
}
// >> Operation
nada := func() (resp.Value, commandDetails, error) {
// exclude operation due to 'xx' or 'nx' match
switch msg.OutputType {
default:
case JSON:
if msg.OutputType == JSON {
if nx {
return retwerr(errIDAlreadyExists)
} else {
return retwerr(errIDNotFound)
}
case RESP:
return resp.NullValue(), commandDetails{}, nil
}
return retwerr(errors.New("nada unknown output"))
return resp.NullValue(), commandDetails{}, nil
}
col, ok := s.cols.Get(key)
@ -931,11 +930,17 @@ func (s *Server) cmdEXPIRE(msg *Message) (resp.Value, commandDetails, error) {
// PERSIST key id
func (s *Server) cmdPERSIST(msg *Message) (resp.Value, commandDetails, error) {
start := time.Now()
// >> Args
args := msg.Args
if len(args) != 3 {
return retwerr(errInvalidNumberOfArguments)
}
key, id := args[1], args[2]
// >> Operation
col, _ := s.cols.Get(key)
if col == nil {
if msg.OutputType == RESP {
@ -959,6 +964,8 @@ func (s *Server) cmdPERSIST(msg *Message) (resp.Value, commandDetails, error) {
cleared = true
}
// >> Response
var res resp.Value
var d commandDetails
@ -985,62 +992,47 @@ func (s *Server) cmdPERSIST(msg *Message) (resp.Value, commandDetails, error) {
// TTL key id
func (s *Server) cmdTTL(msg *Message) (resp.Value, error) {
start := time.Now()
// >> Args
args := msg.Args
if len(args) != 3 {
return retrerr(errInvalidNumberOfArguments)
}
key, id := args[1], args[2]
var v float64
var ok bool
var ok2 bool
// >> Operation
col, _ := s.cols.Get(key)
if col != nil {
o := col.Get(id)
ok = o != nil
if ok {
if o.Expires() != 0 {
now := start.UnixNano()
if now > o.Expires() {
ok2 = false
} else {
v = float64(o.Expires()-now) / float64(time.Second)
if v < 0 {
v = 0
}
ok2 = true
}
}
if col == nil {
if msg.OutputType == JSON {
return retrerr(errKeyNotFound)
}
return resp.IntegerValue(-2), nil
}
var res resp.Value
switch msg.OutputType {
case JSON:
if ok {
var ttl string
if ok2 {
ttl = strconv.FormatFloat(v, 'f', -1, 64)
} else {
ttl = "-1"
}
res = resp.SimpleStringValue(
`{"ok":true,"ttl":` + ttl + `,"elapsed":"` +
time.Since(start).String() + "\"}")
} else {
if col == nil {
return retrerr(errKeyNotFound)
}
o := col.Get(id)
if o == nil {
if msg.OutputType == JSON {
return retrerr(errIDNotFound)
}
case RESP:
if ok {
if ok2 {
res = resp.IntegerValue(int(v))
} else {
res = resp.IntegerValue(-1)
}
} else {
res = resp.IntegerValue(-2)
}
return resp.IntegerValue(-2), nil
}
return res, nil
var ttl float64
if o.Expires() == 0 {
ttl = -1
} else {
now := start.UnixNano()
ttl = math.Max(float64(o.Expires()-now)/float64(time.Second), 0)
}
// >> Response
if msg.OutputType == JSON {
return resp.SimpleStringValue(
`{"ok":true,"ttl":` + strconv.Itoa(int(ttl)) + `,"elapsed":"` +
time.Since(start).String() + "\"}"), nil
}
return resp.IntegerValue(int(ttl)), nil
}

View File

@ -612,7 +612,7 @@ func (s *Server) commandInScript(msg *Message) (
case "ttl":
res, err = s.cmdTTL(msg)
case "stats":
res, err = s.cmdStats(msg)
res, err = s.cmdSTATS(msg)
case "scan":
res, err = s.cmdScan(msg)
case "nearby":

View File

@ -1080,7 +1080,7 @@ func (s *Server) command(msg *Message, client *Client) (
case "readonly":
res, err = s.cmdReadOnly(msg)
case "stats":
res, err = s.cmdStats(msg)
res, err = s.cmdSTATS(msg)
case "server":
res, err = s.cmdServer(msg)
case "healthz":

View File

@ -45,22 +45,23 @@ func readMemStats() runtime.MemStats {
return ms
}
func (s *Server) cmdStats(msg *Message) (res resp.Value, err error) {
// STATS key [key...]
func (s *Server) cmdSTATS(msg *Message) (resp.Value, error) {
start := time.Now()
vs := msg.Args[1:]
var ms = []map[string]interface{}{}
if len(vs) == 0 {
return NOMessage, errInvalidNumberOfArguments
// >> Args
args := msg.Args
if len(args) < 2 {
return retrerr(errInvalidNumberOfArguments)
}
// >> Operation
var vals []resp.Value
var key string
var ok bool
for {
vs, key, ok = tokenval(vs)
if !ok {
break
}
var ms = []map[string]interface{}{}
for i := 1; i < len(args); i++ {
key := args[i]
col, _ := s.cols.Get(key)
if col != nil {
m := make(map[string]interface{})
@ -83,18 +84,15 @@ func (s *Server) cmdStats(msg *Message) (res resp.Value, err error) {
}
}
}
switch msg.OutputType {
case JSON:
data, err := json.Marshal(ms)
if err != nil {
return NOMessage, err
}
res = resp.StringValue(`{"ok":true,"stats":` + string(data) + `,"elapsed":"` + time.Since(start).String() + "\"}")
case RESP:
res = resp.ArrayValue(vals)
// >> Response
if msg.OutputType == JSON {
data, _ := json.Marshal(ms)
return resp.StringValue(`{"ok":true,"stats":` + string(data) +
`,"elapsed":"` + time.Since(start).String() + "\"}"), nil
}
return res, nil
return resp.ArrayValue(vals), nil
}
func (s *Server) cmdHealthz(msg *Message) (res resp.Value, err error) {

View File

@ -249,94 +249,149 @@ func keys_KEYS_test(mc *mockServer) error {
})
}
func keys_PERSIST_test(mc *mockServer) error {
return mc.DoBatch([][]interface{}{
{"SET", "mykey", "myid", "STRING", "value"}, {"OK"},
{"EXPIRE", "mykey", "myid", 2}, {1},
{"PERSIST", "mykey", "myid"}, {1},
{"PERSIST", "mykey", "myid"}, {0},
})
return mc.DoBatch(
Do("SET", "mykey", "myid", "STRING", "value").OK(),
Do("EXPIRE", "mykey", "myid", 2).Str("1"),
Do("PERSIST", "mykey", "myid").Str("1"),
Do("PERSIST", "mykey", "myid").Str("0"),
Do("PERSIST", "mykey").Err("wrong number of arguments for 'persist' command"),
Do("PERSIST", "mykey2", "myid").Str("0"),
Do("PERSIST", "mykey2", "myid").JSON().Err("key not found"),
Do("PERSIST", "mykey", "myid2").Str("0"),
Do("PERSIST", "mykey", "myid2").JSON().Err("id not found"),
Do("EXPIRE", "mykey", "myid", 2).Str("1"),
Do("PERSIST", "mykey", "myid").JSON().OK(),
)
}
func keys_SET_test(mc *mockServer) error {
return mc.DoBatch(
"point", [][]interface{}{
{"SET", "mykey", "myid", "POINT", 33, -115}, {"OK"},
{"GET", "mykey", "myid", "POINT"}, {"[33 -115]"},
{"GET", "mykey", "myid", "BOUNDS"}, {"[[33 -115] [33 -115]]"},
{"GET", "mykey", "myid", "OBJECT"}, {`{"type":"Point","coordinates":[-115,33]}`},
{"GET", "mykey", "myid", "HASH", 7}, {"9my5xp7"},
{"DEL", "mykey", "myid"}, {"1"},
{"GET", "mykey", "myid"}, {nil},
},
"object", [][]interface{}{
{"SET", "mykey", "myid", "OBJECT", `{"type":"Point","coordinates":[-115,33]}`}, {"OK"},
{"GET", "mykey", "myid", "POINT"}, {"[33 -115]"},
{"GET", "mykey", "myid", "BOUNDS"}, {"[[33 -115] [33 -115]]"},
{"GET", "mykey", "myid", "OBJECT"}, {`{"type":"Point","coordinates":[-115,33]}`},
{"GET", "mykey", "myid", "HASH", 7}, {"9my5xp7"},
{"DEL", "mykey", "myid"}, {"1"},
{"GET", "mykey", "myid"}, {nil},
},
"bounds", [][]interface{}{
{"SET", "mykey", "myid", "BOUNDS", 33, -115, 33, -115}, {"OK"},
{"GET", "mykey", "myid", "POINT"}, {"[33 -115]"},
{"GET", "mykey", "myid", "BOUNDS"}, {"[[33 -115] [33 -115]]"},
{"GET", "mykey", "myid", "OBJECT"}, {`{"type":"Point","coordinates":[-115,33]}`},
{"GET", "mykey", "myid", "HASH", 7}, {"9my5xp7"},
{"DEL", "mykey", "myid"}, {"1"},
{"GET", "mykey", "myid"}, {nil},
},
"hash", [][]interface{}{
{"SET", "mykey", "myid", "HASH", "9my5xp7"}, {"OK"},
{"GET", "mykey", "myid", "HASH", 7}, {"9my5xp7"},
{"DEL", "mykey", "myid"}, {"1"},
{"GET", "mykey", "myid"}, {nil},
},
"field", [][]interface{}{
{"SET", "mykey", "myid", "FIELD", "f1", 33, "FIELD", "a2", 44.5, "HASH", "9my5xp7"}, {"OK"},
{"GET", "mykey", "myid", "WITHFIELDS", "HASH", 7}, {"[9my5xp7 [a2 44.5 f1 33]]"},
{"FSET", "mykey", "myid", "f1", 0}, {1},
{"FSET", "mykey", "myid", "f1", 0}, {0},
{"GET", "mykey", "myid", "WITHFIELDS", "HASH", 7}, {"[9my5xp7 [a2 44.5]]"},
{"DEL", "mykey", "myid"}, {"1"},
{"GET", "mykey", "myid"}, {nil},
},
"string", [][]interface{}{
{"SET", "mykey", "myid", "STRING", "value"}, {"OK"},
{"GET", "mykey", "myid"}, {"value"},
{"SET", "mykey", "myid", "STRING", "value2"}, {"OK"},
{"GET", "mykey", "myid"}, {"value2"},
{"DEL", "mykey", "myid"}, {"1"},
{"GET", "mykey", "myid"}, {nil},
},
// Section: point
Do("SET", "mykey", "myid", "POINT", 33, -115).OK(),
Do("GET", "mykey", "myid", "POINT").Str("[33 -115]"),
Do("GET", "mykey", "myid", "BOUNDS").Str("[[33 -115] [33 -115]]"),
Do("GET", "mykey", "myid", "OBJECT").Str(`{"type":"Point","coordinates":[-115,33]}`),
Do("GET", "mykey", "myid", "HASH", 7).Str("9my5xp7"),
Do("DEL", "mykey", "myid").Str("1"),
Do("GET", "mykey", "myid").Str("<nil>"),
Do("SET", "mykey", "myid", "point", "33", "-112", "99").OK(),
// Section: object
Do("SET", "mykey", "myid", "OBJECT", `{"type":"Point","coordinates":[-115,33]}`).OK(),
Do("GET", "mykey", "myid", "POINT").Str("[33 -115]"),
Do("GET", "mykey", "myid", "BOUNDS").Str("[[33 -115] [33 -115]]"),
Do("GET", "mykey", "myid", "OBJECT").Str(`{"type":"Point","coordinates":[-115,33]}`),
Do("GET", "mykey", "myid", "HASH", 7).Str("9my5xp7"),
Do("DEL", "mykey", "myid").Str("1"),
Do("GET", "mykey", "myid").Str("<nil>"),
// Section: bounds
Do("SET", "mykey", "myid", "BOUNDS", 33, -115, 33, -115).OK(),
Do("GET", "mykey", "myid", "POINT").Str("[33 -115]"),
Do("GET", "mykey", "myid", "BOUNDS").Str("[[33 -115] [33 -115]]"),
Do("GET", "mykey", "myid", "OBJECT").Str(`{"type":"Point","coordinates":[-115,33]}`),
Do("GET", "mykey", "myid", "HASH", 7).Str("9my5xp7"),
Do("DEL", "mykey", "myid").Str("1"),
Do("GET", "mykey", "myid").Str("<nil>"),
// Section: hash
Do("SET", "mykey", "myid", "HASH", "9my5xp7").OK(),
Do("GET", "mykey", "myid", "HASH", 7).Str("9my5xp7"),
Do("DEL", "mykey", "myid").Str("1"),
Do("GET", "mykey", "myid").Str("<nil>"),
Do("SET", "mykey", "myid", "HASH", "9my5xp7").JSON().OK(),
// Section: field
Do("SET", "mykey", "myid", "FIELD", "f1", 33, "FIELD", "a2", 44.5, "HASH", "9my5xp7").OK(),
Do("GET", "mykey", "myid", "WITHFIELDS", "HASH", 7).Str("[9my5xp7 [a2 44.5 f1 33]]"),
Do("FSET", "mykey", "myid", "f1", 0).Str("1"),
Do("FSET", "mykey", "myid", "f1", 0).Str("0"),
Do("GET", "mykey", "myid", "WITHFIELDS", "HASH", 7).Str("[9my5xp7 [a2 44.5]]"),
Do("DEL", "mykey", "myid").Str("1"),
Do("GET", "mykey", "myid").Str("<nil>"),
// Section: string
Do("SET", "mykey", "myid", "STRING", "value").OK(),
Do("GET", "mykey", "myid").Str("value"),
Do("SET", "mykey", "myid", "STRING", "value2").OK(),
Do("GET", "mykey", "myid").Str("value2"),
Do("DEL", "mykey", "myid").Str("1"),
Do("GET", "mykey", "myid").Str("<nil>"),
// Test error conditions
Do("CONFIG", "SET", "maxmemory", "1").OK(),
Do("SET", "mykey", "myid", "STRING", "value2").Err("OOM command not allowed when used memory > 'maxmemory'"),
Do("CONFIG", "SET", "maxmemory", "0").OK(),
Do("SET").Err("wrong number of arguments for 'set' command"),
Do("SET", "mykey", "myid", "FIELD", "f1").Err("wrong number of arguments for 'set' command"),
Do("SET", "mykey", "myid", "FIELD", "z", "1").Err("invalid argument 'z'"),
Do("SET", "mykey", "myid", "EX").Err("wrong number of arguments for 'set' command"),
Do("SET", "mykey", "myid", "EX", "yyy").Err("invalid argument 'yyy'"),
Do("SET", "mykey", "myid", "EX", "123").Err("wrong number of arguments for 'set' command"),
Do("SET", "mykey", "myid", "nx").Err("wrong number of arguments for 'set' command"),
Do("SET", "mykey", "myid", "nx", "xx").Err("invalid argument 'xx'"),
Do("SET", "mykey", "myid", "xx", "nx").Err("invalid argument 'nx'"),
Do("SET", "mykey", "myid", "string").Err("wrong number of arguments for 'set' command"),
Do("SET", "mykey", "myid", "point").Err("wrong number of arguments for 'set' command"),
Do("SET", "mykey", "myid", "point", "33").Err("wrong number of arguments for 'set' command"),
Do("SET", "mykey", "myid", "point", "33f", "-112").Err("invalid argument '33f'"),
Do("SET", "mykey", "myid", "point", "33", "-112f").Err("invalid argument '-112f'"),
Do("SET", "mykey", "myid", "point", "33", "-112f", "99").Err("invalid argument '-112f'"),
Do("SET", "mykey", "myid", "bounds").Err("wrong number of arguments for 'set' command"),
Do("SET", "mykey", "myid", "bounds", "fff", "1", "2", "3").Err("invalid argument 'fff'"),
Do("SET", "mykey", "myid", "hash").Err("wrong number of arguments for 'set' command"),
Do("SET", "mykey", "myid", "object").Err("wrong number of arguments for 'set' command"),
Do("SET", "mykey", "myid", "object", "asd").Err("invalid data"),
Do("SET", "mykey", "myid", "joint").Err("invalid argument 'joint'"),
Do("SET", "mykey", "myid", "XX", "HASH", "9my5xp7").Err("<nil>"),
Do("SET", "mykey", "myid", "XX", "HASH", "9my5xp7").JSON().Err("id not found"),
Do("SET", "mykey", "myid1", "HASH", "9my5xp7").OK(),
Do("SET", "mykey", "myid", "XX", "HASH", "9my5xp7").Err("<nil>"),
Do("SET", "mykey", "myid", "NX", "HASH", "9my5xp7").OK(),
Do("SET", "mykey", "myid", "XX", "HASH", "9my5xp7").OK(),
Do("SET", "mykey", "myid", "NX", "HASH", "9my5xp7").Err("<nil>"),
Do("SET", "mykey", "myid", "NX", "HASH", "9my5xp7").JSON().Err("id already exists"),
)
}
func keys_STATS_test(mc *mockServer) error {
return mc.DoBatch([][]interface{}{
{"STATS", "mykey"}, {"[nil]"},
{"SET", "mykey", "myid", "STRING", "value"}, {"OK"},
{"STATS", "mykey"}, {"[[in_memory_size 9 num_objects 1 num_points 0 num_strings 1]]"},
{"SET", "mykey", "myid2", "STRING", "value"}, {"OK"},
{"STATS", "mykey"}, {"[[in_memory_size 19 num_objects 2 num_points 0 num_strings 2]]"},
{"SET", "mykey", "myid3", "OBJECT", `{"type":"Point","coordinates":[-115,33]}`}, {"OK"},
{"STATS", "mykey"}, {"[[in_memory_size 40 num_objects 3 num_points 1 num_strings 2]]"},
{"DEL", "mykey", "myid"}, {1},
{"STATS", "mykey"}, {"[[in_memory_size 31 num_objects 2 num_points 1 num_strings 1]]"},
{"DEL", "mykey", "myid3"}, {1},
{"STATS", "mykey"}, {"[[in_memory_size 10 num_objects 1 num_points 0 num_strings 1]]"},
{"STATS", "mykey", "mykey2"}, {"[[in_memory_size 10 num_objects 1 num_points 0 num_strings 1] nil]"},
{"DEL", "mykey", "myid2"}, {1},
{"STATS", "mykey"}, {"[nil]"},
{"STATS", "mykey", "mykey2"}, {"[nil nil]"},
})
return mc.DoBatch(
Do("STATS", "mykey").Str("[nil]"),
Do("SET", "mykey", "myid", "STRING", "value").OK(),
Do("STATS", "mykey").Str("[[in_memory_size 9 num_objects 1 num_points 0 num_strings 1]]"),
Do("STATS", "mykey", "hello").JSON().Str(`{"ok":true,"stats":[{"in_memory_size":9,"num_objects":1,"num_points":0,"num_strings":1},null]}`),
Do("SET", "mykey", "myid2", "STRING", "value").OK(),
Do("STATS", "mykey").Str("[[in_memory_size 19 num_objects 2 num_points 0 num_strings 2]]"),
Do("SET", "mykey", "myid3", "OBJECT", `{"type":"Point","coordinates":[-115,33]}`).OK(),
Do("STATS", "mykey").Str("[[in_memory_size 40 num_objects 3 num_points 1 num_strings 2]]"),
Do("DEL", "mykey", "myid").Str("1"),
Do("STATS", "mykey").Str("[[in_memory_size 31 num_objects 2 num_points 1 num_strings 1]]"),
Do("DEL", "mykey", "myid3").Str("1"),
Do("STATS", "mykey").Str("[[in_memory_size 10 num_objects 1 num_points 0 num_strings 1]]"),
Do("STATS", "mykey", "mykey2").Str("[[in_memory_size 10 num_objects 1 num_points 0 num_strings 1] nil]"),
Do("DEL", "mykey", "myid2").Str("1"),
Do("STATS", "mykey").Str("[nil]"),
Do("STATS", "mykey", "mykey2").Str("[nil nil]"),
)
}
func keys_TTL_test(mc *mockServer) error {
return mc.DoBatch([][]interface{}{
{"SET", "mykey", "myid", "STRING", "value"}, {"OK"},
{"EXPIRE", "mykey", "myid", 2}, {1},
{time.Second / 4}, {}, // sleep
{"TTL", "mykey", "myid"}, {1},
})
return mc.DoBatch(
Do("SET", "mykey", "myid", "STRING", "value").OK(),
Do("EXPIRE", "mykey", "myid", 2).Str("1"),
Do("EXPIRE", "mykey", "myid", 2).JSON().OK(),
Sleep(time.Millisecond*10),
Do("TTL", "mykey", "myid").Str("1"),
Do("EXPIRE", "mykey", "myid", 1).Str("1"),
Sleep(time.Millisecond*10),
Do("TTL", "mykey", "myid").Str("0"),
Do("TTL", "mykey", "myid").JSON().Str(`{"ok":true,"ttl":0}`),
Do("TTL", "mykey2", "myid").Str("-2"),
Do("TTL", "mykey", "myid2").Str("-2"),
Do("TTL", "mykey").Err("wrong number of arguments for 'ttl' command"),
Do("SET", "mykey", "myid", "STRING", "value").OK(),
Do("TTL", "mykey", "myid").Str("-1"),
Do("TTL", "mykey2", "myid").JSON().Err("key not found"),
Do("TTL", "mykey", "myid2").JSON().Err("id not found"),
)
}
func keys_SET_EX_test(mc *mockServer) (err error) {
@ -465,7 +520,7 @@ func keys_FLUSHDB_test(mc *mockServer) error {
Do("SET", "mykey2", "myid1", "POINT", 33, -115).OK(),
Do("SETCHAN", "mychan", "INTERSECTS", "mykey1", "BOUNDS", 10, 10, 10, 10).Str("1"),
Do("KEYS", "*").Str("[mykey1 mykey2]"),
Do("CHANS", "*").JSON().Custom(func(s string) error {
Do("CHANS", "*").JSON().Func(func(s string) error {
if gjson.Get(s, "chans.#").Int() != 1 {
return fmt.Errorf("expected '%d', got '%d'", 1, gjson.Get(s, "chans.#").Int())
}

View File

@ -34,7 +34,7 @@ func (cmd *IO) Str(s string) *IO {
cmd.out = s
return cmd
}
func (cmd *IO) Custom(fn func(s string) error) *IO {
func (cmd *IO) Func(fn func(s string) error) *IO {
cmd.out = func(s string) error {
if cmd.json {
if !gjson.Valid(s) {
@ -47,7 +47,7 @@ func (cmd *IO) Custom(fn func(s string) error) *IO {
}
func (cmd *IO) OK() *IO {
return cmd.Custom(func(s string) error {
return cmd.Func(func(s string) error {
if cmd.json {
if gjson.Get(s, "ok").Type != gjson.True {
return errors.New("not ok")
@ -60,7 +60,7 @@ func (cmd *IO) OK() *IO {
}
func (cmd *IO) Err(msg string) *IO {
return cmd.Custom(func(s string) error {
return cmd.Func(func(s string) error {
if cmd.json {
if gjson.Get(s, "ok").Type != gjson.False {
return errors.New("ok=true")