core: sausage-vfs per process (/, /proc, /self, /run), activation, graph, watcher, upgrades

- /proc is a core layer: core/ (PROTOCOL.md, graph.json, graph.dot,
  locks.json, plugins/<name>/discover.json and client.rs) and one folder
  per process (info.json, log, and its /self files).
- Locks are released only after the task ended, the store dropped, and
  in-flight file operations returned.
- core.activate and core.revert with copies, discovery, compatibility
  checks of imports and consumers, groups, force. An admission gate
  makes the snapshot of old processes complete.
- Graph of discover documents by hash, refreshed by a file watcher.
- Upgrades: grace periods (x-sausage-grace), stop, service restarts,
  rpc.retired.
- SDK plugins in the real core (cli/tests/sdk.rs).
This commit is contained in:
2026-10-23 01:05:19 +01:00
parent 151550242b
commit edc2d1a63b
21 changed files with 2306 additions and 101 deletions
+31 -16
View File
@@ -3,7 +3,7 @@
//! An instance is one run of a `wasi:cli/command` component. The host writes
//! NDJSON lines to its stdin and reads NDJSON lines from its stdout.
use std::path::{Path, PathBuf};
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
@@ -17,7 +17,7 @@ use wasmtime_wasi::cli::{AsyncStdinStream, AsyncStdoutStream};
use wasmtime_wasi::p2::bindings::Command;
use wasmtime_wasi::{FsPerms, I32Exit, WasiCtx, WasiCtxView, WasiView};
use wasmtime_wasi::{I32Exit, WasiCtx, WasiCtxView, WasiView};
use wasmtime_wasi_http::{WasiHttpCtx, WasiHttpCtxView, WasiHttpHooks, WasiHttpView};
/// The HTTP transport hooks supplied to WASI processes.
@@ -61,6 +61,7 @@ pub struct ProcessState {
table: ResourceTable,
http: WasiHttpCtx,
hooks: HttpHooks,
fs: sausage_vfs::adapter::Adapter,
}
impl WasiView for ProcessState {
@@ -111,8 +112,10 @@ 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,
/// The virtual namespace exposed through the single root preopen.
pub filesystem: sausage_vfs::Mounts,
/// The store completion barrier used by process cleanup.
pub store_ended: tokio::sync::watch::Sender<bool>,
/// Outgoing HTTP, and TCP, UDP, and DNS through `wasi:sockets`.
pub network: bool,
/// The environment variables supplied to the guest.
@@ -127,12 +130,20 @@ pub struct Instance {
pub stdout: mpsc::Receiver<String>,
/// The stderr lines that the process writes.
pub stderr: mpsc::Receiver<String>,
task: JoinHandle<Exit>,
task: Option<JoinHandle<Exit>>,
store_ended: tokio::sync::watch::Sender<bool>,
}
impl Drop for Instance {
fn drop(&mut self) {
self.task.abort();
if let Some(task) = self.task.take() {
task.abort();
let ended = self.store_ended.clone();
tokio::spawn(async move {
let _ = task.await;
ended.send_replace(true);
});
}
}
}
@@ -166,7 +177,10 @@ impl Instance {
/// Wait until the guest ends.
pub async fn wait(&mut self) -> Exit {
match (&mut self.task).await {
let result = self.task.as_mut().unwrap().await;
self.task.take();
self.store_ended.send_replace(true);
match result {
Ok(exit) => exit,
Err(e) if e.is_cancelled() => Exit::Trap("killed".into()),
Err(e) => Exit::Trap(format!("host task failed: {e}")),
@@ -198,6 +212,10 @@ impl Runtime {
let mut linker: Linker<ProcessState> = Linker::new(&engine);
wasmtime_wasi::p2::add_to_linker_async(&mut linker)?;
sausage_vfs::adapter::add_to_linker(&mut linker, |state| sausage_vfs::adapter::View {
adapter: &mut state.fs,
table: &mut state.table,
})?;
wasmtime_wasi_http::p2::add_only_http_to_linker_async(&mut linker)?;
// Time slices: every 10 ms each running guest yields to the scheduler.
let runtime = Arc::new(Self { engine, linker });
@@ -220,7 +238,7 @@ impl Runtime {
Ok(Component::new(&self.engine, bytes)?)
}
/// Prepare a world preopen off the async workers, then start the WASI task.
/// Start a WASI task with the supplied virtual namespace.
pub async fn start(
self: &Arc<Self>,
component: &Component,
@@ -243,18 +261,13 @@ impl Runtime {
for (k, v) in &cfg.env {
b.env(k, v);
}
// Filesystem setup lives here. T6 replaces this preopen with the VFS.
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(),
http: WasiHttpCtx::new(),
hooks: HttpHooks { allow: cfg.network },
fs: sausage_vfs::adapter::Adapter::new(cfg.filesystem),
};
let (line_tx, line_rx) = mpsc::channel(1024);
@@ -285,6 +298,7 @@ impl Runtime {
});
let rt = self.clone();
let component = component.clone();
cfg.store_ended.send_replace(false);
let task = tokio::spawn(async move {
let mut store = Store::new(&rt.engine, state);
store.epoch_deadline_async_yield_and_update(1);
@@ -311,7 +325,8 @@ impl Runtime {
stdin: Some(host_in),
stdout: line_rx,
stderr: err_rx,
task,
task: Some(task),
store_ended: cfg.store_ended,
})
}
}