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
+65 -16
View File
@@ -1,19 +1,21 @@
use std::collections::HashMap;
use std::io::{self, BufRead, Write};
use std::time::Duration;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use serde_json::{Value, json};
fn reply(out: &mut impl Write, message: &Value, body: Value) {
writeln!(
out,
"{}",
json!({"to": message["from"], "reply_to": message["id"], "body": body})
)
.unwrap();
fn send(out: &mut impl Write, message: Value) {
writeln!(out, "{message}").unwrap();
out.flush().unwrap();
}
fn reply(out: &mut impl Write, message: &Value, body: Value) {
send(
out,
json!({"to": message["from"], "reply_to": message["id"], "body": body}),
);
}
fn io_error(error: io::Error) -> Value {
json!({"type": "error", "error": error.to_string()})
}
@@ -21,7 +23,8 @@ fn io_error(error: io::Error) -> Value {
fn main() {
let actor = std::env::var("SAUSAGE_ACTOR").unwrap_or_default();
let version = std::env::var("SAUSAGE_VERSION").unwrap_or_else(|_| "candidate".to_string());
if actor == "crashloop" && version == "2" {
let candidate = std::env::var_os("SAUSAGE_CANDIDATE").is_some();
if actor == "crashloop" && version == "2" && !candidate {
std::fs::write("/data/state", "bad-v2").unwrap();
panic!("version 2 crashes on startup");
}
@@ -29,6 +32,14 @@ fn main() {
let mut open = HashMap::<String, std::fs::File>::new();
let stdin = io::stdin();
let mut out = io::stdout().lock();
let mut emit_shutdown = false;
if actor == "spawn-race" && !candidate {
let stamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
std::fs::write(format!("/data/start-{stamp}"), "started").unwrap();
}
for line in stdin.lock().lines() {
let Ok(line) = line else { break };
let Ok(message) = serde_json::from_str::<Value>(&line) else {
@@ -43,17 +54,21 @@ fn main() {
let _ = std::fs::write("/data/shutdown", "stopping");
std::thread::sleep(Duration::from_millis(800));
}
if emit_shutdown {
send(
&mut out,
json!({"to": "console", "reply_to": message["id"], "body": {"type": "shutdown-reply"}}),
);
send(&mut out, json!({"body": {"type": "shutdown-publish"}}));
}
break;
}
Some("echo") => json!({"type": "echo", "value": body["value"]}),
Some("publish") => {
writeln!(
out,
"{}",
json!({"body": {"type": "published", "value": body["value"]}})
)
.unwrap();
out.flush().unwrap();
send(
&mut out,
json!({"body": {"type": "published", "value": body["value"]}}),
);
continue;
}
Some("version") => json!({"type": "version", "version": version}),
@@ -62,6 +77,15 @@ fn main() {
Ok(()) => json!({"type": "ok"}),
Err(error) => io_error(error),
},
Some("mkdir") => match std::fs::create_dir_all(&path) {
Ok(()) => json!({"type": "ok"}),
Err(error) => io_error(error),
},
Some("rename") => match std::fs::rename(&path, body["to"].as_str().unwrap_or_default())
{
Ok(()) => json!({"type": "ok"}),
Err(error) => io_error(error),
},
Some("open") => {
let result = (|| -> io::Result<()> {
let mut file = std::fs::File::create(&path)?;
@@ -74,6 +98,21 @@ fn main() {
Err(error) => io_error(error),
}
}
Some("open-truncate") => {
let result = (|| -> io::Result<()> {
let mut file = std::fs::OpenOptions::new()
.write(true)
.truncate(true)
.open(&path)?;
file.write_all(text.as_bytes())?;
open.insert(path.clone(), file);
Ok(())
})();
match result {
Ok(()) => json!({"type": "opened"}),
Err(error) => io_error(error),
}
}
Some("append") => {
let result = (|| -> io::Result<()> {
let mut file = std::fs::OpenOptions::new().append(true).open(&path)?;
@@ -87,6 +126,11 @@ fn main() {
}
}
Some("close") => json!({"type": "closed", "closed": open.remove(&path).is_some()}),
Some("arm-shutdown") => {
emit_shutdown = true;
json!({"type": "armed"})
}
Some("ignore-stdin") => json!({"type": "ignoring"}),
Some("read") => match std::fs::read_to_string(&path) {
Ok(text) => json!({"type": "content", "text": text}),
Err(error) => io_error(error),
@@ -107,5 +151,10 @@ fn main() {
_ => json!({"type": "error", "error": "unknown message"}),
};
reply(&mut out, &message, response);
if body["type"] == "ignore-stdin" {
loop {
std::thread::sleep(Duration::from_secs(60));
}
}
}
}