Skip to main content

actionqueue_engine/selection/
default_selector.rs

1//! Default priority-then-FIFO selector for ready runs.
2//!
3//! This module implements the default run selection policy for v0.1:
4//! - Runs are selected by priority (higher values considered higher priority)
5//! - Runs with the same priority are selected in FIFO order (earlier created_at first)
6//! - Runs with identical priority and created_at are selected by RunId for full tie-breaking
7
8use actionqueue_core::run::run_instance::RunInstance;
9
10use crate::index::ready::ReadyIndex;
11
12/// Result of running the default selector.
13///
14/// This structure contains the runs that should be selected (leased) from the
15/// ready index, in the order they should be selected.
16#[derive(Debug, Clone, PartialEq, Eq)]
17#[must_use]
18pub struct SelectionResult {
19    /// Runs to be selected (leased) in order.
20    selected: Vec<RunInstance>,
21    /// Runs remaining in the ready index after selection.
22    remaining: Vec<RunInstance>,
23}
24
25impl SelectionResult {
26    /// Returns the runs selected for leasing, in order.
27    pub fn selected(&self) -> &[RunInstance] {
28        &self.selected
29    }
30
31    /// Consumes self and returns the selected runs.
32    pub fn into_selected(self) -> Vec<RunInstance> {
33        self.selected
34    }
35
36    /// Returns the runs remaining in the ready index after selection.
37    pub fn remaining(&self) -> &[RunInstance] {
38        &self.remaining
39    }
40}
41
42/// Selector-ready input that pairs a run with a pre-resolved snapshot priority.
43///
44/// The scheduler should populate this structure before selection so sorting never
45/// depends on mutable metadata lookups during comparison.
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct ReadyRunSelectionInput {
48    /// Ready run candidate.
49    run: RunInstance,
50    /// Snapshot priority to use for ordering in this selection pass.
51    priority_snapshot: i32,
52}
53
54impl ReadyRunSelectionInput {
55    /// Returns a reference to the ready run candidate.
56    pub fn run(&self) -> &RunInstance {
57        &self.run
58    }
59
60    /// Returns the snapshot priority.
61    pub fn priority_snapshot(&self) -> i32 {
62        self.priority_snapshot
63    }
64}
65
66impl ReadyRunSelectionInput {
67    /// Creates selector input from a ready run and an explicit snapshot priority.
68    pub fn new(run: RunInstance, priority_snapshot: i32) -> Self {
69        Self { run, priority_snapshot }
70    }
71
72    /// Creates selector input from a ready run using the run's snapshot priority.
73    pub fn from_ready_run(run: RunInstance) -> Self {
74        let priority_snapshot = run.effective_priority();
75        Self::new(run, priority_snapshot)
76    }
77}
78
79/// Builds selector-ready inputs from a ready index.
80///
81/// This captures each run's priority snapshot at input construction time so
82/// selection can be performed without metadata lookups.
83pub fn ready_inputs_from_index(ready: &ReadyIndex) -> Vec<ReadyRunSelectionInput> {
84    ready.runs().iter().cloned().map(ReadyRunSelectionInput::from_ready_run).collect()
85}
86
87/// Default selector that picks runs by priority (descending), FIFO (ascending created_at),
88/// and finally by RunId for full ties.
89///
90/// This function takes selector-ready inputs and returns a selection result
91/// containing runs ordered by priority (higher first), then by creation time
92/// (earlier first), and finally by RunId (deterministic ordering) for runs
93/// with identical priority and created_at values.
94///
95/// # Arguments
96///
97/// * `ready_runs` - Selector inputs containing run and pre-resolved priority
98///
99/// # Returns
100///
101/// A SelectionResult containing:
102/// - `selected`: All ready runs ordered by priority, then FIFO, then RunId
103/// - `remaining`: Always empty in this default selector, because all ready
104///   runs are selected. Future selectors (e.g., N-of-M selection, capacity-
105///   aware selection) may return a non-empty `remaining` set to indicate
106///   runs that were eligible but not selected in this pass.
107///
108/// # Design Note
109///
110/// This is the default selector for v0.1. The selection policy may be made
111/// configurable in future versions (Phase 4+).
112///
113/// # Determinism
114///
115/// The selector is fully deterministic. When two runs have identical priority
116/// and created_at values (a "full tie"), they are ordered by RunId using
117/// lexicographic comparison of the underlying UUID bytes. This guarantees
118/// stable ordering regardless of input order.
119pub fn select_ready_runs(ready_runs: &[ReadyRunSelectionInput]) -> SelectionResult {
120    let mut ready_runs = ready_runs.to_vec();
121
122    // Sort by priority (descending), then by created_at (ascending) for FIFO,
123    // finally by RunId for deterministic ordering of full ties.
124    ready_runs.sort_by(|a, b| {
125        // Compare priority (higher priority first)
126        b.priority_snapshot
127            .cmp(&a.priority_snapshot)
128            // Compare created_at (earlier first for FIFO)
129            .then_with(|| a.run.created_at().cmp(&b.run.created_at()))
130            // Final tie-breaker: RunId (deterministic UUID byte ordering)
131            .then_with(|| a.run.id().cmp(&b.run.id()))
132    });
133
134    let selected = ready_runs.into_iter().map(|ready_run| ready_run.run).collect();
135
136    SelectionResult { selected, remaining: Vec::new() }
137}
138
139#[cfg(test)]
140mod tests {
141    use actionqueue_core::run::run_instance::RunInstance;
142
143    use super::*;
144
145    #[test]
146    fn selects_by_snapshot_priority_descending_then_fifo_for_ties() {
147        let now = 1000;
148        let input_low = ReadyRunSelectionInput::new(
149            RunInstance::new_ready(actionqueue_core::ids::TaskId::new(), now - 10, now + 30, 1)
150                .expect("valid ready run"),
151            1,
152        );
153        let input_high_later = ReadyRunSelectionInput::new(
154            RunInstance::new_ready(actionqueue_core::ids::TaskId::new(), now - 10, now + 20, 5)
155                .expect("valid ready run"),
156            5,
157        );
158        let input_high_earlier = ReadyRunSelectionInput::new(
159            RunInstance::new_ready(actionqueue_core::ids::TaskId::new(), now - 10, now + 10, 5)
160                .expect("valid ready run"),
161            5,
162        );
163
164        // Input order intentionally shuffled.
165        let inputs = vec![input_low, input_high_later, input_high_earlier];
166
167        let result = select_ready_runs(&inputs);
168
169        let ordered_created_at: Vec<u64> =
170            result.selected().iter().map(|run| run.created_at()).collect();
171        assert_eq!(ordered_created_at, vec![now + 10, now + 20, now + 30]);
172    }
173
174    #[test]
175    fn empty_input_returns_empty_selection() {
176        let result = select_ready_runs(&[]);
177
178        assert!(result.selected().is_empty());
179        assert!(result.remaining().is_empty());
180    }
181
182    #[test]
183    fn ready_inputs_from_index_captures_run_priority_snapshot() {
184        let run_a = RunInstance::new_ready(actionqueue_core::ids::TaskId::new(), 100, 200, 11)
185            .expect("valid ready run");
186        let run_b = RunInstance::new_ready(actionqueue_core::ids::TaskId::new(), 100, 300, -2)
187            .expect("valid ready run");
188        let ready = ReadyIndex::from_runs(vec![run_a.clone(), run_b.clone()]);
189
190        let inputs = ready_inputs_from_index(&ready);
191
192        assert_eq!(inputs.len(), 2);
193        assert_eq!(inputs[0].priority_snapshot(), run_a.effective_priority());
194        assert_eq!(inputs[1].priority_snapshot(), run_b.effective_priority());
195        assert_eq!(inputs[0].run().id(), run_a.id());
196        assert_eq!(inputs[1].run().id(), run_b.id());
197    }
198}