Merge 'b696e57dc5ff6393c39daee0c639bf66ce9da06e'
This commit is contained in:
@@ -26,6 +26,17 @@ impl Call {
|
||||
self
|
||||
}
|
||||
/// Receive progress notifications before the result arrives.
|
||||
///
|
||||
/// For `core.watch`, poll the call future alongside work in the handler, then
|
||||
/// drop it when the handler returns. Do not put it in `background()` just to
|
||||
/// keep the watch alive: background tasks keep the process alive, while the
|
||||
/// watch only ends when the process ends. Dropping the future does not cancel
|
||||
/// the host watch, but it lets an otherwise idle process exit normally.
|
||||
///
|
||||
/// Scan only after `armed`, and rescan on `changed`. Events may coalesce, but
|
||||
/// are not rate-limited: watch the narrowest useful path to avoid rescanning
|
||||
/// for unrelated writes (for example, watch a mailbox, not all session data).
|
||||
/// Do not use endpoint connect retries to wait for file publication.
|
||||
pub fn on_notify(mut self, f: impl FnMut(&str, Value) + 'static) -> Self {
|
||||
self.notify = Some(Box::new(f));
|
||||
self
|
||||
|
||||
+3
-1
@@ -356,7 +356,9 @@
|
||||
//!
|
||||
//! 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.
|
||||
//! Core gives every old process 5 seconds of upgrade grace, then closes stdin and allows
|
||||
//! a separate 5-second stop budget before killing it. Discovery-only processes are excluded.
|
||||
//! `#[rpc(grace_seconds = N)]` still emits legacy `x-sausage-grace` metadata, but core ignores it.
|
||||
//! New calls use the new version immediately. Old processes can still use old imports during their grace period.
|
||||
//! [`macro@on_superseded`] handles the notice at grace start, with normal calls still available.
|
||||
//! It is opt-in: without a hook the process continues as before until core stops it.
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
use sausage_plugin::serde_json::{Value, json};
|
||||
use sausage_plugin::{Ctx, Result, call, lock, rpc, schema};
|
||||
struct Probe;
|
||||
/// A virtual path to observe.
|
||||
#[schema]
|
||||
struct WatchPath {
|
||||
/// Directory or file, not necessarily present yet.
|
||||
path: String,
|
||||
}
|
||||
/// The values to add.
|
||||
#[schema]
|
||||
struct Add {
|
||||
@@ -30,6 +36,68 @@ struct Echo {
|
||||
}
|
||||
#[rpc(grace_seconds = 12,endpoints(control=Control))]
|
||||
impl Probe {
|
||||
/// Scan only after the watch is armed; keep the call open until process death.
|
||||
async fn watch(&self, p: WatchPath) -> Result<()> {
|
||||
let path = p.path.clone();
|
||||
call("core.watch", json!({"path":p.path}))
|
||||
.on_notify(move |name, _| {
|
||||
if name == "armed" && path == "/self" {
|
||||
std::fs::write("/self/owned", "visible via proc").unwrap();
|
||||
}
|
||||
let mut entries: Vec<_> = std::fs::read_dir(&path)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(std::result::Result::ok)
|
||||
.map(|e| e.file_name().into_string().unwrap())
|
||||
.collect();
|
||||
entries.sort();
|
||||
let text = std::fs::read_to_string(format!("{path}/before")).ok();
|
||||
Ctx::default()
|
||||
.notify(
|
||||
name,
|
||||
json!({"pid":sausage_plugin::pid(), "entries":entries, "text":text}),
|
||||
)
|
||||
.unwrap();
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Poll the watch in the handler, then return idle without a background task.
|
||||
async fn watch_once(&self, p: WatchPath) -> Result<()> {
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
rc::Rc,
|
||||
task::{Poll, Waker},
|
||||
};
|
||||
let state = Rc::new(RefCell::new((false, None::<Waker>)));
|
||||
let received = state.clone();
|
||||
let mut watch = std::pin::pin!(std::future::IntoFuture::into_future(
|
||||
call("core.watch", json!({"path":p.path})).on_notify(move |name, _| {
|
||||
if name == "armed" {
|
||||
let mut state = received.borrow_mut();
|
||||
state.0 = true;
|
||||
if let Some(waker) = state.1.take() {
|
||||
waker.wake();
|
||||
}
|
||||
}
|
||||
})
|
||||
));
|
||||
std::future::poll_fn(|cx| {
|
||||
if let Poll::Ready(result) = watch.as_mut().poll(cx) {
|
||||
return Poll::Ready(result.map(|_| ()));
|
||||
}
|
||||
let mut state = state.borrow_mut();
|
||||
state.1 = Some(cx.waker().clone());
|
||||
if state.0 {
|
||||
Poll::Ready(Ok(()))
|
||||
} else {
|
||||
Poll::Pending
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Publish a control endpoint until EOF.
|
||||
async fn serve(&self) -> Result<()> {
|
||||
sausage_plugin::listen("/endpoint", std::rc::Rc::new(Control)).await
|
||||
@@ -60,6 +128,11 @@ impl Probe {
|
||||
sausage_plugin::listen("/state/control", std::rc::Rc::new(Control)).await?;
|
||||
Ok(sausage_plugin::pid())
|
||||
}
|
||||
/// Keep an idle listener and ownership lock until opt-in drain ends this process.
|
||||
async fn drain_service(&self) -> Result<String> {
|
||||
lock("/drain-owner").await?.keep();
|
||||
self.service().await
|
||||
}
|
||||
/// Add two numbers.
|
||||
///
|
||||
/// The result is their sum.
|
||||
@@ -170,6 +243,7 @@ impl Probe {
|
||||
#[sausage_plugin::on_stop]
|
||||
async fn stop(&self) {
|
||||
std::fs::write("/stopped", b"yes").unwrap();
|
||||
std::fs::write(format!("/stopped-{}", sausage_plugin::pid()), b"yes").unwrap();
|
||||
Ctx::default().notify("stopped", json!({})).unwrap();
|
||||
assert!(call("other.no", json!({})).await.is_err());
|
||||
}
|
||||
|
||||
@@ -108,6 +108,15 @@ fn protocol() {
|
||||
}
|
||||
p.finishes();
|
||||
}
|
||||
// A handler-owned watch does not keep an idle process alive after return,
|
||||
// even though the host never answers the watch and stdin remains open.
|
||||
let mut p = start(&wasm, &dir);
|
||||
p.request("watch_once", json!({"path":"/mailbox"}));
|
||||
let watch = p.read();
|
||||
assert_eq!(watch["method"], "core.watch");
|
||||
p.send(json!({"jsonrpc":"2.0","method":"core.notify","params":{"call":watch["id"],"name":"armed","params":{}}}));
|
||||
assert_eq!(p.read()["result"], Value::Null);
|
||||
p.finishes();
|
||||
let mut p = start(&wasm, &dir);
|
||||
p.request("rpc.discover", json!({}));
|
||||
let d = p.read();
|
||||
|
||||
Reference in New Issue
Block a user