use std::fmt;
use std::sync::atomic::{AtomicU64, Ordering};
use super::codec::DedupRecord;
use super::{DedupCache, fold_page, len_to_u64};
use crate::durability::head_checkpoint::{HEAD_CHECKPOINT_EVERY, READ_BATCH_SIZE};
use crate::durability::{DurabilityError, StoredEntry, head_checkpoint_key};
const HEAD_RECORD_ATTEMPTS: usize = 8;
static HEAD_RECORD_LOST_RACES: AtomicU64 = AtomicU64::new(0);
static HEAD_REFUSALS: AtomicU64 = AtomicU64::new(0);
static HEAD_REPAIRS: AtomicU64 = AtomicU64::new(0);
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct DedupHeadCounts {
pub refusals: u64,
pub record_lost_races: u64,
pub repairs: u64,
}
impl DedupHeadCounts {
#[must_use]
pub const fn named(&self) -> [(&'static str, u64); 3] {
[
("liminal_dedup_head_refusals_total", self.refusals),
(
"liminal_dedup_head_record_lost_races_total",
self.record_lost_races,
),
("liminal_dedup_head_repairs_total", self.repairs),
]
}
}
#[must_use]
pub fn dedup_head_counts() -> DedupHeadCounts {
DedupHeadCounts {
refusals: HEAD_REFUSALS.load(Ordering::Relaxed),
record_lost_races: HEAD_RECORD_LOST_RACES.load(Ordering::Relaxed),
repairs: HEAD_REPAIRS.load(Ordering::Relaxed),
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum HeadRefusal {
BeyondLog { covered: u64 },
WrongSequence { covered: u64, found: u64 },
ForeignKey { covered: u64 },
}
impl fmt::Display for HeadRefusal {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::BeyondLog { covered } => write!(
formatter,
"the recorded head {covered} claims rows the stream does not hold"
),
Self::WrongSequence { covered, found } => write!(
formatter,
"the recorded head {covered} names a row whose sequence is {found}"
),
Self::ForeignKey { covered } => write!(
formatter,
"the recorded head {covered} names a record of another key"
),
}
}
}
impl DedupCache {
pub(super) async fn latest_past_first_page(
&self,
stream_key: &str,
idempotency_key: &str,
first_page: &[StoredEntry],
) -> Result<(Option<DedupRecord>, u64), DurabilityError> {
let recorded = self
.store
.read_value(&head_checkpoint_key(stream_key))
.await?
.unwrap_or(0);
let mut head_refused = false;
if recorded > 0 {
match self
.verified_head(stream_key, idempotency_key, recorded)
.await?
{
Ok(record) => {
let (latest, next) = self
.fold_from(stream_key, idempotency_key, recorded)
.await?;
if next.saturating_sub(recorded) >= HEAD_CHECKPOINT_EVERY {
self.record_head(stream_key, recorded, next).await;
}
return Ok((latest.or(Some(record)), next));
}
Err(refusal) => {
HEAD_REFUSALS.fetch_add(1, Ordering::Relaxed);
tracing::warn!(
stream = stream_key,
reason = %refusal,
"dedup stream head refused; the stream is walked from its first page"
);
head_refused = true;
}
}
}
let first = fold_page(stream_key, idempotency_key, first_page, 0)?;
let (rest, next) = self
.fold_from(stream_key, idempotency_key, len_to_u64(first_page.len())?)
.await?;
if head_refused {
self.repair_head(stream_key, recorded, next).await;
} else {
self.record_head(stream_key, recorded, next).await;
}
Ok((rest.or(first), next))
}
async fn verified_head(
&self,
stream_key: &str,
idempotency_key: &str,
covered: u64,
) -> Result<Result<DedupRecord, HeadRefusal>, DurabilityError> {
let last = covered.saturating_sub(1);
let Some(stored) = self.store.read_at(stream_key, last).await? else {
return Ok(Err(HeadRefusal::BeyondLog { covered }));
};
if stored.sequence != last {
return Ok(Err(HeadRefusal::WrongSequence {
covered,
found: stored.sequence,
}));
}
let record = DedupRecord::deserialize(&stored.payload)?;
if record.idempotency_key() != idempotency_key {
return Ok(Err(HeadRefusal::ForeignKey { covered }));
}
Ok(Ok(record))
}
async fn fold_from(
&self,
stream_key: &str,
idempotency_key: &str,
offset: u64,
) -> Result<(Option<DedupRecord>, u64), DurabilityError> {
let mut latest = None;
let mut next = offset;
loop {
let page = self
.store
.read_from(stream_key, next, READ_BATCH_SIZE)
.await?;
if let Some(last) = fold_page(stream_key, idempotency_key, &page, next)? {
latest = Some(last);
}
next = next.checked_add(len_to_u64(page.len())?).ok_or_else(|| {
DurabilityError::ConfigError(format!("dedup stream {stream_key} offset overflow"))
})?;
if page.len() < READ_BATCH_SIZE {
return Ok((latest, next));
}
}
}
async fn record_head(&self, stream_key: &str, recorded: u64, next: u64) {
let key = head_checkpoint_key(stream_key);
let mut stored = recorded;
for _ in 0..HEAD_RECORD_ATTEMPTS {
if stored >= next {
return;
}
match self.store.cas(&key, stored, next).await {
Ok(()) => return,
Err(DurabilityError::CursorRegression {
stored: observed, ..
}) if observed >= next => {
HEAD_RECORD_LOST_RACES.fetch_add(1, Ordering::Relaxed);
return;
}
Err(DurabilityError::CursorRegression {
stored: observed, ..
}) if observed != stored => stored = observed,
Err(error) => {
tracing::warn!(
stream = stream_key,
%error,
"dedup stream head was not recorded; the next lookup walks again"
);
return;
}
}
}
}
async fn repair_head(&self, stream_key: &str, refused: u64, next: u64) {
if refused == next {
return;
}
if next == 0 {
tracing::warn!(
stream = stream_key,
refused,
"a refused dedup stream head cannot be repaired while its stream is empty"
);
return;
}
match self
.store
.cas(&head_checkpoint_key(stream_key), refused, next)
.await
{
Ok(()) => {
HEAD_REPAIRS.fetch_add(1, Ordering::Relaxed);
}
Err(DurabilityError::CursorRegression { .. }) => {
HEAD_RECORD_LOST_RACES.fetch_add(1, Ordering::Relaxed);
}
Err(error) => {
tracing::warn!(
stream = stream_key,
%error,
"refused dedup stream head was not repaired; the next lookup walks again"
);
}
}
}
}