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:
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user