├── .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 |
--------------------------------------------------------------------------------