docs(roadmap): mark selective receive (#4) done

Record what landed for recv_match/try_recv_match and update the
no-select gotcha: selective receive stayed per-channel rather than
introducing a cross-channel mailbox.
This commit is contained in:
smarm-dev
2026-06-07 21:51:56 +00:00
parent 2dd61d4a19
commit 02f55fa0a0
+28 -3
View File
@@ -123,8 +123,30 @@ Shipped. What landed vs the plan:
dead-pid link stops a non-trapper / messages a trapper; `unlink` prevents dead-pid link stops a non-trapper / messages a trapper; `unlink` prevents
propagation. propagation.
### 4. Selective receive (independent track) ### 4. Selective receive (independent track) ✅ DONE (`03f3875`)
Channels are strict FIFO MPSC; there's no pattern-matched `receive`. Shipped. What landed vs the plan:
- `Receiver::recv_match(pred) -> Result<T, RecvError>` scans the queued
`VecDeque` front-to-back, removes+returns the first match, leaves the rest in
arrival order; parks and re-scans when nothing matches. `try_recv_match` (the
non-blocking variant, mirroring `try_recv`) rolled in same commit.
- Wakeup turned out cheaper than feared: `Sender::send` *already* took
`parked_receiver` on every push, so "wake on ANY send" needed no send-side
change. The one real edit was relaxing `Sender::drop` to unpark the parked
receiver on the last-sender drop regardless of queue emptiness — a selective
receiver can park on a non-empty no-match queue and must wake to observe
closure. No-op for plain `recv` (only ever parks on an empty queue); stress
suite stays green.
- `pred` is `Fn(&T) -> bool` (not `FnMut`) on purpose: it's re-run from scratch
on every scan, so a stateful predicate would re-count surprisingly. It runs
under the channel lock — keep it cheap/pure, don't re-enter the channel.
- Close semantics: `recv_match` returns `Err(RecvError)` only when closed AND no
queued message matches; a match is still returned on a closed channel.
Non-matches are left for a later `recv`.
- Tests (`tests/selective_recv.rs`): out-of-order match pulled first; non-matches
remain in order; park-on-non-empty then wake on a match; closed-with-only-
non-matches → Err; closed-but-match-present → match; `try_recv_match` states.
Original plan, for reference:
- Add `Receiver::recv_match(pred) -> T`: scan the queued `VecDeque`, remove+return - Add `Receiver::recv_match(pred) -> T`: scan the queued `VecDeque`, remove+return
first match, leave the rest in order; park and re-scan on new arrivals. first match, leave the rest in order; park and re-scan on new arrivals.
- This changes channel wakeup: a parked selective receiver must wake on ANY send - This changes channel wakeup: a parked selective receiver must wake on ANY send
@@ -166,7 +188,10 @@ Channels are strict FIFO MPSC; there's no pattern-matched `receive`.
- **No `select`, no unified per-process mailbox.** Why the supervisor uses the - **No `select`, no unified per-process mailbox.** Why the supervisor uses the
single `supervisor_channel` funnel rather than N monitor channels. trap_exit single `supervisor_channel` funnel rather than N monitor channels. trap_exit
resolved this by giving each trapping actor a dedicated `Receiver<ExitSignal>` resolved this by giving each trapping actor a dedicated `Receiver<ExitSignal>`
inbox (see #3); selective-receive delivery still needs its own decision. inbox (see #3); selective receive (#4) stayed *per-channel* (`recv_match`
scans one channel's queue) rather than introducing a cross-channel mailbox —
if selective receive ever needs to span the monitor/trap inboxes too, that
cross-channel merge is the still-unmade decision.
- **Cooperative-only**: preemption and (future) cancellation both depend on the - **Cooperative-only**: preemption and (future) cancellation both depend on the
actor reaching `check!()`/yield/alloc/blocking points. actor reaching `check!()`/yield/alloc/blocking points.
- `run()` is single-thread (`Config::exact(1)`); tests rely on deterministic - `run()` is single-thread (`Config::exact(1)`); tests rely on deterministic