blob: 3df10aad2a7611f67c6b12aaac838e73b6c05cdc [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//! Session pools (protocol spec §7): a bounded set of open sessions,
//! eagerly grown to `min_size` and lazily to `max_size`, handed out as
//! RAII guards. Round-robin endpoint spread happens naturally via the
//! rotating start index in [`Session::open`].
use std::collections::VecDeque;
use std::ops::{Deref, DerefMut};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Condvar, Mutex};
use std::time::{Duration, Instant};
use crate::client::session::{Session, SessionConfig};
use crate::error::{Error, Result};
/// Configuration for a [`SessionPool`] (or [`TableSessionPool`]).
///
/// `session` carries the per-connection settings — node urls,
/// user/password, dialect/database, connect timeout — shared by every
/// pooled session.
#[derive(Debug, Clone)]
pub struct SessionPoolConfig {
pub session: SessionConfig,
/// Upper bound on live sessions (idle + handed out). Default 8,
/// matching the C# SDK's pool size.
pub max_size: usize,
/// Sessions opened eagerly when the pool is created. Default 0.
pub min_size: usize,
/// How long [`SessionPool::acquire`] waits for an idle session once the
/// pool is at `max_size`. Default 60 s (Node.js `waitTimeout`).
pub acquire_timeout: Duration,
/// Idle sessions unused for longer than this are closed by the lazy
/// sweep (never shrinking the pool below `min_size`). Default 60 s
/// (Node.js `maxIdleTime`).
pub max_idle_time: Duration,
/// Minimum time between idle sweeps. There is **no** timer thread —
/// the sweep runs lazily on pool activity (acquire/release) once this
/// much time has passed since the previous sweep, so a completely idle
/// pool keeps its sessions until the next acquire. Default 30 s
/// (Node.js sweep interval).
pub idle_sweep_interval: Duration,
}
impl Default for SessionPoolConfig {
fn default() -> Self {
Self {
session: SessionConfig::default(),
max_size: 8,
min_size: 0,
acquire_timeout: Duration::from_secs(60),
max_idle_time: Duration::from_secs(60),
idle_sweep_interval: Duration::from_secs(30),
}
}
}
impl SessionPoolConfig {
/// Set endpoints from `"host:port"` node-url strings.
pub fn with_node_urls<S: AsRef<str>>(mut self, node_urls: &[S]) -> Result<Self> {
self.session = self.session.with_node_urls(node_urls)?;
Ok(self)
}
}
/// An idle session together with the moment it went idle, so the sweep
/// can measure how long it has been unused.
struct IdleEntry {
session: Session,
since: Instant,
}
impl IdleEntry {
fn new(session: Session) -> Self {
Self {
session,
since: Instant::now(),
}
}
}
/// Idle sessions plus the pool lifecycle flag, guarded by one mutex so
/// [`Condvar`] waiters observe both consistently. `last_sweep` gates the
/// lazy idle sweep (see [`SessionPoolConfig::idle_sweep_interval`]).
struct PoolState {
idle: VecDeque<IdleEntry>,
closed: bool,
last_sweep: Instant,
}
/// A pool of open tree-model [`Session`]s.
///
/// Sessions are created lazily up to `max_size` (after the eager
/// `min_size`); [`SessionPool::acquire`] blocks up to `acquire_timeout`
/// when the pool is exhausted. Dead sessions are discarded on acquire and
/// on release; sessions idle longer than `max_idle_time` are closed by a
/// lazy sweep that runs on pool activity (no timer thread — see
/// [`SessionPoolConfig::idle_sweep_interval`]), never shrinking the pool
/// below `min_size`. The pool tracks the most recent `USE <db>` seen on any
/// released session and replays it on acquire so every handed-out session
/// is in the pool's current database (spec §6.2).
pub struct SessionPool {
config: SessionPoolConfig,
state: Mutex<PoolState>,
available: Condvar,
/// Sessions alive right now: idle + handed out. Only mutated while
/// holding `state`, so `live < max_size` checks cannot overshoot.
live: AtomicUsize,
/// Pool-level current database, updated from released sessions.
database: Mutex<Option<String>>,
}
impl SessionPool {
/// Create the pool and eagerly open `min_size` sessions.
pub fn new(config: SessionPoolConfig) -> Result<SessionPool> {
if config.min_size > config.max_size {
return Err(Error::Client(format!(
"pool min_size ({}) > max_size ({})",
config.min_size, config.max_size
)));
}
let database = config.session.database.clone();
let pool = SessionPool {
config,
state: Mutex::new(PoolState {
idle: VecDeque::new(),
closed: false,
last_sweep: Instant::now(),
}),
available: Condvar::new(),
live: AtomicUsize::new(0),
database: Mutex::new(database),
};
for _ in 0..pool.config.min_size {
let session = pool.open_session()?;
let mut state = pool.state.lock().expect("pool lock poisoned");
state.idle.push_back(IdleEntry::new(session));
pool.live.fetch_add(1, Ordering::Relaxed);
}
Ok(pool)
}
/// Sessions alive right now (idle + handed out).
pub fn live_count(&self) -> usize {
self.live.load(Ordering::Relaxed)
}
/// Sessions sitting idle in the pool right now.
pub fn idle_count(&self) -> usize {
self.state.lock().expect("pool lock poisoned").idle.len()
}
/// Lazy idle sweep, run at acquire/release time (there is no timer
/// thread — a completely inactive pool is only swept on its next use).
/// No-op until `idle_sweep_interval` has elapsed since the last sweep;
/// then removes sessions idle longer than `max_idle_time`, oldest
/// first, never shrinking the pool (idle + handed out) below
/// `min_size`. Returns the expired sessions — the caller closes them
/// **after** dropping the state lock, so a slow `closeSession` RPC
/// cannot stall other pool users.
#[must_use]
fn sweep_idle(&self, state: &mut PoolState) -> Vec<Session> {
let now = Instant::now();
if now.duration_since(state.last_sweep) < self.config.idle_sweep_interval {
return Vec::new();
}
state.last_sweep = now;
let mut expired = Vec::new();
let mut i = 0;
while i < state.idle.len() {
if self.live.load(Ordering::Relaxed) - expired.len() <= self.config.min_size {
break;
}
if now.duration_since(state.idle[i].since) > self.config.max_idle_time {
expired.push(state.idle.remove(i).expect("index in bounds").session);
} else {
i += 1;
}
}
if !expired.is_empty() {
self.live.fetch_sub(expired.len(), Ordering::Relaxed);
log::debug!("idle sweep evicted {} session(s)", expired.len());
}
expired
}
/// Acquire a session, blocking up to `acquire_timeout` when the pool is
/// at capacity with nothing idle. Dead idle sessions are discarded and
/// the acquire retried; a fresh session is opened while under
/// `max_size`.
pub fn acquire(&self) -> Result<PooledSession<'_>> {
let deadline = Instant::now() + self.config.acquire_timeout;
let mut state = self.state.lock().expect("pool lock poisoned");
loop {
if state.closed {
return Err(Error::Client("session pool is closed".into()));
}
let expired = self.sweep_idle(&mut state);
if !expired.is_empty() {
drop(state);
for mut session in expired {
let _ = session.close();
}
state = self.state.lock().expect("pool lock poisoned");
continue; // re-check closed/idle after re-locking
}
// Idle session available → validate liveness, evict the dead.
while let Some(entry) = state.idle.pop_front() {
if entry.session.is_open() {
drop(state);
return self.hand_out(entry.session);
}
self.live.fetch_sub(1, Ordering::Relaxed);
}
// Below capacity → grow lazily. Count the slot while still
// holding the lock so concurrent acquires cannot overshoot.
if self.live.load(Ordering::Relaxed) < self.config.max_size {
self.live.fetch_add(1, Ordering::Relaxed);
drop(state);
match self.open_session() {
Ok(session) => return self.hand_out(session),
Err(e) => {
self.live.fetch_sub(1, Ordering::Relaxed);
self.available.notify_one();
return Err(e);
}
}
}
// At capacity → wait for a release, bounded by the deadline.
let now = Instant::now();
if now >= deadline {
return Err(Error::Client(format!(
"pool exhausted: no session available within {:?} ({} live, max {})",
self.config.acquire_timeout,
self.live.load(Ordering::Relaxed),
self.config.max_size
)));
}
let (guard, _) = self
.available
.wait_timeout(state, deadline - now)
.expect("pool lock poisoned");
state = guard;
}
}
/// Acquire a session for writes to `device_id`, preferring an idle
/// session already connected to the device's redirected endpoint.
///
/// A status-400 insert response leaves a device → endpoint hint in the
/// session that saw it ([`Session::redirect_hint`]); if any idle
/// session carries such a hint **and** another idle session is
/// connected to that endpoint, that session is handed out. In every
/// other case — no hint, no matching idle session, hint expired — this
/// is exactly [`SessionPool::acquire`]. The pool never opens a new
/// connection to the hinted endpoint (Node.js-style dedicated
/// per-endpoint sessions are future work).
pub fn acquire_for_device(&self, device_id: &str) -> Result<PooledSession<'_>> {
{
let mut state = self.state.lock().expect("pool lock poisoned");
if !state.closed {
// Any idle session may hold the hint (the one that got the
// 400), not necessarily one connected to the hinted node.
let hint = state
.idle
.iter_mut()
.find_map(|e| e.session.redirect_hint(device_id));
if let Some(endpoint) = hint {
let matching = state.idle.iter().position(|e| {
e.session.is_open() && e.session.current_endpoint() == Some(&endpoint)
});
if let Some(pos) = matching {
let entry = state.idle.remove(pos).expect("index in bounds");
drop(state);
return self.hand_out(entry.session);
}
}
}
}
self.acquire()
}
/// Convenience: acquire a session, run one non-query statement, release.
/// `USE <db>` propagates to the whole pool via the database tracking.
pub fn execute_non_query(&self, sql: &str) -> Result<()> {
self.acquire()?.execute_non_query(sql)
}
/// Close the pool: no further acquires; drain and close all idle
/// sessions. Sessions currently handed out are closed when their guards
/// drop.
pub fn close(&self) {
let drained = {
let mut state = self.state.lock().expect("pool lock poisoned");
state.closed = true;
std::mem::take(&mut state.idle)
};
self.live.fetch_sub(drained.len(), Ordering::Relaxed);
for mut entry in drained {
let _ = entry.session.close();
}
self.available.notify_all();
}
fn open_session(&self) -> Result<Session> {
let mut config = self.config.session.clone();
// New sessions start in the pool's current database (config key
// "db"), so no catch-up USE is needed on first hand-out.
config.database = self.database.lock().expect("pool lock poisoned").clone();
let mut session = Session::new(config);
session.open()?;
Ok(session)
}
/// Final step of acquire: sync the session onto the pool's current
/// database before handing it out. On USE failure the session is
/// discarded, not returned to the pool.
fn hand_out(&self, mut session: Session) -> Result<PooledSession<'_>> {
let pool_db = self.database.lock().expect("pool lock poisoned").clone();
if let Some(db) = pool_db {
if session.database() != Some(db.as_str()) {
if let Err(e) = session.execute_non_query(&format!("USE {db}")) {
let _ = session.close();
self.live.fetch_sub(1, Ordering::Relaxed);
self.available.notify_one();
return Err(e);
}
}
}
Ok(PooledSession {
pool: self,
session: Some(session),
})
}
/// Return a session from a dropped guard. Dead sessions are discarded;
/// live ones update the pool database and go back to the idle queue.
fn release(&self, session: Session) {
if !session.is_open() {
self.live.fetch_sub(1, Ordering::Relaxed);
self.available.notify_one();
return;
}
if let Some(db) = session.database() {
let mut pool_db = self.database.lock().expect("pool lock poisoned");
if pool_db.as_deref() != Some(db) {
*pool_db = Some(db.to_string());
}
}
let mut state = self.state.lock().expect("pool lock poisoned");
if state.closed {
drop(state);
self.live.fetch_sub(1, Ordering::Relaxed);
let mut session = session;
let _ = session.close();
} else {
state.idle.push_back(IdleEntry::new(session));
let expired = self.sweep_idle(&mut state);
drop(state);
for mut session in expired {
let _ = session.close();
}
}
self.available.notify_one();
}
/// Test hook: push a pre-built session (possibly dead) into the idle
/// queue, counting it as live.
#[cfg(test)]
fn inject_idle(&self, session: Session) {
let mut state = self.state.lock().expect("pool lock poisoned");
state.idle.push_back(IdleEntry::new(session));
self.live.fetch_add(1, Ordering::Relaxed);
}
}
impl Drop for SessionPool {
fn drop(&mut self) {
self.close();
}
}
/// RAII guard for a pooled [`Session`]. Derefs to the session; returns it
/// to the pool on drop (dead sessions are discarded instead).
pub struct PooledSession<'a> {
pool: &'a SessionPool,
session: Option<Session>,
}
impl std::fmt::Debug for PooledSession<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PooledSession")
.field("open", &self.session.as_ref().is_some_and(Session::is_open))
.finish_non_exhaustive()
}
}
impl Deref for PooledSession<'_> {
type Target = Session;
fn deref(&self) -> &Session {
self.session.as_ref().expect("session taken")
}
}
impl DerefMut for PooledSession<'_> {
fn deref_mut(&mut self) -> &mut Session {
self.session.as_mut().expect("session taken")
}
}
impl Drop for PooledSession<'_> {
fn drop(&mut self) {
if let Some(session) = self.session.take() {
self.pool.release(session);
}
}
}
/// A pool of table-dialect sessions — a [`SessionPool`] whose sessions are
/// opened with `sql_dialect="table"` (and optionally a database), per
/// protocol spec §6. `USE <db>` on any pooled session propagates pool-wide
/// on release.
pub struct TableSessionPool {
pool: SessionPool,
}
impl TableSessionPool {
/// Create the pool, forcing the table dialect on the session config.
pub fn new(mut config: SessionPoolConfig) -> Result<TableSessionPool> {
config.session.sql_dialect = "table".into();
Ok(TableSessionPool {
pool: SessionPool::new(config)?,
})
}
/// Acquire a table-dialect session guard.
pub fn acquire(&self) -> Result<PooledSession<'_>> {
self.pool.acquire()
}
/// Convenience: acquire, run one non-query statement, release.
pub fn execute_non_query(&self, sql: &str) -> Result<()> {
self.pool.execute_non_query(sql)
}
pub fn live_count(&self) -> usize {
self.pool.live_count()
}
pub fn idle_count(&self) -> usize {
self.pool.idle_count()
}
pub fn close(&self) {
self.pool.close()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::connection::Endpoint;
use std::net::TcpStream;
/// Endpoint nothing listens on: connects are refused ~immediately.
fn dead_endpoint_config() -> SessionConfig {
SessionConfig {
endpoints: vec![Endpoint::new("127.0.0.1", 1)],
connect_timeout: Duration::from_millis(200),
..SessionConfig::default()
}
}
fn live_server_available() -> bool {
TcpStream::connect_timeout(
&"127.0.0.1:6667".parse().unwrap(),
Duration::from_millis(300),
)
.is_ok()
}
#[test]
fn config_defaults() {
let cfg = SessionPoolConfig::default();
assert_eq!(cfg.max_size, 8);
assert_eq!(cfg.min_size, 0);
assert_eq!(cfg.acquire_timeout, Duration::from_secs(60));
assert_eq!(cfg.max_idle_time, Duration::from_secs(60));
assert_eq!(cfg.idle_sweep_interval, Duration::from_secs(30));
}
#[test]
fn min_greater_than_max_is_rejected() {
let cfg = SessionPoolConfig {
min_size: 9,
max_size: 8,
..Default::default()
};
assert!(SessionPool::new(cfg).is_err());
}
#[test]
fn exhausted_pool_times_out_with_typed_error() {
// max_size 0: nothing idle, no growth allowed → deadline error.
let cfg = SessionPoolConfig {
max_size: 0,
acquire_timeout: Duration::from_millis(50),
session: dead_endpoint_config(),
..Default::default()
};
let pool = SessionPool::new(cfg).unwrap();
let start = Instant::now();
match pool.acquire() {
Err(Error::Client(msg)) => assert!(msg.contains("pool exhausted"), "{msg}"),
other => panic!("expected pool-exhausted error, got {other:?}"),
}
assert!(start.elapsed() >= Duration::from_millis(50));
}
#[test]
fn dead_idle_sessions_are_evicted_on_acquire() {
let cfg = SessionPoolConfig {
max_size: 1,
acquire_timeout: Duration::from_millis(50),
session: dead_endpoint_config(),
..Default::default()
};
let pool = SessionPool::new(cfg).unwrap();
// A never-opened session is dead (is_open() == false).
pool.inject_idle(Session::new(dead_endpoint_config()));
assert_eq!(pool.live_count(), 1);
// Acquire evicts the dead session, then tries to open a fresh one,
// which fails against the dead endpoint — a connect error, NOT
// "pool exhausted".
match pool.acquire() {
Err(Error::Thrift(_)) => {}
other => panic!("expected thrift connect error, got {other:?}"),
}
// The dead session and the failed growth slot are both released.
assert_eq!(pool.live_count(), 0);
}
#[test]
fn acquire_on_closed_pool_fails() {
let cfg = SessionPoolConfig {
session: dead_endpoint_config(),
..Default::default()
};
let pool = SessionPool::new(cfg).unwrap();
pool.close();
match pool.acquire() {
Err(Error::Client(msg)) => assert!(msg.contains("closed"), "{msg}"),
other => panic!("expected closed-pool error, got {other:?}"),
};
}
#[test]
fn releasing_dead_session_shrinks_live_count() {
let cfg = SessionPoolConfig {
max_size: 1,
session: dead_endpoint_config(),
..Default::default()
};
let pool = SessionPool::new(cfg).unwrap();
// Simulate a handed-out session dying before release.
pool.live.fetch_add(1, Ordering::Relaxed);
pool.release(Session::new(dead_endpoint_config()));
assert_eq!(pool.live_count(), 0);
let state = pool.state.lock().unwrap();
assert!(state.idle.is_empty());
}
#[test]
fn table_pool_forces_table_dialect() {
let cfg = SessionPoolConfig {
session: SessionConfig {
sql_dialect: "tree".into(), // deliberately wrong
..dead_endpoint_config()
},
..Default::default()
};
let pool = TableSessionPool::new(cfg).unwrap();
assert_eq!(pool.pool.config.session.sql_dialect, "table");
}
/// A local listener that accepts and immediately drops connections:
/// `Connection::open` succeeds (giving pool tests real TCP connections
/// with distinct endpoints) while any RPC on them — like the swallowed
/// `closeSession` at pool teardown — fails fast on EOF instead of
/// blocking on a reply that never comes.
fn fake_listener() -> Endpoint {
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
let port = listener.local_addr().expect("local_addr").port();
std::thread::spawn(move || {
for stream in listener.incoming() {
match stream {
Ok(s) => drop(s),
Err(_) => break,
}
}
});
Endpoint::new("127.0.0.1", port)
}
fn injected_session(endpoint: &Endpoint) -> Session {
let mut session = Session::new(dead_endpoint_config());
let connection = crate::connection::Connection::open(
endpoint.clone(),
&crate::connection::ConnectionOptions {
connect_timeout: Duration::from_millis(500),
..Default::default()
},
)
.expect("connect to test listener");
session.test_inject_connection(connection);
session
}
#[test]
fn acquire_for_device_prefers_hinted_endpoint() {
let ep_a = fake_listener();
let ep_b = fake_listener();
let cfg = SessionPoolConfig {
max_size: 4,
acquire_timeout: Duration::from_millis(50),
session: dead_endpoint_config(),
..Default::default()
};
let pool = SessionPool::new(cfg).unwrap();
// s1 (connected to A) holds the redirect hint pointing at B;
// s2 is connected to B — the hinted target.
let mut s1 = injected_session(&ep_a);
s1.test_inject_redirect_hint("root.sg.d1", ep_b.clone());
let s2 = injected_session(&ep_b);
pool.inject_idle(s1);
pool.inject_idle(s2);
// Hinted device → the session on endpoint B, even though the
// session on A is first in the idle queue.
let guard = pool.acquire_for_device("root.sg.d1").unwrap();
assert_eq!(guard.current_endpoint(), Some(&ep_b));
drop(guard);
// Unknown device → plain FIFO acquire (the session on A).
let guard = pool.acquire_for_device("root.sg.unknown").unwrap();
assert_eq!(guard.current_endpoint(), Some(&ep_a));
}
#[test]
fn acquire_for_device_falls_back_when_no_session_matches_hint() {
let ep_a = fake_listener();
let ep_gone = Endpoint::new("10.255.255.1", 6667); // nobody connected here
let cfg = SessionPoolConfig {
max_size: 4,
acquire_timeout: Duration::from_millis(50),
session: dead_endpoint_config(),
..Default::default()
};
let pool = SessionPool::new(cfg).unwrap();
let mut s1 = injected_session(&ep_a);
s1.test_inject_redirect_hint("root.sg.d1", ep_gone);
pool.inject_idle(s1);
// Hint exists but no idle session is connected to that endpoint →
// normal acquire semantics (FIFO hand-out of s1).
let guard = pool.acquire_for_device("root.sg.d1").unwrap();
assert_eq!(guard.current_endpoint(), Some(&ep_a));
}
/// Backdate an idle entry and the sweep clock so eviction tests are
/// deterministic without long sleeps.
fn backdate(pool: &SessionPool, entry_ages: Duration, sweep_age: Duration) {
let mut state = pool.state.lock().unwrap();
state.last_sweep = Instant::now() - sweep_age;
for entry in &mut state.idle {
entry.since = Instant::now() - entry_ages;
}
}
/// Sessions idle past `max_idle_time` are closed by the sweep on the
/// next acquire; the freed capacity is then used to (try to) grow.
#[test]
fn idle_sessions_are_evicted_on_acquire() {
let ep = fake_listener();
let cfg = SessionPoolConfig {
max_size: 4,
acquire_timeout: Duration::from_millis(50),
max_idle_time: Duration::from_millis(5),
idle_sweep_interval: Duration::from_millis(1),
session: dead_endpoint_config(),
..Default::default()
};
let pool = SessionPool::new(cfg).unwrap();
pool.inject_idle(injected_session(&ep));
pool.inject_idle(injected_session(&ep));
assert_eq!(pool.idle_count(), 2);
// Exceed max_idle_time, then acquire: the sweep evicts both idle
// sessions and the acquire grows — which fails against the dead
// endpoint (a connect error, proving the idle queue really was
// emptied rather than handed out).
std::thread::sleep(Duration::from_millis(10));
match pool.acquire() {
Err(Error::Thrift(_)) => {}
other => panic!("expected thrift connect error, got {other:?}"),
}
assert_eq!(pool.idle_count(), 0);
assert_eq!(pool.live_count(), 0);
}
/// The sweep never shrinks the pool (idle + handed out) below
/// `min_size`, evicting oldest-first.
#[test]
fn idle_sweep_respects_min_size_floor() {
let ep = fake_listener();
let cfg = SessionPoolConfig {
max_size: 4,
max_idle_time: Duration::from_millis(5),
idle_sweep_interval: Duration::from_millis(1),
session: dead_endpoint_config(),
..Default::default()
};
let mut pool = SessionPool::new(cfg).unwrap();
pool.config.min_size = 2; // set post-new: eager open would need a server
for _ in 0..3 {
pool.inject_idle(injected_session(&ep));
}
backdate(&pool, Duration::from_millis(10), Duration::from_millis(10));
let mut state = pool.state.lock().unwrap();
let expired = pool.sweep_idle(&mut state);
// All 3 exceed max_idle_time, but only 1 may go: 3 live - 1 = floor.
assert_eq!(expired.len(), 1);
assert_eq!(state.idle.len(), 2);
drop(state);
assert_eq!(pool.live_count(), 2);
}
/// The sweep is a no-op until `idle_sweep_interval` has passed since the
/// previous sweep — even when idle sessions have already expired.
#[test]
fn idle_sweep_is_gated_by_interval() {
let ep = fake_listener();
let cfg = SessionPoolConfig {
max_size: 4,
max_idle_time: Duration::from_millis(1),
idle_sweep_interval: Duration::from_secs(3600),
session: dead_endpoint_config(),
..Default::default()
};
let pool = SessionPool::new(cfg).unwrap();
pool.inject_idle(injected_session(&ep));
// Entry long expired, but the last sweep (pool creation) is recent
// relative to the huge interval → gated.
backdate(&pool, Duration::from_secs(10), Duration::ZERO);
let mut state = pool.state.lock().unwrap();
assert!(pool.sweep_idle(&mut state).is_empty());
assert_eq!(state.idle.len(), 1);
// Once the interval has elapsed the same entry is evicted…
state.last_sweep = Instant::now() - Duration::from_secs(3601);
assert_eq!(pool.sweep_idle(&mut state).len(), 1);
// …and last_sweep was refreshed, so an immediate re-sweep is gated.
assert!(pool.sweep_idle(&mut state).is_empty());
}
/// Releasing a session also drives the sweep: the stale idle session is
/// evicted while the just-released (fresh) one stays.
#[test]
fn release_triggers_sweep_but_keeps_fresh_session() {
let ep = fake_listener();
let cfg = SessionPoolConfig {
max_size: 4,
max_idle_time: Duration::from_millis(5),
idle_sweep_interval: Duration::from_millis(1),
session: dead_endpoint_config(),
..Default::default()
};
let pool = SessionPool::new(cfg).unwrap();
pool.inject_idle(injected_session(&ep));
backdate(&pool, Duration::from_millis(10), Duration::from_millis(10));
// Simulate returning a handed-out session (live already counted).
pool.live.fetch_add(1, Ordering::Relaxed);
pool.release(injected_session(&ep));
assert_eq!(pool.idle_count(), 1, "stale evicted, fresh kept");
assert_eq!(pool.live_count(), 1);
}
/// Live-server tests: acquire/release round-trip, reuse, and blocking
/// hand-off. Skipped when no IoTDB server is reachable.
#[test]
fn live_pool_acquire_release_reuse() {
if !live_server_available() {
eprintln!("skipping live_pool_acquire_release_reuse: no server on 127.0.0.1:6667");
return;
}
let cfg = SessionPoolConfig {
max_size: 2,
acquire_timeout: Duration::from_secs(5),
..Default::default()
};
let pool = SessionPool::new(cfg).unwrap();
{
let mut s1 = pool.acquire().unwrap();
assert!(s1.is_open());
let mut ds = s1.execute_query("SHOW DATABASES").unwrap();
while ds.next_row().unwrap().is_some() {}
drop(ds);
let _s2 = pool.acquire().unwrap();
assert_eq!(pool.live_count(), 2);
}
// Both released; a re-acquire reuses an idle session (no growth).
let _s3 = pool.acquire().unwrap();
assert_eq!(pool.live_count(), 2);
drop(_s3);
pool.close();
assert_eq!(pool.live_count(), 0);
}
#[test]
fn live_pool_waiter_wakes_on_release() {
if !live_server_available() {
eprintln!("skipping live_pool_waiter_wakes_on_release: no server on 127.0.0.1:6667");
return;
}
let cfg = SessionPoolConfig {
max_size: 1,
acquire_timeout: Duration::from_secs(5),
..Default::default()
};
let pool = std::sync::Arc::new(SessionPool::new(cfg).unwrap());
let guard = pool.acquire().unwrap();
let p2 = pool.clone();
let waiter = std::thread::spawn(move || p2.acquire().map(|s| s.is_open()));
std::thread::sleep(Duration::from_millis(100));
drop(guard); // wakes the waiter
assert!(waiter.join().unwrap().unwrap());
assert_eq!(pool.live_count(), 1);
}
}