Channels & Overflow
Channel Model
Section titled “Channel Model”The push event system delivers events to subscribers through bounded MPSC (multi-producer, single-consumer) channels. Each call to EventBus::subscribe() creates a dedicated channel with a single receiver. The event bus is the sole producer; the subscribing crate is the sole consumer.
This design gives each subscriber isolation. A slow consumer does not block other subscribers, and the event bus can apply per-subscriber overflow policies without global coordination.
EventBus::publish() | +---> subscriber A channel (capacity: 64) +---> subscriber B channel (capacity: 16) +---> subscriber C channel (capacity: 256)Subscription API
Section titled “Subscription API”Subscribers declare their requirements through the EventSubscription struct. The event bus returns a SubscriptionHandle containing a channel receiver and metadata for journal replay.
pub struct EventSubscription { pub id: SubscriptionId, /// Which event domains this subscription covers. /// Empty vector = all domains (wildcard). pub domains: Vec<EventDomain>, /// Optional filter: only events from specific sources. pub source_filter: Option<Vec<String>>, /// Optional filter: only specific event types within matched domains. pub type_filter: Option<Vec<String>>, /// Channel capacity before overflow strategy applies. pub channel_capacity: usize, /// What happens when the channel is full. pub overflow: OverflowStrategy, /// Whether this subscription survives PCP restart via event journal. pub durable: bool,}The subscribe() call on the event bus returns a handle:
pub struct SubscriptionHandle { /// Unique subscription ID. pub id: SubscriptionId, /// Channel receiver. Consumer reads events from this. pub receiver: mpsc::Receiver<Arc<PcpPushEvent>>, /// Last sequence number acknowledged by this consumer. pub last_sequence: u64,}Default Capacities
Section titled “Default Capacities”Each subscriber type has a tuned default capacity. These values reflect typical event rates and consumer processing speed.
| Subscriber Type | Default Capacity | Overflow Strategy |
|---|---|---|
| Context crate | 64 | DropOldest |
| Voice crate | 16 | DropOldest |
| Audit logger | 256 | DropNewest |
| Custom / unknown | 32 | DropOldest |
| Debug / diagnostic | 512 | DropNewest |
The context crate receives the most event traffic (it subscribes to AppLifecycle, CapabilityChange, and SystemState), so its channel is larger than the voice crate’s. The audit logger keeps a generous buffer with DropNewest because it prioritizes history over recency. Debug channels get the largest buffer since diagnostic consumers tend to be slow but must not lose data.
Overflow Strategies
Section titled “Overflow Strategies”When a subscriber’s channel reaches capacity, the declared overflow strategy determines what happens to the next event. The strategy is set at subscription time and cannot change during the subscription’s lifetime.
DropOldest
Section titled “DropOldest”Removes the oldest event from the channel and inserts the new one. The consumer always sees the most recent state.
This is the default strategy and the right choice for most consumers. If a crate falls behind during a burst of surface geometry changes, the intermediate positions do not matter. The latest position is the only useful state.
The trade-off is that intermediate states are lost. A consumer that needs to reconstruct a sequence of changes (for example, an animation recorder) should use a larger channel or DropNewest instead.
DropNewest
Section titled “DropNewest”Discards the incoming event. The consumer keeps all older events but misses the most recent one.
This suits consumers that need an unbroken historical sequence, such as the audit logger. If the logger falls behind, dropping new events means the existing log stays complete. The gap appears at the tail, where it can be detected and addressed by replaying from the event journal.
Waits until the channel has space. The event bus holds the event and does not proceed until the consumer drains a slot.
This is dangerous. If the consumer is blocked on something else (a lock, I/O, a downstream call), the Block strategy can propagate back into the event bus and stall other subscribers. In the worst case, it blocks the compositor’s event loop.
Use Block only for consumers that must not lose any event and can guarantee timely processing. Audit and compliance pipelines are the intended audience. Even then, pair it with a generous channel capacity to reduce the chance of blocking.
DropAndLog
Section titled “DropAndLog”Discards the incoming event and writes a warning to the log. No delivery attempt is made.
This suits telemetry and metrics consumers where occasional gaps are acceptable but silent loss is not. The log entry provides an audit trail of when data was dropped, which helps tune channel capacities.
Overflow Diagnostics
Section titled “Overflow Diagnostics”The event bus tracks per-subscription overflow statistics and exposes them through SubscriptionInfo:
pub struct SubscriptionInfo { pub id: SubscriptionId, pub subscriber_name: String, pub domains: Vec<EventDomain>, pub source_filter: Option<Vec<String>>, pub channel_capacity: usize, pub current_depth: usize, pub overflow: OverflowStrategy, pub created_at: chrono::DateTime<chrono::Utc>, pub events_delivered: u64, pub events_dropped: u64,}When a subscription’s drop rate exceeds 10% (events dropped divided by events delivered), the event bus emits a diagnostic warning:
WARN event_bus: subscription "context-crate" drop rate 12.3% (147/1190 events dropped) hint: increase channel_capacity or check consumer latencyThis threshold is not configurable. It is a signal that something in the pipeline needs attention, whether that is a slow consumer, an undersized channel, or an event storm from upstream.
Delivery Guarantees
Section titled “Delivery Guarantees”At-Least-Once
Section titled “At-Least-Once”Every event published to the bus reaches all matching subscribers at least once. The bounded channel holds events until consumed or overflowed. The event journal (see Event Journal) persists events to disk so that durable subscribers can replay missed events after a crash.
Consumers must tolerate duplicate delivery. If the consumer processes an event but PCP crashes before the consumer acknowledges it, the journal replays that event on restart. This is the standard at-least-once trade-off, and it means all event handlers must be idempotent.
Ordering Within Source
Section titled “Ordering Within Source”Events from the same source (the same adapter) arrive in emission order. Per-source sequence numbers are monotonically increasing u64 values, and the event bus publishes sequentially per source. The coalescer preserves this order as well; coalesced events replace earlier events but never reorder them.
Cross-source ordering is not guaranteed. A compositor event and a system bus event may arrive in any order relative to each other. Consumers that need cross-source ordering must buffer and sort by timestamp themselves.
Subscription Lifecycle
Section titled “Subscription Lifecycle”A subscription moves through four states:
subscribe() | v ACTIVE <--- events flowing via channel | | unsubscribe() or subscriber crash v SUSPENDED -- channel dropped, journal retains events | | re-subscribe() within retention window v REPLAYING -- missed events replayed from journal | | replay complete (caught up to current sequence) v ACTIVE -- back to live deliveryIf a subscription stays in the SUSPENDED state longer than the retention window (default one hour), it transitions to EXPIRED. Journal entries for that subscription are purged, and a future re-subscribe starts fresh rather than replaying.
| State | Channel | Journal |
|---|---|---|
ACTIVE |
Open, receiving events | Events appended |
SUSPENDED |
Dropped | Retaining events |
REPLAYING |
Replaying from journal | Draining retained events |
EXPIRED |
Gone | Entries purged |