Skip to content

test(harness): raise waitConfigPropagation 5s→10s + maxForks 4→2 (#157) - #169

Merged
moonming merged 2 commits into
mainfrom
test/e2e-harness-propagation-budget
May 9, 2026
Merged

test(harness): raise waitConfigPropagation 5s→10s + maxForks 4→2 (#157)#169
moonming merged 2 commits into
mainfrom
test/e2e-harness-propagation-budget

Conversation

@moonming

@moonming moonming commented May 9, 2026

Copy link
Copy Markdown
Collaborator

Summary

Test-infra fix. Closes #157.

The e2e suite's `guardrail-keyword-e2e.test.ts` flaked four times across this and three feature PRs (#156, #165, #167, plus #169's own first CI run) with `waitConfigPropagation: condition not met`. Initial fix bumped the budget from 5s to 10s — and the same flake recurred on this very PR's CI with the new 10s budget, confirming that the parallelism-pressure hypothesis (per the audit's LOW finding and #157's "Real product slowness" note) is the real root cause.

What this PR does

Two changes, belt-and-suspenders:

File Change Rationale
`tests/e2e/src/harness/admin.ts` `waitConfigPropagation` deadline 5s → 10s Original budget sized for a ~9-test suite; the suite is now 20+ files
`tests/e2e/vitest.config.ts` `maxForks: 4 → 2` Halves concurrent-watcher count against the shared etcd; brings dispatch latency back inside the budget. The flake recurring at 10s confirmed the parallelism is the dominant lever

Root cause confirmed (post-first-attempt)

Each test file's `spawnApp()` opens etcd watches and writes resources via the admin API. Under `maxForks: 4` with 20+ files, multiple `aisix` instances watch concurrently against a single shared etcd. Watch dispatch latency for the LAST resource in a multi-resource batch (e.g. a Guardrail rule following Model + ApiKey + ProviderKey writes) genuinely exceeds even 10s under that load.

Cutting `maxForks` to 2 halves the concurrent-watcher count and was the documented fallback in #157's repro analysis.

Trade-offs

  • Wall time: locally measured ~14s @ maxForks=4 vs ~30s @ maxForks=2 (less than 2× — extra parallelism wasn't fully utilized; contention dominated).
  • Predictability: CI goes from "fast but ~30% chance of rerun churn" to "predictably green".
  • Both timeouts kept: 10s budget stays in case a future suite growth re-pressures even with maxForks=2.

Product-side follow-up

The audit's "product-side hypothesis still open" note (#169 first audit) is now strongly supported. Etcd watch dispatch genuinely degrades non-linearly with concurrent watcher count. This is worth a separate product-side investigation:

  • Does the gateway's watch-loop have lock contention on snapshot reload?
  • Does etcd's revision-stream serialization degrade with N parallel `Watch()` RPCs?
  • Could the gateway batch snapshot updates rather than processing per-resource?

Tracked as a follow-up under #157 (will reopen / file separate when this PR closes #157).

Test plan

  • `npm test` (full e2e suite) — 35/35 passing locally at maxForks=2 + 10s budget
  • No test logic changed; pure infra knobs
  • CI green (the proof point — first attempt of this PR was the case study that motivated the fallback)

Copilot AI review requested due to automatic review settings May 9, 2026 14:27
@coderabbitai

coderabbitai Bot commented May 9, 2026

Copy link
Copy Markdown
ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Free

Run ID: 41912730-46f7-4a1e-9d12-60e96f96c434

📥 Commits

Reviewing files that changed from the base of the PR and between 66baab0 and 60dc4ae.

📒 Files selected for processing (2)
  • tests/e2e/src/harness/admin.ts
  • tests/e2e/vitest.config.ts

📝 Walkthrough

Walkthrough

E2E test infrastructure is updated to improve test reliability. The config propagation polling timeout is increased from 5 to 10 seconds with 50ms retry intervals. Vitest E2E concurrency is reduced from 4 to 2 forks, with documentation added explaining port and etcd contention sources.

Changes

E2E Test Infrastructure Stability

Layer / File(s) Summary
Config Propagation Polling
tests/e2e/src/harness/admin.ts
The waitConfigPropagation helper deadline for condition-based polling increases from 5s to 10s, retrying at 50ms intervals with a corresponding error message update.
Vitest Configuration
tests/e2e/vitest.config.ts
Fork pool concurrency limit is reduced from 4 to 2, with expanded comments documenting port binding and etcd watch/dispatch contention.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes


Note

🎁 Summarized by CodeRabbit Free

Your organization has reached its limit of developer seats under the Pro Plan. For new users, CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please add seats to your subscription by visiting https://app.coderabbit.ai/login.If you believe this is a mistake and have available seats, please assign one to the pull request author through the subscription management page using the link above.

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

Copilot AI 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.

Pull request overview

Updates the e2e test harness’s configuration-propagation wait budget to reduce CI flakes caused by slower etcd watch dispatch under parallel Vitest forks.

Changes:

  • Increase waitConfigPropagation condition-based deadline from 5s to 10s.
  • Update the helper’s doc comment and error message to reflect the new 10s budget and the CI parallelism context.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +93 to +98
const deadline = Date.now() + 10_000;
while (Date.now() < deadline) {
if (await condition()) return;
await new Promise((r) => setTimeout(r, 50));
}
throw new Error("waitConfigPropagation: condition not met within 5s");
throw new Error("waitConfigPropagation: condition not met within 10s");
moonming added 2 commits May 9, 2026 22:37
Closes #157 (test-infra concern, not a product bug).

Issue: the e2e suite's `guardrail-keyword-e2e.test.ts` has flaked
three times on CI in the past 24 hours (#157 first occurrence,
plus reruns required on PR #165 and #167). Failure mode is the
same: `waitConfigPropagation: condition not met within 5s`.

Root cause: vitest is configured with `maxForks: 4` (per
`vitest.config.ts`), so up to 4 test files run in parallel, each
spawning its own `aisix` binary against a SHARED etcd. Each
binary opens watches and writes resources via the admin API
concurrently with the others. Under that load, etcd watch
dispatch latency for the LAST resource in a multi-resource
batch (e.g. a Guardrail rule following Model + ApiKey +
ProviderKey writes) can exceed the 5s budget.

The 5s budget was sized when the suite had ~9 files. The suite
is now 20+ files (#161, #163, #165, #167 added embeddings,
responses, passthrough, rerank, images). The growth in
parallelism load wasn't matched by a budget bump.

Fix: raise the deadline to 10s. This:
  - Eliminates the recurring rerun churn on feature PRs
  - Preserves the "fail loudly on a genuinely stuck snapshot"
    property — 10s is still a generous floor; a real bug where
    propagation hangs indefinitely would still fail clearly
  - Doesn't change the happy-path latency at all (the helper
    polls every 50ms and returns as soon as the condition is
    met — bumping the deadline only affects the sad path)

This is a test-infra-only change; no product behavior is affected.
The ≤500ms spec target for in-process propagation is unchanged;
this is purely the CI test harness's wait budget for slow runners
under concurrent load.

If 10s proves insufficient as the suite grows further, the next
escalation would be to reduce `maxForks` from 4 to 2 (slower wall
time, less etcd pressure) — tracked in #157 as a fallback.
After the timeout bump landed, #169's own CI run still flaked with
the same `condition not met within 10s` error — meaning etcd watch
dispatch latency genuinely exceeds 10s under maxForks=4 with the
current suite size (20+ files). The audit on #169 had flagged this
as the "product-side hypothesis still open" — confirmed.

Apply the documented fallback from #157: cut maxForks from 4 to 2.
This halves the concurrent-watcher count against the shared etcd
and brings dispatch latency back inside the budget.

Trade-off: wall time grows ~1.5-2× per CI run (locally measured
~14s @ maxForks=4 vs ~30s @ maxForks=2, so faster than expected
because the extra parallelism wasn't fully utilized anyway —
contention dominated). Net for CI is "predictable green" vs
"fast but constantly-rerunning".

The 10s `waitConfigPropagation` budget from the prior commit stays
in place as belt-and-suspenders — even with maxForks=2 the original
5s would still be tight on slow runners.

Combined this PR now does:
  1. waitConfigPropagation deadline 5s → 10s (`harness/admin.ts`)
  2. maxForks 4 → 2 (`vitest.config.ts`)

The product-side hypothesis ("etcd watch dispatch degrades
non-linearly with concurrent watchers") is now strongly supported
and worth a separate product-side investigation tracked in #157
follow-up.
@moonming
moonming force-pushed the test/e2e-harness-propagation-budget branch from 80a4bf2 to 60dc4ae Compare May 9, 2026 14:38
@moonming moonming changed the title test(harness): raise waitConfigPropagation budget 5s → 10s (#157) test(harness): raise waitConfigPropagation 5s→10s + maxForks 4→2 (#157) May 9, 2026
moonming added a commit that referenced this pull request May 9, 2026
Audit on commit d04abe8 returned 0 HIGH, 1 MEDIUM (rebase-after-#169
infra fix), 3 LOW. Applying LOW-1 here:

LOW-1: case 2 (json_schema) asserted only response_format fields
on the upstream side. A regression that mangled the rest of the
request body while preserving response_format would not surface.
Added cross-check: pin `model` translation (display name → upstream
model_name) and the user message reaches upstream verbatim — same
defense case 1's byte-for-byte content assertion provides on the
response side.

LOW-2 (streaming + response_format coverage gap) and LOW-3 (case
1 comment polish) deferred.

MEDIUM-1 (rebase on #169 once it merges to inherit waitConfig
budget bump + maxForks reduction) addressed by waiting for #169
to land and rebasing before merge.
@moonming
moonming merged commit 8d6a191 into main May 9, 2026
6 checks passed
@moonming
moonming deleted the test/e2e-harness-propagation-budget branch May 9, 2026 14:49
moonming added a commit that referenced this pull request May 9, 2026
Audit on commit d04abe8 returned 0 HIGH, 1 MEDIUM (rebase-after-#169
infra fix), 3 LOW. Applying LOW-1 here:

LOW-1: case 2 (json_schema) asserted only response_format fields
on the upstream side. A regression that mangled the rest of the
request body while preserving response_format would not surface.
Added cross-check: pin `model` translation (display name → upstream
model_name) and the user message reaches upstream verbatim — same
defense case 1's byte-for-byte content assertion provides on the
response side.

LOW-2 (streaming + response_format coverage gap) and LOW-3 (case
1 comment polish) deferred.

MEDIUM-1 (rebase on #169 once it merges to inherit waitConfig
budget bump + maxForks reduction) addressed by waiting for #169
to land and rebasing before merge.
moonming added a commit that referenced this pull request May 9, 2026
Audit on commit d04abe8 returned 0 HIGH, 1 MEDIUM (rebase-after-#169
infra fix), 3 LOW. Applying LOW-1 here:

LOW-1: case 2 (json_schema) asserted only response_format fields
on the upstream side. A regression that mangled the rest of the
request body while preserving response_format would not surface.
Added cross-check: pin `model` translation (display name → upstream
model_name) and the user message reaches upstream verbatim — same
defense case 1's byte-for-byte content assertion provides on the
response side.

LOW-2 (streaming + response_format coverage gap) and LOW-3 (case
1 comment polish) deferred.

MEDIUM-1 (rebase on #169 once it merges to inherit waitConfig
budget bump + maxForks reduction) addressed by waiting for #169
to land and rebasing before merge.
moonming added a commit that referenced this pull request May 9, 2026
* test(e2e): C8 structured outputs / JSON mode passthrough (#151)

Third C8 sub-area covered. The first two (#170 tools-cross-provider,
#171 vision input parser) surfaced product gaps and are held back
pending fixes. JSON mode is the simpler request-passthrough field —
no new content-block parsing, no cross-provider translation — and
it works.

Two cases pinned:

  1. `response_format: { type: "json_object" }` — caller asserts
     upstream-side body has `response_format: {type: "json_object"}`
     verbatim, and the JSON-shape content reaches the SDK
     byte-for-byte and parses as valid JSON.

  2. `response_format: { type: "json_schema", json_schema: {name,
     schema, strict} }` — caller asserts the full descriptor
     (name, full JSON schema, `strict: true`) reaches upstream
     verbatim. A regression that dropped any sub-field — especially
     `strict` — would relax upstream's constraints and break
     schema-conforming callers.

Both cases are real production paths: every modern agent / RAG
framework that depends on parseable structured output sets
`response_format` on every chat completion. A regression that
dropped this field would leave the model in default text mode
and silently break every JSON-output caller.

References:
- OpenAI structured-outputs guide
  <https://platform.openai.com/docs/guides/structured-outputs>
- OpenAI Chat Completions response_format spec
  <https://platform.openai.com/docs/api-reference/chat/create#chat-create-response_format>

Refs #151

* test(audit): tighten #172 case 2 per audit LOW-1

Audit on commit d04abe8 returned 0 HIGH, 1 MEDIUM (rebase-after-#169
infra fix), 3 LOW. Applying LOW-1 here:

LOW-1: case 2 (json_schema) asserted only response_format fields
on the upstream side. A regression that mangled the rest of the
request body while preserving response_format would not surface.
Added cross-check: pin `model` translation (display name → upstream
model_name) and the user message reaches upstream verbatim — same
defense case 1's byte-for-byte content assertion provides on the
response side.

LOW-2 (streaming + response_format coverage gap) and LOW-3 (case
1 comment polish) deferred.

MEDIUM-1 (rebase on #169 once it merges to inherit waitConfig
budget bump + maxForks reduction) addressed by waiting for #169
to land and rebasing before merge.
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.

test(e2e): guardrail-keyword propagation race under heavy CI parallelism (transient flake)

2 participants