fix: defer blocked deliveries, claim owner token

This commit is contained in:
2026-10-26 00:33:29 +01:00
parent 53892847f2
commit 0236a488fa
13 changed files with 528 additions and 80 deletions

View File

@@ -159,6 +159,9 @@ impl AckHandle for NoopAckHandle {
async fn nack(&self) -> Result<(), DomainError> {
Ok(())
}
async fn defer(&self, _: std::time::Duration) -> Result<(), DomainError> {
Ok(())
}
}
// ── NoopPersonCommand ─────────────────────────────────────────────────────────

View File

@@ -1,21 +1,36 @@
use async_trait::async_trait;
use chrono::{Duration, NaiveDateTime, Utc};
use chrono::{Duration, NaiveDateTime, Timelike, Utc};
use crate::{
errors::DomainError,
events::EventId,
ports::{Begin, HANDLER_LEASE_SECONDS, ProcessedEventLedger},
ports::{Begin, ClaimToken, HANDLER_LEASE_SECONDS, ProcessedEventLedger},
};
use super::InMemoryStore;
use super::store::LedgerRow;
/// Claim stamps are millisecond-precision UTC, as in the sqlite adapter.
const STAMP: &str = "%Y-%m-%dT%H:%M:%S%.3fZ";
fn now_millis() -> NaiveDateTime {
let now = Utc::now().naive_utc();
now.with_nanosecond(now.nanosecond() / 1_000_000 * 1_000_000)
.expect("truncated nanoseconds are valid")
}
fn token(at: NaiveDateTime) -> ClaimToken {
ClaimToken::new(at.format(STAMP).to_string())
}
/// Mirrors the sqlite adapter: `begin` inserts, or takes over only a stale
/// `in_progress` claim; `(event_id, handler)` is the key.
/// `in_progress` claim; `(event_id, handler)` is the key; `complete` and
/// `release` act only for the token that owns the claim.
#[async_trait]
impl ProcessedEventLedger for InMemoryStore {
async fn begin(&self, id: &EventId, handler: &str) -> Result<Begin, DomainError> {
let now = Utc::now().naive_utc();
let now = now_millis();
let lease = Duration::seconds(HANDLER_LEASE_SECONDS);
let mut rows = self.processed_events.write().unwrap();
let key = (id.value(), handler.to_owned());
match rows.get_mut(&key) {
@@ -28,23 +43,38 @@ impl ProcessedEventLedger for InMemoryStore {
processed_at: now,
},
);
Ok(Begin::Run)
Ok(Begin::Run(token(now)))
}
Some(row) if row.done => Ok(Begin::AlreadyDone),
Some(row) if row.started_at < now - Duration::seconds(HANDLER_LEASE_SECONDS) => {
Some(row) if row.started_at < now - lease => {
row.started_at = now;
Ok(Begin::Run)
row.processed_at = now;
Ok(Begin::Run(token(now)))
}
Some(row) => {
let remaining = (row.started_at + lease - now)
.to_std()
.unwrap_or_default()
.max(std::time::Duration::from_secs(1));
Ok(Begin::InProgressElsewhere {
retry_after: remaining,
})
}
Some(_) => Ok(Begin::InProgressElsewhere),
}
}
async fn complete(&self, id: &EventId, handler: &str) -> Result<(), DomainError> {
async fn complete(
&self,
id: &EventId,
handler: &str,
owner: &ClaimToken,
) -> Result<(), DomainError> {
if let Some(row) = self
.processed_events
.write()
.unwrap()
.get_mut(&(id.value(), handler.to_owned()))
&& token(row.started_at) == *owner
{
row.done = true;
row.processed_at = Utc::now().naive_utc();
@@ -52,10 +82,18 @@ impl ProcessedEventLedger for InMemoryStore {
Ok(())
}
async fn release(&self, id: &EventId, handler: &str) -> Result<(), DomainError> {
async fn release(
&self,
id: &EventId,
handler: &str,
owner: &ClaimToken,
) -> Result<(), DomainError> {
let mut rows = self.processed_events.write().unwrap();
let key = (id.value(), handler.to_owned());
if rows.get(&key).is_some_and(|r| !r.done) {
if rows
.get(&key)
.is_some_and(|r| !r.done && token(r.started_at) == *owner)
{
rows.remove(&key);
}
Ok(())