Documentation
¶
Overview ¶
Package httpcache is an RFC 7234 HTTP cache for Go: freshness and heuristic expiration, conditional revalidation, Vary-aware cache keys, invalidation on unsafe methods, and memory, disk and VFS backends behind one Cache interface.
Layout ¶
The root package does not import a metrics backend. It defines Metrics -- what the cache records -- and defaults to NopMetrics, so a service that never exports metrics does not link Prometheus:
- github.com/soulteary/httpcache-kit/v4/prometheusmetrics -- records into Prometheus via metrics-kit, and with it protobuf.
Measured for a program importing only the root package, v3.0.0 against v4.0.0: 51 fewer linked packages, 17 fewer modules and a 32.5% smaller binary. A program that does use the subpackage pays 0.8% more than it did on v3 -- the cost is deferred, not removed, and only to those who want it.
Logging is not split the same way. The handler logs on paths the cache cannot report any other way, so a logger is not optional in the way a metrics exporter is; logger-kit accounts for 5 of the remaining packages.
Getting started ¶
cache := httpcache.NewMemoryCache()
handler := httpcache.NewHandler(cache, upstream)
handler.Shared = true // a reverse proxy in front of many users
http.ListenAndServe(":8080", handler)
A shared cache refuses to store responses marked private and strips private headers; a private cache does not. Getting this backwards serves one user's response to another, so NewSharedHandler exists to make the choice explicit rather than a field somebody forgets to set.
Recording metrics ¶
Install a recorder once, and every cache and handler in the process reports through it:
registry := metrics.NewRegistry("myproxy")
prometheusmetrics.New(registry) // installs itself as the default
To record somewhere else -- OpenTelemetry, statsd, a test double -- implement Metrics. Embed NopMetrics to inherit no-ops for the methods you do not need, so a method added in a later release cannot break your implementation.
Index ¶
- Constants
- Variables
- func IsDebugLogging() bool
- func SetDebugLogging(b bool)
- func SetDefaultMetrics(m Metrics)
- func SetLogger(log *logger.Logger)
- type Cache
- type CacheConfig
- func (c *CacheConfig) Validate() *CacheConfig
- func (c *CacheConfig) WithCleanupInterval(interval time.Duration) *CacheConfig
- func (c *CacheConfig) WithMaxSize(size int64) *CacheConfig
- func (c *CacheConfig) WithStaleMapTTL(ttl time.Duration) *CacheConfig
- func (c *CacheConfig) WithTTL(ttl time.Duration) *CacheConfig
- type CacheControl
- type CacheStats
- type CleanupResult
- type ExtendedCache
- type Handler
- type HandlerOptions
- type Header
- type Key
- type Metrics
- type NopMetrics
- func (NopMetrics) RecordCacheEviction(string)
- func (NopMetrics) RecordCacheHit(string)
- func (NopMetrics) RecordCacheMiss(string)
- func (NopMetrics) RecordCacheSkip()
- func (NopMetrics) RecordCleanupDuration(float64)
- func (NopMetrics) RecordRetrieveOperation(bool)
- func (NopMetrics) RecordStoreOperation(bool)
- func (NopMetrics) RecordUpstreamDuration(string, int, float64)
- func (NopMetrics) RecordUpstreamError(string)
- func (NopMetrics) SetCacheItemCount(int)
- func (NopMetrics) SetCacheSize(int64)
- func (NopMetrics) SetCacheStaleCount(int)
- func (NopMetrics) UpdateCacheStats(CacheStats)
- type ReadSeekCloser
- type Resource
- func (r *Resource) Age() (time.Duration, error)
- func (r *Resource) DateAfter(d time.Time) bool
- func (r *Resource) Expires() (time.Time, error)
- func (r *Resource) HasExplicitExpiration() bool
- func (r *Resource) HasValidators() bool
- func (r *Resource) Header() http.Header
- func (r *Resource) HeuristicFreshness() time.Duration
- func (r *Resource) IsNonErrorStatus() bool
- func (r *Resource) IsStale() bool
- func (r *Resource) LastModified() time.Time
- func (r *Resource) MarkStale()
- func (r *Resource) MaxAge(shared bool) (time.Duration, error)
- func (r *Resource) MustValidate(shared bool) bool
- func (r *Resource) ReceivedAfter(d time.Time) bool
- func (r *Resource) RemovePrivateHeaders()
- func (r *Resource) SetStoredAt(t time.Time)
- func (r *Resource) Status() int
- func (r *Resource) StoredAfter(d time.Time) bool
- func (r *Resource) Via() string
- type Validator
Constants ¶
const ( CacheHeader = "X-Cache" ProxyDateHeader = "Proxy-Date" )
const (
CacheControlHeader = "Cache-Control"
)
const DefaultCacheTTL = 7 * 24 * time.Hour
DefaultCacheTTL is the default TTL for cached items (7 days)
const DefaultCleanupInterval = 1 * time.Hour
DefaultCleanupInterval is the default interval for cache cleanup (1 hour)
const DefaultMaxCacheSize int64 = 10 * 1024 * 1024 * 1024
DefaultMaxCacheSize is the default maximum cache size (10 GB)
const DefaultStaleMapTTL = 24 * time.Hour
DefaultStaleMapTTL is the default TTL for stale map entries (24 hours)
Variables ¶
var Clock = func() time.Time { return time.Now().UTC() }
var ErrNotFoundInCache = errors.New("not found in cache")
Returned when a resource doesn't exist
var Writes sync.WaitGroup
Functions ¶
func IsDebugLogging ¶
func IsDebugLogging() bool
IsDebugLogging returns whether debug messages are logged. Safe for concurrent use.
func SetDebugLogging ¶
func SetDebugLogging(b bool)
SetDebugLogging sets whether debug messages are logged. Safe for concurrent use.
func SetDefaultMetrics ¶
func SetDefaultMetrics(m Metrics)
SetDefaultMetrics installs the process-wide recorder. Passing nil restores NopMetrics rather than arming a nil that would panic on first use.
prometheusmetrics.New calls this for you.
Types ¶
type Cache ¶
type Cache interface {
Header(key string) (Header, error)
Store(res *Resource, keys ...string) error
Retrieve(key string) (*Resource, error)
Invalidate(keys ...string)
Freshen(res *Resource, keys ...string) error
}
func NewDiskCache ¶
NewDiskCache returns a disk-backed cache
func NewMemoryCache ¶
func NewMemoryCache() Cache
NewMemoryCache returns an ephemeral cache in memory
func NewVFSCache ¶
NewVFSCache returns a cache backend off the provided VFS
type CacheConfig ¶
type CacheConfig struct {
// MaxSize is the maximum size of the cache in bytes.
// When exceeded, the oldest items will be evicted (LRU).
// Set to 0 to disable size-based eviction.
MaxSize int64
// TTL is the time-to-live for cached items.
// Items older than this will be evicted during cleanup.
// Set to 0 to disable TTL-based eviction.
TTL time.Duration
// CleanupInterval is the interval between automatic cleanup runs.
// Set to 0 to disable automatic cleanup.
CleanupInterval time.Duration
// StaleMapTTL is the TTL for stale map entries.
// Stale entries older than this will be removed.
StaleMapTTL time.Duration
}
CacheConfig holds configuration for cache behavior
func DefaultCacheConfig ¶
func DefaultCacheConfig() *CacheConfig
DefaultCacheConfig returns a CacheConfig with sensible defaults
func (*CacheConfig) Validate ¶
func (c *CacheConfig) Validate() *CacheConfig
Validate validates the configuration and applies defaults where needed
func (*CacheConfig) WithCleanupInterval ¶
func (c *CacheConfig) WithCleanupInterval(interval time.Duration) *CacheConfig
WithCleanupInterval sets the cleanup interval
func (*CacheConfig) WithMaxSize ¶
func (c *CacheConfig) WithMaxSize(size int64) *CacheConfig
WithMaxSize sets the maximum cache size
func (*CacheConfig) WithStaleMapTTL ¶
func (c *CacheConfig) WithStaleMapTTL(ttl time.Duration) *CacheConfig
WithStaleMapTTL sets the stale map TTL
func (*CacheConfig) WithTTL ¶
func (c *CacheConfig) WithTTL(ttl time.Duration) *CacheConfig
WithTTL sets the TTL for cached items
type CacheControl ¶
func ParseCacheControl ¶
func ParseCacheControl(input string) (CacheControl, error)
func ParseCacheControlHeaders ¶
func ParseCacheControlHeaders(h http.Header) (CacheControl, error)
func (CacheControl) Add ¶
func (cc CacheControl) Add(key, val string)
func (CacheControl) Has ¶
func (cc CacheControl) Has(key string) bool
func (CacheControl) String ¶
func (cc CacheControl) String() string
type CacheStats ¶
type CacheStats struct {
// TotalSize is the total size of cached items in bytes
TotalSize int64
// ItemCount is the number of cached items
ItemCount int
// StaleCount is the number of stale map entries
StaleCount int
// HitCount is the number of cache hits
HitCount int64
// MissCount is the number of cache misses
MissCount int64
}
CacheStats holds cache statistics
type CleanupResult ¶
type CleanupResult struct {
// RemovedItems is the number of items removed
RemovedItems int
// RemovedBytes is the number of bytes freed
RemovedBytes int64
// RemovedStaleEntries is the number of stale map entries removed
RemovedStaleEntries int
// Duration is how long the cleanup took
Duration time.Duration
}
CleanupResult holds the result of a cleanup operation
type ExtendedCache ¶
type ExtendedCache interface {
Cache
// Stats returns current cache statistics
Stats() CacheStats
// Cleanup runs a manual cleanup cycle
Cleanup() CleanupResult
// Purge removes all cached items
Purge() error
// Close stops the cache and cleanup goroutines
Close() error
}
ExtendedCache extends Cache with management capabilities
func NewDiskCacheWithConfig ¶
func NewDiskCacheWithConfig(dir string, config *CacheConfig) (ExtendedCache, error)
NewDiskCacheWithConfig returns a disk-backed cache with custom configuration
func NewMemoryCacheWithConfig ¶
func NewMemoryCacheWithConfig(config *CacheConfig) ExtendedCache
NewMemoryCacheWithConfig returns an ephemeral cache with custom configuration
func NewVFSCacheWithConfig ¶
func NewVFSCacheWithConfig(fs vfs.VFS, config *CacheConfig) ExtendedCache
NewVFSCacheWithConfig returns a cache backend with custom configuration
type Handler ¶
type Handler struct {
// proxy) rather than private to one (a browser-style cache).
//
// It defaults to false, which is the *private* cache profile: responses
// marked "Cache-Control: private" and responses to requests carrying an
// Authorization header are storable. Deploying a shared cache without
// setting this serves one user's response to another. Prefer
// NewSharedHandler, which cannot be forgotten.
Shared bool
// contains filtered or unexported fields
}
func NewHandler ¶
NewHandler returns a private cache handler with default options (package-level logger). Use NewSharedHandler for a cache that is shared between users, such as a reverse proxy.
func NewHandlerWithOptions ¶
func NewHandlerWithOptions(cache Cache, upstream http.Handler, opts *HandlerOptions) *Handler
NewHandlerWithOptions returns a cache handler with the given options. If opts.Logger is set, it is used for handler logging; otherwise the package-level logger is used.
func NewSharedHandler ¶
NewSharedHandler returns a cache handler configured as a shared cache: it refuses to store "Cache-Control: private" responses and responses to authorized requests unless they are explicitly marked public or carry s-maxage, and it strips headers listed in "private" before storing.
This is the constructor to use for a reverse proxy. NewHandler leaves Shared false, which is correct for a per-user cache and unsafe for a shared one.
func (*Handler) SetMetrics ¶
SetMetrics sets the recorder for this handler.
Passing nil installs NopMetrics rather than a nil interface, which would panic the first time the handler recorded anything.
type HandlerOptions ¶
type HandlerOptions struct {
Logger *logger.Logger
// StaleMarkerTTL is how long the handler remembers an invalidation for a
// Cache that does not implement StaleAt itself.
//
// It must be at least as long as that Cache retains entries: the marker is
// the only record that entries older than it are pre-mutation, so expiring
// it first republishes them as fresh. Only the implementation knows its
// own retention, which is why this is a knob rather than a constant.
//
// Zero means DefaultCacheTTL. A Cache that implements StaleAt keeps its
// own record and ignores this.
StaleMarkerTTL time.Duration
}
HandlerOptions holds optional configuration for NewHandlerWithOptions. Logger injected here is used by the handler for all debug/error logging; if nil, the package-level logger (see SetLogger) is used.
type Key ¶
type Key struct {
// contains filtered or unexported fields
}
Key represents a unique identifier for a resource in the cache
func NewRequestKey ¶
NewRequestKey generates a Key for a request.
The key is derived from the effective request URI only. A request header must never be able to choose which cache entry a response is stored under: letting the *request's* Content-Location pick the key allowed a client to park its own response under another URL's key ("GET /attacker-page" with "Content-Location: /admin"), poisoning a shared cache for every other user.
RFC 7234 does use Content-Location, but the *response's* — and only to invalidate entries, never to select a storage key. See invalidationKeys.
type Metrics ¶
type Metrics interface {
// RecordCacheHit records a cache hit for an HTTP method.
RecordCacheHit(method string)
// RecordCacheMiss records a cache miss for an HTTP method.
RecordCacheMiss(method string)
// RecordCacheSkip records a request that was not cacheable at all.
RecordCacheSkip()
// RecordUpstreamDuration records how long an upstream request took.
RecordUpstreamDuration(method string, status int, durationSeconds float64)
// RecordUpstreamError records an upstream failure by kind.
RecordUpstreamError(errorType string)
// RecordStoreOperation records an attempt to write an entry to the cache.
RecordStoreOperation(success bool)
// RecordRetrieveOperation records an attempt to read an entry from the cache.
RecordRetrieveOperation(found bool)
// SetCacheSize sets the current total size of cached bodies in bytes.
SetCacheSize(sizeBytes int64)
// SetCacheItemCount sets the current number of cached items.
SetCacheItemCount(count int)
// SetCacheStaleCount sets the current number of stale map entries.
SetCacheStaleCount(count int)
// RecordCacheEviction records one eviction, labelled by why it happened.
RecordCacheEviction(reason string)
// RecordCleanupDuration records how long a cleanup pass took.
RecordCleanupDuration(durationSeconds float64)
// UpdateCacheStats sets every gauge from a stats snapshot.
UpdateCacheStats(stats CacheStats)
}
Metrics is what the cache records through. It is an interface, and the root package deliberately provides no implementation that talks to a metrics backend: the Prometheus one lives in the prometheusmetrics subpackage, so a service that does not export metrics never links Prometheus.
Implement it yourself to record into something else -- OpenTelemetry, statsd, a test double, an expvar map. Embed NopMetrics to pick up no-op implementations of the methods you do not care about; new methods added in a future minor release will then not break your type.
func GetDefaultMetrics ¶
func GetDefaultMetrics() Metrics
GetDefaultMetrics returns the current recorder. It never returns nil: until something calls SetDefaultMetrics, it is a NopMetrics.
type NopMetrics ¶
type NopMetrics struct{}
NopMetrics discards everything recorded through it. It is the default, so the cache never has to test for a missing recorder before recording -- and so no call site can panic on a nil one.
func (NopMetrics) RecordCacheEviction ¶
func (NopMetrics) RecordCacheEviction(string)
func (NopMetrics) RecordCacheHit ¶
func (NopMetrics) RecordCacheHit(string)
func (NopMetrics) RecordCacheMiss ¶
func (NopMetrics) RecordCacheMiss(string)
func (NopMetrics) RecordCacheSkip ¶
func (NopMetrics) RecordCacheSkip()
func (NopMetrics) RecordCleanupDuration ¶
func (NopMetrics) RecordCleanupDuration(float64)
func (NopMetrics) RecordRetrieveOperation ¶
func (NopMetrics) RecordRetrieveOperation(bool)
func (NopMetrics) RecordStoreOperation ¶
func (NopMetrics) RecordStoreOperation(bool)
func (NopMetrics) RecordUpstreamDuration ¶
func (NopMetrics) RecordUpstreamDuration(string, int, float64)
func (NopMetrics) RecordUpstreamError ¶
func (NopMetrics) RecordUpstreamError(string)
func (NopMetrics) SetCacheItemCount ¶
func (NopMetrics) SetCacheItemCount(int)
func (NopMetrics) SetCacheSize ¶
func (NopMetrics) SetCacheSize(int64)
func (NopMetrics) SetCacheStaleCount ¶
func (NopMetrics) SetCacheStaleCount(int)
func (NopMetrics) UpdateCacheStats ¶
func (NopMetrics) UpdateCacheStats(CacheStats)
type Resource ¶
type Resource struct {
ReadSeekCloser
RequestTime, ResponseTime time.Time
// contains filtered or unexported fields
}
func NewResource ¶
func NewResource(statusCode int, body ReadSeekCloser, hdrs http.Header) *Resource
func (*Resource) HasExplicitExpiration ¶
func (*Resource) HasValidators ¶
func (*Resource) HeuristicFreshness ¶
func (*Resource) IsNonErrorStatus ¶
func (*Resource) LastModified ¶
func (*Resource) MustValidate ¶
func (*Resource) RemovePrivateHeaders ¶
func (r *Resource) RemovePrivateHeaders()
func (*Resource) SetStoredAt ¶
ReceivedAfter reports whether this response reached the cache after d.
It reads Proxy-Date, which the handler stamps from the LOCAL clock the moment the upstream response arrives, and only falls back to the origin's Date for a response stored without one.
Deciding this from Date alone was wrong in both directions. A response that carries no Date at all -- which a direct http.Handler upstream may well omit -- never counted as superseding anything, so an invalidated key stayed stale on every retrieval and was refetched until the marker was swept. An origin whose clock trails the cache's did the same. Proxy-Date is always present and always this machine's clock.
Receive time, not store time: an older store still in flight when a mutation lands carries a Proxy-Date from BEFORE the invalidation, so the marker still wins and the pre-mutation body is not republished as fresh.
Both stamps are HTTP dates, so this is second-granular: a replacement that arrives in the same second as the invalidation does not count as superseding it and is refetched once more. That is the safe direction to round in. SetStoredAt records when this cache wrote the entry.
func (*Resource) StoredAfter ¶
StoredAfter reports whether this cache's copy is newer than d.
It prefers the cache's OWN store time to the response's dates, because Date and Proxy-Date are HTTP dates: one-second granularity, formatted with http.TimeFormat. An invalidation marker is a full-precision time.Time, so a response stored or revalidated in the same second as the invalidation is never "after" it -- and being judged against a marker it can never clear, the entry was re-marked stale and revalidated upstream on EVERY request until the marker was swept. The store time is also ours rather than the origin's, so a trailing origin clock cannot produce the same deadlock.
Falls back to the header dates for a resource that did not come from this cache, which is the only thing available for one.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package prometheusmetrics records httpcache activity into Prometheus.
|
Package prometheusmetrics records httpcache activity into Prometheus. |