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