Skip to content

The publisher's assets held through the loading cache - #1375

Draft
jwrosewell wants to merge 6 commits into
feature/33-geo-callfrom
feature/34-asset-cache
Draft

jwrosewell wants to merge 6 commits into
feature/33-geo-callfrom
feature/34-asset-cache

Conversation

@jwrosewell

Copy link
Copy Markdown
Collaborator

Summary

Changes

Area Change
crates/trusted-server-core/src 10 files, 2440 lines added, 55 removed
docs/guide 1 file, 19 lines added, 0 removed
CHANGELOG.md 1 file, 1 line added, 0 removed
crates/trusted-server-adapter-axum/tests 1 file, 1 line added, 1 removed

Closes

Closes #1324

Test plan

CI: 19 of 19 checks green, the fork's CI on f6e598f (jwrosewell#29).

  • cargo test-fastly && cargo test-axum
  • cargo clippy-fastly && cargo clippy-axum
  • cargo fmt --all -- --check
  • JS tests: cd crates/trusted-server-js/lib && npx vitest run
  • JS format: cd crates/trusted-server-js/lib && npm run format
  • Docs format: cd docs && npm run format
  • WASM build: cargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1
  • Manual testing via fastly compute serve
  • Other: the full workflow set of this repository, run on the pull request named above

Checklist

  • Changes follow AGENTS.md conventions
  • No unwrap() in production code (clippy denies unwrap_used in CI)
  • Uses log macros as AGENTS.md asks (not println!)
  • New code has tests
  • No secrets or credentials committed

