use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::time::Duration;
use claviron_db::DbService;
use sqlx::pool::PoolConnection;
use sqlx::{AssertSqlSafe, Executor, PgConnection, Postgres};
use tokio::runtime::Handle;
use tokio::time::timeout;
use tracing::warn;
use super::{Parameter, QueryResult};
const RESET_SESSION: &str = "CLOSE ALL; \
RESET ALL; \
UNLISTEN *; \
SELECT pg_advisory_unlock_all(); \
DISCARD TEMP; \
DISCARD SEQUENCES";
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum SessionError {
#[error("Claviron PostgreSQL is not configured")]
Unconfigured,
#[error("PostgreSQL pool is not registered: {pool}")]
UnknownPool { pool: String },
#[error("PostgreSQL connection is unavailable: {source}")]
Unavailable {
pool: String,
#[source]
source: sqlx::Error,
},
#[error(
"the request's PostgreSQL session is already in use on this thread"
)]
Busy,
#[error(
"synchronous PostgreSQL access must run outside a Tokio runtime thread"
)]
RuntimeThread,
}
const UNRESPONSIVE_GRACE: Duration = Duration::from_secs(5);
struct Lease {
connection: Option<PoolConnection<Postgres>>,
runtime: Handle,
in_transaction: bool,
cursors: usize,
dirty: bool,
}
impl Lease {
fn connection(&mut self) -> &mut PgConnection {
self.connection
.as_mut()
.expect("a lease holds its connection until it is handed back")
}
fn discard(&mut self) {
if let Some(connection) = self.connection.take() {
let _guard = self.runtime.enter();
drop(connection.detach());
}
}
}
impl Drop for Lease {
fn drop(&mut self) {
let Some(connection) = self.connection.take() else {
return;
};
let _guard = self.runtime.enter();
drop(connection);
}
}
thread_local! {
static LEASES: RefCell<HashMap<String, Lease>> = RefCell::new(HashMap::new());
static IN_REQUEST: Cell<bool> = const { Cell::new(false) };
}
pub(crate) fn begin_request(runtime: &Handle) {
release_all(runtime, "a new request started");
IN_REQUEST.set(true);
}
pub(crate) fn end_request(runtime: &Handle) {
IN_REQUEST.set(false);
release_all(runtime, "the request ended");
}
pub fn with_request_connection<T>(
pool: &str,
run: impl FnOnce(&mut PgConnection, &Handle) -> T,
) -> Result<T, SessionError> {
let bridge = super::bridge().map_err(|_| SessionError::Unconfigured)?;
with_lease(&bridge.service, &bridge.runtime, pool, |lease| {
lease.dirty = true;
run(lease.connection(), &bridge.runtime)
})
}
pub(super) fn execute(
service: &DbService,
runtime: &Handle,
pool: &str,
sql: &str,
parameters: Vec<Parameter>,
) -> Result<QueryResult, String> {
bounded(service, runtime, pool, sql, async |connection| {
super::execute_on(connection, sql, parameters).await
})
}
pub(super) fn execute_unprepared(
service: &DbService,
runtime: &Handle,
pool: &str,
sql: &str,
) -> Result<u64, String> {
bounded(service, runtime, pool, sql, async |connection| {
super::execute_unprepared_on(connection, sql).await
})
}
fn bounded<T>(
service: &DbService,
runtime: &Handle,
pool: &str,
sql: &str,
run: impl AsyncFnOnce(&mut PgConnection) -> Result<T, sqlx::Error>,
) -> Result<T, String> {
let budget = service
.postgres_statement_timeout(pool)
.map(|bound| bound + UNRESPONSIVE_GRACE);
let unresponsive = Cell::new(false);
let dirties = leaves_session_state(sql);
let result = with_lease(service, runtime, pool, |lease| {
lease.dirty |= dirties;
runtime.block_on(async {
let query = run(lease.connection());
let Some(budget) = budget else {
return query.await.map_err(|error| super::describe(&error));
};
match timeout(budget, query).await {
Ok(result) => result.map_err(|error| super::describe(&error)),
Err(_) => {
unresponsive.set(true);
Err(format!(
"PostgreSQL did not answer within {budget:?}; the connection was dropped"
))
}
}
})
})
.map_err(|error| error.to_string());
if unresponsive.get() {
warn!(pool = %pool, "PostgreSQL statement went unanswered; discarding the connection");
discard(pool);
}
result?
}
pub(super) fn begin(
service: &DbService,
runtime: &Handle,
pool: &str,
statement: &str,
) -> Result<(), String> {
with_lease(service, runtime, pool, |lease| {
if lease.in_transaction {
return Err(
"a PostgreSQL transaction is already open on this connection"
.to_owned(),
);
}
runtime
.block_on(
lease.connection().execute(AssertSqlSafe(statement.to_owned())),
)
.map_err(|error| super::describe(&error))?;
lease.in_transaction = true;
Ok(())
})
.map_err(|error| error.to_string())?
}
pub(super) fn finish(
service: &DbService,
runtime: &Handle,
pool: &str,
commit: bool,
) -> Result<(), String> {
let result = with_lease(service, runtime, pool, |lease| {
if !lease.in_transaction {
return Err("no PostgreSQL transaction is open on this connection"
.to_owned());
}
lease.in_transaction = false;
runtime
.block_on(lease.connection().execute(AssertSqlSafe(
if commit { "COMMIT" } else { "ROLLBACK" }.to_owned(),
)))
.map(|_| ())
.map_err(|error| super::describe(&error))
})
.map_err(|error| error.to_string());
if !IN_REQUEST.get() && !pinned(pool) {
release(runtime, pool);
}
result?
}
pub(super) fn in_transaction(pool: &str) -> bool {
LEASES.with_borrow(|leases| {
leases.get(pool).is_some_and(|lease| lease.in_transaction)
})
}
pub(super) fn disconnect(
runtime: &Handle,
pool: &str,
) -> Result<(), SessionError> {
blocking_thread()?;
let lease = LEASES.with(|leases| {
leases
.try_borrow_mut()
.map(|mut leases| leases.remove(pool))
.map_err(|_| SessionError::Busy)
})?;
if let Some(lease) = lease {
clean(runtime, pool, lease);
}
Ok(())
}
fn pinned(pool: &str) -> bool {
LEASES.with_borrow(|leases| {
leases
.get(pool)
.is_some_and(|lease| lease.in_transaction || lease.cursors > 0)
})
}
pub(super) fn declare_cursor(
service: &DbService,
runtime: &Handle,
pool: &str,
sql: &str,
parameters: Vec<Parameter>,
) -> Result<(), String> {
with_lease(service, runtime, pool, |lease| {
lease.dirty = true;
runtime
.block_on(super::execute_on(lease.connection(), sql, parameters))
.map_err(|error| super::describe(&error))?;
lease.cursors += 1;
Ok(())
})
.map_err(|error| error.to_string())?
}
pub(super) fn close_cursor(
service: &DbService,
runtime: &Handle,
pool: &str,
sql: &str,
) -> Result<(), String> {
let result = with_lease(service, runtime, pool, |lease| {
lease.cursors = lease.cursors.saturating_sub(1);
runtime
.block_on(lease.connection().execute(AssertSqlSafe(sql.to_owned())))
.map(|_| ())
.map_err(|error| super::describe(&error))
})
.map_err(|error| error.to_string());
if !IN_REQUEST.get() && !pinned(pool) {
release(runtime, pool);
}
result?
}
fn with_lease<T, F>(
service: &DbService,
runtime: &Handle,
pool: &str,
run: F,
) -> Result<T, SessionError>
where
F: FnOnce(&mut Lease) -> T,
{
blocking_thread()?;
let handle = service
.postgres_named(pool)
.ok_or_else(|| SessionError::UnknownPool { pool: pool.to_owned() })?;
let result = LEASES.with(|leases| {
let mut leases =
leases.try_borrow_mut().map_err(|_| SessionError::Busy)?;
if !leases.contains_key(pool) {
let connection =
runtime.block_on(handle.acquire()).map_err(|source| {
SessionError::Unavailable { pool: pool.to_owned(), source }
})?;
leases.insert(
pool.to_owned(),
Lease {
connection: Some(connection),
runtime: runtime.clone(),
in_transaction: false,
cursors: 0,
dirty: false,
},
);
}
let lease = leases.get_mut(pool).expect("the lease was just inserted");
Ok(run(lease))
});
if !IN_REQUEST.get() && !pinned(pool) {
release(runtime, pool);
}
result
}
fn discard(pool: &str) {
if let Some(mut lease) =
LEASES.with_borrow_mut(|leases| leases.remove(pool))
{
lease.discard();
}
}
fn release(
runtime: &Handle,
pool: &str,
) {
let Some(lease) = LEASES.with_borrow_mut(|leases| leases.remove(pool))
else {
return;
};
clean(runtime, pool, lease);
}
fn release_all(
runtime: &Handle,
reason: &str,
) {
if blocking_thread().is_err() {
warn!(%reason, "PostgreSQL session cannot be released from a runtime thread");
return;
}
for (pool, lease) in LEASES.with_borrow_mut(std::mem::take) {
if lease.in_transaction {
warn!(
pool = %pool,
%reason,
"PostgreSQL transaction was still open; rolling it back"
);
}
clean(runtime, &pool, lease);
}
}
fn clean(
runtime: &Handle,
pool: &str,
mut lease: Lease,
) {
if lease.in_transaction
&& let Err(error) = runtime.block_on(
lease.connection().execute(AssertSqlSafe("ROLLBACK".to_owned())),
)
{
warn!(pool = %pool, %error, "PostgreSQL rollback failed; discarding the connection");
lease.discard();
return;
}
if lease.dirty
&& let Err(error) = runtime.block_on(
lease.connection().execute(AssertSqlSafe(RESET_SESSION.to_owned())),
)
{
warn!(pool = %pool, %error, "PostgreSQL session reset failed; discarding the connection");
lease.discard();
return;
}
tracing::trace!(pool = %pool, reset = lease.dirty, "PostgreSQL connection returned to the pool");
}
fn blocking_thread() -> Result<(), SessionError> {
match Handle::try_current() {
Ok(_) => Err(SessionError::RuntimeThread),
Err(_) => Ok(()),
}
}
fn leaves_session_state(sql: &str) -> bool {
let keyword = sql
.trim_start()
.split(|character: char| character.is_whitespace() || character == '(')
.next()
.unwrap_or_default();
let ordinary =
["SELECT", "INSERT", "UPDATE", "DELETE", "WITH", "VALUES", "TABLE"]
.iter()
.any(|candidate| keyword.eq_ignore_ascii_case(candidate));
if !ordinary {
return true;
}
contains_ignore_ascii_case(sql, "advisory")
}
fn contains_ignore_ascii_case(
haystack: &str,
needle: &str,
) -> bool {
haystack
.as_bytes()
.windows(needle.len())
.any(|window| window.eq_ignore_ascii_case(needle.as_bytes()))
}