Skip to content

Commit

Permalink
Initial implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
Anton Tolokan committed Mar 1, 2024
1 parent 1c4e69a commit 9c4d661
Show file tree
Hide file tree
Showing 9 changed files with 240 additions and 1 deletion.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# IDE
.idea

# Dependency directories (remove the comment below to include it)
# vendor/

Expand Down
12 changes: 12 additions & 0 deletions CONTRIB.md
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

39 changes: 39 additions & 0 deletions Makefile
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/ \;
10 changes: 9 additions & 1 deletion README.md
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.
3 changes: 3 additions & 0 deletions examples/otter-redis-example/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module otter-redis

go 1.21
8 changes: 8 additions & 0 deletions go.mod
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
)
4 changes: 4 additions & 0 deletions go.sum
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=
140 changes: 140 additions & 0 deletions pkg/simple/cache.go
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()
}
22 changes: 22 additions & 0 deletions pkg/sweet/cache.go
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()
}

0 comments on commit 9c4d661

Please sign in to comment.