Skip to content

fix(worker): bound hierarchical startup with readiness protocol - #1396

Merged
ChaoWao merged 1 commit into
hw-native-sys:mainfrom
ChaoWao:feat/recursive-worker-startup-readiness
Jul 18, 2026
Merged

fix(worker): bound hierarchical startup with readiness protocol#1396
ChaoWao merged 1 commit into
hw-native-sys:mainfrom
ChaoWao:feat/recursive-worker-startup-readiness

Conversation

@ChaoWao

@ChaoWao ChaoWao commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

Summary

Hierarchical Worker startup could hang forever. A chip (L2) child that failed ChipWorker.init only wrote its error and exited, and the parent spun on while state != INIT_DONE with no timeout and no liveness check, so a dead or slow child deadlocked startup (the #1003 / #980 class of hang). Next-level (L4→L3) children had no readiness barrier at all: a failing inner init unwound back through the forked copy of the caller's frames instead of reporting a bounded error.

This makes startup a strong READY boundary — it brings the worker subtree up READY, or returns a bounded error in finite time, and rolls a failed epoch back. It is the P0 lifecycle groundwork the downstream memory-ABI / directed-scheduling / EdgeChannel / cross-machine work all sit on.

Changes

  • StatesINIT_READY / INIT_FAILED (renamed INIT_DONE), mirrored in the C++ MailboxState enum for parity.
  • Child readiness + no-unwind entry — every forked child (sub, chip, next-level) publishes INIT_READY after its own init succeeds, or INIT_FAILED carrying the original error. A shared _forked_child_main guarantees a child always terminates via os._exit and never unwinds an exception into the parent's startup frames (which would let it SIGKILL its inherited-PID siblings); INIT_READY is published only after all fallible setup (inner init and identity-table build).
  • Parent barrier _await_children_ready — waits per child with a deadline (startup_timeout_s, default 300s, validated positive + finite) and a waitpid(WNOHANG) liveness check. INIT_FAILED, a dead child, or a blown deadline raises a bounded RuntimeError. Applied to the sub, chip, and next-level edges.
  • Rollback (_abort_hierarchical, on any pre-scheduler BaseException incl. KeyboardInterrupt) — next-level children that reached their serve loop are closed gracefully (SHUTDOWN + bounded wait) so they unlink their own nested shms; the rest are SIGKILLed; PIDs the barrier already reaped are excluded from the kill (no reused-PID signal); all are reaped and the parent mailboxes freed.

Scope / non-goals (honest boundaries)

  • Next-level children are still started lazily on first run() — this PR does not move _start_hierarchical into init(). The barrier is deliberately recursive plumbing so a later eager-init change makes INIT_READY propagate up only after the whole subtree is ready, with no protocol change. Landing eager-init first would only move the hang from first run() to init().
  • The happy-path barrier tests verify the next-level barrier + dispatch; they do not assert full L4→L3→L2 eager readiness (descendants are lazy).
  • Residual leak: a next-level sibling still inside inner.init() when another child fails cannot service SHUTDOWN, so it is SIGKILLed and its partially-created nested shms are reclaimed by the multiprocessing resource_tracker at interpreter exit rather than at rollback. Closing that race needs the eager-init handshake (follow-up). Children that reached READY leak nothing.

startup_timeout_s is a per-Worker config kwarg (mirrors py_control_timeout_s / remote_session_timeout_s), not a behavior gate — the bounded wait is unconditional. Default is generous so a legitimately slow init is never falsely reaped; the point is to bound hangs.

Testing

New device-free ut coverage (tests/ut/py/test_worker/test_startup_readiness.py, next-level + sub init injection, faked ChipWorker on a2a3sim): bounded init-failure error, child-exit-before-ready, deadline-fires, rollback-reaps-no-leak, graceful-close of a READY sibling, sub-edge liveness, timeout config validation, and healthy-tree happy paths. Every failure test is wrapped in a hard SIGALRM budget so a regression fails fast instead of hanging CI; confirmed the pre-fix baseline hangs on these.

  • tests/ut/py/test_worker/ + test_remote_l3_lifecycle — 186 passed, 2 skipped
  • pre-commit (ruff check/format, clang-format, clang-tidy, cpplint, pyright) clean on changed files
  • a2a3 runtime rebuild clean (C++ enum parity); zero net /dev/shm leak across the rollback tests
  • Hardware (onboard) tests — chip path exercised device-free via faked ChipWorker; not yet run on silicon

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1c97c6b4-d3ae-4c3b-819a-cd4801b4147b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Hierarchical worker startup now uses _INIT_READY and _INIT_FAILED mailbox states, bounded readiness polling, configurable startup timeouts, recursive failure propagation, and child cleanup. New tests cover successful startup, initialization errors, hard exits, hangs, and chip startup behavior.

Changes

Hierarchical startup readiness

Layer / File(s) Summary
Readiness states and child signaling
python/simpler/worker.py, src/common/hierarchical/worker_manager.h
Defines successful and failed initialization states, validates startup_timeout_s, and publishes child initialization outcomes through the mailbox.
Recursive readiness barrier
python/simpler/worker.py
Waits for all chip and next-level children, detects failures or premature exits, enforces deadlines, resets ready states, and aborts pre-scheduler failures.
Startup readiness coverage
tests/ut/py/test_worker/test_startup_readiness.py
Tests successful recursive startup, initialization exceptions, hard exits, hangs, cleanup, and chip initialization failures.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ParentWorker
  participant ChildWorker
  participant Mailbox
  participant Scheduler
  ChildWorker->>Mailbox: Publish INIT_READY or INIT_FAILED
  ParentWorker->>Mailbox: Await child readiness
  Mailbox-->>ParentWorker: Return state or error message
  ParentWorker->>Scheduler: Start dispatch after all children are ready
Loading

Possibly related PRs

Poem

A rabbit saw children queue in a row,
With ready or failed states set to show.
No endless spin, no startup fright,
Hops now wait for bounded light.
Then tasks leap forth when all is right.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: bounding hierarchical worker startup with a readiness protocol.
Description check ✅ Passed The description matches the changeset and explains the new readiness states, bounded waits, rollback, and tests.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces a robust startup readiness handshake protocol for hierarchical workers, replacing the previous unbounded spin-wait with a bounded readiness barrier. It adds INIT_READY and INIT_FAILED states, monitors child process liveness using waitpid, and enforces a configurable startup timeout to prevent hangs. It also ensures proper cleanup of spawned processes and shared memory on startup failure, and includes comprehensive unit tests to verify these changes. I have no feedback to provide as the implementation is solid and well-tested.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@coderabbitai coderabbitai 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.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@python/simpler/worker.py`:
- Around line 3581-3589: Update the hierarchical startup failure handler
surrounding the readiness wait to catch BaseException rather than only
Exception, ensuring KeyboardInterrupt and SystemExit enter the existing cleanup
path. Preserve the _abort_hierarchical call for pre-scheduler failures, set
_hierarchical_start_state to "failed", notify _hierarchical_start_cv, and
re-raise the original exception.
- Around line 1774-1778: Update the startup timeout validation immediately after
assigning self._startup_timeout_s in the worker initialization flow to reject
non-finite values as well as values less than or equal to zero. Use the existing
numeric validation context around self._startup_timeout_s and raise the same
configuration error for NaN or infinity, preserving acceptance of only finite
positive timeouts.
- Around line 3616-3626: Update the initialization wait logic around os.waitpid
and _abort_hierarchical so any PID consumed by waitpid, including both normal
exited-worker handling and ChildProcessError paths as applicable, is marked as
reaped and removed or excluded from the retained PID collection before rollback.
Ensure _abort_hierarchical filters these consumed PIDs and never sends SIGKILL
to a PID already reaped by waitpid.

In `@tests/ut/py/test_worker/test_startup_readiness.py`:
- Around line 202-215: Guarantee teardown for both happy-path worker tests by
moving w4.close() into each test’s finally block before shared-memory cleanup.
Update the sites at tests/ut/py/test_worker/test_startup_readiness.py lines
202-215 and 233-249; preserve the existing assertions and cleanup behavior.
- Around line 52-54: Update the two-child startup readiness test and its
_increment_counter helper to eliminate the shared counter read-modify-write
race. Give each child process its own shared-memory counter slot, or synchronize
access so increments are atomic, and adjust the assertions around the affected
test flow to verify both child increments reliably.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4c867ebe-2d1f-4a26-84e6-830d08876e39

📥 Commits

Reviewing files that changed from the base of the PR and between 8fba232 and e6c1b00.

📒 Files selected for processing (3)
  • python/simpler/worker.py
  • src/common/hierarchical/worker_manager.h
  • tests/ut/py/test_worker/test_startup_readiness.py

Comment thread python/simpler/worker.py Outdated
Comment thread python/simpler/worker.py Outdated
Comment thread python/simpler/worker.py
Comment thread tests/ut/py/test_worker/test_startup_readiness.py
Comment thread tests/ut/py/test_worker/test_startup_readiness.py
@ChaoWao
ChaoWao force-pushed the feat/recursive-worker-startup-readiness branch 2 times, most recently from 74c51cb to 23194d6 Compare July 18, 2026 10:17
Hierarchical Worker startup could hang forever. A chip (L2) child that
failed ChipWorker.init only wrote its error and exited, and the parent
spun on `while state != INIT_DONE` with no timeout and no liveness check,
so a dead or slow child deadlocked startup (the hw-native-sys#1003 / hw-native-sys#980 class of
hang). Next-level (L4->L3) children had no readiness barrier at all: a
failing inner init unwound back through the forked copy of the caller's
frames instead of reporting a bounded error.

Make startup a strong READY boundary:

- States: INIT_READY / INIT_FAILED (renamed INIT_DONE), mirrored in the
  C++ MailboxState enum for parity.
- Every forked child (sub, chip, next-level) publishes INIT_READY after
  its own init succeeds, or INIT_FAILED carrying the original error.
  A shared `_forked_child_main` guarantees a child always terminates via
  os._exit and never unwinds an exception into the parent's startup
  frames (which would let it SIGKILL its inherited-PID siblings); READY is
  published only after all fallible setup (inner init AND identity tables).
- Parent barrier `_await_children_ready` waits per child with a deadline
  (`startup_timeout_s`, default 300s, validated positive+finite) and a
  waitpid(WNOHANG) liveness check; INIT_FAILED, a dead child, or a blown
  deadline raises a bounded RuntimeError. Applied to the sub, chip, and
  next-level edges (chip and next-level had no/weak barriers before).
- Rollback (`_abort_hierarchical`, run on any pre-scheduler BaseException,
  including KeyboardInterrupt): next-level children that reached their
  serve loop are closed gracefully (SHUTDOWN + bounded wait) so they
  unlink their own nested shms; the rest are SIGKILLed; PIDs the barrier
  already reaped are excluded from the kill (no reused-PID signal); all
  are reaped and the mailboxes freed. A sibling still mid-init when
  another fails is SIGKILLed and its nested shms are reclaimed by the
  resource_tracker at exit — closing that race needs the eager-init
  handshake (follow-up).

Next-level children are still started lazily on first run(); the barrier
is recursive plumbing so a later eager-init change makes INIT_READY
propagate up only after the whole subtree is ready, with no protocol
change.

Adds device-free ut coverage (next-level + sub init injection, faked
ChipWorker on a2a3sim): bounded init-failure error, child-exit-before-
ready, deadline-fires, rollback-reaps-no-leak, graceful-close of a READY
sibling, sub-edge liveness, timeout config validation, and healthy-tree
happy paths. Every failure test is wrapped in a hard SIGALRM budget so a
regression fails fast instead of hanging CI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ChaoWao
ChaoWao merged commit 35b76c5 into hw-native-sys:main Jul 18, 2026
16 checks passed
@ChaoWao
ChaoWao deleted the feat/recursive-worker-startup-readiness branch July 19, 2026 02:59
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