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::{AbortHandle, 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 running_abort: Option<AbortHandle>,
136 session: Option<S>,
137}
138
139#[derive(Clone, Debug, Serialize)]
141pub struct MemberInfo {
142 pub id: u64,
144 pub busy: bool,
146 pub context: QueryContext,
148 pub last_used: DateTime<Utc>,
150 pub query_count: u64,
152 pub running: Option<RunningQuery>,
154}
155
156pub struct Checkout<S = SnowflakeSession> {
163 _permit: OwnedSemaphorePermit,
164 id: u64,
165 base: QueryContext,
166 current: QueryContext,
167 session: Option<S>,
169 slots: Weak<StdMutex<Vec<Slot<S>>>>,
171 done: bool,
173}
174
175impl<S> Checkout<S> {
176 pub fn session(&self) -> &S {
178 self.session
179 .as_ref()
180 .unwrap_or_else(|| unreachable!("session is present until checkin/discard"))
181 }
182 pub fn base(&self) -> &QueryContext {
184 &self.base
185 }
186 pub fn current(&self) -> &QueryContext {
188 &self.current
189 }
190 pub fn id(&self) -> u64 {
192 self.id
193 }
194}
195
196impl<S> Drop for Checkout<S> {
197 fn drop(&mut self) {
198 if self.done {
199 return;
200 }
201 if let Some(slots) = self.slots.upgrade() {
204 let mut slots = slots
205 .lock()
206 .unwrap_or_else(std::sync::PoisonError::into_inner);
207 slots.retain(|slot| slot.id != self.id);
208 }
209 }
210}
211
212#[derive(Clone, Debug)]
214struct PoolMeta {
215 created_at: DateTime<Utc>,
216 last_used: DateTime<Utc>,
217 query_count: u64,
218}
219
220#[derive(Clone, Debug, Serialize)]
222pub struct SessionInfo {
223 pub id: u64,
225 pub account: String,
227 pub user: String,
229 pub created_at: DateTime<Utc>,
231 pub last_used: DateTime<Utc>,
233 pub query_count: u64,
235 pub sessions: usize,
237 pub max_sessions: usize,
239 pub members: Vec<MemberInfo>,
241}
242
243pub struct SessionPool<S = SnowflakeSession> {
248 id: u64,
249 key: SessionKey,
250 max: usize,
251 permits: Arc<Semaphore>,
252 auth_gate: Arc<TokioMutex<()>>,
254 slots: Arc<StdMutex<Vec<Slot<S>>>>,
255 idle_notify: Notify,
258 next_member_id: AtomicU64,
259 meta: StdMutex<PoolMeta>,
260}
261
262impl<S> SessionPool<S> {
263 #[must_use]
266 pub fn new(
267 id: u64,
268 key: SessionKey,
269 max: usize,
270 now: DateTime<Utc>,
271 auth_gate: Arc<TokioMutex<()>>,
272 ) -> Self {
273 let max = max.max(1);
274 Self {
275 id,
276 key,
277 max,
278 permits: Arc::new(Semaphore::new(max)),
279 auth_gate,
280 slots: Arc::new(StdMutex::new(Vec::new())),
281 idle_notify: Notify::new(),
282 next_member_id: AtomicU64::new(1),
283 meta: StdMutex::new(PoolMeta {
284 created_at: now,
285 last_used: now,
286 query_count: 0,
287 }),
288 }
289 }
290
291 pub async fn checkout<F, Fut, E>(&self, create: F) -> std::result::Result<Checkout<S>, E>
303 where
304 F: FnOnce() -> Fut,
305 Fut: std::future::Future<Output = std::result::Result<(S, QueryContext), E>>,
306 {
307 let permit = Arc::clone(&self.permits)
308 .acquire_owned()
309 .await
310 .unwrap_or_else(|_| unreachable!("pool semaphore is never closed"));
311
312 let gate_guard = loop {
317 if let Some((id, base, current, session)) = self.take_idle() {
318 return Ok(self.make_checkout(permit, id, base, current, session));
319 }
320 let notified = self.idle_notify.notified();
323 tokio::pin!(notified);
324 let _ = notified.as_mut().enable();
325 if let Some((id, base, current, session)) = self.take_idle() {
326 return Ok(self.make_checkout(permit, id, base, current, session));
327 }
328 tokio::select! {
329 biased;
330 () = &mut notified => {} guard = self.auth_gate.lock() => break guard, }
333 };
334
335 let _gate = gate_guard;
338 if let Some((id, base, current, session)) = self.take_idle() {
339 return Ok(self.make_checkout(permit, id, base, current, session));
340 }
341 let (session, base) = create().await?;
342 let id = self.next_member_id.fetch_add(1, Ordering::Relaxed);
343 self.lock_slots().push(Slot {
344 id,
345 base: base.clone(),
346 current: base.clone(),
347 last_used: Utc::now(),
348 query_count: 0,
349 running: None,
350 running_abort: None,
351 session: None, });
353 Ok(self.make_checkout(permit, id, base.clone(), base, session))
354 }
355
356 fn take_idle(&self) -> Option<(u64, QueryContext, QueryContext, S)> {
358 let mut slots = self.lock_slots();
359 for slot in slots.iter_mut().rev() {
360 if let Some(session) = slot.session.take() {
361 return Some((slot.id, slot.base.clone(), slot.current.clone(), session));
362 }
363 }
364 None
365 }
366
367 fn make_checkout(
369 &self,
370 permit: OwnedSemaphorePermit,
371 id: u64,
372 base: QueryContext,
373 current: QueryContext,
374 session: S,
375 ) -> Checkout<S> {
376 Checkout {
377 _permit: permit,
378 id,
379 base,
380 current,
381 session: Some(session),
382 slots: Arc::downgrade(&self.slots),
383 done: false,
384 }
385 }
386
387 #[must_use]
395 pub fn checkout_all_idle(&self) -> Vec<Checkout<S>> {
396 let mut checkouts = Vec::new();
397 while let Ok(permit) = Arc::clone(&self.permits).try_acquire_owned() {
398 match self.take_idle() {
399 Some((id, base, current, session)) => {
400 checkouts.push(self.make_checkout(permit, id, base, current, session));
401 }
402 None => break,
404 }
405 }
406 checkouts
407 }
408
409 pub fn restore(&self, mut checkout: Checkout<S>) {
413 checkout.done = true;
414 let session = checkout.session.take();
415 {
416 let mut slots = self.lock_slots();
417 if let Some(slot) = slots.iter_mut().find(|slot| slot.id == checkout.id) {
418 slot.session = session;
419 }
420 }
421 self.idle_notify.notify_waiters();
423 }
425
426 pub fn checkin(&self, mut checkout: Checkout<S>, current: QueryContext) {
428 checkout.done = true;
429 let session = checkout.session.take();
430 {
431 let mut slots = self.lock_slots();
432 if let Some(slot) = slots.iter_mut().find(|slot| slot.id == checkout.id) {
433 slot.current = current;
434 slot.last_used = Utc::now();
435 slot.running = None;
436 slot.running_abort = None;
437 slot.session = session;
438 }
439 }
440 self.idle_notify.notify_waiters();
442 }
444
445 pub fn start_query(&self, member_id: u64, sql: String, abort: Option<AbortHandle>) {
450 let mut slots = self.lock_slots();
451 if let Some(slot) = slots.iter_mut().find(|slot| slot.id == member_id) {
452 slot.query_count += 1;
453 slot.running = Some(RunningQuery {
454 sql,
455 started_at: Utc::now(),
456 });
457 slot.running_abort = abort;
458 }
459 }
460
461 #[must_use]
467 pub fn abort_handles(&self, member: Option<u64>) -> Vec<AbortHandle> {
468 let slots = self.lock_slots();
469 slots
470 .iter()
471 .filter(|slot| match member {
472 Some(id) => slot.id == id,
473 None => true,
474 })
475 .filter_map(|slot| slot.running_abort.clone())
476 .collect()
477 }
478
479 pub fn discard(&self, mut checkout: Checkout<S>) {
482 checkout.done = true;
483 self.lock_slots().retain(|slot| slot.id != checkout.id);
484 drop(checkout);
485 }
486
487 pub fn touch(&self) {
489 let mut meta = self.lock_meta();
490 meta.last_used = Utc::now();
491 meta.query_count += 1;
492 }
493
494 #[must_use]
496 pub fn id(&self) -> u64 {
497 self.id
498 }
499
500 #[must_use]
502 pub fn live(&self) -> usize {
503 self.lock_slots().len()
504 }
505
506 #[must_use]
508 pub fn info(&self) -> SessionInfo {
509 let members: Vec<MemberInfo> = {
510 let slots = self.lock_slots();
511 let mut members: Vec<MemberInfo> = slots
512 .iter()
513 .map(|slot| MemberInfo {
514 id: slot.id,
515 busy: slot.session.is_none(),
516 context: slot.current.clone(),
517 last_used: slot.last_used,
518 query_count: slot.query_count,
519 running: slot.running.clone(),
520 })
521 .collect();
522 members.sort_by_key(|m| m.id);
523 members
524 };
525 let meta = self.lock_meta();
526 SessionInfo {
527 id: self.id,
528 account: self.key.account.clone(),
529 user: self.key.user.clone(),
530 created_at: meta.created_at,
531 last_used: meta.last_used,
532 query_count: meta.query_count,
533 sessions: members.len(),
534 max_sessions: self.max,
535 members,
536 }
537 }
538
539 fn lock_slots(&self) -> MutexGuard<'_, Vec<Slot<S>>> {
540 self.slots
541 .lock()
542 .unwrap_or_else(std::sync::PoisonError::into_inner)
543 }
544
545 fn lock_meta(&self) -> MutexGuard<'_, PoolMeta> {
546 self.meta
547 .lock()
548 .unwrap_or_else(std::sync::PoisonError::into_inner)
549 }
550}
551
552#[derive(Clone)]
557pub struct PoolRegistry {
558 map: Arc<StdMutex<HashMap<SessionKey, Arc<SessionPool>>>>,
559 auth_gate: Arc<TokioMutex<()>>,
560 next_pool_id: Arc<AtomicU64>,
561}
562
563impl Default for PoolRegistry {
564 fn default() -> Self {
565 Self::new()
566 }
567}
568
569impl PoolRegistry {
570 #[must_use]
572 pub fn new() -> Self {
573 Self {
574 map: Arc::new(StdMutex::new(HashMap::new())),
575 auth_gate: Arc::new(TokioMutex::new(())),
576 next_pool_id: Arc::new(AtomicU64::new(1)),
577 }
578 }
579
580 pub fn get_or_create(&self, key: &SessionKey, max: usize) -> Arc<SessionPool> {
584 let mut map = self.lock();
585 if let Some(pool) = map.get(key) {
586 return Arc::clone(pool);
587 }
588 let id = self.next_pool_id.fetch_add(1, Ordering::Relaxed);
589 let pool = Arc::new(SessionPool::new(
590 id,
591 key.clone(),
592 max,
593 Utc::now(),
594 Arc::clone(&self.auth_gate),
595 ));
596 map.insert(key.clone(), Arc::clone(&pool));
597 pool
598 }
599
600 #[must_use]
604 pub fn get(&self, key: &SessionKey) -> Option<Arc<SessionPool>> {
605 self.lock().get(key).map(Arc::clone)
606 }
607
608 #[must_use]
610 pub fn get_by_id(&self, id: u64) -> Option<Arc<SessionPool>> {
611 self.lock()
612 .values()
613 .find(|pool| pool.id() == id)
614 .map(Arc::clone)
615 }
616
617 pub fn remove(&self, key: &SessionKey) -> Option<Arc<SessionPool>> {
619 self.lock().remove(key)
620 }
621
622 pub fn remove_by_id(&self, id: u64) -> Option<Arc<SessionPool>> {
624 let key = {
625 let map = self.lock();
626 map.iter()
627 .find(|(_, pool)| pool.id() == id)
628 .map(|(key, _)| key.clone())
629 };
630 key.and_then(|key| self.remove(&key))
631 }
632
633 pub fn take_all(&self) -> Vec<Arc<SessionPool>> {
635 self.lock().drain().map(|(_, pool)| pool).collect()
636 }
637
638 #[must_use]
641 pub fn pools(&self) -> Vec<Arc<SessionPool>> {
642 let mut pools: Vec<Arc<SessionPool>> = self.lock().values().map(Arc::clone).collect();
643 pools.sort_by_key(|pool| pool.id());
644 pools
645 }
646
647 #[must_use]
649 pub fn snapshot(&self) -> Vec<SessionInfo> {
650 let mut infos: Vec<SessionInfo> = self.lock().values().map(|pool| pool.info()).collect();
651 infos.sort_by_key(|info| info.id);
652 infos
653 }
654
655 #[must_use]
657 pub fn len(&self) -> usize {
658 self.lock().len()
659 }
660
661 #[must_use]
663 pub fn is_empty(&self) -> bool {
664 self.lock().is_empty()
665 }
666
667 fn lock(&self) -> MutexGuard<'_, HashMap<SessionKey, Arc<SessionPool>>> {
668 self.map
669 .lock()
670 .unwrap_or_else(std::sync::PoisonError::into_inner)
671 }
672}
673
674#[cfg(test)]
675#[allow(clippy::unwrap_used, clippy::expect_used)]
676mod tests {
677 use std::sync::atomic::AtomicU32;
678
679 use super::*;
680
681 fn ctx() -> QueryContext {
682 QueryContext::default()
683 }
684
685 fn fake_pool(max: usize) -> (SessionPool<u32>, Arc<AtomicU32>) {
686 let pool = SessionPool::<u32>::new(
687 1,
688 SessionKey::new("ACCT", "user"),
689 max,
690 Utc::now(),
691 Arc::new(TokioMutex::new(())),
692 );
693 (pool, Arc::new(AtomicU32::new(0)))
694 }
695
696 async fn fake_create(
697 counter: &AtomicU32,
698 ) -> std::result::Result<(u32, QueryContext), std::convert::Infallible> {
699 Ok((counter.fetch_add(1, Ordering::Relaxed), ctx()))
700 }
701
702 #[tokio::test]
703 async fn start_query_records_running_then_checkin_clears_it() {
704 let (pool, calls) = fake_pool(2);
705 let c = pool.checkout(|| fake_create(&calls)).await.unwrap();
706 pool.start_query(
707 c.id(),
708 "SELECT 1".to_string(),
709 Some(AbortHandle::noop_for_test()),
710 );
711
712 let member = pool.info().members[0].clone();
713 assert!(member.busy);
714 assert_eq!(member.query_count, 1);
715 assert_eq!(
716 member.running.as_ref().map(|r| r.sql.as_str()),
717 Some("SELECT 1")
718 );
719 assert_eq!(pool.abort_handles(None).len(), 1);
721 assert_eq!(pool.abort_handles(Some(c.id())).len(), 1);
722 assert!(pool.abort_handles(Some(c.id() + 99)).is_empty());
723
724 pool.checkin(c, ctx());
725 let member = pool.info().members[0].clone();
726 assert!(!member.busy);
727 assert!(member.running.is_none(), "running cleared on checkin");
728 assert_eq!(member.query_count, 1, "count persists after checkin");
729 assert!(
731 pool.abort_handles(None).is_empty(),
732 "abort handle cleared on checkin"
733 );
734 }
735
736 #[test]
737 fn overlay_lets_overrides_win() {
738 let base = QueryContext {
739 warehouse: Some("WH".into()),
740 role: Some("R".into()),
741 database: Some("DB".into()),
742 schema: Some("S".into()),
743 };
744 let overrides = QueryContext {
745 warehouse: Some("OTHER_WH".into()),
746 ..QueryContext::default()
747 };
748 let eff = base.overlay(&overrides);
749 assert_eq!(eff.warehouse.as_deref(), Some("OTHER_WH"));
750 assert_eq!(eff.role.as_deref(), Some("R")); assert_eq!(eff.database.as_deref(), Some("DB"));
752 }
753
754 #[test]
755 fn summary_renders_set_dimensions_or_default() {
756 assert_eq!(QueryContext::default().summary(), "(default)");
757 let c = QueryContext {
758 warehouse: Some("WH".into()),
759 role: Some("R".into()),
760 ..QueryContext::default()
761 };
762 assert_eq!(c.summary(), "WH/R");
763 }
764
765 #[tokio::test]
766 async fn checkin_reuses_session_and_lists_members() {
767 let (pool, calls) = fake_pool(4);
768 let c1 = pool.checkout(|| fake_create(&calls)).await.unwrap();
769 let id1 = c1.id();
770 let info = pool.info();
772 assert_eq!(info.members.len(), 1);
773 assert!(info.members[0].busy);
774 pool.checkin(c1, ctx());
775 assert!(!pool.info().members[0].busy);
777 let c2 = pool.checkout(|| fake_create(&calls)).await.unwrap();
779 assert_eq!(c2.id(), id1);
780 assert_eq!(calls.load(Ordering::Relaxed), 1, "should not create twice");
781 assert_eq!(pool.live(), 1);
782 pool.checkin(c2, ctx());
783 }
784
785 #[tokio::test]
786 async fn never_exceeds_capacity_and_blocks_until_checkin() {
787 let (pool, calls) = fake_pool(2);
788 let c1 = pool.checkout(|| fake_create(&calls)).await.unwrap();
789 let c2 = pool.checkout(|| fake_create(&calls)).await.unwrap();
790 assert_eq!(pool.live(), 2);
791 assert_eq!(pool.info().members.iter().filter(|m| m.busy).count(), 2);
793 let third = tokio::time::timeout(
795 std::time::Duration::from_millis(50),
796 pool.checkout(|| fake_create(&calls)),
797 )
798 .await;
799 assert!(third.is_err(), "third checkout should block at capacity");
800 pool.checkin(c1, ctx());
801 let c3 = pool.checkout(|| fake_create(&calls)).await.unwrap();
802 assert_eq!(pool.live(), 2, "reuse, not grow");
803 assert_eq!(calls.load(Ordering::Relaxed), 2);
804 pool.checkin(c2, ctx());
805 pool.checkin(c3, ctx());
806 }
807
808 #[tokio::test]
809 async fn discard_frees_capacity_for_a_fresh_session() {
810 let (pool, calls) = fake_pool(1);
811 let c1 = pool.checkout(|| fake_create(&calls)).await.unwrap();
812 assert_eq!(pool.live(), 1);
813 pool.discard(c1); assert_eq!(pool.live(), 0);
815 assert!(pool.info().members.is_empty());
816 let c2 = pool.checkout(|| fake_create(&calls)).await.unwrap();
817 assert_eq!(calls.load(Ordering::Relaxed), 2, "fresh session created");
818 assert_eq!(pool.live(), 1);
819 pool.checkin(c2, ctx());
820 }
821
822 #[tokio::test]
823 async fn orphaned_checkout_frees_its_slot_on_drop() {
824 let (pool, calls) = fake_pool(2);
825 {
826 let _c = pool.checkout(|| fake_create(&calls)).await.unwrap();
827 assert_eq!(pool.live(), 1);
828 }
830 assert_eq!(pool.live(), 0, "the Drop guard removed the orphaned slot");
831 assert!(pool.info().members.is_empty());
832 let c = pool.checkout(|| fake_create(&calls)).await.unwrap();
834 pool.checkin(c, ctx());
835 }
836
837 #[tokio::test]
838 async fn waiter_grabs_freed_session_without_waiting_out_the_in_flight_auth() {
839 let (pool, calls) = fake_pool(2);
840 let pool = Arc::new(pool);
841
842 let c1 = pool.checkout(|| fake_create(&calls)).await.unwrap();
844 assert_eq!(calls.load(Ordering::Relaxed), 1);
845
846 let held = Arc::clone(&pool.auth_gate).lock_owned().await;
849
850 let pool2 = Arc::clone(&pool);
853 let calls2 = Arc::clone(&calls);
854 let waiter = tokio::spawn(async move {
855 pool2
856 .checkout(move || async move {
857 let id = calls2.fetch_add(1, Ordering::Relaxed);
858 Ok::<(u32, QueryContext), std::convert::Infallible>((
859 id,
860 QueryContext::default(),
861 ))
862 })
863 .await
864 .unwrap()
865 });
866
867 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
869 pool.checkin(c1, ctx());
870
871 let c2 = tokio::time::timeout(std::time::Duration::from_secs(2), waiter)
874 .await
875 .expect("waiter must not block on the held auth gate")
876 .unwrap();
877 assert_eq!(
878 calls.load(Ordering::Relaxed),
879 1,
880 "reused the freed session — no new auth"
881 );
882 assert_eq!(pool.live(), 1, "still one session");
883
884 drop(held);
885 pool.checkin(c2, ctx());
886 }
887
888 #[tokio::test]
889 async fn checkout_all_idle_borrows_only_idle_sessions() {
890 let (pool, calls) = fake_pool(4);
891 let c1 = pool.checkout(|| fake_create(&calls)).await.unwrap();
892 let c2 = pool.checkout(|| fake_create(&calls)).await.unwrap();
893 let busy_id = c2.id();
894 pool.checkin(c1, ctx());
895
896 let borrowed = pool.checkout_all_idle();
898 assert_eq!(borrowed.len(), 1);
899 assert_ne!(borrowed[0].id(), busy_id);
900 assert_eq!(pool.live(), 2);
902 assert_eq!(pool.info().members.iter().filter(|m| m.busy).count(), 2);
903
904 for c in borrowed {
905 pool.restore(c);
906 }
907 pool.checkin(c2, ctx());
908 assert_eq!(calls.load(Ordering::Relaxed), 2);
910 assert_eq!(pool.live(), 2);
911 }
912
913 #[tokio::test]
914 async fn checkout_all_idle_is_empty_when_nothing_is_idle() {
915 let (pool, calls) = fake_pool(2);
916 assert!(pool.checkout_all_idle().is_empty(), "empty pool");
917 let c = pool.checkout(|| fake_create(&calls)).await.unwrap();
918 assert!(pool.checkout_all_idle().is_empty(), "all busy");
919 pool.checkin(c, ctx());
920 }
921
922 #[tokio::test]
923 async fn restore_preserves_last_used_and_context() {
924 let (pool, calls) = fake_pool(2);
925 let c = pool.checkout(|| fake_create(&calls)).await.unwrap();
926 let recorded = QueryContext {
927 warehouse: Some("WH".into()),
928 ..QueryContext::default()
929 };
930 pool.checkin(c, recorded.clone());
931 let before = pool.info().members[0].clone();
932
933 tokio::time::sleep(std::time::Duration::from_millis(5)).await;
935 let borrowed = pool.checkout_all_idle();
936 assert_eq!(borrowed.len(), 1);
937 for c in borrowed {
938 pool.restore(c);
939 }
940
941 let after = pool.info().members[0].clone();
942 assert_eq!(after.last_used, before.last_used, "not a query");
943 assert_eq!(after.context, recorded, "recorded context untouched");
944 assert_eq!(after.query_count, before.query_count);
945 }
946
947 #[tokio::test]
948 async fn discarding_a_borrowed_idle_session_frees_capacity() {
949 let (pool, calls) = fake_pool(1);
950 let c = pool.checkout(|| fake_create(&calls)).await.unwrap();
951 pool.checkin(c, ctx());
952
953 let mut borrowed = pool.checkout_all_idle();
954 pool.discard(borrowed.pop().unwrap()); assert_eq!(pool.live(), 0);
956
957 let c = pool.checkout(|| fake_create(&calls)).await.unwrap();
959 assert_eq!(calls.load(Ordering::Relaxed), 2);
960 pool.checkin(c, ctx());
961 }
962
963 #[tokio::test]
964 async fn checkout_during_heartbeat_borrow_waits_and_reuses() {
965 let (pool, calls) = fake_pool(1);
966 let pool = Arc::new(pool);
967 let c = pool.checkout(|| fake_create(&calls)).await.unwrap();
968 let id = c.id();
969 pool.checkin(c, ctx());
970
971 let mut borrowed = pool.checkout_all_idle();
973 assert_eq!(borrowed.len(), 1);
974
975 let pool2 = Arc::clone(&pool);
978 let calls2 = Arc::clone(&calls);
979 let waiter = tokio::spawn(async move {
980 pool2
981 .checkout(move || async move {
982 let id = calls2.fetch_add(1, Ordering::Relaxed);
983 Ok::<(u32, QueryContext), std::convert::Infallible>((
984 id,
985 QueryContext::default(),
986 ))
987 })
988 .await
989 .unwrap()
990 });
991 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
992 assert!(!waiter.is_finished(), "waiter blocks while borrowed");
993
994 pool.restore(borrowed.pop().unwrap());
995 let c = tokio::time::timeout(std::time::Duration::from_secs(2), waiter)
996 .await
997 .expect("waiter must proceed once the session is restored")
998 .unwrap();
999 assert_eq!(c.id(), id, "reused the restored session");
1000 assert_eq!(calls.load(Ordering::Relaxed), 1, "no new auth");
1001 pool.checkin(c, ctx());
1002 }
1003
1004 #[test]
1005 fn registry_pools_returns_handles_without_draining() {
1006 let registry = PoolRegistry::new();
1007 assert!(registry.pools().is_empty());
1008 let p1 = registry.get_or_create(&SessionKey::new("A", "u"), 4);
1009 let p2 = registry.get_or_create(&SessionKey::new("B", "u"), 4);
1010 let pools = registry.pools();
1011 assert_eq!(
1012 pools.iter().map(|p| p.id()).collect::<Vec<_>>(),
1013 vec![p1.id(), p2.id()],
1014 "ordered by id"
1015 );
1016 assert_eq!(registry.len(), 2, "not drained");
1017 }
1018
1019 #[test]
1020 fn registry_get_and_get_by_id_never_create() {
1021 let registry = PoolRegistry::new();
1022 let key = SessionKey::new("ACCT", "user");
1023 assert!(registry.get(&key).is_none());
1025 assert!(registry.get_by_id(1).is_none());
1026 assert!(registry.is_empty());
1027
1028 let pool = registry.get_or_create(&key, 4);
1029 assert_eq!(registry.get(&key).map(|p| p.id()), Some(pool.id()));
1030 assert_eq!(
1031 registry.get_by_id(pool.id()).map(|p| p.id()),
1032 Some(pool.id())
1033 );
1034 assert!(registry.get_by_id(pool.id() + 99).is_none());
1035 }
1036
1037 #[test]
1038 fn registry_get_or_create_is_idempotent_per_key() {
1039 let registry = PoolRegistry::new();
1040 assert!(registry.is_empty());
1041 let key = SessionKey::new("ACCT", "user");
1042 let p1 = registry.get_or_create(&key, 4);
1043 let p2 = registry.get_or_create(&key, 4);
1044 assert_eq!(p1.id(), p2.id());
1045 assert_eq!(registry.len(), 1);
1046 assert!(registry.remove(&key).is_some());
1047 assert!(registry.remove(&key).is_none());
1048 }
1049}