use super::{Request, guard};
use crate::store::{codec, keys};
use crate::timers::{DueTimer, Fired, TimerCtx, TimerHandler, TimerKind, registry::kinds};
use crate::{
Batch, BoxFuture, MaybeSend, MaybeSync, NamespaceStore, Precondition, StoreError, Value,
};
use serde::{Deserialize, Serialize};
use std::sync::{
Arc,
atomic::{AtomicU32, Ordering},
};
#[derive(Debug, Clone)]
pub struct SliceBudget {
used: Arc<AtomicU32>,
limit: u32,
parent: Option<crate::indexed::budget::SliceBudget>,
}
impl SliceBudget {
#[must_use]
pub fn new(limit: u32) -> Self {
Self {
used: Arc::new(AtomicU32::new(0)),
limit,
parent: None,
}
}
#[must_use]
pub fn with_parent(limit: u32, parent: crate::indexed::budget::SliceBudget) -> Self {
Self {
parent: Some(parent),
..Self::new(limit)
}
}
pub fn reset(&self) {
self.used.store(0, Ordering::SeqCst);
}
#[must_use]
pub fn charge(&self, operations: u32) -> bool {
let reserved = self
.used
.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |used| {
used.checked_add(operations)
.filter(|total| *total <= self.limit)
})
.is_ok();
reserved
&& self
.parent
.as_ref()
.is_none_or(|parent| parent.charge_many(operations).is_ok())
}
#[must_use]
pub fn used(&self) -> u32 {
self.used.load(Ordering::SeqCst)
}
}
pub trait PurgeSink: MaybeSend + MaybeSync {
fn deliver<'a>(&'a self, request: &'a Request) -> BoxFuture<'a, Result<(), StoreError>>;
}
pub trait LocalInvalidation: MaybeSend + MaybeSync {
fn invalidate<'a>(
&'a self,
request: &'a Request,
cursor: u32,
budget: &'a SliceBudget,
) -> BoxFuture<'a, Result<Option<u32>, StoreError>>;
fn invalidate_checkpoint<'a>(
&'a self,
request: &'a Request,
checkpoint: &'a [u8],
budget: &'a SliceBudget,
) -> BoxFuture<'a, Result<Option<Vec<u8>>, StoreError>> {
Box::pin(async move {
let cursor = if checkpoint.is_empty() {
0
} else {
u32::from_be_bytes(
checkpoint
.try_into()
.map_err(|_| StoreError::Corrupt("invalid purge cursor".into()))?,
)
};
Ok(self
.invalidate(request, cursor, budget)
.await?
.map(|next| next.to_be_bytes().to_vec()))
})
}
}
#[derive(Debug, Clone, Copy)]
pub struct NoLocalCache;
impl LocalInvalidation for NoLocalCache {
fn invalidate<'a>(
&'a self,
_: &'a Request,
_: u32,
_: &'a SliceBudget,
) -> BoxFuture<'a, Result<Option<u32>, StoreError>> {
Box::pin(async { Ok(None) })
}
}
#[derive(Debug, Default, Serialize, Deserialize)]
struct Progress {
checkpoint: Vec<u8>,
local_done: bool,
attempt: u32,
}
pub struct PurgeDelivery {
local: Arc<dyn LocalInvalidation>,
sink: Option<Arc<dyn PurgeSink>>,
budget: SliceBudget,
}
impl core::fmt::Debug for PurgeDelivery {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("PurgeDelivery")
.field("budget", &self.budget)
.finish_non_exhaustive()
}
}
impl PurgeDelivery {
#[must_use]
pub fn new(
local: Arc<dyn LocalInvalidation>,
sink: Option<Arc<dyn PurgeSink>>,
budget: SliceBudget,
) -> Self {
Self {
local,
sink,
budget,
}
}
fn resume(now: u64, progress: &Progress, failed: bool) -> Result<Fired, StoreError> {
let delay = if failed {
1000u64
.saturating_mul(1u64 << progress.attempt.min(10))
.min(900_000)
} else {
1
};
Ok(Fired::Reschedule {
due_at_ms: now.saturating_add(delay),
value: Value::new(
serde_json::to_vec(progress)
.map_err(|_| StoreError::Corrupt("invalid purge progress".into()))?,
),
batch: Batch::new(),
})
}
}
impl<S: NamespaceStore> TimerHandler<S> for PurgeDelivery {
fn kind(&self) -> TimerKind {
kinds::CACHE_PURGE
}
fn max_per_tick(&self) -> Option<u32> {
Some(1)
}
fn fire<'a>(
&'a self,
ctx: &'a TimerCtx<'a, S>,
timer: &'a DueTimer,
) -> BoxFuture<'a, Result<Fired, StoreError>> {
self.fire_with_local(self.local.as_ref(), ctx, timer)
}
}
impl PurgeDelivery {
pub fn fire_with_local<'a, S: NamespaceStore>(
&'a self,
local: &'a dyn LocalInvalidation,
ctx: &'a TimerCtx<'a, S>,
timer: &'a DueTimer,
) -> BoxFuture<'a, Result<Fired, StoreError>> {
Box::pin(async move {
let id = core::str::from_utf8(&timer.reference)
.map_err(|_| StoreError::Corrupt("invalid purge timer id".into()))?;
let key = keys::cache_purge(id)?;
let Some(value) = ctx.store.get(ctx.partition, &key).await? else {
return Ok(Fired::Done(Batch::new().require(Precondition::Absent(key))));
};
let request: Request = serde_json::from_slice(value.as_bytes())
.map_err(|_| StoreError::Corrupt("invalid purge work".into()))?;
request.validate()?;
if request.purge_id != id {
return Err(StoreError::Corrupt("purge id mismatch".into()));
}
let mut progress: Progress = if timer.value.as_bytes().is_empty() {
Progress::default()
} else {
serde_json::from_slice(timer.value.as_bytes())
.map_err(|_| StoreError::Corrupt("invalid purge progress".into()))?
};
if !progress.local_done {
if progress.checkpoint.len() > 4096 {
return Err(StoreError::Corrupt("oversized purge checkpoint".into()));
}
match local
.invalidate_checkpoint(&request, &progress.checkpoint, &self.budget)
.await
{
Ok(None) => progress.local_done = true,
Ok(Some(cursor)) => {
if cursor.len() > 4096 {
return Err(StoreError::Corrupt("oversized purge checkpoint".into()));
}
progress.checkpoint = cursor;
return Self::resume(ctx.now_ms, &progress, false);
}
Err(_) => {
progress.attempt = progress.attempt.saturating_add(1);
return Self::resume(ctx.now_ms, &progress, true);
}
}
}
if let Some(sink) = &self.sink {
if !self.budget.charge(1) {
return Self::resume(ctx.now_ms, &progress, false);
}
if sink.deliver(&request).await.is_err() {
progress.attempt = progress.attempt.saturating_add(1);
return Self::resume(ctx.now_ms, &progress, true);
}
}
let prior = ctx
.store
.get(ctx.partition, &keys::outcome_backlog())
.await?;
let mut backlog = prior
.as_ref()
.map(codec::decode_backlog)
.transpose()?
.unwrap_or_default();
let size = u64::try_from(key.as_bytes().len() + value.as_bytes().len())
.map_err(|_| StoreError::Corrupt("purge size overflow".into()))?;
backlog.rows = backlog
.rows
.checked_sub(1)
.ok_or_else(|| StoreError::Corrupt("purge backlog underflow".into()))?;
backlog.bytes = backlog
.bytes
.checked_sub(size)
.ok_or_else(|| StoreError::Corrupt("purge backlog underflow".into()))?;
let mut batch = Batch::new()
.require(Precondition::Equals(key.clone(), value))
.require(guard(keys::outcome_backlog(), prior.as_ref()))
.delete(key);
if request.trigger == super::Trigger::Manual {
let audit = crate::admin::plan_system(
ctx.store,
ctx.partition,
"system:timer",
"system:timer/PurgeCacheComplete",
std::slice::from_ref(&request.purge_id),
ctx.now_ms,
)
.await?;
batch.preconditions.extend(audit.preconditions);
batch.writes.extend(audit.writes);
}
batch = batch.put(keys::outcome_backlog(), codec::encode_backlog(&backlog));
Ok(Fired::Done(batch))
})
}
}