use std::convert::Infallible;
use std::env;
use std::sync::Arc;
use ruststream::OutgoingMessage;
use ruststream::memory::prelude::*;
use ruststream::testing::TestApp;
use ruststream_sqlx::dialect::{Column, Form, OutboxDialect, Postgres as PgDialect, TableSpec};
use ruststream_sqlx::outbox::OUTBOX_ID_HEADER;
use ruststream_sqlx::{Outbox, outbox};
use serde::{Deserialize, Serialize};
use sqlx::{AssertSqlSafe, PgConnection, PgPool, Postgres};
use ruststream::{Broker, Bytes, ConnectedBroker, HeaderMap, PublishPolicy, Publisher};
use super::{MESSAGES, WARMUP, blocks, live};
const SWITCH: &str = "RUSTSTREAM_SQLX_OUTBOX";
const ID_TEXT: u64 = 1;
const INSERT: &str = "INSERT INTO outbox (name, payload) VALUES ($1, $2) RETURNING id";
const REPLY: &[u8] = br#"{"id":1}"#;
const TABLE: TableSpec<'static> = TableSpec::new("outbox", Column::new("id"), Form::RowLock)
.group(Column::new("name"))
.processed_at(Column::new("processed_at"))
.payload(Column::new("payload"))
.database_clock();
#[derive(Debug, ruststream_sqlx::Outbox, sqlx::FromRow)]
#[outbox(table = "outbox")]
struct Record {
#[field(id)]
id: i64,
#[field(name)]
name: String,
#[field(payload)]
payload: Vec<u8>,
#[field(processed_at)]
processed_at: Option<chrono::DateTime<chrono::Utc>>,
}
impl outbox::Publish<Postgres> for Record {
async fn publish(conn: &mut PgConnection, msg: &OutgoingMessage<'_>) -> sqlx::Result<i64> {
sqlx::query_scalar(INSERT)
.bind(msg.name())
.bind(msg.payload())
.fetch_one(conn)
.await
}
}
#[derive(Serialize, Deserialize, Outgoing)]
#[outgoing(name = "in")]
struct Command {
id: i64,
}
#[derive(Serialize, Deserialize, Outgoing)]
#[outgoing(name = "out")]
struct Reply {
id: i64,
}
#[subscriber("in", reply)]
async fn relay(cmd: &Command) -> Reply {
Reply { id: cmd.id }
}
#[derive(Clone)]
struct Statements(Arc<(String, String)>);
#[derive(Clone, FromRef)]
struct Db {
pool: PgPool,
statements: Statements,
}
#[subscriber("in", reply)]
async fn relay_raw(_cmd: &Command, State(pool): State<PgPool>) -> Reply {
let mut conn = pool.acquire().await.expect("a connection");
let id = sqlx::query_scalar(INSERT)
.bind("out")
.bind(REPLY)
.fetch_one(&mut *conn)
.await
.expect("the insert");
Reply { id }
}
#[subscriber("out")]
async fn sink(_reply: &Reply) -> HandlerOutcome {
HandlerOutcome::ack()
}
#[subscriber("out")]
async fn sink_raw(
reply: &Reply,
State(pool): State<PgPool>,
State(Statements(statements)): State<Statements>,
) -> HandlerOutcome {
let (fetch, mark) = &*statements;
let record: Option<Record> = {
let mut conn = pool.acquire().await.expect("a connection");
sqlx::query_as(AssertSqlSafe(fetch.as_str()))
.bind(reply.id)
.fetch_optional(&mut *conn)
.await
.expect("the fetch")
};
let record = record.expect("the record");
let mut conn = pool.acquire().await.expect("a connection");
sqlx::query(AssertSqlSafe(mark.as_str()))
.bind(record.id)
.execute(&mut *conn)
.await
.expect("the mark");
HandlerOutcome::ack()
}
#[derive(Clone, Copy)]
enum Service {
Bare,
Untracked,
TrackedPublish,
RawPublish,
Tracked,
Raw,
}
async fn cost(pool: &PgPool, service: Service) -> u64 {
let fetch = PgDialect.outbox_fetch(&TABLE).expect("the fetch");
let mark = PgDialect.outbox_mark(&TABLE).expect("the mark");
let state = Db {
pool: pool.clone(),
statements: Statements(Arc::new((fetch.sql().to_owned(), mark.sql().to_owned()))),
};
let app = RustStream::new(AppInfo::new("allocations", "0.0.0"))
.on_startup(async move |()| Ok::<_, Infallible>(state));
let tracking = Outbox::new(pool.clone()).register::<Record>(match service {
Service::Untracked => "elsewhere",
_ => "out",
});
let tb = match service {
Service::Bare => {
TestApp::start(app.with_broker(MemoryBroker::new(), |b| {
b.include(relay).out_reply(Publish);
b.include(sink);
}))
.await
}
Service::Untracked | Service::Tracked => {
TestApp::start(
app.layer(tracking.layer())
.publish_layer(tracking.publish_layer())
.with_broker(MemoryBroker::new(), |b| {
b.include(relay).out_reply(Publish);
b.include(sink);
}),
)
.await
}
Service::TrackedPublish => {
TestApp::start(app.publish_layer(tracking.publish_layer()).with_broker(
MemoryBroker::new(),
|b| {
b.include(relay).out_reply(Publish);
b.include(sink);
},
))
.await
}
Service::RawPublish => {
TestApp::start(app.with_broker(MemoryBroker::new(), |b| {
b.include(relay_raw).out_reply(Publish);
b.include(sink);
}))
.await
}
Service::Raw => {
TestApp::start(app.with_broker(MemoryBroker::new(), |b| {
b.include(relay_raw).out_reply(Publish);
b.include(sink_raw);
}))
.await
}
}
.expect("the service starts");
let mut start = 0;
for command in 0..WARMUP + MESSAGES {
if command == WARMUP {
start = blocks();
}
tb.broker::<MemoryBroker>()
.message(&Command { id: 1 })
.publish()
.await
.expect("the command settles");
}
let per_command = (blocks() - start) / MESSAGES;
tb.shutdown().await.expect("the service stops");
per_command
}
fn tracking_on() -> bool {
if env::var(SWITCH).as_deref() == Ok("on") {
return true;
}
assert!(
env::var(live::REQUIRE_LIVE).map_or(true, |value| value.is_empty()),
"{} is set, so the outbox's allocations must be measured, but {SWITCH} is not `on`",
live::REQUIRE_LIVE,
);
eprintln!("{SWITCH} is not `on`; skipping the outbox's allocations");
false
}
pub(super) async fn assert_outbox(pool: &PgPool) {
if !tracking_on() {
return;
}
let bare = cost(pool, Service::Bare).await;
let untracked = cost(pool, Service::Untracked).await;
dhat::assert_eq!(untracked, bare);
let tracked_publish = cost(pool, Service::TrackedPublish).await;
let raw_publish = cost(pool, Service::RawPublish).await;
let tracked = cost(pool, Service::Tracked).await;
let raw = cost(pool, Service::Raw).await;
dhat::assert!(
tracked - tracked_publish <= raw - raw_publish,
"a tracked delivery allocates {} where the raw loop allocates {}",
tracked - tracked_publish,
raw - raw_publish,
);
assert_tracked_publish(pool).await;
}
async fn assert_tracked_publish(pool: &PgPool) {
let connected = MemoryBroker::new()
.connect()
.await
.expect("the broker connects");
let plain = Publish.pair(&connected).await.expect("the publisher pairs");
let tracking = Outbox::new(pool.clone()).register::<Record>("plain");
let wrapped = tracking.wrap(Publish.pair(&connected).await.expect("the publisher pairs"));
let mut start = 0;
for message in 0..WARMUP + MESSAGES {
if message == WARMUP {
start = blocks();
}
wrapped
.publish(OutgoingMessage::new("plain", b"\x01"), None)
.await
.expect("the tracked publish");
}
let tracked = (blocks() - start) / MESSAGES;
for message in 0..WARMUP + MESSAGES {
if message == WARMUP {
start = blocks();
}
let mut headers = HeaderMap::new();
headers.insert(OUTBOX_ID_HEADER, Bytes::from_static(b"1"));
let msg = OutgoingMessage::new("plain", b"\x01").with_headers(headers);
let mut conn = pool.acquire().await.expect("a connection");
let _id: i64 = sqlx::query_scalar(INSERT)
.bind(msg.name())
.bind(msg.payload())
.fetch_one(&mut *conn)
.await
.expect("the insert");
drop(conn);
plain.publish(msg, None).await.expect("the publish");
}
let raw = (blocks() - start) / MESSAGES;
dhat::assert_eq!(tracked, raw + ID_TEXT);
connected.shutdown().await.expect("the broker shuts down");
}