From c2dc1e34d2cde4cbe19021e5c2ff45dccbbf1bd5 Mon Sep 17 00:00:00 2001 From: Josh Fyne Date: Thu, 12 Jan 2023 09:26:39 +0000 Subject: [PATCH] feat: Adds a way to use generics with this library. --- memoize.go | 38 +++++++++++++++++++++++++++ memoize_test.go | 69 +++++++++++++++++++++++++++++++++++++++++++++++++ readme.md | 30 +++++++++++++++++++++ 3 files changed, 137 insertions(+) diff --git a/memoize.go b/memoize.go index 1ff6656..57d5cb7 100644 --- a/memoize.go +++ b/memoize.go @@ -1,6 +1,7 @@ package memoize import ( + "errors" "time" "github.com/patrickmn/go-cache" @@ -47,3 +48,40 @@ func (m *Memoizer) Memoize(key string, fn func() (interface{}, error)) (interfac }) return value, err, false } + +// ErrMismatchedType if data returned from the cache does not match the expected type. +var ErrMismatchedType = errors.New("data returned does not match expected type") + +// MemoizedFunction the expensive function to be called. +type MemoizedFunction[T any] func() (T, error) + +// Call executes and returns the results of the given function, unless there was a cached value of the same key. +// Only one execution is in-flight for a given key at a time. +// The boolean return value indicates whether v was previously stored. +func Call[T any](m *Memoizer, key string, fn MemoizedFunction[T]) (T, error, bool) { + // Check cache + value, found := m.Storage.Get(key) + if found { + v, ok := value.(T) + if !ok { + return v, ErrMismatchedType, true + } + return v, nil, true + } + + // Combine memoized function with a cache store + value, err, _ := m.group.Do(key, func() (any, error) { + data, innerErr := fn() + + if innerErr == nil { + m.Storage.Set(key, data, cache.DefaultExpiration) + } + + return data, innerErr + }) + v, ok := value.(T) + if !ok { + return v, ErrMismatchedType, false + } + return v, err, false +} diff --git a/memoize_test.go b/memoize_test.go index cf34c2e..81814b3 100644 --- a/memoize_test.go +++ b/memoize_test.go @@ -83,3 +83,72 @@ func (t *F) TestFailure() { t.So(result.(int), ShouldEqual, 2) t.So(cached, ShouldBeTrue) } + +// TestBasicGenerics adopts the code from readme.md into a simple test case +// but using generics. +func (t *F) TestBasicGenerics() { + expensiveCalls := 0 + + // Function tracks how many times its been called + expensive := func() (int, error) { + expensiveCalls++ + return expensiveCalls, nil + } + + cache := NewMemoizer(90*time.Second, 10*time.Minute) + + // First call SHOULD NOT be cached + result, err, cached := Call(cache, "key1", expensive) + t.So(err, ShouldBeNil) + t.So(result, ShouldEqual, 1) + t.So(cached, ShouldBeFalse) + + // Second call on same key SHOULD be cached + result, err, cached = Call(cache, "key1", expensive) + t.So(err, ShouldBeNil) + t.So(result, ShouldEqual, 1) + t.So(cached, ShouldBeTrue) + + // First call on a new key SHOULD NOT be cached + result, err, cached = Call(cache, "key2", expensive) + t.So(err, ShouldBeNil) + t.So(result, ShouldEqual, 2) + t.So(cached, ShouldBeFalse) +} + +// TestFailureGenerics checks that failed function values are not cached +// when using generics. +func (t *F) TestFailureGenerics() { + calls := 0 + + // This function will fail IFF it has not been called before. + twoForTheMoney := func() (int, error) { + calls++ + + if calls == 1 { + return calls, errors.New("Try again") + } else { + return calls, nil + } + } + + cache := NewMemoizer(90*time.Second, 10*time.Minute) + + // First call should fail, and not be cached + result, err, cached := Call(cache, "key1", twoForTheMoney) + t.So(err, ShouldNotBeNil) + t.So(result, ShouldEqual, 1) + t.So(cached, ShouldBeFalse) + + // Second call should succeed, and not be cached + result, err, cached = Call(cache, "key1", twoForTheMoney) + t.So(err, ShouldBeNil) + t.So(result, ShouldEqual, 2) + t.So(cached, ShouldBeFalse) + + // Third call should succeed, and be cached + result, err, cached = Call(cache, "key1", twoForTheMoney) + t.So(err, ShouldBeNil) + t.So(result, ShouldEqual, 2) + t.So(cached, ShouldBeTrue) +} diff --git a/readme.md b/readme.md index a32d902..a7ec041 100644 --- a/readme.md +++ b/readme.md @@ -51,3 +51,33 @@ In the example above, `result` is: All the hard stuff is punted to patrickmn's [go-cache](https://github.com/patrickmn/go-cache) and the Go team's [x/sync/singleflight](https://github.com/golang/sync), I just lashed them together. Also note that `cache.Storage` is exported, so you can use the underlying cache features - such as [Flush](https://godoc.org/github.com/patrickmn/go-cache#Cache.Flush) or [SaveFile](https://godoc.org/github.com/patrickmn/go-cache#Cache.SaveFile). + +### Generics + +You can also use generics. The same example as above + +```golang +import ( + "time" + + "github.com/kofalt/go-memoize" +) + +// Any expensive call that you wish to cache +expensive := func() (string, error) { + time.Sleep(3 * time.Second) + return "some data", nil +} + +// Cache expensive calls in memory for 90 seconds, purging old entries every 10 minutes. +cache := memoize.NewMemoizer(90*time.Second, 10*time.Minute) + +// This will call the expensive func, and return a string. +result, err, cached := memoize.Call(cache, "key1", expensive) + +// This will be cached +result, err, cached = memoize.Call(cache, "key1", expensive) + +// This uses a new cache key, so expensive is called again +result, err, cached = memoize.Call(cache, "key2", expensive) +```