1use core::num::NonZeroU64;
16use serde::{Deserialize, Serialize};
17use std::collections::{BTreeMap, BTreeSet};
18
19use crate::abi::{EntityId, Principal, Tick, TypeCode};
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
23#[serde(transparent)]
24pub struct ScheduledActionId(pub NonZeroU64);
25
26impl ScheduledActionId {
27 #[inline]
29 pub const fn new(v: u64) -> Option<Self> {
30 match NonZeroU64::new(v) {
31 Some(n) => Some(Self(n)),
32 None => None,
33 }
34 }
35
36 #[inline]
38 pub const fn get(self) -> u64 {
39 self.0.get()
40 }
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
46pub(crate) struct SchedKey {
47 pub at: Tick,
48 pub seq: u64,
49 pub id: ScheduledActionId,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub(crate) struct ScheduledEntry {
54 pub id: ScheduledActionId,
55 pub at: Tick,
56 pub actor: Option<EntityId>,
57 pub principal: Principal,
58 pub action_type_code: TypeCode,
59 pub action_bytes: Vec<u8>,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
65pub(crate) struct Scheduler {
66 ready: BTreeMap<SchedKey, ScheduledEntry>,
67 by_id: BTreeMap<ScheduledActionId, SchedKey>,
68 by_actor: BTreeMap<EntityId, BTreeSet<ScheduledActionId>>,
69 next_seq: u64,
71 next_id: u64,
73}
74
75impl Scheduler {
76 pub(crate) fn new() -> Self {
77 Self {
78 ready: BTreeMap::new(),
79 by_id: BTreeMap::new(),
80 by_actor: BTreeMap::new(),
81 next_seq: 0,
82 next_id: 0,
83 }
84 }
85
86 pub(crate) fn schedule(
88 &mut self,
89 at: Tick,
90 actor: Option<EntityId>,
91 principal: Principal,
92 type_code: TypeCode,
93 bytes: Vec<u8>,
94 ) -> ScheduledActionId {
95 self.next_id = self.next_id.saturating_add(1);
99 let id = ScheduledActionId(
100 NonZeroU64::new(self.next_id).expect("next_id incremented before use; never zero"),
101 );
102
103 let seq = self.next_seq;
104 self.next_seq = self.next_seq.saturating_add(1);
105
106 let key = SchedKey { at, seq, id };
107 let entry = ScheduledEntry {
108 id,
109 at,
110 actor,
111 principal,
112 action_type_code: type_code,
113 action_bytes: bytes,
114 };
115
116 self.ready.insert(key, entry);
117 self.by_id.insert(id, key);
118 if let Some(actor_id) = actor {
119 self.by_actor.entry(actor_id).or_default().insert(id);
120 }
121
122 id
123 }
124
125 pub(crate) fn schedule_with_id(
129 &mut self,
130 id: ScheduledActionId,
131 at: Tick,
132 actor: Option<EntityId>,
133 principal: Principal,
134 type_code: TypeCode,
135 bytes: Vec<u8>,
136 ) {
137 if id.get() > self.next_id {
138 self.next_id = id.get();
139 }
140
141 let seq = self.next_seq;
142 self.next_seq = self.next_seq.saturating_add(1);
143
144 let key = SchedKey { at, seq, id };
145 let entry = ScheduledEntry {
146 id,
147 at,
148 actor,
149 principal,
150 action_type_code: type_code,
151 action_bytes: bytes,
152 };
153
154 self.ready.insert(key, entry);
155 self.by_id.insert(id, key);
156 if let Some(actor_id) = actor {
157 self.by_actor.entry(actor_id).or_default().insert(id);
158 }
159 }
160
161 pub(crate) fn cancel(&mut self, id: ScheduledActionId) -> Option<ScheduledEntry> {
166 let key = self.by_id.remove(&id)?;
167 let entry = self
168 .ready
169 .remove(&key)
170 .expect("ready/by_id consistency violated");
171 if let Some(actor_id) = entry.actor {
172 if let Some(set) = self.by_actor.get_mut(&actor_id) {
173 set.remove(&id);
174 if set.is_empty() {
175 self.by_actor.remove(&actor_id);
176 }
177 }
178 }
179 Some(entry)
180 }
181
182 #[cfg_attr(not(test), allow(dead_code))]
187 pub(crate) fn cancel_by_actor(&mut self, actor: EntityId) -> Vec<ScheduledEntry> {
188 let Some(ids) = self.by_actor.remove(&actor) else {
189 return Vec::new();
190 };
191 let mut cancelled = Vec::with_capacity(ids.len());
192 for id in ids {
193 if let Some(key) = self.by_id.remove(&id) {
194 if let Some(entry) = self.ready.remove(&key) {
195 cancelled.push(entry);
196 }
197 }
198 }
199 cancelled
200 }
201
202 pub(crate) fn pop_due(&mut self, now: Tick) -> Option<ScheduledEntry> {
205 let (&key, _) = self.ready.first_key_value()?;
206 if key.at > now {
207 return None;
208 }
209 let entry = self
210 .ready
211 .remove(&key)
212 .expect("first_key_value just returned this key");
213 self.by_id.remove(&entry.id);
214 if let Some(actor_id) = entry.actor {
215 if let Some(set) = self.by_actor.get_mut(&actor_id) {
216 set.remove(&entry.id);
217 if set.is_empty() {
218 self.by_actor.remove(&actor_id);
219 }
220 }
221 }
222 Some(entry)
223 }
224
225 pub(crate) fn validate(&self) -> Result<(), &'static str> {
230 if self.ready.len() != self.by_id.len() {
232 return Err("scheduler ready/by_id size mismatch");
233 }
234 for (key, entry) in &self.ready {
235 if entry.id != key.id {
236 return Err("scheduler entry id does not match its key");
237 }
238 match self.by_id.get(&entry.id) {
239 Some(k) if *k == *key => {}
240 _ => return Err("scheduler by_id does not map back to the ready key"),
241 }
242 }
243 for (actor, ids) in &self.by_actor {
245 if ids.is_empty() {
246 return Err("scheduler by_actor holds an empty actor set");
247 }
248 for id in ids {
249 let Some(key) = self.by_id.get(id) else {
250 return Err("scheduler by_actor references an unknown id");
251 };
252 match self.ready.get(key) {
253 Some(entry) if entry.actor == Some(*actor) => {}
254 _ => return Err("scheduler by_actor actor mismatch"),
255 }
256 }
257 }
258 for entry in self.ready.values() {
262 if let Some(actor) = entry.actor {
263 match self.by_actor.get(&actor) {
264 Some(set) if set.contains(&entry.id) => {}
265 _ => return Err("scheduler ready entry missing from its by_actor index"),
266 }
267 }
268 }
269 for key in self.ready.keys() {
271 if key.seq >= self.next_seq {
272 return Err("scheduler next_seq not monotonic over live keys");
273 }
274 if key.id.get() > self.next_id {
275 return Err("scheduler next_id not monotonic over live ids");
276 }
277 }
278 Ok(())
279 }
280
281 #[cfg_attr(not(test), allow(dead_code))]
284 #[inline]
285 pub(crate) fn len(&self) -> usize {
286 self.ready.len()
287 }
288
289 #[cfg_attr(not(test), allow(dead_code))]
290 #[inline]
291 pub(crate) fn is_empty(&self) -> bool {
292 self.ready.is_empty()
293 }
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299 use crate::abi::{EntityId, Tick, TypeCode};
300
301 fn p() -> Principal {
302 Principal::System
303 }
304 fn tc() -> TypeCode {
305 TypeCode(1)
306 }
307
308 #[test]
309 fn empty_state() {
310 let s = Scheduler::new();
311 assert_eq!(s.len(), 0);
312 assert!(s.is_empty());
313 }
314
315 #[test]
316 fn schedule_then_pop_due_single() {
317 let mut s = Scheduler::new();
318 let id = s.schedule(Tick(5), None, p(), tc(), vec![1, 2, 3]);
319 assert_eq!(s.len(), 1);
320 let entry = s.pop_due(Tick(5)).expect("entry due");
321 assert_eq!(entry.id, id);
322 assert_eq!(entry.at, Tick(5));
323 assert_eq!(entry.action_bytes, vec![1, 2, 3]);
324 assert!(s.is_empty());
325 }
326
327 #[test]
328 fn pop_due_before_time_returns_none() {
329 let mut s = Scheduler::new();
330 s.schedule(Tick(10), None, p(), tc(), vec![]);
331 assert!(s.pop_due(Tick(9)).is_none());
332 assert_eq!(s.len(), 1);
333 }
334
335 #[test]
336 fn pop_due_at_exact_tick_pops() {
337 let mut s = Scheduler::new();
338 let id = s.schedule(Tick(5), None, p(), tc(), vec![]);
339 assert_eq!(s.pop_due(Tick(5)).unwrap().id, id);
340 }
341
342 #[test]
343 fn pop_due_ordering_by_tick() {
344 let mut s = Scheduler::new();
345 let id_late = s.schedule(Tick(20), None, p(), tc(), vec![]);
346 let id_early = s.schedule(Tick(5), None, p(), tc(), vec![]);
347 let id_mid = s.schedule(Tick(10), None, p(), tc(), vec![]);
348 assert_eq!(s.pop_due(Tick(100)).unwrap().id, id_early);
349 assert_eq!(s.pop_due(Tick(100)).unwrap().id, id_mid);
350 assert_eq!(s.pop_due(Tick(100)).unwrap().id, id_late);
351 }
352
353 #[test]
354 fn pop_due_tiebreak_by_seq() {
355 let mut s = Scheduler::new();
356 let id1 = s.schedule(Tick(5), None, p(), tc(), vec![1]);
357 let id2 = s.schedule(Tick(5), None, p(), tc(), vec![2]);
358 let id3 = s.schedule(Tick(5), None, p(), tc(), vec![3]);
359 assert_eq!(s.pop_due(Tick(5)).unwrap().id, id1);
360 assert_eq!(s.pop_due(Tick(5)).unwrap().id, id2);
361 assert_eq!(s.pop_due(Tick(5)).unwrap().id, id3);
362 }
363
364 #[test]
365 fn cancel_removes_entry() {
366 let mut s = Scheduler::new();
367 let id = s.schedule(Tick(5), None, p(), tc(), vec![]);
368 let cancelled = s.cancel(id).expect("found");
369 assert_eq!(cancelled.id, id);
370 assert!(s.is_empty());
371 assert!(s.pop_due(Tick(100)).is_none());
372 }
373
374 #[test]
375 fn cancel_unknown_returns_none() {
376 let mut s = Scheduler::new();
377 let bogus = ScheduledActionId::new(999).unwrap();
378 assert!(s.cancel(bogus).is_none());
379 }
380
381 #[test]
382 fn cancel_by_actor_removes_all() {
383 let mut s = Scheduler::new();
384 let actor = EntityId::new(1).unwrap();
385 let other = EntityId::new(2).unwrap();
386 let _ = s.schedule(Tick(5), Some(actor), p(), tc(), vec![]);
387 let _ = s.schedule(Tick(10), Some(actor), p(), tc(), vec![]);
388 let id_other = s.schedule(Tick(7), Some(other), p(), tc(), vec![]);
389 assert_eq!(s.len(), 3);
390 let cancelled = s.cancel_by_actor(actor);
391 assert_eq!(cancelled.len(), 2);
392 assert_eq!(s.len(), 1);
393 assert_eq!(s.pop_due(Tick(100)).unwrap().id, id_other);
394 }
395
396 #[test]
397 fn cancel_by_actor_unknown_returns_empty() {
398 let mut s = Scheduler::new();
399 let actor = EntityId::new(99).unwrap();
400 assert!(s.cancel_by_actor(actor).is_empty());
401 }
402
403 #[test]
404 fn schedule_id_monotonic() {
405 let mut s = Scheduler::new();
406 let id1 = s.schedule(Tick(0), None, p(), tc(), vec![]);
407 let id2 = s.schedule(Tick(0), None, p(), tc(), vec![]);
408 let id3 = s.schedule(Tick(0), None, p(), tc(), vec![]);
409 assert!(id1 < id2);
410 assert!(id2 < id3);
411 assert_eq!(id1.get(), 1);
412 assert_eq!(id3.get(), 3);
413 }
414
415 #[test]
416 fn no_tombstones() {
417 let mut s = Scheduler::new();
419 let id1 = s.schedule(Tick(5), None, p(), tc(), vec![]);
420 let _id2 = s.schedule(Tick(5), None, p(), tc(), vec![]);
421 assert_eq!(s.len(), 2);
422 s.cancel(id1);
423 assert_eq!(s.len(), 1);
424 }
425
426 #[test]
427 fn determinism_same_sequence() {
428 fn run() -> Vec<u64> {
429 let mut s = Scheduler::new();
430 s.schedule(Tick(3), None, p(), tc(), vec![]);
431 s.schedule(Tick(1), None, p(), tc(), vec![]);
432 s.schedule(Tick(2), None, p(), tc(), vec![]);
433 s.schedule(Tick(1), None, p(), tc(), vec![]);
434 let mut out = Vec::new();
435 while let Some(e) = s.pop_due(Tick(100)) {
436 out.push(e.id.get());
437 }
438 out
439 }
440 assert_eq!(run(), run());
441 }
442
443 #[test]
444 fn validate_accepts_consistent_scheduler() {
445 let mut s = Scheduler::new();
446 s.schedule(Tick(5), Some(EntityId::new(1).unwrap()), p(), tc(), vec![1]);
447 s.schedule(Tick(3), None, p(), tc(), vec![2]);
448 s.schedule(Tick(5), Some(EntityId::new(1).unwrap()), p(), tc(), vec![3]);
449 assert!(s.validate().is_ok());
450 }
451
452 #[test]
453 fn validate_rejects_broken_ready_by_id_bijection() {
454 let mut s = Scheduler::new();
455 s.schedule(Tick(5), None, p(), tc(), vec![1]);
456 s.by_id.clear(); assert!(s.validate().is_err());
458 }
459
460 #[test]
461 fn validate_rejects_missing_by_actor_index() {
462 let mut s = Scheduler::new();
463 s.schedule(Tick(5), Some(EntityId::new(1).unwrap()), p(), tc(), vec![1]);
464 s.by_actor.clear(); assert!(s.validate().is_err());
466 }
467}