fix: race between probe-pod TTL GC and _wait_for_probe_pod poll - #2700
Conversation
…on probe path PR #2699 raised the route's probe-pod wait to 75s, leaving the _post() helper's 60s default too tight for any test that proceeds past the CNI gate (probe launches → wait can approach the route's 75s ceiling). Surfaced by #2689 comment 4443847011: test_default_pipeline_id_and_role_pass_label_validation hit a 60s read-timeout while the sibling test_probe_runs_and_returns_expected_shape ran 10s later and passed with its explicit timeout=90. test_pipeline_id_regex_valid_at_boundaries_pass has the same latent bug (4 parametrize cases of valid labels that launch the probe with the default 60s). Bumping the default to 90s matches the explicit timeout already used in test_probe_runs_and_returns_expected_shape, the concurrency test, and the JSON-validity test, and sits at the route's 90s HTTP-timeout ceiling. 400-path tests (label rejections) return fast and are unaffected.
This comment has been minimized.
This comment has been minimized.
Check Failure: Integration Tests / Integration TestsWhat's failing: Root cause: This is the same Why this PR doesn't fix it: This PR only changes the test-side default Evidence it's a flake, not a PR-introduced regression:
What needs to be done:
Suggestion: Retry first. The autofixer is not the right place to add probe-wait headroom — that change touches — Authored by egg |
Autofix tracking{"Test/Integration Tests / Integration Tests": 1} |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Verdict: Approve
Tiny, well-motivated change. The 60s default was structurally below the server-side 75s _wait_for_probe_pod ceiling that #2699 just established, and several tests in the same file already work around it with explicit timeout=90. Bumping the default closes that gap. No security, correctness, or regression risk.
Verified
orchestrator/routes/deployment.py:1273calls_wait_for_probe_pod(..., timeout=75.0); the inline comment at line 1271 explicitly anchors 75s under "the route's 90s HTTP-timeout ceiling." Old 60s client default → guaranteedReadTimeoutwhenever the probe path is exercised and scheduling drifts past 60s.- The new 90s default matches the existing explicit overrides in this file (
test_probe_runs_and_returns_expected_shape:437,test_concurrent_validate_network_isolation_calls_get_distinct_probe_ids:518,test_validation_routes_reject_invalid_json:638,647,test_no_orphan_probe_jobs_after_call:710). The helper now matches its callers' de-facto convention. - The 400-path tests (
test_invalid_pipeline_id_returns_400,test_invalid_role_returns_400,test_pipeline_id_regex_boundary_violations_return_400[*]) reject at the label-validator before reaching the CNI gate and return in milliseconds — bumping the upper bound has no effect on them. Confirmed by inspection. - CI on this branch (run 25817353877) shows the stated targets now pass:
test_default_pipeline_id_and_role_pass_label_validationPASSEDtest_pipeline_id_regex_valid_at_boundaries_pass[single-char | max-length-63 | middle-dot-underscore-hyphen | uppercase-allowed]— all 4 PASSED
Non-blocking note
The "Integration Tests job passes on this branch" checkbox in the test plan won't be satisfied by this change alone. CI run 25817353877 still has one failure:
FAILED test_probe_runs_and_returns_expected_shape -
Failed: probe pod did not complete before route timeout:
{'error': 'probe_timeout', 'probe_id': 'dcb7da2be765'}
That test already had timeout=90 pre-PR, so it's untouched by this diff. The failure shape is the server-side _wait_for_probe_pod returning the probe_timeout 200-response — i.e. the probe pod did not become observable within the server's 75s deadline. The 90s client ceiling is irrelevant here; the client cleanly received an HTTP 200 with a probe_timeout payload well before 90s.
So the residual flake is at a different layer than this PR addresses — it's the same class of issue #2699 was hardening against (probe-pod scheduling on the k3s integration cluster occasionally exceeding the server-side deadline), not a client-timeout problem. Not in scope to fix here, but worth knowing: if this recurs, the next bump is on the server-side _wait_for_probe_pod timeout, not on _post.
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Review feedback dispositionThanks for the review. The PR is APPROVED by Items
SummaryNo commits added. Branch HEAD is still — Authored by egg |
This comment has been minimized.
This comment has been minimized.
Root cause of the test_probe_runs_and_returns_expected_shape flake on PR #2700 CI: the probe Job set ttlSecondsAfterFinished=0, which races _wait_for_probe_pod's 1Hz poll. The probe Job/pod could transition to Succeeded (or Failed via activeDeadlineSeconds), then be GC'd by the TTL-after-finished controller before the next poll observed the terminal phase. The wait loop would then scan an empty list until its 75s ceiling and return probe_timeout. Evidence (Actions run 25817353877): - Orchestrator log durations were bimodal: 9-12s (happy) or 75-76s (full ceiling), no middle ground. - The route's finally _delete_probe_job got 404 on the hung calls: "(404) Reason: Not Found" — Job was already gone, GC'd by ttl=0. - The pod's k8s events stop at Started; no DeadlineExceeded, no Killing — the pod completed normally but was reaped before the poll observed it. Fix: bump ttlSecondsAfterFinished from 0 to 30. The route's try/finally remains the primary cleanup path; 30s is the backstop that also gives the 1Hz poll a guaranteed observation window and ensures _read_probe_log can still read the pod's stdout after _wait_for_probe_pod returns. Also bumps integration helper _post() default timeout from 60s to 90s. PR #2699 raised the route's probe-pod wait to 75s, leaving the helper's 60s default structurally too tight for any test that lets the route proceed past the CNI gate. The bot caught test_default_pipeline_id_and_role_pass_label_validation on #2689 comment 4443847011; test_pipeline_id_regex_valid_at_boundaries_pass had the same latent bug across 4 parametrize cases. 90s matches the explicit timeout already used in the probe-result/concurrency/JSON tests. Files: - orchestrator/routes/deployment.py: ttlSecondsAfterFinished 0 → 30 - orchestrator/tests/test_deployment_routes.py: matching assertion - orchestrator/mcp_tools.py: tool description text - integration_tests/test_deployment_validation_logic.py: _post() default timeout 60 → 90; TestProbeJobCleanup docstring + failure message updated to reference ttl=30
There was a problem hiding this comment.
Verdict: Approve
The new commit (559a06b) addresses the residual probe_timeout flake my previous review flagged as out-of-scope. The expanded scope is justified — the PR description's bimodal-distribution evidence from run 25817353877 is convincing, and Integration Tests now pass on this commit.
Verified the race hypothesis
Traced the full flow in orchestrator/routes/deployment.py:
_wait_for_probe_pod(lines 1115-1130) pollslist_namespaced_podat 1Hz against theegg.io/probe-idselector, returning only onphase in {Succeeded, Failed}.- With
ttlSecondsAfterFinished=0, the TTL-after-finished controller cascade-deletes the Job + pod within sub-second of Job completion — narrower than the 1Hz poll window. The 404 on thefinally _delete_probe_jobcalls in the failure run confirms the Job was gone by the time the route reachedfinally. ttlSecondsAfterFinished=30gives the 1Hz poll ~30 attempts to observe terminal phase. Combined withactiveDeadlineSeconds=30, max time from Job submit to pod GC is bounded at ~60s, safely under the 75s wait ceiling._read_probe_log(lines 1133-1161) runs immediately after_wait_for_probe_podreturns — the 30s window leaves ample headroom for the log read before TTL fires. Existing try/except already covers GC-during-read._delete_probe_jobusespropagation_policy="Background"(line 1174) and never raises, so the route's explicit cleanup is unaffected by the bumped TTL.
Consistency check
All 5 references to ttlSecondsAfterFinished for the probe Job now read 30:
orchestrator/routes/deployment.py:1034— manifest valueorchestrator/tests/test_deployment_routes.py:969— unit-test assertionorchestrator/mcp_tools.py:1115— MCP tool descriptionintegration_tests/test_deployment_validation_logic.py:685,717,749— docstring + comment + failure message
Other ttlSecondsAfterFinished references in the repo (e.g. startup_reconciliation.py:210, agent-Job default of 600s) are unrelated and correctly left alone.
Stricter cleanup test as a side benefit
test_no_orphan_probe_jobs_after_call is now strictly stronger than before. With the old ttl=0, a silent finally failure could be masked by the TTL controller cleaning up within the test's 15s window. With ttl=30, the 15s deadline is below the TTL fire time, so the test now fails on any finally regression. The updated comment captures this:
15s + the ~10s probe runtime still sits well under ttl + GC sync
Minor nit (non-blocking): the comment is mildly awkward — the 15s is deliberately below ttl=30 so the test catches finally regressions rather than relying on the TTL backstop. Reading it as "the headroom lets TTL fire if finally failed" would be wrong. Not worth respinning.
CI
- Integration Tests / Integration Tests: ✅ (the run that motivated this fix now passes)
- Unit Tests: ✅
- All other required checks: ✅
— Authored by egg
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
The previous wording read as if the 15s window existed so the ttlSecondsAfterFinished=30 backstop could fire during the test. That inverts the intent: 15s is deliberately below ttl=30 so a regression in the route's try/finally path surfaces as a test failure here instead of being masked by the TTL controller sweeping the orphan inside the poll window. No behavior change; comment-only.
Review feedback dispositionLatest review ( Items
Summary
— Authored by egg |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Verdict: Approve
The single new commit (6156c6e) is a comment-only change that correctly addresses the non-blocking nit I raised on 559a06b. No behavior delta; the substantive changes (ttlSecondsAfterFinished 0→30, _post() default timeout 60→90, and matching unit-test + MCP tool description updates) are unchanged and remain approved.
Verified the comment is now accurate
Traced the timing for test_no_orphan_probe_jobs_after_call:
orchestrator/routes/deployment.py:1034— probe Job setsttlSecondsAfterFinished=30andactiveDeadlineSeconds=30.- TTL countdown starts at Job-Finished, which on the normal path is ~probe-runtime (~10s) into route execution.
- The route's
finally _delete_probe_jobruns immediately after_wait_for_probe_podreturns, then the route returns to the client. integration_tests/test_deployment_validation_logic.py:720—deadline = time.time() + 15starts after_postreturns.- Effective TTL fire time (normal probe completion path): ~Job-Finished + 30s, i.e. ~30s after the route returned.
- 15s poll window < 30s TTL window → the TTL controller cannot mask a
finally-path regression by sweeping the orphan inside the poll window.
The new wording captures that intent directly:
The 15s deadline is deliberately below ttlSecondsAfterFinished=30: a finally-path regression must surface as a test failure here, not get masked by the TTL controller sweeping the orphan inside the poll window.
This is the correct framing. The old wording read as if the 15s window was for the TTL backstop to fire during the test, which would have required a window > 30s; the new version makes the invariant explicit.
Scope of delta
git diff 559a06b..HEADis one comment block inintegration_tests/test_deployment_validation_logic.py. Nothing else.- No new tests, no production code changes, no CI surface change. The previously green Integration Tests / Unit Tests result on
559a06bcarries over.
— Authored by egg
|
egg review completed. View run logs 8 previous review(s) hidden. |
Summary
Two flake fixes for
validate-network-isolationintegration tests:1. Probe-pod TTL race (root cause of #2700's first red CI)
test_probe_runs_and_returns_expected_shapeintermittently saw the route return theprobe_timeoutshape instead of the happy-path result. The probe Job setttlSecondsAfterFinished=0, which races_wait_for_probe_pod's 1Hz poll:phase=Succeeded(orFailedviaactiveDeadlineSeconds)ttlSecondsAfterFinished=0→ TTL controller cascade-deletes the Job/pod within millisecondsprobe_timeoutEvidence from Actions run 25817353877:
finally _delete_probe_jobgot 404 on the hung calls:"(404) Reason: Not Found"— the Job was already gone, GC'd byttl=0.Started; noDeadlineExceeded, noKilling— the pod completed normally and was reaped before the poll observed it.Fix: bump
ttlSecondsAfterFinishedfrom0to30. The route'stry/finallyremains the primary cleanup path; 30s is the backstop that guarantees both:_wait_for_probe_podsees the terminal phase before GC_read_probe_logcan still read the pod's stdout after_wait_for_probe_podreturns2.
_post()helper default timeout (60s→90s)PR #2699 raised the route's
_wait_for_probe_podfrom 30s → 75s. The integration helper_post()still defaulted totimeout=60, leaving any test that proceeds past the CNI gate (valid labels → probe launches) structurally underweight against the route's 75s ceiling.Surfaced by #2689 comment 4443847011.
test_pipeline_id_regex_valid_at_boundaries_passhad the same latent bug across 4 parametrize cases. 90s matches the explicittimeout=90already used intest_probe_runs_and_returns_expected_shape, the concurrency test, and the JSON-validity test.Files
orchestrator/routes/deployment.py—ttlSecondsAfterFinished0 → 30 with explanatory commentorchestrator/tests/test_deployment_routes.py— matching unit-test assertionorchestrator/mcp_tools.py— tool description textintegration_tests/test_deployment_validation_logic.py—_post()default timeout 60 → 90;TestProbeJobCleanupdocstring + failure message updated to referencettl=30Test plan
test_probe_runs_and_returns_expected_shapeconsistently observes the terminal phase (no more bimodal hangs).test_no_orphan_probe_jobs_after_callstill passes — the route's explicitfinallycleanup runs before the test polls, sottl=30is invisible to it on the happy path.test_default_pipeline_id_and_role_pass_label_validationandtest_pipeline_id_regex_valid_at_boundaries_pass(4 parametrize cases) pass with the bumped default_post()timeout.Closes
This PR root-causes the probe hang that was opened as #2701; that issue can be closed when this lands.