Conversation
|
🚫 This pull request was removed from the merge queue because it was pushed to by @harsh839. Please re-submit it in order to merge. See more details here.
After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here |
|
🤖 Automated review (Claude) — first review of this PR. Fully automated, no human in the loop; advisory only, not an approval (I can't approve or merge). Reviewed from the diff/code as source of truth, not the description. No blocking issues found. I verified the load-bearing pieces:
One non-blocking simplification. The guard is inserted on the coordinator-attached ( Minor edge note (also non-blocking): containment only holds while the shell stays alive. A shell that forks a background child and then exits immediately ( Generated by Claude Code |
|
Addressed the non-blocking suggestion: the shell guard is now scoped to the in-process
The
|
|
🤖 Automated review (fully automated Claude Code review — no human vetted this; advisory only, not an approval, and I can't approve or merge). Reviewed from the diff. New commit This won't compile for Windows targets. In
None of these exist in libc's Windows bindings, so Suggest gating the Otherwise the mechanism holds up: Generated by Claude Code |
|
Fixed the Windows-compile blocker: Verified: |
|
🤖 Automated review (fully automated Claude Code review — no human vetted this; advisory only, not an approval, and I can't approve or merge). Reviewed from the diff. Re-reviewed at Generated by Claude Code |
|
@phil-opp Could you proceed with this merge? Claude re-review is clean (no blocking issues) and all CI checks are green. If it's in the queue for trunk, please submit it — or let me know if the author-side |
|
🤖 Automated review by Claude — fully automated review; no human has vetted this. Advisory only, posted like an outside contributor — I can't approve or merge. One blocking issue that the earlier passes and PR CI didn't surface (the The new hidden Worth deciding how to resolve rather than just regenerating the snapshot: Generated by Claude Code |
|
Addressed the blocking surface-snapshot issue — new head
Verified: |
|
🤖 Automated review by Claude — fully automated review, no human vetted this; advisory only, not an approval. Re-reviewed at Generated by Claude Code |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
🤖 Automated review (Claude Code): this was generated by a fully automated review with no human in the loop, so treat it as advisory only. I re-read head The guard dumps core itself when the guarded program crashes ( When the guarded program dies from a signal whose default action dumps core (SIGSEGV, SIGABRT, SIGBUS, SIGFPE, SIGILL, SIGQUIT, …), In practice, suppose a C/C++/Rust program runs as a
That sends users looking for a dora bug that doesn't exist. On I checked the mechanism with a small repro: a child resets SIGABRT to A minimal fix that keeps the exact Generated by Claude Code |
The exit-time containment ran for every unix node, so on the coordinator-attached `dora up` path a node that exited on its own had whatever it left in its process group `SIGKILL`ed with no grace period, no log line and no way to opt out. That is a silent 1.0 behaviour change well past what this PR's title says, and past what dora-rs#3472 asks: a helper, viewer or launcher a node started is allowed to keep running, and a child the node itself stopped just before returning is mid-cleanup, not abandoned. `contain_exited_group` now takes the spawn path's word on it, read from the same `DORA_RUN_PARENT_PID` marker that already distinguishes in-process `dora run` from `dora up` for PDEATHSIG and the Windows Job Object, and takes an abandoned group only on `dora run`. That is the whole of dora-rs#3472: `dora run` is about to exit, nothing else can reach the fork, and leaving it behind is the hang being fixed. Off that path a dataflow that wants its strays reaped stops the node, which is the stop-ladder half below and is not gated. Both `SIGKILL`s now say so, so "my child vanished" has an answer in the daemon log instead of only in this diff. Signed-off-by: harsh839 <harshbhargav440@gmail.com> Assisted-by: Claude
|
Agreed with the finding, and took the first of your two suggestions. Pushed The exit-time kill is now gated on the in-process So the behaviour change is now the one this PR's title describes and no more. Both
On the two coverage points: Still no test for the |
|
🤖 This is a fully automated review by Claude Code, with no human in the loop. Treat it as advisory only. I re-reviewed Fixed: the gating works.
That fixes the earlier finding that the exit-time kill applied to every unix node on every path. New issue: every normal node exit under
Still open from earlier reviews, not addressed by the new commit:
Generated by Claude Code |
A review of d792114, and two findings from earlier rounds it had left open. The one I introduced: the abandon branch logged and signalled an empty group. A node that does its work and exits — a source that sends N messages and returns, anything that stops when its inputs close — leaves an empty group, so on `dora run`, which prints warnings, every short-lived node announced that it had been SIGKILLed. It now asks whether the group still has members first, the same question the ladder below already asks, for the same reason: process-wrap's group wait has reaped the group by now, so a member still in it is still running. The guard used to die from the guarded process's signal with the inherited `ulimit -c` intact, dumping a core whose executable is `dora` — so `coredumpctl` answered "dora segfaulted" for a crash inside a node's program. `re_raise` zeroes the limit first. The node's own dumps are unaffected: it was exec'd with the limit still set, long before. `grace_duration` is whatever a dataflow says it is, and `Instant + Duration` panics on overflow, so a typo took the daemon's event loop down instead of stopping a node. The duration is clamped to a century, which is what a number that large means, and leaves the two deadlines arithmetic that cannot overflow. Signed-off-by: harsh839 <harshbhargav440@gmail.com> Assisted-by: Claude
|
Took all three. Pushed Spurious WARN on every normal exit — my bug, from the last commit. Right, and worse than a log line: it signalled unconditionally. An empty group now returns before the warning and before the Core dumps attributed to Grace-duration panic — agreed, fixed. Clamped to a century. That is what a number that large means to whoever wrote it, and it leaves both deadlines arithmetic that cannot overflow rather than relying on the clamp being big enough. A new test drives the real thing with Unverified here: the Windows path is untouched by all of this, and I still cannot cross-check it locally — Still no |
|
🤖 This is a fully automated review by Claude Code (no human in the loop). Treat it as advisory. I re-reviewed Fixed:
Issues I found:
Generated by Claude Code |
|
🤖 Automated review (Claude Code). This review is fully automated, with no human in the loop. Treat it as advisory only. I re-read head
With their fixes reverted, Generated by Claude Code |
|
🤖 Automated review (Claude Code). This review is fully automated, with no human in the loop. Treat it as advisory only. Fresh pass on the unchanged head A group of only zombies still counts as live, so a stop that already succeeded waits for the SIGKILL deadline (
Earlier findings that are still open:
Generated by Claude Code |
|
🤖 Additional review notes (Claude Code). Found while reviewing head
Generated by Claude Code |
…ut the stop-channel invariant Two review notes on 052521b. The core-limit test set `rlim_max: RLIM_INFINITY` after checking the premise with a comparison that also accepted a finite hard limit above 8 MiB — an unprivileged process cannot raise a finite hard limit to infinity, so the `setrlimit` would have failed with EPERM instead of the premise's own message. It also called `clear_core_dumps` in this process, which lowers the *hard* limit to 0 and cannot be undone without privilege: every other test in the binary would have run with core dumps silently disarmed. It now forks, keeps the hard limit where it found it, and reports through the exit status. `ProcessHandle::submit` is a blocking `send` on the event loop, and the channel holds 2 while the ladder sends 3. That is intended and the reasoning is not obvious, so it is now written down where the call is: the wait task is the only consumer and sits in a `select!` on the receiver for as long as the node lives, so a slot frees as soon as it is scheduled; and a full channel would mean the node is already gone and the receiver dropped, which fails fast instead of blocking. The review suggested `try_send` with a logged fallback, which would be a regression — it drops the marker when the channel is full, and the marker is the thing that holds the group for its grace period. Signed-off-by: harsh839 <harshbhargav440@gmail.com> Assisted-by: Claude
|
Took the two code notes, skipped the third, and rewrote the PR description. Core-limit test — both halves right. It set You also suggested going through Blocking I did not take the PR description — removed. The "Note for the merge queue" section described a stacking that ended on 09-24, and pointed at a CI: fmt and clippy clean on both crates, 335 daemon and 431 CLI lib tests, and the lifecycle e2e is unchanged at 24/25 with the |
|
🤖 Automated follow-up review (Claude Code). This review is fully automated, with no human in the loop. Treat it as advisory only. I re-reviewed
Still open from earlier reviews (this commit doesn't touch
Generated by Claude Code |
…e-limit guards Four claims in the comments and docs did not survive checking, and a comment that is wrong is worse than none: it is what the next reader trusts instead of re-deriving. - `ProcessHandle::submit` blocks for a stronger reason than the one given. The old text argued the wait task drains the channel; it cannot, and a full channel with a live-but-unscheduled receiver would block. The real reason is that this is the first message on a fresh per-incarnation channel, so it cannot be full at all. - `group_has_members` was justified by process-wrap's group wait reaping every member. It only reaps *our* children: a reparented grandchild returns ECHILD and ends its loop. A zombie does answer `killpg(pgid, 0)` with 0, so a reaped-away orphan can read as a member. The leader being reaped is the part that actually holds, and the residue costs a no-op signal. - The `dora run` justification said the run "is about to exit". It need not be: a source that returns leaves the same unreachable group whether the dataflow has three seconds or three minutes left. The orphan is the reason, not the countdown. - `clear_core_dumps` claimed to leave no dump behind, which is only true of the file-based case. Where `core_pattern` pipes to a handler — `systemd-coredump`, `apport` — the kernel collects regardless of RLIMIT_CORE. The zombie finding also had a test consequence: `process_alive` used `kill(pid, 0)`, which reports a zombie as present, so the suite's verdict depended on whether the host's PID 1 had reaped a reparented grandchild. It now reads the state from /proc on Linux, and a regression test asserts the `kill`-says-present, state-says-gone gap that made the fix necessary. Signed-off-by: harsh839 <harshbhargav440@gmail.com> Assisted-by: Claude
|
Thanks — the I argued the wait task drains the channel so a slot frees. It cannot: a full The zombie point was also right, and my comment was wrong about the mechanism. /* fork a child in its own group, let it exit, and never reap it */
killpg(pgid, 0) -> 0 /* non-empty: the zombie is counted */So the residue is real: a reparented grandchild that has exited but that its It also had a test consequence I had not thought about. The other two were overstated claims in comments, now corrected rather than
Not addressing: the bundling. You have raised it three times and I have offered Local: 336 daemon lib tests, fmt and clippy clean, lifecycle e2e 24/25 with The |
|
🤖 Automated review (Claude Code) — this review is fully automated, with no human in the loop. Treat it as advisory only.
There is one remaining issue. It is the 09-30 06:57Z finding, which this commit acknowledges in a comment but leaves unfixed: The new loop comment understates what a zombie costs. In
This commit also makes the gap harder to see. The tests' Suggested fix: apply the same Generated by Claude Code |
|
🤖 Fully automated review by Claude Code. No human checked this; it's advisory only. I found one new issue at Under the
On
Generated by Claude Code |
|
🤖 Automated review (Claude Code). This review is fully automated, with no human in the loop, so treat it as advisory only. I reviewed head A fresh pass found one point the thread hasn't raised yet. The new e2e tests add roughly 5–6 minutes to the required PR CI.
Suggestion: keep one fast regression test in the PR job, for example The open items already on this thread still stand. Generated by Claude Code |
killpg(pgid, 0) cannot tell a corpse from a running process: a zombie stays in its group until it is reaped. For our own children that gap is invisible, because the process-wait task reaps them. For a reparented grandchild it is not -- only PID 1 can reap that, and an init that leaves orphans lying may never. On the stop branch this decides when the wait *ends*, and the caller sends the node's exit report only after it returns, so a zombie holds back the dataflow finishing, `dora run` exiting and any restart for the rest of the grace period, and logs a false "ignored the stop grace period" on the way. Reading it as empty is safe: a group cannot gain a member except by a member forking into it. The scan costs a few milliseconds per group, so the loop pays it once every ZOMBIE_CHECK_EVERY polls rather than every poll. Signed-off-by: harsh839 <harshbhargav440@gmail.com> Assisted-by: Claude
The guard host is the `dora` CLI, which under the `dora-rs-cli` wheel is a python console script, and the spawner applies the node's environment to the whole command. A `PYTHONHOME` meant for the node's interpreter can leave that python unable to start at all -- verified: `PYTHONHOME=/nonexistent python3 -c ...` aborts before any code runs -- which takes the guard down with it, and losing the guard silently loses the containment it exists to provide. Hand both variables to the guard under a `DORA_SHELL_GUARD_` prefix and override them with an empty value, which python reads as unset, so the host starts on its own interpreter. The guard restores them on the child, which is the process they were meant for. Overriding rather than removing is deliberate: `clonable_command::Command::env_remove` deletes the entry instead of recording the removal, so the variable is inherited anyway. Verified against 0.2.0 with a standalone probe. Signed-off-by: harsh839 <harshbhargav440@gmail.com> Assisted-by: Claude
|
Both substantive findings are fixed in 1. Zombie-only group ( The new test uses the production function, not a helper. Making it deterministic took some doing: a reparented grandchild does not survive, because systemd reaps instantly — the very case the fix exists for is the one a normal host cannot reproduce. So the fixture is 2. Wheel env regression ( One thing worth recording: I first used 3. CI time — I could not reproduce the estimate, so I changed nothing. Measured the whole file the way the e2e job runs it (
So the five tests add ~21s total, about 1s each — not 5-6 minutes. The bulk of that ~9 minute job is pre-existing. Two notes on the per-test figure, since a single test reports a misleading number:
Happy to move any of the five behind |
|
Automated review by Claude (fully automated, not a human review). Reviewed at I found the following issues: 1.
The second point is a regression for plain The new test sets the daemon process env in place of the node env. That is the one setup where the ordering doesn't matter, so the test doesn't catch this. One fix is to move the node's values to the prefixed names after the descriptor env has been applied, in 2. In the stop branch of The caller sends It also means the replayed SIGTERM/SIGKILL can go out up to 750 ms after the last membership check. That widens the pgid-reuse window the loop comment says stays within one poll interval. Doing the cheap Generated by Claude Code |
|
🤖 Automated review (fully automated review by Claude Code — no human has vetted this; please verify the findings). I did a fresh pass over head 1. Clearing the variables also takes away the guard host's own
2. The new tests modify the process environment in a multi-threaded test binary
Generated by Claude Code |
|
Automated review by Claude (fully automated, not a human review) — reviewed at I did a fresh pass focused on the platform-specific code: the Linux 1. Linux: The check is The same predicate decides both branches, so:
It is uncommon, but it happens: some C/C++ daemons and runtimes end 2. The Each call opens and reads Windows: I re-checked the gating by inspection. Everything unix-only ( Verification: I found #1 by reading the code and confirmed the kernel behaviour with the probe described above (Linux 6.18). I did not run it through Generated by Claude Code |
|
🤖 Automated review (fully automated Claude Code review; no human vetted this). I re-reviewed head 1. Regression from the latest commit (
2. Every graceful stop of a unix node now waits about 750 ms before the exit is reported, on both
One general note: the diff is now about 2.7k lines. A large share of that is comments, and some of them are long design write-ups that repeat each other ( Generated by Claude Code |
`cargo deny` fails the whole workspace on `yoke-derive 0.8.3`, which has been yanked since main locked it. It arrives from `cargo`/`tokio` and is not related to dora-rs#3472, but it reds the audit gate on every PR until the lock moves. `cargo update -p yoke-derive` takes 0.8.3 -> 0.8.4, the current non-yanked release, and leaves every other dependency alone. Checked against the crates.io index that nothing else in the lock is yanked, so this is the whole of the gate's complaint. Signed-off-by: harsh839 <harshbhargav440@gmail.com> Assisted-by: Claude
Three defects in the zombie handling, all reported by the automated review of e008f07: `killpg(pgid, 0)` counted as the whole answer was checked only on the fourth poll, so every graceful stop of every unix node waited out three 250ms sleeps before its exit report went out -- and with it the dataflow finishing, `dora run` exiting and any restart. The cheap syscall is now asked on every poll, which is where a correctly-stopped node's empty group is noticed, and only the `/proc` walk that tells a corpse from a live member is throttled. `state != "Z"` read a live process as a corpse when its main thread had ended while other threads kept running (`pthread_exit(0)` out of `main`, which some C/C++ daemons do; verified here as `state=Z` with `num_threads=2` while `killpg` still answers 0). Both branches used that predicate, so such a group was left alone on `dora run` -- the dora-rs#3472 orphan again -- and skipped the SIGKILL on the stop path. Only a single-threaded `Z` counts as dead now. The walk itself is blocking filesystem I/O and ran inline on the tokio workers that also drive the daemon event loop, once a second per stopping node. It goes to a blocking thread, and a join error counts as "still a member": acting on a group that could not be looked at risks killing a recycled one. Signed-off-by: harsh839 <harshbhargav440@gmail.com> Assisted-by: Claude
7727602 read `std::env::var` while building the guard command, but the node's `env:` is applied to that command afterwards by `compose_node_env`. So the value 7727602 moved aside was the daemon's own, the node's value landed on the guard host anyway -- the wheel failure it set out to fix was still there -- and the guard then put the daemon's value, or an empty one, on the child, so a shell node lost its own `env: PYTHONPATH` that works on main. The swap now runs in the spawner, once `compose_node_env` has put the node's value there, and moves only what the node set: the guard host keeps the daemon's value, which under the wheel is what lets it import `dora_cli` at all. Clearing that instead took the guard down for every shell node in a colcon or `pip install --target` workspace. Both new tests also stop writing to the process environment, which is global and was being mutated from libtest's parallel threads under a "single-threaded" comment that was not true. Signed-off-by: harsh839 <harshbhargav440@gmail.com> Assisted-by: Claude
|
Thanks — all six code findings were real. Fixed in three commits; details and evidence below. 1 + 2 + 3 — the python env handoff (three reviews, one root cause) — All three are the same bug: The swap now runs in
4 — Both tests are gone as written. The CLI test now goes through a 3-line seam ( 5 — Confirmed with the probe you describe, on this host: 6 — blocking
The ~750 ms stop delay — Worse than reported, in one respect: it was in Also: the Audit gate — The red gate is real but not from this PR: Validation — Not done, deliberately: the |
|
🤖 Automated review (fully automated Claude Code review; no human checked it) I reviewed the three commits since the last automated review (
The I found the following issues in the new commits: 1.
2. No test covers the ordering bug that
The earlier general note still applies: the PR is ~2.85k lines and still bundles the unrelated Generated by Claude Code |
Summary
Closes #3472.
path: shellnodes runsh -c <args>with no dora code in them, so the in-nodeDORA_RUN_PARENT_PIDorphan guard never arms. They were contained only by the daemon's spawn-timePR_SET_PDEATHSIG, which reaches the direct child alone: a shell that forks a background child (sh -c 'sleep 1000 & wait') leaks that child toppid 1whendora runis SIGKILLed.Guard routing on the
dora runpath only: a shell node now spawns a hiddendora __shell-guardCLI subcommand as the daemon's direct child and process-group leader. It arms on the injectedDORA_RUN_PARENT_PIDand, once that process is gone,killpgs its whole group — guard, shell, and background forks together — mirroring the in-nodeorphan_guard(it also clears the daemon's PDEATHSIG once its own 500ms poll is running, exactly like the in-node guard does).Stop-path signals: the guard now catches the daemon's stop signals (SIGTERM/SIGINT/SIGHUP), forwards them to the shell, and stays alive to reap it. This keeps the node registered past the grace period so the group SIGKILL escalation still lands on a TERM-ignoring shell and its background forks — nothing survives a graceful stop.
Included in this pattern: for a TERM-ignoring shell, the whole signal ladder ends the group; a SIGTERM'd daemon/group behaves the same way. The
dora up+dora start(coordinator-attached) path is unchanged and spawns the shell directly — nodes there are meant to outlive the daemon (#2029), and withoutDORA_RUN_PARENT_PIDthe guard could never arm, so it is deliberately not used. Windowscmd /Cis unchanged.Validation
run_killed_by_sigkill_does_not_orphan_shell_nodes: a shell node publishes the shell's pid and asleep 1000 &background child's pid,dora runis SIGKILLed, and both must be gone within 30s. Verified it fails without the fix (only the shell is PDEATHSIG-contained; the child survives) and passes with it.run_stop_does_not_orphan_term_ignoring_shell_nodes: a TERM-ignoring shell + background child must both be gone after a normal--stop-afterstop; the CLI exit is awaited through a boundedtry_wait(60s) that fails with the stderr tail rather than hanging.run_killed_by_sigkill_does_not_orphan_nodes,run_killed_before_node_init_does_not_orphan,run_killed_by_sigterm_terminates_nodes_and_exits.smoke_shell_node_allowed_with_flag/smoke_shell_node_blocked_without_flagstill pass (gate intact).cargo clippy -p dora-cli -p dora-daemon --all-targets -- -D warnings,cargo fmt --all -- --check,cargo test -p dora-cli --lib(420) and-p dora-daemon --lib(280) all clean.Note for the merge queue
This branch was stacked on #3541 while both were being reworked, and was de-stacked on 09-24: it no longer carries
feat/3489-mcap-export, and the diff has nodora recording exportin it. It stands on its own againstmain.