#[cfg(feature = "inbox")]
use std::any::Any as Dynamic;
use std::any::type_name;
use std::error::Error as StdError;
use std::fmt::{self, Display};
use std::io::Write;
use std::mem;
use std::str::{self, FromStr};
use std::sync::Arc;
use ruststream::runtime::{Outgoing, PublishLayer, PublishNext, PublishPipeline};
use ruststream::{Bytes, HeaderMap, OutgoingMessage, Publisher};
use sqlx::Database;
#[cfg(feature = "inbox")]
use sqlx::any::AnyConnectOptions;
#[cfg(feature = "inbox")]
use sqlx::error::DatabaseError;
#[cfg(all(feature = "inbox", feature = "sqlite"))]
use sqlx::sqlite::SqliteConnectOptions;
#[cfg(feature = "inbox")]
use sqlx::{Pool, SqlStr};
#[cfg(feature = "inbox")]
use sqlx_core::transaction::TransactionManager;
#[cfg(feature = "inbox")]
use tokio::time::{error::Elapsed, timeout};
use super::OUTBOX_ID_HEADER;
use super::error::OutboxError;
use super::events::Publish;
use super::registry::RecordList;
use super::switch::enabled;
#[cfg(feature = "inbox")]
use crate::inbox::form::advisory::database_of;
#[cfg(feature = "inbox")]
use crate::inbox::writers::{self, Holding};
use crate::outbox::store::Store;
pub struct TrackingPublishLayer<DB: Database, Records> {
store: Arc<Store<DB>>,
records: Records,
}
impl<DB: Database, Records> TrackingPublishLayer<DB, Records> {
pub(super) const fn new(store: Arc<Store<DB>>, records: Records) -> Self {
Self { store, records }
}
}
impl<DB: Database, Records: Copy> Clone for TrackingPublishLayer<DB, Records> {
fn clone(&self) -> Self {
Self {
store: Arc::clone(&self.store),
records: self.records,
}
}
}
impl<DB: Database, Records: fmt::Debug> fmt::Debug for TrackingPublishLayer<DB, Records> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TrackingPublishLayer")
.field("records", &self.records)
.finish_non_exhaustive()
}
}
impl<DB: Database, Records: RecordList<DB>> PublishLayer for TrackingPublishLayer<DB, Records> {
async fn on_publish<'a, N: PublishPipeline, P: Publisher>(
&'a self,
out: &'a mut Outgoing<'a>,
next: PublishNext<'a, N, P>,
) -> Result<(), Box<dyn StdError + Send + Sync>> {
if enabled() && self.records.contains(out.name()) {
let headers = mem::take(out.headers_mut());
let msg =
OutgoingMessage::with_payload(out.name(), out.payload()).with_headers(headers);
let recorded = self.records.record(&self.store, &msg).await;
let (_, _, headers) = msg.into_parts();
*out.headers_mut() = headers;
if let Some(id) = recorded {
out.headers_mut().insert(OUTBOX_ID_HEADER, id?);
}
}
next.run(out).await
}
}
pub(super) async fn record<DB: Database, Record: Publish<DB>>(
name: &'static str,
store: &Store<DB>,
msg: &OutgoingMessage<'_>,
) -> Result<Bytes, OutboxError> {
let pool = store.get().ok_or(OutboxError::NoPool { name })?;
let failed = |source| OutboxError::Record {
name,
record: type_name::<Record>(),
source,
};
#[cfg(feature = "inbox")]
if writers::any_listed()
&& let Some(database) = store.one_writer(|| one_writer(pool))
{
match writers::holding(database) {
Holding::Nothing => {}
Holding::HoldsWriteLock => {
return Err(OutboxError::WriteLockHeld {
name,
record: type_name::<Record>(),
});
}
Holding::MayHoldWriteLock => {
return record_alone::<DB, Record>(name, store, pool, msg).await;
}
}
}
let mut conn = store.acquire(pool).await.map_err(failed)?;
let id = Record::publish(&mut conn, msg).await.map_err(failed)?;
Ok(id_value(&id))
}
#[cfg(feature = "inbox")]
fn one_writer<DB: Database>(pool: &Pool<DB>) -> Option<u64> {
let options = pool.connect_options();
let options: &dyn Dynamic = &*options;
#[cfg(feature = "sqlite")]
let sqlite = options.is::<SqliteConnectOptions>();
#[cfg(not(feature = "sqlite"))]
let sqlite = false;
let sqlite = sqlite
|| options
.downcast_ref::<AnyConnectOptions>()
.is_some_and(|any| any.database_url.scheme() == "sqlite");
sqlite.then(|| database_of(pool))
}
#[cfg(feature = "inbox")]
async fn record_alone<DB: Database, Record: Publish<DB>>(
name: &'static str,
store: &Store<DB>,
pool: &Pool<DB>,
msg: &OutgoingMessage<'_>,
) -> Result<Bytes, OutboxError> {
let record = type_name::<Record>();
let failed = |source| OutboxError::Record {
name,
record,
source,
};
let waited = pool.options().get_acquire_timeout();
let held = || OutboxError::WriteLockHeld { name, record };
let mut conn = store.acquire(pool).await.map_err(failed)?;
let begin = DB::TransactionManager::begin(&mut conn, Some(SqlStr::from_static(TAKE_LOCK)));
match timeout(waited, begin).await {
Ok(Ok(())) => {}
Ok(Err(error)) if locked(&error) => return Err(held()),
Ok(Err(error)) => {
conn.close_on_drop();
return Err(failed(error));
}
Err(Elapsed { .. }) => {
conn.close_on_drop();
return Err(held());
}
}
let written = match Record::publish(&mut conn, msg).await {
Ok(id) => DB::TransactionManager::commit(&mut conn).await.map(|()| id),
Err(error) => Err(error),
};
match written {
Ok(id) => Ok(id_value(&id)),
Err(error) => {
if DB::TransactionManager::rollback(&mut conn).await.is_err() {
conn.close_on_drop();
}
Err(if locked(&error) {
held()
} else {
failed(error)
})
}
}
}
#[cfg(feature = "inbox")]
const TAKE_LOCK: &str = "BEGIN IMMEDIATE";
#[cfg(feature = "inbox")]
fn locked(error: &sqlx::Error) -> bool {
const SQLITE_BUSY: i32 = 5;
const SQLITE_LOCKED: i32 = 6;
error
.as_database_error()
.and_then(DatabaseError::code)
.and_then(|code| code.parse::<i32>().ok())
.is_some_and(|code| matches!(code & 0xff, SQLITE_BUSY | SQLITE_LOCKED))
}
const ID_ON_STACK: usize = 64;
pub(super) fn id_value(id: &impl Display) -> Bytes {
let mut text = [0_u8; ID_ON_STACK];
let mut rest = &mut text[..];
if write!(rest, "{id}").is_ok() {
let len = ID_ON_STACK - rest.len();
return Bytes::copy_from_slice(&text[..len]);
}
Bytes::copy_from_slice(id.to_string().as_bytes())
}
pub(super) fn carried_id<Id: FromStr>(headers: &HeaderMap) -> Option<Result<Id, String>> {
let value = headers.get(OUTBOX_ID_HEADER)?;
Some(
str::from_utf8(value)
.ok()
.and_then(|text| text.parse().ok())
.ok_or_else(|| String::from_utf8_lossy(value).into_owned()),
)
}
#[cfg(test)]
mod tests {
use ruststream::HeaderMap;
use super::{ID_ON_STACK, OUTBOX_ID_HEADER, carried_id, id_value};
#[test]
fn an_id_survives_the_header() {
let mut headers = HeaderMap::new();
headers.insert(OUTBOX_ID_HEADER, 42_i64.to_string());
assert_eq!(carried_id::<i64>(&headers), Some(Ok(42)));
headers.insert(OUTBOX_ID_HEADER, "7f3a".to_owned());
assert_eq!(carried_id::<String>(&headers), Some(Ok("7f3a".to_owned())));
}
#[test]
fn an_id_value_holds_the_ids_text_whatever_its_length() {
assert_eq!(&id_value(&42_i64)[..], b"42");
assert_eq!(&id_value(&i64::MIN)[..], i64::MIN.to_string().as_bytes());
let long = "7".repeat(ID_ON_STACK + 1);
assert_eq!(&id_value(&long)[..], long.as_bytes());
}
#[test]
fn a_missing_or_foreign_header_is_told_apart() {
let mut headers = HeaderMap::new();
assert_eq!(carried_id::<i64>(&headers), None);
headers.insert(OUTBOX_ID_HEADER, "seven");
assert_eq!(carried_id::<i64>(&headers), Some(Err("seven".to_owned())));
headers.insert(OUTBOX_ID_HEADER, &b"\xff7"[..]);
assert_eq!(
carried_id::<i64>(&headers),
Some(Err("\u{fffd}7".to_owned()))
);
}
}