use std::sync::{
Arc,
atomic::{AtomicUsize, Ordering},
};
use slatedb::{Db, DbTransaction, IsolationLevel};
use tokio::sync::{Mutex, MutexGuard, watch};
use super::{Landed, Prepared, StagedWrite, commit_batch, fold, head_view_for, prepare_and_stage};
use crate::{
catalog::{CatalogSnapshot, SnapshotId, projection::ProjectionCache},
error::{Error, Result},
transaction::{operations::ChangeSet, verbs::Transaction},
};
const MAX_BATCH_MEMBERS: usize = 64;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Outcome {
Committed,
LostRace,
Nothing,
}
pub(crate) struct Staged {
pub(crate) ids: Vec<SnapshotId>,
pub(crate) ours: Vec<ChangeSet>,
pub(crate) head_before: u64,
pub(crate) contributed: bool,
pub(crate) outcome: watch::Receiver<Option<Outcome>>,
}
struct Batch {
db_tx: DbTransaction,
base: Arc<CatalogSnapshot>,
premise: Option<CatalogSnapshot>,
folded: usize,
head_before: u64,
head: u64,
members: usize,
writes: Vec<StagedWrite>,
outcome: watch::Sender<Option<Outcome>>,
}
impl Batch {
async fn open(db: &Db, projections: &std::sync::RwLock<ProjectionCache>) -> Result<Self> {
let db_tx = db
.begin(IsolationLevel::Snapshot)
.await
.map_err(Error::from)?;
let base = match head_view_for(&db_tx, projections).await {
Ok(base) => base,
Err(err) => {
db_tx.rollback();
return Err(err);
}
};
let head_before = base.snapshot.snapshot_id;
Ok(Self {
db_tx,
base,
premise: None,
folded: 0,
head_before,
head: head_before,
members: 0,
writes: Vec::new(),
outcome: watch::Sender::new(None),
})
}
fn refold(&mut self) -> Result<()> {
if self.folded == self.writes.len() {
return Ok(());
}
let mut premise = self.premise.take().unwrap_or_else(|| (*self.base).clone());
fold::fold_batch(&mut premise, &self.writes[self.folded..])?;
self.folded = self.writes.len();
self.premise = Some(premise);
Ok(())
}
async fn stage<F>(&mut self, members: &[F]) -> Result<(Vec<SnapshotId>, Vec<ChangeSet>)>
where
F: Fn(&mut Transaction) -> Result<()>,
{
let mut ids = Vec::with_capacity(members.len());
let mut ours = Vec::new();
for member in members {
self.members += 1;
self.refold()?;
let premise = self.premise.as_ref().unwrap_or(&self.base);
let prepared = prepare_and_stage(&self.db_tx, member, premise).await?;
match prepared {
Prepared::Nothing { head } => ids.push(SnapshotId::new(head)),
Prepared::Staged {
ours: theirs,
commits,
writes,
} => {
ids.push(SnapshotId::new(commits));
ours.push(*theirs);
self.head = commits;
self.writes.extend(writes);
}
}
}
Ok((ids, ours))
}
}
struct Shared {
forming: Option<Batch>,
in_flight: bool,
}
pub(crate) struct Coalescer {
shared: Mutex<Shared>,
arriving: AtomicUsize,
flights: watch::Sender<u64>,
projections: Arc<std::sync::RwLock<ProjectionCache>>,
}
impl Coalescer {
pub(crate) fn new(projections: Arc<std::sync::RwLock<ProjectionCache>>) -> Self {
Self {
shared: Mutex::new(Shared {
forming: None,
in_flight: false,
}),
arriving: AtomicUsize::new(0),
flights: watch::Sender::new(0),
projections,
}
}
fn arrive(self: &Arc<Self>) -> Arrival {
self.arriving.fetch_add(1, Ordering::AcqRel);
Arrival {
coalescer: Arc::clone(self),
settled: false,
}
}
async fn admit(&self) -> MutexGuard<'_, Shared> {
loop {
let shared = self.shared.lock().await;
if !shared.in_flight {
return shared;
}
let mut flights = self.flights.subscribe();
drop(shared);
let _ = flights.changed().await;
}
}
pub(crate) async fn stage<F>(self: &Arc<Self>, db: &Db, members: &[F]) -> Result<Staged>
where
F: Fn(&mut Transaction) -> Result<()>,
{
let arrival = self.arrive();
let mut shared = self.admit().await;
let mut batch = match shared.forming.take() {
Some(batch) => batch,
None => Batch::open(db, &self.projections).await?,
};
let staged_before = batch.writes.len();
let staged = match batch.stage(members).await {
Ok(staged) => staged,
Err(err) => {
batch.db_tx.rollback();
return Err(err);
}
};
let (ids, ours) = staged;
let outcome = batch.outcome.subscribe();
let head_before = batch.head_before;
let contributed = batch.writes.len() > staged_before;
let alone = arrival.settle() == 0;
let full = batch.members >= MAX_BATCH_MEMBERS;
if (alone || full) && !batch.writes.is_empty() {
shared.in_flight = true;
drop(shared);
self.launch(batch);
} else if batch.writes.is_empty() {
batch.db_tx.rollback();
} else {
shared.forming = Some(batch);
}
Ok(Staged {
ids,
ours,
head_before,
contributed,
outcome,
})
}
fn launch(self: &Arc<Self>, batch: Batch) {
let coalescer = Arc::clone(self);
drop(tokio::spawn(async move { coalescer.land(batch).await }));
}
async fn land(self: Arc<Self>, batch: Batch) {
let Batch {
db_tx,
base,
head_before,
head,
writes,
outcome,
..
} = batch;
let landed =
match commit_batch(db_tx, head_before, head, &writes, &base, &self.projections).await {
Ok(Landed::Committed) => Outcome::Committed,
Ok(Landed::LostRace) => Outcome::LostRace,
Err(err) => {
tracing::warn!(error = %err, "commit batch failed to write; its members retry");
Outcome::Nothing
}
};
let _ = outcome.send(Some(landed));
let mut shared = self.shared.lock().await;
shared.in_flight = false;
self.flights
.send_modify(|flight| *flight = flight.wrapping_add(1));
}
async fn seal_abandoned(self: Arc<Self>) {
let mut shared = self.shared.lock().await;
if shared.in_flight || self.arriving.load(Ordering::Acquire) > 0 {
return;
}
let Some(batch) = shared.forming.take() else {
return;
};
if batch.writes.is_empty() {
batch.db_tx.rollback();
return;
}
shared.in_flight = true;
drop(shared);
self.land(batch).await;
}
}
pub(crate) async fn await_outcome(mut outcome: watch::Receiver<Option<Outcome>>) -> Outcome {
loop {
if let Some(landed) = *outcome.borrow_and_update() {
return landed;
}
if outcome.changed().await.is_err() {
return outcome.borrow().unwrap_or(Outcome::Nothing);
}
}
}
struct Arrival {
coalescer: Arc<Coalescer>,
settled: bool,
}
impl Arrival {
fn settle(mut self) -> usize {
self.settled = true;
self.coalescer.arriving.fetch_sub(1, Ordering::AcqRel) - 1
}
}
impl Drop for Arrival {
fn drop(&mut self) {
if self.settled {
return;
}
if self.coalescer.arriving.fetch_sub(1, Ordering::AcqRel) != 1 {
return;
}
if let Ok(runtime) = tokio::runtime::Handle::try_current() {
let coalescer = Arc::clone(&self.coalescer);
drop(runtime.spawn(async move { coalescer.seal_abandoned().await }));
}
}
}