Notify superseded plugin processes and support SDK draining

This commit is contained in:
Sausage
2026-11-01 20:51:08 +00:00
parent c54edea596
commit 4da8a28392
12 changed files with 314 additions and 21 deletions
+9
View File
@@ -12,6 +12,7 @@ pub(crate) struct OldProcess {
pub(crate) struct Upgrade {
plugin: String,
previous: String,
current: String,
grace: Duration,
processes: Vec<OldProcess>,
services: Vec<(Service, Option<watch::Receiver<bool>>)>,
@@ -67,6 +68,7 @@ impl Host {
Upgrade {
plugin: plugin.into(),
previous,
current: current.into(),
grace: Duration::from_secs_f64(grace),
processes,
services,
@@ -74,6 +76,13 @@ impl Host {
}
/// Lets old calls finish, stops them after grace, and starts each lifecycle hook once.
pub(crate) fn begin_upgrade(&self, upgrade: Upgrade) {
// Queue the notice before returning from activation, without waiting on guest code.
for process in &upgrade.processes {
self.send(
&process.pid,
json!({"jsonrpc":"2.0","method":"rpc.superseded","params":{"current":upgrade.current}}),
);
}
let host = self.clone();
tokio::spawn(async move {
if host.0.shutdown.load(Ordering::SeqCst) {
+70
View File
@@ -839,6 +839,76 @@ async fn old_process_grace_and_new_calls() {
"2"
);
marker(world.0.join("state/notes/ended-1"), "ended").await;
let notice: Value = serde_json::from_slice(
&std::fs::read(world.0.join(format!("state/notes/superseded-{pid}"))).unwrap(),
)
.unwrap();
assert_eq!(notice["params"]["current"], fixture_hash(2));
host.stop().await;
}
/// PROTOCOL 3.7: every old process gets the new hash while still running, before grace ends.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn superseded_notifies_each_old_process() {
let world = TestWorld::new();
let mut old = document("hold");
old["x-sausage-grace"] = json!(60);
world.plugin("notes", old, document("hold"));
let host = world.start().await;
call(&host, "notes.hold", json!({})).await;
call(&host, "notes.hold", json!({})).await;
let pids: Vec<_> = host
.processes()
.into_iter()
.filter(|(_, process)| process.method == "hold")
.map(|(pid, _)| pid)
.collect();
assert_eq!(pids.len(), 2);
activate(&host, "notes").await.unwrap();
let expected =
json!({"jsonrpc":"2.0","method":"rpc.superseded","params":{"current":fixture_hash(2)}})
.to_string();
for pid in pids {
// marker's existing 30-second bound is shorter than the old 60-second grace.
marker(
world.0.join(format!("state/notes/superseded-{pid}")),
&expected,
)
.await;
assert_eq!(host.processes()[&pid].state, "running");
}
assert_eq!(
call(&host, "notes.version", json!({})).await["version"],
"2"
);
host.stop().await;
}
/// PROTOCOL 3.7: a service that exits on the notice restarts before its grace deadline.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn superseded_service_exits_early() {
let world = TestWorld::new();
let mut old = document("serve");
old["x-sausage-grace"] = json!(60);
world.plugin("notes", old, document("serve"));
std::fs::write(
world.0.join("config/boot.yaml"),
json!([{"method":"notes.serve","params":{"exit_on_superseded":true}}]).to_string(),
)
.unwrap();
let host = world.start().await;
marker(world.0.join("state/notes/service"), "1").await;
let pid = host
.processes()
.into_iter()
.find(|(_, p)| p.method == "serve")
.unwrap()
.0;
activate(&host, "notes").await.unwrap();
marker(world.0.join("state/notes/service"), "2").await;
assert!(!host.processes().contains_key(&pid));
assert!(
!world.0.join("state/notes/ended-1").exists(),
"old service must exit on the notice, not EOF"
);
host.stop().await;
}
/// PROTOCOL 3.7: retirement receives the previous hash only after old processes end.
+18 -1
View File
@@ -89,7 +89,24 @@ fn main() {
if request["method"] == "serve" {
std::fs::write(format!("/state/{plugin}/service"), version).unwrap();
}
for _ in lines {}
for line in lines {
let Ok(message) = serde_json::from_str::<Value>(&line.unwrap()) else {
continue;
};
if message["method"] == "rpc.superseded" {
std::fs::write(
format!(
"/state/{plugin}/superseded-{}",
std::env::var("SAUSAGE_PID").unwrap()
),
message.to_string(),
)
.unwrap();
if request["params"]["exit_on_superseded"] == true {
return;
}
}
}
std::fs::write(format!("/state/{plugin}/ended-{version}"), "ended").unwrap();
return;
}