Skip to content

Let go of a process that will not die - #75

Merged
yuchanns merged 2 commits into
mainfrom
f-20260909-stop-without-blocking
Sep 9, 2026
Merged

Let go of a process that will not die#75
yuchanns merged 2 commits into
mainfrom
f-20260909-stop-without-blocking

Conversation

@yuchanns

@yuchanns yuchanns commented Sep 9, 2026

Copy link
Copy Markdown
Owner

stop() waited for a process to exit and for its readers to see EOF, both without a bound, and both while holding the lifecycle lock that start() needs. When a grandchild inherited the pipe — the ordinary case on Windows, where CreateProcess hands every inheritable handle to the child — that EOF never arrived, so shutting a runtime down blocked forever and no new inbound could start a replacement.

Reaching the kill step now means we have already given up on this process, so it stops being ours: this side of the pipes is closed, the readers and watcher are cancelled, and the process is handed to a reaper task. Closing our own end is what unblocks everything, since the exit waiters only complete once the pipes count as disconnected — whoever still holds the write end is welcome to keep it. stop() marks the state and returns, so the lock is free immediately.

The reaper waits for the returncode without a deadline, warning once it has waited too long. A timeout there would only abandon the process: nothing would reap it, nothing would retry, and we would not know it existed. It publishes the exit when it arrives, unless a newer process already owns the supervisor.

Verified on both platforms. A test with real processes — a child that ignores SIGTERM plus a grandchild holding stdout — took 60.42s and failed before the change and passes in 1.36s after it. On Windows the same scenario returns from stop(timeout=1) in 1.006s with no process left behind.

Also fixes four tests that assumed a POSIX host, which the first full Windows run exposed: two hardcoded POSIX values for creationflags and the upgrade instructions, one stripped a real O_BINARY and so wrote in text mode, and one gave a node about two seconds to publish its endpoint, which is not enough for a loaded Windows box. That last one now waits on the clock and reports the child's exit and stderr when it gives up.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 9, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-09T16:50:42.409473Z 042572f Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@yuchanns

yuchanns commented Sep 9, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cb38f31c50

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/bazaar_compute_node/contrib/codex/process.py Outdated
Comment thread src/bazaar_compute_node/contrib/codex/process.py Outdated
Comment thread src/bazaar_compute_node/contrib/codex/process.py Outdated
Comment thread src/bazaar_compute_node/contrib/codex/process.py Outdated
Comment thread tests/app/test_daemon_process.py
@yuchanns
yuchanns force-pushed the f-20260909-stop-without-blocking branch 3 times, most recently from 47d2272 to f700b39 Compare September 9, 2026 13:26
@yuchanns

yuchanns commented Sep 9, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f700b39623

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if self._generation == generation:
self._returncode = returncode

async def _join_within(self, deadline: float) -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Inline the single-use join deadline helper

_join_within is called only once and merely computes the remaining deadline before delegating to _join_tasks; inline this fragment into stop() instead of adding a one-use wrapper, as required by the repository's explicit convention.

AGENTS.md reference: AGENTS.md:L2-L2

Useful? React with 👍 / 👎.

Comment thread tests/contrib/test_codex.py Outdated

assert supervisor.state is JsonlProcessState.STOPPED
assert elapsed < 10
assert not supervisor.is_running

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remove the prohibited negative assertion

In this held-pipe test, assert not supervisor.is_running is a redundant negative assertion immediately after the positive STOPPED contract; remove it and retain the positive observable checks because the repository explicitly prohibits reverse/exact assertions in tests.

AGENTS.md reference: AGENTS.md:L17-L17

Useful? React with 👍 / 👎.

