Repository navigation
v1.0.1
ONRouter 1.0.1 — three config-and-cache semantics gaps closed, each with the evidence bar set on 1.0.0: a disk cold tier for proxy_cache, conditional revalidation on expiry, and a REWRITE phase at server level.
What's new
proxy_cache on disk: proxy_cache_path <dir> … disk=on
Until now proxy_cache_path accepted a path and used only keys_zone, so a restart dropped every cached response and levels / max_size / inactive were decoration. 1.0.1 gives the response cache the same cold tier that lua_shared_dict disk= already has — the same store code, the same accounting, a separate metric family:
proxy_cache_path logs/cache_disk keys_zone=big:64m disk=on max_size=2g max_value=1m;- Whatever LRU pushes out of
keys_zonewhile still fresh is written asynchronously: one writer thread per zone, temp+rename, self-describing entries with CRC. The request path never waits on the disk. - The next request for that key is served from the tier and promoted back into memory, so the second hit is memory-only.
X-Cache-StatusstaysHIT— nginx's vocabulary has no "disk hit" and clients and front proxies parse that value — and the tier is reported byX-Cache-Tier: disk. - A restart rebuilds the index by scanning the directory.
onrouter_proxy_cache_disk_operations_total{zone=…,op="warm_recovered"}is the count it recovered. - Opt-in. A zone without
disk=onnever creates its directory; behaviour is byte-identical to 1.0.0. - Only fresh entries, only entries worth keeping.
spill_after(default 1) is the gate, and withproxy_cache_learningenabled it reads the same TinyLFU sketch as memory admission — so one-shot scan traffic can neither enter memory nor fill a disk. Responses abovemax_value(default 4 MiB) stay in memory. - Corruption never becomes a response. An entry failing magic / version / CRC / length is dropped and counted in
corrupt, and the request goes to the origin. The payload format carries its own version and refuses a lying length field instead of trusting it into an allocation. invalidatealso forgets the cold copy, otherwise a purge would resurrect on the next read.cache_governor_tier deepnow spillsproxy_cachevictims into the tier when one exists, so reclaiming memory no longer has to mean throwing the cache away.-tprints the per-zone tier (root, cap,levels, gate, blob cap) the way the split and schedule self-descriptions do, and namesmin_free_size=/use_temp_path=as unimplemented rather than ignoring them in silence. Adisk=onthat cannot open its directory fails the config load instead of quietly downgrading; hot reload drains the old store's queue before opening the new one, so a reload never leaves two stores on one directory.
This is a cold tier, not nginx's full write-through disk cache: an object that never left memory is not on disk. Size keys_zone for the hot set, don't lean on max_size.
proxy_cache_revalidate on; — ask, don't resend
An expired entry used to mean "fetch the whole object again". Now the gateway takes the cached entry's own ETag / Last-Modified to the origin; a 304 refreshes the expiry, replays the stored body and reports X-Cache-Status: REVALIDATED. What is saved is the second transfer of an object nobody changed — not the round trip, so this is not a substitute for staying at the origin (that would be proxy_cache_background_update, still open, see below).
Two properties are deliberate: the client's own If-* headers are replaced on this path, because "the copy in our cache is unchanged" is not "the copy in your browser is unchanged"; and headers absent from the 304 are kept, never dropped, so a content-type cannot vanish from a revalidated entry.
REWRITE at server level
return / rewrite / set / if belong to nginx's REWRITE phase, which runs before location matching. ONRouter executed that phase only inside a location, so a server-level one produced a -t warning and was skipped — which made the plainest HTTPS-redirect vhost in the world,
server {
listen 80;
server_name www.example.com;
return 301 https://$host$request_uri;
}a config that passes -t and then answers 404. It now returns 301. Server-level set/rewrite also change which location matches, as they do in nginx.
One asymmetry to know: server-level return runs before access control (nginx-consistent — a redirect-only vhost has no location to run it after), while a location-level return still runs after allow/deny/auth_basic/access_by_*. That difference is ONRouter's deliberate choice, documented in the capability matrix; the two levels are not interchangeable.
Also fixed on the way: return never expanded variables in its body or redirect URL, so return 301 https://$host$request_uri sent the literal $host. Variables are now interpolated, and unrecognised names stay literal.
Metrics
onrouter_proxy_cache_disk_{entries,bytes,value_bytes,capacity_bytes,pending_jobs,pending_bytes} and onrouter_proxy_cache_disk_operations_total{zone,op} with op in spill / spill_skipped / cold_hit / promote / evict / expire / corrupt / cold_error / never_read / warm_recovered. The family is absent when no zone declares disk=on. Shared-dictionary tiers keep their own onrouter_dict_disk_* family so response-cache bytes are never read as dictionary bytes.
Verification
cargo test --workspace→ 503/503 (490onrouter-server+ 13onrouter-config); clippy with no new findings.- New
conf/proxy_disk.conf+tools/proxy_disk_e2e.py→ 39/39 on real traffic with three zones of the same 8-entrykeys_zone(disk=on spill_after=0,disk=on spill_after=3, nodisk=control): evicted-fresh entries really land in the directory asORDKrecords, a cold hit returnsHIT+X-Cache-Tier: diskwith the backend counter unchanged — the only evidence that matters — promotion then removes the tier marker, the control zone's directory is never created, the gated zone writes nothing for one-shot traffic, flipping a byte in every cold file yields correct bodies with 12 refetches and acorruptcount that moves, and after a stop/startwarm_recoveredequals the index size reported before shutdown. - No regression in the cache suites it could have broken:
proxy_cache13/13,cache_advanced32/32,cache_header_gap25/25,disk_cache53/53,learning_cache34/34,still_online25/25,governor32/32 — 398 assertions across 13 suites in total, all green. tools/set_if_e2e.pyis now 34/34 andtools/proxy_cache_e2e.py21/21; the six CI feature suites
total 184 assertions (canary 22, wasm 23, resilience 17, affinity 7, proxy_split 76, proxy_disk 39).conf/tutorial_full.confverifies end to end at 38/38, including its new/coldendpoint, so the tutorial snippet is the config that actually ran.-tover all 90 shipped configs: unchanged (only the two known environment-bound cases,tls_legacy.confwhich needs thenative-tlsrebuild andstream_ssl_preread_e2e.confwhich is stream-only by design).- Full roll on the final binary:
python run_regression.py reg_101_roll.txt→TOTAL 86 scripts, nonzero=0. - 21-minute soak: PASS — 2,231,814 requests, 0 failures, 1778.1 req/s, private memory
18.58 → 19.29 MiB (+0.71 MiB, +0.021 MiB/min against a 1 MiB/min gate), peak 21.07 MiB; the cold
tier churned the whole time (81,723 spills / 80,986 cold hits / 80,986 promotions,corrupt0,
never_read0,pending_jobs0). A cold-tier-only run agrees: +1.07 MiB over 2,175,742 requests.
Documentation
docs/OPERATIONS.md§9.1 — what the tier does and does not promise, thespill_after/max_value/max_sizeknobs, why a cold hit keeps the nginx status word, and the metric family.docs/TUTORIAL.md§5 — a runnable 8-entrycoldzone that shows the spill, theX-Cache-Tier: diskhit and the promotion in threecurls; §11.2 adds the new suite.docs/CAPABILITY_MATRIX.md— the "proxy_cache is memory-only" gap line, the server-level-REWRITE row in the ignored-directives table and the disk-tier roadmap item are all replaced by implemented rows with evidence;proxy_cache_background_updatemoves to the roadmap with the reason it is not shipped (a background refetch carries its own content-negotiation headers into an entry other clients read, and the cache key has noVarydimension yet — that is a variant-keying task, not a threading task).README.md— 1.0.1 in the feature map and in the cache section, replacing the old "the path is never written" sentence.
中文摘要
1.0.1 只做一件事,但按 1.0 的证据标准做完:proxy_cache_path <目录> … disk=on 让响应缓存拿到与 lua_shared_dict disk= 同一套磁盘冷层。被 keys_zone 挤出去且还没过期的响应异步落盘(每区一个写线程、temp+rename、条目自描述带 CRC),下次同 key 请求从磁盘读回并提升进内存,重启靠扫目录重建索引。是开关而非默认:没写 disk=on 的区连目录都不创建,行为与 1.0.0 逐字节一致。冷命中的 X-Cache-Status 仍是 HIT(不改 nginx 词表),层级由 X-Cache-Tier: disk 说明,账目在 onrouter_proxy_cache_disk_*。spill_after 决定谁值得写盘,开着缓存学习时与内存准入用同一把频率草图,一次性扫库流量既进不了内存也写不满磁盘;坏条目一律按未命中回源并计入 corrupt,绝不拼成响应发出去;invalidate 会连磁盘副本一起摘。深睡在配了冷层的区改为下沉而不是丢弃。min_free_size= / use_temp_path= 在 -t 里明说未实现。注意它是冷层不是 nginx 的全量写盘:没被挤出过内存的对象不在磁盘上。
Full Changelog: v1.0.0...v1.0.1