@@ -39,11 +39,6 @@ const RESTART_BACKOFF_MAX: Duration = Duration::from_secs(60);
3939/// Must not depend on UI/API polls — desktop `--autostart` has no browser traffic.
4040const SUPERVISOR_TICK : Duration = Duration :: from_secs ( 2 ) ;
4141
42- /// Grace period when killing a persisted orphan on panel restore. Keep this short:
43- /// the previous panel is already gone, and blocking startup for [`STOP_GRACE_SECS`]
44- /// per orphan makes cold start feel broken. SIGKILL follows if needed.
45- const ORPHAN_STOP_GRACE_SECS : i64 = 2 ;
46-
4742#[ derive( Debug , Clone , Serialize , Deserialize ) ]
4843struct PersistedBot {
4944 id : String ,
@@ -179,7 +174,10 @@ impl ProcessRuntime {
179174 Ok ( e) => e,
180175 Err ( _) => return Ok ( ( ) ) ,
181176 } ;
182- let mut inner = self . inner . lock ( ) . unwrap ( ) ;
177+ // Collect first so orphan stops can run concurrently. Sequential waits
178+ // would be N × STOP_GRACE_SECS on a multi-bot restore.
179+ let mut pending: Vec < ( String , LiveBot , bool ) > = Vec :: new ( ) ;
180+ let mut orphans: Vec < ( u32 , Option < u64 > ) > = Vec :: new ( ) ;
183181 for entry in entries. filter_map ( |e| e. ok ( ) ) {
184182 let path = entry. path ( ) ;
185183 if path. extension ( ) . and_then ( |e| e. to_str ( ) ) != Some ( "json" ) {
@@ -199,24 +197,39 @@ impl ProcessRuntime {
199197 restart_after : None ,
200198 restart_failures : 0 ,
201199 } ;
202- // Kill any orphan left from a previous panel process before we spawn
203- // again — otherwise two market makers share one wallet. Only signal
204- // the pid when it still looks like our stitch binary (and, on Linux,
205- // the starttime matches) so a recycled PID is never killed .
200+ // Only signal the pid when it still looks like our stitch binary
201+ // (and, on Linux, the starttime matches) so a recycled PID is never
202+ // killed. Clear the persisted pid before waiting so a crash mid-
203+ // restore doesn't leave a stale claim .
206204 if let Some ( pid) = live. record . pid . take ( ) {
207205 let starttime = live. record . pid_starttime . take ( ) ;
208- terminate_managed_pid ( pid, starttime, & self . stitch_bin , ORPHAN_STOP_GRACE_SECS ) ;
209- // If graceful stop didn't land (stuck child, ignored SIGTERM),
210- // don't leave the orphan running beside the respawn.
211- if pid_is_our_stitch ( pid, starttime, & self . stitch_bin ) {
212- tracing:: warn!(
213- pid,
214- "orphan survived graceful stop on restore; sending SIGKILL"
215- ) ;
216- terminate_pid ( pid, 0 ) ;
217- }
206+ orphans. push ( ( pid, starttime) ) ;
218207 let _ = persist_record ( & self . state_dir , & live. record ) ;
219208 }
209+ pending. push ( ( name, live, wanted) ) ;
210+ }
211+
212+ // Full STOP_GRACE_SECS so a mid-tick bot can finish after SIGTERM —
213+ // a 2s grace would SIGKILL during that window. Parallelize so fleet
214+ // size doesn't multiply cold-start latency.
215+ let stitch_bin = & self . stitch_bin ;
216+ std:: thread:: scope ( |scope| {
217+ for ( pid, starttime) in orphans {
218+ scope. spawn ( move || {
219+ terminate_managed_pid ( pid, starttime, stitch_bin, STOP_GRACE_SECS ) ;
220+ if pid_is_our_stitch ( pid, starttime, stitch_bin) {
221+ tracing:: warn!(
222+ pid,
223+ "orphan survived graceful stop on restore; sending SIGKILL"
224+ ) ;
225+ terminate_pid ( pid, 0 ) ;
226+ }
227+ } ) ;
228+ }
229+ } ) ;
230+
231+ let mut inner = self . inner . lock ( ) . unwrap ( ) ;
232+ for ( name, mut live, wanted) in pending {
220233 if wanted {
221234 if let Err ( e) = spawn_bot ( & self . stitch_bin , & self . state_dir , & mut live) {
222235 tracing:: error!( "failed to restore {}: {e:#}" , live. record. name) ;
@@ -672,18 +685,27 @@ fn pid_kill0_exists(pid: u32) -> bool {
672685}
673686
674687/// Poll briefly for [`process_starttime`] after spawn — right after `execve` the
675- /// `/proc` entry can briefly be missing under load.
688+ /// `/proc` entry can briefly be missing under load. No-op on non-Linux: starttime
689+ /// is always `None` there, so polling would only burn ~500ms per spawn.
676690fn wait_process_starttime ( pid : u32 ) -> Option < u64 > {
677- for _ in 0 ..50 {
678- if let Some ( start) = process_starttime ( pid) {
679- return Some ( start) ;
680- }
681- if !process_alive ( pid) {
682- return None ;
691+ #[ cfg( target_os = "linux" ) ]
692+ {
693+ for _ in 0 ..50 {
694+ if let Some ( start) = process_starttime ( pid) {
695+ return Some ( start) ;
696+ }
697+ if !process_alive ( pid) {
698+ return None ;
699+ }
700+ std:: thread:: sleep ( Duration :: from_millis ( 10 ) ) ;
683701 }
684- std:: thread:: sleep ( Duration :: from_millis ( 10 ) ) ;
702+ process_starttime ( pid)
703+ }
704+ #[ cfg( not( target_os = "linux" ) ) ]
705+ {
706+ let _ = pid;
707+ None
685708 }
686- process_starttime ( pid)
687709}
688710
689711/// Linux starttime from `/proc/<pid>/stat` (field 22), used as a pid-reuse guard.
0 commit comments