Host: fixes from the review, with regression tests

- one live instance per actor: check after the lock; dropping an instance kills it
- state snapshots keep target/ and .git/; only source copies skip them
- one shutdown deadline; output during shutdown is still routed
- deliveries route stdout meanwhile; a stuck stdin kills the instance
  (no partial lines) and the sender gets undeliverable; control requests
  are not blocked by a stuck delivery
- Host::stop aborts running broker and cargo requests first
- compile a version before installing it; roll back if it does not start
- unique archive names; a failed archive keeps the actor manageable
- commit on close: truncate without create; directory renames
- candidates see SAUSAGE_VERSION and the existing state of a new actor
- the host publishes PROTOCOL.md at /actor/broker/public/
This commit is contained in:
2026-10-15 12:03:26 +01:00
parent 69cf80a114
commit 9d575b6a62
11 changed files with 811 additions and 220 deletions
+9 -14
View File
@@ -18,7 +18,7 @@ use std::time::Instant;
use anyhow::{Context, Result, anyhow, bail};
use serde_json::{Value, json};
use tokio::process::Command;
use tokio::sync::{Mutex, mpsc};
use tokio::sync::mpsc;
use include_dir::{Dir, include_dir};
@@ -38,17 +38,16 @@ pub struct BuildOutput {
pub warnings: String,
}
/// All builds share one target folder, so they run one at a time.
pub struct Cargo {
world: World,
/// One build at a time: all builds share one target folder.
lock: Mutex<()>,
}
impl Cargo {
/// Create the actor and install the SDK of this host into `data/sdk/`.
pub fn new(world: World) -> Result<Self> {
write_dir(&SDK, &world.data_dir(ID).join("sdk"))?;
Ok(Self { world, lock: Mutex::new(()) })
Ok(Self { world })
}
/// Build the crate in `source` (a host path) and publish the `.wasm` file
@@ -57,12 +56,11 @@ impl Cargo {
if !source.join("Cargo.toml").is_file() {
bail!("{} has no Cargo.toml", source.display());
}
let _guard = self.lock.lock().await;
let started = Instant::now();
let data = self.world.data_dir(ID);
let key = format!("{:016x}", fxhash(source.to_string_lossy().as_bytes()));
let build_dir = data.join("work").join(&key);
world::replace_tree(source, &build_dir)?;
world::replace_source(source, &build_dir)?;
let manifest = build_dir.join("Cargo.toml");
std::fs::write(&manifest, prepare_manifest(&std::fs::read_to_string(&manifest)?, Some(&data.join("sdk")))?)?;
@@ -100,16 +98,13 @@ impl Cargo {
Ok(BuildOutput { wasm: dest, seconds: started.elapsed().as_secs_f64(), warnings: strip_progress(&stderr) })
}
pub async fn run_actor(self: Arc<Self>, broker: Arc<Broker>, mut mailbox: mpsc::Receiver<Envelope>) {
/// The native `cargo` actor. Builds run one at a time, in order.
pub async fn run_actor(self, broker: Arc<Broker>, mut mailbox: mpsc::Receiver<Envelope>) {
while let Some(env) = mailbox.recv().await {
let this = self.clone();
let broker = broker.clone();
tokio::spawn(async move {
let body = this.handle(&broker, &env).await.unwrap_or_else(|e| {
json!({"type": "build_failed", "source": env.body.get("source"), "errors": format!("{e:#}")})
});
broker.send(ID, Some(&env.from), None, Some(&env.id), body);
let body = self.handle(&broker, &env).await.unwrap_or_else(|e| {
json!({"type": "build_failed", "source": env.body.get("source"), "errors": format!("{e:#}")})
});
broker.send(ID, Some(&env.from), None, Some(&env.id), body);
}
}