Patterns — Mailbox and Topology Patterns
Concepts: Tools — Mailbox.
API: API Reference — Mailbox.
Mailbox patterns
Try-receive polling
When to use.
- Non-blocking work loop.
Code shape.
Batch receive
When to use.
- Empty an entire mailbox in one call.
Code shape.
Why.
- Reduces synchronization overhead.
- Natural bulk processing.
Out-of-band priority
When to use.
- Shutdown.
- Urgent control messages.
Code shape.
Why.
- OOB messages always precede normal traffic.
- FIFO inside the OOB region.
Mailbox close recovery
When to use.
- Every close. Not only the ones you expect to find something.
Code shape.
var rem: polynode.ItemList = mbx.close();
while (rem.popFirst()) |ih| {
// release: free it, or put it back into a pool
}
Why.
-
The mailbox never touches an item, so everything it held comes back to someone. At close that someone is you.
-
Which release applies — free, or return to a pool — is yours to know. The mailbox does not know and never did.
-
Run it unconditionally.
closecan be called more than once and gives back an empty list after the first, so the loop is always safe: on a mailbox still holding
items, on one already empty, on one closed twice. -
Nothing leaks.
- Close is also the end-of-stream signal for blocked receivers (see Group shutdown in Shutdown & Master Patterns).
Do not.
-
Do not write
_ = mbx.close(). It drops what the mailbox gave back, and the items it drops keep their list links —Mbox.sendasserts an unlinked
item, so they cannot be sent again. -
Do not reason about whether the mailbox is empty. The empty case costs nothing, and the reasoning is what a later edit breaks.
Release a refused transfer
When to use.
- Every
sendthat can meet a closed mailbox, and everyputthat can meet a closed pool.
Code shape.
mbx.send(&slot) catch |err| {
pl.put(&slot); // it came from the pool, it goes back there
return err;
};
Why.
-
A refused transfer does not happen.
send/send_oobreturnerror.Closedbefore clearing the slot, andputon a closed pool is a no-op — either
way the item is still yours. -
put_allis the list form: it stops at the first refusal and leaves the rest in your list. Check the list after the call. -
A
deferon the slot covers this path already. A baretry mbx.send(&slot)with no defer does not.
Example: examples/layer4/056-job_pool_circular.zig.
Wake blocked receivers without a message
When to use.
- Re-check external state (a flag flipped outside the mailbox) without sending a real item.
- Poke a Master blocked in
receive()so it re-evaluates its loop condition.
Code shape.
mbx.receive(&slot, null) catch |err| switch (err) {
error.Wakeup => {
if (shutdown.load(.acquire)) return;
continue; // spurious poke, re-check and keep waiting
},
else => ...,
};
Why.
- Distinct from
close(): the mailbox is not torn down, sending still works afterward. - Distinct from
send(): nothing is queued, no item to free. - Only receivers already blocked at the time of the call return
error.Wakeup— a receiver that startsreceive()afterward is not affected.
Example: examples/layer2/097-wake_up_all.zig.
Topology patterns
Recurring shapes for connecting mailboxes and workers. Each is a composition of the
Mailbox patterns above, not a new mechanism.
Request-Response
When to use.
- One side asks, the other answers, on two dedicated mailboxes.
Pattern.
main ──Event(request)──► req_mbx ──► worker
│ process
V
main ◄──Event(response)── resp_mbx ◄── worker
Why.
- Request and response never share a mailbox — no risk of the caller receiving its own request back.
- Caller blocks on
resp_mbxwith a timeout; worker loops onreq_mbxuntil closed.
Example: examples/layer2/057-request_response.zig, examples/layer4/021-request_response.zig.
Pipeline
When to use.
- A chain of stages, each transforming and forwarding.
Pattern.
Why.
- Each stage owns one item at a time — the slot rule holds at every hop.
- A sentinel value (e.g.
code == -1) signals end-of-stream down the chain; the last stage frees it.
Example: examples/layer2/056-pipeline.zig, examples/layer4/020-pipeline_masters.zig.
Fan-In
When to use.
- Several concurrent senders, one shared mailbox, one receiver.
Pattern.
Why.
- The mailbox itself does the merging — no separate synchronization needed.
- Batch receive plus polymorphic dispatch (mixed item types) empties it in one pass.
Example: examples/layer2/058-fan_in.zig, examples/layer4/053-pool_fan_in.zig.
Fan-Out
When to use.
- Several worker threads compete for items on one shared mailbox.
Pattern.
main ──items──► mailbox ──► worker A
├──► worker B (compete; each item goes to exactly one)
└──► worker C
Why.
- The mailbox does the load distribution. No round-robin logic in application code.
Mbox.closereturns any item left unclaimed — the closer must free it.
Example: examples/layer2/061-fan_out.zig, examples/layer4/054-pool_fan_out.zig.