Files
rutster/LEARNING.md
Aaron D. Lee ee3938864b
Some checks failed
CI / fmt (push) Has been cancelled
CI / clippy (push) Has been cancelled
CI / test (1.85) (push) Has been cancelled
CI / test (stable) (push) Has been cancelled
CI / deny (push) Has been cancelled
CI / sim-bench (stable) (push) Has been cancelled
CI / twilio-live (manual only) (push) Has been cancelled
slice-4½: rutster-sim seed + CI-regressed thresholds (S1-S8) (#18)
Co-authored-by: Aaron D. Lee <himself@adlee.work>
Co-committed-by: Aaron D. Lee <himself@adlee.work>
2026-07-05 16:21:07 +00:00

8.9 KiB

LEARNING.md — to learn concept X, read file Y

This index maps a Rust concept you might be learning to the file where slice 1 makes the concept concrete. Each entry is a worked example you can read in cargo doc --open plus the source file itself.

Concepts + pointers

  • Newtype pattern (type-safety via single-field wrappers)crates/rutster-call-model/src/lib.rsChannelId(Uuid). The newtype stops us from mixing up a ChannelId with some future SessionId at the type-system level. Compile-enforced where a comment could only ask.

  • enum for closed state sets + exhaustive matchcrates/rutster-call-model/src/lib.rsChannelState (New → Connecting → Connected → Closing → Closed). Exhaustiveness checking forces every match to consider each state; adding a state later surfaces every site that needs handling.

  • Sans-IO pattern (no I/O inside the library; input via method calls, output via return values)crates/rutster-media/src/loop_driver.rs — the str0m poll loop. Rtc::handle_input takes a network packet as a struct argument, not from a socket the library owns; poll_output returns Transmit packets the caller sends. Fully testable without a network — str0m integration tests use this property to drive faster than realtime.

  • Trait design for extension points (a futures-compatible seam)crates/rutster-media/src/pcm.rs — the AudioSource / AudioSink traits. Slice 1 wires an EchoAudioPipe between them; step 2 swaps that for a real WSS tap client without touching RtcSession. The traits describe what the splice point does, not how it's filled.

  • Error enums with thiserror + hot-path match-and-continuecrates/rutster-media/src/lib.rs (MediaError) and crates/rutster-media/src/opus_codec.rs (OpusDecoder::decode returns Option<PcmFrame>). Cold path: thiserror-derived enum + ?. Hot path: match-and-continue, never ?, never panic — "drop + observe, don't crash" (spec §3.8).

  • Arc<Mutex<T>> vs Arc<RwLock<T>> — when each is rightcrates/rutster/src/session_map.rs. The RtcSession lives behind Arc<Mutex<...>> because every access mutates it (str0m's &mut self contract) — RwLock's read-mode would be useless. Comment on the struct explains the trade-off.

  • DashMap for sharded concurrent mapscrates/rutster/src/session_map.rs. DashMap shards its inner map so two handlers operating on different ChannelIds don't contend; HashMap wrapped in a single Mutex would serialize every access.

  • str0m 0.21's single-mutation invariantcrates/rutster-media/src/loop_driver.rs. Mutate (handle_input / Writer::write) → drain poll_output to Output::Timeout → next mutate. Violating this leaves str0m in an inconsistent state.

  • tokio graceful shutdown via signal handlerscrates/rutster/src/main.rs (shutdown_signal). Ctrl-C / SIGTERM drops the AppState; the AppState drops the DashMap; the DashMap drops every RtcSession. No in-flight call preservation in slice 1.

  • include_str! for embedding static assetscrates/rutster/src/routes.rs (include_str!("../static/index.html")). The HTML test client is compiled into the binary at build time — no separate file to ship, no disk IO to serve it.

  • mpsc + oneshot for cold-path task supervisioncrates/rutster/src/tap_engine.rs — how a spawned tokio task is supervised + cancelled via oneshot::Receiver.

  • VecDeque as a bounded playout ring with drop-oldest policycrates/rutster-tap/src/tap_audio_pipe.rs — why a manual ring (not mpsc) when the overflow policy is drop-oldest, not drop-newest.

  • Async WS connect + Sink/Stream traitscrates/rutster-tap/src/tap_client.rstokio_tungstenite::connect_async, WebSocketStream, the SinkExt/StreamExt extension traits, tokio::select! over inbound + outbound + close.

  • Box<dyn Trait + Send> field widening (the seam test)crates/rutster-media/src/rtc_session.rs — why the pipe field type changed from EchoAudioPipe to Box<dyn AudioSource + AudioSink + Send> so loop_driver's call sites are byte-identical (slice-2 §8.5 #6).

  • Zero-sized marker newtype for state flagscrates/rutster-call-model/src/lib.rsTapHandle(()) compiles Option<TapHandle> to a single bool; no runtime cost for the type-system marker.

Slice 3 — OpenAI Realtime brain

  • async-trait patterns / async fns in trait objectscrates/rutster/src/tool_registry.rs — the Tool trait's async fn call and how async-trait lowers it to Pin<Box<dyn Future + Send>> (the same boxing the slice-1 pipe widening used, now applied to trait methods).
  • OpenAI Realtime adapter + event translation (pure-function layer)crates/rutster-brain-realtime/src/translator.rs — the §4.2 event mapping table as pure fns: tap audio_in ⇄ OpenAI input_audio_buffer.append, response.audio.deltaaudio_out, function_call_arguments.donefunction_call. Testable without a network (the pump is in a separate file).
  • Tap protocol additive extension + forward-compat via #[serde(other)]crates/rutster-tap/src/protocol.rs — how slice-3 adds speech_started, speech_stopped, function_call, function_call_output, tools.update without breaking slice-2 brains: unknown variants deserialize to a catch-all arm instead of erroring (forward-compat by construction).
  • Side-channel mpsc for FOB-boundary dispatchcrates/rutster/src/session_map.rs — how drive_all_sessions drains a function-call mpsc side channel into the tap WS writer without blocking the 20 ms media loop. The pattern: hot loop polls try_recv; cold path spawns.
  • HTTP request builder for WS subprotocol handshakecrates/rutster-brain-realtime/src/openai_client.rs — why we hand-roll the WS upgrade Request instead of letting connect_async build it: to set Authorization + OpenAI-Beta headers on the WS handshake (tungstenite doesn't expose subprotocol/auth headers via the simple connect_async(url) entry point — see openai_headers / openai_realtime_url).

Slice 4½ — benchmark + simulation harness

  • Measurement discipline: the caller's clockcrates/rutster-sim/src/sim_audio_pipe.rs — the AudioPipe test-double that IS the caller. Both onset + receipt timestamps are captured inside the SimAudioPipe (the wall-clock the caller started speaking + the wall-clock the caller heard the reply). The harness cannot lie about latency because the only clock it uses is the caller's (spec §2.2 — the load-bearing design choice).
  • Post-hoc p50/p99 computation + dedup of noise capturescrates/rutster-sim/src/latency.rs — the LatencyProbe consumes Capture events + pairs CallerLoudOnset with the next BargeKillObserved (kill-time) and the next CallerHeardReply (mouth-to-ear). Captures without a prior onset are silently dropped (the SimAudioPipe captures BargeKillObserved unconditionally on empty ring; the probe is the dedup gate).
  • In-process concurrency sweep + the doctrine-drift detectorcrates/rutster-sim/src/concurrency.rs — the ConcurrencyRunner spawns N SimCalls at levels [1, 10, 50] (the spearhead-scale envelope) + aggregates per-call latencies. Per spec §2.4: 1 isolates the baseline, 10 is the warm working set, 50 is the saturation point.
  • Atomic accumulators on the hot path: compare_exchange_weakcrates/rutster-sim/src/tick_lag.rsTickLagStats keeps a per-tick max + overrun counts via AtomicU64. The CAS loop on max is the idiomatic pattern for "atomic max update" in Rust (a single load-then-CAS-on-fail loop, ordering::Relaxed because stat counters don't need cross-thread synchronization ordering). 3 atomic ops per tick (max CAS + conditional fetch_add + unconditional fetch_add) — no Mutex, lock-free hot path.
  • pub(crate) visibility for cross-module helpercrates/rutster-sim/src/latency.rspercentile_ms is pub(crate) (not pub nor private). The ConcurrencyRunner (sibling module) needs it to compute p50/p99 over the merged sample across N probes — merging kill_times() samples per-probe (not concatenating Capture vectors) avoids the interleaved-captures-corrupt-LatencyProbe-pairing problem (each probe has its own Option<Instant> pairing cursor; merging captures across probes would interleave their cursors).

How to read

  1. cargo doc --open — every module has a //! doc comment; the doc tree is the high-level map.
  2. Pick a concept above; open the named file. The first occurrence of each non-obvious pattern has a // comment explaining why.
  3. Cross-ref back to the spec sections cited inline (spec §3.8, ADR-0002, etc.) for the architecture-level rationale.