3.3 KiB
3.3 KiB
Cycle 6: Per-Actor Mailbox Backpressure — Development History
Commit:
265992c· 6 files · 163 insertions, 8 deletions
Motivation
Before this change, swactor mailboxes were unbounded — a fast producer could flood a slow consumer's mailbox without limit, eventually exhausting memory. Every production actor framework provides some form of backpressure. This was identified as a key weakness in the competitor analysis.
Competitor Analysis
| Framework | Default Capacity | Overflow Policy | Backpressure Model |
|---|---|---|---|
| Erlang/OTP | Unbounded | N/A (pobox for opt-in bounding) | Process isolation limits blast radius |
| Actix | 16 | do_send() bypasses for internal msgs |
Tiny default, force callers to handle |
| Kameo | 64 | Bounded tokio mpsc (sender blocks) | Blocking backpressure |
| Tokio mpsc | User-specified | Bounded (sender blocks or permit pattern) | Blocking or try_send |
| Go channels | User-specified | Blocking send / non-blocking select | Blocking backpressure |
| Swactor (before) | Unbounded | None | None |
Key observation: Actix's default capacity of 16 is aggressive — it forces callers to think about message flow. Kameo's 64 matches swactor's message budget. The consensus across frameworks: bounded by default, with configurable overflow policy.
Implementation
- Added
MailboxOverflowenum:DropNewest(discard incoming when full) andDropOldest(evict oldest to make room) - Added
default_mailbox_capacityandmailbox_overflowtoRuntimeConfig - Default:
capacity=0(unbounded) — 100% backward compatible ActorSlotstores per-actor capacity and policy (initialized from runtime defaults at spawn time)deliver()in worker enforces bounds; dropped messages tracked viadrops_this_tickcountermessages_dropped: AtomicU64added toWorkerStatsandWorkerInfo
Key files modified: src/config.rs, src/worker.rs, src/runtime.rs, src/stats.rs, tests/runtime_api.rs
Design Decisions
- DropNewest vs DropOldest (not blocking) — swactor's synchronous tick model can't block the sender (it would deadlock the entire worker). Drop policies are the only viable option for a sync runtime.
- Default unbounded — backward compatibility. Users opt into backpressure by setting capacity > 0.
- Per-runtime defaults, not per-actor — simpler configuration. Per-actor overrides could be added later via a builder pattern on spawn.
- Drop counting — critical for observability. Without it, users can't tell if their system is losing messages.
- No DropRandom — the two policies cover the common cases. DropNewest protects against producer floods (newest messages are redundant). DropOldest keeps the freshest state (useful for sensor/status actors).
Tests Added
4 new tests (60 → 64 total):
bounded_mailbox_drop_newest_caps_at_capacity— 50 msgs sent, capacity 10 → only 10 delivered (oldest 10)bounded_mailbox_drop_oldest_keeps_newest— 10 msgs sent, capacity 5 → newest 5 keptunbounded_mailbox_delivers_all_messages— backward compatibility: capacity=0 delivers everythingbounded_mailbox_refills_after_processing— capacity 5, process batch, refill works correctly
Result
- 64 tests pass
- All workspace crates compile
- Swactor weakness "no backpressure" resolved