-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added a cached loader for xload (#21)
* Added a cached loader * Added examples & tests * Added DisableEmptyValueHit option * Used sync.Map for cache --------- Co-authored-by: ajatprabha <[email protected]>
- Loading branch information
1 parent
3ce0d74
commit c03ca60
Showing
11 changed files
with
595 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
package cached | ||
|
||
import ( | ||
"sync" | ||
"time" | ||
) | ||
|
||
// Cacher is the interface for custom cache implementations. | ||
// | ||
//go:generate mockery --name Cacher --structname MockCache --filename mock_test.go --outpkg cached --output . | ||
type Cacher interface { | ||
Get(key string) (string, error) | ||
Set(key, value string, ttl time.Duration) error | ||
} | ||
|
||
type mv struct { | ||
val string | ||
ttl time.Time | ||
} | ||
|
||
// MapCache is a simple cache implementation using a map. | ||
type MapCache struct { | ||
m sync.Map | ||
|
||
now func() time.Time | ||
} | ||
|
||
// NewMapCache returns a new MapCache. | ||
func NewMapCache() *MapCache { | ||
return &MapCache{ | ||
m: sync.Map{}, | ||
now: time.Now, | ||
} | ||
} | ||
|
||
// Get returns the value for the given key, if cached. | ||
func (c *MapCache) Get(key string) (string, error) { | ||
v, ok := c.m.Load(key) | ||
if !ok { | ||
return "", nil | ||
} | ||
|
||
mv, ok := v.(*mv) | ||
|
||
if !ok || c.now().After(mv.ttl) { | ||
c.delete(key) | ||
|
||
return "", nil | ||
} | ||
|
||
return mv.val, nil | ||
} | ||
|
||
// Set sets the value for the given key. | ||
func (c *MapCache) Set(key, value string, ttl time.Duration) error { | ||
v := &mv{ | ||
val: value, | ||
ttl: c.now().Add(ttl), | ||
} | ||
|
||
c.m.Store(key, v) | ||
|
||
return nil | ||
} | ||
|
||
func (c *MapCache) delete(key string) { | ||
c.m.Delete(key) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
package cached | ||
|
||
import ( | ||
"testing" | ||
"time" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestMapCache(t *testing.T) { | ||
cache := NewMapCache() | ||
|
||
err := cache.Set("foo", "bar", 1*time.Minute) | ||
assert.NoError(t, err) | ||
|
||
v, err := cache.Get("foo") | ||
assert.NoError(t, err) | ||
assert.Equal(t, "bar", v) | ||
|
||
v, err = cache.Get("not-found") | ||
assert.NoError(t, err) | ||
assert.Equal(t, "", v) | ||
} | ||
|
||
func TestMapCacheExpired(t *testing.T) { | ||
cache := NewMapCache() | ||
now := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC) | ||
|
||
cache.now = func() time.Time { | ||
return now | ||
} | ||
|
||
err := cache.Set("foo", "bar", 1*time.Minute) | ||
assert.NoError(t, err) | ||
|
||
cache.now = func() time.Time { | ||
return now.Add(2 * time.Minute) | ||
} | ||
|
||
v, err := cache.Get("foo") | ||
assert.NoError(t, err) | ||
assert.Equal(t, "", v) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
// Package cached provides a cached provider for xload. | ||
// This loader can be used to cache the results of any loader. | ||
// | ||
// Useful for loaders that have a high latency, or loaders that are | ||
// called frequently. | ||
package cached |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
package cached_test | ||
|
||
import ( | ||
"context" | ||
"time" | ||
|
||
"github.com/gojekfarm/xtools/xload" | ||
"github.com/gojekfarm/xtools/xload/providers/cached" | ||
) | ||
|
||
func Example() { | ||
// This example shows how to use the cached loader | ||
// with a remote loader. | ||
|
||
ctx := context.Background() | ||
cfg := struct { | ||
Title string `env:"TITLE"` | ||
Link string `env:"LINK"` | ||
ButtonLabel string `env:"BUTTON_LABEL"` | ||
}{} | ||
|
||
remoteLoader := xload.LoaderFunc(func(ctx context.Context, key string) (string, error) { | ||
// Load the value from a remote source. | ||
|
||
return "", nil | ||
}) | ||
|
||
err := xload.Load( | ||
ctx, &cfg, | ||
cached.NewLoader( | ||
remoteLoader, | ||
cached.TTL(5*60*time.Minute), | ||
), | ||
) | ||
if err != nil { | ||
panic(err) | ||
} | ||
} | ||
|
||
type CustomCache struct{} | ||
|
||
func NewCustomCache() *CustomCache { | ||
return &CustomCache{} | ||
} | ||
|
||
func (c *CustomCache) Get(key string) (string, error) { | ||
return "", nil | ||
} | ||
|
||
func (c *CustomCache) Set(key, value string, ttl time.Duration) error { | ||
return nil | ||
} | ||
|
||
func Example_customCache() { | ||
// This example shows how to use a custom cache | ||
// with the cached loader. | ||
|
||
ctx := context.Background() | ||
cfg := struct { | ||
Title string `env:"TITLE"` | ||
Link string `env:"LINK"` | ||
ButtonLabel string `env:"BUTTON_LABEL"` | ||
}{} | ||
|
||
remoteLoader := xload.LoaderFunc(func(ctx context.Context, key string) (string, error) { | ||
// Load the value from a remote source. | ||
|
||
return "", nil | ||
}) | ||
|
||
err := xload.Load( | ||
ctx, &cfg, | ||
cached.NewLoader( | ||
remoteLoader, | ||
cached.TTL(5*60*time.Minute), | ||
cached.Cache(NewCustomCache()), | ||
), | ||
) | ||
if err != nil { | ||
panic(err) | ||
} | ||
} | ||
|
||
func Example_disableEmptyValueHit() { | ||
// By default, the cached loader caches empty values. | ||
// This example shows how to disable caching of empty values | ||
// with the cached loader. | ||
|
||
ctx := context.Background() | ||
cfg := struct { | ||
Title string `env:"TITLE"` | ||
Link string `env:"LINK"` | ||
ButtonLabel string `env:"BUTTON_LABEL"` | ||
}{} | ||
|
||
remoteLoader := xload.LoaderFunc(func(ctx context.Context, key string) (string, error) { | ||
// Load the value from a remote source. | ||
|
||
return "", nil | ||
}) | ||
|
||
err := xload.Load( | ||
ctx, &cfg, | ||
cached.NewLoader( | ||
remoteLoader, | ||
cached.TTL(5*60*time.Minute), | ||
cached.DisableEmptyValueHit, | ||
), | ||
) | ||
if err != nil { | ||
panic(err) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
module github.com/gojekfarm/xtools/xload/providers/cached | ||
|
||
go 1.20 | ||
|
||
replace github.com/gojekfarm/xtools/xload => ../.. | ||
|
||
require ( | ||
github.com/gojekfarm/xtools/xload v0.0.0-00010101000000-000000000000 | ||
github.com/stretchr/testify v1.8.4 | ||
) | ||
|
||
require ( | ||
github.com/davecgh/go-spew v1.1.1 // indirect | ||
github.com/pmezard/go-difflib v1.0.0 // indirect | ||
github.com/sourcegraph/conc v0.3.0 // indirect | ||
github.com/spf13/cast v1.5.1 // indirect | ||
github.com/stretchr/objx v0.5.0 // indirect | ||
go.uber.org/atomic v1.7.0 // indirect | ||
go.uber.org/multierr v1.9.0 // indirect | ||
gopkg.in/yaml.v3 v3.0.1 // indirect | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= | ||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= | ||
github.com/gotidy/ptr v1.4.0 h1:7++suUs+HNHMnyz6/AW3SE+4EnBhupPSQTSI7QNijVc= | ||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= | ||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= | ||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= | ||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= | ||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= | ||
github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= | ||
github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= | ||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= | ||
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= | ||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= | ||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= | ||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= | ||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= | ||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= | ||
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= | ||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= | ||
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= | ||
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= | ||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= | ||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
package cached | ||
|
||
import ( | ||
"context" | ||
|
||
"github.com/gojekfarm/xtools/xload" | ||
) | ||
|
||
// NewLoader returns a new cached loader. | ||
// | ||
// The cached loader uses these defaults: | ||
// - TTL: 5 minutes. Configurable via `TTL` option. | ||
// - Cache: A simple unbounded map cache. Configurable via `Cache` option. | ||
// - Empty value hit: Enabled. Configurable via `DisableEmptyValueHit` option. | ||
func NewLoader(l xload.Loader, opts ...Option) xload.LoaderFunc { | ||
o := defaultOptions() | ||
|
||
o.apply(opts...) | ||
|
||
return xload.LoaderFunc(func(ctx context.Context, key string) (string, error) { | ||
v, err := o.cache.Get(key) | ||
if err != nil { | ||
return "", err | ||
} | ||
|
||
if v != "" { | ||
return v, nil | ||
} | ||
|
||
loaded, err := l.Load(ctx, key) | ||
if err != nil { | ||
return "", err | ||
} | ||
|
||
if loaded == "" && !o.emptyHit { | ||
return "", nil | ||
} | ||
|
||
err = o.cache.Set(key, loaded, o.ttl) | ||
|
||
return loaded, err | ||
}) | ||
} |
Oops, something went wrong.