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 #[must_use]
389 pub fn checkout_all_idle(&self) -> Vec<Checkout<S>> {
390 let mut checkouts = Vec::new();
391 while let Ok(permit) = Arc::clone(&self.permits).try_acquire_owned() {
392 match self.take_idle() {
393 Some((id, base, current, session)) => {
394 checkouts.push(self.make_checkout(permit, id, base, current, session));
395 }
396 None => break,
398 }
399 }
400 checkouts
401 }
402
403 pub fn restore(&self, mut checkout: Checkout<S>) {
407 checkout.done = true;
408 let session = checkout.session.take();
409 {
410 let mut slots = self.lock_slots();
411 if let Some(slot) = slots.iter_mut().find(|slot| slot.id == checkout.id) {
412 slot.session = session;
413 }
414 }
415 self.idle_notify.notify_waiters();
417 }
419
420 pub fn checkin(&self, mut checkout: Checkout<S>, current: QueryContext) {
422 checkout.done = true;
423 let session = checkout.session.take();
424 {
425 let mut slots = self.lock_slots();
426 if let Some(slot) = slots.iter_mut().find(|slot| slot.id == checkout.id) {
427 slot.current = current;
428 slot.last_used = Utc::now();
429 slot.running = None;
430 slot.session = session;
431 }
432 }
433 self.idle_notify.notify_waiters();
435 }
437
438 pub fn start_query(&self, member_id: u64, sql: String) {
441 let mut slots = self.lock_slots();
442 if let Some(slot) = slots.iter_mut().find(|slot| slot.id == member_id) {
443 slot.query_count += 1;
444 slot.running = Some(RunningQuery {
445 sql,
446 started_at: Utc::now(),
447 });
448 }
449 }
450
451 pub fn discard(&self, mut checkout: Checkout<S>) {
454 checkout.done = true;
455 self.lock_slots().retain(|slot| slot.id != checkout.id);
456 drop(checkout);
457 }
458
459 pub fn touch(&self) {
461 let mut meta = self.lock_meta();
462 meta.last_used = Utc::now();
463 meta.query_count += 1;
464 }
465
466 #[must_use]
468 pub fn id(&self) -> u64 {
469 self.id
470 }
471
472 #[must_use]
474 pub fn live(&self) -> usize {
475 self.lock_slots().len()
476 }
477
478 #[must_use]
480 pub fn info(&self) -> SessionInfo {
481 let members: Vec<MemberInfo> = {
482 let slots = self.lock_slots();
483 let mut members: Vec<MemberInfo> = slots
484 .iter()
485 .map(|slot| MemberInfo {
486 id: slot.id,
487 busy: slot.session.is_none(),
488 context: slot.current.clone(),
489 last_used: slot.last_used,
490 query_count: slot.query_count,
491 running: slot.running.clone(),
492 })
493 .collect();
494 members.sort_by_key(|m| m.id);
495 members
496 };
497 let meta = self.lock_meta();
498 SessionInfo {
499 id: self.id,
500 account: self.key.account.clone(),
501 user: self.key.user.clone(),
502 created_at: meta.created_at,
503 last_used: meta.last_used,
504 query_count: meta.query_count,
505 sessions: members.len(),
506 max_sessions: self.max,
507 members,
508 }
509 }
510
511 fn lock_slots(&self) -> MutexGuard<'_, Vec<Slot<S>>> {
512 self.slots
513 .lock()
514 .unwrap_or_else(std::sync::PoisonError::into_inner)
515 }
516
517 fn lock_meta(&self) -> MutexGuard<'_, PoolMeta> {
518 self.meta
519 .lock()
520 .unwrap_or_else(std::sync::PoisonError::into_inner)
521 }
522}
523
524#[derive(Clone)]
529pub struct PoolRegistry {
530 map: Arc<StdMutex<HashMap<SessionKey, Arc<SessionPool>>>>,
531 auth_gate: Arc<TokioMutex<()>>,
532 next_pool_id: Arc<AtomicU64>,
533}
534
535impl Default for PoolRegistry {
536 fn default() -> Self {
537 Self::new()
538 }
539}
540
541impl PoolRegistry {
542 #[must_use]
544 pub fn new() -> Self {
545 Self {
546 map: Arc::new(StdMutex::new(HashMap::new())),
547 auth_gate: Arc::new(TokioMutex::new(())),
548 next_pool_id: Arc::new(AtomicU64::new(1)),
549 }
550 }
551
552 pub fn get_or_create(&self, key: &SessionKey, max: usize) -> Arc<SessionPool> {
556 let mut map = self.lock();
557 if let Some(pool) = map.get(key) {
558 return Arc::clone(pool);
559 }
560 let id = self.next_pool_id.fetch_add(1, Ordering::Relaxed);
561 let pool = Arc::new(SessionPool::new(
562 id,
563 key.clone(),
564 max,
565 Utc::now(),
566 Arc::clone(&self.auth_gate),
567 ));
568 map.insert(key.clone(), Arc::clone(&pool));
569 pool
570 }
571
572 pub fn remove(&self, key: &SessionKey) -> Option<Arc<SessionPool>> {
574 self.lock().remove(key)
575 }
576
577 pub fn remove_by_id(&self, id: u64) -> Option<Arc<SessionPool>> {
579 let key = {
580 let map = self.lock();
581 map.iter()
582 .find(|(_, pool)| pool.id() == id)
583 .map(|(key, _)| key.clone())
584 };
585 key.and_then(|key| self.remove(&key))
586 }
587
588 pub fn take_all(&self) -> Vec<Arc<SessionPool>> {
590 self.lock().drain().map(|(_, pool)| pool).collect()
591 }
592
593 #[must_use]
596 pub fn pools(&self) -> Vec<Arc<SessionPool>> {
597 let mut pools: Vec<Arc<SessionPool>> = self.lock().values().map(Arc::clone).collect();
598 pools.sort_by_key(|pool| pool.id());
599 pools
600 }
601
602 #[must_use]
604 pub fn snapshot(&self) -> Vec<SessionInfo> {
605 let mut infos: Vec<SessionInfo> = self.lock().values().map(|pool| pool.info()).collect();
606 infos.sort_by_key(|info| info.id);
607 infos
608 }
609
610 #[must_use]
612 pub fn len(&self) -> usize {
613 self.lock().len()
614 }
615
616 #[must_use]
618 pub fn is_empty(&self) -> bool {
619 self.lock().is_empty()
620 }
621
622 fn lock(&self) -> MutexGuard<'_, HashMap<SessionKey, Arc<SessionPool>>> {
623 self.map
624 .lock()
625 .unwrap_or_else(std::sync::PoisonError::into_inner)
626 }
627}
628
629#[cfg(test)]
630#[allow(clippy::unwrap_used, clippy::expect_used)]
631mod tests {
632 use std::sync::atomic::AtomicU32;
633
634 use super::*;
635
636 fn ctx() -> QueryContext {
637 QueryContext::default()
638 }
639
640 fn fake_pool(max: usize) -> (SessionPool<u32>, Arc<AtomicU32>) {
641 let pool = SessionPool::<u32>::new(
642 1,
643 SessionKey::new("ACCT", "user"),
644 max,
645 Utc::now(),
646 Arc::new(TokioMutex::new(())),
647 );
648 (pool, Arc::new(AtomicU32::new(0)))
649 }
650
651 async fn fake_create(
652 counter: &AtomicU32,
653 ) -> std::result::Result<(u32, QueryContext), std::convert::Infallible> {
654 Ok((counter.fetch_add(1, Ordering::Relaxed), ctx()))
655 }
656
657 #[tokio::test]
658 async fn start_query_records_running_then_checkin_clears_it() {
659 let (pool, calls) = fake_pool(2);
660 let c = pool.checkout(|| fake_create(&calls)).await.unwrap();
661 pool.start_query(c.id(), "SELECT 1".to_string());
662
663 let member = pool.info().members[0].clone();
664 assert!(member.busy);
665 assert_eq!(member.query_count, 1);
666 assert_eq!(
667 member.running.as_ref().map(|r| r.sql.as_str()),
668 Some("SELECT 1")
669 );
670
671 pool.checkin(c, ctx());
672 let member = pool.info().members[0].clone();
673 assert!(!member.busy);
674 assert!(member.running.is_none(), "running cleared on checkin");
675 assert_eq!(member.query_count, 1, "count persists after checkin");
676 }
677
678 #[test]
679 fn overlay_lets_overrides_win() {
680 let base = QueryContext {
681 warehouse: Some("WH".into()),
682 role: Some("R".into()),
683 database: Some("DB".into()),
684 schema: Some("S".into()),
685 };
686 let overrides = QueryContext {
687 warehouse: Some("OTHER_WH".into()),
688 ..QueryContext::default()
689 };
690 let eff = base.overlay(&overrides);
691 assert_eq!(eff.warehouse.as_deref(), Some("OTHER_WH"));
692 assert_eq!(eff.role.as_deref(), Some("R")); assert_eq!(eff.database.as_deref(), Some("DB"));
694 }
695
696 #[test]
697 fn summary_renders_set_dimensions_or_default() {
698 assert_eq!(QueryContext::default().summary(), "(default)");
699 let c = QueryContext {
700 warehouse: Some("WH".into()),
701 role: Some("R".into()),
702 ..QueryContext::default()
703 };
704 assert_eq!(c.summary(), "WH/R");
705 }
706
707 #[tokio::test]
708 async fn checkin_reuses_session_and_lists_members() {
709 let (pool, calls) = fake_pool(4);
710 let c1 = pool.checkout(|| fake_create(&calls)).await.unwrap();
711 let id1 = c1.id();
712 let info = pool.info();
714 assert_eq!(info.members.len(), 1);
715 assert!(info.members[0].busy);
716 pool.checkin(c1, ctx());
717 assert!(!pool.info().members[0].busy);
719 let c2 = pool.checkout(|| fake_create(&calls)).await.unwrap();
721 assert_eq!(c2.id(), id1);
722 assert_eq!(calls.load(Ordering::Relaxed), 1, "should not create twice");
723 assert_eq!(pool.live(), 1);
724 pool.checkin(c2, ctx());
725 }
726
727 #[tokio::test]
728 async fn never_exceeds_capacity_and_blocks_until_checkin() {
729 let (pool, calls) = fake_pool(2);
730 let c1 = pool.checkout(|| fake_create(&calls)).await.unwrap();
731 let c2 = pool.checkout(|| fake_create(&calls)).await.unwrap();
732 assert_eq!(pool.live(), 2);
733 assert_eq!(pool.info().members.iter().filter(|m| m.busy).count(), 2);
735 let third = tokio::time::timeout(
737 std::time::Duration::from_millis(50),
738 pool.checkout(|| fake_create(&calls)),
739 )
740 .await;
741 assert!(third.is_err(), "third checkout should block at capacity");
742 pool.checkin(c1, ctx());
743 let c3 = pool.checkout(|| fake_create(&calls)).await.unwrap();
744 assert_eq!(pool.live(), 2, "reuse, not grow");
745 assert_eq!(calls.load(Ordering::Relaxed), 2);
746 pool.checkin(c2, ctx());
747 pool.checkin(c3, ctx());
748 }
749
750 #[tokio::test]
751 async fn discard_frees_capacity_for_a_fresh_session() {
752 let (pool, calls) = fake_pool(1);
753 let c1 = pool.checkout(|| fake_create(&calls)).await.unwrap();
754 assert_eq!(pool.live(), 1);
755 pool.discard(c1); assert_eq!(pool.live(), 0);
757 assert!(pool.info().members.is_empty());
758 let c2 = pool.checkout(|| fake_create(&calls)).await.unwrap();
759 assert_eq!(calls.load(Ordering::Relaxed), 2, "fresh session created");
760 assert_eq!(pool.live(), 1);
761 pool.checkin(c2, ctx());
762 }
763
764 #[tokio::test]
765 async fn orphaned_checkout_frees_its_slot_on_drop() {
766 let (pool, calls) = fake_pool(2);
767 {
768 let _c = pool.checkout(|| fake_create(&calls)).await.unwrap();
769 assert_eq!(pool.live(), 1);
770 }
772 assert_eq!(pool.live(), 0, "the Drop guard removed the orphaned slot");
773 assert!(pool.info().members.is_empty());
774 let c = pool.checkout(|| fake_create(&calls)).await.unwrap();
776 pool.checkin(c, ctx());
777 }
778
779 #[tokio::test]
780 async fn waiter_grabs_freed_session_without_waiting_out_the_in_flight_auth() {
781 let (pool, calls) = fake_pool(2);
782 let pool = Arc::new(pool);
783
784 let c1 = pool.checkout(|| fake_create(&calls)).await.unwrap();
786 assert_eq!(calls.load(Ordering::Relaxed), 1);
787
788 let held = Arc::clone(&pool.auth_gate).lock_owned().await;
791
792 let pool2 = Arc::clone(&pool);
795 let calls2 = Arc::clone(&calls);
796 let waiter = tokio::spawn(async move {
797 pool2
798 .checkout(move || async move {
799 let id = calls2.fetch_add(1, Ordering::Relaxed);
800 Ok::<(u32, QueryContext), std::convert::Infallible>((
801 id,
802 QueryContext::default(),
803 ))
804 })
805 .await
806 .unwrap()
807 });
808
809 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
811 pool.checkin(c1, ctx());
812
813 let c2 = tokio::time::timeout(std::time::Duration::from_secs(2), waiter)
816 .await
817 .expect("waiter must not block on the held auth gate")
818 .unwrap();
819 assert_eq!(
820 calls.load(Ordering::Relaxed),
821 1,
822 "reused the freed session — no new auth"
823 );
824 assert_eq!(pool.live(), 1, "still one session");
825
826 drop(held);
827 pool.checkin(c2, ctx());
828 }
829
830 #[tokio::test]
831 async fn checkout_all_idle_borrows_only_idle_sessions() {
832 let (pool, calls) = fake_pool(4);
833 let c1 = pool.checkout(|| fake_create(&calls)).await.unwrap();
834 let c2 = pool.checkout(|| fake_create(&calls)).await.unwrap();
835 let busy_id = c2.id();
836 pool.checkin(c1, ctx());
837
838 let borrowed = pool.checkout_all_idle();
840 assert_eq!(borrowed.len(), 1);
841 assert_ne!(borrowed[0].id(), busy_id);
842 assert_eq!(pool.live(), 2);
844 assert_eq!(pool.info().members.iter().filter(|m| m.busy).count(), 2);
845
846 for c in borrowed {
847 pool.restore(c);
848 }
849 pool.checkin(c2, ctx());
850 assert_eq!(calls.load(Ordering::Relaxed), 2);
852 assert_eq!(pool.live(), 2);
853 }
854
855 #[tokio::test]
856 async fn checkout_all_idle_is_empty_when_nothing_is_idle() {
857 let (pool, calls) = fake_pool(2);
858 assert!(pool.checkout_all_idle().is_empty(), "empty pool");
859 let c = pool.checkout(|| fake_create(&calls)).await.unwrap();
860 assert!(pool.checkout_all_idle().is_empty(), "all busy");
861 pool.checkin(c, ctx());
862 }
863
864 #[tokio::test]
865 async fn restore_preserves_last_used_and_context() {
866 let (pool, calls) = fake_pool(2);
867 let c = pool.checkout(|| fake_create(&calls)).await.unwrap();
868 let recorded = QueryContext {
869 warehouse: Some("WH".into()),
870 ..QueryContext::default()
871 };
872 pool.checkin(c, recorded.clone());
873 let before = pool.info().members[0].clone();
874
875 tokio::time::sleep(std::time::Duration::from_millis(5)).await;
877 let borrowed = pool.checkout_all_idle();
878 assert_eq!(borrowed.len(), 1);
879 for c in borrowed {
880 pool.restore(c);
881 }
882
883 let after = pool.info().members[0].clone();
884 assert_eq!(after.last_used, before.last_used, "not a query");
885 assert_eq!(after.context, recorded, "recorded context untouched");
886 assert_eq!(after.query_count, before.query_count);
887 }
888
889 #[tokio::test]
890 async fn discarding_a_borrowed_idle_session_frees_capacity() {
891 let (pool, calls) = fake_pool(1);
892 let c = pool.checkout(|| fake_create(&calls)).await.unwrap();
893 pool.checkin(c, ctx());
894
895 let mut borrowed = pool.checkout_all_idle();
896 pool.discard(borrowed.pop().unwrap()); assert_eq!(pool.live(), 0);
898
899 let c = pool.checkout(|| fake_create(&calls)).await.unwrap();
901 assert_eq!(calls.load(Ordering::Relaxed), 2);
902 pool.checkin(c, ctx());
903 }
904
905 #[tokio::test]
906 async fn checkout_during_heartbeat_borrow_waits_and_reuses() {
907 let (pool, calls) = fake_pool(1);
908 let pool = Arc::new(pool);
909 let c = pool.checkout(|| fake_create(&calls)).await.unwrap();
910 let id = c.id();
911 pool.checkin(c, ctx());
912
913 let mut borrowed = pool.checkout_all_idle();
915 assert_eq!(borrowed.len(), 1);
916
917 let pool2 = Arc::clone(&pool);
920 let calls2 = Arc::clone(&calls);
921 let waiter = tokio::spawn(async move {
922 pool2
923 .checkout(move || async move {
924 let id = calls2.fetch_add(1, Ordering::Relaxed);
925 Ok::<(u32, QueryContext), std::convert::Infallible>((
926 id,
927 QueryContext::default(),
928 ))
929 })
930 .await
931 .unwrap()
932 });
933 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
934 assert!(!waiter.is_finished(), "waiter blocks while borrowed");
935
936 pool.restore(borrowed.pop().unwrap());
937 let c = tokio::time::timeout(std::time::Duration::from_secs(2), waiter)
938 .await
939 .expect("waiter must proceed once the session is restored")
940 .unwrap();
941 assert_eq!(c.id(), id, "reused the restored session");
942 assert_eq!(calls.load(Ordering::Relaxed), 1, "no new auth");
943 pool.checkin(c, ctx());
944 }
945
946 #[test]
947 fn registry_pools_returns_handles_without_draining() {
948 let registry = PoolRegistry::new();
949 assert!(registry.pools().is_empty());
950 let p1 = registry.get_or_create(&SessionKey::new("A", "u"), 4);
951 let p2 = registry.get_or_create(&SessionKey::new("B", "u"), 4);
952 let pools = registry.pools();
953 assert_eq!(
954 pools.iter().map(|p| p.id()).collect::<Vec<_>>(),
955 vec![p1.id(), p2.id()],
956 "ordered by id"
957 );
958 assert_eq!(registry.len(), 2, "not drained");
959 }
960
961 #[test]
962 fn registry_get_or_create_is_idempotent_per_key() {
963 let registry = PoolRegistry::new();
964 assert!(registry.is_empty());
965 let key = SessionKey::new("ACCT", "user");
966 let p1 = registry.get_or_create(&key, 4);
967 let p2 = registry.get_or_create(&key, 4);
968 assert_eq!(p1.id(), p2.id());
969 assert_eq!(registry.len(), 1);
970 assert!(registry.remove(&key).is_some());
971 assert!(registry.remove(&key).is_none());
972 }
973}