1use 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#[derive(Debug, Clone, Copy, Eq, PartialEq)]
21pub enum CacheIoPreparation {
22 New,
24 Joined,
26}
27
28#[derive(Debug, Clone, Copy, Eq, PartialEq)]
30pub enum CacheIoAdmission {
31 Admitted,
33 AtCapacity,
35 Cancelled,
37}
38
39#[derive(Debug, Clone, Copy, Eq, PartialEq)]
41pub enum CacheIoStartDisposition {
42 Execute,
44 Discard,
46}
47
48#[derive(Debug, Clone, Copy, Eq, PartialEq)]
50pub enum CacheIoCompletionDisposition {
51 Publish,
53 Discard,
55}
56
57#[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 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 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 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 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 pub fn begin(
144 &mut self,
145 key: &CacheIoOperationKey,
146 ) -> Result<CacheIoStartDisposition, CacheIoExecutionStateError> {
147 let Some(phase) = self.operations.get(key).copied() else {
148 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 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 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 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 pub const fn queued(&self) -> usize {
227 self.queued
228 }
229
230 pub const fn peak_queued(&self) -> usize {
232 self.peak_queued
233 }
234}
235
236#[derive(Debug, Clone, Copy, Eq, PartialEq, thiserror::Error)]
238pub enum CacheIoExecutionStateError {
239 #[error("cache I/O queue capacity must be nonzero")]
241 ZeroCapacity,
242 #[error("cache I/O operation is unknown or already retired")]
244 UnknownOperation,
245 #[error("cache I/O operation cannot be admitted from its current phase")]
247 InvalidAdmission,
248 #[error("cache I/O admission cannot be rolled back from its current phase")]
250 InvalidAdmissionRollback,
251 #[error("cache I/O operation cannot start from its current phase")]
253 InvalidStart,
254 #[error("cache I/O operation cannot complete from its current phase")]
256 InvalidCompletion,
257 #[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}