Skip to main content

sectorsync_core/
event.rs

1//! Cross-station event envelopes and bounded priority queues.
2
3use std::collections::VecDeque;
4
5use crate::ids::{EntityId, EventId, OwnerEpoch, StationId, Tick};
6
7/// Event priority class.
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub enum EventPriority {
10    /// Must be delivered or cause backpressure.
11    Critical,
12    /// Should be delivered with bounded retry/ack policy.
13    Important,
14    /// Can be dropped, merged, or downgraded under pressure.
15    BestEffort,
16}
17
18/// Core event kind understood by the runtime.
19#[derive(Clone, Debug, PartialEq, Eq)]
20pub enum EventKind {
21    /// User-defined event kind id.
22    Custom(u32),
23    /// Prepare a two-phase entity handoff.
24    HandoffPrepare {
25        /// Entity being handed off.
26        entity_id: EntityId,
27    },
28    /// Commit a two-phase entity handoff.
29    HandoffCommit {
30        /// Entity being handed off.
31        entity_id: EntityId,
32        /// New owner epoch.
33        owner_epoch: OwnerEpoch,
34    },
35}
36
37/// Event envelope routed between stations.
38#[derive(Clone, Debug, PartialEq, Eq)]
39pub struct StationEvent {
40    /// Idempotency key.
41    pub id: EventId,
42    /// Source station.
43    pub source: StationId,
44    /// Target station.
45    pub target: StationId,
46    /// Tick observed at source.
47    pub source_tick: Tick,
48    /// Tick at which target should apply the event.
49    pub target_tick: Tick,
50    /// Priority class.
51    pub priority: EventPriority,
52    /// Event payload kind.
53    pub kind: EventKind,
54}
55
56/// Bounded queue limits by priority.
57#[derive(Clone, Copy, Debug, PartialEq, Eq)]
58pub struct EventQueueLimits {
59    /// Critical queue capacity.
60    pub critical: usize,
61    /// Important queue capacity.
62    pub important: usize,
63    /// Best-effort queue capacity.
64    pub best_effort: usize,
65}
66
67impl Default for EventQueueLimits {
68    fn default() -> Self {
69        Self {
70            critical: 1024,
71            important: 4096,
72            best_effort: 8192,
73        }
74    }
75}
76
77/// Outcome of a queue push.
78#[derive(Clone, Copy, Debug, PartialEq, Eq)]
79pub enum PushOutcome {
80    /// Event was accepted without dropping another event.
81    Accepted,
82    /// A best-effort event was dropped to admit the new one.
83    DroppedOldestBestEffort,
84}
85
86/// Event queue error.
87#[derive(Clone, Copy, Debug, PartialEq, Eq)]
88pub enum EventQueueError {
89    /// Reliable queue is full and caller must apply backpressure.
90    QueueFull(EventPriority),
91}
92
93impl core::fmt::Display for EventQueueError {
94    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
95        match self {
96            Self::QueueFull(priority) => write!(f, "{priority:?} event queue is full"),
97        }
98    }
99}
100
101impl std::error::Error for EventQueueError {}
102
103/// Bounded priority queues for station events.
104#[derive(Clone, Debug)]
105pub struct EventQueues {
106    limits: EventQueueLimits,
107    critical: VecDeque<StationEvent>,
108    important: VecDeque<StationEvent>,
109    best_effort: VecDeque<StationEvent>,
110}
111
112impl EventQueues {
113    /// Creates empty event queues.
114    pub fn new(limits: EventQueueLimits) -> Self {
115        Self {
116            limits,
117            critical: VecDeque::new(),
118            important: VecDeque::new(),
119            best_effort: VecDeque::new(),
120        }
121    }
122
123    /// Pushes an event according to priority and queue semantics.
124    pub fn push(&mut self, event: StationEvent) -> Result<PushOutcome, EventQueueError> {
125        match event.priority {
126            EventPriority::Critical => {
127                if self.critical.len() >= self.limits.critical {
128                    Err(EventQueueError::QueueFull(EventPriority::Critical))
129                } else {
130                    self.critical.push_back(event);
131                    Ok(PushOutcome::Accepted)
132                }
133            }
134            EventPriority::Important => {
135                if self.important.len() >= self.limits.important {
136                    Err(EventQueueError::QueueFull(EventPriority::Important))
137                } else {
138                    self.important.push_back(event);
139                    Ok(PushOutcome::Accepted)
140                }
141            }
142            EventPriority::BestEffort => {
143                let outcome = if self.best_effort.len() >= self.limits.best_effort {
144                    self.best_effort.pop_front();
145                    PushOutcome::DroppedOldestBestEffort
146                } else {
147                    PushOutcome::Accepted
148                };
149                self.best_effort.push_back(event);
150                Ok(outcome)
151            }
152        }
153    }
154
155    /// Pops the next event, preferring higher priority.
156    pub fn pop_next(&mut self) -> Option<StationEvent> {
157        self.critical
158            .pop_front()
159            .or_else(|| self.important.pop_front())
160            .or_else(|| self.best_effort.pop_front())
161    }
162
163    /// Appends events ready at `current_tick` while retaining delayed events in place.
164    ///
165    /// Priority and FIFO order match repeated [`Self::pop_next`] calls. The
166    /// caller owns `out` and may retain its capacity across ticks.
167    pub fn drain_ready_into(&mut self, current_tick: Tick, out: &mut Vec<StationEvent>) -> usize {
168        let before = out.len();
169        drain_priority_ready(&mut self.critical, current_tick, out);
170        drain_priority_ready(&mut self.important, current_tick, out);
171        drain_priority_ready(&mut self.best_effort, current_tick, out);
172        out.len() - before
173    }
174
175    /// Returns total queued events.
176    pub fn len(&self) -> usize {
177        self.critical.len() + self.important.len() + self.best_effort.len()
178    }
179
180    /// Returns whether all queues are empty.
181    pub fn is_empty(&self) -> bool {
182        self.len() == 0
183    }
184
185    /// Returns slots retained by one event priority queue.
186    pub fn retained_capacity(&self, priority: EventPriority) -> usize {
187        match priority {
188            EventPriority::Critical => self.critical.capacity(),
189            EventPriority::Important => self.important.capacity(),
190            EventPriority::BestEffort => self.best_effort.capacity(),
191        }
192    }
193
194    /// Returns slots retained across all event priority queues.
195    pub fn total_retained_capacity(&self) -> usize {
196        self.critical
197            .capacity()
198            .saturating_add(self.important.capacity())
199            .saturating_add(self.best_effort.capacity())
200    }
201}
202
203fn drain_priority_ready(
204    queue: &mut VecDeque<StationEvent>,
205    current_tick: Tick,
206    out: &mut Vec<StationEvent>,
207) {
208    let queued = queue.len();
209    for _ in 0..queued {
210        let event = queue
211            .pop_front()
212            .expect("initial queue length bounds the drain loop");
213        if event.target_tick <= current_tick {
214            out.push(event);
215        } else {
216            queue.push_back(event);
217        }
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224    use crate::ids::EventId;
225
226    fn event(id: u64, priority: EventPriority, target_tick: u64) -> StationEvent {
227        StationEvent {
228            id: EventId::new(id),
229            source: StationId::new(1),
230            target: StationId::new(2),
231            source_tick: Tick::new(0),
232            target_tick: Tick::new(target_tick),
233            priority,
234            kind: EventKind::Custom(u32::try_from(id).expect("test id fits u32")),
235        }
236    }
237
238    #[test]
239    fn ready_drain_preserves_priority_fifo_and_delayed_order() {
240        let mut queues = EventQueues::new(EventQueueLimits {
241            critical: 8,
242            important: 8,
243            best_effort: 8,
244        });
245        for value in [
246            event(1, EventPriority::Critical, 3),
247            event(2, EventPriority::Critical, 1),
248            event(3, EventPriority::Important, 1),
249            event(4, EventPriority::Important, 4),
250            event(5, EventPriority::BestEffort, 1),
251            event(6, EventPriority::BestEffort, 5),
252        ] {
253            queues.push(value).expect("test queue has capacity");
254        }
255        let mut ready = Vec::with_capacity(8);
256
257        assert_eq!(queues.drain_ready_into(Tick::new(1), &mut ready), 3);
258        assert_eq!(
259            ready.iter().map(|event| event.id).collect::<Vec<_>>(),
260            [EventId::new(2), EventId::new(3), EventId::new(5)]
261        );
262        assert_eq!(queues.len(), 3);
263
264        ready.clear();
265        assert_eq!(queues.drain_ready_into(Tick::new(5), &mut ready), 3);
266        assert_eq!(
267            ready.iter().map(|event| event.id).collect::<Vec<_>>(),
268            [EventId::new(1), EventId::new(4), EventId::new(6)]
269        );
270        assert!(queues.is_empty());
271    }
272
273    #[test]
274    fn event_queues_allocate_lazily_and_retain_peak_capacity() {
275        let mut queues = EventQueues::new(EventQueueLimits::default());
276        assert_eq!(queues.total_retained_capacity(), 0);
277
278        for (offset, priority) in [
279            EventPriority::Critical,
280            EventPriority::Important,
281            EventPriority::BestEffort,
282        ]
283        .into_iter()
284        .enumerate()
285        {
286            for index in 0..8 {
287                queues
288                    .push(event(
289                        u64::try_from(offset * 8 + index).expect("test id fits u64"),
290                        priority,
291                        0,
292                    ))
293                    .expect("event burst should queue");
294            }
295            assert!(queues.retained_capacity(priority) >= 8);
296        }
297        let peak = queues.total_retained_capacity();
298        while queues.pop_next().is_some() {}
299        assert_eq!(queues.total_retained_capacity(), peak);
300    }
301}