├── .github └── FUNDING.yml ├── LICENSE ├── README.md ├── README.v1.md ├── bench_test.go ├── example_test.go ├── go.mod ├── issue-list.markdown ├── proxy.go ├── proxy_test.go ├── zcache.go └── zcache_test.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: arp242 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2019 Patrick Mylund Nielsen and the go-cache contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | zcache is an in-memory key:value store/cache with time-based evictions. 2 | 3 | It is suitable for applications running on a single machine. It's essentially a 4 | thread-safe map with expiration times. Any object can be stored, for a given 5 | duration or forever, and the cache can be safely used by multiple goroutines. 6 | 7 | Although zcache isn't meant to be used as a persistent datastore, the contents 8 | can be saved to and loaded from a file (using `c.Items()` to retrieve the items 9 | map to serialize, and `NewFrom()` to create a cache from a deserialized one) to 10 | recover from downtime quickly. 11 | 12 | The canonical import path is `zgo.at/zcache/v2`, or `zgo.at/zcache` for the v1. 13 | Reference docs are at https://godocs.io/zgo.at/zcache/v2 and 14 | https://godocs.io/zgo.at/zcache 15 | 16 | This is a fork of https://github.com/patrickmn/go-cache – which no longer seems 17 | actively maintained. There are two versions of zcache, both of which are 18 | maintained: 19 | 20 | - v1 is 100% compatible with go-cache and a drop-in replacement with various 21 | enhancements. 22 | - v2 makes various incompatible changes to the API; some functions calls are 23 | improved and it uses generics, which requires Go 1.18. 24 | 25 | **This README documents v2; see [README.v1.md](/README.v1.md) for the v1 26 | README.** See the "changes" section below for a list of changes. 27 | 28 | Usage 29 | ----- 30 | Some examples from `example_test.go`: 31 | 32 | ```go 33 | func ExampleSimple() { 34 | // Create a cache with a default expiration time of 5 minutes, and which 35 | // purges expired items every 10 minutes. 36 | // 37 | // This creates a cache with string keys and values, with Go 1.18 type 38 | // parameters. 39 | c := zcache.New[string, string](5*time.Minute, 10*time.Minute) 40 | 41 | // Set the value of the key "foo" to "bar", with the default expiration. 42 | c.Set("foo", "bar") 43 | 44 | // Set the value of the key "baz" to "never", with no expiration time. The 45 | // item won't be removed until it's removed with c.Delete("baz"). 46 | c.SetWithExpire("baz", "never", zcache.NoExpiration) 47 | 48 | // Get the value associated with the key "foo" from the cache; due to the 49 | // use of type parameters this is a string, and no type assertions are 50 | // needed. 51 | foo, ok := c.Get("foo") 52 | if ok { 53 | fmt.Println(foo) 54 | } 55 | 56 | // Output: bar 57 | } 58 | 59 | func ExampleStruct() { 60 | type MyStruct struct{ Value string } 61 | 62 | // Create a new cache that stores a specific struct. 63 | c := zcache.New[string, *MyStruct](zcache.NoExpiration, zcache.NoExpiration) 64 | c.Set("cache", &MyStruct{Value: "value"}) 65 | 66 | v, _ := c.Get("cache") 67 | fmt.Printf("%#v\n", v) 68 | 69 | // Output: &zcache_test.MyStruct{Value:"value"} 70 | } 71 | 72 | func ExampleAny() { 73 | // Create a new cache that stores any value, behaving similar to zcache v1 74 | // or go-cache. 75 | c := zcache.New[string, any](zcache.NoExpiration, zcache.NoExpiration) 76 | 77 | c.Set("a", "value 1") 78 | c.Set("b", 42) 79 | 80 | a, _ := c.Get("a") 81 | b, _ := c.Get("b") 82 | 83 | // This needs type assertions. 84 | p := func(a string, b int) { fmt.Println(a, b) } 85 | p(a.(string), b.(int)) 86 | 87 | // Output: value 1 42 88 | } 89 | 90 | func ExampleProxy() { 91 | type Site struct { 92 | ID int 93 | Hostname string 94 | } 95 | 96 | site := &Site{ 97 | ID: 42, 98 | Hostname: "example.com", 99 | } 100 | 101 | // Create a new site which caches by site ID (int), and a "proxy" which 102 | // caches by the hostname (string). 103 | c := zcache.New[int, *Site](zcache.NoExpiration, zcache.NoExpiration) 104 | p := zcache.NewProxy[string, int, *Site](c) 105 | 106 | p.Set(42, "example.com", site) 107 | 108 | siteByID, ok := c.Get(42) 109 | fmt.Printf("%v %v\n", ok, siteByID) 110 | 111 | siteByHost, ok := p.Get("example.com") 112 | fmt.Printf("%v %v\n", ok, siteByHost) 113 | 114 | // They're both the same object/pointer. 115 | fmt.Printf("%v\n", siteByID == siteByHost) 116 | 117 | // Output: 118 | // true &{42 example.com} 119 | // true &{42 example.com} 120 | // true 121 | } 122 | ``` 123 | 124 | Changes 125 | ------- 126 | ### Incompatible changes in v2 127 | - Use type parameters instead of `map[string]interface{}`; you can get the same 128 | as before with `zcache.New[string, any](..)`, but if you know you will only 129 | store `MyStruct` you can use `zcache.New[string, *MyStruct](..)` for 130 | additional type safety. 131 | 132 | - Remove `Save()`, `SaveFile()`, `Load()`, `LoadFile()`; you can still persist 133 | stuff to disk by using `Items()` and `NewFrom()`. These methods were already 134 | deprecated. 135 | 136 | - Rename `Set()` to `SetWithExpire()`, and rename `SetDefault()` to `Set()`. 137 | Most of the time you want to use the default expiry time, so make that the 138 | easier path. 139 | 140 | - The `Increment*` and `Decrement*` functions have been removed; you can replace 141 | them with `Modify()`: 142 | 143 | cache := New[string, int](DefaultExpiration, 0) 144 | cache.Set("one", 1) 145 | cache.Modify("one", func(v int) int { return v + 1 }) 146 | 147 | The performance of this is roughly the same as the old Increment, and this is 148 | a more generic method that can also be used for other things like appending to 149 | a slice. 150 | 151 | - Rename `Flush()` to `Reset()`; I think that more clearly conveys what it's 152 | intended for as `Flush()` is typically used to flush a buffer or the like. 153 | 154 | ### Compatible changes from go-cache 155 | All these changes are in both v1 and v2: 156 | 157 | - Add `Keys()` to list all keys. 158 | - Add `Touch()` to update the expiry on an item. 159 | - Add `GetStale()` to get items even after they've expired. 160 | - Add `Pop()` to get an item and delete it. 161 | - Add `Modify()` to atomically modify existing cache entries (e.g. lists, maps). 162 | - Add `DeleteAll()` to remove all items from the cache with onEvicted call. 163 | - Add `DeleteFunc()` to remove specific items from the cache atomically. 164 | - Add `Rename()` to rename keys, retaining the value and expiry. 165 | - Add `Proxy` type, to access cache items under a different key. 166 | - Various small internal and documentation improvements. 167 | 168 | See [issue-list.markdown](/issue-list.markdown) for a complete run-down of the 169 | PRs/issues for go-cache and what was and wasn't included. 170 | 171 | FAQ 172 | --- 173 | 174 | ### How can I limit the size of the cache? Is there an option for this? 175 | Not really; zcache is intended as a thread-safe map with time-based eviction. 176 | This keeps it nice and simple. Adding something like a LRU eviction mechanism 177 | not only makes the code more complex, it also makes the library worse for cases 178 | where you just want a map since it requires additional memory and makes some 179 | operations more expensive (unless a new API is added which make the API worse 180 | for those use cases). 181 | 182 | So unless I or someone else comes up with a way to do this which doesn't detract 183 | anything from the simple map use case, I'd rather not add it. Perhaps wrapping 184 | `zcache.Cache` and overriding some methods could work, but I haven't looked at 185 | it. 186 | 187 | tl;dr: this isn't designed to solve every caching use case. That's a feature. 188 | -------------------------------------------------------------------------------- /README.v1.md: -------------------------------------------------------------------------------- 1 | zcache is an in-memory key:value store/cache with time-based evictions. 2 | 3 | It is suitable for applications running on a single machine. It's essentially a 4 | thread-safe `map[string]interface{}` with expiration times. Any object can be 5 | stored, for a given duration or forever, and the cache can be safely used by 6 | multiple goroutines. 7 | 8 | Although zcache isn't meant to be used as a persistent datastore, the entire 9 | cache can be saved to and loaded from a file (using `c.Items()` to retrieve the 10 | items map to serialize, and `NewFrom()` to create a cache from a deserialized 11 | one) to recover from downtime quickly. (See the docs for `NewFrom()` for 12 | caveats.) 13 | 14 | The canonical import path is `zgo.at/zcache`, and reference docs are at 15 | https://godocs.io/zgo.at/zcache 16 | 17 | --- 18 | 19 | This is a fork of https://github.com/patrickmn/go-cache – which no longer seems 20 | actively maintained. v1 is intended to be 100% compatible and a drop-in 21 | replacement. 22 | 23 | See [issue-list.markdown](/issue-list.markdown) for a complete run-down of the 24 | PRs/issues for go-cache and what was and wasn't included; in short: 25 | 26 | - Add `Keys()` to list all keys. 27 | - Add `Touch()` to update the expiry on an item. 28 | - Add `GetStale()` to get items even after they've expired. 29 | - Add `Pop()` to get an item and delete it. 30 | - Add `Modify()` to atomically modify existing cache entries (e.g. lists, maps). 31 | - Add `DeleteAll()` to remove all items from the cache with onEvicted call. 32 | - Add `DeleteFunc()` to remove specific items from the cache atomically. 33 | - Add `Rename()` to rename keys, retaining the value and expiry. 34 | - Add `Proxy` type, to access cache items under a different key. 35 | - Various small internal and documentation improvements. 36 | 37 | 38 | Usage 39 | ----- 40 | 41 | ```go 42 | package main 43 | 44 | import ( 45 | "fmt" 46 | "time" 47 | 48 | "zgo.at/zcache" 49 | ) 50 | 51 | func main() { 52 | // Create a cache with a default expiration time of 5 minutes, and which 53 | // purges expired items every 10 minutes 54 | c := zcache.New(5*time.Minute, 10*time.Minute) 55 | 56 | // Set the value of the key "foo" to "bar", with the default expiration time 57 | c.Set("foo", "bar", zcache.DefaultExpiration) 58 | 59 | // Set the value of the key "baz" to 42, with no expiration time 60 | // (the item won't be removed until it is re-set, or removed using 61 | // c.Delete("baz") 62 | c.Set("baz", 42, zcache.NoExpiration) 63 | 64 | // Get the string associated with the key "foo" from the cache 65 | foo, ok := c.Get("foo") 66 | if ok { 67 | fmt.Println(foo) 68 | } 69 | 70 | // Since Go is statically typed, and cache values can be anything, type 71 | // assertion is needed when values are being passed to functions that don't 72 | // take arbitrary types, (i.e. interface{}). The simplest way to do this for 73 | // values which will only be used once--e.g. for passing to another 74 | // function--is: 75 | foo, ok := c.Get("foo") 76 | if ok { 77 | MyFunction(foo.(string)) 78 | } 79 | 80 | // This gets tedious if the value is used several times in the same function. 81 | // You might do either of the following instead: 82 | if x, ok := c.Get("foo"); ok { 83 | foo := x.(string) 84 | // ... 85 | } 86 | // or 87 | var foo string 88 | if x, ok := c.Get("foo"); ok { 89 | foo = x.(string) 90 | } 91 | // ... 92 | // foo can then be passed around freely as a string 93 | 94 | // Want performance? Store pointers! 95 | c.Set("foo", &MyStruct, zcache.DefaultExpiration) 96 | if x, ok := c.Get("foo"); ok { 97 | foo := x.(*MyStruct) 98 | // ... 99 | } 100 | } 101 | ``` 102 | 103 | FAQ 104 | --- 105 | 106 | ### How can I limit the size of the cache? Is there an option for this? 107 | 108 | Not really; zcache is intended as a thread-safe `map[string]interface{}` with 109 | time-based eviction. This keeps it nice and simple. Adding something like a LRU 110 | eviction mechanism not only makes the code more complex, it also makes the 111 | library worse for cases where you just want a `map[string]interface{}` since it 112 | requires additional memory and makes some operations more expensive (unless a 113 | new API is added which make the API worse for those use cases). 114 | 115 | So unless I or someone else comes up with a way to do this which doesn't detract 116 | anything from the simple `map[string]interface{}` use case, I'd rather not add 117 | it. Perhaps wrapping `zcache.Cache` and overriding some methods could work, but 118 | I haven't looked at it. 119 | 120 | tl;dr: this isn't designed to solve every caching use case. That's a feature. 121 | -------------------------------------------------------------------------------- /bench_test.go: -------------------------------------------------------------------------------- 1 | package zcache 2 | 3 | import ( 4 | "runtime" 5 | "strconv" 6 | "sync" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func benchmarkGet(b *testing.B, exp time.Duration) { 12 | b.StopTimer() 13 | tc := New[string, any](exp, 0) 14 | tc.Set("foo", "bar") 15 | b.StartTimer() 16 | for i := 0; i < b.N; i++ { 17 | tc.Get("foo") 18 | } 19 | } 20 | 21 | func benchmarkGetConcurrent(b *testing.B, exp time.Duration) { 22 | b.StopTimer() 23 | tc := New[string, any](exp, 0) 24 | tc.Set("foo", "bar") 25 | wg := new(sync.WaitGroup) 26 | workers := runtime.NumCPU() 27 | each := b.N / workers 28 | wg.Add(workers) 29 | b.StartTimer() 30 | for i := 0; i < workers; i++ { 31 | go func() { 32 | for j := 0; j < each; j++ { 33 | tc.Get("foo") 34 | } 35 | wg.Done() 36 | }() 37 | } 38 | wg.Wait() 39 | } 40 | 41 | func benchmarkSet(b *testing.B, exp time.Duration) { 42 | b.StopTimer() 43 | tc := New[string, any](exp, 0) 44 | b.StartTimer() 45 | for i := 0; i < b.N; i++ { 46 | tc.Set("foo", "bar") 47 | } 48 | } 49 | 50 | func BenchmarkGetExpiring(b *testing.B) { benchmarkGet(b, 5*time.Minute) } 51 | func BenchmarkGetNotExpiring(b *testing.B) { benchmarkGet(b, NoExpiration) } 52 | func BenchmarkGetConcurrentExpiring(b *testing.B) { benchmarkGetConcurrent(b, 5*time.Minute) } 53 | func BenchmarkGetConcurrentNotExpiring(b *testing.B) { benchmarkGetConcurrent(b, NoExpiration) } 54 | func BenchmarkSetExpiring(b *testing.B) { benchmarkSet(b, 5*time.Minute) } 55 | func BenchmarkSetNotExpiring(b *testing.B) { benchmarkSet(b, NoExpiration) } 56 | 57 | func BenchmarkRWMutexMapGet(b *testing.B) { 58 | b.StopTimer() 59 | m := map[string]string{ 60 | "foo": "bar", 61 | } 62 | mu := sync.RWMutex{} 63 | b.StartTimer() 64 | for i := 0; i < b.N; i++ { 65 | mu.RLock() 66 | _ = m["foo"] 67 | mu.RUnlock() 68 | } 69 | } 70 | 71 | func BenchmarkRWMutexInterfaceMapGetStruct(b *testing.B) { 72 | b.StopTimer() 73 | s := struct{ name string }{name: "foo"} 74 | m := map[interface{}]string{ 75 | s: "bar", 76 | } 77 | mu := sync.RWMutex{} 78 | b.StartTimer() 79 | for i := 0; i < b.N; i++ { 80 | mu.RLock() 81 | _ = m[s] 82 | mu.RUnlock() 83 | } 84 | } 85 | 86 | func BenchmarkRWMutexInterfaceMapGetString(b *testing.B) { 87 | b.StopTimer() 88 | m := map[interface{}]string{ 89 | "foo": "bar", 90 | } 91 | mu := sync.RWMutex{} 92 | b.StartTimer() 93 | for i := 0; i < b.N; i++ { 94 | mu.RLock() 95 | _ = m["foo"] 96 | mu.RUnlock() 97 | } 98 | } 99 | 100 | func BenchmarkRWMutexMapGetConcurrent(b *testing.B) { 101 | b.StopTimer() 102 | m := map[string]string{ 103 | "foo": "bar", 104 | } 105 | mu := sync.RWMutex{} 106 | wg := new(sync.WaitGroup) 107 | workers := runtime.NumCPU() 108 | each := b.N / workers 109 | wg.Add(workers) 110 | b.StartTimer() 111 | for i := 0; i < workers; i++ { 112 | go func() { 113 | for j := 0; j < each; j++ { 114 | mu.RLock() 115 | _ = m["foo"] 116 | mu.RUnlock() 117 | } 118 | wg.Done() 119 | }() 120 | } 121 | wg.Wait() 122 | } 123 | 124 | func BenchmarkRWMutexMapSet(b *testing.B) { 125 | b.StopTimer() 126 | m := map[string]string{} 127 | mu := sync.RWMutex{} 128 | b.StartTimer() 129 | for i := 0; i < b.N; i++ { 130 | mu.Lock() 131 | m["foo"] = "bar" 132 | mu.Unlock() 133 | } 134 | } 135 | 136 | func BenchmarkCacheSetDelete(b *testing.B) { 137 | b.StopTimer() 138 | tc := New[string, any](DefaultExpiration, 0) 139 | b.StartTimer() 140 | for i := 0; i < b.N; i++ { 141 | tc.Set("foo", "bar") 142 | tc.Delete("foo") 143 | } 144 | } 145 | 146 | func BenchmarkRWMutexMapSetDelete(b *testing.B) { 147 | b.StopTimer() 148 | m := map[string]string{} 149 | mu := sync.RWMutex{} 150 | b.StartTimer() 151 | for i := 0; i < b.N; i++ { 152 | mu.Lock() 153 | m["foo"] = "bar" 154 | mu.Unlock() 155 | mu.Lock() 156 | delete(m, "foo") 157 | mu.Unlock() 158 | } 159 | } 160 | 161 | func BenchmarkCacheSetDeleteSingleLock(b *testing.B) { 162 | b.StopTimer() 163 | tc := New[string, any](DefaultExpiration, 0) 164 | b.StartTimer() 165 | for i := 0; i < b.N; i++ { 166 | tc.mu.Lock() 167 | tc.set("foo", "bar", DefaultExpiration) 168 | tc.delete("foo") 169 | tc.mu.Unlock() 170 | } 171 | } 172 | 173 | func BenchmarkRWMutexMapSetDeleteSingleLock(b *testing.B) { 174 | b.StopTimer() 175 | m := map[string]string{} 176 | mu := sync.RWMutex{} 177 | b.StartTimer() 178 | for i := 0; i < b.N; i++ { 179 | mu.Lock() 180 | m["foo"] = "bar" 181 | delete(m, "foo") 182 | mu.Unlock() 183 | } 184 | } 185 | 186 | func BenchmarkDeleteExpiredLoop(b *testing.B) { 187 | b.StopTimer() 188 | tc := New[string, any](5*time.Minute, 0) 189 | tc.mu.Lock() 190 | for i := 0; i < 100000; i++ { 191 | tc.set(strconv.Itoa(i), "bar", DefaultExpiration) 192 | } 193 | tc.mu.Unlock() 194 | b.StartTimer() 195 | for i := 0; i < b.N; i++ { 196 | tc.DeleteExpired() 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package zcache_test 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "zgo.at/zcache/v2" 8 | ) 9 | 10 | func ExampleCache_simple() { 11 | // Create a cache with a default expiration time of 5 minutes, and which 12 | // purges expired items every 10 minutes. 13 | // 14 | // This creates a cache with string keys and values, with Go 1.18 type 15 | // parameters. 16 | c := zcache.New[string, string](5*time.Minute, 10*time.Minute) 17 | 18 | // Set the value of the key "foo" to "bar", with the default expiration. 19 | c.Set("foo", "bar") 20 | 21 | // Set the value of the key "baz" to "never", with no expiration time. The 22 | // item won't be removed until it's removed with c.Delete("baz"). 23 | c.SetWithExpire("baz", "never", zcache.NoExpiration) 24 | 25 | // Get the value associated with the key "foo" from the cache; due to the 26 | // use of type parameters this is a string, and no type assertions are 27 | // needed. 28 | foo, ok := c.Get("foo") 29 | if ok { 30 | fmt.Println(foo) 31 | } 32 | 33 | // Output: bar 34 | } 35 | 36 | func ExampleCache_struct() { 37 | type MyStruct struct{ Value string } 38 | 39 | // Create a new cache that stores a specific struct. 40 | c := zcache.New[string, *MyStruct](zcache.NoExpiration, zcache.NoExpiration) 41 | c.Set("cache", &MyStruct{Value: "value"}) 42 | 43 | v, _ := c.Get("cache") 44 | fmt.Printf("%#v\n", v) 45 | 46 | // Output: &zcache_test.MyStruct{Value:"value"} 47 | } 48 | 49 | func ExampleCache_any() { 50 | // Create a new cache that stores any value, behaving similar to zcache v1 51 | // or go-cache. 52 | c := zcache.New[string, any](zcache.NoExpiration, zcache.NoExpiration) 53 | 54 | c.Set("a", "value 1") 55 | c.Set("b", 42) 56 | 57 | a, _ := c.Get("a") 58 | b, _ := c.Get("b") 59 | 60 | // This needs type assertions. 61 | p := func(a string, b int) { fmt.Println(a, b) } 62 | p(a.(string), b.(int)) 63 | 64 | // Output: value 1 42 65 | } 66 | 67 | func ExampleProxy() { 68 | type Site struct { 69 | ID int 70 | Hostname string 71 | } 72 | 73 | site := &Site{ 74 | ID: 42, 75 | Hostname: "example.com", 76 | } 77 | 78 | // Create a new site which caches by site ID (int), and a "proxy" which 79 | // caches by the hostname (string). 80 | c := zcache.New[int, *Site](zcache.NoExpiration, zcache.NoExpiration) 81 | p := zcache.NewProxy[string, int, *Site](c) 82 | 83 | p.Set(42, "example.com", site) 84 | 85 | siteByID, ok := c.Get(42) 86 | fmt.Printf("%v %v\n", ok, siteByID) 87 | 88 | siteByHost, ok := p.Get("example.com") 89 | fmt.Printf("%v %v\n", ok, siteByHost) 90 | 91 | // They're both the same object/pointer. 92 | fmt.Printf("%v\n", siteByID == siteByHost) 93 | 94 | // Output: 95 | // true &{42 example.com} 96 | // true &{42 example.com} 97 | // true 98 | } 99 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module zgo.at/zcache/v2 2 | 3 | go 1.18 4 | -------------------------------------------------------------------------------- /issue-list.markdown: -------------------------------------------------------------------------------- 1 | All PRs and issues from go-cache (excluding some fluff) and why they have or 2 | haven't been included. 3 | 4 | Included 5 | -------- 6 | 7 | This was included, although perhaps not exactly as mentioned, but the use case 8 | or issue that was reported should be resolved. 9 | 10 | 11 | - [add Expire ExpireAt command by zebozhuang](https://github.com/patrickmn/go-cache/pull/20)
12 | [Update item with same expiry by pranjal5215](https://github.com/patrickmn/go-cache/pull/42)
13 | [Add UpdateExpiration method. by dossy](https://github.com/patrickmn/go-cache/pull/66)
14 | [add GetWithExpirationUpdate by sbabiv](https://github.com/patrickmn/go-cache/pull/96)
15 | [GetWithExpirationUpdate - atomic implementation by paddlesteamer](https://github.com/patrickmn/go-cache/pull/126)
16 | [best way to extend time on an not expired object?](https://github.com/patrickmn/go-cache/issues/65)
17 | [Adding some utility functions to cache: by EVODelavega](https://github.com/patrickmn/go-cache/pull/55) 18 | 19 | Added as `Touch()`; PR 55 also added `Pop()`, which seems like a useful thing 20 | to add as well. 21 | 22 | - [Add GetPossiblyExpired() by Freeaqingme](https://github.com/patrickmn/go-cache/pull/47)
23 | [Add method to retrieve CacheItem rather than the value of the item by rahulraheja](https://github.com/patrickmn/go-cache/pull/53)
24 | [Add functionality allowing to get expired value (stale) from cache by oskarwojciski](https://github.com/patrickmn/go-cache/pull/63)
25 | [Get Expired Item](https://github.com/patrickmn/go-cache/issues/107) 26 | 27 | Get expired cache items; added as `GetStale()`. 28 | 29 | I didn't use `GetItem()` or `GetCacheItem()` as I felt that it should be clear 30 | from the name you're getting potentially expired items. 31 | 32 | Potentually, a `GetStaleWithExpiration()` could be added too; but I'm not sure 33 | how valuable that is. 34 | 35 | - [Add Iterate by youjianglong](https://github.com/patrickmn/go-cache/pull/78)
36 | [Add method for getting all cache keys by alex-ant](https://github.com/patrickmn/go-cache/pull/81)
37 | [Add method for delete by regex rule by vmpartner](https://github.com/patrickmn/go-cache/pull/100) 38 | 39 | All of them are essentially the same issue: do something with all keys. Added 40 | a `Keys()` method to return an (unsorted) list of keys. 41 | 42 | - [Add Map function (Read/Replace) in single lock](https://github.com/patrickmn/go-cache/issues/118)
43 | [added atomic list-append operation by sgeisbacher](https://github.com/patrickmn/go-cache/pull/97) 44 | 45 | Both of these issues are essentially the same: the ability to atomically 46 | modify existing values. Instead of adding a []string-specific implementation a 47 | generic Modify() seems better to me, so add that. 48 | 49 | - [Add remove method, if key exists, delete and return elements by yinbaoqiang](https://github.com/patrickmn/go-cache/pull/77)
50 | 51 | Added as Pop() 52 | 53 | 54 | Not included 55 | ------------ 56 | 57 | Issues and PRs that were *not* included with a short explanation why. You can 58 | open an issue if you feel I made a mistake and we can look at it again :-) 59 | 60 | - [Add OnMissing callback by adregner](https://github.com/patrickmn/go-cache/pull/106)
61 | [Add Memoize by clitetailor](https://github.com/patrickmn/go-cache/pull/113)
62 | [GetOrSet method to handle case for atomic get and set if not exists by technicianted](https://github.com/patrickmn/go-cache/pull/117) 63 | 64 | These all address the same problem: populate data on a cache Get() miss. 65 | 66 | The problem with a `GetOrSet(set func())`-type method is that the map will be 67 | locked while the `set` callback is running. This could be fixed by unlocking 68 | the map, but then it's no longer atomic and you need to be very careful to not 69 | spawn several `GetOrSet()`s (basically, it doesn't necessarily make things 70 | more convenient). Since a cache is useful for getting expensive-to-get data 71 | this seems like it could be a realistic problem. 72 | 73 | This is also the problem with an `OnMiss()` callback: you run the risk of 74 | spawning a bucketload of OnMiss() callbacks. I also don't especially care much 75 | for the UX of such a callback, since it's kind of a "action at a distance" 76 | thing. 77 | 78 | This could be solved with [`zsync.Once`]([zstd/once.go at master](https://github.com/zgoat/zstd/blob/master/zsync/once.go#L6)) though, 79 | then only subsequent GetOrSet calls will block. The downside is that is that 80 | keys may still be modified with Set() and other functions while this is 81 | running. I'm not sure if that's a big enough of an issue. 82 | 83 | I'm not entirely sure what the value of a simple `GetOrSet(k string, 84 | valueIfNotSet interface{})` is. If you already have the value, then why do you 85 | need this? You can just set it (or indeed, if you already have the value then 86 | why do you need a cache at all?) 87 | 88 | For now, I decided to not add it. 89 | 90 | - [what if onEvicted func very slow](https://github.com/patrickmn/go-cache/issues/49)
91 | 92 | You can start your own goroutine if you want; this can be potential 93 | tricky/dangerous as you really don't want to spawn thousands of goroutines at 94 | the same time, and it may be surprising for some. 95 | 96 | - [Feature request: add multiple get method.](https://github.com/patrickmn/go-cache/issues/108)
97 | 98 | The performance difference is not that large compared to a for loop (about 99 | 970ns/op vs 1450 ns/op for 50 items, and it adds an alloc), it's not clear how 100 | to make a consistent API for this (how do you return found? what if there are 101 | duplicate keys?), and overall I don't really think it's worth it. 102 | 103 | - [Feature request: max size and/or max objects](https://github.com/patrickmn/go-cache/issues/5)
104 | [An Unobtrusive LRU for the best time cache I've used for go by cognusion](https://github.com/patrickmn/go-cache/pull/17) 105 | 106 | See FAQ; maybe we can add this as a wrapper and new `zcache.LRUCache` or some 107 | such. Max size is even harder, since getting the size of an object is 108 | non-trivial. 109 | 110 | - [Added BST for efficient deletion by beppeben](https://github.com/patrickmn/go-cache/pull/27)
111 | 112 | Seems to solve a specific use case, but makes stuff quite a bit more complex 113 | and the performance regresses for some use cases. 114 | 115 | - [expose a flag to indicate if it was expired or removed in OnEvicted()](https://github.com/patrickmn/go-cache/issues/57)
116 | [add isExpired bool to OnEvicted callback signature by Ashtonian](https://github.com/patrickmn/go-cache/pull/58) 117 | 118 | Unclear use case; although passing the Item instead of value to OnEvicted() 119 | wouldn't be a bad idea (but incompatible). 120 | 121 | - [Add function which increase int64 or set in cache if not exists yet by oskarwojciski](https://github.com/patrickmn/go-cache/pull/62)
122 | 123 | This makes the entire increment/decrement stuff even worse; need to rethink 124 | that entire API. An option to set it if it doesn't exist would be better. 125 | 126 | - [Changing RWMutexMap to sync.Map by vidmed](https://github.com/patrickmn/go-cache/pull/72)
127 | 128 | Unclear if this is a good idea, because performance may either increase or 129 | regress. Won't include. 130 | 131 | - [Delete from the cache on Get if the item expired (to trigger onEvicted) by fdurand](https://github.com/patrickmn/go-cache/pull/75/files)
132 | 133 | Not a good idea IMO, makes Get() performance unpredictable, and can be solved 134 | by just running the janitor more often. Would also complicate the "get even if 135 | expired" functionality. 136 | 137 | - [Add a Noop cache implementation by sylr](https://github.com/patrickmn/go-cache/pull/92)
138 | [Request: Add formal interface for go-cache](https://github.com/patrickmn/go-cache/issues/116) 139 | 140 | You don't really need this; you can define your own interfaces already. 141 | Mocking out a in-memory cache with a "fake" implementation also seems like a 142 | weird thing to do. Worst part is: this will lock down the API. Can't add new 143 | functions without breaking it. 144 | Not adding it. 145 | 146 | - [Add prometheus metrics by sylr](https://github.com/patrickmn/go-cache/pull/94)
147 | 148 | This PR makes things worse for everyone who doesn't use Prometheus (i.e. most 149 | people). Clearly this is not a good idea. You can still add it as a wrapper if 150 | you want. 151 | 152 | - [Flush calls onEvicted by pavelbazika](https://github.com/patrickmn/go-cache/pull/122)
153 | 154 | This is a breaking change, since Flush() now works different. You can also 155 | already do this by getting all the items and deleting one-by-one (or getting 156 | all the items, Flush(), and calling onEvict()). 157 | 158 | - [The OnEvicted function is not called if a value is re-set after expiration but before deletion](https://github.com/patrickmn/go-cache/issues/48)
159 | 160 | I'm not so sure this is actually a bug, as you're overwriting values. 161 | 162 | - [Allow querying of expiration, cleanup durations](https://github.com/patrickmn/go-cache/issues/104)
163 | 164 | You can already get a list of items with Items(); so not sure what the use 165 | case is here? Not clear enough to do anything with as it stands. 166 | 167 | - [Implemented a faster version of the Size() function](https://github.com/patrickmn/go-cache/pull/129)
168 | 169 | Because this only counts primitives (and not maps, structs, slices) it's very 170 | limited. This kind of stuff is out-of-scope for v1 anyway. 171 | -------------------------------------------------------------------------------- /proxy.go: -------------------------------------------------------------------------------- 1 | package zcache 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | // Proxy a cache, allowing access to the same cache entries with different keys. 8 | // 9 | // This is useful if you want to keep a cache which may be accessed by different 10 | // keys in various different code paths. For example, a "site" may be accessed 11 | // by ID or by CNAME. Proxy keys can have a different type than cache keys. 12 | // 13 | // Proxy keys don't have an expiry and are never automatically deleted, the 14 | // logic being that the same "proxy → key" mapping should always be valid. The 15 | // items in the underlying cache can still be expired or deleted, and you can 16 | // still manually call Delete() or Reset(). 17 | type Proxy[ProxyK, MainK comparable, V any] struct { 18 | cache *Cache[MainK, V] 19 | mu sync.RWMutex 20 | m map[ProxyK]MainK 21 | } 22 | 23 | // NewProxy creates a new proxied cache. 24 | func NewProxy[ProxyK, MainK comparable, V any](c *Cache[MainK, V]) *Proxy[ProxyK, MainK, V] { 25 | return &Proxy[ProxyK, MainK, V]{cache: c, m: make(map[ProxyK]MainK)} 26 | } 27 | 28 | // Proxy items from "proxyKey" to "mainKey". 29 | func (p *Proxy[ProxyK, MainK, V]) Proxy(mainKey MainK, proxyKey ProxyK) { 30 | p.mu.Lock() 31 | defer p.mu.Unlock() 32 | p.m[proxyKey] = mainKey 33 | } 34 | 35 | // Delete stops proxying "proxyKey" to "mainKey". 36 | // 37 | // This only removes the proxy link, not the entry from the main cache. 38 | func (p *Proxy[ProxyK, MainK, V]) Delete(proxyKey ProxyK) { 39 | p.mu.Lock() 40 | defer p.mu.Unlock() 41 | delete(p.m, proxyKey) 42 | } 43 | 44 | // Reset removes all proxied keys (but not the underlying cache). 45 | func (p *Proxy[ProxyK, MainK, V]) Reset() { 46 | p.mu.Lock() 47 | defer p.mu.Unlock() 48 | p.m = make(map[ProxyK]MainK) 49 | } 50 | 51 | // Key gets the main key for this proxied entry, if it exist. 52 | // 53 | // The boolean value indicates if this proxy key is set. 54 | func (p *Proxy[ProxyK, MainK, V]) Key(proxyKey ProxyK) (MainK, bool) { 55 | p.mu.RLock() 56 | defer p.mu.RUnlock() 57 | mainKey, ok := p.m[proxyKey] 58 | return mainKey, ok 59 | } 60 | 61 | // Cache gets the associated cache. 62 | func (p *Proxy[ProxyK, MainK, V]) Cache() *Cache[MainK, V] { 63 | return p.cache 64 | } 65 | 66 | // Set a new item in the main cache with the key mainKey, and proxy to that with 67 | // proxyKey. 68 | // 69 | // This behaves like zcache.Cache.Set() otherwise. 70 | func (p *Proxy[ProxyK, MainK, V]) Set(mainKey MainK, proxyKey ProxyK, v V) { 71 | p.mu.Lock() 72 | p.m[proxyKey] = mainKey 73 | p.mu.Unlock() 74 | p.cache.Set(mainKey, v) 75 | } 76 | 77 | // Get a proxied cache item with zcache.Cache.Get() 78 | func (p *Proxy[ProxyK, MainK, V]) Get(proxyKey ProxyK) (V, bool) { 79 | p.mu.RLock() 80 | mainKey, ok := p.m[proxyKey] 81 | if !ok { 82 | p.mu.RUnlock() 83 | return p.cache.zero(), false 84 | } 85 | p.mu.RUnlock() 86 | 87 | return p.cache.Get(mainKey) 88 | } 89 | 90 | // Items gets all items in this proxy, as proxyKey → mainKey 91 | func (p *Proxy[ProxyK, MainK, V]) Items() map[ProxyK]MainK { 92 | p.mu.RLock() 93 | defer p.mu.RUnlock() 94 | 95 | m := make(map[ProxyK]MainK, len(p.m)) 96 | for k, v := range p.m { 97 | m[k] = v 98 | } 99 | return m 100 | } 101 | -------------------------------------------------------------------------------- /proxy_test.go: -------------------------------------------------------------------------------- 1 | package zcache 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestProxy(t *testing.T) { 9 | c := New[string, any](NoExpiration, 0) 10 | pc := NewProxy[string, string, any](c) 11 | 12 | has := func(v any, ok bool) { 13 | t.Helper() 14 | if !ok { 15 | t.Error("ok false") 16 | } 17 | if v != "vvv" { 18 | t.Errorf("value wrong: %q", v) 19 | } 20 | } 21 | not := func(v interface{}, ok bool) { 22 | t.Helper() 23 | if ok { 24 | t.Error("ok true") 25 | } 26 | if v != nil { 27 | t.Errorf("value not nil: %q", v) 28 | } 29 | } 30 | 31 | c.Set("k", "vvv") 32 | pc.Proxy("k", "p") 33 | has(pc.Get("p")) 34 | not(pc.Get("k")) 35 | 36 | pc.Delete("k") 37 | has(pc.Get("p")) 38 | pc.Delete("p") 39 | not(pc.Get("p")) 40 | 41 | pc.Set("main", "proxy", "vvv") 42 | has(pc.Get("proxy")) 43 | not(pc.Get("main")) 44 | 45 | if !reflect.DeepEqual(pc.Items(), map[string]string{"proxy": "main"}) { 46 | t.Error() 47 | } 48 | 49 | if k, ok := pc.Key("adsasdasd"); k != "" || ok != false { 50 | t.Error() 51 | } 52 | 53 | if k, ok := pc.Key("proxy"); k != "main" || ok != true { 54 | t.Error() 55 | } 56 | 57 | if pc.Cache() != c { 58 | t.Error() 59 | } 60 | 61 | pc.Reset() 62 | if len(pc.m) != 0 { 63 | t.Error() 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /zcache.go: -------------------------------------------------------------------------------- 1 | // Package zcache is an in-memory key:value store/cache with time-based evictions. 2 | // 3 | // It is suitable for applications running on a single machine. Its major 4 | // advantage is that it's essentially a thread-safe map with expiration times. 5 | // Any object can be stored, for a given duration or forever, and the cache can 6 | // be safely used by multiple goroutines. 7 | // 8 | // Although zcache isn't meant to be used as a persistent datastore, the 9 | // contents can be saved to and loaded from a file (using `c.Items()` to 10 | // retrieve the items map to serialize, and `NewFrom()` to create a cache from a 11 | // deserialized one) to recover from downtime quickly. 12 | package zcache 13 | 14 | import ( 15 | "fmt" 16 | "runtime" 17 | "sync" 18 | "time" 19 | ) 20 | 21 | const ( 22 | // NoExpiration indicates a cache item never expires. 23 | NoExpiration time.Duration = -1 24 | 25 | // DefaultExpiration indicates to use the cache default expiration time. 26 | // Equivalent to passing in the same expiration duration as was given to 27 | // New() or NewFrom() when the cache was created (e.g. 5 minutes.) 28 | DefaultExpiration time.Duration = 0 29 | ) 30 | 31 | type ( 32 | // Cache is a thread-safe in-memory key/value store. 33 | Cache[K comparable, V any] struct { 34 | *cache[K, V] // If this is confusing, see the comment at newCacheWithJanitor() 35 | } 36 | 37 | cache[K comparable, V any] struct { 38 | defaultExpiration time.Duration 39 | items map[K]Item[V] 40 | mu sync.RWMutex 41 | onEvicted func(K, V) 42 | janitor *janitor[K, V] 43 | } 44 | 45 | // Item stored in the cache; it holds the value and the expiration time as 46 | // timestamp. 47 | Item[V any] struct { 48 | Object V 49 | Expiration int64 50 | } 51 | ) 52 | 53 | // New creates a new cache with a given expiration duration and cleanup 54 | // interval. 55 | // 56 | // If the expiration duration is less than 1 (or NoExpiration) the items in the 57 | // cache never expire (by default) and must be deleted manually. 58 | // 59 | // If the cleanup interval is less than 1 expired items are not deleted from the 60 | // cache before calling c.DeleteExpired(). 61 | func New[K comparable, V any](defaultExpiration, cleanupInterval time.Duration) *Cache[K, V] { 62 | return newCacheWithJanitor(defaultExpiration, cleanupInterval, make(map[K]Item[V])) 63 | } 64 | 65 | // NewFrom creates a new cache like New() and populates the cache with the given 66 | // items. 67 | // 68 | // The passed map will serve as the underlying map for the cache. This is useful 69 | // for starting from a deserialized cache (serialized using e.g. gob.Encode() on 70 | // c.Items()), or passing in e.g. make(map[string]Item, 500) to improve startup 71 | // performance when the cache is expected to reach a certain minimum size. 72 | // 73 | // The map is *not* copied and only the cache's methods synchronize access to this 74 | // map, so it is not recommended to keep any references to the map around after 75 | // creating a cache. If need be, the map can be accessed at a later point using 76 | // c.Items() (which creates a copy of the map). 77 | // 78 | // Note regarding serialization: When using e.g. gob, make sure to 79 | // gob.Register() the individual types stored in the cache before encoding a map 80 | // retrieved with c.Items() and to register those same types before decoding a 81 | // blob containing an items map. 82 | func NewFrom[K comparable, V any](defaultExpiration, cleanupInterval time.Duration, items map[K]Item[V]) *Cache[K, V] { 83 | return newCacheWithJanitor(defaultExpiration, cleanupInterval, items) 84 | } 85 | 86 | func newCache[K comparable, V any](de time.Duration, m map[K]Item[V]) *cache[K, V] { 87 | if de == 0 { 88 | de = -1 89 | } 90 | c := &cache[K, V]{ 91 | defaultExpiration: de, 92 | items: m, 93 | } 94 | return c 95 | } 96 | 97 | func newCacheWithJanitor[K comparable, V any](de time.Duration, ci time.Duration, m map[K]Item[V]) *Cache[K, V] { 98 | c := newCache(de, m) 99 | // This trick ensures that the janitor goroutine (which is running 100 | // DeleteExpired on c forever) does not keep the returned C object from 101 | // being garbage collected. When it is garbage collected, the finalizer 102 | // stops the janitor goroutine, after which c can be collected. 103 | C := &Cache[K, V]{c} 104 | if ci > 0 { 105 | runJanitor(c, ci) 106 | runtime.SetFinalizer(C, stopJanitor[K, V]) 107 | } 108 | return C 109 | } 110 | 111 | // Set a cache item, replacing any existing item. 112 | func (c *cache[K, V]) Set(k K, v V) { c.SetWithExpire(k, v, DefaultExpiration) } 113 | 114 | // Touch replaces the expiry of a key with the default expiration and returns 115 | // the current value, if any. 116 | // 117 | // The boolean return value indicates if this item was set. 118 | func (c *cache[K, V]) Touch(k K) (V, bool) { return c.TouchWithExpire(k, DefaultExpiration) } 119 | 120 | // Add an item to the cache only if it doesn't exist yet or if it has expired. 121 | // 122 | // It will return an error if the cache key already exists. 123 | func (c *cache[K, V]) Add(k K, v V) error { return c.AddWithExpire(k, v, DefaultExpiration) } 124 | 125 | // Replace sets a new value for the key only if it already exists and isn't 126 | // expired. 127 | // 128 | // It will return an error if the cache key doesn't exist. 129 | func (c *cache[K, V]) Replace(k K, v V) error { return c.ReplaceWithExpire(k, v, DefaultExpiration) } 130 | 131 | // SetWithExpire sets a cache item, replacing any existing item. 132 | // 133 | // If the duration is 0 (DefaultExpiration), the cache's default expiration time 134 | // is used. If it is -1 (NoExpiration), the item never expires. 135 | func (c *cache[K, V]) SetWithExpire(k K, v V, d time.Duration) { 136 | // "Inlining" of set 137 | var e int64 138 | if d == DefaultExpiration { 139 | d = c.defaultExpiration 140 | } 141 | if d > 0 { 142 | e = time.Now().Add(d).UnixNano() 143 | } 144 | c.mu.Lock() 145 | defer c.mu.Unlock() 146 | c.items[k] = Item[V]{ 147 | Object: v, 148 | Expiration: e, 149 | } 150 | } 151 | 152 | // TouchWithExpire replaces the expiry of a key and returns the current value, if any. 153 | // 154 | // The boolean return value indicates if this item was set. If the duration is 0 155 | // (DefaultExpiration), the cache's default expiration time is used. If it is -1 156 | // (NoExpiration), the item never expires. 157 | func (c *cache[K, V]) TouchWithExpire(k K, d time.Duration) (V, bool) { 158 | if d == DefaultExpiration { 159 | d = c.defaultExpiration 160 | } 161 | 162 | c.mu.Lock() 163 | defer c.mu.Unlock() 164 | 165 | item, ok := c.items[k] 166 | if !ok { 167 | return c.zero(), false 168 | } 169 | 170 | item.Expiration = time.Now().Add(d).UnixNano() 171 | c.items[k] = item 172 | return item.Object, true 173 | } 174 | 175 | // AddWithExpire adds an item to the cache only if it doesn't exist yet, or if 176 | // it has expired. 177 | // 178 | // It will return an error if the cache key already exists. If the duration is 0 179 | // (DefaultExpiration), the cache's default expiration time is used. If it is -1 180 | // (NoExpiration), the item never expires. 181 | func (c *cache[K, V]) AddWithExpire(k K, v V, d time.Duration) error { 182 | c.mu.Lock() 183 | defer c.mu.Unlock() 184 | 185 | _, ok := c.get(k) 186 | if ok { 187 | return fmt.Errorf("zcache.Add: item %v already exists", k) 188 | } 189 | c.set(k, v, d) 190 | return nil 191 | } 192 | 193 | // ReplaceWithExpire sets a new value for the key only if it already exists and isn't 194 | // expired. 195 | // 196 | // It will return an error if the cache key doesn't exist. If the duration is 0 197 | // (DefaultExpiration), the cache's default expiration time is used. If it is -1 198 | // (NoExpiration), the item never expires. 199 | func (c *cache[K, V]) ReplaceWithExpire(k K, v V, d time.Duration) error { 200 | c.mu.Lock() 201 | defer c.mu.Unlock() 202 | 203 | _, ok := c.get(k) 204 | if !ok { 205 | return fmt.Errorf("zcache.Replace: item %v doesn't exist", k) 206 | } 207 | c.set(k, v, d) 208 | return nil 209 | } 210 | 211 | // Get an item from the cache. 212 | // 213 | // Returns the item or the zero value and a bool indicating whether the key is 214 | // set. 215 | func (c *cache[K, V]) Get(k K) (V, bool) { 216 | c.mu.RLock() 217 | defer c.mu.RUnlock() 218 | 219 | // "Inlining" of get and Expired 220 | item, ok := c.items[k] 221 | if !ok { 222 | return c.zero(), false 223 | } 224 | if item.Expiration > 0 && time.Now().UnixNano() > item.Expiration { 225 | return c.zero(), false 226 | } 227 | return item.Object, true 228 | } 229 | 230 | // GetStale gets an item from the cache without checking if it's expired. 231 | // 232 | // Returns the item or the zero value and a bool indicating whether the key was 233 | // expired and a bool indicating whether the key was set. 234 | func (c *cache[K, V]) GetStale(k K) (v V, expired bool, ok bool) { 235 | c.mu.RLock() 236 | defer c.mu.RUnlock() 237 | 238 | // "Inlining" of get and Expired 239 | item, ok := c.items[k] 240 | if !ok { 241 | return c.zero(), false, false 242 | } 243 | return item.Object, 244 | item.Expiration > 0 && time.Now().UnixNano() > item.Expiration, 245 | true 246 | } 247 | 248 | // GetWithExpire returns an item and its expiration time from the cache. 249 | // 250 | // It returns the item or the zero value, the expiration time if one is set (if 251 | // the item never expires a zero value for time.Time is returned), and a bool 252 | // indicating whether the key was set. 253 | func (c *cache[K, V]) GetWithExpire(k K) (V, time.Time, bool) { 254 | c.mu.RLock() 255 | defer c.mu.RUnlock() 256 | 257 | // "Inlining" of get and Expired 258 | item, ok := c.items[k] 259 | if !ok { 260 | return c.zero(), time.Time{}, false 261 | } 262 | 263 | if item.Expiration > 0 { 264 | if time.Now().UnixNano() > item.Expiration { 265 | return c.zero(), time.Time{}, false 266 | } 267 | 268 | // Return the item and the expiration time 269 | return item.Object, time.Unix(0, item.Expiration), true 270 | } 271 | 272 | // If expiration <= 0 (i.e. no expiration time set) then return the item 273 | // and a zeroed time.Time 274 | return item.Object, time.Time{}, true 275 | } 276 | 277 | // Modify the value of an existing key. 278 | // 279 | // This is thread-safe; for example to increment a number: 280 | // 281 | // cache.Modify("one", func(v int) int { return v + 1 }) 282 | // 283 | // Or setting a map key: 284 | // 285 | // cache.Modify("key", func(v map[string]string) map[string]string { 286 | // v["k"] = "v" 287 | // return v 288 | // }) 289 | // 290 | // This is thread-safe and can be safely run by multiple goroutines modifying 291 | // the same key. If you would use Get() + Set() then two goroutines may Get() 292 | // the same value and the modification of one of them will be lost. 293 | // 294 | // This is not run for keys that are not set yet; the boolean return indicates 295 | // if the key was set and if the function was applied. 296 | func (c *cache[K, V]) Modify(k K, f func(V) V) (V, bool) { 297 | c.mu.Lock() 298 | defer c.mu.Unlock() 299 | 300 | // "Inlining" of get and Expired 301 | item, ok := c.items[k] 302 | if !ok { 303 | return c.zero(), false 304 | } 305 | if item.Expiration > 0 && time.Now().UnixNano() > item.Expiration { 306 | return c.zero(), false 307 | } 308 | 309 | item.Object = f(item.Object) 310 | c.items[k] = item 311 | return item.Object, true 312 | } 313 | 314 | // Delete an item from the cache. Does nothing if the key is not in the cache. 315 | func (c *cache[K, V]) Delete(k K) { 316 | c.mu.Lock() 317 | v, evicted := c.delete(k) 318 | c.mu.Unlock() 319 | if evicted { 320 | c.onEvicted(k, v) 321 | } 322 | } 323 | 324 | // Rename a key; the value and expiry will be left untouched; onEvicted will not 325 | // be called. 326 | // 327 | // Existing keys will be overwritten; returns false is the src key doesn't 328 | // exist. 329 | func (c *cache[K, V]) Rename(src, dst K) bool { 330 | c.mu.Lock() 331 | defer c.mu.Unlock() 332 | 333 | // "Inlining" of get and Expired 334 | item, ok := c.items[src] 335 | if !ok { 336 | return false 337 | } 338 | if item.Expiration > 0 && time.Now().UnixNano() > item.Expiration { 339 | return false 340 | } 341 | 342 | delete(c.items, src) 343 | c.items[dst] = item 344 | return true 345 | } 346 | 347 | // Pop gets an item from the cache and deletes it. 348 | // 349 | // The bool return indicates if the item was set. 350 | func (c *cache[K, V]) Pop(k K) (V, bool) { 351 | c.mu.Lock() 352 | 353 | // "Inlining" of get and Expired 354 | item, ok := c.items[k] 355 | if !ok { 356 | c.mu.Unlock() 357 | return c.zero(), false 358 | } 359 | if item.Expiration > 0 && time.Now().UnixNano() > item.Expiration { 360 | c.mu.Unlock() 361 | return c.zero(), false 362 | } 363 | 364 | v, evicted := c.delete(k) 365 | c.mu.Unlock() 366 | if evicted { 367 | c.onEvicted(k, v) 368 | } 369 | 370 | return item.Object, true 371 | } 372 | 373 | // DeleteExpired deletes all expired items from the cache. 374 | func (c *cache[K, V]) DeleteExpired() { 375 | var evictedItems []keyAndValue[K, V] 376 | now := time.Now().UnixNano() 377 | c.mu.Lock() 378 | 379 | for k, v := range c.items { 380 | // "Inlining" of expired 381 | if v.Expiration > 0 && now > v.Expiration { 382 | ov, evicted := c.delete(k) 383 | if evicted { 384 | evictedItems = append(evictedItems, keyAndValue[K, V]{k, ov}) 385 | } 386 | } 387 | } 388 | c.mu.Unlock() 389 | for _, v := range evictedItems { 390 | c.onEvicted(v.key, v.value) 391 | } 392 | } 393 | 394 | // OnEvicted sets an function to call when an item is evicted from the cache. 395 | // 396 | // The function is run with the key and value. This is also run when a cache 397 | // item is is deleted manually, but *not* when it is overwritten. 398 | // 399 | // Can be set to nil to disable it (the default). 400 | func (c *cache[K, V]) OnEvicted(f func(K, V)) { 401 | c.mu.Lock() 402 | defer c.mu.Unlock() 403 | c.onEvicted = f 404 | } 405 | 406 | // Items returns a copy of all unexpired items in the cache. 407 | func (c *cache[K, V]) Items() map[K]Item[V] { 408 | c.mu.RLock() 409 | defer c.mu.RUnlock() 410 | 411 | m := make(map[K]Item[V], len(c.items)) 412 | now := time.Now().UnixNano() 413 | for k, v := range c.items { 414 | // "Inlining" of Expired 415 | if v.Expiration > 0 && now > v.Expiration { 416 | continue 417 | } 418 | m[k] = v 419 | } 420 | return m 421 | } 422 | 423 | // Keys gets a list of all keys, in no particular order. 424 | func (c *cache[K, V]) Keys() []K { 425 | c.mu.RLock() 426 | defer c.mu.RUnlock() 427 | 428 | keys := make([]K, 0, len(c.items)) 429 | now := time.Now().UnixNano() 430 | for k, v := range c.items { 431 | // "Inlining" of Expired 432 | if v.Expiration > 0 && now > v.Expiration { 433 | continue 434 | } 435 | keys = append(keys, k) 436 | } 437 | return keys 438 | } 439 | 440 | // ItemCount returns the number of items in the cache. 441 | // 442 | // This may include items that have expired but have not yet been cleaned up. 443 | func (c *cache[K, V]) ItemCount() int { 444 | c.mu.RLock() 445 | defer c.mu.RUnlock() 446 | return len(c.items) 447 | } 448 | 449 | // Reset deletes all items from the cache without calling OnEvicted. 450 | func (c *cache[K, V]) Reset() { 451 | c.mu.Lock() 452 | defer c.mu.Unlock() 453 | c.items = map[K]Item[V]{} 454 | } 455 | 456 | // DeleteAll deletes all items from the cache and returns them. 457 | // 458 | // This calls OnEvicted for returned items. 459 | func (c *cache[K, V]) DeleteAll() map[K]Item[V] { 460 | c.mu.Lock() 461 | items := c.items 462 | c.items = map[K]Item[V]{} 463 | c.mu.Unlock() 464 | 465 | if c.onEvicted != nil { 466 | for k, v := range items { 467 | c.onEvicted(k, v.Object) 468 | } 469 | } 470 | 471 | return items 472 | } 473 | 474 | // DeleteFunc deletes and returns cache items matched by the filter function. 475 | // 476 | // The item will be deleted if the callback's first return argument is true. The 477 | // loop will stop if the second return argument is true. 478 | // 479 | // OnEvicted is called for deleted items. 480 | func (c *cache[K, V]) DeleteFunc(filter func(key K, item Item[V]) (del, stop bool)) map[K]Item[V] { 481 | c.mu.Lock() 482 | m := map[K]Item[V]{} 483 | for k, v := range c.items { 484 | del, stop := filter(k, v) 485 | if del { 486 | m[k] = Item[V]{ 487 | Object: v.Object, 488 | Expiration: v.Expiration, 489 | } 490 | c.delete(k) 491 | } 492 | if stop { 493 | break 494 | } 495 | } 496 | c.mu.Unlock() 497 | 498 | if c.onEvicted != nil { 499 | for k, v := range m { 500 | c.onEvicted(k, v.Object) 501 | } 502 | } 503 | 504 | return m 505 | } 506 | 507 | func (c *cache[K, V]) set(k K, v V, d time.Duration) { 508 | var e int64 509 | if d == DefaultExpiration { 510 | d = c.defaultExpiration 511 | } 512 | if d > 0 { 513 | e = time.Now().Add(d).UnixNano() 514 | } 515 | c.items[k] = Item[V]{ 516 | Object: v, 517 | Expiration: e, 518 | } 519 | } 520 | 521 | func (c *cache[K, V]) get(k K) (V, bool) { 522 | item, ok := c.items[k] 523 | if !ok { 524 | return c.zero(), false 525 | } 526 | // "Inlining" of Expired 527 | if item.Expiration > 0 && time.Now().UnixNano() > item.Expiration { 528 | return c.zero(), false 529 | } 530 | return item.Object, true 531 | } 532 | 533 | func (c *cache[K, V]) delete(k K) (V, bool) { 534 | if c.onEvicted != nil { 535 | if v, ok := c.items[k]; ok { 536 | delete(c.items, k) 537 | return v.Object, true 538 | } 539 | } 540 | delete(c.items, k) 541 | 542 | return c.zero(), false 543 | } 544 | 545 | func (c *cache[K, V]) zero() V { 546 | var zeroValue V 547 | return zeroValue 548 | } 549 | 550 | type keyAndValue[K comparable, V any] struct { 551 | key K 552 | value V 553 | } 554 | 555 | type janitor[K comparable, V any] struct { 556 | Interval time.Duration 557 | stop chan bool 558 | } 559 | 560 | func (j *janitor[K, V]) run(c *cache[K, V]) { 561 | ticker := time.NewTicker(j.Interval) 562 | for { 563 | select { 564 | case <-ticker.C: 565 | c.DeleteExpired() 566 | case <-j.stop: 567 | ticker.Stop() 568 | return 569 | } 570 | } 571 | } 572 | 573 | func stopJanitor[K comparable, V any](c *Cache[K, V]) { 574 | c.janitor.stop <- true 575 | } 576 | 577 | func runJanitor[K comparable, V any](c *cache[K, V], ci time.Duration) { 578 | j := &janitor[K, V]{ 579 | Interval: ci, 580 | stop: make(chan bool), 581 | } 582 | c.janitor = j 583 | go j.run(c) 584 | } 585 | -------------------------------------------------------------------------------- /zcache_test.go: -------------------------------------------------------------------------------- 1 | package zcache 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "reflect" 7 | "runtime" 8 | "sort" 9 | "sync" 10 | "testing" 11 | "time" 12 | ) 13 | 14 | func wantKeys(t *testing.T, tc *Cache[string, any], want []string, dontWant []string) { 15 | t.Helper() 16 | 17 | for _, k := range want { 18 | _, ok := tc.Get(k) 19 | if !ok { 20 | t.Errorf("key not found: %q", k) 21 | } 22 | } 23 | 24 | for _, k := range dontWant { 25 | v, ok := tc.Get(k) 26 | if ok { 27 | t.Errorf("key %q found with value %v", k, v) 28 | } 29 | if v != nil { 30 | t.Error("v is not nil:", v) 31 | } 32 | } 33 | } 34 | 35 | func TestCache(t *testing.T) { 36 | tc := New[string, any](DefaultExpiration, 0) 37 | 38 | a, found := tc.Get("a") 39 | if found || a != nil { 40 | t.Error("Getting A found value that shouldn't exist:", a) 41 | } 42 | 43 | b, found := tc.Get("b") 44 | if found || b != nil { 45 | t.Error("Getting B found value that shouldn't exist:", b) 46 | } 47 | 48 | c, found := tc.Get("c") 49 | if found || c != nil { 50 | t.Error("Getting C found value that shouldn't exist:", c) 51 | } 52 | 53 | tc.Set("a", 1) 54 | tc.Set("b", "b") 55 | tc.Set("c", 3.5) 56 | 57 | v, found := tc.Get("a") 58 | if !found { 59 | t.Error("a was not found while getting a2") 60 | } 61 | if v == nil { 62 | t.Error("v for a is nil") 63 | } else if a2 := v.(int); a2+2 != 3 { 64 | t.Error("a2 (which should be 1) plus 2 does not equal 3; value:", a2) 65 | } 66 | 67 | v, found = tc.Get("b") 68 | if !found { 69 | t.Error("b was not found while getting b2") 70 | } 71 | if v == nil { 72 | t.Error("v for b is nil") 73 | } else if b2 := v.(string); b2+"B" != "bB" { 74 | t.Error("b2 (which should be b) plus B does not equal bB; value:", b2) 75 | } 76 | 77 | v, found = tc.Get("c") 78 | if !found { 79 | t.Error("c was not found while getting c2") 80 | } 81 | if v == nil { 82 | t.Error("v for c is nil") 83 | } else if c2 := v.(float64); c2+1.2 != 4.7 { 84 | t.Error("c2 (which should be 3.5) plus 1.2 does not equal 4.7; value:", c2) 85 | } 86 | } 87 | 88 | func TestCacheTimes(t *testing.T) { 89 | var found bool 90 | 91 | tc := New[string, int](50*time.Millisecond, 1*time.Millisecond) 92 | tc.Set("a", 1) 93 | tc.SetWithExpire("b", 2, NoExpiration) 94 | tc.SetWithExpire("c", 3, 20*time.Millisecond) 95 | tc.SetWithExpire("d", 4, 70*time.Millisecond) 96 | 97 | <-time.After(25 * time.Millisecond) 98 | _, found = tc.Get("c") 99 | if found { 100 | t.Error("Found c when it should have been automatically deleted") 101 | } 102 | 103 | <-time.After(30 * time.Millisecond) 104 | _, found = tc.Get("a") 105 | if found { 106 | t.Error("Found a when it should have been automatically deleted") 107 | } 108 | 109 | _, found = tc.Get("b") 110 | if !found { 111 | t.Error("Did not find b even though it was set to never expire") 112 | } 113 | 114 | _, found = tc.Get("d") 115 | if !found { 116 | t.Error("Did not find d even though it was set to expire later than the default") 117 | } 118 | 119 | <-time.After(20 * time.Millisecond) 120 | _, found = tc.Get("d") 121 | if found { 122 | t.Error("Found d when it should have been automatically deleted (later than the default)") 123 | } 124 | } 125 | 126 | func TestNewFrom(t *testing.T) { 127 | m := map[string]Item[int]{ 128 | "a": { 129 | Object: 1, 130 | Expiration: 0, 131 | }, 132 | "b": { 133 | Object: 2, 134 | Expiration: 0, 135 | }, 136 | } 137 | tc := NewFrom[string, int](DefaultExpiration, 0, m) 138 | a, found := tc.Get("a") 139 | if !found { 140 | t.Fatal("Did not find a") 141 | } 142 | if a != 1 { 143 | t.Fatal("a is not 1") 144 | } 145 | b, found := tc.Get("b") 146 | if !found { 147 | t.Fatal("Did not find b") 148 | } 149 | if b != 2 { 150 | t.Fatal("b is not 2") 151 | } 152 | } 153 | 154 | func TestStorePointerToStruct(t *testing.T) { 155 | type TestStruct struct { 156 | Num int 157 | Children []*TestStruct 158 | } 159 | 160 | tc := New[string, any](DefaultExpiration, 0) 161 | tc.Set("foo", &TestStruct{Num: 1}) 162 | v, found := tc.Get("foo") 163 | if !found { 164 | t.Fatal("*TestStruct was not found for foo") 165 | } 166 | foo := v.(*TestStruct) 167 | foo.Num++ 168 | 169 | y, found := tc.Get("foo") 170 | if !found { 171 | t.Fatal("*TestStruct was not found for foo (second time)") 172 | } 173 | bar := y.(*TestStruct) 174 | if bar.Num != 2 { 175 | t.Fatal("TestStruct.Num is not 2") 176 | } 177 | } 178 | 179 | func TestOnEvicted(t *testing.T) { 180 | tc := New[string, int](DefaultExpiration, 0) 181 | tc.Set("foo", 3) 182 | if tc.onEvicted != nil { 183 | t.Fatal("tc.onEvicted is not nil") 184 | } 185 | works := false 186 | tc.OnEvicted(func(k string, v int) { 187 | if k == "foo" && v == 3 { 188 | works = true 189 | } 190 | tc.Set("bar", 4) 191 | }) 192 | tc.Delete("foo") 193 | v, _ := tc.Get("bar") 194 | if !works { 195 | t.Error("works bool not true") 196 | } 197 | if v != 4 { 198 | t.Error("bar was not 4") 199 | } 200 | } 201 | 202 | func TestTouch(t *testing.T) { 203 | tc := New[string, string](DefaultExpiration, 0) 204 | 205 | tc.SetWithExpire("a", "b", 5*time.Second) 206 | _, first, _ := tc.GetWithExpire("a") 207 | v, ok := tc.TouchWithExpire("a", 10*time.Second) 208 | if !ok { 209 | t.Fatal("!ok") 210 | } 211 | _, second, _ := tc.GetWithExpire("a") 212 | if v != "b" { 213 | t.Error("wrong value") 214 | } 215 | if first.Equal(second) { 216 | t.Errorf("not updated\nfirst: %s\nsecond: %s", first, second) 217 | } 218 | } 219 | 220 | func TestGetWithExpire(t *testing.T) { 221 | tc := New[string, any](DefaultExpiration, 0) 222 | 223 | a, expiration, ok := tc.GetWithExpire("a") 224 | if ok || a != nil || !expiration.IsZero() { 225 | t.Error("Getting A found value that shouldn't exist:", a) 226 | } 227 | 228 | b, expiration, ok := tc.GetWithExpire("b") 229 | if ok || b != nil || !expiration.IsZero() { 230 | t.Error("Getting B found value that shouldn't exist:", b) 231 | } 232 | 233 | c, expiration, ok := tc.GetWithExpire("c") 234 | if ok || c != nil || !expiration.IsZero() { 235 | t.Error("Getting C found value that shouldn't exist:", c) 236 | } 237 | 238 | tc.Set("a", 1) 239 | tc.Set("b", "b") 240 | tc.Set("c", 3.5) 241 | tc.SetWithExpire("d", 1, NoExpiration) 242 | tc.SetWithExpire("e", 1, 50*time.Millisecond) 243 | 244 | v, expiration, ok := tc.GetWithExpire("a") 245 | if !ok { 246 | t.Error("a was not found while getting a2") 247 | } 248 | if v == nil { 249 | t.Error("v for a is nil") 250 | } else if a2 := v.(int); a2+2 != 3 { 251 | t.Error("a2 (which should be 1) plus 2 does not equal 3; value:", a2) 252 | } 253 | if !expiration.IsZero() { 254 | t.Error("expiration for a is not a zeroed time") 255 | } 256 | 257 | v, expiration, ok = tc.GetWithExpire("b") 258 | if !ok { 259 | t.Error("b was not found while getting b2") 260 | } 261 | if v == nil { 262 | t.Error("v for b is nil") 263 | } else if b2 := v.(string); b2+"B" != "bB" { 264 | t.Error("b2 (which should be b) plus B does not equal bB; value:", b2) 265 | } 266 | if !expiration.IsZero() { 267 | t.Error("expiration for b is not a zeroed time") 268 | } 269 | 270 | v, expiration, ok = tc.GetWithExpire("c") 271 | if !ok { 272 | t.Error("c was not found while getting c2") 273 | } 274 | if v == nil { 275 | t.Error("v for c is nil") 276 | } else if c2 := v.(float64); c2+1.2 != 4.7 { 277 | t.Error("c2 (which should be 3.5) plus 1.2 does not equal 4.7; value:", c2) 278 | } 279 | if !expiration.IsZero() { 280 | t.Error("expiration for c is not a zeroed time") 281 | } 282 | 283 | v, expiration, ok = tc.GetWithExpire("d") 284 | if !ok { 285 | t.Error("d was not found while getting d2") 286 | } 287 | if v == nil { 288 | t.Error("v for d is nil") 289 | } else if d2 := v.(int); d2+2 != 3 { 290 | t.Error("d (which should be 1) plus 2 does not equal 3; value:", d2) 291 | } 292 | if !expiration.IsZero() { 293 | t.Error("expiration for d is not a zeroed time") 294 | } 295 | 296 | v, expiration, ok = tc.GetWithExpire("e") 297 | if !ok { 298 | t.Error("e was not found while getting e2") 299 | } 300 | if v == nil { 301 | t.Error("v for e is nil") 302 | } else if e2 := v.(int); e2+2 != 3 { 303 | t.Error("e (which should be 1) plus 2 does not equal 3; value:", e2) 304 | } 305 | if expiration.UnixNano() != tc.items["e"].Expiration { 306 | t.Error("expiration for e is not the correct time") 307 | } 308 | if expiration.UnixNano() < time.Now().UnixNano() { 309 | t.Error("expiration for e is in the past") 310 | } 311 | } 312 | 313 | func TestGetStale(t *testing.T) { 314 | tc := New[string, any](5*time.Millisecond, 0) 315 | 316 | tc.Set("x", "y") 317 | 318 | v, exp, ok := tc.GetStale("x") 319 | if !ok { 320 | t.Errorf("Did not get expired item: %v", v) 321 | } 322 | if exp { 323 | t.Error("exp set") 324 | } 325 | if v.(string) != "y" { 326 | t.Errorf("value wrong: %v", v) 327 | } 328 | 329 | time.Sleep(10 * time.Millisecond) 330 | 331 | v, ok = tc.Get("x") 332 | if ok || v != nil { 333 | t.Fatalf("Get retrieved expired item: %v", v) 334 | } 335 | 336 | v, exp, ok = tc.GetStale("x") 337 | if !ok { 338 | t.Errorf("Did not get expired item: %v", v) 339 | } 340 | if !exp { 341 | t.Error("exp not set") 342 | } 343 | if v.(string) != "y" { 344 | t.Errorf("value wrong: %v", v) 345 | } 346 | } 347 | 348 | func TestAdd(t *testing.T) { 349 | tc := New[string, any](DefaultExpiration, 0) 350 | err := tc.Add("foo", "bar") 351 | if err != nil { 352 | t.Error("Couldn't add foo even though it shouldn't exist") 353 | } 354 | err = tc.Add("foo", "baz") 355 | if err == nil { 356 | t.Error("Successfully added another foo when it should have returned an error") 357 | } 358 | } 359 | 360 | func TestReplace(t *testing.T) { 361 | tc := New[string, string](DefaultExpiration, 0) 362 | err := tc.Replace("foo", "bar") 363 | if err == nil { 364 | t.Error("Replaced foo when it shouldn't exist") 365 | } 366 | tc.Set("foo", "bar") 367 | err = tc.Replace("foo", "bar") 368 | if err != nil { 369 | t.Error("Couldn't replace existing key foo") 370 | } 371 | } 372 | 373 | func TestDelete(t *testing.T) { 374 | tc := New[string, any](DefaultExpiration, 0) 375 | 376 | tc.Set("foo", "bar") 377 | tc.Delete("foo") 378 | wantKeys(t, tc, []string{}, []string{"foo"}) 379 | } 380 | 381 | type onEvictTest struct { 382 | sync.Mutex 383 | items []struct { 384 | k string 385 | v interface{} 386 | } 387 | } 388 | 389 | func (o *onEvictTest) add(k string, v interface{}) { 390 | if k == "race" { 391 | return 392 | } 393 | o.Lock() 394 | o.items = append(o.items, struct { 395 | k string 396 | v interface{} 397 | }{k, v}) 398 | o.Unlock() 399 | } 400 | 401 | func TestPop(t *testing.T) { 402 | tc := New[string, any](DefaultExpiration, 0) 403 | 404 | var onEvict onEvictTest 405 | tc.OnEvicted(onEvict.add) 406 | 407 | tc.Set("foo", "val") 408 | 409 | v, ok := tc.Pop("foo") 410 | wantKeys(t, tc, []string{}, []string{"foo"}) 411 | if !ok { 412 | t.Error("ok is false") 413 | } 414 | if v.(string) != "val" { 415 | t.Errorf("wrong value: %v", v) 416 | } 417 | 418 | v, ok = tc.Pop("nonexistent") 419 | if ok { 420 | t.Error("ok is true") 421 | } 422 | if v != nil { 423 | t.Errorf("v is not nil") 424 | } 425 | 426 | if fmt.Sprintf("%v", onEvict.items) != `[{foo val}]` { 427 | t.Errorf("onEvicted: %v", onEvict.items) 428 | } 429 | } 430 | 431 | func TestModify(t *testing.T) { 432 | tc := New[string, []string](DefaultExpiration, 0) 433 | 434 | tc.Set("k", []string{"x"}) 435 | v, ok := tc.Modify("k", func(v []string) []string { 436 | return append(v, "y") 437 | }) 438 | if !ok { 439 | t.Error("ok is false") 440 | } 441 | if fmt.Sprintf("%v", v) != `[x y]` { 442 | t.Errorf("value wrong: %v", v) 443 | } 444 | 445 | _, ok = tc.Modify("doesntexist", func(v []string) []string { 446 | t.Error("should not be called") 447 | return nil 448 | }) 449 | if ok { 450 | t.Error("ok is true") 451 | } 452 | 453 | v, ok = tc.Modify("k", func(v []string) []string { return nil }) 454 | if !ok { 455 | t.Error("ok not set") 456 | } 457 | if v != nil { 458 | t.Error("v not nil") 459 | } 460 | } 461 | 462 | func TestModifyIncrement(t *testing.T) { 463 | tc := New[string, int](DefaultExpiration, 0) 464 | tc.Set("one", 1) 465 | 466 | have, _ := tc.Modify("one", func(v int) int { return v + 2 }) 467 | if have != 3 { 468 | t.Fatal() 469 | } 470 | 471 | have, _ = tc.Modify("one", func(v int) int { return v - 1 }) 472 | if have != 2 { 473 | t.Fatal() 474 | } 475 | } 476 | 477 | func TestItems(t *testing.T) { 478 | tc := New[string, any](DefaultExpiration, 1*time.Millisecond) 479 | tc.Set("foo", "1") 480 | tc.Set("bar", "2") 481 | tc.Set("baz", "3") 482 | tc.SetWithExpire("exp", "4", 1) 483 | time.Sleep(10 * time.Millisecond) 484 | if n := tc.ItemCount(); n != 3 { 485 | t.Errorf("Item count is not 3 but %d", n) 486 | } 487 | 488 | keys := tc.Keys() 489 | sort.Strings(keys) 490 | if fmt.Sprintf("%v", keys) != "[bar baz foo]" { 491 | t.Errorf("%v", keys) 492 | } 493 | 494 | want := map[string]Item[any]{ 495 | "foo": {Object: "1"}, 496 | "bar": {Object: "2"}, 497 | "baz": {Object: "3"}, 498 | } 499 | if !reflect.DeepEqual(tc.Items(), want) { 500 | t.Errorf("%v", tc.Items()) 501 | } 502 | } 503 | 504 | func TestReset(t *testing.T) { 505 | tc := New[string, any](DefaultExpiration, 0) 506 | tc.Set("foo", "bar") 507 | tc.Set("baz", "yes") 508 | tc.Reset() 509 | v, found := tc.Get("foo") 510 | if found { 511 | t.Error("foo was found, but it should have been deleted") 512 | } 513 | if v != nil { 514 | t.Error("v is not nil:", v) 515 | } 516 | v, found = tc.Get("baz") 517 | if found { 518 | t.Error("baz was found, but it should have been deleted") 519 | } 520 | if v != nil { 521 | t.Error("v is not nil:", v) 522 | } 523 | } 524 | 525 | func TestDeleteAll(t *testing.T) { 526 | tc := New[string, any](DefaultExpiration, 0) 527 | tc.Set("foo", 3) 528 | if tc.onEvicted != nil { 529 | t.Fatal("tc.onEvicted is not nil") 530 | } 531 | works := false 532 | tc.OnEvicted(func(k string, v interface{}) { 533 | if k == "foo" && v.(int) == 3 { 534 | works = true 535 | } 536 | }) 537 | tc.DeleteAll() 538 | if !works { 539 | t.Error("works bool not true") 540 | } 541 | } 542 | 543 | func TestDeleteFunc(t *testing.T) { 544 | tc := New[string, any](NoExpiration, 0) 545 | tc.Set("foo", 3) 546 | tc.Set("bar", 4) 547 | 548 | works := false 549 | tc.OnEvicted(func(k string, v interface{}) { 550 | if k == "foo" && v.(int) == 3 { 551 | works = true 552 | } 553 | }) 554 | 555 | tc.DeleteFunc(func(k string, v Item[any]) (bool, bool) { 556 | return k == "foo" && v.Object.(int) == 3, false 557 | }) 558 | 559 | if !works { 560 | t.Error("onEvicted isn't called for 'foo'") 561 | } 562 | 563 | _, found := tc.Get("bar") 564 | if !found { 565 | t.Error("bar shouldn't be removed from the cache") 566 | } 567 | 568 | tc.Set("boo", 5) 569 | 570 | count := tc.ItemCount() 571 | 572 | // Only one item should be deleted here 573 | tc.DeleteFunc(func(k string, v Item[any]) (bool, bool) { 574 | return true, true 575 | }) 576 | 577 | if tc.ItemCount() != count-1 { 578 | t.Errorf("unexpected number of items in the cache. item count expected %d, found %d", count-1, tc.ItemCount()) 579 | } 580 | } 581 | 582 | // Make sure the janitor is stopped after GC frees up. 583 | func TestFinal(t *testing.T) { 584 | has := func() bool { 585 | s := make([]byte, 8192) 586 | runtime.Stack(s, true) 587 | return bytes.Contains(s, []byte("zgo.at/zcache/v2.(*janitor[...]).run")) 588 | } 589 | 590 | tc := New[string, any](10*time.Millisecond, 10*time.Millisecond) 591 | tc.Set("asd", "zxc") 592 | 593 | if !has() { 594 | t.Fatal("no janitor goroutine before GC") 595 | } 596 | runtime.GC() 597 | if has() { 598 | t.Fatal("still have janitor goroutine after GC") 599 | } 600 | } 601 | 602 | func TestRename(t *testing.T) { 603 | tc := New[string, int](NoExpiration, 0) 604 | tc.Set("foo", 3) 605 | tc.SetWithExpire("bar", 4, 1) 606 | 607 | if tc.Rename("nonex", "asd") { 608 | t.Error() 609 | } 610 | if tc.Rename("bar", "expired") { 611 | t.Error() 612 | } 613 | if v, _, ok := tc.GetStale("bar"); !ok || v != 4 { 614 | t.Error() 615 | } 616 | 617 | if !tc.Rename("foo", "RENAME") { 618 | t.Error() 619 | } 620 | 621 | if v, ok := tc.Get("RENAME"); !ok || v != 3 { 622 | t.Error() 623 | } 624 | 625 | if _, ok := tc.Get("foo"); ok { 626 | t.Error() 627 | } 628 | } 629 | --------------------------------------------------------------------------------