Broker exit request and nix run .#loop for self-updating hosts
- broker: `{"type":"exit"}` answers `exiting`; `sausage run` then stops all
actors and exits with code 75.
- flake: apps `loop` / `loop-internal` run `nix run --refresh git+ssh://.../surma/sausage -- run <world>`
in a loop; exit code 0 (/quit) ends the loop, anything else restarts.
- PROTOCOL.md, README.md, test in cli/tests/host.rs.
Not compiled: written by the Sausage agent, which cannot build the native host.
This commit is contained in:
+2
-1
@@ -83,7 +83,7 @@ message.
|
|||||||
Requests to `broker`. The answer is a message whose `reply_to` is the ID of
|
Requests to `broker`. The answer is a message whose `reply_to` is the ID of
|
||||||
the request: the request's `type` in past tense (`spawned`, `upgraded`,
|
the request: the request's `type` in past tense (`spawned`, `upgraded`,
|
||||||
`rolled_back`, `stopped`, `started`, `subscribed`, `unsubscribed`,
|
`rolled_back`, `stopped`, `started`, `subscribed`, `unsubscribed`,
|
||||||
`scanned`), or `{"type": "error", "reason": "..."}`.
|
`scanned`, `exiting`), or `{"type": "error", "reason": "..."}`.
|
||||||
|
|
||||||
| `type` | Fields | Effect |
|
| `type` | Fields | Effect |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
@@ -93,6 +93,7 @@ the request: the request's `type` in past tense (`spawned`, `upgraded`,
|
|||||||
| `stop`, `start` | `actor` | stop or start an actor |
|
| `stop`, `start` | `actor` | stop or start an actor |
|
||||||
| `subscribe`, `unsubscribe` | `actor` | receive (or not) what `actor` publishes |
|
| `subscribe`, `unsubscribe` | `actor` | receive (or not) what `actor` publishes |
|
||||||
| `scan` | | boot new or changed `actor.wasm` files |
|
| `scan` | | boot new or changed `actor.wasm` files |
|
||||||
|
| `exit` | | answer `exiting`, stop every actor, and end the `sausage` process (a wrapper like `nix run .#loop` starts it again, updated) |
|
||||||
|
|
||||||
Paths are `/actor/...`, or the sender's `/public/...` or `/data/...`.
|
Paths are `/actor/...`, or the sender's `/public/...` or `/data/...`.
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,16 @@ the base URL, the model, and the token. It reads `SAUSAGE_API`,
|
|||||||
values go to `world/actor/agent/data/config.json`. Both commands take another
|
values go to `world/actor/agent/data/config.json`. Both commands take another
|
||||||
folder as an argument, for example `nix run -- run my-world`.
|
folder as an argument, for example `nix run -- run my-world`.
|
||||||
|
|
||||||
|
To keep a world on the newest version, run it in a loop:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
nix run .#loop -- my-world # or .#loop-internal (Gitea on the LAN)
|
||||||
|
```
|
||||||
|
|
||||||
|
The loop runs `nix run --refresh git+ssh://…/surma/sausage -- run my-world`.
|
||||||
|
When the host crashes or an actor sends `@broker {"type":"exit"}`, the loop
|
||||||
|
fetches the newest commit and starts it again. `/quit` ends the loop.
|
||||||
|
|
||||||
In the console:
|
In the console:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
|||||||
+18
-2
@@ -1,6 +1,6 @@
|
|||||||
//! The broker: message routing, mailboxes, subscriptions, the version store,
|
//! The broker: message routing, mailboxes, subscriptions, the version store,
|
||||||
//! and the requests to `broker` (spawn, upgrade, rollback, stop, start,
|
//! and the requests to `broker` (spawn, upgrade, rollback, stop, start,
|
||||||
//! subscribe, unsubscribe, scan). `supervisor.rs` runs each WASM actor.
|
//! subscribe, unsubscribe, scan, exit). `supervisor.rs` runs each WASM actor.
|
||||||
//!
|
//!
|
||||||
//! The broker accepts only `.wasm` files. It does not know any source language.
|
//! The broker accepts only `.wasm` files. It does not know any source language.
|
||||||
//! The world folder is the source of truth: `scan` boots every
|
//! The world folder is the source of truth: `scan` boots every
|
||||||
@@ -58,6 +58,8 @@ pub struct Broker {
|
|||||||
metas: Mutex<BTreeMap<String, ActorMeta>>,
|
metas: Mutex<BTreeMap<String, ActorMeta>>,
|
||||||
op_locks: Mutex<HashMap<String, Arc<tokio::sync::Mutex<()>>>>,
|
op_locks: Mutex<HashMap<String, Arc<tokio::sync::Mutex<()>>>>,
|
||||||
seq: AtomicU64,
|
seq: AtomicU64,
|
||||||
|
/// Set by the `exit` request: the host process should stop.
|
||||||
|
exit: tokio::sync::Notify,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Broker {
|
impl Broker {
|
||||||
@@ -71,9 +73,17 @@ impl Broker {
|
|||||||
metas: Mutex::new(metas),
|
metas: Mutex::new(metas),
|
||||||
op_locks: Mutex::new(HashMap::new()),
|
op_locks: Mutex::new(HashMap::new()),
|
||||||
seq: AtomicU64::new(1),
|
seq: AtomicU64::new(1),
|
||||||
|
exit: tokio::sync::Notify::new(),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Wait until an actor sends `{"type": "exit"}` to `broker`. The `sausage`
|
||||||
|
/// binary then stops all actors and exits, so that a wrapper such as
|
||||||
|
/// `nix run .#loop` can start a fresh (updated) host.
|
||||||
|
pub async fn exit_requested(&self) {
|
||||||
|
self.exit.notified().await;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn meta(&self, id: &str) -> Option<ActorMeta> {
|
pub fn meta(&self, id: &str) -> Option<ActorMeta> {
|
||||||
self.metas.lock().unwrap().get(id).cloned()
|
self.metas.lock().unwrap().get(id).cloned()
|
||||||
}
|
}
|
||||||
@@ -427,7 +437,13 @@ impl Broker {
|
|||||||
Ok(json!({"type": format!("{ty}d"), "actor": publisher}))
|
Ok(json!({"type": format!("{ty}d"), "actor": publisher}))
|
||||||
}
|
}
|
||||||
"scan" => Ok(json!({"type": "scanned", "changes": self.scan().await})),
|
"scan" => Ok(json!({"type": "scanned", "changes": self.scan().await})),
|
||||||
other => bail!("unknown broker request {other:?}. Known: spawn, upgrade, rollback, stop, start, subscribe, unsubscribe, scan"),
|
"exit" => {
|
||||||
|
self.log("broker", &format!("exit requested by {}", env.from));
|
||||||
|
// notify_one keeps the permit if nobody waits yet.
|
||||||
|
self.exit.notify_one();
|
||||||
|
Ok(json!({"type": "exiting"}))
|
||||||
|
}
|
||||||
|
other => bail!("unknown broker request {other:?}. Known: spawn, upgrade, rollback, stop, start, subscribe, unsubscribe, scan, exit"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+12
-1
@@ -92,9 +92,20 @@ async fn init(dir: PathBuf) -> Result<()> {
|
|||||||
async fn run(dir: PathBuf) -> Result<()> {
|
async fn run(dir: PathBuf) -> Result<()> {
|
||||||
let host = Host::start(&dir).await?;
|
let host = Host::start(&dir).await?;
|
||||||
tokio::spawn(console::run_mailbox(host.mailbox(console::ID)));
|
tokio::spawn(console::run_mailbox(host.mailbox(console::ID)));
|
||||||
console::run_input(host.broker.clone()).await;
|
let broker = host.broker.clone();
|
||||||
|
let exit = tokio::select! {
|
||||||
|
_ = console::run_input(host.broker.clone()) => false,
|
||||||
|
_ = broker.exit_requested() => true,
|
||||||
|
};
|
||||||
println!("Stopping actors …");
|
println!("Stopping actors …");
|
||||||
host.stop().await;
|
host.stop().await;
|
||||||
|
if exit {
|
||||||
|
// The console still blocks on a stdin read that cannot be cancelled,
|
||||||
|
// so the runtime would not shut down on its own. Exit code 75 tells
|
||||||
|
// `nix run .#loop` to start the host again (0 = /quit = stop the loop).
|
||||||
|
println!("Exiting, as an actor asked the broker to.");
|
||||||
|
std::process::exit(75);
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -127,6 +127,18 @@ async fn broker_does_not_answer_messages_with_reply_to() {
|
|||||||
host.stop().await;
|
host.stop().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn exit_request_is_answered_and_signals_the_host() {
|
||||||
|
let world = create_world("broker-exit");
|
||||||
|
let (host, mut mailbox) = start(&world).await;
|
||||||
|
let exiting = request(&host, &mut mailbox, json!({"type": "exit"})).await;
|
||||||
|
assert_eq!(exiting.body["type"], "exiting");
|
||||||
|
tokio::time::timeout(Duration::from_secs(1), host.broker.exit_requested())
|
||||||
|
.await
|
||||||
|
.expect("exit request did not reach the host");
|
||||||
|
host.stop().await;
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn filesystem_permissions_and_public_sharing() {
|
async fn filesystem_permissions_and_public_sharing() {
|
||||||
let (_, host, mut mailbox) = filesystem_world("filesystem-permissions").await;
|
let (_, host, mut mailbox) = filesystem_world("filesystem-permissions").await;
|
||||||
|
|||||||
@@ -35,11 +35,32 @@
|
|||||||
'';
|
'';
|
||||||
meta.mainProgram = "sausage";
|
meta.mainProgram = "sausage";
|
||||||
};
|
};
|
||||||
|
# `nix run .#loop -- <world>` runs the newest `sausage run <world>`
|
||||||
|
# from the Gitea repo, again and again: after a crash or a broker
|
||||||
|
# `exit` request it refreshes the flake and starts the host again,
|
||||||
|
# so the world updates itself. `/quit` (exit code 0) ends the loop.
|
||||||
|
# `--internal` (or `.#loop-internal`) uses Gitea's SSH port on the LAN.
|
||||||
|
publicUrl = "git+ssh://containeruser@gitea.surma.technology:2222/surma/sausage";
|
||||||
|
internalUrl = "git+ssh://containeruser@10.0.0.2:2222/surma/sausage";
|
||||||
|
loop = name: url: pkgs.writeShellScriptBin name ''
|
||||||
|
url="${url}"
|
||||||
|
if [ "''${1:-}" = "--internal" ]; then url="${internalUrl}"; shift; fi
|
||||||
|
if [ $# -eq 0 ]; then set -- world; fi
|
||||||
|
while true; do
|
||||||
|
nix run --refresh "$url#default" -- run "$@"
|
||||||
|
code=$?
|
||||||
|
if [ "$code" -eq 0 ]; then exit 0; fi
|
||||||
|
echo "sausage-loop: sausage exited with $code, starting the newest version in 2 s …" >&2
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
'';
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
packages.default = sausage;
|
packages.default = sausage;
|
||||||
|
|
||||||
apps.default = { type = "app"; program = "${sausage}/bin/sausage"; };
|
apps.default = { type = "app"; program = "${sausage}/bin/sausage"; };
|
||||||
|
apps.loop = { type = "app"; program = "${loop "sausage-loop" publicUrl}/bin/sausage-loop"; };
|
||||||
|
apps.loop-internal = { type = "app"; program = "${loop "sausage-loop-internal" internalUrl}/bin/sausage-loop-internal"; };
|
||||||
|
|
||||||
devShells.default = pkgs.mkShell {
|
devShells.default = pkgs.mkShell {
|
||||||
packages = [ rust pkgs.wasmtime pkgs.wasm-tools ];
|
packages = [ rust pkgs.wasmtime pkgs.wasm-tools ];
|
||||||
|
|||||||
Reference in New Issue
Block a user