use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex as StdMutex, MutexGuard, Weak};
use chrono::{DateTime, Utc};
use serde::Serialize;
use tokio::sync::{Mutex as TokioMutex, Notify, OwnedSemaphorePermit, Semaphore};
use crate::snowflake::client::SnowflakeSession;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct SessionKey {
pub account: String,
pub user: String,
}
impl SessionKey {
pub fn new(account: impl Into<String>, user: impl Into<String>) -> Self {
Self {
account: account.into(),
user: user.into(),
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub struct QueryContext {
pub warehouse: Option<String>,
pub role: Option<String>,
pub database: Option<String>,
pub schema: Option<String>,
}
impl QueryContext {
#[must_use]
pub fn overlay(&self, overrides: &Self) -> Self {
Self {
warehouse: overrides
.warehouse
.clone()
.or_else(|| self.warehouse.clone()),
role: overrides.role.clone().or_else(|| self.role.clone()),
database: overrides.database.clone().or_else(|| self.database.clone()),
schema: overrides.schema.clone().or_else(|| self.schema.clone()),
}
}
#[must_use]
pub fn summary(&self) -> String {
let parts: Vec<&str> = [
self.warehouse.as_deref(),
self.role.as_deref(),
self.database.as_deref(),
self.schema.as_deref(),
]
.into_iter()
.flatten()
.collect();
if parts.is_empty() {
"(default)".to_string()
} else {
parts.join("/")
}
}
}
#[derive(Clone, Debug, Serialize)]
pub struct RunningQuery {
pub sql: String,
pub started_at: DateTime<Utc>,
}
struct Slot<S> {
id: u64,
base: QueryContext,
current: QueryContext,
last_used: DateTime<Utc>,
query_count: u64,
running: Option<RunningQuery>,
session: Option<S>,
}
#[derive(Clone, Debug, Serialize)]
pub struct MemberInfo {
pub id: u64,
pub busy: bool,
pub context: QueryContext,
pub last_used: DateTime<Utc>,
pub query_count: u64,
pub running: Option<RunningQuery>,
}
pub struct Checkout<S = SnowflakeSession> {
_permit: OwnedSemaphorePermit,
id: u64,
base: QueryContext,
current: QueryContext,
session: Option<S>,
slots: Weak<StdMutex<Vec<Slot<S>>>>,
done: bool,
}
impl<S> Checkout<S> {
pub fn session(&self) -> &S {
self.session
.as_ref()
.unwrap_or_else(|| unreachable!("session is present until checkin/discard"))
}
pub fn base(&self) -> &QueryContext {
&self.base
}
pub fn current(&self) -> &QueryContext {
&self.current
}
pub fn id(&self) -> u64 {
self.id
}
}
impl<S> Drop for Checkout<S> {
fn drop(&mut self) {
if self.done {
return;
}
if let Some(slots) = self.slots.upgrade() {
let mut slots = slots
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
slots.retain(|slot| slot.id != self.id);
}
}
}
#[derive(Clone, Debug)]
struct PoolMeta {
created_at: DateTime<Utc>,
last_used: DateTime<Utc>,
query_count: u64,
}
#[derive(Clone, Debug, Serialize)]
pub struct SessionInfo {
pub id: u64,
pub account: String,
pub user: String,
pub created_at: DateTime<Utc>,
pub last_used: DateTime<Utc>,
pub query_count: u64,
pub sessions: usize,
pub max_sessions: usize,
pub members: Vec<MemberInfo>,
}
pub struct SessionPool<S = SnowflakeSession> {
id: u64,
key: SessionKey,
max: usize,
permits: Arc<Semaphore>,
auth_gate: Arc<TokioMutex<()>>,
slots: Arc<StdMutex<Vec<Slot<S>>>>,
idle_notify: Notify,
next_member_id: AtomicU64,
meta: StdMutex<PoolMeta>,
}
impl<S> SessionPool<S> {
#[must_use]
pub fn new(
id: u64,
key: SessionKey,
max: usize,
now: DateTime<Utc>,
auth_gate: Arc<TokioMutex<()>>,
) -> Self {
let max = max.max(1);
Self {
id,
key,
max,
permits: Arc::new(Semaphore::new(max)),
auth_gate,
slots: Arc::new(StdMutex::new(Vec::new())),
idle_notify: Notify::new(),
next_member_id: AtomicU64::new(1),
meta: StdMutex::new(PoolMeta {
created_at: now,
last_used: now,
query_count: 0,
}),
}
}
pub async fn checkout<F, Fut, E>(&self, create: F) -> std::result::Result<Checkout<S>, E>
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = std::result::Result<(S, QueryContext), E>>,
{
let permit = Arc::clone(&self.permits)
.acquire_owned()
.await
.unwrap_or_else(|_| unreachable!("pool semaphore is never closed"));
let gate_guard = loop {
if let Some((id, base, current, session)) = self.take_idle() {
return Ok(self.make_checkout(permit, id, base, current, session));
}
let notified = self.idle_notify.notified();
tokio::pin!(notified);
let _ = notified.as_mut().enable();
if let Some((id, base, current, session)) = self.take_idle() {
return Ok(self.make_checkout(permit, id, base, current, session));
}
tokio::select! {
biased;
() = &mut notified => {} guard = self.auth_gate.lock() => break guard, }
};
let _gate = gate_guard;
if let Some((id, base, current, session)) = self.take_idle() {
return Ok(self.make_checkout(permit, id, base, current, session));
}
let (session, base) = create().await?;
let id = self.next_member_id.fetch_add(1, Ordering::Relaxed);
self.lock_slots().push(Slot {
id,
base: base.clone(),
current: base.clone(),
last_used: Utc::now(),
query_count: 0,
running: None,
session: None, });
Ok(self.make_checkout(permit, id, base.clone(), base, session))
}
fn take_idle(&self) -> Option<(u64, QueryContext, QueryContext, S)> {
let mut slots = self.lock_slots();
for slot in slots.iter_mut().rev() {
if let Some(session) = slot.session.take() {
return Some((slot.id, slot.base.clone(), slot.current.clone(), session));
}
}
None
}
fn make_checkout(
&self,
permit: OwnedSemaphorePermit,
id: u64,
base: QueryContext,
current: QueryContext,
session: S,
) -> Checkout<S> {
Checkout {
_permit: permit,
id,
base,
current,
session: Some(session),
slots: Arc::downgrade(&self.slots),
done: false,
}
}
pub fn checkin(&self, mut checkout: Checkout<S>, current: QueryContext) {
checkout.done = true;
let session = checkout.session.take();
{
let mut slots = self.lock_slots();
if let Some(slot) = slots.iter_mut().find(|slot| slot.id == checkout.id) {
slot.current = current;
slot.last_used = Utc::now();
slot.running = None;
slot.session = session;
}
}
self.idle_notify.notify_waiters();
}
pub fn start_query(&self, member_id: u64, sql: String) {
let mut slots = self.lock_slots();
if let Some(slot) = slots.iter_mut().find(|slot| slot.id == member_id) {
slot.query_count += 1;
slot.running = Some(RunningQuery {
sql,
started_at: Utc::now(),
});
}
}
pub fn discard(&self, mut checkout: Checkout<S>) {
checkout.done = true;
self.lock_slots().retain(|slot| slot.id != checkout.id);
drop(checkout);
}
pub fn touch(&self) {
let mut meta = self.lock_meta();
meta.last_used = Utc::now();
meta.query_count += 1;
}
#[must_use]
pub fn id(&self) -> u64 {
self.id
}
#[must_use]
pub fn live(&self) -> usize {
self.lock_slots().len()
}
#[must_use]
pub fn info(&self) -> SessionInfo {
let members: Vec<MemberInfo> = {
let slots = self.lock_slots();
let mut members: Vec<MemberInfo> = slots
.iter()
.map(|slot| MemberInfo {
id: slot.id,
busy: slot.session.is_none(),
context: slot.current.clone(),
last_used: slot.last_used,
query_count: slot.query_count,
running: slot.running.clone(),
})
.collect();
members.sort_by_key(|m| m.id);
members
};
let meta = self.lock_meta();
SessionInfo {
id: self.id,
account: self.key.account.clone(),
user: self.key.user.clone(),
created_at: meta.created_at,
last_used: meta.last_used,
query_count: meta.query_count,
sessions: members.len(),
max_sessions: self.max,
members,
}
}
fn lock_slots(&self) -> MutexGuard<'_, Vec<Slot<S>>> {
self.slots
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn lock_meta(&self) -> MutexGuard<'_, PoolMeta> {
self.meta
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
}
#[derive(Clone)]
pub struct PoolRegistry {
map: Arc<StdMutex<HashMap<SessionKey, Arc<SessionPool>>>>,
auth_gate: Arc<TokioMutex<()>>,
next_pool_id: Arc<AtomicU64>,
}
impl Default for PoolRegistry {
fn default() -> Self {
Self::new()
}
}
impl PoolRegistry {
#[must_use]
pub fn new() -> Self {
Self {
map: Arc::new(StdMutex::new(HashMap::new())),
auth_gate: Arc::new(TokioMutex::new(())),
next_pool_id: Arc::new(AtomicU64::new(1)),
}
}
pub fn get_or_create(&self, key: &SessionKey, max: usize) -> Arc<SessionPool> {
let mut map = self.lock();
if let Some(pool) = map.get(key) {
return Arc::clone(pool);
}
let id = self.next_pool_id.fetch_add(1, Ordering::Relaxed);
let pool = Arc::new(SessionPool::new(
id,
key.clone(),
max,
Utc::now(),
Arc::clone(&self.auth_gate),
));
map.insert(key.clone(), Arc::clone(&pool));
pool
}
pub fn remove(&self, key: &SessionKey) -> Option<Arc<SessionPool>> {
self.lock().remove(key)
}
pub fn remove_by_id(&self, id: u64) -> Option<Arc<SessionPool>> {
let key = {
let map = self.lock();
map.iter()
.find(|(_, pool)| pool.id() == id)
.map(|(key, _)| key.clone())
};
key.and_then(|key| self.remove(&key))
}
pub fn take_all(&self) -> Vec<Arc<SessionPool>> {
self.lock().drain().map(|(_, pool)| pool).collect()
}
#[must_use]
pub fn snapshot(&self) -> Vec<SessionInfo> {
let mut infos: Vec<SessionInfo> = self.lock().values().map(|pool| pool.info()).collect();
infos.sort_by_key(|info| info.id);
infos
}
#[must_use]
pub fn len(&self) -> usize {
self.lock().len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.lock().is_empty()
}
fn lock(&self) -> MutexGuard<'_, HashMap<SessionKey, Arc<SessionPool>>> {
self.map
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use std::sync::atomic::AtomicU32;
use super::*;
fn ctx() -> QueryContext {
QueryContext::default()
}
fn fake_pool(max: usize) -> (SessionPool<u32>, Arc<AtomicU32>) {
let pool = SessionPool::<u32>::new(
1,
SessionKey::new("ACCT", "user"),
max,
Utc::now(),
Arc::new(TokioMutex::new(())),
);
(pool, Arc::new(AtomicU32::new(0)))
}
async fn fake_create(
counter: &AtomicU32,
) -> std::result::Result<(u32, QueryContext), std::convert::Infallible> {
Ok((counter.fetch_add(1, Ordering::Relaxed), ctx()))
}
#[tokio::test]
async fn start_query_records_running_then_checkin_clears_it() {
let (pool, calls) = fake_pool(2);
let c = pool.checkout(|| fake_create(&calls)).await.unwrap();
pool.start_query(c.id(), "SELECT 1".to_string());
let member = pool.info().members[0].clone();
assert!(member.busy);
assert_eq!(member.query_count, 1);
assert_eq!(
member.running.as_ref().map(|r| r.sql.as_str()),
Some("SELECT 1")
);
pool.checkin(c, ctx());
let member = pool.info().members[0].clone();
assert!(!member.busy);
assert!(member.running.is_none(), "running cleared on checkin");
assert_eq!(member.query_count, 1, "count persists after checkin");
}
#[test]
fn overlay_lets_overrides_win() {
let base = QueryContext {
warehouse: Some("WH".into()),
role: Some("R".into()),
database: Some("DB".into()),
schema: Some("S".into()),
};
let overrides = QueryContext {
warehouse: Some("OTHER_WH".into()),
..QueryContext::default()
};
let eff = base.overlay(&overrides);
assert_eq!(eff.warehouse.as_deref(), Some("OTHER_WH"));
assert_eq!(eff.role.as_deref(), Some("R")); assert_eq!(eff.database.as_deref(), Some("DB"));
}
#[test]
fn summary_renders_set_dimensions_or_default() {
assert_eq!(QueryContext::default().summary(), "(default)");
let c = QueryContext {
warehouse: Some("WH".into()),
role: Some("R".into()),
..QueryContext::default()
};
assert_eq!(c.summary(), "WH/R");
}
#[tokio::test]
async fn checkin_reuses_session_and_lists_members() {
let (pool, calls) = fake_pool(4);
let c1 = pool.checkout(|| fake_create(&calls)).await.unwrap();
let id1 = c1.id();
let info = pool.info();
assert_eq!(info.members.len(), 1);
assert!(info.members[0].busy);
pool.checkin(c1, ctx());
assert!(!pool.info().members[0].busy);
let c2 = pool.checkout(|| fake_create(&calls)).await.unwrap();
assert_eq!(c2.id(), id1);
assert_eq!(calls.load(Ordering::Relaxed), 1, "should not create twice");
assert_eq!(pool.live(), 1);
pool.checkin(c2, ctx());
}
#[tokio::test]
async fn never_exceeds_capacity_and_blocks_until_checkin() {
let (pool, calls) = fake_pool(2);
let c1 = pool.checkout(|| fake_create(&calls)).await.unwrap();
let c2 = pool.checkout(|| fake_create(&calls)).await.unwrap();
assert_eq!(pool.live(), 2);
assert_eq!(pool.info().members.iter().filter(|m| m.busy).count(), 2);
let third = tokio::time::timeout(
std::time::Duration::from_millis(50),
pool.checkout(|| fake_create(&calls)),
)
.await;
assert!(third.is_err(), "third checkout should block at capacity");
pool.checkin(c1, ctx());
let c3 = pool.checkout(|| fake_create(&calls)).await.unwrap();
assert_eq!(pool.live(), 2, "reuse, not grow");
assert_eq!(calls.load(Ordering::Relaxed), 2);
pool.checkin(c2, ctx());
pool.checkin(c3, ctx());
}
#[tokio::test]
async fn discard_frees_capacity_for_a_fresh_session() {
let (pool, calls) = fake_pool(1);
let c1 = pool.checkout(|| fake_create(&calls)).await.unwrap();
assert_eq!(pool.live(), 1);
pool.discard(c1); assert_eq!(pool.live(), 0);
assert!(pool.info().members.is_empty());
let c2 = pool.checkout(|| fake_create(&calls)).await.unwrap();
assert_eq!(calls.load(Ordering::Relaxed), 2, "fresh session created");
assert_eq!(pool.live(), 1);
pool.checkin(c2, ctx());
}
#[tokio::test]
async fn orphaned_checkout_frees_its_slot_on_drop() {
let (pool, calls) = fake_pool(2);
{
let _c = pool.checkout(|| fake_create(&calls)).await.unwrap();
assert_eq!(pool.live(), 1);
}
assert_eq!(pool.live(), 0, "the Drop guard removed the orphaned slot");
assert!(pool.info().members.is_empty());
let c = pool.checkout(|| fake_create(&calls)).await.unwrap();
pool.checkin(c, ctx());
}
#[tokio::test]
async fn waiter_grabs_freed_session_without_waiting_out_the_in_flight_auth() {
let (pool, calls) = fake_pool(2);
let pool = Arc::new(pool);
let c1 = pool.checkout(|| fake_create(&calls)).await.unwrap();
assert_eq!(calls.load(Ordering::Relaxed), 1);
let held = Arc::clone(&pool.auth_gate).lock_owned().await;
let pool2 = Arc::clone(&pool);
let calls2 = Arc::clone(&calls);
let waiter = tokio::spawn(async move {
pool2
.checkout(move || async move {
let id = calls2.fetch_add(1, Ordering::Relaxed);
Ok::<(u32, QueryContext), std::convert::Infallible>((
id,
QueryContext::default(),
))
})
.await
.unwrap()
});
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
pool.checkin(c1, ctx());
let c2 = tokio::time::timeout(std::time::Duration::from_secs(2), waiter)
.await
.expect("waiter must not block on the held auth gate")
.unwrap();
assert_eq!(
calls.load(Ordering::Relaxed),
1,
"reused the freed session — no new auth"
);
assert_eq!(pool.live(), 1, "still one session");
drop(held);
pool.checkin(c2, ctx());
}
#[test]
fn registry_get_or_create_is_idempotent_per_key() {
let registry = PoolRegistry::new();
assert!(registry.is_empty());
let key = SessionKey::new("ACCT", "user");
let p1 = registry.get_or_create(&key, 4);
let p2 = registry.get_or_create(&key, 4);
assert_eq!(p1.id(), p2.id());
assert_eq!(registry.len(), 1);
assert!(registry.remove(&key).is_some());
assert!(registry.remove(&key).is_none());
}
}