cache

package
v0.17.1 Latest Latest
Warning

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

Go to latest
Published: Jun 6, 2026 License: MIT Imports: 18 Imported by: 0

Documentation

Overview

Package cache is an HTTP response-cache middleware with a pluggable storage backend. It implements a CDN-style honor-origin policy: it caches a response only when the origin opts in via explicit Cache-Control/Expires freshness; refuses private/no-store/no-cache, Set-Cookie, and Vary: *; honors Vary (keying per varied request header); GET/HEAD only; and ignores the client's request Cache-Control so a client can't bust the shared cache. Concurrent misses for one key collapse into a single origin fetch. It is fail-static: any storage error degrades to a cache miss, never an error to the client. The response is tagged X-Cache: HIT|MISS.

Two storage backends ship: an in-memory one (NewMemory, bodies held in RAM, lost on restart) and a disk-backed one (NewDisk, survives restarts, streams bodies to disk so it isn't bounded by RSS). Both bound their total size with LRU eviction and a per-object cap. Plug either (or your own Storage) into New.

store, _ := cache.NewDisk("/var/cache/app", 1<<30) // 1 GiB on disk
m := cache.New(store, cache.Options{MaxFileSize: 8 << 20})
srv.Use(m) // mount it ahead of the upstream/handler it should cache

Only origin-opted-in (public, fresh) content is cached, so per-user or authorization-sensitive responses must be marked uncacheable by the origin.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Cache

type Cache struct {
	// contains filtered or unexported fields
}

Cache is the HTTP response-cache middleware. It implements parapet.Middleware (ServeHandler). Construct with New, giving it a Storage backend.

func New

func New(storage Storage, opts Options) *Cache

New builds the cache middleware over the given Storage backend (memory or disk).

func (*Cache) ServeHandler

func (c *Cache) ServeHandler(next http.Handler) http.Handler

ServeHandler implements parapet.Middleware: it wraps next (the upstream/handler whose responses are cached). A hit short-circuits next; a miss fetches via next and stores.

type DiskStorage

type DiskStorage struct {
	// contains filtered or unexported fields
}

DiskStorage is a disk-backed cache backend. Entries survive restarts and the body is streamed to disk, so the cache isn't bounded by RSS. The total on-disk byte cap is held by an in-memory LRU, re-seeded from disk by a background startup scan that also reaps orphans, torn writes, and expired entries. Safe for concurrent use by a single Cache (see Storage).

Layout (sharded by the first 2 hex chars of the key):

<dir>/<aa>/<key>.body   response body bytes
<dir>/<aa>/<key>.meta   JSON sidecar (written last)
<dir>/tmp/<key>.<seq>   in-progress writes, atomically renamed on commit

func NewDisk

func NewDisk(dir string, maxSize int64) (*DiskStorage, error)

NewDisk creates (or opens) a disk storage rooted at dir, bounded to maxSize total body bytes. It starts a background scan that re-seeds the byte cap from surviving entries and reaps orphans/expired files off the serving path — the cap simply lags until the scan completes. Returns an error only if the dir can't be initialized.

func (*DiskStorage) Delete

func (s *DiskStorage) Delete(key string)

Delete removes the entry under key (meta first, so a half-deleted entry reads as a clean miss rather than a torn read) and its LRU accounting.

func (*DiskStorage) Get

func (s *DiskStorage) Get(key string) (Meta, []byte, bool)

Get reads the entry under key, touching its LRU recency on a hit. ok=false on any miss / corruption / torn read (fail-static — treated as a cache miss). The body length is checked against meta.Size so a reader can never serve an old meta paired with a concurrently-rewritten body (the two files are read non-atomically; this guards the framing-corruption case).

func (*DiskStorage) Range added in v0.17.1

func (s *DiskStorage) Range(fn func(key string, m Meta) bool)

Range walks the shard dirs reading each entry's .meta sidecar and calls fn(key, Meta), stopping early if fn returns false. It holds no lock, so fn may Delete the entry it is visiting (the keys come from a directory snapshot taken before fn runs). Unreadable/corrupt sidecars are skipped. For maintenance only, off the serving path.

func (*DiskStorage) Writer

func (s *DiskStorage) Writer(key string) (EntryWriter, error)

Writer streams a new entry's body to a temp file; Commit fsyncs + renames it into place (body first, meta last) and admits it to the byte cap; Abort discards the temp file. Returns an error if the temp file can't be created.

type EntryWriter

type EntryWriter interface {
	io.Writer
	// Commit persists the streamed body with meta and admits it to the byte cap
	// (evicting LRU victims). A failure is fail-static: the entry is not cached.
	Commit(meta Meta) error
	// Abort discards the streamed body (e.g. truncated/over-cap/panicked fill).
	Abort()
}

EntryWriter streams one cached body and finalizes it. Exactly one of Commit or Abort must be called; after either, the writer is spent. Abort after Commit (or vice versa) is a no-op. Backends admit the entry to their capacity bound (LRU) inside Commit.

type MemoryStorage

type MemoryStorage struct {
	// contains filtered or unexported fields
}

MemoryStorage is an in-memory cache backend: bodies are held in RAM and lost on restart. Total size is bounded by LRU eviction (the cap passed to NewMemory) plus the middleware's per-object cap. Safe for concurrent use.

func NewMemory

func NewMemory(maxSize int64) *MemoryStorage

NewMemory creates an in-memory storage bounded to maxSize total body bytes.

func (*MemoryStorage) Delete

func (s *MemoryStorage) Delete(key string)

Delete removes the entry under key.

func (*MemoryStorage) Get

func (s *MemoryStorage) Get(key string) (Meta, []byte, bool)

Get returns the entry under key, touching its LRU recency on a hit.

func (*MemoryStorage) Range added in v0.17.1

func (s *MemoryStorage) Range(fn func(key string, m Meta) bool)

Range snapshots the current (key, Meta) pairs under the read lock, then calls fn for each WITHOUT holding the lock — so fn may Delete entries (which takes the write lock) without deadlocking. The snapshot copies only metadata, not bodies.

func (*MemoryStorage) Writer

func (s *MemoryStorage) Writer(key string) (EntryWriter, error)

Writer returns a buffer-backed writer; Commit stores it (bodies are in RAM either way for this backend).

type Meta

type Meta struct {
	Status     int         `json:"status"`
	Header     http.Header `json:"header"`
	PrimaryHex string      `json:"primary"`        // primary key hash (host+method+scheme+uri)
	Host       string      `json:"host,omitempty"` // normalized host (lowercased, port-stripped); for out-of-band Range maintenance
	URI        string      `json:"uri,omitempty"`  // request-uri (path+query); for out-of-band Range maintenance
	Vary       []string    `json:"vary"`           // lowercased Vary header names
	Created    int64       `json:"created"`        // unix nanos
	FreshUntil int64       `json:"fresh"`          // unix nanos; entry is stale after this
	Size       int64       `json:"size"`           // body bytes (== eviction weight)
}

Meta is the stored metadata for a cached response. Backends persist it alongside the body (the disk backend marshals it to JSON).

type Options

type Options struct {
	// InvalidatedAfter, when non-nil, is consulted on every cache hit to support
	// out-of-band invalidation (cache purge). It receives the request and the
	// stored entry's Meta and returns an invalidation epoch in unix nanos: a hit
	// whose Meta.Created is <= the returned epoch is treated as stale (reaped and
	// served as a miss), exactly like a passed FreshUntil. Return 0 (or any value
	// below the entry's Created) to keep the entry. It runs only on a hit, so it
	// costs nothing while the cache is idle; nil disables the check entirely (zero
	// overhead). The callee owns its own concurrency.
	InvalidatedAfter func(r *http.Request, m Meta) int64

	// MaxFileSize caps a cacheable response's body. A GET response larger than
	// this (by Content-Length, or mid-stream) is not cached but still served in
	// full. Defaults to 8 MiB when <= 0.
	MaxFileSize int64
}

Options configures the cache middleware.

type Storage

type Storage interface {
	// Get returns the entry stored under key, or ok=false on a miss. A hit should
	// be counted as a recent use (LRU). The returned body must not be mutated by
	// the caller. Any internal error is reported as a miss (fail-static).
	Get(key string) (meta Meta, body []byte, ok bool)

	// Writer begins storing an entry under key. The caller streams the body to the
	// returned EntryWriter and then calls Commit (to persist) or Abort (to
	// discard). It returns an error if a writer can't be opened (then the entry is
	// simply not cached). The disk backend streams to a temp file so the body
	// never has to be buffered whole in RAM.
	Writer(key string) (EntryWriter, error)

	// Delete removes the entry under key (e.g. when the middleware finds it stale).
	Delete(key string)

	// Range calls fn for each currently-stored entry (key + its Meta), stopping
	// early if fn returns false. It is for out-of-band maintenance — e.g. a purge
	// reaper that physically deletes entries matching an external predicate — not
	// the serving path. Iteration order is unspecified, and the snapshot is
	// best-effort: fn may observe an entry that is concurrently deleted, and an
	// entry written during the walk may or may not be visited. fn MAY call
	// Delete(key) on this storage (a backend must not hold a lock across fn that
	// Delete would need). fn MUST NOT call Writer.
	Range(fn func(key string, m Meta) bool)
}

Storage is the cache backend: where cached entries live and how total size is bounded. The middleware handles policy, keys, Vary, locking, and X-Cache; Storage only persists bytes and enforces capacity (LRU + per-object cap).

Implementations must be safe for concurrent use BY A SINGLE Cache instance. One backing store (e.g. a DiskStorage dir) must be owned by one Cache: concurrent same-key writes are otherwise unsynchronized. The middleware serializes same-key fills with its fill lock and only writes a key it has just missed, so within one Cache the store never sees a same-key Get racing a Set.

Jump to

Keyboard shortcuts

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