fix(cron): make script timeout process-tree cleanup bounded - #71506
fix(cron): make script timeout process-tree cleanup bounded#71506yinkev wants to merge 4 commits into
Conversation
…aned descendants (NousResearch#71148) Run cron scripts in their own session so the child and all its descendants form a single process group. On timeout, use os.killpg() (POSIX) or taskkill /T (Windows) to kill the entire process tree instead of only the direct child PID. Before: subprocess.run(capture_output=True, timeout=...) only kills the direct child. Descendant processes survive, get re-parented to PID 1, and accumulate across repeated runs. After: subprocess.Popen(start_new_session=True) + proc.communicate(timeout=...). On TimeoutExpired the whole process group is killed, preventing orphan process accumulation.
55a7fc0 to
2dcfdd7
Compare
# Conflicts: # tests/cron/test_cron_script.py
teknium1
left a comment
There was a problem hiding this comment.
Thanks for preserving the original implementation credit while completing the bounded cross-platform cleanup. The premise is still present on current main: cron/scheduler.py:2303-2311 uses subprocess.run(..., capture_output=True, timeout=...), and cron/scheduler.py:2335-2336 only reports timeout after the direct-child behavior of subprocess.run.
Problems
- The POSIX regression does not assert the isolation prerequisite.
tests/cron/test_script_timeout_tree.py:51-61mocksos.killpg, buttests/cron/test_cron_script.py:203-214does not assertstart_new_session=True. A future regression could retain the expected mocked kill call without creating the separate group that makes it reach descendants.
Suggested changes
- Assert
captured["kwargs"]["start_new_session"] is Truein the non-WindowsPopencontract test.
Automated hermes-sweeper review.
| monkeypatch.setattr(sched_mod.sys, "platform", "linux") | ||
| monkeypatch.setattr(sched_mod.subprocess, "run", fake_run) | ||
| monkeypatch.setattr( | ||
| sched_mod.subprocess, "Popen", _successful_popen(captured) |
There was a problem hiding this comment.
Please also assert captured["kwargs"]["start_new_session"] is True here. The timeout test mocks os.killpg, so without this contract assertion it would not catch a regression that launches the child in the scheduler's process group rather than an isolated group.
SummarySeven PRs address or reference the two causes in the issue complex: process-tree leaks on script timeout and misclassification of script timeouts as provider timeouts. #59379 is the recorded best fix for the two-cause issue; #71506 and #74123 provide broader process-cleanup alternatives, while the remaining PRs are partial or duplicate implementations. Related pull requests
Duplicates#59574 and closed #59924 substantially duplicate the two-cause implementation in #59379. #68262, #71152, #71506, and #74123 overlap on process-tree cleanup; #71152 is superseded by the bounded cleanup in #71506, while #74123 is a broader alternative cleanup implementation. Suggested consolidationFor #71506, keep open with a salvage path: retain its bounded post-kill drain and cross-platform cleanup ideas, add the requested start_new_session contract assertion, and either rebase onto #59379 or split the cleanup slice for incorporation there; do not recommend merging it. Keep #74123 available as the stronger process-cleanup reference, while author-action on #59379 should rebase or split the complete two-cause fix; close #59574 as a duplicate of #59379, close #71152 as superseded by #71506/#74123, and leave #59924 and #68262 closed as duplicate/reference implementations. Complex graphflowchart TD
classDef open fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
classDef merged fill:#dcfce7,stroke:#15803d,color:#14532d
classDef closed fill:#e5e7eb,stroke:#6b7280,color:#1f2937
classDef unverified fill:#f3f4f6,stroke:#9ca3af,color:#374151
classDef best stroke-width:3px,stroke:#b45309
classDef target stroke-width:3px,stroke:#4338ca
I59549(["issue #59549 (open)"])
I71148(["issue #71148 (open)"])
subgraph Dup59379 ["PRs duplicating each other"]
P59379["PR #59379 (open)"]
P59574["PR #59574 (open)"]
P59924["PR #59924 (closed)"]
P68262["PR #68262 (closed)"]
P71152["PR #71152 (open)"]
P71506["PR #71506 (open)"]
P74123["PR #74123 (open)"]
end
P71506 -.->|partial| I59549
P71506 -->|best fix| I71148
class I59549 open
class I71148 open
class P59379 open
class P59574 open
class P59924 closed
class P68262 closed
class P71152 open
class P71506 open
class P74123 open
class P59379 best
class P59379 best
class P71506 best
class P74123 best
class P71506 target
click I59549 "https://github.com/NousResearch/hermes-agent/issues/59549"
click I71148 "https://github.com/NousResearch/hermes-agent/issues/71148"
click P59379 "https://github.com/NousResearch/hermes-agent/pull/59379"
click P59574 "https://github.com/NousResearch/hermes-agent/pull/59574"
click P59924 "https://github.com/NousResearch/hermes-agent/pull/59924"
click P68262 "https://github.com/NousResearch/hermes-agent/pull/68262"
click P71152 "https://github.com/NousResearch/hermes-agent/pull/71152"
click P71506 "https://github.com/NousResearch/hermes-agent/pull/71506"
click P74123 "https://github.com/NousResearch/hermes-agent/pull/74123"
Graph: solid arrow = fixes / best fix, dashed arrow = partial or unverified (see edge label); boxed group = PRs duplicating each other; amber border = best fix; indigo border = target; gray node = closed (state tag in the node label). Cross-PR triage: Reviewed 7 pull requests and 2 issues in this complex. Each diff was read against this issue; Assessment working set: 64 kB of PR diffs, 21 kB of issue/PR text, 15 kB of discussion (21 comments), 20 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch. |
monerostar
left a comment
There was a problem hiding this comment.
Ubuntu 26.04 linux-5800x (kernel 7.0.0-28-generic) here.
Live check on PR head 011fad0:
- pytest tests/cron/test_script_timeout_tree.py tests/cron/test_cron_script.py: 24 passed in 1.48s
- main scheduler has no killpg / start_new_session path for cron scripts. PR adds _terminate_cron_script_tree with os.killpg and starts the script session with start_new_session=True so the group kill can land.
Looks good from Linux for the orphaned-descendant timeout class.
Decision gate: superseded as a merge branchI compared current The underlying bug still exists on current This branch contains two useful design choices: direct POSIX
#74123 materially supersedes this branch for the cleanup half: it adds bounded kill-and-reap handling, real descendant and inherited-pipe regressions, non-timeout failure cleanup, an actual own-session execution test, Windows This matches the current maintainer triage: keep #71506 available as salvageable reference material, but do not recommend merging it; use #74123 as the stronger cleanup reference and #59379 for the complete two-cause issue. RecommendationLeave #71506 open and unchanged as a reference branch. Consolidate a maintained replacement from:
I am intentionally not rebasing or pushing this branch and not resolving its review thread, because adding the single assertion would make the stale branch look merge-ready while duplicating a materially stronger implementation. |
|
Closing per #85125 Phase 4a: the cron script timeout now tree-kills via agent.deadline.kill_process_tree (PR #93795, merged) — including setsid'd descendants. The post-kill pipe drain is bounded by _drain_script_pipes (communicate timeout=5s → kill → wait timeout=5s → abandon to OS reaper). @yinkev's process-tree cleanup approach informed the design; credit preserved in the 4a PR body. |
Summary
Fixes #71148.
Relationship to #71152
This preserves @webtecnica's original implementation commit and authorship, then addresses the concrete review/CI blockers on #71152:
taskkillnon-zero now falls back toproc.kill();communicate()is bounded to one second;killpgis guarded for the Windows-footgun checker and falls back to direct-child kill;CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOWthrough the shared compatibility helper;Popen-contract tests are updated rather than continuing to mocksubprocess.run;/T /Ftree kill.Verification
py_compile, Windows-footgun scan, andgit diff --check: clean.