Skip to content

test: make test_gap_cron_cronjob wait on a barrier, not a deadline - #10722

Closed
proggeramlug wants to merge 2 commits into
mainfrom
fix-10581-cron-barrier
Closed

proggeramlug wants to merge 2 commits into
mainfrom
fix-10581-cron-barrier

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

test-files/test_gap_cron_cronjob.ts started a * * * * * * CronJob and then waited
with

const tickDeadline = Date.now() + 10_000;
while ((ticks < 2 || autoTicks < 2) && Date.now() < tickDeadline) {  }

That deadline is a timeout, not a barrier: when it expired first the loop exited and
the fixture printed false on two lines expected to read true, and dropped the
tick 1 / tick 2 lines entirely — a four-line output divergence the harness classifies
as a parity_fail, i.e. reports as a compiler regression. The header comment's claim that
the output was "deterministic despite the timing" held only while two ticks of a
one-per-second schedule landed inside ten seconds.

It carries the test_gap_ prefix, so it is inside the gap shards pr-gate actually runs,
and it is absent from test-parity/gap_snapshot.json (= expected to pass). It already
cost real work: it failed in a merge-queue validation and #10530 was held out of a train
on the strength of it, after which a --trace llvm A/B showed byte-identical IR.

The fix

The wait is now a barrier with no deadline. The printed text becomes a function of
CronJob's behaviour alone: either the ticks arrive and the fixture prints its one expected
output, or nothing dispatches and the harness's own PERRY_RUN_TIMEOUT kills the run —
which it classifies as a CRASH/timeout, distinctly from a parity mismatch, so a contended
runner can no longer make this look like a miscompile.

There is deliberately no fallback bound. Any bound that prints, throws or exits
differently on expiry reintroduces exactly this defect at a different threshold: a
false-printing 30-second deadline is the identical bug with a longer fuse.

Worth noting for anyone reaching for "just make the deadline bigger": PERRY_RUN_TIMEOUT
defaults to 10s
(run_parity_tests.sh:58) — the same 10 seconds as the fixture's old
deadline. The old bound was therefore unreachable in practice; it could only ever have
fired in a photo finish with the harness's own kill.

Nothing is weakened. The assertion moved from a printed comparison into the loop's exit
condition, which the program cannot pass without satisfying. The two-arg non-auto-starting
form, the four-arg start=true form, and start()/stop() dispatch are all still
exercised, and tick 1 / tick 2 remain in the diff as positive evidence that the manual
job fired. The never-started job's line got stronger: it printed a hardcoded true, and
now prints neverTicks === 0 from a real counter, checked after the barrier — i.e. after
at least two cron seconds have demonstrably elapsed with that job unstarted. Output bytes
are unchanged.

Test that fails without the fix

An identical 11-second synchronous event-loop stall injected at the same point into the
old and new fixtures — a deterministic stand-in for the loaded runner:

######## ARM A: OLD fixture (main) under an 11s stall ########   [Node 26.5.1]
constructed, ticks now: 0
manual ticked at least twice: false
auto ticked at least twice: false
never-started stayed quiet: true
done

######## ARM B: NEW fixture under the SAME 11s stall ########
constructed, ticks now: 0
tick 1
tick 2
manual ticked at least twice: true
auto ticked at least twice: true
never-started stayed quiet: true
done

Same result under Perry. Cross-engine byte comparison:

=== Node(new) vs Perry(new), unperturbed ===       BYTE-IDENTICAL
=== Node(new) vs Perry(new), 11s stall ===         BYTE-IDENTICAL
=== oracle vs OLD-stalled: the parity_fail the gate would report ===
2,5c2,3
< tick 1
< tick 2
< manual ticked at least twice: true
< auto ticked at least twice: true
---
> manual ticked at least twice: false
> auto ticked at least twice: false

Note that under the stall both engines produce the same wrong output, so an equally
starved pair would still match. The parity_fail arises because Node and Perry run as
separate processes at different moments, so the load hits one and not the other — which is
exactly the observed failure.

Verification

Using prebuilt compiler: …/perrybin/perry
PASS  test_gap_cron_cronjob
Parity Pass: 1   Parity Fail: 0   Crashed: 0   Skipped: 0
HARNESS_EXIT=0
{"kind":"result","id":"test_gap_cron_cronjob","status":"pass","at":"2026-09-19T10:25:59Z"}

