use async_trait::async_trait; use crate::{errors::DomainError, events::EventId}; /// How long an `in_progress` claim blocks other deliveries of the same /// (event, handler). Past it the claim is presumed dead (crashed worker) and /// the next `begin` takes it over. Must exceed any handler's runtime. pub const HANDLER_LEASE_SECONDS: i64 = 1800; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Begin { /// The caller now holds the claim and must run the handler, then /// `complete` or `release`. Run, /// The handler already finished this event; skip it. AlreadyDone, /// Another delivery holds a live claim; do not run, retry the delivery later. InProgressElsewhere, } /// Per-(event, handler) claim ledger that stops redelivered events from /// re-running handlers. /// /// Guarantee: two deliveries never run the same handler for the same event /// concurrently (`begin` is atomic across workers). After a crash mid-handler /// the claim lapses after `HANDLER_LEASE_SECONDS` and the handler runs again, /// so delivery is at-least-once and handlers must be idempotent. #[async_trait] pub trait ProcessedEventLedger: Send + Sync { /// Atomically claims (event, handler): inserts a claim if there is none, or /// takes over an `in_progress` claim older than the lease; otherwise /// reports the existing state. async fn begin(&self, id: &EventId, handler: &str) -> Result; /// Marks the claim done: the handler will not run for this event again. async fn complete(&self, id: &EventId, handler: &str) -> Result<(), DomainError>; /// Drops an `in_progress` claim so a retry can run immediately. async fn release(&self, id: &EventId, handler: &str) -> Result<(), DomainError>; /// Removes rows (done, or abandoned `in_progress`) first recorded before /// `cutoff`. Returns the number removed. async fn delete_older_than(&self, cutoff: chrono::NaiveDateTime) -> Result; }