use std::collections::{BTreeMap, BTreeSet};
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct RecordKey(String);
impl RecordKey {
pub fn new(path: impl Into<String>) -> Self {
Self(path.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for RecordKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SealedRecord {
pub key: RecordKey,
pub bytes: Vec<u8>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FlushBatch {
pub id: u64,
pub records: Vec<SealedRecord>,
pub deletes: Vec<RecordKey>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum PendingOp {
Write(Vec<u8>),
Delete,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize)]
#[serde(transparent)]
pub struct RequiredSeq(Option<u64>);
impl RequiredSeq {
pub const NONE: RequiredSeq = RequiredSeq(None);
}
pub trait FlushEpoch {
fn seal_for_request(&mut self) -> (RequiredSeq, Option<FlushBatch>);
fn is_released(&self, required: RequiredSeq) -> bool;
}
#[derive(Debug, Default)]
pub struct FlushGate {
next_batch_id: u64,
pending: BTreeMap<RecordKey, PendingOp>,
unacked_batches: BTreeSet<u64>,
}
impl FlushGate {
pub fn new() -> Self {
Self::default()
}
pub fn mark_dirty(&mut self, key: RecordKey, bytes: Vec<u8>) {
self.pending.insert(key, PendingOp::Write(bytes));
}
pub fn mark_deleted(&mut self, key: RecordKey) {
self.pending.insert(key, PendingOp::Delete);
}
pub fn take_batch(&mut self) -> Option<FlushBatch> {
if self.pending.is_empty() {
return None;
}
let id = self.next_batch_id;
self.next_batch_id += 1;
let mut records = Vec::new();
let mut deletes = Vec::new();
for (key, op) in std::mem::take(&mut self.pending) {
match op {
PendingOp::Write(bytes) => records.push(SealedRecord { key, bytes }),
PendingOp::Delete => deletes.push(key),
}
}
self.unacked_batches.insert(id);
Some(FlushBatch { id, records, deletes })
}
pub fn ack(&mut self, id: u64) {
self.unacked_batches.remove(&id);
}
}
impl FlushEpoch for FlushGate {
fn seal_for_request(&mut self) -> (RequiredSeq, Option<FlushBatch>) {
if self.pending.is_empty() {
return (RequiredSeq(self.next_batch_id.checked_sub(1)), None);
}
let batch = self
.take_batch()
.expect("pending is non-empty (checked above), so take_batch always returns Some here");
(RequiredSeq(Some(batch.id)), Some(batch))
}
fn is_released(&self, required: RequiredSeq) -> bool {
match required.0 {
None => true,
Some(seq) => self.unacked_batches.range(..=seq).next().is_none(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn seal_for_request_blocks_release_until_the_sealed_batch_is_acked() {
let mut gate = FlushGate::new();
gate.mark_dirty(RecordKey::new("messenger/dev/account"), b"v1".to_vec());
let (required, batch) = gate.seal_for_request();
let batch = batch.expect("the pending write was sealed into a batch");
assert!(!gate.is_released(required), "sealed but not yet acked");
gate.ack(batch.id);
assert!(gate.is_released(required), "sealed and acked: safe to release");
}
#[test]
fn a_request_minted_with_nothing_pending_depends_on_the_last_batch_already_taken() {
let mut gate = FlushGate::new();
gate.mark_dirty(RecordKey::new("messenger/dev/a"), b"1".to_vec());
let (first_required, first_batch) = gate.seal_for_request();
let first_batch = first_batch.expect("first mutation was sealed");
let (second_required, second_batch) = gate.seal_for_request();
assert!(second_batch.is_none(), "nothing was pending, so no new batch was forced");
assert_eq!(second_required, first_required, "both wait on the same, already-produced batch");
gate.ack(first_batch.id);
assert!(gate.is_released(first_required));
assert!(gate.is_released(second_required));
}
#[test]
fn a_request_minted_before_any_mutation_has_no_dependency() {
let gate = FlushGate::new();
assert!(gate.is_released(RequiredSeq::NONE));
}
#[test]
fn requests_produced_after_a_flush_are_not_blocked_by_later_dirty_records() {
let mut gate = FlushGate::new();
gate.mark_dirty(RecordKey::new("messenger/dev/a"), b"1".to_vec());
let (required1, batch1) = gate.seal_for_request();
let batch1 = batch1.expect("first batch");
gate.ack(batch1.id);
assert!(gate.is_released(required1), "fully flushed and acked: safe to release");
gate.mark_dirty(RecordKey::new("messenger/dev/b"), b"2".to_vec());
assert!(gate.is_released(required1), "still released: batch1 already acked");
let (required2, batch2) = gate.seal_for_request();
let batch2 = batch2.expect("second batch");
assert_ne!(batch1.id, batch2.id, "each flush cycle gets a fresh id");
assert!(!gate.is_released(required2), "a request minted just now waits on the fresh batch");
gate.ack(batch2.id);
assert!(gate.is_released(required2), "second cycle flushed independently of the first");
}
#[test]
fn ack_of_unknown_batch_is_ignored() {
let mut gate = FlushGate::new();
gate.ack(999); assert!(gate.is_released(RequiredSeq::NONE));
gate.mark_dirty(RecordKey::new("messenger/dev/a"), b"1".to_vec());
let (required, batch) = gate.seal_for_request();
let batch = batch.expect("pending");
gate.ack(batch.id + 1); assert!(!gate.is_released(required));
gate.ack(batch.id);
assert!(gate.is_released(required));
}
#[test]
fn deletes_ride_in_the_batch() {
let mut gate = FlushGate::new();
gate.mark_dirty(RecordKey::new("messenger/dev/keep"), b"kept".to_vec());
gate.mark_deleted(RecordKey::new("messenger/dev/gone"));
let batch = gate.take_batch().expect("pending");
assert_eq!(batch.records.len(), 1);
assert_eq!(batch.records[0].key.as_str(), "messenger/dev/keep");
assert_eq!(batch.records[0].bytes, b"kept".to_vec());
assert_eq!(batch.deletes.len(), 1);
assert_eq!(batch.deletes[0].as_str(), "messenger/dev/gone");
}
#[test]
fn take_batch_returns_none_when_nothing_is_pending() {
let mut gate = FlushGate::new();
assert!(gate.take_batch().is_none());
}
#[test]
fn a_later_write_to_the_same_key_overwrites_an_earlier_pending_delete() {
let mut gate = FlushGate::new();
let key = RecordKey::new("messenger/dev/x");
gate.mark_deleted(key.clone());
gate.mark_dirty(key.clone(), b"new".to_vec());
let batch = gate.take_batch().expect("pending");
assert!(batch.deletes.is_empty());
assert_eq!(batch.records.len(), 1);
assert_eq!(batch.records[0].bytes, b"new".to_vec());
}
}