-
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.
- Loading branch information
Anton Tolokan
committed
Mar 1, 2024
1 parent
1c4e69a
commit 9c4d661
Showing
9 changed files
with
240 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
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,12 @@ | ||
# How to contribute | ||
|
||
1. Use go-sweet-cache and give us feedback | ||
2. Give us a star | ||
3. Improve [documentation](README.md) | ||
4. Help with any of the [open issues](https://github.com/derbylock/go-sweet-cache/issues) | ||
|
||
Thanks for your contribution! | ||
|
||
# Directory structure | ||
The project layout follows https://github.com/golang-standards/project-layout | ||
|
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,39 @@ | ||
# A Self-Documenting Makefile: http://marmelab.com/blog/2016/02/29/auto-documented-makefile.html | ||
|
||
VERSION=$(shell git describe --always --tags | cut -d "v" -f 2) | ||
LINKER_FLAGS=-s -w -X github.com/derbylock/go-sweet-cache/build.Version=${VERSION} | ||
GOLANGCILINT_VERSION=v1.52.2 | ||
|
||
.PHONY: test | ||
test: | ||
scripts/test.sh | ||
|
||
.PHONY: build | ||
build: | ||
@echo "==> Building go-sweet-cache test binary" | ||
go build -ldflags "$(LINKER_FLAGS)" -o ./bin/go-sweet-cache-demo $(MCLI_SOURCE_FILES) | ||
|
||
.PHONY: deps | ||
deps: ## Download go module dependencies | ||
@echo "==> Installing go.mod dependencies..." | ||
go mod download | ||
go mod tidy | ||
|
||
.PHONY: lint | ||
lint: ## Run linter | ||
golangci-lint run | ||
|
||
.PHONY: localize | ||
localize: ## Run localizer | ||
go generate go-localize -input localizations_src -output localizations | ||
|
||
.PHONY: devtools | ||
devtools: ## Install dev tools | ||
@echo "==> Installing dev tools..." | ||
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell go env GOPATH)/bin $(GOLANGCILINT_VERSION) | ||
|
||
.PHONY: link-git-hooks | ||
link-git-hooks: ## Install git hooks | ||
@echo "==> Installing all git hooks..." | ||
find .git/hooks -type l -exec rm {} \; | ||
find .githooks -type f -exec ln -sf ../../{} .git/hooks/ \; |
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 |
---|---|---|
@@ -1,2 +1,10 @@ | ||
# go-sweet-cache | ||
Go cache library which works on top of a simple caching interface and provides out of box support for generics, singleflighted provider, 2-level eviction (actuality and usability ttl), multileveling (local cache->remote cache), monitoring. Actually anything you need for real simple caching in your app. | ||
Go cache library which works on top of a simple caching interface and provides out of box support for: | ||
- generics | ||
- singleflighted provider | ||
- provider's timeouted retrieval | ||
- 2-level eviction (actual and usable ttl) | ||
- multileveling (local cache->remote cache) | ||
- monitoring | ||
|
||
Actually anything you need for real simple caching in your app. |
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,3 @@ | ||
module otter-redis | ||
|
||
go 1.21 |
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,8 @@ | ||
module github.com/derbylock/go-sweet-cache | ||
|
||
go 1.21.7 | ||
|
||
require ( | ||
golang.org/x/sync v0.6.0 // indirect | ||
resenje.org/singleflight v0.4.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,4 @@ | ||
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= | ||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= | ||
resenje.org/singleflight v0.4.1 h1:ryGHRaOBwhnZLyf34LMDf4AsTSHrs4hdGPdG/I4Hmac= | ||
resenje.org/singleflight v0.4.1/go.mod h1:lAgQK7VfjG6/pgredbQfmV0RvG/uVhKo6vSuZ0vCWfk= |
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,140 @@ | ||
package simple | ||
|
||
import ( | ||
"context" | ||
"time" | ||
|
||
"github.com/derbylock/go-sweet-cache/pkg/sweet" | ||
"resenje.org/singleflight" | ||
) | ||
|
||
var _ sweet.Cacher[string, any] = &Cache[string, any]{} | ||
|
||
// SimpleCache is an interface for a simple cache that can store key-value pairs. | ||
type SimpleCache interface { | ||
// Set adds a key-value pair to the cache. | ||
// Returns false if key was not set because of some reason | ||
Set(key any, value any) bool | ||
|
||
// Get retrieves a value from the cache by its key. | ||
Get(key any) (any, bool) | ||
|
||
// Remove removes a key-value pair from the cache. | ||
Remove(key any) | ||
|
||
// Clear clears all key-value pairs from the cache. | ||
Clear() | ||
|
||
// SetWithTTL works like Set but adds a key-value pair to the cache that will expire after the specified TTL | ||
// (time to live) has passed. A zero value means the value never expires, which is identical to calling Set. | ||
// A negative value is a no-op and the value is discarded. | ||
SetWithTTL(key any, value any, ttl time.Duration) bool | ||
} | ||
|
||
type cacheItem[K comparable, V any] struct { | ||
value V | ||
err error | ||
actual time.Time | ||
usable time.Time | ||
} | ||
|
||
type Cache[K comparable, V any] struct { | ||
back SimpleCache | ||
sfg *singleflight.Group[K, V] | ||
now func() *time.Time | ||
} | ||
|
||
func NewCache[K comparable, V any](back SimpleCache, now func() *time.Time) *Cache[K, V] { | ||
return &Cache[K, V]{ | ||
back: back, | ||
sfg: &singleflight.Group[K, V]{}, | ||
now: now, | ||
} | ||
} | ||
|
||
func (c Cache[K, V]) GetOrProvide(ctx context.Context, key K, valueProvider sweet.ValueProvider[K, V]) (V, error) { | ||
now := c.now() | ||
if cachedVal, ok := c.back.Get(key); ok { | ||
if item, ok := cachedVal.(cacheItem[K, V]); ok { | ||
if now.Before(item.actual) { | ||
return item.value, item.err | ||
} | ||
if now.Before(item.usable) { | ||
go func() { | ||
c.updateValueFromProvider(ctx, key, valueProvider) | ||
}() | ||
return item.value, item.err | ||
} | ||
} | ||
} | ||
|
||
v, _, err := c.updateValueFromProvider(ctx, key, valueProvider) | ||
return v, err | ||
} | ||
|
||
func (c Cache[K, V]) updateValueFromProvider( | ||
ctx context.Context, | ||
key K, | ||
valueProvider sweet.ValueProvider[K, V], | ||
) (V, bool, error) { | ||
return c.sfg.Do(ctx, key, func(ctx context.Context) (V, error) { | ||
v, actualTll, usableTtl, err := valueProvider(ctx, key) | ||
// use new now after the value provided | ||
now := c.now() | ||
item := cacheItem[K, V]{ | ||
value: v, | ||
err: err, | ||
actual: now.Add(actualTll), | ||
usable: now.Add(usableTtl), | ||
} | ||
c.back.SetWithTTL(key, item, usableTtl) | ||
return v, err | ||
}) | ||
} | ||
|
||
func (c Cache[K, V]) GetOrProvideAsync( | ||
ctx context.Context, | ||
key K, | ||
valueProvider sweet.ValueProvider[K, V], | ||
defaultValue V, | ||
) (V, error) { | ||
now := c.now() | ||
if cachedVal, ok := c.back.Get(key); ok { | ||
if item, ok := cachedVal.(cacheItem[K, V]); ok { | ||
if now.Before(item.actual) { | ||
return item.value, item.err | ||
} | ||
if now.Before(item.usable) { | ||
go func() { | ||
c.updateValueFromProvider(ctx, key, valueProvider) | ||
}() | ||
return item.value, item.err | ||
} | ||
} | ||
} | ||
|
||
go func() { | ||
c.updateValueFromProvider(ctx, key, valueProvider) | ||
}() | ||
return defaultValue, nil | ||
} | ||
|
||
func (c Cache[K, V]) Get(key K) (V, bool, error) { | ||
now := c.now() | ||
if cachedVal, ok := c.back.Get(key); ok { | ||
if item, ok := cachedVal.(cacheItem[K, V]); ok { | ||
if now.Before(item.usable) { | ||
return item.value, true, item.err | ||
} | ||
} | ||
} | ||
return *new(V), false, nil | ||
} | ||
|
||
func (c Cache[K, V]) Remove(key K) { | ||
c.back.Remove(key) | ||
} | ||
|
||
func (c Cache[K, V]) Clear() { | ||
c.back.Clear() | ||
} |
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,22 @@ | ||
package sweet | ||
|
||
import ( | ||
"context" | ||
"time" | ||
) | ||
|
||
// ValueProvider is a function that provides a value for a given key, along with the actual and usable TTLs. | ||
// | ||
// The function takes a key of type K and returns a value of type V, along with the actual TTL and usable TTL, | ||
// or an error if the value could not be provided. | ||
// | ||
// Context should be used by provider to properly process cancellation, e.g. because of timeout. | ||
type ValueProvider[K comparable, V any] func(ctx context.Context, key K) (val V, actualTTL time.Duration, usableTTL time.Duration, err error) | ||
|
||
type Cacher[K comparable, V any] interface { | ||
GetOrProvide(ctx context.Context, key K, valueProvider ValueProvider[K, V]) (V, error) | ||
GetOrProvideAsync(ctx context.Context, key K, valueProvider ValueProvider[K, V], defaultValue V) (V, error) | ||
Get(key K) (V, bool, error) | ||
Remove(key K) | ||
Clear() | ||
} |