Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
d37d283
A module declares the caches it loads through
jwrosewell Oct 12, 2026
7d0f70e
Merge 31-caching-release into 32-cache-features
jwrosewell Oct 13, 2026
d9656fd
A declared cache with no table takes the one loading cache module the…
jwrosewell Oct 13, 2026
1133273
The deploy check refuses the cache table the cache build refuses
Jamesr51d Oct 13, 2026
106fa71
Merge 31-caching-release into 32-cache-features
jwrosewell Oct 13, 2026
0f84dd6
The probe compares the two refusals of a misspelt cache table by message
jwrosewell Oct 13, 2026
6fe25f7
The cache check's test compares the two refusals of a misspelt table …
jwrosewell Oct 13, 2026
6fd925c
Merge split/31-caching-release into split/32-cache-features
Jamesr51d Oct 13, 2026
55256f7
Merge split/31-caching-release into split/32-cache-features
Jamesr51d Oct 13, 2026
eeb0143
Merge 31-caching-release into 32-cache-features
jwrosewell Oct 13, 2026
9d7268e
Merge 31-caching-release into 32-cache-features
jwrosewell Oct 13, 2026
4a7f422
Merge 31-caching-release into 32-cache-features
jwrosewell Oct 13, 2026
169d709
Merge 31-caching-release into 32-cache-features
jwrosewell Oct 14, 2026
0ef3ff7
The module cache declarations design
jwrosewell Oct 15, 2026
be4f478
The module cache declarations design takes its round 1 review
jwrosewell Oct 15, 2026
778247e
The module cache declarations design takes its round 2 review
jwrosewell Oct 15, 2026
bfafd90
Merge split/31-caching-release into split/32-cache-features
jwrosewell Oct 16, 2026
7cfc978
Merge split/31-caching-release into split/32-cache-features
jwrosewell Oct 16, 2026
12f3114
Merge split/31-caching-release into split/32-cache-features
jwrosewell Oct 16, 2026
7937250
Merge split/31-caching-release into split/32-cache-features
jwrosewell Oct 16, 2026
f291022
Merge split/31-caching-release into split/32-cache-features
jwrosewell Oct 16, 2026
c1e149a
Merge split/31-caching-release into split/32-cache-features
jwrosewell Oct 16, 2026
b8e6114
Merge the review fixes into split/32-cache-features
jwrosewell Oct 16, 2026
414ccfa
The design header names the pull request that carries it
jwrosewell Oct 16, 2026
50567d8
Merge split/31-caching-release into split/32-cache-features
jwrosewell Oct 16, 2026
0a35941
Merge split/31-caching-release into split/32-cache-features
jwrosewell Oct 16, 2026
d8ac700
Merge split/31-caching-release into split/32-cache-features
jwrosewell Oct 16, 2026
ff0631a
Merge split/31-caching-release into split/32-cache-features
jwrosewell Oct 16, 2026
cb738d5
Merge split/31-caching-release into split/32-cache-features
jwrosewell Oct 16, 2026
2d32375
Merge split/31-caching-release into split/32-cache-features
jwrosewell Oct 16, 2026
6e752f2
The module cache design names the shared store on each adapter
jwrosewell Oct 17, 2026
503c072
Merge split/31-caching-release into split/32-cache-features
jwrosewell Oct 18, 2026
b2a1164
Merge split/31-caching-release into split/32-cache-features
jwrosewell Oct 18, 2026
22618db
Merge split/31-caching-release into split/32-cache-features
jwrosewell Oct 18, 2026
9786f09
The configuration guide says what a declared table changes and when t…
jwrosewell Oct 18, 2026
613b386
The integration guide lists the builder method that declares a module…
jwrosewell Oct 18, 2026
97fc713
The cache declaration checks are pub(crate), since only core calls them
jwrosewell Oct 18, 2026
e0a8a90
The module cache design takes its four follow-ups out of the open que…
jwrosewell Oct 18, 2026
9266831
The module cache design ends with its review log, as the other design…
jwrosewell Oct 18, 2026
ce141ad
The module cache design says the deploy check refuses a module nothin…
jwrosewell Oct 18, 2026
3ab68ac
Merge split/31-caching-release into split/32-cache-features
jwrosewell Oct 18, 2026
5bd9c06
Merge split/31-caching-release into split/32-cache-features
jwrosewell Oct 19, 2026
dfe77f2
Merge split/31-caching-release into split/32-cache-features
jwrosewell Oct 19, 2026
db0d995
Merge split/31-caching-release into split/32-cache-features
jwrosewell Oct 19, 2026
4a1f03c
Merge split/31-caching-release into split/32-cache-features
jwrosewell Oct 19, 2026
606ad07
Merge split/31-caching-release into split/32-cache-features
jwrosewell Oct 28, 2026
a973685
Merge split/31-caching-release into split/32-cache-features
jwrosewell Oct 28, 2026
59d81cf
Merge split/31-caching-release into split/32-cache-features
jwrosewell Oct 28, 2026
084cf79
The probe's cache test no longer counts a stock loading cache
jwrosewell Oct 28, 2026
d3c0d4a
The declared cache example names a module a deployment links
jwrosewell Oct 28, 2026
6e79f53
Merge split/31-caching-release into split/32-cache-features
jwrosewell Oct 28, 2026
f9c7cb9
Row 32 takes the final module cache declarations design
jwrosewell Oct 28, 2026
1539206
Row 32's design copy calls its pull request ready
jwrosewell Oct 28, 2026
89ee1bc
Merge split/31-caching-release into split/32-cache-features
jwrosewell Oct 28, 2026
7a8a86a
Merge split/31-caching-release into split/32-cache-features
jwrosewell Oct 28, 2026
a6a7d3b
Row 32's design copy carries the date it is proposed, 2026-10-27
jwrosewell Oct 28, 2026
56dd3e8
Merge split/31-caching-release into split/32-cache-features
jwrosewell Oct 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- A module declares the caches it loads through, by name, and a deployment selects a loading cache for each in its own `[cache.<name>]` table, the way `[cache.robots-txt]` selects one for robots.txt. A `[cache.<name>]` table naming a cache no linked module declares is refused by `ts config validate` and when the deployment starts, naming the ones it does, and a module declaring `robots-txt` or a name another module declares is refused. `IntegrationBuilder::with_cache_features` declares them, `CacheSettings` has a new public field `declared`, `CacheSettings::loading_caches` returns a `Vec` in place of an array, listing core's own and the declared ones, and `LoadingCaches::insert` takes any name.
- **Breaking for crates outside core:** the cookie constants `COOKIE_TS_EC`, `COOKIE_TS_EC_RESOLVED`, `COOKIE_TS_EC_PULL_COMPLETE`, `COOKIE_TS_EIDS` and `COOKIE_TS_TESTER` are `COOKIE_EC`, `COOKIE_EC_RESOLVED`, `COOKIE_EC_PULL_COMPLETE`, `COOKIE_EIDS` and `COOKIE_TESTER`, holding the entry's name alone, and a cookie is named through the request's public prefix with `services.public_prefix().cookie_name(COOKIE_EC)`. A module's response header naming a cookie under the public prefix is refused as one under `ts-` was.
- A robots.txt module's answer is held by the loading cache of the one loading cache module the build links, or of the module `[cache.robots-txt] module` names, briefly in memory and in the host's shared store outside it, so a module is asked again only when no copy young enough is held, rather than by every request. With no loading cache module linked, or several and none named, nothing is held and every request for the file asks each module, as every request did when no adapter gave the file a key-value store. The last answer is kept apart for 30 days and served when the module cannot answer, in the value cache on Fastly and in the key-value store on Cloudflare and Spin, and Axum, which has no key-value store, keeps none.
- **Breaking:** the Cloudflare and Spin adapters declare their key-value store, the Workers KV binding `TRUSTED_SERVER_KV` and Spin's `default` store, so EdgeZero opens it for every request, where every request was given a store whose every call failed, and a loading cache module that a deployment links keeps its copies there between requests. A declared store is required, so a Worker without the binding, or a Spin component that cannot open `default`, fails every request before any route runs.
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/testing/seam-probe/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ workspace = true

