Skip to content

Commit 398cf52

Browse files
committed
Add bounded Wasm cache store metadata
1 parent cba8dd1 commit 398cf52

14 files changed

Lines changed: 382 additions & 74 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,20 @@ behavior when the change improves security or project direction.
1515
`cache-lookup` hooks.
1616
- Add symbolic `X-Device-Class` context and fixed
1717
`wasm-device-class=mobile|desktop` cache-key variants.
18+
- Add fixed-ID Wasm cache-store TTL overrides and cache tag assignment.
1819
- Add live listener tests proving separate mobile and desktop cache variants
1920
MISS independently and HIT the original variant on repeat.
21+
- Add live listener tests proving a plugin TTL override expires an otherwise
22+
longer-lived origin response.
2023

2124
### Security
2225

2326
- Reject unknown cache-key component IDs, unknown values, duplicate labels, and
2427
component counts above the hard cap through the plugin fail mode.
28+
- Reject unknown TTL IDs, duplicate TTL overrides, unknown tag IDs, and tag
29+
counts above the hard cap through the plugin fail mode.
2530
- Keep arbitrary request headers, raw cache-key bytes, request bodies, cached
26-
objects, TTL override, and tag assignment unavailable in this slice.
31+
objects, arbitrary TTLs, and arbitrary tag strings unavailable in this slice.
2732

2833
## 1.7.4 - 2026-07-07
2934

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ Fluxheim is licensed under the European Union Public Licence 1.2.
137137
| GeoIP/Geo-Context policy || `1.4.5`; optional `geoip` feature with local MMDB support for MaxMind GeoIP2/GeoLite2 and CIRCL Geo Open datasets, plus vhost/route country and ASN ACLs. |
138138
| Pingora-free runtime || `1.6.34`; normal Fluxheim builds no longer compile Pingora crates. Server/listener/TLS, HTTP/1, HTTP/2, WebSocket, cache, load-balancer, admin, metrics, stream, and background-service paths run through Fluxheim-owned Rust crates. |
139139
| HTTP/3/QUIC || Planned as a Fluxheim-owned `1.9` protocol milestone using the Rust `quinn`/`h3` stack after the `1.8` macOS/Windows production parity line. |
140-
| WASM extensibility | 🧪 | Active `1.7.x` line. `1.7.0` added the optional `wasm` feature, strict plugin-file loading, bounded Wasmtime execution, and real-Wasm smoke coverage. `1.7.1` adds config-level plugin registry validation, deterministic attachment ordering, admission limits, metrics, and live native HTTP/1 access-decision hooks. `1.7.2` adds bounded native HTTP/1 request/response header hooks. `1.7.3` starts bounded native HTTP/1 route-decision hooks with configured canary and mirror branch selection, including selected native load-balanced and persistent routes. `1.7.4` starts VCL-like cache-policy hooks with bounded cache-lookup and cache-store decisions for continue/pass/bypass/skip-store/deny around cache lookup and storage. `1.7.5` adds bounded symbolic cache-key component hooks for low-cardinality cache variants. Direct backend choice, plugin-provided persistence keys, dynamic mirror/shadow target choice, and richer TTL/tag store policy hooks remain staged for later `1.7.x`. |
140+
| WASM extensibility | 🧪 | Active `1.7.x` line. `1.7.0` added the optional `wasm` feature, strict plugin-file loading, bounded Wasmtime execution, and real-Wasm smoke coverage. `1.7.1` adds config-level plugin registry validation, deterministic attachment ordering, admission limits, metrics, and live native HTTP/1 access-decision hooks. `1.7.2` adds bounded native HTTP/1 request/response header hooks. `1.7.3` starts bounded native HTTP/1 route-decision hooks with configured canary and mirror branch selection, including selected native load-balanced and persistent routes. `1.7.4` starts VCL-like cache-policy hooks with bounded cache-lookup and cache-store decisions for continue/pass/bypass/skip-store/deny around cache lookup and storage. `1.7.5` adds bounded symbolic cache-key component hooks for low-cardinality cache variants plus fixed-ID cache-store TTL/tag metadata. Direct backend choice, plugin-provided persistence keys, dynamic mirror/shadow target choice, and richer store policy hooks remain staged for later `1.7.x`. |
141141

