Skip to main content

capsule_runtime/
lanes.rs

1//! Which queued task runs next.
2//!
3//! A capsule runs one task at a time. Tasks that arrive while one is running wait, and the
4//! order they wait in is decided here: by lane first, by arrival second. A lane is read off the
5//! task's own [`TaskOrigin`], so nothing declares a lane and nothing can be filed in one its
6//! origin does not name.
7//!
8//! A capsule with a single source of tasks sees every task land in the same lane and come back
9//! out in arrival order. Lanes decide something only where two sources are in flight at once.
10
11use std::collections::VecDeque;
12
13use crate::{a2a::IncomingTask, origin::TaskOrigin};
14
15/// Ordering classes the queue chooses between. Declared lowest to highest, so `Ord` is precedence.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
17pub enum TaskLane {
18    /// Work with nobody waiting on it: a timer, a webhook, a finished background shell command, the
19    /// runtime's own housekeeping.
20    Bg,
21    /// A message from another capsule, which has a task of its own blocked on the answer.
22    Peer,
23    /// A person is waiting.
24    User,
25}
26
27impl TaskLane {
28    /// The lane an origin names.
29    ///
30    /// Exhaustive by construction: a seventh [`TaskOrigin`] fails to compile here rather than
31    /// falling into a lane nobody chose for it.
32    pub fn for_origin(origin: TaskOrigin) -> Self {
33        match origin {
34            TaskOrigin::User => Self::User,
35            TaskOrigin::Peer => Self::Peer,
36            // Nothing is blocked on any of these. `System` is the runtime enqueuing work for
37            // itself — a retry, a sweep — which is background work by the same reading.
38            TaskOrigin::Schedule | TaskOrigin::Event | TaskOrigin::Completion => Self::Bg,
39            TaskOrigin::System => Self::Bg,
40        }
41    }
42
43    /// The lowercase spelling, as it appears on a `task_start` trace record.
44    pub fn as_str(&self) -> &'static str {
45        match self {
46            Self::User => "user",
47            Self::Peer => "peer",
48            Self::Bg => "bg",
49        }
50    }
51}
52
53/// The tasks a capsule has accepted but not yet started, one queue per lane.
54///
55/// Admission is not this type's job: a task reaches [`Self::push`] only after the registry has
56/// counted it against `queue_depth`, and it stays counted until `start_task` runs.
57pub(crate) struct LaneQueue {
58    user: VecDeque<IncomingTask>,
59    peer: VecDeque<IncomingTask>,
60    bg: VecDeque<IncomingTask>,
61}
62
63impl LaneQueue {
64    pub(crate) fn new() -> Self {
65        Self {
66            user: VecDeque::new(),
67            peer: VecDeque::new(),
68            bg: VecDeque::new(),
69        }
70    }
71
72    /// File a task in the lane its own origin names.
73    ///
74    /// Takes no lane argument, so no caller can misfile a task.
75    pub(crate) fn push(&mut self, task: IncomingTask) {
76        let lane = TaskLane::for_origin(task.provenance.origin());
77        self.lane_mut(lane).push_back(task);
78    }
79
80    /// Take the front of the highest non-empty lane, with the lane it came from.
81    ///
82    /// `active` is the lane of the task the capsule is running, if any. `None` is returned
83    /// whenever `active` is `Some` — before any lane is looked at, whatever lane is active and
84    /// whatever lanes hold tasks — and whenever every lane is empty. A refusal moves nothing: the
85    /// task that would have been chosen is still at the front of its lane afterwards.
86    ///
87    /// That refusal is the only thing standing between this queue and preemption. A call site
88    /// must pass the real active lane; passing `None` to force a selection while a task runs
89    /// grants exactly the preemption the design excludes.
90    pub(crate) fn next(&mut self, active: Option<TaskLane>) -> Option<(TaskLane, IncomingTask)> {
91        if active.is_some() {
92            return None;
93        }
94        for lane in [TaskLane::User, TaskLane::Peer, TaskLane::Bg] {
95            if let Some(task) = self.lane_mut(lane).pop_front() {
96                return Some((lane, task));
97            }
98        }
99        None
100    }
101
102    fn lane_mut(&mut self, lane: TaskLane) -> &mut VecDeque<IncomingTask> {
103        match lane {
104            TaskLane::User => &mut self.user,
105            TaskLane::Peer => &mut self.peer,
106            TaskLane::Bg => &mut self.bg,
107        }
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114    use crate::origin::TaskProvenance;
115
116    const ALL_LANES: [TaskLane; 3] = [TaskLane::User, TaskLane::Peer, TaskLane::Bg];
117
118    fn task(id: &str, origin: TaskOrigin) -> IncomingTask {
119        IncomingTask {
120            task_id: id.to_string(),
121            context_id: "ctx_001".to_string(),
122            message_id: format!("msg_{id}"),
123            message_text: id.to_string(),
124            traceparent: None,
125            provenance: TaskProvenance::derive(origin, None),
126            source: crate::a2a::SOURCE_A2A,
127            forget_session: false,
128            caller_member: None,
129            member_request_id: None,
130            caller: Some(crate::identity::TaskCaller::Operator),
131            reference_task_ids: Vec::new(),
132            part_kinds: Vec::new(),
133            images: Vec::new(),
134        }
135    }
136
137    /// An origin that puts a task in `lane`, for a test that cares about the lane and not about
138    /// which of the origins mapping to it was used.
139    fn origin_for(lane: TaskLane) -> TaskOrigin {
140        match lane {
141            TaskLane::User => TaskOrigin::User,
142            TaskLane::Peer => TaskOrigin::Peer,
143            TaskLane::Bg => TaskOrigin::Event,
144        }
145    }
146
147    fn drain(queue: &mut LaneQueue) -> Vec<(TaskLane, String)> {
148        let mut drained = Vec::new();
149        while let Some((lane, task)) = queue.next(None) {
150            drained.push((lane, task.task_id));
151        }
152        drained
153    }
154
155    #[test]
156    fn every_origin_names_a_lane() {
157        for (origin, expected) in [
158            (TaskOrigin::User, TaskLane::User),
159            (TaskOrigin::Peer, TaskLane::Peer),
160            (TaskOrigin::Schedule, TaskLane::Bg),
161            (TaskOrigin::Event, TaskLane::Bg),
162            (TaskOrigin::Completion, TaskLane::Bg),
163            (TaskOrigin::System, TaskLane::Bg),
164        ] {
165            assert_eq!(
166                TaskLane::for_origin(origin),
167                expected,
168                "{origin:?} belongs in {expected:?}"
169            );
170        }
171    }
172
173    #[test]
174    fn lane_order_is_precedence() {
175        assert!(TaskLane::User > TaskLane::Peer);
176        assert!(TaskLane::Peer > TaskLane::Bg);
177        for (lane, spelling) in [
178            (TaskLane::User, "user"),
179            (TaskLane::Peer, "peer"),
180            (TaskLane::Bg, "bg"),
181        ] {
182            assert_eq!(lane.as_str(), spelling);
183        }
184    }
185
186    /// Arrival order and lane order disagree on every adjacent pair, so nothing here comes out
187    /// right by accident of insertion.
188    #[test]
189    fn every_lane_drains_before_the_one_below_it() {
190        let mut queue = LaneQueue::new();
191        for (id, origin) in [
192            ("bg-1", TaskOrigin::Schedule),
193            ("user-1", TaskOrigin::User),
194            ("bg-2", TaskOrigin::Event),
195            ("peer-1", TaskOrigin::Peer),
196            ("user-2", TaskOrigin::User),
197            ("bg-3", TaskOrigin::Completion),
198            ("peer-2", TaskOrigin::Peer),
199        ] {
200            queue.push(task(id, origin));
201        }
202
203        assert_eq!(
204            drain(&mut queue),
205            vec![
206                (TaskLane::User, "user-1".to_string()),
207                (TaskLane::User, "user-2".to_string()),
208                (TaskLane::Peer, "peer-1".to_string()),
209                (TaskLane::Peer, "peer-2".to_string()),
210                (TaskLane::Bg, "bg-1".to_string()),
211                (TaskLane::Bg, "bg-2".to_string()),
212                (TaskLane::Bg, "bg-3".to_string()),
213            ]
214        );
215        assert!(
216            queue.next(None).is_none(),
217            "the queue is empty once drained"
218        );
219    }
220
221    /// The case the lanes exist for: a person's task arriving last still runs next.
222    #[test]
223    fn a_user_task_behind_six_completions_runs_first() {
224        let mut queue = LaneQueue::new();
225        for i in 1..=6 {
226            queue.push(task(&format!("done-{i}"), TaskOrigin::Completion));
227        }
228        queue.push(task("asked", TaskOrigin::User));
229
230        let drained = drain(&mut queue);
231        assert_eq!(drained[0], (TaskLane::User, "asked".to_string()));
232        assert_eq!(
233            drained[1..],
234            (1..=6)
235                .map(|i| (TaskLane::Bg, format!("done-{i}")))
236                .collect::<Vec<_>>()
237        );
238    }
239
240    #[test]
241    fn tasks_of_one_origin_drain_in_push_order() {
242        let mut queue = LaneQueue::new();
243        for id in ["first", "second", "third"] {
244            queue.push(task(id, TaskOrigin::Peer));
245        }
246        assert_eq!(
247            drain(&mut queue),
248            vec![
249                (TaskLane::Peer, "first".to_string()),
250                (TaskLane::Peer, "second".to_string()),
251                (TaskLane::Peer, "third".to_string()),
252            ]
253        );
254    }
255
256    /// No lane preempts any other, including its own, and a refusal costs the queued task
257    /// nothing.
258    #[test]
259    fn nothing_is_yielded_while_a_task_is_active() {
260        for active in ALL_LANES {
261            for queued in ALL_LANES {
262                let mut queue = LaneQueue::new();
263                queue.push(task("waiting", origin_for(queued)));
264                assert!(
265                    queue.next(Some(active)).is_none(),
266                    "a {queued:?} task must not preempt an active {active:?} task"
267                );
268                assert_eq!(
269                    queue.next(None).map(|(lane, task)| (lane, task.task_id)),
270                    Some((queued, "waiting".to_string())),
271                    "the refused {queued:?} task must still be queued afterwards"
272                );
273            }
274        }
275    }
276}