Skip to content

ci(windows): the batch leg is one test run, so it must not queue against itself - #4876

Merged
lidge-jun merged 1 commit into
devfrom
codex/2580-windows-no-queue
Sep 17, 2026
Merged

lidge-jun merged 1 commit into
devfrom
codex/2580-windows-no-queue

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Sep 17, 2026

Copy link
Copy Markdown
Owner

Summary

dev went red on windows 4/9 of dispatch 35191675127, and the cause is mine.

#4851 replaced the Windows leg's single bun test invocation with 25 sequential batch invocations. tests/preload.ts takes a user-scoped machine-local lock, and line 101 makes it win32-only — so Linux and macOS have run this same batch runner unqueued for a long time, while Windows had never batched at all. The lock joins workers sharing a run ID and blocks anything with a different one, so each batch began queueing behind the previous batch's stragglers:

[test] bare Bun worker 2548 is waiting for test run pid 7272 to release the user lock.
##[warning]Bun test process timed out after 480s in shard 4/9 batch 6/25.
##[error]Shard 4/9 batch 6: every file passed alone, so the timeout lives in multi-file process state.

The first file of that batch then ran in 11.25s during attribution. The eight minutes were queue, not work — which is exactly why a larger bound would only have hidden it for longer.

scripts/test-run-lock.ts already names this case in its own timeout message: set OCX_TEST_NO_QUEUE=1 only when overlapping test runners are intentional. A dedicated CI job running its own batches back to back is one logical test run, so the batch step now sets it. What the queue protects against — an unrelated second suite stacking load on a developer's machine — cannot happen inside that job, and each batch still creates its isolated home and arms the live-home and service-manager guards before it would have reached the lock.

The lock's own unit tests now pin an explicitly queued environment, so the workflow bypass can never silently turn their acquisitions into no-ops. That was the real hazard in disabling a guard by environment variable, and it is closed rather than assumed away.

The 480-second batch bound and the 30-minute job ceiling are unchanged. Nothing was widened.

Verification

No local suite, focused test, typecheck, build, or install was run; this lane is hosted-CI-only by task contract. Verification is static plus exact-head hosted CI, and the Windows behaviour can only be proven by a lane=all dispatch on this head.

Evidence the wait was a queue rather than work:

  • Eight of nine shards passed on the same dispatch at the same SHA; only 4/9 waited out its bound.
  • The blocked file completed in 11.25s once admitted.
  • The waiting message names a different owning PID, which is the lock's documented cross-run-ID block.
  • Linux and macOS have used this batch runner for a long time without this failure, and they never take the lock.

The alternative considered and rejected was reverting Windows to a single un-batched invocation. That would have restored green equally well but given up the per-batch bound that names a wedged file, which is why #4851 added batching in the first place.

Checklist

  • Root cause fixed; no bound widened to cover a queue
  • The guard's own tests pin the queued path so the bypass cannot make them vacuous
  • Workflow-shape tests and structure/ docs updated
  • No production behaviour changed
  • Targets dev

Summary by CodeRabbit

  • Bug Fixes

    • Improved Windows CI batch reliability by preventing separate test batches from blocking one another.
    • Preserved isolated test environments and safeguards for each batch.
    • Improved failure attribution when test processes crash or time out.
  • Documentation

    • Clarified Windows CI behavior, process isolation, queue handling, and failure reporting.
  • Tests

    • Added coverage to verify queue handling and environment settings across clean, timed-out, and crashed test runs.

…nst itself

dev went red on windows 4/9 of dispatch 35191675127, and the cause is mine.

#4851 replaced the Windows leg's single `bun test` invocation with 25 sequential
batch invocations. tests/preload.ts takes a user-scoped machine-local lock, and
line 101 makes it win32-only, so Linux and macOS have run this same batch runner
unqueued for a long time while Windows had never batched at all. The lock joins
workers that share a run ID and blocks anything with a different one, so each batch
now queued behind the previous batch's stragglers:

  [test] bare Bun worker 2548 is waiting for test run pid 7272 to release the user lock.
  ##[warning]Bun test process timed out after 480s in shard 4/9 batch 6/25.
  ##[error]Shard 4/9 batch 6: every file passed alone

