sqlite ack/nack/defer only if claim still held; use shared backoff

This commit is contained in:
2026-10-26 01:52:28 +01:00
parent dfa5e1a4db
commit fe9f284531
2 changed files with 141 additions and 37 deletions

View File

@@ -303,3 +303,84 @@ mod lease {
assert!(error.unwrap().contains("undecodable"));
}
}
mod stale_handle {
use domain::{
events::{DomainEvent, EventEnvelope},
ports::{EventConsumer, EventPublisher},
value_objects::UserId,
};
use futures::StreamExt;
use super::{claim_batch, config};
use crate::event_queue::{CLAIM_LEASE_SECONDS, SqliteEventQueue};
/// Claims a row, lets its lease lapse, and has another worker reclaim it.
/// Returns the first (now stale) worker's envelope.
async fn reclaimed() -> (sqlx::SqlitePool, EventEnvelope) {
let pool = sqlx::sqlite::SqlitePoolOptions::new()
.max_connections(1)
.connect("sqlite::memory:")
.await
.unwrap();
crate::migrate(&pool).await.unwrap();
let q = SqliteEventQueue::create(pool.clone(), config(10));
q.publish(&DomainEvent::UserUpdated {
user_id: UserId::generate(),
})
.await
.unwrap();
let stale = q.consume().next().await.unwrap().unwrap();
let old = (chrono::Utc::now() - chrono::Duration::seconds(CLAIM_LEASE_SECONDS + 60))
.format("%Y-%m-%dT%H:%M:%SZ")
.to_string();
sqlx::query("UPDATE event_queue SET claimed_at = ?")
.bind(old)
.execute(&pool)
.await
.unwrap();
assert_eq!(claim_batch(&pool, &config(10)).await.unwrap().len(), 1);
// Real reclaims happen a lease later, so their timestamp differs from
// the stale handle's; the claim clock has only second resolution.
let second_claim = (chrono::Utc::now() - chrono::Duration::seconds(5))
.format("%Y-%m-%dT%H:%M:%SZ")
.to_string();
sqlx::query("UPDATE event_queue SET claimed_at = ?")
.bind(second_claim)
.execute(&pool)
.await
.unwrap();
(pool, stale)
}
async fn state(pool: &sqlx::SqlitePool) -> (String, i32) {
sqlx::query_as("SELECT status, attempts FROM event_queue")
.fetch_one(pool)
.await
.unwrap()
}
#[tokio::test]
async fn a_stale_ack_cannot_complete_a_reclaimed_row() {
let (pool, stale) = reclaimed().await;
stale.ack().await.unwrap();
assert_eq!(state(&pool).await, ("processing".into(), 0));
}
#[tokio::test]
async fn a_stale_nack_cannot_spend_an_attempt_on_a_reclaimed_row() {
let (pool, stale) = reclaimed().await;
stale.nack().await.unwrap();
assert_eq!(state(&pool).await, ("processing".into(), 0));
}
#[tokio::test]
async fn a_stale_defer_cannot_release_a_reclaimed_row() {
let (pool, stale) = reclaimed().await;
stale
.defer(std::time::Duration::from_secs(60))
.await
.unwrap();
assert_eq!(state(&pool).await, ("processing".into(), 0));
}
}