Skip to content

Commit

Permalink
Added a cached loader for xload (#21)
Browse files Browse the repository at this point in the history
* Added a cached loader

* Added examples & tests

* Added DisableEmptyValueHit option

* Used sync.Map for cache

---------

Co-authored-by: ajatprabha <[email protected]>
  • Loading branch information
sonnes and ajatprabha authored Sep 4, 2023
1 parent 3ce0d74 commit c03ca60
Show file tree
Hide file tree
Showing 11 changed files with 595 additions and 1 deletion.
68 changes: 68 additions & 0 deletions xload/providers/cached/cache.go
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)
}
43 changes: 43 additions & 0 deletions xload/providers/cached/cache_test.go
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)
}
6 changes: 6 additions & 0 deletions xload/providers/cached/doc.go
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
113 changes: 113 additions & 0 deletions xload/providers/cached/example_test.go
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)
}
}
21 changes: 21 additions & 0 deletions xload/providers/cached/go.mod
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
)
33 changes: 33 additions & 0 deletions xload/providers/cached/go.sum
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=
43 changes: 43 additions & 0 deletions xload/providers/cached/loader.go
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
})
}
Loading

0 comments on commit c03ca60

Please sign in to comment.