Skip to main content

eredu_runtime/cache/
executor.rs

1//! Backend-neutral bounded admission for cache backing-store operations.
2
3use std::collections::HashMap;
4
5use super::CacheIoOperationKey;
6
7#[derive(Debug, Clone, Copy, Eq, PartialEq)]
8enum OperationPhase {
9    Prepared,
10    Queued,
11    InFlight,
12    CancelledPrepared,
13    CancelledQueued,
14    CancelledInFlight,
15    Completed,
16    CompletedCancelled,
17}
18
19/// Result of preparing an exact cache I/O key.
20#[derive(Debug, Clone, Copy, Eq, PartialEq)]
21pub enum CacheIoPreparation {
22    /// The backend must allocate resources for a new operation.
23    New,
24    /// An existing exact operation owns the result and should be joined.
25    Joined,
26}
27
28/// Result of requesting bounded queue admission for prepared cache I/O.
29#[derive(Debug, Clone, Copy, Eq, PartialEq)]
30pub enum CacheIoAdmission {
31    /// The physical executor may enqueue this operation.
32    Admitted,
33    /// Queue capacity must become available before retrying admission.
34    AtCapacity,
35    /// Cancellation won before physical enqueue.
36    Cancelled,
37}
38
39/// Action a physical executor takes after receiving one admitted operation.
40#[derive(Debug, Clone, Copy, Eq, PartialEq)]
41pub enum CacheIoStartDisposition {
42    /// Execute the backend task and report its exact completion.
43    Execute,
44    /// Drop a queued task whose logical operation was cancelled or retired.
45    Discard,
46}
47
48/// Publication action after a physically started operation completes.
49#[derive(Debug, Clone, Copy, Eq, PartialEq)]
50pub enum CacheIoCompletionDisposition {
51    /// Publish the backend result to exact completion waiters.
52    Publish,
53    /// Cancellation won; discard the backend result and retained resources.
54    Discard,
55}
56
57/// Canonical admission and cancellation state for a bounded cache I/O worker.
58///
59/// Backends own task payloads, worker threads, files, buffers, and completion
60/// objects. This state owns exact-key coalescing, bounded admission, queued and
61/// in-flight cancellation, and peak queue accounting.
62#[derive(Debug)]
63pub struct CacheIoExecutionState {
64    capacity: usize,
65    queued: usize,
66    peak_queued: usize,
67    operations: HashMap<CacheIoOperationKey, OperationPhase>,
68}
69
70impl CacheIoExecutionState {
71    /// Creates an empty state with finite nonzero queue capacity.
72    pub fn new(capacity: usize) -> Result<Self, CacheIoExecutionStateError> {
73        if capacity == 0 {
74            return Err(CacheIoExecutionStateError::ZeroCapacity);
75        }
76        Ok(Self {
77            capacity,
78            queued: 0,
79            peak_queued: 0,
80            operations: HashMap::new(),
81        })
82    }
83
84    /// Registers an exact key or joins its existing completion owner.
85    pub fn prepare(&mut self, key: CacheIoOperationKey) -> CacheIoPreparation {
86        match self.operations.entry(key) {
87            std::collections::hash_map::Entry::Vacant(entry) => {
88                entry.insert(OperationPhase::Prepared);
89                CacheIoPreparation::New
90            }
91            std::collections::hash_map::Entry::Occupied(_) => CacheIoPreparation::Joined,
92        }
93    }
94
95    /// Attempts to admit one previously prepared operation.
96    pub fn admit(
97        &mut self,
98        key: &CacheIoOperationKey,
99    ) -> Result<CacheIoAdmission, CacheIoExecutionStateError> {
100        let phase = self
101            .operations
102            .get(key)
103            .copied()
104            .ok_or(CacheIoExecutionStateError::UnknownOperation)?;
105        match phase {
106            OperationPhase::Prepared => {
107                if self.queued == self.capacity {
108                    return Ok(CacheIoAdmission::AtCapacity);
109                }
110                self.queued += 1;
111                self.peak_queued = self.peak_queued.max(self.queued);
112                self.operations.insert(key.clone(), OperationPhase::Queued);
113                Ok(CacheIoAdmission::Admitted)
114            }
115            OperationPhase::CancelledPrepared => Ok(CacheIoAdmission::Cancelled),
116            _ => Err(CacheIoExecutionStateError::InvalidAdmission),
117        }
118    }
119
120    /// Rolls back physical enqueue failure without losing exact-key ownership.
121    pub fn rollback_admission(
122        &mut self,
123        key: &CacheIoOperationKey,
124    ) -> Result<(), CacheIoExecutionStateError> {
125        match self.operations.get(key) {
126            Some(OperationPhase::Queued) => {
127                self.queued -= 1;
128                self.operations
129                    .insert(key.clone(), OperationPhase::Prepared);
130                Ok(())
131            }
132            Some(OperationPhase::CancelledQueued) => {
133                self.queued -= 1;
134                self.operations
135                    .insert(key.clone(), OperationPhase::CancelledPrepared);
136                Ok(())
137            }
138            _ => Err(CacheIoExecutionStateError::InvalidAdmissionRollback),
139        }
140    }
141
142    /// Begins one operation received by the physical executor.
143    pub fn begin(
144        &mut self,
145        key: &CacheIoOperationKey,
146    ) -> Result<CacheIoStartDisposition, CacheIoExecutionStateError> {
147        let Some(phase) = self.operations.get(key).copied() else {
148            // A cancelled queued ticket may be retired by its waiter before
149            // the physical executor drains the opaque task.
150            return Ok(CacheIoStartDisposition::Discard);
151        };
152        match phase {
153            OperationPhase::Queued => {
154                self.queued -= 1;
155                self.operations
156                    .insert(key.clone(), OperationPhase::InFlight);
157                Ok(CacheIoStartDisposition::Execute)
158            }
159            OperationPhase::CancelledQueued => {
160                self.queued -= 1;
161                self.operations
162                    .insert(key.clone(), OperationPhase::CompletedCancelled);
163                Ok(CacheIoStartDisposition::Discard)
164            }
165            _ => Err(CacheIoExecutionStateError::InvalidStart),
166        }
167    }
168
169    /// Cancels prepared, queued, or physically executing work exactly once.
170    pub fn cancel(&mut self, key: &CacheIoOperationKey) -> bool {
171        let Some(phase) = self.operations.get(key).copied() else {
172            return false;
173        };
174        let cancelled = match phase {
175            OperationPhase::Prepared => OperationPhase::CancelledPrepared,
176            OperationPhase::Queued => OperationPhase::CancelledQueued,
177            OperationPhase::InFlight => OperationPhase::CancelledInFlight,
178            _ => return false,
179        };
180        self.operations.insert(key.clone(), cancelled);
181        true
182    }
183
184    /// Resolves one exact physically executed operation.
185    pub fn complete(
186        &mut self,
187        key: &CacheIoOperationKey,
188    ) -> Result<CacheIoCompletionDisposition, CacheIoExecutionStateError> {
189        match self.operations.get(key) {
190            Some(OperationPhase::InFlight) => {
191                self.operations
192                    .insert(key.clone(), OperationPhase::Completed);
193                Ok(CacheIoCompletionDisposition::Publish)
194            }
195            Some(OperationPhase::CancelledInFlight) => {
196                self.operations
197                    .insert(key.clone(), OperationPhase::CompletedCancelled);
198                Ok(CacheIoCompletionDisposition::Discard)
199            }
200            _ => Err(CacheIoExecutionStateError::InvalidCompletion),
201        }
202    }
203
204    /// Releases exact-key ownership after task resources are safe to drop.
205    pub fn retire(
206        &mut self,
207        key: &CacheIoOperationKey,
208    ) -> Result<bool, CacheIoExecutionStateError> {
209        let Some(phase) = self.operations.get(key).copied() else {
210            return Ok(false);
211        };
212        if matches!(
213            phase,
214            OperationPhase::Queued
215                | OperationPhase::InFlight
216                | OperationPhase::CancelledQueued
217                | OperationPhase::CancelledInFlight
218        ) {
219            return Err(CacheIoExecutionStateError::OperationStillOwned);
220        }
221        self.operations.remove(key);
222        Ok(true)
223    }
224
225    /// Current physically queued operation count.
226    pub const fn queued(&self) -> usize {
227        self.queued
228    }
229
230    /// Largest physically queued operation count.
231    pub const fn peak_queued(&self) -> usize {
232        self.peak_queued
233    }
234}
235
236/// Invalid cache I/O executor lifecycle use.
237#[derive(Debug, Clone, Copy, Eq, PartialEq, thiserror::Error)]
238pub enum CacheIoExecutionStateError {
239    /// Bounded I/O admission requires at least one slot.
240    #[error("cache I/O queue capacity must be nonzero")]
241    ZeroCapacity,
242    /// The exact operation key was never prepared or was already retired.
243    #[error("cache I/O operation is unknown or already retired")]
244    UnknownOperation,
245    /// Admission was attempted from a non-prepared phase.
246    #[error("cache I/O operation cannot be admitted from its current phase")]
247    InvalidAdmission,
248    /// Physical enqueue rollback did not own an admitted operation.
249    #[error("cache I/O admission cannot be rolled back from its current phase")]
250    InvalidAdmissionRollback,
251    /// The physical executor received an operation from an invalid phase.
252    #[error("cache I/O operation cannot start from its current phase")]
253    InvalidStart,
254    /// Completion did not correspond to a physically executing operation.
255    #[error("cache I/O operation cannot complete from its current phase")]
256    InvalidCompletion,
257    /// Retirement preceded physical dequeue or exact completion.
258    #[error("cache I/O operation still owns queued or in-flight resources")]
259    OperationStillOwned,
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265    use crate::cache::CacheIoOperationKind;
266    use eredu_core::cache::{CacheBlockId, CacheRepresentation};
267
268    fn key(block: i64) -> CacheIoOperationKey {
269        CacheIoOperationKey {
270            generation: 7,
271            id: CacheBlockId {
272                session_id: 1,
273                global_layer: 0,
274                representation: CacheRepresentation::KeyValue,
275                start: block,
276                end: block + 1,
277                rank: None,
278            },
279            kind: CacheIoOperationKind::Read,
280        }
281    }
282
283    #[test]
284    fn exact_keys_coalesce_and_capacity_is_core_owned() {
285        let mut state = CacheIoExecutionState::new(1).unwrap();
286        let first = key(0);
287        let second = key(1);
288        assert_eq!(state.prepare(first.clone()), CacheIoPreparation::New);
289        assert_eq!(state.prepare(first.clone()), CacheIoPreparation::Joined);
290        assert_eq!(state.admit(&first).unwrap(), CacheIoAdmission::Admitted);
291        assert_eq!(state.prepare(second.clone()), CacheIoPreparation::New);
292        assert_eq!(state.admit(&second).unwrap(), CacheIoAdmission::AtCapacity);
293        assert_eq!(
294            state.begin(&first).unwrap(),
295            CacheIoStartDisposition::Execute
296        );
297        assert_eq!(state.admit(&second).unwrap(), CacheIoAdmission::Admitted);
298        assert_eq!(state.peak_queued(), 1);
299    }
300
301    #[test]
302    fn queued_and_in_flight_cancellation_preserve_exact_ownership() {
303        let mut state = CacheIoExecutionState::new(2).unwrap();
304        let queued = key(0);
305        state.prepare(queued.clone());
306        state.admit(&queued).unwrap();
307        assert!(state.cancel(&queued));
308        assert_eq!(
309            state.retire(&queued),
310            Err(CacheIoExecutionStateError::OperationStillOwned)
311        );
312        assert_eq!(
313            state.begin(&queued).unwrap(),
314            CacheIoStartDisposition::Discard
315        );
316        assert!(!state.cancel(&queued));
317        assert!(state.retire(&queued).unwrap());
318
319        let active = key(1);
320        state.prepare(active.clone());
321        state.admit(&active).unwrap();
322        assert_eq!(
323            state.begin(&active).unwrap(),
324            CacheIoStartDisposition::Execute
325        );
326        assert!(state.cancel(&active));
327        assert_eq!(
328            state.complete(&active).unwrap(),
329            CacheIoCompletionDisposition::Discard
330        );
331        assert!(state.retire(&active).unwrap());
332    }
333
334    #[test]
335    fn failed_physical_enqueue_rolls_back_capacity_for_recovery() {
336        let mut state = CacheIoExecutionState::new(1).unwrap();
337        let operation = key(0);
338        state.prepare(operation.clone());
339        state.admit(&operation).unwrap();
340        state.rollback_admission(&operation).unwrap();
341        assert_eq!(state.queued(), 0);
342        assert_eq!(state.admit(&operation).unwrap(), CacheIoAdmission::Admitted);
343    }
344}