Skip to main content

deepstrike_core/scheduler/
wait_index.rs

1//! `WaitIndex` is the derived reverse index that turns "an event arrived" into "which tasks wake
2//! up" without scanning every task. It is rebuilt from durable task wait sets after restore.
3
4use std::collections::{BTreeMap, HashMap};
5
6use super::tcb::{
7    ApprovalId, ChannelId, LogicalDeadline, ResourceKey, SignalFilter, SubscriptionId, TaskId,
8    WaitCondition, WaitSet,
9};
10use crate::runtime::kernel::wire::EffectId;
11
12/// One indexable event key. A single `WaitCondition` can expand into more than one key —
13/// `Children` fans out into one `Child` key per child id, since any of them completing is a fact
14/// the parent's wait needs to observe.
15#[derive(Debug, Clone, PartialEq, Eq, Hash)]
16pub enum WaitKey {
17    Effect(EffectId),
18    Child(TaskId),
19    Approval(ApprovalId),
20    Signal(SignalFilter),
21    Timer(LogicalDeadline),
22    Channel(ChannelId),
23    Resource(ResourceKey),
24    External(SubscriptionId),
25}
26
27impl WaitKey {
28    pub(crate) fn keys_for(condition: &WaitCondition) -> Vec<WaitKey> {
29        match condition {
30            WaitCondition::Effect(id) => vec![WaitKey::Effect(id.clone())],
31            WaitCondition::Child(id) => vec![WaitKey::Child(id.clone())],
32            WaitCondition::Children(ids) => ids.iter().cloned().map(WaitKey::Child).collect(),
33            WaitCondition::Approval(id) => vec![WaitKey::Approval(id.clone())],
34            WaitCondition::Signal(filter) => vec![WaitKey::Signal(filter.clone())],
35            WaitCondition::Timer(deadline) => vec![WaitKey::Timer(*deadline)],
36            WaitCondition::Channel(id) => vec![WaitKey::Channel(id.clone())],
37            WaitCondition::Resource(key) => vec![WaitKey::Resource(key.clone())],
38            WaitCondition::External(id) => vec![WaitKey::External(id.clone())],
39        }
40    }
41
42    pub(crate) fn matches(&self, condition: &WaitCondition) -> bool {
43        Self::keys_for(condition).contains(self)
44    }
45}
46
47/// One `HashMap` keyed by [`WaitKey`] rather than one map per condition kind (the spec's §3
48/// sketch shows several typed buckets): a single generic index is simpler to keep correct and
49/// still answers "who is waiting on this key" in O(1) average, which is the actual requirement.
50#[derive(Debug, Clone, Default)]
51pub struct WaitIndex {
52    tasks_by_key: HashMap<WaitKey, Vec<TaskId>>,
53    /// spc_003-05: deadline (ms) → waiting task ids, kept in lockstep with the `Timer` entries in
54    /// `tasks_by_key`. `BTreeMap` gives cheap "everything due by `now_ms`" range queries without
55    /// reaching for a heavier structure than correctness needs.
56    timers: BTreeMap<u64, Vec<TaskId>>,
57}
58
59impl WaitIndex {
60    pub fn new() -> Self {
61        Self::default()
62    }
63
64    pub fn insert(&mut self, task_id: TaskId, condition: &WaitCondition) {
65        for key in WaitKey::keys_for(condition) {
66            if let WaitKey::Timer(LogicalDeadline(ms)) = key {
67                self.timers.entry(ms).or_default().push(task_id.clone());
68            }
69            let bucket = self.tasks_by_key.entry(key).or_default();
70            if !bucket.contains(&task_id) {
71                bucket.push(task_id.clone());
72            }
73        }
74    }
75
76    pub fn remove(&mut self, task_id: &TaskId, condition: &WaitCondition) {
77        for key in WaitKey::keys_for(condition) {
78            if let WaitKey::Timer(LogicalDeadline(ms)) = key
79                && let Some(bucket) = self.timers.get_mut(&ms)
80            {
81                bucket.retain(|id| id != task_id);
82                if bucket.is_empty() {
83                    self.timers.remove(&ms);
84                }
85            }
86            if let Some(bucket) = self.tasks_by_key.get_mut(&key) {
87                bucket.retain(|id| id != task_id);
88                if bucket.is_empty() {
89                    self.tasks_by_key.remove(&key);
90                }
91            }
92        }
93    }
94
95    pub fn lookup(&self, key: &WaitKey) -> &[TaskId] {
96        self.tasks_by_key
97            .get(key)
98            .map(|ids| ids.as_slice())
99            .unwrap_or(&[])
100    }
101
102    /// spc_003-06 / spec §3: "Effect E completed → `WaitIndex[E]` → wake task" — the general wake
103    /// primitive every event-arrival path uses. Removes and returns every task waiting on exactly
104    /// `key`. Idempotent by construction: waking an already-empty (or never-registered) key finds
105    /// nothing and returns `[]`, which is what makes a redelivered/duplicate completion event safe
106    /// — a second wake for the same key is a harmless no-op, not a second transition.
107    pub fn wake(&mut self, key: &WaitKey) -> Vec<TaskId> {
108        let woken = self.tasks_by_key.remove(key).unwrap_or_default();
109        if let WaitKey::Timer(LogicalDeadline(ms)) = key {
110            self.timers.remove(ms);
111        }
112        woken
113    }
114
115    /// spc_003 debt closure / spec §4: register `task_id` against every condition in `wait_set`
116    /// (via the existing [`Self::insert`], so each condition gets the same O(1)-average key
117    /// indexing every other wait does) and track the set itself so [`Self::notify`] can evaluate
118    /// `WaitMode::Any`/`WaitMode::All` satisfaction as individual conditions fire.
119    pub fn register_wait_set(&mut self, task_id: TaskId, wait_set: WaitSet) {
120        for condition in &wait_set.conditions {
121            self.insert(task_id.clone(), condition);
122        }
123    }
124
125    /// spc_003 debt closure: notify every task registered under `key` (via
126    /// [`Self::register_wait_set`]) that this condition fired, and return the ones whose whole
127    /// `WaitSet` is now satisfied (`Any` ⇒ this condition alone; `All` ⇒ every condition fired).
128    /// A task not yet fully satisfied stays registered under its remaining keys — this is the one
129    /// difference from [`Self::wake`], which always unconditionally removes on any hit. A task
130    /// with no tracked `WaitSet` (i.e. one only ever registered through [`Self::insert`] directly)
131    /// is not touched here — call [`Self::wake`] for that path, as before.
132    pub fn notify(&mut self, key: &WaitKey) -> Vec<TaskId> {
133        self.lookup(key).to_vec()
134    }
135
136    /// spc_003-05: remove and return every task waiting on a `Timer` whose deadline has passed
137    /// (`deadline <= now_ms`), matching the `now_ms >= deadline` expiry predicate used elsewhere
138    /// in this crate (`signals/queue.rs::escalate_deadlines`). Built on [`Self::wake`] — expiry is
139    /// just "wake every due `Timer` key," nothing bespoke.
140    pub fn expire_timers(&mut self, now_ms: u64) -> Vec<TaskId> {
141        let due_deadlines: Vec<u64> = self.timers.range(..=now_ms).map(|(ms, _)| *ms).collect();
142        due_deadlines
143            .into_iter()
144            .flat_map(|ms| self.wake(&WaitKey::Timer(LogicalDeadline(ms))))
145            .collect()
146    }
147
148    /// Timer keys due under the journal-owned logical clock. Satisfaction/lifecycle mutation is
149    /// deliberately left to `TaskTable::notify`; this index only answers which keys are due.
150    pub(crate) fn due_timer_keys(&self, now_ms: u64) -> Vec<WaitKey> {
151        self.timers
152            .range(..=now_ms)
153            .map(|(ms, _)| WaitKey::Timer(LogicalDeadline(*ms)))
154            .collect()
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161    use crate::scheduler::tcb::WaitCondition;
162
163    #[test]
164    fn insert_then_lookup_hits() {
165        let mut index = WaitIndex::new();
166        let effect = EffectId::new("e1").unwrap();
167        index.insert(
168            TaskId::from("task-1"),
169            &WaitCondition::Effect(effect.clone()),
170        );
171
172        assert_eq!(
173            index.lookup(&WaitKey::Effect(effect)),
174            &[TaskId::from("task-1")]
175        );
176    }
177
178    #[test]
179    fn remove_then_lookup_is_empty() {
180        let mut index = WaitIndex::new();
181        let effect = EffectId::new("e1").unwrap();
182        let condition = WaitCondition::Effect(effect.clone());
183        index.insert(TaskId::from("task-1"), &condition);
184
185        index.remove(&TaskId::from("task-1"), &condition);
186
187        assert!(index.lookup(&WaitKey::Effect(effect)).is_empty());
188    }
189
190    #[test]
191    fn two_tasks_waiting_on_the_same_effect_both_come_back() {
192        let mut index = WaitIndex::new();
193        let effect = EffectId::new("e1").unwrap();
194        let condition = WaitCondition::Effect(effect.clone());
195        index.insert(TaskId::from("task-1"), &condition);
196        index.insert(TaskId::from("task-2"), &condition);
197
198        let hits = index.lookup(&WaitKey::Effect(effect));
199        assert_eq!(hits.len(), 2);
200        assert!(hits.contains(&TaskId::from("task-1")));
201        assert!(hits.contains(&TaskId::from("task-2")));
202    }
203
204    use crate::scheduler::tcb::{WaitMode, WaitSet};
205
206    #[test]
207    fn wait_set_registration_indexes_every_condition_without_owning_satisfaction() {
208        let mut index = WaitIndex::new();
209        let e1 = EffectId::new("e1").unwrap();
210        let e2 = EffectId::new("e2").unwrap();
211        let wait_set = WaitSet {
212            mode: WaitMode::Any,
213            conditions: vec![
214                WaitCondition::Effect(e1.clone()),
215                WaitCondition::Effect(e2.clone()),
216            ],
217        };
218        index.register_wait_set(TaskId::from("task-1"), wait_set);
219
220        assert_eq!(
221            index.notify(&WaitKey::Effect(e1)),
222            vec![TaskId::from("task-1")]
223        );
224        assert_eq!(
225            index.lookup(&WaitKey::Effect(e2)),
226            &[TaskId::from("task-1")],
227            "the reverse index reports candidates; the TCB owns satisfaction and cleanup"
228        );
229    }
230
231    #[test]
232    fn notify_is_a_non_mutating_reverse_lookup_for_all_mode_too() {
233        let mut index = WaitIndex::new();
234        let e1 = EffectId::new("e1").unwrap();
235        let e2 = EffectId::new("e2").unwrap();
236        let wait_set = WaitSet {
237            mode: WaitMode::All,
238            conditions: vec![
239                WaitCondition::Effect(e1.clone()),
240                WaitCondition::Effect(e2.clone()),
241            ],
242        };
243        index.register_wait_set(TaskId::from("task-1"), wait_set);
244
245        assert_eq!(
246            index.notify(&WaitKey::Effect(e1.clone())),
247            &[TaskId::from("task-1")]
248        );
249        assert_eq!(
250            index.notify(&WaitKey::Effect(e2)),
251            vec![TaskId::from("task-1")]
252        );
253        assert_eq!(
254            index.notify(&WaitKey::Effect(e1)),
255            vec![TaskId::from("task-1")],
256            "dedupe is durable TCB state, not reverse-index state"
257        );
258    }
259
260    #[test]
261    fn heterogeneous_wait_set_registers_child_and_timer_keys() {
262        // Matches spc_003 §2's own usage example: `wait_any(child_result, user_reply, deadline)`.
263        let mut index = WaitIndex::new();
264        let wait_set = WaitSet {
265            mode: WaitMode::Any,
266            conditions: vec![
267                WaitCondition::Child(TaskId::from("child-1")),
268                WaitCondition::Timer(LogicalDeadline(1_000)),
269            ],
270        };
271        index.register_wait_set(TaskId::from("task-1"), wait_set);
272
273        assert_eq!(
274            index.notify(&WaitKey::Child(TaskId::from("child-1"))),
275            vec![TaskId::from("task-1")]
276        );
277        assert_eq!(
278            index.lookup(&WaitKey::Timer(LogicalDeadline(1_000))),
279            &[TaskId::from("task-1")]
280        );
281    }
282}