fix: relay shutdown, claim lease, dead-letter undecodable, dedup window

This commit is contained in:
2026-10-26 00:11:06 +01:00
parent c06d3d9207
commit fa8e94817a
10 changed files with 304 additions and 45 deletions

View File

@@ -190,3 +190,89 @@ mod identity {
assert!(envelope.id().is_none());
}
}
mod lease {
use domain::ports::EventConsumer;
use futures::StreamExt;
use super::{claim_batch, config};
use crate::event_queue::SqliteEventQueue;
async fn pool() -> sqlx::SqlitePool {
let pool = sqlx::sqlite::SqlitePoolOptions::new()
.max_connections(1)
.connect("sqlite::memory:")
.await
.unwrap();
crate::migrate(&pool).await.unwrap();
pool
}
async fn claim_then_age(pool: &sqlx::SqlitePool, claimed_at: Option<&str>) {
super::insert(pool, 1).await;
assert_eq!(claim_batch(pool, &config(10)).await.unwrap().len(), 1);
sqlx::query("UPDATE event_queue SET claimed_at = ?")
.bind(claimed_at)
.execute(pool)
.await
.unwrap();
}
fn ago(seconds: i64) -> String {
(chrono::Utc::now() - chrono::Duration::seconds(seconds))
.format("%Y-%m-%dT%H:%M:%SZ")
.to_string()
}
#[tokio::test]
async fn an_expired_claim_is_reclaimed() {
let pool = pool().await;
claim_then_age(&pool, Some(&ago(super::super::CLAIM_LEASE_SECONDS + 60))).await;
let again = claim_batch(&pool, &config(10)).await.unwrap();
assert_eq!(again.len(), 1, "stranded row is claimed again");
let claimed: String = sqlx::query_scalar("SELECT claimed_at FROM event_queue")
.fetch_one(&pool)
.await
.unwrap();
assert!(claimed > ago(60), "lease restarted: {claimed}");
}
#[tokio::test]
async fn a_legacy_processing_row_without_claimed_at_counts_as_expired() {
let pool = pool().await;
claim_then_age(&pool, None).await;
assert_eq!(claim_batch(&pool, &config(10)).await.unwrap().len(), 1);
}
#[tokio::test]
async fn a_fresh_claim_is_not_reclaimed() {
let pool = pool().await;
claim_then_age(&pool, Some(&ago(30))).await;
assert!(claim_batch(&pool, &config(10)).await.unwrap().is_empty());
}
#[tokio::test]
async fn an_undecodable_row_is_dead_lettered_not_left_processing() {
let pool = pool().await;
sqlx::query("INSERT INTO event_queue (event_type, payload) VALUES ('t', 'not json')")
.execute(&pool)
.await
.unwrap();
let q = SqliteEventQueue::create(pool.clone(), config(10));
let first = q.consume().next().await.unwrap();
assert!(first.is_err());
let (status, error): (String, Option<String>) =
sqlx::query_as("SELECT status, last_error FROM event_queue")
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(status, "dead_lettered");
assert!(error.unwrap().contains("undecodable"));
}
}