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}