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.
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.