Skip to content

ariadne: app-wide HTTP(s) cache in the state tree (§13.9, PR2) - #472

Merged
transfix merged 1 commit into
masterfrom
feat/http-cache-state-tree
Sep 29, 2026
Merged

transfix merged 1 commit into
masterfrom
feat/http-cache-state-tree

Conversation

@transfix

Copy link
Copy Markdown
Owner

PR2 of the §13.9 HTTP-cache work, on the cvc::net facade (#468) + the state_exec bytes bridge (#469). Adds a caching http(s):// READ handler backed by the cvc::app state tree: repeated import:/load:/source: of the same URL is served from sys.net.http_cache.entries.<key> instead of re-fetching.

What it does

  • Entry = sys.net.http_cache.entries.<key> (key = cvc::sha256_hex(normalized-URL)): body on the node's data() (a raw std::string blob — shows as bytes in state_exec and serves via state://…?data, per state_exec: data-channel bytes as a raw std::string (coherent with ?data + the HTTP cache) #469); metadata as child value nodes.
  • Freshness (soft) — serves with no network while now < fetched_at + freshness_ttl. Cache-Control: max-age (anchored at now - Age so a CDN hit isn't over-fresh); else a validator-present ⇒ revalidate heuristic; no-cache ⇒ revalidate; no-store ⇒ bypass.
  • Revalidation — a stale entry / miss does a conditional GET (If-None-Match/If-Modified-Since): 304 refreshes the entry (honoring the 304's own directives + refreshed validators) and serves the cached body; 200 replaces body + validators + TTL.
  • Retention (hard) — rides the state tree's own node expiry: expireAt = fetched_at + max(retention, freshness), sweepExpired() on access frees the body blob; a successful store invalidates the entry.
  • Single-flight — concurrent requests for one URL coalesce to one transfer.
  • Credentialed bypass — a request with provider auth headers or URL-embedded userinfo bypasses the cache (no cross-principal reuse in a process-global cache).

Prerequisite refactor

Shared request-build / response-map / store policy moves out of uri_http.cpp's anon namespace into a private (non-installed) uri_http_detail.h (consult_http_provider, http_header_present, map_http_fetch_result, http_store_uncached), so the cache and the plain handler apply identical policy. map_http_fetch_result also now maps a 304 to an error, so a bodyless 304 can never surface as empty-body success on any path.

Concurrency (the load-bearing fix)

cvc::state's findDescendant/operator() hand back a bare pointer/ref whose only owner is the parent's child map, so a concurrent sweepExpired() can free a node mid-access (UAF). All access to the entries subtree (sweep + snapshot read + write + invalidation) is serialized under one process-wide cache_mutex, held only for the brief tree work and released across the blocking network send; cache_mutex and the single-flight flight_mutex are never nested.

Review + tests

Reviewed adversarially (4 lenses × find-then-verify, medium fleet); 15 real findings, all fixed in this PR — the three UAFs + the replace-torn-read (→ cache_mutex), 304 correctness (unsolicited/unconditional 304, no-store/no-cache on revalidation, validator merge), Cache-Control tokenizing, Age anchoring, userinfo bypass. uri_http_cache_test — 12 tests, all offline via PR1's set_http_client fake (incl. single-flight coalescing and a concurrent-distinct-keys smoke, 0/20 flaky). The existing 9 http handler + 159 loader + 7 net facade tests stay green (the refactor is behavior-preserving). No platform-specific code — rides PR1's validated wasm backend.

Deferred to PR3

sys.net.http_cache.policy.* knobs incl. a max-entries/max-bytes LRU budget (today bounded only by per-entry 64 MiB + retention), full Vary/private, Expires-based freshness, richer URL normalization. The transport stays synchronous (§13.8's fetch_sync); an async/awaitable chunked (http-get) intrinsic (off-thread + parking via compute_async → post_message → msg-recv) is a later PR the cache rides unchanged.

Add a caching http(s):// READ handler backed by the cvc::app state tree, over
the cvc::net facade (PR1). register_cached_http_uri_handler(app) parks entries at
sys.net.http_cache.entries.<key> (key = cvc::sha256_hex of the normalized URL):
the body on the entry node's data() channel (a raw std::string blob -- reads back
as `bytes` in state_exec and serves via state://...?data, per the precursor), and
metadata (status/etag/last_modified/content_type/effective_url/fetched_at/
freshness_ttl) as child value nodes.

Freshness (SOFT) serves with no network while now < fetched_at + freshness_ttl
(Cache-Control max-age, anchored at now-Age; else a validator-present-=>revalidate
heuristic; no-cache => revalidate; no-store => bypass). A stale entry or a miss
does a conditional GET (If-None-Match / If-Modified-Since): a 304 refreshes the
entry (honoring the 304's own directives + any refreshed validators) and serves
the cached body, a 200 replaces body + validators + TTL. Retention (HARD) rides
the state tree's node expiry: expireAt = fetched_at + max(retention, freshness),
swept on access (entries.sweepExpired()), which frees the body blob; a successful
store invalidates the entry. Concurrent requests for one URL coalesce
(single-flight). A credentialed request -- provider auth headers OR URL-embedded
userinfo -- BYPASSES the cache (no cross-principal reuse in a process-global cache).

Prerequisite refactor: the shared request-build / response-map / store policy moves
out of uri_http.cpp's anonymous namespace into a private (non-installed)
uri_http_detail.h -- consult_http_provider, http_header_present,
map_http_fetch_result, http_store_uncached -- so the cache and the plain handler
apply identical policy. map_http_fetch_result now also maps a 304 to an error, so
a bodyless 304 can never surface as an empty-body success on any path.

Concurrency: cvc::state's findDescendant()/operator() hand back a bare pointer/ref
whose only owner is the parent's child map, so a concurrent sweepExpired() can free
a node mid-access. All access to the entries subtree (sweep + snapshot read + write
+ invalidation) is serialized under one process-wide cache_mutex, held only for the
brief tree work and released across the (blocking) network send; cache_mutex and the
single-flight flight_mutex are never held simultaneously.

Reviewed adversarially (15 findings, all fixed here). Tests: uri_http_cache_test
(12, all offline via PR1's set_http_client fake) -- fresh-hit-no-network,
304-serve-cached, 200-replace, no-store bypass, retention eviction, credentialed +
userinfo bypass, unsolicited-304 error, 304 no-store eviction, Age anchoring,
single-flight coalescing, concurrent distinct keys. The existing 9 http handler
tests + 159 loader + 7 net facade tests stay green (the uri_http refactor is
behavior-preserving). No platform-specific code -- rides PR1's validated wasm
backend.

Deferred to PR3: sys.net.http_cache.policy.* knobs incl. a max-entries/max-bytes
LRU budget (today bounded only by per-entry 64 MiB + retention), full Vary/private,
Expires-based freshness, richer URL normalization. The transport stays synchronous
(§13.8's fetch_sync); an async/awaitable chunked (http-get) intrinsic is a later PR
the cache rides unchanged.
@transfix
transfix merged commit 53d96eb into master Sep 29, 2026
13 checks passed
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