Skip to content

Commit 720ecb7

Browse files
committed
feat: cache variants
1 parent ef56ed4 commit 720ecb7

47 files changed

Lines changed: 2152 additions & 229 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,15 @@ wait0 is an ultra-fast cache-first HTTP reverse proxy written in Go that serves
3232
│ ├── revalidation/ # Revalidate and warmup orchestration
3333
│ ├── discovery/ # Sitemap discovery and URL normalization
3434
│ ├── stats/ # Metrics collector, periodic stats loop, proc probes
35+
│ ├── cachevariant/ # Cache-Variant Expr compilation and evaluation
3536
│ └── cache/ # Cache internals (RAM + LevelDB + codec)
3637
├── debug/
3738
│ ├── debug-compose.yml # Local debug stack (origin + wait0)
3839
│ └── wait0.yaml # Debug configuration example
3940
├── docs/
4041
│ ├── for-developers.md # Build, config, and operations guide
41-
│ └── api-endpoints.md # Endpoint and response reference
42+
│ ├── api-endpoints.md # Endpoint and response reference
43+
│ └── cache-variant-complexity.md # Cache variant storage/operation complexity
4244
├── scripts/
4345
│ ├── coverage.sh # Coverage helper script
4446
│ ├── publish.sh # Publishing helper script
@@ -56,6 +58,7 @@ wait0 is an ultra-fast cache-first HTTP reverse proxy written in Go that serves
5658
| internal/wait0/service_core.go | Root composition: constructs module controllers and adapters |
5759
| internal/wait0/dashboard/controller.go | Dashboard HTTP controller (Basic Auth + bridge endpoints + HTML shell) |
5860
| internal/wait0/proxy/controller.go | Main request handling path and cache hit/miss/bypass flow |
61+
| internal/wait0/cachevariant/engine.go | Cache-Variant Expr parsing, compilation, header projection, and evaluation |
5962
| internal/wait0/revalidation/controller.go | Async revalidation and warmup orchestration |
6063
| internal/wait0/cache_ram.go / cache_disk.go | Root cache facades wrapping `internal/wait0/cache` |
6164
| internal/wait0/config.go | YAML configuration loading and rule parsing |
@@ -68,6 +71,7 @@ wait0 is an ultra-fast cache-first HTTP reverse proxy written in Go that serves
6871
| README | README.md | Project landing page and quick start |
6972
| For Developers | docs/for-developers.md | Build, config, and operations guide |
7073
| API Endpoints | docs/api-endpoints.md | Endpoint and response reference |
74+
| Cache Variant Complexity | docs/cache-variant-complexity.md | Root manifests, subkeys, and operation complexity |
7175
| Docker Hub notes | DOCKERHUB.md | Alias to README for Docker Hub presentation |
7276

7377
## Build & Development Commands

README.md

Lines changed: 89 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,10 @@ Notes:
133133

