swactor/docs/development_history/CYCLE_01_FAIRNESS.md
Developer 9ad90aed12 docs: add development history pages for all 19 cfuzz improvement cycles
One page per cycle covering motivation, competitor analysis, implementation,
design decisions, tests added, and results. Plus an overview/index page.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 16:36:52 +00:00

2.9 KiB
Raw Blame History

Cycle 1: Per-Actor Message Budget for Tick Fairness — Development History

Commit: ef87f7e · 8 files · Priority: P0 (critical bug fix)


Motivation

The tick_all function in worker.rs drained the entire mailbox for each actor before moving to the next:

while let Some(msg) = slot.mailbox.pop_front() {
    // processes ALL messages for actor A before moving to actor B
}

If actor A had 10,000 queued messages, all other actors on the same worker were completely starved until A finished. This is a critical fairness bug — every other runtime studied prevents this.

Competitor Analysis

Runtime Fairness Mechanism Budget
Erlang/OTP BEAM Reduction counting, preemptive 4,000 reductions
Tokio Cooperative budgeting 128–256 operations
libuv/Node.js Round-robin across handlers No single handler drains completely
Linux CFS vruntime-based fairness Time slices enforced
Ractor N/A (1 task = 1 actor via tokio) Inherited from tokio
Swactor (before) None Unlimited drain

The BEAM's reduction budget (4,000 per process before preemption) is the gold standard for actor fairness. Tokio's cooperative budget (128 ops) serves a similar purpose for async tasks. Actix has a 256-message assertion guard that validates the approach.

Implementation

  • Added actor_message_budget: usize to RuntimeConfig (default: 64)
  • Modified tick_all in worker.rs to break after budget messages per actor per tick
  • budget=0 means unlimited (100% backward compatible)
  • Updated RuntimeConfig struct literals across all crates (python, runtime-dashboard, mt_benchmarks)

Key files modified: src/worker.rs, src/config.rs, benches/runtime_benchmarks.rs, tests/runtime_api.rs

Design Decisions

  • Budget of 64 chosen as default — between BEAM's 4,000 (too generous for swactor's coarser granularity) and tokio's 128 (per-op vs per-message). Benchmarks showed budget=32 was slightly faster for throughput, but 64 provides more fairness headroom.
  • Per-runtime, not per-actor — simpler configuration, matching the BEAM model where the reduction budget is global. Per-actor budgets could be added later as an extension.
  • budget=0 means unlimited — backward compatibility for users who want the old behavior.

Tests Added

3 new behavioral tests (42 → 45 total):

  • hot_actor_does_not_starve_cold_actor — hot actor with many messages doesn't prevent cold actor from processing
  • unlimited_budget_drains_all — budget=0 preserves old behavior
  • budget_messages_drain_across_multiple_ticks — excess messages carry over to next tick

Benchmarks added: fairness/cold_latency_under_pressure, fairness/throughput_by_budget

Result

  • 45 tests pass (42 original + 3 new)
  • All workspace crates compile
  • Baseline benchmarks established for future comparison