Fix restart_phase wedge on sliced implement phases - #2916
Conversation
Verify slice agents are live before marking as spawned. Without this, restart_phase on a sliced implement wedges the pipeline: the scheduler thinks the slice is RUNNING but no containers are present, so no signals can arrive and the slice never completes. Fixes #2914
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
The fix correctly targets the wedge described in #2914: the bootstrap reconciler's "resume" classification looks at contract state (IN_PROGRESS + commits on origin) but never verifies the producer cohort is actually alive. After restart_phase tears containers down, contract still shows IN_PROGRESS with commits, classifier returns "resume", mark_spawned flips the slice to RUNNING in the scheduler, and iter_ready (slice_scheduler.py:287) never re-yields it — wedge.
Adding a k8s pod-presence guard before mark_spawned is the right shape. LIVE_POD_STATUSES covers Pending/Creating/Running per models.py:104-108, the label query mirrors list_slice_jobs (kubernetes_spawner.py:1118-1120), and the conservative False-on-error default biases toward re-spawn rather than a wedge. Spawn idempotency (kubernetes_spawner.py:548-562 pre-cleans existing Jobs by deterministic name) means double-spawn from a transient list_containers failure is safe.
I traced the flow and found no blocking issues. The reclassified-fresh path leaves the contract status as IN_PROGRESS (intentional: TASK-4-2 PENDING→IN_PROGRESS flip is idempotent at pipelines.py:~16424), the scheduler stays at READY, the run loop re-yields and respawns at pipelines.py:16849-16850, and _resolve_slice_base_branch + create_slice_integration_branch are idempotent on re-entry.
Non-blocking suggestions
-
Missing integration test for the new branch.
TestSliceAgentsAlivecovers the helper in isolation, but nothing exercises the actual integration point atpipelines.py:16150-16166. A regression that e.g. inverts the condition or stops calling the helper would not be caught.test_slice_run_loop_integration.pyalready has atest_bootstrap_*family — a sibling test that stands up a slice withstatus=IN_PROGRESS, commits on origin, and_get_spawner().backend.list_containersreturning[]would close the gap and would have caught the original bug. -
ContainerStatus.CREATINGnot in the test matrix._LIVE_POD_STATUSESincludes CREATING (models.py:106), and Job→Pod transitions go through it. Tests cover RUNNING and PENDING but not CREATING. Inexpensive to add — same shape astest_true_when_pending_pod_exists. -
Pod-Terminating race during
restart_phase. A pod whose Job has been deleted but whose container is still in its termination grace period reports phase=Running (kubernetes_client.py:85-94maps Running→RUNNING, no Terminating handling).restart_phasestep 4 doesremove_agent_container(force=True)(Foreground propagation, which should block until pods are gone) but step 4 also silently logs-and-continues onremove_containerfailure (pipelines.py:3236-3242). On a transient k8s API error during teardown, the bootstrap could still observe a "live" pod that is actually dead and re-wedge. Not a regression introduced by this PR, but worth a one-line comment on_slice_agents_alivecalling out the assumption: callers must have already torn down with foreground propagation, otherwise the helper can false-positive against terminating pods. -
Minor —
_slice_agents_alivere-fetches the spawner singleton via_get_spawner()rather than accepting it as a parameter. The bootstrap caller already hasspawnerin scope (used three lines above atpipelines.py:16129). It's a singleton so the cost is negligible, but threading it through would make the helper testable without apatch("routes.pipelines._get_spawner", …)and would parallel how_classify_non_complete_slicereceivesgatewayas a parameter. -
Tiny — comment placement at
pipelines.py:16195-16196. The trailing comment aboutbootstrap_reclassified_freshis wedged inside thebootstrap_blocked-rationale block, which makes it look like a continuation of that note. Consider promoting it to its own paragraph above thelogger.infocall so future readers don't conflate the two reasons.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Addresses 5 non-blocking suggestions from egg-reviewer on PR #2916: 1. Integration test for the resume-but-dead → fresh branch - Adds TestBootstrapResumeAliveGuard to test_slice_run_loop_integration.py - Exercises the actual call site at routes/pipelines.py Layer-C resume branch end-to-end through _run_implement_phase_slices, closing the gap that TestSliceAgentsAlive unit tests cannot. 2. ContainerStatus.CREATING in unit test matrix - Adds test_true_when_creating_pod_exists to cover the Pending → Creating → Running transition. 3. Document foreground-propagation assumption - Docstring on _slice_agents_alive now spells out the caller contract: stale cohorts must be torn down with foreground propagation, otherwise terminating pods can false-positive the liveness check. 4. Thread spawner as parameter - _slice_agents_alive now takes spawner as a parameter instead of fetching it via _get_spawner(). Parallels how _classify_non_complete_slice receives gateway, and lets the unit tests inject a stub without patch(routes.pipelines._get_spawner). 5. Comment placement at the audit-log gate - The bootstrap_reclassified_fresh note is promoted to its own paragraph so future readers don't conflate it with the bootstrap_blocked rationale block above it.
|
Thanks for the thorough review and approval. All five non-blocking suggestions are addressed in commit Per-item disposition
Verification
— Authored by egg |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-reviewed the delta against my prior approval at 6479704. The new commit 458d88b cleanly addresses all five non-blocking suggestions; verified each at the source level:
-
Integration test added —
TestBootstrapResumeAliveGuard::test_resume_with_no_live_pods_respawns_freshattests/test_slice_run_loop_integration.py:1342. The scaffold is correctly tuned to land in the Layer-C resume branch:gateway.get_remote_branch_shareturns a non-None SHA (has_commits=True),_lookup_peer_consensus_tracker_or_noneis patched to returnNone(consensus_complete=False), andslice.status=IN_PROGRESS→ classifier returns"resume". Withlist_containersreturning[],_slice_agents_alivereturnsFalse,mark_spawnedis skipped, and the slice falls through to the regular wave path. Themock_run_phase.call_args_listassertion proves the slice ran, and the secondary assertion (label selector includesegg.slice.id) proves the probe actually fired. Without the guard the test would deadlock —mark_spawnedwould flip slice-1 to RUNNING,iter_readywould never re-yield it,all_done()would never return True (RUNNING is not terminal —slice_scheduler.py:561-569), and pytest's global 60s timeout would kill it. Regression coverage holds. -
ContainerStatus.CREATINGcovered —test_true_when_creating_pod_existsattest_slice_4_restart_hardening.py:1164. Mirrors the RUNNING/PENDING shape;LIVE_POD_STATUSESinmodels.py:104-108does include CREATING, so the assertion is correct. -
Caller-contract docstring —
routes/pipelines.py:1026-1036now spells out that callers must have torn down stale cohorts with foreground propagation, citing thekubernetes_client.pyRunning→RUNNING mapping that omits a Terminating-specific status. This is the right warning to capture inline next to the helper. -
Spawner threaded as parameter — Signature is now
_slice_agents_alive(spawner, pipeline_id, slice_id)atpipelines.py:1018. The single call site atpipelines.py:16166passes the in-scopespawner(the same variable already used by_bootstrap_check_onefor its Layer-B gateway probe). Parallels_classify_non_complete_slice(gateway, …). The_get_spawner()lookup inside the helper is gone, and unit tests inject the stub directly withoutpatch("routes.pipelines._get_spawner", …). -
Audit-log comment promoted —
pipelines.py:16207-16211now sits in its own paragraph abovelogger.info. Reads as a parallel rationale to thebootstrap_blockednote rather than a continuation of it.
I ran the targeted suites locally:
pytest orchestrator/tests/test_slice_run_loop_integration.py::TestBootstrapResumeAliveGuard \
orchestrator/tests/test_slice_4_restart_hardening.py::TestSliceAgentsAlive -x
# 8 passed
Non-blocking
- Docstring location inaccuracy —
test_slice_run_loop_integration.py:1311-1314. TheTestBootstrapResumeAliveGuarddocstring says the integration test exercisesroutes/pipelines.py:_bootstrap_check_one's Layer-Cresumebranch._bootstrap_check_one(pipelines.py:16040-16082) is the Layer-B inner closure that handles merged-detection (line 16057'sis_slice_branch_merged_into_parent). The Layer-C resume branch lives atpipelines.py:16161-16177, directly inside the top-level loop in_run_implement_phase_slices— not inside_bootstrap_check_one. A future reader chasing the cross-reference will land in the wrong function. One-line fix: drop the:_bootstrap_check_onequalifier and just point at the Layer-Cresumebranch in_run_implement_phase_slices.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
The docstring incorrectly pointed at _bootstrap_check_one (the Layer-B inner closure that handles merged-detection). The Layer-C resume branch the test actually exercises lives directly inside the top-level loop in _run_implement_phase_slices. Authored-by: egg
|
Thanks for the re-review. The single non-blocking docstring inaccuracy is addressed. Per-item disposition
Verification
— Authored by egg |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Re-reviewed the delta against my prior approval at 458d88b. The new commit d988dfd is a docstring-only fix for the single non-blocking item I raised last round.
Verified at source level:
- Old docstring pointed at
routes/pipelines.py:_bootstrap_check_one's Layer-C resume branch._bootstrap_check_oneis the Layer-B inner closure atpipelines.py:16040-16082— its job is callingis_slice_branch_merged_into_parent, not resume classification. - New docstring at
test_slice_run_loop_integration.py:1310-1313now reads "exercising the actual call site at the Layer-Cresumebranch inroutes/pipelines.py:_run_implement_phase_slicesend-to-end" — matches the actual call path. The resume branch is atpipelines.py:16161-16177, inside the top-level loop in_run_implement_phase_slices, not nested inside_bootstrap_check_one.
Everything else in the PR is unchanged from 458d88b: the _slice_agents_alive helper, its call site at pipelines.py:16166, the integration test body (test_resume_with_no_live_pods_respawns_fresh), and the ContainerStatus.CREATING unit test. No new code surface to scrutinize.
LGTM.
— Authored by egg
|
egg review completed. View run logs 7 previous review(s) hidden. |
* Fix restart_phase wedge on sliced implement Verify slice agents are live before marking as spawned. Without this, restart_phase on a sliced implement wedges the pipeline: the scheduler thinks the slice is RUNNING but no containers are present, so no signals can arrive and the slice never completes. Fixes #2914 * Address egg-reviewer non-blocking suggestions Addresses 5 non-blocking suggestions from egg-reviewer on PR #2916: 1. Integration test for the resume-but-dead → fresh branch - Adds TestBootstrapResumeAliveGuard to test_slice_run_loop_integration.py - Exercises the actual call site at routes/pipelines.py Layer-C resume branch end-to-end through _run_implement_phase_slices, closing the gap that TestSliceAgentsAlive unit tests cannot. 2. ContainerStatus.CREATING in unit test matrix - Adds test_true_when_creating_pod_exists to cover the Pending → Creating → Running transition. 3. Document foreground-propagation assumption - Docstring on _slice_agents_alive now spells out the caller contract: stale cohorts must be torn down with foreground propagation, otherwise terminating pods can false-positive the liveness check. 4. Thread spawner as parameter - _slice_agents_alive now takes spawner as a parameter instead of fetching it via _get_spawner(). Parallels how _classify_non_complete_slice receives gateway, and lets the unit tests inject a stub without patch(routes.pipelines._get_spawner). 5. Comment placement at the audit-log gate - The bootstrap_reclassified_fresh note is promoted to its own paragraph so future readers don't conflate it with the bootstrap_blocked rationale block above it. * Fix docstring cross-reference in TestBootstrapResumeAliveGuard The docstring incorrectly pointed at _bootstrap_check_one (the Layer-B inner closure that handles merged-detection). The Layer-C resume branch the test actually exercises lives directly inside the top-level loop in _run_implement_phase_slices. Authored-by: egg --------- Co-authored-by: egg-reviewer[bot] <261018737+egg-reviewer[bot]@users.noreply.github.com>
Summary
Fixes #2914
restart_phaseon a sliced implement phase was wedging the pipeline. After stopping containers and clearing per-slice consensus trackers, the bootstrap reconciler would classify the slice as "resume" and callscheduler.mark_spawned(slice_id)— telling the run loop the cohort was already live. No agents would ever spawn, and the pipeline would hang indefinitely.Root Cause
The bootstrap reconciliation's Layer-C classifier checked contract state and git commits to distinguish "resume" from "fresh", but never verified the slice's agents were actually running. After
restart_phasestops all containers, the contract still showsIN_PROGRESSwith commits, so the reconciler classified as "resume" and marked the slice as spawned — even though no pods existed.Fix
_slice_agents_alive(pipeline_id, slice_id): Queries k8s for live pods labeled with bothegg.pipeline.idandegg.slice.id. ReturnsTrueonly if at least one pod is in a live state (Pending/Creating/Running). ReturnsFalseon zero pods or API errors — the conservative default forces re-spawn rather than risking a wedge.Modified resume branch (~line 16149 in
routes/pipelines.py): Before callingscheduler.mark_spawned(s.id), verify agents are actually live. If not alive, treat as "fresh" instead (don't mark_spawned, let the scheduler re-yield READY and spawn a new cohort).Testing
Added 6 unit tests in
tests/orchestrator/test_slice_4_restart_hardening.py:Truewhen running/pending pods existFalsewhen no pods or only terminal (Exited/Failed) pods existFalseon k8s API errors (defensive: force re-spawn on uncertainty)egg.pipeline.idandegg.slice.idImpact
Restores
restart_phaseas a viable recovery for failed/wedged slices in sliced implement phases. Operators can now userestart_phaseinstead of having to cancel and resubmit the entire pipeline.