diff --git a/README.md b/README.md index 67b3ea3..9c4626d 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](60)) // 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 078331d..a380f0d 100644 --- a/bench_test.go +++ b/bench_test.go @@ -3,6 +3,7 @@ package mcache import ( "fmt" "testing" + "time" ) // BenchmarkWrite @@ -11,7 +12,7 @@ func BenchmarkWrite(b *testing.B) { 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() @@ -22,7 +23,7 @@ func BenchmarkRead(b *testing.B) { mcache := NewCache[int]() for i := 0; i < b.N; i++ { - mcache.Set(fmt.Sprintf("%d", i), i, int64(i)) + mcache.Set(fmt.Sprintf("%d", i), i, time.Minute) } b.ResetTimer() @@ -39,7 +40,7 @@ func BenchmarkRWD(b *testing.B) { 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)) } diff --git a/examples/example.go b/examples/example.go index 89d3f13..97c9c71 100644 --- a/examples/example.go +++ b/examples/example.go @@ -14,8 +14,8 @@ func demo() { 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/readme_example/main.go b/examples/readme_example/main.go index 81bf037..460edd1 100644 --- a/examples/readme_example/main.go +++ b/examples/readme_example/main.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "time" "github.com/parMaster/mcache" ) @@ -10,7 +11,7 @@ func main() { 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 { diff --git a/main.go b/main.go index 7ba515a..2793c82 100644 --- a/main.go +++ b/main.go @@ -16,7 +16,7 @@ const ( // CacheItem is a struct for cache item type CacheItem[T any] struct { value T - expiration int64 + expiration time.Time } // Cache is a struct for cache @@ -53,20 +53,22 @@ func NewCache[T any](options ...func(*Cache[T])) *Cache[T] { // 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[T]) Set(key string, value T, 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() @@ -80,7 +82,7 @@ func (c *Cache[T]) Set(key string, value T, 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[T]) Get(key string) (T, error) { var none T @@ -109,7 +111,8 @@ func (c *Cache[T]) 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() @@ -143,21 +146,22 @@ func (c *Cache[T]) Clear() error { // Cleanup is a method for deleting expired keys 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[T any](ttl int64) func(*Cache[T]) { +// 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 384e37b..8ec9753 100644 --- a/main_test.go +++ b/main_test.go @@ -11,7 +11,7 @@ import ( type testItem struct { key string value string - ttl int64 + ttl time.Duration } func Test_SimpleTest_Mcache(t *testing.T) { @@ -22,14 +22,14 @@ func Test_SimpleTest_Mcache(t *testing.T) { 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") @@ -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[string](1)) + cache := NewCache(WithCleanup[string](time.Second * 1)) // Set a value with a TTL of 1 second err := cache.Set("key", "value", 1)