Documentation
¶
Overview ¶
Package weir is a shared HTTP cache engine that sits between an HTTP server and an origin. It implements RFC 9111 caching and adds defenses against origin stampedes, origin and storage outages, and cache-key manipulation.
Usage ¶
Build an Engine with New, answer each request with Engine.Serve, and call Engine.Close on shutdown. An Origin fetches what the cache cannot answer; Serve is the only path that reaches it. The zero Config is valid and picks safe defaults, including an in-process memory store.
e, err := weir.New(weir.Config{})
if err != nil {
return err // wraps ErrInvalidConfig
}
defer e.Close(context.Background())
origin := &weirhttp.TransportOrigin{Target: originURL}
http.ListenAndServe(addr, weirhttp.Handler(e, origin))
Package weirhttp wraps an Engine for net/http, and the Caddy adapter lives in the separate module github.com/AshwinSathian/weir/caddy.
Compatibility ¶
From v1.0.0 the exported API follows semantic versioning. Construct Config and the structs inside it with field names, never positionally, because minor releases may add fields; a new field's zero value keeps the old behavior. The store.Store interface is fixed for v1, and new storage features arrive as optional capability interfaces in package store (Scrubber, Sizer and others) that the engine finds with a type assertion. Event kinds, CacheInfo.Detail values and Event.Reason strings can also gain new values in a minor release, so match them with a default case.
The design is specified in docs/ at the repository root; start with docs/README.md. Requirement IDs such as FR-COA-4 and threat IDs such as T-19 in comments point into docs/01-technical-spec.md and docs/06-threat-model.md.
Index ¶
- Variables
- func RetryAfter(err error) (time.Duration, bool)
- func StatusCode(err error) int
- func TrackingParams() []string
- type BreakerConfig
- type BreakerState
- type BypassConfig
- type CacheGroupsConfig
- type CacheInfo
- type ClientConfig
- type CoalesceConfig
- type Config
- type Engine
- func (e *Engine) Close(ctx context.Context) error
- func (e *Engine) Purge(ctx context.Context, p Purge) error
- func (e *Engine) Serve(ctx context.Context, req *Request, origin Origin) (*Response, error)
- func (e *Engine) SetMode(m Mode, ttl time.Duration) error
- func (e *Engine) Stats() EngineStats
- func (e *Engine) Warm(ctx context.Context, reqs iter.Seq[*Request], origin Origin) (WarmStats, error)
- type EngineStats
- type Event
- type EventKind
- type ExperimentConfig
- type ExperimentConsent
- type ExperimentsConfig
- type ForwardConfig
- type ForwardMode
- type FreshnessConfig
- type FwdReason
- type KeyConfig
- type LimiterConfig
- type LimitsConfig
- type MissRateConfig
- type Mode
- type NegativeConfig
- type Observer
- type Origin
- type OriginError
- type OriginFunc
- type Purge
- type PurgeMode
- type Request
- type RequestError
- type Response
- type RetryError
- type StaleReason
- type StorableConfig
- type TimeoutsConfig
- type VariantConfig
- type VaryMode
- type WarmConfig
- type WarmStats
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrInvalidConfig is returned by New for any invalid Config field. ErrInvalidConfig = errors.New("weir: invalid config") // ErrInvalidRequest means the request failed input validation (FR-VAL). // Serve returns it wrapped in *RequestError. ErrInvalidRequest = errors.New("weir: invalid request") // ErrShed means no origin slot was free within the queue budget and // nothing stale could be served. Serve returns it wrapped in *RetryError. ErrShed = errors.New("weir: origin capacity exhausted") // ErrCircuitOpen means the breaker is open and nothing stale could be // served. Serve returns it wrapped in *RetryError. ErrCircuitOpen = errors.New("weir: origin circuit open") // ErrOriginTimeout means the origin did not answer within Timeouts.Origin // (Timeouts.Background for background refresh and Warm). ErrOriginTimeout = errors.New("weir: origin timeout") // ErrMustRevalidate means a must-revalidate entry could not be validated. ErrMustRevalidate = errors.New("weir: must-revalidate response could not be validated") // ErrOnlyIfCached means an only-if-cached request found no usable stored // response. ErrOnlyIfCached = errors.New("weir: only-if-cached and no stored response") // ErrOrigin means the origin returned a transport error. Serve returns it // wrapped in *OriginError. ErrOrigin = errors.New("weir: origin error") // ErrClosed means the engine is closed. ErrClosed = errors.New("weir: engine closed") // ErrUpgradeNotSupported means a CONNECT or protocol upgrade request // reached Serve (FR-UPG-1). Adapters route those around the engine. ErrUpgradeNotSupported = errors.New("weir: connect and protocol upgrades not supported") // ErrEagerUnsupported means an eager purge wrote its epoch but the store // cannot scrub, so matching records were not deleted (FR-PRG-8). ErrEagerUnsupported = errors.New("weir: store cannot scrub; epoch written, delete skipped") )
Sentinel errors. Compare with errors.Is; the engine returns them wrapped. StatusCode maps each to the status an adapter should send (01 §4).
Functions ¶
func RetryAfter ¶
RetryAfter returns the Retry-After hint when err classifies as shed or circuit open through a *RetryError, rounded up to whole seconds and never negative. A hint wrapped inside an *OriginError belongs to the origin and is not returned.
Example ¶
package main
import (
"errors"
"fmt"
"time"
"github.com/AshwinSathian/weir"
)
func main() {
err := &weir.RetryError{Err: weir.ErrShed, After: 1500 * time.Millisecond}
d, ok := weir.RetryAfter(err)
fmt.Println(d, ok)
_, ok = weir.RetryAfter(errors.New("plain"))
fmt.Println(ok)
}
Output: 2s true false
func StatusCode ¶
StatusCode maps err to the status an adapter should send (01 §4). Unknown errors, and nil, map to 502.
The outermost recognized error in the chain decides, walking in the same order as errors.Is. An *OriginError maps to 502 whatever it wraps: an origin that returns weir errors (another engine) or its own context errors must not turn an origin failure into 400, 499, 503 or 504.
Example ¶
package main
import (
"errors"
"fmt"
"github.com/AshwinSathian/weir"
)
func main() {
fmt.Println(weir.StatusCode(nil))
fmt.Println(weir.StatusCode(errors.New("boom")))
fmt.Println(weir.StatusCode(weir.ErrClosed))
// The values below depend on the error mapping in 01 §4.
Output:
func TrackingParams ¶
func TrackingParams() []string
TrackingParams returns Key.QueryDrop patterns for the click and campaign identifiers that ad networks and mail tools append to links. They rarely change a response, and each distinct value would otherwise mint a key. Weir drops nothing by default (D30); opt in with
weir.Config{Key: weir.KeyConfig{QueryDrop: weir.TrackingParams()}}
Dropped parameters never reach the origin (FR-KEY-5), so leave out any name the origin reads. Each call returns a new slice that the caller may extend or trim.
Example ¶
package main
import (
"context"
"fmt"
"io"
"net/http"
"strings"
"sync/atomic"
"time"
"github.com/AshwinSathian/weir"
)
// newExampleOrigin counts calls and returns a response cacheable for a minute.
func newExampleOrigin(calls *atomic.Int64) weir.Origin {
return weir.OriginFunc(func(_ context.Context, req *weir.Request) (*weir.Response, error) {
calls.Add(1)
return &weir.Response{
StatusCode: http.StatusOK,
Header: http.Header{"Cache-Control": {"max-age=60"}},
Body: io.NopCloser(strings.NewReader("hello " + req.Path)),
}, nil
})
}
func exampleGet(path string) *weir.Request {
return &weir.Request{Method: http.MethodGet, Scheme: "http", Host: "example.com", Path: path, Header: http.Header{}}
}
func exampleClose(e *weir.Engine) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := e.Close(ctx); err != nil {
fmt.Println("close:", err)
}
}
func main() {
e, err := weir.New(weir.Config{
Key: weir.KeyConfig{QueryDrop: weir.TrackingParams()},
})
if err != nil {
fmt.Println("new:", err)
return
}
defer exampleClose(e)
var calls atomic.Int64
origin := newExampleOrigin(&calls)
for _, q := range []string{"utm_source=a", "utm_source=b", "fbclid=xyz"} {
req := exampleGet("/page")
req.RawQuery = q
resp, err := e.Serve(context.Background(), req, origin)
if err != nil {
fmt.Println("serve:", err)
return
}
_, _ = io.Copy(io.Discard, resp.Body)
resp.Body.Close()
}
fmt.Println("origin calls:", calls.Load())
}
Output: origin calls: 1
Types ¶
type BreakerConfig ¶
type BreakerConfig struct {
Window time.Duration // 0: 10s (10 buckets)
MinRequests int // 0: 20
FailureRatio float64 // 0: 0.5; (0, 1]
OpenFor time.Duration // 0: 5s
MaxOpenFor time.Duration // 0: max(60s, OpenFor); at least OpenFor
HalfOpenProbes int // 0: 1
CountStatus500 bool
Disable bool
}
BreakerConfig tunes the origin circuit breaker (FR-BRK).
type BreakerState ¶
type BreakerState uint8
BreakerState is the origin circuit breaker's state.
const ( BreakerClosed BreakerState = iota // requests reach the origin normally BreakerHalfOpen // a few probe requests test whether the origin recovered BreakerOpen // origin fetches fail fast until the open period ends )
BreakerState values.
type BypassConfig ¶
type BypassConfig struct {
Cookies []string
Headers []string // canonicalized
ReportStrippedCookies time.Duration // FR-OBS-5; 0: 5m; negative disables
}
BypassConfig lists request cookies and headers that skip the cache.
type CacheGroupsConfig ¶
type CacheGroupsConfig struct{ Ignore bool }
CacheGroupsConfig controls RFC 9875 cache groups. The zero value honors them. Ignore switches off Cache-Group-Invalidation, the field with which an origin response invalidates groups (FR-INV-2); set it when parties that do not trust each other share one host (T-25). Cache-Groups is read either way, so Purge by group keeps working (FR-STO-10).
type CacheInfo ¶
type CacheInfo struct {
Hit bool // answered without contacting the origin for this request
Fwd FwdReason // why the request went forward; FwdNone when Hit
Stale StaleReason // non-zero when a stale response was served
FwdStatus int // origin status when Fwd != FwdNone (304 on revalidation)
Stored bool // the forwarded response was stored
Collapsed bool // this request waited on another request's flight
TTL time.Duration // remaining freshness at send time; negative when stale
// Detail is a short label for the Cache-Status detail parameter. Serve
// sets it to one of two values: "negative" (a cached origin error
// answered) or "hit-for-miss" (the URI is known to be unstorable, so the
// request skipped coalescing). It is empty otherwise. When it is empty
// and Stale is not StaleNone, the Cache-Status header carries the stale
// reason instead: "stale-while-revalidate", "stale-if-error", "shed",
// "circuit-open" or "coalesce-timeout". New values may be added in a
// minor release, so treat an unknown value as informational.
Detail string
}
CacheInfo describes how Serve produced a response. It feeds the Cache-Status header (FR-SRV-9) and events.
type ClientConfig ¶
type ClientConfig struct{ HonorRevalidation bool }
ClientConfig controls client request directives. HonorRevalidation lets no-cache and max-age=0 reach the origin (D5).
type CoalesceConfig ¶
type CoalesceConfig struct {
LeaderMaxAge time.Duration // 0: min(10s, Timeouts.Origin); at most Timeouts.Origin
FollowerMaxWait time.Duration // 0: min(10s, Timeouts.Origin); at most Timeouts.Origin
// HitForMissTTL is how long an unstorable URI is remembered so requests
// skip coalescing. Zero means the default 30s; there is no way to set a
// zero TTL today.
HitForMissTTL time.Duration
}
CoalesceConfig bounds request coalescing (FR-COA).
type Config ¶
type Config struct {
// Store holds cached entries. Nil makes New create an in-process memory
// store capped at 40% of GOMEMLIMIT (clamped to 16 MiB to 8 GiB), or
// 256 MiB, with a logged warning, when GOMEMLIMIT is unset (FR-MEM-1).
// Close closes only a store the engine created. A store you pass in is
// yours to close, after Engine.Close returns.
Store store.Store
Key KeyConfig
Forward ForwardConfig
Bypass BypassConfig
Storable StorableConfig
Freshness FreshnessConfig
Coalesce CoalesceConfig
Limiter LimiterConfig
Breaker BreakerConfig
Negative NegativeConfig
MissRate MissRateConfig
CacheGroups CacheGroupsConfig
Client ClientConfig
Timeouts TimeoutsConfig
Warm WarmConfig
Limits LimitsConfig
Experiments ExperimentsConfig // zero value: no A/B bucketing (docs/10)
CacheStatus string // Cache-Status member name (FR-SRV-9); "": "Weir"
NoCacheStatus bool // omit the Cache-Status header (T-27)
Observer Observer
Logger *slog.Logger // nil: discard
Rand func() float64 // [0, 1); safe for concurrent use; nil: rand.Float64
}
Config configures an Engine (01 §6, 04 §1.1). The zero value is valid and yields the defaults. New copies it, so later changes to the caller's slices have no effect. Construct it and the structs inside it with field names, as minor releases may add fields.
Every boolean is false by default. A field named Disable turns off a whole subsystem (Breaker.Disable, Negative.Disable, MissRate.Disable), and a No* field turns off one feature inside a subsystem (NoJitter, NoEarlyRefresh, NoTraceHeaders, NoCacheStatus). Strip* and Ignore change how input is handled (Storable.StripSetCookie, CacheGroups.Ignore). The remaining booleans switch a behavior on: Key.QuerySort, Key.NormalizePath, Breaker.CountStatus500, MissRate.Throttle, Client.HonorRevalidation and Experiments.DownstreamKeysOnVariant. The one switch that is not a boolean is Bypass.ReportStrippedCookies, which a negative value turns off.
Numeric fields documented as "0: default" cannot be set to zero; zero selects the default, and a negative value is rejected with ErrInvalidConfig.
type Engine ¶
type Engine struct {
// contains filtered or unexported fields
}
Engine makes shared-cache decisions per request (04 §6.1). It is safe for concurrent use (FR-LCY-3).
func New ¶
New validates cfg, applies defaults and returns a ready engine. It returns an error wrapping ErrInvalidConfig for any invalid field (FR-LCY-1).
Example ¶
package main
import (
"context"
"fmt"
"time"
"github.com/AshwinSathian/weir"
)
func exampleClose(e *weir.Engine) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := e.Close(ctx); err != nil {
fmt.Println("close:", err)
}
}
func main() {
e, err := weir.New(weir.Config{})
if err != nil {
fmt.Println("new:", err)
return
}
defer exampleClose(e)
fmt.Println("engine ready")
}
Output: engine ready
func (*Engine) Close ¶
Close rejects new Serve calls with ErrClosed, waits for engine goroutines until ctx is done, then cancels the rest and waits for them (FR-LCY-2). It returns ctx's error when the grace period ran out. It closes the store only if the engine created it. Later calls wait for the first until their own ctx is done and return nil or ctx's error.
Example ¶
package main
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"strings"
"sync/atomic"
"time"
"github.com/AshwinSathian/weir"
)
// newExampleOrigin counts calls and returns a response cacheable for a minute.
func newExampleOrigin(calls *atomic.Int64) weir.Origin {
return weir.OriginFunc(func(_ context.Context, req *weir.Request) (*weir.Response, error) {
calls.Add(1)
return &weir.Response{
StatusCode: http.StatusOK,
Header: http.Header{"Cache-Control": {"max-age=60"}},
Body: io.NopCloser(strings.NewReader("hello " + req.Path)),
}, nil
})
}
func exampleGet(path string) *weir.Request {
return &weir.Request{Method: http.MethodGet, Scheme: "http", Host: "example.com", Path: path, Header: http.Header{}}
}
func main() {
e, err := weir.New(weir.Config{})
if err != nil {
fmt.Println("new:", err)
return
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
fmt.Println("close:", e.Close(ctx))
var calls atomic.Int64
_, err = e.Serve(ctx, exampleGet("/"), newExampleOrigin(&calls))
fmt.Println("serve after close:", errors.Is(err, weir.ErrClosed), "origin calls:", calls.Load())
}
Output: close: <nil> serve after close: true origin calls: 0
func (*Engine) Purge ¶ added in v1.0.0
Purge makes the entries p names stale (PurgeSoft) or unreachable (PurgeHard) by writing one epoch per tag (FR-PRG-1, 04 §7). Its cost is the number of tags, not the number of matching entries, which are judged at their next lookup (FR-PRG-4). All of p is validated before the first write; invalid input returns an error matching ErrInvalidRequest and purges nothing. That includes a group name no stored response can carry: one longer than Limits.MaxGroupBytes or with a byte outside 0x20 to 0x7E (FR-STO-10). A store error stops the call and is returned: epochs already written stay, EvPurge is still emitted for them, and repeating the call is safe; with Eager, the tags written before the failure are not scrubbed. The memory store refuses a hard purge of a new URL or group past its MaxHardEpochs with an error matching store.ErrUnavailable (05 E-6); purge with All instead. Such a refusal does not count toward the store breaker.
A purge shows at the next lookup. With the memory store a soft purge by URL or group is timed to the next whole second (05 E-7), so its stale-while-revalidate and stale-if-error windows can run up to 1 s long, and a refresh sent in that second may be repeated once.
Eager with PurgeSoft is invalid input. Eager with PurgeHard writes its epochs, then deletes the matching records when the store implements store.Scrubber (FR-PRG-8); otherwise it returns ErrEagerUnsupported with the epochs in place. A scrub error is returned too, and the entries stay unreachable.
Example ¶
package main
import (
"context"
"fmt"
"io"
"net/http"
"strings"
"sync/atomic"
"time"
"github.com/AshwinSathian/weir"
)
// newExampleOrigin counts calls and returns a response cacheable for a minute.
func newExampleOrigin(calls *atomic.Int64) weir.Origin {
return weir.OriginFunc(func(_ context.Context, req *weir.Request) (*weir.Response, error) {
calls.Add(1)
return &weir.Response{
StatusCode: http.StatusOK,
Header: http.Header{"Cache-Control": {"max-age=60"}},
Body: io.NopCloser(strings.NewReader("hello " + req.Path)),
}, nil
})
}
func exampleGet(path string) *weir.Request {
return &weir.Request{Method: http.MethodGet, Scheme: "http", Host: "example.com", Path: path, Header: http.Header{}}
}
func exampleClose(e *weir.Engine) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := e.Close(ctx); err != nil {
fmt.Println("close:", err)
}
}
func main() {
e, err := weir.New(weir.Config{})
if err != nil {
fmt.Println("new:", err)
return
}
defer exampleClose(e)
var calls atomic.Int64
origin := newExampleOrigin(&calls)
fetch := func() {
resp, err := e.Serve(context.Background(), exampleGet("/page"), origin)
if err != nil {
fmt.Println("serve:", err)
return
}
_, _ = io.Copy(io.Discard, resp.Body)
resp.Body.Close()
}
fetch()
fetch()
fmt.Println("calls before purge:", calls.Load())
err = e.Purge(context.Background(), weir.Purge{
Mode: weir.PurgeHard,
URLs: []string{"http://example.com/page"},
})
if err != nil {
fmt.Println("purge:", err)
return
}
fetch()
fmt.Println("calls after purge:", calls.Load())
}
Output: calls before purge: 1 calls after purge: 2
func (*Engine) Serve ¶
Serve answers one request. It never returns (nil, nil). On a nil error the caller owns resp.Body and must close it. A non-nil error means no response could be produced; StatusCode(err) gives the status an adapter should send.
Example ¶
package main
import (
"context"
"fmt"
"io"
"net/http"
"strings"
"sync/atomic"
"time"
"github.com/AshwinSathian/weir"
)
// newExampleOrigin counts calls and returns a response cacheable for a minute.
func newExampleOrigin(calls *atomic.Int64) weir.Origin {
return weir.OriginFunc(func(_ context.Context, req *weir.Request) (*weir.Response, error) {
calls.Add(1)
return &weir.Response{
StatusCode: http.StatusOK,
Header: http.Header{"Cache-Control": {"max-age=60"}},
Body: io.NopCloser(strings.NewReader("hello " + req.Path)),
}, nil
})
}
func exampleGet(path string) *weir.Request {
return &weir.Request{Method: http.MethodGet, Scheme: "http", Host: "example.com", Path: path, Header: http.Header{}}
}
func exampleClose(e *weir.Engine) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := e.Close(ctx); err != nil {
fmt.Println("close:", err)
}
}
func main() {
e, err := weir.New(weir.Config{})
if err != nil {
fmt.Println("new:", err)
return
}
defer exampleClose(e)
var calls atomic.Int64
origin := newExampleOrigin(&calls)
for range 2 {
resp, err := e.Serve(context.Background(), exampleGet("/page"), origin)
if err != nil {
fmt.Println("serve:", err)
return
}
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
fmt.Printf("%d %q hit=%v\n", resp.StatusCode, body, resp.Cache.Hit)
}
fmt.Println("origin calls:", calls.Load())
}
Output: 200 "hello /page" hit=false 200 "hello /page" hit=true origin calls: 1
func (*Engine) SetMode ¶
SetMode switches the incident mode for ttl, after which it reverts to ModeNormal (FR-MODE-1, D33). ttl must be in (0, 24 h]. Modes are not persisted. It returns an error wrapping ErrInvalidConfig for a bad mode or ttl. Expiry is noticed, and its EvMode emitted, by the first Serve after the ttl runs out.
func (*Engine) Stats ¶ added in v1.0.0
func (e *Engine) Stats() EngineStats
Stats returns point-in-time gauges for exporters: in-flight origin fetches, queued fetches, breaker state, and store bytes when the store reports them (04 §9.2). Both limiter pools count (FR-LIM-7). It only reads: an open breaker whose period has ended reports half-open without moving, so Stats never calls the Observer (FR-OBS-2) and is safe inside a collector's lock.
func (*Engine) Warm ¶
func (e *Engine) Warm(ctx context.Context, reqs iter.Seq[*Request], origin Origin) (WarmStats, error)
Warm fetches each request through the normal path at background priority and stores what is storable (FR-WRM-1, T6.4). Warm.Concurrency workers fetch at once; each waits for a limiter slot outside the foreground reserve until ctx ends. A request with a fresh entry, or whose key another request's flight fetches and stores, counts as skipped (FR-WRM-2). Requests Weir would never store (unsafe methods, Range, only-if-cached) are not sent and count as not stored. Warm stops at the first ctx cancellation and returns ctx's error. Once Close starts, it takes no new request, lets running fetches finish within Close's grace, and returns ErrClosed.
With experiments configured, Warm stores the control variant (or the winner) of each matching experiment whatever Cookie the request carries (docs/10 §4); other variants fill on demand.
Example ¶
package main
import (
"context"
"fmt"
"io"
"net/http"
"slices"
"strings"
"sync/atomic"
"time"
"github.com/AshwinSathian/weir"
)
// newExampleOrigin counts calls and returns a response cacheable for a minute.
func newExampleOrigin(calls *atomic.Int64) weir.Origin {
return weir.OriginFunc(func(_ context.Context, req *weir.Request) (*weir.Response, error) {
calls.Add(1)
return &weir.Response{
StatusCode: http.StatusOK,
Header: http.Header{"Cache-Control": {"max-age=60"}},
Body: io.NopCloser(strings.NewReader("hello " + req.Path)),
}, nil
})
}
func exampleGet(path string) *weir.Request {
return &weir.Request{Method: http.MethodGet, Scheme: "http", Host: "example.com", Path: path, Header: http.Header{}}
}
func exampleClose(e *weir.Engine) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := e.Close(ctx); err != nil {
fmt.Println("close:", err)
}
}
func main() {
e, err := weir.New(weir.Config{})
if err != nil {
fmt.Println("new:", err)
return
}
defer exampleClose(e)
var calls atomic.Int64
origin := newExampleOrigin(&calls)
reqs := slices.Values([]*weir.Request{exampleGet("/a"), exampleGet("/b")})
st, err := e.Warm(context.Background(), reqs, origin)
fmt.Printf("%+v %v\n", st, err)
st, err = e.Warm(context.Background(), reqs, origin)
fmt.Printf("%+v %v\n", st, err)
}
Output: {Fetched:2 Skipped:0 NotStored:0 Failed:0} <nil> {Fetched:0 Skipped:2 NotStored:0 Failed:0} <nil>
type EngineStats ¶
type EngineStats struct {
Inflight int // origin fetches holding limiter slots
Queued int // fetches waiting for a slot
BreakerState BreakerState // BreakerClosed, BreakerHalfOpen or BreakerOpen
StoreBytes int64 // -1 when the store does not implement store.Sizer
}
EngineStats is a point-in-time snapshot for gauges, polled by exporters.
type Event ¶
type Event struct {
Kind EventKind
Time time.Time
Partition string // truncated to 256 bytes; empty for engine-wide events; never a metric label
Duration time.Duration // fetch, wait, or open duration where relevant
Status int
Reason string // fixed vocabulary per kind, safe as a metric label
Info CacheInfo // for EvRequest
// Set only on EvExposure. Experiment and Variant are operator
// configuration. Visitor is a 16-character hex prefix of SHA-256 of the
// visitor id.
Experiment string // experiment name, from configuration
Variant string // variant name, from configuration
Visitor string // 16 hex characters; unbounded, never a metric label
}
Event is one engine event. Reason uses a fixed vocabulary per Kind and never contains request data.
type EventKind ¶
type EventKind uint8
EventKind identifies an event. Its String form is safe as a metric label.
const ( EvRequest EventKind = iota + 1 // every Serve return EvFetchStart // before every Origin.Fetch EvFetchEnd // after every Origin.Fetch EvCoalesceJoin // a request joined an existing flight EvCoalesceTimeout // a wait on a flight expired and the request stopped waiting EvShed // the limiter refused EvStaleServed // a stale response was served EvRefreshDropped // a background refresh was not started EvBreakerState // breaker transition EvStoreError // the store guard saw store.ErrUnavailable EvStoreBreaker // the store guard opened or closed EvKeyRejected // request validation failed EvNotStored // storability failed EvVaryOverflow // the variant cap was reached EvNegativeServed // a negative entry was used EvPurge // Purge or invalidation wrote epochs EvMissRateAnomaly // a window closed with an anomalous partition EvEvict // the memory store evicted EvMode // the incident mode changed (FR-MODE-1) EvExposure // a visitor was first seen in an experiment variant (docs/10 E1) )
EventKind values (04 §9.2).
type ExperimentConfig ¶ added in v1.0.0
type ExperimentConfig struct {
Name string
Salt string // decorrelates experiments; change it to reshuffle visitors
Winner string // a variant name ends the experiment for all visitors; "" keeps it running
Host string // normalized, lower case; "" matches any host
Path string // exact path, or a prefix when it ends in "*"; "" matches any
Variants []VariantConfig
}
ExperimentConfig is one static experiment. The first variant is the control arm, served when consent is missing.
type ExperimentConsent ¶ added in v1.0.0
type ExperimentConsent struct {
Cookie string // consent cookie name, for example "cmp_consent"
Value string // an element of its value, for example "analytics=yes"; elements are split on '&', '|' and ','
}
ExperimentConsent gates bucketing (E6). With Cookie set and the cookie absent or lacking Value, a visitor gets the control variant, no cookie is set and no exposure is recorded. With the zero value, visitors are bucketed without a gate and New logs a warning.
type ExperimentsConfig ¶ added in v1.0.0
type ExperimentsConfig struct {
Cookie string // assignment cookie name; "": "weir_v"
// SigningKeys sign and verify the assignment cookie: 1 to 4 keys of 32
// to 256 bytes each. The first signs and all verify, which allows
// rotation. New returns an error wrapping ErrInvalidConfig for any other
// count or length, and when Experiments is set without a key.
SigningKeys [][]byte
Header string // forwarded variant header; "": "Weir-Variant"; no underscore
MaxActive int // experiments that may apply to one request; 0: 4; at most 8
Consent ExperimentConsent
Experiments []ExperimentConfig
// DownstreamKeysOnVariant declares that every cache in front of Weir keys
// on the variant (Weir-Variant, or the experiment cookie). Only then do
// matched responses keep public, s-maxage and CDN-Cache-Control, except
// the one that sets the assignment cookie; by default all are rewritten
// to private (E7, T-36).
DownstreamKeysOnVariant bool
}
ExperimentsConfig turns on A/B bucketing at the cache (docs/10 §3). The zero value turns it off. Each matching experiment adds a (experiment, variant) dimension to the cache key and sets Header on the forwarded request, so the origin sees exactly what was keyed (INV-1). A client's own Header is always discarded (T-35).
type ForwardConfig ¶
type ForwardConfig struct {
Mode ForwardMode
Allow []string // canonicalized; added to the trace headers
NoTraceHeaders bool // do not forward traceparent, tracestate, X-Request-Id (they are forwarded by default)
}
ForwardConfig controls which request headers reach the origin (FR-FWD).
type ForwardMode ¶
type ForwardMode uint8
ForwardMode selects the forwarding policy.
const ( ForwardStrict ForwardMode = iota ForwardAll // logs a warning at New )
ForwardMode values.
type FreshnessConfig ¶
type FreshnessConfig struct {
Jitter float64 // 0: 0.10 unless NoJitter; [0, 0.5]
NoJitter bool
JitterMinLifetime time.Duration // 0: 10s
EarlyRefreshBeta float64 // 0: 1.0
NoEarlyRefresh bool
// HeuristicFraction is the share of the time since Last-Modified used as
// heuristic freshness. Zero means the default 0.1; there is no way to
// set zero today (use Freshness.HeuristicMax to bound the result).
HeuristicFraction float64
HeuristicMax time.Duration // 0: 1h
DefaultTTL time.Duration // 0: none
DefaultStaleWhileRevalidate time.Duration // 0: none
DefaultStaleIfError time.Duration // 0: none
Keep time.Duration // 0: 5m; only entries with a validator
}
FreshnessConfig tunes lifetimes, jitter and early refresh (FR-FRS).
type FwdReason ¶
type FwdReason uint8
FwdReason is why a request went to the origin. Values mirror the fwd parameter of RFC 9211 §2.2.
const ( FwdNone FwdReason = iota // not forwarded FwdBypass // a bypass rule matched (fwd=bypass) FwdMethod // the method is not cacheable (fwd=method) FwdURIMiss // no stored response for the URI (fwd=uri-miss) FwdVaryMiss // stored responses exist, none match Vary (fwd=vary-miss) FwdRequest // the request's directives forced forwarding (fwd=request) FwdStale // the stored response was stale (fwd=stale) )
FwdReason values.
type KeyConfig ¶
type KeyConfig struct {
QueryDrop []string // exact names or "prefix*"
QueryKeep []string // allowlist; empty means keep all
QuerySort bool
NormalizePath bool
Headers []string // canonicalized with http.CanonicalHeaderKey
Cookies []string // order is the forwarded order
Vary VaryMode
VaryAllow []string // canonicalized; required for sensitive Vary names
MaxVaryHeaders int // 0: 8
MaxVariants int // 0: 8
AcceptEncoding []string // empty: ["gzip"]; lowercased
}
KeyConfig controls which request parts form the cache key (FR-KEY).
type LimiterConfig ¶
type LimiterConfig struct {
MaxConcurrent int // 0: 64
// MaxQueue is the number of fetches that may wait for a slot. Zero means
// the default 1024; there is no way to set a zero-length queue today.
MaxQueue int
MaxQueueWait time.Duration // 0: 2s
MaxPerPartition int // 0: 16; clamped to MaxConcurrent
// ReserveForeground is the number of slots background refresh and Warm
// may not use. Zero means MaxConcurrent/4, at least 1 when MaxConcurrent
// is 2 or more; there is no way to reserve none then. It must stay below
// MaxConcurrent.
ReserveForeground int
MaxPerHost int // 0: off (FR-FAIR-1); caps in-flight fetches per normalized host, in each pool
MaxUpload int // 0: MaxConcurrent/4, at least 1; requests with a body use this separate pool (D25)
}
LimiterConfig bounds origin concurrency (FR-LIM).
type LimitsConfig ¶
type LimitsConfig struct {
MaxPathBytes int // 0: 8192
MaxQueryBytes int // 0: 8192
MaxQueryParams int // 0: 256
MaxKeyedHeaderBytes int // 0: 1024
MaxGroups int // 0: 32
MaxGroupBytes int // 0: 128
}
LimitsConfig bounds request input (FR-VAL-1, FR-VAL-3, FR-STO-10).
type MissRateConfig ¶
type MissRateConfig struct {
Window time.Duration // 0: 10s
TopK int // 0: 64
MinMisses int // 0: 500
MinRatio float64 // 0: 0.9; at most 1
// Throttle caps an anomalous partition at one origin fetch for the
// following window (FR-MR-3). The tracker cannot tell a flood from a
// busy path whose responses are never stored or always revalidated
// (no-store, no-cache, max-age=0, Authorization or Range traffic): past MinMisses such a path is throttled too, and
// most of its requests are then shed. Run with Throttle off first:
// every partition the warning names would be capped. Give paths known
// to be uncacheable a bypass rule; bypassed requests are not counted.
// No effect with Disable.
Throttle bool
Disable bool
}
MissRateConfig tunes the miss-rate detector (FR-MR-*).
type NegativeConfig ¶
NegativeConfig tunes negative caching of origin errors (FR-NEG).
type Observer ¶
type Observer interface{ Observe(Event) }
Observer receives engine events (04 §9). Observe is called synchronously on the request path, so implementations must be fast, must not block, must not panic and must be safe for concurrent use. The engine does not recover an Observer panic: it ends the request or flight it ran in, and the origin response that call held is closed only when the garbage collector finalizes its connection.
type Origin ¶
Origin produces responses for forwarded requests. Fetch must honor ctx: return promptly once ctx is done, and make body reads fail after that. Close depends on it (FR-LCY-2). Fetch may be called after the request that triggered it has finished (background refresh), and concurrently.
type OriginError ¶
type OriginError struct {
Err error
}
OriginError wraps a transport error from Origin.Fetch. It matches ErrOrigin under errors.Is and unwraps to Err.
func (*OriginError) Is ¶
func (e *OriginError) Is(target error) bool
Is reports whether target is ErrOrigin.
func (*OriginError) Unwrap ¶
func (e *OriginError) Unwrap() error
Unwrap returns the origin's error.
type OriginFunc ¶
OriginFunc adapts a function to the Origin interface.
type Purge ¶
type Purge struct {
Mode PurgeMode // PurgeSoft (zero value) or PurgeHard
All bool
URLs []string // absolute http(s) URLs; parsed and rewritten exactly like requests
Origin string // "scheme://host[:port]"; required when Groups is non-empty
Groups []string
Eager bool // with PurgeHard, also delete matching records now, if the store implements store.Scrubber
}
Purge describes what Engine.Purge invalidates (FR-PRG-1). Invalid input rejects the whole call before any epoch is written.
type Request ¶
type Request struct {
Method string // exactly as received; methods are case-sensitive
Scheme string // "http" or "https"
Host string // authority (host[:port]) the client addressed
Path string // origin-form path, still percent-encoded, as received
RawQuery string // query without the leading '?', as received
Header http.Header // never mutated by Weir
Body io.ReadCloser // nil for GET and HEAD; passed through for other methods
}
Request is one client request as the adapter received it (01 §4).
type RequestError ¶
type RequestError struct {
Reason string
}
RequestError reports why a request failed validation. It matches ErrInvalidRequest under errors.Is.
func (*RequestError) Unwrap ¶
func (e *RequestError) Unwrap() error
Unwrap returns ErrInvalidRequest.
type Response ¶
type Response struct {
StatusCode int
Header http.Header
Body io.ReadCloser // never nil on responses returned by Serve
// Cache describes how Serve produced the response. Serve sets it; an
// Origin's value is ignored.
Cache CacheInfo
}
Response is a response from an Origin or from Serve. The receiver of a Response from Serve owns Body and must close it.
type RetryError ¶
type RetryError struct {
Err error // ErrShed or ErrCircuitOpen
After time.Duration // RetryAfter rounds it up to whole seconds
}
RetryError carries a Retry-After hint for shed and circuit-open errors.
type StaleReason ¶
type StaleReason uint8
StaleReason is why a stale response was served.
const ( StaleNone StaleReason = iota // the response was not stale StaleWhileRevalidate // within stale-while-revalidate StaleIfError // the origin failed, within stale-if-error StaleShed // the limiter shed the fetch StaleCircuitOpen // the breaker was open StaleCoalesceTimeout // the wait on another request's flight timed out )
StaleReason values.
type StorableConfig ¶
type StorableConfig struct {
Statuses []int // nil: default set; never 206, 304, 500, 502, 503, 504
MaxObjectBytes int64 // 0: 1 MiB, body plus headers
StripSetCookie bool
StreamTypes []string // Content-Type media types streamed like text/event-stream (FR-STR-1)
}
StorableConfig limits what may be stored (FR-STO).
type TimeoutsConfig ¶
type TimeoutsConfig struct {
Origin time.Duration // 0: 30s
Background time.Duration // 0: 30s; background refresh and Warm (FR-TMO-1)
Store time.Duration // 0: 50ms (remote stores only)
StreamIdle time.Duration // 0: 60s; bounds a streamed body by idle time, not total time (D26)
}
TimeoutsConfig bounds origin and store calls (FR-TMO).
type VariantConfig ¶ added in v1.0.0
VariantConfig is one arm of an experiment. Weight is relative to the other arms.
type WarmConfig ¶
type WarmConfig struct{ Concurrency int } // 0: min(4, MaxConcurrent-ReserveForeground); at most that
WarmConfig tunes Engine.Warm.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
caddy
module
|
|
|
examples
|
|
|
weirproxy
command
Command weirproxy is a caching reverse proxy built from weir and weirhttp with default settings:
|
Command weirproxy is a caching reverse proxy built from weir and weirhttp with default settings: |
|
internal
|
|
|
breaker
Package breaker is the engine's origin circuit breaker: a failure ratio over a rolling window with a minimum volume, counting gateway failures only (docs/04-lld.md §8.3, docs/02-architecture.md ADR-7).
|
Package breaker is the engine's origin circuit breaker: a failure ratio over a rolling window with a minimum volume, counting gateway failures only (docs/04-lld.md §8.3, docs/02-architecture.md ADR-7). |
|
coalesce
Package coalesce holds the flight table that lets concurrent misses for one key share a single origin fetch (docs/04-lld.md §8.1, docs/03-hld.md §3.3).
|
Package coalesce holds the flight table that lets concurrent misses for one key share a single origin fetch (docs/04-lld.md §8.1, docs/03-hld.md §3.3). |
|
experiments
Package experiments holds the parts of A/B experiment bucketing that need no engine: the configuration and its load-time checks, the signed visitor cookie, and the deterministic variant pick (docs/10-experiments-spec.md §2 to §4, threat T-35).
|
Package experiments holds the parts of A/B experiment bucketing that need no engine: the configuration and its load-time checks, the signed visitor cookie, and the deterministic variant pick (docs/10-experiments-spec.md §2 to §4, threat T-35). |
|
httpcc
Package httpcc parses and evaluates HTTP caching fields (RFC 9111): Cache-Control directives, lifetimes and entry evaluation (docs/04-lld.md §4).
|
Package httpcc parses and evaluates HTTP caching fields (RFC 9111): Cache-Control directives, lifetimes and entry evaluation (docs/04-lld.md §4). |
|
keys
Package keys validates requests, normalizes the inputs that reach the cache key and rewrites the forwarded request to match (docs/04-lld.md §3).
|
Package keys validates requests, normalizes the inputs that reach the cache key and rewrites the forwarded request to match (docs/04-lld.md §3). |
|
limiter
Package limiter bounds concurrent origin fetches: a global cap, a cap per partition, a foreground reserve and a bounded FIFO queue (docs/04-lld.md §8.2, docs/02-architecture.md ADR-8).
|
Package limiter bounds concurrent origin fetches: a global cap, a cap per partition, a foreground reserve and a bounded FIFO queue (docs/04-lld.md §8.2, docs/02-architecture.md ADR-8). |
|
missrate
Package missrate finds partitions whose requests are nearly all misses: a Space-Saving summary of requests and misses per partition over a fixed window, in O(TopK) memory whatever the number of partitions (docs/04-lld.md §8.4, docs/02-architecture.md ADR-9).
|
Package missrate finds partitions whose requests are nearly all misses: a Space-Saving summary of requests and misses per partition over a fixed window, in O(TopK) memory whatever the number of partitions (docs/04-lld.md §8.4, docs/02-architecture.md ADR-9). |
|
sfv
Package sfv parses the HTTP structured fields (RFC 9651) that Weir reads from origin responses: the List of Strings carried by Cache-Groups and Cache-Group-Invalidation (docs/04-lld.md §8.5), and the Dictionary of targeted cache control fields such as CDN-Cache-Control (§13.2).
|
Package sfv parses the HTTP structured fields (RFC 9651) that Weir reads from origin responses: the List of Strings carried by Cache-Groups and Cache-Group-Invalidation (docs/04-lld.md §8.5), and the Dictionary of targeted cache control fields such as CDN-Cache-Control (§13.2). |
|
testorigin
Package testorigin is a programmable weir.Origin for engine and load tests (docs/07-testing-strategy.md §3).
|
Package testorigin is a programmable weir.Origin for engine and load tests (docs/07-testing-strategy.md §3). |
|
Package loadtest holds the real-time load and adversarial scenarios of docs/07-testing-strategy.md §9.
|
Package loadtest holds the real-time load and adversarial scenarios of docs/07-testing-strategy.md §9. |
|
observe
|
|
|
prom
module
|
|
|
Package store defines the storage contract the engine caches through: the Store interface, the Entry record, purge epochs and the optional capability interfaces.
|
Package store defines the storage contract the engine caches through: the Store interface, the Entry record, purge epochs and the optional capability interfaces. |
|
memory
Package memory is the default in-process store: sharded, byte-weighted S3-FIFO (ADR-6).
|
Package memory is the default in-process store: sharded, byte-weighted S3-FIFO (ADR-6). |
|
storetest
Package storetest is the conformance suite for store.Store implementations (docs/05-storage-interface-spec.md section 8).
|
Package storetest is the conformance suite for store.Store implementations (docs/05-storage-interface-spec.md section 8). |
|
valkey
module
|
|
|
Package weirhttp adapts a weir.Engine to net/http: middleware and a handler that serve requests through the engine, conversions between net/http and weir types, and an Origin backed by an http.RoundTripper.
|
Package weirhttp adapts a weir.Engine to net/http: middleware and a handler that serve requests through the engine, conversions between net/http and weir types, and an Origin backed by an http.RoundTripper. |