sqlite ack/nack/defer only if claim still held; use shared backoff
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user