New upstream version 2.8.0+ds
Sascha Steinbiss
2 years ago
14 | 14 | |
15 | 15 | Note (issue #25): by default, due to historic reasons, the TTL will be reset on each cache hit and you need to explicitly configure the cache to use a TTL that will not get extended. |
16 | 16 | |
17 | [](https://travis-ci.org/ReneKroon/ttlcache) | |
17 | [](https://travis-ci.com/ReneKroon/ttlcache) | |
18 | 18 | [](https://goreportcard.com/report/github.com/ReneKroon/ttlcache) |
19 | 19 | [](https://coveralls.io/github/ReneKroon/ttlcache?branch=master) |
20 | 20 | [](https://github.com/ReneKroon/ttlcache/issues) |
21 | 21 | [](https://github.com/ReneKroon/ttlcache/LICENSE) |
22 | 22 | |
23 | 23 | ## Usage |
24 | ||
25 | `go get github.com/ReneKroon/ttlcache/v2` | |
24 | 26 | |
25 | 27 | You can copy it as a full standalone demo program. The first snippet is basic usage, where the second exploits more options in the cache. |
26 | 28 |
0 | 0 | package ttlcache |
1 | 1 | |
2 | 2 | import ( |
3 | "golang.org/x/sync/singleflight" | |
4 | 3 | "sync" |
5 | 4 | "time" |
5 | ||
6 | "golang.org/x/sync/singleflight" | |
6 | 7 | ) |
7 | 8 | |
8 | 9 | // CheckExpireCallback is used as a callback for an external check on item expiration |
21 | 22 | // SimpleCache interface enables a quick-start. Interface for basic usage. |
22 | 23 | type SimpleCache interface { |
23 | 24 | Get(key string) (interface{}, error) |
25 | GetWithTTL(key string) (interface{}, time.Duration, error) | |
24 | 26 | Set(key string, data interface{}) error |
25 | 27 | SetTTL(ttl time.Duration) error |
26 | 28 | SetWithTTL(key string, data interface{}, ttl time.Duration) error |
280 | 282 | return cache.GetByLoader(key, nil) |
281 | 283 | } |
282 | 284 | |
285 | // GetWithTTL has exactly the same behaviour as Get but also returns | |
286 | // the remaining TTL for an specific item at the moment it its retrieved | |
287 | func (cache *Cache) GetWithTTL(key string) (interface{}, time.Duration, error) { | |
288 | return cache.GetByLoaderWithTtl(key, nil) | |
289 | } | |
290 | ||
283 | 291 | // GetByLoader can take a per key loader function (ie. to propagate context) |
284 | 292 | func (cache *Cache) GetByLoader(key string, customLoaderFunction LoaderFunction) (interface{}, error) { |
285 | cache.mutex.Lock() | |
286 | if cache.isShutDown { | |
287 | cache.mutex.Unlock() | |
288 | return nil, ErrClosed | |
293 | dataToReturn, _, err := cache.GetByLoaderWithTtl(key, customLoaderFunction) | |
294 | ||
295 | return dataToReturn, err | |
296 | } | |
297 | ||
298 | // GetByLoaderWithTtl can take a per key loader function (ie. to propagate context) | |
299 | func (cache *Cache) GetByLoaderWithTtl(key string, customLoaderFunction LoaderFunction) (interface{}, time.Duration, error) { | |
300 | cache.mutex.Lock() | |
301 | if cache.isShutDown { | |
302 | cache.mutex.Unlock() | |
303 | return nil, 0, ErrClosed | |
289 | 304 | } |
290 | 305 | |
291 | 306 | cache.metrics.Hits++ |
292 | 307 | item, exists, triggerExpirationNotification := cache.getItem(key) |
293 | 308 | |
294 | 309 | var dataToReturn interface{} |
310 | ttlToReturn := time.Duration(0) | |
295 | 311 | if exists { |
296 | 312 | cache.metrics.Retrievals++ |
297 | 313 | dataToReturn = item.data |
314 | ttlToReturn = time.Until(item.expireAt) | |
315 | if ttlToReturn < 0 { | |
316 | ttlToReturn = 0 | |
317 | } | |
298 | 318 | } |
299 | 319 | |
300 | 320 | var err error |
313 | 333 | } |
314 | 334 | |
315 | 335 | if loaderFunction != nil && !exists { |
316 | cache.mutex.Unlock() | |
317 | dataToReturn, err, _ = cache.loaderLock.Do(key, func() (interface{}, error) { | |
336 | type loaderResult struct { | |
337 | data interface{} | |
338 | ttl time.Duration | |
339 | } | |
340 | ch := cache.loaderLock.DoChan(key, func() (interface{}, error) { | |
318 | 341 | // cache is not blocked during io |
319 | invokeData, err := cache.invokeLoader(key, loaderFunction) | |
320 | return invokeData, err | |
342 | invokeData, ttl, err := cache.invokeLoader(key, loaderFunction) | |
343 | lr := &loaderResult{ | |
344 | data: invokeData, | |
345 | ttl: ttl, | |
346 | } | |
347 | return lr, err | |
321 | 348 | }) |
349 | cache.mutex.Unlock() | |
350 | res := <-ch | |
351 | dataToReturn = res.Val.(*loaderResult).data | |
352 | ttlToReturn = res.Val.(*loaderResult).ttl | |
353 | err = res.Err | |
322 | 354 | } |
323 | 355 | |
324 | 356 | if triggerExpirationNotification { |
325 | 357 | cache.expirationNotification <- true |
326 | 358 | } |
327 | 359 | |
328 | return dataToReturn, err | |
329 | } | |
330 | ||
331 | func (cache *Cache) invokeLoader(key string, loaderFunction LoaderFunction) (dataToReturn interface{}, err error) { | |
332 | var ttl time.Duration | |
333 | ||
360 | return dataToReturn, ttlToReturn, err | |
361 | } | |
362 | ||
363 | func (cache *Cache) invokeLoader(key string, loaderFunction LoaderFunction) (dataToReturn interface{}, ttl time.Duration, err error) { | |
334 | 364 | dataToReturn, ttl, err = loaderFunction(key) |
335 | 365 | if err == nil { |
336 | 366 | err = cache.SetWithTTL(key, dataToReturn, ttl) |
338 | 368 | dataToReturn = nil |
339 | 369 | } |
340 | 370 | } |
341 | return dataToReturn, err | |
371 | return dataToReturn, ttl, err | |
342 | 372 | } |
343 | 373 | |
344 | 374 | // Remove removes an item from the cache if it exists, triggers expiration callback when set. Can return ErrNotFound if the entry was not present. |
32 | 32 | |
33 | 33 | } |
34 | 34 | |
35 | // Issue 45 : This test was used to test different code paths for best performance. | |
36 | func TestCache_GetByLoaderRace(t *testing.T) { | |
37 | t.Skip() | |
38 | t.Parallel() | |
39 | cache := NewCache() | |
40 | cache.SetTTL(time.Microsecond) | |
41 | defer cache.Close() | |
42 | ||
43 | loaderInvocations := uint64(0) | |
44 | inFlight := uint64(0) | |
45 | ||
46 | globalLoader := func(key string) (data interface{}, ttl time.Duration, err error) { | |
47 | atomic.AddUint64(&inFlight, 1) | |
48 | atomic.AddUint64(&loaderInvocations, 1) | |
49 | time.Sleep(time.Microsecond) | |
50 | assert.Equal(t, uint64(1), inFlight) | |
51 | defer atomic.AddUint64(&inFlight, ^uint64(0)) | |
52 | return "global", 0, nil | |
53 | ||
54 | } | |
55 | cache.SetLoaderFunction(globalLoader) | |
56 | ||
57 | for i := 0; i < 1000; i++ { | |
58 | wg := sync.WaitGroup{} | |
59 | for i := 0; i < 1000; i++ { | |
60 | wg.Add(1) | |
61 | go func() { | |
62 | key, _ := cache.Get("test") | |
63 | assert.Equal(t, "global", key) | |
64 | wg.Done() | |
65 | ||
66 | }() | |
67 | } | |
68 | wg.Wait() | |
69 | t.Logf("Invocations: %d\n", loaderInvocations) | |
70 | loaderInvocations = 0 | |
71 | } | |
72 | ||
73 | } | |
74 | ||
35 | 75 | // Issue / PR #39: add customer loader function for each Get() # |
36 | 76 | // some middleware prefers to define specific context's etc per Get. |
37 | 77 | // This is faciliated by supplying a loder function with Get's. |
67 | 107 | defaultKey, _ := cache.GetByLoader("test", nil) |
68 | 108 | assert.Equal(t, "global", defaultKey) |
69 | 109 | |
110 | cache.Remove("test") | |
111 | } | |
112 | ||
113 | func TestCache_GetByLoaderWithTtl(t *testing.T) { | |
114 | t.Parallel() | |
115 | cache := NewCache() | |
116 | defer cache.Close() | |
117 | ||
118 | globalTtl := time.Duration(time.Minute) | |
119 | globalLoader := func(key string) (data interface{}, ttl time.Duration, err error) { | |
120 | return "global", globalTtl, nil | |
121 | } | |
122 | cache.SetLoaderFunction(globalLoader) | |
123 | ||
124 | localTtl := time.Duration(time.Hour) | |
125 | localLoader := func(key string) (data interface{}, ttl time.Duration, err error) { | |
126 | return "local", localTtl, nil | |
127 | } | |
128 | ||
129 | key, ttl, _ := cache.GetWithTTL("test") | |
130 | assert.Equal(t, "global", key) | |
131 | assert.Equal(t, ttl, globalTtl) | |
132 | cache.Remove("test") | |
133 | ||
134 | localKey, ttl2, _ := cache.GetByLoaderWithTtl("test", localLoader) | |
135 | assert.Equal(t, "local", localKey) | |
136 | assert.Equal(t, ttl2, localTtl) | |
137 | cache.Remove("test") | |
138 | ||
139 | globalKey, ttl3, _ := cache.GetByLoaderWithTtl("test", globalLoader) | |
140 | assert.Equal(t, "global", globalKey) | |
141 | assert.Equal(t, ttl3, globalTtl) | |
142 | cache.Remove("test") | |
143 | ||
144 | defaultKey, ttl4, _ := cache.GetByLoaderWithTtl("test", nil) | |
145 | assert.Equal(t, "global", defaultKey) | |
146 | assert.Equal(t, ttl4, globalTtl) | |
70 | 147 | cache.Remove("test") |
71 | 148 | } |
72 | 149 | |
751 | 828 | assert.Equal(t, []string{"hello"}, keys, "Expected keys contains 'hello'") |
752 | 829 | } |
753 | 830 | |
831 | func TestCacheGetWithTTL(t *testing.T) { | |
832 | t.Parallel() | |
833 | ||
834 | cache := NewCache() | |
835 | defer cache.Close() | |
836 | ||
837 | data, ttl, exists := cache.GetWithTTL("hello") | |
838 | assert.Equal(t, exists, ErrNotFound, "Expected empty cache to return no data") | |
839 | assert.Nil(t, data, "Expected data to be empty") | |
840 | assert.Equal(t, int(ttl), 0, "Expected item TTL to be 0") | |
841 | ||
842 | cache.Set("hello", "world") | |
843 | data, ttl, exists = cache.GetWithTTL("hello") | |
844 | assert.NotNil(t, data, "Expected data to be not nil") | |
845 | assert.Equal(t, nil, exists, "Expected data to exist") | |
846 | assert.Equal(t, "world", (data.(string)), "Expected data content to be 'world'") | |
847 | assert.Equal(t, int(ttl), 0, "Expected item TTL to be 0") | |
848 | ||
849 | orgttl := time.Duration(500 * time.Millisecond) | |
850 | cache.SetWithTTL("hello", "world", orgttl) | |
851 | time.Sleep(10 * time.Millisecond) | |
852 | data, ttl, exists = cache.GetWithTTL("hello") | |
853 | assert.NotNil(t, data, "Expected data to be not nil") | |
854 | assert.Equal(t, nil, exists, "Expected data to exist") | |
855 | assert.Equal(t, "world", (data.(string)), "Expected data content to be 'world'") | |
856 | assert.Less(t, ttl, orgttl, "Expected item TTL to be less than the original TTL") | |
857 | assert.NotEqual(t, int(ttl), 0, "Expected item TTL to be not 0") | |
858 | } | |
859 | ||
860 | func TestCache_TestGetWithTTLAndLoaderFunction(t *testing.T) { | |
861 | t.Parallel() | |
862 | cache := NewCache() | |
863 | ||
864 | cache.SetLoaderFunction(func(key string) (data interface{}, ttl time.Duration, err error) { | |
865 | return nil, 0, ErrNotFound | |
866 | }) | |
867 | ||
868 | _, ttl, err := cache.GetWithTTL("1") | |
869 | assert.Equal(t, ErrNotFound, err, "Expected error to be ErrNotFound") | |
870 | assert.Equal(t, int(ttl), 0, "Expected item TTL to be 0") | |
871 | ||
872 | orgttl := time.Duration(1 * time.Second) | |
873 | cache.SetLoaderFunction(func(key string) (data interface{}, ttl time.Duration, err error) { | |
874 | return "1", orgttl, nil | |
875 | }) | |
876 | ||
877 | value, ttl, found := cache.GetWithTTL("1") | |
878 | assert.Equal(t, nil, found) | |
879 | assert.Equal(t, "1", value) | |
880 | assert.Equal(t, ttl, orgttl, "Expected item TTL to be the same as the original TTL") | |
881 | cache.Close() | |
882 | ||
883 | value, ttl, found = cache.GetWithTTL("1") | |
884 | assert.Equal(t, ErrClosed, found) | |
885 | assert.Equal(t, nil, value) | |
886 | assert.Equal(t, int(ttl), 0, "Expected returned ttl for an ErrClosed err to be 0") | |
887 | } | |
888 | ||
754 | 889 | func TestCacheExpirationCallbackFunction(t *testing.T) { |
755 | 890 | t.Parallel() |
756 | 891 |