Notify replaced plugin processes at upgrade grace start

Send rpc.superseded with the newly active hash before activation returns.
Add an opt-in SDK hook and drain helper that closes SDK listeners and idle
connections without cancelling active handlers or background work.
Document the additive notice and preserve existing grace, stop and restart rules.

Cover immediate notices, ignored notices, early service restarts, SDK draining
and duplicate-hook diagnostics.

Card: SAU-352
This commit is contained in:
SAU-352 implementer
2026-10-29 18:50:10 +00:00
committed by surma
parent c54edea596
commit c04baee67c
12 changed files with 314 additions and 21 deletions
+26 -6
View File
@@ -153,6 +153,7 @@ pub fn rpc(args: TokenStream, input: TokenStream) -> TokenStream {
let mut stop = quote!();
let mut has_stop = false;
let mut has_retired = false;
let mut has_superseded = false;
for member in &mut item.items {
let ImplItem::Fn(f) = member else { continue };
let retired = f.attrs.iter().any(|a| {
@@ -161,6 +162,12 @@ pub fn rpc(args: TokenStream, input: TokenStream) -> TokenStream {
.last()
.is_some_and(|s| s.ident == "on_retired")
});
let superseded = f.attrs.iter().any(|a| {
a.path()
.segments
.last()
.is_some_and(|s| s.ident == "on_superseded")
});
let stopping = f.attrs.iter().any(|a| {
a.path()
.segments
@@ -179,11 +186,16 @@ pub fn rpc(args: TokenStream, input: TokenStream) -> TokenStream {
"only one on_retired method is allowed",
));
}
if superseded && std::mem::replace(&mut has_superseded, true) {
return Err(syn::Error::new(
f.span(),
"only one on_superseded method is allowed",
));
}
f.attrs.retain(|a| {
!a.path()
.segments
.last()
.is_some_and(|s| s.ident == "on_retired" || s.ident == "on_stop")
!a.path().segments.last().is_some_and(|s| {
s.ident == "on_retired" || s.ident == "on_stop" || s.ident == "on_superseded"
})
});
documented(&f.attrs, f.span())?;
if f.sig.inputs.is_empty()
@@ -224,7 +236,9 @@ pub fn rpc(args: TokenStream, input: TokenStream) -> TokenStream {
stop = quote!(fn stop(&self)->::std::pin::Pin<Box<dyn ::std::future::Future<Output=()> + '_>> {Box::pin(async move {let _=self.#name() #await_;})});
continue;
}
let wire = if retired {
let wire = if superseded {
"rpc.superseded".to_string()
} else if retired {
"rpc.retired".to_string()
} else {
name.unraw().to_string()
@@ -253,7 +267,7 @@ pub fn rpc(args: TokenStream, input: TokenStream) -> TokenStream {
};
let doc = docs(&f.attrs);
arms.push(quote!(#wire => { fn params_type<T: ::sausage_plugin::Params>() {} params_type::<#p>(); let result=self.#name(#(#args),*) #await_ ?; Ok(::sausage_plugin::serde_json::to_value(result)?) }));
if !retired {
if !retired && !superseded {
entries.push(quote!(::sausage_plugin::method::<#p,#output>(&mut doc,#wire,#doc);));
}
}
@@ -328,6 +342,12 @@ pub fn import(input: TokenStream) -> TokenStream {
pub fn on_retired(_: TokenStream, input: TokenStream) -> TokenStream {
input
}
/// Mark the method that handles `rpc.superseded` at the start of upgrade grace.
/// Calls still work here. Use `sausage_plugin::drain` to finish active work and exit.
#[proc_macro_attribute]
pub fn on_superseded(_: TokenStream, input: TokenStream) -> TokenStream {
input
}
/// Mark the hook that runs at stdin EOF without a live call transport.
#[proc_macro_attribute]
pub fn on_stop(_: TokenStream, input: TokenStream) -> TokenStream {