use std::pin::pin;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::time::Duration;
use futures::future::BoxFuture;
use sqlx::pool::PoolConnection;
use sqlx::{Connection, Database, Error, Pool};
use tokio::runtime::Handle;
use tokio::sync::Notify;
use super::{ProcessKey, ProcessLocks};
use crate::inbox::engine::Settling;
#[cfg(feature = "testing")]
use crate::inbox::testing::off_clock;
pub(crate) const CLOSE_TIMEOUT: Duration = Duration::from_secs(5);
pub(crate) type Unlock<DB> = for<'a> fn(
&'a mut <DB as Database>::Connection,
Settling,
&'a str,
) -> BoxFuture<'a, Result<bool, Error>>;
pub(crate) struct Unlocking<DB: Database> {
pub(crate) unlock: Unlock<DB>,
pub(crate) cx: Settling,
pub(crate) key: String,
}
pub(crate) struct Session<DB: Database> {
conn: Option<PoolConnection<DB>>,
locked: bool,
open: bool,
process: Option<ProcessKey>,
closing: &'static Closing,
#[cfg(feature = "testing")]
claiming: Option<Counted>,
}
impl<DB: Database> Session<DB> {
pub(crate) async fn acquire(pool: &Pool<DB>, closing: &'static Closing) -> Result<Self, Error> {
Ok(Self::on(pool.acquire().await?, closing))
}
pub(crate) fn try_acquire(pool: &Pool<DB>, closing: &'static Closing) -> Option<Self> {
pool.try_acquire().map(|conn| Self::on(conn, closing))
}
fn on(conn: PoolConnection<DB>, closing: &'static Closing) -> Self {
Self {
conn: Some(conn),
locked: false,
open: false,
process: None,
closing,
#[cfg(feature = "testing")]
claiming: closing.in_process().then(|| Counted::new(closing, false)),
}
}
pub(crate) const fn locked(&self) -> bool {
self.locked
}
#[cfg(feature = "testing")]
pub(crate) fn entered(&mut self) {
self.claiming = None;
}
pub(crate) fn conn(&mut self) -> &mut DB::Connection {
self.conn
.as_mut()
.expect("a session holds its connection until it ends")
}
pub(crate) fn conn_ref(&self) -> &DB::Connection {
self.conn
.as_ref()
.expect("a session holds its connection until it ends")
}
pub(crate) const fn set_locked(&mut self, locked: bool) {
self.locked = locked;
}
pub(crate) const fn set_open(&mut self, open: bool) {
self.open = open;
}
pub(crate) fn take_in_process(&mut self, database: u64, key: &str) -> bool {
self.process = ProcessLocks::try_take(database, key);
self.process.is_some()
}
pub(crate) fn free_in_process(&mut self) {
self.process = None;
}
pub(crate) fn release(mut self) {
self.end(None);
}
pub(crate) fn release_unlocking(mut self, unlocking: Unlocking<DB>) {
self.end(Some(unlocking));
}
fn end(&mut self, unlocking: Option<Unlocking<DB>>) {
self.process = None;
let Some(conn) = self.conn.take() else {
return;
};
let _entered = self.closing.runtime.enter();
if self.locked || self.open {
self.closing
.close(conn, self.locked, unlocking.filter(|_| self.locked));
} else {
drop(conn);
}
}
}
impl<DB: Database> Drop for Session<DB> {
fn drop(&mut self) {
self.end(None);
}
}
pub(crate) struct Closing {
in_flight: AtomicUsize,
done: Notify,
runtime: Handle,
shutting: AtomicBool,
forced: AtomicUsize,
#[cfg(feature = "testing")]
in_process: AtomicBool,
}
impl Closing {
pub(crate) fn leak(runtime: Handle) -> &'static Self {
Box::leak(Box::new(Self {
in_flight: AtomicUsize::new(0),
done: Notify::new(),
runtime,
shutting: AtomicBool::new(false),
forced: AtomicUsize::new(0),
#[cfg(feature = "testing")]
in_process: AtomicBool::new(false),
}))
}
pub(crate) fn begin_shutdown(&self) {
self.shutting.store(true, Ordering::SeqCst);
}
pub(crate) fn forced(&self) -> usize {
self.forced.load(Ordering::Acquire)
}
#[cfg(feature = "testing")]
pub(crate) fn in_process(&self) -> bool {
self.in_process.load(Ordering::Acquire)
}
pub(crate) async fn settled(&self) {
loop {
let mut notified = pin!(self.done.notified());
notified.as_mut().enable();
if self.in_flight.load(Ordering::Acquire) == 0 {
return;
}
notified.await;
}
}
#[cfg(feature = "testing")]
pub(crate) fn run_in_process(&self) {
self.in_process.store(true, Ordering::Release);
}
fn close<DB: Database>(
&'static self,
conn: PoolConnection<DB>,
locked: bool,
unlocking: Option<Unlocking<DB>>,
) {
let mut raw = conn.detach();
let counted = Counted::new(self, locked);
let work = async move {
let _counted = counted;
let closed = async move {
if let Some(Unlocking { unlock, cx, key }) = unlocking {
let _ = unlock(&mut raw, cx, &key).await;
}
raw.close().await
};
let _ = tokio::time::timeout(CLOSE_TIMEOUT, closed).await;
};
#[cfg(feature = "testing")]
if self.in_process.load(Ordering::Acquire) {
drop(self.runtime.spawn(async move {
let _ = off_clock(work).await;
}));
return;
}
drop(self.runtime.spawn(work));
}
}
struct Counted {
closing: &'static Closing,
locked: bool,
}
impl Counted {
fn new(closing: &'static Closing, locked: bool) -> Self {
closing.in_flight.fetch_add(1, Ordering::AcqRel);
Self { closing, locked }
}
}
impl Drop for Counted {
fn drop(&mut self) {
let closing = self.closing;
if self.locked && closing.shutting.load(Ordering::SeqCst) {
closing.forced.fetch_add(1, Ordering::AcqRel);
}
if closing.in_flight.fetch_sub(1, Ordering::AcqRel) == 1 {
closing.done.notify_waiters();
}
}
}
#[cfg(all(test, feature = "sqlite"))]
mod tests {
use std::future::pending;
use std::pin::pin;
use std::sync::atomic::Ordering;
use std::time::Duration;
use futures::future::BoxFuture;
use futures::poll;
use ruststream_sqlx_dialect::{Column, Form, KeyPart, TableSpec};
use sqlx::sqlite::SqlitePoolOptions;
use sqlx::{Error, SqliteConnection};
use tokio::runtime::{Builder, Handle};
use tokio::time::Instant;
use super::{CLOSE_TIMEOUT, Closing, Counted, Session, Unlocking};
use crate::inbox::engine::{IdAt, Now, Prepared, Settling};
use crate::inbox::queue::Queue;
const KEY: &[KeyPart<'static>] = &[KeyPart::Literal("jobs-"), KeyPart::Column("id")];
fn settling() -> Settling {
let queue: &'static Queue = Box::leak(Box::new(Queue {
name: "jobs",
table: "jobs",
row: "Job",
spec: TableSpec::new("jobs", Column::new("id"), Form::Advisory(KEY))
.payload(Column::new("payload")),
id_at: IdAt::First,
native_retry_after: false,
kinds: None,
prepared: Prepared::default(),
begin_claim: None,
counted_attempt: false,
one_writer: false,
poll_interval: Duration::from_secs(1),
lease: None,
cap: None,
}));
Settling {
queue,
now: Now::default(),
}
}
fn unanswered<'a>(
_: &'a mut SqliteConnection,
_: Settling,
_: &'a str,
) -> BoxFuture<'a, Result<bool, Error>> {
Box::pin(pending())
}
#[tokio::test]
async fn settled_waits_for_every_close_in_flight() {
let closing = Closing::leak(Handle::current());
let first = Counted::new(closing, false);
let second = Counted::new(closing, false);
let mut settled = pin!(closing.settled());
assert!(poll!(settled.as_mut()).is_pending());
drop(first);
assert!(
poll!(settled.as_mut()).is_pending(),
"a close is still in flight"
);
drop(second);
assert!(poll!(settled.as_mut()).is_ready());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_session_that_may_hold_a_lock_leaves_its_pool_and_closes() -> Result<(), Error> {
let pool = SqlitePoolOptions::new()
.max_connections(2)
.connect("sqlite::memory:")
.await?;
let closing = Closing::leak(Handle::current());
let mut session = Session::acquire(&pool, closing).await?;
assert_eq!(pool.size(), 1);
session.set_locked(true);
session.release();
assert_eq!(pool.size(), 0, "the connection left its pool at once");
closing.settled().await;
assert_eq!(closing.in_flight.load(Ordering::Acquire), 0);
pool.close().await;
Ok(())
}
#[tokio::test]
async fn an_unlock_that_never_answers_ends_with_the_close_at_its_timeout() -> Result<(), Error>
{
let pool = SqlitePoolOptions::new()
.max_connections(2)
.connect("sqlite::memory:")
.await?;
let closing = Closing::leak(Handle::current());
let mut session = Session::acquire(&pool, closing).await?;
session.set_locked(true);
tokio::time::pause();
let started = Instant::now();
session.release_unlocking(Unlocking {
unlock: unanswered,
cx: settling(),
key: "jobs-1".to_owned(),
});
closing.settled().await;
let ended = started.elapsed();
assert!(
ended >= CLOSE_TIMEOUT && ended < CLOSE_TIMEOUT + Duration::from_millis(10),
"one timeout ended the unlock and the close together: {ended:?}"
);
assert_eq!(closing.in_flight.load(Ordering::Acquire), 0);
tokio::time::resume();
pool.close().await;
Ok(())
}
#[test]
fn a_close_whose_runtime_is_gone_drops_with_its_connection() -> Result<(), Error> {
let gone = Builder::new_current_thread()
.build()
.map_err(Error::Io)?
.handle()
.clone();
let runtime = Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
.build()
.map_err(Error::Io)?;
runtime.block_on(async move {
let pool = SqlitePoolOptions::new()
.max_connections(2)
.connect("sqlite::memory:")
.await?;
let closing = Closing::leak(gone);
let mut session = Session::acquire(&pool, closing).await?;
session.set_open(true);
session.release();
assert_eq!(pool.size(), 0, "the connection never went back to its pool");
assert_eq!(
closing.in_flight.load(Ordering::Acquire),
0,
"the close dropped unrun and left the count"
);
pool.close().await;
Ok(())
})
}
}