[dependencies]
async-trait = { workspace = true }
bytes = { workspace = true }
edgezero-core = { workspace = true }
error-stack = { workspace = true }
http = { workspace = true }
Expand Down
63 changes: 63 additions & 0 deletions crates/testing/seam-probe/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -702,6 +702,9 @@ impl SeamProbeProxy {
"request_preparer_runs": prepared.map_or(0, |marker| marker.runs),
"public_prefix": services.public_prefix().as_str(),
"server_key_prefix": services.server_key_prefix().as_str(),
"answers_cache_keeps_values": services
.loading_cache_for(SEAM_PROBE_ANSWERS_CACHE)
.keeps_values(),
})
.to_string();

Expand Down Expand Up @@ -886,6 +889,66 @@ pub fn builder() -> IntegrationBuilder {
.with_request_preparer(prepare_request)
.with_demand(&SEAM_PROBE_DEMAND)
.with_adserver(&SEAM_PROBE_ADSERVER)
.with_cache_features(&[SEAM_PROBE_ANSWERS_CACHE])
}

/// The cache the probe declares and loads its answers through, selected in
/// `[cache.seam-probe-answers]`. The report route says whether the cache
/// that table selected keeps values.
pub const SEAM_PROBE_ANSWERS_CACHE: &str = "seam-probe-answers";