The first file of that batch then ran in 11.25s during attribution. The eight
minutes were queue, not work, which is why a larger bound would only have hidden it
for longer.

scripts/test-run-lock.ts already names this case in its own timeout message: set
OCX_TEST_NO_QUEUE=1 only when overlapping test runners are intentional. A dedicated
CI job running its own batches back to back is one logical test run, so the batch
step now sets it. What the queue protects against - an unrelated second suite
stacking load on a developer's machine - cannot happen in that job, and each batch
still creates its isolated home and arms the live-home and service-manager guards
before it would have reached the lock.

The lock's own unit tests now pin an explicitly queued environment, so the workflow
bypass can never silently turn their acquisitions into no-ops. That was the real
risk in disabling a guard by environment variable.

The 480-second batch bound and the 30-minute job ceiling are unchanged.

No local suite, focused test, typecheck, build, or install was run.
@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner September 17, 2026 07:53
@github-actions

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@github-actions github-actions Bot added the chore Maintenance, CI, tests, refactors, or build changes (not a user-facing bug or feature). label Sep 17, 2026
@coderabbitai

coderabbitai Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 51a008c9-313c-43fb-a1e8-500e1e59fca7

📥 Commits

Reviewing files that changed from the base of the PR and between 7868f5d and 3776e44.

📒 Files selected for processing (6)
  • .github/workflows/ci.yml
  • structure/ops/docs-and-release.md
  • structure/overview.md
  • tests/ci-workflows/ci-crash-disposition.test.ts
  • tests/ci-workflows/ci-workflows.test.ts
  • tests/ci-workflows/test-runner.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.


📝 Walkthrough

Walkthrough

The Windows batch workflow now disables the user-scoped test queue. Documentation records the behavior. CI tests verify queue-bypass propagation and explicitly preserve queued lock tests.

Changes

CI queue bypass

Layer / File(s) Summary
Queue bypass wiring and documentation
.github/workflows/ci.yml, tests/ci-workflows/ci-workflows.test.ts, structure/ops/docs-and-release.md, structure/overview.md
The Windows batch step sets OCX_TEST_NO_QUEUE=1. Documentation and workflow tests describe and assert the setting.
Queue bypass propagation validation
tests/ci-workflows/ci-crash-disposition.test.ts
The fake Bun records the queue-bypass value. Tests verify it for normal and timeout-attribution processes.
Queued lock test isolation
tests/ci-workflows/test-runner.test.ts
Lock tests pass an explicit empty environment so they exercise queued lock behavior without inheriting the workflow opt-out.

Priority: ⬇️ Low

Estimated code review effort: 2 (Simple) | ~12 minutes

Change: Bug fix

Merge Risk: ⚪ Minimal · up to 3776e

