Skip to content

test+fix: harden probe-flake in validate-network-isolation - #2699

Merged
jwbron merged 1 commit into
mainfrom
egg/harden-probe-flake-2688
May 13, 2026
Merged

test+fix: harden probe-flake in validate-network-isolation#2699
jwbron merged 1 commit into
mainfrom
egg/harden-probe-flake-2688

Conversation

@jwbron

@jwbron jwbron commented May 13, 2026

Copy link
Copy Markdown
Owner

Summary

  • Bump _wait_for_probe_pod timeout from 30s → 75s in orchestrator/routes/deployment.py. 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.
  • In integration_tests/test_deployment_validation_logic.py::test_probe_runs_and_returns_expected_shape, handle the probe-timeout shape explicitly with pytest.fail(...) instead of letting data["result"] raise a bare KeyError: '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

  • Integration Tests job passes (was previously flaking on this exact test).
  • Confirm test_probe_runs_and_returns_expected_shape still asserts the four gateway_reachable / internet_blocked / agent_pods_unreachable / orchestrator_api_reachable invariants on the happy path.

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.
@jwbron
jwbron merged commit d26a8bc into main May 13, 2026
15 checks passed

@egg-reviewer egg-reviewer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_pod poll → 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.skipintegration_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_pod has 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-897 mock _wait_for_probe_pod directly, so they're unaffected by the value change.
  • The route's probe_timeout shape (line 1280) is unchanged — the new test branch correctly matches that shape (probe_id present, no result).
  • _delete_probe_job runs in finally, 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

@james-in-a-box

Copy link
Copy Markdown
Contributor

egg review completed. View run logs

jwbron added a commit that referenced this pull request May 13, 2026
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
jwbron added a commit that referenced this pull request May 13, 2026
* 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>
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