use std::future::Future;
use sqlx::Pool;
use tokio::runtime::Handle;
use tokio::time::{Instant, MissedTickBehavior};
use tokio_util::sync::CancellationToken;
use crate::inbox::broker::Shared;
use crate::inbox::database::QueueDatabase;
use crate::inbox::engine::{Events, Now, Settled, Settling};
use crate::inbox::queue::Queue;
#[cfg(feature = "testing")]
use crate::inbox::testing::off_clock;
pub(crate) mod claim;
pub(crate) mod settle;
mod slots;
use slots::{Extended, Leases, Resolving, Round};
pub(crate) struct LeaseBook<DB: QueueDatabase, Row: Events<DB>> {
leases: Leases<Row::Id, Row::Token>,
pool: Pool<DB>,
runtime: Handle,
queue: &'static Queue,
now: Now,
#[cfg(feature = "testing")]
in_process: bool,
}
#[derive(Debug)]
pub(crate) struct Slot(usize);
impl Slot {
const fn into_index(self) -> usize {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct LeaseGone;
impl<DB: QueueDatabase, Row: Events<DB>> LeaseBook<DB, Row> {
pub(crate) fn leak(shared: &Shared<DB>, queue: &'static Queue) -> &'static Self {
Box::leak(Box::new(Self {
leases: Leases::default(),
pool: shared.pool.clone(),
runtime: shared.runtime.clone(),
queue,
#[cfg(feature = "testing")]
now: shared.harness.now(),
#[cfg(not(feature = "testing"))]
now: Now::default(),
#[cfg(feature = "testing")]
in_process: shared.harness.in_process(),
}))
}
pub(crate) fn enter(&self, id: &Row::Id, lease: Row::Token) -> Slot {
self.leases.enter(id, lease)
}
pub(crate) fn settling(
&self,
slot: Slot,
) -> impl Future<Output = Result<Row::Token, LeaseGone>> + Send + '_ {
self.leases.settling(slot)
}
pub(crate) fn take_ahead(&self, slot: Slot) -> Result<Row::Token, LeaseGone> {
self.leases.take_ahead(slot)
}
pub(crate) const fn pool(&self) -> &Pool<DB> {
&self.pool
}
pub(crate) const fn runtime(&self) -> &Handle {
&self.runtime
}
async fn round(&'static self, round: Round<Row::Id, Row::Token>) -> Round<Row::Id, Row::Token> {
#[cfg(feature = "testing")]
if self.in_process {
return off_clock(self.logged(round)).await.unwrap_or_default();
}
self.logged(round).await
}
async fn logged(&self, mut round: Round<Row::Id, Row::Token>) -> Round<Row::Id, Row::Token> {
if let Err(error) = self.extend(&mut round).await {
tracing::warn!(
target: "ruststream_sqlx",
subscription = self.queue.name,
table = self.queue.table,
row = self.queue.row,
%error,
"a round of lease extensions failed: the deliveries in work keep the leases they \
hold until the next round",
);
}
round
}
async fn extend(&self, round: &mut Round<Row::Id, Row::Token>) -> Result<(), sqlx::Error> {
if !self.leases.any_held() {
return Ok(());
}
let mut conn = self.pool.acquire().await?;
let next = Row::lease(self.queue, self.now)?.expiry;
if self.leases.mark(next, round) == 0 {
return Ok(());
}
let cx = Settling {
queue: self.queue,
now: self.now,
};
let resolving = Resolving {
leases: &self.leases,
round,
};
let transactional = self.queue.prepared.transactional;
for target in resolving.round.marked_mut() {
if transactional && self.leases.taken_ahead(target.slot) {
continue;
}
let extended = Row::extend(&mut conn, &cx, &target.id, &target.held, &next).await;
target.outcome = Some(match extended {
Ok(Settled::Written | Settled::Untouched) => Extended::Written,
Ok(Settled::Lost) if transactional && self.leases.taken_ahead(target.slot) => {
Extended::Lost
}
Ok(Settled::Lost) => {
tracing::warn!(
target: "ruststream_sqlx",
subscription = self.queue.name,
table = self.queue.table,
row = self.queue.row,
id = ?target.id,
"a lease in work was gone when the keeper came to extend it: it ran out \
and another claim took the row, or the row is gone; the delivery's \
settlement will not take effect",
);
Extended::Lost
}
Err(error) => {
tracing::warn!(
target: "ruststream_sqlx",
subscription = self.queue.name,
table = self.queue.table,
row = self.queue.row,
id = ?target.id,
%error,
"a lease extension failed: the delivery keeps the lease it holds until the \
next round",
);
Extended::Failed
}
});
}
Ok(())
}
}
pub(crate) async fn keep<DB, Row>(book: &'static LeaseBook<DB, Row>, stop: CancellationToken)
where
DB: QueueDatabase,
Row: Events<DB>,
{
let Some(period) = book.queue.lease.map(|lease| lease / 2) else {
return;
};
let mut ticks = tokio::time::interval_at(Instant::now() + period, period);
ticks.set_missed_tick_behavior(MissedTickBehavior::Delay);
let mut round = Round::default();
loop {
tokio::select! {
biased;
() = stop.cancelled() => return,
_ = ticks.tick() => {}
}
round = book.round(round).await;
}
}