use super::DjogiContext;
pub(crate) struct OwnedPinnedCtx {
ctx: DjogiContext,
clean: bool,
}
impl OwnedPinnedCtx {
pub(crate) fn new(ctx: DjogiContext) -> Self {
Self { ctx, clean: false }
}
#[allow(dead_code)] pub(crate) fn mark_clean(&mut self) {
self.clean = true;
}
}
impl Drop for OwnedPinnedCtx {
fn drop(&mut self) {
if !self.clean {
self.ctx.detach_transaction_connection_mut();
} else {
}
}
}
impl std::ops::Deref for OwnedPinnedCtx {
type Target = DjogiContext;
fn deref(&self) -> &DjogiContext {
&self.ctx
}
}
impl std::ops::DerefMut for OwnedPinnedCtx {
fn deref_mut(&mut self) -> &mut DjogiContext {
&mut self.ctx
}
}
#[allow(clippy::large_enum_variant)]
#[allow(dead_code)] pub(crate) enum PinnedCtx<'a> {
Owned(OwnedPinnedCtx),
Borrowed(&'a mut DjogiContext),
}
impl<'a> std::ops::Deref for PinnedCtx<'a> {
type Target = DjogiContext;
fn deref(&self) -> &DjogiContext {
match self {
PinnedCtx::Owned(guard) => guard,
PinnedCtx::Borrowed(ctx) => ctx,
}
}
}
impl<'a> std::ops::DerefMut for PinnedCtx<'a> {
fn deref_mut(&mut self) -> &mut DjogiContext {
match self {
PinnedCtx::Owned(guard) => guard,
PinnedCtx::Borrowed(ctx) => ctx,
}
}
}
impl<'a> PinnedCtx<'a> {
pub(crate) fn mark_clean(&mut self) {
if let PinnedCtx::Owned(guard) = self {
guard.mark_clean();
}
}
}
#[cfg(test)]
mod tests {
use crate::pg::pool::DjogiPool;
#[tokio::test]
async fn cancellation_drop_detaches_connection_from_pool() {
let database_url = std::env::var("DATABASE_URL").unwrap_or_default();
if database_url.is_empty() {
tracing::info!(
"cancellation_drop_detaches_connection_from_pool: \
DATABASE_URL not set, skipping"
);
return;
}
let pool = DjogiPool::builder(&database_url)
.max_size(2)
.build()
.await
.expect("pool build should succeed");
let mut ctx = super::DjogiContext::from_pool(pool.clone());
let mut pinned = ctx
.pin_for_migration()
.await
.expect("pin_for_migration should checkout a connection");
let old_pid_row = pinned
.query_one("SELECT pg_backend_pid()", &[])
.await
.expect("pg_backend_pid query should succeed");
let old_pid: i32 = old_pid_row
.try_get(0)
.expect("pg_backend_pid column should be decodable as i32");
let _lock_row = pinned
.query_one(
"SELECT pg_try_advisory_lock($1)",
&[&(998877665544332211i64)],
)
.await
.expect("advisory lock acquisition should succeed");
drop(pinned);
tokio::task::yield_now().await;
let mut new_conn = pool
.get()
.await
.expect("pool get should succeed after pinned drop");
let new_pid_row = new_conn
.query_one("SELECT pg_backend_pid()", &[])
.await
.expect("pg_backend_pid query on fresh connection should succeed");
let new_pid: i32 = new_pid_row
.try_get(0)
.expect("pg_backend_pid column should be decodable as i32");
assert_ne!(
old_pid, new_pid,
"BUG (GH #331): dropping PinnedCtx::Owned without mark_clean() \
returned the connection to the pool instead of detaching it. \
Old PID {old_pid} == New PID {new_pid} means the same physical \
connection was recycled with the advisory lock still held, \
poisoning subsequent checkouts."
);
}
}