Skip to content

fix(runtime): raise timeout_value only when a receive would wait - #15

Merged
mremond merged 2 commits into
mainfrom
fm/task-2aff2638
Nov 12, 2026
Merged

mremond merged 2 commits into
mainfrom
fm/task-2aff2638

Conversation

@mremond

@mremond mremond commented Nov 12, 2026 •

Copy link
Copy Markdown
Member

Why

A receive whose after value is not a valid timeout raised timeout_value before it looked at the mailbox, so a matching message already queued was never received. BEAM receives that message and raises only when the receive would have to wait.

What changed

  • A receive with an invalid after value now receives a matching message already in the mailbox, as on BEAM.
  • With no matching message it still raises timeout_value at once, keeps the queued messages and never waits for a later one.
  • Inside try, an error from the after expression or from earlier in the try body reaches the handler before the receive looks at the mailbox.
  • The receive_timeout_domain fixture asserts this order and runs natively and on BEAM.

Before and after

Before: `self() ! x, receive X -> {got, X} after id(foo) -> tmo end` raised timeout_value natively while BEAM OTP 29 returned {got, x}, and the old fixture failed on BEAM at its first queued-message check.

After: The same receive returns {got, x} natively, all 11 probe cases print what BEAM OTP 29 prints, and the fixture passes 10 of 10 runs natively, with one scheduler and on BEAM.

Nothing on screen changes: this is how compiled Erlang programs behave at run time, shown by their printed results.

Checked

  • 11 receive cases with invalid and valid timeouts, run natively and on BEAM OTP 29: Native output now matches BEAM; before the change 4 cases differed
  • Independent review comparing about 210 receive shapes with BEAM: Found one regression inside try in the first version, now fixed; the second pass found none
  • receive_timeout_domain fixture: Fails natively before the change; passes 10 of 10 natively, 10 of 10 with one scheduler and 10 of 10 on BEAM after it
  • Release build, clippy with warnings as errors, Rust workspace tests and the allocator check: All exit 0; 2142 Rust tests pass, 1 ignored
  • Full end-to-end suite, locally on macOS and in CI on Linux and macOS: 472 pass and 2 skip locally; every CI shard passes, one Linux shard after a rerun of an unrelated intermittent ordering test
  • Native timer lateness probe, all 24 cases, on this change and on main: 24 of 24 pass on both; an earlier run on a heavily loaded machine had one sample 2.08 ms early
How it was checked

cargo build --release --locked (runtime first, then the compiler)

cargo clippy --workspace --all-targets --locked -- -D warnings

cargo test --workspace --locked --no-fail-fast

python3 scripts/test-allocator-config.py

nera test (the full end-to-end suite)

nera test receive_timeout_domain, ten times, also with NERA_SCHEDULERS=1, and the same fixture on BEAM OTP 29 ten times

python3 scripts/test-native-timer.py, on this change and on a build of main

Probe programs compiled natively before and after the change and run on BEAM OTP 29

Risk

Risk above low: Blast radius, medium — Every receive with an after clause compiles through the changed lowering and check

Full risk assessment
Dimension Level Why
Blast radius medium Every receive with an after clause compiles through the changed lowering and check
Behavior low Only invalid after values behave differently, now as on BEAM
Undo low Revert the commits; no saved data or settings involved
Evidence low Shown end to end against BEAM with probes and the fixture
Sensitivity low No sign-ins, keys, data or releases touched
Novelty low Moves an existing check within the receive lowering
Dependencies low No dependency changes
Not done

A receive with an invalid after value, inside a try ... after cleanup block that runs in another try, now raises the earlier error instead of timeout_value; the cause is the separate defect that runs such cleanup blocks with the error still pending.

Windows was not run, and Linux ran only in CI.

A receive whose after value is not a valid timeout now receives a
matching message already in the mailbox, as on BEAM. The receive records
the invalid value when it starts and raises timeout_value from its
timeout check, which runs once the mailbox scan finds no match. Queued
messages that do not match stay in the mailbox, and the receive never
waits for a later message.

The receive_timeout_domain fixture asserts this order and runs natively
and on BEAM.
Inside try, an operation that raised earlier in the body, or in the
after expression itself, reaches the handler before the receive scans
the mailbox. Without this check a matching message was received while
the error was still pending; with an empty mailbox the receive replaced
the error with timeout_value.

The receive_timeout_domain fixture covers both orders.
@github-actions github-actions Bot added this to the 0.1.1 milestone Nov 12, 2026
@mremond
mremond merged commit 3ac3cf9 into main Nov 12, 2026
41 of 43 checks passed
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.

1 participant