Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

21 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 instead of returning the error — for at most Config.StaleTTL past expiry. The cap is a hard wall-clock bound anchored at the last successful fetch: failed refreshes never extend it.

stale-while-revalidate (GetAsync)

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

negative caching (NotFound + NegativeTTL)

An error your NotFound classifier reports as an authoritative "does not exist" evicts any cached value — a deleted key is never served stale — and is itself cached for NegativeTTL, so a hammered missing key is answered from cache instead of hitting the upstream. Keep NegativeTTL short: a negative entry makes a key that was just created upstream look missing until it expires.

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. A hard cap anchored at the last successful fetch; past it the value is treated as gone. 0 disables serving stale on error.
MaxConcurrentRefresh Caps concurrent background refreshes (GetAsync) across all keys. Defaults to 1.
Capacity Bounds the number of entries; dead entries are evicted first, then arbitrary ones. <= 0 means unbounded. Set it whenever keys come from user input.
NotFound Optional func(err error) bool classifying a fetch error as an authoritative "does not exist". Such misses evict any cached value and are negatively cached. Nil treats every error as transient.
NegativeTTL How long an authoritative miss is served from cache before re-fetching. 0 disables negative caching (misses still evict).
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().
Clock Optional func() time.Time used for all expiry checks. Defaults to time.Now. Override in tests to control TTLs deterministically; must be safe for concurrent use.

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

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages