test+fix: harden probe-flake in validate-network-isolation - #2699
Conversation
Two changes that fix the intermittent KeyError: 'result' flake in test_probe_runs_and_returns_expected_shape (seen on #2688 and unrelated branches today): 1. orchestrator/routes/deployment.py — bump _wait_for_probe_pod timeout from 30s to 75s. The k3s integration cluster intermittently took longer than 30s to schedule the probe pod, so the route returned the probe_timeout shape (probe_id present, no result). 75s leaves headroom under the test's 90s HTTP timeout. 2. integration_tests/test_deployment_validation_logic.py — when the probe-timeout shape does occur, call pytest.fail() with the actual response dict instead of letting data["result"] raise a bare KeyError. The failure now points at the probe instead of looking like a generic test bug.
There was a problem hiding this comment.
Review of PR #2699
Small, well-scoped flake fix. No blocking issues. One minor inaccuracy in an added comment and a couple of non-blocking observations.
Non-blocking
1. Comment mis-attributes the 90 s ceiling — orchestrator/routes/deployment.py:1270-1272
# 30s was too tight: probe-pod scheduling on the k3s integration
# cluster intermittently exceeded the deadline. 75s sits under the
# require_lifecycle_secret route's 90s HTTP-timeout ceiling.
pod = _wait_for_probe_pod(k8s, namespace, probe_id, timeout=75.0)require_lifecycle_secret (orchestrator/lifecycle_auth.py) is purely a Bearer-token check — it imposes no HTTP timeout. The 90 s is the test client's timeout=90 parameter on the requests.post call in integration_tests/test_deployment_validation_logic.py:437. Naming the decorator in the comment is misleading; a future reader chasing the source of the 90 s ceiling will look in the wrong place.
Suggested rewrite:
# 30s was too tight: probe-pod scheduling on the k3s integration
# cluster intermittently exceeded the deadline. 75s leaves headroom
# under the integration test's 90s HTTP-client timeout
# (test_probe_runs_and_returns_expected_shape).2. End-to-end latency budget is tight — same line
Worst-case wall clock when the pod runs to the deadline:
_wait_for_probe_podpoll → up to 75 s_read_probe_log(urllib3 read-to-EOF on the pod log) → a few s_delete_probe_job(best-effort, propagation=Background) → ~1 s
That sits at roughly 80 s against the test's 90 s client timeout, leaving ~10 s headroom. If the underlying k3s flake gets worse (or log reads ever stall — _read_probe_log swallows exceptions but does not impose an inner timeout), the test will start failing on the requests-side ReadTimeout rather than the route's probe_timeout shape, and the new diagnostic branch in the test won't fire. Worth keeping in mind for the next bump: the route timeout and the client timeout need to be tuned as a pair.
3. pytest.fail vs pytest.skip — integration_tests/test_deployment_validation_logic.py:446-447
if "result" not in data:
pytest.fail(f"probe pod did not complete before route timeout: {data!r}")fail is the right call here — the PR explicitly wants probe-timeout occurrences to remain visible so the timeout can be retuned, not silenced. Flagging only so it's a deliberate choice, not an oversight: pytest.skip would silence ongoing flakes, which is not what this PR is going for. Leave as-is.
Verified, no concerns
_wait_for_probe_podhas only one production caller (validate_network_isolation); the timeout bump has no other blast radius.- Unit tests in
orchestrator/tests/test_deployment_routes.py:834-897mock_wait_for_probe_poddirectly, so they're unaffected by the value change. - The route's
probe_timeoutshape (line 1280) is unchanged — the new test branch correctly matches that shape (probe_idpresent, noresult). _delete_probe_jobruns infinally, so the bumped timeout cannot leak Jobs even on a slow pod.- No new locking, no new state, no security-sensitive paths touched.
— Authored by egg
|
egg review completed. View run logs |
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
* test: bump _post default timeout 60s→90s for validate-network-isolation 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. * fix: race between probe-pod TTL GC and _wait_for_probe_pod poll 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 * Clarify TestProbeJobCleanup deadline comment per review 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. --------- Co-authored-by: egg-reviewer[bot] <261018737+egg-reviewer[bot]@users.noreply.github.com>
Summary
_wait_for_probe_podtimeout from 30s → 75s inorchestrator/routes/deployment.py. The k3s integration cluster intermittently took longer than 30s to schedule the probe pod, so the route returned theprobe_timeoutshape (probe_idpresent, noresult). 75s leaves headroom under the test's 90s HTTP timeout.integration_tests/test_deployment_validation_logic.py::test_probe_runs_and_returns_expected_shape, handle the probe-timeout shape explicitly withpytest.fail(...)instead of lettingdata["result"]raise a bareKeyError: 'result'. Future flakes now name the probe, not look like a generic test bug.Surfaced by #2688 comment. The same
KeyError: 'result'was hitting unrelated branches today (e.g.egg/doc-update-populate-contract-errors).Test plan
test_probe_runs_and_returns_expected_shapestill asserts the fourgateway_reachable / internet_blocked / agent_pods_unreachable / orchestrator_api_reachableinvariants on the happy path.