Skip to content

mbrostami/lastcache

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

19 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Go Reference Go Report Card Coverage

LastCache

v2 is a generics rewrite with built-in single-flight. On the v1 API? See Migrating from v1.

LastCache is a generic, concurrency-safe in-memory cache implementing stale-if-error and stale-while-revalidate, with built-in single-flight so a burst of concurrent requests for the same key triggers at most one fetch.

go get github.com/mbrostami/lastcache/v2

stale-if-error (Get / GetStale)

When a fetch fails and a previous value is still around, the cache serves that stale value for up to Config.StaleTTL instead of returning the error.

stale-while-revalidate (GetAsync)

An expired value is returned immediately while a single background goroutine refreshes it.

Usage

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/mbrostami/lastcache/v2"
)

func main() {
	cache := lastcache.New[string, string](lastcache.Config{
		TTL:      time.Minute,
		StaleTTL: 10 * time.Second, // serve stale up to 10s when a refresh fails
	})

	fetch := func(ctx context.Context, key string) (string, error) {
		// load from db / upstream / etc.
		return "value-for-" + key, nil
	}

	// Common case: fresh value, or transparently a stale one on error.
	v, err := cache.Get(context.Background(), "user:42", fetch)
	fmt.Println(v, err)

	// When you need to know it was served stale:
	res, err := cache.GetStale(context.Background(), "user:42", fetch)
	fmt.Println(res.Value, res.Stale, res.Err, err)

	// stale-while-revalidate: returns immediately, refreshes in the background.
	res = cache.GetAsync(context.Background(), "user:42", fetch)
	fmt.Println(res.Value, res.Stale)
}

API

func New[K comparable, V any](config Config) *Cache[K, V]

func (c *Cache[K, V]) Get(ctx, key, fetch) (V, error)            // fresh or stale-on-error, single-flighted
func (c *Cache[K, V]) GetStale(ctx, key, fetch) (Result[V], error) // same, but reports staleness
func (c *Cache[K, V]) GetAsync(ctx, key, fetch) Result[V]        // serve now, refresh in background
func (c *Cache[K, V]) Set(key, value)
func (c *Cache[K, V]) Delete(key)
func (c *Cache[K, V]) TTL(key) time.Duration
func (c *Cache[K, V]) Range(func(key K, value V, ttl time.Duration) bool)

fetch is a plain func(ctx context.Context, key K) (V, error).

type Result[V any] struct {
	Value V
	Stale bool  // value is expired but served (error or in-progress refresh)
	Err   error // underlying fetch error when stale; nil for fresh values
}

Config

Field Meaning
TTL How long a fetched value stays fresh. Defaults to 1 minute.
StaleTTL How long a stale value may be served after expiry when a refresh fails. 0 disables serving stale.
MaxConcurrentRefresh Caps concurrent background refreshes (GetAsync) across all keys. Defaults to 1.
OnError Optional func(key any, err error) called when a background refresh fails.
Context Base context for background refreshes (they outlive the request). Defaults to context.Background().

Migrating from v1

v2 is a rewrite:

  • Generic Cache[K, V] instead of any (no more type assertions).
  • Get / GetStale / GetAsync replace LoadOrStore / AsyncLoadOrStore and the WithCtx variants; ctx is now a parameter.
  • The fetch callback is (ctx, key) (V, error) - the useStale bool return is gone; serving stale is controlled by StaleTTL.
  • GetAsync returns a Result instead of a chan error; background errors go to Config.OnError.
  • Config: GlobalTTL -> TTL, ExtendTTL -> StaleTTL, AsyncSemaphore -> MaxConcurrentRefresh.
  • Built-in single-flight: concurrent requests for the same key share one fetch.

About

In-memory cache strategy implementation of stale-while-revalidate and stale-if-error with zero dependency - Go module

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages