Skip to content

[core] [test-only] Poll for the deferred check instead of sleeping past it - #3507

Merged
VaguelySerious merged 1 commit into
mainfrom
peter/windows-events-consumer-poll
Aug 13, 2026
Merged

[core] [test-only] Poll for the deferred check instead of sleeping past it#3507
VaguelySerious merged 1 commit into
mainfrom
peter/windows-events-consumer-poll

Conversation

@VaguelySerious

Copy link
Copy Markdown
Member

Unit Tests (windows-latest) was red on main with two independent failures.
#3503 fixed the world-local one. This is the other, and it is why that lane
stayed red after #3503 landed:

X AssertionError: expected undefined to deeply equal { eventId: 'evnt_22', …(5) }
❯ src/events-consumer.test.ts:1005:38

Main runs 31626621981 and 31631614229, rotating between lines 1005 and 1123.

The defect

Eleven call sites slept MIN_DEFERRED_CHECK_DELAY_MS * 4 and then asserted an
outcome the deferred unconsumed-event check produces. That check has no fixed
schedule: it first waits for delivery to go idle, which is its own poll loop,
and only after that resolves does it arm a getDeferredCheckDelayMs() timer.
The sleep bounds when the timer becomes eligible, not when it has run, and on
a loaded runner with a coarse timer it is not close.

The helper's own doc comment already said so:

Assertions that a check DID fire should poll instead: the window is a lower
bound on when the timer is eligible to run, and a loaded runner with a coarse
timer can take considerably longer to get there.

All eleven call sites ignored it.

The fix

afterDeferredCheck(assertions) polls the whole assertion block instead. The
positive assertions gate the poll, and the negatives alongside them are then
evaluated at the moment the check is known to have fired, which is what they
were meant to assert. Where a site already had a trailing vi.waitFor, that is
left outside the block.

Evidence

Reproduced without a Windows runner. The test stubs
WORKFLOW_DEFERRED_CHECK_DELAY_MS to MIN_DEFERRED_CHECK_DELAY_MS; raising
that stub 20x makes the fixed sleep too short by construction:

OLD @ stub*20: Tests 2 failed | 43 passed (45)
❯ src/events-consumer.test.ts:1005:38
❯ src/events-consumer.test.ts:1123:49
NEW @ stub*20: Tests 45 passed (45)

Those are exactly the two lines CI reports. At the normal stub both pass, so the
old file is not wrong on a fast machine, only unbounded on a slow one.

Test-only, so the changeset is empty.

`Unit Tests (windows-latest)` had two independent failures. #3503 fixed the
world-local one. This is the other: `events-consumer.test.ts`, rotating
between lines 1005 and 1123 across main runs 31626621981 and 31631614229.
Eleven call sites slept `MIN_DEFERRED_CHECK_DELAY_MS * 4` and then asserted an
outcome the deferred unconsumed-event check produces. That check is not on a
fixed schedule. It waits for delivery to go idle, which is its own poll loop,
and only then arms a `getDeferredCheckDelayMs()` timer, so the sleep is a lower
bound on when the timer becomes eligible rather than a guarantee it has run.
The helper's own comment already said assertions that a check DID fire should
poll instead; every one of the eleven ignored it.
Replace the sleep with `afterDeferredCheck`, which polls the assertion block.
The positive assertions gate the poll and the negatives alongside them are
evaluated once the check is known to have fired, which is what they mean.
Reproduced without a Windows runner by raising the test's stubbed delay 20x,
which makes the fixed sleep too short by construction: the old file fails at
exactly 1005 and 1123, the two lines CI reports, and the new file passes 45/45
at the same delay.
@VaguelySerious
VaguelySerious requested a review from a team as a code ownerAugust 12, 2026 20:37
@vercel

vercelBot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
example-nextjs-workflow-turbopackReadyReadyPreviewAug 12, 2026 8:42pm
example-nextjs-workflow-webpackReadyReadyPreviewAug 12, 2026 8:42pm
example-workflowReadyReadyPreviewAug 12, 2026 8:42pm
workbench-astro-workflowReadyReadyPreviewAug 12, 2026 8:42pm
workbench-express-workflowReadyReadyPreviewAug 12, 2026 8:42pm
workbench-fastify-workflowReadyReadyPreviewAug 12, 2026 8:42pm
workbench-hono-workflowReadyReadyPreviewAug 12, 2026 8:42pm
workbench-nestjs-workflowReadyReadyPreviewAug 12, 2026 8:42pm
workbench-nitro-workflowReadyReadyPreviewAug 12, 2026 8:42pm
workbench-nuxt-workflowReadyReadyPreviewAug 12, 2026 8:42pm
workbench-python-workflowErrorErrorAug 12, 2026 8:42pm
workbench-sveltekit-workflowReadyReadyPreviewAug 12, 2026 8:42pm
workbench-tanstack-start-workflowReadyReadyPreviewAug 12, 2026 8:42pm
workbench-vite-workflowReadyReadyPreviewAug 12, 2026 8:42pm
workflow-docsReadyReadyPreview, v0Aug 12, 2026 8:42pm
workflow-swc-playgroundReadyReadyPreviewAug 12, 2026 8:42pm
workflow-tarballsReadyReadyPreviewAug 12, 2026 8:42pm
workflow-webReadyReadyPreviewAug 12, 2026 8:42pm

@changeset-bot

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f446a2a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 0 packages

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actionsBot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

E2E Test Summary

Summary
PassedFailedSkippedTotal
✅ ▲ Vercel Production346605904056
✅ 💻 Local Development381005584368
✅ 📦 Local Production381005584368
✅ 🐘 Local Postgres381005584368
✅ 🪟 Windows31200312
✅ vercel-multi-region270027
Total152350226417499
Details by Category

✅ ▲ Vercel Production

AppPassedFailedSkipped
✅ astro-node128028
✅ astro-quickjs128028
✅ example-node128028
✅ example-quickjs128028
✅ express-node128028
✅ express-quickjs128028
✅ fastify-node128028
✅ fastify-quickjs128028
✅ hono-node128028
✅ hono-quickjs128028
✅ nest-node128028
✅ nest-quickjs128028
✅ nextjs-turbopack-node15303
✅ nextjs-turbopack-quickjs15303
✅ nextjs-webpack-node15303
✅ nextjs-webpack-quickjs15303
✅ nitro-node128028
✅ nitro-quickjs128028
✅ nuxt-node128028
✅ nuxt-quickjs128028
✅ sveltekit-node14709
✅ sveltekit-quickjs14709
✅ tanstack-start-node128028
✅ tanstack-start-quickjs128028
✅ vite-node128028
✅ vite-quickjs128028

✅ 💻 Local Development

AppPassedFailedSkipped
✅ astro-stable-node130026
✅ astro-stable-quickjs130026
✅ express-stable-node130026
✅ express-stable-quickjs130026
✅ fastify-stable-node130026
✅ fastify-stable-quickjs130026
✅ hono-stable-node130026
✅ hono-stable-quickjs130026
✅ nest-stable-node130026
✅ nest-stable-quickjs130026
✅ nextjs-turbopack-canary-node137019
✅ nextjs-turbopack-canary-quickjs137019
✅ nextjs-turbopack-stable-node15600
✅ nextjs-turbopack-stable-quickjs15600
✅ nextjs-webpack-canary-node137019
✅ nextjs-webpack-canary-quickjs137019
✅ nextjs-webpack-stable-node15600
✅ nextjs-webpack-stable-quickjs15600
✅ nitro-stable-node130026
✅ nitro-stable-quickjs130026
✅ nuxt-stable-node130026
✅ nuxt-stable-quickjs130026
✅ sveltekit-stable-node14907
✅ sveltekit-stable-quickjs14907
✅ tanstack-start-node130026
✅ tanstack-start-quickjs130026
✅ vite-stable-node130026
✅ vite-stable-quickjs130026

✅ 📦 Local Production

AppPassedFailedSkipped
✅ astro-stable-node130026
✅ astro-stable-quickjs130026
✅ express-stable-node130026
✅ express-stable-quickjs130026
✅ fastify-stable-node130026
✅ fastify-stable-quickjs130026
✅ hono-stable-node130026
✅ hono-stable-quickjs130026
✅ nest-stable-node130026
✅ nest-stable-quickjs130026
✅ nextjs-turbopack-canary-node137019
✅ nextjs-turbopack-canary-quickjs137019
✅ nextjs-turbopack-stable-node15600
✅ nextjs-turbopack-stable-quickjs15600
✅ nextjs-webpack-canary-node137019
✅ nextjs-webpack-canary-quickjs137019
✅ nextjs-webpack-stable-node15600
✅ nextjs-webpack-stable-quickjs15600
✅ nitro-stable-node130026
✅ nitro-stable-quickjs130026
✅ nuxt-stable-node130026
✅ nuxt-stable-quickjs130026
✅ sveltekit-stable-node14907
✅ sveltekit-stable-quickjs14907
✅ tanstack-start-node130026
✅ tanstack-start-quickjs130026
✅ vite-stable-node130026
✅ vite-stable-quickjs130026

✅ 🐘 Local Postgres

AppPassedFailedSkipped
✅ astro-stable-node130026
✅ astro-stable-quickjs130026
✅ express-stable-node130026
✅ express-stable-quickjs130026
✅ fastify-stable-node130026
✅ fastify-stable-quickjs130026
✅ hono-stable-node130026
✅ hono-stable-quickjs130026
✅ nest-stable-node130026
✅ nest-stable-quickjs130026
✅ nextjs-turbopack-canary-node137019
✅ nextjs-turbopack-canary-quickjs137019
✅ nextjs-turbopack-stable-node15600
✅ nextjs-turbopack-stable-quickjs15600
✅ nextjs-webpack-canary-node137019
✅ nextjs-webpack-canary-quickjs137019
✅ nextjs-webpack-stable-node15600
✅ nextjs-webpack-stable-quickjs15600
✅ nitro-stable-node130026
✅ nitro-stable-quickjs130026
✅ nuxt-stable-node130026
✅ nuxt-stable-quickjs130026
✅ sveltekit-stable-node14907
✅ sveltekit-stable-quickjs14907
✅ tanstack-start-node130026
✅ tanstack-start-quickjs130026
✅ vite-stable-node130026
✅ vite-stable-quickjs130026

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack-node15600
✅ nextjs-turbopack-quickjs15600

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@github-actions

github-actionsBot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit f446a2a · Wed, 12 Aug 2026 20:56:13 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep305 (-69%) 💚1368 🔴 (+22%) 🔻1374 🔴 (+14%)1774 🔴 (+9.9%)30
TTFSstream1308 (+23%) 🔻1365 🔴 (+21%) 🔻1402 🔴 (+21%) 🔻1774 🔴 (+47%) 🔻30
TTFShook + stream1571 (+229%) 🔻1660 🔴 (+15%) 🔻1706 🔴 (+14%)2955 🔴 (+79%) 🔻30
STSO1020 steps (inline)115 (+9.5%)169 (±0%)189 (-5.5%)339 (-6.9%)1019
WO1020 steps168680 (+1.2%)168680 (+1.2%)168680 (+1.2%)168680 (+1.2%)1
SLstream latency80 (-9.1%)112 🔴 (-14%)129 🔴 (-36%) 💚145 🔴 (-53%) 💚30
SOstream overhead (text)97 (-21%) 💚158 (-25%) 💚194 (-40%) 💚361 (-73%) 💚30
SOstream overhead (structured)123 (+27%) 🔻165 (-24%) 💚210 (-17%) 💚473 (-97%) 💚30
📈 STSO distribution vs main (inline / queue-hop histograms)

1020 steps (inline)

Cumulative STSO time: main 165564ms → this run 167484ms (Δ +1920ms, +1%)

100-150 ms ██████████┃████████ main 509 this 297 -212
150-200 ms ███████████████░░░░░░░░┃ main 408 this 657 +249
200-250 ms ┃█ main 61 this 41 -20
250-300 ms ┃ main 14 this 10 -4
300-350 ms ┃ main 12 this 7 -5
350-400 ms ┃ main 8 this 2 -6
400-450 ms ┃ main 4 this 1 -3
450-500 ms ┃ main 1 this 2 +1
500-550 ms ┃ main 1 this 0 -1
550-600 ms ┃ main 0 this 2 +2
750-800 ms ┃ main 1 this 0 -1
ℹ️ Metric definitions & methodology

The collapsed STSO distribution section above buckets every step gap of the sequential-steps run (not a sampled window), split by whether the step ending the gap ran inline — in the same warm process as the step before it, so the gap is pure framework overhead — or after a queue-hop — the first step of a fresh process, which pays queue dispatch, client reinit and event-log replay. Bars overlay the two runs: is main, marks where this run lands, bridges the gap when this run has more samples in a bucket.

Best/P75/P90/P99 deltas compare against the most recent benchmark run on main at the time of this run. 🔻 flags a delta worse than +15%, 💚 one better than −15%.

Metrics — TTFS: time to first step body (in-deployment start() → first step body, deployment clocks) · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (whole-run time outside step bodies, in-deployment anchored) · SL: stream latency (in-deployment write → read propagation, readAt - writtenAt) · SO: stream overhead (end-to-end write+consume time beyond the modelled generation window)

Scenarios — step: one trivial no-op step, no stream; no hooks, so the run stays in turbo mode (in-process fast path) · stream: one streaming step; no hooks, so the run stays in turbo mode (in-process fast path) · hook + stream: registers a hook before one step, which exits turbo mode (dispatch path) · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges, and WO is the whole-run overhead outside step bodies · stream latency: parallel reader/writer steps on a dedicated stream; SL is the in-deployment write->read propagation (readAt - writtenAt) · stream overhead (text): writer streams 300 variable-length text token deltas paced at 100/s for 3s (a haiku-size LLM's token throughput) while a parallel reader drains the whole stream; SO is the end-to-end write+consume time beyond the 3s generation window (overhead/backpressure) · stream overhead (structured): same workload as stream overhead (text), but each delta is an AI-SDK-style structured object ({ type: 'text-delta', id, text }) instead of a raw string, so the SO gap vs the text scenario is the added serialization cost

🔴 marks a percentile over its target (within target is left unmarked). Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · SO 250/500/1000

All metrics are measured from deployment-side timestamps only. Runs are triggered by an in-deployment route that stamps the anchor (clientStart) right before start(), so the CI runner’s request and its path through api.vercel.com sit outside every measured window. TTFS = in-deployment start() → first step body (turbo uses the in-process fast path, non-turbo the dispatch path), and includes the VQS dispatch hop plus any /flow cold start. STSO/WO are measured between step bodies on the deployment. SL is measured inside the workflow (parallel reader/writer steps), so it no longer includes the api.vercel.com read path.

Cold starts are kept in the numbers on purpose — they are part of real bursty-workload latency. The workbench deployment cold-starts the /flow invocation for a large fraction of runs, inflating P75+; the Best column shows the fastest (warm-start) sample for comparison.

@github-actions

Copy link
Copy Markdown
Contributor

Sim World

Simulated world deterministic testing for races. Traces

🟠 Mint-ordered log — 6 fail of 41 total

log=mint-ordered · fence=per-spec

scenariooutcomeeventsvirtreplayviolations
smoke-no-stepscompleted30msok0
smoke-one-stepcompleted60msok0
hook-at-step-startedcompleted120msok0
hook-at-step-completedcompleted120msok0
hook-at-hook-createdcompleted120msok0
deadline-hook-winscompleted71.0hok0
deadline-expirescompleted71.0hok0
long-sleepcompleted1130.0dok0
hook-never-arrivesstalled30msskipped0
step-retries-twicecompleted102.0sok0
parallel-stepscompleted90msok0
hook-on-execution-statecompleted120msok0
peek-hook-before-branchcompleted120msok0
peek-hook-after-branchcompleted120msok0
peek-hook-at-registrationcompleted120msok0
race-hook-before-probecompleted120msok0
race-hook-after-probecompleted120msok0
race-duplicate-deliverycompleted130msok0
attr-hook-before-stepcompleted110msok0
attr-hook-after-stepcompleted110msok0
attr-from-step-bodycompleted130msok0
fork-hook-after-timeoutcompleted141.0mok0
fork-hook-before-timeoutcompleted141.0mok0
count-hook-after-timeoutcompleted171.0mok0
count-hook-before-timeoutcompleted201.0mok0
stale-read-step-count-forkcompleted171.0mMISMATCH1
stale-read-equal-step-countscompleted141.0mMISMATCH1
step-vs-step-forkcompleted120msMISMATCH1
step-vs-step-fork-fencedcompleted120msMISMATCH1
fence-catches-benign-directioncompleted125msok0
in-flight-before-decisioncompleted171.0mMISMATCH1
in-flight-before-decision-countedcompleted201.0mok0
in-flight-after-decisionfailed142.0mMISMATCH1
stale-read-step-count-fork-fencedcompleted201.0mok0
fork-hook-winscompleted131.0mok0
fork-timeout-winscompleted131.0mok0
unclaimed-payload-under-forkcompleted171.0mok0
claimed-payload-under-forkcompleted171.0mok0
writers-independent-step-bodiescompleted120msok0
writers-scripted-tempocompleted120msok0
cancel-mid-stepcancelled70msskipped0

Full trace: world-sim-mint.txt

🟢 Append-only log — 0 fail of 41 total

log=append-only · fence=per-spec

scenariooutcomeeventsvirtreplayviolations
smoke-no-stepscompleted30msok0
smoke-one-stepcompleted60msok0
hook-at-step-startedcompleted120msok0
hook-at-step-completedcompleted120msok0
hook-at-hook-createdcompleted120msok0
deadline-hook-winscompleted71.0hok0
deadline-expirescompleted71.0hok0
long-sleepcompleted1130.0dok0
hook-never-arrivesstalled30msskipped0
step-retries-twicecompleted102.0sok0
parallel-stepscompleted90msok0
hook-on-execution-statecompleted120msok0
peek-hook-before-branchcompleted120msok0
peek-hook-after-branchcompleted120msok0
peek-hook-at-registrationcompleted120msok0
race-hook-before-probecompleted120msok0
race-hook-after-probecompleted120msok0
race-duplicate-deliverycompleted130msok0
attr-hook-before-stepcompleted110msok0
attr-hook-after-stepcompleted110msok0
attr-from-step-bodycompleted130msok0
fork-hook-after-timeoutcompleted141.0mok0
fork-hook-before-timeoutcompleted141.0mok0
count-hook-after-timeoutcompleted171.0mok0
count-hook-before-timeoutcompleted201.0mok0
stale-read-step-count-forkcompleted201.0mok0
stale-read-equal-step-countscompleted141.0mok0
step-vs-step-forkcompleted120msok0
step-vs-step-fork-fencedcompleted120msok0
fence-catches-benign-directioncompleted125msok0
in-flight-before-decisioncompleted171.0mok0
in-flight-before-decision-countedcompleted171.0mok0
in-flight-after-decisioncompleted192.0mok0
stale-read-step-count-fork-fencedcompleted201.0mok0
fork-hook-winscompleted131.0mok0
fork-timeout-winscompleted131.0mok0
unclaimed-payload-under-forkcompleted171.0mok0
claimed-payload-under-forkcompleted171.0mok0
writers-independent-step-bodiescompleted120msok0
writers-scripted-tempocompleted120msok0
cancel-mid-stepcancelled70msskipped0

Full trace: world-sim-append-only.txt

@VaguelySerious
VaguelySerious merged commit 3450c86 into mainAug 13, 2026
544 of 555 checks passed
@VaguelySerious
VaguelySerious deleted the peter/windows-events-consumer-poll branch August 13, 2026 15:59
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 3450c86 (AI decision).

This is a test-only flaky-test fix, which would normally qualify, but the tests it repairs do not exist on stable: git show origin/stable:packages/core/src/events-consumer.test.ts is a 562-line file with no waitPastDeferredCheck, no MIN_DEFERRED_CHECK_DELAY_MS, and none of the duplicate-skipping/deferred-check cases the diff rewrites. The underlying implementation also differs — stable uses a fixed DEFERRED_CHECK_DELAY_MS = 100 constant with no getDeferredCheckDelayMs() or idle-wait poll loop — so there is no corresponding Windows flake on the maintenance line to fix.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

3450c867f642c5aa41661eb917c96135f5f2874d

Sign up for freeto 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.

2 participants

@VaguelySerious@karthikscale3