use std::convert::Infallible;
use std::fmt;
use std::future::Future;
use std::marker::PhantomData;
use ruststream::runtime::{Context, Ctx, FromContext};
use ruststream::{BuildContext, ContextField, Field, IncomingMessage};
use sqlx::Pool as DbPool;
use super::database::QueueDatabase;
use super::delivery::InboxDelivery;
use super::engine::Events;
use super::pools::ServicePool;
use super::transactional::{InboxMode, Lender};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub struct InboxContext {
attempt: Option<u64>,
}
impl<M: IncomingMessage> BuildContext<M> for InboxContext {
fn build(msg: &M) -> Self {
Self {
attempt: msg.redelivery_count(),
}
}
}
pub struct PoolContext<DB: QueueDatabase> {
pool: &'static ServicePool<DB>,
attempt: Option<u64>,
}
impl<DB: QueueDatabase> Clone for PoolContext<DB> {
fn clone(&self) -> Self {
*self
}
}
impl<DB: QueueDatabase> Copy for PoolContext<DB> {}
impl<DB: QueueDatabase> fmt::Debug for PoolContext<DB> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PoolContext")
.field("attempt", &self.attempt)
.finish_non_exhaustive()
}
}
impl<DB, Row, Mode> BuildContext<InboxDelivery<DB, Row, Mode>> for PoolContext<DB>
where
DB: QueueDatabase,
Row: Events<DB>,
Mode: InboxMode,
{
fn build(msg: &InboxDelivery<DB, Row, Mode>) -> Self {
Self {
pool: msg.pool(),
attempt: msg.redelivery_count(),
}
}
}
pub struct TxContext<DB: QueueDatabase> {
lender: Lender<DB>,
pool: &'static ServicePool<DB>,
attempt: Option<u64>,
}
impl<DB: QueueDatabase> TxContext<DB> {
pub(crate) const fn new(
lender: Lender<DB>,
pool: &'static ServicePool<DB>,
attempt: Option<u64>,
) -> Self {
Self {
lender,
pool,
attempt,
}
}
}
impl<DB: QueueDatabase> Clone for TxContext<DB> {
fn clone(&self) -> Self {
*self
}
}
impl<DB: QueueDatabase> Copy for TxContext<DB> {}
impl<DB: QueueDatabase> fmt::Debug for TxContext<DB> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TxContext")
.field("lender", &self.lender)
.field("attempt", &self.attempt)
.finish_non_exhaustive()
}
}
#[doc(hidden)]
#[diagnostic::on_unimplemented(
message = "the handler takes `Ctx<keys::Tx<..>>`, and `{Self}` comes from a subscription \
without transactional mode",
label = "the delivery's transaction is lent only in transactional mode",
note = "mount the handler with `.transactional()`: `b.include(handler.transactional())`"
)]
pub trait TransactionalDelivery<DB: QueueDatabase> {
fn tx_context(&self) -> TxContext<DB>;
}
impl<DB, Row, Mode> BuildContext<InboxDelivery<DB, Row, Mode>> for TxContext<DB>
where
DB: QueueDatabase,
Row: Events<DB>,
InboxDelivery<DB, Row, Mode>: TransactionalDelivery<DB>,
{
fn build(msg: &InboxDelivery<DB, Row, Mode>) -> Self {
msg.tx_context()
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub struct Attempt;
impl ContextField for Attempt {
type Context = InboxContext;
type Value = Option<u64>;
fn read(self, src: &InboxContext) -> Option<u64> {
src.attempt
}
}
pub struct Tx<DB>(PhantomData<fn() -> DB>);
impl<DB> Default for Tx<DB> {
fn default() -> Self {
Self(PhantomData)
}
}
impl<DB> Clone for Tx<DB> {
fn clone(&self) -> Self {
*self
}
}
impl<DB> Copy for Tx<DB> {}
impl<DB> fmt::Debug for Tx<DB> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("Tx")
}
}
impl<DB: QueueDatabase> ContextField for Tx<DB> {
type Context = TxContext<DB>;
type Value = super::Tx<DB>;
fn read(self, src: &TxContext<DB>) -> super::Tx<DB> {
super::Tx::lent_by(src.lender).expect(
"the delivery's transaction is lent already: a handler reads `Ctx<keys::Tx<..>>` \
once per delivery",
)
}
}
pub struct Pool<DB>(PhantomData<fn() -> DB>);
impl<DB> Default for Pool<DB> {
fn default() -> Self {
Self(PhantomData)
}
}
impl<DB> Clone for Pool<DB> {
fn clone(&self) -> Self {
*self
}
}
impl<DB> Copy for Pool<DB> {}
impl<DB> fmt::Debug for Pool<DB> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("Pool")
}
}
impl<DB: QueueDatabase> ContextField for Pool<DB> {
type Context = PoolContext<DB>;
type Value = DbPool<DB>;
fn read(self, src: &PoolContext<DB>) -> DbPool<DB> {
src.pool.for_handler()
}
}
#[derive(Clone, Copy)]
struct AttemptOf;
impl<DB: QueueDatabase> Field<PoolContext<DB>> for AttemptOf {
type Value<'a>
= Option<u64>
where
DB: 'a;
fn get(self, src: &PoolContext<DB>) -> Option<u64> {
src.attempt
}
}
impl<DB: QueueDatabase> Field<TxContext<DB>> for AttemptOf {
type Value<'a>
= Option<u64>
where
DB: 'a;
fn get(self, src: &TxContext<DB>) -> Option<u64> {
src.attempt
}
}
#[derive(Clone, Copy)]
struct PoolOf;
impl<DB: QueueDatabase> Field<TxContext<DB>> for PoolOf {
type Value<'a>
= DbPool<DB>
where
DB: 'a;
fn get(self, src: &TxContext<DB>) -> DbPool<DB> {
src.pool.for_handler()
}
}
impl<S: Sync, DB: QueueDatabase> FromContext<PoolContext<DB>, S> for Ctx<Attempt> {
type Rejection = Infallible;
fn from_context(
ctx: &mut Context<'_, PoolContext<DB>, S>,
) -> impl Future<Output = Result<Self, Infallible>> + Send {
let attempt = ctx.context(AttemptOf);
async move { Ok(Self(attempt)) }
}
}
impl<S: Sync, DB: QueueDatabase> FromContext<TxContext<DB>, S> for Ctx<Attempt> {
type Rejection = Infallible;
fn from_context(
ctx: &mut Context<'_, TxContext<DB>, S>,
) -> impl Future<Output = Result<Self, Infallible>> + Send {
let attempt = ctx.context(AttemptOf);
async move { Ok(Self(attempt)) }
}
}
impl<S: Sync, DB: QueueDatabase> FromContext<TxContext<DB>, S> for Ctx<Pool<DB>> {
type Rejection = Infallible;
fn from_context(
ctx: &mut Context<'_, TxContext<DB>, S>,
) -> impl Future<Output = Result<Self, Infallible>> + Send {
let pool = ctx.context(PoolOf);
async move { Ok(Self(pool)) }
}
}
#[cfg(all(test, feature = "sqlite"))]
mod tests {
use ruststream::ContextField;
use sqlx::sqlite::SqlitePoolOptions;
use sqlx::{Error, SqlitePool};
use std::num::NonZeroU32;
use tokio::runtime::Handle;
use super::{Tx, TxContext};
use crate::inbox::pools::ServicePool;
use crate::inbox::transactional::TxBook;
use crate::inbox::tx::PoolTx;
#[tokio::test]
#[should_panic(expected = "lent already")]
async fn a_second_read_of_the_transaction_panics() {
let pool: SqlitePool = SqlitePoolOptions::new()
.connect("sqlite::memory:")
.await
.expect("an in-memory database opens");
let pool = ServicePool::leak(pool, Handle::current(), NonZeroU32::MIN);
let opened: Result<PoolTx<_>, Error> = PoolTx::begin(pool, None).await;
let hold = TxBook::leak().enter(opened.expect("begins"));
let cx = TxContext::new(hold.lender(), pool, Some(1));
let _first = Tx::default().read(&cx);
let _second = Tx::default().read(&cx);
}
}