Comment thread tests/contrib/test_codex.py Outdated
Comment on lines +977 to +981
subprocess.Popen(
[sys.executable, "-c", "import time; time.sleep(60)"],
stdout=sys.stdout,
stderr=sys.stderr,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reap the grandchild created by the test

Every run of this test detaches a Python grandchild and loses its handle when the parent exits, so after the assertion completes that interpreter continues sleeping for 60 seconds; the second test and its restart create additional orphans, allowing repeated or parallel test runs to accumulate processes. Record the grandchild PID and terminate it in test cleanup after the supervisor behavior has been observed.

Useful? React with 👍 / 👎.

Comment on lines +535 to +536
future.set_exception(
JsonlProcessExited(returncode=-1, stderr_tail=self.stderr_tail)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the actual exit code for pending requests

When the direct process has already exited but an inherited pipe forces the task-join abandonment path, process.returncode and usually _returncode contain the real status, yet every pending request is failed with the fabricated status -1. Callers therefore receive incorrect diagnostics—for example, a clean exit is reported as -1—so use the known process return code and reserve an unknown value for the forced-kill case where it has not arrived yet.

Useful? React with 👍 / 👎.

@yuchanns
yuchanns force-pushed the f-20260909-stop-without-blocking branch from f700b39 to 3523bd5 Compare September 9, 2026 13:49
@yuchanns

yuchanns commented Sep 9, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3523bd5cc5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +248 to +250
self._abandon(process)
self._state = ProcessState.STOPPED
self._exit_event.set()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reset Claude exit signals before restarting

When start() is called after this forced-abandonment branch, the replacement process inherits a set _exit_event and the _CLOSED item queued by _abandon(), because Claude's start() increments the generation but resets neither object. Consequently, wait() returns immediately with the abandoned run's status and receive() can report that the new, still-running process has exited; clear the previous lifecycle's event and queue before spawning the replacement.

Useful? React with 👍 / 👎.

Comment on lines +248 to +250
self._abandon(process)
self._state = ProcessState.STOPPED
self._exit_event.set()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reset Claude exit signals before restarting

When the forced-stop supervisor is started again, this remains set because start() only increments the generation; wait() therefore returns immediately with the prior or still-unknown result instead of waiting for the replacement process. Likewise, _abandon() leaves _CLOSED in the incoming queue, so the replacement's first receive() reports the abandoned exit. Clear the exit event and stale queue state when beginning the new lifecycle.

Useful? React with 👍 / 👎.

@yuchanns
yuchanns force-pushed the f-20260909-stop-without-blocking branch from 3523bd5 to cb4384e Compare September 9, 2026 14:07
@yuchanns

yuchanns commented Sep 9, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cb4384e68b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread tests/contrib/test_claude.py Outdated
Comment on lines +630 to +633
script = "import time; time.sleep(30)"
supervisor = ProcessSupervisor(
ProcessSpec(sys.executable, ("-c", script), tmp_path, os.environ)
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Move the substituted Claude process scenario to E2E

This test substitutes a Python sleeper for the external Claude process and places the scenario in the unit-test suite, so it cannot verify the actual provider process lifecycle. Cover this with the real Claude executable as an E2E test, using TestChannel as the control plane, instead of a replacement executable.

AGENTS.md reference: AGENTS.md:L18-L20

Useful? React with 👍 / 👎.

Comment thread tests/contrib/test_claude.py Outdated
Comment on lines +640 to +645
assert supervisor.returncode is None
# a caller asking about this run has to be kept waiting
with pytest.raises(TimeoutError):
await supervisor.wait(timeout=0.2)
with pytest.raises(TimeoutError):
async with asyncio.timeout(0.2):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Replace the negative restart assertions

The test establishes correctness solely by asserting that no return code, wait completion, or received item appears during fixed 200 ms windows. These reverse and timing-exact assertions are prohibited by the repository test convention and can pass without demonstrating that the replacement lifecycle eventually behaves correctly; assert a positive observable result from the restarted lifecycle instead.

AGENTS.md reference: AGENTS.md:L17-L17

Useful? React with 👍 / 👎.

@yuchanns
yuchanns force-pushed the f-20260909-stop-without-blocking branch from cb4384e to f9fc4ae Compare September 9, 2026 14:37
@yuchanns

yuchanns commented Sep 9, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f9fc4ae558

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/bazaar_compute_node/contrib/claude/process.py Outdated
Comment on lines +259 to +260
process.kill()
await process.wait()
self._abandon(process)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Wait for SIGKILL before declaring abandonment

When a Claude child ignores SIGTERM but dies normally on SIGKILL, this calls _abandon() immediately after sending the kill, without using the final 10% of the caller's timeout to observe termination. As a result, an ordinary forced termination is reported to blocked receive() callers as ClaudeProcessExited(None) and is unnecessarily detached into a reaper, even though the documented close sequence reserves the remaining timeout for bounded reap; wait through the final deadline after kill() and abandon only if that wait also expires.

AGENTS.md reference: AGENTS.md:L7-L7

Useful? React with 👍 / 👎.

@yuchanns
yuchanns force-pushed the f-20260909-stop-without-blocking branch 2 times, most recently from c2339b5 to 1f2ce3d Compare September 9, 2026 16:10
@yuchanns

yuchanns commented Sep 9, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1f2ce3dcba

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

# the process may already have exited here, and then its status is the
# honest answer; -1 is reserved for a kill whose result has not arrived
exited = JsonlProcessExited(
returncode=process.returncode if process.returncode is not None else -1,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Report an unobserved exit status as unknown

When forced abandonment occurs before process.wait() observes the kill, -1 is not an unknown-status sentinel: on POSIX, negative return codes identify signals, so this falsely reports SIGHUP termination. The fresh code now preserves known statuses, but JsonlProcessExited already accepts None and renders it as unknown; pass None here until the reaper obtains the actual result.

Useful? React with 👍 / 👎.

Comment thread tests/contrib/test_claude.py Outdated
Comment on lines +601 to +602
assert supervisor.returncode == -signal.SIGKILL
assert await supervisor.wait(timeout=1) == -signal.SIGKILL

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Replace the exact SIGKILL return-code assertions

These assertions make the new test depend on an exact platform-specific return-code representation, contrary to the repository's explicit prohibition on exact assertions; verify the positive lifecycle outcome without requiring equality to -SIGKILL.

AGENTS.md reference: AGENTS.md:L17-L17

Useful? React with 👍 / 👎.

Comment on lines +190 to +191
_kill_process(process)
await process.wait()
await self._join_tasks()
self._abandon(process)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reserve time to observe the Codex kill

When the child consumes the whole deadline by ignoring stdin closure and SIGTERM but terminates normally on SIGKILL, this sequence abandons it immediately after sending the kill instead of waiting for that result. stop() can therefore report completion while the process and transport are still live, allowing shutdown or a replacement connection to overlap an ordinary forced termination; reserve a final bounded slice of the timeout for process.wait() and abandon only if that expires.

Useful? React with 👍 / 👎.

yuchanns and others added 2 commits September 9, 2026 12:32
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@yuchanns
yuchanns force-pushed the f-20260909-stop-without-blocking branch from 1f2ce3d to 042572f Compare September 9, 2026 16:32
@yuchanns

yuchanns commented Sep 9, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

Provided git ref 042572fb33b86c351c0f8d4bea92331e8672046b does not exist
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@yuchanns

yuchanns commented Sep 9, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

Provided git ref 042572fb33b86c351c0f8d4bea92331e8672046b does not exist
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@yuchanns

yuchanns commented Sep 9, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 042572fb33

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1080 to +1081
await supervisor.start(timeout=5)
await supervisor.stop(timeout=1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Wait for the signal handler before stopping

On a slow or heavily loaded host, start() only confirms that the subprocess was created; it does not guarantee that the script has installed its SIGTERM handler. Calling stop() immediately can therefore deliver SIGTERM before signal.signal(...) runs, and the assertions still pass because any termination produces a non-None return code, so this test never exercises the advertised SIGKILL path. Have the child publish a readiness marker after installing the handler and wait for it before calling stop().

Useful? React with 👍 / 👎.

@yuchanns
yuchanns merged commit 1fa9ff2 into main Sep 9, 2026
1 check passed
@yuchanns
yuchanns deleted the f-20260909-stop-without-blocking branch September 9, 2026 18:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant