Skip to content

Amortize STATIC_REVALIDATE with a per-file TTL and single cache lookup#244

Merged
diolektor merged 1 commit into
mainfrom
feature/static-revalidate-ttl
Jun 28, 2026
Merged

Amortize STATIC_REVALIDATE with a per-file TTL and single cache lookup#244
diolektor merged 1 commit into
mainfrom
feature/static-revalidate-ttl

Conversation

@diolektor

Copy link
Copy Markdown
Contributor

Summary

Makes STATIC_REVALIDATE=on cheap enough to be a sane choice in development by amortizing its filesystem check, while collapsing the static-serve cache path to a single access. The default stays off.

Previously STATIC_REVALIDATE=on performed a stat() on every cache hit (and the serve path could stat() twice — once for the 304 check, once for the content fetch), all under a write lock. This made the mode too expensive to leave on.

What changed

Per-file TTL revalidation. A cached file's mtime is re-checked at most once every 3 seconds per file (STATIC_REVALIDATE_TTL), not per request. Within the window the entry is served under a shared read lock with no syscall. On-disk changes become visible within 3 seconds.

Single combined lookup. FileCache::lookup() resolves the conditional (304) check and the content fetch in one cache access and returns a Lookup enum, so a served static hit performs at most one stat() (was up to two). serve() now makes one cache access.

Single-flight revalidation (claim-then-stat). When the window expires, the slow path claims it under the write lock (bumping last_checked) before releasing the lock and running stat() unlocked. Concurrent callers then see a fresh window and skip their own syscall, so a thundering herd on one file collapses to a single stat() per window — and the syscall never holds the content lock.

LRU promotion preserved. The slow path uses get_mut() to promote a hot file to MRU once per window, so it is not evicted ahead of colder, later-inserted entries under the 64 MiB byte-budget pressure.

Consolidation. The private revalidate_locked() helper is removed; lookup() is the single revalidation path. get_content() / check_not_modified() are now #[cfg(test)] thin wrappers over lookup(), removing duplicated revalidate-then-build logic.

Tradeoffs (intentional)

  • Within the 3s window a hot file is promoted to MRU only once per window (not per hit) — per-hit promotion would require a write lock on every static hit, serializing the hot path. Real impact only under sustained churn with a full byte budget.
  • The revalidation stat() is a synchronous std::fs call on the Tokio worker (not spawn_blocking): on a local FS a stat is far cheaper than blocking-pool dispatch, and it now runs at most once per window per file. Deployments with a slow/network static root (NFS, sshfs) should keep revalidation off.

Docs

static-files and configuration references (en/ru/zh) and the CHANGELOG now describe the once-per-3s window and within-3s visibility instead of per-hit revalidation.

Testing

  • cargo fmt --check, cargo clippy --all-targets --no-default-features -D warnings — clean
  • cargo test --no-default-features — 865 lib tests pass, no failures across the suite
  • New: test_content_cache_revalidation_window_keeps_entry, test_revalidation_promotes_hot_entry_on_access; immediate-eviction tests switched to a zero-TTL cache.

Performance:
  - STATIC_REVALIDATE=on now re-checks a cached file's mtime at most once every 3 seconds per file (STATIC_REVALIDATE_TTL) instead of stat()-ing on every request. Within the window the entry is served under a shared read lock with no syscall; ContentEntry carries a last_checked: Instant to bound the check.
  - Combine the 304 check and content fetch into one FileCache::lookup() returning a Lookup enum, so a served static hit performs at most one stat() (previously check_304_headers() + get_content() could each stat()). serve() now makes a single cache access.
  - Single-flight revalidation via claim-then-stat: the slow path claims the window under the write lock (bumping last_checked) before releasing it and running stat() unlocked, so a thundering herd on one file collapses to one syscall per window and the syscall never holds the content lock.
  - Restore LRU promotion in revalidation mode via get_mut() on the slow path, so a hot file is promoted to MRU once per window and is not evicted ahead of colder, later-inserted entries under byte-budget pressure.

Refactor:
  - Replace FileCache.validate_content: bool with revalidate_ttl: Option<Duration>; add with_revalidation_ttl() (None disables, Some(ZERO) revalidates every hit). with_revalidation(_, bool) maps on to the 3s TTL.
  - Remove the private revalidate_locked() helper; lookup() is now the single revalidation path. get_content() and check_not_modified() become #[cfg(test)] thin wrappers over lookup(), eliminating duplicated revalidate-then-build logic.
  - Guard slow-path eviction on the stat'd mtime so a concurrently-reinserted fresh entry is not discarded.

Docs:
  - Update static-files and configuration references (en/ru/zh) and CHANGELOG to describe the once-per-3s window and within-3s visibility instead of per-hit revalidation. Default remains off.

Tests:
  - Add test_content_cache_revalidation_window_keeps_entry (change masked within the window) and test_revalidation_promotes_hot_entry_on_access (hot entry survives eviction); switch the immediate-eviction tests to a zero-TTL cache.
@diolektor diolektor merged commit e8df8d0 into main Jun 28, 2026
6 checks passed
@diolektor diolektor deleted the feature/static-revalidate-ttl branch June 28, 2026 11:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant