cache

package
v9.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 2, 2026 License: AGPL-3.0 Imports: 5 Imported by: 0

Documentation

Overview

Package cache provides a generic caching interface with support for multiple backend implementations including Redis and in-memory stores.

Index

Examples

Constants

View Source
const NoExpiry time.Duration = -1

NoExpiry is the WithExpiry value for entries that should never expire. It is distinct from zero: a zero expiry means "use the cache's configured default".

Variables

View Source
var (
	ErrNotFound = errors.New("not found")

	// ErrNamespaceRequired indicates a Flush (or an unscoped DeleteByPrefix)
	// was attempted on a provider whose backing store may be shared and which
	// has no configured key namespace — so it cannot know which entries it
	// owns. Configure a namespace (e.g. the redis provider's
	// Config.Namespace) to enable whole-cache operations.
	ErrNamespaceRequired = errors.New("operation requires a configured key namespace")

	// ErrUnavailable indicates the cache did not perform the operation because
	// its backing store is unreachable — typically a tripped circuit breaker.
	//
	// It is deliberately distinct from ErrNotFound. A read that answers "not
	// found" during an outage is indistinguishable from one that answers it
	// because the key really is absent, and a caller whose whole purpose is to
	// notice absence — idempotency's FailClosed, say — is then told exactly what
	// it must not be told. A write that answers nil during an outage is worse:
	// it reports that a delete happened, and the stale value it did not delete
	// is served for the rest of its TTL.
	//
	// Callers that would rather treat unavailability as a miss can say so, by
	// checking for this error; callers that must not cannot be tricked into it.
	ErrUnavailable = errors.New("cache is unavailable")
)

Functions

func EffectiveExpiry

func EffectiveExpiry(defaultExpiry time.Duration, opts ...WriteOption) time.Duration

EffectiveExpiry resolves a write's options against a cache's default expiry, returning the duration the entry should live: a positive duration, or zero meaning "never expire". The three input states resolve as documented on WithExpiry — an unset/zero expiry takes defaultExpiry, and a negative expiry (NoExpiry) or negative default resolves to no expiry. Providers should treat this as the single source of truth for expiry semantics so backends cannot drift.

Types

type Cache

type Cache[T any] interface {
	Get(ctx context.Context, key string) (*T, error)
	// GetMany fetches multiple keys in as few round trips as possible.
	// Missing keys are omitted from the returned map, so a key's absence
	// from the result is a cache miss.
	GetMany(ctx context.Context, keys []string) (map[string]*T, error)
	Set(ctx context.Context, key string, value *T, opts ...WriteOption) error
	// SetMany stores multiple values at once. WriteOptions apply to the
	// whole batch: every item gets the same expiry resolution as a single
	// Set call would.
	SetMany(ctx context.Context, items map[string]*T, opts ...WriteOption) error
	Delete(ctx context.Context, key string) error
	// DeleteMany removes multiple keys in as few round trips as possible.
	// Keys that are already absent are not an error.
	DeleteMany(ctx context.Context, keys []string) error
	// DeleteByPrefix removes every entry whose key begins with prefix.
	// Providers on shared backing stores that have no configured
	// namespace reject an empty prefix with ErrNamespaceRequired, since
	// that would delete entries they cannot prove they own.
	DeleteByPrefix(ctx context.Context, prefix string) error
	// Flush removes every entry this cache owns. Providers whose backing
	// store may be shared (redis) require a configured namespace to know
	// what they own and return ErrNamespaceRequired without one;
	// providers that wholly own their store (memory) always succeed.
	Flush(ctx context.Context) error
	Ping(ctx context.Context) error
	// Close releases the resources the cache holds — a connection pool, a
	// background sweep — and is safe to call more than once. It does not
	// evict anything: entries in a shared backing store outlive the handle
	// that wrote them. After Close the cache must not be used again.
	Close() error
}

Cache is our wrapper interface for a cache. Batched reads, writes, and deletes are part of the interface — every provider implements them, and batching is the primary access pattern for high-volume consumers.

Writes accept optional WriteOptions. A write with no options (or a zero expiry) uses the cache's configured default expiry; WithExpiry overrides it per call, and WithExpiry(NoExpiry) pins the entry against expiry entirely.

Keys are scoped to the cache instance: a provider with a configured namespace prepends it to every key transparently, so callers never see or supply namespaced keys. There is deliberately no way to reach entries outside the cache's own namespace — a generic inspect-everything client is a debugging tool, not a production surface, and belongs in a separate Debug type if it is ever needed.

Example (Batch)
package main

import (
	"context"
	"fmt"

	"github.com/primandproper/platform-go/v9/cache/memory"
)