/// The id of the loading cache module the probe offers, selected in a
/// `[cache.<name>]` table as `module = "seam-probe"`.
pub const SEAM_PROBE_LOADING_CACHE_ID: &str = "loading-cache.seam-probe";

/// A loading cache that keeps every value in this process's memory for as
/// long as the process runs, enough to prove from outside core that a table
/// selected it.
#[derive(Default)]
struct MemoryLoadingCache {
held: Mutex<BTreeMap<String, bytes::Bytes>>,
}

#[async_trait(?Send)]
impl trusted_server_core::platform::PlatformLoadingCache for MemoryLoadingCache {
async fn get_or_load(
&self,
_services: &RuntimeServices,
_spec: &trusted_server_core::platform::CacheSpec,
key: &trusted_server_core::platform::StoreKey,
load: trusted_server_core::platform::Loader,
) -> Result<bytes::Bytes, trusted_server_core::platform::LoadError> {
if let Some(held) = self.held.lock().expect("lock").get(&key.to_string()) {
return Ok(held.clone());
}
let loaded = load().await?.bytes;
self.held
.lock()
.expect("lock")
.insert(key.to_string(), loaded.clone());
Ok(loaded)
}

async fn remove(
&self,
_services: &RuntimeServices,
_spec: &trusted_server_core::platform::CacheSpec,
key: &trusted_server_core::platform::StoreKey,
) {
self.held.lock().expect("lock").remove(&key.to_string());
}
}

fn memory_loading_cache() -> Arc<dyn trusted_server_core::platform::PlatformLoadingCache> {
Arc::new(MemoryLoadingCache::default())
}

/// The builder offering the probe's loading cache, for a test that proves a
/// declared cache's table selects a module from outside core.
#[must_use]
pub fn loading_cache_builder() -> IntegrationBuilder {
IntegrationBuilder::implementations(SEAM_PROBE_LOADING_CACHE_ID, SEAM_PROBE_SOURCE)
.with_loading_cache(memory_loading_cache)
}

/// The vendor's name, which names the table `[shared.seam-probe]` that runs
Expand Down
117 changes: 117 additions & 0 deletions crates/trusted-server-adapter-axum/tests/seam_probe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,123 @@ fn deploy_validation_rejects_a_violation_of_the_modules_own_rule() {
);
}

// ---------------------------------------------------------------------------
// 3b. A cache the probe declares is selected in its own table
// ---------------------------------------------------------------------------

/// Reads the probe's report through the router, as a page would.
async fn report_with(settings: Settings, integrations: &[IntegrationBuilder]) -> serde_json::Value {
let mut service = service_with(settings, integrations);
let response = get(&mut service, seam_probe::SEAM_PROBE_REPORT_PATH).await;
assert_eq!(
response.status().as_u16(),
200,
"the report route should answer"
);
let body = body_text(response).await;
serde_json::from_str(&body).expect("the report route should return JSON")
}

/// The id of a loading cache module these tests link beside the probe's.
const SECOND_LOADING_CACHE_ID: &str = "loading-cache.memory";

/// Core's `MemoryLoadingCache`, which keeps what it loads.
fn memory_loading_cache() -> Arc<dyn trusted_server_core::platform::PlatformLoadingCache> {
Arc::new(trusted_server_core::platform::test_support::MemoryLoadingCache::default())
}

