From c9ef97cacbd63e3da1181f544b64fad5ca1a02d1 Mon Sep 17 00:00:00 2001 From: Andrei Avram Date: Mon, 3 Dec 2018 20:58:46 +0200 Subject: [PATCH 1/9] Add Keys method. Retrieves cached keys by regex pattern. --- README.md | 29 +++++++++++-------- fastcache.go | 21 ++++++++++++++ fastcache_test.go | 61 ++++++++++++++++++++++++++++++++++++++++ fastcache_timing_test.go | 33 ++++++++++++++++++++++ 4 files changed, 132 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index ac6fbaf..6cbc475 100644 --- a/README.md +++ b/README.md @@ -31,18 +31,23 @@ GOMAXPROCS=4 go test github.com/VictoriaMetrics/fastcache -bench='Set|Get' -benc goos: linux goarch: amd64 pkg: github.com/VictoriaMetrics/fastcache -BenchmarkBigCacheSet-4 2000 10566656 ns/op 6.20 MB/s 4660369 B/op 6 allocs/op -BenchmarkBigCacheGet-4 2000 6902694 ns/op 9.49 MB/s 684169 B/op 131076 allocs/op -BenchmarkBigCacheSetGet-4 1000 17579118 ns/op 7.46 MB/s 5046744 B/op 131083 allocs/op -BenchmarkCacheSet-4 5000 3808874 ns/op 17.21 MB/s 1142 B/op 2 allocs/op -BenchmarkCacheGet-4 5000 3293849 ns/op 19.90 MB/s 1140 B/op 2 allocs/op -BenchmarkCacheSetGet-4 2000 8456061 ns/op 15.50 MB/s 2857 B/op 5 allocs/op -BenchmarkStdMapSet-4 2000 10559382 ns/op 6.21 MB/s 268413 B/op 65537 allocs/op -BenchmarkStdMapGet-4 5000 2687404 ns/op 24.39 MB/s 2558 B/op 13 allocs/op -BenchmarkStdMapSetGet-4 100 154641257 ns/op 0.85 MB/s 387405 B/op 65558 allocs/op -BenchmarkSyncMapSet-4 500 24703219 ns/op 2.65 MB/s 3426543 B/op 262411 allocs/op -BenchmarkSyncMapGet-4 5000 2265892 ns/op 28.92 MB/s 2545 B/op 79 allocs/op -BenchmarkSyncMapSetGet-4 1000 14595535 ns/op 8.98 MB/s 3417190 B/op 262277 allocs/op + +BenchmarkBigCacheSet-4 2000 10937855 ns/op 5.99 MB/s 4660369 B/op 6 allocs/op +BenchmarkBigCacheGet-4 2000 6985426 ns/op 9.38 MB/s 684169 B/op 131076 allocs/op +BenchmarkBigCacheSetGet-4 1000 17301294 ns/op 7.58 MB/s 5046746 B/op 131083 allocs/op +BenchmarkCacheSet-4 5000 3975946 ns/op 16.48 MB/s 1142 B/op 2 allocs/op +BenchmarkCacheGet-4 5000 3572679 ns/op 18.34 MB/s 1141 B/op 2 allocs/op +BenchmarkCacheSetGet-4 2000 9337256 ns/op 14.04 MB/s 2856 B/op 5 allocs/op +BenchmarkStdMapSet-4 2000 14684273 ns/op 4.46 MB/s 268423 B/op 65537 allocs/op +BenchmarkStdMapGet-4 5000 2833647 ns/op 23.13 MB/s 2561 B/op 13 allocs/op +BenchmarkStdMapSetGet-4 100 137417861 ns/op 0.95 MB/s 387356 B/op 65558 allocs/op +BenchmarkSyncMapSet-4 1000 23300189 ns/op 2.81 MB/s 3417183 B/op 262277 allocs/op +BenchmarkSyncMapGet-4 5000 2316508 ns/op 28.29 MB/s 2543 B/op 79 allocs/op +BenchmarkSyncMapSetGet-4 2000 10444529 ns/op 12.55 MB/s 3412527 B/op 262210 allocs/op +BenchmarkSaveToFile-4 50 259800249 ns/op 129.15 MB/s 55739129 B/op 3091 allocs/op +BenchmarkLoadFromFile-4 100 121189395 ns/op 276.88 MB/s 98089036 B/op 8748 allocs/op +BenchmarkCache_Keys-8 100000 18359 ns/op 16.34 MB/s 39072 B/op 30 allocs/op + ``` `MB/s` column here actually means `millions of operations per second`. diff --git a/fastcache.go b/fastcache.go index 3192cbd..1f2174c 100644 --- a/fastcache.go +++ b/fastcache.go @@ -2,6 +2,7 @@ package fastcache import ( "fmt" + "regexp" "sync" "sync/atomic" @@ -186,6 +187,26 @@ func (c *Cache) UpdateStats(s *Stats) { s.InvalidValueHashErrors += atomic.LoadUint64(&c.bigStats.InvalidValueHashErrors) } +// Keys retrieves all cached keys matching regex pattern +func (c *Cache) Keys(pattern string) (keys [][]byte, err error) { + r, err := regexp.Compile(pattern) + if err != nil { + return + } + + for _, b := range c.buckets { + for _, chunk := range b.chunks { + if len(chunk) > 0 { + if key := chunk[4 : 4+chunk[1]]; r.Match(key) { + keys = append(keys, key) + } + } + } + } + + return +} + type bucket struct { mu sync.RWMutex diff --git a/fastcache_test.go b/fastcache_test.go index d90f9f2..24d786f 100644 --- a/fastcache_test.go +++ b/fastcache_test.go @@ -168,6 +168,67 @@ func TestCacheGetSetConcurrent(t *testing.T) { } } +func TestCacheKeys(t *testing.T) { + keys := []string{ + "username", + "firstname", + "lastname", + } + + c := New(100 * len(keys)) + defer c.Reset() + + for _, k := range keys { + c.Set([]byte(k), nil) + } + + tests := []struct { + Pattern string + Expected []string + }{ + { + Pattern: "", + Expected: keys, + }, + { + Pattern: "name$", + Expected: keys, + }, + { + Pattern: "st", + Expected: []string{ + "firstname", + "lastname", + }, + }, + { + Pattern: " ", + Expected: nil, + }, + } + + for _, tt := range tests { + result, err := c.Keys(tt.Pattern) + if err != nil { + t.Fatal(err) + } + + count := 0 + for _, r := range result { + for _, e := range tt.Expected { + if string(r) == e { + count++ + break + } + } + } + + if count != len(tt.Expected) { + t.Fatalf("failed to retrievs keys by pattern: \"%s\"", tt.Pattern) + } + } +} + func testCacheGetSet(c *Cache, itemsCount int) error { for i := 0; i < itemsCount; i++ { k := []byte(fmt.Sprintf("key %d", i)) diff --git a/fastcache_timing_test.go b/fastcache_timing_test.go index 9a13df3..e824ad3 100644 --- a/fastcache_timing_test.go +++ b/fastcache_timing_test.go @@ -387,3 +387,36 @@ func BenchmarkSyncMapSetGet(b *testing.B) { } }) } + +func BenchmarkCache_Keys(b *testing.B) { + keys := []string{ + "username", + "firstname", + "lastname", + } + pattern := "name$" + + items := 100 * len(keys) + + c := New(items) + defer c.Reset() + + b.ReportAllocs() + b.SetBytes(int64(items)) + + for _, k := range keys { + c.Set([]byte(k), nil) + } + + for n := 0; n < b.N; n++ { + keys, err := c.Keys(pattern) + + if err != nil { + panic(fmt.Errorf("BUG: error for valid pattern \"%s\" (%s)", pattern, err)) + } + + if len(keys) == 0 { + panic(fmt.Errorf("BUG: no key for valid pattern \"%s\"", pattern)) + } + } +} From af060158268c4797e502bfc08e9b0332c906d2f8 Mon Sep 17 00:00:00 2001 From: Andrei Avram Date: Mon, 3 Dec 2018 21:13:56 +0200 Subject: [PATCH 2/9] Test with invalid regex. --- fastcache_test.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/fastcache_test.go b/fastcache_test.go index 24d786f..100e3a2 100644 --- a/fastcache_test.go +++ b/fastcache_test.go @@ -227,6 +227,14 @@ func TestCacheKeys(t *testing.T) { t.Fatalf("failed to retrievs keys by pattern: \"%s\"", tt.Pattern) } } + + result, err := c.Keys("*") + if result != nil { + t.Fatal("expected no matches") + } + if err == nil { + t.Fatal("expected regex error") + } } func testCacheGetSet(c *Cache, itemsCount int) error { From 591074d3311e9003be40b59d9ca34e778c634ac2 Mon Sep 17 00:00:00 2001 From: Andrei Avram Date: Wed, 5 Dec 2018 23:21:09 +0200 Subject: [PATCH 3/9] Implement cache items visitor. --- README.md | 3 +- fastcache.go | 41 ++++++++++++++-------- fastcache_test.go | 75 ++++++++-------------------------------- fastcache_timing_test.go | 37 ++++++++------------ 4 files changed, 56 insertions(+), 100 deletions(-) diff --git a/README.md b/README.md index 6cbc475..c3d9a2f 100644 --- a/README.md +++ b/README.md @@ -46,8 +46,7 @@ BenchmarkSyncMapGet-4 5000 2316508 ns/op 28.29 MB/s 2543 B/o BenchmarkSyncMapSetGet-4 2000 10444529 ns/op 12.55 MB/s 3412527 B/op 262210 allocs/op BenchmarkSaveToFile-4 50 259800249 ns/op 129.15 MB/s 55739129 B/op 3091 allocs/op BenchmarkLoadFromFile-4 100 121189395 ns/op 276.88 MB/s 98089036 B/op 8748 allocs/op -BenchmarkCache_Keys-8 100000 18359 ns/op 16.34 MB/s 39072 B/op 30 allocs/op - +BenchmarkCache_VisitAllEntries-4 50000 245913 ns/op 40.66 MB/s 170 B/op 2 allocs/op ``` `MB/s` column here actually means `millions of operations per second`. diff --git a/fastcache.go b/fastcache.go index 1f2174c..d4c2108 100644 --- a/fastcache.go +++ b/fastcache.go @@ -2,7 +2,6 @@ package fastcache import ( "fmt" - "regexp" "sync" "sync/atomic" @@ -187,24 +186,38 @@ func (c *Cache) UpdateStats(s *Stats) { s.InvalidValueHashErrors += atomic.LoadUint64(&c.bigStats.InvalidValueHashErrors) } -// Keys retrieves all cached keys matching regex pattern -func (c *Cache) Keys(pattern string) (keys [][]byte, err error) { - r, err := regexp.Compile(pattern) - if err != nil { - return - } - +// VisitAllEntries calls f for all the cache entries. +// +// The function returns immediately if f returns non-nil error. +// It returns the given error. +// +// f cannot hold pointers to k and v contents after returning. +func (c *Cache) VisitAllEntries(f func(k, v []byte) error) error { for _, b := range c.buckets { - for _, chunk := range b.chunks { - if len(chunk) > 0 { - if key := chunk[4 : 4+chunk[1]]; r.Match(key) { - keys = append(keys, key) - } + b.mu.RLock() + for _, idx := range b.m { + idx &= (1 << bucketSizeBits) - 1 + chunkIdx := idx / chunkSize + chunk := b.chunks[chunkIdx] + + kvLenBuf := chunk[idx : idx+4] + keyLen := (uint64(kvLenBuf[0]) << 8) | uint64(kvLenBuf[1]) + valLen := (uint64(kvLenBuf[2]) << 8) | uint64(kvLenBuf[3]) + + idx += 4 + key := chunk[idx : idx+keyLen] + + idx += keyLen + value := chunk[idx : idx+valLen] + + if err := f(key, value); err != nil { + return err } } + b.mu.RUnlock() } - return + return nil } type bucket struct { diff --git a/fastcache_test.go b/fastcache_test.go index 100e3a2..b7f13a2 100644 --- a/fastcache_test.go +++ b/fastcache_test.go @@ -168,73 +168,26 @@ func TestCacheGetSetConcurrent(t *testing.T) { } } -func TestCacheKeys(t *testing.T) { - keys := []string{ - "username", - "firstname", - "lastname", - } - - c := New(100 * len(keys)) +func TestCacheVisitAllEntries(t *testing.T) { + itemsCount := 10000 + c := New(30 * itemsCount) defer c.Reset() - for _, k := range keys { - c.Set([]byte(k), nil) - } + data := make(map[string][]byte) - tests := []struct { - Pattern string - Expected []string - }{ - { - Pattern: "", - Expected: keys, - }, - { - Pattern: "name$", - Expected: keys, - }, - { - Pattern: "st", - Expected: []string{ - "firstname", - "lastname", - }, - }, - { - Pattern: " ", - Expected: nil, - }, + for i := 0; i < itemsCount; i++ { + k := []byte(fmt.Sprintf("key %d", i)) + v := []byte(fmt.Sprintf("value %d", i)) + c.Set(k, v) + data[string(k)] = v } - for _, tt := range tests { - result, err := c.Keys(tt.Pattern) - if err != nil { - t.Fatal(err) + _ = c.VisitAllEntries(func(k, v []byte) error { + if string(data[string(k)]) != string(v) { + t.Fatal("error fetching (k, v) pair") } - - count := 0 - for _, r := range result { - for _, e := range tt.Expected { - if string(r) == e { - count++ - break - } - } - } - - if count != len(tt.Expected) { - t.Fatalf("failed to retrievs keys by pattern: \"%s\"", tt.Pattern) - } - } - - result, err := c.Keys("*") - if result != nil { - t.Fatal("expected no matches") - } - if err == nil { - t.Fatal("expected regex error") - } + return nil + }) } func testCacheGetSet(c *Cache, itemsCount int) error { diff --git a/fastcache_timing_test.go b/fastcache_timing_test.go index e824ad3..c4eaf26 100644 --- a/fastcache_timing_test.go +++ b/fastcache_timing_test.go @@ -388,35 +388,26 @@ func BenchmarkSyncMapSetGet(b *testing.B) { }) } -func BenchmarkCache_Keys(b *testing.B) { - keys := []string{ - "username", - "firstname", - "lastname", - } - pattern := "name$" - - items := 100 * len(keys) - - c := New(items) +func BenchmarkCache_VisitAllEntries(b *testing.B) { + itemsCount := 10000 + c := New(30 * itemsCount) defer c.Reset() b.ReportAllocs() - b.SetBytes(int64(items)) + b.SetBytes(int64(itemsCount)) - for _, k := range keys { - c.Set([]byte(k), nil) + data := make(map[string][]byte) + + for i := 0; i < itemsCount; i++ { + k := []byte(fmt.Sprintf("key %d", i)) + v := []byte(fmt.Sprintf("value %d", i)) + c.Set(k, v) + data[string(k)] = v } for n := 0; n < b.N; n++ { - keys, err := c.Keys(pattern) - - if err != nil { - panic(fmt.Errorf("BUG: error for valid pattern \"%s\" (%s)", pattern, err)) - } - - if len(keys) == 0 { - panic(fmt.Errorf("BUG: no key for valid pattern \"%s\"", pattern)) - } + _ = c.VisitAllEntries(func(k, v []byte) error { + return nil + }) } } From 6456ba3eb22a917eec99d0d2f1ac7345bac427d8 Mon Sep 17 00:00:00 2001 From: Andrei Avram Date: Thu, 6 Dec 2018 17:53:43 +0200 Subject: [PATCH 4/9] Improve docs. --- fastcache.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fastcache.go b/fastcache.go index d4c2108..b160ecd 100644 --- a/fastcache.go +++ b/fastcache.go @@ -189,9 +189,10 @@ func (c *Cache) UpdateStats(s *Stats) { // VisitAllEntries calls f for all the cache entries. // // The function returns immediately if f returns non-nil error. -// It returns the given error. +// It returns the error returned by f. // // f cannot hold pointers to k and v contents after returning. +// f cannot modify k and v contents. func (c *Cache) VisitAllEntries(f func(k, v []byte) error) error { for _, b := range c.buckets { b.mu.RLock() From 2593d33c901a0fbabef4ea783e62ca675e3166b3 Mon Sep 17 00:00:00 2001 From: Andrei Avram Date: Thu, 6 Dec 2018 18:21:57 +0200 Subject: [PATCH 5/9] Improve tests. --- fastcache_test.go | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/fastcache_test.go b/fastcache_test.go index b7f13a2..b81dcf9 100644 --- a/fastcache_test.go +++ b/fastcache_test.go @@ -1,6 +1,8 @@ package fastcache import ( + "bytes" + "errors" "fmt" "runtime" "sync" @@ -173,21 +175,44 @@ func TestCacheVisitAllEntries(t *testing.T) { c := New(30 * itemsCount) defer c.Reset() - data := make(map[string][]byte) + expected := make(map[string][]byte) for i := 0; i < itemsCount; i++ { k := []byte(fmt.Sprintf("key %d", i)) v := []byte(fmt.Sprintf("value %d", i)) c.Set(k, v) - data[string(k)] = v + expected[string(k)] = v } - _ = c.VisitAllEntries(func(k, v []byte) error { - if string(data[string(k)]) != string(v) { - t.Fatal("error fetching (k, v) pair") - } + result := make(map[string][]byte) + + err := c.VisitAllEntries(func(k, v []byte) error { + result[string(k)] = v return nil }) + + if err != nil { + t.Fatalf("error was not expected: %s", err) + } + + if len(result) != len(expected) { + t.Fatalf("visitor returned %d items instead of %d", len(result), len(expected)) + } + + for k, v := range expected { + if bytes.Compare(result[k], v) != 0 { + t.Fatalf("invalid value %v for key \"%s\", expected %v", result[k], k, v) + } + } + + callBackErr := errors.New("err") + err = c.VisitAllEntries(func(k, v []byte) error { + return callBackErr + }) + + if err != callBackErr { + t.Fatal("error was expected") + } } func testCacheGetSet(c *Cache, itemsCount int) error { From 0790c446e312740306fe7ccc7996a622b0f8a4ae Mon Sep 17 00:00:00 2001 From: Andrei Avram Date: Thu, 6 Dec 2018 18:23:17 +0200 Subject: [PATCH 6/9] Move visitor function to bucket. --- fastcache.go | 49 +++++++++++++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/fastcache.go b/fastcache.go index b160ecd..2d926c4 100644 --- a/fastcache.go +++ b/fastcache.go @@ -195,27 +195,9 @@ func (c *Cache) UpdateStats(s *Stats) { // f cannot modify k and v contents. func (c *Cache) VisitAllEntries(f func(k, v []byte) error) error { for _, b := range c.buckets { - b.mu.RLock() - for _, idx := range b.m { - idx &= (1 << bucketSizeBits) - 1 - chunkIdx := idx / chunkSize - chunk := b.chunks[chunkIdx] - - kvLenBuf := chunk[idx : idx+4] - keyLen := (uint64(kvLenBuf[0]) << 8) | uint64(kvLenBuf[1]) - valLen := (uint64(kvLenBuf[2]) << 8) | uint64(kvLenBuf[3]) - - idx += 4 - key := chunk[idx : idx+keyLen] - - idx += keyLen - value := chunk[idx : idx+valLen] - - if err := f(key, value); err != nil { - return err - } + if err := b.VisitAllEntries(f); err != nil { + return err } - b.mu.RUnlock() } return nil @@ -417,3 +399,30 @@ func (b *bucket) Del(h uint64) { delete(b.m, h) b.mu.Unlock() } + +func (b *bucket) VisitAllEntries(f func(k, v []byte) error) error { + b.mu.RLock() + for _, idx := range b.m { + idx &= (1 << bucketSizeBits) - 1 + chunkIdx := idx / chunkSize + chunk := b.chunks[chunkIdx] + + kvLenBuf := chunk[idx : idx+4] + keyLen := (uint64(kvLenBuf[0]) << 8) | uint64(kvLenBuf[1]) + valLen := (uint64(kvLenBuf[2]) << 8) | uint64(kvLenBuf[3]) + + idx += 4 + key := chunk[idx : idx+keyLen] + + idx += keyLen + value := chunk[idx : idx+valLen] + + if err := f(key, value); err != nil { + b.mu.RUnlock() + return err + } + } + b.mu.RUnlock() + + return nil +} From 98d9aa39b9dd46886d8d73d5cf6530340b177b89 Mon Sep 17 00:00:00 2001 From: Andrei Avram Date: Thu, 6 Dec 2018 18:49:36 +0200 Subject: [PATCH 7/9] Limit key and value capacities. --- fastcache.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fastcache.go b/fastcache.go index 2d926c4..c9acd92 100644 --- a/fastcache.go +++ b/fastcache.go @@ -412,10 +412,10 @@ func (b *bucket) VisitAllEntries(f func(k, v []byte) error) error { valLen := (uint64(kvLenBuf[2]) << 8) | uint64(kvLenBuf[3]) idx += 4 - key := chunk[idx : idx+keyLen] + key := chunk[idx : idx+keyLen : idx+keyLen] idx += keyLen - value := chunk[idx : idx+valLen] + value := chunk[idx : idx+valLen : idx+valLen] if err := f(key, value); err != nil { b.mu.RUnlock() From 7f5248ea382b1e2963e0f52a0ead96b005fdf15b Mon Sep 17 00:00:00 2001 From: Andrei Avram Date: Thu, 6 Dec 2018 19:40:44 +0200 Subject: [PATCH 8/9] Skip overwritten entries. --- fastcache.go | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/fastcache.go b/fastcache.go index c9acd92..c837d6e 100644 --- a/fastcache.go +++ b/fastcache.go @@ -402,24 +402,28 @@ func (b *bucket) Del(h uint64) { func (b *bucket) VisitAllEntries(f func(k, v []byte) error) error { b.mu.RLock() - for _, idx := range b.m { - idx &= (1 << bucketSizeBits) - 1 - chunkIdx := idx / chunkSize - chunk := b.chunks[chunkIdx] + for _, v := range b.m { + idx := v & ((1 << bucketSizeBits) - 1) + gen := v >> bucketSizeBits + + if gen == b.gen && idx < b.idx || gen+1 == b.gen && idx >= b.idx { + chunkIdx := idx / chunkSize + chunk := b.chunks[chunkIdx] - kvLenBuf := chunk[idx : idx+4] - keyLen := (uint64(kvLenBuf[0]) << 8) | uint64(kvLenBuf[1]) - valLen := (uint64(kvLenBuf[2]) << 8) | uint64(kvLenBuf[3]) + kvLenBuf := chunk[idx : idx+4] + keyLen := (uint64(kvLenBuf[0]) << 8) | uint64(kvLenBuf[1]) + valLen := (uint64(kvLenBuf[2]) << 8) | uint64(kvLenBuf[3]) - idx += 4 - key := chunk[idx : idx+keyLen : idx+keyLen] + idx += 4 + key := chunk[idx : idx+keyLen : idx+keyLen] - idx += keyLen - value := chunk[idx : idx+valLen : idx+valLen] + idx += keyLen + value := chunk[idx : idx+valLen : idx+valLen] - if err := f(key, value); err != nil { - b.mu.RUnlock() - return err + if err := f(key, value); err != nil { + b.mu.RUnlock() + return err + } } } b.mu.RUnlock() From 39cf0ef7bc1f377740c7c0feaa9680d1f4a528b4 Mon Sep 17 00:00:00 2001 From: Andrei Avram Date: Thu, 6 Dec 2018 19:45:33 +0200 Subject: [PATCH 9/9] Improve docs. --- fastcache.go | 1 + 1 file changed, 1 insertion(+) diff --git a/fastcache.go b/fastcache.go index c837d6e..e173642 100644 --- a/fastcache.go +++ b/fastcache.go @@ -191,6 +191,7 @@ func (c *Cache) UpdateStats(s *Stats) { // The function returns immediately if f returns non-nil error. // It returns the error returned by f. // +// f is called sequentially for all the entries in the cache. // f cannot hold pointers to k and v contents after returning. // f cannot modify k and v contents. func (c *Cache) VisitAllEntries(f func(k, v []byte) error) error {