142142
See [Production Readiness](docs/production-readiness.md) for the precise
143143
stable-core promise and deployment checks. See
@@ -258,7 +258,7 @@ Individual module features:
258258
| `php-fpm` | No | PHP-FPM FastCGI bridge for WordPress-style PHP applications. Implies `proxy` and `web`; not included in default/focused images. |
259259
| `privacy-mode` | No | Zero-retention static/proxy build profile. |
260260
| `security` | Yes | Compile-time security profile marker plus release hardening checks. Runtime enforcement lives in the concrete config, TLS, filesystem, admin, and request-handling modules. |
261-
| `wasm` | No | Optional `1.7.x` WebAssembly policy runtime. `1.7.5` supports live native HTTP/1 access-decision hooks, bounded request/response header hooks, constrained route-decision hooks with configured canary and mirror branch selection, selected native load-balanced/persistent routes, bounded cache-lookup hooks, bounded cache-store skip/deny hooks, and bounded symbolic cache-key component hooks. Later `1.7.x` releases add direct backend choice, plugin-provided persistence keys, dynamic mirror/shadow target choice, and richer TTL/tag store policy hooks. |
261+
| `wasm` | No | Optional `1.7.x` WebAssembly policy runtime. `1.7.5` supports live native HTTP/1 access-decision hooks, bounded request/response header hooks, constrained route-decision hooks with configured canary and mirror branch selection, selected native load-balanced/persistent routes, bounded cache-lookup hooks, bounded cache-store skip/deny hooks, bounded symbolic cache-key component hooks, and fixed-ID cache-store TTL/tag metadata. Later `1.7.x` releases add direct backend choice, plugin-provided persistence keys, dynamic mirror/shadow target choice, and richer store policy hooks. |
262262
| `wasm-proxy-abi` | No | Reserved compatibility preview for a reviewed safe subset of proxy-oriented Wasm ABI calls; depends on `wasm` and remains off by default. |
263263
| `wasm-wasi` | No | Reserved WASI capability preview; depends on `wasm` and remains off by default with no filesystem/network/process capabilities unless explicitly granted in a later release. |
264264
| `tls` | No | Internal TLS marker used by TLS/ACME code; select a concrete backend for serving. |

