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