use crate::lineage::{Planner, StartAt};
use std::collections::{BTreeSet, VecDeque};
use std::time::Instant;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Lease {
pub shard_id: String,
pub iterator: Option<String>,
pub position: StartAt,
pub throttles: u32,
pub latest_since_ms: Option<i64>,
pub replaying_from_trim: bool,
}
impl Lease {
pub fn new(shard_id: impl Into<String>, position: StartAt) -> Self {
Self {
shard_id: shard_id.into(),
iterator: None,
position,
throttles: 0,
latest_since_ms: None,
replaying_from_trim: false,
}
}
}
#[derive(Debug)]
pub struct Scheduler {
planner: Planner,
queue: VecDeque<(Lease, Instant)>,
active: BTreeSet<String>,
concurrency: usize,
}
impl Scheduler {
pub fn new(planner: Planner, concurrency: usize) -> Self {
Self {
planner,
queue: VecDeque::new(),
active: BTreeSet::new(),
concurrency: concurrency.max(1),
}
}
pub fn planner(&mut self) -> &mut Planner {
&mut self.planner
}
pub fn ready(&self) -> Vec<String> {
self.planner.ready()
}
pub fn admit(&mut self, shard_id: &str, start: StartAt, now: Instant) {
self.planner.start(shard_id);
self.queue.push_back((Lease::new(shard_id, start), now));
}
pub fn dispatch(&mut self, now: Instant) -> Vec<Lease> {
let mut out = Vec::new();
while self.active.len() < self.concurrency {
let Some(pos) = self.queue.iter().position(|(_, due)| *due <= now) else {
break;
};
let (lease, _) = self.queue.remove(pos).expect("position is in range");
self.active.insert(lease.shard_id.clone());
out.push(lease);
}
out
}
pub fn yielded(&mut self, lease: Lease, not_before: Instant) {
self.active.remove(&lease.shard_id);
self.queue.push_back((lease, not_before));
}
pub fn finished(&mut self, shard_id: &str) {
self.active.remove(shard_id);
self.planner.finish(shard_id);
}
pub fn next_wakeup(&self) -> Option<Instant> {
if self.active.len() >= self.concurrency {
return None;
}
self.queue.iter().map(|(_, due)| *due).min()
}
pub fn is_idle(&self) -> bool {
self.active.is_empty() && self.queue.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lineage::ShardInfo;
use std::collections::BTreeMap;
use std::time::Duration;
fn shard(id: &str, parent: Option<&str>) -> ShardInfo {
ShardInfo {
id: id.into(),
parent: parent.map(str::to_string),
}
}
fn admit_ready(s: &mut Scheduler, now: Instant) {
for id in s.ready() {
s.admit(&id, StartAt::TrimHorizon, now);
}
}
#[test]
fn more_shards_than_workers_all_make_progress() {
let planner = Planner::new(
(0..5).map(|i| shard(&format!("s{i}"), None)).collect(),
&BTreeSet::new(),
);
let mut s = Scheduler::new(planner, 2);
let now = Instant::now();
admit_ready(&mut s, now);
let mut turns: BTreeMap<String, usize> = BTreeMap::new();
for _ in 0..10 {
let leases = s.dispatch(now);
assert!(leases.len() <= 2);
for mut l in leases {
*turns.entry(l.shard_id.clone()).or_default() += 1;
l.iterator = Some("it".into());
s.yielded(l, now);
}
}
assert_eq!(turns.len(), 5, "every shard got a turn: {turns:?}");
assert!(turns.values().all(|&n| n == 4), "turns are even: {turns:?}");
assert!(!s.is_idle());
}
#[test]
fn children_are_admitted_only_after_parents_drain() {
let planner = Planner::new(
vec![
shard("p", None),
shard("c1", Some("p")),
shard("c2", Some("p")),
],
&BTreeSet::new(),
);
let mut s = Scheduler::new(planner, 1);
let now = Instant::now();
admit_ready(&mut s, now);
assert!(s.ready().is_empty());
let lease = s.dispatch(now).pop().unwrap();
assert_eq!(lease.shard_id, "p");
assert!(s.planner().has_children("p"));
s.yielded(lease, now);
assert!(
s.ready().is_empty(),
"an open parent keeps children waiting"
);
let lease = s.dispatch(now).pop().unwrap();
s.finished(&lease.shard_id);
assert_eq!(s.ready(), vec!["c1", "c2"]);
admit_ready(&mut s, now);
assert_eq!(s.dispatch(now)[0].shard_id, "c1");
}
#[test]
fn deferred_shards_wait_and_wakeups_follow_free_slots() {
let planner = Planner::new(vec![shard("a", None), shard("b", None)], &BTreeSet::new());
let mut s = Scheduler::new(planner, 1);
let now = Instant::now();
let later = now + Duration::from_secs(1);
s.admit("a", StartAt::Latest, later);
s.admit("b", StartAt::Latest, now);
let got = s.dispatch(now);
assert_eq!(got.len(), 1);
assert_eq!(got[0].shard_id, "b", "a is not due yet");
assert_eq!(s.next_wakeup(), None, "no free slot");
s.finished("b");
assert_eq!(s.next_wakeup(), Some(later));
assert!(s.dispatch(now).is_empty());
let a = s.dispatch(later).pop().unwrap();
assert_eq!(a, Lease::new("a", StartAt::Latest));
s.finished("a");
assert!(s.is_idle());
assert_eq!(s.next_wakeup(), None);
assert_eq!(Scheduler::new(Planner::default(), 0).concurrency, 1);
}
}