Skip to content

fix(py): kill a worker that stops itself instead of freezing the host - #415

Open
jat255 wants to merge 3 commits into
jat255/fj8r-plot-capturefrom
jat255/d5sr-stopped-worker-freeze
Open

jat255 wants to merge 3 commits into
jat255/fj8r-plot-capturefrom
jat255/d5sr-stopped-worker-freeze

Conversation

@jat255

@jat255 jat255 commented Oct 10, 2026 •

Copy link
Copy Markdown
Collaborator

This PR stops model code from freezing the host by stopping its own worker (os.kill(os.getpid(), signal.SIGSTOP)). Each local worker now gets a thread that kills the worker's process group when it stops, and the call reports a crash.

Stacked on #411.

Agent-written details

Why the host froze. On macOS, Python 3.14's asyncio treats waitid()'s report of a stopped child as an exit. It then calls a blocking waitpid() on the event-loop thread, which waits until the child really exits. Killing the stopped worker ends that wait. The reasoning is in the docstring of _kill_when_stopped (diff).

Same behaviour on every platform. The guard runs wherever os.waitid exists (Linux, and macOS from 3.13). A stopped worker is therefore reported as a crash on every platform, rather than as a call timeout on Linux. macOS on 3.11 and 3.12 has no os.waitid, so there a stopped worker still runs into the call timeout. asyncio on those versions does not freeze.

Rejected alternatives:

  • Denying self-signals in the seatbelt profile. A child of the worker can still stop it, and the unsafe opt-in has no sandbox.
  • Replacing asyncio's child watcher with a kqueue exit watch. That would add a second way to spawn the worker.

Tests. The write-bound test used SIGSTOP to make the worker stop reading. It now uses a stand-in worker that announces it's ready and never reads stdin. The new self-stop test hangs on 3.14 without the fix and passes with it. ruff, pyrefly, and the full suite pass, and the driver tests also pass on 3.14.

kata: d5sr

@jat255 jat255 added py Affects the Python implementation needs-manual-review Agent-created work that needs a human review labels Oct 10, 2026
@jat255
jat255 added this pull request to stack #416 October 10, 2026 01:34
@github-actions

Copy link
Copy Markdown
Contributor

Preview root: https://posit-dev.github.io/commons/pr-415/

Python site preview: https://posit-dev.github.io/commons/pr-415/py/

Built from the latest commit on this branch. The R links in it point at the published R site, which no pull request rebuilds.

jat255 added 3 commits October 9, 2026 19:51
On macOS, Python 3.14's asyncio treats waitid()'s report of a stopped
child as an exit and calls a blocking waitpid() on the event-loop thread,
so model code running os.kill(os.getpid(), signal.SIGSTOP) froze the host
until the worker really exited, which nothing could then make it do.

Each local worker session now starts a thread that waits for the worker
to stop or exit without reaping it, and SIGKILLs the process group if it
stopped. The blocked waitpid() returns at once and the driver reports a
crash. This runs on every platform with os.waitid, so a stopped worker
is reported the same way everywhere rather than as a call timeout on
Linux. macOS before 3.13 has no os.waitid, and keeps the timeout; its
asyncio does not freeze.

The write-bound test used SIGSTOP to make the worker stop reading; it
now uses a stand-in worker that announces itself ready and never reads.
@jat255
jat255 force-pushed the jat255/d5sr-stopped-worker-freeze branch from adf4cf8 to c705930 Compare October 10, 2026 01:51
@jat255
jat255 marked this pull request as ready for review October 10, 2026 01:51
@jat255 jat255 removed the needs-manual-review Agent-created work that needs a human review label Oct 10, 2026
@jat255 jat255 added this to the py-M6: code execution milestone Oct 10, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

py Affects the Python implementation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant