Repository navigation
Releases: AshwinSathian/weir
Release list
Weir v1.1.0
Four modules at one version: the root module, observe/prom, store/valkey and caddy. Build Caddy with both pins:
go get github.com/AshwinSathian/weir@v1.1.0
xcaddy build --with github.com/AshwinSathian/weir@v1.1.0 --with github.com/AshwinSathian/weir/caddy@v1.1.0Hardening after an adversarial review of v1.0.1 (cards HRD-01 to HRD-18, docs/cards/22-hardening.md). Several fixes add exported fields, so this is a minor release. Each entry names the requirement it changes.
Changed behavior
Read these before upgrading. Each has a way back where one makes sense.
-
Limiter defaults:
Limiter.MaxQueueis 8192 (was 1024) andLimiter.MaxQueueWait5 s (was 2 s) (FR-LIM-2). The count only bounds memory; with 1024 places, 5 000 keys that expired together shed 3 850 requests that an origin answering in 20 ms would have served in 1.6 s.Retry-Afteron a shed is now 5 to 8 seconds, and a follower waits for its flight up to 40 s (Timeouts.Originplus twiceMaxQueueWait). Set both fields to get the old behavior. -
Limiter: a foreground fetch for a URL under a hit-for-miss marker may start past
Limiter.MaxPerPartitionwhile more thanReserveForegroundplusLimiter.PartitionHeadroom(new, defaultMaxPerPartition) slots are free, which is while fewer than 32 of 64 are in use at the defaults (FR-LIM-3). A busy URL that cannot be stored was held to 16 concurrent fetches with the origin idle, which failed 339 of 500 simultaneous clients. Requests without a marker, a cache-busting flood among them, are capped as before (T-11).PartitionHeadroomequal toMaxConcurrentrestores the strict cap for every fetch. -
Breaker: a foreground request refused because the half-open probe is in flight waits for it, for at most
Limiter.MaxQueueWait, and is then served or refused; at mostMaxQueue/4 wait at once (FR-CB-4). Before, 199 of 200 clients that arrived with the probe got 503 from an origin that had recovered. A request with a stale response to serve does not wait. -
Caddy: new
max_object_bytes(the adapter had no key forStorable.MaxObjectBytes, so nothing over 1 MiB could be stored) andlimiter { partition_headroom }. -
Coalescing: a follower whose
Coalesce.FollowerMaxWaitexpires without a servable stale entry keeps waiting for its flight untilTimeouts.Originplus twiceLimiter.MaxQueueWaitafter it joined, and only then fetches for itself (FR-COA-4). Before, 100 requests for one key behind an origin that answered in 15 s made 16 fetches and 84 responses 503.EvCoalesceTimeout{direct}now means the later deadline. -
Breaker: it opens only when the failures in the window come from at least
Breaker.MinPartitions(new, default 2) partitions, or when nothing in the window succeeded (FR-CB-2). A path whose half-open probe failed leaves the next probe to another path for one second (FR-CB-4). Before, 20 responses 504 from one path opened it for every path.MinPartitions: 1is the old rule; set it for an origin whose traffic is one path. -
Markers: an object over
Storable.MaxObjectByteswrites a hit-for-miss marker, and a full response that is not storable replaces the stale response the request found (FR-STO-12). Behind an over-size marker the response is streamed, and a response whoseContent-Lengthexceeds the limit is streamed without the buffered read. An error status never replaces a stored response. Cost: a URI that turnsprivateloses its stale-if-error copy at once. -
Streams:
Timeouts.StreamIdlealso ends a stream that nobody reads for that long, andweirhttp.WriteResponsegives each write of a live stream a deadline of the same length (FR-TMO-2). A consumer that pauses longer thanStreamIdlebetween reads of a streamed response is cut. -
Invalidation: on an in-process store with no keyed header, keyed cookie or experiment, an unsafe request writes its invalidation epoch only when the URI has a record or a fetch in progress (FR-INV-1), and a variant is served only while its vary spec lists it (FR-KEY-7). Before, 500 000 unsafe requests to uncached URIs sent about half of all other entries back to the origin.
-
weirhttp.TransportOriginremovesForwarded,X-Real-IpandX-Forwarded-*from requests that are passed through, by default (FR-FWD-8).XForwarded: weirhttp.XForwardedKeepis the old behavior;XForwardedSetsets them from the request. A cacheable fetch is sent as the engine built it in every mode, so a field thatForward.AlloworKey.Headersnames still reaches the origin. -
Caddy: every forwarded request has a replacer bound to itself, so
{http.request.header.X},{http.request.cookie.X}and{http.request.uri}behindweirread what the origin is sent (08 §6). Placeholders that a matcher or handler in front ofweirdefined ({http.regexp.*},{http.auth.*},mapoutputs) are empty behind it;client_placeholdersrestores the old binding. -
A
GETorHEADthat declares a body is passed through with it and never cached (FR-FWD-10). Before, the body was dropped and the answer cached. -
An encoded response is stored as if
VarynamedAccept-Encoding, so a compressed body withoutVaryis no longer served to a client that asked for another coding (FR-STO-14). -
A 304 whose entity-tag does not weakly match the stored one is discarded and the request repeated without conditionals (FR-SRV-3).
-
Closereturns 5 s after it has cancelled the engine's goroutines even when anOriginignores its context, with an error that says so (FR-LCY-2). Before, it did not return. -
A stalled or failed request body ends in
ErrRequestBodyTimeout(408) orErrRequestBody(400) and is not a breaker outcome (FR-CB-1). One path holds at most half the upload pool (FR-LIM-7). -
A 502, 503 or 504 marked
no-storeorprivatewrites no negative entry (FR-NEG-1). TheRetry-Afterof a shed request is the queue wait plus 0 to half of it in whole seconds, 2 or 3 seconds at the defaults (FR-LIM-5). -
Memory store:
MaxHardEpochsdefaults to 100 000 (was 10 000). The engine's own store and the Caddy adapter use shards of about 16 MiB, so a store above 256 MiB has more than 16 shards and its largest object is 1.6 MiB unlessStorable.MaxObjectBytesasks for more (05 §5.1). -
Hosts may contain
_(FR-VAL-1).TRACEandOPTIONShonorMax-Forwards(FR-FWD-9).
Added
Request.ClientandLimiter.MaxPerClient(0: off): a cap on one client's requests at the origin, whatever URLs they name (FR-FAIR-4).weirhttp.RequestFromfillsClientfrom the peer address; the Caddy adapter usesclient_ipand gainslimiter { max_per_client N }.Breaker.MinPartitions.Request.PassThrough, set on the request anOriginreceives for a pass-through.store.FetchTracker, an optional capability of in-process stores that the memory store implements (FR-INV-1).ErrRequestBody,ErrRequestBodyTimeout.weirhttp.XForwardedModeandTransportOrigin.XForwarded.- Response trailers of a streamed response reach the client: a response body may implement
Trailer() http.Header, and bothweirhttporigins do (FR-FWD-8). Config.KeyGeneration(): a digest of the settings that change what an unchanged key means, for snapshot file names and remote key prefixes (FR-SNP-4).memory.Shards(maxBytes, maxObject).- With
Client.HonorRevalidation, requestmax-ageandmin-freshare applied (FR-SRV-8). - Caddy key
client_placeholders.
Performance
Measured on the reference machine against v1.0.1, eight interleaved runs (docs/benchmarks.md): a hit takes 669 ns instead of 964 and allocates 608 B in 8 objects instead of 1 536 B in 12; hits from 12 goroutines take 225 ns each instead of 1 005; make load serves 1 331 k steady hits a second where it served 578 k.
- The miss-rate tracker no longer takes one mutex per cacheable request: a hit takes a read lock and never replaces a counter (FR-MR-1).
- A hit keeps its request state on the stack and does not build the forwarded header, which only a miss, a pass-through or a vary lookup reads (NFR-5).
- A request that creates a flight looks at the store once more before it fetches, so a lookup that just missed the previous flight's response no longer costs a second fetch (FR-COA-1).
- Hard-epoch lookups are skipped when no hard epoch is newer than the entry. The limiter's release walk stops once no slot is free.
observe/promkeeps the request counter of each outcome once resolved: one request event costs about 30 ns instead of 95 across 12 goroutines.- A stored response gets a
Content-Lengthwhen the origin sent none, so a hit is written with its length (FR-STO-11).weirhttp.WriteResponseno longer deep-copies the header.
Known limits, unchanged
- Coalescing is per process, and a remote store has no local tier in front of it (deferred card HRD-20).
- With
Limiter.MaxPerClientoff, which is the default, a flood of distinct paths can still take every origin slot (T-11, R-2). - An unknown method still invalidates on 2xx or 3xx, as RFC 9111 §4.4 requires;
identity;q=0still getsidentity.
Earlier entries under this heading:
- The root module no longer ships the website:
site/has its owngo.mod, which keeps it out of the module zip (about 0.16 MB smaller thanv1.0.1). No change to any Go package. - Repository:
mainandrelease/*are protected by GitHub rulesets (pull request and a greencheckformain; no force-push or deletion for both).
Weir v1.0.1
Documentation release. No change to the behavior or the API of any module; the only Go changes are two tests and one doc comment. It exists so that pkg.go.dev shows the current README.
- Documentation: a website at https://weir.ashwinsathian.com (source in
site/, built from this repository), a shorter README that links to it,CONTRIBUTING.md,CODE_OF_CONDUCT.mdand issue templates. The README's table of settings that take a protection back moved to the site's tuning page. - Site: links inside text taken from repository documents now point at the right GitHub path, and the build fails on a GitHub link to a file that is not in the repository.
- Wording corrections in docs/01 (status line, the targeted cache-control non-goal), docs/03 (a note on what its flows do not cover), the runbook (8.9 step 1 agrees with 7.8 about
caddy validate) and docs/benchmarks.md (a "Current results" section). No requirement, default or signature changed. - Documents: threat model rows T-46 to T-51 for the experiment dimensions (signing keys, header aliases,
Set-Cookieisolation, the assignment cookie on the forward, cookie tossing, exposure floods), with residual risks R-9 and R-10 and runbook advice to use{env.NAME}key placeholders and a__Host-cookie name. No behavior change. store/valkeyis tested only against Valkey 8.1 (in CI). The v1.0.0 notes below said "Valkey 7 or later" without saying what was tested; they now do.- README, runbook and the Caddy package comment name
v1.0.1in their install lines. Tags:v1.0.1,observe/prom/v1.0.1,store/valkey/v1.0.1andcaddy/v1.0.1.
Install
go get github.com/AshwinSathian/weir@v1.0.1
go get github.com/AshwinSathian/weir/observe/prom@v1.0.1
go get github.com/AshwinSathian/weir/store/valkey@v1.0.1
xcaddy build --with github.com/AshwinSathian/weir@v1.0.1 --with github.com/AshwinSathian/weir/caddy@v1.0.1The four modules are tagged together: use one version for all of them. Documentation: https://pkg.go.dev/github.com/AshwinSathian/weir@v1.0.1
Weir v1.0.0
First stable release. It supersedes the v0.1.0 tag, which predates most of the engine and cannot be used with the sub-modules.
Engine (root module)
- RFC 9111 shared-cache decisions per request, including conditional requests,
Vary,Age,Cache-Status(RFC 9211), single-range responses from cache, and targeted cache-control (RFC 9213). - Request coalescing with a follower wait bound and leader aging, TTL jitter, probabilistic early refresh, stale-while-revalidate and stale-if-error, a circuit breaker, negative caching and lazy purges (soft, hard and Cache Groups; optional eager hard purge).
- An origin concurrency limiter with per-partition fairness, an upload pool, a per-host cap and per-owner store quota for multi-tenant use, and a miss-rate anomaly detector.
- A cache key that is a security boundary: the forwarded request equals the keyed request, with strict
Varyand cookie handling and fuzzed parsers. - Memory store (S3-FIFO) with snapshots across restarts; the
store.Storeinterface and its optional capability interfaces. - Experiment-aware key dimensions (
Config.Experiments): signed assignment cookie, consent gate, up to 4 signing keys of 32 to 256 bytes,EvExposure. weirhttpadapters fornet/http.
Modules
observe/prom: Prometheus observer and collector.store/valkey: shared store for several nodes (Valkey 7 or later;maxmemory-policymust bevolatile-lfu, anothervolatile-*policy ornoeviction).caddy: theweirhandler, Caddyfile and JSON configuration, admin purge API,experimentsblock. Built against Caddy v2.11.7.
Fixed before the first release candidate
- Experiment signing keys are limited to 4 keys and 256 bytes each.
- A stored response or negative entry whose status cannot be written by an HTTP server is treated as a miss.
TransportOriginno longer panics whenhttp.DefaultTransporthas been replaced, and returns an error for a missingTarget.- An origin body whose
Readpanics is closed instead of leaked.
Changed between v1.0.0-rc.1 and v1.0.0
Key.QuerySortsorts by parameter name only and keeps the order of a repeated name's values (ids=3&ids=1is no longer rewritten toids=1&ids=3).- An unkeyed
Cache-ControlorPragmarequest field is forwarded only as a plain directive list (at most 8 lines and 256 bytes, letters, digits,-,_,=,,and space); anything else is dropped for that request. - A
Rangerequest that also carriesIf-MatchorIf-Unmodified-Sincegets the full 200 instead of a 206: Weir does not evaluate those preconditions. store.Entry.Sizecharges 48 bytes per header name and 16 per header value and vary name, so a memory store holds somewhat fewer small entries per byte ofMaxBytesand can no longer be made to hold many times its limit.store/valkey: newConfig.MaxObjectBytes(64 MiB). Larger values are never read whole and count as misses;*Storenow reports its limit, soweir.Newrejects a largerStorable.MaxObjectBytes.- The root module builds for
js/wasmandwasip1/wasm.
Requirements
- Go 1.27.2 or later for anything you deploy. Earlier 1.27 releases carry standard library vulnerabilities that
govulncheckreports againstweirhttp.
Known limits
- Pointer-light memory layout deferred (M16-02, D36): GC cost at 1M entries is about 1 µs per request on the reference machine (Apple M4 Pro; 1.76 µs on a 4-core Xeon), inside the 2 µs gate.
- The
caddymodule imports the root module'sinternal/keys, so the four modules must be used at the same version (with xcaddy, pin the root module andcaddyto it explicitly). Engine.Closewaits for origin goroutines; anOriginthat ignores its context can delay it.store/valkey: a new or flushed server stores nothing usable forMaxClockSkew+ 1 s (2 s by default) after the first request; a value at an entry key that fitsMaxObjectBytesbut is not a Weir record counts as a store error. The Caddyfile has no key forMaxObjectBytes.
Install
go get github.com/AshwinSathian/weir@v1.0.0
go get github.com/AshwinSathian/weir/observe/prom@v1.0.0
go get github.com/AshwinSathian/weir/store/valkey@v1.0.0
xcaddy build --with github.com/AshwinSathian/weir@v1.0.0 --with github.com/AshwinSathian/weir/caddy@v1.0.0The four modules are tagged together (v1.0.0, observe/prom/v1.0.0, store/valkey/v1.0.0, caddy/v1.0.0): use one version for all of them. Documentation: https://pkg.go.dev/github.com/AshwinSathian/weir@v1.0.0