fix: per-handler claim lease in ledger, nats max_deliver, pin handler names

This commit is contained in:
2026-10-26 00:24:56 +01:00
parent 5681d0c034
commit 53892847f2
14 changed files with 489 additions and 118 deletions

View File

@@ -10,11 +10,13 @@ async fn removes_only_rows_older_than_the_retention_window() {
store.seed_processed_event(
&stale,
"h",
true,
now - chrono::Duration::days(PROCESSED_EVENTS_RETENTION_DAYS + 1),
);
store.seed_processed_event(
&fresh,
"h",
true,
now - chrono::Duration::days(PROCESSED_EVENTS_RETENTION_DAYS - 1),
);
@@ -23,9 +25,9 @@ async fn removes_only_rows_older_than_the_retention_window() {
.await
.unwrap();
use domain::ports::ProcessedEventLedger;
assert!(!store.has_processed(&stale, "h").await.unwrap());
assert!(store.has_processed(&fresh, "h").await.unwrap());
use domain::ports::{Begin, ProcessedEventLedger};
assert_eq!(store.begin(&stale, "h").await.unwrap(), Begin::Run);
assert_eq!(store.begin(&fresh, "h").await.unwrap(), Begin::AlreadyDone);
}
#[test]

View File

@@ -477,3 +477,83 @@ async fn duplicate_handler_names_are_rejected() {
let err = result.err().expect("duplicate names must be rejected");
assert!(err.to_string().contains("same"), "{err}");
}
#[tokio::test]
async fn a_handler_already_running_elsewhere_is_not_run_concurrently() {
use domain::ports::{Begin, ProcessedEventLedger};
let (handler, calls) = CountingHandler::new("slow", 0);
let store = InMemoryStore::new();
let id = domain::events::EventId::new();
// Another delivery holds a live claim on this (event, handler).
assert_eq!(store.begin(&id, "slow").await.unwrap(), Begin::Run);
let nacks = Arc::new(Mutex::new(0));
let consumer = RedeliveringConsumer {
id: Some(id),
event: movie_discovered(),
times: 1,
nacks: Arc::clone(&nacks),
};
WorkerService::new(Arc::new(consumer), vec![handler], store)
.unwrap()
.run(tokio::sync::watch::channel(false).1)
.await;
assert_eq!(
*calls.lock().unwrap(),
0,
"must not run beside the claim holder"
);
assert_eq!(*nacks.lock().unwrap(), 1, "delivery is retried later");
}
#[tokio::test]
async fn a_stale_in_progress_claim_is_taken_over() {
use domain::ports::HANDLER_LEASE_SECONDS;
let (handler, calls) = CountingHandler::new("crashed", 0);
let store = InMemoryStore::new();
let id = domain::events::EventId::new();
store.seed_processed_event(
&id,
"crashed",
false,
chrono::Utc::now().naive_utc() - chrono::Duration::seconds(HANDLER_LEASE_SECONDS + 1),
);
let nacks = Arc::new(Mutex::new(0));
let consumer = RedeliveringConsumer {
id: Some(id),
event: movie_discovered(),
times: 1,
nacks: Arc::clone(&nacks),
};
WorkerService::new(Arc::new(consumer), vec![handler], store)
.unwrap()
.run(tokio::sync::watch::channel(false).1)
.await;
assert_eq!(*calls.lock().unwrap(), 1);
assert_eq!(*nacks.lock().unwrap(), 0);
}
#[tokio::test]
async fn a_failed_handler_releases_its_claim_so_redelivery_retries() {
let (handler, calls) = CountingHandler::new("flaky", 1);
let store = InMemoryStore::new();
let nacks = Arc::new(Mutex::new(0));
let consumer = RedeliveringConsumer {
id: Some(domain::events::EventId::new()),
event: movie_discovered(),
times: 3,
nacks: Arc::clone(&nacks),
};
WorkerService::new(Arc::new(consumer), vec![handler], store.clone())
.unwrap()
.run(tokio::sync::watch::channel(false).1)
.await;
// Delivery 1 fails and releases; 2 succeeds; 3 is skipped as done.
assert_eq!(*calls.lock().unwrap(), 2);
assert_eq!(*nacks.lock().unwrap(), 1);
}