cron@4.4.0 installed via npm ci (the harness needs node_modules; a fresh tree
silently drops package-backed fixtures otherwise). Eight consecutive Node runs gave one
distinct output hash in 1.86–2.04 s — roughly a fifth of the 10 s run budget.
./scripts/check_file_size.sh and cargo fmt --all -- --check both exit 0.

Provenance caveat. The worktree had 11 GiB free, below this repo's 25 GiB floor for a
parity build (ENOSPC here produces fake failures naming innocent files), so I did not
build the compiler from this base. I used a prebuilt perry 0.5.1598 — the version
4715bc2fa1 declares — copied out of a shared target pool into an isolated scratch dir
with its coherent libperry_{runtime,stdlib}.a and ext archives, so no other session's
directory was written to. This change touches no Rust, so compiler identity is not
load-bearing for the conclusion, but the binary is not one built from this SHA.

Follow-up

Two sibling fixtures have the same shape and are not touched here — filed as #10720:
test_gap_9592_child_timeout_threads (a 1 s deadline whose expiry prints
timeout threads released: false) and test_gap_9493_child_stdin_backpressure (a watchdog
that resolve(false)s). Both are in gate scope.

Closes #10581

Summary by CodeRabbit

  • Bug Fixes

    • Prevented CronJob validation from reporting false parity failures when scheduled ticks are delayed.
    • Improved handling of timing delays so genuine execution timeouts are distinguished from output mismatches.
  • Tests

    • Strengthened CronJob coverage for manual, automatic, and non-started jobs.
    • Preserved expected output while making checks wait for the required scheduled activity before completing.

…10581)

The fixture raced a `* * * * * *` CronJob against a fixed `Date.now() + 10_000`.
The deadline is a timeout, not a barrier: when it expired first the loop exited
and the fixture printed `false` on two lines expected to read `true`, dropping
`tick 1`/`tick 2` as well — a four-line divergence the harness reports as a
`parity_fail`, i.e. as a miscompile. It is inside pr-gate's gap shards and
absent from gap_snapshot.json, and it already held #10530 out of a train.

The wait now has no deadline, so the printed text is a function of CronJob's
behaviour alone: the ticks arrive, or PERRY_RUN_TIMEOUT kills the run and the
harness classifies that as a crash/timeout rather than a parity mismatch. No
fallback bound — any bound that prints or throws on expiry is the same defect
with a longer fuse, and the old 10s was unreachable anyway because
PERRY_RUN_TIMEOUT is also 10s.

The never-started job now prints `neverTicks === 0` from a real counter instead
of a hardcoded `true`, checked after the barrier. Output bytes unchanged.

Verified with an identical 11s event-loop stall injected into both the old and
new fixtures: under Node 26.5.1 and Perry v0.5.1598 the old one diverges and the
new one is byte-identical to the unstalled oracle. Harness run exits 0 with
journal status `pass`; 8 Node runs gave one distinct output in 1.86-2.04s.
@coderabbitai

coderabbitai Bot commented Sep 19, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 15726650-b9ee-4f6f-bb4d-4fd32c623d91

📥 Commits

Reviewing files that changed from the base of the PR and between 4715bc2 and d12f887.

📒 Files selected for processing (2)
  • changelog.d/10722-cron-cronjob-barrier.md
  • test-files/test_gap_cron_cronjob.ts

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


📝 Walkthrough

Walkthrough

The CronJob gap fixture no longer uses a 10-second wall-clock deadline. It waits until both active jobs tick twice, tracks unexpected ticks from the never-started job, and documents the change.

Changes

CronJob barrier fixture

Layer / File(s) Summary
Deadline-free CronJob barrier
test-files/test_gap_cron_cronjob.ts, changelog.d/10722-cron-cronjob-barrier.md
The fixture waits for both active jobs to reach two ticks without a local deadline. The never-started callback increments neverTicks, and the final check uses neverTicks === 0. The changelog records the timeout classification and barrier behavior.

Priority: ➖ Normal

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

Change: Bug fix · Severity of issue fixed: Medium

