1use std::collections::HashMap;
26use std::sync::atomic::{AtomicU64, Ordering};
27use std::sync::{Arc, Mutex as StdMutex, MutexGuard, Weak};
28
29use chrono::{DateTime, Utc};
30use serde::Serialize;
31use tokio::sync::{Mutex as TokioMutex, Notify, OwnedSemaphorePermit, Semaphore};
32
33use crate::snowflake::client::SnowflakeSession;
34
35#[derive(Clone, Debug, PartialEq, Eq, Hash)]
40pub struct SessionKey {
41 pub account: String,
43 pub user: String,
45}
46
47impl SessionKey {
48 pub fn new(account: impl Into<String>, user: impl Into<String>) -> Self {
50 Self {
51 account: account.into(),
52 user: user.into(),
53 }
54 }
55}
56
57#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
63pub struct QueryContext {
64 pub warehouse: Option<String>,
66 pub role: Option<String>,
68 pub database: Option<String>,
70 pub schema: Option<String>,
72}
73
74impl QueryContext {
75 #[must_use]
80 pub fn overlay(&self, overrides: &Self) -> Self {
81 Self {
82 warehouse: overrides
83 .warehouse
84 .clone()
85 .or_else(|| self.warehouse.clone()),
86 role: overrides.role.clone().or_else(|| self.role.clone()),
87 database: overrides.database.clone().or_else(|| self.database.clone()),
88 schema: overrides.schema.clone().or_else(|| self.schema.clone()),
89 }
90 }
91
92 #[must_use]
94 pub fn summary(&self) -> String {
95 let parts: Vec<&str> = [
96 self.warehouse.as_deref(),
97 self.role.as_deref(),
98 self.database.as_deref(),
99 self.schema.as_deref(),
100 ]
101 .into_iter()
102 .flatten()
103 .collect();
104 if parts.is_empty() {
105 "(default)".to_string()
106 } else {
107 parts.join("/")
108 }
109 }
110}
111
112#[derive(Clone, Debug, Serialize)]
114pub struct RunningQuery {
115 pub sql: String,
117 pub started_at: DateTime<Utc>,
119}
120
121struct Slot<S> {
125 id: u64,
126 base: QueryContext,
127 current: QueryContext,
128 last_used: DateTime<Utc>,
129 query_count: u64,
130 running: Option<RunningQuery>,
131 session: Option<S>,
132}
133
134#[derive(Clone, Debug, Serialize)]
136pub struct MemberInfo {
137 pub id: u64,
139 pub busy: bool,
141 pub context: QueryContext,
143 pub last_used: DateTime<Utc>,
145 pub query_count: u64,
147 pub running: Option<RunningQuery>,
149}
150
151pub struct Checkout<S = SnowflakeSession> {
158 _permit: OwnedSemaphorePermit,
159 id: u64,
160 base: QueryContext,
161 current: QueryContext,
162 session: Option<S>,
164 slots: Weak<StdMutex<Vec<Slot<S>>>>,
166 done: bool,
168}
169
170impl<S> Checkout<S> {
171 pub fn session(&self) -> &S {
173 self.session
174 .as_ref()
175 .unwrap_or_else(|| unreachable!("session is present until checkin/discard"))
176 }
177 pub fn base(&self) -> &QueryContext {
179 &self.base
180 }
181 pub fn current(&self) -> &QueryContext {
183 &self.current
184 }
185 pub fn id(&self) -> u64 {
187 self.id
188 }
189}
190
191impl<S> Drop for Checkout<S> {
192 fn drop(&mut self) {
193 if self.done {
194 return;
195 }
196 if let Some(slots) = self.slots.upgrade() {
199 let mut slots = slots
200 .lock()
201 .unwrap_or_else(std::sync::PoisonError::into_inner);
202 slots.retain(|slot| slot.id != self.id);
203 }
204 }
205}
206
207#[derive(Clone, Debug)]
209struct PoolMeta {
210 created_at: DateTime<Utc>,
211 last_used: DateTime<Utc>,
212 query_count: u64,
213}
214
215#[derive(Clone, Debug, Serialize)]
217pub struct SessionInfo {
218 pub id: u64,
220 pub account: String,
222 pub user: String,
224 pub created_at: DateTime<Utc>,
226 pub last_used: DateTime<Utc>,
228 pub query_count: u64,
230 pub sessions: usize,
232 pub max_sessions: usize,
234 pub members: Vec<MemberInfo>,
236}
237
238pub struct SessionPool<S = SnowflakeSession> {
243 id: u64,
244 key: SessionKey,
245 max: usize,
246 permits: Arc<Semaphore>,
247 auth_gate: Arc<TokioMutex<()>>,
249 slots: Arc<StdMutex<Vec<Slot<S>>>>,
250 idle_notify: Notify,
253 next_member_id: AtomicU64,
254 meta: StdMutex<PoolMeta>,
255}
256
257impl<S> SessionPool<S> {
258 #[must_use]
261 pub fn new(
262 id: u64,
263 key: SessionKey,
264 max: usize,
265 now: DateTime<Utc>,
266 auth_gate: Arc<TokioMutex<()>>,
267 ) -> Self {
268 let max = max.max(1);
269 Self {
270 id,
271 key,
272 max,
273 permits: Arc::new(Semaphore::new(max)),
274 auth_gate,
275 slots: Arc::new(StdMutex::new(Vec::new())),
276 idle_notify: Notify::new(),
277 next_member_id: AtomicU64::new(1),
278 meta: StdMutex::new(PoolMeta {
279 created_at: now,
280 last_used: now,
281 query_count: 0,
282 }),
283 }
284 }
285
286 pub async fn checkout<F, Fut, E>(&self, create: F) -> std::result::Result<Checkout<S>, E>
298 where
299 F: FnOnce() -> Fut,
300 Fut: std::future::Future<Output = std::result::Result<(S, QueryContext), E>>,
301 {
302 let permit = Arc::clone(&self.permits)
303 .acquire_owned()
304 .await
305 .unwrap_or_else(|_| unreachable!("pool semaphore is never closed"));
306
307 let gate_guard = loop {
312 if let Some((id, base, current, session)) = self.take_idle() {
313 return Ok(self.make_checkout(permit, id, base, current, session));
314 }
315 let notified = self.idle_notify.notified();
318 tokio::pin!(notified);
319 let _ = notified.as_mut().enable();
320 if let Some((id, base, current, session)) = self.take_idle() {
321 return Ok(self.make_checkout(permit, id, base, current, session));
322 }
323 tokio::select! {
324 biased;
325 () = &mut notified => {} guard = self.auth_gate.lock() => break guard, }
328 };
329
330 let _gate = gate_guard;
333 if let Some((id, base, current, session)) = self.take_idle() {
334 return Ok(self.make_checkout(permit, id, base, current, session));
335 }
336 let (session, base) = create().await?;
337 let id = self.next_member_id.fetch_add(1, Ordering::Relaxed);
338 self.lock_slots().push(Slot {
339 id,
340 base: base.clone(),
341 current: base.clone(),
342 last_used: Utc::now(),
343 query_count: 0,
344 running: None,
345 session: None, });
347 Ok(self.make_checkout(permit, id, base.clone(), base, session))
348 }
349
350 fn take_idle(&self) -> Option<(u64, QueryContext, QueryContext, S)> {
352 let mut slots = self.lock_slots();
353 for slot in slots.iter_mut().rev() {
354 if let Some(session) = slot.session.take() {
355 return Some((slot.id, slot.base.clone(), slot.current.clone(), session));
356 }
357 }
358 None
359 }
360
361 fn make_checkout(
363 &self,
364 permit: OwnedSemaphorePermit,
365 id: u64,
366 base: QueryContext,
367 current: QueryContext,
368 session: S,
369 ) -> Checkout<S> {
370 Checkout {
371 _permit: permit,
372 id,
373 base,
374 current,
375 session: Some(session),
376 slots: Arc::downgrade(&self.slots),
377 done: false,
378 }
379 }
380
381 pub fn checkin(&self, mut checkout: Checkout<S>, current: QueryContext) {
383 checkout.done = true;
384 let session = checkout.session.take();
385 {
386 let mut slots = self.lock_slots();
387 if let Some(slot) = slots.iter_mut().find(|slot| slot.id == checkout.id) {
388 slot.current = current;
389 slot.last_used = Utc::now();
390 slot.running = None;
391 slot.session = session;
392 }
393 }
394 self.idle_notify.notify_waiters();
396 }
398
399 pub fn start_query(&self, member_id: u64, sql: String) {
402 let mut slots = self.lock_slots();
403 if let Some(slot) = slots.iter_mut().find(|slot| slot.id == member_id) {
404 slot.query_count += 1;
405 slot.running = Some(RunningQuery {
406 sql,
407 started_at: Utc::now(),
408 });
409 }
410 }
411
412 pub fn discard(&self, mut checkout: Checkout<S>) {
415 checkout.done = true;
416 self.lock_slots().retain(|slot| slot.id != checkout.id);
417 drop(checkout);
418 }
419
420 pub fn touch(&self) {
422 let mut meta = self.lock_meta();
423 meta.last_used = Utc::now();
424 meta.query_count += 1;
425 }
426
427 #[must_use]
429 pub fn id(&self) -> u64 {
430 self.id
431 }
432
433 #[must_use]
435 pub fn live(&self) -> usize {
436 self.lock_slots().len()
437 }
438
439 #[must_use]
441 pub fn info(&self) -> SessionInfo {
442 let members: Vec<MemberInfo> = {
443 let slots = self.lock_slots();
444 let mut members: Vec<MemberInfo> = slots
445 .iter()
446 .map(|slot| MemberInfo {
447 id: slot.id,
448 busy: slot.session.is_none(),
449 context: slot.current.clone(),
450 last_used: slot.last_used,
451 query_count: slot.query_count,
452 running: slot.running.clone(),
453 })
454 .collect();
455 members.sort_by_key(|m| m.id);
456 members
457 };
458 let meta = self.lock_meta();
459 SessionInfo {
460 id: self.id,
461 account: self.key.account.clone(),
462 user: self.key.user.clone(),
463 created_at: meta.created_at,
464 last_used: meta.last_used,
465 query_count: meta.query_count,
466 sessions: members.len(),
467 max_sessions: self.max,
468 members,
469 }
470 }
471
472 fn lock_slots(&self) -> MutexGuard<'_, Vec<Slot<S>>> {
473 self.slots
474 .lock()
475 .unwrap_or_else(std::sync::PoisonError::into_inner)
476 }
477
478 fn lock_meta(&self) -> MutexGuard<'_, PoolMeta> {
479 self.meta
480 .lock()
481 .unwrap_or_else(std::sync::PoisonError::into_inner)
482 }
483}
484
485#[derive(Clone)]
490pub struct PoolRegistry {
491 map: Arc<StdMutex<HashMap<SessionKey, Arc<SessionPool>>>>,
492 auth_gate: Arc<TokioMutex<()>>,
493 next_pool_id: Arc<AtomicU64>,
494}
495
496impl Default for PoolRegistry {
497 fn default() -> Self {
498 Self::new()
499 }
500}
501
502impl PoolRegistry {
503 #[must_use]
505 pub fn new() -> Self {
506 Self {
507 map: Arc::new(StdMutex::new(HashMap::new())),
508 auth_gate: Arc::new(TokioMutex::new(())),
509 next_pool_id: Arc::new(AtomicU64::new(1)),
510 }
511 }
512
513 pub fn get_or_create(&self, key: &SessionKey, max: usize) -> Arc<SessionPool> {
517 let mut map = self.lock();
518 if let Some(pool) = map.get(key) {
519 return Arc::clone(pool);
520 }
521 let id = self.next_pool_id.fetch_add(1, Ordering::Relaxed);
522 let pool = Arc::new(SessionPool::new(
523 id,
524 key.clone(),
525 max,
526 Utc::now(),
527 Arc::clone(&self.auth_gate),
528 ));
529 map.insert(key.clone(), Arc::clone(&pool));
530 pool
531 }
532
533 pub fn remove(&self, key: &SessionKey) -> Option<Arc<SessionPool>> {
535 self.lock().remove(key)
536 }
537
538 pub fn remove_by_id(&self, id: u64) -> Option<Arc<SessionPool>> {
540 let key = {
541 let map = self.lock();
542 map.iter()
543 .find(|(_, pool)| pool.id() == id)
544 .map(|(key, _)| key.clone())
545 };
546 key.and_then(|key| self.remove(&key))
547 }
548
549 pub fn take_all(&self) -> Vec<Arc<SessionPool>> {
551 self.lock().drain().map(|(_, pool)| pool).collect()
552 }
553
554 #[must_use]
556 pub fn snapshot(&self) -> Vec<SessionInfo> {
557 let mut infos: Vec<SessionInfo> = self.lock().values().map(|pool| pool.info()).collect();
558 infos.sort_by_key(|info| info.id);
559 infos
560 }
561
562 #[must_use]
564 pub fn len(&self) -> usize {
565 self.lock().len()
566 }
567
568 #[must_use]
570 pub fn is_empty(&self) -> bool {
571 self.lock().is_empty()
572 }
573
574 fn lock(&self) -> MutexGuard<'_, HashMap<SessionKey, Arc<SessionPool>>> {
575 self.map
576 .lock()
577 .unwrap_or_else(std::sync::PoisonError::into_inner)
578 }
579}
580
581#[cfg(test)]
582#[allow(clippy::unwrap_used, clippy::expect_used)]
583mod tests {
584 use std::sync::atomic::AtomicU32;
585
586 use super::*;
587
588 fn ctx() -> QueryContext {
589 QueryContext::default()
590 }
591
592 fn fake_pool(max: usize) -> (SessionPool<u32>, Arc<AtomicU32>) {
593 let pool = SessionPool::<u32>::new(
594 1,
595 SessionKey::new("ACCT", "user"),
596 max,
597 Utc::now(),
598 Arc::new(TokioMutex::new(())),
599 );
600 (pool, Arc::new(AtomicU32::new(0)))
601 }
602
603 async fn fake_create(
604 counter: &AtomicU32,
605 ) -> std::result::Result<(u32, QueryContext), std::convert::Infallible> {
606 Ok((counter.fetch_add(1, Ordering::Relaxed), ctx()))
607 }
608
609 #[tokio::test]
610 async fn start_query_records_running_then_checkin_clears_it() {
611 let (pool, calls) = fake_pool(2);
612 let c = pool.checkout(|| fake_create(&calls)).await.unwrap();
613 pool.start_query(c.id(), "SELECT 1".to_string());
614
615 let member = pool.info().members[0].clone();
616 assert!(member.busy);
617 assert_eq!(member.query_count, 1);
618 assert_eq!(
619 member.running.as_ref().map(|r| r.sql.as_str()),
620 Some("SELECT 1")
621 );
622
623 pool.checkin(c, ctx());
624 let member = pool.info().members[0].clone();
625 assert!(!member.busy);
626 assert!(member.running.is_none(), "running cleared on checkin");
627 assert_eq!(member.query_count, 1, "count persists after checkin");
628 }
629
630 #[test]
631 fn overlay_lets_overrides_win() {
632 let base = QueryContext {
633 warehouse: Some("WH".into()),
634 role: Some("R".into()),
635 database: Some("DB".into()),
636 schema: Some("S".into()),
637 };
638 let overrides = QueryContext {
639 warehouse: Some("OTHER_WH".into()),
640 ..QueryContext::default()
641 };
642 let eff = base.overlay(&overrides);
643 assert_eq!(eff.warehouse.as_deref(), Some("OTHER_WH"));
644 assert_eq!(eff.role.as_deref(), Some("R")); assert_eq!(eff.database.as_deref(), Some("DB"));
646 }
647
648 #[test]
649 fn summary_renders_set_dimensions_or_default() {
650 assert_eq!(QueryContext::default().summary(), "(default)");
651 let c = QueryContext {
652 warehouse: Some("WH".into()),
653 role: Some("R".into()),
654 ..QueryContext::default()
655 };
656 assert_eq!(c.summary(), "WH/R");
657 }
658
659 #[tokio::test]
660 async fn checkin_reuses_session_and_lists_members() {
661 let (pool, calls) = fake_pool(4);
662 let c1 = pool.checkout(|| fake_create(&calls)).await.unwrap();
663 let id1 = c1.id();
664 let info = pool.info();
666 assert_eq!(info.members.len(), 1);
667 assert!(info.members[0].busy);
668 pool.checkin(c1, ctx());
669 assert!(!pool.info().members[0].busy);
671 let c2 = pool.checkout(|| fake_create(&calls)).await.unwrap();
673 assert_eq!(c2.id(), id1);
674 assert_eq!(calls.load(Ordering::Relaxed), 1, "should not create twice");
675 assert_eq!(pool.live(), 1);
676 pool.checkin(c2, ctx());
677 }
678
679 #[tokio::test]
680 async fn never_exceeds_capacity_and_blocks_until_checkin() {
681 let (pool, calls) = fake_pool(2);
682 let c1 = pool.checkout(|| fake_create(&calls)).await.unwrap();
683 let c2 = pool.checkout(|| fake_create(&calls)).await.unwrap();
684 assert_eq!(pool.live(), 2);
685 assert_eq!(pool.info().members.iter().filter(|m| m.busy).count(), 2);
687 let third = tokio::time::timeout(
689 std::time::Duration::from_millis(50),
690 pool.checkout(|| fake_create(&calls)),
691 )
692 .await;
693 assert!(third.is_err(), "third checkout should block at capacity");
694 pool.checkin(c1, ctx());
695 let c3 = pool.checkout(|| fake_create(&calls)).await.unwrap();
696 assert_eq!(pool.live(), 2, "reuse, not grow");
697 assert_eq!(calls.load(Ordering::Relaxed), 2);
698 pool.checkin(c2, ctx());
699 pool.checkin(c3, ctx());
700 }
701
702 #[tokio::test]
703 async fn discard_frees_capacity_for_a_fresh_session() {
704 let (pool, calls) = fake_pool(1);
705 let c1 = pool.checkout(|| fake_create(&calls)).await.unwrap();
706 assert_eq!(pool.live(), 1);
707 pool.discard(c1); assert_eq!(pool.live(), 0);
709 assert!(pool.info().members.is_empty());
710 let c2 = pool.checkout(|| fake_create(&calls)).await.unwrap();
711 assert_eq!(calls.load(Ordering::Relaxed), 2, "fresh session created");
712 assert_eq!(pool.live(), 1);
713 pool.checkin(c2, ctx());
714 }
715
716 #[tokio::test]
717 async fn orphaned_checkout_frees_its_slot_on_drop() {
718 let (pool, calls) = fake_pool(2);
719 {
720 let _c = pool.checkout(|| fake_create(&calls)).await.unwrap();
721 assert_eq!(pool.live(), 1);
722 }
724 assert_eq!(pool.live(), 0, "the Drop guard removed the orphaned slot");
725 assert!(pool.info().members.is_empty());
726 let c = pool.checkout(|| fake_create(&calls)).await.unwrap();
728 pool.checkin(c, ctx());
729 }
730
731 #[tokio::test]
732 async fn waiter_grabs_freed_session_without_waiting_out_the_in_flight_auth() {
733 let (pool, calls) = fake_pool(2);
734 let pool = Arc::new(pool);
735
736 let c1 = pool.checkout(|| fake_create(&calls)).await.unwrap();
738 assert_eq!(calls.load(Ordering::Relaxed), 1);
739
740 let held = Arc::clone(&pool.auth_gate).lock_owned().await;
743
744 let pool2 = Arc::clone(&pool);
747 let calls2 = Arc::clone(&calls);
748 let waiter = tokio::spawn(async move {
749 pool2
750 .checkout(move || async move {
751 let id = calls2.fetch_add(1, Ordering::Relaxed);
752 Ok::<(u32, QueryContext), std::convert::Infallible>((
753 id,
754 QueryContext::default(),
755 ))
756 })
757 .await
758 .unwrap()
759 });
760
761 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
763 pool.checkin(c1, ctx());
764
765 let c2 = tokio::time::timeout(std::time::Duration::from_secs(2), waiter)
768 .await
769 .expect("waiter must not block on the held auth gate")
770 .unwrap();
771 assert_eq!(
772 calls.load(Ordering::Relaxed),
773 1,
774 "reused the freed session — no new auth"
775 );
776 assert_eq!(pool.live(), 1, "still one session");
777
778 drop(held);
779 pool.checkin(c2, ctx());
780 }
781
782 #[test]
783 fn registry_get_or_create_is_idempotent_per_key() {
784 let registry = PoolRegistry::new();
785 assert!(registry.is_empty());
786 let key = SessionKey::new("ACCT", "user");
787 let p1 = registry.get_or_create(&key, 4);
788 let p2 = registry.get_or_create(&key, 4);
789 assert_eq!(p1.id(), p2.id());
790 assert_eq!(registry.len(), 1);
791 assert!(registry.remove(&key).is_some());
792 assert!(registry.remove(&key).is_none());
793 }
794}