crates/fluxheim-server/src/native_http1_proxy_load_balanced.rs

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -445,19 +445,33 @@ impl NativeHttp1Proxy {
445445
}
446446
#[cfg(feature = "wasm")]
447447
let store_result = if let Some(hooks) = wasm_hooks {
448-
match hooks
448+
let decision = hooks
449449
.cache_store_decision(
450450
NativeWasmCacheStoreContext::from_request_response(
451451
&request, &response,
452452
),
453453
)
454-
.await
455-
{
454+
.await;
455+
match decision.outcome {
456456
NativeWasmCacheStoreOutcome::Continue => {
457457
if *status == "REVALIDATED" {
458-
cache.store_revalidated(key, &request, &response).await
458+
cache
459+
.store_revalidated_with_metadata(
460+
key,
461+
&request,
462+
&response,
463+
decision.metadata,
464+
)
465+
.await
459466
} else {
460-
cache.store(key, &request, &response).await
467+
cache
468+
.store_with_metadata(
469+
key,
470+
&request,
471+
&response,
472+
decision.metadata,
473+
)
474+
.await
461475
}
462476
}
463477
NativeWasmCacheStoreOutcome::Skip(reason) => Err(reason),

crates/fluxheim-server/src/native_http1_proxy_memory_cache.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,12 @@ pub(crate) struct NativeProxyCacheKeyComponent {
8888
pub(crate) value: &'static str,
8989
}
9090

91+
#[derive(Clone, Debug, Default, Eq, PartialEq)]
92+
pub(crate) struct NativeProxyCacheStoreMetadata {
93+
pub(crate) ttl_override: Option<Duration>,
94+
pub(crate) cache_tags: Vec<&'static str>,
95+
}
96+
9197
#[derive(Debug)]
9298
pub(crate) enum NativePeerFillDecision {
9399
Skip,

crates/fluxheim-server/src/native_http1_proxy_memory_cache/store.rs

Lines changed: 68 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,13 @@ use crate::native_http1_proxy_cache_headers::{
1414
use crate::native_http1_proxy_cache_policy::native_cache_expiry_times;
1515
use crate::{NativeHttp1Request, NativeHttp1Response};
1616
use fluxheim_cache::{
17-
CacheRequestView, VaryCachePolicy, cache_vary_policy, range_response_cache_admission_rejection,
18-
response_age_secs, response_cache_admission_rejection,
19-
response_range_cache_admission_rejection, selected_cache_range_request,
17+
CacheRequestView, VaryCachePolicy, cache_vary_policy, collect_cache_tags,
18+
range_response_cache_admission_rejection, response_age_secs,
19+
response_cache_admission_rejection, response_range_cache_admission_rejection,
20+
selected_cache_range_request,
2021
};
2122

22-
use super::NativeProxyMemoryCache;
23+
use super::{NativeProxyCacheStoreMetadata, NativeProxyMemoryCache};
2324

2425
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2526
enum NativeCacheStoreMode {
@@ -34,9 +35,31 @@ impl NativeProxyMemoryCache {
3435
key: &str,
3536
request: &NativeHttp1Request,
3637
response: &NativeHttp1Response,
38+
) -> Result<(), &'static str> {
39+
self.store_with_metadata(
40+
key,
41+
request,
42+
response,
43+
NativeProxyCacheStoreMetadata::default(),
44+
)
45+
.await
46+
}
47+
48+
pub(crate) async fn store_with_metadata(
49+
&self,
50+
key: &str,
51+
request: &NativeHttp1Request,
52+
response: &NativeHttp1Response,
53+
metadata: NativeProxyCacheStoreMetadata,
3754
) -> Result<(), &'static str> {
3855
let result = self
39-
.store_inner(key, request, response, NativeCacheStoreMode::Origin)
56+
.store_inner(
57+
key,
58+
request,
59+
response,
60+
NativeCacheStoreMode::Origin,
61+
metadata,
62+
)
4063
.await;
4164
if let Err(reason) = result
4265
&& reason != "cache-min-uses"
@@ -51,9 +74,31 @@ impl NativeProxyMemoryCache {
5174
key: &str,
5275
request: &NativeHttp1Request,
5376
response: &NativeHttp1Response,
77+
) -> Result<(), &'static str> {
78+
self.store_revalidated_with_metadata(
79+
key,
80+
request,
81+
response,
82+
NativeProxyCacheStoreMetadata::default(),
83+
)
84+
.await
85+
}
86+
87+
pub(crate) async fn store_revalidated_with_metadata(
88+
&self,
89+
key: &str,
90+
request: &NativeHttp1Request,
91+
response: &NativeHttp1Response,
92+
metadata: NativeProxyCacheStoreMetadata,
5493
) -> Result<(), &'static str> {
5594
let result = self
56-
.store_inner(key, request, response, NativeCacheStoreMode::Revalidated)
95+
.store_inner(
96+
key,
97+
request,
98+
response,
99+
NativeCacheStoreMode::Revalidated,
100+
metadata,
101+
)
57102
.await;
58103
if let Err(reason) = result
59104
&& reason != "cache-min-uses"
@@ -116,6 +161,7 @@ impl NativeProxyMemoryCache {
116161
request,
117162
&refreshed_entry.to_response(),
118163
NativeCacheStoreMode::Revalidated,
164+
NativeProxyCacheStoreMetadata::default(),
119165
)
120166
.await?;
121167
Ok(refreshed_entry)
@@ -128,7 +174,13 @@ impl NativeProxyMemoryCache {
128174
response: &NativeHttp1Response,
129175
) -> Result<(), &'static str> {
130176
let result = self
131-
.store_inner(key, request, response, NativeCacheStoreMode::PeerFill)
177+
.store_inner(
178+
key,
179+
request,
180+
response,
181+
NativeCacheStoreMode::PeerFill,
182+
NativeProxyCacheStoreMetadata::default(),
183+
)
132184
.await;
133185
if let Err(reason) = result
134186
&& reason != "cache-min-uses"
@@ -144,6 +196,7 @@ impl NativeProxyMemoryCache {
144196
request: &NativeHttp1Request,
145197
response: &NativeHttp1Response,
146198
mode: NativeCacheStoreMode,
199+
metadata: NativeProxyCacheStoreMetadata,
147200
) -> Result<(), &'static str> {
148201
let body_len = response.body().len() as u64;
149202
if body_len > self.config.max_object_bytes.as_u64() {
@@ -178,9 +231,9 @@ impl NativeProxyMemoryCache {
178231
}
179232
}
180233
let ttl = match mode {
181-
NativeCacheStoreMode::Origin | NativeCacheStoreMode::Revalidated => {
182-
native_cache_ttl(response.status(), &headers, &self.config)
183-
}
234+
NativeCacheStoreMode::Origin | NativeCacheStoreMode::Revalidated => metadata
235+
.ttl_override
236+
.or_else(|| native_cache_ttl(response.status(), &headers, &self.config)),
184237
NativeCacheStoreMode::PeerFill => {
185238
native_peer_fill_cache_ttl(response.status(), &headers, &self.config)
186239
}
@@ -237,7 +290,11 @@ impl NativeProxyMemoryCache {
237290
stored_at,
238291
weight,
239292
};
240-
let cache_tags = native_response_cache_tags(response, &self.config);
293+
let mut cache_tags = native_response_cache_tags(response, &self.config);
294+
let mut cache_tag_bytes = cache_tags.iter().map(String::len).sum();
295+
for tag in metadata.cache_tags {
296+
collect_cache_tags(tag, &mut cache_tags, &mut cache_tag_bytes);
297+
}
241298
let disk_key = NativeDiskCacheStoreKey {
242299
combined: store_key.clone(),
243300
primary: key.to_owned(),

crates/fluxheim-server/src/native_http1_proxy_static_dispatch.rs

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -363,19 +363,33 @@ impl NativeHttp1Proxy {
363363
}
364364
#[cfg(feature = "wasm")]
365365
let store_result = if let Some(hooks) = wasm_hooks {
366-
match hooks
366+
let decision = hooks
367367
.cache_store_decision(
368368
NativeWasmCacheStoreContext::from_request_response(
369369
&request, &response,
370370
),
371371
)
372-
.await
373-
{
372+
.await;
373+
match decision.outcome {
374374
NativeWasmCacheStoreOutcome::Continue => {
375375
if *status == "REVALIDATED" {
376-
cache.store_revalidated(key, &request, &response).await
376+
cache
377+
.store_revalidated_with_metadata(
378+
key,
379+
&request,
380+
&response,
381+
decision.metadata,
382+
)
383+
.await
377384
} else {
378-
cache.store(key, &request, &response).await
385+
cache
386+
.store_with_metadata(
387+
key,
388+
&request,
389+
&response,
390+
decision.metadata,
391+
)
392+
.await
379393
}
380394
}
381395
NativeWasmCacheStoreOutcome::Skip(reason) => Err(reason),

0 commit comments

Comments
 (0)