questdb/db.rs
1/*******************************************************************************
2 * ___ _ ____ ____
3 * / _ \ _ _ ___ ___| |_| _ \| __ )
4 * | | | | | | |/ _ \/ __| __| | | | _ \
5 * | |_| | |_| | __/\__ \ |_| |_| | |_) |
6 * \__\_\\__,_|\___||___/\__|____/|____/
7 *
8 * Copyright (c) 2014-2019 Appsicle
9 * Copyright (c) 2019-2025 QuestDB
10 *
11 * Licensed under the Apache License, Version 2.0 (the "License");
12 * you may not use this file except in compliance with the License.
13 * You may obtain a copy of the License at
14 *
15 * http://www.apache.org/licenses/LICENSE-2.0
16 *
17 * Unless required by applicable law or agreed to in writing, software
18 * distributed under the License is distributed on an "AS IS" BASIS,
19 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20 * See the License for the specific language governing permissions and
21 * limitations under the License.
22 *
23 ******************************************************************************/
24
25//! QWP ingestion connection pool.
26//!
27//! `QuestDb` is a thread-safe pool of store-and-forward producer handles to a
28//! single QuestDB QWP/WebSocket endpoint. By default (Java parity) `connect`
29//! is eager: it pre-opens the warm minimums (`sender_pool_min`,
30//! `query_pool_min`), honoring `initial_connect_retry` for the ingest
31//! senders (readers always connect fail-fast), so a down server fails the
32//! constructor fast. With `lazy_connect=true` the pool tolerates a down
33//! server at startup: `connect` performs no blocking network I/O,
34//! `query_pool_min` defaults to 0, and borrowing
35//! [`QuestDb::borrow_sender`] creates a local store-and-forward producer
36//! immediately whose background runner connects later, so callers can buffer
37//! while the server is absent. In disk-backed store-and-forward mode either
38//! variant may pre-open parked recovery senders whose initial connect and
39//! replay run in the background. Direct ingestion senders open their
40//! transport on first borrow.
41//! The pools auto-grow up to their configured caps (`sender_pool_max` /
42//! `query_pool_max`) on demand and (under `pool_reap=auto`)
43//! run a background thread that closes above-minimum idle entries after
44//! `idle_timeout_ms`.
45//!
46//! Each pool slot is handed out as a [`BorrowedSender`] which returns
47//! itself to the pool on `Drop`. Slots whose underlying connection has
48//! latched terminal state are dropped on return instead of being
49//! recycled.
50
51use std::fmt::{self, Debug, Formatter};
52use std::marker::PhantomData;
53#[cfg(feature = "_egress")]
54use std::ops::{Deref, DerefMut};
55use std::path::{Path, PathBuf};
56use std::rc::Rc;
57use std::sync::atomic::{AtomicBool, Ordering};
58use std::sync::{Arc, Condvar, Mutex};
59use std::thread::{self, JoinHandle};
60use std::time::{Duration, Instant};
61
62#[cfg(feature = "_egress")]
63use crate::egress::Reader;
64use crate::ingress::conn_events;
65use crate::ingress::rejection_events;
66use crate::ingress::sender::is_candidate_orphan;
67use crate::ingress::sender::qwp_ws::QwpWsHostHealthTracker;
68use crate::ingress::{Buffer, SenderBuilder};
69use crate::ingress::{
70 QwpWsConnector, QwpWsManagedSlotExclusion, RawQwpWsRoundStream, ReconnectReason,
71};
72// The reconnect backoff helpers are only consumed by the retry-capable borrow
73// paths: Polars `reborrow_with_retry` and the FFI owned
74// `*_with_retry` entry points. Keep the import unconditional (so the shared
75// re-export chain that feeds it stays live) but quiet the unused-import lint in
76// the plain library build that compiles neither retry path.
77#[cfg_attr(
78 not(any(
79 feature = "polars-ingress",
80 feature = "polars-egress",
81 feature = "ffi-support"
82 )),
83 allow(unused_imports)
84)]
85use crate::ingress::{reconnect_backoff_step, reconnect_error_is_terminal};
86use crate::{Result, error};
87
88/// Connect-string parsing for the [`QuestDb`] pool. Shared by every borrow
89/// kind (store-and-forward ingestion, direct ingestion, reader), so it lives
90/// with the pool rather than under a payload encoder.
91mod conf;
92
93use crate::ingress::AckLevel;
94use crate::ingress::column_sender::conn::ColumnConn;
95use crate::ingress::column_sender::{DirectSenderCore, PooledSenderCore};
96use conf::PoolReap;
97
98/// FFI escape-hatch surface: owned (lifetime-free) pool handles and the entry
99/// points that mint them, for the `questdb-rs-ffi` C-ABI crate. Hidden,
100/// feature-gated, and not part of the public Rust API — normal Rust users
101/// borrow lifetime-bound handles via [`QuestDb::borrow_sender`] (and, with
102/// egress, `QuestDb::borrow_reader`).
103/// Only `questdb-rs-ffi` enables the `ffi-support` feature.
104#[cfg(feature = "ffi-support")]
105#[doc(hidden)]
106pub mod ffi_support;
107
108/// Lower bound on the reaper's wake interval.
109const REAPER_MIN_TICK: Duration = Duration::from_secs(5);
110
111/// Poison-tolerant lock helper. The pool must survive a panic in another
112/// thread's locked region: under `panic=abort` (FFI consumers) poisoning
113/// can never be observed, but `questdb-rs` library consumers run with
114/// `panic=unwind` and a single panicking thread would otherwise turn
115/// every subsequent borrow/return into a panic via `.expect("poisoned")`.
116fn lock_state<S>(m: &Mutex<PoolState<S>>) -> std::sync::MutexGuard<'_, PoolState<S>> {
117 m.lock().unwrap_or_else(|e| e.into_inner())
118}
119
120fn lock_health(
121 m: &Mutex<QwpWsHostHealthTracker>,
122) -> std::sync::MutexGuard<'_, QwpWsHostHealthTracker> {
123 m.lock().unwrap_or_else(|e| e.into_inner())
124}
125
126#[cfg(feature = "_egress")]
127fn lock_reader_state(m: &Mutex<ReaderPoolState>) -> std::sync::MutexGuard<'_, ReaderPoolState> {
128 m.lock().unwrap_or_else(|e| e.into_inner())
129}
130
131/// RAII guard that increments `state.in_use` on construction and
132/// decrements it on drop unless [`InUseSlot::commit`] is called first.
133/// Closes the leak window between `state.in_use += 1` and the connect
134/// round: a panic in the connect path (allocator OOM,
135/// TLS handshake panic) would otherwise skip the matching decrement
136/// and permanently strand a pool slot.
137struct InUseSlot<'a, S> {
138 state: &'a Mutex<PoolState<S>>,
139 cv: &'a Condvar,
140 slot_index: Option<usize>,
141 armed: bool,
142}
143
144impl<S> InUseSlot<'_, S> {
145 fn commit(mut self) {
146 self.armed = false;
147 }
148}
149
150impl<S> Drop for InUseSlot<'_, S> {
151 fn drop(&mut self) {
152 if self.armed {
153 let mut state = lock_state(self.state);
154 state.in_use = state.in_use.saturating_sub(1);
155 state.free_slot_index(self.slot_index);
156 self.cv.notify_all();
157 }
158 }
159}
160
161#[cfg(feature = "_egress")]
162struct ReaderInUseSlot<'a> {
163 inner: &'a DbInner,
164 armed: bool,
165}
166
167#[cfg(feature = "_egress")]
168impl ReaderInUseSlot<'_> {
169 fn commit(mut self) {
170 self.armed = false;
171 }
172}
173
174#[cfg(feature = "_egress")]
175impl Drop for ReaderInUseSlot<'_> {
176 fn drop(&mut self) {
177 if self.armed {
178 {
179 let mut state = lock_reader_state(&self.inner.reader_state);
180 state.in_use = state.in_use.saturating_sub(1);
181 }
182 self.inner.reader_cv.notify_all();
183 }
184 }
185}
186
187struct SenderSlotRelease<'a> {
188 inner: &'a DbInner,
189 slot_index: Option<usize>,
190 decrement_in_use: bool,
191 decrement_closing: bool,
192}
193
194impl Drop for SenderSlotRelease<'_> {
195 fn drop(&mut self) {
196 if self.slot_index.is_none() && !self.decrement_in_use && !self.decrement_closing {
197 return;
198 }
199 let mut state = lock_state(&self.inner.state);
200 if self.decrement_in_use {
201 state.in_use = state.in_use.saturating_sub(1);
202 }
203 if self.decrement_closing {
204 state.closing = state.closing.saturating_sub(1);
205 }
206 state.free_slot_index(self.slot_index);
207 self.inner.cv.notify_all();
208 }
209}
210
211/// Connection pool for QWP/WebSocket ingestion and egress.
212///
213/// Construct with [`QuestDb::connect`]. Share the pool across threads — its
214/// internal state is `Mutex`-guarded so [`QuestDb::borrow_sender`] /
215/// [`QuestDb::reap_idle`] / Drop-driven returns are safe to interleave.
216///
217/// Each borrow ([`BorrowedSender`] / the internal direct sender) is **not**
218/// `Send` — it belongs to the thread that borrowed it. To ingest in parallel,
219/// borrow one sender per worker thread from the same `QuestDb`.
220/// Optional per-pool event handlers for [`QuestDb::connect_with_handlers`].
221#[derive(Default)]
222#[non_exhaustive]
223pub struct ConnectHandlers {
224 /// Connection lifecycle listener; see [`QuestDb::connect_with_listener`].
225 pub connection_listener: Option<crate::ingress::ConnectionListener>,
226 /// Listener inbox capacity; `0` selects the default (64).
227 pub connection_event_inbox_capacity: usize,
228 /// Server-rejection handler; without one every rejection is logged.
229 pub error_handler: Option<crate::ingress::QwpWsErrorHandler>,
230 /// Handler inbox capacity; `0` selects the default (64).
231 pub error_inbox_capacity: usize,
232}
233
234pub struct QuestDb {
235 inner: Arc<DbInner>,
236 reaper: Option<JoinHandle<()>>,
237}
238
239struct DbInner {
240 /// Original connect string. Kept verbatim so the reader pool
241 /// (`Reader::from_conf`) can spin up a new connection with the same
242 /// settings. The sender pools connect through pre-parsed builders so they
243 /// can override only the managed disk-SF slot id.
244 #[cfg(feature = "_egress")]
245 conf: String,
246 /// Resolved, reusable QWP/WebSocket connect ingredients (endpoint list,
247 /// TLS, auth, config). Every sender connection — first-borrow open,
248 /// auto-grow, and failover re-borrow — opens through this connector so it rotates
249 /// across the configured endpoints. A single-endpoint pool behaves
250 /// exactly as before (one endpoint, no rotation).
251 connector: QwpWsConnector,
252 /// Buffer-factory configuration retained directly on the pool root so a
253 /// caller can create a QWP/WebSocket Buffer without borrowing a sender.
254 buffer_max_name_len: usize,
255 /// One health tracker shared by every connect attempt. A connect failure
256 /// or a mid-stream transport death marks the offending endpoint unhealthy
257 /// so subsequent borrows skip it until it re-probes healthy; role rejects
258 /// rotate to the writable primary. Pool-level (not per-conn) so the pool
259 /// stops handing out connections to a dead peer rather than rediscovering
260 /// it one connection at a time.
261 health: Mutex<QwpWsHostHealthTracker>,
262 /// Warm minimum the reaper preserves in the store-and-forward
263 /// ingestion pool.
264 sender_pool_min: usize,
265 /// Hard cap on the store-and-forward ingestion pool and on the direct
266 /// column-sender pool (both are ingestion-side connections).
267 sender_pool_max: usize,
268 /// Warm minimum the reaper preserves in the reader pool.
269 #[cfg(feature = "_egress")]
270 query_pool_min: usize,
271 /// Hard cap on the reader pool.
272 #[cfg(feature = "_egress")]
273 query_pool_max: usize,
274 /// How long an at-cap borrow waits for a connection to be returned
275 /// before failing. Zero disables waiting (fail-fast).
276 acquire_timeout: Duration,
277 /// `sf_dir` set: store-and-forward senders use pool-minted disk slots.
278 sf_disk: bool,
279 /// Configured `sender_id` kept as the slot base. Disk-backed pool slots are
280 /// minted as `<base>-ingest-<index>`.
281 slot_base_id: String,
282 /// Managed ingestion slot range excluded from orphan scans so sibling
283 /// senders do not adopt each other's live pool slots.
284 managed_slot_exclusion: Option<QwpWsManagedSlotExclusion>,
285 /// Same-base managed slots left outside this pool's live index range by a
286 /// larger previous run. Snapshotted once before the pool is published and
287 /// reused by every sender build, so borrow-triggered growth never rescans
288 /// `sf_dir`. The pool namespace is exclusive: a same-base slot created
289 /// after connect is recovered by the next pool instance.
290 out_of_range_recovery_candidates: Vec<PathBuf>,
291 idle_timeout: Duration,
292 /// Pool-wide connection lifecycle event source (dispatcher + attempt
293 /// counter + success-classification state). Fixed at connect — with a
294 /// listener via [`QuestDb::connect_with_listener`], disabled otherwise —
295 /// before any recovery sender is pre-opened, so every direct or
296 /// store-and-forward emitter reports through it from its first connect.
297 conn_events: Arc<conn_events::ConnectionEventSource>,
298 state: Mutex<PoolState<PooledSenderCore>>,
299 /// Always-direct column-sender pool, independent of `sf_dir`. Backs
300 /// [`QuestDb::borrow_direct_column_sender`] (DataFrame ingestion). Lazy-init
301 /// like the reader pool: starts empty, opens a direct
302 /// connection on demand, recycles through its own free list and the shared
303 /// `sender_pool_max` cap. Kept separate from `state` so DataFrame ingest always
304 /// gets a plain pipelined connection even when `state` is in
305 /// store-and-forward mode.
306 direct_state: Mutex<PoolState<DirectSenderCore>>,
307 /// Reader pool. Lazy-init: starts empty, populated on first
308 /// `borrow_reader_owned` call. Sized by `query_pool_min` /
309 /// `query_pool_max` with the shared `idle_timeout`, but
310 /// tracks and caps them on an independent free list, so heavy ingest
311 /// can't starve queries. The caps are enforced separately, so the
312 /// combined live connection count across the store-and-forward ingress,
313 /// direct ingestion, and reader pools can reach up to
314 /// `2 * sender_pool_max + query_pool_max`.
315 #[cfg(feature = "_egress")]
316 reader_state: Mutex<ReaderPoolState>,
317 /// Wakes the reaper thread on `shutdown` and lets a disk-SF borrow wait
318 /// briefly for an in-flight slot close to release its flock.
319 cv: Condvar,
320 /// Wakes at-cap direct-pool borrows when a direct sender is returned
321 /// or a reservation is rolled back. Paired with `direct_state`.
322 direct_cv: Condvar,
323 /// Wakes at-cap reader borrows when a reader is returned or a
324 /// reservation is rolled back. Paired with `reader_state`.
325 #[cfg(feature = "_egress")]
326 reader_cv: Condvar,
327 /// Pool-wide server-rejection event source. Every rejection a
328 /// store-and-forward runner records is published through it: to the
329 /// user handler on a dedicated dispatcher thread when one was
330 /// registered, otherwise to the log (warn for retriable policies,
331 /// error for terminal), so silence is never the default.
332 rejections: Arc<rejection_events::RejectionEventSource>,
333 shutdown: AtomicBool,
334}
335
336#[derive(Default)]
337struct SlotReservations(Option<Vec<bool>>);
338
339impl SlotReservations {
340 fn with_disk_slots(pool_max: usize) -> Self {
341 Self(Some(vec![false; pool_max]))
342 }
343
344 fn reserved_total(&self, fallback_total: usize) -> usize {
345 match &self.0 {
346 Some(slots) => slots.iter().filter(|in_use| **in_use).count(),
347 None => fallback_total,
348 }
349 }
350
351 fn allocate(&mut self) -> Option<usize> {
352 let slots = self.0.as_mut()?;
353 let index = slots.iter().position(|in_use| !*in_use)?;
354 slots[index] = true;
355 Some(index)
356 }
357
358 fn reserve(&mut self, index: usize) -> bool {
359 let Some(slots) = self.0.as_mut() else {
360 return false;
361 };
362 let Some(slot) = slots.get_mut(index) else {
363 return false;
364 };
365 if *slot {
366 return false;
367 }
368 *slot = true;
369 true
370 }
371
372 fn free(&mut self, slot_index: Option<usize>) {
373 if let (Some(slots), Some(index)) = (&mut self.0, slot_index)
374 && let Some(slot) = slots.get_mut(index)
375 {
376 *slot = false;
377 }
378 }
379}
380
381struct PoolState<S> {
382 /// Idle connections. Borrow/return is LIFO on the back (push/pop);
383 /// the reaper drains the oldest entries from the front. Keeps hot
384 /// connections warm in the common case while the reaper still
385 /// retires entries in age order.
386 free: Vec<PoolEntry<S>>,
387 /// Sum of currently-borrowed senders + in-flight grow operations.
388 in_use: usize,
389 /// Reserved disk slots whose sender has started close/drop but has not yet
390 /// released the slot flock. Borrowers at cap may wait for this to complete.
391 closing: usize,
392 /// Disk-backed store-and-forward slot reservations. Empty for in-memory
393 /// SF and direct senders; populated for pool-minted disk slot indices.
394 slots: SlotReservations,
395}
396
397impl<S> Default for PoolState<S> {
398 fn default() -> Self {
399 Self {
400 free: Vec::new(),
401 in_use: 0,
402 closing: 0,
403 slots: SlotReservations::default(),
404 }
405 }
406}
407
408impl<S> PoolState<S> {
409 fn total(&self) -> usize {
410 self.free.len() + self.in_use
411 }
412
413 fn with_disk_slots(pool_max: usize) -> Self {
414 Self {
415 free: Vec::new(),
416 in_use: 0,
417 closing: 0,
418 slots: SlotReservations::with_disk_slots(pool_max),
419 }
420 }
421
422 fn reserved_total(&self) -> usize {
423 self.slots.reserved_total(self.total())
424 }
425
426 fn allocate_slot_index(&mut self) -> Option<usize> {
427 self.slots.allocate()
428 }
429
430 fn reserve_slot_index(&mut self, index: usize) -> bool {
431 self.slots.reserve(index)
432 }
433
434 fn free_slot_index(&mut self, slot_index: Option<usize>) {
435 self.slots.free(slot_index);
436 }
437}
438
439struct PoolEntry<S> {
440 sender: S,
441 slot_index: Option<usize>,
442 last_idle_at: Instant,
443}
444
445struct PooledSender<S> {
446 sender: S,
447 slot_index: Option<usize>,
448}
449
450#[cfg(feature = "_egress")]
451#[derive(Default)]
452struct ReaderPoolState {
453 /// Idle readers, oldest at front, newest at back (push on return /
454 /// pop on borrow). Same FIFO/LIFO discipline as the sender free list.
455 free: Vec<ReaderPoolEntry>,
456 /// Currently-borrowed readers + in-flight grow operations.
457 in_use: usize,
458}
459
460#[cfg(feature = "_egress")]
461impl ReaderPoolState {
462 fn total(&self) -> usize {
463 self.free.len() + self.in_use
464 }
465}
466
467#[cfg(feature = "_egress")]
468struct ReaderPoolEntry {
469 /// The reader carries its own per-connection state (symbol dict,
470 /// schema registry, request-id sequence) inside itself, so unlike
471 /// the sender pool we don't need to track them as separate fields.
472 reader: Reader,
473 last_idle_at: Instant,
474}
475
476/// Connection counts for a single pool inside a [`QuestDb`], part of the
477/// unstable diagnostics snapshot returned by [`QuestDb::dbg_pool_counts`].
478///
479/// **Not semver-stable.** `#[doc(hidden)]` and `#[non_exhaustive]`; exists for
480/// soak / leak harnesses to assert the pool drains back to a steady baseline
481/// after load and failover episodes.
482#[doc(hidden)]
483#[non_exhaustive]
484#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
485pub struct DbgPoolCount {
486 /// Idle connections parked on the free list.
487 pub free: usize,
488 /// Borrowed connections plus in-flight grow operations.
489 pub in_use: usize,
490 /// Disk store-and-forward slots that have begun close/drop but have not
491 /// yet released their slot flock. Always 0 for the direct and reader
492 /// pools (they hold no disk slots).
493 pub closing: usize,
494}
495
496/// Per-pool connection-count snapshot for a [`QuestDb`], for soak / leak
497/// diagnostics. **Not semver-stable** (`#[doc(hidden)]`, `#[non_exhaustive]`).
498///
499/// The ingestion and direct pools are each capped at `sender_pool_max` and
500/// the reader pool at `query_pool_max`, so `free + in_use` summed across all
501/// three fields can reach `2 * sender_pool_max + query_pool_max` when egress
502/// is enabled.
503#[doc(hidden)]
504#[non_exhaustive]
505#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
506pub struct DbgPoolCounts {
507 /// Store-and-forward ingestion pool (the pool behind `borrow_sender`).
508 pub ingress: DbgPoolCount,
509 /// Always-direct column-sender pool (DataFrame ingest, the pool behind
510 /// `borrow_direct_column_sender`).
511 pub column_direct: DbgPoolCount,
512 /// Reader (egress) pool. Always zero when the crate is built without an
513 /// egress feature.
514 pub reader: DbgPoolCount,
515}
516
517struct ManagedSlotRecoveryCandidate {
518 index: usize,
519 path: PathBuf,
520}
521
522#[derive(Default)]
523struct ManagedSlotRecoveryScan {
524 in_range: Vec<ManagedSlotRecoveryCandidate>,
525 out_of_range: Vec<PathBuf>,
526}
527
528fn managed_slot_exclusion(base: &str, pool_max: usize) -> QwpWsManagedSlotExclusion {
529 QwpWsManagedSlotExclusion::new(managed_slot_prefix(base), pool_max)
530}
531
532fn managed_slot_id(base: &str, index: usize) -> String {
533 managed_slot_exclusion(base, usize::MAX).slot_name(index)
534}
535
536fn managed_slot_prefix(base: &str) -> String {
537 format!("{base}-ingest-")
538}
539
540fn parse_managed_slot_id(base: &str, name: &str) -> Option<usize> {
541 managed_slot_exclusion(base, usize::MAX).parse_index(name)
542}
543
544fn managed_slot_recovery_scan_from(
545 sf_dir: &Path,
546 base: &str,
547 pool_max: usize,
548) -> ManagedSlotRecoveryScan {
549 let Ok(entries) = std::fs::read_dir(sf_dir) else {
550 return ManagedSlotRecoveryScan::default();
551 };
552 let mut scan = ManagedSlotRecoveryScan::default();
553 for entry in entries.flatten() {
554 let slot_path = entry.path();
555 if !slot_path.is_dir() {
556 continue;
557 }
558 let Some(name) = entry.file_name().to_str().map(str::to_owned) else {
559 continue;
560 };
561 let Some(index) = parse_managed_slot_id(base, &name) else {
562 continue;
563 };
564 if !is_candidate_orphan(&slot_path) {
565 continue;
566 }
567 // In-range managed slots are owned by this live pool even when they
568 // are not currently borrowed. They are pre-opened by connect-time
569 // recovery, so a sibling drainer must not take their flock.
570 if index < pool_max {
571 scan.in_range.push(ManagedSlotRecoveryCandidate {
572 index,
573 path: slot_path,
574 });
575 } else {
576 // The pool-lifetime snapshot emits this warning once per candidate
577 // instead of repeating it on every borrow-triggered sender build.
578 log::warn!(
579 "adopting out-of-range store-and-forward slot `{}`; \
580 `<sender_id>-ingest-*` directories under \
581 sf_dir belong to the QuestDb pool namespace, so use a unique \
582 sender_id for pools sharing an sf_dir",
583 slot_path.display()
584 );
585 scan.out_of_range.push(slot_path);
586 }
587 }
588 scan
589}
590
591/// Pre-open dirty in-range disk-SF slots at connect so a restart at lower
592/// concurrency still replays all recoverable queued frames without waiting for
593/// the exact high index to be borrowed again.
594///
595/// Recovery senders count toward the ingestion pool total like ordinary parked
596/// senders. They are reaped only after their queues are delivered and idle past
597/// the timeout, and drain on pool close via `drain_sfa_senders_bounded`. Each
598/// pre-opened sender also enrolls the pool's snapshotted out-of-range managed
599/// slots in its orphan-drainer set, so those slots may begin replay at connect
600/// as well.
601fn preopen_recovery_senders(
602 inner: &Arc<DbInner>,
603 in_range_candidates: &[ManagedSlotRecoveryCandidate],
604) {
605 for candidate in in_range_candidates {
606 preopen_recovery_sender(
607 inner,
608 candidate.index,
609 &candidate.path,
610 &inner.out_of_range_recovery_candidates,
611 );
612 }
613}
614
615fn preopen_recovery_sender(
616 inner: &Arc<DbInner>,
617 index: usize,
618 slot_path: &Path,
619 recovery_candidates: &[PathBuf],
620) {
621 let slot = {
622 let mut state = lock_state(&inner.state);
623 if !state.reserve_slot_index(index) {
624 return;
625 }
626 state.in_use += 1;
627 InUseSlot {
628 state: &inner.state,
629 cv: &inner.cv,
630 slot_index: Some(index),
631 armed: true,
632 }
633 };
634
635 match connect_sfa_pool_with_recovery_candidates(inner, Some(index), recovery_candidates, true) {
636 Ok(sender) => {
637 let slot_index = slot.slot_index;
638 {
639 let mut state = lock_state(&inner.state);
640 state.in_use = state.in_use.saturating_sub(1);
641 state.free.push(PoolEntry {
642 sender,
643 slot_index,
644 last_idle_at: Instant::now(),
645 });
646 }
647 slot.commit();
648 inner.cv.notify_all();
649 }
650 Err(err) => {
651 log::warn!(
652 "skipping parked store-and-forward ingestion slot `{}` during recovery: {}",
653 slot_path.display(),
654 err
655 );
656 }
657 }
658}
659
660impl QuestDb {
661 /// Open a pool against `conf`.
662 ///
663 /// The connect string must use a QWP/WebSocket schema (`ws::` /
664 /// `wss::` / `ws::` / `wss::`). Pool-specific keys are recognised:
665 ///
666 /// | Key | Default | Meaning |
667 /// |----------------------|---------|------------------------------------------------------------------|
668 /// | `sender_pool_min` | 1 | Warm minimum of the ingestion pool, pre-opened at connect unless `lazy_connect=true`. |
669 /// | `sender_pool_max` | 4 | Hard cap on the ingestion pool; the direct column-sender pool used by DataFrame ingestion is capped separately at the same value. |
670 /// | `query_pool_min` | 1 (0 when lazy) | Warm minimum of the reader pool, pre-opened at connect unless `lazy_connect=true`. |
671 /// | `query_pool_max` | 4 | Hard cap on the reader pool. |
672 /// | `acquire_timeout_ms` | 5000 | How long an at-cap borrow waits for a return before failing; `0` fails immediately. |
673 /// | `idle_timeout_ms` | 60000 | Above-minimum idle connections are closed after this long. |
674 /// | `pool_reap` | `auto` | `auto` runs a background reaper; `manual` requires `reap_idle`. |
675 /// | `lazy_connect` | `false` | Tolerate a down server at startup: connect opens nothing, senders buffer and connect in the background, readers connect on first borrow. |
676 ///
677 /// Key names and defaults match the Java client's `QuestDBBuilder`; the
678 /// Java-only lifecycle keys (`max_lifetime_ms`, `housekeeper_interval_ms`,
679 /// `query_close_timeout_ms`) have no counterpart here — the reaper tick
680 /// and `close_flush_timeout` own those responsibilities.
681 ///
682 /// [`Self::borrow_sender`] is always store-and-forward (in-memory when no
683 /// `sf_dir`, disk-backed when set). Setting `sf_dir` gives every pooled
684 /// sender its own slot directory, minted from the configured `sender_id`
685 /// base as `<base>-ingest-<index>`. Those `<sender_id>-ingest-*`
686 /// directories are reserved for this pool namespace under `sf_dir`; use a
687 /// unique `sender_id` for each pool that shares an `sf_dir`.
688 /// `sender_pool_min` / `sender_pool_max` apply to this unified ingestion pool. At
689 /// cap, borrows return `InvalidApiCall` except disk-backed
690 /// ingestion borrows can wait up to `close_flush_timeout` (default 5s)
691 /// while an in-flight slot close releases its lock. For a plain pipelined
692 /// (non-SF) connection — used by DataFrame ingestion — see
693 /// [`Self::borrow_direct_column_sender`].
694 ///
695 /// Startup matches the Java client. By default `connect` is **eager**:
696 /// it pre-opens `sender_pool_min` ingest senders — honoring only an
697 /// explicitly set `initial_connect_retry`: `off` (the default) fails
698 /// fast, `sync` retries within the reconnect budget, `async` connects in
699 /// the background — and `query_pool_min` readers, which have no retry
700 /// mode and always connect synchronously, failing fast; `sync` governs
701 /// only the ingest side. Reconnect-to-sync promotion applies only to
702 /// standalone [`SenderBuilder::build`]; pools honor only an explicitly
703 /// set mode. Bare `initial_connect_retry=async` is likewise not a
704 /// non-blocking startup while `query_pool_min > 0`; `lazy_connect=true`
705 /// is. Growth borrows beyond the minimum honor the same rules.
706 ///
707 /// With `lazy_connect=true` the pool tolerates a down server at startup:
708 /// `connect` performs no blocking network I/O, `query_pool_min` defaults
709 /// to 0 (readers connect lazily on first use), and every ingest borrow
710 /// creates its local store-and-forward producer immediately and connects
711 /// in the background, so the borrower can buffer while the server is
712 /// absent. An explicit blocking `initial_connect_retry` alongside
713 /// `lazy_connect=true` is rejected as a configuration conflict. In
714 /// disk-backed store-and-forward mode, either variant may pre-open parked
715 /// recovery senders whose initial connect and replay run in the
716 /// background. Direct senders open their transport on first borrow.
717 /// `sender_pool_min` / `query_pool_min` are the warm minimums the reaper
718 /// keeps.
719 ///
720 /// # Store-and-forward durability
721 ///
722 /// Disk store-and-forward (`sf_dir`) writes queued frames and their symbol
723 /// dictionary to disk but does **not** `fsync` — the data is *page-cache
724 /// durable*, matching the standalone QWP/WebSocket sender. That survives a **process / JVM
725 /// crash** (unacked frames replay on the next borrow / recovery), but **not**
726 /// a **host / power crash**, which can lose or tear unflushed pages. A
727 /// recovery that finds a torn symbol dictionary (or a frame whose dictionary
728 /// cannot be re-registered on the fresh server) fails loudly with a
729 /// **terminal, resend-required** error —
730 /// [`StoreResendRequired`](crate::ErrorCode::StoreResendRequired), a code
731 /// *distinct from* the transient [`SocketError`](crate::ErrorCode::SocketError)
732 /// you would retry, so callers can branch on it directly. The sender's own
733 /// reconnect/failover loops treat it as terminal (they stop) rather than
734 /// retrying it to their deadline. Those rows must be re-ingested from their
735 /// source, not retried in place.
736 /// In-memory store-and-forward (no `sf_dir`) has no cross-restart durability.
737 ///
738 pub fn connect(conf: &str) -> Result<Self> {
739 Self::connect_with_handlers(conf, ConnectHandlers::default())
740 }
741
742 /// [`Self::connect`] with a connection lifecycle listener. Events (see
743 /// [`ConnectionEventKind`](crate::ingress::ConnectionEventKind)) are
744 /// delivered on a dedicated dispatcher thread through a bounded
745 /// inbox — a slow listener can never stall connect, publish, or
746 /// reconnect paths; on overflow the oldest undelivered event is
747 /// dropped (counted by [`Self::connection_events_dropped`]).
748 ///
749 /// All direct and store-and-forward senders share this one source and
750 /// inbox. Concurrent emitters are serialized into the inbox in emission
751 /// order. `inbox_capacity == 0` selects the default (64).
752 ///
753 /// The listener is registered before the pool opens anything, so it
754 /// observes every transition — including the initial
755 /// [`Connected`](crate::ingress::ConnectionEventKind::Connected) of disk
756 /// recovery senders pre-opened by connect itself. This is the only way to
757 /// attach a listener to a pool: registration after connect would race
758 /// those recovery connects and could miss them.
759 pub fn connect_with_listener(
760 conf: &str,
761 listener: crate::ingress::ConnectionListener,
762 inbox_capacity: usize,
763 ) -> Result<Self> {
764 Self::connect_with_handlers(
765 conf,
766 ConnectHandlers {
767 connection_listener: Some(listener),
768 connection_event_inbox_capacity: inbox_capacity,
769 ..ConnectHandlers::default()
770 },
771 )
772 }
773
774 /// [`Self::connect`] with any combination of a connection lifecycle
775 /// listener (see [`Self::connect_with_listener`]) and a server-rejection
776 /// handler.
777 ///
778 /// The rejection handler receives every server rejection any of the
779 /// pool's store-and-forward connections records — including rejections
780 /// for frames whose lease was already returned — on a dedicated
781 /// dispatcher thread through a bounded inbox (overflow drops the oldest
782 /// event, counted by [`Self::rejection_events_dropped`]). Without a
783 /// handler every rejection is logged instead: warn for retriable
784 /// policies (the frames are replayed, not lost), error for terminal
785 /// ones. Use the handler for dead-lettering, alerting, and metrics;
786 /// producer-side abort logic belongs with the terminal error raised by
787 /// the sender calls themselves.
788 pub fn connect_with_handlers(conf: &str, handlers: ConnectHandlers) -> Result<Self> {
789 let conn_events = match handlers.connection_listener {
790 Some(listener) => conn_events::ConnectionEventSource::new(
791 listener,
792 handlers.connection_event_inbox_capacity,
793 ),
794 None => conn_events::ConnectionEventSource::disabled(),
795 };
796 let rejections = match handlers.error_handler {
797 Some(handler) => rejection_events::RejectionEventSource::with_handler(
798 handler,
799 handlers.error_inbox_capacity,
800 ),
801 None => rejection_events::RejectionEventSource::logging_default(),
802 };
803 Self::connect_impl(conf, conn_events, rejections)
804 }
805
806 fn connect_impl(
807 conf: &str,
808 conn_events: conn_events::ConnectionEventSource,
809 rejections: rejection_events::RejectionEventSource,
810 ) -> Result<Self> {
811 let parsed = conf::parse(conf)?;
812 // The public ingestion pool is always store-and-forward: in-memory
813 // queues when no `sf_dir`, disk-backed pool-minted slots when set.
814 let sf_disk = parsed.sf_disk;
815 let pool_cfg = parsed.pool;
816
817 let mut builder = SenderBuilder::from_conf(conf)?;
818 if pool_cfg.lazy_connect {
819 // Java's lazy_connect injects an async initial connect into the
820 // ingest config once; every pooled sender then inherits it.
821 builder.force_async_initial_connect();
822 }
823 let buffer_max_name_len = builder.configured_max_name_len();
824 let connector = builder.build_qwp_ws_connector()?;
825 let health = QwpWsHostHealthTracker::new(connector.endpoint_count());
826 let slot_base_id = connector.sender_id().to_owned();
827 let managed_slot_exclusion = if sf_disk {
828 Some(managed_slot_exclusion(
829 &slot_base_id,
830 pool_cfg.sender_pool_max,
831 ))
832 } else {
833 None
834 };
835 // Snapshot managed recovery before any sender is built. In-range
836 // entries are retained locally for connect-time pre-open; out-of-range
837 // entries live on DbInner and are reused by prewarm and later growth.
838 // This matches Java SenderPool's cached out-of-range worklist and keeps
839 // the borrow path free of top-level sf_dir scans.
840 let recovery_scan = if sf_disk {
841 connector
842 .sf_dir()
843 .map(|sf_dir| {
844 managed_slot_recovery_scan_from(sf_dir, &slot_base_id, pool_cfg.sender_pool_max)
845 })
846 .unwrap_or_default()
847 } else {
848 ManagedSlotRecoveryScan::default()
849 };
850 let ManagedSlotRecoveryScan {
851 in_range: in_range_recovery_candidates,
852 out_of_range: out_of_range_recovery_candidates,
853 } = recovery_scan;
854
855 // Start empty; connect-time recovery may pre-open dirty disk-SF slots
856 // after `inner` exists, otherwise the pools open on first borrow.
857 let free = Vec::new();
858
859 let inner = Arc::new(DbInner {
860 #[cfg(feature = "_egress")]
861 conf: conf.to_owned(),
862 connector,
863 buffer_max_name_len,
864 health: Mutex::new(health),
865 sender_pool_min: pool_cfg.sender_pool_min,
866 sender_pool_max: pool_cfg.sender_pool_max,
867 #[cfg(feature = "_egress")]
868 query_pool_min: pool_cfg.query_pool_min,
869 #[cfg(feature = "_egress")]
870 query_pool_max: pool_cfg.query_pool_max,
871 acquire_timeout: pool_cfg.acquire_timeout,
872 sf_disk,
873 slot_base_id,
874 managed_slot_exclusion,
875 out_of_range_recovery_candidates,
876 idle_timeout: pool_cfg.idle_timeout,
877 state: Mutex::new(if sf_disk {
878 PoolState::with_disk_slots(pool_cfg.sender_pool_max)
879 } else {
880 PoolState {
881 free,
882 ..PoolState::default()
883 }
884 }),
885 direct_state: Mutex::new(PoolState::default()),
886 #[cfg(feature = "_egress")]
887 reader_state: Mutex::new(ReaderPoolState::default()),
888 cv: Condvar::new(),
889 direct_cv: Condvar::new(),
890 #[cfg(feature = "_egress")]
891 reader_cv: Condvar::new(),
892 rejections: Arc::new(rejections),
893 shutdown: AtomicBool::new(false),
894 conn_events: Arc::new(conn_events),
895 });
896
897 let reaper = match pool_cfg.pool_reap {
898 PoolReap::Auto => Some(spawn_reaper(Arc::clone(&inner)).map_err(|err| {
899 inner.shutdown.store(true, Ordering::SeqCst);
900 crate::Error::new(
901 crate::ErrorCode::SocketError,
902 format!("Failed to spawn pool reaper thread: {err}"),
903 )
904 })?),
905 PoolReap::Manual => None,
906 };
907
908 let db = Self { inner, reaper };
909 // Prewarm BEFORE recovery pre-open, matching the Java client's order.
910 // Prewarm adopts each dirty in-range slot through its deterministic
911 // managed id and enrolls the snapshotted out-of-range candidates, so
912 // its foreground connect genuinely probes the server; recovery must
913 // not run first or its background-connecting (forced-async) senders
914 // would sit in the free list and satisfy the warm minimum without any
915 // connect, silently voiding the eager fail-fast contract whenever a
916 // previous run left queued data behind. On error the drop of `db`
917 // closes whatever was opened.
918 if !pool_cfg.lazy_connect {
919 prewarm_min_connections(&db)?;
920 }
921 // Recovery pre-open then re-adopts any dirty slots prewarm did not
922 // claim; their initial connect and replay run in the background.
923 preopen_recovery_senders(&db.inner, &in_range_recovery_candidates);
924 Ok(db)
925 }
926
927 /// Create a caller-owned QWP/WebSocket row buffer using this pool's
928 /// configured table/column name limit. The buffer is independent of any
929 /// particular sender borrow and may be filled or moved before it is
930 /// published by a store-and-forward sender from this pool.
931 pub fn new_buffer(&self) -> Buffer {
932 Buffer::qwp_ws_with_max_name_len(self.inner.buffer_max_name_len)
933 }
934
935 /// Configured name limit used by [`Self::new_buffer`]. Exposed for the C++
936 /// wrapper so a moved-from buffer can lazily recreate the same kind of
937 /// buffer without retaining a pool reference.
938 #[doc(hidden)]
939 pub fn buffer_max_name_len(&self) -> usize {
940 self.inner.buffer_max_name_len
941 }
942
943 /// Borrow a sender.
944 ///
945 /// Selection: pop the most-recently-returned slot from the free list;
946 /// failing that, open a new connection if we are below `sender_pool_max`;
947 /// failing that, in disk-backed store-and-forward mode only, wait up to
948 /// `close_flush_timeout` (default 5s) while an in-flight slot close
949 /// releases its lock; failing that, wait up to `acquire_timeout_ms` for a
950 /// return (`acquire_timeout_ms=0` fails fast); failing that, return
951 /// `InvalidApiCall`.
952 ///
953 /// A borrow that opens a new connection honors `initial_connect_retry`:
954 /// `off` (the default) connects synchronously and fails fast, `sync`
955 /// retries within the reconnect budget before returning. Under
956 /// `lazy_connect=true` the connection starts in the background instead,
957 /// so the borrow succeeds even while the server is away; see
958 /// [`Self::connect`].
959 pub fn borrow_sender(&self) -> Result<BorrowedSender<'_>> {
960 let cs = self.pick_sender()?;
961 Ok(BorrowedSender(SenderHandle::new(self, cs)))
962 }
963
964 /// Borrow a **direct** (non-store-and-forward) column sender from the
965 /// always-direct pool, independent of `sf_dir`.
966 ///
967 /// Not part of the public API: the direct sender is the transport behind
968 /// [`Self::flush_arrow_batch`] / [`Self::flush_polars_dataframe`], which own
969 /// their own commit + replay. Hidden from the docs; callers ingest through
970 /// those entry points rather than handling a sender.
971 #[doc(hidden)]
972 pub fn borrow_direct_column_sender(&self) -> Result<BorrowedDirectColumnSender<'_>> {
973 let cs = pick_direct_sender(&self.inner)?;
974 Ok(BorrowedDirectColumnSender(DirectSenderHandle::new(
975 self, cs,
976 )))
977 }
978
979 /// Flush a single Arrow [`RecordBatch`](arrow::array::RecordBatch) to
980 /// `table` in one call.
981 ///
982 /// This is the recommended entry point for one-off Arrow ingestion: it
983 /// borrows a direct column sender from the pool, publishes the batch as a
984 /// commit boundary, waits for the server `Ok` ack, and returns the sender
985 /// to the pool — callers never handle a sender.
986 ///
987 /// `timestamp_column` selects where each row's designated timestamp comes
988 /// from:
989 /// * `Some(col)` — source it from the named `Timestamp(_)` column of
990 /// `batch` (mirrors the old `flush_arrow_batch_at_column`).
991 /// * `None` — let the server stamp each row on arrival (mirrors the old
992 /// `flush_arrow_batch_at_now`).
993 ///
994 /// `overrides` carries per-column wire-type hints (e.g. promote a UTF-8
995 /// column to SYMBOL, or a UInt32 to IPv4); pass `&[]` when the Arrow schema
996 /// is self-describing.
997 ///
998 /// `ack_level` chooses how far the call blocks before returning:
999 /// * `None` — wait for the connect string's default, i.e. the same level
1000 /// the store-and-forward senders use: [`AckLevel::Durable`] when the
1001 /// Enterprise-only durable mode is enabled with
1002 /// `request_durable_ack=on`, otherwise [`AckLevel::Ok`].
1003 /// * `Some(level)` — wait for exactly `level`. [`AckLevel::Durable`]
1004 /// requires QuestDB Enterprise and `request_durable_ack=on`; otherwise
1005 /// the call is rejected with [`ErrorCode::InvalidApiCall`].
1006 ///
1007 /// The call publishes the batch as a commit boundary and blocks until the
1008 /// resolved acknowledgement level is reached. An `Ok` acknowledgement
1009 /// confirms server acceptance; only the Enterprise durable level confirms
1010 /// durable coverage. On a transient [`ErrorCode::FailoverRetry`] it
1011 /// surfaces the error rather than replaying (the batch is fully owned by
1012 /// the caller, so retrying is a plain re-call); the DataFrame path
1013 /// ([`Self::flush_polars_dataframe`]) re-drives automatically instead.
1014 ///
1015 /// [`ErrorCode::FailoverRetry`]: crate::ErrorCode::FailoverRetry
1016 /// [`ErrorCode::InvalidApiCall`]: crate::ErrorCode::InvalidApiCall
1017 #[cfg(feature = "arrow-ingress")]
1018 pub fn flush_arrow_batch<'t, T>(
1019 &self,
1020 table: T,
1021 batch: &arrow::array::RecordBatch,
1022 timestamp_column: Option<crate::ingress::ColumnName<'_>>,
1023 overrides: &[crate::ingress::column_sender::ArrowColumnOverride<'_>],
1024 ack_level: Option<AckLevel>,
1025 ) -> Result<()>
1026 where
1027 T: TryInto<crate::ingress::TableName<'t>>,
1028 crate::Error: From<T::Error>,
1029 {
1030 let ack = ack_level.unwrap_or_else(|| self.default_ack_level());
1031 let mut sender = self.borrow_direct_column_sender()?;
1032 // `table` is moved into exactly one arm, so the generic `T` flows
1033 // straight through to the chosen `_and_wait` method unchanged.
1034 match timestamp_column {
1035 Some(ts) => {
1036 sender.flush_arrow_batch_at_column_and_wait(table, batch, ts, overrides, ack)
1037 }
1038 None => sender.flush_arrow_batch_at_now_and_wait(table, batch, overrides, ack),
1039 }
1040 }
1041
1042 /// The ack level these convenience flushes wait for when the caller does
1043 /// not name one: [`AckLevel::Durable`] when the connect string enabled the
1044 /// Enterprise-only durable mode with `request_durable_ack=on`, otherwise
1045 /// [`AckLevel::Ok`]. Mirrors the level the store-and-forward senders use
1046 /// for the same pool.
1047 #[cfg(feature = "arrow-ingress")]
1048 pub(crate) fn default_ack_level(&self) -> AckLevel {
1049 if self.inner.connector.request_durable_ack() {
1050 AckLevel::Durable
1051 } else {
1052 AckLevel::Ok
1053 }
1054 }
1055
1056 /// FFI escape hatch: like [`Self::borrow_sender`] but the returned
1057 /// handle is not lifetime-bound to `&self`. Carries an `Arc<DbInner>`
1058 /// internally so it can outlive the user-facing `QuestDb` pointer
1059 /// (the pool's return path stays alive as long as any borrow is
1060 /// outstanding; after pool close, returned handles are dropped instead of
1061 /// recycled).
1062 ///
1063 /// Hidden from the Rust API because Rust callers should prefer the
1064 /// lifetime-bound `borrow_sender`, which catches use-after-close at
1065 /// compile time. C callers reach this through `questdb_db_borrow_sender`.
1066 #[cfg(feature = "ffi-support")]
1067 pub(crate) fn borrow_sender_owned(&self) -> Result<OwnedSender> {
1068 let cs = self.pick_sender()?;
1069 Ok(OwnedSender::new(Arc::clone(&self.inner), cs))
1070 }
1071
1072 /// Like [`borrow_sender_owned`] but retries the connect within `budget`
1073 /// using the pool's reconnect backoff (the cluster may be electing a
1074 /// primary). Backs the C ABI's `questdb_db_borrow_sender_with_retry`.
1075 #[cfg(feature = "ffi-support")]
1076 pub(crate) fn borrow_sender_owned_with_retry(&self, budget: Duration) -> Result<OwnedSender> {
1077 let deadline = Instant::now().checked_add(budget);
1078 let cs = reconnect_pick(&self.inner, deadline, pick_sfa_sender)?;
1079 Ok(OwnedSender::new(Arc::clone(&self.inner), cs))
1080 }
1081
1082 /// FFI escape hatch: like [`Self::borrow_direct_column_sender`] but the
1083 /// returned handle is not lifetime-bound to `&self` (carries an
1084 /// `Arc<DbInner>` so it can outlive the user-facing `QuestDb` pointer).
1085 /// Backs the C ABI's `questdb_db_borrow_direct_sender`. Hidden from
1086 /// the Rust API; Rust callers should prefer the lifetime-bound
1087 /// [`Self::borrow_direct_column_sender`].
1088 #[cfg(feature = "ffi-support")]
1089 pub(crate) fn borrow_direct_column_sender_owned(&self) -> Result<OwnedDirectColumnSender> {
1090 let cs = pick_direct_sender(&self.inner)?;
1091 Ok(OwnedDirectColumnSender::new(Arc::clone(&self.inner), cs))
1092 }
1093
1094 /// Like [`borrow_direct_column_sender_owned`] but retries the connect
1095 /// within `budget` using the reconnect backoff. Backs the C ABI's
1096 /// `questdb_db_borrow_direct_sender_with_retry`.
1097 #[cfg(feature = "ffi-support")]
1098 pub(crate) fn borrow_direct_column_sender_owned_with_retry(
1099 &self,
1100 budget: Duration,
1101 ) -> Result<OwnedDirectColumnSender> {
1102 let deadline = Instant::now().checked_add(budget);
1103 let cs = reconnect_pick(&self.inner, deadline, pick_direct_sender)?;
1104 Ok(OwnedDirectColumnSender::new(Arc::clone(&self.inner), cs))
1105 }
1106
1107 fn pick_sender(&self) -> Result<PooledSender<PooledSenderCore>> {
1108 pick_sfa_sender(&self.inner)
1109 }
1110
1111 fn pick_replacement_sender(&self) -> Result<PooledSender<DirectSenderCore>> {
1112 if self.inner.shutdown.load(Ordering::SeqCst) {
1113 return Err(error::fmt!(
1114 InvalidApiCall,
1115 "QuestDb pool is closed; cannot replace sender"
1116 ));
1117 }
1118 // Same-handle replacement: the borrowed direct sender already owns one
1119 // logical in-use slot, so this must not reserve another one or
1120 // sender_sender_pool_max=1 would reject replacing a dead direct connection.
1121 if let Some(entry) = lock_state(&self.inner.direct_state).free.pop() {
1122 return Ok(PooledSender {
1123 sender: entry.sender,
1124 slot_index: entry.slot_index,
1125 });
1126 }
1127
1128 let conn = connect_conn_pool(&self.inner)?;
1129 Ok(PooledSender {
1130 sender: DirectSenderCore::new(
1131 conn,
1132 crate::ingress::SymbolGlobalDict::new(),
1133 crate::ingress::column_sender::encoder::EncodeScratch::new(),
1134 false,
1135 ),
1136 slot_index: None,
1137 })
1138 }
1139
1140 /// Manually reap idle connections.
1141 ///
1142 /// Closes free-list entries that have been idle longer than
1143 /// `idle_timeout_ms`, never shrinking the sender pools below
1144 /// `sender_pool_min` or the reader pool below `query_pool_min`. Returns
1145 /// the number of connections closed.
1146 ///
1147 /// Under the default `pool_reap=auto`, a background thread invokes this
1148 /// logic periodically and this call is harmless. Under
1149 /// `pool_reap=manual`, callers that want shrinking must invoke this on
1150 /// their own cadence.
1151 pub fn reap_idle(&self) -> usize {
1152 reap_idle_inner(&self.inner)
1153 }
1154
1155 /// Total connection events discarded by the listener inbox's
1156 /// drop-oldest policy. `0` when no listener is registered.
1157 pub fn connection_events_dropped(&self) -> u64 {
1158 self.inner.conn_events.dropped()
1159 }
1160
1161 /// Total connection events delivered to the listener. `0` when no
1162 /// listener is registered.
1163 pub fn connection_events_delivered(&self) -> u64 {
1164 self.inner.conn_events.delivered()
1165 }
1166
1167 /// Total server rejections delivered to the rejection handler (or to
1168 /// the default log handler when none was registered).
1169 pub fn rejection_events_delivered(&self) -> u64 {
1170 self.inner.rejections.delivered()
1171 }
1172
1173 /// Total server rejections discarded by the rejection handler inbox's
1174 /// drop-oldest policy. Always `0` without a registered handler: the
1175 /// default log handler has no inbox.
1176 pub fn rejection_events_dropped(&self) -> u64 {
1177 self.inner.rejections.dropped()
1178 }
1179
1180 /// Snapshot per-pool connection counts for diagnostics.
1181 ///
1182 /// Soak / leak harnesses read this on a cadence and assert every pool
1183 /// returns to a steady baseline after load and failover episodes (an FD /
1184 /// connection leak shows up as `in_use` or `free` failing to fall back).
1185 ///
1186 /// Each pool's lock is taken in turn (never two at once), so every field
1187 /// is internally consistent but the three are not a single atomic instant —
1188 /// fine for a monitoring snapshot. **Not semver-stable** (`#[doc(hidden)]`,
1189 /// `#[non_exhaustive]` result); mirrors the `questdb_db_dbg_reader_*_count`
1190 /// FFI diagnostics precedent.
1191 #[doc(hidden)]
1192 pub fn dbg_pool_counts(&self) -> DbgPoolCounts {
1193 let ingress = {
1194 let s = lock_state(&self.inner.state);
1195 DbgPoolCount {
1196 free: s.free.len(),
1197 in_use: s.in_use,
1198 closing: s.closing,
1199 }
1200 };
1201 let column_direct = {
1202 let s = lock_state(&self.inner.direct_state);
1203 DbgPoolCount {
1204 free: s.free.len(),
1205 in_use: s.in_use,
1206 closing: s.closing,
1207 }
1208 };
1209 #[cfg(feature = "_egress")]
1210 let reader = {
1211 let s = lock_reader_state(&self.inner.reader_state);
1212 DbgPoolCount {
1213 free: s.free.len(),
1214 in_use: s.in_use,
1215 closing: 0,
1216 }
1217 };
1218 #[cfg(not(feature = "_egress"))]
1219 let reader = DbgPoolCount::default();
1220 DbgPoolCounts {
1221 ingress,
1222 column_direct,
1223 reader,
1224 }
1225 }
1226
1227 /// Close the pool: stop the reaper (if any), reject future borrows, drop
1228 /// all idle connections, and consume `self`.
1229 ///
1230 /// FFI-owned outstanding handles remain return/drop-safe through their
1231 /// internal pool reference, but return after close drops the connection
1232 /// instead of recycling it.
1233 ///
1234 /// Drop has the same effect; `close` exists for parity with the C ABI
1235 /// (where `Drop` is not available) and to give callers a place to handle
1236 /// any reaper-join errors explicitly in the future.
1237 pub fn close(self) {
1238 drop(self);
1239 }
1240
1241 /// The pool's reconnect backoff budget, parsed from the connect string's
1242 /// `reconnect_*` keys.
1243 #[cfg(any(feature = "polars-ingress", feature = "polars-egress"))]
1244 pub(crate) fn reconnect_policy(&self) -> crate::ingress::ReconnectPolicy {
1245 self.inner.connector.reconnect_policy()
1246 }
1247
1248 /// The pool's failover budget (`reconnect_max_duration`, default 300s).
1249 /// Exposed so the C ABI can let callers bound an overall failover deadline.
1250 #[cfg(feature = "ffi-support")]
1251 pub(crate) fn reconnect_max_duration(&self) -> Duration {
1252 self.inner.connector.reconnect_policy().max_duration()
1253 }
1254
1255 /// Snapshot the number of idle (free) connections currently in the pool.
1256 #[cfg(test)]
1257 pub(crate) fn free_count(&self) -> usize {
1258 lock_state(&self.inner.state).free.len()
1259 }
1260
1261 /// Snapshot the number of currently-borrowed (or in-flight-being-built)
1262 /// connections.
1263 #[cfg(test)]
1264 pub(crate) fn in_use_count(&self) -> usize {
1265 lock_state(&self.inner.state).in_use
1266 }
1267
1268 /// Snapshot the number of disk store-and-forward column slots currently
1269 /// waiting for their close/drop path to release the slot flock.
1270 #[cfg(all(test, feature = "ffi-support"))]
1271 pub(crate) fn closing_count(&self) -> usize {
1272 lock_state(&self.inner.state).closing
1273 }
1274
1275 /// Snapshot the number of idle (free) senders in the always-direct pool.
1276 #[cfg(test)]
1277 pub(crate) fn direct_free_count(&self) -> usize {
1278 lock_state(&self.inner.direct_state).free.len()
1279 }
1280
1281 /// Snapshot the number of currently-borrowed senders in the always-direct
1282 /// pool.
1283 #[cfg(test)]
1284 pub(crate) fn direct_in_use_count(&self) -> usize {
1285 lock_state(&self.inner.direct_state).in_use
1286 }
1287
1288 /// Borrow a query [`Reader`] from the egress pool.
1289 ///
1290 /// Egress companion to [`Self::borrow_sender`]: pulls a [`Reader`]
1291 /// from the pool's reader free list, lazily opening a fresh connection
1292 /// (via `Reader::from_conf` on the original connect string) when the
1293 /// free list is empty and the pool is below `query_pool_max`. The reader
1294 /// pool is lazily grown and capped **independently** of the two ingestion
1295 /// pools, so heavy ingest can't starve queries and vice versa (the
1296 /// combined live-connection ceiling across all three pools is
1297 /// `2 * sender_pool_max + query_pool_max`).
1298 ///
1299 /// Borrow at the cap waits up to `acquire_timeout_ms` for a return
1300 /// (`acquire_timeout_ms=0` fails fast), then returns
1301 /// [`InvalidApiCall`](crate::ErrorCode::InvalidApiCall).
1302 ///
1303 /// The returned [`BorrowedReader`] derefs to `Reader`, so the usual
1304 /// `prepare` / `execute` cursor flow works unchanged, and returns the
1305 /// reader to the pool on `Drop` — unless its transport has been torn
1306 /// down (or [`BorrowedReader::drop_on_return`] was called), in which
1307 /// case it is dropped and the next borrow opens a fresh one.
1308 ///
1309 /// Like [`BorrowedSender`], [`BorrowedReader`] is **not** `Send` or
1310 /// `Sync`: borrow one reader per worker thread from the same `QuestDb`.
1311 #[cfg(feature = "_egress")]
1312 pub fn borrow_reader(&self) -> crate::error::Result<BorrowedReader<'_>> {
1313 let reader = self.pick_reader()?;
1314 Ok(BorrowedReader::new(self, reader))
1315 }
1316
1317 /// FFI escape hatch: borrow a reader from the egress pool.
1318 ///
1319 /// Same shape as [`Self::borrow_sender_owned`] but pulls a
1320 /// [`Reader`] from the reader free list (lazily opens one if the
1321 /// free list is empty and total < `query_pool_max`). Returned via
1322 /// [`OwnedReader`]'s Drop: see the sender variant for the same
1323 /// pattern.
1324 #[cfg(all(feature = "_egress", feature = "ffi-support"))]
1325 pub(crate) fn borrow_reader_owned(&self) -> crate::error::Result<OwnedReader> {
1326 let reader = self.pick_reader()?;
1327 Ok(OwnedReader {
1328 inner: Arc::clone(&self.inner),
1329 reader: Some(reader),
1330 must_close: false,
1331 })
1332 }
1333
1334 /// Construct an opaque pool reference that downstream code (the
1335 /// FFI's `reader` wrapper, in particular) can hold to return
1336 /// readers without having to expose [`DbInner`].
1337 #[cfg(all(feature = "_egress", feature = "ffi-support"))]
1338 pub(crate) fn reader_pool_handle(&self) -> ReaderPoolHandle {
1339 ReaderPoolHandle {
1340 inner: Arc::clone(&self.inner),
1341 }
1342 }
1343
1344 #[cfg(feature = "_egress")]
1345 fn pick_reader(&self) -> crate::error::Result<Reader> {
1346 use crate::{Error, ErrorCode};
1347 let slot = {
1348 let mut state = lock_reader_state(&self.inner.reader_state);
1349 let mut acquire_deadline = None;
1350 loop {
1351 if self.inner.shutdown.load(Ordering::SeqCst) {
1352 return Err(Error::new(
1353 ErrorCode::InvalidApiCall,
1354 "QuestDb pool is closed; cannot borrow reader",
1355 ));
1356 }
1357 if let Some(entry) = state.free.pop() {
1358 state.in_use += 1;
1359 drop(state);
1360 return Ok(entry.reader);
1361 }
1362 if state.total() < self.inner.query_pool_max {
1363 break;
1364 }
1365 if let Some(wait_for) =
1366 remaining_wait(&mut acquire_deadline, self.inner.acquire_timeout)
1367 {
1368 let (next_state, _) = match self.inner.reader_cv.wait_timeout(state, wait_for) {
1369 Ok((guard, result)) => (guard, result),
1370 Err(poisoned) => poisoned.into_inner(),
1371 };
1372 state = next_state;
1373 continue;
1374 }
1375 return Err(Error::new(
1376 ErrorCode::InvalidApiCall,
1377 format!(
1378 "Reader pool exhausted: {} readers are currently borrowed at \
1379 the `query_pool_max` cap of {} after waiting \
1380 acquire_timeout_ms={}. Release a reader, or raise \
1381 `query_pool_max` / `acquire_timeout_ms`.",
1382 state.in_use,
1383 self.inner.query_pool_max,
1384 self.inner.acquire_timeout.as_millis()
1385 ),
1386 ));
1387 }
1388 state.in_use += 1;
1389 ReaderInUseSlot {
1390 inner: &self.inner,
1391 armed: true,
1392 }
1393 };
1394 let reader = Reader::from_conf(&self.inner.conf)?;
1395 slot.commit();
1396 Ok(reader)
1397 }
1398
1399 /// Snapshot the number of idle (free) readers currently in the pool.
1400 #[cfg(all(feature = "_egress", any(test, feature = "ffi-support")))]
1401 pub(crate) fn reader_free_count(&self) -> usize {
1402 lock_reader_state(&self.inner.reader_state).free.len()
1403 }
1404
1405 /// Snapshot the number of currently-borrowed readers.
1406 #[cfg(all(feature = "_egress", any(test, feature = "ffi-support")))]
1407 pub(crate) fn reader_in_use_count(&self) -> usize {
1408 lock_reader_state(&self.inner.reader_state).in_use
1409 }
1410}
1411
1412impl Debug for QuestDb {
1413 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1414 let state = lock_state(&self.inner.state);
1415 let mut s = f.debug_struct("QuestDb");
1416 s.field("sender_pool_min", &self.inner.sender_pool_min)
1417 .field("sender_pool_max", &self.inner.sender_pool_max);
1418 #[cfg(feature = "_egress")]
1419 s.field("query_pool_min", &self.inner.query_pool_min)
1420 .field("query_pool_max", &self.inner.query_pool_max);
1421 s.field("acquire_timeout", &self.inner.acquire_timeout)
1422 .field("free", &state.free.len())
1423 .field("in_use", &state.in_use)
1424 .finish()
1425 }
1426}
1427
1428impl Drop for QuestDb {
1429 fn drop(&mut self) {
1430 // Wake the reaper and any at-cap borrow waits, and let them
1431 // observe shutdown.
1432 self.inner.shutdown.store(true, Ordering::SeqCst);
1433 // Notifying under the mutex avoids the lost-wakeup race where the
1434 // waiter has just released the lock and is about to wait.
1435 {
1436 let _g = lock_state(&self.inner.state);
1437 self.inner.cv.notify_all();
1438 }
1439 {
1440 let _g = lock_state(&self.inner.direct_state);
1441 self.inner.direct_cv.notify_all();
1442 }
1443 #[cfg(feature = "_egress")]
1444 {
1445 let _g = lock_reader_state(&self.inner.reader_state);
1446 self.inner.reader_cv.notify_all();
1447 }
1448 if let Some(handle) = self.reaper.take() {
1449 let _ = handle.join();
1450 }
1451 // Close idle resources now. Outstanding borrows hold their own Arc and
1452 // will be dropped instead of recycled when they return after shutdown.
1453 drain_idle_inner(&self.inner);
1454 // FFI-owned senders may outlive the public pool handle. Detach and join
1455 // the dispatchers after idle emitters are gone; any outstanding sender
1456 // still holds the sources but can no longer reach the user callbacks.
1457 // Returning from pool close is therefore a callback/user_data fence.
1458 self.inner.conn_events.close();
1459 self.inner.rejections.close();
1460 }
1461}
1462
1463struct SenderHandle<'a> {
1464 db: &'a QuestDb,
1465 sender: Option<PooledSenderCore>,
1466 slot_index: Option<usize>,
1467 _not_send: PhantomData<Rc<()>>,
1468}
1469
1470impl<'a> SenderHandle<'a> {
1471 fn new(db: &'a QuestDb, sender: PooledSender<PooledSenderCore>) -> Self {
1472 Self {
1473 db,
1474 sender: Some(sender.sender),
1475 slot_index: sender.slot_index,
1476 _not_send: PhantomData,
1477 }
1478 }
1479
1480 fn inner_mut(&mut self) -> &mut PooledSenderCore {
1481 self.sender
1482 .as_mut()
1483 .expect("borrowed sender already returned")
1484 }
1485
1486 fn inner_ref(&self) -> &PooledSenderCore {
1487 self.sender
1488 .as_ref()
1489 .expect("borrowed sender already returned")
1490 }
1491}
1492
1493struct DirectSenderHandle<'a> {
1494 db: &'a QuestDb,
1495 sender: Option<DirectSenderCore>,
1496 _not_send: PhantomData<Rc<()>>,
1497}
1498
1499impl<'a> DirectSenderHandle<'a> {
1500 fn new(db: &'a QuestDb, sender: PooledSender<DirectSenderCore>) -> Self {
1501 debug_assert!(sender.slot_index.is_none());
1502 Self {
1503 db,
1504 sender: Some(sender.sender),
1505 _not_send: PhantomData,
1506 }
1507 }
1508
1509 fn inner_mut(&mut self) -> &mut DirectSenderCore {
1510 self.sender
1511 .as_mut()
1512 .expect("borrowed direct sender already returned")
1513 }
1514
1515 #[cfg(test)]
1516 fn inner_ref(&self) -> &DirectSenderCore {
1517 self.sender
1518 .as_ref()
1519 .expect("borrowed direct sender already returned")
1520 }
1521
1522 #[cfg(any(feature = "polars-ingress", feature = "polars-egress"))]
1523 pub(crate) fn reconnect_policy(&self) -> crate::ingress::ReconnectPolicy {
1524 self.db.reconnect_policy()
1525 }
1526
1527 /// Drop the current connection (and its paired connection-scoped
1528 /// `SymbolGlobalDict`) back to the pool and obtain a fresh one **behind
1529 /// the same handle**, so the caller's borrowed direct sender stays valid.
1530 ///
1531 /// This is the direct sender's failover primitive: after a transient
1532 /// (`ErrorCode::FailoverRetry`) flush/sync failure, call this to swap onto
1533 /// a live connection — the pool's connect path rotates across endpoints,
1534 /// skips the dead one, and follows the writable primary. The dropped
1535 /// connection's dict is discarded with it; the fresh connection brings its
1536 /// own dict, consistent with the server it talks to, so the unchanged
1537 /// delta-dict encoder re-drives correctly on the re-iterated source.
1538 ///
1539 /// The current connection stays behind this handle until a replacement has
1540 /// been opened. If replacement connect fails, the handle remains populated
1541 /// (possibly with a terminal connection) so later safe calls report errors
1542 /// instead of panicking. Once replacement succeeds, a failed connection is
1543 /// dropped (not recycled); a clean connection with un-sync'd in-flight
1544 /// frames is also dropped, mirroring [`Drop`], so the next borrower never
1545 /// commits this caller's data.
1546 pub fn reborrow_from_pool(&mut self) -> Result<()> {
1547 if let Some(sender) = self.sender.as_mut() {
1548 // reborrow is a failover path, not a forced rotate. A healthy,
1549 // fully-sync'd connection needs no replacement; replacing it would
1550 // open a fresh connection and recycle this one, growing the pool.
1551 if sender.in_flight() == 0 && !sender.must_close() && !sender.transport_dead() {
1552 return Ok(());
1553 }
1554 if sender.in_flight() > 0 {
1555 log::warn!(
1556 "direct sender failover dropped a connection with un-sync'd \
1557 deferred frame(s); their data is discarded. Re-drive the source \
1558 from the last successful sync(), not from the failing chunk."
1559 );
1560 sender.mark_must_close();
1561 }
1562 record_sender_transport_failure(&self.db.inner, sender);
1563 }
1564 let fresh = self.db.pick_replacement_sender()?;
1565 debug_assert!(fresh.slot_index.is_none());
1566 if let Some(old) = self.sender.replace(fresh.sender) {
1567 finish_replaced_sender(&self.db.inner, old);
1568 }
1569 Ok(())
1570 }
1571
1572 /// Retry [`reborrow_from_pool`] within `deadline` using the row API's
1573 /// reconnect backoff (centered-jittered, role-reject reset; `AuthError` /
1574 /// `ProtocolVersionError` terminal). On terminal failure or budget
1575 /// exhaustion the handle stays populated (per [`reborrow_from_pool`]), so a
1576 /// later call reports a typed error rather than panicking.
1577 #[cfg(any(feature = "polars-ingress", feature = "polars-egress"))]
1578 pub(crate) fn reborrow_with_retry(&mut self, deadline: Option<Instant>) -> Result<()> {
1579 let policy = self.reconnect_policy();
1580 let mut backoff = policy.initial_backoff();
1581 loop {
1582 match self.reborrow_from_pool() {
1583 Ok(()) => return Ok(()),
1584 Err(e)
1585 if reconnect_error_is_terminal(&e) || reconnect_deadline_expired(deadline) =>
1586 {
1587 return Err(e);
1588 }
1589 Err(e) => {
1590 let (sleep_for, next) = reconnect_backoff_step(
1591 &e,
1592 policy.initial_backoff(),
1593 policy.max_backoff(),
1594 backoff,
1595 );
1596 sleep_until_deadline(sleep_for, deadline);
1597 backoff = next;
1598 }
1599 }
1600 }
1601 }
1602}
1603
1604impl Debug for SenderHandle<'_> {
1605 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1606 f.debug_struct("SenderHandle")
1607 .field("sender", &self.sender)
1608 .finish()
1609 }
1610}
1611
1612impl Debug for DirectSenderHandle<'_> {
1613 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1614 f.debug_struct("DirectSenderHandle")
1615 .field("sender", &self.sender)
1616 .finish()
1617 }
1618}
1619
1620/// Store-and-forward QWP sender borrowed from a [`QuestDb`] pool — the
1621/// handle returned by [`QuestDb::borrow_sender`].
1622///
1623/// [`Self::flush`] appends a frame to the connection's store-and-forward queue
1624/// and returns as soon as it is accepted locally (no server round-trip); the
1625/// connection's background runner delivers it asynchronously. While the handle
1626/// is borrowed or parked in the pool the runner keeps delivering, so returning
1627/// or dropping the handle does not by itself lose accepted frames.
1628///
1629/// Delivery is completed best-effort when the pool is closed or the connection
1630/// is retired, bounded by `close_flush_timeout` (default 5s): an in-memory
1631/// queue whose server stays unreachable past that window drops its undelivered
1632/// tail, logging a warning. For a hard guarantee, call [`Self::wait`] before
1633/// closing the pool — it blocks until the frames published so far reach the
1634/// requested [`AckLevel`], i.e. confirms delivery — or configure `sf_dir` for
1635/// crash-durable on-disk persistence with replay. [`Self::flush_and_wait`]
1636/// combines the two ("publish this batch and return once it is delivered");
1637/// its wait is bounded by the pool-wide `request_timeout` setting, so compose
1638/// [`Self::flush`] then [`Self::wait`] if you want to pass an explicit
1639/// timeout instead.
1640/// Use FSNs only for non-blocking progress tracking while this borrowed sender
1641/// is still held: they are stream watermarks, not portable receipts to check
1642/// through an arbitrary later pool borrow.
1643///
1644/// Not `Send` or `Sync`.
1645///
1646/// The lease cannot outlive its pool:
1647///
1648/// ```compile_fail
1649/// use questdb::{BorrowedSender, QuestDb};
1650///
1651/// fn escape() -> BorrowedSender<'static> {
1652/// let db = QuestDb::connect("ws::addr=localhost:9000;").unwrap();
1653/// db.borrow_sender().unwrap()
1654/// }
1655/// ```
1656///
1657/// It cannot be moved to another thread:
1658///
1659/// ```compile_fail
1660/// use questdb::QuestDb;
1661///
1662/// let db = QuestDb::connect("ws::addr=localhost:9000;").unwrap();
1663/// let sender = db.borrow_sender().unwrap();
1664/// std::thread::scope(|scope| {
1665/// scope.spawn(move || drop(sender));
1666/// });
1667/// ```
1668///
1669/// Nor can a shared reference be sent to another thread:
1670///
1671/// ```compile_fail
1672/// use questdb::QuestDb;
1673///
1674/// let db = QuestDb::connect("ws::addr=localhost:9000;").unwrap();
1675/// let sender = db.borrow_sender().unwrap();
1676/// std::thread::scope(|scope| {
1677/// scope.spawn(|| std::hint::black_box(&sender));
1678/// });
1679/// ```
1680pub struct BorrowedSender<'a>(SenderHandle<'a>);
1681
1682impl<'a> BorrowedSender<'a> {
1683 #[cfg(test)]
1684 pub(crate) fn effective_frame_cap_for_test(&self) -> (usize, bool) {
1685 self.0.inner_ref().effective_frame_cap()
1686 }
1687
1688 /// Create a caller-owned QWP/WebSocket [`Buffer`] using the pool's
1689 /// configured name limit. The buffer is not tied to this lease and may be
1690 /// flushed by another sender borrowed from the same pool.
1691 pub fn new_buffer(&self) -> Buffer {
1692 self.0.db.new_buffer()
1693 }
1694
1695 /// Encode and publish `chunk` into the store-and-forward queue, returning
1696 /// as soon as the frame is accepted locally (no server round-trip). On
1697 /// success `chunk` is cleared; on a delivery-uncertain failure the error
1698 /// is tagged [`in_doubt`](crate::Error::in_doubt).
1699 pub fn flush(&mut self, chunk: &mut crate::ingress::column_sender::Chunk<'_>) -> Result<()> {
1700 self.0.inner_mut().flush(chunk)
1701 }
1702
1703 /// Publish a caller-owned QWP/WebSocket [`Buffer`] into this sender's local
1704 /// store-and-forward queue and clear it after local acceptance.
1705 pub fn flush_buffer(&mut self, buffer: &mut Buffer) -> Result<()> {
1706 self.0.inner_mut().flush_buffer(buffer)
1707 }
1708
1709 /// Publish a caller-owned QWP/WebSocket [`Buffer`] without clearing it.
1710 pub fn flush_buffer_and_keep(&mut self, buffer: &Buffer) -> Result<()> {
1711 self.0.inner_mut().flush_buffer_and_keep(buffer)
1712 }
1713
1714 /// Publish and clear a QWP/WebSocket [`Buffer`], returning its local frame
1715 /// sequence number. Empty buffers publish no frame and return `None`.
1716 pub fn flush_buffer_and_get_fsn(&mut self, buffer: &mut Buffer) -> Result<Option<u64>> {
1717 self.0.inner_mut().flush_buffer_and_get_fsn(buffer)
1718 }
1719
1720 /// Publish a QWP/WebSocket [`Buffer`] without clearing it and return its
1721 /// local frame sequence number. Empty buffers return `None`.
1722 pub fn flush_buffer_and_keep_and_get_fsn(&mut self, buffer: &Buffer) -> Result<Option<u64>> {
1723 self.0.inner_mut().flush_buffer_and_keep_and_get_fsn(buffer)
1724 }
1725
1726 /// Publish and clear a QWP/WebSocket [`Buffer`], then wait for the requested
1727 /// ACK boundary using the pool's configured request timeout.
1728 pub fn flush_buffer_and_wait(
1729 &mut self,
1730 buffer: &mut Buffer,
1731 ack_level: AckLevel,
1732 ) -> Result<()> {
1733 self.0.inner_mut().flush_buffer_and_wait(buffer, ack_level)
1734 }
1735
1736 /// Publish `chunk` into the store-and-forward queue as a completion
1737 /// boundary, then wait until every frame published on this handle so far
1738 /// reaches `ack_level` — [`Self::flush`] followed by [`Self::wait`] in one
1739 /// call. Unlike [`Self::wait`], which takes an explicit timeout argument,
1740 /// this call's wait is bounded by the pool-wide `request_timeout` setting
1741 /// (the no-progress timeout fires when the ack watermark stops advancing
1742 /// for that long); compose the two calls yourself to choose the timeout
1743 /// per call.
1744 ///
1745 /// `AckLevel::Durable` requires QuestDB Enterprise and a pool opened with
1746 /// `request_durable_ack=on`; otherwise the call is rejected up front
1747 /// (`InvalidApiCall`) before `chunk` is touched.
1748 ///
1749 /// Failure contract: if local publication fails, `chunk` is untouched and
1750 /// retryable. Once the frame is accepted into the queue `chunk` is cleared
1751 /// even if the wait then fails. On the no-progress timeout
1752 /// ([`ErrorCode::FailoverRetry`](crate::ErrorCode)) the frames remain
1753 /// queued and the background runner keeps delivering them — recover by
1754 /// calling [`Self::wait`] until it returns `Ok`, not by re-flushing
1755 /// (which would deliver the same rows twice). A terminal server rejection
1756 /// or transport failure instead ends delivery on this sender: drop the
1757 /// borrow and recover per the rejection policy.
1758 pub fn flush_and_wait(
1759 &mut self,
1760 chunk: &mut crate::ingress::column_sender::Chunk<'_>,
1761 ack_level: AckLevel,
1762 ) -> Result<()> {
1763 self.0.inner_mut().flush_and_wait(chunk, ack_level)
1764 }
1765
1766 /// Encode and publish `chunk` into the store-and-forward queue and return
1767 /// the highest published frame sequence number.
1768 ///
1769 /// This is the non-blocking progress-tracking form of [`Self::flush`]:
1770 /// success means the frame was accepted locally, not that the server has
1771 /// ACKed it. If the chunk is split into multiple frames, the returned FSN
1772 /// is the final frame boundary; cumulative ACK coverage of that boundary
1773 /// covers the whole chunk. Use [`Self::wait`] when you only need a simple
1774 /// blocking barrier for everything published so far. Treat the returned
1775 /// FSN as meaningful only with this sender stream while this borrow is
1776 /// held.
1777 pub fn flush_and_get_fsn(
1778 &mut self,
1779 chunk: &mut crate::ingress::column_sender::Chunk<'_>,
1780 ) -> Result<Option<u64>> {
1781 self.0.inner_mut().flush_and_get_fsn(chunk)
1782 }
1783
1784 /// Return the highest frame sequence number published locally by this
1785 /// sender, or `None` if no frame has been published.
1786 ///
1787 /// This is a stream watermark for the currently borrowed sender, not a
1788 /// portable receipt to check through an arbitrary later pool borrow.
1789 pub fn published_fsn(&self) -> Result<Option<u64>> {
1790 self.0.inner_ref().published_fsn()
1791 }
1792
1793 /// Return the highest frame sequence number completed by server ACK or
1794 /// server-side reject-and-continue, or `None` if no frame has completed.
1795 ///
1796 /// In Enterprise durable-ACK mode this watermark advances after durable
1797 /// ACK coverage; use [`Self::wait`] when you need an explicit
1798 /// [`AckLevel::Ok`] or [`AckLevel::Durable`] barrier. Compare it only with
1799 /// FSNs produced by this same sender stream.
1800 pub fn acked_fsn(&self) -> Result<Option<u64>> {
1801 self.0.inner_ref().acked_fsn()
1802 }
1803
1804 /// Wait up to `timeout` for every frame published through this lease so
1805 /// far to reach `ack_level`. Short-circuits when the lease published
1806 /// nothing or the watermark already covers its latest frame. The barrier
1807 /// is a watermark check plus a terminal-latch check: only a terminal
1808 /// connection failure fails it. Server rejections are delivered to the
1809 /// pool's rejection handler (default: logged; see
1810 /// [`QuestDb::connect_with_handlers`]) rather than raised here;
1811 /// retriable ones are replayed by the queue. `AckLevel::Durable` requires
1812 /// QuestDB Enterprise and a pool opened with `request_durable_ack=on`.
1813 ///
1814 /// `timeout` is a no-progress deadline (it fires only if the ack watermark
1815 /// fails to advance for that long); `Duration::ZERO` waits indefinitely.
1816 /// On expiry it returns an [`ErrorCode::FailoverRetry`](crate::ErrorCode)
1817 /// error; the frames remain queued and the background runner keeps
1818 /// delivering them, so recover by calling `wait()` again until it returns
1819 /// `Ok` — not by re-flushing, which would deliver the same rows twice.
1820 pub fn wait(&mut self, ack_level: AckLevel, timeout: Duration) -> Result<()> {
1821 self.0.inner_mut().wait(ack_level, timeout)
1822 }
1823
1824 /// Force this borrowed connection to be dropped (not recycled) on return.
1825 ///
1826 /// Use normal `Drop` for healthy connections: the return path already
1827 /// retires connections that latched terminal state, or whose pool has been
1828 /// closed. Call this after abandoning work or handling an error where the
1829 /// next borrower must not inherit this backend. If queued
1830 /// store-and-forward frames must not be lost, call [`Self::wait`] first or
1831 /// configure `sf_dir` for replay.
1832 pub fn drop_on_return(&mut self) {
1833 self.0.inner_mut().mark_must_close()
1834 }
1835
1836 #[cfg(test)]
1837 pub(crate) fn must_close_for_test(&self) -> bool {
1838 self.0.inner_ref().must_close()
1839 }
1840
1841 /// Always `true` for an SF handle (it wraps a store-and-forward backend).
1842 /// Retained for symmetry with [`BorrowedDirectColumnSender`] and test assertions.
1843 #[cfg(test)]
1844 pub(crate) fn is_store_and_forward(&self) -> bool {
1845 true
1846 }
1847
1848 /// In-flight (published-but-unacked) frame count. Always 0 for the SF
1849 /// backend, whose queue tracks delivery internally.
1850 #[cfg(test)]
1851 pub(crate) fn in_flight(&self) -> u32 {
1852 0
1853 }
1854
1855 /// Encode and publish an Arrow [`RecordBatch`](arrow::array::RecordBatch)
1856 /// into the queue, letting the server stamp each row's designated
1857 /// timestamp on arrival. Publish-only; call [`Self::wait`] for an ack.
1858 #[cfg(feature = "arrow-ingress")]
1859 pub fn flush_arrow_batch_at_now<'t, T>(
1860 &mut self,
1861 table: T,
1862 batch: &arrow::array::RecordBatch,
1863 overrides: &[crate::ingress::column_sender::ArrowColumnOverride<'_>],
1864 ) -> Result<()>
1865 where
1866 T: TryInto<crate::ingress::TableName<'t>>,
1867 crate::Error: From<T::Error>,
1868 {
1869 self.0
1870 .inner_mut()
1871 .flush_arrow_batch_at_now(table, batch, overrides)
1872 }
1873
1874 /// ACKing counterpart of [`Self::flush_arrow_batch_at_now`]: publish the
1875 /// batch as a completion boundary, then wait for `ack_level`. The same
1876 /// contract as [`Self::flush_and_wait`] applies.
1877 #[cfg(feature = "arrow-ingress")]
1878 pub fn flush_arrow_batch_at_now_and_wait<'t, T>(
1879 &mut self,
1880 table: T,
1881 batch: &arrow::array::RecordBatch,
1882 overrides: &[crate::ingress::column_sender::ArrowColumnOverride<'_>],
1883 ack_level: AckLevel,
1884 ) -> Result<()>
1885 where
1886 T: TryInto<crate::ingress::TableName<'t>>,
1887 crate::Error: From<T::Error>,
1888 {
1889 self.0
1890 .inner_mut()
1891 .flush_arrow_batch_at_now_and_wait(table, batch, overrides, ack_level)
1892 }
1893
1894 /// Arrow counterpart of [`Self::flush_and_get_fsn`], letting the server
1895 /// stamp each row's designated timestamp on arrival.
1896 #[cfg(feature = "arrow-ingress")]
1897 pub fn flush_arrow_batch_at_now_and_get_fsn<'t, T>(
1898 &mut self,
1899 table: T,
1900 batch: &arrow::array::RecordBatch,
1901 overrides: &[crate::ingress::column_sender::ArrowColumnOverride<'_>],
1902 ) -> Result<Option<u64>>
1903 where
1904 T: TryInto<crate::ingress::TableName<'t>>,
1905 crate::Error: From<T::Error>,
1906 {
1907 self.0
1908 .inner_mut()
1909 .flush_arrow_batch_at_now_and_get_fsn(table, batch, overrides)
1910 }
1911
1912 /// Encode and publish an Arrow [`RecordBatch`](arrow::array::RecordBatch)
1913 /// into the queue, sourcing the designated timestamp from the named
1914 /// column. Publish-only; call [`Self::wait`] for an ack.
1915 #[cfg(feature = "arrow-ingress")]
1916 pub fn flush_arrow_batch_at_column<'t, T>(
1917 &mut self,
1918 table: T,
1919 batch: &arrow::array::RecordBatch,
1920 ts_column: crate::ingress::ColumnName<'_>,
1921 overrides: &[crate::ingress::column_sender::ArrowColumnOverride<'_>],
1922 ) -> Result<()>
1923 where
1924 T: TryInto<crate::ingress::TableName<'t>>,
1925 crate::Error: From<T::Error>,
1926 {
1927 self.0
1928 .inner_mut()
1929 .flush_arrow_batch_at_column(table, batch, ts_column, overrides)
1930 }
1931
1932 /// ACKing counterpart of [`Self::flush_arrow_batch_at_column`]: publish
1933 /// the batch as a completion boundary, then wait for `ack_level`. The same
1934 /// contract as [`Self::flush_and_wait`] applies.
1935 #[cfg(feature = "arrow-ingress")]
1936 pub fn flush_arrow_batch_at_column_and_wait<'t, T>(
1937 &mut self,
1938 table: T,
1939 batch: &arrow::array::RecordBatch,
1940 ts_column: crate::ingress::ColumnName<'_>,
1941 overrides: &[crate::ingress::column_sender::ArrowColumnOverride<'_>],
1942 ack_level: AckLevel,
1943 ) -> Result<()>
1944 where
1945 T: TryInto<crate::ingress::TableName<'t>>,
1946 crate::Error: From<T::Error>,
1947 {
1948 self.0
1949 .inner_mut()
1950 .flush_arrow_batch_at_column_and_wait(table, batch, ts_column, overrides, ack_level)
1951 }
1952
1953 /// Arrow counterpart of [`Self::flush_and_get_fsn`], sourcing the
1954 /// designated timestamp from the named column.
1955 #[cfg(feature = "arrow-ingress")]
1956 pub fn flush_arrow_batch_at_column_and_get_fsn<'t, T>(
1957 &mut self,
1958 table: T,
1959 batch: &arrow::array::RecordBatch,
1960 ts_column: crate::ingress::ColumnName<'_>,
1961 overrides: &[crate::ingress::column_sender::ArrowColumnOverride<'_>],
1962 ) -> Result<Option<u64>>
1963 where
1964 T: TryInto<crate::ingress::TableName<'t>>,
1965 crate::Error: From<T::Error>,
1966 {
1967 self.0
1968 .inner_mut()
1969 .flush_arrow_batch_at_column_and_get_fsn(table, batch, ts_column, overrides)
1970 }
1971}
1972
1973impl Debug for BorrowedSender<'_> {
1974 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1975 f.debug_tuple("BorrowedSender").field(&self.0).finish()
1976 }
1977}
1978
1979/// Direct (pipelined, non-store-and-forward) column sender borrowed from a
1980/// [`QuestDb`] pool — the handle returned by
1981/// [`QuestDb::borrow_direct_column_sender`], used by DataFrame ingestion.
1982///
1983/// [`Self::flush`] pipelines a deferred frame; [`Self::commit`] (or
1984/// [`Self::flush_and_wait`] on the final chunk) sends the commit boundary and
1985/// waits for `ack_level`. Normal `Drop` makes a best-effort commit of
1986/// uncommitted deferred frames at the pool's default ack level. If that commit
1987/// fails, or if [`Self::drop_on_return`] was requested, those frames are
1988/// discarded; for deterministic error handling, call [`Self::commit`] or
1989/// [`Self::flush_and_wait`] yourself and re-drive from the last successful
1990/// commit after failure.
1991///
1992/// Not `Send` or `Sync`.
1993pub struct BorrowedDirectColumnSender<'a>(DirectSenderHandle<'a>);
1994
1995impl<'a> BorrowedDirectColumnSender<'a> {
1996 /// Encode and pipeline `chunk` as a deferred frame without waiting. The
1997 /// frame is not committed until [`Self::commit`] / [`Self::flush_and_wait`].
1998 pub fn flush(&mut self, chunk: &mut crate::ingress::column_sender::Chunk<'_>) -> Result<()> {
1999 self.0.inner_mut().flush(chunk)
2000 }
2001
2002 /// Publish `chunk` as a non-deferred commit boundary and block until it
2003 /// (and all prior pipelined frames) reach `ack_level`.
2004 pub fn flush_and_wait(
2005 &mut self,
2006 chunk: &mut crate::ingress::column_sender::Chunk<'_>,
2007 ack_level: AckLevel,
2008 ) -> Result<()> {
2009 self.0.inner_mut().flush_and_wait(chunk, ack_level)
2010 }
2011
2012 /// Send the commit boundary for all pipelined frames and block until they
2013 /// reach `ack_level`. This is the direct sender's explicit durability
2014 /// checkpoint; normal `Drop` attempts the same kind of commit best-effort,
2015 /// but callers that need deterministic error handling should call
2016 /// `commit()` themselves.
2017 pub fn commit(&mut self, ack_level: AckLevel) -> Result<()> {
2018 self.0.inner_mut().sync(ack_level)
2019 }
2020
2021 /// Failover primitive: swap onto a fresh connection from the pool behind
2022 /// the same handle after a transient flush failure. No-op on a healthy,
2023 /// fully-committed connection.
2024 pub fn reborrow_from_pool(&mut self) -> Result<()> {
2025 self.0.reborrow_from_pool()
2026 }
2027
2028 #[cfg(any(feature = "polars-ingress", feature = "polars-egress"))]
2029 pub(crate) fn reborrow_with_retry(&mut self, deadline: Option<Instant>) -> Result<()> {
2030 self.0.reborrow_with_retry(deadline)
2031 }
2032
2033 #[cfg(any(feature = "polars-ingress", feature = "polars-egress"))]
2034 pub(crate) fn reconnect_policy(&self) -> crate::ingress::ReconnectPolicy {
2035 self.0.reconnect_policy()
2036 }
2037
2038 /// The pool's default ack level (see [`QuestDb::default_ack_level`]),
2039 /// reached through the handle's owning `QuestDb`.
2040 #[cfg(feature = "polars-ingress")]
2041 pub(crate) fn default_ack_level(&self) -> AckLevel {
2042 self.0.db.default_ack_level()
2043 }
2044
2045 /// Force this borrowed connection to be dropped (not recycled) on return.
2046 ///
2047 /// Use normal `Drop` for healthy connections: the return path already
2048 /// retires connections that latched terminal state, or whose pool has been
2049 /// closed. Call this after abandoning deferred frames or handling an error
2050 /// where the next borrower must not inherit this backend. Call this only
2051 /// after you are done using the handle. To preserve deferred frames, commit
2052 /// them successfully with [`Self::commit`] or [`Self::flush_and_wait`]
2053 /// before calling `drop_on_return()`; after this call the connection is
2054 /// terminal and later commit/flush attempts may fail.
2055 pub fn drop_on_return(&mut self) {
2056 self.0.inner_mut().mark_must_close()
2057 }
2058
2059 #[cfg(test)]
2060 pub(crate) fn must_close_for_test(&self) -> bool {
2061 self.0.inner_ref().must_close()
2062 }
2063
2064 /// Always `false` for a direct handle. Retained for symmetry with
2065 /// [`BorrowedSender`] and test assertions.
2066 #[cfg(test)]
2067 pub(crate) fn is_store_and_forward(&self) -> bool {
2068 false
2069 }
2070
2071 /// In-flight (published-but-unacked) deferred frame count.
2072 #[cfg(test)]
2073 pub(crate) fn in_flight(&self) -> u32 {
2074 self.0.inner_ref().in_flight()
2075 }
2076
2077 /// Publish-only Arrow flush (server-stamped). Pair with [`Self::commit`].
2078 /// Only the DataFrame checkpoint loop pipelines publish-only frames, so this
2079 /// is gated on `polars-ingress` (a plain `arrow-ingress` build reaches the
2080 /// server only through the ACKing `flush_arrow_batch`).
2081 #[cfg(feature = "polars-ingress")]
2082 pub(crate) fn flush_arrow_batch_at_now<'t, T>(
2083 &mut self,
2084 table: T,
2085 batch: &arrow::array::RecordBatch,
2086 overrides: &[crate::ingress::column_sender::ArrowColumnOverride<'_>],
2087 ) -> Result<()>
2088 where
2089 T: TryInto<crate::ingress::TableName<'t>>,
2090 crate::Error: From<T::Error>,
2091 {
2092 self.0
2093 .inner_mut()
2094 .flush_arrow_batch_at_now(table, batch, overrides)
2095 }
2096
2097 /// Publish-only Arrow flush (column-stamped). Pair with [`Self::commit`].
2098 /// `polars-ingress`-gated for the same reason as
2099 /// [`Self::flush_arrow_batch_at_now`].
2100 #[cfg(feature = "polars-ingress")]
2101 pub(crate) fn flush_arrow_batch_at_column<'t, T>(
2102 &mut self,
2103 table: T,
2104 batch: &arrow::array::RecordBatch,
2105 ts_column: crate::ingress::ColumnName<'_>,
2106 overrides: &[crate::ingress::column_sender::ArrowColumnOverride<'_>],
2107 ) -> Result<()>
2108 where
2109 T: TryInto<crate::ingress::TableName<'t>>,
2110 crate::Error: From<T::Error>,
2111 {
2112 self.0
2113 .inner_mut()
2114 .flush_arrow_batch_at_column(table, batch, ts_column, overrides)
2115 }
2116
2117 /// ACKing Arrow flush (server-stamped): publish as a commit boundary and
2118 /// wait for `ack_level`.
2119 #[cfg(feature = "arrow-ingress")]
2120 pub(crate) fn flush_arrow_batch_at_now_and_wait<'t, T>(
2121 &mut self,
2122 table: T,
2123 batch: &arrow::array::RecordBatch,
2124 overrides: &[crate::ingress::column_sender::ArrowColumnOverride<'_>],
2125 ack_level: AckLevel,
2126 ) -> Result<()>
2127 where
2128 T: TryInto<crate::ingress::TableName<'t>>,
2129 crate::Error: From<T::Error>,
2130 {
2131 self.0
2132 .inner_mut()
2133 .flush_arrow_batch_at_now_and_wait(table, batch, overrides, ack_level)
2134 }
2135
2136 /// ACKing Arrow flush (column-stamped): publish as a commit boundary and
2137 /// wait for `ack_level`.
2138 #[cfg(feature = "arrow-ingress")]
2139 pub(crate) fn flush_arrow_batch_at_column_and_wait<'t, T>(
2140 &mut self,
2141 table: T,
2142 batch: &arrow::array::RecordBatch,
2143 ts_column: crate::ingress::ColumnName<'_>,
2144 overrides: &[crate::ingress::column_sender::ArrowColumnOverride<'_>],
2145 ack_level: AckLevel,
2146 ) -> Result<()>
2147 where
2148 T: TryInto<crate::ingress::TableName<'t>>,
2149 crate::Error: From<T::Error>,
2150 {
2151 self.0
2152 .inner_mut()
2153 .flush_arrow_batch_at_column_and_wait(table, batch, ts_column, overrides, ack_level)
2154 }
2155}
2156
2157impl Debug for BorrowedDirectColumnSender<'_> {
2158 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
2159 f.debug_tuple("BorrowedDirectColumnSender")
2160 .field(&self.0)
2161 .finish()
2162 }
2163}
2164
2165impl Drop for SenderHandle<'_> {
2166 fn drop(&mut self) {
2167 let Some(sender) = self.sender.take() else {
2168 return;
2169 };
2170 return_sfa_to_pool(&self.db.inner, sender, self.slot_index);
2171 }
2172}
2173
2174impl Drop for DirectSenderHandle<'_> {
2175 fn drop(&mut self) {
2176 let Some(mut sender) = self.sender.take() else {
2177 return;
2178 };
2179 commit_in_flight_on_drop(self.db.inner.connector.request_durable_ack(), &mut sender);
2180 return_direct_to_pool(&self.db.inner, sender);
2181 }
2182}
2183
2184/// Owned (lifetime-free) variant of a borrowed sender used by the C FFI.
2185///
2186/// Holds an `Arc<DbInner>` so the pool's return path outlives the
2187/// user-facing `QuestDb` pointer — the C ABI can free its `questdb_db*`
2188/// before dropping outstanding `qwp_sender*` or `qwp_direct_sender*`
2189/// handles. After pool close, returned handles are dropped instead of recycled.
2190#[cfg(feature = "ffi-support")]
2191pub struct OwnedSender {
2192 inner: Arc<DbInner>,
2193 sender: Option<PooledSenderCore>,
2194 slot_index: Option<usize>,
2195}
2196
2197#[cfg(feature = "ffi-support")]
2198impl OwnedSender {
2199 fn new(inner: Arc<DbInner>, sender: PooledSender<PooledSenderCore>) -> Self {
2200 Self {
2201 inner,
2202 sender: Some(sender.sender),
2203 slot_index: sender.slot_index,
2204 }
2205 }
2206
2207 /// Borrow the underlying [`PooledSenderCore`] mutably. Always returns a
2208 /// live reference until `Drop` runs.
2209 pub fn get_mut(&mut self) -> &mut PooledSenderCore {
2210 self.sender
2211 .as_mut()
2212 .expect("OwnedSender already returned to the pool")
2213 }
2214
2215 /// Inspect the wrapped sender without taking ownership.
2216 pub fn get(&self) -> &PooledSenderCore {
2217 self.sender
2218 .as_ref()
2219 .expect("OwnedSender already returned to the pool")
2220 }
2221
2222 /// `true` after the originating pool has been closed. FFI callers use
2223 /// this to reject new work on checked-out handles while still allowing
2224 /// return/drop to clean up safely.
2225 pub fn pool_closed(&self) -> bool {
2226 self.inner.shutdown.load(Ordering::SeqCst)
2227 }
2228
2229 /// Force this sender to be dropped instead of recycled when the owned FFI
2230 /// handle is released.
2231 pub fn mark_must_close(&mut self) {
2232 self.get_mut().mark_must_close();
2233 }
2234
2235 /// `true` when this sender cannot be returned to the pool, either because
2236 /// the sender is terminal or because its originating pool has closed.
2237 pub fn must_close(&self) -> bool {
2238 self.pool_closed() || self.get().must_close()
2239 }
2240}
2241
2242#[cfg(feature = "ffi-support")]
2243impl Drop for OwnedSender {
2244 fn drop(&mut self) {
2245 if let Some(sender) = self.sender.take() {
2246 return_sfa_to_pool(&self.inner, sender, self.slot_index);
2247 }
2248 }
2249}
2250
2251/// Backing of an [`OwnedDirectColumnSender`]: either a slot returned to a
2252/// pool, or a poolless connection owned outright.
2253#[cfg(feature = "ffi-support")]
2254enum DirectBacking {
2255 Pool(Arc<DbInner>),
2256 Standalone { request_durable_ack: bool },
2257}
2258
2259/// Owned variant of the hidden direct sender used by the C FFI. Either
2260/// borrowed from a [`QuestDb`] pool or built standalone from a config string.
2261#[cfg(feature = "ffi-support")]
2262pub struct OwnedDirectColumnSender {
2263 backing: DirectBacking,
2264 sender: Option<DirectSenderCore>,
2265}
2266
2267#[cfg(feature = "ffi-support")]
2268impl OwnedDirectColumnSender {
2269 fn new(inner: Arc<DbInner>, sender: PooledSender<DirectSenderCore>) -> Self {
2270 debug_assert!(sender.slot_index.is_none());
2271 Self {
2272 backing: DirectBacking::Pool(inner),
2273 sender: Some(sender.sender),
2274 }
2275 }
2276
2277 /// Build a direct column sender from a QWP/WebSocket config string,
2278 /// opening its own connection and owning it outright — no pool.
2279 pub fn from_conf(conf: &str) -> Result<Self> {
2280 Self::from_builder(&SenderBuilder::from_conf(conf)?)
2281 }
2282
2283 /// Build a direct column sender from an already-configured
2284 /// [`SenderBuilder`] (which carries auth/TLS applied programmatically,
2285 /// not just what a config string encodes), owning its own connection
2286 /// with no pool. The builder is only borrowed.
2287 pub fn from_builder(builder: &SenderBuilder) -> Result<Self> {
2288 let connector = builder.build_qwp_ws_connector()?;
2289 let health = Mutex::new(QwpWsHostHealthTracker::new(connector.endpoint_count()));
2290 let raw = connector.connect_round_pooled(&health, None)?;
2291 let conn = ColumnConn::from_round_stream(raw)?;
2292 let sender = DirectSenderCore::new(
2293 conn,
2294 crate::ingress::SymbolGlobalDict::new(),
2295 crate::ingress::column_sender::encoder::EncodeScratch::new(),
2296 false,
2297 );
2298 Ok(Self {
2299 backing: DirectBacking::Standalone {
2300 request_durable_ack: connector.request_durable_ack(),
2301 },
2302 sender: Some(sender),
2303 })
2304 }
2305
2306 pub fn get_mut(&mut self) -> &mut DirectSenderCore {
2307 self.sender
2308 .as_mut()
2309 .expect("OwnedDirectColumnSender already released")
2310 }
2311
2312 pub fn get(&self) -> &DirectSenderCore {
2313 self.sender
2314 .as_ref()
2315 .expect("OwnedDirectColumnSender already released")
2316 }
2317
2318 pub fn pool_closed(&self) -> bool {
2319 match &self.backing {
2320 DirectBacking::Pool(inner) => inner.shutdown.load(Ordering::SeqCst),
2321 DirectBacking::Standalone { .. } => false,
2322 }
2323 }
2324
2325 pub fn mark_must_close(&mut self) {
2326 self.get_mut().mark_must_close();
2327 }
2328
2329 pub fn must_close(&self) -> bool {
2330 self.pool_closed() || self.get().must_close()
2331 }
2332}
2333
2334#[cfg(feature = "ffi-support")]
2335impl Drop for OwnedDirectColumnSender {
2336 fn drop(&mut self) {
2337 let Some(mut sender) = self.sender.take() else {
2338 return;
2339 };
2340 match &self.backing {
2341 DirectBacking::Pool(inner) => {
2342 commit_in_flight_on_drop(inner.connector.request_durable_ack(), &mut sender);
2343 return_direct_to_pool(inner, sender);
2344 }
2345 DirectBacking::Standalone {
2346 request_durable_ack,
2347 } => {
2348 commit_in_flight_on_drop(*request_durable_ack, &mut sender);
2349 }
2350 }
2351 }
2352}
2353
2354/// A query [`Reader`] borrowed from a [`QuestDb`] pool.
2355///
2356/// Egress companion to [`BorrowedSender`]. Derefs to `Reader`, so the usual
2357/// `prepare` / `execute` cursor flow works unchanged. On `Drop` the reader
2358/// is returned to the reader pool, unless its transport has been torn down
2359/// (or [`Self::drop_on_return`] was called), in which case it is dropped
2360/// and the next borrow opens a fresh one.
2361///
2362/// `BorrowedReader` is **not** `Send` or `Sync`: the borrowed connection
2363/// belongs to the borrowing thread for the duration of the borrow.
2364#[cfg(feature = "_egress")]
2365pub struct BorrowedReader<'a> {
2366 db: &'a QuestDb,
2367 reader: Option<Reader>,
2368 must_close: bool,
2369 /// !Send / !Sync marker, mirroring [`BorrowedSender`].
2370 _not_send: PhantomData<Rc<()>>,
2371}
2372
2373#[cfg(feature = "_egress")]
2374impl<'a> BorrowedReader<'a> {
2375 fn new(db: &'a QuestDb, reader: Reader) -> Self {
2376 Self {
2377 db,
2378 reader: Some(reader),
2379 must_close: false,
2380 _not_send: PhantomData,
2381 }
2382 }
2383
2384 /// Force this borrowed reader to be dropped (not recycled) when the borrow
2385 /// ends.
2386 ///
2387 /// Use normal `Drop` for healthy readers: the return path already retires
2388 /// readers whose transport was torn down, or whose pool has been closed.
2389 /// Call this after abandoning work or handling an error where the next
2390 /// borrower must not inherit this connection.
2391 pub fn drop_on_return(&mut self) {
2392 self.must_close = true;
2393 }
2394}
2395
2396#[cfg(feature = "_egress")]
2397impl Debug for BorrowedReader<'_> {
2398 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
2399 // `Reader` is not `Debug`; surface only the handle state.
2400 f.debug_struct("BorrowedReader")
2401 .field("borrowed", &self.reader.is_some())
2402 .field("must_close", &self.must_close)
2403 .finish()
2404 }
2405}
2406
2407#[cfg(feature = "_egress")]
2408impl Deref for BorrowedReader<'_> {
2409 type Target = Reader;
2410
2411 fn deref(&self) -> &Self::Target {
2412 self.reader
2413 .as_ref()
2414 .expect("borrowed reader already returned")
2415 }
2416}
2417
2418#[cfg(feature = "_egress")]
2419impl DerefMut for BorrowedReader<'_> {
2420 fn deref_mut(&mut self) -> &mut Self::Target {
2421 self.reader
2422 .as_mut()
2423 .expect("borrowed reader already returned")
2424 }
2425}
2426
2427#[cfg(feature = "_egress")]
2428impl Drop for BorrowedReader<'_> {
2429 fn drop(&mut self) {
2430 if let Some(reader) = self.reader.take() {
2431 return_reader_to_pool(&self.db.inner, reader, self.must_close);
2432 }
2433 }
2434}
2435
2436/// Owned (lifetime-free) variant of a borrowed reader used by the C FFI.
2437///
2438/// Holds an `Arc<DbInner>` for the same reason [`OwnedSender`] does: the
2439/// C ABI can free its `questdb_db*` pointer before dropping outstanding
2440/// reader handles. After pool close, returned readers are dropped instead of
2441/// recycled.
2442///
2443/// `must_close` short-circuits the return path: when set, the reader is
2444/// dropped instead of being returned to the pool. Pool shutdown has the same
2445/// effect. The egress-side
2446/// cursor lifecycle uses this to force-close readers whose underlying
2447/// transport has been torn down by a mid-stream cursor drop.
2448#[cfg(all(feature = "_egress", feature = "ffi-support"))]
2449pub struct OwnedReader {
2450 inner: Arc<DbInner>,
2451 reader: Option<Reader>,
2452 must_close: bool,
2453}
2454
2455#[cfg(all(feature = "_egress", feature = "ffi-support"))]
2456impl OwnedReader {
2457 /// Inspect the wrapped reader without taking ownership.
2458 pub fn get(&self) -> &Reader {
2459 self.reader
2460 .as_ref()
2461 .expect("OwnedReader already returned to the pool")
2462 }
2463
2464 /// Borrow the underlying reader mutably.
2465 pub fn get_mut(&mut self) -> &mut Reader {
2466 self.reader
2467 .as_mut()
2468 .expect("OwnedReader already returned to the pool")
2469 }
2470
2471 /// Mark this reader for must-close: it will be dropped on Drop
2472 /// instead of returned to the pool.
2473 pub fn mark_must_close(&mut self) {
2474 self.must_close = true;
2475 }
2476
2477 /// Take the inner reader, leaving the wrapper inert. Used by the
2478 /// FFI to expose the raw `Reader` to other call sites that don't
2479 /// know about the pool (e.g. monitoring stat getters).
2480 ///
2481 /// After this call, `Drop` no longer decrements the pool's
2482 /// `in_use` counter — the caller has assumed responsibility for
2483 /// either dropping the returned `Reader` into oblivion (e.g.
2484 /// `qwp_reader_close`'s leak-on-active branch) or routing it
2485 /// back to the pool via [`ReaderPoolHandle::return_reader`].
2486 /// Forgetting both permanently burns one pool slot.
2487 pub fn take(mut self) -> Option<Reader> {
2488 self.reader.take()
2489 }
2490}
2491
2492#[cfg(all(feature = "_egress", feature = "ffi-support"))]
2493impl Drop for OwnedReader {
2494 fn drop(&mut self) {
2495 if let Some(reader) = self.reader.take() {
2496 return_reader_to_pool(&self.inner, reader, self.must_close);
2497 }
2498 }
2499}
2500
2501/// Opaque handle to a [`QuestDb`] pool, used by the FFI's
2502/// `reader` wrapper to return readers without exposing
2503/// `DbInner`. Cheap to clone (just bumps the inner `Arc`).
2504#[cfg(all(feature = "_egress", feature = "ffi-support"))]
2505#[derive(Clone)]
2506pub struct ReaderPoolHandle {
2507 inner: Arc<DbInner>,
2508}
2509
2510#[cfg(all(feature = "_egress", feature = "ffi-support"))]
2511impl ReaderPoolHandle {
2512 /// Return a [`Reader`] to the pool it came from. If `must_close`
2513 /// is set the reader is dropped instead of recycled — matching
2514 /// the [`OwnedReader::mark_must_close`] semantics.
2515 pub fn return_reader(&self, reader: Reader, must_close: bool) {
2516 return_reader_to_pool(&self.inner, reader, must_close);
2517 }
2518
2519 /// `true` after the originating pool has been closed.
2520 pub fn pool_closed(&self) -> bool {
2521 self.inner.shutdown.load(Ordering::SeqCst)
2522 }
2523
2524 /// Release the `in_use` slot that was reserved when this reader
2525 /// was borrowed, without returning the `Reader` itself. Used by
2526 /// the FFI leak-on-active path: when a `qwp_reader_close` arrives
2527 /// with a cursor still live, the underlying `Reader` cannot be
2528 /// extracted (UnsafeCell aliasing with the in-flight `&mut Reader`),
2529 /// so it leaks — but the pool's borrow accounting must still drop
2530 /// the slot or a `query_pool_max` slot is permanently burned.
2531 pub fn release_leaked_slot(&self) {
2532 let mut state = lock_reader_state(&self.inner.reader_state);
2533 state.in_use = state.in_use.saturating_sub(1);
2534 }
2535}
2536
2537#[cfg(feature = "_egress")]
2538fn return_reader_to_pool(inner: &Arc<DbInner>, reader: Reader, must_close: bool) {
2539 let must_close = must_close || reader.transport_torn_down();
2540 let mut state = lock_reader_state(&inner.reader_state);
2541 state.in_use = state.in_use.saturating_sub(1);
2542 if !must_close && !inner.shutdown.load(Ordering::SeqCst) {
2543 state.free.push(ReaderPoolEntry {
2544 reader,
2545 last_idle_at: Instant::now(),
2546 });
2547 }
2548 drop(state);
2549 inner.reader_cv.notify_all();
2550}
2551
2552/// Pop a free connection or open a fresh one within `sender_pool_max`; at cap,
2553/// wait up to `acquire_timeout_ms` for a return. Reserves the pool slot under
2554/// one lock so a concurrent return can't race past the cap.
2555/// Recyclability hooks shared by the store-and-forward and direct sender
2556/// pools so `pick_sender_inner` can retire free-list entries that latched
2557/// terminal state while parked instead of lending them out.
2558trait PoolableSender {
2559 fn is_stale(&self) -> bool;
2560 fn drain_for_retire(&mut self, inner: &DbInner);
2561}
2562
2563impl PoolableSender for PooledSenderCore {
2564 fn is_stale(&self) -> bool {
2565 self.must_close()
2566 }
2567
2568 fn drain_for_retire(&mut self, inner: &DbInner) {
2569 drain_sfa_before_drop(inner, self);
2570 }
2571}
2572
2573impl PoolableSender for DirectSenderCore {
2574 fn is_stale(&self) -> bool {
2575 self.must_close()
2576 }
2577
2578 fn drain_for_retire(&mut self, _inner: &DbInner) {}
2579}
2580
2581fn retire_stale_entry<S: PoolableSender>(inner: &Arc<DbInner>, entry: PoolEntry<S>) {
2582 let _release = entry.slot_index.is_some().then_some(SenderSlotRelease {
2583 inner: inner.as_ref(),
2584 slot_index: entry.slot_index,
2585 decrement_in_use: false,
2586 decrement_closing: true,
2587 });
2588 let mut sender = entry.sender;
2589 sender.drain_for_retire(inner);
2590 drop(sender);
2591}
2592
2593fn pick_sender_inner<S: PoolableSender>(
2594 inner: &Arc<DbInner>,
2595 pool: &Mutex<PoolState<S>>,
2596 cv: &Condvar,
2597 sfa: bool,
2598 connect: impl FnOnce(Option<usize>) -> Result<S>,
2599) -> Result<PooledSender<S>> {
2600 let slot = {
2601 let mut state = lock_state(pool);
2602 let mut close_wait_deadline = None;
2603 let mut acquire_deadline = None;
2604 loop {
2605 if inner.shutdown.load(Ordering::SeqCst) {
2606 return Err(error::fmt!(
2607 InvalidApiCall,
2608 "QuestDb pool is closed; cannot borrow sender"
2609 ));
2610 }
2611 if let Some(entry) = state.free.pop() {
2612 if entry.sender.is_stale() {
2613 if entry.slot_index.is_some() {
2614 state.closing += 1;
2615 }
2616 drop(state);
2617 retire_stale_entry(inner, entry);
2618 state = lock_state(pool);
2619 continue;
2620 }
2621 state.in_use += 1;
2622 drop(state);
2623 return Ok(PooledSender {
2624 sender: entry.sender,
2625 slot_index: entry.slot_index,
2626 });
2627 }
2628 if state.reserved_total() < inner.sender_pool_max {
2629 break;
2630 }
2631 let wait_timeout = inner.connector.close_flush_timeout();
2632 if sfa
2633 && inner.sf_disk
2634 && state.closing > 0
2635 && let Some(wait_for) = remaining_wait(&mut close_wait_deadline, wait_timeout)
2636 {
2637 let (next_state, _) = match cv.wait_timeout(state, wait_for) {
2638 Ok((guard, result)) => (guard, result),
2639 Err(poisoned) => poisoned.into_inner(),
2640 };
2641 state = next_state;
2642 continue;
2643 }
2644 if let Some(wait_for) = remaining_wait(&mut acquire_deadline, inner.acquire_timeout) {
2645 let (next_state, _) = match cv.wait_timeout(state, wait_for) {
2646 Ok((guard, result)) => (guard, result),
2647 Err(poisoned) => poisoned.into_inner(),
2648 };
2649 state = next_state;
2650 continue;
2651 }
2652 return Err(error::fmt!(
2653 InvalidApiCall,
2654 "Connection pool exhausted: {} sender(s) in use at the \
2655 sender_pool_max cap of {} after waiting acquire_timeout_ms={}. \
2656 Drop a borrowed sender, or raise sender_pool_max / \
2657 acquire_timeout_ms.",
2658 state.in_use,
2659 inner.sender_pool_max,
2660 inner.acquire_timeout.as_millis()
2661 ));
2662 }
2663 let slot_index = state.allocate_slot_index();
2664 debug_assert_eq!(slot_index.is_some(), sfa && inner.sf_disk);
2665 state.in_use += 1;
2666 InUseSlot {
2667 state: pool,
2668 cv,
2669 slot_index,
2670 armed: true,
2671 }
2672 };
2673 let sender = connect(slot.slot_index)?;
2674 let slot_index = slot.slot_index;
2675 slot.commit();
2676 Ok(PooledSender { sender, slot_index })
2677}
2678
2679fn pick_sfa_sender(inner: &Arc<DbInner>) -> Result<PooledSender<PooledSenderCore>> {
2680 let mut picked = pick_sender_inner(inner, &inner.state, &inner.cv, true, |slot_index| {
2681 connect_sfa_pool(inner, slot_index)
2682 })?;
2683 picked.sender.rebase_lease_observation();
2684 Ok(picked)
2685}
2686
2687fn pick_direct_sender(inner: &Arc<DbInner>) -> Result<PooledSender<DirectSenderCore>> {
2688 pick_sender_inner(
2689 inner,
2690 &inner.direct_state,
2691 &inner.direct_cv,
2692 false,
2693 |_slot_index| {
2694 let conn = connect_conn_pool(inner)?;
2695 Ok(DirectSenderCore::new(
2696 conn,
2697 crate::ingress::SymbolGlobalDict::new(),
2698 crate::ingress::column_sender::encoder::EncodeScratch::new(),
2699 false,
2700 ))
2701 },
2702 )
2703}
2704
2705/// Java-parity eager startup: open ingest senders until the pool holds
2706/// `sender_pool_min` and readers until it holds `query_pool_min`, honoring
2707/// an explicitly set `initial_connect_retry`. Runs BEFORE recovery pre-open,
2708/// so every warm sender performs a real foreground connect — adopting dirty
2709/// disk slots (and replaying them) along the way via the borrow path's
2710/// recovery candidates. All warm borrows are held at once so each opens a
2711/// distinct connection, then returned to the free lists; on the first
2712/// failure the already-opened connections are returned and the error
2713/// propagates to `connect()`.
2714fn prewarm_min_connections(db: &QuestDb) -> Result<()> {
2715 let inner = &db.inner;
2716 let mut warm = Vec::new();
2717 let mut outcome: Result<()> = Ok(());
2718 for _ in 0..inner.sender_pool_min {
2719 match pick_sfa_sender(inner) {
2720 Ok(sender) => warm.push(sender),
2721 Err(err) => {
2722 outcome = Err(err);
2723 break;
2724 }
2725 }
2726 }
2727 for picked in warm {
2728 return_sfa_to_pool(inner, picked.sender, picked.slot_index);
2729 }
2730 outcome?;
2731 #[cfg(feature = "_egress")]
2732 {
2733 let mut warm = Vec::new();
2734 let mut outcome: Result<()> = Ok(());
2735 for _ in 0..inner.query_pool_min {
2736 match db.pick_reader() {
2737 Ok(reader) => warm.push(reader),
2738 Err(err) => {
2739 outcome = Err(err);
2740 break;
2741 }
2742 }
2743 }
2744 for reader in warm {
2745 return_reader_to_pool(inner, reader, false);
2746 }
2747 outcome?;
2748 }
2749 Ok(())
2750}
2751
2752fn connect_sfa_pool(inner: &Arc<DbInner>, slot_index: Option<usize>) -> Result<PooledSenderCore> {
2753 // The connector already carries the pool's resolved initial-connect mode.
2754 connect_sfa_pool_with_recovery_candidates(
2755 inner,
2756 slot_index,
2757 &inner.out_of_range_recovery_candidates,
2758 false,
2759 )
2760}
2761
2762fn connect_sfa_pool_with_recovery_candidates(
2763 inner: &Arc<DbInner>,
2764 slot_index: Option<usize>,
2765 recovery_candidates: &[PathBuf],
2766 force_async_initial_connect: bool,
2767) -> Result<PooledSenderCore> {
2768 let sender_id = slot_index.map(|index| managed_slot_id(&inner.slot_base_id, index));
2769 let state = inner
2770 .connector
2771 .connect_sfa_background_with_pool_slot(
2772 sender_id.as_deref(),
2773 inner.managed_slot_exclusion.as_slice(),
2774 recovery_candidates,
2775 Arc::clone(&inner.conn_events),
2776 Arc::clone(&inner.rejections),
2777 force_async_initial_connect,
2778 )
2779 .map_err(|err| {
2780 crate::Error::new(
2781 err.code(),
2782 format!("Failed to open store-and-forward sender: {}", err.msg()),
2783 )
2784 })?;
2785 PooledSenderCore::new_store_and_forward(
2786 state,
2787 inner.connector.max_buf_size(),
2788 inner.connector.request_durable_ack(),
2789 inner.connector.request_timeout(),
2790 )
2791}
2792
2793/// Re-acquire a live connection within `deadline`, retrying with the pool's
2794/// reconnect backoff: a failed pick (every endpoint role-rejecting while the
2795/// cluster elects a primary, or a transient transport error) backs off and
2796/// retries; `AuthError` / `ProtocolVersionError` and deadline exhaustion are
2797/// terminal.
2798#[cfg(feature = "ffi-support")]
2799fn reconnect_pick<S>(
2800 inner: &Arc<DbInner>,
2801 deadline: Option<Instant>,
2802 mut pick: impl FnMut(&Arc<DbInner>) -> Result<PooledSender<S>>,
2803) -> Result<PooledSender<S>> {
2804 let policy = inner.connector.reconnect_policy();
2805 let mut backoff = policy.initial_backoff();
2806 loop {
2807 match pick(inner) {
2808 Ok(cs) => return Ok(cs),
2809 Err(e) if reconnect_error_is_terminal(&e) || reconnect_deadline_expired(deadline) => {
2810 return Err(e);
2811 }
2812 Err(e) => {
2813 let (sleep_for, next) = reconnect_backoff_step(
2814 &e,
2815 policy.initial_backoff(),
2816 policy.max_backoff(),
2817 backoff,
2818 );
2819 sleep_until_deadline(sleep_for, deadline);
2820 backoff = next;
2821 }
2822 }
2823 }
2824}
2825
2826#[cfg(any(
2827 feature = "polars-ingress",
2828 feature = "polars-egress",
2829 feature = "ffi-support"
2830))]
2831pub(crate) fn reconnect_deadline_expired(deadline: Option<Instant>) -> bool {
2832 deadline.is_some_and(|d| Instant::now() >= d)
2833}
2834
2835fn remaining_wait(deadline: &mut Option<Instant>, timeout: Duration) -> Option<Duration> {
2836 if timeout.is_zero() {
2837 return None;
2838 }
2839 let now = Instant::now();
2840 let deadline = deadline.get_or_insert_with(|| now.checked_add(timeout).unwrap_or(now));
2841 let remaining = deadline.saturating_duration_since(now);
2842 if remaining.is_zero() {
2843 None
2844 } else {
2845 Some(remaining)
2846 }
2847}
2848
2849#[cfg(any(
2850 feature = "polars-ingress",
2851 feature = "polars-egress",
2852 feature = "ffi-support"
2853))]
2854fn sleep_until_deadline(sleep_for: Duration, deadline: Option<Instant>) {
2855 let d = match deadline {
2856 Some(dl) => sleep_for.min(dl.saturating_duration_since(Instant::now())),
2857 None => sleep_for,
2858 };
2859 if !d.is_zero() {
2860 thread::sleep(d);
2861 }
2862}
2863
2864/// Open one direct connection through the live pool's `connector`. The shared
2865/// health tracker is locked only per tracker operation (pick/claim/record),
2866/// never across the
2867/// blocking TCP+TLS+WS-upgrade handshake — so concurrent cold-start borrows do
2868/// not serialize end-to-end, and dead-sender returns that also grab
2869/// `inner.health` (via [`record_sender_transport_failure`]) are not stalled
2870/// behind one slow / black-holed connect.
2871fn connect_conn_pool(inner: &Arc<DbInner>) -> Result<ColumnConn> {
2872 let raw: RawQwpWsRoundStream = inner
2873 .connector
2874 .connect_round_pooled(&inner.health, Some(inner.conn_events.as_ref()))?;
2875 ColumnConn::from_round_stream(raw)
2876}
2877
2878/// Best-effort commit of un-sync'd deferred frames on drop, so the natural
2879/// `flush()`-loop-then-drop path doesn't silently lose data. Commits at the
2880/// pool's default ack level so a `request_durable_ack=on` pool still waits for
2881/// the durability ACK instead of silently downgrading to `Ok`. On failure the
2882/// connection is latched `must_close` so the next borrower can't commit these
2883/// frames under a foreign table.
2884///
2885/// Gated on [`can_drain_in_flight`](DirectSenderCore::can_drain_in_flight), not
2886/// `!must_close()`: a connection retired for a **full symbol dictionary**
2887/// (`SymbolDictFull`) is `spent` but its transport is healthy, so its deferred
2888/// tail — frames the caller already flushed, referencing already-interned
2889/// symbols — is committed here rather than discarded. A symbol-less commit
2890/// interns nothing, so the full dictionary does not block it. Only a hard latch
2891/// (transport death, or a prior failed commit) skips the attempt.
2892fn commit_in_flight_on_drop(request_durable_ack: bool, sender: &mut DirectSenderCore) {
2893 if sender.in_flight() == 0 {
2894 return;
2895 }
2896 let ack = if request_durable_ack {
2897 AckLevel::Durable
2898 } else {
2899 AckLevel::Ok
2900 };
2901 let committed = sender.can_drain_in_flight() && sender.sync(ack).is_ok();
2902 if !committed {
2903 log::warn!(
2904 "direct sender dropped with un-sync'd deferred frame(s) that could \
2905 not be committed; their data is discarded. Call sync() (or \
2906 flush_and_wait() on the final chunk) before the handle is dropped."
2907 );
2908 sender.mark_must_close();
2909 }
2910}
2911
2912/// Best-effort delivery of a store-and-forward connection's queued frames just
2913/// before it is dropped (not recycled) — on pool shutdown or a `must_close`
2914/// return. While a connection is parked in the free list its background runner
2915/// keeps delivering, but dropping it stops the runner, so we give the queue a
2916/// bounded window (the configured `close_flush_timeout`) to finish. On timeout
2917/// or a terminal transport the undelivered frames are discarded with a warning,
2918/// mirroring [`commit_in_flight_on_drop`] for the direct backend.
2919fn drain_sfa_before_drop(inner: &DbInner, sender: &mut PooledSenderCore) {
2920 let timeout = inner.connector.close_flush_timeout();
2921 if timeout.is_zero() {
2922 return;
2923 }
2924 let durable = inner.connector.request_durable_ack();
2925 if sender.sfa_fully_delivered(durable) {
2926 return;
2927 }
2928 sender.begin_close();
2929 if let Err(err) = sender.drain_to_deadline(Instant::now().checked_add(timeout)) {
2930 log::warn!(
2931 "store-and-forward sender dropped with frame(s) that could \
2932 not be delivered within close_flush_timeout; their data is \
2933 discarded. Call wait() before closing the pool, or set sf_dir for \
2934 crash-durable persistence. Cause: {err}"
2935 );
2936 }
2937}
2938
2939/// Batched [`drain_sfa_before_drop`] for the connections retired together when
2940/// the pool is closed. Every runner is signalled first (non-blocking) so their
2941/// deliveries overlap, then each is awaited under a *single* shared deadline —
2942/// so total close time is roughly one `close_flush_timeout` no matter how many
2943/// connections are draining, instead of the sum.
2944fn drain_sfa_senders_bounded(inner: &DbInner, senders: &mut [PooledSenderCore]) {
2945 let timeout = inner.connector.close_flush_timeout();
2946 if timeout.is_zero() || senders.is_empty() {
2947 return;
2948 }
2949 let durable = inner.connector.request_durable_ack();
2950 for sender in senders.iter() {
2951 sender.begin_close();
2952 }
2953 let deadline = Instant::now().checked_add(timeout);
2954 for sender in senders.iter_mut() {
2955 if sender.sfa_fully_delivered(durable) {
2956 continue;
2957 }
2958 if let Err(err) = sender.drain_to_deadline(deadline) {
2959 log::warn!(
2960 "store-and-forward sender dropped on pool close with \
2961 frame(s) that could not be delivered within close_flush_timeout; \
2962 their data is discarded. Call wait() before close, or set \
2963 sf_dir for crash-durable persistence. Cause: {err}"
2964 );
2965 }
2966 }
2967}
2968
2969fn return_sfa_to_pool(
2970 inner: &Arc<DbInner>,
2971 mut sender: PooledSenderCore,
2972 slot_index: Option<usize>,
2973) {
2974 let must_close = sender.must_close();
2975 let release_slot;
2976 {
2977 let mut state = lock_state(&inner.state);
2978 if !must_close && !inner.shutdown.load(Ordering::SeqCst) {
2979 state.in_use = state.in_use.saturating_sub(1);
2980 state.free.push(PoolEntry {
2981 sender,
2982 slot_index,
2983 last_idle_at: Instant::now(),
2984 });
2985 inner.cv.notify_all();
2986 return;
2987 }
2988 release_slot = slot_index.is_some();
2989 if release_slot {
2990 state.closing += 1;
2991 } else {
2992 state.in_use = state.in_use.saturating_sub(1);
2993 }
2994 }
2995 let _release = release_slot.then_some(SenderSlotRelease {
2996 inner: inner.as_ref(),
2997 slot_index,
2998 decrement_in_use: true,
2999 decrement_closing: true,
3000 });
3001 // Not recycling: this connection and its background runner are about to be
3002 // dropped, so drain its queue first (bounded, outside the pool lock).
3003 drain_sfa_before_drop(inner, &mut sender);
3004 drop(sender);
3005}
3006
3007fn return_direct_to_pool(inner: &Arc<DbInner>, sender: DirectSenderCore) {
3008 let must_close = sender.must_close();
3009 record_sender_transport_failure(inner, &sender);
3010 {
3011 let mut state = lock_state(&inner.direct_state);
3012 state.in_use = state.in_use.saturating_sub(1);
3013 if !must_close && !inner.shutdown.load(Ordering::SeqCst) {
3014 state.free.push(PoolEntry {
3015 sender,
3016 slot_index: None,
3017 last_idle_at: Instant::now(),
3018 });
3019 }
3020 }
3021 inner.direct_cv.notify_all();
3022}
3023
3024fn finish_replaced_sender(inner: &Arc<DbInner>, sender: DirectSenderCore) {
3025 let must_close = sender.must_close();
3026 record_sender_transport_failure(inner, &sender);
3027 {
3028 let mut state = lock_state(&inner.direct_state);
3029 if !must_close
3030 && !inner.shutdown.load(Ordering::SeqCst)
3031 && state.total() < inner.sender_pool_max
3032 {
3033 state.free.push(PoolEntry {
3034 sender,
3035 slot_index: None,
3036 last_idle_at: Instant::now(),
3037 });
3038 }
3039 }
3040 inner.direct_cv.notify_all();
3041}
3042
3043fn record_sender_transport_failure(inner: &Arc<DbInner>, sender: &DirectSenderCore) {
3044 if sender.transport_dead() {
3045 let idx = sender.endpoint_idx();
3046 lock_health(&inner.health)
3047 .record_mid_stream_failure(idx, Some(ReconnectReason::RetryableFailure));
3048 if let Some(endpoint) = inner.connector.endpoint(idx) {
3049 inner
3050 .conn_events
3051 .disconnected(&endpoint.host, &endpoint.port);
3052 }
3053 }
3054}
3055
3056fn spawn_reaper(inner: Arc<DbInner>) -> std::io::Result<JoinHandle<()>> {
3057 let tick = reaper_tick(inner.idle_timeout);
3058 thread::Builder::new()
3059 .name("questdb-ingress-pool-reaper".to_string())
3060 .spawn(move || reaper_loop(inner, tick))
3061}
3062
3063fn reaper_tick(idle_timeout: Duration) -> Duration {
3064 let twelfth = idle_timeout / 12;
3065 if twelfth > REAPER_MIN_TICK {
3066 twelfth
3067 } else {
3068 REAPER_MIN_TICK
3069 }
3070}
3071
3072fn reaper_loop(inner: Arc<DbInner>, tick: Duration) {
3073 loop {
3074 // Check shutdown WHILE holding the lock so a concurrent Drop's
3075 // notify-under-lock is never lost: Drop sets `shutdown` then
3076 // acquires the same lock to notify, so either we observe
3077 // `shutdown=true` before sleeping or we are sleeping when the
3078 // notify arrives.
3079 let state = lock_state(&inner.state);
3080 if inner.shutdown.load(Ordering::SeqCst) {
3081 break;
3082 }
3083 let (state, _) = inner
3084 .cv
3085 .wait_timeout(state, tick)
3086 .unwrap_or_else(|e| e.into_inner());
3087 if inner.shutdown.load(Ordering::SeqCst) {
3088 break;
3089 }
3090 drop(state);
3091 reap_idle_inner(&inner);
3092 }
3093}
3094
3095fn reap_idle_inner(inner: &DbInner) -> usize {
3096 let mut dropped = reap_idle_senders(inner);
3097 dropped += reap_idle_direct_senders(inner);
3098 #[cfg(feature = "_egress")]
3099 {
3100 dropped += reap_idle_readers(inner);
3101 }
3102 dropped
3103}
3104
3105fn drain_idle_inner(inner: &DbInner) -> usize {
3106 let mut dropped = drain_idle_senders(inner);
3107 dropped += drain_idle_direct_senders(inner);
3108 #[cfg(feature = "_egress")]
3109 {
3110 dropped += drain_idle_readers(inner);
3111 }
3112 dropped
3113}
3114
3115fn drain_idle_senders(inner: &DbInner) -> usize {
3116 let mut to_drop: Vec<PooledSenderCore> = {
3117 let mut state = lock_state(&inner.state);
3118 state.free.drain(..).map(|entry| entry.sender).collect()
3119 };
3120 let dropped = to_drop.len();
3121 // The Main pool is store-and-forward: deliver each connection's queued
3122 // frames (bounded by close_flush_timeout, shared across all of them) before
3123 // the runners are stopped on drop. Done outside the pool lock.
3124 drain_sfa_senders_bounded(inner, &mut to_drop);
3125 drop(to_drop);
3126 dropped
3127}
3128
3129fn drain_idle_direct_senders(inner: &DbInner) -> usize {
3130 let to_drop: Vec<DirectSenderCore> = {
3131 let mut state = lock_state(&inner.direct_state);
3132 state.free.drain(..).map(|entry| entry.sender).collect()
3133 };
3134 let dropped = to_drop.len();
3135 drop(to_drop);
3136 dropped
3137}
3138
3139#[cfg(feature = "_egress")]
3140fn drain_idle_readers(inner: &DbInner) -> usize {
3141 let to_drop: Vec<Reader> = {
3142 let mut state = lock_reader_state(&inner.reader_state);
3143 state.free.drain(..).map(|entry| entry.reader).collect()
3144 };
3145 let dropped = to_drop.len();
3146 drop(to_drop);
3147 dropped
3148}
3149
3150fn reap_idle_senders(inner: &DbInner) -> usize {
3151 let durable = inner.connector.request_durable_ack();
3152 let mut dropped = 0;
3153 while let Some((sender, slot_index)) = take_reapable_column_sender(inner, durable) {
3154 let _release = slot_index.is_some().then_some(SenderSlotRelease {
3155 inner,
3156 slot_index,
3157 decrement_in_use: false,
3158 decrement_closing: true,
3159 });
3160 drop(sender);
3161 dropped += 1;
3162 }
3163 dropped
3164}
3165
3166fn take_reapable_column_sender(
3167 inner: &DbInner,
3168 durable: bool,
3169) -> Option<(PooledSenderCore, Option<usize>)> {
3170 let mut state = lock_state(&inner.state);
3171 let now = Instant::now();
3172 // Free-list is oldest at front, newest at back (push on return /
3173 // pop on borrow). We must protect `total() >= sender_pool_min` after the
3174 // drop, so we only remove an entry if total stays above the floor.
3175 let mut i = 0;
3176 while i < state.free.len() {
3177 if state.total() <= inner.sender_pool_min {
3178 return None;
3179 }
3180 let idle_for = now.saturating_duration_since(state.free[i].last_idle_at);
3181 // Never evict a connection whose store-and-forward queue still holds
3182 // undelivered frames: its background runner is still delivering, and
3183 // dropping it now would lose that data. It becomes reapable once the
3184 // runner drains it (or the transport goes terminal). `sfa_fully_delivered`
3185 // is a lock-free progress read in the healthy case.
3186 if idle_for > inner.idle_timeout && state.free[i].sender.sfa_fully_delivered(durable) {
3187 let entry = state.free.remove(i);
3188 if entry.slot_index.is_some() {
3189 state.closing += 1;
3190 }
3191 return Some((entry.sender, entry.slot_index));
3192 }
3193 i += 1;
3194 }
3195 None
3196}
3197
3198fn reap_idle_direct_senders(inner: &DbInner) -> usize {
3199 // Direct pool is lazy-init (no pre-population at connect), so there is no
3200 // warm-min floor to preserve — reap any sender parked longer than the idle
3201 // timeout. The direct pool has no configured warm minimum.
3202 let to_drop: Vec<DirectSenderCore> = {
3203 let mut state = lock_state(&inner.direct_state);
3204 let mut to_drop = Vec::new();
3205 let now = Instant::now();
3206 let mut i = 0;
3207 while i < state.free.len() {
3208 let idle_for = now.saturating_duration_since(state.free[i].last_idle_at);
3209 if idle_for > inner.idle_timeout {
3210 let entry = state.free.remove(i);
3211 to_drop.push(entry.sender);
3212 } else {
3213 i += 1;
3214 }
3215 }
3216 to_drop
3217 };
3218 let dropped = to_drop.len();
3219 drop(to_drop);
3220 dropped
3221}
3222
3223#[cfg(feature = "_egress")]
3224fn reap_idle_readers(inner: &DbInner) -> usize {
3225 // `query_pool_min` readers are pre-opened at connect by default
3226 // (none under lazy_connect, where the pool fills on first borrow);
3227 // either way `query_pool_min` is the reaper's floor here.
3228 let to_drop: Vec<Reader> = {
3229 let mut state = lock_reader_state(&inner.reader_state);
3230 let mut to_drop = Vec::new();
3231 let now = Instant::now();
3232 let mut i = 0;
3233 while i < state.free.len() {
3234 if state.total() <= inner.query_pool_min {
3235 break;
3236 }
3237 let idle_for = now.saturating_duration_since(state.free[i].last_idle_at);
3238 if idle_for > inner.idle_timeout {
3239 let entry = state.free.remove(i);
3240 to_drop.push(entry.reader);
3241 } else {
3242 i += 1;
3243 }
3244 }
3245 to_drop
3246 };
3247 let dropped = to_drop.len();
3248 drop(to_drop);
3249 dropped
3250}
3251
3252const _: fn() = || {
3253 fn assert_send_sync<T: Send + Sync>() {}
3254 assert_send_sync::<QuestDb>();
3255 #[cfg(feature = "ffi-support")]
3256 {
3257 fn assert_send<T: Send>() {}
3258 assert_send::<OwnedSender>();
3259 assert_send::<OwnedDirectColumnSender>();
3260 }
3261};
3262
3263const _: fn() = || {
3264 trait AmbiguousIfSend<A> {
3265 fn _disambiguate() {}
3266 }
3267 impl<T: ?Sized> AmbiguousIfSend<()> for T {}
3268 impl<T: ?Sized + Send> AmbiguousIfSend<u8> for T {}
3269 fn assert_not_send<T: ?Sized>() {
3270 let _: fn() = <T as AmbiguousIfSend<_>>::_disambiguate;
3271 }
3272 assert_not_send::<BorrowedSender<'_>>();
3273 assert_not_send::<BorrowedDirectColumnSender<'_>>();
3274 #[cfg(feature = "_egress")]
3275 assert_not_send::<BorrowedReader<'_>>();
3276 assert_not_send::<crate::ingress::column_sender::Chunk<'_>>();
3277};
3278
3279const _: fn() = || {
3280 trait AmbiguousIfSync<A> {
3281 fn _disambiguate() {}
3282 }
3283 impl<T: ?Sized> AmbiguousIfSync<()> for T {}
3284 impl<T: ?Sized + Sync> AmbiguousIfSync<u8> for T {}
3285 fn assert_not_sync<T: ?Sized>() {
3286 let _: fn() = <T as AmbiguousIfSync<_>>::_disambiguate;
3287 }
3288 assert_not_sync::<BorrowedSender<'_>>();
3289 assert_not_sync::<BorrowedDirectColumnSender<'_>>();
3290 #[cfg(feature = "_egress")]
3291 assert_not_sync::<BorrowedReader<'_>>();
3292 assert_not_sync::<crate::ingress::column_sender::Chunk<'_>>();
3293};
3294
3295#[cfg(test)]
3296mod tests {
3297 use std::fs;
3298
3299 use tempfile::TempDir;
3300
3301 use super::{SlotReservations, managed_slot_recovery_scan_from};
3302
3303 fn dirty_slot(root: &std::path::Path, name: &str) {
3304 let slot = root.join(name);
3305 fs::create_dir(&slot).unwrap();
3306 fs::write(slot.join("sf-0.sfa"), b"queued").unwrap();
3307 }
3308
3309 #[test]
3310 fn managed_slot_recovery_candidates_exclude_live_pool_range() {
3311 let temp = TempDir::new().unwrap();
3312 dirty_slot(temp.path(), "default-ingest-0");
3313 dirty_slot(temp.path(), "default-ingest-1");
3314 dirty_slot(temp.path(), "default-ingest-2");
3315 dirty_slot(temp.path(), &format!("default-{}-2", "col"));
3316 dirty_slot(temp.path(), &format!("default-{}-2", "row"));
3317
3318 let mut actual = managed_slot_recovery_scan_from(temp.path(), "default", 2).out_of_range;
3319 actual.sort();
3320
3321 assert_eq!(actual, vec![temp.path().join("default-ingest-2")]);
3322 }
3323
3324 #[test]
3325 fn slot_reservations_reserve_specific_index() {
3326 let mut disk = SlotReservations::with_disk_slots(2);
3327 assert!(disk.reserve(1));
3328 assert!(!disk.reserve(1), "double-reserve must fail");
3329 assert!(!disk.reserve(2), "out-of-range reserve must fail");
3330 disk.free(Some(1));
3331 assert!(disk.reserve(1), "freed slot can be reserved again");
3332
3333 let mut in_memory = SlotReservations::default();
3334 assert!(!in_memory.reserve(0));
3335 }
3336}