use crate::common;
use crate::common::OrderCreated;
use reliar_core::{Envelope, MessageId, MessageType, Metadata, Publisher as _, Serializer as _};
use reliar_outbox::{OutboxPolicy, OutboxPublisher, RecordingPublisher};
use reliar_store_postgres::{PostgresOutboxSettings, PostgresOutboxStore};
fn serialize(envelope: Envelope<OrderCreated>) -> reliar_core::SerializedEnvelope {
let ser = reliar_core::JsonSerializer;
let bytes = ser.serialize(&envelope.body).unwrap();
let mut out = envelope.map_body(|_| bytes);
out.metadata.delivery.content_type = ser.content_type().clone();
out
}
fn build_outbox(
store: PostgresOutboxStore,
) -> OutboxPublisher<PostgresOutboxStore, RecordingPublisher> {
OutboxPublisher::new(
store,
RecordingPublisher::default(),
OutboxPolicy::default(),
)
}
async fn stage_is_a_publisher_and_its_future_is_send_through_tokio_spawn() {
let pool = common::fresh_db().await;
let store = PostgresOutboxStore::new(pool.clone()).await.unwrap();
let outbox = build_outbox(store);
let serialized = serialize(Envelope::builder(OrderCreated { order_id: 7 }).build());
let id = serialized.id;
let mut tx = pool.begin().await.unwrap();
tokio::spawn(async move {
outbox
.in_transaction(&mut tx)
.publish(&serialized)
.await
.unwrap();
tx.commit().await.unwrap();
})
.await
.unwrap();
let count: i64 = sqlx::query_scalar("SELECT count(*) FROM outbox WHERE id = $1")
.bind(id.as_uuid())
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(count, 1);
}
async fn commit_makes_the_row_visible() {
let pool = common::fresh_db().await;
let store = PostgresOutboxStore::new(pool.clone()).await.unwrap();
let outbox = build_outbox(store);
let serialized = serialize(Envelope::builder(OrderCreated { order_id: 1 }).build());
let id = serialized.id;
let mut tx = pool.begin().await.unwrap();
outbox
.in_transaction(&mut tx)
.publish(&serialized)
.await
.unwrap();
let count_before: i64 = sqlx::query_scalar("SELECT count(*) FROM outbox")
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(
count_before, 0,
"not visible to another connection before commit"
);
tx.commit().await.unwrap();
let count_after: i64 = sqlx::query_scalar("SELECT count(*) FROM outbox WHERE id = $1")
.bind(id.as_uuid())
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(count_after, 1);
}
async fn rollback_leaves_nothing() {
let pool = common::fresh_db().await;
let store = PostgresOutboxStore::new(pool.clone()).await.unwrap();
let outbox = build_outbox(store);
let serialized = serialize(Envelope::builder(OrderCreated { order_id: 2 }).build());
let mut tx = pool.begin().await.unwrap();
outbox
.in_transaction(&mut tx)
.publish(&serialized)
.await
.unwrap();
tx.rollback().await.unwrap();
let count: i64 = sqlx::query_scalar("SELECT count(*) FROM outbox")
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(count, 0);
}
async fn content_type_is_the_envelopes_not_the_stores() {
let pool = common::fresh_db().await;
let store = PostgresOutboxStore::new(pool.clone()).await.unwrap();
let outbox = build_outbox(store);
let envelope = Envelope::builder(OrderCreated { order_id: 3 }).build();
let vnd = common::TestVndSerializer;
let bytes = vnd.serialize(&envelope.body).unwrap();
let mut serialized = envelope.map_body(|_| bytes);
serialized.metadata.delivery.content_type = vnd.content_type().clone();
let id = serialized.id;
let mut tx = pool.begin().await.unwrap();
outbox
.in_transaction(&mut tx)
.publish(&serialized)
.await
.unwrap();
tx.commit().await.unwrap();
let content_type: String = sqlx::query_scalar("SELECT content_type FROM outbox WHERE id = $1")
.bind(id.as_uuid())
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(
content_type,
vnd.content_type().as_str(),
"must be the caller's own content type, not the store's default JSON"
);
assert_ne!(content_type, reliar_core::ContentType::JSON.as_str());
}
async fn duplicate_message_id_is_rejected() {
let pool = common::fresh_db().await;
let store = PostgresOutboxStore::new(pool.clone()).await.unwrap();
let outbox = build_outbox(store);
let serialized = serialize(Envelope::builder(OrderCreated { order_id: 4 }).build());
let mut tx = pool.begin().await.unwrap();
outbox
.in_transaction(&mut tx)
.publish(&serialized)
.await
.unwrap();
tx.commit().await.unwrap();
let mut tx = pool.begin().await.unwrap();
let result = outbox.in_transaction(&mut tx).publish(&serialized).await;
let err = result.expect_err("a reused MessageId must be rejected");
match err {
reliar_outbox::RouteError::Stage(reliar_store_postgres::EnqueueError::Duplicate { id }) => {
assert_eq!(id, serialized.id);
}
other => panic!("expected RouteError::Stage(EnqueueError::Duplicate), got {other:?}"),
}
}
async fn stage_persists_the_envelopes_own_message_type_not_a_rust_types_constants() {
let pool = common::fresh_db().await;
let store = PostgresOutboxStore::new(pool.clone()).await.unwrap();
let outbox = build_outbox(store);
let message_type = MessageType::new("a.type.no.rust.struct.declares", 7);
let serialized = reliar_core::SerializedEnvelope::from_parts(
MessageId::new(),
message_type.clone(),
bytes::Bytes::from_static(b"{}"),
Metadata::default(),
None,
);
let mut tx = pool.begin().await.unwrap();
outbox
.in_transaction(&mut tx)
.publish(&serialized)
.await
.unwrap();
tx.commit().await.unwrap();
let row: (String, i32) =
sqlx::query_as("SELECT message_type, message_version FROM outbox WHERE id = $1")
.bind(serialized.id.as_uuid())
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(row.0, message_type.name());
assert_eq!(row.1, i32::from(message_type.version()));
}
async fn stage_honours_a_non_default_schema() {
const CUSTOM_SCHEMA: &str = "acme_reliar_routing";
let base = common::fresh_unmigrated_db().await;
reliar_store_postgres::migrate(
&base,
reliar_store_postgres::MigrateOptions::default().schema(CUSTOM_SCHEMA),
)
.await
.expect("migrate into a non-default schema");
let base_options = base.connect_options().as_ref().clone();
let scoped_pool = sqlx::PgPool::connect_with(
base_options.options([("search_path", &format!("{CUSTOM_SCHEMA},public"))]),
)
.await
.unwrap();
let settings = PostgresOutboxSettings::default().schema(CUSTOM_SCHEMA);
let store = PostgresOutboxStore::with_settings(scoped_pool.clone(), settings)
.await
.expect("construction succeeds when search_path matches the configured schema");
let outbox = build_outbox(store);
let serialized = serialize(Envelope::builder(OrderCreated { order_id: 5 }).build());
let id = serialized.id;
let mut tx = scoped_pool.begin().await.unwrap();
outbox
.in_transaction(&mut tx)
.publish(&serialized)
.await
.expect("stage succeeds in the custom schema");
tx.commit().await.unwrap();
let count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
"SELECT count(*) FROM {CUSTOM_SCHEMA}.outbox WHERE id = $1"
)))
.bind(id.as_uuid())
.fetch_one(&scoped_pool)
.await
.unwrap();
assert_eq!(
count, 1,
"the row must land in the configured custom schema"
);
}
async fn stage_with_enqueue_sets_search_path_restores_the_callers_value() {
let pool = common::fresh_db().await;
let settings = PostgresOutboxSettings::default().enqueue_sets_search_path(true);
let store = PostgresOutboxStore::with_settings(pool.clone(), settings)
.await
.unwrap();
let outbox = build_outbox(store);
let mut tx = pool.begin().await.unwrap();
let before: String = sqlx::query_scalar("SELECT current_setting('search_path')")
.fetch_one(&mut *tx)
.await
.unwrap();
let serialized = serialize(Envelope::builder(OrderCreated { order_id: 6 }).build());
outbox
.in_transaction(&mut tx)
.publish(&serialized)
.await
.unwrap();
let after: String = sqlx::query_scalar("SELECT current_setting('search_path')")
.fetch_one(&mut *tx)
.await
.unwrap();
assert_eq!(
before, after,
"stage() must restore the caller's search_path"
);
tx.commit().await.unwrap();
}
async fn positional_ok_is_not_durability_a_later_stage_failure_aborts_the_whole_transaction() {
let pool = common::fresh_db().await;
let store = PostgresOutboxStore::new(pool.clone()).await.unwrap();
let outbox = build_outbox(store);
let already_committed = serialize(Envelope::builder(OrderCreated { order_id: 90 }).build());
let mut seed_tx = pool.begin().await.unwrap();
outbox
.in_transaction(&mut seed_tx)
.publish(&already_committed)
.await
.unwrap();
seed_tx.commit().await.unwrap();
let first = serialize(Envelope::builder(OrderCreated { order_id: 91 }).build());
let second = serialize(Envelope::builder(OrderCreated { order_id: 92 }).build());
let third_reused_id = already_committed.clone();
let mut tx = pool.begin().await.unwrap();
let results = outbox
.in_transaction(&mut tx)
.publish_batch(&[first.clone(), second.clone(), third_reused_id])
.await;
assert!(results[0].is_ok(), "the first statement was accepted");
assert!(results[1].is_ok(), "the second statement was accepted");
assert!(
results[2].is_err(),
"the third reuses a committed id and aborts the transaction"
);
tx.rollback().await.unwrap();
for id in [first.id, second.id] {
let count: i64 = sqlx::query_scalar("SELECT count(*) FROM outbox WHERE id = $1")
.bind(id.as_uuid())
.fetch_one(&pool)
.await
.unwrap();
assert_eq!(
count, 0,
"an earlier positional Ok must not survive the transaction's abort"
);
}
}
pub(crate) fn trials(rt: &'static tokio::runtime::Runtime) -> Vec<libtest_mimic::Trial> {
vec![
libtest_mimic::Trial::test(
"routing_enqueue::stage_is_a_publisher_and_its_future_is_send_through_tokio_spawn",
move || {
rt.block_on(stage_is_a_publisher_and_its_future_is_send_through_tokio_spawn());
Ok(())
},
),
libtest_mimic::Trial::test("routing_enqueue::commit_makes_the_row_visible", move || {
rt.block_on(commit_makes_the_row_visible());
Ok(())
}),
libtest_mimic::Trial::test("routing_enqueue::rollback_leaves_nothing", move || {
rt.block_on(rollback_leaves_nothing());
Ok(())
}),
libtest_mimic::Trial::test(
"routing_enqueue::content_type_is_the_envelopes_not_the_stores",
move || {
rt.block_on(content_type_is_the_envelopes_not_the_stores());
Ok(())
},
),
libtest_mimic::Trial::test(
"routing_enqueue::duplicate_message_id_is_rejected",
move || {
rt.block_on(duplicate_message_id_is_rejected());
Ok(())
},
),
libtest_mimic::Trial::test(
"routing_enqueue::stage_persists_the_envelopes_own_message_type_not_a_rust_types_constants",
move || {
rt.block_on(
stage_persists_the_envelopes_own_message_type_not_a_rust_types_constants(),
);
Ok(())
},
),
libtest_mimic::Trial::test(
"routing_enqueue::stage_honours_a_non_default_schema",
move || {
rt.block_on(stage_honours_a_non_default_schema());
Ok(())
},
),
libtest_mimic::Trial::test(
"routing_enqueue::stage_with_enqueue_sets_search_path_restores_the_callers_value",
move || {
rt.block_on(stage_with_enqueue_sets_search_path_restores_the_callers_value());
Ok(())
},
),
libtest_mimic::Trial::test(
"routing_enqueue::positional_ok_is_not_durability_a_later_stage_failure_aborts_the_whole_transaction",
move || {
rt.block_on(
positional_ok_is_not_durability_a_later_stage_failure_aborts_the_whole_transaction(),
);
Ok(())
},
),
]
}