use serde::{Serialize, de::DeserializeOwned};
use tokio::sync::{broadcast, mpsc};
use tracing::Instrument;
use std::collections::BTreeSet;
use std::sync::Arc;
use crate::{
config::MailboxConfig,
handle::{OwnedTaskHandle, spawn_supervised},
out::event::PersistentDelivery,
sequence::EventSequence,
tables::{MailboxTables, PersistentEventRows},
};
pub(crate) enum GapFillRequest {
Abandoned(Vec<EventSequence>),
Stalled(EventSequence),
StallCleared,
Historical(Vec<EventSequence>),
}
pub(crate) struct GapFiller {
tx: mpsc::UnboundedSender<GapFillRequest>,
_handle: Arc<OwnedTaskHandle>,
}
impl Clone for GapFiller {
fn clone(&self) -> Self {
Self {
tx: self.tx.clone(),
_handle: self._handle.clone(),
}
}
}
impl GapFiller {
pub fn spawn<P, Tables>(
pool: &sqlx::PgPool,
requests: mpsc::UnboundedReceiver<GapFillRequest>,
tx: mpsc::UnboundedSender<GapFillRequest>,
cache_fill_sender: broadcast::Sender<PersistentDelivery<P>>,
notifier_tx: mpsc::UnboundedSender<(EventSequence, EventSequence)>,
config: &MailboxConfig,
) -> Self
where
P: Serialize + DeserializeOwned + Send + Sync + 'static,
Tables: MailboxTables,
{
let task = GapFillerTask::<P, Tables> {
pool: pool.clone(),
cache_fill_sender,
notifier_tx,
grace: config.gap_fill_grace,
batch_limit: config.gap_fill_batch_limit,
page_size: config.backfill_page_size.max(1),
stall: None,
historical: BTreeSet::new(),
historical_marker: None,
historical_proven: false,
historical_due: None,
_tables: std::marker::PhantomData,
};
let handle = spawn_supervised("obix::gap_filler", task.run(requests));
Self {
tx,
_handle: Arc::new(OwnedTaskHandle::new(handle)),
}
}
pub fn report_sender(&self) -> mpsc::UnboundedSender<GapFillRequest> {
self.tx.clone()
}
}
struct StallEpisode {
from: u64,
next_due: tokio::time::Instant,
marker: Option<(String, EventSequence)>,
}
struct GapFillerTask<P, Tables>
where
P: Serialize + DeserializeOwned + Send + Sync + 'static,
{
pool: sqlx::PgPool,
cache_fill_sender: broadcast::Sender<PersistentDelivery<P>>,
notifier_tx: mpsc::UnboundedSender<(EventSequence, EventSequence)>,
grace: std::time::Duration,
batch_limit: usize,
page_size: usize,
stall: Option<StallEpisode>,
historical: BTreeSet<u64>,
historical_marker: Option<String>,
historical_proven: bool,
historical_due: Option<tokio::time::Instant>,
_tables: std::marker::PhantomData<Tables>,
}
impl<P, Tables> GapFillerTask<P, Tables>
where
P: Serialize + DeserializeOwned + Send + Sync + 'static,
Tables: MailboxTables,
{
const REFILL_INTERVAL: std::time::Duration = std::time::Duration::from_secs(1);
const PROOF_POLL_INTERVAL: std::time::Duration = std::time::Duration::from_millis(250);
const COMPENSATION_RETRY_INTERVAL: std::time::Duration = std::time::Duration::from_millis(500);
const COMPENSATION_MAX_ATTEMPTS: u32 = 5;
async fn run(mut self, mut requests: mpsc::UnboundedReceiver<GapFillRequest>) {
loop {
let deadline = self.next_deadline();
tokio::select! {
request = requests.recv() => match request {
Some(request) => self.accept(request),
None => return,
},
_ = async {
match deadline {
Some(deadline) => tokio::time::sleep_until(deadline).await,
None => std::future::pending::<()>().await,
}
} => {}
}
while let Ok(request) = requests.try_recv() {
self.accept(request);
}
self.step().await;
}
}
fn next_deadline(&self) -> Option<tokio::time::Instant> {
[
self.historical_due,
self.stall.as_ref().map(|stall| stall.next_due),
]
.into_iter()
.flatten()
.min()
}
fn accept(&mut self, request: GapFillRequest) {
let now = tokio::time::Instant::now();
match request {
GapFillRequest::Abandoned(sequences) => {
self.spawn_compensation(sequences);
}
GapFillRequest::Stalled(stalled_on) => {
let from = u64::from(stalled_on);
let covered = self.stall.as_ref().is_some_and(|stall| {
let window_end = match &stall.marker {
Some((_, head)) => u64::from(*head).min(stall.from + self.page_size as u64),
None => stall.from + self.page_size as u64,
};
from < window_end
});
if !covered {
self.stall = Some(StallEpisode {
from,
next_due: now + self.grace,
marker: None,
});
}
}
GapFillRequest::StallCleared => {
self.stall = None;
}
GapFillRequest::Historical(sequences) => {
self.historical.extend(sequences.into_iter().map(u64::from));
self.historical_due.get_or_insert(now);
}
}
}
async fn step(&mut self) {
let now = tokio::time::Instant::now();
let stall_batch = if self
.stall
.as_ref()
.is_some_and(|stall| stall.next_due <= now)
{
self.prepare_stall_batch().await
} else {
Vec::new()
};
let historical_batch = if self.historical_due.is_some_and(|due| due <= now) {
self.prepare_historical_batch(self.batch_limit.saturating_sub(stall_batch.len()))
.await
} else {
Vec::new()
};
self.locked_fill(stall_batch, historical_batch).await;
}
fn spawn_compensation(&self, sequences: Vec<EventSequence>) {
let pool = self.pool.clone();
let cache_fill_sender = self.cache_fill_sender.clone();
let notifier_tx = self.notifier_tx.clone();
tokio::spawn(async move {
let mut attempts = 0;
loop {
match Tables::fill_gaps::<P>(&pool, sequences.clone()).await {
Ok(placeholders) => {
deliver_and_notify(&cache_fill_sender, ¬ifier_tx, placeholders);
return;
}
Err(error) => {
attempts += 1;
record_compensation_failed(&error, attempts);
if attempts >= Self::COMPENSATION_MAX_ATTEMPTS {
return;
}
tokio::time::sleep(Self::COMPENSATION_RETRY_INTERVAL).await;
}
}
}
});
}
async fn prepare_stall_batch(&mut self) -> Vec<EventSequence> {
let now = tokio::time::Instant::now();
let pool = self.pool.clone();
let Some(stall) = self.stall.as_mut() else {
return Vec::new();
};
stall.next_due = now + Self::REFILL_INTERVAL;
let (marker, head) = match &stall.marker {
Some((marker, head)) => (marker.clone(), *head),
None => match Tables::abandonment_marker(&pool).await {
Ok((marker, head)) => {
stall.marker = Some((marker.clone(), head));
(marker, head)
}
Err(error) => {
record_gap_fill_failed(&error);
return Vec::new();
}
},
};
let from = stall.from;
let fill_up_to = u64::from(head).min(from + self.page_size as u64);
let page_span = tracing::info_span!(
"obix.gap_filler.stall_page",
from = from,
limit = self.page_size,
fill_up_to = fill_up_to,
rows = tracing::field::Empty,
missing = tracing::field::Empty,
);
let events =
match Tables::load_next_page::<P>(&pool, EventSequence::from(from), self.page_size)
.instrument(page_span.clone())
.await
{
Ok(events) => events,
Err(error) => {
record_gap_fill_failed(&error);
return Vec::new();
}
};
page_span.record("rows", events.len());
let mut present = std::collections::HashSet::new();
for item in events {
let delivery = PersistentDelivery::from(item);
present.insert(u64::from(delivery.sequence()));
let _ = self.cache_fill_sender.send(delivery);
}
let missing: Vec<EventSequence> = ((from + 1)..=fill_up_to)
.filter(|sequence| !present.contains(sequence))
.take(self.batch_limit)
.map(EventSequence::from)
.collect();
page_span.record("missing", missing.len());
if missing.is_empty() {
self.stall = None;
return Vec::new();
}
match Tables::abandonment_proof_passed(&pool, &marker).await {
Ok(true) => missing,
Ok(false) => Vec::new(),
Err(error) => {
record_gap_fill_failed(&error);
Vec::new()
}
}
}
async fn prepare_historical_batch(&mut self, capacity: usize) -> Vec<EventSequence> {
let now = tokio::time::Instant::now();
if self.historical.is_empty() {
self.historical_due = None;
return Vec::new();
}
if capacity == 0 {
return Vec::new();
}
if !self.historical_proven {
let marker = match self.historical_marker.clone() {
Some(marker) => marker,
None => match Tables::abandonment_marker(&self.pool).await {
Ok((marker, _)) => {
self.historical_marker = Some(marker.clone());
marker
}
Err(error) => {
record_gap_fill_failed(&error);
self.historical_due = Some(now + Self::REFILL_INTERVAL);
return Vec::new();
}
},
};
match Tables::abandonment_proof_passed(&self.pool, &marker).await {
Ok(true) => self.historical_proven = true,
Ok(false) => {
self.historical_due = Some(now + Self::PROOF_POLL_INTERVAL);
return Vec::new();
}
Err(error) => {
record_gap_fill_failed(&error);
self.historical_due = Some(now + Self::REFILL_INTERVAL);
return Vec::new();
}
}
}
self.historical
.iter()
.take(capacity)
.copied()
.map(EventSequence::from)
.collect()
}
async fn locked_fill(
&mut self,
stall_batch: Vec<EventSequence>,
historical_batch: Vec<EventSequence>,
) {
if stall_batch.is_empty() && historical_batch.is_empty() {
return;
}
let now = tokio::time::Instant::now();
let mut combined = stall_batch.clone();
combined.extend(historical_batch.iter().copied());
match Tables::fill_gaps_deduped::<P>(&self.pool, combined).await {
Ok(Some(placeholders)) => {
for sequence in &historical_batch {
self.historical.remove(&u64::from(*sequence));
}
if !historical_batch.is_empty() {
self.historical_due = if self.historical.is_empty() {
None
} else {
Some(now)
};
}
let stall_set: std::collections::HashSet<u64> =
stall_batch.iter().map(|s| u64::from(*s)).collect();
let mut stall_range: Option<(EventSequence, EventSequence)> = None;
for item in placeholders {
let delivery = PersistentDelivery::from(item);
let sequence = delivery.sequence();
if stall_set.contains(&u64::from(sequence)) {
stall_range = Some(match stall_range {
Some((lo, hi)) => (lo.min(sequence), hi.max(sequence)),
None => (sequence, sequence),
});
}
let _ = self.cache_fill_sender.send(delivery);
}
if let Some(range) = stall_range {
let _ = self.notifier_tx.send(range);
}
}
Ok(None) => {
if !historical_batch.is_empty() {
self.historical_due = Some(now + Self::PROOF_POLL_INTERVAL);
}
}
Err(error) => {
record_gap_fill_failed(&error);
if !historical_batch.is_empty() {
self.historical_due = Some(now + Self::REFILL_INTERVAL);
}
}
}
}
}
fn deliver_and_notify<P>(
cache_fill_sender: &broadcast::Sender<PersistentDelivery<P>>,
notifier_tx: &mpsc::UnboundedSender<(EventSequence, EventSequence)>,
rows: PersistentEventRows<P>,
) where
P: Serialize + DeserializeOwned + Send + Sync + 'static,
{
let mut range: Option<(EventSequence, EventSequence)> = None;
for item in rows {
let delivery = PersistentDelivery::from(item);
let sequence = delivery.sequence();
range = Some(match range {
Some((lo, hi)) => (lo.min(sequence), hi.max(sequence)),
None => (sequence, sequence),
});
let _ = cache_fill_sender.send(delivery);
}
if let Some(range) = range {
let _ = notifier_tx.send(range);
}
}
#[tracing::instrument(
name = "obix.gap_filler.compensation_failed",
level = "warn",
skip_all,
fields(error = %error, attempts = attempts),
)]
fn record_compensation_failed(error: &sqlx::Error, attempts: u32) {}
#[tracing::instrument(
name = "obix.gap_filler.fill_failed",
level = "warn",
skip_all,
fields(error = %error),
)]
fn record_gap_fill_failed(error: &sqlx::Error) {}