use crate::common;
use crate::common::inbox::{
InsertBusinessRow, InsertThenFail, SelfCompletingHandler, business_row_count,
create_business_table, message,
};
use std::time::Duration;
use reliar_core::{Classify, FailureKind, MessageId};
use reliar_inbox::{InboxClaim, InboxOutcome, InboxProcessError, InboxScope, InboxStore};
use reliar_store_postgres::{PostgresInboxSettings, PostgresInboxStore};
async fn process_on_fresh_key_runs_handler_once_and_completes() {
let pool = common::fresh_db().await;
create_business_table(&pool).await;
let store =
PostgresInboxStore::with_settings(pool.clone(), PostgresInboxSettings::default()).unwrap();
let scope = InboxScope::new("orders-projection").unwrap();
let id = MessageId::new();
let mut tx = pool.begin().await.unwrap();
let outcome = store
.process(
&mut tx,
&scope,
message(id),
&InsertBusinessRow { value: 1 },
)
.await
.unwrap();
assert_eq!(outcome, InboxOutcome::Processed(1));
tx.commit().await.unwrap();
assert_eq!(business_row_count(&pool).await, 1);
let record = store.find(&scope, id).await.unwrap().unwrap();
assert!(record.completed_at.is_some());
}
async fn process_on_completed_key_never_runs_handler() {
let pool = common::fresh_db().await;
create_business_table(&pool).await;
let store =
PostgresInboxStore::with_settings(pool.clone(), PostgresInboxSettings::default()).unwrap();
let scope = InboxScope::new("orders-projection").unwrap();
let id = MessageId::new();
let mut tx = pool.begin().await.unwrap();
store
.process(
&mut tx,
&scope,
message(id),
&InsertBusinessRow { value: 1 },
)
.await
.unwrap();
tx.commit().await.unwrap();
let record_before = store.find(&scope, id).await.unwrap().unwrap();
let mut tx = pool.begin().await.unwrap();
let outcome = store
.process(
&mut tx,
&scope,
message(id),
&InsertBusinessRow { value: 2 },
)
.await
.unwrap();
assert!(matches!(outcome, InboxOutcome::AlreadyCompleted { .. }));
tx.rollback().await.unwrap();
assert_eq!(
business_row_count(&pool).await,
1,
"the redelivery's handler must never run"
);
let record_after = store.find(&scope, id).await.unwrap().unwrap();
assert_eq!(record_before.completed_at, record_after.completed_at);
assert_eq!(record_before.attempts, record_after.attempts);
}
async fn process_with_in_progress_claim_never_runs_handler_and_tx_stays_usable() {
let pool = common::fresh_db().await;
create_business_table(&pool).await;
let store =
PostgresInboxStore::with_settings(pool.clone(), PostgresInboxSettings::default()).unwrap();
let scope = InboxScope::new("orders-projection").unwrap();
let id = MessageId::new();
let mut tx_a = pool.begin().await.unwrap();
store.claim(&mut tx_a, &scope, message(id)).await.unwrap();
let mut tx_b = pool.begin().await.unwrap();
let outcome = tokio::time::timeout(
Duration::from_millis(500),
store.process(
&mut tx_b,
&scope,
message(id),
&InsertBusinessRow { value: 99 },
),
)
.await
.expect("process must return within 500ms instead of blocking on the advisory lock")
.unwrap();
assert_eq!(outcome, InboxOutcome::InProgress);
let other_id = MessageId::new();
let claim = store
.claim(&mut tx_b, &scope, message(other_id))
.await
.unwrap();
assert_eq!(claim, InboxClaim::Claimed { attempt: 1 });
tx_b.rollback().await.unwrap();
tx_a.rollback().await.unwrap();
assert_eq!(
business_row_count(&pool).await,
0,
"InProgress must never run InsertBusinessRow's handler"
);
}
async fn process_when_handler_errors_does_not_complete() {
let pool = common::fresh_db().await;
create_business_table(&pool).await;
let store =
PostgresInboxStore::with_settings(pool.clone(), PostgresInboxSettings::default()).unwrap();
let scope = InboxScope::new("orders-projection").unwrap();
let id = MessageId::new();
let mut tx = pool.begin().await.unwrap();
let err = store
.process(&mut tx, &scope, message(id), &InsertThenFail { value: 1 })
.await
.unwrap_err();
assert!(matches!(err, InboxProcessError::Handler(_)));
assert!(
std::error::Error::source(&err).is_some(),
"the handler's error is the source"
);
let completed_at: Option<time::OffsetDateTime> =
sqlx::query_scalar("SELECT completed_at FROM inbox WHERE scope = $1 AND message_id = $2")
.bind(scope.as_str())
.bind(id.as_uuid())
.fetch_one(&mut *tx)
.await
.unwrap();
assert!(
completed_at.is_none(),
"complete must not run after a handler error"
);
tx.rollback().await.unwrap();
assert_eq!(
business_row_count(&pool).await,
0,
"the handler's own write rolls back with the claim"
);
}
async fn process_store_error_forwards_kind_handler_error_is_transient() {
let pool = common::fresh_db().await;
create_business_table(&pool).await;
let store =
PostgresInboxStore::with_settings(pool.clone(), PostgresInboxSettings::default()).unwrap();
let scope = InboxScope::new("orders-projection").unwrap();
let id = MessageId::new();
let mut tx = pool.begin().await.unwrap();
let handler = SelfCompletingHandler {
store: &store,
scope: scope.clone(),
id,
};
let process_err = store
.process(&mut tx, &scope, message(id), &handler)
.await
.unwrap_err();
match &process_err {
InboxProcessError::Store(err) => assert_eq!(err.kind(), FailureKind::Permanent),
other => panic!("expected InboxProcessError::Store, got {other:?}"),
}
assert_eq!(
process_err.kind(),
FailureKind::Permanent,
"Classify must forward the store's own kind, not override it"
);
tx.rollback().await.unwrap();
let failing_id = MessageId::new();
let mut tx = pool.begin().await.unwrap();
let process_err = store
.process(
&mut tx,
&scope,
message(failing_id),
&InsertThenFail { value: 1 },
)
.await
.unwrap_err();
assert_eq!(process_err.kind(), FailureKind::Transient);
tx.rollback().await.unwrap();
}
async fn rollback_discards_claim_row_so_reclaim_restarts_at_attempt_one() {
let pool = common::fresh_db().await;
create_business_table(&pool).await;
let store =
PostgresInboxStore::with_settings(pool.clone(), PostgresInboxSettings::default()).unwrap();
let scope = InboxScope::new("orders-projection").unwrap();
let id = MessageId::new();
{
let mut tx = pool.begin().await.unwrap();
let outcome = store
.process(
&mut tx,
&scope,
message(id),
&InsertBusinessRow { value: 1 },
)
.await
.unwrap();
assert_eq!(outcome, InboxOutcome::Processed(1));
}
assert!(
store.find(&scope, id).await.unwrap().is_none(),
"the claim row must not survive an uncommitted rollback"
);
assert_eq!(business_row_count(&pool).await, 0);
let mut tx = pool.begin().await.unwrap();
let outcome = store
.process(
&mut tx,
&scope,
message(id),
&InsertBusinessRow { value: 1 },
)
.await
.unwrap();
assert_eq!(outcome, InboxOutcome::Processed(1));
tx.commit().await.unwrap();
assert_eq!(business_row_count(&pool).await, 1);
}
pub(crate) fn trials(rt: &'static tokio::runtime::Runtime) -> Vec<libtest_mimic::Trial> {
vec![
libtest_mimic::Trial::test(
"inbox_process::process_on_fresh_key_runs_handler_once_and_completes",
move || {
rt.block_on(process_on_fresh_key_runs_handler_once_and_completes());
Ok(())
},
),
libtest_mimic::Trial::test(
"inbox_process::process_on_completed_key_never_runs_handler",
move || {
rt.block_on(process_on_completed_key_never_runs_handler());
Ok(())
},
),
libtest_mimic::Trial::test(
"inbox_process::process_with_in_progress_claim_never_runs_handler_and_tx_stays_usable",
move || {
rt.block_on(
process_with_in_progress_claim_never_runs_handler_and_tx_stays_usable(),
);
Ok(())
},
),
libtest_mimic::Trial::test(
"inbox_process::process_when_handler_errors_does_not_complete",
move || {
rt.block_on(process_when_handler_errors_does_not_complete());
Ok(())
},
),
libtest_mimic::Trial::test(
"inbox_process::process_store_error_forwards_kind_handler_error_is_transient",
move || {
rt.block_on(process_store_error_forwards_kind_handler_error_is_transient());
Ok(())
},
),
libtest_mimic::Trial::test(
"inbox_process::rollback_discards_claim_row_so_reclaim_restarts_at_attempt_one",
move || {
rt.block_on(rollback_discards_claim_row_so_reclaim_restarts_at_attempt_one());
Ok(())
},
),
]
}