This PR changes only Windows CI queueing and its validation; no production behavior or material current-head risk is identified.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 3 files. (3 skipped: 3 … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: disabling queueing between sequential Windows CI batches that form one logical test run.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 3 files. (3 skipped: 3 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/2580-windows-no-queue

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@lidge-jun

Copy link
Copy Markdown
Owner Author

리뷰 · 우선순위 76 / 80

이 PR은 Windows CI 배치 다리가 자기 자신과 큐잉하지 않도록 고칩니다. 현재 dev(HEAD 7868f5df5, 패키지 2.58.0, tip #4867)에는 #4851이 이미 들어가 있어서, Windows 샤드는 예전처럼 한 번의 bun test가 아니라 scripts/ci/run-bun-test-batches.sh로 파일 6개 / 480초짜리 배치를 연쇄로 돌립니다. 그런데 tests/preload.ts의 사용자 잠금은 win32에서만 잡히고, 서로 다른 test-run ID를 가진 Bun 프로세스는 앞 프로세스의 잔여 자식이 잠금을 풀 때까지 기다립니다. 그래서 배치 N의 스트래글러가 있으면 배치 N+1이 실제 테스트를 시작도 못 한 채 480초 프로세스 한도에 걸립니다. 작성자가 가리킨 dispatch 35191675127의 windows 4/9 batch 6/25가 그 케이스이고, 대기 메시지에 다른 PID가 보이며 attribution에서는 첫 파일이 11.25초에 끝난 점이 “큐이지 작업이 아님”을 뒷받침합니다. Linux/macOS는 같은 배치 러너를 오래 써도 잠금을 안 잡아서 이 함정에 안 빠졌습니다.

고치는 방법은 한도를 늘리는 게 아니라, Windows 배치 step에만 OCX_TEST_NO_QUEUE=1을 넣는 것입니다. scripts/test-run-lock.ts가 이미 이 탈출구를 문서화해 두었고(“overlapping test runners are intentional”일 때만), 한 CI job 안의 연속 배치는 논리적으로 한 번의 테스트 런입니다. 프로덕션 동작은 건드리지 않고, 480초 배치 한도와 30분 job 천장도 그대로입니다. 각 프로세스는 잠금 경계 전에 격리 home과 live-home / service-manager 가드를 그대로 설치합니다. 잠금 자체를 없애는 게 아니라, “한 job 안의 배치끼리 서로 경쟁 러너로 취급하지 말라”는 계약만 맞춥니다.

위험했던 부분은 env로 가드를 끄는 순간 단위 테스트까지 같이 no-op이 되는 것이었습니다. 이 PR은 tests/ci-workflows/test-runner.test.tsbun test user lock describe에 빈 queuedTestEnv를 명시해, 워크플로 우회와 무관하게 큐 경로를 강제로 검증합니다. ci-workflows.test.ts는 Windows step env에 OCX_TEST_NO_QUEUE=1이 있는지를 고정하고, ci-crash-disposition.test.ts는 픽스처 call 로그에 플래그를 남겨 primary와 attribution 스윕 모두에 우회가 전달되는지까지 확인합니다. structure/ops/docs-and-release.mdstructure/overview.md에도 같은 계약을 적어 두어 문서와 워크플로가 어긋나지 않습니다.

types.ts/config.ts 분할과 무관하고, 중복 PR도 아닙니다. base는 dev이고 tip 7868f5df5를 포함한 ready PR입니다. 남은 검증 공백은 “이 수정이 Windows에서 실제로 큐를 없앴는지”인데, PR 이벤트에서는 windows N/9가 skip이라 push CI만으로는 증명되지 않습니다. 작성자도 lane=all dispatch로만 증명하겠다고 했고, 한도를 늘리지 않은 선택은 #4851이 배치를 넣은 이유(웨지 파일을 이름 붙이는 것)를 보존합니다. 전체적으로 원인·수정·가드 테스트·문서가 한 줄로 맞고, 메인테이너 CI 위생으로 방향과도 잘 맞습니다.

.github/workflows/ci.yml (platform-windows Test env) - OCX_TEST_NO_QUEUE=1을 Windows 배치 step에만 두어, win32 전용 user lock이 연속 배치를 서로 막는 경로를 끈다. 한도 확대가 아니라 큐 제거다.

tests/ci-workflows/test-runner.test.ts (queuedTestEnv) - 잠금 단위 테스트가 프로세스에 상속된 OCX_TEST_NO_QUEUE에 물리지 않도록 빈 env로 큐 경로를 고정한다. 우회가 테스트를 공허하게 만드는 위험을 닫는다.

tests/ci-workflows/ci-workflows.test.ts (windowsTest.env.OCX_TEST_NO_QUEUE) - 워크플로 shape 테스트가 Windows step에 우회 env가 남는 것을 고정한다. 나중에 env가 빠지면 CI shape가 바로 빨개진다.

tests/ci-workflows/ci-crash-disposition.test.ts (noQueueFlags) - primary뿐 아니라 attribution 스윕에도 플래그가 전달되는지 확인한다. 실패 프로세스 생존자가 진단 스윕까지 다시 큐에 태우는 회귀를 막는다.

structure/ops/docs-and-release.md · structure/overview.md - Windows 배치가 한 logical runner라는 계약을 문서에 반영한다. 워크플로만 바꾸고 문서가 남는 드리프트를 피한다.

메인테이너의 판단이 필요한 지점

  • PR push CI는 windows N/9를 skip하므로, 실측 증명은 lane=all dispatch(또는 동등한 Windows 풀스위트)로만 닫힌다. exact-head Linux/macOS 그린만으로 merge할지, Windows 증거까지 기다릴지.
  • self-hosted Windows에서 job이 겹칠 때도 NO_QUEUE가 job 경계를 넘는지 한 번 더 볼지(설계상 step env 범위라 job 간은 별개로 두는 편이 맞음).
  • ci(windows): restore the margin the six-shard leg lost, and make a breach legible #4851 배치 계약을 유지한 채 이 우회를 표준으로 둘지, 장기적으로 배치 간 run ID를 공유하도록 잠금 모델을 바꿀지.

너의 추천
KEEP — tip 7868f5df5 기준 exact-head CI가 그린이면 merge 후보. Windows 재발을 닫으려면 merge 전후 한 번 lane=all로 windows 9/9가 큐 대기 없이 끝나는지 확인하는 것이 맞다. 한도 확대나 배치 원복은 하지 말 것.

이 댓글은 grok-bot이 작성했습니다

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3776e44144

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .github/workflows/ci.yml
BUN_TEST_FILE_SCOPE: all
BUN_TEST_BATCH_SIZE: "6"
BUN_TEST_BATCH_TIMEOUT_SECONDS: "480"
OCX_TEST_NO_QUEUE: "1"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep the Windows lock integration test enabled

Setting this variable on the whole workflow step propagates it into every Bun test process, so tests/ci-workflows/test-runner.test.ts:1038 evaluates its Windows-only test.if(... && process.env[TEST_RUN_NO_QUEUE_ENV] !== "1") to false. That test is the only executed coverage proving nested Windows Bun processes inherit and validate the live lock capability; because it is already skipped on non-Windows platforms, this change silently disables it everywhere. Scope the bypass so the outer batch runner consumes it without exposing it to test code, or explicitly restore the queued environment for this integration case.

AGENTS.md reference: .github/AGENTS.md:L18-L18

Useful? React with 👍 / 👎.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-17T07:56:28.472651Z 3776e44 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@Ingwannu Ingwannu left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Requesting changes on exact head 3776e44144c832361ce93ae5feb19b99a2ab8220.

The current Codex review finding is valid and blocks this workflow change. Setting OCX_TEST_NO_QUEUE=1 on the entire Windows batch step reaches every Bun test process. tests/ci-workflows/test-runner.test.ts gates its only Windows nested-lock integration case with process.env[TEST_RUN_NO_QUEUE_ENV] !== "1", so that case is now skipped on Windows; it was already skipped on Linux/macOS. The new queuedTestEnv = {} arguments preserve direct unit cases but do not restore this process-level integration test.

Please scope the bypass so only the outer batch-runner lock is disabled while the tests receive an explicitly queued environment, or change the Windows integration fixture so it deliberately clears the bypass and proves the complete inherited lock capability without competing with the workflow runner. Add a workflow-shape assertion for that boundary. The in-progress lane=all result cannot close this blocker because the current head can report green with the relevant integration test silently skipped.

@lidge-jun
lidge-jun enabled auto-merge (squash) September 17, 2026 08:33
@lidge-jun
lidge-jun disabled auto-merge September 17, 2026 08:34
@lidge-jun
lidge-jun merged commit f1dfda8 into dev Sep 17, 2026
83 of 86 checks passed
@lidge-jun
lidge-jun deleted the codex/2580-windows-no-queue branch September 17, 2026 08:34
lidge-jun added a commit that referenced this pull request Sep 17, 2026
… diagnostics

The evidence this PR's budget argument rested on turned out to be an artifact of
how it was produced. Both evidence branches were dispatched from refs whose
merge-base predates #4876, and workflow_dispatch reads the workflow from the
dispatched ref, so those runs used a ci.yml without OCX_TEST_NO_QUEUE and
reproduced exactly the batch-serialization bug #4876 had already fixed. The
batch-4 log shows it directly: one line saying a bare Bun worker is waiting for
another test run to release the user lock, then eight minutes with no (pass) at
all. Confirmed on the branches: OCX_TEST_NO_QUEUE appears 0 times in
codex/ci-evidence-4875's ci.yml and once in dev's.

So whether these deadlines are actually too small is undetermined again, and the
COLD_SPAWN_BUDGET_MS use is withdrawn. Both files keep their original bounds.

What does not depend on that evidence stays. Neither wait observed its child, so
a child that died and one that was merely slow produced the same message; both
now race the exit and report the code and stderr immediately, re-checking the
awaited signal first so a child that signalled and then exited is not
misreported. And the owner harness's timeout branch awaited a promise that only
resolves at EOF while describing a child that is still running, which could
never settle; it is bounded now.

Those are diagnostic defects on their own terms, and they are what would have
made the original evidence readable in the first place.
lidge-jun added a commit that referenced this pull request Sep 17, 2026
…eout (#4902)

* test(windows): size the first-child readiness wait for cold start, and report dead children

Two Windows shards failed on two different files during heavy queue contention,
and both are the same defect: an in-test deadline that has to cover a Windows
cold first child start, sized for an idle runner.

run 35211904734 windows 3/9 failed at waitFor(holdMarker), not at the contention
assertion -- that line was never reached, and holdMs was never approached. run
35210400258 windows 7/9 failed at the first wait of its case, for "listening",
with no events at all. dev at 6d19a07 passed all nine Windows shards on an
idle queue (run 35215552842), so neither is a defect in the code under test.

This is not a number raised to make red go away. The deadlines were smaller than
the range this repository has already measured for the wait they bound.
COLD_SPAWN_BUDGET_MS records a first child publishing at 50.7s while the next
spawn in the same file was ready in 1.76s, with surviving readiness waits from
2.0s to 19.7s; codex-write-lock.test.ts says in its own comment that a holder
child boots in 8-19s on a loaded shard, and bounded that wait at 15s. A case the
file calls normal is slower than the deadline it has to meet. Each file now
spends that named ceiling exactly once, on its first child, as the constant's
own contract requires; every later wait keeps the ordinary bound.

The second defect is that neither wait observed the child. A child that died and
one that was merely slow produced the same message, so CI could not tell them
apart, and the two want opposite fixes. Both waits now race the exit and report
the code and stderr immediately. That is also what makes the native-main
evidence readable: its stderr promise resolves at EOF, so an empty stderr in
that message means the child had already exited silently rather than that it was
still booting.

The timeout branch in the owner harness also awaited that same EOF promise while
claiming the child was still running, which could never settle. It is bounded now.

Closes #4901

* test(windows): drop the readiness-budget change, keep the child-death diagnostics

The evidence this PR's budget argument rested on turned out to be an artifact of
how it was produced. Both evidence branches were dispatched from refs whose
merge-base predates #4876, and workflow_dispatch reads the workflow from the
dispatched ref, so those runs used a ci.yml without OCX_TEST_NO_QUEUE and
reproduced exactly the batch-serialization bug #4876 had already fixed. The
batch-4 log shows it directly: one line saying a bare Bun worker is waiting for
another test run to release the user lock, then eight minutes with no (pass) at
all. Confirmed on the branches: OCX_TEST_NO_QUEUE appears 0 times in
codex/ci-evidence-4875's ci.yml and once in dev's.

So whether these deadlines are actually too small is undetermined again, and the
COLD_SPAWN_BUDGET_MS use is withdrawn. Both files keep their original bounds.

What does not depend on that evidence stays. Neither wait observed its child, so
a child that died and one that was merely slow produced the same message; both
now race the exit and report the code and stderr immediately, re-checking the
awaited signal first so a child that signalled and then exited is not
misreported. And the owner harness's timeout branch awaited a promise that only
resolves at EOF while describing a child that is still running, which could
never settle; it is bounded now.

Those are diagnostic defects on their own terms, and they are what would have
made the original evidence readable in the first place.
@Ingwannu

Copy link
Copy Markdown
Owner

Post-merge follow-up: the step-wide OCX_TEST_NO_QUEUE=1 is still the right fix for batch self-queueing, but it also causes the Windows-only nested live-lock regression in tests/ci-workflows/test-runner.test.ts to skip in that same step. I opened #4991 to restore that coverage without reintroducing the outer queue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

chore Maintenance, CI, tests, refactors, or build changes (not a user-facing bug or feature).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants