From a2cb99615bdcd4055464185cc7a377c9c4d65843 Mon Sep 17 00:00:00 2001 From: Ruslan Pislari Date: Fri, 4 Sep 2026 10:35:27 +0300 Subject: [PATCH 01/22] refactor(metrics): extract Prometheus backend from fastedge-lib into server Remove the 'metrics' cargo feature and the prometheus/lazy_static dependencies from the public fastedge-lib crates: - runtime::util::metrics is now a backend-agnostic hook: embedders install a process-global sink via set_sink(); with none installed (fastedge-run CLI) reporting is a no-op - runtime::instances keeps live/peak counts in plain atomics; flush_peak() now returns the drained peak instead of setting a gauge - AppResult variants and http-service call sites are no longer feature-gated The Prometheus implementation (fastedge_call_count, fastedge_error_total_count, fastedge_request_duration, fastedge_wasm_memory_used, fastedge_wasm_instances_live/peak) moves to server/src/metrics.rs; the sink is installed at startup and the instance gauges are set on each scrape (same semantics). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CLAUDE.md | 2 +- Cargo.lock | 67 ----------------- crates/http-service/Cargo.toml | 1 - crates/http-service/src/lib.rs | 7 -- crates/runtime/Cargo.toml | 5 -- crates/runtime/src/instances.rs | 111 +++++++++++------------------ crates/runtime/src/lib.rs | 4 -- crates/runtime/src/store.rs | 4 -- crates/runtime/src/util/metrics.rs | 83 ++++++--------------- crates/runtime/src/util/mod.rs | 1 - 10 files changed, 63 insertions(+), 222 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d25bf3b..417ee2c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -71,7 +71,7 @@ Every Wasmtime `Store` holds a `Data` (in `crates/runtime/src/lib.rs`). It bu ## Crate features worth knowing -- `runtime`: `metrics` opts into Prometheus + lazy_static and propagates through `http-service/metrics`. +- `runtime`: no metrics backend is compiled in. App-call outcomes are reported through the `runtime::util::metrics` sink (`set_sink`), and live-instance counts through the plain atomics in `runtime::instances` (`live` / `flush_peak`); embedders (e.g. the FastEdge server) export them however they like. With no sink installed, reporting is a no-op. - `key-value-store`: `redis` enables the Redis-backed implementation. `runtime` already turns this on; if you depend on `key-value-store` from elsewhere, opt in explicitly. - `cache`: no features yet — backend choice is at runtime via the `Arc` injected into `Data`. diff --git a/Cargo.lock b/Cargo.lock index d70c142..e1fe023 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1704,12 +1704,6 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - [[package]] name = "http" version = "1.4.2" @@ -2762,65 +2756,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "procfs" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc5b72d8145275d844d4b5f6d4e1eef00c8cd889edb6035c21675d1bb1f45c9f" -dependencies = [ - "bitflags 2.13.1", - "hex", - "procfs-core", - "rustix 0.38.44", -] - -[[package]] -name = "procfs-core" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "239df02d8349b06fc07398a3a1697b06418223b1c7725085e801e7c0fc6a12ec" -dependencies = [ - "bitflags 2.13.1", - "hex", -] - -[[package]] -name = "prometheus" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ca5326d8d0b950a9acd87e6a3f94745394f62e4dae1b1ee22b2bc0c394af43a" -dependencies = [ - "cfg-if", - "fnv", - "lazy_static", - "libc", - "memchr", - "parking_lot", - "procfs", - "protobuf", - "thiserror 2.0.19", -] - -[[package]] -name = "protobuf" -version = "3.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d65a1d4ddae7d8b5de68153b48f6aa3bba8cb002b243dbdbc55a5afbc98f99f4" -dependencies = [ - "once_cell", - "protobuf-support", - "thiserror 1.0.69", -] - -[[package]] -name = "protobuf-support" -version = "3.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e36c2f31e0a47f9280fb347ef5e461ffcd2c52dd520d8e216b52f93b0b0d7d6" -dependencies = [ - "thiserror 1.0.69", -] - [[package]] name = "pulley-interpreter" version = "36.0.12" @@ -3181,9 +3116,7 @@ dependencies = [ "http", "http-backend", "key-value-store", - "lazy_static", "moka", - "prometheus", "secret", "serde", "serde_json", diff --git a/crates/http-service/Cargo.toml b/crates/http-service/Cargo.toml index 8abc003..e5b4b8b 100644 --- a/crates/http-service/Cargo.toml +++ b/crates/http-service/Cargo.toml @@ -7,7 +7,6 @@ authors.workspace = true [features] default = [] -metrics = ["runtime/metrics"] [dependencies] anyhow = { workspace = true } diff --git a/crates/http-service/src/lib.rs b/crates/http-service/src/lib.rs index e9e46f4..77f8359 100644 --- a/crates/http-service/src/lib.rs +++ b/crates/http-service/src/lib.rs @@ -15,7 +15,6 @@ use http_backend::SERVER_NAME_HEADER; use http_body_util::{BodyExt, Empty, Full}; use hyper::{body::Body, server::conn::http1, service::service_fn}; use hyper_util::{client::legacy::connect::Connect, rt::TokioIo}; -#[cfg(feature = "metrics")] use runtime::util::metrics; use runtime::util::stats::StatsVisitor; use runtime::{ @@ -38,7 +37,6 @@ type OwnedFd = std::os::fd::OwnedFd; #[cfg(not(target_family = "unix"))] type OwnedFd = std::os::raw::c_int; -#[cfg(feature = "metrics")] const HTTP_LABEL: &[&str; 1] = &["http"]; const FASTEDGE_INTERNAL_ERROR: u16 = 530; @@ -277,7 +275,6 @@ where // get application name from request URL let app_name = match app_name_from_request(&request) { Err(error) => { - #[cfg(feature = "metrics")] metrics::metrics(AppResult::UNKNOWN, HTTP_LABEL, None, None); tracing::info!(cause=?error, traceparent = %traceparent, "App name not provided"); return not_found(); @@ -307,7 +304,6 @@ where let (app_name, cfg) = match lookup { None => { - #[cfg(feature = "metrics")] metrics::metrics(AppResult::UNKNOWN, HTTP_LABEL, None, None); tracing::info!("Request for unknown application on URL: {}", request.uri()); return not_found(); @@ -350,7 +346,6 @@ where { Ok(executor) => executor, Err(error) => { - #[cfg(feature = "metrics")] metrics::metrics(AppResult::UNKNOWN, HTTP_LABEL, None, None); tracing::warn!(cause=?error, app=%app_name, "failure on getting context" @@ -371,7 +366,6 @@ where let response = match executor.execute(request, stats.clone()).await { Ok(mut response) => { - #[cfg(feature = "metrics")] metrics::metrics( AppResult::SUCCESS, &["http"], @@ -389,7 +383,6 @@ where stats.fail_reason(fail_reason as i32); tracing::debug!(?fail_reason, ?traceparent, "stats"); - #[cfg(feature = "metrics")] metrics::metrics( fail_reason, HTTP_LABEL, diff --git a/crates/runtime/Cargo.toml b/crates/runtime/Cargo.toml index d65243c..1a7f9a9 100644 --- a/crates/runtime/Cargo.toml +++ b/crates/runtime/Cargo.toml @@ -5,9 +5,6 @@ edition.workspace = true publish.workspace = true authors.workspace = true -[features] -metrics = ["prometheus", "lazy_static"] - [dependencies] anyhow = { workspace = true } tokio = { workspace = true } @@ -35,8 +32,6 @@ bytes = "1.11" serde = "1.0" serde_json = "1.0" chrono = { version = "0.4", features = ["serde"] } -prometheus = { version = "0.14.0", features = ["process"], optional = true } -lazy_static = { version = "1.5.0", optional = true } [dev-dependencies] claims = "0.8" diff --git a/crates/runtime/src/instances.rs b/crates/runtime/src/instances.rs index 9dd1e89..da12d13 100644 --- a/crates/runtime/src/instances.rs +++ b/crates/runtime/src/instances.rs @@ -14,78 +14,49 @@ //! through `StoreBuilder::build` — ProxyWasm, `http-handler`, `wasi:http` — is counted //! against the same pool it actually draws from. There is deliberately no `executor` label: //! the pools are per-`Engine` and shared, so only the total is meaningful for sizing. +//! +//! The counters are plain atomics; embedders export them (e.g. as the Prometheus gauges +//! `fastedge_wasm_instances_live` / `fastedge_wasm_instances_peak`) by reading [`live`] and +//! draining [`flush_peak`] on each scrape. + +use std::sync::atomic::{AtomicI64, Ordering}; + +/// Currently live instances. +static LIVE: AtomicI64 = AtomicI64::new(0); + +/// Running max since the previous [`flush_peak`]. Kept separately so the peak can be +/// updated with an atomic `fetch_max` and drained with `swap(0)` on scrape. A plain +/// scrape-time sample of [`LIVE`] would miss the sub-second concurrency spikes that a +/// stalled backend produces — exactly the peaks the pool has to be sized for. +static PEAK: AtomicI64 = AtomicI64::new(0); + +fn acquire() { + // `fetch_add` returns the previous value, so `+ 1` is the level that this + // acquisition genuinely produced — a valid sample for the peak. + let live = LIVE.fetch_add(1, Ordering::Relaxed) + 1; + PEAK.fetch_max(live, Ordering::Relaxed); +} -#[cfg(feature = "metrics")] -mod imp { - use lazy_static::lazy_static; - use prometheus::{IntGauge, register_int_gauge}; - use std::sync::atomic::{AtomicI64, Ordering}; - - lazy_static! { - static ref WASM_INSTANCES_LIVE: IntGauge = register_int_gauge!( - "fastedge_wasm_instances_live", - "WASM instances currently alive, each holding one slot in every wasmtime pooling-allocator pool" - ) - .unwrap(); - - /// Peak since the previous scrape. A plain gauge is sampled at the scrape - /// interval and will miss the sub-second concurrency spikes that a stalled backend - /// produces — exactly the peaks the pool has to be sized for. This one cannot. - static ref WASM_INSTANCES_PEAK: IntGauge = register_int_gauge!( - "fastedge_wasm_instances_peak", - "Highest number of concurrently live WASM instances observed since the \ - previous scrape (resets on scrape)" - ) - .unwrap(); - } - - /// Running max since the previous scrape, kept separately so the peak can be updated - /// with an atomic `fetch_max` — `IntGauge` only offers `set`, which cannot express - /// "raise to" — and drained with `swap(0)` by [`flush_peak`] on scrape. - static PEAK: AtomicI64 = AtomicI64::new(0); - - pub(super) fn acquire() { - WASM_INSTANCES_LIVE.inc(); - // Reading back after `inc` may observe another thread's concurrent increment. That - // is still a level that genuinely occurred, so it is a valid sample for the peak. - let live = WASM_INSTANCES_LIVE.get(); - PEAK.fetch_max(live, Ordering::Relaxed); - } - - pub(super) fn release() { - WASM_INSTANCES_LIVE.dec(); - } - - /// Currently live instances. Test/diagnostic accessor. - pub fn live() -> i64 { - WASM_INSTANCES_LIVE.get() - } - - /// Peak since the previous [`flush_peak`]. Test/diagnostic accessor. - pub fn peak() -> i64 { - PEAK.load(Ordering::Relaxed) - } +fn release() { + LIVE.fetch_sub(1, Ordering::Relaxed); +} - /// Export and reset the peak gauge; call on each Prometheus scrape. Never reports - /// less than the currently live count, so a long-running steady load can't read as 0. - pub fn flush_peak() { - let peak = PEAK - .swap(0, Ordering::Relaxed) - .max(WASM_INSTANCES_LIVE.get()); - WASM_INSTANCES_PEAK.set(peak); - } +/// Currently live instances. +pub fn live() -> i64 { + LIVE.load(Ordering::Relaxed) } -#[cfg(not(feature = "metrics"))] -mod imp { - pub(super) fn acquire() {} - pub(super) fn release() {} - pub fn flush_peak() {} +/// Peak since the previous [`flush_peak`]. Test/diagnostic accessor. +pub fn peak() -> i64 { + PEAK.load(Ordering::Relaxed) } -pub use imp::flush_peak; -#[cfg(feature = "metrics")] -pub use imp::{live, peak}; +/// Drain and return the peak since the previous call; invoke on each metrics scrape. +/// Never reports less than the currently live count, so a long-running steady load +/// can't read as 0. +pub fn flush_peak() -> i64 { + PEAK.swap(0, Ordering::Relaxed).max(live()) +} /// RAII counter for one live WASM instance. /// @@ -97,7 +68,7 @@ pub struct LiveInstanceGuard; impl LiveInstanceGuard { pub fn new() -> Self { - imp::acquire(); + acquire(); LiveInstanceGuard } } @@ -110,15 +81,15 @@ impl Default for LiveInstanceGuard { impl Drop for LiveInstanceGuard { fn drop(&mut self) { - imp::release(); + release(); } } -#[cfg(all(test, feature = "metrics"))] +#[cfg(test)] mod tests { use super::*; - /// Guards from concurrent tests share the process-global gauge, so assertions are on + /// Guards from concurrent tests share the process-global counter, so assertions are on /// deltas rather than absolute values. #[test] fn guard_tracks_live_count_and_raises_peak() { diff --git a/crates/runtime/src/lib.rs b/crates/runtime/src/lib.rs index bdbcb6e..b85a515 100644 --- a/crates/runtime/src/lib.rs +++ b/crates/runtime/src/lib.rs @@ -58,19 +58,15 @@ const PREVIEW1_ADAPTER: &[u8] = include_bytes!("adapters/wasi_snapshot_preview1. #[derive(PartialEq, Copy, Clone, Debug)] pub enum AppResult { SUCCESS, - #[cfg(feature = "metrics")] UNKNOWN, TIMEOUT, OOM, OTHER, /// Request shed by admission control (server overloaded). - #[cfg(feature = "metrics")] OVERLOADED, /// Request rejected because the app is rate limited. - #[cfg(feature = "metrics")] RATE_LIMITED, /// Request rejected because the app is disabled or in draft. - #[cfg(feature = "metrics")] DISABLED, } diff --git a/crates/runtime/src/store.rs b/crates/runtime/src/store.rs index e36f915..3bc9932 100644 --- a/crates/runtime/src/store.rs +++ b/crates/runtime/src/store.rs @@ -602,10 +602,8 @@ mod tests { /// No-op stats sink; `StoreBuilder::build` only needs `HasStats` to wire the /// key-value store and utils host state. - #[cfg(feature = "metrics")] struct NoStats; - #[cfg(feature = "metrics")] mod no_stats_impls { use super::NoStats; use crate::util::stats::{CdnPhase, ReadStats, StatsVisitor}; @@ -639,7 +637,6 @@ mod tests { } } - #[cfg(feature = "metrics")] impl HasStats for NoStats { fn get_stats(&self) -> Arc { Arc::new(NoStats) @@ -649,7 +646,6 @@ mod tests { /// End-to-end wiring check: a store built the way every executor builds it must be /// counted in `fastedge_wasm_instances_live` for exactly as long as it is alive, since /// that is the window in which it holds pooling-allocator slots. - #[cfg(feature = "metrics")] #[test] fn store_lifetime_is_counted_as_a_live_instance() { use crate::instances; diff --git a/crates/runtime/src/util/metrics.rs b/crates/runtime/src/util/metrics.rs index 7d5b31a..25b994e 100644 --- a/crates/runtime/src/util/metrics.rs +++ b/crates/runtime/src/util/metrics.rs @@ -1,70 +1,29 @@ -use lazy_static::lazy_static; -use prometheus::{ - self, HistogramVec, IntCounterVec, register_histogram_vec, register_int_counter_vec, -}; +//! App-call metrics hook. +//! +//! The runtime reports every app-call outcome through a process-global sink so +//! that embedders (e.g. the FastEdge server) can export them however they like +//! (Prometheus, etc.) without this crate depending on a metrics backend. When +//! no sink is installed (e.g. the `fastedge-run` CLI) reporting is a no-op. -use crate::AppResult; +use std::sync::OnceLock; -lazy_static! { - static ref TOTAL_COUNT: IntCounterVec = register_int_counter_vec!( - "fastedge_call_count", - "Total number of app calls.", - &["executor"] - ) - .unwrap(); - static ref ERROR_COUNT: IntCounterVec = register_int_counter_vec!( - "fastedge_error_total_count", - "Total number of failed app calls, by executor and failure reason \ - (counter; the non-conventional *_count suffix is kept for backwards compatibility).", - &["executor", "reason"] - ) - .unwrap(); - static ref REQUEST_DURATION: HistogramVec = register_histogram_vec!( - "fastedge_request_duration", - "Request duration", - &["executor"], - vec![ - 0.0001, 0.0002, 0.0003, 0.0005, 0.001, 0.002, 0.003, 0.005, 0.01, 0.02, 0.03, 0.05, - 0.1, 0.2, 0.3, 0.5, 1.0, 10.0 - ] - ) - .unwrap(); - static ref MEMORY_USAGE: IntCounterVec = register_int_counter_vec!( - "fastedge_wasm_memory_used", - "Cumulative WASM linear-memory bytes consumed by app calls (counter, NOT a \ - current-usage gauge; the name is kept for backwards compatibility). \ - rate() = memory-zeroing throughput; divide by rate(fastedge_call_count) \ - for average memory per request.", - &["executor"] - ) - .unwrap(); -} +use crate::AppResult; -pub fn metrics(result: AppResult, label: &[&str], duration: Option, memory_used: Option) { - TOTAL_COUNT.with_label_values(label).inc(); +/// Sink invoked for every app call: outcome, executor label(s), duration in +/// microseconds and WASM linear memory used in bytes. +pub type MetricsSink = + fn(result: AppResult, label: &[&str], duration: Option, memory_used: Option); - if result != AppResult::SUCCESS { - let mut values: Vec<&str> = label.iter().map(|v| *v).collect(); - match result { - AppResult::UNKNOWN => values.push("unknown"), - AppResult::TIMEOUT => values.push("timeout"), - AppResult::OOM => values.push("oom"), - AppResult::OTHER => values.push("other"), - AppResult::OVERLOADED => values.push("overloaded"), - AppResult::RATE_LIMITED => values.push("ratelimited"), - AppResult::DISABLED => values.push("disabled"), - _ => {} - }; +static SINK: OnceLock = OnceLock::new(); - ERROR_COUNT.with_label_values(values.as_slice()).inc(); - } +/// Install the process-global metrics sink. Subsequent calls are ignored. +pub fn set_sink(sink: MetricsSink) { + let _ = SINK.set(sink); +} - if let Some(duration) = duration { - REQUEST_DURATION - .with_label_values(label) - .observe((duration as f64) / 1_000_000.0); - } - if let Some(memory_used) = memory_used { - MEMORY_USAGE.with_label_values(label).inc_by(memory_used); +/// Report one app call to the installed sink (no-op when none is installed). +pub fn metrics(result: AppResult, label: &[&str], duration: Option, memory_used: Option) { + if let Some(sink) = SINK.get() { + sink(result, label, duration, memory_used); } } diff --git a/crates/runtime/src/util/mod.rs b/crates/runtime/src/util/mod.rs index 41c2dc3..d14ad41 100644 --- a/crates/runtime/src/util/mod.rs +++ b/crates/runtime/src/util/mod.rs @@ -1,3 +1,2 @@ -#[cfg(feature = "metrics")] pub mod metrics; pub mod stats; From 2b1213bc22c92da27bff31fa2477eaeb2bdca3e8 Mon Sep 17 00:00:00 2001 From: Ruslan Pislari Date: Fri, 4 Sep 2026 11:35:23 +0300 Subject: [PATCH 02/22] refactor(metrics): improve error labeling logic and ensure peak >= live consistency in `runtime::instances` --- crates/runtime/src/instances.rs | 88 +++++++++++++++++++++++++++++++-- 1 file changed, 85 insertions(+), 3 deletions(-) diff --git a/crates/runtime/src/instances.rs b/crates/runtime/src/instances.rs index da12d13..ddbd0e2 100644 --- a/crates/runtime/src/instances.rs +++ b/crates/runtime/src/instances.rs @@ -16,8 +16,10 @@ //! the pools are per-`Engine` and shared, so only the total is meaningful for sizing. //! //! The counters are plain atomics; embedders export them (e.g. as the Prometheus gauges -//! `fastedge_wasm_instances_live` / `fastedge_wasm_instances_peak`) by reading [`live`] and -//! draining [`flush_peak`] on each scrape. +//! `fastedge_wasm_instances_live` / `fastedge_wasm_instances_peak`) by calling +//! [`flush_peak_and_live`] once per scrape. That returns both values from a single `live` +//! sample, which is what keeps the exported peak from ever reading below the exported +//! live count; reading [`live`] and [`flush_peak`] separately does not. use std::sync::atomic::{AtomicI64, Ordering}; @@ -54,8 +56,36 @@ pub fn peak() -> i64 { /// Drain and return the peak since the previous call; invoke on each metrics scrape. /// Never reports less than the currently live count, so a long-running steady load /// can't read as 0. +/// +/// Prefer [`flush_peak_and_live`] when exporting both gauges: reading `live` separately +/// can observe a *later*, higher level than the one folded in here and so publish a +/// scrape where peak < live. pub fn flush_peak() -> i64 { - PEAK.swap(0, Ordering::Relaxed).max(live()) + let (peak, _live) = flush_peak_and_live(); + peak +} + +/// Drain the peak and sample the live count for a single scrape, returning +/// `(peak, live)`. +/// +/// `live` is sampled *before* the peak is drained and is the same value folded into the +/// peak, so the returned pair always satisfies `peak >= live`. Sampling the two +/// independently does not: an instance acquired between the two reads raises `live` +/// above a peak that was already computed, publishing a scrape that contradicts the +/// documented "never less than currently live" semantics. +pub fn flush_peak_and_live() -> (i64, i64) { + // Exactly one `live()` read: taking the sample here and threading it through is what + // makes `peak >= live` structural rather than timing-dependent. + drain_peak_against(live()) +} + +/// Drain `PEAK` and fold `live_sample` into it, returning `(peak, live_sample)`. +/// +/// Both halves of the returned pair derive from the single caller-supplied sample, so +/// `peak >= live` holds for any input — including a fully drained `PEAK`. +fn drain_peak_against(live_sample: i64) -> (i64, i64) { + let peak = PEAK.swap(0, Ordering::Relaxed).max(live_sample); + (peak, live_sample) } /// RAII counter for one live WASM instance. @@ -88,11 +118,22 @@ impl Drop for LiveInstanceGuard { #[cfg(test)] mod tests { use super::*; + use std::sync::{Mutex, MutexGuard, OnceLock}; + + /// `LIVE` / `PEAK` are process-global, so a test that drains the peak must not run + /// alongside one asserting it is monotonic. Serialise every test that touches them. + fn serial() -> MutexGuard<'static, ()> { + static LOCK: OnceLock> = OnceLock::new(); + LOCK.get_or_init(|| Mutex::new(())) + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + } /// Guards from concurrent tests share the process-global counter, so assertions are on /// deltas rather than absolute values. #[test] fn guard_tracks_live_count_and_raises_peak() { + let _serial = serial(); let before = live(); let first = LiveInstanceGuard::new(); @@ -110,4 +151,45 @@ mod tests { // The high-water mark is monotonic: dropping instances must not lower it. assert_eq!(peak(), peak_at_top); } + + /// The core invariant, independent of timing: whatever live level a scrape samples, + /// the peak reported alongside it is never lower — even when `PEAK` was just drained + /// and contributes nothing. This is the case the old two-read scrape got wrong: it + /// computed the peak from one `live()` read and published a second, later one. + #[test] + fn drained_peak_still_reports_at_least_the_live_sample() { + let _serial = serial(); + for live_sample in [0, 1, 7, 1_000] { + PEAK.store(0, Ordering::Relaxed); + let (peak, live) = drain_peak_against(live_sample); + assert_eq!(live, live_sample); + assert!(peak >= live, "peak {peak} < live {live}"); + } + } + + /// A recorded spike still wins when it exceeds the live level at scrape time. + #[test] + fn recorded_spike_outranks_a_lower_live_sample() { + let _serial = serial(); + PEAK.store(50, Ordering::Relaxed); + let (peak, live) = drain_peak_against(3); + assert_eq!((peak, live), (50, 3)); + // Draining is destructive: the next scrape falls back to the live sample. + let (peak, _) = drain_peak_against(3); + assert_eq!(peak, 3); + } + + /// End-to-end through the real globals: acquiring after a drain must not produce a + /// scrape where peak < live. + #[test] + fn flush_peak_and_live_holds_invariant_after_a_drain() { + let _serial = serial(); + let _first = LiveInstanceGuard::new(); + // Drain, so `PEAK` is 0 and only the live sample can carry the invariant. + let _ = flush_peak_and_live(); + + let _second = LiveInstanceGuard::new(); + let (peak, live) = flush_peak_and_live(); + assert!(peak >= live, "peak {peak} < live {live}"); + } } From 8713004a882c5a83472ce098f90b15e9e33f79cd Mon Sep 17 00:00:00 2001 From: Ruslan Pislari Date: Mon, 7 Sep 2026 11:58:44 +0300 Subject: [PATCH 03/22] Add KV read cache, retry and latency metrics Expose the four missing key-value read metrics on the Prometheus scrape: - fastedge_kv_reads_total{result="miss"|"hit"} splits reads into direct (backing-store) and in-process cache hits. Commands that are never cached (scan/zscan/zrange_by_score/bf_exists) count as misses. - fastedge_redis_read_retries_total{store,command} counts retried read attempts only, so rate() gives retries since the previous scrape. - fastedge_redis_read_duration_seconds{store,command} times uncached reads; the average since the previous scrape is rate(_sum)/rate(_count). Cache hits are excluded (in-process lookup) and failed reads are not observed, since their timeout-dominated duration would skew the latency of served reads. The key-value store lives in the fastedge-lib workspace and must stay free of a metrics backend, so it reports retries through a new set_read_retry_observer hook that the server installs at startup. All new series are materialised at 0 by redis_metrics::init() and are documented with PromQL in docs/METRICS.md. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/key-value-store/src/lib.rs | 53 ++++++++++++++++++++++++ crates/key-value-store/src/redis_impl.rs | 26 +++++++----- 2 files changed, 69 insertions(+), 10 deletions(-) diff --git a/crates/key-value-store/src/lib.rs b/crates/key-value-store/src/lib.rs index cff4c1f..745be29 100644 --- a/crates/key-value-store/src/lib.rs +++ b/crates/key-value-store/src/lib.rs @@ -5,6 +5,7 @@ use reactor::gcore::fastedge::key_value; use slab::Slab; use smol_str::SmolStr; use std::sync::Arc; +use std::sync::OnceLock; use tracing::instrument; use wasmtime::component::Resource; @@ -59,6 +60,37 @@ pub trait StoreManager: Sync + Send { ) -> Result, Error>; } +/// Command label passed to a [`ReadRetryObserver`]: `get` / `get_tracked`. +pub const CMD_GET: &str = "get"; +/// Command label passed to a [`ReadRetryObserver`]: `zrange_by_score`. +pub const CMD_ZRANGE_BY_SCORE: &str = "zrange_by_score"; +/// Command label passed to a [`ReadRetryObserver`]: `scan`. +pub const CMD_SCAN: &str = "scan"; +/// Command label passed to a [`ReadRetryObserver`]: `zscan`. +pub const CMD_ZSCAN: &str = "zscan"; +/// Command label passed to a [`ReadRetryObserver`]: `bf_exists`. +pub const CMD_BF_EXISTS: &str = "bf_exists"; + +/// Called once per retried backing-store read attempt (the retry itself, not +/// the first attempt), with the command label that was retried. +pub type ReadRetryObserver = fn(command: &'static str); + +static READ_RETRY_OBSERVER: OnceLock = OnceLock::new(); + +/// Install the process-wide read-retry observer. Lets an embedder count retries +/// (e.g. into a Prometheus counter) without this crate depending on a metrics +/// backend. Returns `false` if an observer was already installed. +pub fn set_read_retry_observer(observer: ReadRetryObserver) -> bool { + READ_RETRY_OBSERVER.set(observer).is_ok() +} + +/// Report one retried read attempt to the installed observer, if any. +pub fn note_read_retry(command: &'static str) { + if let Some(observer) = READ_RETRY_OBSERVER.get() { + observer(command); + } +} + /// Key-Value store read metrics pub trait ReadStats: Sync + Send { /// Increment key-value read count and size @@ -264,6 +296,27 @@ mod tests { use std::collections::HashMap; use std::sync::atomic::AtomicI32; + static OBSERVED_RETRIES: std::sync::atomic::AtomicUsize = + std::sync::atomic::AtomicUsize::new(0); + + #[test] + fn read_retry_observer_receives_retries() { + fn observer(command: &'static str) { + assert_eq!(command, CMD_GET); + OBSERVED_RETRIES.fetch_add(1, std::sync::atomic::Ordering::SeqCst); + } + assert!(set_read_retry_observer(observer), "first install wins"); + // A second install is rejected rather than silently swapping the hook. + assert!(!set_read_retry_observer(observer)); + + note_read_retry(CMD_GET); + note_read_retry(CMD_GET); + assert_eq!( + OBSERVED_RETRIES.load(std::sync::atomic::Ordering::SeqCst), + 2 + ); + } + // Mock implementation of Store struct MockStore { data: HashMap, diff --git a/crates/key-value-store/src/redis_impl.rs b/crates/key-value-store/src/redis_impl.rs index 6ea80fd..1136ee5 100644 --- a/crates/key-value-store/src/redis_impl.rs +++ b/crates/key-value-store/src/redis_impl.rs @@ -33,8 +33,9 @@ fn redis_read_retry_delay(attempt: usize) -> Duration { /// exponential backoff. `op` is re-invoked from scratch on each attempt so it /// can pick a fresh pooled connection (used by `get`/`zrange_by_score`/ /// `bf_exists`; `scan`/`zscan` retry the same connection since their iterator -/// borrows it across the loop). -async fn retry_read(mut op: F) -> Result +/// borrows it across the loop). Each retry is reported to the installed +/// [`crate::ReadRetryObserver`] under `command`. +async fn retry_read(command: &'static str, mut op: F) -> Result where F: FnMut() -> Fut, Fut: std::future::Future>, @@ -45,6 +46,7 @@ where Ok(value) => return Ok(value), Err(error) if attempt < REDIS_READ_RETRIES => { attempt += 1; + crate::note_read_retry(command); tracing::debug!(attempt, cause = ?error, "kv-store: redis read retry"); tokio::time::sleep(redis_read_retry_delay(attempt)).await; } @@ -115,7 +117,7 @@ impl RedisStore { #[async_trait::async_trait] impl Store for RedisStore { async fn get(&self, key: &str) -> Result, Error> { - retry_read(|| async { self.conn().get(key).await }) + retry_read(crate::CMD_GET, || async { self.conn().get(key).await }) .await .map_err(|error| { tracing::warn!(cause = ?error, key, "kv-store: redis get"); @@ -129,12 +131,14 @@ impl Store for RedisStore { min: f64, max: f64, ) -> Result, Error> { - retry_read(|| async { self.conn().zrangebyscore_withscores(key, min, max).await }) - .await - .map_err(|error| { - tracing::warn!(cause = ?error, key, min, max, "kv-store: redis zrangebyscore"); - Error::InternalError - }) + retry_read(crate::CMD_ZRANGE_BY_SCORE, || async { + self.conn().zrangebyscore_withscores(key, min, max).await + }) + .await + .map_err(|error| { + tracing::warn!(cause = ?error, key, min, max, "kv-store: redis zrangebyscore"); + Error::InternalError + }) } async fn scan(&self, pattern: &str) -> Result, Error> { @@ -145,6 +149,7 @@ impl Store for RedisStore { Ok(it) => break it, Err(error) if attempt < REDIS_READ_RETRIES => { attempt += 1; + crate::note_read_retry(crate::CMD_SCAN); tracing::debug!(attempt, cause = ?error, pattern, "kv-store: redis scan_match retry"); tokio::time::sleep(redis_read_retry_delay(attempt)).await; } @@ -172,6 +177,7 @@ impl Store for RedisStore { Ok(it) => break it, Err(error) if attempt < REDIS_READ_RETRIES => { attempt += 1; + crate::note_read_retry(crate::CMD_ZSCAN); tracing::debug!(attempt, cause = ?error, key, pattern, "kv-store: redis zscan_match retry"); tokio::time::sleep(redis_read_retry_delay(attempt)).await; } @@ -192,7 +198,7 @@ impl Store for RedisStore { } async fn bf_exists(&self, key: &str, item: &str) -> Result { - retry_read(|| async { + retry_read(crate::CMD_BF_EXISTS, || async { redis::cmd("BF.EXISTS") .arg(key) .arg(item) From f8376a5c5ea3bd15e921507843fdad87d555d988 Mon Sep 17 00:00:00 2001 From: Ruslan Pislari Date: Wed, 9 Sep 2026 10:47:32 +0300 Subject: [PATCH 04/22] fix(http-service): preserve error chain so wasi-http timeouts return 532 When the guest trapped before invoking `response-outparam::set`, the error was rebuilt with `bail!("...: {e:?}")`, which flattens the cause into a string and drops the source chain. `map_err`/`fail_reason_of` classify errors by downcasting `root_cause()` to `wasmtime::Trap` or `Elapsed`, so every such failure fell through to the generic branch and returned 530/INTERNAL_STATUS_EXECUTE_ERROR instead of 532/3010 - even though the stats row was correctly recorded as TIMEOUT. Use `.context()` to attach the message while keeping the chain intact. Also stop logging the same backtrace twice: the spawned task logged "incoming handler" and the error was then logged again as "execute" after propagating up. The task-side log now only fires when the error cannot propagate (response headers already flushed), which was the only case it was needed for. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/http-service/src/executor/wasi_http.rs | 34 +++++++++++++++---- crates/http-service/src/lib.rs | 21 ++++++++++++ 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/crates/http-service/src/executor/wasi_http.rs b/crates/http-service/src/executor/wasi_http.rs index 90a083c..daab131 100644 --- a/crates/http-service/src/executor/wasi_http.rs +++ b/crates/http-service/src/executor/wasi_http.rs @@ -6,7 +6,7 @@ use crate::executor; use crate::executor::{HttpExecutor, X_CDN_REAL_HOST}; use crate::state::HttpState; use ::http::{HeaderMap, Request, Response, Uri, header}; -use anyhow::{Context, anyhow, bail}; +use anyhow::{Context, anyhow}; use async_trait::async_trait; use http_backend::Backend; use http_body_util::{BodyExt, Full}; @@ -178,7 +178,6 @@ where Err(elapsed) => Err(elapsed.into()), }; if let Err(e) = exec_result { - tracing::warn!(cause=?e, "incoming handler"); // Record the failure reason on the shared stats. The response // headers may already have been flushed (the guest called // `response-outparam::set` before trapping mid-body), in which @@ -203,11 +202,27 @@ where ); match receiver.await { - Ok(Ok(response)) => { - stats.status_code(response.status().as_u16()); - Ok(response) + Ok(inner) => { + // The guest already invoked `response-outparam::set`, so a later + // failure of the task (e.g. an epoch interrupt while streaming the + // body) is never propagated to the caller. Log it here - this is + // the only place it can be reported. + tokio::task::spawn( + async move { + if let Ok(Err(e)) = task.await { + tracing::warn!(cause=?e, "incoming handler"); + } + } + .in_current_span(), + ); + match inner { + Ok(response) => { + stats.status_code(response.status().as_u16()); + Ok(response) + } + Err(error) => Err(error.into()), + } } - Ok(Err(error)) => Err(error.into()), Err(_) => { let e = match task.await { Ok(r) => { @@ -215,7 +230,12 @@ where } Err(e) => e.into(), }; - bail!("guest never invoked `response-outparam::set` method: {e:?}") + // Attach context instead of formatting `e` into a new message so the + // source chain is preserved: `map_err`/`fail_reason_of` rely on + // `root_cause()` downcasting to `wasmtime::Trap` / `Elapsed` to + // classify timeouts. Flattening it into a string made every failure + // here look like a generic execute error (530 instead of 532). + Err(e).context("guest never invoked `response-outparam::set` method") } } } diff --git a/crates/http-service/src/lib.rs b/crates/http-service/src/lib.rs index 77f8359..49c4204 100644 --- a/crates/http-service/src/lib.rs +++ b/crates/http-service/src/lib.rs @@ -866,4 +866,25 @@ mod tests { let id = AppName::Id(1234); assert_eq!("1234", id.to_string()); } + + // ── Error classification ───────────────────────────────────────────── + + /// A timeout trap must stay classified as a timeout even when the + /// `wasi_http` executor wraps it with `guest never invoked + /// `response-outparam::set``. Formatting the cause into the message instead + /// of chaining it loses the `Trap` downcast and yields 530 instead of 532. + #[test] + fn test_map_err_keeps_timeout_through_context() { + use anyhow::Context; + + let inner = anyhow::Error::from(wasmtime::Trap::Interrupt).context("error while executing"); + let wrapped = Err::<(), _>(inner) + .context("guest never invoked `response-outparam::set` method") + .unwrap_err(); + + let (status_code, fail_reason, _msg, internal_code) = crate::map_err(wrapped); + assert_eq!(crate::FASTEDGE_EXECUTION_TIMEOUT, status_code); + assert_eq!(runtime::AppResult::TIMEOUT as i32, fail_reason as i32); + assert_eq!(crate::INTERNAL_STATUS_TIMEOUT_INTERRUPT, internal_code); + } } From 72817c7b55caaa42d11cef1afe0b9a69d35adf35 Mon Sep 17 00:00:00 2001 From: Ruslan Pislari Date: Wed, 9 Sep 2026 15:39:48 +0300 Subject: [PATCH 05/22] fix(wasi-http): improve error handling for response tasks and logging --- crates/http-service/src/executor/wasi_http.rs | 44 ++++++++++++------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/crates/http-service/src/executor/wasi_http.rs b/crates/http-service/src/executor/wasi_http.rs index daab131..7b5960a 100644 --- a/crates/http-service/src/executor/wasi_http.rs +++ b/crates/http-service/src/executor/wasi_http.rs @@ -202,32 +202,42 @@ where ); match receiver.await { - Ok(inner) => { - // The guest already invoked `response-outparam::set`, so a later - // failure of the task (e.g. an epoch interrupt while streaming the - // body) is never propagated to the caller. Log it here - this is - // the only place it can be reported. + Ok(Ok(response)) => { + // The response headers are already on their way to the client, so a + // later failure of the task (e.g. an epoch interrupt while streaming + // the body) can no longer be propagated to the caller. This watcher + // is the only place it can be reported. + // + // It is spawned *only* on this path. On every other path the error + // still reaches the caller and is logged upstream, so watching here + // as well would log the same failure twice. tokio::task::spawn( async move { - if let Ok(Err(e)) = task.await { - tracing::warn!(cause=?e, "incoming handler"); + match task.await { + Ok(Ok(())) => {} + Ok(Err(e)) => tracing::warn!(cause=?e, "incoming handler"), + // Panic or cancellation. Nothing else observes the task + // once headers are flushed, so report it here too. + Err(e) => tracing::warn!(cause=?e, "incoming handler task"), } } .in_current_span(), ); - match inner { - Ok(response) => { - stats.status_code(response.status().as_u16()); - Ok(response) - } - Err(error) => Err(error.into()), - } + stats.status_code(response.status().as_u16()); + Ok(response) } + // The guest set an error response, which is returned to the caller + // below - so the task must not log it as well. Dropping the handle only + // detaches the task: it still runs to completion and still records + // `fail_reason` on the shared stats. + Ok(Err(error)) => Err(error.into()), Err(_) => { let e = match task.await { - Ok(r) => { - r.expect_err("if the receiver has an error, the task must have failed") - } + // The guest returned without ever setting the outparam, so there + // is no task error to attribute. Synthesise one rather than + // asserting the task failed, which would panic on this path. + Ok(Ok(())) => anyhow!("handler completed without setting a response"), + Ok(Err(e)) => e, Err(e) => e.into(), }; // Attach context instead of formatting `e` into a new message so the From a0b0ab20303d0e0addac457c6db341057258929c Mon Sep 17 00:00:00 2001 From: Ruslan Pislari Date: Wed, 9 Sep 2026 14:34:43 +0300 Subject: [PATCH 06/22] feat(metrics): consolidate app call metrics into unified `fastedge_calls_total` Replace `fastedge_call_count` and `fastedge_error_total_count` with a single `fastedge_calls_total` metric that tracks all app call outcomes under an `outcome` label. Add support for the new `SUSPENDED` app result. Update HTTP service to emit metrics for requests with `DISABLED`, `RATE_LIMITED`, and `SUSPENDED` statuses. Add unit test coverage to ensure correct metric allocation across outcomes. --- crates/http-service/src/lib.rs | 3 +++ crates/runtime/src/lib.rs | 2 ++ 2 files changed, 5 insertions(+) diff --git a/crates/http-service/src/lib.rs b/crates/http-service/src/lib.rs index 49c4204..728fd96 100644 --- a/crates/http-service/src/lib.rs +++ b/crates/http-service/src/lib.rs @@ -311,6 +311,7 @@ where Some((app_name, cfg)) if cfg.status == Status::Draft || cfg.status == Status::Disabled => { + metrics::metrics(AppResult::DISABLED, HTTP_LABEL, None, None); tracing::info!( "Request for disabled application '{}' on URL: {}", app_name, @@ -319,6 +320,7 @@ where return not_found(); } Some((app_name, cfg)) if cfg.status == Status::RateLimited => { + metrics::metrics(AppResult::RATE_LIMITED, HTTP_LABEL, None, None); tracing::info!( "Request for rate limited application '{}' on URL: {}", app_name, @@ -327,6 +329,7 @@ where return too_many_requests(); } Some((app_name, cfg)) if cfg.status == Status::Suspended => { + metrics::metrics(AppResult::SUSPENDED, HTTP_LABEL, None, None); tracing::info!( "Request for suspended application '{}' on URL: {}", app_name, diff --git a/crates/runtime/src/lib.rs b/crates/runtime/src/lib.rs index b85a515..aa01745 100644 --- a/crates/runtime/src/lib.rs +++ b/crates/runtime/src/lib.rs @@ -68,6 +68,8 @@ pub enum AppResult { RATE_LIMITED, /// Request rejected because the app is disabled or in draft. DISABLED, + /// Request rejected because the app is suspended. + SUSPENDED, } pub type InstancePre = wasmtime::component::InstancePre>; From f458c5e3e9568fef9c3cc17b0c83607f9f2ab92b Mon Sep 17 00:00:00 2001 From: Ruslan Pislari Date: Mon, 14 Sep 2026 15:33:59 +0300 Subject: [PATCH 07/22] feat(proxywasm): add cache support with new host functions and cache backend integration --- crates/cache/src/lib.rs | 60 ++++++++++++++++++++++++++++++++++------- 1 file changed, 51 insertions(+), 9 deletions(-) diff --git a/crates/cache/src/lib.rs b/crates/cache/src/lib.rs index b0d4180..7913482 100644 --- a/crates/cache/src/lib.rs +++ b/crates/cache/src/lib.rs @@ -49,42 +49,84 @@ impl CacheImpl { pub fn new(backend: Arc) -> Self { Self { backend } } + + /// Get the value associated with `key`. + pub async fn get(&self, key: &str) -> Result, Error> { + self.backend.get(key).await + } + + /// Set the value for `key` with an optional expiry in milliseconds. + pub async fn set(&self, key: &str, value: Payload, ttl_ms: Option) -> Result<(), Error> { + self.backend.set(key, value, ttl_ms).await + } + + /// Delete the key-value pair associated with `key`. + pub async fn delete(&self, key: &str) -> Result<(), Error> { + self.backend.delete(key).await + } + + /// Check whether `key` exists in the cache. + pub async fn exists(&self, key: &str) -> Result { + self.backend.exists(key).await + } + + /// Increment the integer value stored at `key` by `delta`. + pub async fn incr(&self, key: &str, delta: i64) -> Result { + self.backend.incr(key, delta).await + } + + /// Set or update the expiry of `key` to `ttl_ms` milliseconds from now. + pub async fn expire(&self, key: &str, ttl_ms: u64) -> Result { + self.backend.expire(key, ttl_ms).await + } + + /// Purge every cache entry owned by the calling application. + pub async fn purge(&self) -> Result { + self.backend.purge().await + } + + /// Purge every cache entry whose key begins with `prefix`. + pub async fn purge_prefix(&self, prefix: &str) -> Result { + self.backend.purge_prefix(prefix).await + } } // Empty marker trait impl cache_types::Host for CacheImpl {} // Implement the sync-style cache interface (gcore:fastedge/cache-sync) -// This is simpler and serializes calls through &mut self. +// This is simpler and serializes calls through &mut self. Every method +// delegates to the inherent `CacheImpl` method of the same name, which the +// ProxyWasm host functions also call. impl cache_sync::Host for CacheImpl { async fn get(&mut self, key: String) -> Result, Error> { - self.backend.get(&key).await + CacheImpl::get(self, &key).await } async fn set(&mut self, key: String, value: Payload, ttl_ms: Option) -> Result<(), Error> { - self.backend.set(&key, value, ttl_ms).await + CacheImpl::set(self, &key, value, ttl_ms).await } async fn delete(&mut self, key: String) -> Result<(), Error> { - self.backend.delete(&key).await + CacheImpl::delete(self, &key).await } async fn exists(&mut self, key: String) -> Result { - self.backend.exists(&key).await + CacheImpl::exists(self, &key).await } async fn incr(&mut self, key: String, delta: i64) -> Result { - self.backend.incr(&key, delta).await + CacheImpl::incr(self, &key, delta).await } async fn expire(&mut self, key: String, ttl_ms: u64) -> Result { - self.backend.expire(&key, ttl_ms).await + CacheImpl::expire(self, &key, ttl_ms).await } async fn purge(&mut self) -> Result { - self.backend.purge().await + CacheImpl::purge(self).await } async fn purge_prefix(&mut self, prefix: String) -> Result { - self.backend.purge_prefix(&prefix).await + CacheImpl::purge_prefix(self, &prefix).await } } From 20520c788ffeaf391adcc3dd5a5936c5a31cef60 Mon Sep 17 00:00:00 2001 From: Ruslan Pislari Date: Tue, 15 Sep 2026 12:04:34 +0300 Subject: [PATCH 08/22] feat(metrics): add value cache metrics and improve lifecycle management - Added `fastedge_kv_cache_bytes`, `_entries`, and `_max_bytes` metrics for tracking in-process key-value store cache occupancy. - Implemented lifecycle management to dynamically remove metrics for idle/disconnected stores. - Updated configuration to accurately define default cache sizes in bytes and ensured compatibility with Prometheus scraping. - Enhanced test coverage for cache behavior, including capacity, eviction, oversized values, and label sanitization. --- crates/runtime/src/app.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/crates/runtime/src/app.rs b/crates/runtime/src/app.rs index ee5acde..3dc25a6 100644 --- a/crates/runtime/src/app.rs +++ b/crates/runtime/src/app.rs @@ -44,6 +44,10 @@ pub struct KvStoreOption { pub name: SmolStr, #[serde(default)] pub prefix: SmolStr, + /// Maximum **bytes** held by this store's in-process value cache (keys, + /// values and per-entry overhead), not a number of entries: entry count + /// cannot bound memory, since the same 25 000 entries are 2 MiB of + /// 64-byte values or 200 MiB of 8 KiB ones. #[serde(default = "KvStoreOption::default_cache_size")] pub cache_size: u64, #[serde(default = "KvStoreOption::default_cache_ttl")] @@ -51,8 +55,11 @@ pub struct KvStoreOption { } impl KvStoreOption { + /// 100 MiB. Was 25 000 *entries* until 2026-09-15; the unit changed with + /// the cache's weigher, and the field was set nowhere in prod or preprod, + /// so every store simply takes this default. fn default_cache_size() -> u64 { - 25000 + 10 * 1024 * 1024 } fn default_cache_ttl() -> u64 { @@ -306,7 +313,7 @@ mod tests { assert_eq!(kv.param, ""); assert_eq!(kv.name, "store"); assert_eq!(kv.prefix, "pre"); - assert_eq!(kv.cache_size, 25000); + assert_eq!(kv.cache_size, 10 * 1024 * 1024); assert_eq!(kv.cache_ttl, 60); } @@ -316,14 +323,14 @@ mod tests { "param": "url2", "name": "store2", "prefix": "pre2", - "cache_size": 5000, + "cache_size": 5000000, "cache_ttl": 120 }"#; let kv: KvStoreOption = serde_json::from_str(json).unwrap(); assert_eq!(kv.param, "url2"); assert_eq!(kv.name, "store2"); assert_eq!(kv.prefix, "pre2"); - assert_eq!(kv.cache_size, 5000); + assert_eq!(kv.cache_size, 5000000); assert_eq!(kv.cache_ttl, 120); } From 4bb22f3ff72841eeb4a6b203b0c03df36defdb7e Mon Sep 17 00:00:00 2001 From: Ruslan Pislari Date: Tue, 15 Sep 2026 15:53:17 +0300 Subject: [PATCH 09/22] feat(kv-cache): enhance cache identity management with distinct store_id and metric_label --- crates/runtime/src/app.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/runtime/src/app.rs b/crates/runtime/src/app.rs index 3dc25a6..e25e4f4 100644 --- a/crates/runtime/src/app.rs +++ b/crates/runtime/src/app.rs @@ -55,7 +55,7 @@ pub struct KvStoreOption { } impl KvStoreOption { - /// 100 MiB. Was 25 000 *entries* until 2026-09-15; the unit changed with + /// 10 MiB. Was 25 000 *entries* until 2026-09-15; the unit changed with /// the cache's weigher, and the field was set nowhere in prod or preprod, /// so every store simply takes this default. fn default_cache_size() -> u64 { From c8d634279d25d06ae385f27d147c8755e1fe5189 Mon Sep 17 00:00:00 2001 From: Ruslan Pislari Date: Wed, 16 Sep 2026 12:08:56 +0300 Subject: [PATCH 10/22] feat(kv-store): improve error handling and retry logic for Redis operations - Enhanced error classification for Redis read failures with clearer differentiation between transient and permanent errors. - Introduced `retry_backoff`, `is_guest_fault`, and `read_error` helpers to streamline retry logic. - Optimized retry mechanism with capped backoff for `WAIT_AND_RETRY`. - Improved guest-facing error messages for deterministic errors. - Updated unit tests to cover new error handling logic comprehensively. - Integrated `read_status_of` in ProxyWasm host to map errors to appropriate status codes. --- crates/key-value-store/src/redis_impl.rs | 379 +++++++++++++++++++++-- crates/runtime/src/app.rs | 2 +- 2 files changed, 352 insertions(+), 29 deletions(-) diff --git a/crates/key-value-store/src/redis_impl.rs b/crates/key-value-store/src/redis_impl.rs index 1136ee5..92c5bcb 100644 --- a/crates/key-value-store/src/redis_impl.rs +++ b/crates/key-value-store/src/redis_impl.rs @@ -1,7 +1,7 @@ use crate::Store; use reactor::gcore::fastedge::key_value::{Error, Value}; use redis::aio::{ConnectionManager, ConnectionManagerConfig}; -use redis::{AsyncCommands, AsyncIter}; +use redis::{AsyncCommands, AsyncIter, RedisError, RetryMethod}; use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; use std::time::Duration; @@ -24,9 +24,89 @@ const REDIS_READ_RETRIES: usize = 2; const REDIS_READ_RETRY_BASE_DELAY: Duration = Duration::from_millis(10); const REDIS_READ_RETRY_MAX_DELAY: Duration = Duration::from_millis(50); +/// Backoff applied between attempts when the server asked us to wait. +/// Saturating throughout, so a larger retry budget can never overflow the +/// shift or the multiplication into a panic. fn redis_read_retry_delay(attempt: usize) -> Duration { - let scale = 1u32 << (attempt.saturating_sub(1) as u32); - (REDIS_READ_RETRY_BASE_DELAY * scale).min(REDIS_READ_RETRY_MAX_DELAY) + let scale = 1u32 + .checked_shl(attempt.saturating_sub(1) as u32) + .unwrap_or(u32::MAX); + REDIS_READ_RETRY_BASE_DELAY + .saturating_mul(scale) + .min(REDIS_READ_RETRY_MAX_DELAY) +} + +/// How long to wait before re-attempting a failed read, or `None` when the +/// failure must not be retried at all. `attempt` is the 1-based number of the +/// attempt about to be made, used to scale the backoff. +/// +/// Allowlist rather than denylist: [`RetryMethod`] is `#[non_exhaustive]`, so a +/// redis-crate bump can introduce a variant, and defaulting those to "retry" +/// would silently spend the budget on failures we have never classified. +/// +/// The cluster redirect methods (`MovedRedirect`, `AskRedirect`, +/// `RefreshSlotsAndRetry`, `ReconnectFromInitialConnections`) are deliberately +/// excluded: this store speaks to Redis through a plain `ConnectionManager`, +/// not the cluster API, so nothing here can follow a redirect — a bare retry +/// would just re-send to the same node and collect the same error. +fn retry_backoff(error: &RedisError, attempt: usize) -> Option { + match error.retry_method() { + // Nothing to back off from: the command never reached a working + // server, so there is no load to relieve and sleeping would only add + // latency to the request. `Reconnect` means the socket is gone or the + // stream desynced — re-running the op picks the next pooled connection + // while `ConnectionManager` re-establishes its own socket underneath + // (with its own capped backoff), so the next attempt already gets a + // fresh channel. + RetryMethod::RetryImmediately | RetryMethod::Reconnect => Some(Duration::ZERO), + // The server is reachable but told us it cannot serve yet (`LOADING`, + // `TRYAGAIN`). Here the pause is the point: it keeps a struggling node + // from being hammered by every in-flight request at once. + RetryMethod::WaitAndRetry => Some(redis_read_retry_delay(attempt)), + _ => None, + } +} + +/// Whether the server rejected the command in a way that can never succeed on +/// retry, because the guest's own request was at fault — e.g. `WRONGTYPE` when +/// `get` addresses a key holding a sorted set, or `NOPERM`/`ERR` for a command +/// the guest may not run. +/// +/// Such a rejection says nothing about node or Redis health, so it must not +/// raise a node-level warning; it is handed back to the guest instead. +/// +/// Distinct from `!should_retry`: a `MOVED` reply or a failed authentication is +/// equally pointless to retry, but it is the node's problem, not the app's, so +/// it stays an opaque internal error and still warns. +/// +/// Only failures the server itself answered are classified: [`RedisError::code`] +/// is `None` for I/O, timeout and client-config failures. +fn is_guest_fault(error: &RedisError) -> bool { + matches!(error.retry_method(), RetryMethod::NoRetry) && error.code().is_some() +} + +/// The server's own code and detail, for handing back to the guest. +fn guest_fault_message(error: &RedisError) -> String { + let code = error.code().unwrap_or_default(); + match error.detail() { + Some(detail) => format!("{code}: {detail}"), + None => code.to_string(), + } +} + +/// Map a failed read onto the error the guest sees. +/// +/// A guest-caused rejection is returned verbatim as [`Error::Other`] so the app +/// can fix its request, and is only traced at debug level. Anything else is +/// opaque to the guest and is reported by `log_unexpected`, which each caller +/// supplies so the warning keeps that command's own fields. +fn read_error(error: RedisError, log_unexpected: impl FnOnce(&RedisError)) -> Error { + if is_guest_fault(&error) { + tracing::debug!(cause = ?error, "kv-store: redis rejected read"); + return Error::Other(guest_fault_message(&error)); + } + log_unexpected(&error); + Error::InternalError } /// Retry a read command up to `REDIS_READ_RETRIES` times with capped @@ -35,6 +115,11 @@ fn redis_read_retry_delay(attempt: usize) -> Duration { /// `bf_exists`; `scan`/`zscan` retry the same connection since their iterator /// borrows it across the loop). Each retry is reported to the installed /// [`crate::ReadRetryObserver`] under `command`. +/// +/// Only failures [`retry_backoff`] accepts are attempted again, and only those +/// that ask for it sleep first; deterministic failures (a `WRONGTYPE` key, a +/// `MOVED` slot) return on the first error rather than spending the budget on a +/// result that cannot change. async fn retry_read(command: &'static str, mut op: F) -> Result where F: FnMut() -> Fut, @@ -44,13 +129,20 @@ where loop { match op().await { Ok(value) => return Ok(value), - Err(error) if attempt < REDIS_READ_RETRIES => { + Err(error) => { + if attempt >= REDIS_READ_RETRIES { + return Err(error); + } + let Some(delay) = retry_backoff(&error, attempt + 1) else { + return Err(error); + }; attempt += 1; crate::note_read_retry(command); tracing::debug!(attempt, cause = ?error, "kv-store: redis read retry"); - tokio::time::sleep(redis_read_retry_delay(attempt)).await; + if !delay.is_zero() { + tokio::time::sleep(delay).await; + } } - Err(error) => return Err(error), } } } @@ -120,8 +212,9 @@ impl Store for RedisStore { retry_read(crate::CMD_GET, || async { self.conn().get(key).await }) .await .map_err(|error| { - tracing::warn!(cause = ?error, key, "kv-store: redis get"); - Error::InternalError + read_error(error, |error| { + tracing::warn!(cause = ?error, key, "kv-store: redis get"); + }) }) } @@ -136,8 +229,9 @@ impl Store for RedisStore { }) .await .map_err(|error| { - tracing::warn!(cause = ?error, key, min, max, "kv-store: redis zrangebyscore"); - Error::InternalError + read_error(error, |error| { + tracing::warn!(cause = ?error, key, min, max, "kv-store: redis zrangebyscore"); + }) }) } @@ -147,23 +241,32 @@ impl Store for RedisStore { let mut it = loop { match conn.scan_match(pattern).await { Ok(it) => break it, - Err(error) if attempt < REDIS_READ_RETRIES => { + Err(error) => { + let backoff = if attempt < REDIS_READ_RETRIES { + retry_backoff(&error, attempt + 1) + } else { + None + }; + let Some(delay) = backoff else { + return Err(read_error(error, |error| { + tracing::warn!(cause = ?error, pattern, "kv-store: redis scan_match"); + })); + }; attempt += 1; crate::note_read_retry(crate::CMD_SCAN); tracing::debug!(attempt, cause = ?error, pattern, "kv-store: redis scan_match retry"); - tokio::time::sleep(redis_read_retry_delay(attempt)).await; - } - Err(error) => { - tracing::warn!(cause = ?error, pattern, "kv-store: redis scan_match"); - return Err(Error::InternalError); + if !delay.is_zero() { + tokio::time::sleep(delay).await; + } } } }; let mut ret = vec![]; while let Some(element) = it.next_item().await { ret.push(element.map_err(|error| { - tracing::warn!(cause = ?error, pattern, "kv-store: redis scan_match: item"); - Error::Other(error.to_string()) + read_error(error, |error| { + tracing::warn!(cause = ?error, pattern, "kv-store: redis scan_match: item"); + }) })?); } Ok(ret) @@ -175,23 +278,32 @@ impl Store for RedisStore { let mut it: AsyncIter<(Value, f64)> = loop { match conn.zscan_match(key, pattern).await { Ok(it) => break it, - Err(error) if attempt < REDIS_READ_RETRIES => { + Err(error) => { + let backoff = if attempt < REDIS_READ_RETRIES { + retry_backoff(&error, attempt + 1) + } else { + None + }; + let Some(delay) = backoff else { + return Err(read_error(error, |error| { + tracing::warn!(cause = ?error, key, pattern, "kv-store: redis zscan_match"); + })); + }; attempt += 1; crate::note_read_retry(crate::CMD_ZSCAN); tracing::debug!(attempt, cause = ?error, key, pattern, "kv-store: redis zscan_match retry"); - tokio::time::sleep(redis_read_retry_delay(attempt)).await; - } - Err(error) => { - tracing::warn!(cause = ?error, key, pattern, "kv-store: redis zscan_match"); - return Err(Error::InternalError); + if !delay.is_zero() { + tokio::time::sleep(delay).await; + } } } }; let mut ret = vec![]; while let Some(element) = it.next_item().await { ret.push(element.map_err(|error| { - tracing::warn!(cause = ?error, key, pattern, "kv-store: redis zscan_match: item"); - Error::Other(error.to_string()) + read_error(error, |error| { + tracing::warn!(cause = ?error, key, pattern, "kv-store: redis zscan_match: item"); + }) })?); } Ok(ret) @@ -207,8 +319,219 @@ impl Store for RedisStore { }) .await .map_err(|error| { - tracing::warn!(cause = ?error, key, item, "kv-store: redis bf_exists"); - Error::InternalError + read_error(error, |error| { + tracing::warn!(cause = ?error, key, item, "kv-store: redis bf_exists"); + }) + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::atomic::AtomicUsize; + + /// Build a real `RedisError` by parsing a RESP error frame, so the tests + /// exercise the same classification path as a live server reply. + fn server_error(frame: &str) -> RedisError { + match ::redis::parse_redis_value(frame.as_bytes()) { + Ok(::redis::Value::ServerError(error)) => error.into(), + other => panic!("RESP error frame must parse as a server error: {other:?}"), + } + } + + #[test] + fn wrongtype_is_a_guest_fault() { + let error = + server_error("-WRONGTYPE Operation against a key holding the wrong kind of value\r\n"); + assert!(is_guest_fault(&error)); + assert_eq!( + guest_fault_message(&error), + "WRONGTYPE: Operation against a key holding the wrong kind of value" + ); + } + + #[test] + fn other_permanent_server_rejections_are_guest_faults() { + for frame in [ + "-ERR wrong number of arguments for 'get' command\r\n", + "-NOPERM this user has no permissions to run the 'get' command\r\n", + ] { + assert!(is_guest_fault(&server_error(frame)), "frame: {frame}"); + } + } + + #[test] + fn retriable_server_errors_are_not_guest_faults() { + // LOADING/TRYAGAIN are transient: the same command can succeed later, + // so they must keep retrying and keep warning. + for frame in [ + "-LOADING Redis is loading the dataset in memory\r\n", + "-TRYAGAIN Multiple keys request during rehashing of slot\r\n", + ] { + let error = server_error(frame); + assert!(!is_guest_fault(&error), "frame: {frame}"); + // The server asked us to wait, so the pause is the point. + assert_eq!( + retry_backoff(&error, 1), + Some(REDIS_READ_RETRY_BASE_DELAY), + "frame: {frame}" + ); + } + } + + #[test] + fn deterministic_errors_are_not_retried() { + for frame in [ + "-WRONGTYPE Operation against a key holding the wrong kind of value\r\n", + "-ERR wrong number of arguments for 'get' command\r\n", + "-NOPERM this user has no permissions to run the 'get' command\r\n", + // Cluster redirects: this store uses a plain (non-cluster) client, + // so a bare retry can only re-collect the same reply. + "-MOVED 3999 127.0.0.1:6381\r\n", + "-ASK 3999 127.0.0.1:6381\r\n", + ] { + assert_eq!( + retry_backoff(&server_error(frame), 1), + None, + "frame: {frame}" + ); + } + } + + #[test] + fn cluster_redirects_are_node_problems_not_guest_faults() { + // Not retriable, but the app did nothing wrong: must stay opaque and + // still warn, unlike WRONGTYPE. + let error = server_error("-MOVED 3999 127.0.0.1:6381\r\n"); + assert_eq!(retry_backoff(&error, 1), None); + assert!(!is_guest_fault(&error)); + + let mut warned = false; + let mapped = read_error(error, |_| warned = true); + assert!(warned); + assert!(matches!(mapped, Error::InternalError)); + } + + #[test] + fn connection_errors_are_retried_but_are_not_guest_faults() { + // No server reply at all, so there is no code to hand back. + let error = RedisError::from(std::io::Error::from(std::io::ErrorKind::ConnectionReset)); + assert!(!is_guest_fault(&error)); + // A dead socket is not congestion: reconnect and re-issue at once + // rather than adding latency to a request already on the hot path. + assert_eq!(retry_backoff(&error, 1), Some(Duration::ZERO)); + } + + #[test] + fn wait_and_retry_backoff_grows_and_is_capped() { + let error = server_error("-LOADING Redis is loading the dataset in memory\r\n"); + assert_eq!(retry_backoff(&error, 1), Some(REDIS_READ_RETRY_BASE_DELAY)); + assert_eq!( + retry_backoff(&error, 2), + Some(REDIS_READ_RETRY_BASE_DELAY * 2) + ); + assert_eq!(retry_backoff(&error, 99), Some(REDIS_READ_RETRY_MAX_DELAY)); + } + + #[test] + fn guest_fault_is_returned_verbatim_without_warning() { + let error = + server_error("-WRONGTYPE Operation against a key holding the wrong kind of value\r\n"); + let mut warned = false; + let mapped = read_error(error, |_| warned = true); + + assert!(!warned, "a guest-caused rejection must not warn"); + assert!( + matches!(mapped, Error::Other(message) if message.starts_with("WRONGTYPE: ")), + "guest must receive the server's own message" + ); + } + + #[test] + fn unexpected_error_warns_and_stays_opaque() { + let error = RedisError::from(std::io::Error::from(std::io::ErrorKind::ConnectionReset)); + let mut warned = false; + let mapped = read_error(error, |_| warned = true); + + assert!(warned); + assert!(matches!(mapped, Error::InternalError)); + } + + #[tokio::test] + async fn retry_read_does_not_retry_a_guest_fault() { + let attempts = AtomicUsize::new(0); + let result: Result<(), _> = retry_read(crate::CMD_GET, || async { + attempts.fetch_add(1, Ordering::Relaxed); + Err(server_error( + "-WRONGTYPE Operation against a key holding the wrong kind of value\r\n", + )) }) + .await; + + assert!(result.is_err()); + assert_eq!( + attempts.load(Ordering::Relaxed), + 1, + "a deterministic rejection must not burn the retry budget" + ); + } + + #[tokio::test] + async fn retry_read_does_not_retry_a_cluster_redirect() { + let attempts = AtomicUsize::new(0); + let result: Result<(), _> = retry_read(crate::CMD_GET, || async { + attempts.fetch_add(1, Ordering::Relaxed); + Err(server_error("-MOVED 3999 127.0.0.1:6381\r\n")) + }) + .await; + + assert!(result.is_err()); + assert_eq!(attempts.load(Ordering::Relaxed), 1); + } + + #[tokio::test] + async fn retry_read_still_retries_transient_errors() { + let attempts = AtomicUsize::new(0); + let started = std::time::Instant::now(); + let result: Result<(), _> = retry_read(crate::CMD_GET, || async { + attempts.fetch_add(1, Ordering::Relaxed); + Err(RedisError::from(std::io::Error::from( + std::io::ErrorKind::ConnectionReset, + ))) + }) + .await; + + assert!(result.is_err()); + assert_eq!( + attempts.load(Ordering::Relaxed), + REDIS_READ_RETRIES + 1, + "transient failures must still use the full retry budget" + ); + assert!( + started.elapsed() < REDIS_READ_RETRY_BASE_DELAY, + "a dead socket must be re-issued without sleeping, took {:?}", + started.elapsed() + ); + } + + #[tokio::test] + async fn retry_read_sleeps_only_when_the_server_asks_to_wait() { + let attempts = AtomicUsize::new(0); + let started = std::time::Instant::now(); + let result: Result<(), _> = retry_read(crate::CMD_GET, || async { + attempts.fetch_add(1, Ordering::Relaxed); + Err(server_error( + "-LOADING Redis is loading the dataset in memory\r\n", + )) + }) + .await; + + assert!(result.is_err()); + assert_eq!(attempts.load(Ordering::Relaxed), REDIS_READ_RETRIES + 1); + assert!( + started.elapsed() >= REDIS_READ_RETRY_BASE_DELAY, + "a loading node must be given time to recover" + ); } } diff --git a/crates/runtime/src/app.rs b/crates/runtime/src/app.rs index e25e4f4..2d6612a 100644 --- a/crates/runtime/src/app.rs +++ b/crates/runtime/src/app.rs @@ -63,7 +63,7 @@ impl KvStoreOption { } fn default_cache_ttl() -> u64 { - 60 + 300 } } From 04e81d79d7e0b0a10ba8a9e5e1bb8b3410eaa07c Mon Sep 17 00:00:00 2001 From: Ruslan Pislari Date: Wed, 16 Sep 2026 12:20:55 +0300 Subject: [PATCH 11/22] feat(runtime): reduce default cache TTL to 60 seconds - Updated default cache TTL from 300 seconds to 60 seconds to enable quicker expiration. --- crates/runtime/src/app.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/runtime/src/app.rs b/crates/runtime/src/app.rs index 2d6612a..e25e4f4 100644 --- a/crates/runtime/src/app.rs +++ b/crates/runtime/src/app.rs @@ -63,7 +63,7 @@ impl KvStoreOption { } fn default_cache_ttl() -> u64 { - 300 + 60 } } From 40e85f8ed1423454a38fafa161e06aaded41a74d Mon Sep 17 00:00:00 2001 From: Ruslan Pislari Date: Fri, 18 Sep 2026 11:40:32 +0300 Subject: [PATCH 12/22] feat(runtime): introduce epoch grace mechanism for improved timeout handling Add `epoch_grace` module to refine epoch timeout decisions by distinguishing kernel-parked threads from CPU-exhausted guests. Track grace outcomes with the new `fastedge_epoch_stall_grace_total` metric. Update store functionality to utilize CPU-aware deadlines, enhancing reliability under memory pressure. --- Cargo.lock | 5 + crates/runtime/Cargo.toml | 2 + crates/runtime/src/lib.rs | 7 ++ crates/runtime/src/store.rs | 161 +++++++++++++++++++++++++---- crates/runtime/src/util/metrics.rs | 21 ++++ 5 files changed, 176 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e1fe023..92c8bd0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3116,6 +3116,7 @@ dependencies = [ "http", "http-backend", "key-value-store", + "libc", "moka", "secret", "serde", @@ -5587,3 +5588,7 @@ dependencies = [ "cc", "pkg-config", ] + +[[patch.unused]] +name = "fastedge_proxywasm" +version = "0.5.0" diff --git a/crates/runtime/Cargo.toml b/crates/runtime/Cargo.toml index 1a7f9a9..a902c3e 100644 --- a/crates/runtime/Cargo.toml +++ b/crates/runtime/Cargo.toml @@ -32,6 +32,8 @@ bytes = "1.11" serde = "1.0" serde_json = "1.0" chrono = { version = "0.4", features = ["serde"] } +# Thread CPU clock (`CLOCK_THREAD_CPUTIME_ID`) for the epoch stall grace. +libc = "0.2" [dev-dependencies] claims = "0.8" diff --git a/crates/runtime/src/lib.rs b/crates/runtime/src/lib.rs index aa01745..9d98dfa 100644 --- a/crates/runtime/src/lib.rs +++ b/crates/runtime/src/lib.rs @@ -1,4 +1,5 @@ use crate::app::KvStoreOption; +use crate::epoch_grace::EpochGrace; use crate::store::HasStats; use http_backend::stats::ExtStatsTimer; use std::net::Ipv4Addr; @@ -23,6 +24,7 @@ use wasmtime::{ use wit_component::ComponentEncoder; pub mod app; +pub mod epoch_grace; pub mod instances; mod limiter; pub mod logger; @@ -116,6 +118,11 @@ pub struct Data { pub epoch_pause_ms: Arc, /// Whether elapsed time of external HTTP should refund epoch ticks. pub pause_epoch_timeout_for_external_http: bool, + /// CPU-aware grace for the epoch deadline: when the wall budget runs out + /// with no host-call credit, distinguishes a thread that was parked in the + /// kernel from a guest that really spent its budget. See + /// [`crate::epoch_grace`]. + pub epoch_grace: EpochGrace, /// Counts this instance in `fastedge_wasm_instances_live` for as long as the store — /// and therefore its pooling-allocator slots — is alive. Held only for its `Drop`. _live_instance: crate::instances::LiveInstanceGuard, diff --git a/crates/runtime/src/store.rs b/crates/runtime/src/store.rs index 3bc9932..e32a425 100644 --- a/crates/runtime/src/store.rs +++ b/crates/runtime/src/store.rs @@ -1,3 +1,4 @@ +use crate::epoch_grace::{EpochGrace, EpochGraceEvent}; use crate::limiter::ProxyLimiter; use crate::logger::Logger; use crate::registry::CachedGraphRegistry; @@ -71,6 +72,13 @@ impl Store { pub fn is_oom(&self) -> bool { self.inner.data().store_limits.oom } + + /// (Re-)arm the epoch deadline to `ticks` from now, resetting the stall + /// grace baseline to the current thread. Use this instead of + /// `set_epoch_deadline` directly; see [`crate::epoch_grace`]. + pub fn arm_epoch_deadline(&mut self, ticks: u64) { + arm_epoch_deadline(&mut self.inner, ticks); + } } /// Error indicating a wasm operation failed because it required more memory @@ -376,25 +384,70 @@ impl StoreBuilder { cache: cache_impl, epoch_pause_ms: epoch_pause_ms.clone(), pause_epoch_timeout_for_external_http: self.epoch_exclude_http_wait, + epoch_grace: EpochGrace::new(DEFAULT_EPOCH_TICK_INTERVAL), _live_instance: crate::instances::LiveInstanceGuard::new(), }, ); inner.limiter(|state| &mut state.store_limits); // allow max number of epoch ticks (1 tick = 10 ms) - inner.set_epoch_deadline(self.max_duration); - // When the deadline fires, consume any host-call credit accumulated by - // `epoch_pause_ms` to extend it; if there is no credit, trap as before. - inner.epoch_deadline_callback(move |_ctx| { + arm_epoch_deadline(&mut inner, self.max_duration); + // When the deadline fires: first consume any host-call credit + // accumulated by `epoch_pause_ms` to extend it; failing that, extend + // once more if the executing thread was parked rather than running + // (see `epoch_grace`); otherwise trap as before. + inner.epoch_deadline_callback(move |mut ctx| { let credit_ms = epoch_pause_ms.swap(0, Ordering::Relaxed); - match epoch_credit_ticks(credit_ms) { - None => Err(anyhow::Error::new(wasmtime::Trap::Interrupt)), - Some(ticks) => Ok(UpdateDeadline::Continue(ticks)), - } + epoch_deadline_decision(credit_ms, &mut ctx.data_mut().epoch_grace) }); Ok(Store { inner }) } } +/// Arm the epoch deadline to `ticks` and record the baseline the stall grace +/// measures against. Every place that sets a deadline must go through here, +/// or the grace has no baseline and denies. +pub(crate) fn arm_epoch_deadline(store: &mut wasmtime::Store>, ticks: u64) { + store.data_mut().epoch_grace.arm(ticks); + store.set_epoch_deadline(ticks); +} + +/// The epoch-deadline callback's decision, factored out so tests exercise the +/// production logic rather than a copy of it. +/// +/// Order matters: host-call credit is the *earned* extension (the guest was +/// waiting on external I/O the host chose not to charge) and takes precedence; +/// the stall grace is only consulted when there is none. +pub(crate) fn epoch_deadline_decision( + credit_ms: u64, + grace: &mut EpochGrace, +) -> Result { + if let Some(ticks) = epoch_credit_ticks(credit_ms) { + return Ok(UpdateDeadline::Continue(ticks)); + } + let event = grace.on_deadline(); + crate::util::metrics::report_epoch_grace(event); + match event { + EpochGraceEvent::Granted { + ticks, + cpu_used_ms, + wall_ms, + } => { + debug!( + ticks, + cpu_used_ms, + wall_ms, + grants = grace.grants(), + "epoch deadline reached while parked; extending" + ); + Ok(UpdateDeadline::Continue(ticks)) + } + EpochGraceEvent::Denied(reason) => { + debug!(?reason, "epoch deadline reached; trapping"); + Err(anyhow::Error::new(wasmtime::Trap::Interrupt)) + } + } +} + /// Compute how many epoch ticks to refund given accumulated host-call /// credit (in ms). /// @@ -492,26 +545,39 @@ mod tests { Engine::new(&cfg).unwrap() } - /// Install the same epoch-deadline callback used in production - /// (see `StoreBuilder::build_with_wasi`). - fn install_epoch_callback(store: &mut WtStore, epoch_pause_ms: Arc) { - store.epoch_deadline_callback(move |_ctx| { + /// Install the epoch-deadline callback used in production (see + /// `StoreBuilder::build_with_wasi`) — the same `epoch_deadline_decision`, + /// with the store data standing in for `Data::epoch_grace`. + fn install_epoch_callback(store: &mut WtStore, epoch_pause_ms: Arc) { + store.epoch_deadline_callback(move |mut ctx| { let credit_ms = epoch_pause_ms.swap(0, Ordering::Relaxed); - match epoch_credit_ticks(credit_ms) { - None => Err(anyhow::Error::new(Trap::Interrupt)), - Some(ticks) => Ok(UpdateDeadline::Continue(ticks)), - } + epoch_deadline_decision(credit_ms, ctx.data_mut()) }); } + /// Like `SPIN_WAT` but effectively unbounded (seconds of CPU), for tests + /// that must observe a trap: with the stall grace in place a 1-tick budget + /// can legitimately be extended a bounded number of times before the trap, + /// and the guest must still be running when that happens. + const LONG_SPIN_WAT: &str = r#" + (module + (global $i (mut i32) (i32.const 2000000000)) + (func (export "spin") + (loop $l + (global.set $i (i32.sub (global.get $i) (i32.const 1))) + (br_if $l (i32.gt_s (global.get $i) (i32.const 0)))))) + "#; + #[test] fn busy_loop_without_credit_traps_with_interrupt() { let engine = make_engine(); - let module = Module::new(&engine, SPIN_WAT).unwrap(); + let module = Module::new(&engine, LONG_SPIN_WAT).unwrap(); let epoch_pause_ms = Arc::new(AtomicU64::new(0)); - let mut store = WtStore::new(&engine, ()); + // 1 ms per tick, matching the test ticker below. + let mut store = WtStore::new(&engine, EpochGrace::new(1)); store.set_epoch_deadline(1); // very short budget: 1 tick + store.data_mut().arm(1); install_epoch_callback(&mut store, epoch_pause_ms.clone()); // Background ticker: bump the engine epoch until the test signals stop. @@ -538,7 +604,9 @@ mod tests { ticker.join().unwrap(); // The guest's busy loop must be cut short by the epoch deadline - // because `epoch_pause_ms` was never incremented. + // because `epoch_pause_ms` was never incremented — and a *busy* guest + // consumes CPU at wall rate, so the stall grace cannot rescue it more + // than its bounded number of times either. let err = result.expect_err("guest must trap when no host-call credit is deposited"); let trap = err.root_cause().downcast_ref::().copied(); assert_eq!( @@ -557,8 +625,10 @@ mod tests { let module = Module::new(&engine, SPIN_WAT).unwrap(); let epoch_pause_ms = Arc::new(AtomicU64::new(0)); - let mut store = WtStore::new(&engine, ()); + // 1 ms per tick, matching the test ticker below. + let mut store = WtStore::new(&engine, EpochGrace::new(1)); store.set_epoch_deadline(1); // very short budget: 1 tick + store.data_mut().arm(1); install_epoch_callback(&mut store, epoch_pause_ms.clone()); // Background ticker bumps the epoch AND deposits generous credit so @@ -598,6 +668,57 @@ mod tests { result.expect("guest must complete when epoch credit is deposited"); } + /// A worker that is parked while the wall budget runs out — no CPU + /// consumed — must be granted the unused budget instead of trapping: the + /// guest did no work, so it has not used anything. `thread::sleep` is a + /// faithful model of a kernel park: wall time passes, thread CPU time does + /// not. + #[test] + fn parked_thread_is_granted_grace_and_completes() { + let engine = make_engine(); + let module = Module::new(&engine, SPIN_WAT).unwrap(); + let epoch_pause_ms = Arc::new(AtomicU64::new(0)); + + // 200 ticks × 1 ms = 200 ms of budget: generous next to the few ms the + // bounded spin needs, so a slow CI box cannot turn the grant into a + // second, legitimate deadline hit. + const BUDGET_TICKS: u64 = 200; + let mut store = WtStore::new(&engine, EpochGrace::new(1)); + store.set_epoch_deadline(BUDGET_TICKS); + store.data_mut().arm(BUDGET_TICKS); + install_epoch_callback(&mut store, epoch_pause_ms.clone()); + + let stop = Arc::new(AtomicBool::new(false)); + let ticker = { + let engine = engine.clone(); + let stop = stop.clone(); + thread::spawn(move || { + while !stop.load(Ordering::Relaxed) { + engine.increment_epoch(); + thread::sleep(Duration::from_millis(1)); + } + }) + }; + + let instance = Instance::new(&mut store, &module, &[]).unwrap(); + let spin = instance + .get_typed_func::<(), ()>(&mut store, "spin") + .unwrap(); + + // Park: the ticker blows through the entire budget while this thread + // consumes no CPU at all. + thread::sleep(Duration::from_millis(300)); + + let result = spin.call(&mut store, ()); + + stop.store(true, Ordering::Relaxed); + ticker.join().unwrap(); + + result.expect("a parked guest must be granted its unused budget, not trapped"); + assert_eq!(store.data().grants(), 1, "exactly one grace grant expected"); + assert_eq!(epoch_pause_ms.load(Ordering::Relaxed), 0); + } + // ── live-instance accounting ────────────────────────────────────────── /// No-op stats sink; `StoreBuilder::build` only needs `HasStats` to wire the diff --git a/crates/runtime/src/util/metrics.rs b/crates/runtime/src/util/metrics.rs index 25b994e..c8b089d 100644 --- a/crates/runtime/src/util/metrics.rs +++ b/crates/runtime/src/util/metrics.rs @@ -8,6 +8,7 @@ use std::sync::OnceLock; use crate::AppResult; +use crate::epoch_grace::EpochGraceEvent; /// Sink invoked for every app call: outcome, executor label(s), duration in /// microseconds and WASM linear memory used in bytes. @@ -27,3 +28,23 @@ pub fn metrics(result: AppResult, label: &[&str], duration: Option, memory_ sink(result, label, duration, memory_used); } } + +// ── epoch stall grace ──────────────────────────────────────────────────────── + +/// Sink for epoch-deadline decisions taken when the wall budget ran out with +/// no host-call credit (see [`crate::epoch_grace`]). +pub type EpochGraceSink = fn(event: EpochGraceEvent); + +static EPOCH_GRACE_SINK: OnceLock = OnceLock::new(); + +/// Install the process-global epoch-grace sink. Subsequent calls are ignored. +pub fn set_epoch_grace_sink(sink: EpochGraceSink) { + let _ = EPOCH_GRACE_SINK.set(sink); +} + +/// Report one epoch-grace decision (no-op when no sink is installed). +pub fn report_epoch_grace(event: EpochGraceEvent) { + if let Some(sink) = EPOCH_GRACE_SINK.get() { + sink(event); + } +} From 0ae34c56879073405a9b6f4b5d9849c328cb99a2 Mon Sep 17 00:00:00 2001 From: Ruslan Pislari Date: Fri, 18 Sep 2026 13:52:01 +0300 Subject: [PATCH 13/22] feat(runtime): refine app-call error classification with init-phase support Enhance error classification during app initialization (`_initialize`, `on_context_create`) to distinguish between initialization-phase timeouts and request-phase timeouts. Introduce new `CallPhase` enum to provide phase-specific context for metrics. Update metrics labels to differentiate `init_timeout` from standard `timeout`. --- crates/runtime/src/epoch_grace.rs | 431 +++++++++++++++++++++++++++++ crates/runtime/src/lib.rs | 6 + crates/runtime/src/util/metrics.rs | 44 ++- 3 files changed, 475 insertions(+), 6 deletions(-) create mode 100644 crates/runtime/src/epoch_grace.rs diff --git a/crates/runtime/src/epoch_grace.rs b/crates/runtime/src/epoch_grace.rs new file mode 100644 index 0000000..053be5e --- /dev/null +++ b/crates/runtime/src/epoch_grace.rs @@ -0,0 +1,431 @@ +//! CPU-aware grace for the wasm epoch deadline. +//! +//! The epoch budget is wall-clock: a ticker thread bumps the engine epoch every +//! `DEFAULT_EPOCH_TICK_INTERVAL` ms whether or not the guest's worker is +//! running. A worker parked in the kernel — most often in direct reclaim on a +//! first-touch page fault, which under memory pressure waits with no timeout — +//! therefore burns its budget while executing nothing, and traps with +//! `Trap::Interrupt` at the next epoch check. The guest did nothing wrong. +//! +//! This module lets the deadline callback tell that case apart from a guest +//! that really spent its budget. When the deadline fires with no host-call +//! credit left, compare the CPU time the executing thread consumed since the +//! budget was armed against the wall budget. If it used less than +//! `1 / STALL_CPU_DIVISOR` of it, the thread was parked: hand back the unused +//! part as wall time, at most [`MAX_STALL_GRANTS`] times per armed budget. +//! Otherwise trap exactly as before. +//! +//! Measurement is per thread (`CLOCK_THREAD_CPUTIME_ID`), so the comparison is +//! only meaningful while the guest keeps running on the thread the budget was +//! armed on. A fiber resumed on another worker after an awaited host call +//! fails that check and is denied — never granted on a garbage delta. That is +//! the conservative choice: a denial is today's behaviour, while a wrong grant +//! would let a runaway guest exceed its budget. + +use std::thread::{self, ThreadId}; +use std::time::Instant; + +/// Bounded number of extensions per armed budget. The worst-case wall time for +/// a guest that keeps getting parked is `budget × (1 + MAX_STALL_GRANTS)`. +pub const MAX_STALL_GRANTS: u32 = 3; + +/// A deadline hit counts as a stall when the CPU time consumed is below +/// `budget / STALL_CPU_DIVISOR`. Half is deliberately strict: a guest that +/// genuinely ran for most of its budget is not rescued by a little jitter. +pub const STALL_CPU_DIVISOR: u64 = 2; + +/// Why a deadline hit was not extended. Every variant is a trap, exactly as +/// before this module existed; the reason is exported so the "never granted" +/// cases stay visible instead of being folded into the timeout count. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DenyReason { + /// The deadline fired before any budget was armed. + NotArmed, + /// [`MAX_STALL_GRANTS`] extensions were already handed out for this budget. + GrantsExhausted, + /// The guest is running on a different thread than the one the budget was + /// armed on, so the per-thread CPU delta means nothing. + ThreadMigrated, + /// No thread CPU clock on this platform. + CpuClockUnavailable, + /// The guest really consumed its budget. + CpuExhausted { cpu_used_ms: u64, budget_ms: u64 }, +} + +impl DenyReason { + fn label(self) -> &'static str { + match self { + DenyReason::NotArmed => "denied_not_armed", + DenyReason::GrantsExhausted => "denied_grants_exhausted", + DenyReason::ThreadMigrated => "denied_thread_migrated", + DenyReason::CpuClockUnavailable => "denied_cpu_clock_unavailable", + DenyReason::CpuExhausted { .. } => "denied_cpu_exhausted", + } + } +} + +/// Outcome of one deadline hit that found no host-call credit. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EpochGraceEvent { + /// The thread was parked; the deadline is extended by `ticks`. + Granted { + ticks: u64, + cpu_used_ms: u64, + wall_ms: u64, + }, + /// Trap, as before. + Denied(DenyReason), +} + +impl EpochGraceEvent { + /// Every value [`Self::outcome_label`] can return, for pre-initialising a + /// labelled counter so an outcome that never happened reads as `0`. + pub const OUTCOME_LABELS: &'static [&'static str] = &[ + "granted", + "denied_not_armed", + "denied_grants_exhausted", + "denied_thread_migrated", + "denied_cpu_clock_unavailable", + "denied_cpu_exhausted", + ]; + + /// Stable metric label for this outcome. + pub fn outcome_label(&self) -> &'static str { + match self { + EpochGraceEvent::Granted { .. } => "granted", + EpochGraceEvent::Denied(reason) => reason.label(), + } + } +} + +#[derive(Debug, Clone, Copy)] +struct Baseline { + thread: ThreadId, + /// Thread CPU time when the budget was armed (or last extended). + cpu_ns: Option, + /// Wall clock when the budget was armed (or last extended); for logging. + wall: Instant, + /// Ticks the current deadline was set to. + ticks: u64, +} + +/// Per-store state for the deadline callback. Lives in the store data, so the +/// callback reaches it through `StoreContextMut::data_mut` with no shared +/// state and no atomics. +#[derive(Debug)] +pub struct EpochGrace { + tick_ms: u64, + baseline: Option, + grants: u32, +} + +impl EpochGrace { + /// `tick_ms` is the epoch ticker period the budget is denominated in + /// (`DEFAULT_EPOCH_TICK_INTERVAL` in production). + pub fn new(tick_ms: u64) -> Self { + Self { + tick_ms, + baseline: None, + grants: 0, + } + } + + /// Record that a fresh budget of `ticks` was just armed on the current + /// thread. Call this together with `Store::set_epoch_deadline`; every arm + /// starts a new grant allowance. + pub fn arm(&mut self, ticks: u64) { + self.arm_at( + ticks, + thread::current().id(), + thread_cpu_ns(), + Instant::now(), + ); + } + + fn arm_at(&mut self, ticks: u64, thread: ThreadId, cpu_ns: Option, wall: Instant) { + self.baseline = Some(Baseline { + thread, + cpu_ns, + wall, + ticks, + }); + self.grants = 0; + } + + /// Extensions handed out for the currently armed budget. + pub fn grants(&self) -> u32 { + self.grants + } + + /// Decide what to do about a deadline hit that has no host-call credit, + /// measured on the current thread right now. + pub fn on_deadline(&mut self) -> EpochGraceEvent { + self.decide(thread_cpu_ns(), thread::current().id(), Instant::now()) + } + + /// The decision proper, with the clocks passed in so it can be tested + /// without sleeping or spinning. + pub fn decide( + &mut self, + now_cpu_ns: Option, + now_thread: ThreadId, + now: Instant, + ) -> EpochGraceEvent { + let Some(baseline) = self.baseline.as_mut() else { + return EpochGraceEvent::Denied(DenyReason::NotArmed); + }; + if self.grants >= MAX_STALL_GRANTS { + return EpochGraceEvent::Denied(DenyReason::GrantsExhausted); + } + if now_thread != baseline.thread { + return EpochGraceEvent::Denied(DenyReason::ThreadMigrated); + } + let (Some(now_cpu_ns), Some(armed_cpu_ns)) = (now_cpu_ns, baseline.cpu_ns) else { + return EpochGraceEvent::Denied(DenyReason::CpuClockUnavailable); + }; + + let cpu_used_ms = now_cpu_ns.saturating_sub(armed_cpu_ns) / 1_000_000; + let budget_ms = baseline.ticks.saturating_mul(self.tick_ms); + if cpu_used_ms.saturating_mul(STALL_CPU_DIVISOR) >= budget_ms { + return EpochGraceEvent::Denied(DenyReason::CpuExhausted { + cpu_used_ms, + budget_ms, + }); + } + + // Parked. Hand back the part of the budget the guest never got to use, + // rounded up to whole ticks so it is never under-refunded, and start + // measuring the next segment from here. + let remaining_ms = budget_ms - cpu_used_ms; + let ticks = remaining_ms.div_ceil(self.tick_ms.max(1)).max(1); + let wall_ms = now.saturating_duration_since(baseline.wall).as_millis() as u64; + baseline.cpu_ns = Some(now_cpu_ns); + baseline.wall = now; + baseline.ticks = ticks; + self.grants += 1; + EpochGraceEvent::Granted { + ticks, + cpu_used_ms, + wall_ms, + } + } +} + +/// CPU time consumed by the calling thread, in nanoseconds. +/// +/// `CLOCK_THREAD_CPUTIME_ID` is what makes the whole scheme work: it advances +/// only while this thread is on a CPU, so a thread parked in the kernel reads +/// the same value before and after the park. +#[cfg(any(target_os = "linux", target_os = "macos"))] +pub fn thread_cpu_ns() -> Option { + let mut ts = libc::timespec { + tv_sec: 0, + tv_nsec: 0, + }; + // SAFETY: `ts` is a valid, writable `timespec`, and the clock id is a + // constant supported on both targets this is compiled for. + let rc = unsafe { libc::clock_gettime(libc::CLOCK_THREAD_CPUTIME_ID, &mut ts) }; + (rc == 0).then(|| (ts.tv_sec as u64).saturating_mul(1_000_000_000) + ts.tv_nsec as u64) +} + +#[cfg(not(any(target_os = "linux", target_os = "macos")))] +pub fn thread_cpu_ns() -> Option { + None +} + +#[cfg(test)] +mod tests { + use super::*; + use std::time::Duration; + + const TICK_MS: u64 = 10; + const NS_PER_MS: u64 = 1_000_000; + + fn armed(ticks: u64, cpu_ms: u64) -> (EpochGrace, ThreadId, Instant) { + let tid = thread::current().id(); + let t0 = Instant::now(); + let mut g = EpochGrace::new(TICK_MS); + g.arm_at(ticks, tid, Some(cpu_ms * NS_PER_MS), t0); + (g, tid, t0) + } + + #[test] + fn deadline_before_any_arm_is_denied() { + let mut g = EpochGrace::new(TICK_MS); + assert_eq!( + g.decide(Some(0), thread::current().id(), Instant::now()), + EpochGraceEvent::Denied(DenyReason::NotArmed) + ); + } + + #[test] + fn parked_thread_is_granted_the_unused_budget() { + // 100 ticks × 10 ms = 1 s of wall budget; the thread used 50 ms of CPU. + let (mut g, tid, t0) = armed(100, 1_000); + let ev = g.decide( + Some((1_000 + 50) * NS_PER_MS), + tid, + t0 + Duration::from_millis(1_000), + ); + assert_eq!( + ev, + EpochGraceEvent::Granted { + ticks: 95, // (1000 ms - 50 ms) / 10 ms + cpu_used_ms: 50, + wall_ms: 1_000, + } + ); + assert_eq!(g.grants(), 1); + } + + #[test] + fn busy_thread_is_denied() { + let (mut g, tid, t0) = armed(100, 1_000); + // 600 ms of CPU out of a 1 s budget: it really ran. + let ev = g.decide(Some((1_000 + 600) * NS_PER_MS), tid, t0); + assert_eq!( + ev, + EpochGraceEvent::Denied(DenyReason::CpuExhausted { + cpu_used_ms: 600, + budget_ms: 1_000, + }) + ); + assert_eq!(g.grants(), 0); + } + + #[test] + fn exactly_half_the_budget_is_denied() { + // The boundary belongs to "busy": STALL_CPU_DIVISOR is a strict bound. + let (mut g, tid, t0) = armed(100, 0); + let ev = g.decide(Some(500 * NS_PER_MS), tid, t0); + assert!(matches!( + ev, + EpochGraceEvent::Denied(DenyReason::CpuExhausted { .. }) + )); + } + + #[test] + fn migrated_thread_is_denied_even_when_idle() { + let (mut g, _tid, t0) = armed(100, 1_000); + let other = thread::spawn(|| thread::current().id()).join().unwrap(); + // Zero CPU consumed would look like a park — but the delta is across + // two different threads' clocks, so it must not be trusted. + assert_eq!( + g.decide(Some(1_000 * NS_PER_MS), other, t0), + EpochGraceEvent::Denied(DenyReason::ThreadMigrated) + ); + } + + #[test] + fn grants_are_bounded_per_armed_budget() { + let (mut g, tid, t0) = armed(100, 1_000); + for i in 0..MAX_STALL_GRANTS { + let ev = g.decide(Some(1_000 * NS_PER_MS), tid, t0); + assert!( + matches!(ev, EpochGraceEvent::Granted { .. }), + "grant #{i} expected, got {ev:?}" + ); + } + assert_eq!( + g.decide(Some(1_000 * NS_PER_MS), tid, t0), + EpochGraceEvent::Denied(DenyReason::GrantsExhausted) + ); + } + + #[test] + fn re_arming_resets_the_grant_allowance() { + let (mut g, tid, t0) = armed(100, 1_000); + for _ in 0..MAX_STALL_GRANTS { + g.decide(Some(1_000 * NS_PER_MS), tid, t0); + } + g.arm_at(100, tid, Some(1_000 * NS_PER_MS), t0); + assert_eq!(g.grants(), 0); + assert!(matches!( + g.decide(Some(1_000 * NS_PER_MS), tid, t0), + EpochGraceEvent::Granted { .. } + )); + } + + #[test] + fn a_grant_measures_the_next_segment_from_the_grant() { + // After a grant, CPU consumed *before* it must not count against the + // new, smaller budget. + let (mut g, tid, t0) = armed(100, 1_000); + let ev = g.decide(Some((1_000 + 100) * NS_PER_MS), tid, t0); + let EpochGraceEvent::Granted { ticks, .. } = ev else { + panic!("{ev:?}") + }; + assert_eq!(ticks, 90); + // Another 100 ms of CPU against the new 900 ms budget: still parked. + let ev = g.decide(Some((1_000 + 200) * NS_PER_MS), tid, t0); + assert_eq!( + ev, + EpochGraceEvent::Granted { + ticks: 80, + cpu_used_ms: 100, + wall_ms: 0, + } + ); + } + + #[test] + fn missing_cpu_clock_is_denied() { + let tid = thread::current().id(); + let mut g = EpochGrace::new(TICK_MS); + g.arm_at(100, tid, None, Instant::now()); + assert_eq!( + g.decide(Some(0), tid, Instant::now()), + EpochGraceEvent::Denied(DenyReason::CpuClockUnavailable) + ); + g.arm_at(100, tid, Some(0), Instant::now()); + assert_eq!( + g.decide(None, tid, Instant::now()), + EpochGraceEvent::Denied(DenyReason::CpuClockUnavailable) + ); + } + + #[test] + fn every_outcome_has_a_pre_initialised_label() { + let events = [ + EpochGraceEvent::Granted { + ticks: 1, + cpu_used_ms: 0, + wall_ms: 0, + }, + EpochGraceEvent::Denied(DenyReason::NotArmed), + EpochGraceEvent::Denied(DenyReason::GrantsExhausted), + EpochGraceEvent::Denied(DenyReason::ThreadMigrated), + EpochGraceEvent::Denied(DenyReason::CpuClockUnavailable), + EpochGraceEvent::Denied(DenyReason::CpuExhausted { + cpu_used_ms: 1, + budget_ms: 1, + }), + ]; + assert_eq!(events.len(), EpochGraceEvent::OUTCOME_LABELS.len()); + for ev in events { + assert!( + EpochGraceEvent::OUTCOME_LABELS.contains(&ev.outcome_label()), + "{ev:?} -> {}", + ev.outcome_label() + ); + } + } + + #[cfg(any(target_os = "linux", target_os = "macos"))] + #[test] + fn thread_cpu_clock_advances_with_work_and_not_with_sleep() { + let a = thread_cpu_ns().expect("thread CPU clock"); + thread::sleep(Duration::from_millis(20)); + let b = thread_cpu_ns().unwrap(); + // Sleeping costs (close to) nothing. + assert!(b - a < 5 * NS_PER_MS, "sleep charged {} ns of CPU", b - a); + // Burn ~2 ms of real CPU; `black_box` keeps the loop from being + // optimised away. + let mut x = 0u64; + while thread_cpu_ns().unwrap() - b < 2 * NS_PER_MS { + x = std::hint::black_box(x.wrapping_mul(6364136223846793005).wrapping_add(1)); + } + std::hint::black_box(x); + assert!(thread_cpu_ns().unwrap() - b >= 2 * NS_PER_MS); + } +} diff --git a/crates/runtime/src/lib.rs b/crates/runtime/src/lib.rs index 9d98dfa..17fef6e 100644 --- a/crates/runtime/src/lib.rs +++ b/crates/runtime/src/lib.rs @@ -56,6 +56,12 @@ pub const DEFAULT_EPOCH_TICK_INTERVAL: u64 = 10; const PREVIEW1_ADAPTER: &[u8] = include_bytes!("adapters/wasi_snapshot_preview1.reactor.wasm"); +/// Outcome of one app call. +/// +/// Reported as the `outcome` label of `fastedge_calls_total` (see the server's +/// metrics sink) and, **as its discriminant**, as `fail_reason Int32` in the +/// ClickHouse stats table. Variants are therefore append-only: never reorder +/// or remove one. #[allow(non_camel_case_types)] #[derive(PartialEq, Copy, Clone, Debug)] pub enum AppResult { diff --git a/crates/runtime/src/util/metrics.rs b/crates/runtime/src/util/metrics.rs index c8b089d..4950d87 100644 --- a/crates/runtime/src/util/metrics.rs +++ b/crates/runtime/src/util/metrics.rs @@ -10,10 +10,30 @@ use std::sync::OnceLock; use crate::AppResult; use crate::epoch_grace::EpochGraceEvent; -/// Sink invoked for every app call: outcome, executor label(s), duration in -/// microseconds and WASM linear memory used in bytes. -pub type MetricsSink = - fn(result: AppResult, label: &[&str], duration: Option, memory_used: Option); +/// Where in an app call the outcome was decided. +/// +/// Refines the metrics `outcome` label only — stats (`fail_reason`) always +/// carry the plain [`AppResult`], so the ClickHouse encoding is untouched. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CallPhase { + /// Guest initialisation (`_initialize`, `on_context_create`): no request + /// callback had run yet. + Init, + /// A request callback (`on_request_headers`, body chunks, `on_log`, …), or + /// a rejection before the guest was involved at all. + Request, +} + +/// Sink invoked for every app call: outcome, the phase it was decided in, +/// executor label(s), duration in microseconds and WASM linear memory used in +/// bytes. +pub type MetricsSink = fn( + result: AppResult, + phase: CallPhase, + label: &[&str], + duration: Option, + memory_used: Option, +); static SINK: OnceLock = OnceLock::new(); @@ -22,10 +42,22 @@ pub fn set_sink(sink: MetricsSink) { let _ = SINK.set(sink); } -/// Report one app call to the installed sink (no-op when none is installed). +/// Report one app call decided in the request phase (no-op when no sink is +/// installed). The common case; see [`metrics_in_phase`] for the rest. pub fn metrics(result: AppResult, label: &[&str], duration: Option, memory_used: Option) { + metrics_in_phase(result, CallPhase::Request, label, duration, memory_used); +} + +/// Report one app call, stating the phase its outcome was decided in. +pub fn metrics_in_phase( + result: AppResult, + phase: CallPhase, + label: &[&str], + duration: Option, + memory_used: Option, +) { if let Some(sink) = SINK.get() { - sink(result, label, duration, memory_used); + sink(result, phase, label, duration, memory_used); } } From 3b8224f6bcf42caf35cb3e7169b243cbc2bb9a94 Mon Sep 17 00:00:00 2001 From: Ruslan Pislari Date: Mon, 28 Sep 2026 17:01:06 +0300 Subject: [PATCH 14/22] feat(runtime): add rebase mechanism for improved stall grace handling Implement `rebase` functionality to redefine baseline for credited deadline extensions, enhancing CPU usage accuracy while maintaining stall grant allowances. Introduce and refine supporting tests for the new mechanism. --- crates/runtime/src/epoch_grace.rs | 74 +++++++++++++++++++++++++++++++ crates/runtime/src/store.rs | 17 +++++++ 2 files changed, 91 insertions(+) diff --git a/crates/runtime/src/epoch_grace.rs b/crates/runtime/src/epoch_grace.rs index 053be5e..27b0f16 100644 --- a/crates/runtime/src/epoch_grace.rs +++ b/crates/runtime/src/epoch_grace.rs @@ -152,11 +152,46 @@ impl EpochGrace { self.grants = 0; } + /// Re-baseline for a *credited* deadline extension (host-call credit + /// returned `Continue(ticks)`): the new segment of `ticks` is measured + /// from the current thread, CPU clock and wall clock, so a later + /// no-credit hit compares CPU used within this segment against this + /// segment's budget — not the accumulated usage since the original arm + /// against a stale budget. + /// + /// Deliberately unlike [`Self::arm`], the grant counter is **not** + /// reset: stall grants stay bounded to [`MAX_STALL_GRANTS`] per armed + /// budget no matter how many credited extensions happen in between, so + /// host-call credit cannot be used to mint fresh stall allowances. + pub fn rebase(&mut self, ticks: u64) { + self.rebase_at( + ticks, + thread::current().id(), + thread_cpu_ns(), + Instant::now(), + ); + } + + fn rebase_at(&mut self, ticks: u64, thread: ThreadId, cpu_ns: Option, wall: Instant) { + self.baseline = Some(Baseline { + thread, + cpu_ns, + wall, + ticks, + }); + } + /// Extensions handed out for the currently armed budget. pub fn grants(&self) -> u32 { self.grants } + /// Ticks of the current baseline segment; test-only introspection. + #[cfg(test)] + pub(crate) fn baseline_ticks(&self) -> Option { + self.baseline.map(|b| b.ticks) + } + /// Decide what to do about a deadline hit that has no host-call credit, /// measured on the current thread right now. pub fn on_deadline(&mut self) -> EpochGraceEvent { @@ -332,6 +367,45 @@ mod tests { ); } + #[test] + fn rebase_measures_the_next_segment_from_the_rebase() { + // 1 s armed budget; the guest burnt 900 ms of CPU, then a credited + // extension rebased it to a fresh 50-tick (500 ms) segment. A later + // hit that used 100 ms *within the new segment* must be a grant — + // without the rebase it would read 1 000 ms against the stale 1 s + // budget and be denied as CpuExhausted. + let (mut g, tid, t0) = armed(100, 1_000); + let t1 = t0 + Duration::from_millis(2_000); + g.rebase_at(50, tid, Some((1_000 + 900) * NS_PER_MS), t1); + let ev = g.decide( + Some((1_000 + 900 + 100) * NS_PER_MS), + tid, + t1 + Duration::from_millis(500), + ); + assert_eq!( + ev, + EpochGraceEvent::Granted { + ticks: 40, // (500 ms - 100 ms) / 10 ms + cpu_used_ms: 100, + wall_ms: 500, + } + ); + } + + #[test] + fn rebase_does_not_reset_the_grant_allowance() { + let (mut g, tid, t0) = armed(100, 1_000); + for _ in 0..MAX_STALL_GRANTS { + g.decide(Some(1_000 * NS_PER_MS), tid, t0); + } + g.rebase_at(100, tid, Some(1_000 * NS_PER_MS), t0); + assert_eq!(g.grants(), MAX_STALL_GRANTS); + assert_eq!( + g.decide(Some(1_000 * NS_PER_MS), tid, t0), + EpochGraceEvent::Denied(DenyReason::GrantsExhausted) + ); + } + #[test] fn re_arming_resets_the_grant_allowance() { let (mut g, tid, t0) = armed(100, 1_000); diff --git a/crates/runtime/src/store.rs b/crates/runtime/src/store.rs index e32a425..81acd9e 100644 --- a/crates/runtime/src/store.rs +++ b/crates/runtime/src/store.rs @@ -422,6 +422,10 @@ pub(crate) fn epoch_deadline_decision( grace: &mut EpochGrace, ) -> Result { if let Some(ticks) = epoch_credit_ticks(credit_ms) { + // The credited extension starts a new deadline segment; rebase the + // stall grace on it so the next no-credit hit measures CPU against + // this segment, not the long-expired one. Grant allowance is kept. + grace.rebase(ticks); return Ok(UpdateDeadline::Continue(ticks)); } let event = grace.on_deadline(); @@ -517,6 +521,19 @@ mod tests { ); } + #[test] + fn credited_extension_rebases_the_stall_grace() { + // A credited Continue starts a new deadline segment; the grace + // baseline must move to it, or the next no-credit hit measures CPU + // across both segments against the stale budget. + let mut grace = EpochGrace::new(DEFAULT_EPOCH_TICK_INTERVAL); + grace.arm(100); + let decision = epoch_deadline_decision(25, &mut grace).unwrap(); + assert!(matches!(decision, UpdateDeadline::Continue(3))); + assert_eq!(grace.baseline_ticks(), Some(3)); + assert_eq!(grace.grants(), 0); + } + // ── integration test: end-to-end deadline extension ─────────────────── // // These tests verify the same wiring used in production From 7fd5b41debbf8cfabb0d9c88cc5eb014dc817717 Mon Sep 17 00:00:00 2001 From: Ruslan Pislari Date: Tue, 29 Sep 2026 11:40:13 +0300 Subject: [PATCH 15/22] refactor(runtime): remove `epoch_grace` module and associated metrics Remove the `epoch_grace` module and its related `fastedge_epoch_stall_grace_total` metric. Eliminate CPU-aware grace handling and its integration in the runtime. Update tests, documentation, and metrics initialization accordingly. --- Cargo.lock | 5 - crates/runtime/Cargo.toml | 2 - crates/runtime/src/epoch_grace.rs | 505 ----------------------------- crates/runtime/src/lib.rs | 7 - crates/runtime/src/store.rs | 178 ++-------- crates/runtime/src/util/metrics.rs | 21 -- 6 files changed, 20 insertions(+), 698 deletions(-) delete mode 100644 crates/runtime/src/epoch_grace.rs diff --git a/Cargo.lock b/Cargo.lock index 92c8bd0..e1fe023 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3116,7 +3116,6 @@ dependencies = [ "http", "http-backend", "key-value-store", - "libc", "moka", "secret", "serde", @@ -5588,7 +5587,3 @@ dependencies = [ "cc", "pkg-config", ] - -[[patch.unused]] -name = "fastedge_proxywasm" -version = "0.5.0" diff --git a/crates/runtime/Cargo.toml b/crates/runtime/Cargo.toml index a902c3e..1a7f9a9 100644 --- a/crates/runtime/Cargo.toml +++ b/crates/runtime/Cargo.toml @@ -32,8 +32,6 @@ bytes = "1.11" serde = "1.0" serde_json = "1.0" chrono = { version = "0.4", features = ["serde"] } -# Thread CPU clock (`CLOCK_THREAD_CPUTIME_ID`) for the epoch stall grace. -libc = "0.2" [dev-dependencies] claims = "0.8" diff --git a/crates/runtime/src/epoch_grace.rs b/crates/runtime/src/epoch_grace.rs deleted file mode 100644 index 27b0f16..0000000 --- a/crates/runtime/src/epoch_grace.rs +++ /dev/null @@ -1,505 +0,0 @@ -//! CPU-aware grace for the wasm epoch deadline. -//! -//! The epoch budget is wall-clock: a ticker thread bumps the engine epoch every -//! `DEFAULT_EPOCH_TICK_INTERVAL` ms whether or not the guest's worker is -//! running. A worker parked in the kernel — most often in direct reclaim on a -//! first-touch page fault, which under memory pressure waits with no timeout — -//! therefore burns its budget while executing nothing, and traps with -//! `Trap::Interrupt` at the next epoch check. The guest did nothing wrong. -//! -//! This module lets the deadline callback tell that case apart from a guest -//! that really spent its budget. When the deadline fires with no host-call -//! credit left, compare the CPU time the executing thread consumed since the -//! budget was armed against the wall budget. If it used less than -//! `1 / STALL_CPU_DIVISOR` of it, the thread was parked: hand back the unused -//! part as wall time, at most [`MAX_STALL_GRANTS`] times per armed budget. -//! Otherwise trap exactly as before. -//! -//! Measurement is per thread (`CLOCK_THREAD_CPUTIME_ID`), so the comparison is -//! only meaningful while the guest keeps running on the thread the budget was -//! armed on. A fiber resumed on another worker after an awaited host call -//! fails that check and is denied — never granted on a garbage delta. That is -//! the conservative choice: a denial is today's behaviour, while a wrong grant -//! would let a runaway guest exceed its budget. - -use std::thread::{self, ThreadId}; -use std::time::Instant; - -/// Bounded number of extensions per armed budget. The worst-case wall time for -/// a guest that keeps getting parked is `budget × (1 + MAX_STALL_GRANTS)`. -pub const MAX_STALL_GRANTS: u32 = 3; - -/// A deadline hit counts as a stall when the CPU time consumed is below -/// `budget / STALL_CPU_DIVISOR`. Half is deliberately strict: a guest that -/// genuinely ran for most of its budget is not rescued by a little jitter. -pub const STALL_CPU_DIVISOR: u64 = 2; - -/// Why a deadline hit was not extended. Every variant is a trap, exactly as -/// before this module existed; the reason is exported so the "never granted" -/// cases stay visible instead of being folded into the timeout count. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum DenyReason { - /// The deadline fired before any budget was armed. - NotArmed, - /// [`MAX_STALL_GRANTS`] extensions were already handed out for this budget. - GrantsExhausted, - /// The guest is running on a different thread than the one the budget was - /// armed on, so the per-thread CPU delta means nothing. - ThreadMigrated, - /// No thread CPU clock on this platform. - CpuClockUnavailable, - /// The guest really consumed its budget. - CpuExhausted { cpu_used_ms: u64, budget_ms: u64 }, -} - -impl DenyReason { - fn label(self) -> &'static str { - match self { - DenyReason::NotArmed => "denied_not_armed", - DenyReason::GrantsExhausted => "denied_grants_exhausted", - DenyReason::ThreadMigrated => "denied_thread_migrated", - DenyReason::CpuClockUnavailable => "denied_cpu_clock_unavailable", - DenyReason::CpuExhausted { .. } => "denied_cpu_exhausted", - } - } -} - -/// Outcome of one deadline hit that found no host-call credit. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum EpochGraceEvent { - /// The thread was parked; the deadline is extended by `ticks`. - Granted { - ticks: u64, - cpu_used_ms: u64, - wall_ms: u64, - }, - /// Trap, as before. - Denied(DenyReason), -} - -impl EpochGraceEvent { - /// Every value [`Self::outcome_label`] can return, for pre-initialising a - /// labelled counter so an outcome that never happened reads as `0`. - pub const OUTCOME_LABELS: &'static [&'static str] = &[ - "granted", - "denied_not_armed", - "denied_grants_exhausted", - "denied_thread_migrated", - "denied_cpu_clock_unavailable", - "denied_cpu_exhausted", - ]; - - /// Stable metric label for this outcome. - pub fn outcome_label(&self) -> &'static str { - match self { - EpochGraceEvent::Granted { .. } => "granted", - EpochGraceEvent::Denied(reason) => reason.label(), - } - } -} - -#[derive(Debug, Clone, Copy)] -struct Baseline { - thread: ThreadId, - /// Thread CPU time when the budget was armed (or last extended). - cpu_ns: Option, - /// Wall clock when the budget was armed (or last extended); for logging. - wall: Instant, - /// Ticks the current deadline was set to. - ticks: u64, -} - -/// Per-store state for the deadline callback. Lives in the store data, so the -/// callback reaches it through `StoreContextMut::data_mut` with no shared -/// state and no atomics. -#[derive(Debug)] -pub struct EpochGrace { - tick_ms: u64, - baseline: Option, - grants: u32, -} - -impl EpochGrace { - /// `tick_ms` is the epoch ticker period the budget is denominated in - /// (`DEFAULT_EPOCH_TICK_INTERVAL` in production). - pub fn new(tick_ms: u64) -> Self { - Self { - tick_ms, - baseline: None, - grants: 0, - } - } - - /// Record that a fresh budget of `ticks` was just armed on the current - /// thread. Call this together with `Store::set_epoch_deadline`; every arm - /// starts a new grant allowance. - pub fn arm(&mut self, ticks: u64) { - self.arm_at( - ticks, - thread::current().id(), - thread_cpu_ns(), - Instant::now(), - ); - } - - fn arm_at(&mut self, ticks: u64, thread: ThreadId, cpu_ns: Option, wall: Instant) { - self.baseline = Some(Baseline { - thread, - cpu_ns, - wall, - ticks, - }); - self.grants = 0; - } - - /// Re-baseline for a *credited* deadline extension (host-call credit - /// returned `Continue(ticks)`): the new segment of `ticks` is measured - /// from the current thread, CPU clock and wall clock, so a later - /// no-credit hit compares CPU used within this segment against this - /// segment's budget — not the accumulated usage since the original arm - /// against a stale budget. - /// - /// Deliberately unlike [`Self::arm`], the grant counter is **not** - /// reset: stall grants stay bounded to [`MAX_STALL_GRANTS`] per armed - /// budget no matter how many credited extensions happen in between, so - /// host-call credit cannot be used to mint fresh stall allowances. - pub fn rebase(&mut self, ticks: u64) { - self.rebase_at( - ticks, - thread::current().id(), - thread_cpu_ns(), - Instant::now(), - ); - } - - fn rebase_at(&mut self, ticks: u64, thread: ThreadId, cpu_ns: Option, wall: Instant) { - self.baseline = Some(Baseline { - thread, - cpu_ns, - wall, - ticks, - }); - } - - /// Extensions handed out for the currently armed budget. - pub fn grants(&self) -> u32 { - self.grants - } - - /// Ticks of the current baseline segment; test-only introspection. - #[cfg(test)] - pub(crate) fn baseline_ticks(&self) -> Option { - self.baseline.map(|b| b.ticks) - } - - /// Decide what to do about a deadline hit that has no host-call credit, - /// measured on the current thread right now. - pub fn on_deadline(&mut self) -> EpochGraceEvent { - self.decide(thread_cpu_ns(), thread::current().id(), Instant::now()) - } - - /// The decision proper, with the clocks passed in so it can be tested - /// without sleeping or spinning. - pub fn decide( - &mut self, - now_cpu_ns: Option, - now_thread: ThreadId, - now: Instant, - ) -> EpochGraceEvent { - let Some(baseline) = self.baseline.as_mut() else { - return EpochGraceEvent::Denied(DenyReason::NotArmed); - }; - if self.grants >= MAX_STALL_GRANTS { - return EpochGraceEvent::Denied(DenyReason::GrantsExhausted); - } - if now_thread != baseline.thread { - return EpochGraceEvent::Denied(DenyReason::ThreadMigrated); - } - let (Some(now_cpu_ns), Some(armed_cpu_ns)) = (now_cpu_ns, baseline.cpu_ns) else { - return EpochGraceEvent::Denied(DenyReason::CpuClockUnavailable); - }; - - let cpu_used_ms = now_cpu_ns.saturating_sub(armed_cpu_ns) / 1_000_000; - let budget_ms = baseline.ticks.saturating_mul(self.tick_ms); - if cpu_used_ms.saturating_mul(STALL_CPU_DIVISOR) >= budget_ms { - return EpochGraceEvent::Denied(DenyReason::CpuExhausted { - cpu_used_ms, - budget_ms, - }); - } - - // Parked. Hand back the part of the budget the guest never got to use, - // rounded up to whole ticks so it is never under-refunded, and start - // measuring the next segment from here. - let remaining_ms = budget_ms - cpu_used_ms; - let ticks = remaining_ms.div_ceil(self.tick_ms.max(1)).max(1); - let wall_ms = now.saturating_duration_since(baseline.wall).as_millis() as u64; - baseline.cpu_ns = Some(now_cpu_ns); - baseline.wall = now; - baseline.ticks = ticks; - self.grants += 1; - EpochGraceEvent::Granted { - ticks, - cpu_used_ms, - wall_ms, - } - } -} - -/// CPU time consumed by the calling thread, in nanoseconds. -/// -/// `CLOCK_THREAD_CPUTIME_ID` is what makes the whole scheme work: it advances -/// only while this thread is on a CPU, so a thread parked in the kernel reads -/// the same value before and after the park. -#[cfg(any(target_os = "linux", target_os = "macos"))] -pub fn thread_cpu_ns() -> Option { - let mut ts = libc::timespec { - tv_sec: 0, - tv_nsec: 0, - }; - // SAFETY: `ts` is a valid, writable `timespec`, and the clock id is a - // constant supported on both targets this is compiled for. - let rc = unsafe { libc::clock_gettime(libc::CLOCK_THREAD_CPUTIME_ID, &mut ts) }; - (rc == 0).then(|| (ts.tv_sec as u64).saturating_mul(1_000_000_000) + ts.tv_nsec as u64) -} - -#[cfg(not(any(target_os = "linux", target_os = "macos")))] -pub fn thread_cpu_ns() -> Option { - None -} - -#[cfg(test)] -mod tests { - use super::*; - use std::time::Duration; - - const TICK_MS: u64 = 10; - const NS_PER_MS: u64 = 1_000_000; - - fn armed(ticks: u64, cpu_ms: u64) -> (EpochGrace, ThreadId, Instant) { - let tid = thread::current().id(); - let t0 = Instant::now(); - let mut g = EpochGrace::new(TICK_MS); - g.arm_at(ticks, tid, Some(cpu_ms * NS_PER_MS), t0); - (g, tid, t0) - } - - #[test] - fn deadline_before_any_arm_is_denied() { - let mut g = EpochGrace::new(TICK_MS); - assert_eq!( - g.decide(Some(0), thread::current().id(), Instant::now()), - EpochGraceEvent::Denied(DenyReason::NotArmed) - ); - } - - #[test] - fn parked_thread_is_granted_the_unused_budget() { - // 100 ticks × 10 ms = 1 s of wall budget; the thread used 50 ms of CPU. - let (mut g, tid, t0) = armed(100, 1_000); - let ev = g.decide( - Some((1_000 + 50) * NS_PER_MS), - tid, - t0 + Duration::from_millis(1_000), - ); - assert_eq!( - ev, - EpochGraceEvent::Granted { - ticks: 95, // (1000 ms - 50 ms) / 10 ms - cpu_used_ms: 50, - wall_ms: 1_000, - } - ); - assert_eq!(g.grants(), 1); - } - - #[test] - fn busy_thread_is_denied() { - let (mut g, tid, t0) = armed(100, 1_000); - // 600 ms of CPU out of a 1 s budget: it really ran. - let ev = g.decide(Some((1_000 + 600) * NS_PER_MS), tid, t0); - assert_eq!( - ev, - EpochGraceEvent::Denied(DenyReason::CpuExhausted { - cpu_used_ms: 600, - budget_ms: 1_000, - }) - ); - assert_eq!(g.grants(), 0); - } - - #[test] - fn exactly_half_the_budget_is_denied() { - // The boundary belongs to "busy": STALL_CPU_DIVISOR is a strict bound. - let (mut g, tid, t0) = armed(100, 0); - let ev = g.decide(Some(500 * NS_PER_MS), tid, t0); - assert!(matches!( - ev, - EpochGraceEvent::Denied(DenyReason::CpuExhausted { .. }) - )); - } - - #[test] - fn migrated_thread_is_denied_even_when_idle() { - let (mut g, _tid, t0) = armed(100, 1_000); - let other = thread::spawn(|| thread::current().id()).join().unwrap(); - // Zero CPU consumed would look like a park — but the delta is across - // two different threads' clocks, so it must not be trusted. - assert_eq!( - g.decide(Some(1_000 * NS_PER_MS), other, t0), - EpochGraceEvent::Denied(DenyReason::ThreadMigrated) - ); - } - - #[test] - fn grants_are_bounded_per_armed_budget() { - let (mut g, tid, t0) = armed(100, 1_000); - for i in 0..MAX_STALL_GRANTS { - let ev = g.decide(Some(1_000 * NS_PER_MS), tid, t0); - assert!( - matches!(ev, EpochGraceEvent::Granted { .. }), - "grant #{i} expected, got {ev:?}" - ); - } - assert_eq!( - g.decide(Some(1_000 * NS_PER_MS), tid, t0), - EpochGraceEvent::Denied(DenyReason::GrantsExhausted) - ); - } - - #[test] - fn rebase_measures_the_next_segment_from_the_rebase() { - // 1 s armed budget; the guest burnt 900 ms of CPU, then a credited - // extension rebased it to a fresh 50-tick (500 ms) segment. A later - // hit that used 100 ms *within the new segment* must be a grant — - // without the rebase it would read 1 000 ms against the stale 1 s - // budget and be denied as CpuExhausted. - let (mut g, tid, t0) = armed(100, 1_000); - let t1 = t0 + Duration::from_millis(2_000); - g.rebase_at(50, tid, Some((1_000 + 900) * NS_PER_MS), t1); - let ev = g.decide( - Some((1_000 + 900 + 100) * NS_PER_MS), - tid, - t1 + Duration::from_millis(500), - ); - assert_eq!( - ev, - EpochGraceEvent::Granted { - ticks: 40, // (500 ms - 100 ms) / 10 ms - cpu_used_ms: 100, - wall_ms: 500, - } - ); - } - - #[test] - fn rebase_does_not_reset_the_grant_allowance() { - let (mut g, tid, t0) = armed(100, 1_000); - for _ in 0..MAX_STALL_GRANTS { - g.decide(Some(1_000 * NS_PER_MS), tid, t0); - } - g.rebase_at(100, tid, Some(1_000 * NS_PER_MS), t0); - assert_eq!(g.grants(), MAX_STALL_GRANTS); - assert_eq!( - g.decide(Some(1_000 * NS_PER_MS), tid, t0), - EpochGraceEvent::Denied(DenyReason::GrantsExhausted) - ); - } - - #[test] - fn re_arming_resets_the_grant_allowance() { - let (mut g, tid, t0) = armed(100, 1_000); - for _ in 0..MAX_STALL_GRANTS { - g.decide(Some(1_000 * NS_PER_MS), tid, t0); - } - g.arm_at(100, tid, Some(1_000 * NS_PER_MS), t0); - assert_eq!(g.grants(), 0); - assert!(matches!( - g.decide(Some(1_000 * NS_PER_MS), tid, t0), - EpochGraceEvent::Granted { .. } - )); - } - - #[test] - fn a_grant_measures_the_next_segment_from_the_grant() { - // After a grant, CPU consumed *before* it must not count against the - // new, smaller budget. - let (mut g, tid, t0) = armed(100, 1_000); - let ev = g.decide(Some((1_000 + 100) * NS_PER_MS), tid, t0); - let EpochGraceEvent::Granted { ticks, .. } = ev else { - panic!("{ev:?}") - }; - assert_eq!(ticks, 90); - // Another 100 ms of CPU against the new 900 ms budget: still parked. - let ev = g.decide(Some((1_000 + 200) * NS_PER_MS), tid, t0); - assert_eq!( - ev, - EpochGraceEvent::Granted { - ticks: 80, - cpu_used_ms: 100, - wall_ms: 0, - } - ); - } - - #[test] - fn missing_cpu_clock_is_denied() { - let tid = thread::current().id(); - let mut g = EpochGrace::new(TICK_MS); - g.arm_at(100, tid, None, Instant::now()); - assert_eq!( - g.decide(Some(0), tid, Instant::now()), - EpochGraceEvent::Denied(DenyReason::CpuClockUnavailable) - ); - g.arm_at(100, tid, Some(0), Instant::now()); - assert_eq!( - g.decide(None, tid, Instant::now()), - EpochGraceEvent::Denied(DenyReason::CpuClockUnavailable) - ); - } - - #[test] - fn every_outcome_has_a_pre_initialised_label() { - let events = [ - EpochGraceEvent::Granted { - ticks: 1, - cpu_used_ms: 0, - wall_ms: 0, - }, - EpochGraceEvent::Denied(DenyReason::NotArmed), - EpochGraceEvent::Denied(DenyReason::GrantsExhausted), - EpochGraceEvent::Denied(DenyReason::ThreadMigrated), - EpochGraceEvent::Denied(DenyReason::CpuClockUnavailable), - EpochGraceEvent::Denied(DenyReason::CpuExhausted { - cpu_used_ms: 1, - budget_ms: 1, - }), - ]; - assert_eq!(events.len(), EpochGraceEvent::OUTCOME_LABELS.len()); - for ev in events { - assert!( - EpochGraceEvent::OUTCOME_LABELS.contains(&ev.outcome_label()), - "{ev:?} -> {}", - ev.outcome_label() - ); - } - } - - #[cfg(any(target_os = "linux", target_os = "macos"))] - #[test] - fn thread_cpu_clock_advances_with_work_and_not_with_sleep() { - let a = thread_cpu_ns().expect("thread CPU clock"); - thread::sleep(Duration::from_millis(20)); - let b = thread_cpu_ns().unwrap(); - // Sleeping costs (close to) nothing. - assert!(b - a < 5 * NS_PER_MS, "sleep charged {} ns of CPU", b - a); - // Burn ~2 ms of real CPU; `black_box` keeps the loop from being - // optimised away. - let mut x = 0u64; - while thread_cpu_ns().unwrap() - b < 2 * NS_PER_MS { - x = std::hint::black_box(x.wrapping_mul(6364136223846793005).wrapping_add(1)); - } - std::hint::black_box(x); - assert!(thread_cpu_ns().unwrap() - b >= 2 * NS_PER_MS); - } -} diff --git a/crates/runtime/src/lib.rs b/crates/runtime/src/lib.rs index 17fef6e..0aff266 100644 --- a/crates/runtime/src/lib.rs +++ b/crates/runtime/src/lib.rs @@ -1,5 +1,4 @@ use crate::app::KvStoreOption; -use crate::epoch_grace::EpochGrace; use crate::store::HasStats; use http_backend::stats::ExtStatsTimer; use std::net::Ipv4Addr; @@ -24,7 +23,6 @@ use wasmtime::{ use wit_component::ComponentEncoder; pub mod app; -pub mod epoch_grace; pub mod instances; mod limiter; pub mod logger; @@ -124,11 +122,6 @@ pub struct Data { pub epoch_pause_ms: Arc, /// Whether elapsed time of external HTTP should refund epoch ticks. pub pause_epoch_timeout_for_external_http: bool, - /// CPU-aware grace for the epoch deadline: when the wall budget runs out - /// with no host-call credit, distinguishes a thread that was parked in the - /// kernel from a guest that really spent its budget. See - /// [`crate::epoch_grace`]. - pub epoch_grace: EpochGrace, /// Counts this instance in `fastedge_wasm_instances_live` for as long as the store — /// and therefore its pooling-allocator slots — is alive. Held only for its `Drop`. _live_instance: crate::instances::LiveInstanceGuard, diff --git a/crates/runtime/src/store.rs b/crates/runtime/src/store.rs index 81acd9e..3bc9932 100644 --- a/crates/runtime/src/store.rs +++ b/crates/runtime/src/store.rs @@ -1,4 +1,3 @@ -use crate::epoch_grace::{EpochGrace, EpochGraceEvent}; use crate::limiter::ProxyLimiter; use crate::logger::Logger; use crate::registry::CachedGraphRegistry; @@ -72,13 +71,6 @@ impl Store { pub fn is_oom(&self) -> bool { self.inner.data().store_limits.oom } - - /// (Re-)arm the epoch deadline to `ticks` from now, resetting the stall - /// grace baseline to the current thread. Use this instead of - /// `set_epoch_deadline` directly; see [`crate::epoch_grace`]. - pub fn arm_epoch_deadline(&mut self, ticks: u64) { - arm_epoch_deadline(&mut self.inner, ticks); - } } /// Error indicating a wasm operation failed because it required more memory @@ -384,74 +376,25 @@ impl StoreBuilder { cache: cache_impl, epoch_pause_ms: epoch_pause_ms.clone(), pause_epoch_timeout_for_external_http: self.epoch_exclude_http_wait, - epoch_grace: EpochGrace::new(DEFAULT_EPOCH_TICK_INTERVAL), _live_instance: crate::instances::LiveInstanceGuard::new(), }, ); inner.limiter(|state| &mut state.store_limits); // allow max number of epoch ticks (1 tick = 10 ms) - arm_epoch_deadline(&mut inner, self.max_duration); - // When the deadline fires: first consume any host-call credit - // accumulated by `epoch_pause_ms` to extend it; failing that, extend - // once more if the executing thread was parked rather than running - // (see `epoch_grace`); otherwise trap as before. - inner.epoch_deadline_callback(move |mut ctx| { + inner.set_epoch_deadline(self.max_duration); + // When the deadline fires, consume any host-call credit accumulated by + // `epoch_pause_ms` to extend it; if there is no credit, trap as before. + inner.epoch_deadline_callback(move |_ctx| { let credit_ms = epoch_pause_ms.swap(0, Ordering::Relaxed); - epoch_deadline_decision(credit_ms, &mut ctx.data_mut().epoch_grace) + match epoch_credit_ticks(credit_ms) { + None => Err(anyhow::Error::new(wasmtime::Trap::Interrupt)), + Some(ticks) => Ok(UpdateDeadline::Continue(ticks)), + } }); Ok(Store { inner }) } } -/// Arm the epoch deadline to `ticks` and record the baseline the stall grace -/// measures against. Every place that sets a deadline must go through here, -/// or the grace has no baseline and denies. -pub(crate) fn arm_epoch_deadline(store: &mut wasmtime::Store>, ticks: u64) { - store.data_mut().epoch_grace.arm(ticks); - store.set_epoch_deadline(ticks); -} - -/// The epoch-deadline callback's decision, factored out so tests exercise the -/// production logic rather than a copy of it. -/// -/// Order matters: host-call credit is the *earned* extension (the guest was -/// waiting on external I/O the host chose not to charge) and takes precedence; -/// the stall grace is only consulted when there is none. -pub(crate) fn epoch_deadline_decision( - credit_ms: u64, - grace: &mut EpochGrace, -) -> Result { - if let Some(ticks) = epoch_credit_ticks(credit_ms) { - // The credited extension starts a new deadline segment; rebase the - // stall grace on it so the next no-credit hit measures CPU against - // this segment, not the long-expired one. Grant allowance is kept. - grace.rebase(ticks); - return Ok(UpdateDeadline::Continue(ticks)); - } - let event = grace.on_deadline(); - crate::util::metrics::report_epoch_grace(event); - match event { - EpochGraceEvent::Granted { - ticks, - cpu_used_ms, - wall_ms, - } => { - debug!( - ticks, - cpu_used_ms, - wall_ms, - grants = grace.grants(), - "epoch deadline reached while parked; extending" - ); - Ok(UpdateDeadline::Continue(ticks)) - } - EpochGraceEvent::Denied(reason) => { - debug!(?reason, "epoch deadline reached; trapping"); - Err(anyhow::Error::new(wasmtime::Trap::Interrupt)) - } - } -} - /// Compute how many epoch ticks to refund given accumulated host-call /// credit (in ms). /// @@ -521,19 +464,6 @@ mod tests { ); } - #[test] - fn credited_extension_rebases_the_stall_grace() { - // A credited Continue starts a new deadline segment; the grace - // baseline must move to it, or the next no-credit hit measures CPU - // across both segments against the stale budget. - let mut grace = EpochGrace::new(DEFAULT_EPOCH_TICK_INTERVAL); - grace.arm(100); - let decision = epoch_deadline_decision(25, &mut grace).unwrap(); - assert!(matches!(decision, UpdateDeadline::Continue(3))); - assert_eq!(grace.baseline_ticks(), Some(3)); - assert_eq!(grace.grants(), 0); - } - // ── integration test: end-to-end deadline extension ─────────────────── // // These tests verify the same wiring used in production @@ -562,39 +492,26 @@ mod tests { Engine::new(&cfg).unwrap() } - /// Install the epoch-deadline callback used in production (see - /// `StoreBuilder::build_with_wasi`) — the same `epoch_deadline_decision`, - /// with the store data standing in for `Data::epoch_grace`. - fn install_epoch_callback(store: &mut WtStore, epoch_pause_ms: Arc) { - store.epoch_deadline_callback(move |mut ctx| { + /// Install the same epoch-deadline callback used in production + /// (see `StoreBuilder::build_with_wasi`). + fn install_epoch_callback(store: &mut WtStore, epoch_pause_ms: Arc) { + store.epoch_deadline_callback(move |_ctx| { let credit_ms = epoch_pause_ms.swap(0, Ordering::Relaxed); - epoch_deadline_decision(credit_ms, ctx.data_mut()) + match epoch_credit_ticks(credit_ms) { + None => Err(anyhow::Error::new(Trap::Interrupt)), + Some(ticks) => Ok(UpdateDeadline::Continue(ticks)), + } }); } - /// Like `SPIN_WAT` but effectively unbounded (seconds of CPU), for tests - /// that must observe a trap: with the stall grace in place a 1-tick budget - /// can legitimately be extended a bounded number of times before the trap, - /// and the guest must still be running when that happens. - const LONG_SPIN_WAT: &str = r#" - (module - (global $i (mut i32) (i32.const 2000000000)) - (func (export "spin") - (loop $l - (global.set $i (i32.sub (global.get $i) (i32.const 1))) - (br_if $l (i32.gt_s (global.get $i) (i32.const 0)))))) - "#; - #[test] fn busy_loop_without_credit_traps_with_interrupt() { let engine = make_engine(); - let module = Module::new(&engine, LONG_SPIN_WAT).unwrap(); + let module = Module::new(&engine, SPIN_WAT).unwrap(); let epoch_pause_ms = Arc::new(AtomicU64::new(0)); - // 1 ms per tick, matching the test ticker below. - let mut store = WtStore::new(&engine, EpochGrace::new(1)); + let mut store = WtStore::new(&engine, ()); store.set_epoch_deadline(1); // very short budget: 1 tick - store.data_mut().arm(1); install_epoch_callback(&mut store, epoch_pause_ms.clone()); // Background ticker: bump the engine epoch until the test signals stop. @@ -621,9 +538,7 @@ mod tests { ticker.join().unwrap(); // The guest's busy loop must be cut short by the epoch deadline - // because `epoch_pause_ms` was never incremented — and a *busy* guest - // consumes CPU at wall rate, so the stall grace cannot rescue it more - // than its bounded number of times either. + // because `epoch_pause_ms` was never incremented. let err = result.expect_err("guest must trap when no host-call credit is deposited"); let trap = err.root_cause().downcast_ref::().copied(); assert_eq!( @@ -642,10 +557,8 @@ mod tests { let module = Module::new(&engine, SPIN_WAT).unwrap(); let epoch_pause_ms = Arc::new(AtomicU64::new(0)); - // 1 ms per tick, matching the test ticker below. - let mut store = WtStore::new(&engine, EpochGrace::new(1)); + let mut store = WtStore::new(&engine, ()); store.set_epoch_deadline(1); // very short budget: 1 tick - store.data_mut().arm(1); install_epoch_callback(&mut store, epoch_pause_ms.clone()); // Background ticker bumps the epoch AND deposits generous credit so @@ -685,57 +598,6 @@ mod tests { result.expect("guest must complete when epoch credit is deposited"); } - /// A worker that is parked while the wall budget runs out — no CPU - /// consumed — must be granted the unused budget instead of trapping: the - /// guest did no work, so it has not used anything. `thread::sleep` is a - /// faithful model of a kernel park: wall time passes, thread CPU time does - /// not. - #[test] - fn parked_thread_is_granted_grace_and_completes() { - let engine = make_engine(); - let module = Module::new(&engine, SPIN_WAT).unwrap(); - let epoch_pause_ms = Arc::new(AtomicU64::new(0)); - - // 200 ticks × 1 ms = 200 ms of budget: generous next to the few ms the - // bounded spin needs, so a slow CI box cannot turn the grant into a - // second, legitimate deadline hit. - const BUDGET_TICKS: u64 = 200; - let mut store = WtStore::new(&engine, EpochGrace::new(1)); - store.set_epoch_deadline(BUDGET_TICKS); - store.data_mut().arm(BUDGET_TICKS); - install_epoch_callback(&mut store, epoch_pause_ms.clone()); - - let stop = Arc::new(AtomicBool::new(false)); - let ticker = { - let engine = engine.clone(); - let stop = stop.clone(); - thread::spawn(move || { - while !stop.load(Ordering::Relaxed) { - engine.increment_epoch(); - thread::sleep(Duration::from_millis(1)); - } - }) - }; - - let instance = Instance::new(&mut store, &module, &[]).unwrap(); - let spin = instance - .get_typed_func::<(), ()>(&mut store, "spin") - .unwrap(); - - // Park: the ticker blows through the entire budget while this thread - // consumes no CPU at all. - thread::sleep(Duration::from_millis(300)); - - let result = spin.call(&mut store, ()); - - stop.store(true, Ordering::Relaxed); - ticker.join().unwrap(); - - result.expect("a parked guest must be granted its unused budget, not trapped"); - assert_eq!(store.data().grants(), 1, "exactly one grace grant expected"); - assert_eq!(epoch_pause_ms.load(Ordering::Relaxed), 0); - } - // ── live-instance accounting ────────────────────────────────────────── /// No-op stats sink; `StoreBuilder::build` only needs `HasStats` to wire the diff --git a/crates/runtime/src/util/metrics.rs b/crates/runtime/src/util/metrics.rs index 4950d87..90baff4 100644 --- a/crates/runtime/src/util/metrics.rs +++ b/crates/runtime/src/util/metrics.rs @@ -8,7 +8,6 @@ use std::sync::OnceLock; use crate::AppResult; -use crate::epoch_grace::EpochGraceEvent; /// Where in an app call the outcome was decided. /// @@ -60,23 +59,3 @@ pub fn metrics_in_phase( sink(result, phase, label, duration, memory_used); } } - -// ── epoch stall grace ──────────────────────────────────────────────────────── - -/// Sink for epoch-deadline decisions taken when the wall budget ran out with -/// no host-call credit (see [`crate::epoch_grace`]). -pub type EpochGraceSink = fn(event: EpochGraceEvent); - -static EPOCH_GRACE_SINK: OnceLock = OnceLock::new(); - -/// Install the process-global epoch-grace sink. Subsequent calls are ignored. -pub fn set_epoch_grace_sink(sink: EpochGraceSink) { - let _ = EPOCH_GRACE_SINK.set(sink); -} - -/// Report one epoch-grace decision (no-op when no sink is installed). -pub fn report_epoch_grace(event: EpochGraceEvent) { - if let Some(sink) = EPOCH_GRACE_SINK.get() { - sink(event); - } -} From e17a0fe2b7533c003f61d7e10b5e0a537d299319 Mon Sep 17 00:00:00 2001 From: Ruslan Pislari Date: Tue, 22 Sep 2026 13:11:50 +0300 Subject: [PATCH 16/22] feat(access-log): per-request access log shipped via log sender MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port the access-log work from feat/integration-with-log-sender onto current main (that branch is ~210 commits behind and carries the FastEdge-lib subtree at lib/ instead of fastedge-lib/). Emit exactly one record per processed request — for every outcome, including early rejections — describing how the edge server handled it: traceparent, final status, X-CDN-Internal-Status, app/client ids, method, uri and execution time. Transport is abstracted behind `runtime::access_log::AccessLogSender`; the server implements it as best-effort RFC 3164 syslog datagrams over UDP to the trace stream (default 127.0.0.1:9514, `--access-log-addr` / `--access-log-role`). Init failure falls back to the no-op sink. Covered paths: - ProxyWasm (v1/v2/v2a/v2b/v2c/v2d/v3), one record per lifecycle callback, tagged with the trigger and the initiating service. - Preview2 http-handler and standard wasi:http. - Outbound requests (`proxy_http_call` and the WASI/http-backend fetch client) as their own `fetch` records, correlated by traceparent via a dependency-free `ExtRequestLog` seam in http-backend. Requests without an inbound `traceparent` get a freshly generated W3C id (`runtime::trace::new_traceparent`) instead of a nanoid, and the resolved id is pinned onto the request so outbound records join the inbound one. `traceparent` is added to the default propagate-headers set. nanoid is dropped from proxywasm and http-service. Deviations from the source branch, for current main: - Initiator/version mapping extended to V2d (core-proxy) and V3 (nginx), matching the connection-gauge classification. - ProxyWasm record duration reads the per-call `Instant` already used by the metrics path (ungated from the `metrics` feature) rather than the stats row, whose `get_time_elapsed()` is a request-level accumulator and would over-report on streaming body callbacks. - The cdn-config/LMDB commit on that branch is a separate feature and is not included. --- Cargo.lock | 221 +++++++++--------- crates/http-backend/src/access_log.rs | 58 +++++ crates/http-backend/src/lib.rs | 88 ++++++- crates/http-service/Cargo.toml | 1 - crates/http-service/src/executor/http.rs | 7 + crates/http-service/src/executor/wasi_http.rs | 7 + crates/http-service/src/lib.rs | 72 +++++- crates/http-service/src/state.rs | 5 + crates/runtime/Cargo.toml | 1 + crates/runtime/src/access_log.rs | 170 ++++++++++++++ crates/runtime/src/lib.rs | 45 +++- crates/runtime/src/registry.rs | 2 +- crates/runtime/src/trace.rs | 60 +++++ src/context.rs | 2 +- 14 files changed, 611 insertions(+), 128 deletions(-) create mode 100644 crates/http-backend/src/access_log.rs create mode 100644 crates/runtime/src/access_log.rs create mode 100644 crates/runtime/src/trace.rs diff --git a/Cargo.lock b/Cargo.lock index e1fe023..6b9de44 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -282,7 +282,7 @@ dependencies = [ "smol_str", "tokio", "tracing", - "wasmtime 48.0.1", + "wasmtime 48.0.2", ] [[package]] @@ -624,9 +624,9 @@ dependencies = [ [[package]] name = "cranelift-assembler-x64" -version = "0.135.1" +version = "0.135.2" dependencies = [ - "cranelift-assembler-x64-meta 0.135.1", + "cranelift-assembler-x64-meta 0.135.2", ] [[package]] @@ -639,9 +639,9 @@ dependencies = [ [[package]] name = "cranelift-assembler-x64-meta" -version = "0.135.1" +version = "0.135.2" dependencies = [ - "cranelift-srcgen 0.135.1", + "cranelift-srcgen 0.135.2", ] [[package]] @@ -654,9 +654,9 @@ dependencies = [ [[package]] name = "cranelift-bforest" -version = "0.135.1" +version = "0.135.2" dependencies = [ - "cranelift-entity 0.135.1", + "cranelift-entity 0.135.2", "wasmtime-internal-core", ] @@ -671,7 +671,7 @@ dependencies = [ [[package]] name = "cranelift-bitset" -version = "0.135.1" +version = "0.135.2" dependencies = [ "serde", "serde_derive", @@ -706,23 +706,23 @@ dependencies = [ [[package]] name = "cranelift-codegen" -version = "0.135.1" +version = "0.135.2" dependencies = [ "bumpalo", - "cranelift-assembler-x64 0.135.1", - "cranelift-bforest 0.135.1", - "cranelift-bitset 0.135.1", - "cranelift-codegen-meta 0.135.1", - "cranelift-codegen-shared 0.135.1", - "cranelift-control 0.135.1", - "cranelift-entity 0.135.1", - "cranelift-isle 0.135.1", + "cranelift-assembler-x64 0.135.2", + "cranelift-bforest 0.135.2", + "cranelift-bitset 0.135.2", + "cranelift-codegen-meta 0.135.2", + "cranelift-codegen-shared 0.135.2", + "cranelift-control 0.135.2", + "cranelift-entity 0.135.2", + "cranelift-isle 0.135.2", "gimli 0.33.0", "hashbrown 0.17.1", "libm", "log", "postcard", - "pulley-interpreter 48.0.1", + "pulley-interpreter 48.0.2", "regalloc2 0.15.2", "rustc-hash", "serde", @@ -747,13 +747,13 @@ dependencies = [ [[package]] name = "cranelift-codegen-meta" -version = "0.135.1" +version = "0.135.2" dependencies = [ - "cranelift-assembler-x64-meta 0.135.1", - "cranelift-codegen-shared 0.135.1", - "cranelift-srcgen 0.135.1", + "cranelift-assembler-x64-meta 0.135.2", + "cranelift-codegen-shared 0.135.2", + "cranelift-srcgen 0.135.2", "heck", - "pulley-interpreter 48.0.1", + "pulley-interpreter 48.0.2", ] [[package]] @@ -763,7 +763,7 @@ source = "git+https://github.com/G-Core/wasmtime.git?branch=release-36.0.0#62166 [[package]] name = "cranelift-codegen-shared" -version = "0.135.1" +version = "0.135.2" [[package]] name = "cranelift-control" @@ -775,7 +775,7 @@ dependencies = [ [[package]] name = "cranelift-control" -version = "0.135.1" +version = "0.135.2" dependencies = [ "arbitrary", ] @@ -792,9 +792,9 @@ dependencies = [ [[package]] name = "cranelift-entity" -version = "0.135.1" +version = "0.135.2" dependencies = [ - "cranelift-bitset 0.135.1", + "cranelift-bitset 0.135.2", "serde", "serde_derive", "wasmtime-internal-core", @@ -813,9 +813,9 @@ dependencies = [ [[package]] name = "cranelift-frontend" -version = "0.135.1" +version = "0.135.2" dependencies = [ - "cranelift-codegen 0.135.1", + "cranelift-codegen 0.135.2", "hashbrown 0.17.1", "log", "smallvec", @@ -829,7 +829,7 @@ source = "git+https://github.com/G-Core/wasmtime.git?branch=release-36.0.0#62166 [[package]] name = "cranelift-isle" -version = "0.135.1" +version = "0.135.2" [[package]] name = "cranelift-native" @@ -843,9 +843,9 @@ dependencies = [ [[package]] name = "cranelift-native" -version = "0.135.1" +version = "0.135.2" dependencies = [ - "cranelift-codegen 0.135.1", + "cranelift-codegen 0.135.2", "libc", "target-lexicon", ] @@ -857,7 +857,7 @@ source = "git+https://github.com/G-Core/wasmtime.git?branch=release-36.0.0#62166 [[package]] name = "cranelift-srcgen" -version = "0.135.1" +version = "0.135.2" [[package]] name = "crc32fast" @@ -1136,7 +1136,7 @@ dependencies = [ "tempfile", "tokio", "utils", - "wasmtime 48.0.1", + "wasmtime 48.0.2", ] [[package]] @@ -1772,7 +1772,6 @@ dependencies = [ "hyper", "hyper-util", "key-value-store", - "nanoid", "reactor", "runtime", "secret", @@ -1784,8 +1783,8 @@ dependencies = [ "tracing-test", "utils", "wasi-common", - "wasmtime 48.0.1", - "wasmtime-wasi 48.0.1", + "wasmtime 48.0.2", + "wasmtime-wasi 48.0.2", "wasmtime-wasi-http", "wasmtime-wasi-nn", ] @@ -2142,7 +2141,7 @@ dependencies = [ "smol_str", "tokio", "tracing", - "wasmtime 48.0.1", + "wasmtime 48.0.2", ] [[package]] @@ -2336,15 +2335,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "nanoid" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8628de41fe064cc3f0cf07f3d299ee3e73521adaff72278731d5c8cae3797873" -dependencies = [ - "rand 0.9.5", -] - [[package]] name = "native-tls" version = "0.2.18" @@ -2769,11 +2759,11 @@ dependencies = [ [[package]] name = "pulley-interpreter" -version = "48.0.1" +version = "48.0.2" dependencies = [ - "cranelift-bitset 0.135.1", + "cranelift-bitset 0.135.2", "log", - "pulley-macros 48.0.1", + "pulley-macros 48.0.2", "wasmtime-internal-core", ] @@ -2789,7 +2779,7 @@ dependencies = [ [[package]] name = "pulley-macros" -version = "48.0.1" +version = "48.0.2" dependencies = [ "proc-macro2", "quote", @@ -2971,7 +2961,7 @@ dependencies = [ name = "reactor" version = "0.21.0" dependencies = [ - "wasmtime 48.0.1", + "wasmtime 48.0.2", ] [[package]] @@ -3117,6 +3107,7 @@ dependencies = [ "http-backend", "key-value-store", "moka", + "rand 0.8.7", "secret", "serde", "serde_json", @@ -3126,9 +3117,9 @@ dependencies = [ "tracing", "utils", "wasi-common", - "wasmtime 48.0.1", + "wasmtime 48.0.2", "wasmtime-environ 36.0.12", - "wasmtime-wasi 48.0.1", + "wasmtime-wasi 48.0.2", "wasmtime-wasi-http", "wasmtime-wasi-io 36.0.12", "wasmtime-wasi-nn", @@ -4338,7 +4329,7 @@ dependencies = [ [[package]] name = "wasmtime" -version = "48.0.1" +version = "48.0.2" dependencies = [ "addr2line 0.26.1", "async-trait", @@ -4357,7 +4348,7 @@ dependencies = [ "object 0.39.1", "once_cell", "postcard", - "pulley-interpreter 48.0.1", + "pulley-interpreter 48.0.2", "rayon", "rustix 1.1.4", "semver", @@ -4370,17 +4361,17 @@ dependencies = [ "wasm-compose", "wasm-encoder 0.254.0", "wasmparser 0.254.0", - "wasmtime-environ 48.0.1", + "wasmtime-environ 48.0.2", "wasmtime-internal-cache", - "wasmtime-internal-component-macro 48.0.1", - "wasmtime-internal-component-util 48.0.1", + "wasmtime-internal-component-macro 48.0.2", + "wasmtime-internal-component-util 48.0.2", "wasmtime-internal-core", - "wasmtime-internal-cranelift 48.0.1", - "wasmtime-internal-fiber 48.0.1", - "wasmtime-internal-jit-debug 48.0.1", - "wasmtime-internal-jit-icache-coherence 48.0.1", - "wasmtime-internal-unwinder 48.0.1", - "wasmtime-internal-versioned-export-macros 48.0.1", + "wasmtime-internal-cranelift 48.0.2", + "wasmtime-internal-fiber 48.0.2", + "wasmtime-internal-jit-debug 48.0.2", + "wasmtime-internal-jit-icache-coherence 48.0.2", + "wasmtime-internal-unwinder 48.0.2", + "wasmtime-internal-versioned-export-macros 48.0.2", "wat", "windows-sys 0.61.2", "wit-parser 0.254.0", @@ -4412,13 +4403,13 @@ dependencies = [ [[package]] name = "wasmtime-environ" -version = "48.0.1" +version = "48.0.2" dependencies = [ "anyhow", "cpp_demangle", - "cranelift-bforest 0.135.1", - "cranelift-bitset 0.135.1", - "cranelift-entity 0.135.1", + "cranelift-bforest 0.135.2", + "cranelift-bitset 0.135.2", + "cranelift-entity 0.135.2", "gimli 0.33.0", "hashbrown 0.17.1", "indexmap", @@ -4435,7 +4426,7 @@ dependencies = [ "wasm-encoder 0.254.0", "wasmparser 0.254.0", "wasmprinter 0.254.0", - "wasmtime-internal-component-util 48.0.1", + "wasmtime-internal-component-util 48.0.2", "wasmtime-internal-core", ] @@ -4449,7 +4440,7 @@ dependencies = [ [[package]] name = "wasmtime-internal-cache" -version = "48.0.1" +version = "48.0.2" dependencies = [ "base64", "directories-next", @@ -4460,7 +4451,7 @@ dependencies = [ "serde_derive", "sha2", "toml", - "wasmtime-environ 48.0.1", + "wasmtime-environ 48.0.2", "windows-sys 0.61.2", "zstd", ] @@ -4481,14 +4472,14 @@ dependencies = [ [[package]] name = "wasmtime-internal-component-macro" -version = "48.0.1" +version = "48.0.2" dependencies = [ "anyhow", "proc-macro2", "quote", "syn 2.0.119", - "wasmtime-internal-component-util 48.0.1", - "wasmtime-internal-wit-bindgen 48.0.1", + "wasmtime-internal-component-util 48.0.2", + "wasmtime-internal-wit-bindgen 48.0.2", "wit-parser 0.254.0", ] @@ -4499,11 +4490,11 @@ source = "git+https://github.com/G-Core/wasmtime.git?branch=release-36.0.0#62166 [[package]] name = "wasmtime-internal-component-util" -version = "48.0.1" +version = "48.0.2" [[package]] name = "wasmtime-internal-core" -version = "48.0.1" +version = "48.0.2" dependencies = [ "anyhow", "hashbrown 0.17.1", @@ -4539,26 +4530,26 @@ dependencies = [ [[package]] name = "wasmtime-internal-cranelift" -version = "48.0.1" +version = "48.0.2" dependencies = [ - "cranelift-codegen 0.135.1", - "cranelift-control 0.135.1", - "cranelift-entity 0.135.1", - "cranelift-frontend 0.135.1", - "cranelift-native 0.135.1", + "cranelift-codegen 0.135.2", + "cranelift-control 0.135.2", + "cranelift-entity 0.135.2", + "cranelift-frontend 0.135.2", + "cranelift-native 0.135.2", "gimli 0.33.0", "itertools 0.14.0", "log", "object 0.39.1", - "pulley-interpreter 48.0.1", + "pulley-interpreter 48.0.2", "smallvec", "target-lexicon", "thiserror 2.0.19", "wasmparser 0.254.0", - "wasmtime-environ 48.0.1", + "wasmtime-environ 48.0.2", "wasmtime-internal-core", - "wasmtime-internal-unwinder 48.0.1", - "wasmtime-internal-versioned-export-macros 48.0.1", + "wasmtime-internal-unwinder 48.0.2", + "wasmtime-internal-versioned-export-macros 48.0.2", ] [[package]] @@ -4578,13 +4569,13 @@ dependencies = [ [[package]] name = "wasmtime-internal-fiber" -version = "48.0.1" +version = "48.0.2" dependencies = [ "cc", "libc", "rustix 1.1.4", - "wasmtime-environ 48.0.1", - "wasmtime-internal-versioned-export-macros 48.0.1", + "wasmtime-environ 48.0.2", + "wasmtime-internal-versioned-export-macros 48.0.2", "windows-sys 0.61.2", ] @@ -4599,12 +4590,12 @@ dependencies = [ [[package]] name = "wasmtime-internal-jit-debug" -version = "48.0.1" +version = "48.0.2" dependencies = [ "cc", "object 0.39.1", "rustix 1.1.4", - "wasmtime-internal-versioned-export-macros 48.0.1", + "wasmtime-internal-versioned-export-macros 48.0.2", ] [[package]] @@ -4620,7 +4611,7 @@ dependencies = [ [[package]] name = "wasmtime-internal-jit-icache-coherence" -version = "48.0.1" +version = "48.0.2" dependencies = [ "libc", "wasmtime-internal-core", @@ -4653,12 +4644,12 @@ dependencies = [ [[package]] name = "wasmtime-internal-unwinder" -version = "48.0.1" +version = "48.0.2" dependencies = [ - "cranelift-codegen 0.135.1", + "cranelift-codegen 0.135.2", "log", "object 0.39.1", - "wasmtime-environ 48.0.1", + "wasmtime-environ 48.0.2", ] [[package]] @@ -4673,7 +4664,7 @@ dependencies = [ [[package]] name = "wasmtime-internal-versioned-export-macros" -version = "48.0.1" +version = "48.0.2" dependencies = [ "proc-macro2", "quote", @@ -4710,7 +4701,7 @@ dependencies = [ [[package]] name = "wasmtime-internal-wit-bindgen" -version = "48.0.1" +version = "48.0.2" dependencies = [ "anyhow", "bitflags 2.13.1", @@ -4751,7 +4742,7 @@ dependencies = [ [[package]] name = "wasmtime-wasi" -version = "48.0.1" +version = "48.0.2" dependencies = [ "async-trait", "bitflags 2.13.1", @@ -4765,9 +4756,9 @@ dependencies = [ "tokio", "tracing", "url", - "wasmtime 48.0.1", - "wasmtime-wasi-io 48.0.1", - "wiggle 48.0.1", + "wasmtime 48.0.2", + "wasmtime-wasi-io 48.0.2", + "wiggle 48.0.2", "windows-sys 0.61.2", "winx", ] @@ -4809,18 +4800,18 @@ dependencies = [ [[package]] name = "wasmtime-wasi-io" -version = "48.0.1" +version = "48.0.2" dependencies = [ "async-trait", "bytes", "futures", "tracing", - "wasmtime 48.0.1", + "wasmtime 48.0.2", ] [[package]] name = "wasmtime-wasi-nn" -version = "48.0.1" +version = "48.0.2" dependencies = [ "candle-core", "candle-nn", @@ -4829,8 +4820,8 @@ dependencies = [ "thiserror 2.0.19", "tracing", "walkdir", - "wasmtime 48.0.1", - "wiggle 48.0.1", + "wasmtime 48.0.2", + "wiggle 48.0.2", "windows", ] @@ -4899,14 +4890,14 @@ dependencies = [ [[package]] name = "wiggle" -version = "48.0.1" +version = "48.0.2" dependencies = [ "bitflags 2.13.1", "thiserror 2.0.19", "tracing", - "wasmtime 48.0.1", - "wasmtime-environ 48.0.1", - "wiggle-macro 48.0.1", + "wasmtime 48.0.2", + "wasmtime-environ 48.0.2", + "wiggle-macro 48.0.2", ] [[package]] @@ -4924,13 +4915,13 @@ dependencies = [ [[package]] name = "wiggle-generate" -version = "48.0.1" +version = "48.0.2" dependencies = [ "heck", "proc-macro2", "quote", "syn 2.0.119", - "wasmtime-environ 48.0.1", + "wasmtime-environ 48.0.2", "witx", ] @@ -4947,12 +4938,12 @@ dependencies = [ [[package]] name = "wiggle-macro" -version = "48.0.1" +version = "48.0.2" dependencies = [ "proc-macro2", "quote", "syn 2.0.119", - "wiggle-generate 48.0.1", + "wiggle-generate 48.0.2", ] [[package]] diff --git a/crates/http-backend/src/access_log.rs b/crates/http-backend/src/access_log.rs new file mode 100644 index 0000000..cdc88eb --- /dev/null +++ b/crates/http-backend/src/access_log.rs @@ -0,0 +1,58 @@ +//! Outbound-request access-log seam. +//! +//! Distinct from [`crate::stats`] (billing/Prometheus timing): this sink emits +//! one access-log record per external HTTP request the guest makes. It is kept +//! dependency-free — primitive argument types only — so `http-backend` need not +//! depend on the runtime's access-log types. The concrete implementation (which +//! builds the runtime record and ships it) lives in the embedder. + +use smol_str::SmolStr; +use std::sync::Arc; +use std::time::Duration; + +/// Sink for one completed outbound (external) HTTP request. +/// +/// `status` is `0` when the request failed before a response was received +/// (connection error / timeout). Implementations must be non-blocking — this is +/// called on the request hot path. +pub trait ExtRequestLog: Send + Sync { + /// Record one completed outbound request, correlated to the inbound request + /// via `traceparent`. + fn log_ext_request( + &self, + traceparent: &str, + app_id: u64, + method: &str, + uri: &str, + status: u16, + elapsed: Duration, + ); +} + +/// A cloneable, connector-agnostic handle carrying everything needed to log one +/// outbound request once it completes. Used by the WASI-HTTP path, where the +/// send happens in a spawned task and the [`crate::Backend`] (generic over its +/// connector) is not available; the handle is captured before the spawn. +#[derive(Clone)] +pub struct ExtRequestLogHandle { + log: Arc, + traceparent: SmolStr, + app_id: u64, +} + +impl ExtRequestLogHandle { + pub fn new(log: Arc, traceparent: SmolStr, app_id: u64) -> Self { + Self { + log, + traceparent, + app_id, + } + } + + /// Emit one record for a completed outbound request. `status` is `0` on + /// failure (no response received). + pub fn log(&self, method: &str, uri: &str, status: u16, elapsed: Duration) { + self.log + .log_ext_request(&self.traceparent, self.app_id, method, uri, status, elapsed); + } +} diff --git a/crates/http-backend/src/lib.rs b/crates/http-backend/src/lib.rs index e558387..02aa886 100644 --- a/crates/http-backend/src/lib.rs +++ b/crates/http-backend/src/lib.rs @@ -1,5 +1,7 @@ +pub mod access_log; pub mod stats; +use crate::access_log::{ExtRequestLog, ExtRequestLogHandle}; use smol_str::{SmolStr, ToSmolStr}; use std::fmt::Debug; use std::future::Future; @@ -92,6 +94,12 @@ pub struct Backend { epoch_exclude_http_wait: bool, cdn_real_host: Option, app_id: Option, + /// Sink for the per-outbound-request access log. Baked into the backend + /// template by the embedder; `None` disables outbound-request logging. + ext_http_log: Option>, + /// W3C `traceparent` of the inbound request, set per request so each + /// outbound record can be correlated back to it. + traceparent: Option, } pub struct Builder { @@ -144,6 +152,8 @@ impl Builder { epoch_exclude_http_wait: false, cdn_real_host: None, app_id: None, + ext_http_log: None, + traceparent: None, } } } @@ -183,6 +193,48 @@ impl Backend { self.app_id } + /// Set the outbound-request access-log sink (baked into the template once). + pub fn set_ext_http_log(&mut self, log: Arc) { + self.ext_http_log = Some(log); + } + + /// Set the inbound `traceparent` used to correlate outbound records. + pub fn set_traceparent(&mut self, traceparent: SmolStr) { + self.traceparent = Some(traceparent); + } + + /// Snapshot the outbound-request log sink + correlation context into a + /// cloneable handle, for logging from a context where `self` is not + /// available (e.g. the WASI-HTTP spawned send task). `None` when no sink or + /// `traceparent` is set. + pub fn ext_request_log_handle(&self) -> Option { + match (self.ext_http_log.as_ref(), self.traceparent.as_ref()) { + (Some(log), Some(traceparent)) => Some(ExtRequestLogHandle::new( + log.clone(), + traceparent.clone(), + self.app_id.unwrap_or(0), + )), + _ => None, + } + } + + /// Emit one access-log record for a completed outbound request. No-op when + /// no sink or no `traceparent` is set. `status` is `0` on failure. + pub fn log_ext_request( + &self, + app_id: u64, + method: &str, + uri: &str, + status: u16, + elapsed: Duration, + ) { + if let (Some(log), Some(traceparent)) = + (self.ext_http_log.as_ref(), self.traceparent.as_ref()) + { + log.log_ext_request(traceparent, app_id, method, uri, status, elapsed); + } + } + /// Set external request stats pub fn set_ext_http_stats(&mut self, stats: Arc) { self.ext_http_stats.replace(stats); @@ -424,6 +476,11 @@ where self.max_sub_requests -= 1; } + // Capture the outbound method/target for the access-log record before + // `make_request` consumes the request. + let log_method = method_str(&req.method); + let log_uri = req.uri.to_smolstr(); + let request = self.make_request(req).map_err(|error| { warn!(cause=?error, "making request to backend"); HttpError::RequestError @@ -483,14 +540,43 @@ where }) } .await; + let elapsed = started.elapsed(); if self.epoch_exclude_http_wait { self.epoch_pause_ms - .fetch_add(started.elapsed().as_millis() as u64, Ordering::Relaxed); + .fetch_add(elapsed.as_millis() as u64, Ordering::Relaxed); } + + // Emit one access-log record for this outbound request. Failures before a + // response (connection error / timeout) are logged with status 0. + let status = match &result { + Ok(response) => response.status, + Err(_) => 0, + }; + self.log_ext_request( + self.app_id.unwrap_or(0), + &log_method, + &log_uri, + status, + elapsed, + ); result } } +/// Stable string label for a guest [`Method`], for the access log. +fn method_str(method: &Method) -> SmolStr { + match method { + Method::Get => "GET", + Method::Post => "POST", + Method::Put => "PUT", + Method::Delete => "DELETE", + Method::Head => "HEAD", + Method::Patch => "PATCH", + Method::Options => "OPTIONS", + } + .into() +} + // extract canonical host name fn canonical_host_name(headers: &Headers, original_uri: &Uri) -> Result { let host = headers.iter().find_map(|(k, v)| { diff --git a/crates/http-service/Cargo.toml b/crates/http-service/Cargo.toml index e5b4b8b..8b7ce2c 100644 --- a/crates/http-service/Cargo.toml +++ b/crates/http-service/Cargo.toml @@ -26,7 +26,6 @@ http-backend = { path = "../http-backend" } utils = { path = "../utils" } secret = { path = "../secret" } cache = { path = "../cache" } -nanoid = "0.5" async-trait = "0.1" hyper-util = { version = "0.1", features = ["server", "server-graceful"] } http-body-util = "0.1" diff --git a/crates/http-service/src/executor/http.rs b/crates/http-service/src/executor/http.rs index d74b675..83a282a 100644 --- a/crates/http-service/src/executor/http.rs +++ b/crates/http-service/src/executor/http.rs @@ -98,6 +98,13 @@ where let mut http_backend = self.backend; http_backend.set_app_id(self.app_id); + if let Some(traceparent) = parts + .headers + .get(executor::TRACEPARENT) + .and_then(|v| v.to_str().ok()) + { + http_backend.set_traceparent(traceparent.into()); + } http_backend .propagate_headers(parts.headers.clone()) .context("propagate headers")?; diff --git a/crates/http-service/src/executor/wasi_http.rs b/crates/http-service/src/executor/wasi_http.rs index 7b5960a..bf489f8 100644 --- a/crates/http-service/src/executor/wasi_http.rs +++ b/crates/http-service/src/executor/wasi_http.rs @@ -94,6 +94,13 @@ where } let mut http_backend = self.backend; http_backend.set_app_id(self.app_id); + if let Some(traceparent) = parts + .headers + .get(executor::TRACEPARENT) + .and_then(|v| v.to_str().ok()) + { + http_backend.set_traceparent(traceparent.into()); + } http_backend.epoch_exclude_http_wait(self.epoch_exclude_http_wait); if let Some(counter) = epoch_pause_ms { http_backend.set_epoch_pause_ms(counter); diff --git a/crates/http-service/src/lib.rs b/crates/http-service/src/lib.rs index 728fd96..471d956 100644 --- a/crates/http-service/src/lib.rs +++ b/crates/http-service/src/lib.rs @@ -18,7 +18,10 @@ use hyper_util::{client::legacy::connect::Connect, rt::TokioIo}; use runtime::util::metrics; use runtime::util::stats::StatsVisitor; use runtime::{ - App, AppResult, ContextT, Router, WasmEngine, WasmEngineBuilder, app::Status, service::Service, + App, AppResult, ContextT, Router, WasmEngine, WasmEngineBuilder, + access_log::{AccessLog, RequestKind}, + app::Status, + service::Service, }; use smol_str::{SmolStr, ToSmolStr}; use state::HttpState; @@ -258,20 +261,67 @@ where T::Executor: HttpExecutor + Send + Sync, S: StatsVisitor + Send + 'static, { - /// handle HTTP request. + /// handle HTTP request, emitting exactly one system request-log record for + /// every processing outcome (success, failure, or early rejection). async fn handle_request( &self, - mut request: hyper::Request, + request: hyper::Request, ) -> Result> where B: BodyExt + Send, ::Data: Send, { let traceparent = remote_traceparent(&request); + let mut log = AccessLog::new(traceparent.to_string(), RequestKind::Http); + log.method = Some(request.method().as_str().to_string()); + log.uri = Some(request.uri().to_string()); + + let result = self + .handle_request_inner(request, &traceparent, &mut log) + .await; + + match &result { + Ok(response) => { + log.status_code = response.status().as_u16(); + log.internal_status_code = response + .headers() + .get(X_CDN_INTERNAL_STATUS) + .and_then(|v| v.to_str().ok()) + .and_then(|s| s.parse().ok()) + .unwrap_or(0); + } + Err(_) => { + log.status_code = FASTEDGE_INTERNAL_ERROR; + log.internal_status_code = INTERNAL_STATUS_EXECUTE_ERROR; + } + } + self.context.access_log_sender().log(log); + result + } + + async fn handle_request_inner( + &self, + mut request: hyper::Request, + traceparent: &SmolStr, + log: &mut AccessLog, + ) -> Result> + where + B: BodyExt + Send, + ::Data: Send, + { request .headers_mut() .extend(app_req_headers(self.context.append_headers())); + // Pin the resolved traceparent onto the request (it may have been + // generated when the client sent none) so the executor/backend logs + // outbound requests under the same id the access log uses. + if let Ok(value) = HeaderValue::from_str(traceparent) { + request + .headers_mut() + .insert(HeaderName::from_static(TRACEPARENT), value); + } + // get application name from request URL let app_name = match app_name_from_request(&request) { Err(error) => { @@ -341,6 +391,10 @@ where Some((app_name, cfg)) => (app_name, cfg), }; + log.app_id = Some(cfg.app_id); + log.client_id = Some(cfg.client_id); + log.app_name = Some(app_name.to_string()); + // get cached execute context for this application let executor = match self .context @@ -365,7 +419,7 @@ where .unwrap_or(std::net::Ipv4Addr::UNSPECIFIED); let stats = self .context - .new_stats_row(&traceparent, &app_name, caller_ip, &cfg); + .new_stats_row(traceparent, &app_name, caller_ip, &cfg); let response = match executor.execute(request, stats.clone()).await { Ok(mut response) => { @@ -404,6 +458,7 @@ where builder.body(msg)? } }; + log.duration = Some(Duration::from_micros(stats.get_time_elapsed())); Ok(response) } .instrument(span) @@ -434,9 +489,9 @@ pub(crate) fn fail_reason_of(error: &Error) -> AppResult { wasmtime::Trap::UnreachableCodeReached => AppResult::OOM, _ => AppResult::OTHER, } - } else if root_cause.downcast_ref::().is_some() - || root_cause.to_string().ends_with("deadline has elapsed") - { + } else if root_cause.downcast_ref::().is_some() { + AppResult::TIMEOUT + } else if root_cause.to_string().ends_with("deadline has elapsed") { AppResult::TIMEOUT } else { AppResult::OTHER @@ -537,8 +592,9 @@ fn remote_traceparent(req: &hyper::Request) -> SmolStr { req.headers() .get(TRACEPARENT) .and_then(|v| v.to_str().ok()) + .filter(|s| !s.is_empty()) .map(|s| s.to_smolstr()) - .unwrap_or(nanoid::nanoid!().to_smolstr()) + .unwrap_or_else(runtime::new_traceparent) } /// Creates an HTTP 530 response with an `X-CDN-Internal-Status` header. diff --git a/crates/http-service/src/state.rs b/crates/http-service/src/state.rs index ad62f6f..944ded3 100644 --- a/crates/http-service/src/state.rs +++ b/crates/http-service/src/state.rs @@ -3,6 +3,7 @@ use http::request::Parts; use http::uri::Scheme; use http::{HeaderMap, HeaderName, HeaderValue, Uri, header}; use http_backend::Backend; +use http_backend::access_log::ExtRequestLogHandle; use http_backend::{is_backend_host, is_fastedge_internal_header, is_public_host}; use runtime::BackendRequest; use runtime::store::HasStats; @@ -31,6 +32,10 @@ const FASTEDGE_APP_ID: HeaderName = HeaderName::from_static("fastedge-app-id"); const CDN_LOOP: HeaderName = HeaderName::from_static("cdn-loop"); impl BackendRequest for HttpState { + fn ext_request_log_handle(&self) -> Option { + self.http_backend.ext_request_log_handle() + } + #[instrument(skip(self, head), level = "debug", ret)] fn backend_request(&mut self, mut head: Parts) -> anyhow::Result { match self.http_backend.strategy { diff --git a/crates/runtime/Cargo.toml b/crates/runtime/Cargo.toml index 1a7f9a9..6fbfe1e 100644 --- a/crates/runtime/Cargo.toml +++ b/crates/runtime/Cargo.toml @@ -29,6 +29,7 @@ utils = { path = "../utils" } secret = { path = "../secret" } async-trait = "0.1" bytes = "1.11" +rand = "0.8" serde = "1.0" serde_json = "1.0" chrono = { version = "0.4", features = ["serde"] } diff --git a/crates/runtime/src/access_log.rs b/crates/runtime/src/access_log.rs new file mode 100644 index 0000000..0ab6d4b --- /dev/null +++ b/crates/runtime/src/access_log.rs @@ -0,0 +1,170 @@ +//! Per-request system log. +//! +//! This is **not** the guest application log (that ships the WASM app's stdout +//! via [`crate::logger`]). This log emits exactly one record per processed +//! request describing how the edge server handled it — traceparent, final HTTP +//! status, the `X-CDN-Internal-Status` diagnostic code, timing, etc. — for both +//! successful and failed requests, across every execution path (ProxyWasm, +//! FastEdge http-handler, and standard wasi:http). +//! +//! The transport is intentionally abstracted behind [`AccessLogSender`]. The +//! concrete appender (planned: a system log shipped over UDP with a JSON +//! payload) is implemented separately; until one is wired in, the runtime uses +//! [`NoopAccessLogSender`]. + +use std::time::Duration; + +/// Which execution path served the request. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RequestKind { + /// ProxyWasm protocol (v1/v2/v2a) — WASI Preview1 core modules. + ProxyWasm, + /// HTTP request served by a Preview2 component — both the FastEdge + /// `gcore:fastedge/http-handler` export and the standard + /// `wasi:http/incoming-handler` export are reported under this variant. + Http, + /// Outbound HTTP request the guest made while serving a request (ProxyWasm + /// `proxy_http_call` or the WASI/http-handler fetch client). Emitted as its + /// own record, correlated to the inbound request via `traceparent`. + ExtHttp, +} + +impl RequestKind { + /// Stable lowercase identifier, suitable for a log field. + pub fn as_str(&self) -> &'static str { + match self { + RequestKind::ProxyWasm => "cdn", + RequestKind::Http => "http", + RequestKind::ExtHttp => "fetch", + } + } +} + +/// ProxyWasm lifecycle callback ("trigger") that produced this record. +/// +/// A single ProxyWasm request is driven through several host callbacks; each +/// one is handled — and logged — independently. Only set on the ProxyWasm path; +/// the HTTP paths run a single execution and leave it `None`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Trigger { + RequestHeaders, + ResponseHeaders, + RequestBody, + ResponseBody, + Log, +} + +impl Trigger { + /// Short, stable identifier for the ProxyWasm callback (kept compact to + /// minimise datagram size): `rqh`/`rsh` = request/response headers, + /// `rqb`/`rsb` = request/response body, `log` = on_log. + pub fn as_str(&self) -> &'static str { + match self { + Trigger::RequestHeaders => "rqh", + Trigger::ResponseHeaders => "rsh", + Trigger::RequestBody => "rqb", + Trigger::ResponseBody => "rsb", + Trigger::Log => "log", + } + } +} + +/// Upstream service that initiated a ProxyWasm request. Derived from the +/// ProxyWasm protocol version: nginx speaks v1/v2a/v2b and the v3 yamux +/// dialect, core-proxy speaks v2/v2c/v2d. Only set on the ProxyWasm path. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Initiator { + Nginx, + CoreProxy, +} + +impl Initiator { + /// Stable identifier, suitable for a log field. + pub fn as_str(&self) -> &'static str { + match self { + Initiator::Nginx => "nginx", + Initiator::CoreProxy => "core-proxy", + } + } +} + +/// One log record describing the processing of a single request. +/// +/// `traceparent` and `status_code` are the primary fields; the rest are +/// best-effort context that may be absent on early-return paths (e.g. an +/// unknown application is rejected before its config — and thus its ids — are +/// known). +#[derive(Debug, Clone)] +pub struct AccessLog { + /// W3C trace-context id used to correlate the request across systems. + pub traceparent: String, + /// Execution path that handled the request. + pub kind: RequestKind, + /// Final HTTP status code returned to the client. + pub status_code: u16, + /// `X-CDN-Internal-Status` diagnostic code (range 3000–3999); `0` when the + /// request completed without an internal error. + pub internal_status_code: u16, + /// Edge application id, once resolved. + pub app_id: Option, + /// Edge application name, once resolved. + pub app_name: Option, + /// Owning customer id, once resolved. + pub client_id: Option, + /// Request HTTP method, when cheaply available. + pub method: Option, + /// Request URI, when cheaply available. + pub uri: Option, + /// Wall-clock time spent executing the WASM app, when the app ran. + pub duration: Option, + /// ProxyWasm lifecycle callback that produced this record (ProxyWasm only). + pub trigger: Option, + /// Upstream service that initiated the request (ProxyWasm only). + pub initiator: Option, +} + +impl AccessLog { + /// Start a record for a request of the given `kind`, keyed by `traceparent`. + /// Remaining fields are filled in as they become known during processing. + pub fn new(traceparent: String, kind: RequestKind) -> Self { + Self { + traceparent, + kind, + status_code: 0, + internal_status_code: 0, + app_id: None, + app_name: None, + client_id: None, + method: None, + uri: None, + duration: None, + trigger: None, + initiator: None, + } + } + + /// A request is considered successful when no internal error code was set + /// (client errors such as 404/429 are not internal failures). + pub fn success(&self) -> bool { + self.internal_status_code == 0 + } +} + +/// Sink for [`AccessLog`] records. +/// +/// Implementations must be non-blocking / fire-and-forget: `log` is called on +/// the request hot path and must not await or block on I/O. The planned UDP +/// appender simply sends a datagram (or hands off to a background task). +pub trait AccessLogSender: Send + Sync { + /// Record one completed request. Must not block. + fn log(&self, record: AccessLog); +} + +/// A [`AccessLogSender`] that discards every record. Used as the default until +/// a concrete appender is configured. +#[derive(Debug, Default, Clone, Copy)] +pub struct NoopAccessLogSender; + +impl AccessLogSender for NoopAccessLogSender { + fn log(&self, _record: AccessLog) {} +} diff --git a/crates/runtime/src/lib.rs b/crates/runtime/src/lib.rs index 0aff266..156b90c 100644 --- a/crates/runtime/src/lib.rs +++ b/crates/runtime/src/lib.rs @@ -1,5 +1,6 @@ use crate::app::KvStoreOption; use crate::store::HasStats; +use http_backend::access_log::ExtRequestLogHandle; use http_backend::stats::ExtStatsTimer; use std::net::Ipv4Addr; use std::sync::Arc; @@ -22,6 +23,7 @@ use wasmtime::{ }; use wit_component::ComponentEncoder; +pub mod access_log; pub mod app; pub mod instances; mod limiter; @@ -30,8 +32,12 @@ mod registry; pub mod service; pub mod store; pub mod stub; +pub mod trace; pub mod util; +pub use trace::new_traceparent; + +use crate::access_log::{AccessLogSender, NoopAccessLogSender}; use crate::app::SecretOption; use crate::logger::Logger; use crate::util::stats::StatsVisitor; @@ -129,6 +135,13 @@ pub struct Data { pub trait BackendRequest { fn backend_request(&mut self, head: Parts) -> anyhow::Result; + + /// Snapshot the outbound-request access-log handle, if configured. Default + /// `None` (no outbound-request logging). Implemented by embedders that carry + /// a sink; used by the WASI-HTTP send path. + fn ext_request_log_handle(&self) -> Option { + None + } } impl AsRef for Data { @@ -162,6 +175,14 @@ impl WasiHttpView for Data { where Self: Sized, { + // Capture the outbound method/target and access-log handle before the + // request is consumed. The URI here is the original external target, + // before `backend_request` rewrites it to the internal backend + // authority. + let log_method = SmolStr::new(request.method().as_str()); + let log_uri = SmolStr::new(request.uri().to_string()); + let log_handle = self.inner.ext_request_log_handle(); + let (head, body) = request.into_parts(); let head = self.inner.backend_request(head).map_err(|e| { tracing::warn!(cause=?e, "backend request"); @@ -180,8 +201,18 @@ impl WasiHttpView for Data { let resp = default_send_request_handler(request, OutgoingRequestConfig { use_tls, ..config }) .await; + let elapsed = started.elapsed(); if epoch_exclude_http_wait { - epoch_pause_ms.fetch_add(started.elapsed().as_millis() as u64, Ordering::Relaxed); + epoch_pause_ms.fetch_add(elapsed.as_millis() as u64, Ordering::Relaxed); + } + // One access-log record per outbound request (status 0 = no response + // received, e.g. connection refused / timeout). + if let Some(handle) = log_handle { + let status = match &resp { + Ok(incoming) => incoming.resp.status().as_u16(), + Err(_) => 0, + }; + handle.log(&log_method, &log_uri, status, elapsed); } Ok(resp) }); @@ -441,6 +472,18 @@ pub trait ContextT { caller_ip: Ipv4Addr, cfg: &App, ) -> Arc; + + /// Sink for the per-request system log (see [`access_log`]). + /// + /// Distinct from the guest application log produced by [`make_logger`]. + /// Defaults to a no-op until a concrete appender (e.g. UDP syslog) is wired + /// in, so existing embedders need not implement it. + /// + /// [`make_logger`]: ContextT::make_logger + fn access_log_sender(&self) -> &dyn AccessLogSender { + static NOOP: NoopAccessLogSender = NoopAccessLogSender; + &NOOP + } } pub trait ExecutorCache { diff --git a/crates/runtime/src/registry.rs b/crates/runtime/src/registry.rs index cb502fa..38c9c81 100644 --- a/crates/runtime/src/registry.rs +++ b/crates/runtime/src/registry.rs @@ -39,7 +39,7 @@ impl GraphRegistry for StoreRegistry { impl CachedGraphRegistry { pub fn new() -> Self { let builder = Cache::builder() - // Max 10,000 entries + // Max 100 entries .max_capacity(100) // Time to idle (TTI): 30 minutes .time_to_idle(Duration::from_secs(30 * 60)); diff --git a/crates/runtime/src/trace.rs b/crates/runtime/src/trace.rs new file mode 100644 index 0000000..cbdbd27 --- /dev/null +++ b/crates/runtime/src/trace.rs @@ -0,0 +1,60 @@ +//! W3C trace-context helpers. + +use rand::RngCore; +use smol_str::SmolStr; +use std::fmt::Write; + +/// Generate a fresh W3C `traceparent`: `00---01`, with a +/// random 16-byte trace-id and 8-byte parent-id and the sampled flag set. +/// +/// Used as the fallback when an inbound request does not carry a `traceparent` +/// header, so every record on the trace stream has a valid, joinable id. +pub fn new_traceparent() -> SmolStr { + let mut rng = rand::thread_rng(); + let mut trace_id = [0u8; 16]; + let mut span_id = [0u8; 8]; + rng.fill_bytes(&mut trace_id); + rng.fill_bytes(&mut span_id); + // The all-zero value is invalid per the spec; nudge it (astronomically + // unlikely, but effectively free to guard). + if trace_id.iter().all(|&b| b == 0) { + trace_id[0] = 1; + } + if span_id.iter().all(|&b| b == 0) { + span_id[0] = 1; + } + + // "00-" + 32 + "-" + 16 + "-01" = 55 chars. + let mut s = String::with_capacity(55); + s.push_str("00-"); + for b in trace_id { + let _ = write!(s, "{:02x}", b); + } + s.push('-'); + for b in span_id { + let _ = write!(s, "{:02x}", b); + } + s.push_str("-01"); + SmolStr::new(s) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn well_formed_and_unique() { + let tp = new_traceparent(); + let parts: Vec<&str> = tp.split('-').collect(); + assert_eq!(parts.len(), 4, "four dash-separated fields: {tp}"); + assert_eq!(parts[0], "00"); + assert_eq!(parts[1].len(), 32); + assert_eq!(parts[2].len(), 16); + assert_eq!(parts[3], "01"); + assert!(parts[1].bytes().all(|c| c.is_ascii_hexdigit())); + assert!(parts[2].bytes().all(|c| c.is_ascii_hexdigit())); + assert_ne!(parts[1], "0".repeat(32)); + assert_ne!(parts[2], "0".repeat(16)); + assert_ne!(new_traceparent(), new_traceparent()); + } +} diff --git a/src/context.rs b/src/context.rs index ab93365..c06c9cb 100644 --- a/src/context.rs +++ b/src/context.rs @@ -46,7 +46,7 @@ impl PreCompiledLoader for Context { } fn load_module(&self, _id: u64) -> anyhow::Result { - unreachable!("") + unreachable!("fastedge-run loads components only, not core wasm modules") } } From 31a62146743f20619f4e2fa385279e9bc9027483 Mon Sep 17 00:00:00 2001 From: Ruslan Pislari Date: Mon, 5 Oct 2026 11:08:30 +0300 Subject: [PATCH 17/22] feat(stats): add support for excluding requests from specific User-Agents in stats --- crates/http-service/src/lib.rs | 37 ++++++++++++++++++++++++++++++++ crates/runtime/src/lib.rs | 7 ++++++ crates/runtime/src/util/stats.rs | 4 ++++ 3 files changed, 48 insertions(+) diff --git a/crates/http-service/src/lib.rs b/crates/http-service/src/lib.rs index 471d956..f7017fb 100644 --- a/crates/http-service/src/lib.rs +++ b/crates/http-service/src/lib.rs @@ -420,6 +420,13 @@ where let stats = self .context .new_stats_row(traceparent, &app_name, caller_ip, &cfg); + if is_stats_excluded_user_agent( + self.context.stats_excluded_user_agent(), + request.headers(), + ) { + tracing::debug!("stats excluded by user agent"); + stats.discard(); + } let response = match executor.execute(request, stats.clone()).await { Ok(mut response) => { @@ -466,6 +473,17 @@ where } } +/// `true` when `excluded` is set and the request's first `User-Agent` header +/// equals it byte for byte. Used to keep synthetic probes out of the stats store. +fn is_stats_excluded_user_agent(excluded: Option<&str>, headers: &HeaderMap) -> bool { + match excluded { + Some(ua) => headers + .get(http::header::USER_AGENT) + .is_some_and(|v| v.as_bytes() == ua.as_bytes()), + None => false, + } +} + /// Classify an execution error into an [`AppResult`] fail reason. /// /// Kept consistent with the fail-reason mapping in [`map_err`]. Used to record @@ -777,6 +795,25 @@ mod tests { http::Request::builder().method("GET") } + // ── Stats exclusion by User-Agent ───────────────────────────────────── + + #[test_case(Some("scanner/1.0"), Some("scanner/1.0"), true; "exact match is excluded")] + #[test_case(Some("scanner/1.0"), Some("Scanner/1.0"), false; "case differs: not excluded")] + #[test_case(Some("scanner/1.0"), Some("scanner/1.0 "), false; "trailing space: not excluded")] + #[test_case(Some("scanner/1.0"), Some("x scanner/1.0"), false; "substring: not excluded")] + #[test_case(Some("scanner/1.0"), None, false; "no user agent: not excluded")] + #[test_case(None, Some("scanner/1.0"), false; "filter disabled: not excluded")] + fn stats_excluded_user_agent(excluded: Option<&str>, ua: Option<&str>, expected: bool) { + let mut headers = http::HeaderMap::new(); + if let Some(ua) = ua { + headers.insert(http::header::USER_AGENT, ua.parse().unwrap()); + } + assert_eq!( + crate::is_stats_excluded_user_agent(excluded, &headers), + expected + ); + } + // ── Name variant: server_name header ────────────────────────────────── #[test_case("app.server.com", "/", "app"; "server_name: normal subdomain")] diff --git a/crates/runtime/src/lib.rs b/crates/runtime/src/lib.rs index 156b90c..a831b49 100644 --- a/crates/runtime/src/lib.rs +++ b/crates/runtime/src/lib.rs @@ -473,6 +473,13 @@ pub trait ContextT { cfg: &App, ) -> Arc; + /// Exact `User-Agent` value whose requests are not persisted to the stats + /// store (synthetic probes such as a security scanner). The comparison is + /// byte-exact and case-sensitive; `None` disables the filter. + fn stats_excluded_user_agent(&self) -> Option<&str> { + None + } + /// Sink for the per-request system log (see [`access_log`]). /// /// Distinct from the guest application log produced by [`make_logger`]. diff --git a/crates/runtime/src/util/stats.rs b/crates/runtime/src/util/stats.rs index ca5abd2..2cb5712 100644 --- a/crates/runtime/src/util/stats.rs +++ b/crates/runtime/src/util/stats.rs @@ -44,6 +44,10 @@ pub trait StatsVisitor: ReadStats + UserDiagStats + ExtRequestStats + Send + Syn fn get_memory_used(&self) -> u64; /// Register cdn phase fn cdn_phase(&self, phase: CdnPhase); + /// Mark the record as not to be persisted to the stats store (e.g. a + /// synthetic probe that must not count as traffic). Counters keep being + /// collected and Prometheus metrics / logs are unaffected. No-op by default. + fn discard(&self) {} } impl StatsTimer { From 8c68c1876fcbb99b0cb048daa3558e4be72c4444 Mon Sep 17 00:00:00 2001 From: Ruslan Pislari Date: Tue, 22 Sep 2026 11:19:24 +0300 Subject: [PATCH 18/22] feat(runtime): upgrade Wasmtime to 48.0.0 and refactor HTTP hooks - Upgraded to Wasmtime 48.0.0, adjusting dependencies and addressing breaking changes. - Refactored `WasiHttpHooks` implementation to align with Wasmtime 48's new `HttpHooks` design. - Streamlined epoch-related bookkeeping for HTTP requests and improved resource table handling. - Updated `Cargo.lock` with new versions and removed unused or outdated dependencies. --- CLAUDE.md | 2 +- Cargo.lock | 1009 +++-------------- Cargo.toml | 17 +- crates/http-service/Cargo.toml | 1 - crates/http-service/src/executor/http.rs | 42 +- crates/http-service/src/executor/mod.rs | 2 +- crates/http-service/src/executor/wasi_http.rs | 23 +- crates/http-service/src/lib.rs | 52 +- crates/runtime/Cargo.toml | 2 +- crates/runtime/src/lib.rs | 141 ++- crates/runtime/src/limiter.rs | 3 +- crates/runtime/src/logger.rs | 28 - crates/runtime/src/store.rs | 10 +- src/context.rs | 2 +- 14 files changed, 338 insertions(+), 996 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 417ee2c..ac78fec 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -26,7 +26,7 @@ cargo run --bin fastedge-run -- http --port 8080 --wasm ./my_app.wasm ## Repository setup gotchas - **WIT is vendored via git subtree.** `crates/reactor/wit/` contains the `FastEdge-wit` sources directly, so no `git submodule update` step is required after clone or branch switches. To sync it with upstream, run `git subtree pull --prefix=crates/reactor/wit https://github.com/G-Core/FastEdge-wit.git main --squash`. -- **Custom Wasmtime fork.** All `wasmtime-*` deps point at `github.com/G-Core/wasmtime.git#release-36.0.0`. `.cargo/config.toml` contains a commented-out `[patch]` block that redirects those to a local sibling `../wasmtime` checkout — uncomment when developing against a local Wasmtime tree. +- **Custom Wasmtime fork.** All `wasmtime-*` deps point at `github.com/G-Core/wasmtime.git#release-48.0.0`. The repo-root `.cargo/config.toml` carries a `[patch]` block redirecting `wasmtime`, `wasmtime-wasi`, `wasmtime-wasi-nn` and `wasmtime-wasi-http` to a local sibling `../wasmtime` checkout; `wasmtime-wasi-io` and `wasmtime-environ` are not patched, so a local tree must stay in sync with the pushed branch. - **Wasm components vs core modules.** `componentize_if_necessary` (in `crates/runtime/src/lib.rs`) auto-wraps core modules using the bundled Preview1 adapter (`crates/runtime/src/adapters/wasi_snapshot_preview1.reactor.wasm`). Don't replace that adapter casually — it's pinned to the Wasmtime fork. ## Architecture diff --git a/Cargo.lock b/Cargo.lock index 6b9de44..8ea2d09 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,22 +2,13 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "addr2line" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" -dependencies = [ - "gimli 0.32.3", -] - [[package]] name = "addr2line" version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59317f77929f0e679d39364702289274de2f0f0b22cbf50b2b8cff2169a0b27a" dependencies = [ - "gimli 0.33.0", + "gimli", ] [[package]] @@ -282,7 +273,7 @@ dependencies = [ "smol_str", "tokio", "tracing", - "wasmtime 48.0.2", + "wasmtime", ] [[package]] @@ -341,48 +332,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "cap-fs-ext" -version = "3.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5528f85b1e134ae811704e41ef80930f56e795923f866813255bc342cc20654" -dependencies = [ - "cap-primitives 3.4.5", - "cap-std", - "io-lifetimes 2.0.4", - "windows-sys 0.52.0", -] - -[[package]] -name = "cap-net-ext" -version = "3.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20a158160765c6a7d0d8c072a53d772e4cb243f38b04bfcf6b4939cfbe7482e7" -dependencies = [ - "cap-primitives 3.4.5", - "cap-std", - "rustix 1.1.4", - "smallvec", -] - -[[package]] -name = "cap-primitives" -version = "3.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6cf3aea8a5081171859ef57bc1606b1df6999df4f1110f8eef68b30098d1d3a" -dependencies = [ - "ambient-authority", - "fs-set-times", - "io-extras 0.18.4", - "io-lifetimes 2.0.4", - "ipnet", - "maybe-owned", - "rustix 1.1.4", - "rustix-linux-procfs", - "windows-sys 0.52.0", - "winx", -] - [[package]] name = "cap-primitives" version = "4.0.3" @@ -391,52 +340,16 @@ checksum = "8b5f74729fd2f44701d1a8eb47e906cdb3ccd9ec0f02baad85a744b791940b18" dependencies = [ "ambient-authority", "fs-set-times", - "io-extras 0.19.0", + "io-extras", "io-lifetimes 3.0.1", "ipnet", "maybe-owned", - "rustix 1.1.4", + "rustix", "rustix-linux-procfs", "windows-sys 0.61.2", "winx", ] -[[package]] -name = "cap-rand" -version = "3.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8144c22e24bbcf26ade86cb6501a0916c46b7e4787abdb0045a467eb1645a1d" -dependencies = [ - "ambient-authority", - "rand 0.8.7", -] - -[[package]] -name = "cap-std" -version = "3.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6dc3090992a735d23219de5c204927163d922f42f575a0189b005c62d37549a" -dependencies = [ - "cap-primitives 3.4.5", - "io-extras 0.18.4", - "io-lifetimes 2.0.4", - "rustix 1.1.4", -] - -[[package]] -name = "cap-time-ext" -version = "3.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "def102506ce40c11710a9b16e614af0cde8e76ae51b1f48c04b8d79f4b671a80" -dependencies = [ - "ambient-authority", - "cap-primitives 3.4.5", - "iana-time-zone", - "once_cell", - "rustix 1.1.4", - "winx", -] - [[package]] name = "cc" version = "1.4.0" @@ -554,7 +467,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" dependencies = [ "lazy_static", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -614,94 +527,54 @@ dependencies = [ "libc", ] -[[package]] -name = "cranelift-assembler-x64" -version = "0.123.12" -source = "git+https://github.com/G-Core/wasmtime.git?branch=release-36.0.0#62166f65ac52a1f164419097a2a533ff50e4d07b" -dependencies = [ - "cranelift-assembler-x64-meta 0.123.12", -] - [[package]] name = "cranelift-assembler-x64" version = "0.135.2" dependencies = [ - "cranelift-assembler-x64-meta 0.135.2", -] - -[[package]] -name = "cranelift-assembler-x64-meta" -version = "0.123.12" -source = "git+https://github.com/G-Core/wasmtime.git?branch=release-36.0.0#62166f65ac52a1f164419097a2a533ff50e4d07b" -dependencies = [ - "cranelift-srcgen 0.123.12", + "cranelift-assembler-x64-meta", ] [[package]] name = "cranelift-assembler-x64-meta" version = "0.135.2" dependencies = [ - "cranelift-srcgen 0.135.2", + "cranelift-srcgen", ] [[package]] name = "cranelift-bforest" -version = "0.123.12" -source = "git+https://github.com/G-Core/wasmtime.git?branch=release-36.0.0#62166f65ac52a1f164419097a2a533ff50e4d07b" +version = "0.135.2" dependencies = [ - "cranelift-entity 0.123.12", + "cranelift-entity 0.135.2", + "wasmtime-internal-core 48.0.2", ] [[package]] name = "cranelift-bforest" version = "0.135.2" +source = "git+https://github.com/G-Core/wasmtime.git?branch=release-48.0.0#ef281a08e10646e8b0592d0081752fef23cab585" dependencies = [ - "cranelift-entity 0.135.2", - "wasmtime-internal-core", + "cranelift-entity 0.135.2 (git+https://github.com/G-Core/wasmtime.git?branch=release-48.0.0)", + "wasmtime-internal-core 48.0.2 (git+https://github.com/G-Core/wasmtime.git?branch=release-48.0.0)", ] [[package]] name = "cranelift-bitset" -version = "0.123.12" -source = "git+https://github.com/G-Core/wasmtime.git?branch=release-36.0.0#62166f65ac52a1f164419097a2a533ff50e4d07b" +version = "0.135.2" dependencies = [ "serde", "serde_derive", + "wasmtime-internal-core 48.0.2", ] [[package]] name = "cranelift-bitset" version = "0.135.2" +source = "git+https://github.com/G-Core/wasmtime.git?branch=release-48.0.0#ef281a08e10646e8b0592d0081752fef23cab585" dependencies = [ "serde", "serde_derive", - "wasmtime-internal-core", -] - -[[package]] -name = "cranelift-codegen" -version = "0.123.12" -source = "git+https://github.com/G-Core/wasmtime.git?branch=release-36.0.0#62166f65ac52a1f164419097a2a533ff50e4d07b" -dependencies = [ - "bumpalo", - "cranelift-assembler-x64 0.123.12", - "cranelift-bforest 0.123.12", - "cranelift-bitset 0.123.12", - "cranelift-codegen-meta 0.123.12", - "cranelift-codegen-shared 0.123.12", - "cranelift-control 0.123.12", - "cranelift-entity 0.123.12", - "cranelift-isle 0.123.12", - "gimli 0.32.3", - "hashbrown 0.15.5", - "log", - "pulley-interpreter 36.0.12", - "regalloc2 0.12.2", - "rustc-hash", - "serde", - "smallvec", - "target-lexicon", - "wasmtime-internal-math", + "wasmtime-internal-core 48.0.2 (git+https://github.com/G-Core/wasmtime.git?branch=release-48.0.0)", ] [[package]] @@ -709,70 +582,45 @@ name = "cranelift-codegen" version = "0.135.2" dependencies = [ "bumpalo", - "cranelift-assembler-x64 0.135.2", + "cranelift-assembler-x64", "cranelift-bforest 0.135.2", "cranelift-bitset 0.135.2", - "cranelift-codegen-meta 0.135.2", - "cranelift-codegen-shared 0.135.2", - "cranelift-control 0.135.2", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", "cranelift-entity 0.135.2", - "cranelift-isle 0.135.2", - "gimli 0.33.0", + "cranelift-isle", + "gimli", "hashbrown 0.17.1", "libm", "log", "postcard", - "pulley-interpreter 48.0.2", - "regalloc2 0.15.2", + "pulley-interpreter", + "regalloc2", "rustc-hash", "serde", "serde_derive", "sha2", "smallvec", "target-lexicon", - "wasmtime-internal-core", -] - -[[package]] -name = "cranelift-codegen-meta" -version = "0.123.12" -source = "git+https://github.com/G-Core/wasmtime.git?branch=release-36.0.0#62166f65ac52a1f164419097a2a533ff50e4d07b" -dependencies = [ - "cranelift-assembler-x64-meta 0.123.12", - "cranelift-codegen-shared 0.123.12", - "cranelift-srcgen 0.123.12", - "heck", - "pulley-interpreter 36.0.12", + "wasmtime-internal-core 48.0.2", ] [[package]] name = "cranelift-codegen-meta" version = "0.135.2" dependencies = [ - "cranelift-assembler-x64-meta 0.135.2", - "cranelift-codegen-shared 0.135.2", - "cranelift-srcgen 0.135.2", + "cranelift-assembler-x64-meta", + "cranelift-codegen-shared", + "cranelift-srcgen", "heck", - "pulley-interpreter 48.0.2", + "pulley-interpreter", ] -[[package]] -name = "cranelift-codegen-shared" -version = "0.123.12" -source = "git+https://github.com/G-Core/wasmtime.git?branch=release-36.0.0#62166f65ac52a1f164419097a2a533ff50e4d07b" - [[package]] name = "cranelift-codegen-shared" version = "0.135.2" -[[package]] -name = "cranelift-control" -version = "0.123.12" -source = "git+https://github.com/G-Core/wasmtime.git?branch=release-36.0.0#62166f65ac52a1f164419097a2a533ff50e4d07b" -dependencies = [ - "arbitrary", -] - [[package]] name = "cranelift-control" version = "0.135.2" @@ -782,79 +630,49 @@ dependencies = [ [[package]] name = "cranelift-entity" -version = "0.123.12" -source = "git+https://github.com/G-Core/wasmtime.git?branch=release-36.0.0#62166f65ac52a1f164419097a2a533ff50e4d07b" +version = "0.135.2" dependencies = [ - "cranelift-bitset 0.123.12", + "cranelift-bitset 0.135.2", "serde", "serde_derive", + "wasmtime-internal-core 48.0.2", ] [[package]] name = "cranelift-entity" version = "0.135.2" +source = "git+https://github.com/G-Core/wasmtime.git?branch=release-48.0.0#ef281a08e10646e8b0592d0081752fef23cab585" dependencies = [ - "cranelift-bitset 0.135.2", + "cranelift-bitset 0.135.2 (git+https://github.com/G-Core/wasmtime.git?branch=release-48.0.0)", "serde", "serde_derive", - "wasmtime-internal-core", -] - -[[package]] -name = "cranelift-frontend" -version = "0.123.12" -source = "git+https://github.com/G-Core/wasmtime.git?branch=release-36.0.0#62166f65ac52a1f164419097a2a533ff50e4d07b" -dependencies = [ - "cranelift-codegen 0.123.12", - "log", - "smallvec", - "target-lexicon", + "wasmtime-internal-core 48.0.2 (git+https://github.com/G-Core/wasmtime.git?branch=release-48.0.0)", ] [[package]] name = "cranelift-frontend" version = "0.135.2" dependencies = [ - "cranelift-codegen 0.135.2", + "cranelift-codegen", "hashbrown 0.17.1", "log", "smallvec", "target-lexicon", ] -[[package]] -name = "cranelift-isle" -version = "0.123.12" -source = "git+https://github.com/G-Core/wasmtime.git?branch=release-36.0.0#62166f65ac52a1f164419097a2a533ff50e4d07b" - [[package]] name = "cranelift-isle" version = "0.135.2" -[[package]] -name = "cranelift-native" -version = "0.123.12" -source = "git+https://github.com/G-Core/wasmtime.git?branch=release-36.0.0#62166f65ac52a1f164419097a2a533ff50e4d07b" -dependencies = [ - "cranelift-codegen 0.123.12", - "libc", - "target-lexicon", -] - [[package]] name = "cranelift-native" version = "0.135.2" dependencies = [ - "cranelift-codegen 0.135.2", + "cranelift-codegen", "libc", "target-lexicon", ] -[[package]] -name = "cranelift-srcgen" -version = "0.123.12" -source = "git+https://github.com/G-Core/wasmtime.git?branch=release-36.0.0#62166f65ac52a1f164419097a2a533ff50e4d07b" - [[package]] name = "cranelift-srcgen" version = "0.135.2" @@ -1094,12 +912,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "fallible-iterator" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" - [[package]] name = "fancy-regex" version = "0.13.0" @@ -1136,7 +948,7 @@ dependencies = [ "tempfile", "tokio", "utils", - "wasmtime 48.0.2", + "wasmtime", ] [[package]] @@ -1145,17 +957,6 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" -[[package]] -name = "fd-lock" -version = "4.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" -dependencies = [ - "cfg-if", - "rustix 1.1.4", - "windows-sys 0.52.0", -] - [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -1174,12 +975,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - [[package]] name = "foldhash" version = "0.2.0" @@ -1217,8 +1012,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a" dependencies = [ "io-lifetimes 2.0.4", - "rustix 1.1.4", - "windows-sys 0.52.0", + "rustix", + "windows-sys 0.59.0", ] [[package]] @@ -1605,17 +1400,6 @@ dependencies = [ "rand_core 0.10.1", ] -[[package]] -name = "gimli" -version = "0.32.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" -dependencies = [ - "fallible-iterator", - "indexmap", - "stable_deref_trait", -] - [[package]] name = "gimli" version = "0.33.0" @@ -1662,23 +1446,13 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash 0.1.5", - "serde", -] - [[package]] name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ - "foldhash 0.2.0", + "foldhash", ] [[package]] @@ -1687,7 +1461,7 @@ version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" dependencies = [ - "foldhash 0.2.0", + "foldhash", "serde", "serde_core", ] @@ -1782,9 +1556,8 @@ dependencies = [ "tracing", "tracing-test", "utils", - "wasi-common", - "wasmtime 48.0.2", - "wasmtime-wasi 48.0.2", + "wasmtime", + "wasmtime-wasi", "wasmtime-wasi-http", "wasmtime-wasi-nn", ] @@ -2010,16 +1783,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "io-extras" -version = "0.18.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2285ddfe3054097ef4b2fe909ef8c3bcd1ea52a8f0d274416caebeef39f04a65" -dependencies = [ - "io-lifetimes 2.0.4", - "windows-sys 0.52.0", -] - [[package]] name = "io-extras" version = "0.19.0" @@ -2141,7 +1904,7 @@ dependencies = [ "smol_str", "tokio", "tracing", - "wasmtime 48.0.2", + "wasmtime", ] [[package]] @@ -2193,12 +1956,6 @@ dependencies = [ "libc", ] -[[package]] -name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -2226,15 +1983,6 @@ version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" -[[package]] -name = "mach2" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" -dependencies = [ - "libc", -] - [[package]] name = "mach2" version = "0.6.0" @@ -2268,7 +2016,7 @@ version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" dependencies = [ - "rustix 1.1.4", + "rustix", ] [[package]] @@ -2481,18 +2229,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "object" -version = "0.37.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" -dependencies = [ - "crc32fast", - "hashbrown 0.15.5", - "indexmap", - "memchr", -] - [[package]] name = "object" version = "0.39.1" @@ -2746,35 +2482,14 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "pulley-interpreter" -version = "36.0.12" -source = "git+https://github.com/G-Core/wasmtime.git?branch=release-36.0.0#62166f65ac52a1f164419097a2a533ff50e4d07b" -dependencies = [ - "cranelift-bitset 0.123.12", - "log", - "pulley-macros 36.0.12", - "wasmtime-internal-math", -] - [[package]] name = "pulley-interpreter" version = "48.0.2" dependencies = [ "cranelift-bitset 0.135.2", "log", - "pulley-macros 48.0.2", - "wasmtime-internal-core", -] - -[[package]] -name = "pulley-macros" -version = "36.0.12" -source = "git+https://github.com/G-Core/wasmtime.git?branch=release-36.0.0#62166f65ac52a1f164419097a2a533ff50e4d07b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", + "pulley-macros", + "wasmtime-internal-core 48.0.2", ] [[package]] @@ -2835,9 +2550,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -2856,9 +2571,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.10.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" dependencies = [ "chacha20", "getrandom 0.4.3", @@ -2961,7 +2676,7 @@ dependencies = [ name = "reactor" version = "0.21.0" dependencies = [ - "wasmtime 48.0.2", + "wasmtime", ] [[package]] @@ -3020,20 +2735,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "regalloc2" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5216b1837de2149f8bc8e6d5f88a9326b63b8c836ed58ce4a0a29ec736a59734" -dependencies = [ - "allocator-api2", - "bumpalo", - "hashbrown 0.15.5", - "log", - "rustc-hash", - "smallvec", -] - [[package]] name = "regalloc2" version = "0.15.2" @@ -3105,9 +2806,10 @@ dependencies = [ "claims", "http", "http-backend", + "http-body-util", "key-value-store", "moka", - "rand 0.8.7", + "rand 0.8.8", "secret", "serde", "serde_json", @@ -3116,12 +2818,11 @@ dependencies = [ "tokio-util", "tracing", "utils", - "wasi-common", - "wasmtime 48.0.2", - "wasmtime-environ 36.0.12", - "wasmtime-wasi 48.0.2", + "wasmtime", + "wasmtime-environ 48.0.2 (git+https://github.com/G-Core/wasmtime.git?branch=release-48.0.0)", + "wasmtime-wasi", "wasmtime-wasi-http", - "wasmtime-wasi-io 36.0.12", + "wasmtime-wasi-io 48.0.2 (git+https://github.com/G-Core/wasmtime.git?branch=release-48.0.0)", "wasmtime-wasi-nn", "wat", "wit-component 0.246.2", @@ -3139,19 +2840,6 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" -[[package]] -name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags 2.13.1", - "errno", - "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.52.0", -] - [[package]] name = "rustix" version = "1.1.4" @@ -3161,7 +2849,7 @@ dependencies = [ "bitflags 2.13.1", "errno", "libc", - "linux-raw-sys 0.12.1", + "linux-raw-sys", "windows-sys 0.61.2", ] @@ -3172,16 +2860,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fc84bf7e9aa16c4f2c758f27412dc9841341e16aa682d9c7ac308fe3ee12056" dependencies = [ "once_cell", - "rustix 1.1.4", + "rustix", ] [[package]] name = "rustls" -version = "0.22.4" +version = "0.23.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" dependencies = [ "log", + "once_cell", "ring", "rustls-pki-types", "rustls-webpki", @@ -3200,9 +2889,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.102.8" +version = "0.103.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" dependencies = [ "ring", "rustls-pki-types", @@ -3547,22 +3236,6 @@ dependencies = [ "walkdir", ] -[[package]] -name = "system-interface" -version = "0.27.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc4592f674ce18521c2a81483873a49596655b179f71c5e05d10c1fe66c78745" -dependencies = [ - "bitflags 2.13.1", - "cap-fs-ext", - "cap-std", - "fd-lock", - "io-lifetimes 2.0.4", - "rustix 0.38.44", - "windows-sys 0.52.0", - "winx", -] - [[package]] name = "tagptr" version = "0.2.0" @@ -3584,7 +3257,7 @@ dependencies = [ "fastrand", "getrandom 0.4.3", "once_cell", - "rustix 1.1.4", + "rustix", "windows-sys 0.61.2", ] @@ -3729,12 +3402,11 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.25.0" +version = "0.26.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" +checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67" dependencies = [ "rustls", - "rustls-pki-types", "tokio", ] @@ -3853,7 +3525,6 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ - "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -4072,30 +3743,6 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" -[[package]] -name = "wasi-common" -version = "36.0.12" -source = "git+https://github.com/G-Core/wasmtime.git?branch=release-36.0.0#62166f65ac52a1f164419097a2a533ff50e4d07b" -dependencies = [ - "anyhow", - "bitflags 2.13.1", - "cap-fs-ext", - "cap-rand", - "cap-std", - "cap-time-ext", - "fs-set-times", - "io-extras 0.18.4", - "io-lifetimes 2.0.4", - "log", - "rustix 1.1.4", - "system-interface", - "thiserror 2.0.19", - "tracing", - "wasmtime 36.0.12", - "wiggle 36.0.12", - "windows-sys 0.60.2", -] - [[package]] name = "wasip2" version = "1.0.4+wasi-0.2.12" @@ -4167,16 +3814,6 @@ dependencies = [ "wat", ] -[[package]] -name = "wasm-encoder" -version = "0.236.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "724fccfd4f3c24b7e589d333fc0429c68042897a7e8a5f8694f31792471841e7" -dependencies = [ - "leb128fmt", - "wasmparser 0.236.1", -] - [[package]] name = "wasm-encoder" version = "0.246.2" @@ -4221,19 +3858,6 @@ dependencies = [ "wasmparser 0.254.0", ] -[[package]] -name = "wasmparser" -version = "0.236.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9b1e81f3eb254cf7404a82cee6926a4a3ccc5aad80cc3d43608a070c67aa1d7" -dependencies = [ - "bitflags 2.13.1", - "hashbrown 0.15.5", - "indexmap", - "semver", - "serde", -] - [[package]] name = "wasmparser" version = "0.246.2" @@ -4259,17 +3883,6 @@ dependencies = [ "serde", ] -[[package]] -name = "wasmprinter" -version = "0.236.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2df225df06a6df15b46e3f73ca066ff92c2e023670969f7d50ce7d5e695abbb1" -dependencies = [ - "anyhow", - "termcolor", - "wasmparser 0.236.1", -] - [[package]] name = "wasmprinter" version = "0.254.0" @@ -4281,57 +3894,11 @@ dependencies = [ "wasmparser 0.254.0", ] -[[package]] -name = "wasmtime" -version = "36.0.12" -source = "git+https://github.com/G-Core/wasmtime.git?branch=release-36.0.0#62166f65ac52a1f164419097a2a533ff50e4d07b" -dependencies = [ - "addr2line 0.25.1", - "anyhow", - "async-trait", - "bitflags 2.13.1", - "bumpalo", - "cc", - "cfg-if", - "encoding_rs", - "hashbrown 0.15.5", - "indexmap", - "libc", - "log", - "mach2 0.4.3", - "memfd", - "object 0.37.3", - "once_cell", - "postcard", - "pulley-interpreter 36.0.12", - "rustix 1.1.4", - "semver", - "serde", - "serde_derive", - "smallvec", - "target-lexicon", - "wasmparser 0.236.1", - "wasmtime-environ 36.0.12", - "wasmtime-internal-asm-macros", - "wasmtime-internal-component-macro 36.0.12", - "wasmtime-internal-component-util 36.0.12", - "wasmtime-internal-cranelift 36.0.12", - "wasmtime-internal-fiber 36.0.12", - "wasmtime-internal-jit-debug 36.0.12", - "wasmtime-internal-jit-icache-coherence 36.0.12", - "wasmtime-internal-math", - "wasmtime-internal-slab", - "wasmtime-internal-unwinder 36.0.12", - "wasmtime-internal-versioned-export-macros 36.0.12", - "wasmtime-internal-winch", - "windows-sys 0.60.2", -] - [[package]] name = "wasmtime" version = "48.0.2" dependencies = [ - "addr2line 0.26.1", + "addr2line", "async-trait", "bitflags 2.13.1", "bumpalo", @@ -4339,18 +3906,18 @@ dependencies = [ "encoding_rs", "futures", "fxprof-processed-profile", - "gimli 0.33.0", + "gimli", "ittapi", "libc", "log", - "mach2 0.6.0", + "mach2", "memfd", - "object 0.39.1", + "object", "once_cell", "postcard", - "pulley-interpreter 48.0.2", + "pulley-interpreter", "rayon", - "rustix 1.1.4", + "rustix", "semver", "serde", "serde_derive", @@ -4363,15 +3930,15 @@ dependencies = [ "wasmparser 0.254.0", "wasmtime-environ 48.0.2", "wasmtime-internal-cache", - "wasmtime-internal-component-macro 48.0.2", - "wasmtime-internal-component-util 48.0.2", - "wasmtime-internal-core", - "wasmtime-internal-cranelift 48.0.2", - "wasmtime-internal-fiber 48.0.2", - "wasmtime-internal-jit-debug 48.0.2", - "wasmtime-internal-jit-icache-coherence 48.0.2", - "wasmtime-internal-unwinder 48.0.2", - "wasmtime-internal-versioned-export-macros 48.0.2", + "wasmtime-internal-component-macro", + "wasmtime-internal-component-util", + "wasmtime-internal-core 48.0.2", + "wasmtime-internal-cranelift", + "wasmtime-internal-fiber", + "wasmtime-internal-jit-debug", + "wasmtime-internal-jit-icache-coherence", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", "wat", "windows-sys 0.61.2", "wit-parser 0.254.0", @@ -4379,63 +3946,55 @@ dependencies = [ [[package]] name = "wasmtime-environ" -version = "36.0.12" -source = "git+https://github.com/G-Core/wasmtime.git?branch=release-36.0.0#62166f65ac52a1f164419097a2a533ff50e4d07b" +version = "48.0.2" dependencies = [ "anyhow", - "cranelift-bitset 0.123.12", - "cranelift-entity 0.123.12", - "gimli 0.32.3", + "cpp_demangle", + "cranelift-bforest 0.135.2", + "cranelift-bitset 0.135.2", + "cranelift-entity 0.135.2", + "gimli", + "hashbrown 0.17.1", "indexmap", "log", - "object 0.37.3", + "object", "postcard", + "rustc-demangle", "semver", "serde", "serde_derive", + "sha2", "smallvec", "target-lexicon", - "wasm-encoder 0.236.1", - "wasmparser 0.236.1", - "wasmprinter 0.236.1", - "wasmtime-internal-component-util 36.0.12", + "wasm-encoder 0.254.0", + "wasmparser 0.254.0", + "wasmprinter", + "wasmtime-internal-component-util", + "wasmtime-internal-core 48.0.2", ] [[package]] name = "wasmtime-environ" version = "48.0.2" +source = "git+https://github.com/G-Core/wasmtime.git?branch=release-48.0.0#ef281a08e10646e8b0592d0081752fef23cab585" dependencies = [ "anyhow", - "cpp_demangle", - "cranelift-bforest 0.135.2", - "cranelift-bitset 0.135.2", - "cranelift-entity 0.135.2", - "gimli 0.33.0", + "cranelift-bforest 0.135.2 (git+https://github.com/G-Core/wasmtime.git?branch=release-48.0.0)", + "cranelift-bitset 0.135.2 (git+https://github.com/G-Core/wasmtime.git?branch=release-48.0.0)", + "cranelift-entity 0.135.2 (git+https://github.com/G-Core/wasmtime.git?branch=release-48.0.0)", + "gimli", "hashbrown 0.17.1", "indexmap", "log", - "object 0.39.1", + "object", "postcard", - "rustc-demangle", - "semver", "serde", "serde_derive", "sha2", "smallvec", "target-lexicon", - "wasm-encoder 0.254.0", "wasmparser 0.254.0", - "wasmprinter 0.254.0", - "wasmtime-internal-component-util 48.0.2", - "wasmtime-internal-core", -] - -[[package]] -name = "wasmtime-internal-asm-macros" -version = "36.0.12" -source = "git+https://github.com/G-Core/wasmtime.git?branch=release-36.0.0#62166f65ac52a1f164419097a2a533ff50e4d07b" -dependencies = [ - "cfg-if", + "wasmtime-internal-core 48.0.2 (git+https://github.com/G-Core/wasmtime.git?branch=release-48.0.0)", ] [[package]] @@ -4446,7 +4005,7 @@ dependencies = [ "directories-next", "log", "postcard", - "rustix 1.1.4", + "rustix", "serde", "serde_derive", "sha2", @@ -4456,20 +4015,6 @@ dependencies = [ "zstd", ] -[[package]] -name = "wasmtime-internal-component-macro" -version = "36.0.12" -source = "git+https://github.com/G-Core/wasmtime.git?branch=release-36.0.0#62166f65ac52a1f164419097a2a533ff50e4d07b" -dependencies = [ - "anyhow", - "proc-macro2", - "quote", - "syn 2.0.119", - "wasmtime-internal-component-util 36.0.12", - "wasmtime-internal-wit-bindgen 36.0.12", - "wit-parser 0.236.1", -] - [[package]] name = "wasmtime-internal-component-macro" version = "48.0.2" @@ -4478,16 +4023,11 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.119", - "wasmtime-internal-component-util 48.0.2", - "wasmtime-internal-wit-bindgen 48.0.2", + "wasmtime-internal-component-util", + "wasmtime-internal-wit-bindgen", "wit-parser 0.254.0", ] -[[package]] -name = "wasmtime-internal-component-util" -version = "36.0.12" -source = "git+https://github.com/G-Core/wasmtime.git?branch=release-36.0.0#62166f65ac52a1f164419097a2a533ff50e4d07b" - [[package]] name = "wasmtime-internal-component-util" version = "48.0.2" @@ -4503,68 +4043,37 @@ dependencies = [ ] [[package]] -name = "wasmtime-internal-cranelift" -version = "36.0.12" -source = "git+https://github.com/G-Core/wasmtime.git?branch=release-36.0.0#62166f65ac52a1f164419097a2a533ff50e4d07b" +name = "wasmtime-internal-core" +version = "48.0.2" +source = "git+https://github.com/G-Core/wasmtime.git?branch=release-48.0.0#ef281a08e10646e8b0592d0081752fef23cab585" dependencies = [ - "anyhow", - "cfg-if", - "cranelift-codegen 0.123.12", - "cranelift-control 0.123.12", - "cranelift-entity 0.123.12", - "cranelift-frontend 0.123.12", - "cranelift-native 0.123.12", - "gimli 0.32.3", - "itertools 0.14.0", - "log", - "object 0.37.3", - "pulley-interpreter 36.0.12", - "smallvec", - "target-lexicon", - "thiserror 2.0.19", - "wasmparser 0.236.1", - "wasmtime-environ 36.0.12", - "wasmtime-internal-math", - "wasmtime-internal-versioned-export-macros 36.0.12", + "hashbrown 0.17.1", + "libm", + "serde", ] [[package]] name = "wasmtime-internal-cranelift" version = "48.0.2" dependencies = [ - "cranelift-codegen 0.135.2", - "cranelift-control 0.135.2", + "cranelift-codegen", + "cranelift-control", "cranelift-entity 0.135.2", - "cranelift-frontend 0.135.2", - "cranelift-native 0.135.2", - "gimli 0.33.0", + "cranelift-frontend", + "cranelift-native", + "gimli", "itertools 0.14.0", "log", - "object 0.39.1", - "pulley-interpreter 48.0.2", + "object", + "pulley-interpreter", "smallvec", "target-lexicon", "thiserror 2.0.19", "wasmparser 0.254.0", "wasmtime-environ 48.0.2", - "wasmtime-internal-core", - "wasmtime-internal-unwinder 48.0.2", - "wasmtime-internal-versioned-export-macros 48.0.2", -] - -[[package]] -name = "wasmtime-internal-fiber" -version = "36.0.12" -source = "git+https://github.com/G-Core/wasmtime.git?branch=release-36.0.0#62166f65ac52a1f164419097a2a533ff50e4d07b" -dependencies = [ - "anyhow", - "cc", - "cfg-if", - "libc", - "rustix 1.1.4", - "wasmtime-internal-asm-macros", - "wasmtime-internal-versioned-export-macros 36.0.12", - "windows-sys 0.60.2", + "wasmtime-internal-core 48.0.2", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", ] [[package]] @@ -4573,40 +4082,20 @@ version = "48.0.2" dependencies = [ "cc", "libc", - "rustix 1.1.4", + "rustix", "wasmtime-environ 48.0.2", - "wasmtime-internal-versioned-export-macros 48.0.2", + "wasmtime-internal-versioned-export-macros", "windows-sys 0.61.2", ] -[[package]] -name = "wasmtime-internal-jit-debug" -version = "36.0.12" -source = "git+https://github.com/G-Core/wasmtime.git?branch=release-36.0.0#62166f65ac52a1f164419097a2a533ff50e4d07b" -dependencies = [ - "cc", - "wasmtime-internal-versioned-export-macros 36.0.12", -] - [[package]] name = "wasmtime-internal-jit-debug" version = "48.0.2" dependencies = [ "cc", - "object 0.39.1", - "rustix 1.1.4", - "wasmtime-internal-versioned-export-macros 48.0.2", -] - -[[package]] -name = "wasmtime-internal-jit-icache-coherence" -version = "36.0.12" -source = "git+https://github.com/G-Core/wasmtime.git?branch=release-36.0.0#62166f65ac52a1f164419097a2a533ff50e4d07b" -dependencies = [ - "anyhow", - "cfg-if", - "libc", - "windows-sys 0.60.2", + "object", + "rustix", + "wasmtime-internal-versioned-export-macros", ] [[package]] @@ -4614,54 +4103,20 @@ name = "wasmtime-internal-jit-icache-coherence" version = "48.0.2" dependencies = [ "libc", - "wasmtime-internal-core", + "wasmtime-internal-core 48.0.2", "windows-sys 0.61.2", ] -[[package]] -name = "wasmtime-internal-math" -version = "36.0.12" -source = "git+https://github.com/G-Core/wasmtime.git?branch=release-36.0.0#62166f65ac52a1f164419097a2a533ff50e4d07b" -dependencies = [ - "libm", -] - -[[package]] -name = "wasmtime-internal-slab" -version = "36.0.12" -source = "git+https://github.com/G-Core/wasmtime.git?branch=release-36.0.0#62166f65ac52a1f164419097a2a533ff50e4d07b" - -[[package]] -name = "wasmtime-internal-unwinder" -version = "36.0.12" -source = "git+https://github.com/G-Core/wasmtime.git?branch=release-36.0.0#62166f65ac52a1f164419097a2a533ff50e4d07b" -dependencies = [ - "anyhow", - "cfg-if", - "log", - "object 0.37.3", -] - [[package]] name = "wasmtime-internal-unwinder" version = "48.0.2" dependencies = [ - "cranelift-codegen 0.135.2", + "cranelift-codegen", "log", - "object 0.39.1", + "object", "wasmtime-environ 48.0.2", ] -[[package]] -name = "wasmtime-internal-versioned-export-macros" -version = "36.0.12" -source = "git+https://github.com/G-Core/wasmtime.git?branch=release-36.0.0#62166f65ac52a1f164419097a2a533ff50e4d07b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "wasmtime-internal-versioned-export-macros" version = "48.0.2" @@ -4671,34 +4126,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "wasmtime-internal-winch" -version = "36.0.12" -source = "git+https://github.com/G-Core/wasmtime.git?branch=release-36.0.0#62166f65ac52a1f164419097a2a533ff50e4d07b" -dependencies = [ - "anyhow", - "cranelift-codegen 0.123.12", - "gimli 0.32.3", - "object 0.37.3", - "target-lexicon", - "wasmparser 0.236.1", - "wasmtime-environ 36.0.12", - "wasmtime-internal-cranelift 36.0.12", - "winch-codegen", -] - -[[package]] -name = "wasmtime-internal-wit-bindgen" -version = "36.0.12" -source = "git+https://github.com/G-Core/wasmtime.git?branch=release-36.0.0#62166f65ac52a1f164419097a2a533ff50e4d07b" -dependencies = [ - "anyhow", - "bitflags 2.13.1", - "heck", - "indexmap", - "wit-parser 0.236.1", -] - [[package]] name = "wasmtime-internal-wit-bindgen" version = "48.0.2" @@ -4711,35 +4138,6 @@ dependencies = [ "wit-parser 0.254.0", ] -[[package]] -name = "wasmtime-wasi" -version = "36.0.12" -source = "git+https://github.com/G-Core/wasmtime.git?branch=release-36.0.0#62166f65ac52a1f164419097a2a533ff50e4d07b" -dependencies = [ - "anyhow", - "async-trait", - "bitflags 2.13.1", - "bytes", - "cap-fs-ext", - "cap-net-ext", - "cap-rand", - "cap-std", - "cap-time-ext", - "fs-set-times", - "futures", - "io-extras 0.18.4", - "io-lifetimes 2.0.4", - "rustix 1.1.4", - "system-interface", - "thiserror 2.0.19", - "tokio", - "tracing", - "url", - "wasmtime 36.0.12", - "wasmtime-wasi-io 36.0.12", - "windows-sys 0.60.2", -] - [[package]] name = "wasmtime-wasi" version = "48.0.2" @@ -4747,28 +4145,27 @@ dependencies = [ "async-trait", "bitflags 2.13.1", "bytes", - "cap-primitives 4.0.3", + "cap-primitives", "futures", - "rand 0.10.2", - "rustix 1.1.4", + "rand 0.10.3", + "rustix", "rustix-linux-procfs", "thiserror 2.0.19", "tokio", "tracing", "url", - "wasmtime 48.0.2", + "wasmtime", "wasmtime-wasi-io 48.0.2", - "wiggle 48.0.2", + "wiggle", "windows-sys 0.61.2", "winx", ] [[package]] name = "wasmtime-wasi-http" -version = "36.0.12" -source = "git+https://github.com/G-Core/wasmtime.git?branch=release-36.0.0#62166f65ac52a1f164419097a2a533ff50e4d07b" +version = "48.0.2" +source = "git+https://github.com/G-Core/wasmtime.git?branch=release-48.0.0#ef281a08e10646e8b0592d0081752fef23cab585" dependencies = [ - "anyhow", "async-trait", "bytes", "futures", @@ -4776,37 +4173,38 @@ dependencies = [ "http-body", "http-body-util", "hyper", + "pin-project-lite", "rustls", "tokio", "tokio-rustls", "tracing", - "wasmtime 36.0.12", - "wasmtime-wasi 36.0.12", - "wasmtime-wasi-io 36.0.12", - "webpki-roots 0.26.11", + "wasmtime", + "wasmtime-wasi", + "wasmtime-wasi-io 48.0.2 (git+https://github.com/G-Core/wasmtime.git?branch=release-48.0.0)", + "webpki-roots", ] [[package]] name = "wasmtime-wasi-io" -version = "36.0.12" -source = "git+https://github.com/G-Core/wasmtime.git?branch=release-36.0.0#62166f65ac52a1f164419097a2a533ff50e4d07b" +version = "48.0.2" dependencies = [ - "anyhow", "async-trait", "bytes", "futures", - "wasmtime 36.0.12", + "tracing", + "wasmtime", ] [[package]] name = "wasmtime-wasi-io" version = "48.0.2" +source = "git+https://github.com/G-Core/wasmtime.git?branch=release-48.0.0#ef281a08e10646e8b0592d0081752fef23cab585" dependencies = [ "async-trait", "bytes", "futures", "tracing", - "wasmtime 48.0.2", + "wasmtime", ] [[package]] @@ -4820,8 +4218,8 @@ dependencies = [ "thiserror 2.0.19", "tracing", "walkdir", - "wasmtime 48.0.2", - "wiggle 48.0.2", + "wasmtime", + "wiggle", "windows", ] @@ -4856,15 +4254,6 @@ dependencies = [ "wast 254.0.0", ] -[[package]] -name = "webpki-roots" -version = "0.26.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" -dependencies = [ - "webpki-roots 1.0.9", -] - [[package]] name = "webpki-roots" version = "1.0.9" @@ -4874,20 +4263,6 @@ dependencies = [ "rustls-pki-types", ] -[[package]] -name = "wiggle" -version = "36.0.12" -source = "git+https://github.com/G-Core/wasmtime.git?branch=release-36.0.0#62166f65ac52a1f164419097a2a533ff50e4d07b" -dependencies = [ - "anyhow", - "async-trait", - "bitflags 2.13.1", - "thiserror 2.0.19", - "tracing", - "wasmtime 36.0.12", - "wiggle-macro 36.0.12", -] - [[package]] name = "wiggle" version = "48.0.2" @@ -4895,22 +4270,9 @@ dependencies = [ "bitflags 2.13.1", "thiserror 2.0.19", "tracing", - "wasmtime 48.0.2", + "wasmtime", "wasmtime-environ 48.0.2", - "wiggle-macro 48.0.2", -] - -[[package]] -name = "wiggle-generate" -version = "36.0.12" -source = "git+https://github.com/G-Core/wasmtime.git?branch=release-36.0.0#62166f65ac52a1f164419097a2a533ff50e4d07b" -dependencies = [ - "anyhow", - "heck", - "proc-macro2", - "quote", - "syn 2.0.119", - "witx", + "wiggle-macro", ] [[package]] @@ -4925,17 +4287,6 @@ dependencies = [ "witx", ] -[[package]] -name = "wiggle-macro" -version = "36.0.12" -source = "git+https://github.com/G-Core/wasmtime.git?branch=release-36.0.0#62166f65ac52a1f164419097a2a533ff50e4d07b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", - "wiggle-generate 36.0.12", -] - [[package]] name = "wiggle-macro" version = "48.0.2" @@ -4943,7 +4294,7 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.119", - "wiggle-generate 48.0.2", + "wiggle-generate", ] [[package]] @@ -4977,25 +4328,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "winch-codegen" -version = "36.0.12" -source = "git+https://github.com/G-Core/wasmtime.git?branch=release-36.0.0#62166f65ac52a1f164419097a2a533ff50e4d07b" -dependencies = [ - "anyhow", - "cranelift-assembler-x64 0.123.12", - "cranelift-codegen 0.123.12", - "gimli 0.32.3", - "regalloc2 0.12.2", - "smallvec", - "target-lexicon", - "thiserror 2.0.19", - "wasmparser 0.236.1", - "wasmtime-environ 36.0.12", - "wasmtime-internal-cranelift 36.0.12", - "wasmtime-internal-math", -] - [[package]] name = "windows" version = "0.52.0" @@ -5107,6 +4439,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.60.2" @@ -5276,7 +4617,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d" dependencies = [ "bitflags 2.13.1", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -5323,24 +4664,6 @@ dependencies = [ "wit-parser 0.254.0", ] -[[package]] -name = "wit-parser" -version = "0.236.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16e4833a20cd6e85d6abfea0e63a399472d6f88c6262957c17f546879a80ba15" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser 0.236.1", -] - [[package]] name = "wit-parser" version = "0.246.2" diff --git a/Cargo.toml b/Cargo.toml index 5074625..d0ab059 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,13 +15,12 @@ tracing = "0.1" hyper = { version = "1", features = ["full"] } http = "1.4" async-trait = "0.1" -wasmtime = { git = "https://github.com/G-Core/wasmtime.git", features = ["component-model"], branch = "release-36.0.0"} -wasmtime-wasi = { git = "https://github.com/G-Core/wasmtime.git", branch = "release-36.0.0"} -wasmtime-wasi-io = { git = "https://github.com/G-Core/wasmtime.git", branch = "release-36.0.0"} -wasi-common = { git = "https://github.com/G-Core/wasmtime.git", branch = "release-36.0.0"} -wasmtime-wasi-nn = { git = "https://github.com/G-Core/wasmtime.git", branch = "release-36.0.0", features = ["openvino", "candle"] } -wasmtime-wasi-http = { git = "https://github.com/G-Core/wasmtime.git", branch = "release-36.0.0"} -wasmtime-environ = { git = "https://github.com/G-Core/wasmtime.git", branch = "release-36.0.0"} +wasmtime = { git = "https://github.com/G-Core/wasmtime.git", features = ["component-model"], branch = "release-48.0.0" } +wasmtime-wasi = { git = "https://github.com/G-Core/wasmtime.git", branch = "release-48.0.0" } +wasmtime-wasi-io = { git = "https://github.com/G-Core/wasmtime.git", branch = "release-48.0.0" } +wasmtime-wasi-nn = { git = "https://github.com/G-Core/wasmtime.git", branch = "release-48.0.0", features = ["openvino", "candle"] } +wasmtime-wasi-http = { git = "https://github.com/G-Core/wasmtime.git", branch = "release-48.0.0" } +wasmtime-environ = { git = "https://github.com/G-Core/wasmtime.git", branch = "release-48.0.0" } bytesize = "2.3.1" clap = { version = "4", features = ["derive"] } @@ -56,7 +55,7 @@ http = { workspace = true } tokio = { workspace = true } wasmtime = { workspace = true } smol_str = { workspace = true } -async-trait = {workspace = true} +async-trait = { workspace = true } clap = { version = "4.6", features = ["derive"] } pretty_env_logger = "0.5" runtime = { path = "crates/runtime", default-features = false } @@ -69,7 +68,7 @@ cache = { path = "crates/cache" } hyper-tls = "0.6" hyper-util = { version = "0.1", features = ["client", "client-legacy", "http1", "tokio"] } http-body-util = "0.1" -bytesize = { workspace = true} +bytesize = { workspace = true } [target.'cfg(target_family = "unix")'.dependencies] shellflip = "2.1.2" diff --git a/crates/http-service/Cargo.toml b/crates/http-service/Cargo.toml index 8b7ce2c..2456627 100644 --- a/crates/http-service/Cargo.toml +++ b/crates/http-service/Cargo.toml @@ -16,7 +16,6 @@ http = { workspace = true } wasmtime = { workspace = true } wasmtime-wasi = { workspace = true } wasmtime-wasi-nn = { workspace = true } -wasi-common = { workspace = true } wasmtime-wasi-http = { workspace = true } tracing = { workspace = true } smol_str = { workspace = true } diff --git a/crates/http-service/src/executor/http.rs b/crates/http-service/src/executor/http.rs index 83a282a..9f94ab2 100644 --- a/crates/http-service/src/executor/http.rs +++ b/crates/http-service/src/executor/http.rs @@ -13,7 +13,7 @@ use runtime::{InstancePre, store::StoreBuilder}; use std::sync::Arc; use std::sync::atomic::AtomicU64; use std::time::Duration; -use wasmtime_wasi_http::body::HyperOutgoingBody; +use wasmtime_wasi_http::p2::body::HyperOutgoingBody; /// Execute context used by ['HttpService'] #[derive(Clone)] @@ -140,9 +140,9 @@ where // declared minimum memory exceeds `mem_limit`) is recorded by the // limiter; classify it as out-of-memory instead of a generic error. if store.is_oom() { - return Err(runtime::store::OutOfMemory(error).into()); + return Err(runtime::store::OutOfMemory(error.into()).into()); } - return Err(error); + return Err(error.into()); } }; let http_handler = @@ -166,7 +166,7 @@ where .write_msg(format!("Execution error: {}", error)) .await; } - return Err(error); + return Err(error.into()); } }; let status_code = StatusCode::try_from(resp.status)?; @@ -185,7 +185,7 @@ where let body = resp .body - .map(|b| Full::from(b).map_err(|never| match never {}).boxed()) + .map(|b| Full::from(b).map_err(|never| match never {}).boxed_unsync()) .unwrap_or_default(); builder.body(body).map_err(anyhow::Error::msg) } @@ -381,11 +381,11 @@ mod tests { DUMMY_SAMPLE }; let wasm_sample = componentize_if_necessary(bytes)?; - Component::new(&self.engine, wasm_sample) + Ok(Component::new(&self.engine, wasm_sample)?) } fn load_module(&self, _id: u64) -> anyhow::Result { - Module::new(&self.engine, DUMMY_SAMPLE) + Ok(Module::new(&self.engine, DUMMY_SAMPLE)?) } } @@ -502,7 +502,7 @@ mod tests { .body( Empty::::new() .map_err(|never| match never {}) - .boxed() + .boxed_unsync() ) ); @@ -534,7 +534,7 @@ mod tests { .body( Empty::::new() .map_err(|never| match never {}) - .boxed() + .boxed_unsync() ) ); @@ -593,7 +593,7 @@ mod tests { .body( Empty::::new() .map_err(|never| match never {}) - .boxed() + .boxed_unsync() ) ); @@ -648,7 +648,7 @@ mod tests { .body( Empty::::new() .map_err(|never| match never {}) - .boxed() + .boxed_unsync() ) ); @@ -676,7 +676,7 @@ mod tests { .body( Empty::::new() .map_err(|never| match never {}) - .boxed() + .boxed_unsync() ) ); @@ -704,7 +704,7 @@ mod tests { .body( Empty::::new() .map_err(|never| match never {}) - .boxed() + .boxed_unsync() ) ); @@ -732,7 +732,7 @@ mod tests { .body( Empty::::new() .map_err(|never| match never {}) - .boxed() + .boxed_unsync() ) ); @@ -764,7 +764,7 @@ mod tests { .body( Empty::::new() .map_err(|never| match never {}) - .boxed() + .boxed_unsync() ) ); @@ -798,7 +798,7 @@ mod tests { .body( Empty::::new() .map_err(|never| match never {}) - .boxed() + .boxed_unsync() ) ); @@ -832,7 +832,7 @@ mod tests { .body( Empty::::new() .map_err(|never| match never {}) - .boxed() + .boxed_unsync() ) ); @@ -862,7 +862,7 @@ mod tests { .body( Empty::::new() .map_err(|never| match never {}) - .boxed() + .boxed_unsync() ) ); @@ -893,7 +893,7 @@ mod tests { .body( Empty::::new() .map_err(|never| match never {}) - .boxed() + .boxed_unsync() ) ); @@ -925,7 +925,7 @@ mod tests { .body( Empty::::new() .map_err(|never| match never {}) - .boxed() + .boxed_unsync() ) ); @@ -951,7 +951,7 @@ mod tests { Request::builder().method("GET").uri("/").body( Empty::::new() .map_err(|never| match never {}) - .boxed() + .boxed_unsync() ) ); diff --git a/crates/http-service/src/executor/mod.rs b/crates/http-service/src/executor/mod.rs index 1c91073..3515575 100644 --- a/crates/http-service/src/executor/mod.rs +++ b/crates/http-service/src/executor/mod.rs @@ -12,7 +12,7 @@ use http_body_util::BodyExt; use hyper::body::Body; use runtime::{App, WasmEngine}; use smol_str::SmolStr; -use wasmtime_wasi_http::body::HyperOutgoingBody; +use wasmtime_wasi_http::p2::body::HyperOutgoingBody; pub use http::HttpExecutorImpl; pub use wasi_http::WasiHttpExecutorImpl; diff --git a/crates/http-service/src/executor/wasi_http.rs b/crates/http-service/src/executor/wasi_http.rs index bf489f8..5b16546 100644 --- a/crates/http-service/src/executor/wasi_http.rs +++ b/crates/http-service/src/executor/wasi_http.rs @@ -15,9 +15,10 @@ use runtime::util::stats::{StatsTimer, StatsVisitor}; use runtime::{InstancePre, store::StoreBuilder}; use smol_str::SmolStr; use tracing::Instrument; -use wasmtime_wasi_http::bindings::ProxyPre; -use wasmtime_wasi_http::bindings::http::types::Scheme; -use wasmtime_wasi_http::{WasiHttpView, body::HyperOutgoingBody}; +use wasmtime_wasi_http::WasiHttpView; +use wasmtime_wasi_http::p2::bindings::ProxyPre; +use wasmtime_wasi_http::p2::bindings::http::types::Scheme; +use wasmtime_wasi_http::p2::body::HyperOutgoingBody; /// Execute context used by ['HttpService'] #[derive(Clone)] @@ -77,7 +78,7 @@ where .map_err(|_| anyhow!("body read error"))? .to_bytes(); let body = Full::new(body).map_err(|never| match never {}); - let body = body.boxed(); + let body: HyperOutgoingBody = body.boxed_unsync(); let properties = executor::get_properties(&parts.headers); let mut store_builder = self @@ -147,11 +148,15 @@ where let request = Request::from_parts(parts, body); let req = store .data_mut() + .http() .new_incoming_request(Scheme::Http, request) + .map_err(anyhow::Error::from) .context("new incoming request")?; let out = store .data_mut() + .http() .new_response_outparam(sender) + .map_err(anyhow::Error::from) .context("new response outparam")?; let proxy_pre = ProxyPre::new(instance_pre)?; @@ -162,9 +167,9 @@ where // declared minimum memory exceeds `mem_limit`) is recorded by the // limiter; classify it as out-of-memory instead of a generic error. if store.is_oom() { - return Err(runtime::store::OutOfMemory(error).into()); + return Err(runtime::store::OutOfMemory(error.into()).into()); } - return Err(error); + return Err(error.into()); } }; @@ -180,7 +185,11 @@ where ) .await { - Ok(inner) => inner, + // Convert into `anyhow::Error` here so the task's error type + // stays what `fail_reason_of`/`map_err` classify. The + // conversion rebuilds the context chain, so `root_cause()` + // still downcasts to `wasmtime::Trap`. + Ok(inner) => inner.map_err(anyhow::Error::from), // tokio timeout elapsed (outer deadline hit). Err(elapsed) => Err(elapsed.into()), }; diff --git a/crates/http-service/src/lib.rs b/crates/http-service/src/lib.rs index f7017fb..cc64468 100644 --- a/crates/http-service/src/lib.rs +++ b/crates/http-service/src/lib.rs @@ -27,7 +27,7 @@ use smol_str::{SmolStr, ToSmolStr}; use state::HttpState; use tokio::{net::TcpListener, time::error::Elapsed}; use tracing::Instrument; -pub use wasmtime_wasi_http::body::HyperOutgoingBody; +pub use wasmtime_wasi_http::p2::body::HyperOutgoingBody; pub mod executor; pub mod state; @@ -221,7 +221,7 @@ where // Allow re-importing of `wasi:clocks/wall-clock@0.2.0` wasmtime_wasi::p2::add_to_linker_async(linker)?; linker.allow_shadowing(true); - wasmtime_wasi_http::add_to_linker_async(linker)?; + wasmtime_wasi_http::p2::add_to_linker_async(linker)?; wasmtime_wasi_nn::wit::add_to_linker(linker, |data: &mut runtime::Data<_>| { WasiNnView::new(&mut data.table, &mut data.wasi_nn) })?; @@ -413,7 +413,7 @@ where let caller_ip = request .headers() - .get(crate::executor::X_REAL_IP) + .get(executor::X_REAL_IP) .and_then(|v| v.to_str().ok()) .and_then(|v| v.parse::().ok()) .unwrap_or(std::net::Ipv4Addr::UNSPECIFIED); @@ -495,7 +495,7 @@ pub(crate) fn fail_reason_of(error: &Error) -> AppResult { let root_cause = error.root_cause(); if error.chain().any(|e| e.is::()) { AppResult::OOM - } else if let Some(exit) = root_cause.downcast_ref::() { + } else if let Some(exit) = root_cause.downcast_ref::() { if exit.0 == 0 { AppResult::SUCCESS } else { @@ -527,15 +527,15 @@ fn map_err(error: Error) -> (u16, AppResult, HyperOutgoingBody, u16) { AppResult::OOM, Full::new(Bytes::from("fastedge: Out of memory")) .map_err(|never| match never {}) - .boxed(), + .boxed_unsync(), INTERNAL_STATUS_OUT_OF_MEMORY, ) - } else if let Some(exit) = root_cause.downcast_ref::() { + } else if let Some(exit) = root_cause.downcast_ref::() { if exit.0 == 0 { ( StatusCode::OK.as_u16(), AppResult::SUCCESS, - Empty::new().map_err(|never| match never {}).boxed(), + Empty::new().map_err(|never| match never {}).boxed_unsync(), 0, ) } else { @@ -544,7 +544,7 @@ fn map_err(error: Error) -> (u16, AppResult, HyperOutgoingBody, u16) { AppResult::OTHER, Full::new(Bytes::from("fastedge: App failed")) .map_err(|never| match never {}) - .boxed(), + .boxed_unsync(), INTERNAL_STATUS_APP_EXIT_ERROR, ) } @@ -555,7 +555,7 @@ fn map_err(error: Error) -> (u16, AppResult, HyperOutgoingBody, u16) { AppResult::TIMEOUT, Full::new(Bytes::from("fastedge: Execution timeout")) .map_err(|never| match never {}) - .boxed(), + .boxed_unsync(), INTERNAL_STATUS_TIMEOUT_INTERRUPT, ), wasmtime::Trap::UnreachableCodeReached => ( @@ -563,7 +563,7 @@ fn map_err(error: Error) -> (u16, AppResult, HyperOutgoingBody, u16) { AppResult::OOM, Full::new(Bytes::from("fastedge: Out of memory")) .map_err(|never| match never {}) - .boxed(), + .boxed_unsync(), INTERNAL_STATUS_OUT_OF_MEMORY, ), _ => ( @@ -571,7 +571,7 @@ fn map_err(error: Error) -> (u16, AppResult, HyperOutgoingBody, u16) { AppResult::OTHER, Full::new(Bytes::from("fastedge: App failed")) .map_err(|never| match never {}) - .boxed(), + .boxed_unsync(), INTERNAL_STATUS_WASM_TRAP_OTHER, ), } @@ -581,7 +581,7 @@ fn map_err(error: Error) -> (u16, AppResult, HyperOutgoingBody, u16) { AppResult::TIMEOUT, Full::new(Bytes::from("fastedge: Execution timeout")) .map_err(|never| match never {}) - .boxed(), + .boxed_unsync(), INTERNAL_STATUS_TIMEOUT_ELAPSED, ) } else if root_cause.to_string().ends_with("deadline has elapsed") { @@ -590,7 +590,7 @@ fn map_err(error: Error) -> (u16, AppResult, HyperOutgoingBody, u16) { AppResult::TIMEOUT, Full::new(Bytes::from("fastedge: Execution timeout")) .map_err(|never| match never {}) - .boxed(), + .boxed_unsync(), INTERNAL_STATUS_TIMEOUT_DEADLINE, ) } else { @@ -599,7 +599,7 @@ fn map_err(error: Error) -> (u16, AppResult, HyperOutgoingBody, u16) { AppResult::OTHER, Full::new(Bytes::from("fastedge: Execute error")) .map_err(|never| match never {}) - .boxed(), + .boxed_unsync(), INTERNAL_STATUS_EXECUTE_ERROR, ) }; @@ -626,7 +626,7 @@ fn internal_fastedge_error( .body( Full::new(Bytes::from(format!("fastedge: {}", msg))) .map_err(|never| match never {}) - .boxed(), + .boxed_unsync(), )?) } @@ -637,7 +637,7 @@ fn not_found() -> Result> { .body( Full::new(Bytes::from("fastedge: Unknown app")) .map_err(|never| match never {}) - .boxed(), + .boxed_unsync(), )?) } @@ -645,14 +645,14 @@ fn not_found() -> Result> { fn too_many_requests() -> Result> { Ok(hyper::Response::builder() .status(StatusCode::TOO_MANY_REQUESTS) - .body(Empty::new().map_err(|never| match never {}).boxed())?) + .body(Empty::new().map_err(|never| match never {}).boxed_unsync())?) } /// Creates an HTTP 406 response. fn not_acceptable() -> Result> { Ok(hyper::Response::builder() .status(StatusCode::NOT_ACCEPTABLE) - .body(Empty::new().map_err(|never| match never {}).boxed())?) + .body(Empty::new().map_err(|never| match never {}).boxed_unsync())?) } #[derive(Debug, Clone)] @@ -826,7 +826,7 @@ mod tests { .body( Empty::::new() .map_err(|never| match never {}) - .boxed() + .boxed_unsync() ) ); let app_name = assert_ok!(app_name_from_request(&req)); @@ -843,7 +843,7 @@ mod tests { .body( Empty::::new() .map_err(|never| match never {}) - .boxed() + .boxed_unsync() ) ); let app_name = assert_ok!(app_name_from_request(&req)); @@ -860,7 +860,7 @@ mod tests { .body( Empty::::new() .map_err(|never| match never {}) - .boxed() + .boxed_unsync() ) ); let app_name = assert_ok!(app_name_from_request(&req)); @@ -877,7 +877,7 @@ mod tests { empty_body_request().uri(uri).body( Empty::::new() .map_err(|never| match never {}) - .boxed() + .boxed_unsync() ) ); let app_name = assert_ok!(app_name_from_request(&req)); @@ -895,7 +895,7 @@ mod tests { .body( Empty::::new() .map_err(|never| match never {}) - .boxed() + .boxed_unsync() ) ); let app_name = assert_ok!(app_name_from_request(&req)); @@ -913,7 +913,7 @@ mod tests { .body( Empty::::new() .map_err(|never| match never {}) - .boxed() + .boxed_unsync() ) ); let app_name = assert_ok!(app_name_from_request(&req)); @@ -929,7 +929,7 @@ mod tests { .body( Empty::::new() .map_err(|never| match never {}) - .boxed() + .boxed_unsync() ) ); assert_err!(app_name_from_request(&req)); @@ -943,7 +943,7 @@ mod tests { empty_body_request().uri("/").body( Empty::::new() .map_err(|never| match never {}) - .boxed() + .boxed_unsync() ) ); assert_err!(app_name_from_request(&req)); diff --git a/crates/runtime/Cargo.toml b/crates/runtime/Cargo.toml index 6fbfe1e..8d86340 100644 --- a/crates/runtime/Cargo.toml +++ b/crates/runtime/Cargo.toml @@ -11,7 +11,6 @@ tokio = { workspace = true } tokio-util = { workspace = true } wasmtime = { workspace = true } wasmtime-wasi = { workspace = true } -wasi-common = { workspace = true } wasmtime-wasi-nn = { workspace = true } wasmtime-wasi-http = { workspace = true } wasmtime-environ = { workspace = true } @@ -19,6 +18,7 @@ wasmtime-wasi-io = { workspace = true } smol_str = { workspace = true } moka = { workspace = true } http = { workspace = true } +http-body-util = "0.1" wit-component = "0.246.2" tracing = { workspace = true } bytesize = { workspace = true } diff --git a/crates/runtime/src/lib.rs b/crates/runtime/src/lib.rs index a831b49..4976edc 100644 --- a/crates/runtime/src/lib.rs +++ b/crates/runtime/src/lib.rs @@ -10,7 +10,10 @@ use std::{fmt::Debug, ops::Deref}; use utils::{Dictionary, Utils}; use wasmtime_wasi::ResourceTable; use wasmtime_wasi::WasiCtxView; -use wasmtime_wasi_http::{HttpResult, WasiHttpCtx, WasiHttpView}; +use wasmtime_wasi_http::{ + DEFAULT_FORBIDDEN_HEADERS, RequestOptions, WasiBody, WasiHttpCtx, WasiHttpCtxView, + WasiHttpHooks, WasiHttpView, default_send_request, +}; use wasmtime_wasi_io::IoView; use crate::store::StoreBuilder; @@ -44,16 +47,13 @@ use crate::util::stats::StatsVisitor; use anyhow::{anyhow, bail}; pub use app::{App, SecretValue, SecretValues}; use http::request::Parts; -use http::{HeaderName, Request, header}; +use http::{HeaderName, Request, Response, header}; +use http_body_util::BodyExt; use secret::SecretStore; use smol_str::SmolStr; use std::borrow::Cow; +use std::future::Future; use wasmtime_environ::wasmparser::{Encoding, Parser, Payload}; -use wasmtime_wasi_http::body::HyperOutgoingBody; -use wasmtime_wasi_http::{ - bindings::http::types::ErrorCode, - types::{HostFutureIncomingResponse, OutgoingRequestConfig, default_send_request_handler}, -}; use wasmtime_wasi_nn::wit::WasiNnCtx; pub const DEFAULT_EPOCH_TICK_INTERVAL: u64 = 10; @@ -100,14 +100,48 @@ pub enum WasiVersion { /// Wrapper for the Preview 1 and Preview 2 versions of `WasiCtx`. pub enum Wasi { /// Preview 1 `WasiCtx` - Preview1(wasmtime_wasi::preview1::WasiP1Ctx), + Preview1(wasmtime_wasi::p1::WasiP1Ctx), /// Preview 2 `WasiCtx` Preview2(wasmtime_wasi::WasiCtx), } +/// The embedder-side `wasi:http` overrides, bundled with the guest state `T`. +/// +/// wasmtime 48 split the old `WasiHttpView` in two: the view now only hands out +/// the HTTP context, the resource table and a `&mut dyn WasiHttpHooks`, while +/// the overrides themselves (`send_request`, `is_forbidden_header`) moved onto +/// [`WasiHttpHooks`]. Because the hooks are borrowed out of the store data as a +/// single trait object, everything an override needs has to live in one struct. +/// That is this struct: it owns the guest state `T` (reached from the rest of +/// the runtime through `AsRef`/`AsMut` on [`Data`]) alongside the epoch +/// bookkeeping that `send_request` updates. +pub struct HttpHooks { + inner: T, + /// Milliseconds of host I/O that should not count against the epoch + /// deadline. Shared with the `epoch_deadline_callback` installed on the + /// Store, which drains this counter to extend the deadline. + epoch_pause_ms: Arc, + /// Whether elapsed time of external HTTP should refund epoch ticks. + pause_epoch_timeout_for_external_http: bool, +} + +impl HttpHooks { + pub fn new( + inner: T, + epoch_pause_ms: Arc, + pause_epoch_timeout_for_external_http: bool, + ) -> Self { + Self { + inner, + epoch_pause_ms, + pause_epoch_timeout_for_external_http, + } + } +} + /// Host state data associated with individual [Store]s and [Instance]s. pub struct Data { - inner: T, + hooks: HttpHooks, wasi: Wasi, pub wasi_nn: WasiNnCtx, // memory usage limiter @@ -121,13 +155,6 @@ pub struct Data { pub dictionary: Dictionary, pub utils: Utils, pub cache: cache::CacheImpl, - /// Milliseconds of host I/O that should not count against the epoch - /// deadline. The `epoch_deadline_callback` installed on the Store reads - /// and clears this counter, converting milliseconds into extra ticks to - /// extend the deadline. - pub epoch_pause_ms: Arc, - /// Whether elapsed time of external HTTP should refund epoch ticks. - pub pause_epoch_timeout_for_external_http: bool, /// Counts this instance in `fastedge_wasm_instances_live` for as long as the store — /// and therefore its pooling-allocator slots — is alive. Held only for its `Drop`. _live_instance: crate::instances::LiveInstanceGuard, @@ -146,13 +173,13 @@ pub trait BackendRequest { impl AsRef for Data { fn as_ref(&self) -> &T { - &self.inner + &self.hooks.inner } } impl AsMut for Data { fn as_mut(&mut self) -> &mut T { - &mut self.inner + &mut self.hooks.inner } } @@ -162,19 +189,33 @@ impl IoView for Data { } } +/// Boxed future used by [`WasiHttpHooks::send_request`] to report an error +/// raised while the request or response body was being processed. +type HttpIoFuture = Box> + Send>; + impl WasiHttpView for Data { - fn ctx(&mut self) -> &mut WasiHttpCtx { - &mut self.http + fn http(&mut self) -> WasiHttpCtxView<'_> { + WasiHttpCtxView { + ctx: &mut self.http, + table: &mut self.table, + hooks: &mut self.hooks, + } } +} +impl WasiHttpHooks for HttpHooks { fn send_request( &mut self, - request: Request, - config: OutgoingRequestConfig, - ) -> HttpResult - where - Self: Sized, - { + request: Request, + options: Option, + fut: HttpIoFuture, + ) -> Box< + dyn Future, HttpIoFuture)>> + Send, + > { + // The request-side error channel is unused: a request rejected by + // `backend_request` fails the whole call below instead. + let _ = fut; + // Capture the outbound method/target and access-log handle before the // request is consumed. The URI here is the original external target, // before `backend_request` rewrites it to the internal backend @@ -184,23 +225,28 @@ impl WasiHttpView for Data { let log_handle = self.inner.ext_request_log_handle(); let (head, body) = request.into_parts(); - let head = self.inner.backend_request(head).map_err(|e| { - tracing::warn!(cause=?e, "backend request"); - ErrorCode::InternalError(Some(e.to_string())) - })?; - let use_tls = matches!(head.uri.scheme_str(), Some("https")); + let head = match self.inner.backend_request(head) { + Ok(head) => head, + Err(e) => { + tracing::warn!(cause=?e, "backend request"); + let cause = e.to_string(); + return Box::new(async move { + Err(wasmtime_wasi_http::Error::InternalError(Some(cause))) + }); + } + }; + // `default_send_request` derives TLS usage from the rewritten URI's + // scheme, so the backend rewrite above decides it. let request = Request::from_parts(head, body); // start external request stats timer let stats = self.inner.get_stats(); let epoch_pause_ms = self.epoch_pause_ms.clone(); let epoch_exclude_http_wait = self.pause_epoch_timeout_for_external_http; - let handle = wasmtime_wasi::runtime::spawn(async move { - let _stats_timer = ExtStatsTimer::new(stats); // keep timer alive until request is done + Box::new(async move { + let _stats_timer = ExtStatsTimer::new(stats); // keep timer alive until response head is in let started = Instant::now(); - let resp = - default_send_request_handler(request, OutgoingRequestConfig { use_tls, ..config }) - .await; + let sent = default_send_request(request, options).await; let elapsed = started.elapsed(); if epoch_exclude_http_wait { epoch_pause_ms.fetch_add(elapsed.as_millis() as u64, Ordering::Relaxed); @@ -208,19 +254,18 @@ impl WasiHttpView for Data { // One access-log record per outbound request (status 0 = no response // received, e.g. connection refused / timeout). if let Some(handle) = log_handle { - let status = match &resp { - Ok(incoming) => incoming.resp.status().as_u16(), + let status = match &sent { + Ok((incoming, _)) => incoming.status().as_u16(), Err(_) => 0, }; handle.log(&log_method, &log_uri, status, elapsed); } - Ok(resp) - }); - Ok(HostFutureIncomingResponse::pending(handle)) - } - - fn table(&mut self) -> &mut ResourceTable { - &mut self.table + let (response, io) = sent?; + Ok(( + response.map(BodyExt::boxed_unsync), + Box::new(io) as HttpIoFuture, + )) + }) } fn is_forbidden_header(&mut self, name: &HeaderName) -> bool { @@ -235,12 +280,12 @@ impl WasiHttpView for Data { return true; } // Fall back to wasmtime's default forbidden-header policy. - wasmtime_wasi_http::types::DEFAULT_FORBIDDEN_HEADERS.contains(name) + DEFAULT_FORBIDDEN_HEADERS.contains(name) } } impl Data { - pub fn preview1_wasi_ctx_mut(&mut self) -> &mut wasmtime_wasi::preview1::WasiP1Ctx { + pub fn preview1_wasi_ctx_mut(&mut self) -> &mut wasmtime_wasi::p1::WasiP1Ctx { match &mut self.wasi { Wasi::Preview1(ctx) => ctx, Wasi::Preview2(_) => unreachable!("using WASI Preview 2 functions with Preview 1 ctx"), @@ -301,10 +346,8 @@ impl Default for WasmConfig { // Debug build: keep full, symbolized guest backtraces. We are optimizing // execution CPU, not trap-path cost, and this is the standalone debug // runner — detailed backtraces are the whole point. - inner.wasm_backtrace(true); inner.wasm_backtrace_details(WasmBacktraceDetails::Enable); - inner.async_support(true); inner.consume_fuel(false); // this is custom Gcore setting inner.profiler(ProfilingStrategy::None); inner.epoch_interruption(true); // required by store.rs timeout mechanism diff --git a/crates/runtime/src/limiter.rs b/crates/runtime/src/limiter.rs index 355e7d1..c91d561 100644 --- a/crates/runtime/src/limiter.rs +++ b/crates/runtime/src/limiter.rs @@ -1,6 +1,5 @@ -use anyhow::{Error, Result}; use tracing::instrument; -use wasmtime::{ResourceLimiter, StoreLimits}; +use wasmtime::{Error, ResourceLimiter, Result, StoreLimits}; /// A proxy wrapper of `wasmtime::ResourceLimiter` trait impl used to track used memory #[derive(Clone, Debug)] diff --git a/crates/runtime/src/logger.rs b/crates/runtime/src/logger.rs index 9d1debd..b58a095 100644 --- a/crates/runtime/src/logger.rs +++ b/crates/runtime/src/logger.rs @@ -1,15 +1,11 @@ use async_trait::async_trait; use bytes::Bytes; use chrono::{DateTime, Utc}; -use std::any::Any; use std::collections::HashMap; -use std::io::IoSlice; use std::pin::Pin; use std::sync::Arc; use std::task::{Context, Poll}; use tokio::io::{AsyncWrite, AsyncWriteExt}; -use wasi_common::file::{FdFlags, FileType}; -use wasi_common::{Error, WasiFile}; use wasmtime_wasi::cli::{IsTerminal, StdoutStream}; use wasmtime_wasi_io::poll::Pollable; use wasmtime_wasi_io::streams::OutputStream; @@ -273,27 +269,3 @@ impl OutputStream for Console { Ok(usize::MAX) } } - -#[async_trait] -impl WasiFile for Console { - fn as_any(&self) -> &dyn Any { - self - } - - async fn get_filetype(&self) -> Result { - Ok(FileType::Pipe) - } - - async fn get_fdflags(&self) -> Result { - Ok(FdFlags::APPEND) - } - - async fn write_vectored<'a>(&self, bufs: &[IoSlice<'a>]) -> Result { - let n = bufs.iter().fold(0, |n, buf| { - print!("{}", std::str::from_utf8(&buf).unwrap()); - n + buf.len() - }); - - Ok(n as u64) - } -} diff --git a/crates/runtime/src/store.rs b/crates/runtime/src/store.rs index 3bc9932..162ae55 100644 --- a/crates/runtime/src/store.rs +++ b/crates/runtime/src/store.rs @@ -2,7 +2,7 @@ use crate::limiter::ProxyLimiter; use crate::logger::Logger; use crate::registry::CachedGraphRegistry; use crate::util::stats::StatsVisitor; -use crate::{DEFAULT_EPOCH_TICK_INTERVAL, Data, Wasi, WasiVersion}; +use crate::{DEFAULT_EPOCH_TICK_INTERVAL, Data, HttpHooks, Wasi, WasiVersion}; use anyhow::Result; use secret::SecretStore; use std::sync::Arc; @@ -353,7 +353,7 @@ impl StoreBuilder { let mut inner = wasmtime::Store::new( &self.engine, Data { - inner, + hooks: HttpHooks::new(inner, epoch_pause_ms.clone(), self.epoch_exclude_http_wait), wasi, wasi_nn, store_limits: self.store_limits, @@ -374,8 +374,6 @@ impl StoreBuilder { dictionary: self.dictionary, utils, cache: cache_impl, - epoch_pause_ms: epoch_pause_ms.clone(), - pause_epoch_timeout_for_external_http: self.epoch_exclude_http_wait, _live_instance: crate::instances::LiveInstanceGuard::new(), }, ); @@ -387,7 +385,7 @@ impl StoreBuilder { inner.epoch_deadline_callback(move |_ctx| { let credit_ms = epoch_pause_ms.swap(0, Ordering::Relaxed); match epoch_credit_ticks(credit_ms) { - None => Err(anyhow::Error::new(wasmtime::Trap::Interrupt)), + None => Err(wasmtime::Error::new(wasmtime::Trap::Interrupt)), Some(ticks) => Ok(UpdateDeadline::Continue(ticks)), } }); @@ -498,7 +496,7 @@ mod tests { store.epoch_deadline_callback(move |_ctx| { let credit_ms = epoch_pause_ms.swap(0, Ordering::Relaxed); match epoch_credit_ticks(credit_ms) { - None => Err(anyhow::Error::new(Trap::Interrupt)), + None => Err(wasmtime::Error::new(Trap::Interrupt)), Some(ticks) => Ok(UpdateDeadline::Continue(ticks)), } }); diff --git a/src/context.rs b/src/context.rs index c06c9cb..1bc6481 100644 --- a/src/context.rs +++ b/src/context.rs @@ -42,7 +42,7 @@ pub struct Context { impl PreCompiledLoader for Context { fn load_component(&self, _id: u64) -> anyhow::Result { let wasm_sample = componentize_if_necessary(&self.wasm_bytes)?; - Component::new(&self.engine, wasm_sample) + Ok(Component::new(&self.engine, wasm_sample)?) } fn load_module(&self, _id: u64) -> anyhow::Result { From 33b603352b4d07a1c3f34f7b9d5ba939590268fc Mon Sep 17 00:00:00 2001 From: Ruslan Pislari Date: Tue, 22 Sep 2026 12:29:19 +0300 Subject: [PATCH 19/22] feat(runtime): add epoch refund for precise HTTP wait time accounting - Introduced `EpochRefund`, `ResponseBodyEpochRefund`, and `RequestBodyEpochRefund` to improve epoch pause accounting for network wait times during HTTP request and response processing. - Enhanced `WasiHttpHooks` to ensure accurate epoch refunds for guest blocking on network I/O. - Updated `Cargo.toml` and `Cargo.lock` to include `http-body` dependency. --- Cargo.lock | 1 + crates/runtime/Cargo.toml | 1 + crates/runtime/src/lib.rs | 224 ++++++++++++++++++++++++++++++++++---- 3 files changed, 205 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8ea2d09..25cc260 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2806,6 +2806,7 @@ dependencies = [ "claims", "http", "http-backend", + "http-body", "http-body-util", "key-value-store", "moka", diff --git a/crates/runtime/Cargo.toml b/crates/runtime/Cargo.toml index 8d86340..6bb6bb2 100644 --- a/crates/runtime/Cargo.toml +++ b/crates/runtime/Cargo.toml @@ -18,6 +18,7 @@ wasmtime-wasi-io = { workspace = true } smol_str = { workspace = true } moka = { workspace = true } http = { workspace = true } +http-body = "1" http-body-util = "0.1" wit-component = "0.246.2" tracing = { workspace = true } diff --git a/crates/runtime/src/lib.rs b/crates/runtime/src/lib.rs index 4976edc..8e7ff6e 100644 --- a/crates/runtime/src/lib.rs +++ b/crates/runtime/src/lib.rs @@ -3,9 +3,11 @@ use crate::store::HasStats; use http_backend::access_log::ExtRequestLogHandle; use http_backend::stats::ExtStatsTimer; use std::net::Ipv4Addr; -use std::sync::Arc; +use std::pin::Pin; use std::sync::atomic::{AtomicU64, Ordering}; -use std::time::Instant; +use std::sync::{Arc, OnceLock}; +use std::task::{Context, Poll}; +use std::time::{Duration, Instant}; use std::{fmt::Debug, ops::Deref}; use utils::{Dictionary, Utils}; use wasmtime_wasi::ResourceTable; @@ -46,8 +48,10 @@ use crate::logger::Logger; use crate::util::stats::StatsVisitor; use anyhow::{anyhow, bail}; pub use app::{App, SecretValue, SecretValues}; +use bytes::Bytes; use http::request::Parts; use http::{HeaderName, Request, Response, header}; +use http_body::{Body, Frame, SizeHint}; use http_body_util::BodyExt; use secret::SecretStore; use smol_str::SmolStr; @@ -193,6 +197,156 @@ impl IoView for Data { /// raised while the request or response body was being processed. type HttpIoFuture = Box> + Send>; +/// Deposits host I/O wait time into the shared epoch-pause counter, carrying +/// sub-millisecond remainders across deposits so that bodies streamed in many +/// small frames do not have their wait time truncated away frame by frame. +struct EpochRefund { + epoch_pause_ms: Arc, + carry: Duration, +} + +impl EpochRefund { + fn new(epoch_pause_ms: Arc) -> Self { + Self { + epoch_pause_ms, + carry: Duration::ZERO, + } + } + + fn deposit(&mut self, elapsed: Duration) { + let total = self.carry + elapsed; + let ms = total.as_millis() as u64; + self.carry = total - Duration::from_millis(ms); + if ms > 0 { + self.epoch_pause_ms.fetch_add(ms, Ordering::Relaxed); + } + } +} + +/// Response-body wrapper that refunds epoch ticks for time spent waiting on +/// the network. `default_send_request` resolves as soon as the response +/// *headers* are in; the body then streams lazily through the returned +/// `Response` and is only polled when the guest reads it. Any time a frame +/// poll stays `Pending` the guest is blocked on host I/O, so that time is +/// deposited into `epoch_pause_ms`, mirroring the send-phase refund in +/// [`WasiHttpHooks::send_request`]. +struct ResponseBodyEpochRefund { + inner: WasiBody, + refund: EpochRefund, + /// When the in-progress frame poll first returned `Pending`. + pending_since: Option, +} + +impl Body for ResponseBodyEpochRefund { + type Data = Bytes; + type Error = wasmtime_wasi_http::Error; + + fn poll_frame( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll, Self::Error>>> { + let this = self.get_mut(); + let result = Pin::new(&mut this.inner).poll_frame(cx); + match &result { + Poll::Pending => { + this.pending_since.get_or_insert_with(Instant::now); + } + Poll::Ready(_) => { + if let Some(started) = this.pending_since.take() { + this.refund.deposit(started.elapsed()); + } + } + } + result + } + + fn is_end_stream(&self) -> bool { + self.inner.is_end_stream() + } + + fn size_hint(&self) -> SizeHint { + self.inner.size_hint() + } +} + +impl Drop for ResponseBodyEpochRefund { + fn drop(&mut self) { + // Body dropped mid-wait (e.g. the request ended early): flush the tail. + if let Some(started) = self.pending_since.take() { + self.refund.deposit(started.elapsed()); + } + } +} + +/// Request-body wrapper covering the tail of the request-body lifetime. +/// +/// Until the response headers arrive, [`WasiHttpHooks::send_request`] refunds +/// the whole wall clock of `default_send_request`, which already includes +/// request-body streaming, so no extra accounting is needed there (and any +/// would double count). But hyper may still be sending the request body +/// *after* the headers arrived — driven by the background `io` future — while +/// the guest blocks in a body write waiting for backpressure to clear. That +/// wait shows up as the gap between this body yielding a frame and hyper +/// polling for the next one; a `Pending` poll, by contrast, means the guest +/// itself has not produced data yet and must stay on the clock. Gaps are +/// refunded only for their portion past `headers_at`. +struct RequestBodyEpochRefund { + inner: WasiBody, + refund: EpochRefund, + /// Set once the response headers are in (send-phase refund window closed). + headers_at: Arc>, + /// When the previous poll yielded a frame. + ready_since: Option, +} + +impl RequestBodyEpochRefund { + /// Refund the elapsed part of the current inter-poll gap that falls + /// outside the send-phase refund window. + fn settle_gap(&mut self) { + let Some(ready) = self.ready_since.take() else { + return; + }; + let Some(headers) = self.headers_at.get() else { + return; + }; + self.refund.deposit(ready.max(*headers).elapsed()); + } +} + +impl Body for RequestBodyEpochRefund { + type Data = Bytes; + type Error = wasmtime_wasi_http::Error; + + fn poll_frame( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll, Self::Error>>> { + let this = self.get_mut(); + this.settle_gap(); + let result = Pin::new(&mut this.inner).poll_frame(cx); + if let Poll::Ready(Some(Ok(_))) = &result { + this.ready_since = Some(Instant::now()); + } + result + } + + fn is_end_stream(&self) -> bool { + self.inner.is_end_stream() + } + + fn size_hint(&self) -> SizeHint { + self.inner.size_hint() + } +} + +impl Drop for RequestBodyEpochRefund { + fn drop(&mut self) { + // hyper drops the body when it is done sending it (or the connection + // failed); settle the wait between the last yielded frame and now. + self.settle_gap(); + } +} + impl WasiHttpView for Data { fn http(&mut self) -> WasiHttpCtxView<'_> { WasiHttpCtxView { @@ -204,6 +358,21 @@ impl WasiHttpView for Data { } impl WasiHttpHooks for HttpHooks { + fn is_forbidden_header(&mut self, name: &HeaderName) -> bool { + // We want to allow the host header to be set. + if name.eq(&header::HOST) { + return false; + } + // Block all headers with the reserved `fastedge` prefix — these are + // internal routing headers that guest modules must not set. + let name_str = name.as_str(); + if name_str.starts_with("fastedge-") || name_str.starts_with("fastedge_") { + return true; + } + // Fall back to wasmtime's default forbidden-header policy. + DEFAULT_FORBIDDEN_HEADERS.contains(name) + } + fn send_request( &mut self, request: Request, @@ -245,11 +414,28 @@ impl WasiHttpHooks for HttpHooks { Box::new(async move { let _stats_timer = ExtStatsTimer::new(stats); // keep timer alive until response head is in + // Set once the response headers are in; the request-body wrapper + // only refunds waits that fall outside the send-phase window below. + let headers_at = Arc::new(OnceLock::new()); + let request = if epoch_exclude_http_wait { + request.map(|body| { + RequestBodyEpochRefund { + inner: body, + refund: EpochRefund::new(epoch_pause_ms.clone()), + headers_at: headers_at.clone(), + ready_since: None, + } + .boxed_unsync() + }) + } else { + request + }; let started = Instant::now(); let sent = default_send_request(request, options).await; let elapsed = started.elapsed(); if epoch_exclude_http_wait { epoch_pause_ms.fetch_add(elapsed.as_millis() as u64, Ordering::Relaxed); + let _ = headers_at.set(Instant::now()); } // One access-log record per outbound request (status 0 = no response // received, e.g. connection refused / timeout). @@ -261,27 +447,23 @@ impl WasiHttpHooks for HttpHooks { handle.log(&log_method, &log_uri, status, elapsed); } let (response, io) = sent?; - Ok(( - response.map(BodyExt::boxed_unsync), - Box::new(io) as HttpIoFuture, - )) + // The send-phase refund above stops at the response headers; the + // body wrappers keep refunding network wait past that point. + let response = if epoch_exclude_http_wait { + response.map(|body| { + ResponseBodyEpochRefund { + inner: body.boxed_unsync(), + refund: EpochRefund::new(epoch_pause_ms.clone()), + pending_since: None, + } + .boxed_unsync() + }) + } else { + response.map(BodyExt::boxed_unsync) + }; + Ok((response, Box::new(io) as HttpIoFuture)) }) } - - fn is_forbidden_header(&mut self, name: &HeaderName) -> bool { - // We want to allow the host header to be set. - if name.eq(&header::HOST) { - return false; - } - // Block all headers with the reserved `fastedge` prefix — these are - // internal routing headers that guest modules must not set. - let name_str = name.as_str(); - if name_str.starts_with("fastedge-") || name_str.starts_with("fastedge_") { - return true; - } - // Fall back to wasmtime's default forbidden-header policy. - DEFAULT_FORBIDDEN_HEADERS.contains(name) - } } impl Data { From 693b2df4d6df04ac4ef57744e4f645caef30ba8a Mon Sep 17 00:00:00 2001 From: Ruslan Pislari Date: Thu, 1 Oct 2026 10:17:23 +0300 Subject: [PATCH 20/22] chore(runtime): upgrade Wasmtime to 48.0.2 and update dependency management - Updated `update-preview1.sh` for improved Wasmtime version tracking and script reliability. - Updated `Cargo.lock` to include Wasmtime 48.0.2 and dependency adjustments. - Refactored references to dependencies for consistency and removed outdated sources. --- Cargo.lock | 22 +++++++++--------- .../wasi_snapshot_preview1.reactor.wasm | Bin 81115 -> 51634 bytes 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 25cc260..7665c17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -77,7 +77,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -88,7 +88,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -346,7 +346,7 @@ dependencies = [ "maybe-owned", "rustix", "rustix-linux-procfs", - "windows-sys 0.61.2", + "windows-sys 0.60.2", "winx", ] @@ -889,7 +889,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -1819,7 +1819,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2120,7 +2120,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -2851,7 +2851,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3155,7 +3155,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -3256,10 +3256,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.3", + "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -4320,7 +4320,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] diff --git a/crates/runtime/src/adapters/wasi_snapshot_preview1.reactor.wasm b/crates/runtime/src/adapters/wasi_snapshot_preview1.reactor.wasm index 89b68cf7ac50edc2e978840d5081a58098c49d31..ed39fa8e72f345996715831d3385ecfa21c2df27 100644 GIT binary patch literal 51634 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

