From 6e9cc8c6e68558426b5b297fa5699beb89f5bea7 Mon Sep 17 00:00:00 2001 From: Dmytro Borshchanenko Date: Sat, 27 Apr 2024 12:41:48 +0300 Subject: [PATCH] v2: ttl is time.Duration, generics instead of interface{} --- README.md | 28 +++++++-------- bench_test.go | 21 ++++++++--- examples/example.go | 6 ++-- examples/hit_or_miss.go | 6 ++-- examples/readme_example/main.go | 9 ++--- main.go | 63 ++++++++++++++++++--------------- main_test.go | 43 +++++++++++----------- 7 files changed, 95 insertions(+), 81 deletions(-) diff --git a/README.md b/README.md index 67b3ea3..cfa1741 100644 --- a/README.md +++ b/README.md @@ -31,11 +31,11 @@ import "github.com/parMaster/mcache" Create a new cache instance using the `NewCache` constructor, and use it to perform cache operations: ```go -cache := mcache.NewCache() +cache := mcache.NewCache[string]() data, err := cache.Get("key") if err != nil { data = ExpensiveFunctionCall() - cache.Set("key", data, 5*60) // cache data for 5 minutes + cache.Set("key", data, 5*time.Minute) // cache data for 5 minutes } ``` ## Examples @@ -45,7 +45,7 @@ See the [examples](https://github.com/parMaster/mcache/tree/main/examples) direc ## API Reference ### Set -Set a key-value pair in the cache. The key must be a string, and the value can be any type that implements the `interface{}` interface, ttl is int64 value in seconds: +Set a key-value pair in the cache. The key must be a string, and the value can be any type, ttl is int64 value in seconds: ```go err := cache.Set("key", "value", 0) @@ -59,7 +59,7 @@ If the key already exists and is not expired, an error `mcache.ErrKeyExists` wil You can also set a key-value pair with an expiration time (in seconds): ```go -err := cache.Set("key", "value", 60) +err := cache.Set("key", "value", time.Minute) if err != nil { // handle error } @@ -67,8 +67,6 @@ if err != nil { The value will automatically expire after the specified duration. -_Note that int64 is used instead of time.Duration for the expiration time. This choise is deliberate to simplify the API. It can be changed in the future, I can break the API but only in the next major version._ - ### Get Retrieve a value from the cache by key: @@ -136,7 +134,7 @@ cache.Cleanup() WithCleanup is a functional option to the NewCache constructor that allows you to specify a cleanup interval: ```go -cache := mcache.NewCache(mcache.WithCleanup(60)) // cleanup every 60 seconds +cache := mcache.NewCache(mcache.WithCleanup[string](time.Minute)) // cleanup every 60 seconds ``` It will basically run a Cleanup method in a goroutine with a time interval. @@ -145,7 +143,7 @@ It will basically run a Cleanup method in a goroutine with a time interval. 100% test coverage: ```shell -$ go test -race . +$ go test -cover -race . ok github.com/parMaster/mcache 8.239s coverage: 100.0% of statements ``` @@ -153,17 +151,15 @@ Blinding fast and efficient: ```shell $ go test -bench . -benchmem - -goos: linux +goos: darwin goarch: amd64 pkg: github.com/parMaster/mcache -cpu: Intel(R) Core(TM) i5-4308U CPU @ 2.80GHz -BenchmarkWrite-4 1797061 815.2 ns/op 247 B/op 3 allocs/op -BenchmarkRead-4 4516329 260.7 ns/op 48 B/op 3 allocs/op -BenchmarkRWD-4 1761019 686.6 ns/op 56 B/op 6 allocs/op +cpu: Intel(R) Core(TM) i5-5257U CPU @ 2.70GHz +BenchmarkWrite-4 1333594 772.8 ns/op 196 B/op 2 allocs/op +BenchmarkRead-4 2578328 490.7 ns/op 15 B/op 1 allocs/op +BenchmarkRWD-4 1277389 939.7 ns/op 47 B/op 5 allocs/op PASS -github.com/parMaster/mcache coverage: 83.0% of statements -ok github.com/parMaster/mcache 7.190s +ok github.com/parMaster/mcache 19.769s ``` ## Contributing diff --git a/bench_test.go b/bench_test.go index 56a7afb..a380f0d 100644 --- a/bench_test.go +++ b/bench_test.go @@ -3,17 +3,16 @@ package mcache import ( "fmt" "testing" + "time" ) -var mcache *Cache - // BenchmarkWrite func BenchmarkWrite(b *testing.B) { - mcache = NewCache() + mcache := NewCache[int]() b.ResetTimer() for i := 0; i < b.N; i++ { - mcache.Set(fmt.Sprintf("%d", i), i, int64(i)) + mcache.Set(fmt.Sprintf("%d", i), i, time.Second) } b.StopTimer() mcache.Cleanup() @@ -21,18 +20,30 @@ func BenchmarkWrite(b *testing.B) { // BenchmarkRead func BenchmarkRead(b *testing.B) { + mcache := NewCache[int]() + + for i := 0; i < b.N; i++ { + mcache.Set(fmt.Sprintf("%d", i), i, time.Minute) + } + + b.ResetTimer() for i := 0; i < b.N; i++ { mcache.Get(fmt.Sprintf("%d", i)) } + b.StopTimer() mcache.Clear() } // BenchmarkRW func BenchmarkRWD(b *testing.B) { + mcache := NewCache[int]() + + b.ResetTimer() for i := 0; i < b.N; i++ { - mcache.Set(fmt.Sprintf("%d", i), i, int64(i)) + mcache.Set(fmt.Sprintf("%d", i), i, time.Hour) mcache.Get(fmt.Sprintf("%d", i)) mcache.Del(fmt.Sprintf("%d", i)) } + b.StopTimer() mcache.Clear() } diff --git a/examples/example.go b/examples/example.go index 5308374..97c9c71 100644 --- a/examples/example.go +++ b/examples/example.go @@ -12,10 +12,10 @@ func demo() { fmt.Println("\r\n Demo 1:") fmt.Println("------------------------------------------------------------") - cache := mcache.NewCache() + cache := mcache.NewCache[string]() - cache.Set("save indefinitely", "value without expiration", 0) // set value without expiration - cache.Set("save for 1 second", "value will expire in 1 second", 1) // set value with expiration in 1 second + cache.Set("save indefinitely", "value without expiration", 0) // set value without expiration + cache.Set("save for 1 second", "value will expire in 1 second", 1*time.Second) // set value with expiration in 1 second exists, err := cache.Has("no such key") // either exists or error can be checked diff --git a/examples/hit_or_miss.go b/examples/hit_or_miss.go index d4c682d..0e50736 100644 --- a/examples/hit_or_miss.go +++ b/examples/hit_or_miss.go @@ -7,7 +7,7 @@ import ( "github.com/parMaster/mcache" ) -func expensive_func() interface{} { +func expensive_func() string { time.Sleep(1 * time.Second) return "expensive result" } @@ -16,7 +16,7 @@ func demo_hit_or_miss() { fmt.Println("\r\n Hit or Miss:") fmt.Println("------------------------------------------------------------") - cache := mcache.NewCache() + cache := mcache.NewCache[string]() // expensive_func will be called only once // because result will be saved in cache for i := 0; i < 10; i++ { @@ -27,6 +27,6 @@ func demo_hit_or_miss() { cache.Set("expensive value", v, 0) continue } - fmt.Println("cache hit - " + v.(string)) + fmt.Println("cache hit - " + v) } } diff --git a/examples/readme_example/main.go b/examples/readme_example/main.go index ee3cdc9..460edd1 100644 --- a/examples/readme_example/main.go +++ b/examples/readme_example/main.go @@ -2,23 +2,24 @@ package main import ( "fmt" + "time" "github.com/parMaster/mcache" ) func main() { - cache := mcache.NewCache() + cache := mcache.NewCache[string]() - cache.Set("key", "value", 5*60) // set value with expiration in 5 minutes + cache.Set("key", "value", time.Minute*5) // set value with expiration in 5 minutes v, err := cache.Get("key") if err != nil { // either error can be checked fmt.Println(err) } - if v != nil { - // or value can be checked for nil + if v != "" { + // or value can be checked for "empty" type value fmt.Println("key =", v) } diff --git a/main.go b/main.go index 6ec6462..2793c82 100644 --- a/main.go +++ b/main.go @@ -14,14 +14,14 @@ const ( ) // CacheItem is a struct for cache item -type CacheItem struct { - value interface{} - expiration int64 +type CacheItem[T any] struct { + value T + expiration time.Time } // Cache is a struct for cache -type Cache struct { - data map[string]CacheItem +type Cache[T any] struct { + data map[string]CacheItem[T] mx sync.RWMutex } @@ -36,9 +36,9 @@ type Cacher interface { } // NewCache is a constructor for Cache -func NewCache(options ...func(*Cache)) *Cache { - c := &Cache{ - data: make(map[string]CacheItem), +func NewCache[T any](options ...func(*Cache[T])) *Cache[T] { + c := &Cache[T]{ + data: make(map[string]CacheItem[T]), } for _, option := range options { @@ -53,24 +53,26 @@ func NewCache(options ...func(*Cache)) *Cache { // If key already exists, but it's expired, set new value and return nil // If key doesn't exist, set new value and return nil // If ttl is 0, set value without expiration -func (c *Cache) Set(key string, value interface{}, ttl int64) error { +func (c *Cache[T]) Set(key string, value T, ttl time.Duration) error { + var zeroTime time.Time + c.mx.RLock() - d, ok := c.data[key] + cached, ok := c.data[key] c.mx.RUnlock() if ok { - if d.expiration == 0 || d.expiration > time.Now().Unix() { + if cached.expiration == zeroTime || cached.expiration.After(time.Now().Add(ttl)) { return fmt.Errorf(ErrKeyExists) } } - var expiration int64 + var expiration time.Time - if ttl > 0 { - expiration = time.Now().Unix() + ttl + if ttl > time.Duration(0) { + expiration = time.Now().Add(ttl) } c.mx.Lock() - c.data[key] = CacheItem{ + c.data[key] = CacheItem[T]{ value: value, expiration: expiration, } @@ -80,13 +82,14 @@ func (c *Cache) Set(key string, value interface{}, ttl int64) error { // Get is a method for getting value by key // If key doesn't exist, return error -// If key exists, but it's expired, return error and delete key +// If key exists, but it's expired, delete key, return zeroa value and error // If key exists and it's not expired, return value -func (c *Cache) Get(key string) (interface{}, error) { +func (c *Cache[T]) Get(key string) (T, error) { + var none T _, err := c.Has(key) if err != nil { - return nil, err + return none, err } // safe return? @@ -100,7 +103,7 @@ func (c *Cache) Get(key string) (interface{}, error) { // If key doesn't exist, return false. // If key exists, but it's expired, return false and delete key. // If key exists and it's not expired, return true. -func (c *Cache) Has(key string) (bool, error) { +func (c *Cache[T]) Has(key string) (bool, error) { c.mx.RLock() d, ok := c.data[key] c.mx.RUnlock() @@ -108,7 +111,8 @@ func (c *Cache) Has(key string) (bool, error) { return false, fmt.Errorf(ErrKeyNotFound) } - if d.expiration != 0 && d.expiration < time.Now().Unix() { + var zeroTime time.Time + if d.expiration != zeroTime && d.expiration.Before(time.Now()) { c.mx.Lock() delete(c.data, key) c.mx.Unlock() @@ -119,7 +123,7 @@ func (c *Cache) Has(key string) (bool, error) { } // Del is a method for deleting key-value pair -func (c *Cache) Del(key string) error { +func (c *Cache[T]) Del(key string) error { _, err := c.Has(key) if err != nil { return err @@ -132,31 +136,32 @@ func (c *Cache) Del(key string) error { } // Clear is a method for clearing cache -func (c *Cache) Clear() error { +func (c *Cache[T]) Clear() error { c.mx.Lock() - c.data = make(map[string]CacheItem) + c.data = make(map[string]CacheItem[T]) c.mx.Unlock() return nil } // Cleanup is a method for deleting expired keys -func (c *Cache) Cleanup() { +func (c *Cache[T]) Cleanup() { c.mx.Lock() + var zeroTime time.Time for k, v := range c.data { - if v.expiration != 0 && v.expiration < time.Now().Unix() { + if v.expiration != zeroTime && v.expiration.Before(time.Now()) { delete(c.data, k) } } c.mx.Unlock() } -// WithCleanup is a functional option for setting interval and starting Cleanup goroutine -func WithCleanup(ttl int64) func(*Cache) { - return func(c *Cache) { +// WithCleanup is a functional option for setting interval to run Cleanup goroutine +func WithCleanup[T any](ttl time.Duration) func(*Cache[T]) { + return func(c *Cache[T]) { go func() { for { c.Cleanup() - time.Sleep(time.Duration(ttl) * time.Second) + time.Sleep(ttl) } }() } diff --git a/main_test.go b/main_test.go index 76bdc25..8ec9753 100644 --- a/main_test.go +++ b/main_test.go @@ -10,26 +10,26 @@ import ( type testItem struct { key string - value interface{} - ttl int64 + value string + ttl time.Duration } func Test_SimpleTest_Mcache(t *testing.T) { - c := NewCache() + c := NewCache[string]() assert.NotNil(t, c) - assert.IsType(t, &Cache{}, c) + assert.IsType(t, &Cache[string]{}, c) assert.NotNil(t, c.data) testItems := []testItem{ - {"key0", "value0", 0}, - {"key1", "value1", 1}, - {"key2", "value2", 20}, - {"key3", "value3", 30}, - {"key4", "value4", 40}, - {"key5", "value5", 50}, - {"key6", "value6", 60}, - {"key7", "value7", 70000000}, + {"key0", "value0", time.Second * 0}, + {"key1", "value1", time.Second * 1}, + {"key2", "value2", time.Second * 20}, + {"key3", "value3", time.Second * 30}, + {"key4", "value4", time.Second * 40}, + {"key5", "value5", time.Second * 50}, + {"key6", "value6", time.Second * 60}, + {"key7", "value7", time.Second * 70000000}, } noSuchKey := "noSuchKey" @@ -40,8 +40,8 @@ func Test_SimpleTest_Mcache(t *testing.T) { for _, item := range testItems { value, err := c.Get(item.key) - assert.NoError(t, err) - assert.Equal(t, item.value, value) + assert.NoError(t, err, fmt.Sprintf("key:%s; val:%s; ttl:%d", item.key, item.value, item.ttl)) + assert.Equal(t, item.value, value, fmt.Sprintf("key:%s; val:%s; ttl:%d", item.key, item.value, item.ttl)) } _, err := c.Get(noSuchKey) @@ -74,9 +74,9 @@ func Test_SimpleTest_Mcache(t *testing.T) { assert.Equal(t, ErrKeyNotFound, err.Error()) } - c.Set("key", "value", 1) + c.Set("key", "value", time.Second*1) time.Sleep(time.Second * 2) - err = c.Set("key", "newvalue", 1) + err = c.Set("key", "newvalue", time.Second*1) assert.NoError(t, err) // old value should be rewritten @@ -85,10 +85,11 @@ func Test_SimpleTest_Mcache(t *testing.T) { assert.Equal(t, "newvalue", value) err = c.Set("key", "not a newer value", 1) - assert.Equal(t, ErrKeyExists, err.Error()) - + if err != nil { + assert.Equal(t, ErrKeyExists, err.Error()) + } time.Sleep(time.Second * 2) - err = c.Set("key", "even newer value", 1) + err = c.Set("key", "even newer value", time.Second*1) // key should be silently rewritten assert.NoError(t, err) value, err = c.Get("key") @@ -111,7 +112,7 @@ func Test_SimpleTest_Mcache(t *testing.T) { } func TestConcurrentSetAndGet(t *testing.T) { - cache := NewCache() + cache := NewCache[string]() // Start multiple goroutines to concurrently set and get values numGoroutines := 10000 @@ -148,7 +149,7 @@ func TestConcurrentSetAndGet(t *testing.T) { // TestWithCleanup tests that the cleanup goroutine is working func TestWithCleanup(t *testing.T) { - cache := NewCache(WithCleanup(1)) + cache := NewCache(WithCleanup[string](time.Second * 1)) // Set a value with a TTL of 1 second err := cache.Set("key", "value", 1)