- 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/
146 lines
5.8 KiB
Markdown
146 lines
5.8 KiB
Markdown
# The Sausage protocol
|
|
|
|
This document is the contract between the host and the actors. An actor in
|
|
any language that follows it works in Sausage. The host publishes this file at
|
|
`/actor/broker/public/PROTOCOL.md`, so every actor can read it.
|
|
|
|
## Actors
|
|
|
|
An actor is a WASI 0.2 command (`wasm32-wasip2`): a program with `main`. The
|
|
host runs one instance per actor. An instance can run forever.
|
|
|
|
The instance gets:
|
|
|
|
- **stdin:** one message per line, from the broker
|
|
- **stdout:** one message per line, to the broker
|
|
- **stderr:** free text, appended to `/actor/<id>/stderr.log`
|
|
- **environment:** `SAUSAGE_ACTOR` (its ID), `SAUSAGE_VERSION` (its version),
|
|
and `SAUSAGE_CANDIDATE=1` during a test run
|
|
- **outgoing HTTP and HTTPS** through `wasi:http` (no raw sockets)
|
|
- **the filesystem** described below
|
|
|
|
## Messages
|
|
|
|
A line on **stdin**:
|
|
|
|
```json
|
|
{"id": "m-17", "from": "console", "reply_to": "agent/a5", "body": {"type": "text", "text": "hi"}}
|
|
```
|
|
|
|
A line on **stdout**:
|
|
|
|
```json
|
|
{"to": "console", "id": "a6", "reply_to": "m-17", "body": {"type": "text", "text": "hello"}}
|
|
```
|
|
|
|
- `body` is required. By convention it is an object with a `"type"`.
|
|
- `to` is the recipient. Without `to`, the broker publishes the message to
|
|
every subscriber of the sender.
|
|
- `id` is optional. If the actor sets it, the message ID is `"<sender>/<id>"`.
|
|
Otherwise, the broker assigns `m-<n>`. The broker always sets `from`.
|
|
- `reply_to` marks a reply. There is no request/response: a reply is a new
|
|
message whose `reply_to` is the ID of the message it answers.
|
|
- Delivery is at most once. Each actor has a mailbox of 1,000 messages. The
|
|
mailbox keeps messages while the actor restarts or is upgraded.
|
|
- If the broker cannot deliver a message, the sender gets
|
|
`{"type": "undeliverable", "reason": "...", "to": "..."}` from `broker`.
|
|
- An actor must keep reading stdin. If a message waits 30 seconds for the
|
|
actor to read it, the broker kills and restarts the actor, and the sender
|
|
gets `undeliverable`.
|
|
|
|
Before the broker stops an instance, it sends
|
|
`{"id": "broker/shutdown", "from": "broker", "body": {"type": "shutdown"}}`
|
|
and then closes stdin. The broker still routes what the actor writes until
|
|
it exits. After 5 seconds, it kills the instance.
|
|
|
|
## Filesystem
|
|
|
|
Each instance sees three folders:
|
|
|
|
| Path | Access | Contents |
|
|
| --- | --- | --- |
|
|
| `/data` | read-write | private state of the actor |
|
|
| `/public` | read-write | files that the actor publishes |
|
|
| `/actor` | read-only | the `actor/` folder of the world |
|
|
|
|
`/actor/<id>/` contains `actor.wasm`, `actor.json`, `source/`, `public/`,
|
|
`status.json`, `stderr.log`, and `versions/<n>/` of every actor. The `data/`
|
|
folders (and the data snapshots in `versions/`) are hidden.
|
|
|
|
A new or truncated file becomes visible when the writer closes it. Before
|
|
that, readers see the old file or no file. If the instance crashes, the write
|
|
is lost. A write to an existing file without truncate (append, edit) goes in
|
|
place.
|
|
|
|
Send large or binary data as a path under `/actor/<id>/public/`, not inside a
|
|
message.
|
|
|
|
## The broker
|
|
|
|
Requests to `broker` (the reply has the same `reply_to`):
|
|
|
|
| `type` | Fields | Effect |
|
|
| --- | --- | --- |
|
|
| `spawn` | `actor`, `wasm`, `source`?, `built_by`? | test, then start a new actor |
|
|
| `upgrade` | `actor`, `wasm`, `source`?, `built_by`? | test, then swap the version |
|
|
| `rollback` | `actor`, `version`? | go back to an older version and its data |
|
|
| `stop`, `start` | `actor` | stop or start an actor |
|
|
| `remove` | `actor` | move the actor's folder to `archive/` |
|
|
| `subscribe`, `unsubscribe` | `actor` | receive (or not) what `actor` publishes |
|
|
| `scan` | | boot new or changed `actor.wasm` files |
|
|
| `list` | | all actors and their state |
|
|
|
|
Paths: `/actor/...`, `/public/...` (the sender's), or `/data/...` (the
|
|
sender's).
|
|
|
|
**Versions.** `versions/<n>/` keeps every version that the broker saw:
|
|
`actor.wasm`, `source/`, and `meta.json`. A version that the broker refused
|
|
has `"rejected"` in its `meta.json`.
|
|
|
|
**Tests.** Before a spawn or an upgrade, the broker runs the candidate on a
|
|
copy of the actor's `/data` and `/public` (empty for a new actor), without
|
|
HTTP. The candidate sees its future `SAUSAGE_VERSION` and
|
|
`SAUSAGE_CANDIDATE=1`. The tests are JSON
|
|
files in `source/tests/*.json`:
|
|
|
|
```json
|
|
{
|
|
"description": "add, then list",
|
|
"steps": [
|
|
{"send": {"type": "add", "text": "milk"}, "expect": {"type": "added"}},
|
|
{"send": {"type": "list"}, "expect": [{"type": "notes"}]}
|
|
]
|
|
}
|
|
```
|
|
|
|
An `expect` entry matches when it is a subset of the reply body. A step can
|
|
also have `from` (the sender, default `test`) and `to` (the expected
|
|
recipient). Each file runs in a fresh instance. Without test files, the
|
|
candidate must stay up for 2 seconds.
|
|
|
|
**Swap.** The broker holds the mailbox, stops the old instance, saves a
|
|
snapshot of `/data` and `/public` in the old version's folder, installs the new
|
|
files, and starts the new instance.
|
|
|
|
**Crashes.** The broker restarts a crashed actor (0.5 s, then doubling, at
|
|
most 30 s). It keeps the message that the actor got just before the crash,
|
|
and delivers it again after the next upgrade. A new version that crashes 3
|
|
times within 30 seconds after an upgrade goes back to the previous version,
|
|
with its data. The actor that asked for the upgrade gets `rolled_back`.
|
|
|
|
**The folder is the truth.** At start, and on `scan`, the broker boots every
|
|
`actor/<id>/actor.wasm`. If the file differs from the installed version, the
|
|
broker copies it to `versions/<n>/` and upgrades to it. If the upgrade fails,
|
|
the installed files come back.
|
|
|
|
## The `cargo` actor
|
|
|
|
```json
|
|
{"type": "build", "source": "/actor/agent/public/drafts/notes"}
|
|
```
|
|
|
|
The reply is `{"type": "built", "wasm": "/actor/cargo/public/builds/<n>/<name>.wasm", ...}`
|
|
or `{"type": "build_failed", "errors": "..."}`. A Rust actor depends on the SDK
|
|
with `sausage-actor = "0.1"`. `cargo` points that dependency at the SDK of the
|
|
host.
|