sausage core: modules per topic, journal writer thread with WAL and batches, hash cache by metadata, 49 tests per rule

Blank stdout lines give no warning. core.exit requests the exit in its
own handler.
This commit is contained in:
2026-10-23 00:32:33 +01:00
parent 218a718a3d
commit 8542b9fcb8
17 changed files with 2016 additions and 814 deletions
+25 -3
View File
@@ -20,6 +20,7 @@ use wasmtime_wasi::p2::bindings::Command;
use wasmtime_wasi::{FsPerms, I32Exit, WasiCtx, WasiCtxView, WasiView};
use wasmtime_wasi_http::{WasiHttpCtx, WasiHttpCtxView, WasiHttpHooks, WasiHttpView};
/// The HTTP transport hooks supplied to WASI processes.
pub struct HttpHooks {
allow: bool,
}
@@ -54,6 +55,7 @@ impl WasiHttpHooks for HttpHooks {
}
}
/// The WASI and HTTP resources owned by one store.
pub struct ProcessState {
ctx: WasiCtx,
table: ResourceTable,
@@ -80,6 +82,7 @@ impl WasiHttpView for ProcessState {
}
}
/// A Wasmtime engine and linker shared by the world.
pub struct Runtime {
engine: Engine,
linker: Linker<ProcessState>,
@@ -106,18 +109,23 @@ impl std::fmt::Display for Exit {
}
}
/// The filesystem, network, and environment supplied to one process.
pub struct InstanceConfig {
/// The world folder to preopen as the guest root.
pub world: PathBuf,
/// Outgoing HTTP, and TCP, UDP, and DNS through `wasi:sockets`.
pub network: bool,
/// The environment variables supplied to the guest.
pub env: Vec<(String, String)>,
}
/// A running guest. Dropping it kills the guest.
pub struct Instance {
/// The host end of stdin, removed when the process stops.
pub stdin: Option<DuplexStream>,
/// Lines that the guest writes to stdout. Closed when the guest ends.
pub stdout: mpsc::Receiver<String>,
/// The stderr lines that the process writes.
pub stderr: mpsc::Receiver<String>,
task: JoinHandle<Exit>,
}
@@ -178,6 +186,7 @@ fn short_trap(full: &str) -> String {
}
impl Runtime {
/// Create an engine with disk caching and a ten-millisecond epoch tick.
pub fn new(cache: &Path) -> Result<Arc<Self>> {
let mut config = Config::new();
config.epoch_interruption(true);
@@ -206,11 +215,17 @@ impl Runtime {
Ok(runtime)
}
/// Compile component bytes through Wasmtime's disk cache.
pub fn compile(&self, bytes: &[u8]) -> Result<Component> {
Ok(Component::new(&self.engine, bytes)?)
}
pub fn start(self: &Arc<Self>, component: &Component, cfg: InstanceConfig) -> Result<Instance> {
/// Prepare a world preopen off the async workers, then start the WASI task.
pub async fn start(
self: &Arc<Self>,
component: &Component,
cfg: InstanceConfig,
) -> Result<Instance> {
let (host_in, guest_in) = tokio::io::duplex(64 * 1024);
let (guest_out, host_out) = tokio::io::duplex(64 * 1024);
@@ -229,7 +244,12 @@ impl Runtime {
b.env(k, v);
}
// Filesystem setup lives here. T6 replaces this preopen with the VFS.
b.preopened_dir(&cfg.world, "/", FsPerms::ReadWrite)?;
let world = cfg.world;
let mut b = tokio::task::spawn_blocking(move || -> Result<_> {
b.preopened_dir(world, "/", FsPerms::ReadWrite)?;
Ok(b)
})
.await??;
let state = ProcessState {
ctx: b.build(),
table: ResourceTable::new(),
@@ -245,7 +265,9 @@ impl Runtime {
if n == 0 {
break;
}
let line = String::from_utf8(std::mem::take(&mut bytes)).unwrap_or_default();
// Preserve a non-blank invalid line so routing emits its warning.
let line = String::from_utf8(std::mem::take(&mut bytes))
.unwrap_or_else(|_| "\u{fffd}".into());
if line_tx.send(line).await.is_err() {
break;
}