134134
| Guide | Description |
135135
|-------|-------------|
136-
| [Usage and configuration](#usage-and-configuration) | [How wait0 works](#how-wait0-works), [query parameters](#query-parameter-caching), [sitemap warmup](#sitemap-warmup), and the [config reference](#config-file-reference) |
136+
| [Usage and configuration](#usage-and-configuration) | [How wait0 works](#how-wait0-works), [cache bypass](#cache-bypass), [cache variants](#cache-variants), [query parameters](#query-parameter-caching), [sitemap warmup](#sitemap-warmup), and the [config reference](#config-file-reference) |
137137
| [For Developers](docs/for-developers.md) | Build/test commands, config reference, runtime options |
138138
| [API Endpoints](docs/api-endpoints.md) | Proxy behavior, invalidation API, schemas, status codes |
139+
| [Cache variant complexity](docs/cache-variant-complexity.md) | Root/subkey storage model and operation complexity |
139140

140141
## Usage and configuration
141142

@@ -145,10 +146,86 @@ wait0 checks RAM, then disk, and waits for the origin only on a cache miss. A ca
145146
146147
wait0 caches only `GET` responses with a `2xx` status, an allowed `Content-Type`, and no `Cache-Control: no-cache` or `no-store` directive. `cachableContentType` defaults to `text/html` and `application/xhtml+xml`, including values with parameters such as `text/html; charset=utf-8`. This keeps wait0 focused on controllable dynamic SWR; static assets should normally be cached by a CDN, Nginx, and browser caching.
147148

148-
An origin response that fails these checks is served as `X-Wait0: bypass` without being stored, with `X-Wait0-Reason` explaining why. If background revalidation receives a disallowed content type, either cache-control directive, or a non-`2xx` status, the existing entry is deleted; a network error leaves it available. A stale response is reported as `X-Wait0: hit`.
149-
150149
`logging.debug_headers` controls wait0 diagnostic response headers and the markers sent to the origin during revalidation. Omit it to enable every supported header, use a subset to select individual headers, or set `debug_headers: []` to disable all diagnostics. Functional headers such as `X-Wait0-CSRF` and origin-provided `X-Wait0-Tag` are not controlled by this option.
151150

151+
### Cache bypass
152+
153+
Request-side bypass rules are useful when a matching response must never be shared. They skip cache lookup, Cache-Variant evaluation, and storage:
154+
155+
```yaml
156+
rules:
157+
- match: PathPrefix(/admin)
158+
bypass: true
159+
160+
- match: PathPrefix(/)
161+
bypassWhenCookies: ['sessionid']
162+
bypassWhenRequestHeaders: ['Authorization']
163+
```
164+
165+
`bypass: true` applies to every request matching the rule. `bypassWhenCookies` applies when any named cookie is present. `bypassWhenRequestHeaders` applies when any named request header is present; header names are matched case-insensitively, and an explicitly present empty header still triggers the bypass. Bypassed and non-`GET` requests are sent upstream as bodyless `GET` requests.
166+
167+
When diagnostic headers are enabled, request-side decisions are reported as follows:
168+
169+
| Condition | `X-Wait0` | `X-Wait0-Reason` |
170+
|-----------|-----------|------------------|
171+
| `bypass: true` | `bypass` | `bypass-rule` |
172+
| Cookie listed by `bypassWhenCookies` is present | `ignore-by-cookie` | `bypass-cookie` |
173+
| Header listed by `bypassWhenRequestHeaders` is present | `ignore-by-request-header` | `bypass-request-header` |
174+
| Request method is not `GET` | `bypass` | `non-get-method` |
175+
176+
wait0 can also fetch an origin response but decline to store it:
177+
178+
| Origin result | `X-Wait0` | `X-Wait0-Reason` |
179+
|---------------|-----------|------------------|
180+
| `Cache-Control` contains `no-cache` or `no-store` | `bypass` | `non-cacheable-cache-control` |
181+
| `Content-Type` is missing, malformed, or not allowed by `cachableContentType` | `bypass` | `non-cacheable-content-type` |
182+
| A `Cache-Variant` declaration cannot compile or evaluate | `bypass` | `cache-variant-expression-error` |
183+
| Status is not `2xx` | `ignore-by-status` | `non-cacheable-status` |
184+
| Origin request fails | `bad-gateway` | `origin-error` |
185+
186+
A non-cacheable miss response is returned to the client without being stored. If background revalidation receives a disallowed content type, either cache-control directive, or a non-`2xx` status, the existing entry is deleted; a network error leaves it available. A stale cached response is still reported as `X-Wait0: hit`.
187+
188+
### Cache variants
189+
190+
Cache variants are useful when an SSR origin renders the same URL differently by country, region, device, cookie, or another request header. The origin declares one or more `Cache-Variant` response headers containing [Expr](https://github.com/expr-lang/expr) expressions:
191+
192+
```
193+
response.append_header(
194+
'Cache-Variant',
195+
`"header('User-Agent') matches '(?i)(Android.*Mobile|iPhone|iPod|IEMobile|Windows Phone|Opera Mini)' ? 'mobile' : 'desktop'"`
196+
)
197+
198+
response.append_header(
199+
'Cache-Variant',
200+
`"let c = header('CF-IPCountry', 'XX'); c == 'CA' && header('CF-Region-Code') == 'ON' ? 'CA-ON' : c"`
201+
)
202+
```
203+
204+
Each expression must return a string. Header names and fallbacks must be string literals. `header('Name')` requires the request header to exist; `header('Name', 'fallback')` supplies a value when it does not. The optional outer double quotes shown above are accepted. Multiple declarations are evaluated in response-header order and form a Cartesian variant family: the examples can produce keys such as `mobile|CA-ON` and `desktop|US`. Combinations are created lazily as requests discover them.
205+
206+
`X-Wait0-Cache-Variant-Key` reports the ordered values joined by `|`. Origin `Cache-Variant` headers remain visible in the client response for diagnosis. Invalid declarations use the expression-error behavior listed under [Cache bypass](#cache-bypass).
207+
208+
Discovered variants are monitored and warmed at their original URL using the request-header values that selected them. For example, discovering `mobile|CA` on `/a` adds a warmup target for `/a`; internal subcache keys are never requested from the origin.
209+
210+
You can seed additional request-header combinations on every warmup loop:
211+
212+
```yaml
213+
rules:
214+
- match: PathPrefix(/)
215+
warmUp:
216+
pauseBetweenRuns: '10s'
217+
maxRequestsAtATime: 20
218+
warmupRequestHeaderPresets:
219+
CF-IPCountry: ['CA', 'US', 'UA']
220+
User-Agent: ['iPad', 'Mozilla']
221+
```
222+
223+
Preset values form a Cartesian product, so this example can make six requests per URL in addition to any distinct discovered variants. Known variant keys are deduplicated. This option can produce substantial origin traffic and cache growth; `warmUp.maxRequestsAtATime` still limits concurrency.
224+
225+
All request headers are allowed, including `Cookie` and authorization headers. wait0 persists the values of headers referenced by `header(...)` so a discovered variant can be refreshed later. Choosing sensitive headers is therefore the operator's responsibility: restrict cache/disk access and avoid expressions that retain secrets unless that storage is acceptable.
226+
227+
See [Cache variant complexity](docs/cache-variant-complexity.md) for the root-manifest, subkey, family, and warmup complexity guarantees.
228+
152229
### Query parameter caching
153230

154231
Cache keys use only the URL path by default, so `/blog`, `/blog?page=1`, and `/blog?utm_source=email` share the same cached response. The first cold request reaches the origin with its complete query and fills the shared entry; later background refreshes omit query parameters that are not in `varyByQueryParams`.
@@ -236,6 +313,8 @@ rules:
236313
priority: 2
237314
# Bypasses cache when any named cookie is present.
238315
bypassWhenCookies: ['sessionid']
316+
# Bypasses cache when any named request header is present.
317+
bypassWhenRequestHeaders: ['Authorization']
239318
# Exact media types eligible for wait0 caching. Parameters such as charset
240319
# are ignored. Defaults to the two values shown here.
241320
cachableContentType: ['text/html', 'application/xhtml+xml']
@@ -249,6 +328,10 @@ rules:
249328
pauseBetweenRuns: '10s'
250329
# Maximum concurrent requests for this warmup rule.
251330
maxRequestsAtATime: 20
331+
# Optional Cartesian header presets used on every warmup loop.
332+
warmupRequestHeaderPresets:
333+
CF-IPCountry: ['CA', 'US', 'UA']
334+
User-Agent: ['iPad', 'Mozilla']
252335
253336
logging:
254337
# Diagnostic response headers and origin revalidation markers. Omit this
@@ -261,6 +344,7 @@ logging:
261344
- X-Wait0-Discovered-By
262345
- X-Wait0-Revalidate-At
263346
- X-Wait0-Revalidate-Entropy
347+
- X-Wait0-Cache-Variant-Key
264348
# To disable every diagnostic header, replace the list above with:
265349
# debug_headers: []
266350
# Logs a stats snapshot at this interval; omit to disable.
@@ -291,6 +375,7 @@ If you do not restart between deploys, proactively refresh cache using invalidat
291375

292376
- Request pipeline: RAM cache -> disk cache -> origin.
293377
- Cache key is path-only by default.
378+
- A variant URL stores a constant-size root manifest; evaluated values select response subkeys without scanning sibling variants.
294379
- Rule field `varyByQueryParams[]` opts specific query parameters into cache identity for matching paths.
295380
- Query parameters not listed in `varyByQueryParams[]` and all fragments are ignored for cache identity.
296381
- Only `GET` requests are cache candidates.
@@ -299,3 +384,4 @@ If you do not restart between deploys, proactively refresh cache using invalidat
299384
- Rule `expiration` marks entries stale but does not evict them; stale responses are served immediately and revalidation is scheduled on a best-effort basis.
300385
- On a cache-path miss or revalidation, an origin non-`2xx` is not cached and any existing key is evicted.
301386
- Invalidation is asynchronous: accept request -> resolve keys by `paths` and `tags` -> delete keys -> recrawl in background. Path invalidation clears all cached query-aware variants for that path.
387+
- Warmup monitors discovered cache variants and may expand `warmupRequestHeaderPresets` into additional Cartesian request-header combinations.

debug/wait0.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ rules:
4343
priority: 2
4444
bypassWhenCookies:
4545
- sessionid
46+
# bypassWhenRequestHeaders:
47+
# - Authorization
4648
expiration: '10s'
4749
warmUp:
4850
pauseBetweenRuns: '10s'

0 commit comments

Comments
 (0)