From 1af7b76c2f469c4fd59d3f56b670a1ddc6269b68 Mon Sep 17 00:00:00 2001 From: Brandon Atkinson Date: Tue, 26 Dec 2023 10:55:03 -0700 Subject: [PATCH] refact: remove memoize --- .github/workflows/pipeline.yaml | 2 +- README.md | 71 --------------------------------- memoize/memoize.go | 67 ------------------------------- 3 files changed, 1 insertion(+), 139 deletions(-) delete mode 100644 memoize/memoize.go diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml index 0834af6..033954c 100644 --- a/.github/workflows/pipeline.yaml +++ b/.github/workflows/pipeline.yaml @@ -46,4 +46,4 @@ jobs: # Run go build - name: Run Go Build - run: go build ./cache && go build ./memoize && go build ./persist + run: go build ./cache && go build ./persist diff --git a/README.md b/README.md index e6b6ab5..9b0fef2 100644 --- a/README.md +++ b/README.md @@ -74,74 +74,3 @@ var GetTeams = cache.OnDisk(filepath.Join("cache", "teams"), time.Hour, func(ctx return teams, nil }) ``` - -## memoize -The memoization package provides functionality for memoizing function results. -You can use these functions to cache function results both in memory as-well-as in an external data store. -Additionally, this cache is set based on the input parameter so different inputs will have their own individual cache. -Be aware that if you are memorizing large amounts of data with long TTLs you may run into OOM issues. -This is especially true for memoization where new entries are made into the cache for every new paramater. - -It's important to note that the memoized function may return expired data. -This can happen when your cached function returns an error but the previous cache value still exists. -In this case valid cache data will be returned along with your function's error. -As the developer it is up to you to determine if this stale data is safe to use or if it should be ignored. - -Example 1: cache function results in memory. This makes repeatedly calling the GetTeams function much faster since -only the first call will result in a network call. -```go -// GetTeam gets a team from an external api. The results will be cached in memory for at least one hour. -// The cached data is tied to the input parameter such that calls with different inputs will have their own -// individual cache. -var GetTeam = memoize.InMemory(time.Hour, func(ctx context.Context, teamName string) (*Team, error) { - client := &http.Client{} - resp, err := client.Get("https://api.weavedev.net/team/" + teamName) - if err != nil { - return nil, err - } - - defer resp.Body.Close() - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - team := &Team{} - err = json.Unmarshal(body, team) - if err != nil { - return nil, err - } - - return team, nil -}) -``` - -Example 2: cache function results in memory and on disk. -Like example 1 this improves performance. -It also allows the cache to be restored across runs which can be useful for short-lived process like cron jobs or cli tools -```go -// GetTeam gets a team from an external api. The results will be cached in memory for at least one hour. -// The cached data is tied to the input parameter such that calls with different inputs will have their own -// individual cache. Additionally, the cache will be backed by the file system so it can be restored between program runs -var GetTeam = memoize.OnDisk(filepath.Join("cache", "team"), time.Hour, func(ctx context.Context, teamName string) (*Team, error) { - client := &http.Client{} - resp, err := client.Get("https://api.weavedev.net/team/" + teamName) - if err != nil { - return nil, err - } - - defer resp.Body.Close() - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - team := &Team{} - err = json.Unmarshal(body, team) - if err != nil { - return nil, err - } - - return team, nil -}) -``` diff --git a/memoize/memoize.go b/memoize/memoize.go deleted file mode 100644 index f72d7b2..0000000 --- a/memoize/memoize.go +++ /dev/null @@ -1,67 +0,0 @@ -package memoize - -import ( - "context" - "path/filepath" - "strings" - "time" - - "github.com/weave-lab/cachin/persist" -) - -// InMemory takes a function and wraps it in an in-memory cache. The function will not be run again if the timeout -// duration has not fully elapsed since its last run with the same input K. Instead, the previously calculated return -// value will be returned instead. The type K may implement the Keyer interface to provide custom type matching. If the -// Keyer interface is not provided, K will be JSON marshalled to determine matching inputs. -func InMemory[T, K any](ttl time.Duration, fn func(context.Context, K) (T, error)) func(context.Context, K, bool) (T, error) { - return SkipErr(Func(nil, "", ttl, fn)) -} - -// OnDisk takes a function and wraps it in an on-disk cache. The function will not be run again if the timeout duration -// has not fully elapsed since its last run with the same input K. Instead the previously calculated return value will -// be returned instead. Additionally, since state is saved on disk, this timeout persists across multiple runs of a -// program. Because this requires writing to a backing file, the cache can fail. If this happens OnDisk will fall back -// on an in-memory cache. The type K may implement the Keyer interface to provide custom type matching. If the Keyer -// interface is not provided, K will be JSON marshalled to determine matching inputs. -func OnDisk[T, K any](dir string, ttl time.Duration, fn func(context.Context, K) (T, error)) func(context.Context, K, bool) (T, error, error) { - key := filepath.Base(dir) - cache := persist.NewFsStore(strings.TrimSuffix(dir, key), false) - - return Func(cache, key, ttl, fn) -} - -// Func takes a function and wraps it in a cache. The returned function will use the provided store to cache the return -// value of the function. The function will not be run again if the timeout duration has not fully elapsed since it's -// last run. Instead, the previously calculated return value will be returned instead. The provided store allows this -// timeout to be respected even across multiple runs. However, because the store may fail this behavior is not guaranteed -// If the store cache does fail, Func will fall back on an in-memory cache. -func Func[T, K any](store persist.Store, key string, ttl time.Duration, fn func(context.Context, K) (T, error)) func(context.Context, K, bool) (T, error, error) { - dataMap := persist.NewDataMap[T](store, key) - - return func(ctx context.Context, in K, forceRefresh bool) (T, error, error) { - data, loadErr := dataMap.Load(ctx, in) - - if forceRefresh || data.IsUnset() || (*data).IsExpired(ttl) { - got, err := fn(ctx, in) - if err != nil { - return data.Get(), loadErr, err - } - - err = data.Set(ctx, got) - if err != nil { - return got, err, nil - } - } - - return data.Get(), nil, nil - } -} - -// SkipErr ignores cache errors in a cached function. It can be used to simplify a functions signature if you don't -// care about cache errors -func SkipErr[T, K any](fn func(context.Context, K, bool) (T, error, error)) func(context.Context, K, bool) (T, error) { - return func(ctx context.Context, in K, b bool) (T, error) { - t, _, err := fn(ctx, in, b) - return t, err - } -}