cache

package
v11.2.0 Latest Latest
Warning

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

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

Documentation

Overview

Package cache stores session records in a cache.Cache.

It is the default backend. Sessions are short-lived, read on every request, and a lost one costs a sign-in rather than a record — which is exactly the shape a cache is good at. Pointed at redis it serves a fleet; pointed at cache/memory it serves a test.

c, _ := cachecfg.NewCache[sessions.Record[Principal]](ctx, &cfg.Cache)
backend, _ := sessionscache.NewBackend(c)
store, _ := sessions.NewStore(backend, sessions.WithIdleTimeout(30*time.Minute))

The cache's configured default expiry is never consulted: every write carries the deadline the Store computed from its Policy, so the two cannot disagree about when a session ends.

What it cannot do

Update is safe against a concurrent sign-out: it goes through cache.SetIfPresent, so the write and the check that the record still exists are one operation, and a request that loaded a session just before sign-out cannot write it back afterwards.

Rename is not. It writes the new identifier and deletes the old one, and no conditional write spans two keys — that needs a transaction, which is what sessions/database has and this does not. A sign-out landing mid-renewal can therefore still leave the renewed session alive.

Where renewal has to be atomic, or where flushing the cache must not sign everybody out, use sessions/database.

Choosing the cache

The memory provider is per-process. Two replicas do not share sessions, so a user is signed in to whichever replica their request lands on and signed out of the others — fine for tests and single-process services, wrong for anything behind a load balancer.

Redis wants a namespace of its own. Session records share a keyspace with whatever else is in that cache otherwise, and a Flush meant for something else signs every user out.

Index

Constants

This section is empty.

Variables

View Source
var ErrNilCache = platformerrors.Wrap(platformerrors.ErrNilInputParameter, "nil session cache")

ErrNilCache indicates NewBackend was called without a cache. It wraps errors.ErrNilInputParameter, so a caller may check either.

Functions

This section is empty.

Types

type Backend

type Backend[T any] struct {
	// contains filtered or unexported fields
}

Backend stores session records in a cache.Cache. It is exported, and returned by NewBackend, so a caller who has chosen cache-backed sessions can depend on that choice rather than on the sessions.Backend seam — matching sessions/database, whose NewBackend has always returned its own *Backend.

func NewBackend

func NewBackend[T any](c cache.Cache[sessions.Record[T]], opts ...Option) (*Backend[T], error)

NewBackend builds a sessions.Backend over a cache.

The cache is required and has no default, because which one it is decides what the sessions mean. A memory cache is per-process: two replicas do not see each other's sessions, so a user is signed in to whichever one their request lands on. Redis is the production answer; memory is for tests and for single-process services that accept losing every session on restart.

The cache's own default expiry is never used — every write carries the deadline the Store computed from its Policy — so a cache built solely for sessions can be configured with any expiry at all.

func (*Backend[T]) Close

func (b *Backend[T]) Close() error

Close releases the cache.

func (*Backend[T]) Create

func (b *Backend[T]) Create(ctx context.Context, id string, record *sessions.Record[T], ttl time.Duration) error

Create stores a record under a freshly minted identifier.

It writes without checking first, which is the one place this backend takes an identifier's uniqueness on faith rather than enforcing it. The identifier is 256 bits from crypto/rand; a read-before-write here would cost a round trip on the hot path to defend against a collision that will not happen. ErrIDConflict is therefore part of the Backend contract that only the database backend can actually report.

func (*Backend[T]) Delete

func (b *Backend[T]) Delete(ctx context.Context, id string) error

Delete removes the record stored under id.

func (*Backend[T]) Load

func (b *Backend[T]) Load(ctx context.Context, id string) (*sessions.Record[T], error)

Load reads the record stored under id.

func (*Backend[T]) Rename

func (b *Backend[T]) Rename(
	ctx context.Context,
	oldID, newID string,
	record *sessions.Record[T],
	ttl time.Duration,
) error

Rename moves a record from oldID to newID.

The new identifier is written before the old one is removed, deliberately. In the reverse order a failed write leaves the user with no session at all, signed out mid-privilege-change; in this order a failed delete leaves the old identifier valid, and the error says so, so the caller refuses the privilege change rather than proceeding with a session an attacker may hold. Neither order is atomic here — that is what sessions/database's transaction is for.

func (*Backend[T]) Update

func (b *Backend[T]) Update(ctx context.Context, id string, record *sessions.Record[T], ttl time.Duration) error

Update overwrites the record stored under an existing identifier.

The condition is what stops a signed-out session from coming back. A request that loaded a session just before Delete removed it would otherwise write it back afterwards, complete with a fresh idle deadline, and the sign-out would not have happened.

SetIfPresent makes that one operation, so there is no interval for the delete to land in: the write either precedes it or is refused by it. An absent record comes back as ErrNotFound, which translate turns into sessions.ErrNotFound — the same answer a load would give, because a session that was revoked mid-request is exactly a session that is not there.

type Option

type Option func(*options)

Option configures the backend at construction.

It carries no type parameter even though NewBackend does: Go cannot infer a type argument from a call's result type, so an Option[T] would force every call site to spell the payload type out by hand forever.

There is no WithMetricsProvider. Every counter worth having describes what an operation meant — a session created, a session expired, an idle deadline refreshed — and only the Store knows that; this layer would only be able to count round trips the cache provider already counts.

func WithLogger

func WithLogger(logger logging.Logger) Option

WithLogger attaches a logger. An absent logger logs nowhere.

func WithTracerProvider

func WithTracerProvider(tracerProvider tracing.Provider) Option

WithTracerProvider attaches a tracer provider. An absent one traces nowhere.

Jump to

Keyboard shortcuts

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