sausage-plugin: the crate docs are the manual for agents; remove PROPOSAL.md
Its content lives in PROTOCOL.md (the contract), DECISIONS.md (the reasons), and the SDK docs (how to write a plugin).
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"openrpc": "1.2.6",
|
||||
"info": {"title": "notes", "version": "0.1.0"},
|
||||
"components": {"schemas": {
|
||||
"AddParams": {
|
||||
"description": "The parameters of add.",
|
||||
"type": "object",
|
||||
"properties": {"text": {"type": "string", "description": "The text to append."}},
|
||||
"required": ["text"]
|
||||
},
|
||||
"Added": {
|
||||
"description": "The result of add.",
|
||||
"type": "object",
|
||||
"properties": {"count": {"type": "integer", "minimum": 0, "description": "The total number of notes."}},
|
||||
"required": ["count"]
|
||||
}
|
||||
}},
|
||||
"methods": [{
|
||||
"name": "add",
|
||||
"summary": "Append a note and return the new count.",
|
||||
"params": [{"name": "text", "required": true, "schema": {"type": "string"}}],
|
||||
"x-sausage-params": {"$ref": "#/components/schemas/AddParams"},
|
||||
"result": {"name": "result", "schema": {"$ref": "#/components/schemas/Added"}}
|
||||
}]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"openrpc": "1.2.6",
|
||||
"info": {"title": "www", "version": "0.1.0"},
|
||||
"components": {"schemas": {
|
||||
"StartParams": {
|
||||
"description": "The server configuration.",
|
||||
"type": "object",
|
||||
"properties": {"port": {"type": "integer", "format": "uint16", "minimum": 0, "maximum": 65535, "description": "The HTTP port."}},
|
||||
"required": ["port"]
|
||||
}
|
||||
}},
|
||||
"methods": [{
|
||||
"name": "start",
|
||||
"summary": "Start the HTTP server and return its process ID.",
|
||||
"params": [{"name": "port", "required": true, "schema": {"type": "integer", "format": "uint16", "minimum": 0, "maximum": 65535}}],
|
||||
"x-sausage-params": {"$ref": "#/components/schemas/StartParams"},
|
||||
"result": {"name": "result", "schema": {"type": "string"}}
|
||||
}],
|
||||
"x-sausage-endpoints": {"control": {"methods": [{
|
||||
"name": "stats",
|
||||
"summary": "Return the number of HTTP requests.",
|
||||
"params": [],
|
||||
"result": {"name": "result", "schema": {"type": "integer", "format": "uint64", "minimum": 0}}
|
||||
}]}}
|
||||
}
|
||||
+374
-36
@@ -1,52 +1,390 @@
|
||||
//! Write single-threaded WASI plugins for Sausage.
|
||||
//!
|
||||
//! Each core call starts a process. Keep its initial value cheap to construct.
|
||||
//! Declare documented parameter and result types with [`macro@schema`].
|
||||
//! Declare methods with [`macro@rpc`], then serve them with [`run`].
|
||||
//! # Start here
|
||||
//!
|
||||
//! A plugin depends on `sausage-plugin = "0.1"` in its `Cargo.toml`.
|
||||
//! Use Rust edition 2024. The cargo plugin supplies the local SDK dependency.
|
||||
//! The SDK re-exports serde, schemars, serde_json, and its macros.
|
||||
//! Build the plugin through `cargo.build`. Activate the returned WASM file through `core.activate`.
|
||||
//! Read `/proc/core/PROTOCOL.md` for the host contract.
|
||||
//!
|
||||
//! Each core call starts a process. Keep the value passed to [`run`] cheap to construct.
|
||||
//! The core also starts a process just for `rpc.discover`.
|
||||
//! Open files, databases, and listeners in methods, not in `main`.
|
||||
//! Log with `eprintln!`. Stdout belongs to JSON-RPC.
|
||||
//!
|
||||
//! # Methods and schemas
|
||||
//!
|
||||
//! Each `fn` in an [`macro@rpc`] impl becomes an RPC method.
|
||||
//! Put helpers in a separate impl block without `#[rpc]`.
|
||||
//! Both synchronous and async methods must follow these rules:
|
||||
//!
|
||||
//! - Take `&self` first.
|
||||
//! - Optionally take `ctx: &Ctx` next.
|
||||
//! - Take at most one parameter argument after that.
|
||||
//! - Use a [`macro@schema`] struct with named fields for that argument.
|
||||
//! - Return [`Result<T>`].
|
||||
//! - Give each method, schema type, and field a doc comment.
|
||||
//!
|
||||
//! The parameter fields become the named JSON parameters.
|
||||
//! A method without parameters accepts an empty object.
|
||||
//! Its discovery entry has no `x-sausage-params` field.
|
||||
//! Raw identifiers such as `r#type` use the plain wire name `type`.
|
||||
//! Method docs become the OpenRPC summary and description.
|
||||
//! Type and field docs become schema descriptions. Generated clients retain these docs.
|
||||
//! Docs are part of the interface. Other agents read them to write callers.
|
||||
//!
|
||||
//! `#[schema]` adds serde and schemars derives. Do not add those derives yourself.
|
||||
//! These types form the supported subset:
|
||||
//!
|
||||
//! - `String`, `bool`, and `()`.
|
||||
//! - `u8`, `u16`, `u32`, `u64`, and `usize`.
|
||||
//! - `i8`, `i16`, `i32`, `i64`, and `isize`.
|
||||
//! - `f32` and `f64`.
|
||||
//! - `Vec<T>` and `Option<T>` with supported element types.
|
||||
//! - Other `#[schema]` structs and enums.
|
||||
//!
|
||||
//! Structs must have named fields. Enums must have unit variants.
|
||||
//! Document the variants to explain their meanings to callers.
|
||||
//! The permitted serde attributes are `rename`, `rename_all`, `default`, and `skip_serializing_if`.
|
||||
//! For example, use `#[serde(default)]` for a field with a default value.
|
||||
//! Use `#[serde(default, skip_serializing_if = "Option::is_none")]` for an omitted optional field.
|
||||
//! `#[schema]` rejects other serde attributes and all `#[schemars(...)]` attributes.
|
||||
//! Tuple structs, unit structs, tuple fields, maps, arbitrary JSON values, and data-carrying variants are outside this subset.
|
||||
//! Such fields cannot appear in a `#[schema]` type.
|
||||
//!
|
||||
//! # A persistent plugin
|
||||
//!
|
||||
//! Calls can run concurrently in separate processes. Protect shared changes with [`lock`].
|
||||
//! Bind the guard to a name: `let _guard = lock(p).await?;`.
|
||||
//! `let _ = lock(p).await?;` releases the lock at once.
|
||||
//! The guard releases the lock when its scope ends. Process exit also releases locks.
|
||||
//! `try_now()` does not wait. Check the returned guard with `held()`.
|
||||
//! `keep()` retains an acquired lock until process exit.
|
||||
//!
|
||||
//! This complete plugin stores notes in [`state_dir`]. [`write_atomic`] replaces the file after a complete temporary write.
|
||||
//! Create the state directory before the first write.
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use sausage_plugin::{rpc, schema, Result};
|
||||
//! struct Calculator;
|
||||
//! /// The numbers to add.
|
||||
//! use sausage_plugin::{lock, rpc, schema, state_dir, write_atomic, Result};
|
||||
//! use sausage_plugin::serde_json;
|
||||
//! use std::path::PathBuf;
|
||||
//!
|
||||
//! /// Persistent notes shared by all calls.
|
||||
//! struct Notes;
|
||||
//! /// The parameters of add.
|
||||
//! #[schema]
|
||||
//! struct AddParams {
|
||||
//! /// The first number.
|
||||
//! a: i32,
|
||||
//! /// The second number.
|
||||
//! b: i32,
|
||||
//! /// The text to append. Newlines are permitted.
|
||||
//! text: String,
|
||||
//! }
|
||||
//! /// The result of add.
|
||||
//! #[schema]
|
||||
//! struct Added {
|
||||
//! /// The total number of notes.
|
||||
//! count: usize,
|
||||
//! }
|
||||
//! #[rpc]
|
||||
//! impl Calculator {
|
||||
//! /// Add two numbers.
|
||||
//! fn add(&self, p: AddParams) -> Result<i32> { Ok(p.a + p.b) }
|
||||
//! impl Notes {
|
||||
//! /// Append a note and return the new count.
|
||||
//! async fn add(&self, p: AddParams) -> Result<Added> {
|
||||
//! std::fs::create_dir_all(state_dir())?;
|
||||
//! let _guard = lock(Self::file()).await?;
|
||||
//! let mut notes = Self::load()?;
|
||||
//! notes.push(p.text);
|
||||
//! write_atomic(Self::file(), serde_json::to_vec(¬es)?)?;
|
||||
//! Ok(Added { count: notes.len() })
|
||||
//! }
|
||||
//! /// List the notes, oldest first.
|
||||
//! fn list(&self) -> Result<Vec<String>> { Self::load() }
|
||||
//! }
|
||||
//! fn main() { sausage_plugin::run(Calculator); }
|
||||
//! // These helpers do not become RPC methods.
|
||||
//! impl Notes {
|
||||
//! fn file() -> PathBuf { state_dir().join("notes.json") }
|
||||
//! fn load() -> Result<Vec<String>> {
|
||||
//! match std::fs::read(Self::file()) {
|
||||
//! Ok(bytes) => Ok(serde_json::from_slice(&bytes)?),
|
||||
//! Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(vec![]),
|
||||
//! Err(e) => Err(e.into()),
|
||||
//! }
|
||||
//! }
|
||||
//! }
|
||||
//! fn main() { sausage_plugin::run(Notes); }
|
||||
//! ```
|
||||
//!
|
||||
//! Use [`call`] for outgoing calls. Await its builder for the JSON result.
|
||||
//! Use `detach()` to detach the child and `on_notify(f)` to receive progress.
|
||||
//! Use [`Ctx::notify`] to report progress to your caller.
|
||||
//! Use [`Ctx::exit_after_response`] to end the process after the response.
|
||||
//! # Calls and process lifetime
|
||||
//!
|
||||
//! [`background`] keeps the process alive while a task runs.
|
||||
//! At stdin EOF, pending calls fail and the optional [`macro@on_stop`] hook runs.
|
||||
//! The stop hook can write files and send notifications, but cannot make calls.
|
||||
//! [`macro@on_retired`] handles `rpc.retired` after an upgrade.
|
||||
//! [`call`] returns a lazy builder. Await it to send the request and receive its JSON result.
|
||||
//! `detach()` lets the child outlive this process. `on_notify(f)` receives its progress notifications.
|
||||
//! Use [`Ctx::notify`] to send progress to the caller of the current request.
|
||||
//! Do not call [`block_on`] in a handler. The runtime cannot nest `block_on` calls.
|
||||
//! Write an async handler and use `.await` instead.
|
||||
//!
|
||||
//! Use [`lock`] before shared state changes. A guard releases its lock on drop.
|
||||
//! `try_now()` returns a guard whose `held()` method reports success.
|
||||
//! `keep()` retains a lock until process exit.
|
||||
//! [`state_dir`], [`write_atomic`], and [`pid`] provide filesystem and identity helpers.
|
||||
//! Use [`macro@test`] for async tests. Native tests fail clearly without a core transport.
|
||||
//! [`macro@import`] reads `imports/<provider>.json` and generates a private typed client.
|
||||
//! Its functions return [`TypedCall`] builders with progress and detach options.
|
||||
//! Declare endpoint types with `#[rpc(endpoints(control = Control))]`.
|
||||
//! [`listen`] publishes a declared interface at an endpoint file.
|
||||
//! [`connect`] verifies its owner before it returns an [`Endpoint`].
|
||||
//! Endpoint calls report progress through [`EndpointCall::on_notify`].
|
||||
//! Local transport errors use code -32000: no transport, stdin EOF, a closed endpoint,
|
||||
//! or an endpoint identity or interface mismatch. Code -32002 belongs to core process failures.
|
||||
//! Log with `eprintln!`. Stdout belongs to JSON-RPC.
|
||||
//! A process ends after its request finishes if it has no remaining work.
|
||||
//! [`background`] keeps it alive while a task runs. An endpoint listener also keeps it alive.
|
||||
//! Neither mechanism detaches the process from its caller. Start long-lived processes detached or as services.
|
||||
//! At stdin EOF, pending outgoing calls fail with -32000. The SDK stops endpoint listeners and runs the optional stop hook.
|
||||
//! The stop hook can write files and send notifications. It cannot make new calls.
|
||||
//! After the serve loop, the SDK flushes stdout and exits, even if background tasks remain.
|
||||
//! [`Ctx::exit_after_response`] ends the process after its response. This also works in an endpoint handler.
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use sausage_plugin::{background, call, rpc, Ctx, Result};
|
||||
//! use sausage_plugin::serde_json::json;
|
||||
//! /// A worker that delegates a job.
|
||||
//! struct Worker;
|
||||
//! #[rpc]
|
||||
//! impl Worker {
|
||||
//! /// Start an attached background task and return immediately.
|
||||
//! fn start(&self, ctx: &Ctx) -> Result<()> {
|
||||
//! ctx.notify("text", "worker started")?;
|
||||
//! background(async {
|
||||
//! let result = call("notes.list", json!({}))
|
||||
//! .on_notify(|name, params| eprintln!("{name}: {params}"))
|
||||
//! .await;
|
||||
//! if let Err(error) = result { eprintln!("{error}"); }
|
||||
//! });
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! /// Flush local files before this process stops.
|
||||
//! #[sausage_plugin::on_stop]
|
||||
//! fn stop(&self) -> Result<()> {
|
||||
//! eprintln!("worker stopped");
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! }
|
||||
//! fn main() { sausage_plugin::run(Worker); }
|
||||
//! ```
|
||||
//!
|
||||
//! # A server with a process endpoint
|
||||
//!
|
||||
//! Declare endpoint types on the main interface. Publish them with [`listen`].
|
||||
//! `listen` returns after it publishes the endpoint file, not after the listener ends.
|
||||
//! `/self/control` belongs to this process. Clients find it at `/proc/<pid>/control`.
|
||||
//! This server counts requests in memory. A new core call cannot inspect the old process's counter.
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use sausage_plugin::{listen, pid, rpc, Ctx, Result};
|
||||
//! use std::{cell::Cell, rc::Rc};
|
||||
//! /// A server with process-local counters.
|
||||
//! struct Server;
|
||||
//! /// The interface of one running server.
|
||||
//! struct Control { hits: Cell<u64> }
|
||||
//! #[rpc(endpoints(control = Control), grace_seconds = 5)]
|
||||
//! impl Server {
|
||||
//! /// Start a counter server and return its process ID.
|
||||
//! async fn start(&self) -> Result<String> {
|
||||
//! listen("/self/control", Rc::new(Control { hits: Cell::new(0) })).await?;
|
||||
//! Ok(pid())
|
||||
//! }
|
||||
//! }
|
||||
//! #[rpc]
|
||||
//! impl Control {
|
||||
//! /// Record a request and return the new count.
|
||||
//! fn record(&self) -> Result<u64> {
|
||||
//! self.hits.set(self.hits.get() + 1);
|
||||
//! Ok(self.hits.get())
|
||||
//! }
|
||||
//! /// Return the number of recorded requests.
|
||||
//! fn stats(&self) -> Result<u64> { Ok(self.hits.get()) }
|
||||
//! /// Stop this process after the client receives the response.
|
||||
//! fn stop(&self, ctx: &Ctx) -> Result<()> {
|
||||
//! ctx.exit_after_response();
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! }
|
||||
//! fn main() { sausage_plugin::run(Server); }
|
||||
//! ```
|
||||
//!
|
||||
//! # A service with a fixed endpoint
|
||||
//!
|
||||
//! A service can publish a fixed path in `/run`. The core clears `/run` when the host starts.
|
||||
//! Configure this service in `/config/boot.json` with `{"method":"secrets.serve","params":{}}`.
|
||||
//! Clients can then connect to `/run/secrets`.
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use sausage_plugin::{listen, rpc, schema, Result};
|
||||
//! use std::rc::Rc;
|
||||
//! /// The service entry point.
|
||||
//! struct Secrets;
|
||||
//! /// The in-memory vault of this service process.
|
||||
//! struct Vault { token: String }
|
||||
//! /// The name of a secret.
|
||||
//! #[schema]
|
||||
//! struct GetParams {
|
||||
//! /// The secret to read.
|
||||
//! name: String,
|
||||
//! }
|
||||
//! #[rpc(endpoints(vault = Vault))]
|
||||
//! impl Secrets {
|
||||
//! /// Load the vault and publish its fixed endpoint.
|
||||
//! async fn serve(&self) -> Result<()> {
|
||||
//! let token = std::fs::read_to_string("/config/secret-token")?;
|
||||
//! listen("/run/secrets", Rc::new(Vault { token })).await
|
||||
//! }
|
||||
//! }
|
||||
//! #[rpc]
|
||||
//! impl Vault {
|
||||
//! /// Read the token, or return None for an unknown name.
|
||||
//! fn get(&self, p: GetParams) -> Result<Option<String>> {
|
||||
//! Ok((p.name == "token").then(|| self.token.clone()))
|
||||
//! }
|
||||
//! }
|
||||
//! fn main() { sausage_plugin::run(Secrets); }
|
||||
//! ```
|
||||
//!
|
||||
//! [`connect`] reads the endpoint file and verifies its owner through `rpc.endpoint`.
|
||||
//! The handshake checks the plugin, owner process ID, and interface name.
|
||||
//! A generated endpoint client also requires its expected interface name.
|
||||
//! A mismatch returns -32000. Endpoint calls bypass a fresh core process.
|
||||
//! `connect` opens and closes a verification connection. Each call opens another connection and repeats the handshake.
|
||||
//! The first call costs three round trips: verification, another handshake, and the request.
|
||||
//! Later calls cost a handshake and a request each. The SDK does not pool connections.
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use sausage_plugin::{connect, Result};
|
||||
//! use sausage_plugin::serde_json::json;
|
||||
//! async fn read_token() -> Result<Option<String>> {
|
||||
//! let vault = connect("/run/secrets").await?;
|
||||
//! vault.require_interface("vault")?;
|
||||
//! vault.call("get", json!({"name":"token"}))
|
||||
//! .on_notify(|name, params| eprintln!("{name}: {params}"))
|
||||
//! .await
|
||||
//! }
|
||||
//! fn main() {}
|
||||
//! ```
|
||||
//!
|
||||
//! # Import another plugin
|
||||
//!
|
||||
//! 1. Copy `/proc/core/plugins/<name>/discover.json` to `source/imports/<name>.json` in your plugin folder.
|
||||
//! 2. Read `/proc/core/plugins/<name>/client.rs` for the exact generated client API.
|
||||
//! 3. Write `sausage_plugin::import!("<name>")` and the code that calls it.
|
||||
//! 4. Build with `cargo.build`. Remove methods that rustc calls unused from the imported JSON document.
|
||||
//! 5. Build again after that change. Call `core.activate` with the resulting WASM path.
|
||||
//!
|
||||
//! The macro reads `imports/<name>.json` relative to `CARGO_MANIFEST_DIR`, not the current folder.
|
||||
//! It generates a private module with typed [`TypedCall`] builders.
|
||||
//! The module retains schema type names and documentation. Await its functions to get their typed results.
|
||||
//! Builders support `detach()` and `on_notify(f)`. Generated endpoint modules provide typed `connect` functions.
|
||||
//! The macro tracks the JSON file with `include_str!`, so Cargo rebuilds after changes.
|
||||
//! It registers the imported contract for `rpc.discover` through inventory.
|
||||
//! Remove unused methods from the JSON, not from generated Rust. This keeps `x-sausage-imports` small.
|
||||
//! Retain the schemas that the remaining methods reference.
|
||||
//!
|
||||
//! This example imports the notes plugin above and a server with a `control` endpoint.
|
||||
//! The `www` provider exposes `start(StartParams { port })` and `control.stats()`.
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use sausage_plugin::{rpc, Result};
|
||||
//! sausage_plugin::import!("notes");
|
||||
//! sausage_plugin::import!("www");
|
||||
//! /// A consumer of notes and the HTTP server.
|
||||
//! struct Consumer;
|
||||
//! #[rpc]
|
||||
//! impl Consumer {
|
||||
//! /// Add a note, start a detached server, and return its request count.
|
||||
//! async fn example(&self) -> Result<u64> {
|
||||
//! let added = notes::add(notes::AddParams { text: "milk".into() }).await?;
|
||||
//! eprintln!("{} notes", added.count);
|
||||
//! let pid = www::start(www::StartParams { port: 8080 }).detach().await?;
|
||||
//! let control = www::control::connect(&format!("/proc/{pid}/control")).await?;
|
||||
//! control.stats().await
|
||||
//! }
|
||||
//! }
|
||||
//! fn main() { sausage_plugin::run(Consumer); }
|
||||
//! ```
|
||||
//!
|
||||
//! # Tests in a world
|
||||
//!
|
||||
//! `cargo.build` runs tests under the WASI test runner.
|
||||
//! Both plain `#[test]` and async [`macro@test`] tests work. Each test gets its own instance and empty temporary world at `/`.
|
||||
//! The current folder is `/`, not the package folder. Read fixtures through `env!("CARGO_MANIFEST_DIR")`.
|
||||
//! `SAUSAGE_PLUGIN` is `test`, so `state_dir()` returns `/state/test`.
|
||||
//! Create directories before writes. `std::env::temp_dir()` does not work under this runner.
|
||||
//! Use an explicit path in the temporary world instead.
|
||||
//! The runner answers core lock, unlock, notify, and log requests. Calls to other plugins fail because no other plugins exist.
|
||||
//! Name filters, `tests/*.rs`, and `--ignored` work. `#[should_panic]` cannot work because WASI cannot catch panics.
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use sausage_plugin::{lock, state_dir, write_atomic};
|
||||
//! #[test]
|
||||
//! fn reads_package_fixture() {
|
||||
//! let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("Cargo.toml");
|
||||
//! assert!(std::fs::read_to_string(path).unwrap().contains("[package]"));
|
||||
//! }
|
||||
//! #[sausage_plugin::test]
|
||||
//! async fn writes_local_state() {
|
||||
//! let file = state_dir().join("test.txt");
|
||||
//! std::fs::create_dir_all(state_dir()).unwrap();
|
||||
//! let _guard = lock(&file).await.unwrap();
|
||||
//! write_atomic(&file, b"saved").unwrap();
|
||||
//! assert_eq!(std::fs::read(file).unwrap(), b"saved");
|
||||
//! }
|
||||
//! fn main() {}
|
||||
//! ```
|
||||
//!
|
||||
//! # Errors
|
||||
//!
|
||||
//! [`Error`] carries `code`, `message`, and optional structured `data`.
|
||||
//! `Error::new` constructs an error without data. Assign `data` when callers need structured details.
|
||||
//! Do not reduce an error to its message when another agent needs its code or data.
|
||||
//!
|
||||
//! | Code | Meaning |
|
||||
//! | --- | --- |
|
||||
//! | -32700 | Invalid JSON. |
|
||||
//! | -32600 | Invalid JSON-RPC request. |
|
||||
//! | -32601 | Unknown method. |
|
||||
//! | -32602 | Invalid method parameters. |
|
||||
//! | -32603 | Internal error, including converted I/O and serde errors. |
|
||||
//! | -32000 | Local SDK error: no transport, stdin EOF, endpoint closure, or identity or interface mismatch. |
|
||||
//! | -32001 | Core: no such plugin. |
|
||||
//! | -32002 | Core: the process ended without an answer. Data includes its exit status and journal tail. |
|
||||
//! | -32003 | Core: activation failed. Data describes the reasons. |
|
||||
//!
|
||||
//! Parameter decoding failures use -32602 instead of the general serde conversion code.
|
||||
//!
|
||||
//! ```
|
||||
//! use sausage_plugin::Error;
|
||||
//! use sausage_plugin::serde_json::json;
|
||||
//! let mut error = Error::new(-32602, "unknown secret");
|
||||
//! error.data = Some(json!({"name":"missing"}));
|
||||
//! assert_eq!(error.code, -32602);
|
||||
//! assert_eq!(error.data.unwrap()["name"], "missing");
|
||||
//! ```
|
||||
//!
|
||||
//! # State and upgrades
|
||||
//!
|
||||
//! The interface check covers calls, not state. Old and new processes share files in `/state`.
|
||||
//! `core.revert` restores code, not state. Preserve compatibility or migrate the state.
|
||||
//! `#[rpc(grace_seconds = N)]` declares the grace period for old processes. The default is 60 seconds.
|
||||
//! New calls use the new version immediately. Old processes can still use old imports during their grace period.
|
||||
//! [`macro@on_retired`] handles `rpc.retired` after the last process of an old version ends.
|
||||
//! This hook is best effort. If the host stops first, the hook does not run.
|
||||
//! New processes must handle unmigrated state. Check a migration marker and migrate if it is absent.
|
||||
//! See section 3.7 of `/proc/core/PROTOCOL.md`.
|
||||
//! Declare at most one stop hook and one retirement hook per interface.
|
||||
//!
|
||||
//! ```no_run
|
||||
//! use sausage_plugin::{rpc, schema, state_dir, write_atomic, Result};
|
||||
//! /// A plugin that records retirement notices.
|
||||
//! struct Migrator;
|
||||
//! /// The version that no longer has running processes.
|
||||
//! #[schema]
|
||||
//! struct RetiredParams {
|
||||
//! /// The SHA-256 of the previous WASM file.
|
||||
//! previous: String,
|
||||
//! }
|
||||
//! #[rpc]
|
||||
//! impl Migrator {
|
||||
//! /// Record a retired version. Normal methods must also handle missing markers.
|
||||
//! #[sausage_plugin::on_retired]
|
||||
//! fn retired(&self, p: RetiredParams) -> Result<()> {
|
||||
//! std::fs::create_dir_all(state_dir())?;
|
||||
//! write_atomic(state_dir().join("retired-version"), p.previous.as_bytes())
|
||||
//! }
|
||||
//! }
|
||||
//! fn main() { sausage_plugin::run(Migrator); }
|
||||
//! ```
|
||||
#![warn(missing_docs)]
|
||||
mod discovery;
|
||||
mod endpoints;
|
||||
|
||||
Reference in New Issue
Block a user