├── .gitignore ├── examples ├── helper │ └── assert.go ├── on-evict.go ├── simple.go ├── typed.go ├── range.go ├── iterators.go ├── batch.go ├── not-found.go ├── vacuum.go └── get-or-set-if-missing.go ├── go.mod ├── log.go ├── go.sum ├── LICENSE ├── models.go ├── lock.go ├── x_fuzz_test.go ├── lock_test.go ├── utils.go ├── x_benchmark_test.go ├── facad.go ├── facad_test.go ├── db.go ├── cache.go ├── README.md └── cache_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.db -------------------------------------------------------------------------------- /examples/helper/assert.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | func AssertNoErr(err error) { 4 | if err != nil { 5 | panic(err) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/modfin/cove 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/mattn/go-sqlite3 v1.14.24 7 | github.com/stretchr/testify v1.9.0 8 | ) 9 | 10 | require ( 11 | github.com/davecgh/go-spew v1.1.1 // indirect 12 | github.com/pmezard/go-difflib v1.0.0 // indirect 13 | gopkg.in/yaml.v3 v3.0.1 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /log.go: -------------------------------------------------------------------------------- 1 | package cove 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | ) 7 | 8 | type discardLogger struct{} 9 | 10 | func (d discardLogger) Enabled(ctx context.Context, level slog.Level) bool { 11 | return true 12 | } 13 | 14 | func (d discardLogger) Handle(ctx context.Context, record slog.Record) error { 15 | return nil 16 | } 17 | 18 | func (d discardLogger) WithAttrs(attrs []slog.Attr) slog.Handler { 19 | return d 20 | } 21 | 22 | func (d discardLogger) WithGroup(name string) slog.Handler { 23 | return d 24 | } 25 | -------------------------------------------------------------------------------- /examples/on-evict.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/modfin/cove" 6 | "github.com/modfin/cove/examples/helper" 7 | ) 8 | 9 | func main() { 10 | 11 | // creates a sqlite cache named ./cove.db in the directory of the execution 12 | // it adds a callback function for eviction notices 13 | cache, err := cove.New( 14 | cove.URITemp(), 15 | cove.DBRemoveOnClose(), 16 | cove.WithEvictCallback( 17 | func(key string, val []byte) { 18 | fmt.Printf("evicted %s: %s\n", key, string(val)) 19 | // Maybe do som stuff, proactively refresh the cache 20 | }), 21 | ) 22 | helper.AssertNoErr(err) 23 | cache.Close() 24 | 25 | _ = cache.Set("key", []byte("evict me")) 26 | _, _ = cache.Evict("key") 27 | // evicted key: evict me 28 | cache.Close() 29 | } 30 | -------------------------------------------------------------------------------- /examples/simple.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/modfin/cove" 6 | "github.com/modfin/cove/examples/helper" 7 | "time" 8 | ) 9 | 10 | func main() { 11 | 12 | // creates a sqlite cache in a temporary directory, 13 | // once the cache is closed the database is removed 14 | // a default TTL of 10 minutes is set 15 | cache, err := cove.New( 16 | cove.URITemp(), 17 | cove.DBRemoveOnClose(), 18 | cove.WithTTL(time.Minute*10), 19 | ) 20 | helper.AssertNoErr(err) 21 | defer cache.Close() 22 | 23 | // set a key value pair in the cache 24 | err = cache.Set("key", []byte("value0")) 25 | helper.AssertNoErr(err) 26 | 27 | // get the value from the cache 28 | value, err := cache.Get("key") 29 | helper.AssertNoErr(err) 30 | 31 | fmt.Println(string(value)) 32 | // value0 33 | } 34 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= 4 | github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 8 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 9 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 10 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 11 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 12 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Rasmus Holm 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /models.go: -------------------------------------------------------------------------------- 1 | package cove 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | ) 7 | 8 | const NS_DEFAULT = "default" 9 | const MAX_PARAMS = 999 10 | const MAX_BLOB_SIZE = 1_000_000_000 - 100 // 1GB - 100 bytes 11 | 12 | const RANGE_MIN = string(byte(0)) 13 | const RANGE_MAX = string(byte(255)) 14 | 15 | // NO_TTL is a constant that represents no ttl, kind of, it is really just a very long time for a cache 16 | const NO_TTL = time.Hour * 24 * 365 * 100 // 100 years (effectively forever) 17 | 18 | var NotFound = errors.New("not found") 19 | 20 | func Zip[V any](keys []string, values []V) []KV[V] { 21 | size := min(len(keys), len(values)) 22 | var res = make([]KV[V], size) 23 | 24 | for i := 0; i < size; i++ { 25 | res[i] = KV[V]{K: keys[i], V: values[i]} 26 | } 27 | return res 28 | } 29 | 30 | func Unzip[V any](kv []KV[V]) (keys []string, vals []V) { 31 | size := len(kv) 32 | keys = make([]string, size) 33 | vals = make([]V, size) 34 | 35 | for i, kv := range kv { 36 | keys[i] = kv.K 37 | vals[i] = kv.V 38 | } 39 | return keys, vals 40 | } 41 | 42 | type KV[T any] struct { 43 | K string 44 | V T 45 | } 46 | 47 | func (kv KV[T]) Unzip() (string, T) { 48 | return kv.K, kv.V 49 | } 50 | 51 | func (kv KV[T]) Key() string { 52 | return kv.K 53 | } 54 | func (kv KV[T]) Value() T { 55 | return kv.V 56 | } 57 | -------------------------------------------------------------------------------- /examples/typed.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/modfin/cove" 6 | "github.com/modfin/cove/examples/helper" 7 | "time" 8 | ) 9 | 10 | type Person struct { 11 | Name string 12 | Age int 13 | } 14 | 15 | func main() { 16 | 17 | // creates a sqlite cache in a temporary directory, 18 | // once the cache is closed the database is removed 19 | // a default TTL of 10 minutes is set 20 | cache, err := cove.New(cove.URITemp(), 21 | cove.DBRemoveOnClose(), 22 | cove.WithTTL(time.Minute*10), 23 | ) 24 | helper.AssertNoErr(err) 25 | defer cache.Close() 26 | 27 | typed := cove.Of[Person](cache) 28 | helper.AssertNoErr(err) 29 | 30 | // set a key value pair in the cache 31 | err = typed.Set("alice", Person{Name: "Alice", Age: 30}) 32 | helper.AssertNoErr(err) 33 | 34 | err = typed.Set("bob", Person{Name: "Bob", Age: 40}) 35 | helper.AssertNoErr(err) 36 | 37 | err = typed.Set("charlie", Person{Name: "Bob", Age: 40}) 38 | helper.AssertNoErr(err) 39 | 40 | // get the value from the cache 41 | alice, err := typed.Get("alice") 42 | helper.AssertNoErr(err) 43 | 44 | fmt.Printf("%+v\n", alice) 45 | // {Name:Alice Age:30} 46 | 47 | zero, err := typed.Get("does-not-exist") 48 | fmt.Println("zero:", fmt.Sprintf("%+v", zero)) 49 | // zero: {Name: Age:0} 50 | 51 | fmt.Println("err == cove.NotFound:", err == cove.NotFound) 52 | // err == cove.NotFound: true 53 | 54 | } 55 | -------------------------------------------------------------------------------- /examples/range.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/modfin/cove" 6 | "github.com/modfin/cove/examples/helper" 7 | "time" 8 | ) 9 | 10 | func main() { 11 | 12 | // creates a sqlite cache in a temporary directory, 13 | // once the cache is closed the database is removed 14 | // a default TTL of 10 minutes is set 15 | cache, err := cove.New( 16 | cove.URITemp(), 17 | cove.DBRemoveOnClose(), 18 | cove.WithTTL(time.Minute*10), 19 | ) 20 | helper.AssertNoErr(err) 21 | defer cache.Close() 22 | 23 | // set a key value pairs in the cache 24 | for i := 0; i < 100; i++ { 25 | err = cache.Set(fmt.Sprintf("key%d", i), []byte(fmt.Sprintf("value%d", i))) 26 | helper.AssertNoErr(err) 27 | } 28 | 29 | // Tuple range 30 | kvs, err := cache.Range("key97", cove.RANGE_MAX) 31 | helper.AssertNoErr(err) 32 | 33 | for _, kv := range kvs { 34 | fmt.Println(kv.K, string(kv.V)) 35 | //key97 value97 36 | //key98 value98 37 | //key99 value99 38 | } 39 | 40 | // Key range 41 | keys, err := cache.Keys(cove.RANGE_MIN, "key1") 42 | helper.AssertNoErr(err) 43 | 44 | for _, key := range keys { 45 | fmt.Println(key) 46 | //key0 47 | //key1 48 | } 49 | 50 | // Value range 51 | values, err := cache.Values(cove.RANGE_MIN, "key1") 52 | helper.AssertNoErr(err) 53 | 54 | for _, value := range values { 55 | fmt.Println(string(value)) 56 | //value0 57 | //value1 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lock.go: -------------------------------------------------------------------------------- 1 | package cove 2 | 3 | import "sync" 4 | 5 | type keyedMutex struct { 6 | mu sync.Mutex 7 | locks map[string]*lockEntry 8 | } 9 | 10 | type lockEntry struct { 11 | mu sync.Mutex 12 | refCount int 13 | } 14 | 15 | func keyedMu() *keyedMutex { 16 | sl := &keyedMutex{ 17 | locks: make(map[string]*lockEntry), 18 | } 19 | 20 | return sl 21 | } 22 | 23 | func (km *keyedMutex) Locked(key string) bool { 24 | km.mu.Lock() 25 | defer km.mu.Unlock() 26 | le, exists := km.locks[key] 27 | return exists && le.refCount > 0 28 | } 29 | 30 | func (km *keyedMutex) TryLocked(key string) bool { 31 | km.mu.Lock() 32 | defer km.mu.Unlock() 33 | le, exists := km.locks[key] 34 | if exists && le.refCount > 0 { 35 | return false 36 | } 37 | if !exists { 38 | le = &lockEntry{} 39 | km.locks[key] = le 40 | } 41 | le.refCount++ 42 | le.mu.Lock() 43 | return true 44 | } 45 | 46 | func (km *keyedMutex) Lock(key string) { 47 | km.mu.Lock() 48 | le, exists := km.locks[key] 49 | if !exists { 50 | le = &lockEntry{} 51 | km.locks[key] = le 52 | } 53 | le.refCount++ 54 | km.mu.Unlock() 55 | 56 | le.mu.Lock() 57 | } 58 | 59 | func (km *keyedMutex) Unlock(key string) { 60 | km.mu.Lock() 61 | defer km.mu.Unlock() 62 | 63 | le, exists := km.locks[key] 64 | if !exists { 65 | panic("unlock of unlocked lock") 66 | } 67 | le.refCount-- 68 | if le.refCount == 0 { 69 | delete(km.locks, key) 70 | } 71 | le.mu.Unlock() 72 | } 73 | -------------------------------------------------------------------------------- /examples/iterators.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/modfin/cove" 6 | "github.com/modfin/cove/examples/helper" 7 | "time" 8 | ) 9 | 10 | // WARNING 11 | // Since iterators don't really have any way of communication errors 12 | // the Con is that errors are dropped when using iterators. 13 | // the Pro is that it is very easy to use, and scan row by row (ie. no need to load all rows into memory) 14 | 15 | func main() { 16 | 17 | // creates a sqlite cache in a temporary directory, 18 | // once the cache is closed the database is removed 19 | // a default TTL of 10 minutes is set 20 | cache, err := cove.New(cove.URITemp(), 21 | cove.DBRemoveOnClose(), 22 | cove.WithTTL(time.Minute*10), 23 | ) 24 | helper.AssertNoErr(err) 25 | defer cache.Close() 26 | 27 | // set a key value pairs in the cache 28 | for i := 0; i < 100; i++ { 29 | err = cache.Set(fmt.Sprintf("key%d", i), []byte(fmt.Sprintf("value%d", i))) 30 | helper.AssertNoErr(err) 31 | } 32 | 33 | // KV iterator 34 | for k, v := range cache.ItrRange("key97", cove.RANGE_MAX) { 35 | fmt.Println(k, string(v)) 36 | //key97 value97 37 | //key98 value98 38 | //key99 value99 39 | } 40 | 41 | // Key iterator 42 | for key := range cache.ItrKeys(cove.RANGE_MIN, "key1") { 43 | fmt.Println(key) 44 | //key0 45 | //key1 46 | } 47 | 48 | // Value iterator 49 | for value := range cache.ItrValues(cove.RANGE_MIN, "key1") { 50 | fmt.Println(string(value)) 51 | //value0 52 | //value1 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /x_fuzz_test.go: -------------------------------------------------------------------------------- 1 | package cove_test 2 | 3 | import ( 4 | "fmt" 5 | "github.com/modfin/cove" 6 | "github.com/stretchr/testify/assert" 7 | "testing" 8 | ) 9 | 10 | // Since we are stepping on eachother with keys, use parallel=1 11 | // go test -v -fuzz=. -run="^#" -parallel=1 -fuzztime 30s 12 | func FuzzGetSet(f *testing.F) { 13 | 14 | testcases := []cove.KV[string]{ 15 | {K: "key", V: "value"}, 16 | {K: "1", V: "2"}, 17 | {K: "2", V: ""}, 18 | {K: "", V: ""}, 19 | {K: "", V: "some value"}, 20 | {K: "SELECT 1", V: "; DROP TABLE users;"}, 21 | {K: "; DROP TABLE users;", V: "sleep 10;"}, 22 | {K: "", V: string([]byte{0xff, 0x7f})}, 23 | {K: string(rune(0)), V: string([]byte{0})}, 24 | {K: string(rune(255)), V: string([]byte{255})}, 25 | {K: "a", V: "1\x12\xd78{"}, 26 | {K: "b", V: "\xff\x7f"}, 27 | {"\xff\x7f\xff\xff\xaa\b", "10"}, 28 | } 29 | for _, tc := range testcases { 30 | f.Add(tc.K, []byte(tc.V)) // Use f.Add to provide a seed corpus 31 | } 32 | cache, err := cove.New(cove.URITemp(), cove.DBRemoveOnClose()) 33 | assert.NoError(f, err) 34 | 35 | f.Fuzz(func(t *testing.T, key string, val []byte) { 36 | fmt.Println("fuzz k:", key, "v:", string(val)) 37 | 38 | err := cache.Set(key, val) 39 | assert.NoError(t, err) 40 | // 41 | v, err := cache.Get(key) 42 | assert.NoError(t, err) 43 | assert.Equal(t, val, v) 44 | 45 | kv, err := cache.Evict(key) 46 | assert.NoError(t, err) 47 | assert.Equal(t, key, kv.K) 48 | assert.Equal(t, val, kv.V) 49 | 50 | _, err = cache.Get(key) 51 | assert.Error(t, err) 52 | assert.Equal(t, err, cove.NotFound) 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /examples/batch.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/modfin/cove" 6 | "github.com/modfin/cove/examples/helper" 7 | ) 8 | 9 | func main() { 10 | 11 | // creates a sqlite cache in a temporary directory, 12 | // once the cache is closed the database is removed 13 | // a default TTL of 10 minutes is set 14 | cache, err := cove.New( 15 | cove.URITemp(), 16 | cove.DBRemoveOnClose(), 17 | cove.WithEvictCallback(func(key string, _ []byte) { 18 | fmt.Println("Callback, key", key, "was evicted") 19 | }), 20 | ) 21 | helper.AssertNoErr(err) 22 | defer cache.Close() 23 | 24 | // Helper functions to construct []KV[[]byte] slice 25 | KeyValueSet := cove.Zip( 26 | []string{"key1", "key2"}, 27 | [][]byte{[]byte("val1"), []byte("val2")}) 28 | 29 | err = cache.BatchSet(KeyValueSet) 30 | helper.AssertNoErr(err) 31 | 32 | kvs, err := cache.BatchGet([]string{"key1", "key2", "key3"}) 33 | helper.AssertNoErr(err) 34 | 35 | for _, kv := range kvs { 36 | fmt.Println(kv.Unzip()) 37 | // output: 38 | // key1 [118 97 108 49] 39 | // key2 [118 97 108 50] 40 | } 41 | 42 | evicted, err := cache.BatchEvict([]string{"key1", "key2", "key3"}) 43 | helper.AssertNoErr(err) 44 | // output: 45 | // Callback, key key1 was evicted 46 | // Callback, key key2 was evicted 47 | 48 | // Helper function to unzip []KV[[]byte] to k/v slices 49 | evictedKeys, evictedVals := cove.Unzip(evicted) 50 | 51 | for i, key := range evictedKeys { 52 | fmt.Println("Evicted,", key, "-", string(evictedVals[i])) 53 | // output: 54 | // Evicted, key1 - val1 55 | // Evicted, key2 - val2 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /examples/not-found.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/modfin/cove" 7 | "github.com/modfin/cove/examples/helper" 8 | "time" 9 | ) 10 | 11 | func main() { 12 | 13 | // creates a sqlite cache in a temporary directory, 14 | // once the cache is closed the database is removed 15 | // a default TTL of 10 minutes is set 16 | cache, err := cove.New( 17 | cove.URITemp(), 18 | cove.DBRemoveOnClose(), 19 | cove.WithTTL(time.Minute*10), 20 | ) 21 | helper.AssertNoErr(err) 22 | defer cache.Close() 23 | 24 | // A key that does not exist will return cove.NotFound error 25 | _, err = cache.Get("key") 26 | fmt.Println("err == cove.NotFound:", err == cove.NotFound) 27 | // err == cove.NotFound: true 28 | fmt.Println("errors.Is(err, cove.NotFound):", errors.Is(err, cove.NotFound)) 29 | // errors.Is(err, cove.NotFound): true 30 | 31 | // A nice pattern to use to split the error handling from the logic of hit or not might be the following 32 | _, err = cache.Get("key") 33 | hit, err := cove.Hit(err) // false, nil 34 | if err != nil { 35 | panic(err) // something went wrong with the sqlite database 36 | } 37 | if !hit { 38 | fmt.Println("key miss") 39 | // key miss 40 | } 41 | 42 | // Or the opposite for convenience using Mis 43 | 44 | // A nice pattern to use to split the error handling from the logic of hit or not might be the following 45 | _, err = cache.Get("key") 46 | miss, err := cove.Miss(err) // false, nil 47 | if err != nil { 48 | panic(err) // something went wrong with the sqlite database 49 | } 50 | if miss { 51 | fmt.Println("key miss") 52 | // key miss 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /examples/vacuum.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/modfin/cove" 5 | "github.com/modfin/cove/examples/helper" 6 | "log/slog" 7 | "time" 8 | ) 9 | 10 | func main() { 11 | 12 | // creates a sqlite cache in a temporary directory, 13 | // once the cache is closed the database is removed 14 | // a default TTL of 500ms 15 | // a logger is set to default slog.Default() 16 | // a vacuum is set to run every 100ms, and vacuum at most 1000 items at a time 17 | cache, err := cove.New( 18 | cove.URITemp(), 19 | cove.DBRemoveOnClose(), 20 | cove.WithTTL(time.Millisecond*500), 21 | 22 | cove.WithLogger(slog.Default()), 23 | 24 | // vacuum every 100ms, and vacuum at most 1000 items at a time 25 | // This might be important to calibrate from your use case, as it might cause a lot of writes to the database 26 | // along with on-evict calls for expired items. 27 | cove.WithVacuum(cove.Vacuum(100*time.Millisecond, 1_000)), 28 | 29 | // on-evict callback prints the key that is evicted 30 | cove.WithEvictCallback(func(key string, value []byte) { 31 | slog.Default().Info("evicting", "key", key) 32 | }), 33 | ) 34 | helper.AssertNoErr(err) 35 | defer cache.Close() 36 | 37 | // set a key value pair in the cache 38 | err = cache.Set("key", []byte("value0")) 39 | helper.AssertNoErr(err) 40 | 41 | // set a key value pair in the cache 42 | err = cache.Set("key1", []byte("value0")) 43 | helper.AssertNoErr(err) 44 | 45 | // set a key value pair in the cache 46 | err = cache.Set("key2", []byte("value0")) 47 | helper.AssertNoErr(err) 48 | 49 | time.Sleep(time.Second) 50 | //2024/10/24 17:05:16 INFO [cove] vacuumed ns=default time=736.684µs n=3 51 | //2024/10/24 17:05:16 INFO evicting key=key 52 | //2024/10/24 17:05:16 INFO evicting key=key1 53 | //2024/10/24 17:05:16 INFO evicting key=key2 54 | 55 | } 56 | -------------------------------------------------------------------------------- /lock_test.go: -------------------------------------------------------------------------------- 1 | package cove 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestKeyedMutexLocked(t *testing.T) { 11 | km := keyedMu() 12 | key := "test-key" 13 | 14 | assert.False(t, km.Locked(key)) 15 | 16 | km.Lock(key) 17 | assert.True(t, km.Locked(key)) 18 | 19 | km.Unlock(key) 20 | assert.False(t, km.Locked(key)) 21 | } 22 | 23 | func TestKeyedMutexTryLocked(t *testing.T) { 24 | km := keyedMu() 25 | key := "test-key" 26 | 27 | assert.True(t, km.TryLocked(key)) 28 | assert.False(t, km.TryLocked(key)) 29 | 30 | km.Unlock(key) 31 | assert.True(t, km.TryLocked(key)) 32 | } 33 | 34 | func TestKeyedMutexLockUnlock(t *testing.T) { 35 | km := keyedMu() 36 | key := "test-key" 37 | 38 | km.Lock(key) 39 | assert.True(t, km.Locked(key)) 40 | 41 | km.Unlock(key) 42 | assert.False(t, km.Locked(key)) 43 | } 44 | 45 | func TestKeyedMutexUnlockWithoutLock(t *testing.T) { 46 | km := keyedMu() 47 | key := "test-key" 48 | 49 | assert.Panics(t, func() { 50 | km.Unlock(key) 51 | }) 52 | } 53 | 54 | func TestKeyedMutexConcurrentAccess(t *testing.T) { 55 | km := keyedMu() 56 | key := "test-key" 57 | var wg sync.WaitGroup 58 | 59 | wg.Add(2) 60 | go func() { 61 | defer wg.Done() 62 | km.Lock(key) 63 | defer km.Unlock(key) 64 | }() 65 | 66 | go func() { 67 | defer wg.Done() 68 | km.Lock(key) 69 | defer km.Unlock(key) 70 | }() 71 | 72 | wg.Wait() 73 | assert.False(t, km.Locked(key)) 74 | } 75 | 76 | func TestKeyedMutexConcurrentAccessMulti(t *testing.T) { 77 | km := keyedMu() 78 | key := "test-key" 79 | var wg sync.WaitGroup 80 | 81 | var size = 1000 82 | var count int 83 | 84 | for i := 0; i < size; i++ { 85 | wg.Add(1) 86 | go func() { 87 | defer wg.Done() 88 | km.Lock(key) 89 | defer km.Unlock(key) 90 | count++ 91 | }() 92 | } 93 | 94 | wg.Wait() 95 | assert.False(t, km.Locked(key)) 96 | assert.Equal(t, size, count) 97 | } 98 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package cove 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "time" 9 | ) 10 | 11 | func Vacuum(interval time.Duration, max int) func(cache *Cache) { 12 | return func(cache *Cache) { 13 | 14 | vacuumNamespace := func(ns *Cache) int { 15 | start := time.Now() 16 | n, err := ns.Vacuum(max) 17 | elapsed := time.Since(start) 18 | if err != nil { 19 | cache.log.Error("could not vacuum namespace", "err", err) 20 | return 0 21 | } 22 | if cache.log != nil && n > 0 { 23 | cache.log.Debug("[cove] vacuumed", "ns", ns.namespace, "time", elapsed, "n", n) 24 | } 25 | if cache.log != nil && n == 0 { 26 | cache.log.Debug("[cove] vacuumed", "ns", ns.namespace, "time", elapsed, "n", n) 27 | } 28 | return n 29 | 30 | } 31 | 32 | tic := time.NewTicker(interval) 33 | for { 34 | select { 35 | case <-tic.C: 36 | case <-cache.closed: 37 | cache.log.Info("[cove] vacuum stopping") 38 | return 39 | } 40 | for _, namespace := range cache.namespaces { 41 | start := time.Now() 42 | for i := 0; i < 100; i++ { 43 | 44 | n := vacuumNamespace(namespace) 45 | 46 | if n == 0 { 47 | break 48 | } 49 | 50 | elapsed := time.Since(start) 51 | if elapsed > time.Second { 52 | break 53 | } 54 | 55 | select { 56 | case <-cache.closed: 57 | cache.log.Info("[cove] vacuum stopping") 58 | return 59 | default: 60 | } 61 | 62 | } 63 | } 64 | } 65 | } 66 | } 67 | 68 | func URITemp() string { 69 | d := fmt.Sprintf("%d-cove", time.Now().Unix()) 70 | uri := filepath.Join(os.TempDir(), d, "cove.db") 71 | os.MkdirAll(filepath.Dir(uri), 0755) 72 | return fmt.Sprintf("file:%s?tmp=true", uri) 73 | } 74 | 75 | func URIFromPath(path string) string { 76 | _ = os.MkdirAll(filepath.Dir(path), 0755) 77 | return fmt.Sprintf("file:%s", path) 78 | } 79 | 80 | func Hit(err error) (bool, error) { 81 | if err == nil { 82 | return true, nil 83 | } 84 | if errors.Is(err, NotFound) { 85 | return false, nil 86 | } 87 | return false, err 88 | } 89 | 90 | func Miss(err error) (bool, error) { 91 | hit, err := Hit(err) 92 | return !hit, err 93 | } 94 | -------------------------------------------------------------------------------- /examples/get-or-set-if-missing.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/modfin/cove" 6 | "github.com/modfin/cove/examples/helper" 7 | "sync" 8 | "time" 9 | ) 10 | 11 | func main() { 12 | 13 | // creates a sqlite cache in a temporary directory, 14 | // once the cache is closed the database is removed 15 | // a default TTL of 10 minutes is set 16 | cache, err := cove.New( 17 | cove.URITemp(), 18 | cove.DBRemoveOnClose(), 19 | cove.WithTTL(time.Minute*10), 20 | ) 21 | helper.AssertNoErr(err) 22 | defer cache.Close() 23 | 24 | fetch := func(key string) ([]byte, error) { 25 | fmt.Println("fetching value for key:", key) 26 | 27 | nano := time.Now().UnixNano() 28 | time.Sleep(500 * time.Millisecond) 29 | val := fmt.Sprintf("async-value-for-%s, at: %d", key, nano) 30 | return []byte(val), nil 31 | } 32 | 33 | // GetOr will return the value if it exists, otherwise it will call the fetch function 34 | // if multiple goroutines call GetOr with the same key, only one will call the fetch function 35 | // the others will wait for the first to finish and retrieve the cached value from the first call. 36 | // It is useful to avoid thundering herd problem. 37 | // This is done by locking on the provided key in the application layer, not the database layer. 38 | // meaning, this might work poorly if multiple applications are using the same sqlite cache files. 39 | wg := sync.WaitGroup{} 40 | for i := 0; i < 10; i++ { 41 | wg.Add(1) 42 | go func() { 43 | val, err := cache.GetOr("MyKey", fetch) 44 | helper.AssertNoErr(err) 45 | fmt.Println(string(val)) 46 | wg.Done() 47 | }() 48 | } 49 | 50 | wg.Wait() 51 | // fetching value for key: MyKey 52 | // async-value-for-MyKey, at: 1729775954803141713 53 | // async-value-for-MyKey, at: 1729775954803141713 54 | // async-value-for-MyKey, at: 1729775954803141713 55 | // async-value-for-MyKey, at: 1729775954803141713 56 | // .... 57 | // .... 58 | // .... 59 | 60 | // Separate namespace can of course lock on the same key without interfering with each other 61 | cache2, err := cache.NS("cache2") 62 | helper.AssertNoErr(err) 63 | 64 | wg = sync.WaitGroup{} 65 | for i := 0; i < 10; i++ { 66 | wg.Add(2) 67 | go func() { 68 | val, err := cache.GetOr("MyOtherKey", fetch) 69 | helper.AssertNoErr(err) 70 | fmt.Println("[cache1]", string(val)) 71 | wg.Done() 72 | }() 73 | 74 | go func() { 75 | val, err := cache2.GetOr("MyOtherKey", fetch) 76 | helper.AssertNoErr(err) 77 | fmt.Println("[cache2]", string(val)) 78 | wg.Done() 79 | }() 80 | } 81 | wg.Wait() 82 | //fetching value for key: MyOtherKey // one fetch from cache1 83 | //fetching value for key: MyOtherKey // and one from cache2 84 | //[cache1] async-value-for-MyOtherKey, at: 1729776686806490668 85 | //[cache2] async-value-for-MyOtherKey, at: 1729776686806426035 86 | //[cache2] async-value-for-MyOtherKey, at: 1729776686806426035 87 | //[cache1] async-value-for-MyOtherKey, at: 1729776686806490668 88 | //[cache1] async-value-for-MyOtherKey, at: 1729776686806490668 89 | //[cache2] async-value-for-MyOtherKey, at: 1729776686806426035 90 | //[cache1] async-value-for-MyOtherKey, at: 1729776686806490668 91 | // .... 92 | // .... 93 | // .... 94 | } 95 | -------------------------------------------------------------------------------- /x_benchmark_test.go: -------------------------------------------------------------------------------- 1 | package cove_test 2 | 3 | import ( 4 | _ "github.com/mattn/go-sqlite3" 5 | "github.com/modfin/cove" 6 | "github.com/stretchr/testify/assert" 7 | "math/rand/v2" 8 | "strconv" 9 | "sync/atomic" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | func BenchmarkSetParallel(b *testing.B) { 15 | 16 | do := func(op ...cove.Op) func(*testing.B) { 17 | return func(b *testing.B) { 18 | cache, err := cove.New(cove.URITemp(), append([]cove.Op{cove.DBRemoveOnClose()}, op...)...) 19 | assert.NoError(b, err) 20 | defer cache.Close() 21 | 22 | value := []byte("1") 23 | 24 | var i int64 25 | b.ResetTimer() 26 | start := time.Now() 27 | b.RunParallel(func(pb *testing.PB) { 28 | for pb.Next() { 29 | ii := atomic.AddInt64(&i, 1) 30 | k := strconv.Itoa(int(ii)) 31 | err = cache.Set(k, value) 32 | assert.NoError(b, err) 33 | } 34 | }) 35 | elapsed := time.Since(start) 36 | b.ReportMetric(float64(i)/elapsed.Seconds(), "insert/sec") 37 | } 38 | } 39 | 40 | b.Run("default", do()) 41 | b.Run("sync-off", do(cove.DBSyncOff())) 42 | b.Run("sync-off+autocheckpoint", do( 43 | cove.DBSyncOff(), 44 | cove.DBPragma("mmap_size = 100000000"), 45 | cove.DBPragma("wal_autocheckpoint = 10000"), 46 | cove.DBPragma("optimize"), 47 | )) 48 | 49 | } 50 | 51 | func BenchmarkGetParallel(b *testing.B) { 52 | 53 | do := func(op ...cove.Op) func(*testing.B) { 54 | return func(b *testing.B) { 55 | 56 | cache, err := cove.New(cove.URITemp(), append([]cove.Op{cove.DBRemoveOnClose()}, op...)...) 57 | assert.NoError(b, err) 58 | defer cache.Close() 59 | 60 | for i := 0; i < b.N; i++ { 61 | key := strconv.Itoa(i) 62 | value := []byte(key) 63 | err = cache.Set(key, value) 64 | assert.NoError(b, err) 65 | } 66 | 67 | var i int64 68 | b.ResetTimer() 69 | start := time.Now() 70 | b.RunParallel(func(pb *testing.PB) { 71 | for pb.Next() { 72 | _ = atomic.AddInt64(&i, 1) 73 | k := strconv.Itoa(int(rand.Float64() * float64(b.N))) 74 | _, err = cache.Get(k) 75 | assert.NoError(b, err) 76 | } 77 | }) 78 | elapsed := time.Since(start) 79 | b.ReportMetric(float64(i)/elapsed.Seconds(), "reads/sec") 80 | } 81 | } 82 | 83 | b.Run("default", do()) 84 | b.Run("sync-off", do(cove.DBSyncOff())) 85 | b.Run("sync-off+autocheckpoint", do( 86 | cove.DBSyncOff(), 87 | cove.DBPragma("mmap_size = 100000000"), 88 | cove.DBPragma("wal_autocheckpoint = 10000"), 89 | cove.DBPragma("optimize"), 90 | )) 91 | 92 | } 93 | 94 | func BenchmarkSetMemParallel(b *testing.B) { 95 | 96 | do := func(valSize int, op ...cove.Op) func(*testing.B) { 97 | return func(b *testing.B) { 98 | cache, err := cove.New(cove.URITemp(), append([]cove.Op{cove.DBRemoveOnClose()}, op...)...) 99 | assert.NoError(b, err) 100 | defer cache.Close() 101 | 102 | var value = make([]byte, valSize) 103 | for i := 0; i < len(value); i++ { 104 | value[i] = byte(rand.Int()) 105 | } 106 | 107 | var i int64 108 | b.ResetTimer() 109 | start := time.Now() 110 | b.RunParallel(func(pb *testing.PB) { 111 | for pb.Next() { 112 | ii := atomic.AddInt64(&i, 1) 113 | k := strconv.Itoa(int(ii)) 114 | err = cache.Set(k, value) 115 | assert.NoError(b, err) 116 | } 117 | }) 118 | elapsed := time.Since(start) 119 | b.ReportMetric(float64(int(i)*len(value)/1_000_000)/elapsed.Seconds(), "write-mb/sec") 120 | } 121 | } 122 | 123 | b.Run("default+0.1mb", do(100_000)) 124 | b.Run("default+1mb", do(1_000_000)) 125 | //b.Run("default+10mb", do(10_000_000)) 126 | b.Run("sync-off+0.1mb", do(100_000, cove.DBSyncOff())) 127 | b.Run("sync-off+1mb", do(1_000_000, cove.DBSyncOff())) 128 | //b.Run("sync-off+10mb", do(10_000_000, cove.DBSyncOff())) 129 | 130 | } 131 | 132 | func BenchmarkSetMem(b *testing.B) { 133 | 134 | do := func(valSize int, op ...cove.Op) func(*testing.B) { 135 | return func(b *testing.B) { 136 | cache, err := cove.New(cove.URITemp(), append([]cove.Op{cove.DBRemoveOnClose()}, op...)...) 137 | assert.NoError(b, err) 138 | defer cache.Close() 139 | 140 | var value = make([]byte, valSize) 141 | for i := 0; i < len(value); i++ { 142 | value[i] = byte(rand.Int()) 143 | } 144 | 145 | b.ResetTimer() 146 | start := time.Now() 147 | 148 | for i := 0; i < b.N; i++ { 149 | k := strconv.Itoa(int(i)) 150 | err = cache.Set(k, value) 151 | assert.NoError(b, err) 152 | } 153 | 154 | elapsed := time.Since(start) 155 | b.ReportMetric(float64(b.N*len(value)/1_000_000)/elapsed.Seconds(), "write-mb/sec") 156 | } 157 | } 158 | 159 | b.Run("default+0.1mb", do(100_000)) 160 | b.Run("default+1mb", do(1_000_000)) 161 | //b.Run("default+10mb", do(10_000_000)) 162 | b.Run("sync-off+0.1mb", do(100_000, cove.DBSyncOff())) 163 | b.Run("sync-off+1mb", do(1_000_000, cove.DBSyncOff())) 164 | //b.Run("sync-off+10mb", do(10_000_000, cove.DBSyncOff())) 165 | 166 | } 167 | 168 | func BenchmarkGetMemParallel(b *testing.B) { 169 | 170 | do := func(valSize int, op ...cove.Op) func(*testing.B) { 171 | return func(b *testing.B) { 172 | cache, err := cove.New(cove.URITemp(), append([]cove.Op{cove.DBRemoveOnClose()}, op...)...) 173 | assert.NoError(b, err) 174 | defer cache.Close() 175 | 176 | var value = make([]byte, valSize) 177 | for i := 0; i < len(value); i++ { 178 | value[i] = byte(rand.Int()) 179 | } 180 | 181 | for i := 0; i < 30; i++ { 182 | key := strconv.Itoa(i) 183 | err = cache.Set(key, value) 184 | assert.NoError(b, err) 185 | } 186 | 187 | var i int64 188 | b.ResetTimer() 189 | start := time.Now() 190 | b.RunParallel(func(pb *testing.PB) { 191 | for pb.Next() { 192 | ii := atomic.AddInt64(&i, 1) % 30 193 | k := strconv.Itoa(int(ii)) 194 | _, err = cache.Get(k) 195 | assert.NoError(b, err, "key %s, N %d", k, b.N) 196 | } 197 | }) 198 | elapsed := time.Since(start) 199 | b.ReportMetric(float64(int(i)*len(value)/1_000_000)/elapsed.Seconds(), "read-mb/sec") 200 | } 201 | } 202 | 203 | b.Run("default+0.1mb", do(100_000)) 204 | b.Run("default+1mb", do(1_000_000)) 205 | //b.Run("default+10mb", do(10_000_000)) 206 | b.Run("sync-off+0.1mb", do(100_000, cove.DBSyncOff())) 207 | b.Run("sync-off+1mb", do(1_000_000, cove.DBSyncOff())) 208 | //b.Run("sync-off+10mb", do(10_000_000, cove.DBSyncOff())) 209 | 210 | } 211 | 212 | func BenchmarkGetMem(b *testing.B) { 213 | 214 | do := func(valSize int, op ...cove.Op) func(*testing.B) { 215 | return func(b *testing.B) { 216 | cache, err := cove.New(cove.URITemp(), append([]cove.Op{cove.DBRemoveOnClose()}, op...)...) 217 | assert.NoError(b, err) 218 | defer cache.Close() 219 | 220 | var value = make([]byte, valSize) 221 | for i := 0; i < len(value); i++ { 222 | value[i] = byte(rand.Int()) 223 | } 224 | 225 | for i := 0; i < 30; i++ { 226 | key := strconv.Itoa(i) 227 | err = cache.Set(key, value) 228 | assert.NoError(b, err) 229 | } 230 | 231 | b.ResetTimer() 232 | start := time.Now() 233 | 234 | for i := 0; i < b.N; i++ { 235 | ii := i % 30 236 | 237 | k := strconv.Itoa(int(ii)) 238 | _, err = cache.Get(k) 239 | assert.NoError(b, err, "key %s, N %d", k, b.N) 240 | } 241 | 242 | elapsed := time.Since(start) 243 | b.ReportMetric(float64(b.N*len(value)/1_000_000)/elapsed.Seconds(), "read-mb/sec") 244 | } 245 | } 246 | 247 | b.Run("default+0.1mb", do(100_000)) 248 | b.Run("default+1mb", do(1_000_000)) 249 | //b.Run("default+10mb", do(10_000_000)) 250 | b.Run("sync-off+0.1mb", do(100_000, cove.DBSyncOff())) 251 | b.Run("sync-off+1mb", do(1_000_000, cove.DBSyncOff())) 252 | //b.Run("sync-off+10mb", do(10_000_000, cove.DBSyncOff())) 253 | 254 | } 255 | -------------------------------------------------------------------------------- /facad.go: -------------------------------------------------------------------------------- 1 | package cove 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | "errors" 7 | "iter" 8 | "time" 9 | ) 10 | 11 | // Of creates a typed cache facade using generics. 12 | // encoding/gob is used for serialization and deserialization 13 | // 14 | // Example: 15 | // 16 | // cache, err := cove.New(cove.URITemp(), cove.DBRemoveOnClose()) 17 | // assert.NoError(err) 18 | // 19 | // // creates a namespace that is separate from the main cache, 20 | // // this helps to avoid key collisions and allows for easier management of keys 21 | // // when using multiple types 22 | // separateNamespace, err := cache.NS("my-strings") 23 | // assert.NoError(err) 24 | // 25 | // stringCache := cove.Of[string](stringNamespace) 26 | // stringCache.Set("hello", "typed world") 27 | // fmt.Println(stringCache.Get("hello")) 28 | // // Output: typed world 29 | func Of[V any](cache *Cache) *TypedCache[V] { 30 | return &TypedCache[V]{ 31 | cache: cache, 32 | } 33 | } 34 | 35 | type TypedCache[V any] struct { 36 | cache *Cache 37 | } 38 | 39 | func (t *TypedCache[V]) decode(b []byte) (V, error) { 40 | var value V 41 | bb := bytes.NewBuffer(b) 42 | dec := gob.NewDecoder(bb) 43 | err := dec.Decode(&value) 44 | return value, err 45 | } 46 | func (t *TypedCache[V]) encode(v V) ([]byte, error) { 47 | var bb bytes.Buffer 48 | enc := gob.NewEncoder(&bb) 49 | err := enc.Encode(v) 50 | return bb.Bytes(), err 51 | } 52 | 53 | // Range returns all key value pairs in the range [from, to] 54 | func (t *TypedCache[V]) Range(from string, to string) ([]KV[V], error) { 55 | kv, err := t.cache.Range(from, to) 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | var kvs []KV[V] 61 | for _, v := range kv { 62 | value, err := t.decode(v.V) 63 | if err != nil { 64 | return nil, err 65 | } 66 | kvs = append(kvs, KV[V]{K: v.K, V: value}) 67 | } 68 | 69 | return kvs, err 70 | } 71 | 72 | // Keys returns all keys in the range [from, to] 73 | func (t *TypedCache[V]) Keys(from string, to string) (keys []string, err error) { 74 | return t.cache.Keys(from, to) 75 | } 76 | 77 | // Values returns all values in the range [from, to] 78 | func (t *TypedCache[V]) Values(from string, to string) (values []V, err error) { 79 | vals, err := t.cache.Values(from, to) 80 | if err != nil { 81 | return nil, err 82 | } 83 | for _, v := range vals { 84 | value, err := t.decode(v) 85 | if err != nil { 86 | return nil, err 87 | } 88 | values = append(values, value) 89 | } 90 | return values, nil 91 | } 92 | 93 | // ItrRange returns a k/v iterator for the range of keys [from, to] 94 | // 95 | // WARNING 96 | // Since iterators don't really have any way of communication errors 97 | // the Con is that errors are dropped when using iterators. 98 | // the Pro is that it is very easy to use, and scan row by row (ie. no need to load all rows into memory) 99 | func (t *TypedCache[V]) ItrRange(from string, to string) iter.Seq2[string, V] { 100 | return func(yield func(string, V) bool) { 101 | t.cache.ItrRange(from, to)(func(k string, v []byte) bool { 102 | value, err := t.decode(v) 103 | if err != nil { 104 | return false 105 | } 106 | return yield(k, value) 107 | }) 108 | } 109 | } 110 | 111 | // ItrKeys returns a key iterator for the range of keys [from, to] 112 | // 113 | // WARNING 114 | // Since iterators don't really have any way of communication errors 115 | // the Con is that errors are dropped when using iterators. 116 | // the Pro is that it is very easy to use, and scan row by row (ie. no need to load all rows into memory) 117 | func (t *TypedCache[V]) ItrKeys(from string, to string) iter.Seq[string] { 118 | return t.cache.ItrKeys(from, to) 119 | } 120 | 121 | // ItrValues returns a value iterator for the range of keys [from, to] 122 | // 123 | // WARNING 124 | // Since iterators don't really have any way of communication errors 125 | // the Con is that errors are dropped when using iterators. 126 | // the Pro is that it is very easy to use, and scan row by row (ie. no need to load all rows into memory) 127 | func (t *TypedCache[V]) ItrValues(from string, to string) iter.Seq[V] { 128 | return func(yield func(V) bool) { 129 | t.cache.ItrValues(from, to)(func(v []byte) bool { 130 | value, err := t.decode(v) 131 | if err != nil { 132 | return false 133 | } 134 | return yield(value) 135 | }) 136 | } 137 | } 138 | 139 | // Get returns the value for the key 140 | func (t *TypedCache[V]) Get(key string) (V, error) { 141 | var z V 142 | b, err := t.cache.Get(key) 143 | if err != nil { 144 | return z, err 145 | } 146 | value, err := t.decode(b) 147 | if err != nil { 148 | return z, err 149 | } 150 | return value, err 151 | } 152 | 153 | // Set sets the value for the key with the default TTL 154 | func (t *TypedCache[V]) Set(key string, value V) error { 155 | return t.SetTTL(key, value, t.cache.ttl) 156 | } 157 | 158 | // SetTTL sets the value for the key with the specified TTL 159 | func (t *TypedCache[V]) SetTTL(key string, value V, ttl time.Duration) error { 160 | b, err := t.encode(value) 161 | if err != nil { 162 | return err 163 | } 164 | return t.cache.SetTTL(key, b, ttl) 165 | } 166 | 167 | // GetOr returns the value for the key, if the key does not exist it will call the getter function to get the value 168 | func (t *TypedCache[V]) GetOr(key string, getter func(key string) (V, error)) (V, error) { 169 | b, err := t.cache.GetOr(key, func(key string) ([]byte, error) { 170 | value, err := getter(key) 171 | if err != nil { 172 | return nil, err 173 | } 174 | return t.encode(value) 175 | }) 176 | var value V 177 | if err != nil { 178 | return value, err 179 | } 180 | return t.decode(b) 181 | } 182 | 183 | // BatchSet sets a batch of key/value pairs in the cache 184 | // 185 | // the BatchSet will take place in one transaction, but split up into sub-batches of MAX_PARAMS/3 size, ie 999/3 = 333, 186 | // in order to have the BatchSet be atomic. If one key fails to set, the whole batch will fail. 187 | // Prefer batches less then MAX_PARAMS 188 | func (t *TypedCache[V]) BatchSet(ziped []KV[V]) error { 189 | var rows []KV[[]byte] 190 | for _, z := range ziped { 191 | b, err := t.encode(z.V) 192 | if err != nil { 193 | return err 194 | } 195 | rows = append(rows, KV[[]byte]{K: z.K, V: b}) 196 | } 197 | return t.cache.BatchSet(rows) 198 | } 199 | 200 | // BatchGet retrieves a batch of keys from the cache 201 | // 202 | // the BatchGet will take place in one transaction, but split up into sub-batches of MAX_PARAMS size, ie 999, 203 | // in order to have the BatchGet be atomic. If one key fails to fetched, the whole batch will fail. 204 | // Prefer batches less then MAX_PARAMS 205 | func (t *TypedCache[V]) BatchGet(keys []string) ([]KV[V], error) { 206 | kvs, err := t.cache.BatchGet(keys) 207 | if err != nil { 208 | return nil, err 209 | } 210 | var zipped []KV[V] 211 | for _, v := range kvs { 212 | value, err := t.decode(v.V) 213 | if err != nil { 214 | return nil, err 215 | } 216 | zipped = append(zipped, KV[V]{K: v.K, V: value}) 217 | } 218 | return zipped, nil 219 | } 220 | 221 | // BatchEvict evicts a batch of keys from the cache 222 | // 223 | // if onEvict is set, it will be called for each key 224 | // the eviction will take place in one transaction, but split up into bacthes of MAX_PARAMS, ie 999, 225 | // in order to have the eviction be atomic. If one key fails to evict, the whole batch will fail. 226 | // Prefer batches less then MAX_PARAMS 227 | func (t *TypedCache[V]) BatchEvict(keys []string) ([]KV[V], error) { 228 | kvs, err := t.cache.BatchEvict(keys) 229 | if err != nil { 230 | return nil, err 231 | } 232 | var zipped []KV[V] 233 | for _, v := range kvs { 234 | value, err := t.decode(v.V) 235 | if err != nil { 236 | return nil, err 237 | } 238 | zipped = append(zipped, KV[V]{K: v.K, V: value}) 239 | } 240 | return zipped, nil 241 | } 242 | 243 | // EvictAll evicts all keys in the cache 244 | // 245 | // onEvict will not be called 246 | func (t *TypedCache[V]) EvictAll() (int, error) { 247 | return t.cache.EvictAll() 248 | } 249 | 250 | // Evict evicts a key from the cache 251 | // 252 | // if onEvict is set, it will be called for key 253 | func (t *TypedCache[V]) Evict(key string) (KV[V], error) { 254 | var res KV[V] 255 | kv, err := t.cache.Evict(key) 256 | 257 | if errors.Is(err, NotFound) { 258 | return res, err 259 | } 260 | if err != nil && !errors.Is(err, NotFound) { 261 | return res, err 262 | } 263 | 264 | bb := bytes.NewBuffer(kv.V) 265 | dec := gob.NewDecoder(bb) 266 | err = dec.Decode(&res.V) 267 | return res, err 268 | } 269 | 270 | // Raw returns the underlying untyped cache 271 | func (t *TypedCache[V]) Raw() *Cache { 272 | return t.cache 273 | } 274 | -------------------------------------------------------------------------------- /facad_test.go: -------------------------------------------------------------------------------- 1 | package cove_test 2 | 3 | import ( 4 | "fmt" 5 | "github.com/modfin/cove" 6 | "sort" 7 | "strconv" 8 | "testing" 9 | "time" 10 | 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TypedCache(t *testing.T) *cove.TypedCache[string] { 15 | cache, err := cove.New(cove.URITemp(), cove.DBRemoveOnClose()) 16 | assert.NoError(t, err) 17 | assert.NotNil(t, cache) 18 | return cove.Of[string](cache) 19 | } 20 | 21 | type Complex struct { 22 | Name string 23 | Ref *Complex 24 | Slice []int 25 | private string 26 | NilErr error 27 | } 28 | 29 | func TypedComplexCache(t *testing.T) *cove.TypedCache[Complex] { 30 | cache, err := cove.New(cove.URITemp(), cove.DBRemoveOnClose()) 31 | assert.NoError(t, err) 32 | assert.NotNil(t, cache) 33 | return cove.Of[Complex](cache) 34 | } 35 | 36 | func TestGetTypedComplexCache(t *testing.T) { 37 | cache := TypedComplexCache(t) 38 | defer cache.Raw().Close() 39 | 40 | a := Complex{ 41 | Name: "name", 42 | NilErr: nil, 43 | Slice: []int{1, 2, 3}, 44 | Ref: &Complex{ 45 | Name: "namespaces", 46 | }, 47 | private: "private", 48 | } 49 | err := cache.Set("a", a) 50 | assert.NoError(t, err) 51 | 52 | aa, err := cache.Get("a") 53 | assert.NoError(t, err) 54 | assert.NotEqual(t, a, aa) 55 | a.private = "" 56 | assert.Equal(t, a, aa) 57 | 58 | } 59 | 60 | func TestGetTypedCacheValue(t *testing.T) { 61 | typedCache := TypedCache(t) 62 | defer typedCache.Raw().Close() 63 | 64 | key := "test-key" 65 | value := "test-value" 66 | 67 | err := typedCache.Set(key, value) 68 | assert.NoError(t, err) 69 | 70 | retrievedValue, err := typedCache.Get(key) 71 | assert.NoError(t, err) 72 | assert.Equal(t, value, retrievedValue) 73 | } 74 | 75 | func TestSetTypedCacheValue(t *testing.T) { 76 | typedCache := TypedCache(t) 77 | defer typedCache.Raw().Close() 78 | 79 | key := "test-key" 80 | value := "test-value" 81 | 82 | err := typedCache.Set(key, value) 83 | assert.NoError(t, err) 84 | 85 | retrievedValue, err := typedCache.Get(key) 86 | assert.NoError(t, err) 87 | assert.Equal(t, value, retrievedValue) 88 | } 89 | 90 | func TestSetTypedCacheValueWithTTL(t *testing.T) { 91 | typedCache := TypedCache(t) 92 | defer typedCache.Raw().Close() 93 | 94 | key := "test-key" 95 | value := "test-value" 96 | ttl := 1 * time.Second 97 | 98 | err := typedCache.SetTTL(key, value, ttl) 99 | assert.NoError(t, err) 100 | 101 | time.Sleep(2 * time.Second) 102 | 103 | _, err = typedCache.Get(key) 104 | assert.Error(t, err) 105 | assert.Equal(t, cove.NotFound, err) 106 | } 107 | 108 | func TestGetOrSetTypedCacheValue(t *testing.T) { 109 | typedCache := TypedCache(t) 110 | defer typedCache.Raw().Close() 111 | 112 | key := "test-key" 113 | value := "test-value" 114 | 115 | retrievedValue, err := typedCache.GetOr(key, func(key string) (string, error) { 116 | return value, nil 117 | }) 118 | assert.NoError(t, err) 119 | assert.Equal(t, value, retrievedValue) 120 | } 121 | 122 | func TestEvictTypedCacheValue(t *testing.T) { 123 | typedCache := TypedCache(t) 124 | defer typedCache.Raw().Close() 125 | 126 | key := "test-key" 127 | value := "test-value" 128 | 129 | err := typedCache.Set(key, value) 130 | assert.NoError(t, err) 131 | 132 | evicted, err := typedCache.Evict(key) 133 | assert.NoError(t, err) 134 | assert.Equal(t, value, evicted.Value()) 135 | 136 | _, err = typedCache.Get(key) 137 | assert.Error(t, err) 138 | assert.Equal(t, cove.NotFound, err) 139 | } 140 | 141 | func TestEvictAllTypedCacheValues(t *testing.T) { 142 | typedCache := TypedCache(t) 143 | defer typedCache.Raw().Close() 144 | 145 | key1 := "test-key1" 146 | value1 := "test-value1" 147 | key2 := "test-key2" 148 | value2 := "test-value2" 149 | 150 | err := typedCache.Set(key1, value1) 151 | assert.NoError(t, err) 152 | err = typedCache.Set(key2, value2) 153 | assert.NoError(t, err) 154 | 155 | count, err := typedCache.EvictAll() 156 | assert.NoError(t, err) 157 | assert.Equal(t, 2, count) 158 | 159 | _, err = typedCache.Get(key1) 160 | assert.Error(t, err) 161 | assert.Equal(t, cove.NotFound, err) 162 | 163 | _, err = typedCache.Get(key2) 164 | assert.Error(t, err) 165 | assert.Equal(t, cove.NotFound, err) 166 | } 167 | 168 | func TestBatchSetTypedCacheValues(t *testing.T) { 169 | typedCache := TypedCache(t) 170 | defer typedCache.Raw().Close() 171 | 172 | ziped := []cove.KV[string]{ 173 | {K: "key1", V: "value1"}, 174 | {K: "key2", V: "value2"}, 175 | } 176 | 177 | err := typedCache.BatchSet(ziped) 178 | assert.NoError(t, err) 179 | 180 | for _, z := range ziped { 181 | retrievedValue, err := typedCache.Get(z.K) 182 | assert.NoError(t, err) 183 | assert.Equal(t, z.V, retrievedValue) 184 | } 185 | } 186 | 187 | func TestBatchGetTypedCacheValues(t *testing.T) { 188 | typedCache := TypedCache(t) 189 | defer typedCache.Raw().Close() 190 | 191 | ziped := []cove.KV[string]{ 192 | {K: "key1", V: "value1"}, 193 | {K: "key2", V: "value2"}, 194 | } 195 | 196 | err := typedCache.BatchSet(ziped) 197 | assert.NoError(t, err) 198 | 199 | keys := []string{"key1", "key2"} 200 | zipped, err := typedCache.BatchGet(keys) 201 | assert.NoError(t, err) 202 | 203 | sort.Slice(zipped, func(i, j int) bool { 204 | return zipped[i].K < zipped[j].K 205 | }) 206 | 207 | assert.Equal(t, ziped, zipped) 208 | } 209 | 210 | func TestCacheBatchEvictTypedSizes(t *testing.T) { 211 | 212 | do := func(itre int) func(t *testing.T) { 213 | return func(t *testing.T) { 214 | cc, err := cove.New(cove.URITemp(), cove.DBRemoveOnClose()) 215 | assert.NoError(t, err) 216 | defer cc.Close() 217 | 218 | typed := cove.Of[string](cc) 219 | 220 | var keys []string 221 | var rows []cove.KV[string] 222 | for i := 0; i < itre; i++ { 223 | k := strconv.Itoa(i) 224 | v := fmt.Sprintf("value_%d", i) 225 | rows = append(rows, cove.KV[string]{K: k, V: v}) 226 | keys = append(keys, k) 227 | err = typed.Set(k, v) 228 | assert.NoError(t, err) 229 | } 230 | 231 | res, err := typed.BatchEvict(keys) 232 | assert.NoError(t, err) 233 | 234 | sort.Slice(res, func(i, j int) bool { 235 | return res[i].K < res[j].K 236 | }) 237 | sort.Slice(rows, func(i, j int) bool { 238 | return rows[i].K < rows[j].K 239 | }) 240 | 241 | assert.Equal(t, len(rows), len(res)) 242 | assert.Equal(t, len(rows), len(keys)) 243 | 244 | for i, row := range rows { 245 | assert.Equal(t, row.K, res[i].K) 246 | assert.Equal(t, row.V, res[i].V) 247 | 248 | _, err = typed.Get(row.K) 249 | assert.Error(t, err) 250 | assert.Equal(t, cove.NotFound, err) 251 | } 252 | 253 | } 254 | } 255 | 256 | t.Run("10", do(10)) 257 | t.Run("11", do(11)) 258 | t.Run("100", do(100)) 259 | t.Run("101", do(101)) 260 | 261 | t.Run(fmt.Sprintf("%d", cove.MAX_PARAMS-1), do(cove.MAX_PARAMS-1)) 262 | t.Run(fmt.Sprintf("%d", cove.MAX_PARAMS), do(cove.MAX_PARAMS)) 263 | t.Run(fmt.Sprintf("%d", cove.MAX_PARAMS+1), do(cove.MAX_PARAMS+1)) 264 | 265 | } 266 | 267 | func TestTypedCacheItrRange(t *testing.T) { 268 | typedCache := TypedCache(t) 269 | defer typedCache.Raw().Close() 270 | 271 | ziped := []cove.KV[string]{ 272 | {K: "key1", V: "value1"}, 273 | {K: "key2", V: "value2"}, 274 | {K: "key3", V: "value3"}, 275 | } 276 | 277 | err := typedCache.BatchSet(ziped) 278 | assert.NoError(t, err) 279 | 280 | var result []cove.KV[string] 281 | typedCache.ItrRange("key1", "key3")(func(k string, v string) bool { 282 | result = append(result, cove.KV[string]{K: k, V: v}) 283 | return true 284 | }) 285 | 286 | sort.Slice(result, func(i, j int) bool { 287 | return result[i].K < result[j].K 288 | }) 289 | 290 | assert.Equal(t, ziped, result) 291 | } 292 | 293 | func TestTypedCacheItrKeys(t *testing.T) { 294 | typedCache := TypedCache(t) 295 | defer typedCache.Raw().Close() 296 | 297 | ziped := []cove.KV[string]{ 298 | {K: "key1", V: "value1"}, 299 | {K: "key2", V: "value2"}, 300 | {K: "key3", V: "value3"}, 301 | } 302 | 303 | err := typedCache.BatchSet(ziped) 304 | assert.NoError(t, err) 305 | 306 | var keys []string 307 | typedCache.ItrKeys("key1", "key3")(func(k string) bool { 308 | keys = append(keys, k) 309 | return true 310 | }) 311 | 312 | sort.Strings(keys) 313 | expectedKeys := []string{"key1", "key2", "key3"} 314 | assert.Equal(t, expectedKeys, keys) 315 | } 316 | 317 | func TestTypedCacheItrValues(t *testing.T) { 318 | typedCache := TypedCache(t) 319 | defer typedCache.Raw().Close() 320 | 321 | ziped := []cove.KV[string]{ 322 | {K: "key1", V: "value1"}, 323 | {K: "key2", V: "value2"}, 324 | {K: "key3", V: "value3"}, 325 | } 326 | 327 | err := typedCache.BatchSet(ziped) 328 | assert.NoError(t, err) 329 | 330 | var values []string 331 | typedCache.ItrValues("key1", "key3")(func(v string) bool { 332 | values = append(values, v) 333 | return true 334 | }) 335 | 336 | sort.Strings(values) 337 | expectedValues := []string{"value1", "value2", "value3"} 338 | assert.Equal(t, expectedValues, values) 339 | } 340 | 341 | func TestTypedCacheValues(t *testing.T) { 342 | typedCache := TypedCache(t) 343 | defer typedCache.Raw().Close() 344 | 345 | ziped := []cove.KV[string]{ 346 | {K: "key1", V: "value1"}, 347 | {K: "key2", V: "value2"}, 348 | {K: "key3", V: "value3"}, 349 | } 350 | 351 | err := typedCache.BatchSet(ziped) 352 | assert.NoError(t, err) 353 | 354 | values, err := typedCache.Values("key1", cove.RANGE_MAX) 355 | assert.NoError(t, err) 356 | sort.Strings(values) 357 | expectedValues := []string{"value1", "value2", "value3"} 358 | assert.Equal(t, expectedValues, values) 359 | } 360 | 361 | func TestTypedCacheKeys(t *testing.T) { 362 | typedCache := TypedCache(t) 363 | defer typedCache.Raw().Close() 364 | 365 | ziped := []cove.KV[string]{ 366 | {K: "key1", V: "value1"}, 367 | {K: "key2", V: "value2"}, 368 | {K: "key3", V: "value3"}, 369 | } 370 | 371 | err := typedCache.BatchSet(ziped) 372 | assert.NoError(t, err) 373 | 374 | values, err := typedCache.Keys("key1", cove.RANGE_MAX) 375 | assert.NoError(t, err) 376 | sort.Strings(values) 377 | expectedValues := []string{"key1", "key2", "key3"} 378 | assert.Equal(t, expectedValues, values) 379 | } 380 | -------------------------------------------------------------------------------- /db.go: -------------------------------------------------------------------------------- 1 | package cove 2 | 3 | import ( 4 | "database/sql" 5 | "errors" 6 | "fmt" 7 | "iter" 8 | "log/slog" 9 | "os" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | type query interface { 15 | Query(query string, args ...any) (*sql.Rows, error) 16 | QueryRow(query string, args ...interface{}) *sql.Row 17 | Exec(query string, args ...any) (sql.Result, error) 18 | } 19 | 20 | func exec(db query, qs ...string) error { 21 | for _, q := range qs { 22 | _, err := db.Exec(q) 23 | if err != nil { 24 | return fmt.Errorf("could not exec, %s, err, %w", q, err) 25 | } 26 | } 27 | return nil 28 | } 29 | 30 | func getRange(db query, from string, to string, tbl string) (kv []KV[[]byte], err error) { 31 | r, err := db.Query(fmt.Sprintf(`SELECT key, value FROM %s WHERE $1 <= key AND key <= $2`, tbl), from, to) 32 | if err != nil { 33 | return nil, fmt.Errorf("could not query in range, %w", err) 34 | } 35 | defer r.Close() 36 | for r.Next() { 37 | var k string 38 | var v []byte 39 | err := r.Scan(&k, &v) 40 | if err != nil { 41 | return nil, fmt.Errorf("could not scan in range, %w", err) 42 | } 43 | kv = append(kv, KV[[]byte]{K: k, V: v}) 44 | } 45 | return kv, nil 46 | } 47 | 48 | func getKeys(db query, from string, to string, tbl string) (keys []string, err error) { 49 | r, err := db.Query(fmt.Sprintf(`SELECT key FROM %s WHERE $1 <= key AND key <= $2`, tbl), from, to) 50 | if err != nil { 51 | return nil, fmt.Errorf("could not query in range, %w", err) 52 | } 53 | defer r.Close() 54 | for r.Next() { 55 | var k string 56 | err := r.Scan(&k) 57 | if err != nil { 58 | return nil, fmt.Errorf("could not scan in range, %w", err) 59 | } 60 | keys = append(keys, k) 61 | } 62 | return keys, nil 63 | } 64 | 65 | func getValues(db query, from string, to string, tbl string) (vals [][]byte, err error) { 66 | r, err := db.Query(fmt.Sprintf(`SELECT value FROM %s WHERE $1 <= key AND key <= $2`, tbl), from, to) 67 | if err != nil { 68 | return nil, fmt.Errorf("could not query in range, %w", err) 69 | } 70 | defer r.Close() 71 | for r.Next() { 72 | var v []byte 73 | err := r.Scan(&v) 74 | if err != nil { 75 | return nil, fmt.Errorf("could not scan in range, %w", err) 76 | } 77 | vals = append(vals, v) 78 | } 79 | return vals, nil 80 | } 81 | 82 | func iterKV(db query, from string, to string, tbl string, log *slog.Logger) iter.Seq2[string, []byte] { 83 | r, err := db.Query(fmt.Sprintf(`SELECT key, value FROM %s WHERE $1 <= key AND key <= $2`, tbl), from, to) 84 | if err != nil { 85 | if log != nil { 86 | log.Error("[cove] iterKV, could not query in iter", "err", err) 87 | return func(yield func(string, []byte) bool) {} 88 | } 89 | _, _ = fmt.Fprintf(os.Stderr, "[cove] iterKV, could not query in iter, %v", err) 90 | return func(yield func(string, []byte) bool) {} 91 | } 92 | 93 | return func(yield func(string, []byte) bool) { 94 | defer r.Close() 95 | for r.Next() { 96 | var k string 97 | var v []byte 98 | err := r.Scan(&k, &v) 99 | if err != nil { 100 | if log != nil { 101 | log.Error("[cove] iterKV, could not scan in iter,", "err", err) 102 | return 103 | } 104 | _, _ = fmt.Fprintf(os.Stderr, "cove: iterKV, could not scan in iter, %v", err) 105 | return 106 | } 107 | if !yield(k, v) { 108 | return 109 | } 110 | } 111 | } 112 | } 113 | 114 | func iterKeys(db query, from string, to string, tbl string, log *slog.Logger) iter.Seq[string] { 115 | r, err := db.Query(fmt.Sprintf(`SELECT key FROM %s WHERE $1 <= key AND key <= $2`, tbl), from, to) 116 | if err != nil { 117 | if log != nil { 118 | log.Error("[cove] could not query in iter,", "err", err) 119 | return func(yield func(string) bool) {} 120 | } 121 | _, _ = fmt.Fprintf(os.Stderr, "cove: could not query in iter, %v", err) 122 | return func(yield func(string) bool) {} 123 | } 124 | 125 | return func(yield func(string) bool) { 126 | defer r.Close() 127 | for r.Next() { 128 | var k string 129 | err := r.Scan(&k) 130 | if err != nil { 131 | 132 | if log != nil { 133 | log.Error("[cove] iterKeys, could not scan in iter,", "err", err) 134 | return 135 | } 136 | 137 | _, _ = fmt.Fprintf(os.Stderr, "cove: iterKeys, could not scan in iter, %v", err) 138 | return 139 | } 140 | if !yield(k) { 141 | return 142 | } 143 | } 144 | } 145 | } 146 | 147 | func iterValues(db query, from string, to string, tbl string, log *slog.Logger) iter.Seq[[]byte] { 148 | r, err := db.Query(fmt.Sprintf(`SELECT value FROM %s WHERE $1 <= key AND key <= $2`, tbl), from, to) 149 | if err != nil { 150 | if log != nil { 151 | log.Error("[cove] iterValues, could not query in iter,", "err", err) 152 | return func(yield func([]byte) bool) {} 153 | } 154 | _, _ = fmt.Fprintf(os.Stderr, "cove: iterValues, could not query in iter, %v", err) 155 | return func(yield func([]byte) bool) {} 156 | } 157 | 158 | return func(yield func([]byte) bool) { 159 | defer r.Close() 160 | for r.Next() { 161 | var v string 162 | err := r.Scan(&v) 163 | if err != nil { 164 | if log != nil { 165 | log.Error("[cove] iterValues, could not scan in iter,", "err", err) 166 | return 167 | } 168 | _, _ = fmt.Fprintf(os.Stderr, "cove: iterValues, could not scan in iter, %v", err) 169 | return 170 | } 171 | if !yield([]byte(v)) { 172 | return 173 | } 174 | } 175 | } 176 | } 177 | 178 | func evictAll(r query, tbl string) (int, error) { 179 | res, err := r.Exec(fmt.Sprintf(` 180 | DELETE FROM %s 181 | `, tbl)) 182 | if err != nil { 183 | return 0, fmt.Errorf("could not delete all in %s, err; %w", tbl, err) 184 | } 185 | i, err := res.RowsAffected() 186 | return int(i), err 187 | } 188 | func evict(r query, key string, tbl string) (KV[[]byte], error) { 189 | var value []byte 190 | err := r.QueryRow(fmt.Sprintf(` 191 | DELETE FROM %s 192 | WHERE key = $1 193 | RETURNING value 194 | `, tbl), key).Scan(&value) 195 | 196 | if errors.Is(err, sql.ErrNoRows) { 197 | return KV[[]byte]{}, NotFound 198 | } 199 | if err != nil { 200 | return KV[[]byte]{}, fmt.Errorf("could not delete key %s, err; %w", key, err) 201 | } 202 | return KV[[]byte]{K: key, V: value}, err 203 | } 204 | 205 | func get(r query, key string, tbl string) ([]byte, error) { 206 | 207 | var value []byte 208 | err := r.QueryRow(fmt.Sprintf(` 209 | SELECT value FROM %s 210 | WHERE key = $1 AND strftime('%%s', 'now') < expire_at 211 | `, tbl), key).Scan(&value) 212 | 213 | if errors.Is(err, sql.ErrNoRows) { 214 | return nil, NotFound 215 | } 216 | if err != nil { 217 | return nil, fmt.Errorf("could not get key %s, err; %w", key, err) 218 | } 219 | return value, nil 220 | } 221 | 222 | func setTTL(r query, key string, value []byte, ttl time.Duration, tbl string) error { 223 | if value == nil { 224 | value = []byte{} 225 | } 226 | 227 | _, err := r.Exec(fmt.Sprintf(` 228 | INSERT INTO %s (key, value, expire_at, ttl) 229 | VALUES ($1, $2, strftime('%%s', 'now')+$3, $3) 230 | ON CONFLICT (key) 231 | DO UPDATE 232 | SET 233 | value = excluded.value, 234 | create_at = excluded.create_at, 235 | expire_at = excluded.expire_at, 236 | ttl = excluded.ttl 237 | `, tbl), key, value, int(ttl.Seconds())) 238 | 239 | if err != nil { 240 | return fmt.Errorf("could not exec, %w", err) 241 | } 242 | return nil 243 | } 244 | 245 | func batchGet(db query, keys []string, tbl string) ([]KV[[]byte], error) { 246 | var values []string 247 | var params []any 248 | for i, key := range keys { 249 | values = append(values, fmt.Sprintf("$%d", i+1)) 250 | params = append(params, key) 251 | } 252 | 253 | q := fmt.Sprintf(` 254 | SELECT key, value FROM %s 255 | WHERE key IN (%s) 256 | AND strftime('%%s', 'now') < expire_at 257 | `, tbl, strings.Join(values, ", ")) 258 | 259 | rows, err := db.Query(q, params...) 260 | if err != nil { 261 | return nil, fmt.Errorf("could not query, %w", err) 262 | } 263 | defer rows.Close() 264 | var kvs []KV[[]byte] 265 | for rows.Next() { 266 | var k string 267 | var v []byte 268 | err := rows.Scan(&k, &v) 269 | if err != nil { 270 | return nil, fmt.Errorf("could not scan, %w", err) 271 | } 272 | kvs = append(kvs, KV[[]byte]{K: k, V: v}) 273 | } 274 | return kvs, nil 275 | } 276 | 277 | func batchSet(db query, rows []KV[[]byte], ttl time.Duration, tbl string) error { 278 | 279 | var values []string 280 | var params []any 281 | 282 | for i, kv := range rows { 283 | values = append(values, fmt.Sprintf("($%d, $%d, strftime('%%s', 'now')+$%d, $%d)", i*3+1, i*3+2, i*3+3, i*3+3)) 284 | params = append(params, kv.K, kv.V, int(ttl.Seconds())) 285 | } 286 | 287 | q := fmt.Sprintf(` 288 | INSERT INTO %s (key, value, expire_at, ttl) 289 | VALUES %s 290 | ON CONFLICT (key) 291 | DO UPDATE 292 | SET 293 | value = excluded.value, 294 | create_at = excluded.create_at, 295 | expire_at = excluded.expire_at, 296 | ttl = excluded.ttl 297 | `, tbl, strings.Join(values, ",")) 298 | 299 | _, err := db.Exec(q, params...) 300 | return err 301 | } 302 | 303 | func batchEvict(r query, keys []string, tbl string) ([]KV[[]byte], error) { 304 | var values []string 305 | var params []any 306 | for i, key := range keys { 307 | values = append(values, fmt.Sprintf("$%d", i+1)) 308 | params = append(params, key) 309 | } 310 | 311 | q := fmt.Sprintf(` 312 | DELETE FROM %s 313 | WHERE key IN (%s) 314 | RETURNING key, value 315 | `, tbl, strings.Join(values, ", ")) 316 | 317 | rows, err := r.Query(q, params...) 318 | if err != nil { 319 | return nil, fmt.Errorf("could not query, %w", err) 320 | } 321 | defer rows.Close() 322 | var kvs []KV[[]byte] 323 | for rows.Next() { 324 | var k string 325 | var v []byte 326 | err := rows.Scan(&k, &v) 327 | if err != nil { 328 | return nil, fmt.Errorf("could not scan, %w", err) 329 | } 330 | kvs = append(kvs, KV[[]byte]{K: k, V: v}) 331 | } 332 | 333 | return kvs, nil 334 | 335 | } 336 | 337 | func vacuumNoResult(r query, max int, tbl string) (int, error) { 338 | q := fmt.Sprintf(` 339 | DELETE FROM %s 340 | WHERE expire_at < strftime('%%s', 'now') 341 | AND key in (SELECT key FROM %s WHERE expire_at < strftime('%%s', 'now') LIMIT $1) 342 | `, tbl, tbl) 343 | 344 | res, err := r.Exec(q, max) 345 | if err != nil { 346 | return 0, fmt.Errorf("could not query, %w", err) 347 | } 348 | i, err := res.RowsAffected() 349 | return int(i), err 350 | } 351 | func vacuum(r query, max int, tbl string) ([]KV[[]byte], error) { 352 | 353 | q := fmt.Sprintf(` 354 | DELETE FROM %s 355 | WHERE expire_at < strftime('%%s', 'now') 356 | AND key in (SELECT key FROM %s WHERE expire_at < strftime('%%s', 'now') LIMIT $1) 357 | RETURNING key, value 358 | `, tbl, tbl) 359 | 360 | rows, err := r.Query(q, max) 361 | if err != nil { 362 | return nil, fmt.Errorf("could not query, %w", err) 363 | } 364 | defer rows.Close() 365 | var kvs []KV[[]byte] 366 | for rows.Next() { 367 | var k string 368 | var v []byte 369 | err := rows.Scan(&k, &v) 370 | if err != nil { 371 | return nil, fmt.Errorf("could not scan, %w", err) 372 | } 373 | kvs = append(kvs, KV[[]byte]{K: k, V: v}) 374 | } 375 | return kvs, nil 376 | } 377 | -------------------------------------------------------------------------------- /cache.go: -------------------------------------------------------------------------------- 1 | package cove 2 | 3 | import ( 4 | "database/sql" 5 | "errors" 6 | "fmt" 7 | _ "github.com/mattn/go-sqlite3" 8 | "iter" 9 | "log/slog" 10 | "os" 11 | "path/filepath" 12 | "strings" 13 | "sync" 14 | "time" 15 | ) 16 | 17 | type Op func(*Cache) error 18 | 19 | func dbPragma(pragma string) Op { 20 | return func(c *Cache) error { 21 | return exec(c.db, 22 | fmt.Sprintf(`pragma %s;`, pragma), 23 | ) 24 | } 25 | } 26 | 27 | // WithLogger sets the logger for the cache 28 | func WithLogger(log *slog.Logger) Op { 29 | return func(c *Cache) error { 30 | if log == nil { 31 | log = slog.New(discardLogger{}) 32 | } 33 | c.log = log 34 | return nil 35 | } 36 | } 37 | 38 | // WithVacuum sets the vacuum function to be called in a go routine 39 | func WithVacuum(vacuum func(cache *Cache)) Op { 40 | return func(cache *Cache) error { 41 | cache.vacuum = vacuum 42 | return nil 43 | } 44 | } 45 | 46 | // WithTTL sets the default TTL for the cache 47 | func WithTTL(defaultTTL time.Duration) Op { 48 | return func(c *Cache) error { 49 | if defaultTTL > time.Duration(0) { 50 | c.ttl = defaultTTL 51 | } 52 | return nil 53 | } 54 | } 55 | 56 | // WithEvictCallback sets the callback function to be called when a key is evicted 57 | func WithEvictCallback(cb func(key string, val []byte)) Op { 58 | return func(c *Cache) error { 59 | c.onEvict = cb 60 | return nil 61 | } 62 | } 63 | 64 | // DBRemoveOnClose is a helper function to remove the database files on close 65 | func DBRemoveOnClose() Op { 66 | return func(cache *Cache) error { 67 | *cache.removeOnClose = true 68 | return nil 69 | } 70 | } 71 | 72 | func dbDefault() Op { 73 | return func(c *Cache) error { 74 | return errors.Join( 75 | dbPragma("journal_mode = WAL")(c), 76 | dbPragma("synchronous = normal")(c), 77 | dbPragma(`auto_vacuum = incremental`)(c), 78 | dbPragma(`incremental_vacuum`)(c), 79 | ) 80 | } 81 | } 82 | 83 | // DBSyncOff is a helper function to set 84 | // 85 | // synchronous = off 86 | // 87 | // this is useful for write performance but effects read performance and durability 88 | func DBSyncOff() Op { 89 | return func(c *Cache) error { 90 | return dbPragma("synchronous = off")(c) 91 | } 92 | } 93 | 94 | // DBPragma is a helper function to set a pragma on the database 95 | // 96 | // see https://www.sqlite.org/pragma.html 97 | // 98 | // example: 99 | // 100 | // DBPragma("journal_size_limit = 6144000") 101 | func DBPragma(s string) Op { 102 | return func(c *Cache) error { 103 | return dbPragma(s)(c) 104 | } 105 | } 106 | 107 | type Cache struct { 108 | //shards 109 | uri string 110 | 111 | ttl time.Duration 112 | 113 | onEvict func(key string, val []byte) 114 | 115 | namespace string 116 | namespaces map[string]*Cache 117 | 118 | mu *sync.Mutex 119 | muKey *keyedMutex 120 | 121 | db *sql.DB 122 | closeOnce *sync.Once 123 | closed chan struct{} 124 | removeOnClose *bool 125 | log *slog.Logger 126 | vacuum func(cache *Cache) 127 | } 128 | 129 | func New(uri string, op ...Op) (*Cache, error) { 130 | db, err := sql.Open("sqlite3", uri) 131 | if err != nil { 132 | return nil, fmt.Errorf("could not open db, %w", err) 133 | } 134 | 135 | // Test the connection 136 | if err := db.Ping(); err != nil { 137 | return nil, fmt.Errorf("could not ping db, %w", err) 138 | } 139 | 140 | c := &Cache{ 141 | uri: uri, 142 | ttl: NO_TTL, 143 | 144 | onEvict: nil, 145 | 146 | vacuum: Vacuum(5*time.Minute, 1_000), // default vacuum 147 | 148 | namespace: NS_DEFAULT, 149 | namespaces: make(map[string]*Cache), 150 | mu: &sync.Mutex{}, 151 | muKey: keyedMu(), 152 | db: db, 153 | log: slog.New(discardLogger{}), 154 | 155 | closeOnce: &sync.Once{}, 156 | closed: make(chan struct{}), 157 | removeOnClose: new(bool), 158 | } 159 | *c.removeOnClose = false 160 | 161 | c.namespaces[c.namespace] = c 162 | 163 | ops := append([]Op{dbDefault()}, op...) 164 | 165 | c.log.Debug("[cove] applying options") 166 | for _, op := range ops { 167 | err := op(c) 168 | if err != nil { 169 | return nil, fmt.Errorf("could not exec op, %w", err) 170 | } 171 | } 172 | 173 | c.log.Debug("[cove] creating schema") 174 | err = schema(c) 175 | if err != nil { 176 | return nil, fmt.Errorf("failed in creating schema, %w", err) 177 | } 178 | 179 | c.log.Debug("[cove] optimizing database") 180 | err = optimize(c) 181 | if err != nil { 182 | return nil, fmt.Errorf("failed in optimizing, %w", err) 183 | } 184 | 185 | if c.vacuum != nil { 186 | c.log.Debug("[cove] starting vacuum") 187 | go c.vacuum(c) 188 | } 189 | 190 | return c, nil 191 | } 192 | 193 | // NS creates a new namespace, if the namespace already exists it will return the existing namespace. 194 | // 195 | // onEvict must be set for every new namespace created using WithEvictCallback. 196 | // NS will create a new table in the database for the namespace in order to isolate it, and the indexes. 197 | func (c *Cache) NS(ns string, ops ...Op) (*Cache, error) { 198 | c.mu.Lock() 199 | defer c.mu.Unlock() 200 | 201 | if _, found := c.namespaces[ns]; found { 202 | return c.namespaces[ns], nil 203 | } 204 | 205 | nc := &Cache{ 206 | uri: c.uri, 207 | ttl: c.ttl, 208 | 209 | onEvict: nil, 210 | vacuum: c.vacuum, 211 | 212 | namespace: ns, 213 | namespaces: c.namespaces, 214 | 215 | mu: c.mu, 216 | muKey: keyedMu(), 217 | 218 | db: c.db, 219 | 220 | closeOnce: c.closeOnce, 221 | closed: c.closed, 222 | removeOnClose: c.removeOnClose, 223 | log: c.log.With("ns", ns), 224 | } 225 | 226 | nc.namespaces[nc.namespace] = nc 227 | 228 | nc.log.Debug("[cove] creating schema") 229 | err := schema(nc) 230 | if err != nil { 231 | return nil, fmt.Errorf("could not create schema for namespace, %w", err) 232 | } 233 | 234 | nc.log.Debug("[cove] applying options") 235 | for _, op := range ops { 236 | err = op(c) 237 | if err != nil { 238 | return nil, fmt.Errorf("could not exec op, %w", err) 239 | } 240 | } 241 | 242 | return nc, nil 243 | } 244 | 245 | func (c *Cache) tbl() string { 246 | return "_cache_" + c.namespace 247 | } 248 | 249 | func optimize(c *Cache) error { 250 | return exec(c.db, `pragma vacuum;`, `pragma optimize;`) 251 | } 252 | 253 | func schema(c *Cache) error { 254 | q := fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %s ( 255 | key TEXT primary key, 256 | value BLOB, 257 | create_at INTEGER DEFAULT (strftime('%%s', 'now')), 258 | expire_at INTEGER, 259 | ttl INTEGER 260 | );`, c.tbl()) 261 | 262 | return exec(c.db, q) 263 | } 264 | 265 | // Close closes the cache and all its namespaces 266 | func (c *Cache) Close() error { 267 | defer func() { 268 | if *c.removeOnClose { 269 | _ = c.removeStore() 270 | } 271 | }() 272 | 273 | defer func() { 274 | c.closeOnce.Do(func() { 275 | close(c.closed) 276 | }) 277 | }() 278 | 279 | return c.db.Close() 280 | } 281 | 282 | func (c *Cache) removeStore() error { 283 | 284 | select { 285 | case <-c.closed: 286 | default: 287 | return fmt.Errorf("db is not closed") 288 | } 289 | 290 | schema, uri, found := strings.Cut(c.uri, ":") 291 | if !found { 292 | return fmt.Errorf("could not find file in uri") 293 | } 294 | if schema != "file" { 295 | return fmt.Errorf("not a file uri") 296 | } 297 | 298 | file, query, _ := strings.Cut(uri, "?") 299 | 300 | c.log.Info("[cove] remove store", "db", file, "shm", fmt.Sprintf("%s-shm", file), "wal", fmt.Sprintf("%s-wal", file)) 301 | 302 | err := errors.Join( 303 | os.Remove(file), 304 | os.Remove(fmt.Sprintf("%s-shm", file)), 305 | os.Remove(fmt.Sprintf("%s-wal", file)), 306 | ) 307 | if strings.Contains(query, "tmp=true") { 308 | c.log.Info("[cove] remove store dir", "dir", filepath.Dir(file)) 309 | err = errors.Join(os.Remove(filepath.Dir(file)), err) 310 | } 311 | 312 | return err 313 | } 314 | 315 | // Get retrieves a value from the cache 316 | func (c *Cache) Get(key string) ([]byte, error) { 317 | return get(c.db, key, c.tbl()) 318 | } 319 | 320 | // GetOr retrieves a value from the cache, if the key does not exist it will call the setter function and set the result. 321 | // 322 | // If multiple goroutines call GetOr with the same key, only one will call the fetch function 323 | // the others will wait for the first to finish and retrieve the cached value from the first call. 324 | // It is useful paradigm to lessen a thundering herd problem. 325 | // This is done by locking on the provided key in the application layer, not the database layer. 326 | // meaning, this might work poorly if multiple applications are using the same sqlite cache files. 327 | func (c *Cache) GetOr(key string, fetch func(k string) ([]byte, error)) ([]byte, error) { 328 | var err error 329 | var value []byte 330 | 331 | value, err = get(c.db, key, c.tbl()) 332 | 333 | if err == nil { 334 | return value, nil 335 | } 336 | 337 | if !errors.Is(err, NotFound) { 338 | return nil, fmt.Errorf("could not get key %s, err; %w", key, err) 339 | } 340 | 341 | if errors.Is(err, NotFound) { 342 | c.muKey.Lock(key) 343 | defer c.muKey.Unlock(key) 344 | 345 | value, err = get(c.db, key, c.tbl()) // if someone else has set the key 346 | if err == nil { 347 | return value, nil 348 | } 349 | 350 | c.log.Debug("[cove] cache miss fetching value, using fetcher", "key", key) 351 | 352 | value, err = fetch(key) 353 | if err != nil { 354 | return nil, fmt.Errorf("could not set value, err; %w", err) 355 | } 356 | err = setTTL(c.db, key, value, c.ttl, c.tbl()) 357 | if err != nil { 358 | return nil, fmt.Errorf("could not set ttl, err; %w", err) 359 | } 360 | return value, nil 361 | 362 | } 363 | return value, nil 364 | } 365 | 366 | // Set sets a value in the cache, with default ttl 367 | func (c *Cache) Set(key string, value []byte) error { 368 | return c.SetTTL(key, value, c.ttl) 369 | } 370 | 371 | // SetTTL sets a value in the cache with a custom ttl 372 | func (c *Cache) SetTTL(key string, value []byte, ttl time.Duration) error { 373 | return setTTL(c.db, key, value, ttl, c.tbl()) 374 | } 375 | 376 | func (c *Cache) tx(eval func(tx *sql.Tx) error) error { 377 | tx, err := c.db.Begin() 378 | if err != nil { 379 | return fmt.Errorf("could not begin tx, err; %w", err) 380 | } 381 | err = eval(tx) 382 | if err != nil { 383 | _ = tx.Rollback() 384 | return fmt.Errorf("could not eval tx, err; %w", err) 385 | } 386 | err = tx.Commit() 387 | if err != nil { 388 | return fmt.Errorf("could not commit tx, err; %w", err) 389 | } 390 | return nil 391 | 392 | } 393 | 394 | // BatchSet sets a batch of key/value pairs in the cache 395 | // 396 | // the BatchSet will take place in one transaction, but split up into sub-batches of MAX_PARAMS/3 size, ie 999/3 = 333, 397 | // in order to have the BatchSet be atomic. If one key fails to set, the whole batch will fail. 398 | // Prefer batches less then MAX_PARAMS 399 | func (c *Cache) BatchSet(rows []KV[[]byte]) error { 400 | size := MAX_PARAMS / 3 401 | 402 | if len(rows) <= size { 403 | return batchSet(c.db, rows, c.ttl, c.tbl()) 404 | } 405 | 406 | err := c.tx(func(tx *sql.Tx) error { 407 | for i := 0; i < len(rows); i += size { 408 | end := i + size 409 | if end > len(rows) { 410 | end = len(rows) 411 | } 412 | chunk := rows[i:end] 413 | err := batchSet(tx, chunk, c.ttl, c.tbl()) 414 | if err != nil { 415 | return fmt.Errorf("could not batch set, err; %w", err) 416 | } 417 | } 418 | return nil 419 | }) 420 | 421 | if err != nil { 422 | return fmt.Errorf("could not set full batch, err; %w", err) 423 | } 424 | 425 | return nil 426 | } 427 | 428 | // BatchGet retrieves a batch of keys from the cache 429 | // 430 | // the BatchGet will take place in one transaction, but split up into sub-batches of MAX_PARAMS size, ie 999, 431 | // in order to have the BatchGet be atomic. If one key fails to fetched, the whole batch will fail. 432 | // Prefer batches less then MAX_PARAMS 433 | func (c *Cache) BatchGet(keys []string) ([]KV[[]byte], error) { 434 | 435 | size := MAX_PARAMS 436 | 437 | if len(keys) <= size { 438 | return batchGet(c.db, keys, c.tbl()) 439 | } 440 | 441 | var res []KV[[]byte] 442 | 443 | err := c.tx(func(tx *sql.Tx) error { 444 | for i := 0; i < len(keys); i += size { 445 | end := i + size 446 | if end > len(keys) { 447 | end = len(keys) 448 | } 449 | chunk := keys[i:end] 450 | kvs, err := batchGet(tx, chunk, c.tbl()) 451 | if err != nil { 452 | return fmt.Errorf("could not batch get, err; %w", err) 453 | } 454 | res = append(res, kvs...) 455 | } 456 | return nil 457 | }) 458 | if err != nil { 459 | return nil, fmt.Errorf("could not get full batch, err; %w", err) 460 | } 461 | 462 | return res, nil 463 | } 464 | 465 | // BatchEvict evicts a batch of keys from the cache 466 | // 467 | // if onEvict is set, it will be called for each key 468 | // the eviction will take place in one transaction, but split up into bacthes of MAX_PARAMS, ie 999, 469 | // in order to have the eviction be atomic. If one key fails to evict, the whole batch will fail. 470 | // Prefer batches less then MAX_PARAMS 471 | func (c *Cache) BatchEvict(keys []string) (evicted []KV[[]byte], err error) { 472 | 473 | defer func() { 474 | if c.onEvict != nil { 475 | for _, kv := range evicted { 476 | key := kv.K 477 | val := kv.V 478 | go c.onEvict(key, val) 479 | } 480 | } 481 | }() 482 | 483 | size := MAX_PARAMS 484 | if len(keys) <= size { 485 | evicted, err = batchEvict(c.db, keys, c.tbl()) 486 | return evicted, err 487 | } 488 | 489 | err = c.tx(func(tx *sql.Tx) error { 490 | for i := 0; i < len(keys); i += size { 491 | end := i + size 492 | if end > len(keys) { 493 | end = len(keys) 494 | } 495 | chunk := keys[i:end] 496 | kvs, err := batchEvict(tx, chunk, c.tbl()) 497 | if err != nil { 498 | return fmt.Errorf("could not batch evict, err; %w", err) 499 | } 500 | evicted = append(evicted, kvs...) 501 | } 502 | return nil 503 | }) 504 | 505 | return evicted, err 506 | } 507 | 508 | // Evict evicts a key from the cache 509 | // if onEvict is set, it will be called for key 510 | func (c *Cache) Evict(key string) (kv KV[[]byte], err error) { 511 | kv, err = evict(c.db, key, c.tbl()) 512 | if err == nil && c.onEvict != nil { 513 | go c.onEvict(kv.K, kv.V) 514 | } 515 | return kv, err 516 | } 517 | 518 | // EvictAll evicts all keys in the cache 519 | // onEvict will not be called 520 | func (c *Cache) EvictAll() (len int, err error) { 521 | return evictAll(c.db, c.tbl()) 522 | } 523 | 524 | // Range returns all key value pairs in the range [from, to] 525 | func (c *Cache) Range(from string, to string) (kv []KV[[]byte], err error) { 526 | return getRange(c.db, from, to, c.tbl()) 527 | } 528 | 529 | // Keys returns all keys in the range [from, to] 530 | func (c *Cache) Keys(from string, to string) (keys []string, err error) { 531 | return getKeys(c.db, from, to, c.tbl()) 532 | } 533 | 534 | // Values returns all values in the range [from, to] 535 | func (c *Cache) Values(from string, to string) (values [][]byte, err error) { 536 | return getValues(c.db, from, to, c.tbl()) 537 | } 538 | 539 | // ItrRange returns an iterator for the range of keys [from, to] 540 | // 541 | // WARNING 542 | // Since iterators don't really have any way of communication errors 543 | // the Con is that errors are dropped when using iterators. 544 | // the Pro is that it is very easy to use, and scan row by row (ie. no need to load all rows into memory) 545 | func (c *Cache) ItrRange(from string, to string) iter.Seq2[string, []byte] { 546 | return iterKV(c.db, from, to, c.tbl(), c.log) 547 | } 548 | 549 | // ItrKeys returns an iterator for the range of keys [from, to] 550 | // 551 | // WARNING 552 | // Since iterators don't really have any way of communication errors 553 | // the Con is that errors are dropped when using iterators. 554 | // the Pro is that it is very easy to use, and scan row by row (ie. no need to load all rows into memory) 555 | func (c *Cache) ItrKeys(from string, to string) iter.Seq[string] { 556 | return iterKeys(c.db, from, to, c.tbl(), c.log) 557 | } 558 | 559 | // ItrValues returns an iterator for the range of values [from, to] 560 | // 561 | // WARNING 562 | // Since iterators don't really have any way of communication errors 563 | // the Con is that errors are dropped when using iterators. 564 | // the Pro is that it is very easy to use, and scan row by row (ie. no need to load all rows into memory) 565 | func (c *Cache) ItrValues(from string, to string) iter.Seq[[]byte] { 566 | return iterValues(c.db, from, to, c.tbl(), c.log) 567 | } 568 | 569 | func (c *Cache) Vacuum(max int) (n int, err error) { 570 | 571 | c.log.Debug("[cove] vacuuming namespace", "max_eviction", max) 572 | if c.onEvict == nil { // Dont do expensive vacuum if no onEvict is set 573 | return vacuumNoResult(c.db, max, c.tbl()) 574 | } 575 | 576 | kvs, err := vacuum(c.db, max, c.tbl()) 577 | if err != nil { 578 | return 0, fmt.Errorf("could not vacuum, err; %w", err) 579 | } 580 | 581 | if c.onEvict != nil { 582 | c.log.Debug("[cove] calling onEvict callback", "evicted", len(kvs)) 583 | for _, kv := range kvs { 584 | k := kv.K 585 | v := kv.V 586 | go c.onEvict(k, v) 587 | } 588 | } 589 | return len(kvs), nil 590 | } 591 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cove 2 | 3 | [![goreportcard.com](https://goreportcard.com/badge/github.com/modfin/cove)](https://goreportcard.com/report/github.com/modfin/cove) 4 | [![PkgGoDev](https://pkg.go.dev/badge/github.com/modfin/cove)](https://pkg.go.dev/github.com/modfin/cove) 5 | 6 | 7 | `cove` is a caching library for Go that utilizes SQLite as the storage backend. It provides a TTL cache for key-value pairs with support for namespaces, batch operations, range scans and eviction callbacks. 8 | 9 | 10 | ## TL;DR 11 | 12 | A TTL caching for Go backed by SQLite. (See examples for usage) 13 | 14 | ```bash 15 | go get github.com/modfin/cove 16 | ``` 17 | 18 | ```go 19 | package main 20 | 21 | import ( 22 | "fmt" 23 | "github.com/modfin/cove" 24 | ) 25 | 26 | func main() { 27 | cache, err := cove.New( 28 | cove.URITemp(), 29 | ) 30 | if err != nil { 31 | panic(err) 32 | } 33 | defer cache.Close() 34 | 35 | ns, err := cache.NS("strings") 36 | if err != nil { 37 | panic(err) 38 | } 39 | // When using generic, use a separate namespace for each type 40 | stringCache := cove.Of[string](ns) 41 | 42 | stringCache.Set("key", "the string") 43 | 44 | str, err := stringCache.Get("key") 45 | hit, err := cove.Hit(err) 46 | if err != nil { 47 | panic(err) 48 | } 49 | if hit { 50 | fmt.Println(str) // Output: the string 51 | } 52 | 53 | str, err = stringCache.GetOr("async-key", func(key string) (string, error) { 54 | return "refreshed string", nil 55 | }) 56 | hit, err = cove.Hit(err) 57 | if err != nil { 58 | panic(err) 59 | } 60 | if hit { 61 | fmt.Println(str) // Output: refreshed string 62 | } 63 | 64 | } 65 | 66 | ``` 67 | 68 | 69 | ## Use case 70 | `cove` is meant to be embedded into your application, and not as a standalone service. 71 | It is a simple key-value store that is meant to be used for caching data that is expensive to compute or retrieve. 72 | `cove` can also be used as a key-value store 73 | 74 | So why SQLite? 75 | 76 | There are plenty of in memory and other caches build in go, \ 77 | eg https://github.com/avelino/awesome-go#cache, performance, concurrency, fast, LFU, LRU, ARC and so on. \ 78 | There are also a few key-value stores build in go that can be embedded (just like cove), \ 79 | eg https://github.com/dgraph-io/badger or https://github.com/boltdb/bolt and probably quite a few more, \ 80 | https://github.com/avelino/awesome-go#databases-implemented-in-go. 81 | 82 | Well if these alternatives suits your use case, use them. 83 | The main benefit of using a cache/kv, from my perspective and the reason for building cove, is that a cache backed by sqlite should be decent 84 | and fast enough in most case. 85 | Its generically just a good solution while probably being outperformed by others in niche cases. 86 | - you can have very large K/V pairs 87 | - you can tune it for your use case 88 | - it should perform decently 89 | - you can cache hundreds of GB. SSD are fast these days. 90 | - page caching and tuning will help you out. 91 | 92 | While sqlite has come a long way since its inception and particular with it running in WAL mode, there are some limitations. 93 | Eg only one writer is allowed at a time. So if you have a write heavy cache, you might want to consider another solution. 94 | With that said it should be fine for most with some tuning to increase write performance, eg `synchronous = off`. 95 | 96 | ## Installation 97 | 98 | To install `cove`, use `go get`: 99 | 100 | ```sh 101 | go get github.com/modfin/cove 102 | ``` 103 | 104 | ### Considerations 105 | Since `cove` uses SQLite as the storage backend, it is important realize that you project now will depend on cgo and that the SQLite library will be compiled into your project. This might not be a problem at all, but it could cause problems in some modern "magic" build tools used in CD/CI pipelines for go. 106 | 107 | ## Usage 108 | 109 | ### Tuning 110 | 111 | cove uses sqlite in WAL mode and writes the data to disk. While probably :memory: works for the most part, it does not have all the cool performance stuff that comes with sqlite in WAL mode on disk and probably will result in som SQL_BUSY errors. 112 | 113 | In general the default tuning is the following 114 | ```sqlite 115 | PRAGMA journal_mode = WAL; 116 | PRAGMA synchronous = normal; 117 | PRAGMA temp_store = memory; 118 | PRAGMA auto_vacuum = incremental; 119 | PRAGMA incremental_vacuum; 120 | ``` 121 | 122 | Have a look at https://www.sqlite.org/pragma.html for tuning your cache to your needs. 123 | 124 | If you are write heavy, you might want to consider `synchronous = off` and dabble with some other settings, eg `wal_autocheckpoint`, to increase write performance. The tradeoff is that you might lose some read performance instead. 125 | 126 | ```go 127 | cache, err := cove.New( 128 | cove.URITemp(), 129 | cove.DBSyncOff(), 130 | // Yes, yes, this can be used to inject sql, but I trust you 131 | // to not let your users arbitrarily configure pragma on your 132 | // sqlite instance. 133 | cove.DBPragma("wal_autocheckpoint = 1000"), 134 | ) 135 | ``` 136 | 137 | 138 | ### Creating a Cache 139 | 140 | To create a cache, use the `New` function. You can specify various options such as TTL, vacuum interval, and eviction callbacks. 141 | 142 | 143 | #### Config 144 | 145 | `cove.URITemp()` Creates a temporary directory in `/tmp` or similar for the database. If combined with a `cove.DBRemoveOnClose()` and a gracefull shutdown, the database will be removed on close 146 | 147 | if you want the cache to persist over restarts or such, you can use `cove.URIFromPath("/path/to/db/cache.db")` instead. 148 | 149 | There are a few options that can be set when creating a cache, see `cove.With*` and `cove.DB*` functions for more information. 150 | 151 | #### Example 152 | 153 | ```go 154 | package main 155 | 156 | import ( 157 | "github.com/modfin/cove" 158 | "time" 159 | ) 160 | 161 | func main() { 162 | cache, err := cove.New( 163 | cove.URITemp(), 164 | cove.DBRemoveOnClose(), 165 | cove.WithTTL(time.Minute*10), 166 | ) 167 | if err != nil { 168 | panic(err) 169 | } 170 | defer cache.Close() 171 | } 172 | ``` 173 | 174 | ### Setting and Getting Values 175 | 176 | You can set and get values from the cache using the `Set` and `Get` methods. 177 | 178 | ```go 179 | package main 180 | 181 | import ( 182 | "fmt" 183 | "github.com/modfin/cove" 184 | "time" 185 | ) 186 | 187 | func main() { 188 | cache, err := cove.New( 189 | cove.URITemp(), 190 | cove.DBRemoveOnClose(), 191 | cove.WithTTL(time.Minute*10), 192 | ) 193 | if err != nil { 194 | panic(err) 195 | } 196 | defer cache.Close() 197 | 198 | // Set a value 199 | err = cache.Set("key", []byte("value0")) 200 | if err != nil { 201 | panic(err) 202 | } 203 | 204 | // Get the value 205 | value, err := cache.Get("key") 206 | hit, err := cove.Hit(err) 207 | if err != nil { 208 | panic(err) 209 | } 210 | 211 | fmt.Println("[Hit]:", hit, "[Value]:", string(value)) 212 | // Output: "[Hit]: true [Value]: value0 213 | } 214 | ``` 215 | 216 | 217 | 218 | ### Handling `NotFound` errors 219 | 220 | If a key is not found in the cache, the `Get` method will return an `NotFound` error. 221 | 222 | You can handle not found errors using the `Hit` and `Miss` helper functions. 223 | 224 | ```go 225 | package main 226 | 227 | import ( 228 | "errors" 229 | "fmt" 230 | "github.com/modfin/cove" 231 | "time" 232 | ) 233 | 234 | func main() { 235 | cache, err := cove.New( 236 | cove.URITemp(), 237 | cove.DBRemoveOnClose(), 238 | cove.WithTTL(time.Minute*10), 239 | ) 240 | if err != nil { 241 | panic(err) 242 | } 243 | defer cache.Close() 244 | 245 | _, err = cache.Get("key") 246 | fmt.Println("err == cove.NotFound:", err == cove.NotFound) 247 | fmt.Println("errors.Is(err, cove.NotFound):", errors.Is(err, cove.NotFound)) 248 | 249 | _, err = cache.Get("key") 250 | hit, err := cove.Hit(err) 251 | if err != nil { // A "real" error has occurred 252 | panic(err) 253 | } 254 | if !hit { 255 | fmt.Println("key miss") 256 | } 257 | 258 | _, err = cache.Get("key") 259 | miss, err := cove.Miss(err) 260 | if err != nil { // A "real" error has occurred 261 | panic(err) 262 | } 263 | if miss { 264 | fmt.Println("key miss") 265 | } 266 | } 267 | ``` 268 | 269 | 270 | 271 | 272 | 273 | ### Using Namespaces 274 | 275 | Namespaces allow you to isolate different sets of keys within the same cache. 276 | 277 | ```go 278 | package main 279 | 280 | import ( 281 | "github.com/modfin/cove" 282 | "time" 283 | "fmt" 284 | ) 285 | 286 | func main() { 287 | cache, err := cove.New( 288 | cove.URITemp(), 289 | cove.DBRemoveOnClose(), 290 | cove.WithTTL(time.Minute*10), 291 | ) 292 | if err != nil { 293 | panic(err) 294 | } 295 | defer cache.Close() 296 | 297 | 298 | err = cache.Set("key", []byte("value0")) 299 | if err != nil { 300 | panic(err) 301 | } 302 | 303 | ns, err := cache.NS("namespace1") 304 | if err != nil { 305 | panic(err) 306 | } 307 | 308 | err = ns.Set("key", []byte("value1")) 309 | if err != nil { 310 | panic(err) 311 | } 312 | 313 | 314 | 315 | 316 | value, err := cache.Get("key") 317 | if err != nil { 318 | panic(err) 319 | } 320 | fmt.Println(string(value)) // Output: value0 321 | 322 | value, err = ns.Get("key") 323 | if err != nil { 324 | panic(err) 325 | } 326 | fmt.Println(string(value)) // Output: value1 327 | 328 | 329 | } 330 | ``` 331 | 332 | ### Eviction Callbacks 333 | 334 | You can set a callback function to be called when a key is evicted from the cache. 335 | 336 | ```go 337 | package main 338 | 339 | import ( 340 | "fmt" 341 | "github.com/modfin/cove" 342 | ) 343 | 344 | func main() { 345 | cache, err := cove.New( 346 | cove.URITemp(), 347 | cove.DBRemoveOnClose(), 348 | cove.WithEvictCallback(func(key string, val []byte) { 349 | fmt.Printf("evicted %s: %s\n", key, string(val)) 350 | }), 351 | ) 352 | if err != nil { 353 | panic(err) 354 | } 355 | defer cache.Close() 356 | 357 | err = cache.Set("key", []byte("evict me")) 358 | if err != nil { 359 | panic(err) 360 | } 361 | 362 | _, err = cache.Evict("key") 363 | if err != nil { 364 | panic(err) 365 | } 366 | // Output: evicted key: evict me 367 | } 368 | ``` 369 | 370 | ### Using Iterators 371 | 372 | Iterators allow you to scan through keys and values without loading all rows into memory. 373 | 374 | > **WARNING** \ 375 | > Since iterators don't really have any way of communication errors \ 376 | > the Con is that errors are dropped when using iterators. \ 377 | > the Pro is that it is very easy to use, and scan row by row (ie. no need to load all rows into memory) 378 | 379 | ```go 380 | package main 381 | 382 | import ( 383 | "fmt" 384 | "github.com/modfin/cove" 385 | "time" 386 | ) 387 | 388 | func main() { 389 | cache, err := cove.New( 390 | cove.URITemp(), 391 | cove.DBRemoveOnClose(), 392 | cove.WithTTL(time.Minute*10), 393 | ) 394 | if err != nil { 395 | panic(err) 396 | } 397 | defer cache.Close() 398 | 399 | for i := 0; i < 100; i++ { 400 | err = cache.Set(fmt.Sprintf("key%d", i), []byte(fmt.Sprintf("value%d", i))) 401 | if err != nil { 402 | panic(err) 403 | } 404 | } 405 | 406 | // KV iterator 407 | for k, v := range cache.ItrRange("key97", cove.RANGE_MAX) { 408 | fmt.Println(k, string(v)) 409 | } 410 | 411 | // Key iterator 412 | for key := range cache.ItrKeys(cove.RANGE_MIN, "key1") { 413 | fmt.Println(key) 414 | } 415 | 416 | // Value iterator 417 | for value := range cache.ItrValues(cove.RANGE_MIN, "key1") { 418 | fmt.Println(string(value)) 419 | } 420 | } 421 | ``` 422 | 423 | 424 | ### Batch Operations 425 | 426 | `cove` provides batch operations to efficiently handle multiple keys in a single operation. This includes `BatchSet`, `BatchGet`, and `BatchEvict`. 427 | 428 | #### BatchSet 429 | 430 | The `BatchSet` method allows you to set multiple key-value pairs in the cache in a single operation. This method ensures atomicity by splitting the batch into sub-batches if necessary. 431 | 432 | ```go 433 | package main 434 | 435 | import ( 436 | "fmt" 437 | "github.com/modfin/cove" 438 | ) 439 | 440 | func assertNoErr(err error) { 441 | if err != nil { 442 | panic(err) 443 | } 444 | } 445 | 446 | func main() { 447 | cache, err := cove.New( 448 | cove.URITemp(), 449 | cove.DBRemoveOnClose(), 450 | ) 451 | assertNoErr(err) 452 | defer cache.Close() 453 | 454 | err = cache.BatchSet([]cove.KV[[]byte]{ 455 | {K: "key1", V: []byte("val1")}, 456 | {K: "key2", V: []byte("val2")}, 457 | }) 458 | assertNoErr(err) 459 | } 460 | ``` 461 | 462 | #### BatchGet 463 | 464 | The `BatchGet` method allows you to retrieve multiple keys from the cache in a single operation. This method ensures atomicity by splitting the batch into sub-batches if necessary. 465 | 466 | ```go 467 | package main 468 | 469 | import ( 470 | "fmt" 471 | "github.com/modfin/cove" 472 | ) 473 | 474 | func assertNoErr(err error) { 475 | if err != nil { 476 | panic(err) 477 | } 478 | } 479 | 480 | func main() { 481 | cache, err := cove.New( 482 | cove.URITemp(), 483 | cove.DBRemoveOnClose(), 484 | ) 485 | assertNoErr(err) 486 | defer cache.Close() 487 | 488 | err = cache.BatchSet([]cove.KV[[]byte]{ 489 | {K: "key1", V: []byte("val1")}, 490 | {K: "key2", V: []byte("val2")}, 491 | }) 492 | assertNoErr(err) 493 | 494 | kvs, err := cache.BatchGet([]string{"key1", "key2", "key3"}) 495 | assertNoErr(err) 496 | 497 | for _, kv := range kvs { 498 | fmt.Println(kv.K, "-", string(kv.V)) 499 | // Output: 500 | // key1 - val1 501 | // key2 - val2 502 | } 503 | } 504 | ``` 505 | 506 | #### BatchEvict 507 | 508 | The `BatchEvict` method allows you to evict multiple keys from the cache in a single operation. This method ensures atomicity by splitting the batch into sub-batches if necessary. 509 | 510 | ```go 511 | package main 512 | 513 | import ( 514 | "fmt" 515 | "github.com/modfin/cove" 516 | ) 517 | 518 | func assertNoErr(err error) { 519 | if err != nil { 520 | panic(err) 521 | } 522 | } 523 | 524 | func main() { 525 | cache, err := cove.New( 526 | cove.URITemp(), 527 | cove.DBRemoveOnClose(), 528 | ) 529 | assertNoErr(err) 530 | defer cache.Close() 531 | 532 | err = cache.BatchSet([]cove.KV[[]byte]{ 533 | {K: "key1", V: []byte("val1")}, 534 | {K: "key2", V: []byte("val2")}, 535 | }) 536 | assertNoErr(err) 537 | 538 | evicted, err := cache.BatchEvict([]string{"key1", "key2", "key3"}) 539 | assertNoErr(err) 540 | 541 | for _, kv := range evicted { 542 | fmt.Println("Evicted,", kv.K, "-", string(kv.V)) 543 | // Output: 544 | // Evicted, key1 - val1 545 | // Evicted, key2 - val2 546 | } 547 | } 548 | ``` 549 | 550 | These batch operations help in efficiently managing multiple keys in the cache, ensuring atomicity and reducing the number of individual operations. 551 | 552 | 553 | 554 | ### Typed Cache 555 | 556 | The `TypedCache` in `cove` provides a way to work with strongly-typed values in the cache, using Go generics. This allows you to avoid manual serialization and deserialization of values, making the code cleaner and less error-prone. 557 | 558 | #### Creating a Typed Cache 559 | 560 | A Typed Cache is simply to use golang generics to wrap the cache and provide type safety and ease of use. 561 | The Typed Cache comes with the same fetchers and api as the untyped cache but adds a marshalling and unmarshalling layer on top of it. 562 | encoding/gob is used for serialization and deserialization of values. 563 | 564 | To create a typed cache, use the `Of` function, passing the existing cache/namespace instance: 565 | 566 | ```go 567 | package main 568 | 569 | import ( 570 | "fmt" 571 | "github.com/modfin/cove" 572 | "time" 573 | ) 574 | 575 | func assertNoErr(err error) { 576 | if err != nil { 577 | panic(err) 578 | } 579 | } 580 | 581 | type Person struct { 582 | Name string 583 | Age int 584 | } 585 | 586 | func main() { 587 | // Create a base cache 588 | cache, err := cove.New( 589 | cove.URITemp(), 590 | cove.DBRemoveOnClose(), 591 | ) 592 | assertNoErr(err) 593 | defer cache.Close() 594 | 595 | personNamespace, err := cache.NS("person") 596 | assertNoErr(err) 597 | 598 | // Create a typed cache for Person struct 599 | typedCache := cove.Of[Person](personNamespace) 600 | 601 | // Set a value in the typed cache 602 | err = typedCache.Set("alice", Person{Name: "Alice", Age: 30}) 603 | assertNoErr(err) 604 | 605 | // Get a value from the typed cache 606 | alice, err := typedCache.Get("alice") 607 | assertNoErr(err) 608 | fmt.Printf("%+v\n", alice) // Output: {Name:Alice Age:30} 609 | } 610 | ``` 611 | 612 | #### Methods 613 | 614 | - **Set**: Sets a value in the cache with the default TTL. 615 | - **Get**: Retrieves a value from the cache. 616 | - **SetTTL**: Sets a value in the cache with a custom TTL. 617 | - **GetOr**: Retrieves a value from the cache or calls a fetch function if the key does not exist. 618 | - **BatchSet**: Sets a batch of key/value pairs in the cache. 619 | - **BatchGet**: Retrieves a batch of keys from the cache. 620 | - **BatchEvict**: Evicts a batch of keys from the cache. 621 | - **Evict**: Evicts a key from the cache. 622 | - **EvictAll**: Evicts all keys in the cache. 623 | - **Range**: Returns all key-value pairs in a specified range. 624 | - **Keys**: Returns all keys in a specified range. 625 | - **Values**: Returns all values in a specified range. 626 | - **ItrRange**: Returns an iterator for key-value pairs in a specified range. 627 | - **ItrKeys**: Returns an iterator for keys in a specified range. 628 | - **ItrValues**: Returns an iterator for values in a specified range. 629 | - **Raw**: Returns the underlying untyped cache. 630 | 631 | The `TypedCache` uses `encoding/gob` for serialization and deserialization of values, ensuring type safety and ease of use. 632 | 633 | 634 | 635 | 636 | ## Benchmarks 637 | 638 | All models are wrong but some are useful. Not sure what category this falls under, 639 | but here are some benchmarks `inserts/sec`, `reads/sec`, `write mb/sec` and `read mb/sec`. 640 | 641 | In general Linux, 4 cores, a ssd and 32 gb of ram it seems to do some 642 | - 20-30k inserts/sec 643 | - 200k reads/sec. 644 | - writes 100-200 mb/sec 645 | - reads 1000-2000 mb/sec. 646 | 647 | It seems fast enough... 648 | 649 | ```txt 650 | 651 | BenchmarkSetParallel/default-4 28_256 insert/sec 652 | BenchmarkSetParallel/sync-off-4 36_523 insert/sec 653 | BenchmarkSetParallel/sync-off+autocheckpoint-4 25_480 insert/sec 654 | 655 | BenchmarkGetParallel/default-4 192_668 reads/sec 656 | BenchmarkGetParallel/sync-off-4 238_714 reads/sec 657 | BenchmarkGetParallel/sync-off+autocheckpoint-4 193_778 reads/sec 658 | 659 | BenchmarkSetMemParallel/default+0.1mb-4 273 write-mb/sec 660 | BenchmarkSetMemParallel/default+1mb-4 261 write-mb/sec 661 | BenchmarkSetMemParallel/sync-off+0.1mb-4 238 write-mb/sec 662 | BenchmarkSetMemParallel/sync-off+1mb-4 212 write-mb/sec 663 | 664 | BenchmarkSetMem/default+0.1mb-4 104 write-mb/sec 665 | BenchmarkSetMem/default+1mb-4 122 write-mb/sec 666 | BenchmarkSetMem/sync-off+0.1mb-4 219 write-mb/sec 667 | BenchmarkSetMem/sync-off+1mb-4 249 write-mb/sec 668 | 669 | BenchmarkGetMemParallel/default+0.1mb-4 2_189 read-mb/sec 670 | BenchmarkGetMemParallel/default+1mb-4 1_566 read-mb/sec 671 | BenchmarkGetMemParallel/sync-off+0.1mb-4 2_194 read-mb/sec 672 | BenchmarkGetMemParallel/sync-off+1mb-4 1_501 read-mb/sec 673 | 674 | BenchmarkGetMem/default+0.1mb-4 764 read-mb/sec 675 | BenchmarkGetMem/default+1mb-4 520 read-mb/sec 676 | BenchmarkGetMem/sync-off+0.1mb-4 719 read-mb/sec 677 | BenchmarkGetMem/sync-off+1mb-4 530 read-mb/sec 678 | 679 | ``` 680 | 681 | 682 | ## TODO 683 | 684 | - [ ] Add hooks or middleware for logging, metrics, eviction strategy etc. 685 | - [ ] More testing 686 | 687 | ## License 688 | 689 | This project is licensed under the MIT License - see the `LICENSE` file for details. 690 | -------------------------------------------------------------------------------- /cache_test.go: -------------------------------------------------------------------------------- 1 | package cove_test 2 | 3 | import ( 4 | "fmt" 5 | "github.com/modfin/cove" 6 | "sort" 7 | "strconv" 8 | "sync" 9 | "testing" 10 | "time" 11 | 12 | _ "github.com/mattn/go-sqlite3" 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | func TestNewCacheWithTempUri(t *testing.T) { 17 | cache, err := cove.New(cove.URITemp(), cove.DBRemoveOnClose()) 18 | assert.NoError(t, err) 19 | assert.NotNil(t, cache) 20 | defer cache.Close() 21 | } 22 | 23 | func TestCacheSetAndGet(t *testing.T) { 24 | cache, err := cove.New(cove.URITemp(), cove.DBRemoveOnClose()) 25 | assert.NoError(t, err) 26 | defer cache.Close() 27 | 28 | key := "test-key" 29 | value := []byte("test-value") 30 | 31 | err = cache.Set(key, value) 32 | assert.NoError(t, err) 33 | 34 | retrievedValue, err := cache.Get(key) 35 | assert.NoError(t, err) 36 | assert.Equal(t, value, retrievedValue) 37 | } 38 | 39 | func TestCacheSetAndGetSize(t *testing.T) { 40 | do := func(size int) func(t *testing.T) { 41 | return func(t *testing.T) { 42 | cache, err := cove.New(cove.URITemp(), cove.DBRemoveOnClose()) 43 | assert.NoError(t, err) 44 | defer cache.Close() 45 | 46 | key := "test-key" 47 | 48 | value := make([]byte, size) 49 | for i := 0; i < size; i++ { 50 | value[i] = byte(i) 51 | } 52 | 53 | err = cache.Set(key, value) 54 | assert.NoError(t, err) 55 | 56 | retrievedValue, err := cache.Get(key) 57 | assert.NoError(t, err) 58 | assert.Equal(t, value, retrievedValue) 59 | 60 | } 61 | } 62 | 63 | t.Run("__1_B", do(1)) 64 | t.Run("__1_kB", do(1_000)) 65 | t.Run("_10_kB", do(10_000)) 66 | t.Run("100_kB", do(100_000)) 67 | t.Run("__1_MB", do(1_000_000)) 68 | t.Run("_10_MB", do(10_000_000)) 69 | //t.Run("100_MB", do(100_000_000)) 70 | //t.Run("__1_GB", do(1_000_000_000-100)) // Maximum blob size in SQLite is 1_000_000_000 aka 1GB (probably need some bytes for metadata) 71 | 72 | } 73 | 74 | func TestCacheSetGetEmpty(t *testing.T) { 75 | cache, err := cove.New(cove.URITemp(), cove.DBRemoveOnClose()) 76 | assert.NoError(t, err) 77 | defer cache.Close() 78 | 79 | key := "test-key" 80 | value := []byte{} 81 | 82 | err = cache.Set(key, value) 83 | assert.NoError(t, err) 84 | 85 | retrievedValue, err := cache.Get(key) 86 | assert.NoError(t, err) 87 | assert.Equal(t, value, retrievedValue) 88 | } 89 | 90 | func TestCacheSetGetNil(t *testing.T) { 91 | cache, err := cove.New(cove.URITemp(), cove.DBRemoveOnClose()) 92 | assert.NoError(t, err) 93 | defer cache.Close() 94 | 95 | key := "test-key" 96 | 97 | err = cache.Set(key, nil) 98 | assert.NoError(t, err) 99 | 100 | exp := []byte{} 101 | retrievedValue, err := cache.Get(key) 102 | assert.NoError(t, err) 103 | assert.Equal(t, exp, retrievedValue) 104 | } 105 | 106 | func TestCacheGetOrSet(t *testing.T) { 107 | cache, err := cove.New(cove.URITemp(), cove.DBRemoveOnClose()) 108 | assert.NoError(t, err) 109 | defer cache.Close() 110 | 111 | key := "test-key" 112 | value := []byte("test-value") 113 | 114 | retrievedValue, err := cache.GetOr(key, func(k string) ([]byte, error) { 115 | return value, nil 116 | }) 117 | assert.NoError(t, err) 118 | assert.Equal(t, value, retrievedValue) 119 | } 120 | 121 | func TestCacheGetOrSetParallel(t *testing.T) { 122 | cache, err := cove.New(cove.URITemp(), cove.DBRemoveOnClose()) 123 | assert.NoError(t, err) 124 | defer cache.Close() 125 | 126 | key := "test-key" 127 | value := []byte("test-value") 128 | 129 | start := make(chan struct{}) 130 | 131 | wg := sync.WaitGroup{} 132 | 133 | do := func() { 134 | <-start 135 | retrievedValue, err := cache.GetOr(key, func(k string) ([]byte, error) { 136 | return value, nil 137 | }) 138 | assert.NoError(t, err) 139 | assert.Equal(t, value, retrievedValue) 140 | wg.Done() 141 | } 142 | 143 | for i := 0; i < 100; i++ { 144 | wg.Add(1) 145 | go do() 146 | } 147 | time.Sleep(1 * time.Second) 148 | close(start) 149 | wg.Wait() 150 | } 151 | 152 | func TestCacheGetOrSetParallelMem(t *testing.T) { 153 | cache, err := cove.New(cove.URITemp(), cove.DBRemoveOnClose()) 154 | assert.NoError(t, err) 155 | defer cache.Close() 156 | 157 | key := "test-key" 158 | value := []byte("test-value") 159 | 160 | start := make(chan struct{}) 161 | 162 | wg := sync.WaitGroup{} 163 | 164 | do := func() { 165 | <-start 166 | retrievedValue, err := cache.GetOr(key, func(k string) ([]byte, error) { 167 | return value, nil 168 | }) 169 | assert.NoError(t, err) 170 | assert.Equal(t, value, retrievedValue) 171 | wg.Done() 172 | } 173 | 174 | for i := 0; i < 100; i++ { 175 | wg.Add(1) 176 | go do() 177 | } 178 | time.Sleep(1 * time.Second) 179 | close(start) 180 | wg.Wait() 181 | } 182 | 183 | func TestCacheEvict(t *testing.T) { 184 | cache, err := cove.New(cove.URITemp(), cove.DBRemoveOnClose()) 185 | assert.NoError(t, err) 186 | defer cache.Close() 187 | 188 | key := "test-key" 189 | value := []byte("test-value") 190 | 191 | err = cache.Set(key, value) 192 | assert.NoError(t, err) 193 | 194 | prevValue, err := cache.Evict(key) 195 | assert.NoError(t, err) 196 | assert.Equal(t, value, prevValue.V) 197 | 198 | _, err = cache.Get(key) 199 | assert.Error(t, err) 200 | assert.Equal(t, cove.NotFound, err) 201 | } 202 | 203 | func TestCacheSetTTL(t *testing.T) { 204 | cache, err := cove.New(cove.URITemp(), cove.DBRemoveOnClose()) 205 | assert.NoError(t, err) 206 | defer cache.Close() 207 | 208 | key := "test-key" 209 | value := []byte("test-value") 210 | ttl := 1 * time.Second 211 | 212 | err = cache.SetTTL(key, value, ttl) 213 | assert.NoError(t, err) 214 | 215 | time.Sleep(2 * time.Second) 216 | 217 | _, err = cache.Get(key) 218 | assert.Error(t, err) 219 | assert.Equal(t, cove.NotFound, err) 220 | } 221 | 222 | func TestCache_Range(t *testing.T) { 223 | cache, err := cove.New(cove.URITemp(), cove.DBRemoveOnClose()) 224 | assert.NoError(t, err) 225 | defer cache.Close() 226 | 227 | // Set some values 228 | err = cache.Set("key1", []byte("value1")) 229 | assert.NoError(t, err) 230 | err = cache.Set("key2", []byte("value2")) 231 | assert.NoError(t, err) 232 | err = cache.Set("key3", []byte("value3")) 233 | assert.NoError(t, err) 234 | 235 | // Test ItrRange 236 | kv, err := cache.Range(cove.RANGE_MIN, cove.RANGE_MAX) 237 | assert.NoError(t, err) 238 | assert.Equal(t, 3, len(kv)) 239 | assert.Equal(t, "value1", string(kv[0].V)) 240 | assert.Equal(t, "value2", string(kv[1].V)) 241 | assert.Equal(t, "value3", string(kv[2].V)) 242 | } 243 | 244 | func TestCache_RangeEmpty(t *testing.T) { 245 | cache, err := cove.New(cove.URITemp(), cove.DBRemoveOnClose()) 246 | assert.NoError(t, err) 247 | defer cache.Close() 248 | 249 | // Test ItrRange 250 | kv, err := cache.Range(cove.RANGE_MIN, cove.RANGE_MAX) 251 | assert.NoError(t, err) 252 | assert.Equal(t, 0, len(kv)) 253 | } 254 | 255 | func TestCache_Keys(t *testing.T) { 256 | cache, err := cove.New(cove.URITemp(), cove.DBRemoveOnClose()) 257 | assert.NoError(t, err) 258 | defer cache.Close() 259 | 260 | // Set some values 261 | err = cache.Set("key1", []byte("value1")) 262 | assert.NoError(t, err) 263 | err = cache.Set("key2", []byte("value2")) 264 | assert.NoError(t, err) 265 | 266 | // Test ItrKeys 267 | keys, err := cache.Keys(cove.RANGE_MIN, cove.RANGE_MAX) 268 | assert.NoError(t, err) 269 | assert.Equal(t, 2, len(keys)) 270 | assert.Equal(t, "key1", keys[0]) 271 | assert.Equal(t, "key2", keys[1]) 272 | } 273 | 274 | func TestCache_Values(t *testing.T) { 275 | cache, err := cove.New(cove.URITemp(), cove.DBRemoveOnClose()) 276 | assert.NoError(t, err) 277 | defer cache.Close() 278 | 279 | // Set some values 280 | err = cache.Set("key1", []byte("value1")) 281 | assert.NoError(t, err) 282 | err = cache.Set("key2", []byte("value2")) 283 | assert.NoError(t, err) 284 | 285 | // Test ItrValues 286 | values, err := cache.Values("key1", "key2") 287 | assert.NoError(t, err) 288 | assert.Equal(t, 2, len(values)) 289 | assert.Equal(t, "value1", string(values[0])) 290 | assert.Equal(t, "value2", string(values[1])) 291 | } 292 | 293 | func TestCacheRange(t *testing.T) { 294 | cache, err := cove.New(cove.URITemp(), cove.DBRemoveOnClose()) 295 | assert.NoError(t, err) 296 | defer cache.Close() 297 | 298 | // Set some key-value pairs in the cache 299 | pairs := []cove.KV[[]byte]{ 300 | {"key1", []byte("value1")}, 301 | {"key2", []byte("value2")}, 302 | {"key3:and:more", []byte("value3")}, 303 | {"key4", []byte("value4")}, 304 | {"key5", []byte("value5")}, 305 | {"key6", []byte("value6")}, 306 | } 307 | 308 | exp := []cove.KV[[]byte]{ 309 | {"key2", []byte("value2")}, 310 | {"key3:and:more", []byte("value3")}, 311 | {"key4", []byte("value4")}, 312 | } 313 | 314 | for _, pair := range pairs { 315 | err = cache.Set(pair.K, pair.V) 316 | assert.NoError(t, err) 317 | } 318 | 319 | // Test the ItrRange function 320 | kvs, err := cache.Range("key2", "key4") 321 | 322 | assert.NoError(t, err) 323 | 324 | sort.Slice(kvs, func(i, j int) bool { 325 | return kvs[i].K < kvs[j].K 326 | }) 327 | sort.Slice(exp, func(i, j int) bool { 328 | return exp[i].K < exp[j].K 329 | }) 330 | 331 | assert.Equal(t, exp, kvs) 332 | } 333 | 334 | func TestCacheIter(t *testing.T) { 335 | cache, err := cove.New(cove.URITemp(), cove.DBRemoveOnClose()) 336 | assert.NoError(t, err) 337 | defer cache.Close() 338 | 339 | // Set some key-value pairs in the cache 340 | pairs := []cove.KV[[]byte]{ 341 | {"key1", []byte("value1")}, 342 | {"key2", []byte("value2")}, 343 | {"key3:and:more", []byte("value3")}, 344 | {"key4", []byte("value4")}, 345 | {"key6", []byte("value5")}, 346 | {"key7", []byte("value6")}, 347 | } 348 | 349 | exp := []cove.KV[[]byte]{ 350 | {"key2", []byte("value2")}, 351 | {"key3:and:more", []byte("value3")}, 352 | {"key4", []byte("value4")}, 353 | } 354 | 355 | for _, pair := range pairs { 356 | err = cache.Set(pair.K, pair.V) 357 | assert.NoError(t, err) 358 | } 359 | 360 | var res []cove.KV[[]byte] 361 | 362 | for k, v := range cache.ItrRange("key2", "key4") { 363 | res = append(res, cove.KV[[]byte]{ 364 | K: k, 365 | V: v, 366 | }) 367 | } 368 | 369 | assert.Equal(t, exp, res) 370 | 371 | var keys []string 372 | for key := range cache.ItrKeys("key2", "key5") { 373 | keys = append(keys, key) 374 | } 375 | assert.Equal(t, []string{"key2", "key3:and:more", "key4"}, keys) 376 | 377 | var vals []string 378 | for val := range cache.ItrValues("key2", "key5") { 379 | vals = append(vals, string(val)) 380 | } 381 | assert.Equal(t, []string{"value2", "value3", "value4"}, vals) 382 | 383 | } 384 | 385 | func TestCacheIterEmpty(t *testing.T) { 386 | cache, err := cove.New(cove.URITemp(), cove.DBRemoveOnClose()) 387 | assert.NoError(t, err) 388 | defer cache.Close() 389 | 390 | // Set some key-value pairs in the cache 391 | 392 | var res []cove.KV[[]byte] 393 | for k, v := range cache.ItrRange("key2", "key4") { 394 | res = append(res, cove.KV[[]byte]{ 395 | K: k, 396 | V: v, 397 | }) 398 | } 399 | 400 | assert.Equal(t, len(res), 0) 401 | 402 | var keys []string 403 | for key := range cache.ItrKeys("key2", "key5") { 404 | keys = append(keys, key) 405 | } 406 | assert.Equal(t, len(keys), 0) 407 | 408 | var vals []string 409 | for val := range cache.ItrValues("key2", "key5") { 410 | vals = append(vals, string(val)) 411 | } 412 | assert.Equal(t, len(vals), 0) 413 | 414 | } 415 | 416 | func TestNS(t *testing.T) { 417 | c1, err := cove.New(cove.URITemp(), cove.DBRemoveOnClose()) 418 | assert.NoError(t, err) 419 | defer c1.Close() 420 | 421 | c1.Set("key1", []byte("ns1")) 422 | c2, err := c1.NS("ns2") 423 | assert.NoError(t, err) 424 | 425 | c2.Set("key1", []byte("ns2")) 426 | 427 | v, err := c1.Get("key1") 428 | assert.NoError(t, err) 429 | assert.Equal(t, []byte("ns1"), v) 430 | 431 | v, err = c2.Get("key1") 432 | assert.NoError(t, err) 433 | assert.Equal(t, []byte("ns2"), v) 434 | 435 | c11, err := c2.NS(cove.NS_DEFAULT) 436 | assert.NoError(t, err) 437 | v, err = c11.Get("key1") 438 | assert.NoError(t, err) 439 | assert.Equal(t, []byte("ns1"), v) 440 | 441 | _, err = c11.Evict("key1") 442 | hit, err := cove.Hit(err) 443 | assert.NoError(t, err) 444 | assert.True(t, hit) 445 | 446 | _, err = c1.Evict("key1") 447 | hit, err = cove.Hit(err) 448 | assert.NoError(t, err) 449 | assert.False(t, hit) 450 | 451 | } 452 | 453 | func TestCacheBatchSetSizes(t *testing.T) { 454 | 455 | do := func(itre int) func(t *testing.T) { 456 | return func(t *testing.T) { 457 | cache, err := cove.New(cove.URITemp(), cove.DBRemoveOnClose()) 458 | assert.NoError(t, err) 459 | defer cache.Close() 460 | 461 | var rows []cove.KV[[]byte] 462 | for i := 0; i < itre; i++ { 463 | rows = append(rows, cove.KV[[]byte]{K: strconv.Itoa(i), V: []byte(fmt.Sprintf("value_%d", i))}) 464 | } 465 | 466 | err = cache.BatchSet(rows) 467 | assert.NoError(t, err) 468 | 469 | for _, row := range rows { 470 | retrievedValue, err := cache.Get(row.K) 471 | assert.NoError(t, err) 472 | assert.Equal(t, row.V, retrievedValue) 473 | } 474 | } 475 | } 476 | 477 | t.Run("10", do(10)) 478 | t.Run("11", do(11)) 479 | t.Run("100", do(100)) 480 | t.Run("101", do(101)) 481 | 482 | t.Run(fmt.Sprintf("%d", cove.MAX_PARAMS/3-1), do(cove.MAX_PARAMS/3-1)) 483 | t.Run(fmt.Sprintf("%d", cove.MAX_PARAMS/3), do(cove.MAX_PARAMS/3)) 484 | t.Run(fmt.Sprintf("%d", cove.MAX_PARAMS/3+1), do(cove.MAX_PARAMS/3+1)) 485 | 486 | t.Run(fmt.Sprintf("%d", cove.MAX_PARAMS-1), do(cove.MAX_PARAMS-1)) 487 | t.Run(fmt.Sprintf("%d", cove.MAX_PARAMS), do(cove.MAX_PARAMS)) 488 | t.Run(fmt.Sprintf("%d", cove.MAX_PARAMS+1), do(cove.MAX_PARAMS+1)) 489 | 490 | t.Run("1_013", do(1013)) 491 | t.Run("10_000", do(10000)) 492 | t.Run("10_007", do(10007)) 493 | 494 | } 495 | 496 | func TestCacheBatchGetSizes(t *testing.T) { 497 | 498 | do := func(itre int) func(t *testing.T) { 499 | return func(t *testing.T) { 500 | cache, err := cove.New(cove.URITemp(), cove.DBRemoveOnClose()) 501 | assert.NoError(t, err) 502 | defer cache.Close() 503 | 504 | var keys []string 505 | var rows []cove.KV[[]byte] 506 | for i := 0; i < itre; i++ { 507 | kv := cove.KV[[]byte]{K: strconv.Itoa(i), V: []byte(fmt.Sprintf("value_%d", i))} 508 | rows = append(rows, kv) 509 | keys = append(keys, kv.K) 510 | err = cache.Set(kv.K, kv.V) 511 | assert.NoError(t, err) 512 | } 513 | 514 | res, err := cache.BatchGet(keys) 515 | assert.NoError(t, err) 516 | 517 | sort.Slice(res, func(i, j int) bool { 518 | return res[i].K < res[j].K 519 | }) 520 | sort.Slice(rows, func(i, j int) bool { 521 | return rows[i].K < rows[j].K 522 | }) 523 | //sort.Strings(keys) 524 | 525 | assert.Equal(t, len(rows), len(res)) 526 | assert.Equal(t, len(rows), len(keys)) 527 | 528 | for i, row := range rows { 529 | assert.NoError(t, err) 530 | assert.Equal(t, string(row.K), string(res[i].K)) 531 | assert.Equal(t, string(row.V), string(res[i].V)) 532 | } 533 | } 534 | } 535 | 536 | t.Run("10", do(10)) 537 | t.Run("11", do(11)) 538 | t.Run("100", do(100)) 539 | t.Run("101", do(101)) 540 | 541 | t.Run(fmt.Sprintf("%d", cove.MAX_PARAMS/3-1), do(cove.MAX_PARAMS/3-1)) 542 | t.Run(fmt.Sprintf("%d", cove.MAX_PARAMS/3), do(cove.MAX_PARAMS/3)) 543 | t.Run(fmt.Sprintf("%d", cove.MAX_PARAMS/3+1), do(cove.MAX_PARAMS/3+1)) 544 | 545 | t.Run(fmt.Sprintf("%d", cove.MAX_PARAMS-1), do(cove.MAX_PARAMS-1)) 546 | t.Run(fmt.Sprintf("%d", cove.MAX_PARAMS), do(cove.MAX_PARAMS)) 547 | t.Run(fmt.Sprintf("%d", cove.MAX_PARAMS+1), do(cove.MAX_PARAMS+1)) 548 | 549 | t.Run("1_013", do(1013)) 550 | t.Run("10_000", do(10000)) 551 | t.Run("10_007", do(10007)) 552 | 553 | } 554 | 555 | func TestCacheBatchEvictSizes(t *testing.T) { 556 | 557 | do := func(itre int) func(t *testing.T) { 558 | return func(t *testing.T) { 559 | 560 | var evicted []cove.KV[[]byte] 561 | var mu sync.Mutex 562 | wgEvicted := sync.WaitGroup{} 563 | wgEvicted.Add(itre) 564 | 565 | cache, err := cove.New(cove.URITemp(), cove.DBRemoveOnClose(), 566 | cove.WithEvictCallback(func(k string, v []byte) { 567 | mu.Lock() 568 | defer mu.Unlock() 569 | defer wgEvicted.Done() 570 | evicted = append(evicted, cove.KV[[]byte]{K: k, V: v}) 571 | }), 572 | ) 573 | assert.NoError(t, err) 574 | defer cache.Close() 575 | 576 | var keys []string 577 | var rows []cove.KV[[]byte] 578 | for i := 0; i < itre; i++ { 579 | kv := cove.KV[[]byte]{K: strconv.Itoa(i), V: []byte(fmt.Sprintf("value_%d", i))} 580 | rows = append(rows, kv) 581 | keys = append(keys, kv.K) 582 | err = cache.Set(kv.K, kv.V) 583 | assert.NoError(t, err) 584 | } 585 | 586 | res, err := cache.BatchEvict(keys) 587 | assert.NoError(t, err) 588 | 589 | sort.Slice(res, func(i, j int) bool { 590 | return res[i].K < res[j].K 591 | }) 592 | sort.Slice(rows, func(i, j int) bool { 593 | return rows[i].K < rows[j].K 594 | }) 595 | 596 | assert.Equal(t, len(rows), len(res)) 597 | assert.Equal(t, len(rows), len(keys)) 598 | 599 | for i, row := range rows { 600 | assert.Equal(t, string(row.K), string(res[i].K)) 601 | assert.Equal(t, string(row.V), string(res[i].V)) 602 | 603 | _, err = cache.Get(row.K) 604 | assert.Error(t, err) 605 | assert.Equal(t, cove.NotFound, err) 606 | } 607 | 608 | wgEvicted.Wait() 609 | sort.Slice(evicted, func(i, j int) bool { 610 | return evicted[i].K < evicted[j].K 611 | }) 612 | assert.Equal(t, rows, evicted) 613 | 614 | } 615 | } 616 | 617 | t.Run("10", do(10)) 618 | t.Run("11", do(11)) 619 | t.Run("100", do(100)) 620 | t.Run("101", do(101)) 621 | 622 | t.Run(fmt.Sprintf("%d", cove.MAX_PARAMS/3-1), do(cove.MAX_PARAMS/3-1)) 623 | t.Run(fmt.Sprintf("%d", cove.MAX_PARAMS/3), do(cove.MAX_PARAMS/3)) 624 | t.Run(fmt.Sprintf("%d", cove.MAX_PARAMS/3+1), do(cove.MAX_PARAMS/3+1)) 625 | 626 | t.Run(fmt.Sprintf("%d", cove.MAX_PARAMS-1), do(cove.MAX_PARAMS-1)) 627 | t.Run(fmt.Sprintf("%d", cove.MAX_PARAMS), do(cove.MAX_PARAMS)) 628 | t.Run(fmt.Sprintf("%d", cove.MAX_PARAMS+1), do(cove.MAX_PARAMS+1)) 629 | 630 | t.Run("1_013", do(1013)) 631 | t.Run("10_000", do(10000)) 632 | t.Run("10_007", do(10007)) 633 | 634 | } 635 | 636 | func TestCacheBatchSet(t *testing.T) { 637 | cache, err := cove.New(cove.URITemp(), cove.DBRemoveOnClose()) 638 | assert.NoError(t, err) 639 | defer cache.Close() 640 | 641 | rows := []cove.KV[[]byte]{ 642 | {K: "key1", V: []byte("value1")}, 643 | {K: "key2", V: []byte("value2")}, 644 | {K: "key3", V: []byte("value3")}, 645 | } 646 | 647 | err = cache.BatchSet(rows) 648 | assert.NoError(t, err) 649 | 650 | for _, row := range rows { 651 | retrievedValue, err := cache.Get(row.K) 652 | assert.NoError(t, err) 653 | assert.Equal(t, row.V, retrievedValue) 654 | } 655 | } 656 | 657 | func TestCacheBatchGet(t *testing.T) { 658 | cache, err := cove.New(cove.URITemp(), cove.DBRemoveOnClose()) 659 | assert.NoError(t, err) 660 | defer cache.Close() 661 | 662 | rows := []cove.KV[[]byte]{ 663 | {K: "key1", V: []byte("value1")}, 664 | {K: "key2", V: []byte("value2")}, 665 | {K: "key3", V: []byte("value3")}, 666 | } 667 | 668 | err = cache.BatchSet(rows) 669 | assert.NoError(t, err) 670 | 671 | keys := []string{"key1", "key2", "key3", "key4"} 672 | retrievedRows, err := cache.BatchGet(keys) 673 | assert.NoError(t, err) 674 | 675 | sort.Slice(retrievedRows, func(i, j int) bool { 676 | return retrievedRows[i].K < retrievedRows[j].K 677 | }) 678 | 679 | assert.Equal(t, rows, retrievedRows) 680 | } 681 | 682 | func TestCacheVacuum(t *testing.T) { 683 | 684 | do := func(itre int) func(t *testing.T) { 685 | return func(t *testing.T) { 686 | 687 | var evicted []cove.KV[[]byte] 688 | var mu sync.Mutex 689 | wgEvicted := sync.WaitGroup{} 690 | wgEvicted.Add(itre) 691 | 692 | ttl := 500 * time.Millisecond 693 | 694 | cache, err := cove.New( 695 | cove.URITemp(), 696 | cove.DBRemoveOnClose(), 697 | cove.WithTTL(ttl), 698 | cove.WithVacuum(cove.Vacuum(100*time.Millisecond, 1000)), 699 | cove.WithEvictCallback(func(k string, v []byte) { 700 | mu.Lock() 701 | defer mu.Unlock() 702 | defer wgEvicted.Done() 703 | evicted = append(evicted, cove.KV[[]byte]{K: k, V: v}) 704 | }), 705 | ) 706 | assert.NoError(t, err) 707 | defer cache.Close() 708 | 709 | var rows []cove.KV[[]byte] 710 | for i := 0; i < itre; i++ { 711 | kv := cove.KV[[]byte]{K: strconv.Itoa(i), V: []byte(fmt.Sprintf("value_%d", i))} 712 | rows = append(rows, kv) 713 | } 714 | 715 | const chunkSize = 100 716 | for i := 0; i < len(rows); i += chunkSize { 717 | end := i + chunkSize 718 | if end > len(rows) { 719 | end = len(rows) 720 | } 721 | err = cache.BatchSet(rows[i:end]) 722 | assert.NoError(t, err) 723 | } 724 | 725 | wgEvicted.Wait() 726 | sort.Slice(evicted, func(i, j int) bool { 727 | return evicted[i].K < evicted[j].K 728 | }) 729 | sort.Slice(rows, func(i, j int) bool { 730 | return rows[i].K < rows[j].K 731 | }) 732 | assert.Equal(t, rows, evicted) 733 | } 734 | } 735 | 736 | t.Run("10", do(10)) 737 | t.Run("1_000", do(1_000)) 738 | t.Run("100_000", do(100_000)) 739 | //t.Run("1_000_000", do(1_000_000)) 740 | } 741 | 742 | func TestSpecific1(t *testing.T) { // from fuzz 743 | cache, err := cove.New( 744 | cove.URITemp(), 745 | cove.DBRemoveOnClose()) 746 | assert.NoError(t, err) 747 | defer cache.Close() 748 | 749 | vv1 := []byte{0x31, 0x12, 0xd7, 0x38, 0x7b} 750 | vv2 := []byte{0x32, 0xc9, 0x42, 0x47, 0x7, 0x93} 751 | err = cache.Set("key1", vv1) 752 | assert.NoError(t, err) 753 | err = cache.Set("key2", vv2) 754 | assert.NoError(t, err) 755 | 756 | v1, err := cache.Get("key1") 757 | assert.NoError(t, err) 758 | assert.Equal(t, vv1, v1) 759 | v2, err := cache.Get("key2") 760 | assert.NoError(t, err) 761 | assert.Equal(t, vv2, v2) 762 | } 763 | func TestSpecific2(t *testing.T) { // from fuzz 764 | cache, err := cove.New( 765 | cove.URITemp(), 766 | cove.DBRemoveOnClose()) 767 | assert.NoError(t, err) 768 | defer cache.Close() 769 | 770 | kk1 := string("\x80") 771 | vv1 := []byte("2") 772 | err = cache.Set(kk1, vv1) 773 | assert.NoError(t, err) 774 | 775 | v1, err := cache.Get(kk1) 776 | assert.NoError(t, err) 777 | assert.Equal(t, vv1, v1) 778 | 779 | kk1 = string(rune(0x80)) 780 | vv1 = []byte("3") 781 | err = cache.Set(kk1, vv1) 782 | assert.NoError(t, err) 783 | 784 | v1, err = cache.Get(kk1) 785 | assert.NoError(t, err) 786 | assert.Equal(t, vv1, v1) 787 | 788 | kk1 = string([]byte{0x80}) 789 | vv1 = []byte("4") 790 | err = cache.Set(kk1, vv1) 791 | assert.NoError(t, err) 792 | 793 | v1, err = cache.Get(kk1) 794 | assert.NoError(t, err) 795 | assert.Equal(t, vv1, v1) 796 | } 797 | 798 | func TestSpecific3(t *testing.T) { // from fuzz 799 | cache, err := cove.New( 800 | cove.URITemp(), 801 | cove.DBRemoveOnClose()) 802 | assert.NoError(t, err) 803 | defer cache.Close() 804 | 805 | kk1 := string("") 806 | vv1 := []byte("\xcd\x00\x14=8E\xbf\x9b\x9e\x81\x0e2\xe7\x17\x10\x94ݿ\xadbT\x92\x9c/\xa8\x8f(\xf07gl\xae\x00J)\x97'\xe5") 807 | err = cache.Set(kk1, vv1) 808 | assert.NoError(t, err) 809 | 810 | v1, err := cache.Get(kk1) 811 | assert.NoError(t, err) 812 | assert.Equal(t, vv1, v1) 813 | } 814 | 815 | // 816 | //func BenchmarkCacheSet(b *testing.B) { 817 | // cache, err := cove.New(cove.URITemp(), cove.DBRemoveOnClose()) 818 | // assert.NoError(b, err) 819 | // defer cache.Close() 820 | // 821 | // value := []byte("1") 822 | // 823 | // var i int64 824 | // b.ResetTimer() 825 | // b.RunParallel(func(pb *testing.PB) { 826 | // for pb.Next() { 827 | // ii := atomic.AddInt64(&i, 1) 828 | // k := strconv.Itoa(int(ii)) 829 | // err = cache.Set(k, value) 830 | // assert.NoError(b, err) 831 | // } 832 | // }) 833 | //} 834 | // 835 | //func BenchmarkCacheGet(b *testing.B) { 836 | // cache, err := cove.New(cove.URITemp(), cove.DBRemoveOnClose()) 837 | // assert.NoError(b, err) 838 | // defer cache.Close() 839 | // 840 | // key := "1" 841 | // value := []byte("1") 842 | // err = cache.Set(key, value) 843 | // b.ResetTimer() 844 | // b.RunParallel(func(pb *testing.PB) { 845 | // for pb.Next() { 846 | // _, err = cache.Get(key) 847 | // assert.NoError(b, err) 848 | // } 849 | // 850 | // }) 851 | //} 852 | // 853 | //func BenchmarkCacheLargeSetGet(bbb *testing.B) { 854 | // 855 | // bench := func(op ...cove.Op) func(*testing.B) { 856 | // return func(b *testing.B) { 857 | // cache, err := cove.New(cove.URITemp(), cove.DBRemoveOnClose(), op...) 858 | // assert.NoError(b, err) 859 | // defer cache.Close() 860 | // 861 | // start := time.Now() 862 | // for i := 0; i < b.N; i++ { 863 | // key := strconv.Itoa(i) 864 | // value := []byte(key) 865 | // err = cache.Set(key, value) 866 | // assert.NoError(b, err) 867 | // } 868 | // elapsed := time.Since(start) 869 | // 870 | // b.ResetTimer() 871 | // b.RunParallel(func(pb *testing.PB) { 872 | // for pb.Next() { 873 | // key := strconv.Itoa(int(rand.Float64() * float64(b.N))) 874 | // _, err = cache.Get(key) 875 | // assert.NoError(b, err) 876 | // 877 | // } 878 | // 879 | // }) 880 | // b.ReportMetric(float64(elapsed.Nanoseconds())/float64(b.N), "insert-namespace/op") 881 | // } 882 | // } 883 | // 884 | // bbb.Run("default", bench(cove.dbDefault())) 885 | // bbb.Run("default/op_journal", bench(cove.dbDefault(), cove.DBJournalSize(cove.OptimizedJournalSize))) 886 | // bbb.Run("default/sync_off", bench(cove.dbDefault(), cove.DBSyncOff())) 887 | // bbb.Run("default/sync_off/op_journal", bench(cove.dbDefault(), cove.DBSyncOff(), cove.DBJournalSize(cove.OptimizedJournalSize))) 888 | // 889 | // //bbb.Run("optimized", bench(cove.DBFullOptimize())) 890 | // //bbb.Run("optimized sync off", bench(cove.DBFullOptimize(), cove.DBSyncOff())) 891 | // 892 | //} 893 | // 894 | //func BenchmarkCacheRealWorld(bbb *testing.B) { 895 | // 896 | // bench := func(op ...cove.Op) func(*testing.B) { 897 | // return func(b *testing.B) { 898 | // cache, err := cove.New(cove.URITemp(), cove.DBRemoveOnClose(), op...) 899 | // assert.NoError(b, err) 900 | // defer cache.Close() 901 | // 902 | // done := make(chan struct{}) 903 | // wg := sync.WaitGroup{} 904 | // wg2 := sync.WaitGroup{} 905 | // mu := sync.Mutex{} 906 | // 907 | // var avgInsert float64 908 | // var avgInsertCount float64 909 | // 910 | // var avgEvict float64 911 | // var avgEvictCount float64 912 | // 913 | // for i := 0; i < runtime.NumCPU()/2; i++ { 914 | // wg.Add(1) 915 | // wg2.Add(1) 916 | // go func() { 917 | // defer wg2.Done() 918 | // wg.Done() 919 | // wg.Wait() 920 | // for i := 0; i < b.N; i++ { 921 | // 922 | // select { 923 | // case <-done: 924 | // return 925 | // default: 926 | // } 927 | // 928 | // key := strconv.Itoa(int(rand.Float64() * float64(b.N))) 929 | // value := []byte("1") 930 | // start := time.Now() 931 | // err := cache.Set(key, value) 932 | // elapsed := time.Since(start) 933 | // assert.NoError(b, err) 934 | // 935 | // mu.Lock() 936 | // 937 | // avgInsert = (avgInsert*avgInsertCount + float64(elapsed.Nanoseconds())) / (avgInsertCount + 1) 938 | // avgInsertCount++ 939 | // 940 | // mu.Unlock() 941 | // 942 | // if i%100 == 0 { 943 | // 944 | // start = time.Now() 945 | // _, _, err := cache.Evict(key) 946 | // elapsed = time.Since(start) 947 | // mu.Lock() 948 | // avgEvict = (avgEvict*avgEvictCount + float64(elapsed.Nanoseconds())) / (avgEvictCount + 1) 949 | // avgEvictCount++ 950 | // mu.Unlock() 951 | // assert.NoError(b, err) 952 | // } 953 | // } 954 | // 955 | // }() 956 | // } 957 | // wg.Wait() 958 | // 959 | // b.ResetTimer() 960 | // b.RunParallel(func(pb *testing.PB) { 961 | // for pb.Next() { 962 | // key := strconv.Itoa(int(rand.Float64() * float64(b.N))) 963 | // _, err := cache.Get(key) 964 | // if errors.Is(cove.NotFound, err) { 965 | // continue 966 | // } 967 | // assert.NoError(b, err) 968 | // } 969 | // }) 970 | // close(done) 971 | // wg2.Wait() 972 | // 973 | // b.ReportMetric(avgInsert, "insert-namespace/op") 974 | // b.ReportMetric(avgEvict, "evict-namespace/op") 975 | // } 976 | // 977 | // } 978 | // 979 | // bbb.Run("default", bench(cove.dbDefault())) 980 | // bbb.Run("default/op_journal", bench(cove.dbDefault(), cove.DBJournalSize(cove.OptimizedJournalSize))) 981 | // bbb.Run("default/sync_off", bench(cove.dbDefault(), cove.DBSyncOff())) 982 | // bbb.Run("default/sync_off/op_journal", bench(cove.dbDefault(), cove.DBSyncOff(), cove.DBJournalSize(cove.OptimizedJournalSize))) 983 | // 984 | // //bbb.Run("optimized", bench(cove.DBFullOptimize())) 985 | // //bbb.Run("optimized sync off", bench(cove.DBFullOptimize(), cove.DBSyncOff())) 986 | // 987 | //} 988 | --------------------------------------------------------------------------------