You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
test_gap_cron_cronjob is timing-dependent and flakes in CI: each side has its own 10 s tick deadline, so a slow moment flips a boolean and the byte comparison fails #10563
Found while triaging a false "regression" on PR #10530. test-files/test_gap_cron_cronjob.ts waits up to 10 s for a * * * * * * CronJob to tick twice and prints manual ticked at least twice: <bool> / auto ticked at least twice: <bool>. Node and Perry each run the program under their own deadline, so a slow moment on EITHER side (including the
Node oracle) flips one boolean and the harness reports an output mismatch — attributed to whatever PR happened to be
running.
Evidence
CI: across the last 40 test.yml runs, 20 gap-suite jobs ran this test; it passed in 19 and failed in 1 (a PR whose
change provably cannot affect it).
That PR's proof: compiling the fixture with --trace llvm on the base commit and on the PR build produces
byte-identical IR (5,438 lines; the only difference is the embedded absolute compile-directory path). cron takes
the native-binding route (crates/perry-ext-cron), so the only TypeScript compiled is the test itself.
Local reruns on both builds: 12/12 pass each, 24 concurrent instances identical, 5/5 pass pinned to one core against
three busy loops.
Failure signature: both exit 0, the first line matches, a later line differs — i.e. a flipped boolean, not a crash.
Impact
A timing-dependent fixture in the ratchet gate makes the gap suite an unreliable signal: a green PR can be blocked and
a real regression can be dismissed as "probably the cron flake". It cost a full investigation cycle on #10530.
Suggested fix
Make the assertions independent of wall-clock progress, for example:
drive the job deterministically (fire the callback via the API instead of waiting on a real schedule), or
assert only facts that do not depend on how many ticks happened within a deadline (e.g. that a never-started job
never fired, that start()/stop() toggle a state flag), printing counts only as >= 1/0, or
give both sides the same generous deadline AND make the printed value insensitive to the deadline being hit
(e.g. wait indefinitely for the first two ticks with a hard timeout that fails loudly rather than printing false).
The same review applies to other timer-driven gap fixtures (test_gap_backoff_options, node-cron variants).
Fixed by 50020e67f1 (#10581), which is on main. The fixed 10-second deadline really is replaced by an unbounded barrier — while (ticks < 2 || autoTicks < 2) await setTimeout(100) — so the flipped-boolean parity mismatch this issue describes is now structurally impossible rather than merely less likely.
Run five times against v0.5.1617: byte-identical to node every time, ~2.0 s against the 10 s PERRY_RUN_TIMEOUT.
One consequence worth stating, since it is a deliberate trade and not an oversight. The wait is now unbounded, so if cron ever stopped dispatching, the run would be killed by PERRY_RUN_TIMEOUT and classified as a crash rather than as a parity mismatch. That is exactly what this issue's own third suggested fix asked for — a crash is a truthful signal where a flipped boolean was a lying one — but it does mean a 10 s budget now gates a ~2 s job, and the fixture's header explains why no smaller fallback bound is permitted.
Related: this fixture was deleted by the cron binding removal and restored in merge train 240, because with the binding gone it now witnesses the real npm package rather than a Rust shim. It passes there: gap_cron ran=1 rc=0 against real cron 4.4.0.
Activity
proggeramlug commentedon Sep 20, 2026
Fixed by
50020e67f1(#10581), which is onmain. The fixed 10-second deadline really is replaced by an unbounded barrier —while (ticks < 2 || autoTicks < 2) await setTimeout(100)— so the flipped-boolean parity mismatch this issue describes is now structurally impossible rather than merely less likely.Run five times against v0.5.1617: byte-identical to node every time, ~2.0 s against the 10 s
PERRY_RUN_TIMEOUT.One consequence worth stating, since it is a deliberate trade and not an oversight. The wait is now unbounded, so if
cronever stopped dispatching, the run would be killed byPERRY_RUN_TIMEOUTand classified as a crash rather than as a parity mismatch. That is exactly what this issue's own third suggested fix asked for — a crash is a truthful signal where a flipped boolean was a lying one — but it does mean a 10 s budget now gates a ~2 s job, and the fixture's header explains why no smaller fallback bound is permitted.Related: this fixture was deleted by the
cronbinding removal and restored in merge train 240, because with the binding gone it now witnesses the real npm package rather than a Rust shim. It passes there:gap_cron ran=1 rc=0against realcron4.4.0.