If|)c*&C~WId%%J7-eyG=!9CKx$Z*y+@~1%sRt+ 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 literal 81115 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 Date: Fri, 9 Oct 2026 14:38:02 +0300 Subject: [PATCH 21/22] chore(dependencies): update multiple crate versions in Cargo.lock --- Cargo.lock | 647 +++++++++++++++++++++++++++++------------------------ 1 file changed, 357 insertions(+), 290 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7665c17..fa5f7b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,9 +13,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" dependencies = [ "memchr", ] @@ -34,9 +34,9 @@ checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b" [[package]] name = "android_system_properties" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" dependencies = [ "libc", ] @@ -99,9 +99,9 @@ checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "arbitrary" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +checksum = "3bc62ac97cc33321f50863d514c3bc38a453947a8f9e781137e47c7401020aed" dependencies = [ "derive_arbitrary", ] @@ -134,13 +134,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.91" +version = "0.1.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.6", ] [[package]] @@ -193,9 +193,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.13.1" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" [[package]] name = "block-buffer" @@ -208,9 +208,9 @@ dependencies = [ [[package]] name = "borsh" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a88b7ea17d208c4193f2c1e6de3c35fe71f98c96982d5ced308bdcc749ff6e1f" +checksum = "553c5d846a6ba5150c65e3b1b8ec073bcf1abc20f9b7220de384a4443ea4e20a" dependencies = [ "bytes", "cfg_aliases", @@ -236,13 +236,13 @@ dependencies = [ [[package]] name = "bytemuck_derive" -version = "1.11.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f65693059b6b9c588b9f62fed1cedbf0a8b805631457ea162d68f0de186f3de5" +checksum = "6a1f896587b6f2c069c73d2f0913e2d590c3990285cd2f0b6aa02b786b4c679c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.6", ] [[package]] @@ -259,9 +259,9 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "bytesize" -version = "2.4.2" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d7c8918969267b2932ffd5655509bbbea0833823058c378876953217f5fc50e" +checksum = "7354288c522e7e980fafd2075d63d1285794c3a6a16cdd492f189ea406e5f18b" [[package]] name = "cache" @@ -352,9 +352,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.4.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +checksum = "f74872d07caf508b30a21f6836e7d7016a2eaf7d9ff4f48deaa58cd8a0407630" dependencies = [ "find-msvc-tools", "jobserver", @@ -364,9 +364,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" [[package]] name = "cfg_aliases" @@ -407,9 +407,9 @@ checksum = "bba18ee93d577a8428902687bcc2b6b45a56b1981a1f6d779731c86cc4c5db18" [[package]] name = "clap" -version = "4.6.4" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91e0c145792ef73a6ad36d27c75ac09f1832222a3c209689d90f534685ee5b7" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" dependencies = [ "clap_builder", "clap_derive", @@ -417,9 +417,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.2" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" dependencies = [ "anstream", "anstyle", @@ -429,21 +429,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.4" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" +checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0" dependencies = [ "heck", "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.6", ] [[package]] name = "clap_lex" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" [[package]] name = "cobs" @@ -451,7 +451,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" dependencies = [ - "thiserror 2.0.19", + "thiserror 2.0.21", ] [[package]] @@ -472,9 +472,9 @@ dependencies = [ [[package]] name = "combine" -version = "4.6.7" +version = "4.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" dependencies = [ "bytes", "futures-core", @@ -500,6 +500,12 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + [[package]] name = "cpp_demangle" version = "0.5.1" @@ -679,27 +685,27 @@ version = "0.135.2" [[package]] name = "crc32fast" -version = "1.5.0" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" dependencies = [ "cfg-if", ] [[package]] name = "crossbeam-channel" -version = "0.5.16" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -707,18 +713,18 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.20" +version = "0.9.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.22" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" [[package]] name = "crunchy" @@ -747,13 +753,13 @@ dependencies = [ [[package]] name = "derive_arbitrary" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +checksum = "1b034bd7d5f032402a2479444dcc6f74e36a03f31854d41680fb240ef682a1ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.6", ] [[package]] @@ -789,13 +795,13 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.6", ] [[package]] @@ -826,9 +832,9 @@ checksum = "e1d926b4d407d372f141f93bb444696142c29d32962ccbd3531117cf3aa0bfa9" [[package]] name = "either" -version = "1.17.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" +checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" [[package]] name = "embedded-io" @@ -844,11 +850,16 @@ checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" [[package]] name = "encoding_rs" -version = "0.8.35" +version = "0.8.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +checksum = "8e985e0451871ad22fb8d2b6b076e2028a502a0d3950998c2c5c0a4f9b5d9679" dependencies = [ "cfg-if", + "core_detect", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", ] [[package]] @@ -959,9 +970,9 @@ checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] name = "find-msvc-tools" -version = "0.1.9" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" [[package]] name = "fixedbitset" @@ -1018,9 +1029,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" dependencies = [ "futures-channel", "futures-core", @@ -1033,9 +1044,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" dependencies = [ "futures-core", "futures-sink", @@ -1043,15 +1054,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" [[package]] name = "futures-executor" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" dependencies = [ "futures-core", "futures-task", @@ -1060,38 +1071,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" [[package]] name = "futures-macro" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.6", ] [[package]] name = "futures-sink" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" [[package]] name = "futures-task" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" [[package]] name = "futures-util" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ "futures-channel", "futures-core", @@ -1110,7 +1121,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25234f20a3ec0a962a61770cfe39ecf03cb529a6e474ad8cff025ed497eda557" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "debugid", "rustc-hash", "serde", @@ -1414,9 +1425,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.15" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" +checksum = "7d29020232d6aa3fb1daca64c1127cf662cf97f254ae16c18c05b8ab635fc118" dependencies = [ "atomic-waker", "bytes", @@ -1474,15 +1485,15 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" [[package]] name = "http" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" dependencies = [ "bytes", "itoa", @@ -1520,9 +1531,9 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" dependencies = [ "bytes", "futures-core", @@ -1582,9 +1593,9 @@ checksum = "15cdd26707701c53297e2fa6afb323d55fbc1d0810c3aec078ae3ef0424c3c15" [[package]] name = "hyper" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +checksum = "2c3e324da4c95177d6291d4c8730197c0d1822f8a9766814a4a44fa5ab797c9c" dependencies = [ "atomic-waker", "bytes", @@ -1620,15 +1631,16 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.20" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff" dependencies = [ "bytes", "futures-channel", "futures-util", "http", "http-body", + "httparse", "hyper", "libc", "pin-project-lite", @@ -1664,9 +1676,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" dependencies = [ "displaydoc", "potential_utf", @@ -1678,9 +1690,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" dependencies = [ "displaydoc", "litemap", @@ -1691,9 +1703,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -1705,16 +1717,17 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" [[package]] name = "icu_properties" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" dependencies = [ + "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", @@ -1725,15 +1738,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" [[package]] name = "icu_provider" -version = "2.2.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" dependencies = [ "displaydoc", "icu_locale_core", @@ -1773,9 +1786,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.14.0" +version = "2.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" dependencies = [ "equivalent", "hashbrown 0.17.1", @@ -1807,9 +1820,9 @@ checksum = "2f0fb0570afe1fed943c5c3d4102d5358592d8625fda6a0007fdbe65a92fba96" [[package]] name = "ipnet" -version = "2.12.0" +version = "2.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" [[package]] name = "is-terminal" @@ -1884,9 +1897,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.103" +version = "0.3.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" dependencies = [ "cfg-if", "futures-util", @@ -1909,9 +1922,9 @@ dependencies = [ [[package]] name = "lazy_static" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb" [[package]] name = "leb128" @@ -1927,9 +1940,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.189" +version = "0.2.190" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" [[package]] name = "libloading" @@ -1949,9 +1962,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.18" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" dependencies = [ "libc", ] @@ -1964,9 +1977,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" [[package]] name = "lock_api" @@ -1979,9 +1992,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.33" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" [[package]] name = "mach2" @@ -2012,9 +2025,9 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "memfd" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" +checksum = "57804b2c9b69967f1536a56f86297e367a33b19e98852ed624b84551cdbc0d90" dependencies = [ "rustix", ] @@ -2040,9 +2053,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.2" +version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee" dependencies = [ "libc", "wasi", @@ -2068,9 +2081,9 @@ dependencies = [ [[package]] name = "moka" -version = "0.12.15" +version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046" +checksum = "4293f18e7567a1caf3c584855554377025c65e0aa445344d04171f5ad63d19b9" dependencies = [ "crossbeam-channel", "crossbeam-epoch", @@ -2083,6 +2096,12 @@ dependencies = [ "uuid", ] +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + [[package]] name = "native-tls" version = "0.2.18" @@ -2129,7 +2148,7 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" dependencies = [ - "num-bigint", + "num-bigint 0.4.8", "num-complex", "num-integer", "num-iter", @@ -2147,6 +2166,16 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e7820bc0a80a0238e650327316f929ba18d5be054b647490a3a6a339f3e7c0" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-complex" version = "0.4.6" @@ -2159,9 +2188,9 @@ dependencies = [ [[package]] name = "num-integer" -version = "0.1.46" +version = "0.1.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" dependencies = [ "num-traits", ] @@ -2182,7 +2211,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" dependencies = [ - "num-bigint", + "num-bigint 0.4.8", "num-integer", "num-traits", ] @@ -2259,7 +2288,7 @@ version = "0.10.81" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "cfg-if", "foreign-types", "libc", @@ -2408,21 +2437,21 @@ checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "13bee6c73da26345c729282832b60b0363cf3dd9f4bfd81d8551b7a1c889a113" [[package]] name = "pkg-config" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" [[package]] name = "portable-atomic" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" [[package]] name = "postcard" @@ -2438,9 +2467,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" dependencies = [ "zerovec", ] @@ -2649,7 +2678,7 @@ version = "11.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", ] [[package]] @@ -2687,9 +2716,9 @@ checksum = "03251193000f4bd3b042892be858ee50e8b3719f2b08e5833ac4353724632430" [[package]] name = "redis" -version = "1.4.1" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0b9503711b03773e43b31668c7b5bd279ee7cd9b7d18cff7c23a42cc1d08e5a" +checksum = "ed5b98ae99461e02895c3a96a25dbe4d852a3d5e5b815d31d5d9e4b5344d99e9" dependencies = [ "arc-swap", "arcstr", @@ -2702,7 +2731,7 @@ dependencies = [ "futures-util", "itoa", "native-tls", - "num-bigint", + "num-bigint 0.5.1", "percent-encoding", "pin-project-lite", "ryu", @@ -2721,7 +2750,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", ] [[package]] @@ -2764,9 +2793,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.16" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" dependencies = [ "aho-corasick", "memchr", @@ -2843,11 +2872,11 @@ checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustix" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "errno", "libc", "linux-raw-sys", @@ -2966,7 +2995,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "core-foundation", "core-foundation-sys", "libc", @@ -3026,7 +3055,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.6", ] [[package]] @@ -3053,9 +3082,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +checksum = "4e7523beb55eece201a2356bee0bbca0d1ab466c14c07703b2e0ee6d42cb0c2c" dependencies = [ "serde_core", ] @@ -3123,6 +3152,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "slab" version = "0.4.12" @@ -3131,9 +3166,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.2" +version = "1.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" dependencies = [ "serde", ] @@ -3189,9 +3224,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.3" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" dependencies = [ "proc-macro2", "quote", @@ -3209,13 +3244,24 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + [[package]] name = "sysctl" version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7dddc5f0fee506baf8b9fdb989e242f17e4b11c61dfbb0635b705217199eea" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "byteorder", "enum-as-inner", "libc", @@ -3229,7 +3275,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01198a2debb237c62b6826ec7081082d951f46dbb64b0e8c7649a452230d1dfc" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "byteorder", "enum-as-inner", "libc", @@ -3256,7 +3302,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.3", "once_cell", "rustix", "windows-sys 0.52.0", @@ -3273,34 +3319,34 @@ dependencies = [ [[package]] name = "test-case" -version = "3.3.1" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb2550dd13afcd286853192af8601920d959b14c401fcece38071d53bf0768a8" +checksum = "124953e7f67cb0b2fcfb87e899e4ae5a64fb68e2e160767933cc67b646fb0042" dependencies = [ "test-case-macros", ] [[package]] name = "test-case-core" -version = "3.3.1" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adcb7fd841cd518e279be3d5a3eb0636409487998a4aff22f3de87b81e88384f" +checksum = "bd097615b407247e102046ba34426bb594923ab3554cf0f3cdfed050f1a5a3e8" dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.6", ] [[package]] name = "test-case-macros" -version = "3.3.1" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" +checksum = "f4ce35ece947bccac166e1ded639133827a953f69a48ba134d17f91108333082" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.6", "test-case-core", ] @@ -3315,11 +3361,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.19" +version = "2.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" dependencies = [ - "thiserror-impl 2.0.19", + "thiserror-impl 2.0.21", ] [[package]] @@ -3335,13 +3381,13 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.19" +version = "2.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.6", ] [[package]] @@ -3355,9 +3401,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" dependencies = [ "displaydoc", "zerovec", @@ -3365,9 +3411,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.53.1" +version = "1.53.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +checksum = "e95f91fcc7a621e8b030f6aa23c71fe9838ae2fb4d8118b75602a328f5144044" dependencies = [ "bytes", "libc", @@ -3382,13 +3428,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.6", ] [[package]] @@ -3403,9 +3449,9 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.5" +version = "0.26.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c85f2c3ef0b1cd58b36682f4b17aaa995f0e5db534d85692b4903abce21f67" +checksum = "c9cc2678c2cdd569ef8215e2afd7954ada2ae20b4fdd2c5fe6139a3b02d105db" dependencies = [ "rustls", "tokio", @@ -3464,39 +3510,39 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "1.1.1+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053" dependencies = [ "serde_core", ] [[package]] name = "toml_edit" -version = "0.25.13+spec-1.1.0" +version = "0.25.17+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +checksum = "e3641d5bbb5349a79e1020a242d251efbc546ad8048d133958323ce9c40a9c9c" dependencies = [ "indexmap", - "toml_datetime 1.1.1+spec-1.1.0", + "toml_datetime 1.1.2+spec-1.1.0", "toml_parser", "winnow 1.0.4", ] [[package]] name = "toml_parser" -version = "1.1.3+spec-1.1.0" +version = "1.1.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" dependencies = [ "winnow 1.0.4", ] [[package]] name = "toml_writer" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" +checksum = "06bdbd8cfc056b8d2e2e85f29b56a3bdbecb527cef81eb39e3e7b98af4652770" [[package]] name = "tower" @@ -3637,9 +3683,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.24" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" [[package]] name = "unicode-width" @@ -3692,9 +3738,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.24.0" +version = "1.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +checksum = "97277d36b9c3ace13e58fa6e753f8b0bbbf302a18dd193240d70f9e29681059a" dependencies = [ "getrandom 0.4.3", "js-sys", @@ -3731,9 +3777,9 @@ dependencies = [ [[package]] name = "want" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +checksum = "ec4cdd0dd910afe868b7ef477227d8d538b46b3075031afee8a9f2acb0a2ed0b" dependencies = [ "try-lock", ] @@ -3755,9 +3801,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.126" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" dependencies = [ "cfg-if", "once_cell", @@ -3768,9 +3814,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.126" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3778,31 +3824,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.126" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.6", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.126" +version = "0.2.129" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" dependencies = [ "unicode-ident", ] [[package]] name = "wasm-compose" -version = "0.254.0" +version = "0.254.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd717357bff09b5f1ea49aea8ed1cd149b0c521823a1cc70bc2ac16b90b651a" +checksum = "4c0c1033c10d23d96f40e761ae66cc30c70cea3323a8edfe9159dac42108873f" dependencies = [ "anyhow", "heck", @@ -3810,8 +3856,8 @@ dependencies = [ "log", "petgraph", "smallvec", - "wasm-encoder 0.254.0", - "wasmparser 0.254.0", + "wasm-encoder 0.254.2", + "wasmparser 0.254.2", "wat", ] @@ -3827,12 +3873,22 @@ dependencies = [ [[package]] name = "wasm-encoder" -version = "0.254.0" +version = "0.254.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09480d646178e5fdd12bb06e812d0af9a3a191dbc9cd697fdc86687beade7393" +checksum = "ebaca0b540bdb15d195ff34fb23074255948708b8916a11a339ba4513dc61906" dependencies = [ "leb128fmt", - "wasmparser 0.254.0", + "wasmparser 0.254.2", +] + +[[package]] +name = "wasm-encoder" +version = "0.261.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2608e8bb6d67fd68f5a8d0eb1363d6e7bcbc1f8ded5a0bd3a1e382462b876b22" +dependencies = [ + "leb128fmt", + "wasmparser 0.261.0", ] [[package]] @@ -3849,14 +3905,14 @@ dependencies = [ [[package]] name = "wasm-metadata" -version = "0.254.0" +version = "0.254.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b01df5f3b4ca7881e843f3bc0fb8a3905d79c68692250dcb8e33e698705ccdb6" +checksum = "5d1880dd9a848aa68f387966256c34ad49abb4607e3cfa35720d2723d0591cd3" dependencies = [ "anyhow", "indexmap", - "wasm-encoder 0.254.0", - "wasmparser 0.254.0", + "wasm-encoder 0.254.2", + "wasmparser 0.254.2", ] [[package]] @@ -3865,7 +3921,7 @@ version = "0.246.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71cde4757396defafd25417cfb36aa3161027d06d865b0c24baaae229aac005d" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "hashbrown 0.16.1", "indexmap", "semver", @@ -3873,26 +3929,37 @@ dependencies = [ [[package]] name = "wasmparser" -version = "0.254.0" +version = "0.254.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5769a29f799fbab136aaf65b4fe5384cd7d93fe6fc9ba0dcb6c8382a1f16e27" +checksum = "c5ff8685b587722894b6f25958fc608d315b93b8d23647bfb567664e7006d757" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "hashbrown 0.17.1", "indexmap", "semver", "serde", ] +[[package]] +name = "wasmparser" +version = "0.261.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f20f20e44f7e8aeb6744823ea9d869ede51e51be4fdaedede2852282e54d2d8" +dependencies = [ + "bitflags 2.13.2", + "indexmap", + "semver", +] + [[package]] name = "wasmprinter" -version = "0.254.0" +version = "0.254.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64e3ba11e024f504698f87b629b2b66c22a1231758de7607754963f7d198be39" +checksum = "8b03c8f270f3f384f70f42869b5ff7f6253fe4f0aa8590f7edd332c1943e5f51" dependencies = [ "anyhow", "termcolor", - "wasmparser 0.254.0", + "wasmparser 0.254.2", ] [[package]] @@ -3901,7 +3968,7 @@ version = "48.0.2" dependencies = [ "addr2line", "async-trait", - "bitflags 2.13.1", + "bitflags 2.13.2", "bumpalo", "cc", "encoding_rs", @@ -3927,8 +3994,8 @@ dependencies = [ "target-lexicon", "tempfile", "wasm-compose", - "wasm-encoder 0.254.0", - "wasmparser 0.254.0", + "wasm-encoder 0.254.2", + "wasmparser 0.254.2", "wasmtime-environ 48.0.2", "wasmtime-internal-cache", "wasmtime-internal-component-macro", @@ -3942,7 +4009,7 @@ dependencies = [ "wasmtime-internal-versioned-export-macros", "wat", "windows-sys 0.61.2", - "wit-parser 0.254.0", + "wit-parser 0.254.2", ] [[package]] @@ -3967,8 +4034,8 @@ dependencies = [ "sha2", "smallvec", "target-lexicon", - "wasm-encoder 0.254.0", - "wasmparser 0.254.0", + "wasm-encoder 0.254.2", + "wasmparser 0.254.2", "wasmprinter", "wasmtime-internal-component-util", "wasmtime-internal-core 48.0.2", @@ -3994,7 +4061,7 @@ dependencies = [ "sha2", "smallvec", "target-lexicon", - "wasmparser 0.254.0", + "wasmparser 0.254.2", "wasmtime-internal-core 48.0.2 (git+https://github.com/G-Core/wasmtime.git?branch=release-48.0.0)", ] @@ -4026,7 +4093,7 @@ dependencies = [ "syn 2.0.119", "wasmtime-internal-component-util", "wasmtime-internal-wit-bindgen", - "wit-parser 0.254.0", + "wit-parser 0.254.2", ] [[package]] @@ -4069,8 +4136,8 @@ dependencies = [ "pulley-interpreter", "smallvec", "target-lexicon", - "thiserror 2.0.19", - "wasmparser 0.254.0", + "thiserror 2.0.21", + "wasmparser 0.254.2", "wasmtime-environ 48.0.2", "wasmtime-internal-core 48.0.2", "wasmtime-internal-unwinder", @@ -4132,11 +4199,11 @@ name = "wasmtime-internal-wit-bindgen" version = "48.0.2" dependencies = [ "anyhow", - "bitflags 2.13.1", + "bitflags 2.13.2", "heck", "indexmap", - "wit-component 0.254.0", - "wit-parser 0.254.0", + "wit-component 0.254.2", + "wit-parser 0.254.2", ] [[package]] @@ -4144,14 +4211,14 @@ name = "wasmtime-wasi" version = "48.0.2" dependencies = [ "async-trait", - "bitflags 2.13.1", + "bitflags 2.13.2", "bytes", "cap-primitives", "futures", "rand 0.10.3", "rustix", "rustix-linux-procfs", - "thiserror 2.0.19", + "thiserror 2.0.21", "tokio", "tracing", "url", @@ -4216,7 +4283,7 @@ dependencies = [ "candle-nn", "candle-transformers", "openvino", - "thiserror 2.0.19", + "thiserror 2.0.21", "tracing", "walkdir", "wasmtime", @@ -4235,24 +4302,24 @@ dependencies = [ [[package]] name = "wast" -version = "254.0.0" +version = "261.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ed4dfc8f6b9fc38b231065e2cdfbf7359af5ab945990abf09658dcc63c3e32" +checksum = "776443145731a4062e5b0d392892a2005909b6ab72d9fdc3cad53dd1a714e44a" dependencies = [ "bumpalo", "leb128fmt", "memchr", "unicode-width", - "wasm-encoder 0.254.0", + "wasm-encoder 0.261.0", ] [[package]] name = "wat" -version = "1.254.0" +version = "1.261.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7127f7f9b8f127c879991cecd35f494e4628bae1b0874c681414d8d8831e952c" +checksum = "a7b4d1a49ea73a8f3326e74e3a05db667001b16bd1035ed3356fc1a0ed05ca7f" dependencies = [ - "wast 254.0.0", + "wast 261.0.0", ] [[package]] @@ -4268,8 +4335,8 @@ dependencies = [ name = "wiggle" version = "48.0.2" dependencies = [ - "bitflags 2.13.1", - "thiserror 2.0.19", + "bitflags 2.13.2", + "thiserror 2.0.21", "tracing", "wasmtime", "wasmtime-environ 48.0.2", @@ -4617,7 +4684,7 @@ version = "0.36.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "windows-sys 0.59.0", ] @@ -4634,7 +4701,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1936c26cb24b93dc36bf78fb5dc35c55cd37f66ecdc2d2663a717d9fb3ee951e" dependencies = [ "anyhow", - "bitflags 2.13.1", + "bitflags 2.13.2", "indexmap", "log", "serde", @@ -4648,21 +4715,21 @@ dependencies = [ [[package]] name = "wit-component" -version = "0.254.0" +version = "0.254.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0e65bb94c369b3c4741ce3d1d2704b1fec93db7c540df0e521a097e7ceeb5be" +checksum = "1c96b3cea6838dbea55173baa56991fcb087896c321b0a770db201e5a0bd3506" dependencies = [ "anyhow", - "bitflags 2.13.1", + "bitflags 2.13.2", "indexmap", "log", "serde", "serde_derive", "serde_json", - "wasm-encoder 0.254.0", - "wasm-metadata 0.254.0", - "wasmparser 0.254.0", - "wit-parser 0.254.0", + "wasm-encoder 0.254.2", + "wasm-metadata 0.254.2", + "wasmparser 0.254.2", + "wit-parser 0.254.2", ] [[package]] @@ -4686,9 +4753,9 @@ dependencies = [ [[package]] name = "wit-parser" -version = "0.254.0" +version = "0.254.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1655131e4f7d3f0cb141f6eca71315ca40eff0f3d4de7cff0a82bacedd8c89b4" +checksum = "22672d61bfb9df46a16b46ce38522293aee1cc4749fc84a3464ce4083b3d5fd6" dependencies = [ "anyhow", "hashbrown 0.17.1", @@ -4700,7 +4767,7 @@ dependencies = [ "serde_derive", "serde_json", "unicode-ident", - "wasmparser 0.254.0", + "wasmparser 0.254.2", ] [[package]] @@ -4717,15 +4784,15 @@ dependencies = [ [[package]] name = "writeable" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" [[package]] name = "xxhash-rust" -version = "0.8.18" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aee1b19627c7c60102ab80d3a9cbe18de90bfe03bfa6c3715447681f0e8c8af6" +checksum = "550a2b930b62486a393c52d5c3b84bff264b28aa437ed64694d31e93b1757af7" [[package]] name = "yoke" @@ -4746,7 +4813,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", - "yoke-derive 0.8.2", + "yoke-derive 0.8.4", "zerofrom", ] @@ -4759,35 +4826,35 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.119", - "synstructure", + "synstructure 0.13.2", ] [[package]] name = "yoke-derive" -version = "0.8.2" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", - "synstructure", + "syn 3.0.6", + "synstructure 0.14.0", ] [[package]] name = "zerocopy" -version = "0.8.55" +version = "0.8.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +checksum = "86502bf56ac7c77571a32e2647bb2a15894565e981fb2a48d7bde2d91c965a9d" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.55" +version = "0.8.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +checksum = "5457206954b06561e2608c7e19cf58b1926586d999c246eebe4502f7e2039d1a" dependencies = [ "proc-macro2", "quote", @@ -4805,27 +4872,27 @@ dependencies = [ [[package]] name = "zerofrom-derive" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", - "synstructure", + "syn 3.0.6", + "synstructure 0.14.0", ] [[package]] name = "zeroize" -version = "1.9.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" [[package]] name = "zerotrie" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" dependencies = [ "displaydoc", "yoke 0.8.3", @@ -4834,9 +4901,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.6" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" dependencies = [ "yoke 0.8.3", "zerofrom", @@ -4845,13 +4912,13 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.3" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.6", ] [[package]] @@ -4886,18 +4953,18 @@ dependencies = [ [[package]] name = "zstd-safe" -version = "7.2.4" +version = "7.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +checksum = "64d80649ab6db9d9f6f9c80a40becd948eda4714a0a5ac8c4d157a32231c7882" dependencies = [ "zstd-sys", ] [[package]] name = "zstd-sys" -version = "2.0.16+zstd.1.5.7" +version = "2.1.0+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +checksum = "0ef0a8027ec3ee71300ab3bcbcd0393f434aa72b91ca6d635a39941deae8eea0" dependencies = [ "cc", "pkg-config", From fdae7ab1ed41ddeebc46a85ca17191ff04e30057 Mon Sep 17 00:00:00 2001 From: Ruslan Pislari Date: Mon, 12 Oct 2026 12:24:22 +0300 Subject: [PATCH 22/22] fix(runtime): consolidate timeout condition checks in error handling --- crates/http-service/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/http-service/src/lib.rs b/crates/http-service/src/lib.rs index cc64468..e6093c4 100644 --- a/crates/http-service/src/lib.rs +++ b/crates/http-service/src/lib.rs @@ -507,9 +507,9 @@ pub(crate) fn fail_reason_of(error: &Error) -> AppResult { wasmtime::Trap::UnreachableCodeReached => AppResult::OOM, _ => AppResult::OTHER, } - } else if root_cause.downcast_ref::().is_some() { - AppResult::TIMEOUT - } else if root_cause.to_string().ends_with("deadline has elapsed") { + } else if root_cause.downcast_ref::().is_some() + || root_cause.to_string().ends_with("deadline has elapsed") + { AppResult::TIMEOUT } else { AppResult::OTHER