From 9a0c6ad58d11bfaa2599497531a1388c3d9af984 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Manuel=20Almaza=20Ramiro?= Date: Fri, 21 Aug 2026 18:18:17 +0200 Subject: [PATCH 01/10] fix(procmgr): anchor in-flight Stop RPCs to SCM shutdown budget RPC stops used an unlimited ShutdownBudget while the supervisor loop was blocked, so a large stop_timeout could exceed the Windows service deadline. Apply the SCM-anchored budget when a stop signal is recorded and interrupt graceful waits on shutdown_notify. --- pkg/procmgr/rust/src/process.rs | 4 +-- pkg/procmgr/rust/src/shutdown.rs | 52 ++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/pkg/procmgr/rust/src/process.rs b/pkg/procmgr/rust/src/process.rs index bfae0bfc21fb..b4f6be300ec1 100644 --- a/pkg/procmgr/rust/src/process.rs +++ b/pkg/procmgr/rust/src/process.rs @@ -9,7 +9,7 @@ use crate::handle::ProcessHandle; #[cfg(windows)] use crate::handle::ProcessWaitControl; use crate::platform; -use crate::shutdown::ShutdownBudget; +use crate::shutdown::{self, ShutdownBudget}; use crate::spawn::{SpawnProfile, profile_for}; use crate::state::ProcessState; use anyhow::{Context, Result, bail}; @@ -1133,7 +1133,7 @@ impl ManagedProcess { } pub async fn wait_for_stop(&mut self) { - self.wait_for_stop_since(ShutdownBudget::unlimited(Instant::now().into())) + self.wait_for_stop_since(ShutdownBudget::unlimited(Instant::now())) .await; } diff --git a/pkg/procmgr/rust/src/shutdown.rs b/pkg/procmgr/rust/src/shutdown.rs index 598476c5be9a..f235c99e1780 100644 --- a/pkg/procmgr/rust/src/shutdown.rs +++ b/pkg/procmgr/rust/src/shutdown.rs @@ -26,6 +26,27 @@ impl ShutdownBudget { } } + /// Prefer the SCM-anchored budget when a Windows service stop is in progress. + pub(crate) fn prefer_service_stop(fallback: Self) -> Self { + #[cfg(windows)] + { + if let Some(signal_time) = crate::platform::service_stop_signal_time() { + return Self::service_stop(signal_time); + } + } + fallback + } + + /// Re-read the active service stop signal (for example after graceful wait). + pub(crate) fn refresh(self) -> Self { + Self::prefer_service_stop(self) + } + + /// Budget for a single Stop RPC or child teardown outside ordered shutdown. + pub(crate) fn for_single_stop() -> Self { + Self::prefer_service_stop(Self::unlimited(Instant::now())) + } + /// Budget for ordered shutdown after a service stop signal. pub(crate) fn service_stop(signal_time: Instant) -> Self { #[cfg(windows)] @@ -66,6 +87,26 @@ impl ShutdownBudget { } } +/// Wait for graceful child exit, or cut short when SCM requests service shutdown. +pub(crate) async fn wait_graceful_or_shutdown>( + graceful_budget: Duration, + fut: F, +) -> Result { + #[cfg(windows)] + { + tokio::select! { + _ = crate::platform::shutdown_notify().notified() => { + Err(tokio::time::error::Elapsed(())) + } + result = tokio::time::timeout(graceful_budget, fut) => result, + } + } + #[cfg(not(windows))] + { + tokio::time::timeout(graceful_budget, fut).await + } +} + pub async fn shutdown_ordered(processes: &mut [ManagedProcess], order: &[usize]) { for &idx in order { processes[idx].request_stop(); @@ -95,6 +136,17 @@ mod tests { test_helpers::make_config(cmd, args) } + #[test] + fn test_prefer_service_stop_without_signal_preserves_fallback() { + let signal_time = Instant::now(); + let fallback = ShutdownBudget::unlimited(signal_time); + let budget = ShutdownBudget::prefer_service_stop(fallback); + assert_eq!( + budget.graceful_budget(Duration::from_secs(90)), + Duration::from_secs(90) + ); + } + #[tokio::test] async fn test_shutdown_all_graceful() { let cfg1 = sleep_config(); From 0f652591ff8ea746cb4a4171abeb83302422137e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Manuel=20Almaza=20Ramiro?= Date: Wed, 26 Aug 2026 19:13:08 +0200 Subject: [PATCH 02/10] fix(procmgr): broadcast shutdown notify and unify graceful stop waits Rebase onto spawn-profiles shutdown latch. Wake all shutdown waiters on SCM stop, route graceful stop through wait_for_shutdown/shutdown_requested, refresh the SCM budget before Windows resource cleanup, and share the service-stop cap helper with auto-start join. --- pkg/procmgr/rust/src/manager/startup.rs | 5 ++-- pkg/procmgr/rust/src/platform/unix/mod.rs | 2 +- pkg/procmgr/rust/src/platform/windows/mod.rs | 2 +- .../rust/src/platform/windows/scm_service.rs | 2 +- pkg/procmgr/rust/src/shutdown.rs | 24 ++++++++++++++----- 5 files changed, 23 insertions(+), 12 deletions(-) diff --git a/pkg/procmgr/rust/src/manager/startup.rs b/pkg/procmgr/rust/src/manager/startup.rs index c4bde263678b..2890e9659f00 100644 --- a/pkg/procmgr/rust/src/manager/startup.rs +++ b/pkg/procmgr/rust/src/manager/startup.rs @@ -121,9 +121,8 @@ async fn join_in_flight_spawn( handle: tokio::task::JoinHandle>, ) { #[cfg(windows)] - if let Some(signal_time) = platform::service_stop_signal_time() { - let budget = shutdown::ShutdownBudget::service_stop(signal_time); - let cap = budget.remaining_cap(Duration::from_secs(180)); + if let Some(cap) = shutdown::ShutdownBudget::remaining_service_stop_cap(Duration::from_secs(180)) + { if cap.is_zero() { warn!( "startup: SCM shutdown budget exhausted; deferring in-flight auto-start to supervisor teardown" diff --git a/pkg/procmgr/rust/src/platform/unix/mod.rs b/pkg/procmgr/rust/src/platform/unix/mod.rs index 93f3646ee5fe..0a57d01dc188 100644 --- a/pkg/procmgr/rust/src/platform/unix/mod.rs +++ b/pkg/procmgr/rust/src/platform/unix/mod.rs @@ -135,7 +135,7 @@ pub async fn shutdown_signal() { #[cfg(test)] pub(crate) fn signal_shutdown_for_test() { mark_shutdown_requested(); - shutdown_notify().notify_one(); + shutdown_notify().notify_waiters(); } #[cfg(test)] diff --git a/pkg/procmgr/rust/src/platform/windows/mod.rs b/pkg/procmgr/rust/src/platform/windows/mod.rs index 27ef67260c2f..b501e8695b4f 100644 --- a/pkg/procmgr/rust/src/platform/windows/mod.rs +++ b/pkg/procmgr/rust/src/platform/windows/mod.rs @@ -426,7 +426,7 @@ pub async fn shutdown_signal() { #[cfg(test)] pub(crate) fn signal_shutdown_for_test() { SHUTDOWN_REQUESTED.store(true, Ordering::SeqCst); - shutdown_notify().notify_one(); + shutdown_notify().notify_waiters(); } #[cfg(test)] diff --git a/pkg/procmgr/rust/src/platform/windows/scm_service.rs b/pkg/procmgr/rust/src/platform/windows/scm_service.rs index bbe189f90352..26c8d1bfc57b 100644 --- a/pkg/procmgr/rust/src/platform/windows/scm_service.rs +++ b/pkg/procmgr/rust/src/platform/windows/scm_service.rs @@ -82,7 +82,7 @@ unsafe extern "system" fn ctrl_handler( SCM_STOP_WAIT_HINT.as_millis() as u32, ); super::record_service_stop_signal(); - super::shutdown_notify().notify_one(); + super::shutdown_notify().notify_waiters(); NO_ERROR } SERVICE_CONTROL_INTERROGATE => NO_ERROR, diff --git a/pkg/procmgr/rust/src/shutdown.rs b/pkg/procmgr/rust/src/shutdown.rs index f235c99e1780..05db9a729a68 100644 --- a/pkg/procmgr/rust/src/shutdown.rs +++ b/pkg/procmgr/rust/src/shutdown.rs @@ -85,18 +85,29 @@ impl ShutdownBudget { pub(crate) fn is_bounded(&self) -> bool { self.deadline.is_some() } + + /// Remaining SCM shutdown budget for a caller-defined cap, if a service stop is in progress. + #[cfg(windows)] + pub(crate) fn remaining_service_stop_cap(cap: Duration) -> Option { + crate::platform::service_stop_signal_time() + .map(|signal_time| Self::service_stop(signal_time).remaining_cap(cap)) + } } -/// Wait for graceful child exit, or cut short when SCM requests service shutdown. +/// Wait for graceful child exit, or cut short when service shutdown is requested. pub(crate) async fn wait_graceful_or_shutdown>( graceful_budget: Duration, fut: F, ) -> Result { + if crate::platform::shutdown_requested() { + return tokio::time::timeout(Duration::ZERO, std::future::pending::()).await; + } #[cfg(windows)] { tokio::select! { - _ = crate::platform::shutdown_notify().notified() => { - Err(tokio::time::error::Elapsed(())) + biased; + _ = crate::platform::wait_for_shutdown() => { + tokio::time::timeout(Duration::ZERO, std::future::pending::()).await } result = tokio::time::timeout(graceful_budget, fut) => result, } @@ -141,9 +152,10 @@ mod tests { let signal_time = Instant::now(); let fallback = ShutdownBudget::unlimited(signal_time); let budget = ShutdownBudget::prefer_service_stop(fallback); - assert_eq!( - budget.graceful_budget(Duration::from_secs(90)), - Duration::from_secs(90) + let graceful = budget.graceful_budget(Duration::from_secs(90)); + assert!( + graceful >= Duration::from_secs(89) && graceful <= Duration::from_secs(90), + "expected ~90s graceful budget without SCM signal, got {graceful:?}" ); } From 69232d3ff65e1a07023bad4f99b264f32a99d47d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Manuel=20Almaza=20Ramiro?= Date: Wed, 26 Aug 2026 19:42:36 +0200 Subject: [PATCH 03/10] fix(procmgr): cap Windows service runtime teardown to SCM budget Call runtime.shutdown_timeout with the remaining service stop budget after supervisor exit so stuck spawn_blocking auto-start work cannot block reporting SERVICE_STOPPED past the SCM wait hint. --- .../rust/src/platform/windows/scm_service.rs | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/pkg/procmgr/rust/src/platform/windows/scm_service.rs b/pkg/procmgr/rust/src/platform/windows/scm_service.rs index 26c8d1bfc57b..e25256958c74 100644 --- a/pkg/procmgr/rust/src/platform/windows/scm_service.rs +++ b/pkg/procmgr/rust/src/platform/windows/scm_service.rs @@ -162,12 +162,34 @@ fn run_service_inner() -> Result<()> { .unwrap_or_else(|| ShutdownBudget::unlimited(Instant::now())); runtime.block_on(join_deferred_spawn_tasks(shutdown_budget)); - let runtime_shutdown_cap = shutdown_budget.remaining_cap(Duration::from_secs(30)); - runtime.shutdown_timeout(runtime_shutdown_cap); - + shutdown_runtime_after_supervisor(runtime); result } +/// Tear down the service Tokio runtime without blocking past the SCM stop budget. +/// +/// After `block_on` returns, outstanding `spawn_blocking` work (for example a +/// stuck Windows auto-start spawn) would otherwise keep `Runtime` drop waiting +/// indefinitely and delay reporting `SERVICE_STOPPED`. +fn shutdown_runtime_after_supervisor(runtime: tokio::runtime::Runtime) { + let signal_time = super::service_stop_signal_time(); + let timeout = signal_time + .map(|signal| service_shutdown_deadline(signal).saturating_duration_since(Instant::now())) + .unwrap_or(Duration::from_secs(60)); + + if signal_time.is_some() { + if timeout.is_zero() { + warn!( + "SCM shutdown budget exhausted before runtime teardown; not waiting for outstanding tasks" + ); + } else { + info!("waiting up to {timeout:?} for runtime teardown (SCM budget)"); + } + } + + runtime.shutdown_timeout(timeout); +} + pub fn run_as_service() -> Result<()> { let name = service_name_wide(); From daa2cd89f897afa0ad921b5b9c28c1f0fb9e9f3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Manuel=20Almaza=20Ramiro?= Date: Fri, 28 Aug 2026 10:04:42 +0200 Subject: [PATCH 04/10] fix(procmgr): keep graceful stop waits after shutdown is requested Ordered shutdown already sets shutdown_requested, so an immediate zero timeout skipped every child's configured graceful interval. --- pkg/procmgr/rust/src/shutdown.rs | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/pkg/procmgr/rust/src/shutdown.rs b/pkg/procmgr/rust/src/shutdown.rs index 05db9a729a68..55090107bfa5 100644 --- a/pkg/procmgr/rust/src/shutdown.rs +++ b/pkg/procmgr/rust/src/shutdown.rs @@ -26,7 +26,6 @@ impl ShutdownBudget { } } - /// Prefer the SCM-anchored budget when a Windows service stop is in progress. pub(crate) fn prefer_service_stop(fallback: Self) -> Self { #[cfg(windows)] { @@ -37,17 +36,14 @@ impl ShutdownBudget { fallback } - /// Re-read the active service stop signal (for example after graceful wait). pub(crate) fn refresh(self) -> Self { Self::prefer_service_stop(self) } - /// Budget for a single Stop RPC or child teardown outside ordered shutdown. pub(crate) fn for_single_stop() -> Self { Self::prefer_service_stop(Self::unlimited(Instant::now())) } - /// Budget for ordered shutdown after a service stop signal. pub(crate) fn service_stop(signal_time: Instant) -> Self { #[cfg(windows)] { @@ -75,7 +71,6 @@ impl ShutdownBudget { self.remaining_cap(from_stop_timeout) } - /// Remaining time for a phase, capped by `cap` and the service deadline. pub(crate) fn remaining_cap(&self, cap: Duration) -> Duration { self.deadline .map(|deadline| cap.min(deadline.saturating_duration_since(Instant::now()))) @@ -94,14 +89,10 @@ impl ShutdownBudget { } } -/// Wait for graceful child exit, or cut short when service shutdown is requested. pub(crate) async fn wait_graceful_or_shutdown>( graceful_budget: Duration, fut: F, ) -> Result { - if crate::platform::shutdown_requested() { - return tokio::time::timeout(Duration::ZERO, std::future::pending::()).await; - } #[cfg(windows)] { tokio::select! { @@ -147,6 +138,26 @@ mod tests { test_helpers::make_config(cmd, args) } + #[tokio::test] + async fn test_already_requested_shutdown_still_waits_graceful_budget() { + let _guard = crate::platform::test_shutdown_lock().await; + crate::platform::reset_shutdown_state_for_test(); + crate::platform::signal_shutdown_for_test(); + + let started = Instant::now(); + let result = + wait_graceful_or_shutdown(Duration::from_millis(150), std::future::pending::<()>()) + .await; + crate::platform::reset_shutdown_state_for_test(); + + assert!(result.is_err(), "pending future should time out"); + let elapsed = started.elapsed(); + assert!( + elapsed >= Duration::from_millis(100), + "already-requested shutdown must not skip the graceful budget, elapsed {elapsed:?}" + ); + } + #[test] fn test_prefer_service_stop_without_signal_preserves_fallback() { let signal_time = Instant::now(); @@ -212,7 +223,6 @@ mod tests { p3.spawn().unwrap(); let mut procs = vec![p1, p2, p3]; - // Reverse order: p3, p2, p1 shutdown_ordered(&mut procs, &[2, 1, 0]).await; assert_eq!(procs[0].state(), ProcessState::Stopped); From 935853a820be42d92a2d2ec4b4562675be2cecc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Manuel=20Almaza=20Ramiro?= Date: Fri, 28 Aug 2026 10:08:45 +0200 Subject: [PATCH 05/10] fix(procmgr): subscribe to shutdown notify before rechecking the flag Register the waiter first so a missed notify_waiters still caps an in-flight Stop to the remaining SCM budget instead of the original stop_timeout. --- pkg/procmgr/rust/src/shutdown.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/pkg/procmgr/rust/src/shutdown.rs b/pkg/procmgr/rust/src/shutdown.rs index 55090107bfa5..82aef4c82247 100644 --- a/pkg/procmgr/rust/src/shutdown.rs +++ b/pkg/procmgr/rust/src/shutdown.rs @@ -95,12 +95,23 @@ pub(crate) async fn wait_graceful_or_shutdown Result { #[cfg(windows)] { + let notified = crate::platform::shutdown_notify().notified(); + tokio::pin!(notified); + notified.as_mut().enable(); + let budget = if crate::platform::shutdown_requested() { + ShutdownBudget::remaining_service_stop_cap(graceful_budget).unwrap_or(graceful_budget) + } else { + graceful_budget + }; + if budget.is_zero() { + return tokio::time::timeout(Duration::ZERO, std::future::pending::()).await; + } tokio::select! { biased; - _ = crate::platform::wait_for_shutdown() => { + _ = notified => { tokio::time::timeout(Duration::ZERO, std::future::pending::()).await } - result = tokio::time::timeout(graceful_budget, fut) => result, + result = tokio::time::timeout(budget, fut) => result, } } #[cfg(not(windows))] From 458688967c6547188ce01d535fb77584cb061359 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Manuel=20Almaza=20Ramiro?= Date: Fri, 28 Aug 2026 11:01:57 +0200 Subject: [PATCH 06/10] fix(procmgr): honor shutdown flag before first notify waiter SCM can record shutdown before startup polls shutdown_signal, so a lost notify left the service in STOP_PENDING. Return immediately when the flag is already set and begin stopping on empty-catalog startup. --- pkg/procmgr/rust/src/manager/runtime.rs | 15 +++++++++++++++ pkg/procmgr/rust/src/platform/unix/mod.rs | 3 +++ pkg/procmgr/rust/src/platform/windows/mod.rs | 3 +++ 3 files changed, 21 insertions(+) diff --git a/pkg/procmgr/rust/src/manager/runtime.rs b/pkg/procmgr/rust/src/manager/runtime.rs index 8e2afe102b69..56a56fbe3453 100644 --- a/pkg/procmgr/rust/src/manager/runtime.rs +++ b/pkg/procmgr/rust/src/manager/runtime.rs @@ -746,6 +746,21 @@ mod tests { platform::reset_shutdown_state_for_test(); } + #[tokio::test] + async fn shutdown_signal_completes_when_requested_before_first_poll() { + let _guard = platform::test_shutdown_lock().await; + platform::reset_shutdown_state_for_test(); + platform::signal_shutdown_for_test(); + + let shutdown = platform::shutdown_signal(); + tokio::pin!(shutdown); + tokio::time::timeout(Duration::from_millis(50), shutdown.as_mut()) + .await + .expect("shutdown should complete when the flag was set before the first poll"); + + platform::reset_shutdown_state_for_test(); + } + #[tokio::test] async fn drain_exits_during_work_drains_beyond_channel_capacity() { let manager = empty_manager(); diff --git a/pkg/procmgr/rust/src/platform/unix/mod.rs b/pkg/procmgr/rust/src/platform/unix/mod.rs index 0a57d01dc188..509a72bf0d7d 100644 --- a/pkg/procmgr/rust/src/platform/unix/mod.rs +++ b/pkg/procmgr/rust/src/platform/unix/mod.rs @@ -110,6 +110,9 @@ pub fn stderr_inheritable() -> bool { } pub(crate) async fn wait_for_shutdown() { + if shutdown_requested() { + return; + } use tokio::signal::unix::{SignalKind, signal}; let mut sigterm = signal(SignalKind::terminate()).expect("failed to register SIGTERM handler"); let mut sigint = signal(SignalKind::interrupt()).expect("failed to register SIGINT handler"); diff --git a/pkg/procmgr/rust/src/platform/windows/mod.rs b/pkg/procmgr/rust/src/platform/windows/mod.rs index b501e8695b4f..2c0678adb7e5 100644 --- a/pkg/procmgr/rust/src/platform/windows/mod.rs +++ b/pkg/procmgr/rust/src/platform/windows/mod.rs @@ -406,6 +406,9 @@ fn default_stable_fleet_policies_dir() -> Option { } pub(crate) async fn wait_for_shutdown() { + if shutdown_requested() { + return; + } tokio::select! { result = tokio::signal::ctrl_c() => { result.expect("failed to register Ctrl+C handler"); From 2cc14e890e4b07a836125a6f4c44a9bdd386046d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Manuel=20Almaza=20Ramiro?= Date: Fri, 28 Aug 2026 11:37:57 +0200 Subject: [PATCH 07/10] fix(procmgr): register shutdown waiter before rechecking flag wait_for_shutdown now pins and enables the notify waiter before rechecking shutdown_requested, closing the check-to-subscribe race that could leave shutdown blocked after an early SCM stop. --- pkg/procmgr/rust/src/platform/unix/mod.rs | 5 ++++- pkg/procmgr/rust/src/platform/windows/mod.rs | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/pkg/procmgr/rust/src/platform/unix/mod.rs b/pkg/procmgr/rust/src/platform/unix/mod.rs index 509a72bf0d7d..6a6891a17a03 100644 --- a/pkg/procmgr/rust/src/platform/unix/mod.rs +++ b/pkg/procmgr/rust/src/platform/unix/mod.rs @@ -110,6 +110,9 @@ pub fn stderr_inheritable() -> bool { } pub(crate) async fn wait_for_shutdown() { + let notified = shutdown_notify().notified(); + tokio::pin!(notified); + notified.as_mut().enable(); if shutdown_requested() { return; } @@ -125,7 +128,7 @@ pub(crate) async fn wait_for_shutdown() { mark_shutdown_requested(); log::info!("received SIGINT"); } - _ = shutdown_notify().notified() => { + _ = notified => { mark_shutdown_requested(); } } diff --git a/pkg/procmgr/rust/src/platform/windows/mod.rs b/pkg/procmgr/rust/src/platform/windows/mod.rs index 2c0678adb7e5..69978230a0aa 100644 --- a/pkg/procmgr/rust/src/platform/windows/mod.rs +++ b/pkg/procmgr/rust/src/platform/windows/mod.rs @@ -406,6 +406,9 @@ fn default_stable_fleet_policies_dir() -> Option { } pub(crate) async fn wait_for_shutdown() { + let notified = shutdown_notify().notified(); + tokio::pin!(notified); + notified.as_mut().enable(); if shutdown_requested() { return; } @@ -415,7 +418,7 @@ pub(crate) async fn wait_for_shutdown() { SHUTDOWN_REQUESTED.store(true, Ordering::SeqCst); log::info!("received Ctrl+C"); } - _ = shutdown_notify().notified() => { + _ = notified => { SHUTDOWN_REQUESTED.store(true, Ordering::SeqCst); log::info!("received service stop request"); } From 72392c4d87ca1eef6db6c6dcb901f063b0540b00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Manuel=20Almaza=20Ramiro?= Date: Fri, 28 Aug 2026 13:01:03 +0200 Subject: [PATCH 08/10] fix(procmgr): resolve CI rustfmt, clippy, and module visibility Gate for_single_stop to Windows, re-export deferred spawn cleanup from manager, and align startup SCM budget check with the base formatting. --- pkg/procmgr/rust/src/manager/mod.rs | 1 + pkg/procmgr/rust/src/manager/startup.rs | 5 +++-- pkg/procmgr/rust/src/platform/windows/scm_service.rs | 2 +- pkg/procmgr/rust/src/shutdown.rs | 1 + 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pkg/procmgr/rust/src/manager/mod.rs b/pkg/procmgr/rust/src/manager/mod.rs index 1de3c622e9d1..461bbdfc7ee5 100644 --- a/pkg/procmgr/rust/src/manager/mod.rs +++ b/pkg/procmgr/rust/src/manager/mod.rs @@ -19,6 +19,7 @@ mod tracked_join; #[cfg(test)] mod tests; +pub(crate) use deferred_cleanup::join_deferred_spawn_tasks; pub(crate) use lifecycle::Lifecycle; pub(crate) use runtime::RuntimeContext; diff --git a/pkg/procmgr/rust/src/manager/startup.rs b/pkg/procmgr/rust/src/manager/startup.rs index 2890e9659f00..c4bde263678b 100644 --- a/pkg/procmgr/rust/src/manager/startup.rs +++ b/pkg/procmgr/rust/src/manager/startup.rs @@ -121,8 +121,9 @@ async fn join_in_flight_spawn( handle: tokio::task::JoinHandle>, ) { #[cfg(windows)] - if let Some(cap) = shutdown::ShutdownBudget::remaining_service_stop_cap(Duration::from_secs(180)) - { + if let Some(signal_time) = platform::service_stop_signal_time() { + let budget = shutdown::ShutdownBudget::service_stop(signal_time); + let cap = budget.remaining_cap(Duration::from_secs(180)); if cap.is_zero() { warn!( "startup: SCM shutdown budget exhausted; deferring in-flight auto-start to supervisor teardown" diff --git a/pkg/procmgr/rust/src/platform/windows/scm_service.rs b/pkg/procmgr/rust/src/platform/windows/scm_service.rs index e25256958c74..61ba7b470d7d 100644 --- a/pkg/procmgr/rust/src/platform/windows/scm_service.rs +++ b/pkg/procmgr/rust/src/platform/windows/scm_service.rs @@ -24,7 +24,7 @@ use windows_sys::Win32::System::Services::{ use crate::config::YamlConfigLoader; use crate::manager::ProcessManager; -use crate::manager::deferred_cleanup::join_deferred_spawn_tasks; +use crate::manager::join_deferred_spawn_tasks; use crate::shutdown::ShutdownBudget; use crate::uuid_gen::V4UuidGenerator; diff --git a/pkg/procmgr/rust/src/shutdown.rs b/pkg/procmgr/rust/src/shutdown.rs index 82aef4c82247..0b3c3f72c6cc 100644 --- a/pkg/procmgr/rust/src/shutdown.rs +++ b/pkg/procmgr/rust/src/shutdown.rs @@ -40,6 +40,7 @@ impl ShutdownBudget { Self::prefer_service_stop(self) } + #[cfg(windows)] pub(crate) fn for_single_stop() -> Self { Self::prefer_service_stop(Self::unlimited(Instant::now())) } From 8e189a10c3fd8b1f8ab07e5c3de99cf8b17c6ad5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Manuel=20Almaza=20Ramiro?= Date: Fri, 28 Aug 2026 13:27:11 +0200 Subject: [PATCH 09/10] fix(procmgr): gate deferred spawn re-export to Windows The join_deferred_spawn_tasks re-export is only used from scm_service, so exporting it on Linux triggers Clippy unused-import failures. --- pkg/procmgr/rust/src/manager/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/procmgr/rust/src/manager/mod.rs b/pkg/procmgr/rust/src/manager/mod.rs index 461bbdfc7ee5..ac64418a5976 100644 --- a/pkg/procmgr/rust/src/manager/mod.rs +++ b/pkg/procmgr/rust/src/manager/mod.rs @@ -19,6 +19,7 @@ mod tracked_join; #[cfg(test)] mod tests; +#[cfg(windows)] pub(crate) use deferred_cleanup::join_deferred_spawn_tasks; pub(crate) use lifecycle::Lifecycle; pub(crate) use runtime::RuntimeContext; From c98e08c3ecb0e67b898e20f2825d36cc78d3e549 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Manuel=20Almaza=20Ramiro?= Date: Fri, 28 Aug 2026 15:42:34 +0200 Subject: [PATCH 10/10] fix(procmgr): wire Stop RPC waits through shutdown-aware helper Route StopWaitPlan graceful waits through wait_graceful_or_shutdown, anchor handle_stop to for_single_stop(), and refresh the SCM budget between graceful and force-kill phases so in-flight Stop RPCs cut short on service shutdown. --- pkg/procmgr/rust/src/manager/process_manager.rs | 5 +---- pkg/procmgr/rust/src/process.rs | 12 +++++++----- pkg/procmgr/rust/src/shutdown.rs | 1 - 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/pkg/procmgr/rust/src/manager/process_manager.rs b/pkg/procmgr/rust/src/manager/process_manager.rs index ccc53485e289..2e133926dfae 100644 --- a/pkg/procmgr/rust/src/manager/process_manager.rs +++ b/pkg/procmgr/rust/src/manager/process_manager.rs @@ -253,10 +253,7 @@ impl ProcessManager { }; self.catalog - .wait_for_process_stop( - idx, - crate::shutdown::ShutdownBudget::unlimited(std::time::Instant::now()), - ) + .wait_for_process_stop(idx, crate::shutdown::ShutdownBudget::for_single_stop()) .await; let state = self.catalog.read_processes().await[idx].state(); diff --git a/pkg/procmgr/rust/src/process.rs b/pkg/procmgr/rust/src/process.rs index b4f6be300ec1..2b30698a7dd6 100644 --- a/pkg/procmgr/rust/src/process.rs +++ b/pkg/procmgr/rust/src/process.rs @@ -236,7 +236,7 @@ impl StopWaitPlan { mut handle, graceful_budget, .. - } => match time::timeout(graceful_budget, &mut handle).await { + } => match shutdown::wait_graceful_or_shutdown(graceful_budget, &mut handle).await { Ok(Ok(status)) => StopWaitResult::Exited(status), Ok(Err(error)) => { warn!("[{name}] watcher join failed during stop: {error:#}"); @@ -249,7 +249,7 @@ impl StopWaitPlan { mut handle, graceful_budget, .. - } => match time::timeout(graceful_budget, handle.wait()).await { + } => match shutdown::wait_graceful_or_shutdown(graceful_budget, handle.wait()).await { Ok(Ok(status)) => StopWaitResult::Exited(Some(status)), Ok(Err(error)) => { warn!("[{name}] wait failed during stop: {error:#}"); @@ -953,6 +953,7 @@ impl ManagedProcess { return false; } + let budget = budget.refresh(); let graceful_budget = budget.graceful_budget(self.stop_timeout()); match result { StopWaitResult::Exited(status) => self.apply_stop_exit(status), @@ -999,7 +1000,7 @@ impl ManagedProcess { self.mark_stopped(); } self.stop_wait_generation.clear(); - self.release_stop_wait_resources(budget).await; + self.release_stop_wait_resources(budget.refresh()).await; } pub(crate) fn has_orphaned_stop_wait(&self) -> bool { @@ -1133,7 +1134,7 @@ impl ManagedProcess { } pub async fn wait_for_stop(&mut self) { - self.wait_for_stop_since(ShutdownBudget::unlimited(Instant::now())) + self.wait_for_stop_since(ShutdownBudget::unlimited(std::time::Instant::now())) .await; } @@ -1267,12 +1268,13 @@ pub(crate) trait StopWaitContext { pub(crate) async fn run_stop_wait( ctx: &mut C, - budget: ShutdownBudget, + mut budget: ShutdownBudget, ) -> Option { let mut owner_needing_complete = None; while let Some(plan) = ctx.plan_stop(budget).await { let owner = plan.owner(); let result = plan.execute().await; + budget = budget.refresh(); if ctx.finalize_stop(owner, result, budget).await { owner_needing_complete = Some(owner); } else { diff --git a/pkg/procmgr/rust/src/shutdown.rs b/pkg/procmgr/rust/src/shutdown.rs index 0b3c3f72c6cc..82aef4c82247 100644 --- a/pkg/procmgr/rust/src/shutdown.rs +++ b/pkg/procmgr/rust/src/shutdown.rs @@ -40,7 +40,6 @@ impl ShutdownBudget { Self::prefer_service_stop(self) } - #[cfg(windows)] pub(crate) fn for_single_stop() -> Self { Self::prefer_service_stop(Self::unlimited(Instant::now())) }