use std::collections::HashMap;
use super::CacheIoOperationKey;
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
enum OperationPhase {
Prepared,
Queued,
InFlight,
CancelledPrepared,
CancelledQueued,
CancelledInFlight,
Completed,
CompletedCancelled,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum CacheIoPreparation {
New,
Joined,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum CacheIoAdmission {
Admitted,
AtCapacity,
Cancelled,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum CacheIoStartDisposition {
Execute,
Discard,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum CacheIoCompletionDisposition {
Publish,
Discard,
}
#[derive(Debug)]
pub struct CacheIoExecutionState {
capacity: usize,
queued: usize,
peak_queued: usize,
operations: HashMap<CacheIoOperationKey, OperationPhase>,
}
impl CacheIoExecutionState {
pub fn new(capacity: usize) -> Result<Self, CacheIoExecutionStateError> {
if capacity == 0 {
return Err(CacheIoExecutionStateError::ZeroCapacity);
}
Ok(Self {
capacity,
queued: 0,
peak_queued: 0,
operations: HashMap::new(),
})
}
pub fn prepare(&mut self, key: CacheIoOperationKey) -> CacheIoPreparation {
match self.operations.entry(key) {
std::collections::hash_map::Entry::Vacant(entry) => {
entry.insert(OperationPhase::Prepared);
CacheIoPreparation::New
}
std::collections::hash_map::Entry::Occupied(_) => CacheIoPreparation::Joined,
}
}
pub fn admit(
&mut self,
key: &CacheIoOperationKey,
) -> Result<CacheIoAdmission, CacheIoExecutionStateError> {
let phase = self
.operations
.get(key)
.copied()
.ok_or(CacheIoExecutionStateError::UnknownOperation)?;
match phase {
OperationPhase::Prepared => {
if self.queued == self.capacity {
return Ok(CacheIoAdmission::AtCapacity);
}
self.queued += 1;
self.peak_queued = self.peak_queued.max(self.queued);
self.operations.insert(key.clone(), OperationPhase::Queued);
Ok(CacheIoAdmission::Admitted)
}
OperationPhase::CancelledPrepared => Ok(CacheIoAdmission::Cancelled),
_ => Err(CacheIoExecutionStateError::InvalidAdmission),
}
}
pub fn rollback_admission(
&mut self,
key: &CacheIoOperationKey,
) -> Result<(), CacheIoExecutionStateError> {
match self.operations.get(key) {
Some(OperationPhase::Queued) => {
self.queued -= 1;
self.operations
.insert(key.clone(), OperationPhase::Prepared);
Ok(())
}
Some(OperationPhase::CancelledQueued) => {
self.queued -= 1;
self.operations
.insert(key.clone(), OperationPhase::CancelledPrepared);
Ok(())
}
_ => Err(CacheIoExecutionStateError::InvalidAdmissionRollback),
}
}
pub fn begin(
&mut self,
key: &CacheIoOperationKey,
) -> Result<CacheIoStartDisposition, CacheIoExecutionStateError> {
let Some(phase) = self.operations.get(key).copied() else {
return Ok(CacheIoStartDisposition::Discard);
};
match phase {
OperationPhase::Queued => {
self.queued -= 1;
self.operations
.insert(key.clone(), OperationPhase::InFlight);
Ok(CacheIoStartDisposition::Execute)
}
OperationPhase::CancelledQueued => {
self.queued -= 1;
self.operations
.insert(key.clone(), OperationPhase::CompletedCancelled);
Ok(CacheIoStartDisposition::Discard)
}
_ => Err(CacheIoExecutionStateError::InvalidStart),
}
}
pub fn cancel(&mut self, key: &CacheIoOperationKey) -> bool {
let Some(phase) = self.operations.get(key).copied() else {
return false;
};
let cancelled = match phase {
OperationPhase::Prepared => OperationPhase::CancelledPrepared,
OperationPhase::Queued => OperationPhase::CancelledQueued,
OperationPhase::InFlight => OperationPhase::CancelledInFlight,
_ => return false,
};
self.operations.insert(key.clone(), cancelled);
true
}
pub fn complete(
&mut self,
key: &CacheIoOperationKey,
) -> Result<CacheIoCompletionDisposition, CacheIoExecutionStateError> {
match self.operations.get(key) {
Some(OperationPhase::InFlight) => {
self.operations
.insert(key.clone(), OperationPhase::Completed);
Ok(CacheIoCompletionDisposition::Publish)
}
Some(OperationPhase::CancelledInFlight) => {
self.operations
.insert(key.clone(), OperationPhase::CompletedCancelled);
Ok(CacheIoCompletionDisposition::Discard)
}
_ => Err(CacheIoExecutionStateError::InvalidCompletion),
}
}
pub fn retire(
&mut self,
key: &CacheIoOperationKey,
) -> Result<bool, CacheIoExecutionStateError> {
let Some(phase) = self.operations.get(key).copied() else {
return Ok(false);
};
if matches!(
phase,
OperationPhase::Queued
| OperationPhase::InFlight
| OperationPhase::CancelledQueued
| OperationPhase::CancelledInFlight
) {
return Err(CacheIoExecutionStateError::OperationStillOwned);
}
self.operations.remove(key);
Ok(true)
}
pub const fn queued(&self) -> usize {
self.queued
}
pub const fn peak_queued(&self) -> usize {
self.peak_queued
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, thiserror::Error)]
pub enum CacheIoExecutionStateError {
#[error("cache I/O queue capacity must be nonzero")]
ZeroCapacity,
#[error("cache I/O operation is unknown or already retired")]
UnknownOperation,
#[error("cache I/O operation cannot be admitted from its current phase")]
InvalidAdmission,
#[error("cache I/O admission cannot be rolled back from its current phase")]
InvalidAdmissionRollback,
#[error("cache I/O operation cannot start from its current phase")]
InvalidStart,
#[error("cache I/O operation cannot complete from its current phase")]
InvalidCompletion,
#[error("cache I/O operation still owns queued or in-flight resources")]
OperationStillOwned,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cache::CacheIoOperationKind;
use eredu_core::cache::{CacheBlockId, CacheRepresentation};
fn key(block: i64) -> CacheIoOperationKey {
CacheIoOperationKey {
generation: 7,
id: CacheBlockId {
session_id: 1,
global_layer: 0,
representation: CacheRepresentation::KeyValue,
start: block,
end: block + 1,
rank: None,
},
kind: CacheIoOperationKind::Read,
}
}
#[test]
fn exact_keys_coalesce_and_capacity_is_core_owned() {
let mut state = CacheIoExecutionState::new(1).unwrap();
let first = key(0);
let second = key(1);
assert_eq!(state.prepare(first.clone()), CacheIoPreparation::New);
assert_eq!(state.prepare(first.clone()), CacheIoPreparation::Joined);
assert_eq!(state.admit(&first).unwrap(), CacheIoAdmission::Admitted);
assert_eq!(state.prepare(second.clone()), CacheIoPreparation::New);
assert_eq!(state.admit(&second).unwrap(), CacheIoAdmission::AtCapacity);
assert_eq!(
state.begin(&first).unwrap(),
CacheIoStartDisposition::Execute
);
assert_eq!(state.admit(&second).unwrap(), CacheIoAdmission::Admitted);
assert_eq!(state.peak_queued(), 1);
}
#[test]
fn queued_and_in_flight_cancellation_preserve_exact_ownership() {
let mut state = CacheIoExecutionState::new(2).unwrap();
let queued = key(0);
state.prepare(queued.clone());
state.admit(&queued).unwrap();
assert!(state.cancel(&queued));
assert_eq!(
state.retire(&queued),
Err(CacheIoExecutionStateError::OperationStillOwned)
);
assert_eq!(
state.begin(&queued).unwrap(),
CacheIoStartDisposition::Discard
);
assert!(!state.cancel(&queued));
assert!(state.retire(&queued).unwrap());
let active = key(1);
state.prepare(active.clone());
state.admit(&active).unwrap();
assert_eq!(
state.begin(&active).unwrap(),
CacheIoStartDisposition::Execute
);
assert!(state.cancel(&active));
assert_eq!(
state.complete(&active).unwrap(),
CacheIoCompletionDisposition::Discard
);
assert!(state.retire(&active).unwrap());
}
#[test]
fn failed_physical_enqueue_rolls_back_capacity_for_recovery() {
let mut state = CacheIoExecutionState::new(1).unwrap();
let operation = key(0);
state.prepare(operation.clone());
state.admit(&operation).unwrap();
state.rollback_admission(&operation).unwrap();
assert_eq!(state.queued(), 0);
assert_eq!(state.admit(&operation).unwrap(), CacheIoAdmission::Admitted);
}
}