diff --git a/internal/server/crud.go b/internal/server/crud.go index 4cb297a9..ced46fa9 100644 --- a/internal/server/crud.go +++ b/internal/server/crud.go @@ -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 } diff --git a/internal/server/scripts.go b/internal/server/scripts.go index 84f9a713..d1948d21 100644 --- a/internal/server/scripts.go +++ b/internal/server/scripts.go @@ -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": diff --git a/internal/server/server.go b/internal/server/server.go index 1730362e..7b1a2781 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -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": diff --git a/internal/server/stats.go b/internal/server/stats.go index 87238561..4c8dcc4b 100644 --- a/internal/server/stats.go +++ b/internal/server/stats.go @@ -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) { diff --git a/tests/keys_test.go b/tests/keys_test.go index b2932816..7a698e09 100644 --- a/tests/keys_test.go +++ b/tests/keys_test.go @@ -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(""), + 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(""), + + // 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(""), + + // 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(""), + 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(""), + + // 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(""), + + // 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(""), + 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(""), + Do("SET", "mykey", "myid", "NX", "HASH", "9my5xp7").OK(), + Do("SET", "mykey", "myid", "XX", "HASH", "9my5xp7").OK(), + Do("SET", "mykey", "myid", "NX", "HASH", "9my5xp7").Err(""), + 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()) } diff --git a/tests/mock_io_test.go b/tests/mock_io_test.go index 72a24e6b..90ed6960 100644 --- a/tests/mock_io_test.go +++ b/tests/mock_io_test.go @@ -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")