Skip to content

Commit 38cdbbf

Browse files
committed
fix(docker): stop orphan bots concurrently on panel restore
- Replace sequential orphan termination with parallel stops using thread scope, preventing cold-start latency from multiplying with fleet size - Use full `STOP_GRACE_SECS` for orphan termination instead of abbreviated 2s grace, allowing mid-tick bots to finish after SIGTERM before SIGKILL - Remove `ORPHAN_STOP_GRACE_SECS` constant now that orphans use the standard grace period - Skip starttime polling on non-Linux platforms where `process_starttime` always returns `None`, eliminating unnecessary ~500ms delay per spawn - Collect pending bots and orphans before locking inner state, enabling concurrent orphan cleanup during restore
1 parent da47aeb commit 38cdbbf

5 files changed

Lines changed: 55 additions & 33 deletions

File tree

.textile-monorepo-source

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
61409799eb0318a3e470357af80721005a4307a4
1+
346ac212f158b5cccad79ed598256ecf9edb01d3

.textile-stitch-release-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.1.137
1+
0.1.138

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "stitch-bot"
3-
version = "0.1.137"
3+
version = "0.1.138"
44
edition = "2021"
55
description = "Stitch — Textile filler-network operator bot; market-makes the filler order book with signed UniswapX limit orders."
66
license = "AGPL-3.0-or-later"

src/panel/docker/process_api.rs

Lines changed: 51 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
4040
const 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)]
4843
struct 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.
676690
fn 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

Comments
 (0)