🚥 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 1 files. (1 skipped: 1 … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: replacing the CronJob test deadline with a barrier.
Description check ✅ Passed The description provides complete technical context, explains the fix, identifies issue #10581, and documents detailed verification results. It does not use the template's section headings or include …
Linked Issues check ✅ Passed For issue #10581, test_gap_cron_cronjob.ts removes the fixed Date.now() + 10_000 deadline and waits until both active jobs reach two ticks. This makes the tick condition a barrier. The `neverTicks…
Out of Scope Changes check ✅ Passed The changes are limited to the linked fixture and a changelog fragment for issue #10581. The fixture comments, barrier logic, counter assertion, and verification documentation directly support the rac…
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 1 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🛠️ Fix failing CI checks 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

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.

proggeramlug pushed a commit that referenced this pull request Sep 19, 2026
`e2e-scoped` has been red on every PR since merge train 218 (v0.5.1596),
failing at "Compute e2e suite scope" in ~16s:

    ci_e2e_scope: these crates/perry-codegen/tests/*.rs suites are in neither
    SOURCE_SUITE_MAP nor SUITE_EXCLUSIONS: error_subclass_field_init,
    typed_collection_receiver_guard

Both suites arrived in 6925754 (#10443/#10446, train 218) and nobody
classified them, which is exactly the condition #7708 added this assertion
for. The failure is content-independent, so it reddens PRs that cannot
possibly have caused it -- #10721 (a Python script) and #10722 (a .ts
fixture) both carry it.

Mapped rather than excluded: both are cheap in-process suites of the shape
SOURCE_SUITE_MAP exists for, and both passed when train 218 ran them as
diff-named suites (2 and 3 tests, 0.01s each). Excluding them would have
hidden working coverage; SUITE_EXCLUSIONS is for a named failing test with
an issue number, which neither has.

Verified discriminating, not merely present: with either entry deleted
`--self-test` exits 1 naming the suite, and exits 0 with both.
proggeramlug pushed a commit that referenced this pull request Sep 19, 2026
`e2e-scoped` has been red on every PR since merge train 218 (v0.5.1596),
failing at "Compute e2e suite scope" in ~16s:

    ci_e2e_scope: these crates/perry-codegen/tests/*.rs suites are in neither
    SOURCE_SUITE_MAP nor SUITE_EXCLUSIONS: error_subclass_field_init,
    typed_collection_receiver_guard

Both suites arrived in 6925754 (#10443/#10446, train 218) and nobody
classified them, which is exactly the condition #7708 added this assertion
for. The failure is content-independent, so it reddens PRs that cannot
possibly have caused it -- #10721 (a Python script) and #10722 (a .ts
fixture) both carry it.

Mapped rather than excluded: both are cheap in-process suites of the shape
SOURCE_SUITE_MAP exists for, and both passed when train 218 ran them as
diff-named suites (2 and 3 tests, 0.01s each). Excluding them would have
hidden working coverage; SUITE_EXCLUSIONS is for a named failing test with
an issue number, which neither has.

Verified discriminating, not merely present: with either entry deleted
`--self-test` exits 1 naming the suite, and exits 0 with both.
@proggeramlug
proggeramlug marked this pull request as ready for review September 19, 2026 13:18
proggeramlug pushed a commit that referenced this pull request Sep 19, 2026
@proggeramlug

Copy link
Copy Markdown
Contributor Author

Landed in merge train 224 (#10742), released as v0.5.1603 — main is now d4ef732ab9.

Closing rather than merging is how trains work here: the PRs were cherry-picked onto one tree, validated together, and landed under the train's own commit, so GitHub cannot mark this one merged even though your change is on main.

One deliberate divergence, for #10719 only: the train carries "total": 580 where the PR carries 581. #10668 landed in train 221 after your measurement and removed one finding, so the baseline conflicted. I resolved it by re-deriving with your own new detector against the assembled tree (--update-baseline, schema 3, total 580, 84 files, --check agreeing at rc=0) rather than hand-merging two numbers taken by different detectors against different trees. The schema 2 → 3 migration is intact.

Validation: all nine cheap gates, cargo check --workspace --all-targets under -D warnings, the release build of all five pinned artifacts, every unit suite, and a 3-area gap sweep with zero unexplained regressions, each area asserted to have run a non-zero number of tests. lint completed its full 6-of-6 compile tier with no failure outside the known-red public-baseline step.

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_gap_cron_cronjob races a per-second cron against a fixed 10s deadline, inside pr-gate's scope

1 participant