From 72aa5122ed6cea5eab094f63050365abbe735d24 Mon Sep 17 00:00:00 2001 From: David Roman Date: Wed, 30 Aug 2023 00:35:30 -0400 Subject: [PATCH 1/8] add: memory usage --- cmd_server.go | 35 +++++++++++++++++++++++++++++++++++ cmd_server_test.go | 17 +++++++++++++++++ go.mod | 2 ++ go.sum | 8 ++++---- 4 files changed, 58 insertions(+), 4 deletions(-) diff --git a/cmd_server.go b/cmd_server.go index 6e51727b..bac4aa56 100644 --- a/cmd_server.go +++ b/cmd_server.go @@ -6,6 +6,7 @@ import ( "strconv" "strings" + "github.com/DmitriyVTitov/size" "github.com/alicebob/miniredis/v2/server" ) @@ -16,6 +17,40 @@ func commandsServer(m *Miniredis) { m.srv.Register("FLUSHDB", m.cmdFlushdb) m.srv.Register("INFO", m.cmdInfo) m.srv.Register("TIME", m.cmdTime) + m.srv.Register("MEMORY", m.cmdMemory) +} + +// MEMORY +func (m *Miniredis) cmdMemory(c *server.Peer, cmd string, args []string) { + if len(args) == 0 { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + if !m.handleAuth(c) { + return + } + if m.checkPubsub(c, cmd) { + return + } + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + switch args[0] { + case "USAGE": + value, ok := db.stringKeys[args[1]] + if !ok { + c.WriteError(ErrKeyNotFound.Error()) + return + } + c.WriteInt(size.Of(value)) + break + default: + c.WriteError(errWrongNumber(cmd)) + } + + }) } // DBSIZE diff --git a/cmd_server_test.go b/cmd_server_test.go index f95bd356..a6063878 100644 --- a/cmd_server_test.go +++ b/cmd_server_test.go @@ -127,3 +127,20 @@ func TestCmdServerTime(t *testing.T) { proto.Error(errWrongNumber("time")), ) } + +// Test Memory Usage +func TestCmdServerMemoryUsage(t *testing.T) { + s, err := Run() + ok(t, err) + defer s.Close() + c, err := proto.Dial(s.Addr()) + ok(t, err) + defer c.Close() + + c.Do("SET", "test:1", "something") + + mustDo(t, c, + "MEMORY", "USAGE", "test:1", + proto.Int(25), + ) +} diff --git a/go.mod b/go.mod index bbaffb47..a239f335 100644 --- a/go.mod +++ b/go.mod @@ -5,4 +5,6 @@ require ( github.com/yuin/gopher-lua v1.1.0 ) +require github.com/DmitriyVTitov/size v1.5.0 + go 1.14 diff --git a/go.sum b/go.sum index 22f21d81..b52694e5 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,12 @@ +github.com/DmitriyVTitov/size v1.5.0 h1:/PzqxYrOyOUX1BXj6J9OuVRVGe+66VL4D9FlUaW515g= +github.com/DmitriyVTitov/size v1.5.0/go.mod h1:le6rNI4CoLQV1b9gzp1+3d7hMAD/uu2QcJ+aYbNgiU0= github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk= github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9 h1:k/gmLsJDWwWqbLCur2yWnJzwQEKRcAHXo6seXGuSwWw= -github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9/go.mod h1:E1AXubJBdNmFERAOucpDIxNzeGfLzg0mYh+UfMWdChA= -github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64 h1:5mLPGnFdSsevFRFc9q3yYbBkB6tsm4aCwwQV/j1JQAQ= -github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/yuin/gopher-lua v1.1.0 h1:BojcDhfyDWgU2f2TOzYK/g5p2gxMrku8oupLDqlnSqE= github.com/yuin/gopher-lua v1.1.0/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= From 7efd59659e789ff992b0caa7ae4c95a4d5b97c98 Mon Sep 17 00:00:00 2001 From: David Roman Date: Wed, 30 Aug 2023 00:39:00 -0400 Subject: [PATCH 2/8] notes --- cmd_server_test.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cmd_server_test.go b/cmd_server_test.go index a6063878..c78dd2d0 100644 --- a/cmd_server_test.go +++ b/cmd_server_test.go @@ -137,10 +137,11 @@ func TestCmdServerMemoryUsage(t *testing.T) { ok(t, err) defer c.Close() - c.Do("SET", "test:1", "something") + c.Do("SET", "foo", "bar") + // Intended only for having metrics not to be 1:1 Redis mustDo(t, c, - "MEMORY", "USAGE", "test:1", - proto.Int(25), + "MEMORY", "USAGE", "foo", + proto.Int(19), // normally, with Redis it should be 56 but we don't have the same overhead as Redis ) } From 20a2df578502daa9c108e532fa8b852404f1530c Mon Sep 17 00:00:00 2001 From: David Roman Date: Wed, 30 Aug 2023 08:24:09 -0400 Subject: [PATCH 3/8] based on master key After re-reading the doc and re-trying on my side on different types, indeed we need to do it across everything I took the suggestion of @alicebob to put the dependency in a folder and credit, I don't know for the polishing required --- cmd_server.go | 52 +++++++++++++++-- cmd_server_test.go | 9 +++ size/readme.md | 2 + size/size.go | 138 +++++++++++++++++++++++++++++++++++++++++++++ size/size_test.go | 65 +++++++++++++++++++++ 5 files changed, 260 insertions(+), 6 deletions(-) create mode 100644 size/readme.md create mode 100644 size/size.go create mode 100644 size/size_test.go diff --git a/cmd_server.go b/cmd_server.go index bac4aa56..070b0e49 100644 --- a/cmd_server.go +++ b/cmd_server.go @@ -6,8 +6,8 @@ import ( "strconv" "strings" - "github.com/DmitriyVTitov/size" "github.com/alicebob/miniredis/v2/server" + "github.com/alicebob/miniredis/v2/size" ) func commandsServer(m *Miniredis) { @@ -38,13 +38,53 @@ func (m *Miniredis) cmdMemory(c *server.Peer, cmd string, args []string) { db := m.db(ctx.selectedDB) switch args[0] { + case "USAGE": - value, ok := db.stringKeys[args[1]] - if !ok { - c.WriteError(ErrKeyNotFound.Error()) - return + var value interface{} + var ok bool + + switch db.keys[args[1]] { + case "string": + if value, ok = db.stringKeys[args[1]]; ok { + c.WriteInt(size.Of(value)) + return + } + case "set": + if value, ok = db.setKeys[args[1]]; ok { + c.WriteInt(size.Of(value)) + return + } + case "hash": + if value, ok = db.hashKeys[args[1]]; ok { + c.WriteInt(size.Of(value)) + return + } + case "list": + if value, ok = db.listKeys[args[1]]; ok { + c.WriteInt(size.Of(value)) + return + } + + case "hll": + if value, ok = db.hllKeys[args[1]]; ok { + c.WriteInt(size.Of(value)) + return + } + + case "zset": + if value, ok = db.sortedsetKeys[args[1]]; ok { + c.WriteInt(size.Of(value)) + return + } + + case "stream": + if value, ok = db.streamKeys[args[1]]; ok { + c.WriteInt(size.Of(value)) + return + } } - c.WriteInt(size.Of(value)) + + c.WriteError(ErrKeyNotFound.Error()) break default: c.WriteError(errWrongNumber(cmd)) diff --git a/cmd_server_test.go b/cmd_server_test.go index c78dd2d0..bc534968 100644 --- a/cmd_server_test.go +++ b/cmd_server_test.go @@ -138,10 +138,19 @@ func TestCmdServerMemoryUsage(t *testing.T) { defer c.Close() c.Do("SET", "foo", "bar") + mustDo(t, c, + "PFADD", "h", "aap", "noot", "mies", + proto.Int(1), + ) // Intended only for having metrics not to be 1:1 Redis mustDo(t, c, "MEMORY", "USAGE", "foo", proto.Int(19), // normally, with Redis it should be 56 but we don't have the same overhead as Redis ) + // Intended only for having metrics not to be 1:1 Redis + mustDo(t, c, + "MEMORY", "USAGE", "h", + proto.Int(124), // normally, with Redis it should be 56 but we don't have the same overhead as Redis + ) } diff --git a/size/readme.md b/size/readme.md new file mode 100644 index 00000000..89220e45 --- /dev/null +++ b/size/readme.md @@ -0,0 +1,2 @@ + +Credits to DmitriyVTitov on his package https://github.com/DmitriyVTitov/size diff --git a/size/size.go b/size/size.go new file mode 100644 index 00000000..43fee6e2 --- /dev/null +++ b/size/size.go @@ -0,0 +1,138 @@ +package size + +import ( + "reflect" + "unsafe" +) + +// Of returns the size of 'v' in bytes. +// If there is an error during calculation, Of returns -1. +func Of(v interface{}) int { + // Cache with every visited pointer so we don't count two pointers + // to the same memory twice. + cache := make(map[uintptr]bool) + return sizeOf(reflect.Indirect(reflect.ValueOf(v)), cache) +} + +// sizeOf returns the number of bytes the actual data represented by v occupies in memory. +// If there is an error, sizeOf returns -1. +func sizeOf(v reflect.Value, cache map[uintptr]bool) int { + switch v.Kind() { + + case reflect.Array: + sum := 0 + for i := 0; i < v.Len(); i++ { + s := sizeOf(v.Index(i), cache) + if s < 0 { + return -1 + } + sum += s + } + + return sum + (v.Cap()-v.Len())*int(v.Type().Elem().Size()) + + case reflect.Slice: + // return 0 if this node has been visited already + if cache[v.Pointer()] { + return 0 + } + cache[v.Pointer()] = true + + sum := 0 + for i := 0; i < v.Len(); i++ { + s := sizeOf(v.Index(i), cache) + if s < 0 { + return -1 + } + sum += s + } + + sum += (v.Cap() - v.Len()) * int(v.Type().Elem().Size()) + + return sum + int(v.Type().Size()) + + case reflect.Struct: + sum := 0 + for i, n := 0, v.NumField(); i < n; i++ { + s := sizeOf(v.Field(i), cache) + if s < 0 { + return -1 + } + sum += s + } + + // Look for struct padding. + padding := int(v.Type().Size()) + for i, n := 0, v.NumField(); i < n; i++ { + padding -= int(v.Field(i).Type().Size()) + } + + return sum + padding + + case reflect.String: + s := v.String() + hdr := (*reflect.StringHeader)(unsafe.Pointer(&s)) + if cache[hdr.Data] { + return int(v.Type().Size()) + } + cache[hdr.Data] = true + return len(s) + int(v.Type().Size()) + + case reflect.Ptr: + // return Ptr size if this node has been visited already (infinite recursion) + if cache[v.Pointer()] { + return int(v.Type().Size()) + } + cache[v.Pointer()] = true + if v.IsNil() { + return int(reflect.New(v.Type()).Type().Size()) + } + s := sizeOf(reflect.Indirect(v), cache) + if s < 0 { + return -1 + } + return s + int(v.Type().Size()) + + case reflect.Bool, + reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Int, reflect.Uint, + reflect.Chan, + reflect.Uintptr, + reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128, + reflect.Func: + return int(v.Type().Size()) + + case reflect.Map: + // return 0 if this node has been visited already (infinite recursion) + if cache[v.Pointer()] { + return 0 + } + cache[v.Pointer()] = true + sum := 0 + keys := v.MapKeys() + for i := range keys { + val := v.MapIndex(keys[i]) + // calculate size of key and value separately + sv := sizeOf(val, cache) + if sv < 0 { + return -1 + } + sum += sv + sk := sizeOf(keys[i], cache) + if sk < 0 { + return -1 + } + sum += sk + } + // Include overhead due to unused map buckets. 10.79 comes + // from https://golang.org/src/runtime/map.go. + return sum + int(v.Type().Size()) + int(float64(len(keys))*10.79) + + case reflect.Interface: + return sizeOf(v.Elem(), cache) + int(v.Type().Size()) + + } + + return -1 +} diff --git a/size/size_test.go b/size/size_test.go new file mode 100644 index 00000000..049febb4 --- /dev/null +++ b/size/size_test.go @@ -0,0 +1,65 @@ +package size + +import ( + "testing" +) + +func TestOf(t *testing.T) { + tests := []struct { + name string + v interface{} + want int + }{ + { + name: "Array", + v: [3]int32{1, 2, 3}, // 3 * 4 = 12 + want: 12, + }, + { + name: "Slice", + v: make([]int64, 2, 5), // 5 * 8 + 24 = 64 + want: 64, + }, + { + name: "String", + v: "ABCdef", // 6 + 16 = 22 + want: 22, + }, + { + name: "Map", + // (8 + 3 + 16) + (8 + 4 + 16) = 55 + // 55 + 8 + 10.79 * 2 = 84 + v: map[int64]string{0: "ABC", 1: "DEFG"}, + want: 84, + }, + { + name: "Struct", + v: struct { + slice []int64 + array [2]bool + structure struct { + i int8 + s string + } + }{ + slice: []int64{12345, 67890}, // 2 * 8 + 24 = 40 + array: [2]bool{true, false}, // 2 * 1 = 2 + structure: struct { + i int8 + s string + }{ + i: 5, // 1 + s: "abc", // 3 * 1 + 16 = 19 + }, // 20 + 7 (padding) = 27 + }, // 40 + 2 + 27 = 69 + 6 (padding) = 75 + want: 75, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := Of(tt.v); got != tt.want { + t.Errorf("Of() = %v, want %v", got, tt.want) + } + }) + } +} From 6f702292e77f1c0aa668b6c3c4159226afadf058 Mon Sep 17 00:00:00 2001 From: David Roman Date: Wed, 30 Aug 2023 08:27:36 -0400 Subject: [PATCH 4/8] Added small test --- integration/server_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/integration/server_test.go b/integration/server_test.go index bc70b832..65e8c28d 100644 --- a/integration/server_test.go +++ b/integration/server_test.go @@ -23,6 +23,8 @@ func TestServer(t *testing.T) { c.Do("DBSIZE") c.Do("FLUSHALL") c.Do("DBSIZE") + c.Do("MEMORY", "USAGE", "foo") + c.Do("MEMORY", "USAGE", "planets") c.Do("FLUSHDB", "aSyNc") c.Do("FLUSHALL", "AsYnC") From b3df4a31c735471992c7c4e955fd92b04eff38cd Mon Sep 17 00:00:00 2001 From: David Roman Date: Wed, 30 Aug 2023 00:35:30 -0400 Subject: [PATCH 5/8] add: memory usage notes based on master key After re-reading the doc and re-trying on my side on different types, indeed we need to do it across everything I took the suggestion of @alicebob to put the dependency in a folder and credit, I don't know for the polishing required Added small test --- cmd_server.go | 75 ++++++++++++++++++++ cmd_server_test.go | 27 ++++++++ go.mod | 2 + go.sum | 8 +-- integration/server_test.go | 2 + size/readme.md | 2 + size/size.go | 138 +++++++++++++++++++++++++++++++++++++ size/size_test.go | 65 +++++++++++++++++ 8 files changed, 315 insertions(+), 4 deletions(-) create mode 100644 size/readme.md create mode 100644 size/size.go create mode 100644 size/size_test.go diff --git a/cmd_server.go b/cmd_server.go index 6e51727b..070b0e49 100644 --- a/cmd_server.go +++ b/cmd_server.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/alicebob/miniredis/v2/server" + "github.com/alicebob/miniredis/v2/size" ) func commandsServer(m *Miniredis) { @@ -16,6 +17,80 @@ func commandsServer(m *Miniredis) { m.srv.Register("FLUSHDB", m.cmdFlushdb) m.srv.Register("INFO", m.cmdInfo) m.srv.Register("TIME", m.cmdTime) + m.srv.Register("MEMORY", m.cmdMemory) +} + +// MEMORY +func (m *Miniredis) cmdMemory(c *server.Peer, cmd string, args []string) { + if len(args) == 0 { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + if !m.handleAuth(c) { + return + } + if m.checkPubsub(c, cmd) { + return + } + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + switch args[0] { + + case "USAGE": + var value interface{} + var ok bool + + switch db.keys[args[1]] { + case "string": + if value, ok = db.stringKeys[args[1]]; ok { + c.WriteInt(size.Of(value)) + return + } + case "set": + if value, ok = db.setKeys[args[1]]; ok { + c.WriteInt(size.Of(value)) + return + } + case "hash": + if value, ok = db.hashKeys[args[1]]; ok { + c.WriteInt(size.Of(value)) + return + } + case "list": + if value, ok = db.listKeys[args[1]]; ok { + c.WriteInt(size.Of(value)) + return + } + + case "hll": + if value, ok = db.hllKeys[args[1]]; ok { + c.WriteInt(size.Of(value)) + return + } + + case "zset": + if value, ok = db.sortedsetKeys[args[1]]; ok { + c.WriteInt(size.Of(value)) + return + } + + case "stream": + if value, ok = db.streamKeys[args[1]]; ok { + c.WriteInt(size.Of(value)) + return + } + } + + c.WriteError(ErrKeyNotFound.Error()) + break + default: + c.WriteError(errWrongNumber(cmd)) + } + + }) } // DBSIZE diff --git a/cmd_server_test.go b/cmd_server_test.go index f95bd356..bc534968 100644 --- a/cmd_server_test.go +++ b/cmd_server_test.go @@ -127,3 +127,30 @@ func TestCmdServerTime(t *testing.T) { proto.Error(errWrongNumber("time")), ) } + +// Test Memory Usage +func TestCmdServerMemoryUsage(t *testing.T) { + s, err := Run() + ok(t, err) + defer s.Close() + c, err := proto.Dial(s.Addr()) + ok(t, err) + defer c.Close() + + c.Do("SET", "foo", "bar") + mustDo(t, c, + "PFADD", "h", "aap", "noot", "mies", + proto.Int(1), + ) + + // Intended only for having metrics not to be 1:1 Redis + mustDo(t, c, + "MEMORY", "USAGE", "foo", + proto.Int(19), // normally, with Redis it should be 56 but we don't have the same overhead as Redis + ) + // Intended only for having metrics not to be 1:1 Redis + mustDo(t, c, + "MEMORY", "USAGE", "h", + proto.Int(124), // normally, with Redis it should be 56 but we don't have the same overhead as Redis + ) +} diff --git a/go.mod b/go.mod index bbaffb47..a239f335 100644 --- a/go.mod +++ b/go.mod @@ -5,4 +5,6 @@ require ( github.com/yuin/gopher-lua v1.1.0 ) +require github.com/DmitriyVTitov/size v1.5.0 + go 1.14 diff --git a/go.sum b/go.sum index 22f21d81..b52694e5 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,12 @@ +github.com/DmitriyVTitov/size v1.5.0 h1:/PzqxYrOyOUX1BXj6J9OuVRVGe+66VL4D9FlUaW515g= +github.com/DmitriyVTitov/size v1.5.0/go.mod h1:le6rNI4CoLQV1b9gzp1+3d7hMAD/uu2QcJ+aYbNgiU0= github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk= github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9 h1:k/gmLsJDWwWqbLCur2yWnJzwQEKRcAHXo6seXGuSwWw= -github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9/go.mod h1:E1AXubJBdNmFERAOucpDIxNzeGfLzg0mYh+UfMWdChA= -github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64 h1:5mLPGnFdSsevFRFc9q3yYbBkB6tsm4aCwwQV/j1JQAQ= -github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/yuin/gopher-lua v1.1.0 h1:BojcDhfyDWgU2f2TOzYK/g5p2gxMrku8oupLDqlnSqE= github.com/yuin/gopher-lua v1.1.0/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/integration/server_test.go b/integration/server_test.go index bc70b832..65e8c28d 100644 --- a/integration/server_test.go +++ b/integration/server_test.go @@ -23,6 +23,8 @@ func TestServer(t *testing.T) { c.Do("DBSIZE") c.Do("FLUSHALL") c.Do("DBSIZE") + c.Do("MEMORY", "USAGE", "foo") + c.Do("MEMORY", "USAGE", "planets") c.Do("FLUSHDB", "aSyNc") c.Do("FLUSHALL", "AsYnC") diff --git a/size/readme.md b/size/readme.md new file mode 100644 index 00000000..89220e45 --- /dev/null +++ b/size/readme.md @@ -0,0 +1,2 @@ + +Credits to DmitriyVTitov on his package https://github.com/DmitriyVTitov/size diff --git a/size/size.go b/size/size.go new file mode 100644 index 00000000..43fee6e2 --- /dev/null +++ b/size/size.go @@ -0,0 +1,138 @@ +package size + +import ( + "reflect" + "unsafe" +) + +// Of returns the size of 'v' in bytes. +// If there is an error during calculation, Of returns -1. +func Of(v interface{}) int { + // Cache with every visited pointer so we don't count two pointers + // to the same memory twice. + cache := make(map[uintptr]bool) + return sizeOf(reflect.Indirect(reflect.ValueOf(v)), cache) +} + +// sizeOf returns the number of bytes the actual data represented by v occupies in memory. +// If there is an error, sizeOf returns -1. +func sizeOf(v reflect.Value, cache map[uintptr]bool) int { + switch v.Kind() { + + case reflect.Array: + sum := 0 + for i := 0; i < v.Len(); i++ { + s := sizeOf(v.Index(i), cache) + if s < 0 { + return -1 + } + sum += s + } + + return sum + (v.Cap()-v.Len())*int(v.Type().Elem().Size()) + + case reflect.Slice: + // return 0 if this node has been visited already + if cache[v.Pointer()] { + return 0 + } + cache[v.Pointer()] = true + + sum := 0 + for i := 0; i < v.Len(); i++ { + s := sizeOf(v.Index(i), cache) + if s < 0 { + return -1 + } + sum += s + } + + sum += (v.Cap() - v.Len()) * int(v.Type().Elem().Size()) + + return sum + int(v.Type().Size()) + + case reflect.Struct: + sum := 0 + for i, n := 0, v.NumField(); i < n; i++ { + s := sizeOf(v.Field(i), cache) + if s < 0 { + return -1 + } + sum += s + } + + // Look for struct padding. + padding := int(v.Type().Size()) + for i, n := 0, v.NumField(); i < n; i++ { + padding -= int(v.Field(i).Type().Size()) + } + + return sum + padding + + case reflect.String: + s := v.String() + hdr := (*reflect.StringHeader)(unsafe.Pointer(&s)) + if cache[hdr.Data] { + return int(v.Type().Size()) + } + cache[hdr.Data] = true + return len(s) + int(v.Type().Size()) + + case reflect.Ptr: + // return Ptr size if this node has been visited already (infinite recursion) + if cache[v.Pointer()] { + return int(v.Type().Size()) + } + cache[v.Pointer()] = true + if v.IsNil() { + return int(reflect.New(v.Type()).Type().Size()) + } + s := sizeOf(reflect.Indirect(v), cache) + if s < 0 { + return -1 + } + return s + int(v.Type().Size()) + + case reflect.Bool, + reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Int, reflect.Uint, + reflect.Chan, + reflect.Uintptr, + reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128, + reflect.Func: + return int(v.Type().Size()) + + case reflect.Map: + // return 0 if this node has been visited already (infinite recursion) + if cache[v.Pointer()] { + return 0 + } + cache[v.Pointer()] = true + sum := 0 + keys := v.MapKeys() + for i := range keys { + val := v.MapIndex(keys[i]) + // calculate size of key and value separately + sv := sizeOf(val, cache) + if sv < 0 { + return -1 + } + sum += sv + sk := sizeOf(keys[i], cache) + if sk < 0 { + return -1 + } + sum += sk + } + // Include overhead due to unused map buckets. 10.79 comes + // from https://golang.org/src/runtime/map.go. + return sum + int(v.Type().Size()) + int(float64(len(keys))*10.79) + + case reflect.Interface: + return sizeOf(v.Elem(), cache) + int(v.Type().Size()) + + } + + return -1 +} diff --git a/size/size_test.go b/size/size_test.go new file mode 100644 index 00000000..049febb4 --- /dev/null +++ b/size/size_test.go @@ -0,0 +1,65 @@ +package size + +import ( + "testing" +) + +func TestOf(t *testing.T) { + tests := []struct { + name string + v interface{} + want int + }{ + { + name: "Array", + v: [3]int32{1, 2, 3}, // 3 * 4 = 12 + want: 12, + }, + { + name: "Slice", + v: make([]int64, 2, 5), // 5 * 8 + 24 = 64 + want: 64, + }, + { + name: "String", + v: "ABCdef", // 6 + 16 = 22 + want: 22, + }, + { + name: "Map", + // (8 + 3 + 16) + (8 + 4 + 16) = 55 + // 55 + 8 + 10.79 * 2 = 84 + v: map[int64]string{0: "ABC", 1: "DEFG"}, + want: 84, + }, + { + name: "Struct", + v: struct { + slice []int64 + array [2]bool + structure struct { + i int8 + s string + } + }{ + slice: []int64{12345, 67890}, // 2 * 8 + 24 = 40 + array: [2]bool{true, false}, // 2 * 1 = 2 + structure: struct { + i int8 + s string + }{ + i: 5, // 1 + s: "abc", // 3 * 1 + 16 = 19 + }, // 20 + 7 (padding) = 27 + }, // 40 + 2 + 27 = 69 + 6 (padding) = 75 + want: 75, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := Of(tt.v); got != tt.want { + t.Errorf("Of() = %v, want %v", got, tt.want) + } + }) + } +} From 13ee9b236232ed06fe08dedbc14789b82d9d4235 Mon Sep 17 00:00:00 2001 From: David Roman Date: Wed, 30 Aug 2023 18:19:09 -0400 Subject: [PATCH 6/8] add: memory usage --- CHANGELOG.md | 12 ++++ README.md | 3 +- cmd_server.go | 75 ++++++++++++++++++++ cmd_server_test.go | 27 ++++++++ cmd_set.go | 45 ++++++++++++ cmd_set_test.go | 31 +++++++++ go.mod | 2 + go.sum | 8 +-- integration/server_test.go | 2 + integration/set_test.go | 6 ++ integration/test.go | 2 +- size/readme.md | 2 + size/size.go | 138 +++++++++++++++++++++++++++++++++++++ size/size_test.go | 65 +++++++++++++++++ 14 files changed, 412 insertions(+), 6 deletions(-) create mode 100644 size/readme.md create mode 100644 size/size.go create mode 100644 size/size_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 873901fc..1249a84f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,18 @@ ## Changelog +### v2.30.5 + +- support SMISMEMBER (thanks @sandyharvie) + + +### v2.30.4 + +- fix ZADD LT/LG (thanks @sejin-P) +- fix COPY (thanks @jerargus) +- quicker SPOP + + ### v2.30.3 - fix lua error_reply (thanks @pkierski) diff --git a/README.md b/README.md index 3ec7ea28..44026db5 100644 --- a/README.md +++ b/README.md @@ -144,13 +144,14 @@ Implemented commands: - SINTERSTORE - SISMEMBER - SMEMBERS + - SMISMEMBER - SMOVE - SPOP -- see m.Seed(...) - SRANDMEMBER -- see m.Seed(...) - SREM + - SSCAN - SUNION - SUNIONSTORE - - SSCAN - Sorted Set keys (complete) - ZADD - ZCARD diff --git a/cmd_server.go b/cmd_server.go index 6e51727b..070b0e49 100644 --- a/cmd_server.go +++ b/cmd_server.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/alicebob/miniredis/v2/server" + "github.com/alicebob/miniredis/v2/size" ) func commandsServer(m *Miniredis) { @@ -16,6 +17,80 @@ func commandsServer(m *Miniredis) { m.srv.Register("FLUSHDB", m.cmdFlushdb) m.srv.Register("INFO", m.cmdInfo) m.srv.Register("TIME", m.cmdTime) + m.srv.Register("MEMORY", m.cmdMemory) +} + +// MEMORY +func (m *Miniredis) cmdMemory(c *server.Peer, cmd string, args []string) { + if len(args) == 0 { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + if !m.handleAuth(c) { + return + } + if m.checkPubsub(c, cmd) { + return + } + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + switch args[0] { + + case "USAGE": + var value interface{} + var ok bool + + switch db.keys[args[1]] { + case "string": + if value, ok = db.stringKeys[args[1]]; ok { + c.WriteInt(size.Of(value)) + return + } + case "set": + if value, ok = db.setKeys[args[1]]; ok { + c.WriteInt(size.Of(value)) + return + } + case "hash": + if value, ok = db.hashKeys[args[1]]; ok { + c.WriteInt(size.Of(value)) + return + } + case "list": + if value, ok = db.listKeys[args[1]]; ok { + c.WriteInt(size.Of(value)) + return + } + + case "hll": + if value, ok = db.hllKeys[args[1]]; ok { + c.WriteInt(size.Of(value)) + return + } + + case "zset": + if value, ok = db.sortedsetKeys[args[1]]; ok { + c.WriteInt(size.Of(value)) + return + } + + case "stream": + if value, ok = db.streamKeys[args[1]]; ok { + c.WriteInt(size.Of(value)) + return + } + } + + c.WriteError(ErrKeyNotFound.Error()) + break + default: + c.WriteError(errWrongNumber(cmd)) + } + + }) } // DBSIZE diff --git a/cmd_server_test.go b/cmd_server_test.go index f95bd356..bc534968 100644 --- a/cmd_server_test.go +++ b/cmd_server_test.go @@ -127,3 +127,30 @@ func TestCmdServerTime(t *testing.T) { proto.Error(errWrongNumber("time")), ) } + +// Test Memory Usage +func TestCmdServerMemoryUsage(t *testing.T) { + s, err := Run() + ok(t, err) + defer s.Close() + c, err := proto.Dial(s.Addr()) + ok(t, err) + defer c.Close() + + c.Do("SET", "foo", "bar") + mustDo(t, c, + "PFADD", "h", "aap", "noot", "mies", + proto.Int(1), + ) + + // Intended only for having metrics not to be 1:1 Redis + mustDo(t, c, + "MEMORY", "USAGE", "foo", + proto.Int(19), // normally, with Redis it should be 56 but we don't have the same overhead as Redis + ) + // Intended only for having metrics not to be 1:1 Redis + mustDo(t, c, + "MEMORY", "USAGE", "h", + proto.Int(124), // normally, with Redis it should be 56 but we don't have the same overhead as Redis + ) +} diff --git a/cmd_set.go b/cmd_set.go index 056a3093..9341d72f 100644 --- a/cmd_set.go +++ b/cmd_set.go @@ -20,6 +20,7 @@ func commandsSet(m *Miniredis) { m.srv.Register("SINTERSTORE", m.cmdSinterstore) m.srv.Register("SISMEMBER", m.cmdSismember) m.srv.Register("SMEMBERS", m.cmdSmembers) + m.srv.Register("SMISMEMBER", m.cmdSmismember) m.srv.Register("SMOVE", m.cmdSmove) m.srv.Register("SPOP", m.cmdSpop) m.srv.Register("SRANDMEMBER", m.cmdSrandmember) @@ -293,6 +294,50 @@ func (m *Miniredis) cmdSmembers(c *server.Peer, cmd string, args []string) { }) } +// SMISMEMBER +func (m *Miniredis) cmdSmismember(c *server.Peer, cmd string, args []string) { + if len(args) < 2 { + setDirty(c) + c.WriteError(errWrongNumber(cmd)) + return + } + if !m.handleAuth(c) { + return + } + if m.checkPubsub(c, cmd) { + return + } + + key, values := args[0], args[1:] + + withTx(m, c, func(c *server.Peer, ctx *connCtx) { + db := m.db(ctx.selectedDB) + + if !db.exists(key) { + c.WriteLen(len(values)) + for range values { + c.WriteInt(0) + } + return + } + + if db.t(key) != "set" { + c.WriteError(ErrWrongType.Error()) + return + } + + c.WriteLen(len(values)) + for _, value := range values { + if db.setIsMember(key, value) { + c.WriteInt(1) + } else { + c.WriteInt(0) + } + } + return + }) +} + // SMOVE func (m *Miniredis) cmdSmove(c *server.Peer, cmd string, args []string) { if len(args) != 3 { diff --git a/cmd_set_test.go b/cmd_set_test.go index 84cae3c6..5893b765 100644 --- a/cmd_set_test.go +++ b/cmd_set_test.go @@ -148,6 +148,37 @@ func TestSismember(t *testing.T) { }) } +// Test SMISMEMBER +func TestSmismember(t *testing.T) { + s, err := Run() + ok(t, err) + defer s.Close() + c, err := proto.Dial(s.Addr()) + ok(t, err) + defer c.Close() + + s.SetAdd("s", "aap", "noot", "mies") + + mustDo(t, c, "SMISMEMBER", "s", "aap", "nosuch", "mies", proto.Ints(1, 0, 1)) + mustDo(t, c, "SMISMEMBER", "q", "aap", "nosuch", "mies", proto.Ints(0, 0, 0)) + + t.Run("errors", func(t *testing.T) { + mustOK(t, c, "SET", "str", "value") + mustDo(t, c, + "SMISMEMBER", "str", "foo", + proto.Error(msgWrongType), + ) + mustDo(t, c, + "SMISMEMBER", + proto.Error(errWrongNumber("smismember")), + ) + mustDo(t, c, + "SMISMEMBER", "set", + proto.Error(errWrongNumber("smismember")), + ) + }) +} + // Test SREM func TestSrem(t *testing.T) { s, err := Run() diff --git a/go.mod b/go.mod index bbaffb47..a239f335 100644 --- a/go.mod +++ b/go.mod @@ -5,4 +5,6 @@ require ( github.com/yuin/gopher-lua v1.1.0 ) +require github.com/DmitriyVTitov/size v1.5.0 + go 1.14 diff --git a/go.sum b/go.sum index 22f21d81..b52694e5 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,12 @@ +github.com/DmitriyVTitov/size v1.5.0 h1:/PzqxYrOyOUX1BXj6J9OuVRVGe+66VL4D9FlUaW515g= +github.com/DmitriyVTitov/size v1.5.0/go.mod h1:le6rNI4CoLQV1b9gzp1+3d7hMAD/uu2QcJ+aYbNgiU0= github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk= github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9 h1:k/gmLsJDWwWqbLCur2yWnJzwQEKRcAHXo6seXGuSwWw= -github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9/go.mod h1:E1AXubJBdNmFERAOucpDIxNzeGfLzg0mYh+UfMWdChA= -github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64 h1:5mLPGnFdSsevFRFc9q3yYbBkB6tsm4aCwwQV/j1JQAQ= -github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/yuin/gopher-lua v1.1.0 h1:BojcDhfyDWgU2f2TOzYK/g5p2gxMrku8oupLDqlnSqE= github.com/yuin/gopher-lua v1.1.0/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/integration/server_test.go b/integration/server_test.go index bc70b832..65e8c28d 100644 --- a/integration/server_test.go +++ b/integration/server_test.go @@ -23,6 +23,8 @@ func TestServer(t *testing.T) { c.Do("DBSIZE") c.Do("FLUSHALL") c.Do("DBSIZE") + c.Do("MEMORY", "USAGE", "foo") + c.Do("MEMORY", "USAGE", "planets") c.Do("FLUSHDB", "aSyNc") c.Do("FLUSHALL", "AsYnC") diff --git a/integration/set_test.go b/integration/set_test.go index 375d8646..6f206519 100644 --- a/integration/set_test.go +++ b/integration/set_test.go @@ -16,9 +16,11 @@ func TestSet(t *testing.T) { c.DoSorted("SMEMBERS", "nosuch") c.Do("SISMEMBER", "s", "aap") c.Do("SISMEMBER", "s", "nosuch") + c.Do("SMISMEMBER", "s", "aap", "noot", "nosuch") c.Do("SCARD", "nosuch") c.Do("SISMEMBER", "nosuch", "nosuch") + c.Do("SMISMEMBER", "nosuch", "nosuch", "nosuch") // failure cases c.Error("wrong number", "SADD") @@ -30,11 +32,14 @@ func TestSet(t *testing.T) { c.Error("wrong number", "SISMEMBER") c.Error("wrong number", "SISMEMBER", "few") c.Error("wrong number", "SISMEMBER", "too", "many", "arguments") + c.Error("wrong number", "SMISMEMBER") + c.Error("wrong number", "SMISMEMBER", "few") // Wrong type c.Do("SET", "str", "I am a string") c.Error("wrong kind", "SADD", "str", "noot", "mies") c.Error("wrong kind", "SMEMBERS", "str") c.Error("wrong kind", "SISMEMBER", "str", "noot") + c.Error("wrong kind", "SMISMEMBER", "str", "noot") c.Error("wrong kind", "SCARD", "str") }) @@ -44,6 +49,7 @@ func TestSet(t *testing.T) { c.Do("SMEMBERS", "q") c.Do("SISMEMBER", "q", "aap") c.Do("SISMEMBER", "q", "noot") + c.Do("SMISMEMBER", "q", "aap", "noot", "nosuch") }) } diff --git a/integration/test.go b/integration/test.go index 1a1b4de3..f91163a6 100644 --- a/integration/test.go +++ b/integration/test.go @@ -569,7 +569,7 @@ func (c *client) ErrorTheSame(msg string, cmd string, args ...string) { if mini != msg { c.t.Errorf("expected (mini)\n%q\nto contain %q\nreal:\n%s", mini, msg, real) } - // real == msg && mini == msg => real == mini, so we don't want to check it explicity + // real == msg && mini == msg => real == mini, so we don't want to check it explicitly } // only receive a command, which can't be an error diff --git a/size/readme.md b/size/readme.md new file mode 100644 index 00000000..89220e45 --- /dev/null +++ b/size/readme.md @@ -0,0 +1,2 @@ + +Credits to DmitriyVTitov on his package https://github.com/DmitriyVTitov/size diff --git a/size/size.go b/size/size.go new file mode 100644 index 00000000..43fee6e2 --- /dev/null +++ b/size/size.go @@ -0,0 +1,138 @@ +package size + +import ( + "reflect" + "unsafe" +) + +// Of returns the size of 'v' in bytes. +// If there is an error during calculation, Of returns -1. +func Of(v interface{}) int { + // Cache with every visited pointer so we don't count two pointers + // to the same memory twice. + cache := make(map[uintptr]bool) + return sizeOf(reflect.Indirect(reflect.ValueOf(v)), cache) +} + +// sizeOf returns the number of bytes the actual data represented by v occupies in memory. +// If there is an error, sizeOf returns -1. +func sizeOf(v reflect.Value, cache map[uintptr]bool) int { + switch v.Kind() { + + case reflect.Array: + sum := 0 + for i := 0; i < v.Len(); i++ { + s := sizeOf(v.Index(i), cache) + if s < 0 { + return -1 + } + sum += s + } + + return sum + (v.Cap()-v.Len())*int(v.Type().Elem().Size()) + + case reflect.Slice: + // return 0 if this node has been visited already + if cache[v.Pointer()] { + return 0 + } + cache[v.Pointer()] = true + + sum := 0 + for i := 0; i < v.Len(); i++ { + s := sizeOf(v.Index(i), cache) + if s < 0 { + return -1 + } + sum += s + } + + sum += (v.Cap() - v.Len()) * int(v.Type().Elem().Size()) + + return sum + int(v.Type().Size()) + + case reflect.Struct: + sum := 0 + for i, n := 0, v.NumField(); i < n; i++ { + s := sizeOf(v.Field(i), cache) + if s < 0 { + return -1 + } + sum += s + } + + // Look for struct padding. + padding := int(v.Type().Size()) + for i, n := 0, v.NumField(); i < n; i++ { + padding -= int(v.Field(i).Type().Size()) + } + + return sum + padding + + case reflect.String: + s := v.String() + hdr := (*reflect.StringHeader)(unsafe.Pointer(&s)) + if cache[hdr.Data] { + return int(v.Type().Size()) + } + cache[hdr.Data] = true + return len(s) + int(v.Type().Size()) + + case reflect.Ptr: + // return Ptr size if this node has been visited already (infinite recursion) + if cache[v.Pointer()] { + return int(v.Type().Size()) + } + cache[v.Pointer()] = true + if v.IsNil() { + return int(reflect.New(v.Type()).Type().Size()) + } + s := sizeOf(reflect.Indirect(v), cache) + if s < 0 { + return -1 + } + return s + int(v.Type().Size()) + + case reflect.Bool, + reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Int, reflect.Uint, + reflect.Chan, + reflect.Uintptr, + reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128, + reflect.Func: + return int(v.Type().Size()) + + case reflect.Map: + // return 0 if this node has been visited already (infinite recursion) + if cache[v.Pointer()] { + return 0 + } + cache[v.Pointer()] = true + sum := 0 + keys := v.MapKeys() + for i := range keys { + val := v.MapIndex(keys[i]) + // calculate size of key and value separately + sv := sizeOf(val, cache) + if sv < 0 { + return -1 + } + sum += sv + sk := sizeOf(keys[i], cache) + if sk < 0 { + return -1 + } + sum += sk + } + // Include overhead due to unused map buckets. 10.79 comes + // from https://golang.org/src/runtime/map.go. + return sum + int(v.Type().Size()) + int(float64(len(keys))*10.79) + + case reflect.Interface: + return sizeOf(v.Elem(), cache) + int(v.Type().Size()) + + } + + return -1 +} diff --git a/size/size_test.go b/size/size_test.go new file mode 100644 index 00000000..049febb4 --- /dev/null +++ b/size/size_test.go @@ -0,0 +1,65 @@ +package size + +import ( + "testing" +) + +func TestOf(t *testing.T) { + tests := []struct { + name string + v interface{} + want int + }{ + { + name: "Array", + v: [3]int32{1, 2, 3}, // 3 * 4 = 12 + want: 12, + }, + { + name: "Slice", + v: make([]int64, 2, 5), // 5 * 8 + 24 = 64 + want: 64, + }, + { + name: "String", + v: "ABCdef", // 6 + 16 = 22 + want: 22, + }, + { + name: "Map", + // (8 + 3 + 16) + (8 + 4 + 16) = 55 + // 55 + 8 + 10.79 * 2 = 84 + v: map[int64]string{0: "ABC", 1: "DEFG"}, + want: 84, + }, + { + name: "Struct", + v: struct { + slice []int64 + array [2]bool + structure struct { + i int8 + s string + } + }{ + slice: []int64{12345, 67890}, // 2 * 8 + 24 = 40 + array: [2]bool{true, false}, // 2 * 1 = 2 + structure: struct { + i int8 + s string + }{ + i: 5, // 1 + s: "abc", // 3 * 1 + 16 = 19 + }, // 20 + 7 (padding) = 27 + }, // 40 + 2 + 27 = 69 + 6 (padding) = 75 + want: 75, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := Of(tt.v); got != tt.want { + t.Errorf("Of() = %v, want %v", got, tt.want) + } + }) + } +} From 03fc757429bfbb6e669bcc2769c902e7acc7012a Mon Sep 17 00:00:00 2001 From: Harmen Date: Mon, 4 Sep 2023 09:44:12 +0200 Subject: [PATCH 7/8] update the int test --- integration/server_test.go | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/integration/server_test.go b/integration/server_test.go index 65e8c28d..da605087 100644 --- a/integration/server_test.go +++ b/integration/server_test.go @@ -23,8 +23,6 @@ func TestServer(t *testing.T) { c.Do("DBSIZE") c.Do("FLUSHALL") c.Do("DBSIZE") - c.Do("MEMORY", "USAGE", "foo") - c.Do("MEMORY", "USAGE", "planets") c.Do("FLUSHDB", "aSyNc") c.Do("FLUSHALL", "AsYnC") @@ -37,6 +35,21 @@ func TestServer(t *testing.T) { c.Error("syntax error", "FLUSHDB", "ASYNC", "ASYNC") c.Error("syntax error", "FLUSHALL", "ASYNC", "foo") }) + + testRaw(t, func(c *client) { + c.Do("SET", "plain", "hello") + c.DoLoosely("MEMORY", "USAGE", "plain") + c.Do("LPUSH", "alist", "hello", "42") + c.DoLoosely("MEMORY", "USAGE", "alist") + c.Do("HSET", "ahash", "key", "value") + c.DoLoosely("MEMORY", "USAGE", "ahash") + c.Do("ZADD", "asset", "0", "line") + c.DoLoosely("MEMORY", "USAGE", "asset") + c.Do("PFADD", "ahll", "123") + c.DoLoosely("MEMORY", "USAGE", "ahll") + c.Do("XADD", "astream", "0-1", "name", "Mercury") + c.DoLoosely("MEMORY", "USAGE", "astream") + }) } func TestServerTLS(t *testing.T) { From 53685a2b6c728d3cb14639f78acdca1540f01a51 Mon Sep 17 00:00:00 2001 From: Harmen Date: Mon, 4 Sep 2023 10:07:09 +0200 Subject: [PATCH 8/8] fix arg checking --- cmd_server.go | 72 ++++++++++++++++---------------------- integration/server_test.go | 5 +++ redis.go | 1 + 3 files changed, 37 insertions(+), 41 deletions(-) diff --git a/cmd_server.go b/cmd_server.go index 070b0e49..b1c81569 100644 --- a/cmd_server.go +++ b/cmd_server.go @@ -3,6 +3,7 @@ package miniredis import ( + "fmt" "strconv" "strings" @@ -37,59 +38,48 @@ func (m *Miniredis) cmdMemory(c *server.Peer, cmd string, args []string) { withTx(m, c, func(c *server.Peer, ctx *connCtx) { db := m.db(ctx.selectedDB) - switch args[0] { - + cmd, args := args[0], args[1:] + switch cmd { case "USAGE": - var value interface{} - var ok bool + if len(args) < 1 { + setDirty(c) + c.WriteError(errWrongNumber("memory|usage")) + return + } + if len(args) > 1 { + setDirty(c) + c.WriteError(msgSyntaxError) + return + } - switch db.keys[args[1]] { + var ( + value interface{} + ok bool + ) + switch db.keys[args[0]] { case "string": - if value, ok = db.stringKeys[args[1]]; ok { - c.WriteInt(size.Of(value)) - return - } + value, ok = db.stringKeys[args[0]] case "set": - if value, ok = db.setKeys[args[1]]; ok { - c.WriteInt(size.Of(value)) - return - } + value, ok = db.setKeys[args[0]] case "hash": - if value, ok = db.hashKeys[args[1]]; ok { - c.WriteInt(size.Of(value)) - return - } + value, ok = db.hashKeys[args[0]] case "list": - if value, ok = db.listKeys[args[1]]; ok { - c.WriteInt(size.Of(value)) - return - } - + value, ok = db.listKeys[args[0]] case "hll": - if value, ok = db.hllKeys[args[1]]; ok { - c.WriteInt(size.Of(value)) - return - } - + value, ok = db.hllKeys[args[0]] case "zset": - if value, ok = db.sortedsetKeys[args[1]]; ok { - c.WriteInt(size.Of(value)) - return - } - + value, ok = db.sortedsetKeys[args[0]] case "stream": - if value, ok = db.streamKeys[args[1]]; ok { - c.WriteInt(size.Of(value)) - return - } + value, ok = db.streamKeys[args[0]] } - - c.WriteError(ErrKeyNotFound.Error()) - break + if !ok { + c.WriteNull() + return + } + c.WriteInt(size.Of(value)) default: - c.WriteError(errWrongNumber(cmd)) + c.WriteError(fmt.Sprintf(msgMemorySubcommand, strings.ToUpper(cmd))) } - }) } diff --git a/integration/server_test.go b/integration/server_test.go index da605087..491079bd 100644 --- a/integration/server_test.go +++ b/integration/server_test.go @@ -49,6 +49,11 @@ func TestServer(t *testing.T) { c.DoLoosely("MEMORY", "USAGE", "ahll") c.Do("XADD", "astream", "0-1", "name", "Mercury") c.DoLoosely("MEMORY", "USAGE", "astream") + c.DoLoosely("MEMORY", "USAGE", "nosuch") + + c.Error("Try MEMORY HELP", "MEMORY", "FOO") + c.Error("wrong number of arguments", "MEMORY", "USAGE") + c.Error("syntax error", "MEMORY", "USAGE", "too", "many") }) } diff --git a/redis.go b/redis.go index 6cc52db0..3fdc7813 100644 --- a/redis.go +++ b/redis.go @@ -54,6 +54,7 @@ const ( msgRankIsZero = "ERR RANK can't be zero: use 1 to start from the first match, 2 from the second ... or use negative to start from the end of the list" msgCountIsNegative = "ERR COUNT can't be negative" msgMaxLengthIsNegative = "ERR MAXLEN can't be negative" + msgMemorySubcommand = "ERR unknown subcommand '%s'. Try MEMORY HELP." ) func errWrongNumber(cmd string) string {