use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum WorkClass {
InteractiveRead,
InteractiveCompute,
Mutation,
IndexChunk,
Maintenance,
}
const WEIGHTS: [(WorkClass, u64); 5] = [
(WorkClass::InteractiveRead, 100),
(WorkClass::InteractiveCompute, 80),
(WorkClass::Mutation, 60),
(WorkClass::IndexChunk, 40),
(WorkClass::Maintenance, 20),
];
impl WorkClass {
pub fn weight(self) -> u64 {
WEIGHTS
.iter()
.find_map(|(class, weight)| (*class == self).then_some(*weight))
.expect("all work classes have weights")
}
pub fn rank(self) -> u8 {
match self {
WorkClass::InteractiveRead => 0,
WorkClass::InteractiveCompute => 1,
WorkClass::Mutation => 2,
WorkClass::IndexChunk => 3,
WorkClass::Maintenance => 4,
}
}
}
#[derive(Debug, Clone)]
pub struct ClassAging {
threshold_ticks: u64,
boost: u64,
tick: u64,
last_served: HashMap<WorkClass, u64>,
eligible_tick: HashMap<WorkClass, u64>,
}
impl Default for ClassAging {
fn default() -> Self {
Self::new(100, 200)
}
}
impl ClassAging {
pub fn new(threshold_ticks: u64, boost: u64) -> Self {
Self {
threshold_ticks,
boost,
tick: 0,
last_served: HashMap::new(),
eligible_tick: HashMap::new(),
}
}
pub fn advance(&mut self) {
self.tick += 1;
}
pub fn note_eligible(&mut self, class: WorkClass) {
self.eligible_tick.entry(class).or_insert(self.tick);
}
pub fn note_served(&mut self, class: WorkClass) {
self.last_served.insert(class, self.tick);
}
pub fn ticks_since_served(&self, class: WorkClass) -> u64 {
let baseline = self
.last_served
.get(&class)
.copied()
.or_else(|| self.eligible_tick.get(&class).copied())
.unwrap_or(self.tick);
self.tick.saturating_sub(baseline)
}
pub fn is_starved(&self, class: WorkClass) -> bool {
self.ticks_since_served(class) >= self.threshold_ticks
}
pub fn effective_weight(&self, class: WorkClass) -> u64 {
if self.is_starved(class) {
class.weight() + self.boost
} else {
class.weight()
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_work_class_weight_ordering() {
let read = WorkClass::InteractiveRead.weight();
let compute = WorkClass::InteractiveCompute.weight();
let mutation = WorkClass::Mutation.weight();
let index = WorkClass::IndexChunk.weight();
let maintenance = WorkClass::Maintenance.weight();
assert!(
read >= compute && compute >= mutation && mutation >= index && index >= maintenance,
"weight ordering violated: {read} >= {compute} >= {mutation} >= {index} >= {maintenance}"
);
assert!(
read > maintenance,
"weights must not all be equal (read={read}, maintenance={maintenance})"
);
assert_eq!(
(read, compute, mutation, index, maintenance),
(100, 80, 60, 40, 20)
);
}
#[test]
fn test_work_class_enum_has_five_distinct_classes() {
let all = [
WorkClass::InteractiveRead,
WorkClass::InteractiveCompute,
WorkClass::Mutation,
WorkClass::IndexChunk,
WorkClass::Maintenance,
];
assert_eq!(all.len(), 5);
let mut sorted = all;
sorted.sort();
assert_eq!(sorted, all, "ordering matches priority ranking");
}
#[test]
fn test_aging_boosts_starved_class_weight() {
let mut ager = ClassAging::new(10, 100);
assert_eq!(ager.effective_weight(WorkClass::Maintenance), 20);
ager.note_eligible(WorkClass::Maintenance);
for _ in 0..10 {
ager.advance();
ager.note_served(WorkClass::InteractiveRead);
}
assert!(ager.is_starved(WorkClass::Maintenance));
assert!(ager.effective_weight(WorkClass::Maintenance) > 100);
}
#[test]
fn test_aging_resets_on_service() {
let mut ager = ClassAging::new(5, 100);
ager.note_eligible(WorkClass::IndexChunk);
for _ in 0..5 {
ager.advance();
}
assert!(ager.is_starved(WorkClass::IndexChunk));
ager.note_served(WorkClass::IndexChunk);
assert!(!ager.is_starved(WorkClass::IndexChunk));
assert_eq!(ager.effective_weight(WorkClass::IndexChunk), 40);
}
}