fix: relay shutdown, claim lease, dead-letter undecodable, dedup window
This commit is contained in:
@@ -5,6 +5,7 @@ mod support;
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
use tokio::sync::Notify;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use domain::{
|
||||
@@ -19,6 +20,9 @@ use uuid::Uuid;
|
||||
#[derive(Default)]
|
||||
struct FakeSink {
|
||||
fail: bool,
|
||||
/// When set, `relay` signals `started` then takes this long.
|
||||
slow: Option<Duration>,
|
||||
started: Notify,
|
||||
seen: Mutex<Vec<Option<EventId>>>,
|
||||
}
|
||||
|
||||
@@ -26,6 +30,10 @@ struct FakeSink {
|
||||
impl EventRelaySink for FakeSink {
|
||||
async fn relay(&self, id: Option<&EventId>, _event: &DomainEvent) -> Result<(), DomainError> {
|
||||
self.seen.lock().unwrap().push(id.cloned());
|
||||
if let Some(delay) = self.slow {
|
||||
self.started.notify_one();
|
||||
tokio::time::sleep(delay).await;
|
||||
}
|
||||
if self.fail {
|
||||
Err(DomainError::InfrastructureError("bus down".into()))
|
||||
} else {
|
||||
@@ -65,6 +73,18 @@ async fn wait_for(pool: &sqlx::SqlitePool, status: &str) {
|
||||
panic!("row never reached {status}: {:?}", row(pool).await);
|
||||
}
|
||||
|
||||
fn start(
|
||||
queue: SqliteEventQueue,
|
||||
sink: Arc<FakeSink>,
|
||||
) -> (
|
||||
tokio::sync::watch::Sender<bool>,
|
||||
tokio::task::JoinHandle<()>,
|
||||
) {
|
||||
let (stop, rx) = tokio::sync::watch::channel(false);
|
||||
let relay = bootstrap::outbox_relay::OutboxRelay::new(Arc::new(queue), sink);
|
||||
(stop, tokio::spawn(relay.run(rx)))
|
||||
}
|
||||
|
||||
fn event() -> DomainEvent {
|
||||
DomainEvent::UserUpdated {
|
||||
user_id: UserId::from_uuid(Uuid::new_v4()),
|
||||
@@ -77,9 +97,10 @@ async fn relay_marks_rows_done_only_after_publish_succeeds() {
|
||||
publisher.publish(&event()).await.unwrap();
|
||||
let sink = Arc::new(FakeSink::default());
|
||||
|
||||
let relay = bootstrap::outbox_relay::spawn(Arc::new(queue), sink.clone());
|
||||
let (stop, relay) = start(queue, sink.clone());
|
||||
wait_for(&pool, "done").await;
|
||||
relay.abort();
|
||||
drop(stop);
|
||||
|
||||
let (_, attempts, event_id) = row(&pool).await;
|
||||
assert_eq!(attempts, 0);
|
||||
@@ -101,7 +122,7 @@ async fn a_failed_publish_leaves_the_row_pending_with_an_attempt_counted() {
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let relay = bootstrap::outbox_relay::spawn(Arc::new(queue), sink.clone());
|
||||
let (stop, relay) = start(queue, sink.clone());
|
||||
for _ in 0..200 {
|
||||
if row(&pool).await.1 == 1 {
|
||||
break;
|
||||
@@ -109,7 +130,35 @@ async fn a_failed_publish_leaves_the_row_pending_with_an_attempt_counted() {
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
relay.abort();
|
||||
drop(stop);
|
||||
|
||||
let (status, attempts, _) = row(&pool).await;
|
||||
assert_eq!((status.as_str(), attempts), ("pending", 1));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn shutdown_lets_the_in_flight_row_finish_then_stops_the_relay() {
|
||||
let (pool, publisher, queue) = outbox().await;
|
||||
publisher.publish(&event()).await.unwrap();
|
||||
publisher.publish(&event()).await.unwrap();
|
||||
let sink = Arc::new(FakeSink {
|
||||
slow: Some(Duration::from_millis(200)),
|
||||
..Default::default()
|
||||
});
|
||||
let (stop, relay) = start(queue, sink.clone());
|
||||
|
||||
sink.started.notified().await;
|
||||
stop.send(true).unwrap();
|
||||
tokio::time::timeout(Duration::from_secs(5), relay)
|
||||
.await
|
||||
.expect("relay stops on shutdown")
|
||||
.unwrap();
|
||||
|
||||
let statuses: Vec<String> = sqlx::query_scalar("SELECT status FROM event_queue ORDER BY id")
|
||||
.fetch_all(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(statuses[0], "done", "in-flight row was completed");
|
||||
assert_ne!(statuses[0], "processing");
|
||||
assert_eq!(sink.seen.lock().unwrap().len(), 1, "no new row after stop");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user