/// A loading cache module other than the probe's, so a build can link two.
fn second_loading_cache_builder() -> IntegrationBuilder {
IntegrationBuilder::implementations(SECOND_LOADING_CACHE_ID, env!("CARGO_PKG_NAME"))
.with_loading_cache(memory_loading_cache)
}

/// `[cache.seam-probe-answers]` selects the loading cache the probe's answers
/// are held in, the way `[cache.robots-txt]` selects core's. Left out, the
/// cache takes the one loading cache module the build links, and keeps
/// nothing when the build links none or several. A stock build links none,
/// so the first two cases link a second module beside the probe's, and only
/// the one whose table is read keeps values.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_cache_the_probe_declares_is_selected_in_its_own_table() {
let named = settings_with(&format!(
r#"
[cache.seam-probe-answers]
module = "seam-probe"
{PROBE_BLOCK}
"#
));
let two = [
seam_probe::builder(),
seam_probe::loading_cache_builder(),
second_loading_cache_builder(),
];

let report = report_with(named, &two).await;
assert_eq!(
report["answers_cache_keeps_values"], true,
"the table should select the module it names: {report}"
);

let report = report_with(settings_with(PROBE_BLOCK), &two).await;
assert_eq!(
report["answers_cache_keeps_values"], false,
"with two loading cache modules linked and no table the cache should keep nothing: {report}"
);

let both = [seam_probe::builder(), seam_probe::loading_cache_builder()];
let report = report_with(settings_with(PROBE_BLOCK), &both).await;
assert_eq!(
report["answers_cache_keeps_values"], true,
"a declared cache with no table should take the one loading cache module linked: {report}"
);

let report = report_with(settings_with(PROBE_BLOCK), &[seam_probe::builder()]).await;
assert_eq!(
report["answers_cache_keeps_values"], false,
"with no loading cache module linked the cache should keep nothing: {report}"
);
}

/// A `[cache.<name>]` table naming a cache no linked module declares is
/// refused by the deploy check and at startup with one message, naming the
/// caches that could be written, so a misspelt name fails `ts config
/// validate` and not the deployment.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_cache_table_no_module_declares_is_refused_by_the_deploy_check_and_at_startup() {
let settings = settings_with(&format!(
r#"
[cache.seam-probe-answres]
module = "seam-probe"
{PROBE_BLOCK}
"#
));
let both = [seam_probe::builder(), seam_probe::loading_cache_builder()];

let deploy = validate_settings_for_deploy_with(&settings, &both)
.expect_err("the deploy check should refuse the misspelt table");
let startup = TrustedServerApp::routes_with_registrations(settings, &both)
.err()
.expect("startup should refuse the misspelt table");

// The message alone: the debug form carries a backtrace where the
// runner asks for one, and the two are raised on different stacks.
let message = format!("{deploy}");
assert!(
message.contains("[cache.seam-probe-answres]"),
"should name the table: {message}"
);
assert!(
message.contains("robots-txt, seam-probe-answers"),
"should list the caches this build declares: {message}"
);
assert_eq!(
message,
format!("{startup}"),
"the deploy check should refuse what startup refuses"
);
}

// ---------------------------------------------------------------------------
// 4. A geo selector naming a module that supplies no module fails at startup
// ---------------------------------------------------------------------------
Expand Down
8 changes: 6 additions & 2 deletions crates/trusted-server-core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -359,11 +359,15 @@ pub fn validate_settings_for_deploy_with(
let mut structural_settings = settings.clone();
structural_settings.prepare_runtime()?;

// A `[cache.<name>]` table is refused here as the cache build refuses it,
// so a misspelt name fails `ts config validate` and not the deployment's
// first request.
let builders: Vec<IntegrationBuilder> =
crate::integrations::all_builders(extra_integrations).collect();
crate::integrations::check_cache_tables(settings, &builders)?;
// A `[cache.<name>]` module no linked crate offers a cache from is refused
// here as making the caches refuses it, so a misspelt module fails
// `ts config validate` rather than the service.
let builders: Vec<IntegrationBuilder> =
crate::integrations::all_builders(extra_integrations).collect();
crate::integrations::check_loading_cache_modules(settings, &builders)?;

// The plan is compiled with the builders the blocks are validated
Expand Down
Loading
Loading