use std::{
sync::{Arc, Mutex, MutexGuard, PoisonError},
time::Duration,
};
use slatedb::{Db, WriteHandle};
use tokio::time::Instant;
pub(crate) struct FlushPacer {
db: Db,
spacing: Duration,
state: Mutex<PacerState>,
flushes: tokio::sync::Mutex<()>,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
struct PacerState {
last_started: Option<Instant>,
scheduled: bool,
in_flight: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Claim {
FlushNow,
FlushAt(Instant),
Await,
}
impl PacerState {
fn claim(&mut self, spacing: Duration, now: Instant) -> Claim {
if self.scheduled {
return Claim::Await;
}
let due = self.last_started.map_or(now, |started| started + spacing);
if !self.in_flight && due <= now {
self.last_started = Some(now);
self.in_flight = true;
return Claim::FlushNow;
}
self.scheduled = true;
Claim::FlushAt(due.max(now))
}
}
impl FlushPacer {
pub(crate) fn new(db: Db, spacing: Duration) -> Arc<Self> {
Arc::new(Self {
db,
spacing,
state: Mutex::new(PacerState::default()),
flushes: tokio::sync::Mutex::new(()),
})
}
fn state(&self) -> MutexGuard<'_, PacerState> {
self.state.lock().unwrap_or_else(PoisonError::into_inner)
}
pub(crate) async fn await_durable(&self, handle: WriteHandle) -> Result<(), slatedb::Error> {
let claim = self.state().claim(self.spacing, Instant::now());
match claim {
Claim::FlushNow => {
let in_flight = InFlight(self);
let _serialized = self.flushes.lock().await;
self.flush(in_flight).await
}
Claim::FlushAt(deadline) => {
let mut scheduled = Scheduled {
pacer: self,
armed: true,
};
tokio::time::sleep_until(deadline).await;
let _serialized = self.flushes.lock().await;
let in_flight = {
let mut state = self.state();
state.scheduled = false;
state.in_flight = true;
state.last_started = Some(Instant::now());
InFlight(self)
};
scheduled.armed = false;
self.flush(in_flight).await
}
Claim::Await => handle.await_durable().await,
}
}
async fn flush(&self, in_flight: InFlight<'_>) -> Result<(), slatedb::Error> {
let outcome = self.db.flush().await;
drop(in_flight);
outcome
}
}
struct Scheduled<'a> {
pacer: &'a FlushPacer,
armed: bool,
}
impl Drop for Scheduled<'_> {
fn drop(&mut self) {
if self.armed {
self.pacer.state().scheduled = false;
}
}
}
struct InFlight<'a>(&'a FlushPacer);
impl Drop for InFlight<'_> {
fn drop(&mut self) {
self.0.state().in_flight = false;
}
}
#[cfg(test)]
mod tests {
use super::*;
const SPACING: Duration = Duration::from_millis(100);
fn at(base: Instant, millis: u64) -> Instant {
base + Duration::from_millis(millis)
}
#[tokio::test(start_paused = true)]
async fn the_first_commit_flushes_at_once() {
let mut state = PacerState::default();
let now = Instant::now();
assert_eq!(state.claim(SPACING, now), Claim::FlushNow);
assert!(state.in_flight);
assert_eq!(state.last_started, Some(now));
}
#[tokio::test(start_paused = true)]
async fn a_commit_after_the_spacing_flushes_at_once() {
let mut state = PacerState::default();
let base = Instant::now();
state.claim(SPACING, base);
state.in_flight = false;
assert_eq!(state.claim(SPACING, at(base, 100)), Claim::FlushNow);
}
#[tokio::test(start_paused = true)]
async fn a_commit_inside_the_spacing_schedules_the_flush_for_when_it_elapses() {
let mut state = PacerState::default();
let base = Instant::now();
state.claim(SPACING, base);
state.in_flight = false;
assert_eq!(
state.claim(SPACING, at(base, 30)),
Claim::FlushAt(at(base, 100))
);
assert!(state.scheduled);
}
#[tokio::test(start_paused = true)]
async fn commits_behind_a_scheduled_flush_ride_on_it() {
let mut state = PacerState::default();
let base = Instant::now();
state.claim(SPACING, base);
state.in_flight = false;
state.claim(SPACING, at(base, 30));
assert_eq!(state.claim(SPACING, at(base, 60)), Claim::Await);
assert_eq!(state.claim(SPACING, at(base, 150)), Claim::Await);
}
#[tokio::test(start_paused = true)]
async fn a_commit_during_a_flush_in_the_air_schedules_the_next_slot() {
let mut state = PacerState::default();
let base = Instant::now();
state.claim(SPACING, base);
assert_eq!(
state.claim(SPACING, at(base, 120)),
Claim::FlushAt(at(base, 120))
);
}
#[tokio::test(start_paused = true)]
async fn zero_spacing_flushes_every_commit_that_finds_no_flush_in_the_air() {
let mut state = PacerState::default();
let base = Instant::now();
assert_eq!(state.claim(Duration::ZERO, base), Claim::FlushNow);
state.in_flight = false;
assert_eq!(state.claim(Duration::ZERO, base), Claim::FlushNow);
assert_eq!(state.claim(Duration::ZERO, base), Claim::FlushAt(base));
}
}