On top of the geo module called with the request (#28). Issue: #1324.

What this adds

What changes.

Every response the publisher's origin sends that is not a page, and every answer of an asset route under [[proxy.asset_routes]], is held through the loading cache (#19), so an asset is fetched once however many readers and instances ask for it at the same moment, and the copy in the host's shared store answers every instance until the origin's own lifetime ends.

[cache.assets]
module = "51degrees"
memory_seconds = 5
store_seconds = 3600

A deployment that names no module in [cache.assets] takes the one loading cache module the build links, as [cache.robots-txt] does, and a build that links none holds nothing and fetches every asset from the origin, which is what happens today.

How it works.

pub mod source_cache;   // records of what an address varies on, and the copies under each request's values
pub mod asset_cache;    // what a kept asset holds, and the address it is held under
pub const ASSETS_CACHE: &str = "assets";
pub const ASSETS_VARY_CACHE: &str = "assets-vary";
  • A copy is kept under what the origin says it varies on. The first response for an address records the request headers its Vary names, and that record is held in the loading cache like any other value. A later request for the address reads the record and is served the copy kept under its own values for those headers, as RFC 9111 section 4.1 describes. Requests for an address no record covers share one load.
  • Two caches, one table. The records and the copies are two caches, assets-vary and assets, so the loading cache module keeps a memory layer for each, and both are selected by the one [cache.assets] table. Core declares both as its own, beside robots-txt, so a module cannot declare either.
  • The asset routes hold their answers the same way. The route handler looks for a copy before it asks the route's origin and keeps the answer by the same rules, under a key that carries the route's origin and prefix, so two routes to one address keep their own copies. An answer the platform's image optimizer makes is not kept, because the platform holds it.
  • What is never kept, whatever the settings say: anything but a GET, anything but a 200, HTML (a page is a template or nothing), a response carrying Set-Cookie, a response whose cache policy says private, no-store or no-cache or gives no lifetime, a response that varies on * or on Cookie, a response larger than a copy holds (1 MiB), and the answer to a request carrying Authorization, a conditional header, a range, or a Cache-Control that asks for the origin (no-cache, no-store, min-fresh or a nonzero max-age).
  • A response that may not be shared is handed back to the request whose load fetched it, so that request is served from one fetch, not two, and the requests that follow fetch their own for a minute rather than wait on a load.
  • A copy is served as the origin's response would be, with the headers that describe the bytes, an Age that counts from the origin's, and the fetch and serve entries of the configuration run on it as they run on a fetched response.
  • Each publisher's copies are its own. Every key starts with the publisher's prefix, so two publishers sharing a store never share a copy.

Why

A deployment with the platform's cache in front of it already has its assets held there. A deployment with nothing in front of it, and one whose fetch phase rewrites an asset before a reader sees it, fetches every asset on every page from the publisher's origin every time, and every instance does. The loading cache was declared so that each feature that holds answers selects the cache that holds them, and assets are the feature it was declared for after robots.txt.

What breaks

Nothing for a deployment that names no [cache.assets] table in a build that links no loading cache module. A deployment whose build links one starts holding shareable assets for the lifetime the origin gives them, so a change at the origin shows after that lifetime, as it does behind any shared cache.

The commits

Commit What it does
7031828 The source cache and the asset cache, in core, selected by [cache.assets]
4e0bb71 The publisher path holds the origin's assets through the loading cache
87a55aa An asset route's answer is held the same way, and the guide documents the table
6d587fd The merges of the chain below
f6e598f Review fixes: a dropped response is handed back, the asset route honours the request's Cache-Control, shared test and hashing helpers

CI being read on f6e598f.

What is not in this pull request

  • A body the fetch phase rewrites is kept as the origin sent it, and the fetch phase runs on each reader's copy. Keeping the rewritten body once, as the sibling deployment did, is a later commit.
  • The publisher's declared rule for inputs the origin varies on without saying so ([publisher.origin_personalization]), which separates copies the origin's own Vary does not. Later, with its tests.
  • The /proxy first-party route, which rehosts third party assets named in a page. It streams, and on Fastly the platform's cache holds it.
  • Pages stay on the template cache, as Remove hard coded Fastly service id #19 said.

jwrosewell and others added 6 commits October 13, 2026 23:52
Core gains source_cache.rs, which keeps each address's record of what the
origin's responses vary on and a copy under each request's values for those
headers, both through the loading cache, so requests for an address share
one load and a copy is served only to a request whose values match, and
asset_cache.rs, which is what a kept asset holds and the address it is held
under. Both are selected by one table, [cache.assets], which CacheSettings
lists after [cache.robots-txt], so a deployment names the module whose
loading cache holds its assets as it does for robots.txt, and a build that
links one takes it without a line in the document.

Nothing holds an asset yet. The publisher path does in the next commit.
Every GET that is not a page is looked for in the asset cache before the
origin is fetched, under the publisher's prefix and the asset's address,
and a copy found is served as the origin's response would be, with an Age
that counts from the origin's. A miss loads once however many requests ask
at the same moment, keeps the response as the origin sent it when it may be
shared, and hands a response that may not be shared back to the request
whose load fetched it, so that request is served from one fetch and the
requests that follow fetch their own for a minute rather than wait.

What is never kept: anything but a 200, HTML, a response carrying
Set-Cookie, one whose cache policy says private, no-store or no-cache or
gives no lifetime, one that varies on * or on Cookie, one larger than a
copy holds, and the answer to a request carrying Authorization. The record
of what an address varies on and the copies are two caches selected by the
one [cache.assets] table, and a deployment whose selected cache keeps
nothing asks the origin as before.
…isher's assets are

A file served through an asset route, under [[proxy.asset_routes]], streamed
from its origin on every request, however many readers and instances asked
for it, while a file the publisher's own origin sends was held through the
loading cache. The route handler now looks for a copy under the same two
caches [cache.assets] selects before it asks the origin, and keeps the
origin's answer the way the publisher path keeps an asset: under what the
origin says it varies on, for the lifetime its cache policy gives, served
with the headers that describe it and an Age that counts from the origin's.
The key carries the route's origin and prefix, so two routes to one address
keep their own copies.

Nothing is kept that may not be shared, by the same rules, and a request a
copy cannot answer is not served one: anything but a GET, a request
carrying Authorization, a conditional header or a range, one that names no
host, and any request while the selected cache keeps nothing. An answer the
platform's image optimizer makes is not kept either, because the platform
holds it. An answer that may not be shared is served to the request whose
load fetched it, from that one fetch.

The publisher path's asset helpers are open to the rest of core so the
route handler shares them, and the memory loading cache the publisher's
tests used lives in the platform's test support so the route's tests use
it too. Four tests cover an answer held and served to the next request
with one origin call, an answer carrying Set-Cookie fetched for each
request, a range request fetched rather than served a copy, and nothing
held while no loading cache keeps values.

The guide documents [cache.assets] beside [cache.robots-txt], with what is
held and what never is, and the changelog says what the asset cache adds.
A body that outgrows a copy while it is read is handed back as what was
read and then the rest. A response whose Vary no longer matches its record
is served to the request that fetched it. Both were dropped, and that
request asked the origin a second time.

The asset caches run their loads in the request that started them, and an
asset route refuses a copy to a request whose Cache-Control asks for the
origin, as the publisher path does. The two lookups share one function, the
origin checks the template cache and asset cache share are one helper, the
three length-prefixed hashes share one, and the tests use the memory loading
cache and the shared test services. The docs describe the copy and its key
as they are built today.

This branch has not been deployed

No deployments
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.

2 participants