func main() {
	ctx := context.Background()
	c, err := memory.NewInMemoryCache[string](0)
	if err != nil {
		panic(err)
	}

	// Batched reads and writes are part of Cache itself — no assertion needed.
	one, two := "one", "two"
	if err = c.SetMany(ctx, map[string]*string{"k1": &one, "k2": &two}); err != nil {
		panic(err)
	}

	// Missing keys are simply absent from the result.
	results, err := c.GetMany(ctx, []string{"k1", "k2", "missing"})
	if err != nil {
		panic(err)
	}

	fmt.Println(len(results))
	fmt.Println(*results["k1"])
}
Output:
2
one
Example (NotFound)
package main

import (
	"context"
	"errors"
	"fmt"

	"github.com/primandproper/platform-go/v9/cache"
	"github.com/primandproper/platform-go/v9/cache/memory"
)

func main() {
	ctx := context.Background()
	c, cacheErr := memory.NewInMemoryCache[string](0)
	if cacheErr != nil {
		panic(cacheErr)
	}

	_, err := c.Get(ctx, "nonexistent")
	fmt.Println(err)
	fmt.Println(errors.Is(err, cache.ErrNotFound))
}
Output:
not found
true
Example (SetAndGet)
package main

import (
	"context"
	"fmt"

	"github.com/primandproper/platform-go/v9/cache/memory"
)

func main() {
	ctx := context.Background()
	c, err := memory.NewInMemoryCache[string](0)
	if err != nil {
		panic(err)
	}

	value := "cached-value"
	if err = c.Set(ctx, "my-key", &value); err != nil {
		panic(err)
	}

	result, err := c.Get(ctx, "my-key")
	if err != nil {
		panic(err)
	}

	fmt.Println(*result)
}
Output:
cached-value

type Codec

type Codec[T any] interface {
	Encode(value *T) ([]byte, error)
	Decode(data []byte) (*T, error)
}

Codec converts values to and from the byte representation a serializing provider stores (redis today; the memory provider holds values directly and never encodes). NewGobCodec is the default; consumers with tight size or latency budgets — large batch reads where per-value codec overhead is the tail latency — supply their own fixed-format Codec through the provider's constructor options.

A Codec must be safe for concurrent use, and Decode must round-trip whatever Encode produced. Values written with one codec are unreadable through another, so changing a deployed cache's codec requires either a key change or a flush.

func NewGobCodec

func NewGobCodec[T any]() Codec[T]

NewGobCodec returns the default gob-backed Codec. Types must be gob-friendly: exported fields only, and interface-typed fields need their concrete types registered with gob.Register.

Example

ExampleNewGobCodec shows the codec the serializing providers use by default. Consumers reach for Codec only to replace it — redis.WithCodec accepts any implementation whose Decode round-trips its own Encode. Note the migration caveat: values written under one codec are unreadable through another.

package main

import (
	"fmt"

	"github.com/primandproper/platform-go/v9/cache"
)

func main() {
	type session struct {
		UserID string
		Roles  []string
	}

	codec := cache.NewGobCodec[session]()

	encoded, err := codec.Encode(&session{UserID: "u-1", Roles: []string{"admin", "auditor"}})
	if err != nil {
		panic(err)
	}

	decoded, err := codec.Decode(encoded)
	if err != nil {
		panic(err)
	}

	fmt.Println(decoded.UserID, decoded.Roles)
}
Output:
u-1 [admin auditor]

type WriteConfig

type WriteConfig struct {
	// Expiry holds the caller's requested expiry: zero for "use the
	// cache's default", NoExpiry (or any negative value) for "never
	// expire", or a positive duration.
	Expiry time.Duration
}

WriteConfig is the resolved per-call write configuration. Providers normally consume it through EffectiveExpiry rather than directly; it is exported so third-party Cache implementations can run the same resolution.

type WriteOption

type WriteOption func(*WriteConfig)

WriteOption configures a single Set or SetMany call.

func WithExpiry

func WithExpiry(expiry time.Duration) WriteOption

WithExpiry sets the expiry for the entries written by this call. Zero defers to the cache's configured default; NoExpiry (or any negative duration) stores the entries without expiry.

Directories

Path Synopsis
Package cachemock provides moq-generated mock implementations of interfaces in the cache package.
Package cachemock provides moq-generated mock implementations of interfaces in the cache package.
slots
Package slots produces Redis Cluster keys whose slots are pre-planned to distribute evenly across the cluster's nodes.
Package slots produces Redis Cluster keys whose slots are pre-planned to distribute evenly across the cluster's nodes.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL