use std::fmt;
use std::num::NonZeroU32;
use std::sync::{Arc, LazyLock, Mutex, PoisonError};
use std::time::Duration;
use ruststream::RetryDeclaration;
use ruststream_sqlx_dialect::{Dialect, Isolation, Opening, Role, TableSpec};
use sqlx::Database;
use super::build::{build, counted_attempt};
use super::check::check;
use super::description::{Description, Timing, refused_declaration, whole_seconds};
use crate::inbox::FormDialect;
use crate::inbox::broker::Shared;
use crate::inbox::database::QueueDatabase;
use crate::inbox::database::notify::listen;
use crate::inbox::engine::{Events, IdAt, Prepared, intern_name};
use crate::inbox::error::SqlxBrokerError;
use crate::inbox::form::advisory::LockBook;
use crate::inbox::form::lease::{self, LeaseBook};
use crate::inbox::form::row_lock::savepoint_of;
use crate::inbox::named::kinds::Kinds;
use crate::inbox::pools::ServicePool;
use crate::inbox::publish::table_of;
use crate::inbox::subscriber::{Holding, InboxSubscriber, Opened};
use crate::inbox::threads::InboxThreads;
use crate::inbox::transactional::{InboxMode, TxBook};
#[doc(hidden)]
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct Queue {
pub name: &'static str,
pub table: &'static str,
pub row: &'static str,
pub spec: TableSpec<'static>,
pub id_at: IdAt,
pub native_retry_after: bool,
pub kinds: Option<Kinds>,
pub prepared: Prepared,
pub begin_claim: Option<&'static str>,
pub counted_attempt: bool,
pub one_writer: bool,
pub poll_interval: Duration,
pub lease: Option<Duration>,
pub cap: Option<Cap>,
}
#[doc(hidden)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Cap {
pub attempts: NonZeroU32,
pub dead_letter: &'static str,
}
static QUEUES: LazyLock<Mutex<Vec<&'static Queue>>> = LazyLock::new(Mutex::default);
impl Queue {
fn intern(self) -> &'static Self {
let mut queues = QUEUES.lock().unwrap_or_else(PoisonError::into_inner);
if let Some(found) = queues.iter().find(|queue| ***queue == self) {
return found;
}
let queue: &'static Self = Box::leak(Box::new(self));
queues.push(queue);
queue
}
}
fn service_pool<DB: Database>(
shared: &Shared<DB>,
name: &str,
threads: Option<&InboxThreads>,
) -> Result<&'static ServicePool<DB>, SqlxBrokerError> {
if let Some(threads) = threads {
shared.count_threads(name, threads)?;
}
let per_thread = threads.map_or(NonZeroU32::MIN, InboxThreads::connections_each);
Ok(ServicePool::leak(
shared.pool.clone(),
shared.runtime.clone(),
per_thread,
))
}
pub(crate) async fn open<DB, Row, Mode>(
shared: &Arc<Shared<DB>>,
form: &FormDialect,
name: &str,
timing: Timing,
declaration: &RetryDeclaration,
description: &Description,
) -> Result<InboxSubscriber<DB, Row, Mode>, SqlxBrokerError>
where
DB: QueueDatabase,
Row: Events<DB>,
Mode: InboxMode,
{
let table = table_of(&description.spec);
let row = description.row;
if shared.is_closed() {
return Err(SqlxBrokerError::Closed);
}
let declared = |reason: String| SqlxBrokerError::Declaration {
subscription: name.to_owned(),
table: table.clone(),
row,
reason,
};
if let Some(refused) = refused_declaration(name, declaration, description) {
return Err(refused);
}
let refused = |source| SqlxBrokerError::Dialect {
subscription: name.to_owned(),
table: table.clone(),
row,
source,
};
let prepared = build(form, declaration, description, &declared).map_err(|err| match err {
SqlxBrokerError::Dialect { source, .. } => refused(source),
other => other,
})?;
let begin_claim = form.begin_claim(&description.spec).map_err(refused)?;
let begin_work = form
.dialect()
.begin(description.spec.opening())
.map_err(refused)?;
if let Some(reason) = lease_unseen_at::<Mode>(form, description) {
return Err(declared(reason));
}
let savepoint = savepoint_of::<Mode>(form);
let prepared = Prepared {
transactional: Mode::TRANSACTIONAL,
begin_work,
savepoint,
..prepared
};
let table_name = intern_name(&table);
let group = description.spec.column(Role::Group).map(|_| name);
let registration = Registration::take(shared, table_name, group, name, row)?;
check(shared, form, &description.spec, &prepared)
.await
.map_err(|unchecked| unchecked.named(name, &table, row))?;
listen(shared, shared.wakes.table(&description.spec), name, row).await?;
let queue = Queue {
name: intern_name(name),
table: table_name,
row,
spec: description.spec,
id_at: description.id_at(),
native_retry_after: description.native_retry_after(),
kinds: description.kinds,
prepared,
begin_claim,
counted_attempt: counted_attempt(form, description, &prepared),
one_writer: one_writer::<DB>(form.dialect()),
poll_interval: timing.poll_interval.unwrap_or(shared.poll_interval),
lease: description
.leased()
.then(|| whole_seconds(timing.lease.unwrap_or(shared.lease))),
cap: cap_of(declaration),
}
.intern();
let (holding, keeper) = match queue.lease {
Some(_) => {
let book = LeaseBook::leak(shared, queue);
let stop = shared.stopping.child_token();
drop(shared.runtime.spawn(lease::keep(book, stop.clone())));
(Holding::Leases(book), Some(stop.drop_guard()))
}
None if description.advisory() => (
Holding::Advisory(LockBook::leak::<Row>(shared, queue)),
None,
),
None => (Holding::Transaction, None),
};
let pool = service_pool(shared, name, timing.threads.as_ref())?;
let lending = (Mode::TRANSACTIONAL && !description.advisory()).then(TxBook::leak);
let wake = shared.wakes.table(&description.spec).subscribe(name);
Ok(InboxSubscriber::new(
Arc::clone(shared),
Opened {
queue,
holding,
registration,
keeper,
pool,
lending,
wake,
},
))
}
fn cap_of(declaration: &RetryDeclaration) -> Option<Cap> {
declaration
.max_attempts()
.zip(declaration.dead_letter())
.map(|(attempts, destination)| Cap {
attempts,
dead_letter: intern_name(destination),
})
}
fn one_writer<DB: Database>(dialect: &dyn Dialect) -> bool {
DB::NAME == "SQLite" || dialect.name() == "sqlite"
}
fn lease_unseen_at<Mode: InboxMode>(
form: &FormDialect,
description: &Description,
) -> Option<String> {
let Opening::Isolation(level @ (Isolation::RepeatableRead | Isolation::Serializable)) =
description.spec.opening()
else {
return None;
};
if !Mode::TRANSACTIONAL || !description.leased() || form.dialect().name() != "postgres" {
return None;
}
Some(format!(
"transactional mode acknowledges a lease delivery by the lease the broker last extended, \
and a Postgres transaction at `isolation = {}` reads rows as its first statement found \
them, so a handler that outlives half its lease would lose its writes: declare \
`isolation = read_committed` or no level, or mount the handler without \
`.transactional()`",
level.attribute(),
))
}
pub(crate) struct Registration<DB: Database> {
shared: Arc<Shared<DB>>,
table: &'static str,
group: Option<String>,
name: String,
}
impl<DB: Database> Registration<DB> {
fn take(
shared: &Arc<Shared<DB>>,
table: &'static str,
group: Option<&str>,
name: &str,
row: &'static str,
) -> Result<Self, SqlxBrokerError> {
let mut queues = shared.queues.lock().unwrap_or_else(PoisonError::into_inner);
if queues
.iter()
.any(|(open, open_group)| *open == table && open_group.as_deref() == group)
{
return Err(SqlxBrokerError::AlreadySubscribed {
subscription: name.to_owned(),
table: table.to_owned(),
row,
});
}
queues.push((table, group.map(str::to_owned)));
drop(queues);
#[cfg(feature = "testing")]
shared.harness.opened(name);
Ok(Self {
shared: Arc::clone(shared),
table,
group: group.map(str::to_owned),
name: name.to_owned(),
})
}
}
impl<DB: Database> Drop for Registration<DB> {
fn drop(&mut self) {
let mut queues = self
.shared
.queues
.lock()
.unwrap_or_else(PoisonError::into_inner);
queues.retain(|(table, group)| !(*table == self.table && *group == self.group));
drop(queues);
#[cfg(feature = "testing")]
self.shared.harness.closed(&self.name);
}
}
impl<DB: Database> fmt::Debug for Registration<DB> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Registration")
.field("table", &self.table)
.field("name", &self.name)
.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use ruststream_sqlx_dialect as dialect;
use super::one_writer;
#[cfg(feature = "sqlite")]
#[test]
fn sqlite_takes_one_writer_at_a_time() {
assert!(one_writer::<sqlx::Sqlite>(&dialect::Sqlite));
}
#[cfg(all(feature = "any", feature = "sqlite"))]
#[test]
fn an_any_pool_on_sqlite_takes_one_writer_at_a_time() {
assert!(one_writer::<sqlx::Any>(&dialect::Sqlite));
}
#[cfg(all(feature = "any", feature = "postgres"))]
#[test]
fn a_server_takes_writers_at_once() {
assert!(!one_writer::<sqlx::Postgres>(&dialect::Postgres));
assert!(!one_writer::<sqlx::Any>(&dialect::Postgres));
}
}