sausage-plugin part 1: run, #[rpc], #[schema], rpc.discover, calls, locks, Ctx, hooks

Checkpoint. Known issues, fixed in part 2: short writes on stdout,
byte-wise stdin reads, Result suffix on all result schemas, lost enum
variant docs, serde errors as invalid params, missing item docs.
This commit is contained in:
2026-10-23 00:05:37 +01:00
parent 4e41061ed4
commit 01712e745a
30 changed files with 1722 additions and 93 deletions
+114
View File
@@ -0,0 +1,114 @@
//! Probe of SDK transport and lifecycle behavior.
use sausage_plugin::serde_json::{Value, json};
use sausage_plugin::{Ctx, Result, call, lock, rpc, schema};
struct Probe;
/// The values to add.
#[schema]
struct Add {
/// The first number.
a: i32,
/// The second number.
b: i32,
}
/// The retired version.
#[schema]
struct Retired {
/// The previous hash.
previous: String,
}
#[rpc(grace_seconds = 12)]
impl Probe {
/// Add two numbers.
///
/// The result is their sum.
fn add(&self, p: Add) -> Result<i32> {
Ok(p.a + p.b)
}
/// Return a deliberate error.
fn fail(&self) -> Result<String> {
Err(sausage_plugin::Error::new(-32010, "deliberate"))
}
/// Exercise calls, notifications, and advisory locks.
async fn exercise(&self, ctx: &Ctx) -> Result<String> {
let a =
call("other.first", json!({})).on_notify(|name, _| eprintln!("notification: {name}"));
let b = call("other.second", json!({})).detach();
let (a, b) = join(a, b).await;
a?;
b?;
ctx.notify("progress", json!({"done":true}))?;
{
let guard = lock("/state/probe/file").await?;
assert!(guard.held());
}
assert!(!lock("/state/probe/file").try_now().await?.held());
Ok("done".into())
}
/// Drop an acquisition before the core grants its lock.
async fn late(&self) -> Result<String> {
{
let mut future = std::pin::pin!(std::future::IntoFuture::into_future(lock("/late")));
std::future::poll_fn(|cx| {
assert!(future.as_mut().poll(cx).is_pending());
std::task::Poll::Ready(())
})
.await;
}
call("other.barrier", json!({})).await?;
lock("/kept").await?.keep();
Ok("late done".into())
}
/// Mark a retired version.
#[sausage_plugin::on_retired]
fn retired(&self, p: Retired) -> Result<String> {
Ok(p.previous)
}
/// Wait until EOF, then run the stop hook.
async fn wait(&self) -> Result<String> {
call("other.wait", json!({})).await?;
Ok("done".into())
}
/// Keep the process alive with a background call.
fn start(&self) -> Result<String> {
sausage_plugin::background(async {
let _ = call("other.background", json!({})).await;
});
Ok("started".into())
}
/// Save an EOF marker.
#[sausage_plugin::on_stop]
async fn stop(&self) {
std::fs::write("/stopped", b"yes").unwrap();
Ctx::default().notify("stopped", json!({})).unwrap();
assert!(call("other.no", json!({})).await.is_err());
}
}
async fn join(
a: impl std::future::IntoFuture<Output = Result<Value>>,
b: impl std::future::IntoFuture<Output = Result<Value>>,
) -> (Result<Value>, Result<Value>) {
let mut a = std::pin::pin!(a.into_future());
let mut b = std::pin::pin!(b.into_future());
let (mut av, mut bv) = (None, None);
std::future::poll_fn(move |cx| {
if av.is_none() {
if let std::task::Poll::Ready(v) = a.as_mut().poll(cx) {
av = Some(v);
}
}
if bv.is_none() {
if let std::task::Poll::Ready(v) = b.as_mut().poll(cx) {
bv = Some(v);
}
}
if av.is_some() && bv.is_some() {
std::task::Poll::Ready((av.take().unwrap(), bv.take().unwrap()))
} else {
std::task::Poll::Pending
}
})
.await
}
fn main() {
sausage_plugin::run(Probe)
}