feat: add wasmtime host with epoch-bounded WasmProvider

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-05 15:23:11 +02:00
parent 58f4bd4fdf
commit eeec821af2
4 changed files with 533 additions and 5 deletions
Generated
+349 -5
View File
@@ -26,6 +26,21 @@ version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "ambient-authority"
version = "0.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "anyhow"
version = "1.0.102"
@@ -176,6 +191,74 @@ version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[package]]
name = "cap-fs-ext"
version = "3.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5528f85b1e134ae811704e41ef80930f56e795923f866813255bc342cc20654"
dependencies = [
"cap-primitives",
"cap-std",
"io-lifetimes",
"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",
"cap-std",
"rustix",
"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",
"io-lifetimes",
"ipnet",
"maybe-owned",
"rustix",
"rustix-linux-procfs",
"windows-sys 0.52.0",
"winx",
]
[[package]]
name = "cap-std"
version = "3.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6dc3090992a735d23219de5c204927163d922f42f575a0189b005c62d37549a"
dependencies = [
"cap-primitives",
"io-extras",
"io-lifetimes",
"rustix",
]
[[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",
"iana-time-zone",
"once_cell",
"rustix",
"winx",
]
[[package]]
name = "cc"
version = "1.2.63"
@@ -200,6 +283,17 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chacha20"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
dependencies = [
"cfg-if",
"cpufeatures 0.3.0",
"rand_core 0.10.1",
]
[[package]]
name = "cmake"
version = "0.1.58"
@@ -292,6 +386,15 @@ dependencies = [
"libc",
]
[[package]]
name = "cpufeatures"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
dependencies = [
"libc",
]
[[package]]
name = "cranelift-assembler-x64"
version = "0.132.0"
@@ -659,6 +762,17 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "fs-set-times"
version = "0.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a"
dependencies = [
"io-lifetimes",
"rustix",
"windows-sys 0.52.0",
]
[[package]]
name = "fs_extra"
version = "1.3.0"
@@ -813,6 +927,7 @@ dependencies = [
"cfg-if",
"libc",
"r-efi 6.0.0",
"rand_core 0.10.1",
"wasip2",
"wasip3",
]
@@ -987,6 +1102,30 @@ dependencies = [
"windows-registry",
]
[[package]]
name = "iana-time-zone"
version = "0.1.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "icu_collections"
version = "2.2.0"
@@ -1121,6 +1260,22 @@ dependencies = [
"tempfile",
]
[[package]]
name = "io-extras"
version = "0.18.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2285ddfe3054097ef4b2fe909ef8c3bcd1ea52a8f0d274416caebeef39f04a65"
dependencies = [
"io-lifetimes",
"windows-sys 0.52.0",
]
[[package]]
name = "io-lifetimes"
version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06432fb54d3be7964ecd3649233cddf80db2832f47fec34c01f65b3d9d774983"
[[package]]
name = "ipnet"
version = "2.12.0"
@@ -1233,6 +1388,12 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "leb128"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6cc46bac87ef8093eed6f272babb833b6443374399985ac8ed28471ee0918545"
[[package]]
name = "leb128fmt"
version = "0.1.0"
@@ -1319,6 +1480,12 @@ version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
[[package]]
name = "maybe-owned"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4"
[[package]]
name = "memchr"
version = "2.8.1"
@@ -1560,7 +1727,7 @@ dependencies = [
"bytes",
"getrandom 0.3.4",
"lru-slab",
"rand",
"rand 0.9.4",
"ring",
"rustc-hash",
"rustls",
@@ -1614,7 +1781,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
dependencies = [
"rand_chacha",
"rand_core",
"rand_core 0.9.5",
]
[[package]]
name = "rand"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
dependencies = [
"chacha20",
"getrandom 0.4.2",
"rand_core 0.10.1",
]
[[package]]
@@ -1624,7 +1802,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core",
"rand_core 0.9.5",
]
[[package]]
@@ -1636,6 +1814,12 @@ dependencies = [
"getrandom 0.3.4",
]
[[package]]
name = "rand_core"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
[[package]]
name = "rayon"
version = "1.12.0"
@@ -1805,6 +1989,16 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "rustix-linux-procfs"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fc84bf7e9aa16c4f2c758f27412dc9841341e16aa682d9c7ac308fe3ee12056"
dependencies = [
"once_cell",
"rustix",
]
[[package]]
name = "rustls"
version = "0.23.40"
@@ -2031,7 +2225,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"cpufeatures 0.2.17",
"digest",
]
@@ -2995,6 +3189,58 @@ dependencies = [
"wit-parser 0.248.0",
]
[[package]]
name = "wasmtime-wasi"
version = "45.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb1e92a304eaafd672718011c69084041db74fa0fcc3532c5920f492c557b721"
dependencies = [
"async-trait",
"bitflags",
"bytes",
"cap-fs-ext",
"cap-net-ext",
"cap-std",
"cap-time-ext",
"cfg-if",
"fs-set-times",
"futures",
"io-extras",
"io-lifetimes",
"rand 0.10.1",
"rustix",
"thiserror 2.0.18",
"tokio",
"tracing",
"url",
"wasmtime",
"wasmtime-wasi-io",
"wiggle",
"windows-sys 0.61.2",
]
[[package]]
name = "wasmtime-wasi-io"
version = "45.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e0013e1f37d2e0e1b030fa186972f6f5819f69814bb07d3b6d3cab0c40b50e2"
dependencies = [
"async-trait",
"bytes",
"futures",
"tracing",
"wasmtime",
]
[[package]]
name = "wast"
version = "35.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ef140f1b49946586078353a453a1d28ba90adfc54dde75710bc1931de204d68"
dependencies = [
"leb128",
]
[[package]]
name = "wast"
version = "251.0.0"
@@ -3014,7 +3260,7 @@ version = "1.251.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81b1086c9e85b95bd6a229a928bc6c6d0662e42af0250c88d067b418831ea4d4"
dependencies = [
"wast",
"wast 251.0.0",
]
[[package]]
@@ -3074,6 +3320,47 @@ dependencies = [
"tower",
"tracing",
"wasmtime",
"wasmtime-wasi",
]
[[package]]
name = "wiggle"
version = "45.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7c048e5d47058ff8b60cef771dd6276f2cf4508bc7f604b93c8d5d643ad41fc"
dependencies = [
"bitflags",
"thiserror 2.0.18",
"tracing",
"wasmtime",
"wasmtime-environ",
"wiggle-macro",
]
[[package]]
name = "wiggle-generate"
version = "45.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b867ae624c2006976985444321d429ee2d0c6784ca5c6e45bc140c48141bb0c"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
"wasmtime-environ",
"witx",
]
[[package]]
name = "wiggle-macro"
version = "45.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "463d6d4c0c100180fdfc586d555ed1c22376114944841629cff0d746666e30a4"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wiggle-generate",
]
[[package]]
@@ -3126,6 +3413,41 @@ dependencies = [
"wasmtime-internal-cranelift",
]
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.2.1"
@@ -3329,6 +3651,16 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1"
[[package]]
name = "winx"
version = "0.36.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d"
dependencies = [
"bitflags",
"windows-sys 0.52.0",
]
[[package]]
name = "wit-bindgen"
version = "0.51.0"
@@ -3527,6 +3859,18 @@ dependencies = [
"wasmparser 0.248.0",
]
[[package]]
name = "witx"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e366f27a5cabcddb2706a78296a40b8fcc451e1a6aba2fc1d94b4a01bdaaef4b"
dependencies = [
"anyhow",
"log",
"thiserror 1.0.69",
"wast 35.0.2",
]
[[package]]
name = "writeable"
version = "0.6.3"
+1
View File
@@ -16,6 +16,7 @@ thiserror = "2"
tokio = { version = "1", features = ["full"] }
tracing = "0.1"
wasmtime = { version = "45", features = ["component-model"] }
wasmtime-wasi = "45"
[dev-dependencies]
http-body-util = "0.1"
+1
View File
@@ -3,3 +3,4 @@ pub mod fetch;
pub mod http;
pub mod model;
pub mod service;
pub mod wasm;
+182
View File
@@ -0,0 +1,182 @@
use std::path::Path;
use wasmtime::component::{Component, Linker};
use wasmtime::{Config, Engine, Store};
use wasmtime_wasi::{ResourceTable, WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView};
use crate::error::HostError;
use crate::model::{Comment, Entry, FetchedResponse, ParseOutcome};
use crate::service::ProviderHandle;
wasmtime::component::bindgen!({
world: "provider",
path: "../../wit",
});
use exports::whoareyou::provider::lookup::{
LookupError as WitLookupError, Response as WitResponse,
};
/// How many epoch ticks a guest call may run. The epoch thread ticks every
/// 100 ms → 50 ticks ≈ 5 s budget per call.
const EPOCH_DEADLINE_TICKS: u64 = 50;
pub const EPOCH_TICK: std::time::Duration = std::time::Duration::from_millis(100);
pub struct HostState {
ctx: WasiCtx,
table: ResourceTable,
}
impl WasiView for HostState {
fn ctx(&mut self) -> WasiCtxView<'_> {
WasiCtxView {
ctx: &mut self.ctx,
table: &mut self.table,
}
}
}
pub fn engine() -> Result<Engine, HostError> {
let mut config = Config::new();
config.epoch_interruption(true);
Ok(Engine::new(&config)?)
}
pub fn linker(engine: &Engine) -> Result<Linker<HostState>, HostError> {
let mut linker = Linker::new(engine);
wasmtime_wasi::p2::add_to_linker_sync(&mut linker)?;
Ok(linker)
}
/// Spawn the thread that advances the engine epoch so runaway guest calls
/// trap instead of hanging the service. Call once at startup.
pub fn spawn_epoch_thread(engine: &Engine) {
let engine = engine.clone();
std::thread::spawn(move || {
loop {
std::thread::sleep(EPOCH_TICK);
engine.increment_epoch();
}
});
}
pub struct WasmProvider {
name: String,
version: String,
engine: Engine,
pre: ProviderPre<HostState>,
}
impl WasmProvider {
/// Compile a component from disk and read its metadata once.
/// Fails fast if the component does not satisfy the provider world.
pub fn load(
engine: &Engine,
linker: &Linker<HostState>,
path: &Path,
) -> Result<Self, HostError> {
let component = Component::from_file(engine, path)?;
let pre = ProviderPre::new(linker.instantiate_pre(&component)?)?;
let mut provider = Self {
name: String::new(),
version: String::new(),
engine: engine.clone(),
pre,
};
let mut store = provider.new_store();
let instance = provider.pre.instantiate(&mut store)?;
let info = instance
.whoareyou_provider_lookup()
.call_metadata(&mut store)?;
provider.name = info.name;
provider.version = info.version;
Ok(provider)
}
pub fn version(&self) -> &str {
&self.version
}
fn new_store(&self) -> Store<HostState> {
// No preopens, no env, no inherited stdio — fully sandboxed guest.
let ctx = WasiCtxBuilder::new().build();
let mut store = Store::new(
&self.engine,
HostState {
ctx,
table: ResourceTable::new(),
},
);
store.set_epoch_deadline(EPOCH_DEADLINE_TICKS);
store
}
}
impl ProviderHandle for WasmProvider {
fn name(&self) -> &str {
&self.name
}
fn requests(&self, number: &str) -> Result<Vec<String>, HostError> {
let mut store = self.new_store();
let instance = self.pre.instantiate(&mut store)?;
let requests = instance
.whoareyou_provider_lookup()
.call_requests(&mut store, number)?;
Ok(requests.into_iter().map(|r| r.url).collect())
}
fn parse(&self, number: &str, responses: &[FetchedResponse]) -> ParseOutcome {
let wit_responses: Vec<WitResponse> = responses
.iter()
.map(|r| WitResponse {
status: r.status,
body: r.body.clone(),
})
.collect();
let mut store = self.new_store();
let result: Result<Result<_, WitLookupError>, wasmtime::Error> = (|| {
let instance = self.pre.instantiate(&mut store)?;
instance
.whoareyou_provider_lookup()
.call_parse(&mut store, number, &wit_responses)
})();
match result {
Ok(Ok(entry)) => ParseOutcome::Ok(Entry {
messages: entry.messages,
history: entry.history,
comments: entry
.comments
.into_iter()
.map(|c| Comment {
timestamp: c.timestamp,
title: c.title,
message: c.message,
})
.collect(),
}),
Ok(Err(WitLookupError::NoData)) => ParseOutcome::NoData,
Ok(Err(WitLookupError::ParseFailed(message))) => ParseOutcome::Failed(message),
// Trap (incl. epoch deadline exceeded) or instantiation failure.
Err(error) => ParseOutcome::Failed(format!("component error: {error}")),
}
}
}