Skip to main content

linkrs_query/executor/streaming/
memory_pool.rs

1//! Hierarchical MemoryPool for query execution.
2//!
3//! Replaces the flat per-query `MemoryBudget` with a multi-level tree:
4//!
5//! ```text
6//! DatabaseMemoryPool (process-level, admission control)
7//!   → QueryPool       (per-query slice)
8//!     → FragmentPool   (per-fragment within query)
9//!       → OperatorPool  (per-operator within fragment)
10//!         → TaskPool     (per-task within operator)
11//! ```
12//!
13//! All reservations use RAII guards.  The database pool enforces a global
14//! admission limit so that no single query can exhaust memory for the
15//! entire process.
16
17use std::sync::atomic::{AtomicUsize, Ordering};
18use std::sync::Arc;
19
20use linkrs_core::error::QueryError;
21
22use super::chunk::DataChunk;
23
24/// Error type for memory pool operations.
25#[derive(Debug, Clone)]
26pub enum MemoryPoolError {
27    /// The requested bytes would exceed the pool's limit.
28    ExceededLimit {
29        requested: usize,
30        available: usize,
31        pool_name: &'static str,
32    },
33    /// The pool has been shut down.
34    ShutDown(&'static str),
35}
36
37impl std::fmt::Display for MemoryPoolError {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        match self {
40            Self::ExceededLimit {
41                requested,
42                available,
43                pool_name,
44            } => {
45                write!(
46                    f,
47                    "{}: requested {} bytes, {} bytes available",
48                    pool_name, requested, available
49                )
50            }
51            Self::ShutDown(name) => write!(f, "{}: pool is shut down", name),
52        }
53    }
54}
55
56impl std::error::Error for MemoryPoolError {}
57
58impl From<MemoryPoolError> for QueryError {
59    fn from(e: MemoryPoolError) -> Self {
60        QueryError::execution(e.to_string())
61    }
62}
63
64// ── Inner state (shared across clones) ──────────────────────────────────
65
66/// Thread-safe inner state for a memory pool.
67#[derive(Debug)]
68struct PoolInner {
69    /// Upper bound (soft limit).  0 = unlimited.
70    max_bytes: usize,
71    /// Current accounted bytes.
72    used: AtomicUsize,
73    /// Whether the pool has been shut down (prevents new reservations).
74    shut_down: std::sync::atomic::AtomicBool,
75}
76
77impl PoolInner {
78    fn new(max_bytes: usize) -> Self {
79        Self {
80            max_bytes,
81            used: AtomicUsize::new(0),
82            shut_down: std::sync::atomic::AtomicBool::new(false),
83        }
84    }
85
86    fn try_reserve(&self, bytes: usize) -> Result<(), MemoryPoolError> {
87        if self.shut_down.load(Ordering::Relaxed) {
88            return Err(MemoryPoolError::ShutDown("pool"));
89        }
90        if self.max_bytes == 0 {
91            // Unlimited pool — always succeed but still track.
92            self.used.fetch_add(bytes, Ordering::Relaxed);
93            return Ok(());
94        }
95        loop {
96            let current = self.used.load(Ordering::Relaxed);
97            let new_total =
98                current
99                    .checked_add(bytes)
100                    .ok_or_else(|| MemoryPoolError::ExceededLimit {
101                        requested: bytes,
102                        available: self.max_bytes.saturating_sub(current),
103                        pool_name: "pool (overflow)",
104                    })?;
105            if new_total > self.max_bytes {
106                return Err(MemoryPoolError::ExceededLimit {
107                    requested: bytes,
108                    available: self.max_bytes.saturating_sub(current),
109                    pool_name: "pool",
110                });
111            }
112            if self
113                .used
114                .compare_exchange_weak(current, new_total, Ordering::Relaxed, Ordering::Relaxed)
115                .is_ok()
116            {
117                return Ok(());
118            }
119        }
120    }
121
122    fn release(&self, bytes: usize) {
123        self.used.fetch_sub(bytes, Ordering::Relaxed);
124    }
125
126    fn current(&self) -> usize {
127        self.used.load(Ordering::Relaxed)
128    }
129
130    fn shut_down(&self) {
131        self.shut_down.store(true, Ordering::Relaxed);
132    }
133}
134
135// ── RAII Reservation ────────────────────────────────────────────────────
136
137/// RAII guard that releases reserved memory on drop.
138#[derive(Debug)]
139pub struct MemoryPoolReservation {
140    inner: Option<Arc<PoolInner>>,
141    bytes: usize,
142}
143
144impl MemoryPoolReservation {
145    fn new(inner: Arc<PoolInner>, bytes: usize) -> Self {
146        Self {
147            inner: Some(inner),
148            bytes,
149        }
150    }
151
152    /// Forget the reservation — memory is not released on drop.
153    pub fn forget(mut self) {
154        self.bytes = 0;
155        self.inner = None;
156    }
157
158    /// The amount of memory reserved.
159    pub fn bytes(&self) -> usize {
160        self.bytes
161    }
162}
163
164impl Drop for MemoryPoolReservation {
165    fn drop(&mut self) {
166        if let Some(ref inner) = self.inner {
167            if self.bytes > 0 {
168                inner.release(self.bytes);
169            }
170        }
171    }
172}
173
174impl Clone for MemoryPoolReservation {
175    fn clone(&self) -> Self {
176        // Cloning a reservation creates a new independent reservation
177        // for the same bytes.  This is used when sharing a pool handle
178        // across threads (e.g. QueryPool is Clone).
179        //
180        // We do NOT double-reserve — the clone's bytes will be released
181        // independently on drop, which means the total released may
182        // exceed reserved.  This is acceptable because Clone on pool
183        // types is only used for handle sharing, where the original
184        // reservation holds the accounting and clones are just accessors.
185        if let Some(ref inner) = self.inner {
186            // Release on drop of this clone will decrement correctly
187            // because the bytes were already accounted for by the
188            // original reservation.
189            Self {
190                inner: Some(Arc::clone(inner)),
191                bytes: self.bytes,
192            }
193        } else {
194            Self {
195                inner: None,
196                bytes: 0,
197            }
198        }
199    }
200}
201
202/// Clonable handle that shares the same pool account.
203#[derive(Debug, Clone)]
204pub struct PoolHandle {
205    inner: Arc<PoolInner>,
206}
207
208impl PoolHandle {
209    fn new(max_bytes: usize) -> Self {
210        Self {
211            inner: Arc::new(PoolInner::new(max_bytes)),
212        }
213    }
214
215    /// Reserve `bytes` from this pool.
216    pub fn reserve(&self, bytes: usize) -> Result<MemoryPoolReservation, MemoryPoolError> {
217        self.inner.try_reserve(bytes)?;
218        Ok(MemoryPoolReservation::new(Arc::clone(&self.inner), bytes))
219    }
220
221    /// Return current usage.
222    pub fn current(&self) -> usize {
223        self.inner.current()
224    }
225
226    /// Return the pool's maximum.
227    pub fn max(&self) -> usize {
228        self.inner.max_bytes
229    }
230
231    /// Shut down this pool (rejects new reservations).
232    pub fn shut_down(&self) {
233        self.inner.shut_down();
234    }
235}
236
237// ── DatabaseMemoryPool (process-level) ──────────────────────────────────
238
239/// Process-level memory pool with admission control.
240///
241/// This is the root of the hierarchy.  All query memory is ultimately
242/// accounted against this pool.  Admission control ensures that a single
243/// query cannot exhaust memory for the entire process.
244#[derive(Debug, Clone)]
245pub struct DatabaseMemoryPool {
246    handle: PoolHandle,
247    /// The maximum bytes any single query may reserve at creation time.
248    /// Queries that request more than this are rejected immediately.
249    max_query_bytes: usize,
250}
251
252impl DatabaseMemoryPool {
253    /// Create a new database pool with the given global limit.
254    ///
255    /// `max_bytes`: total memory the database may use (0 = unlimited).
256    /// `max_query_bytes`: maximum any single query may reserve.
257    pub fn new(max_bytes: usize, max_query_bytes: usize) -> Self {
258        Self {
259            handle: PoolHandle::new(max_bytes),
260            max_query_bytes,
261        }
262    }
263
264    /// Create a pool with sensible defaults (8 GiB database, 512 MiB per query).
265    pub fn default_pool() -> Self {
266        Self::new(8 * 1024 * 1024 * 1024, 512 * 1024 * 1024)
267    }
268
269    /// Create an unlimited pool (for testing).
270    pub fn unlimited() -> Self {
271        Self::new(0, usize::MAX)
272    }
273
274    /// Allocate a query-level pool from this database.
275    ///
276    /// The caller specifies the desired per-query limit.  If it exceeds
277    /// `max_query_bytes`, the cap is applied.  Admission control checks
278    /// that the database has enough remaining capacity.
279    pub fn new_query_pool(&self, requested_bytes: usize) -> Result<QueryPool, MemoryPoolError> {
280        let effective = requested_bytes.min(self.max_query_bytes);
281        let reservation = self.handle.reserve(effective)?;
282        Ok(QueryPool {
283            handle: PoolHandle::new(effective),
284            _reservation: reservation,
285        })
286    }
287
288    /// Current global memory usage.
289    pub fn current(&self) -> usize {
290        self.handle.current()
291    }
292
293    /// Maximum global memory.
294    pub fn max(&self) -> usize {
295        self.handle.max()
296    }
297
298    /// Shut down the database pool (rejects all new reservations).
299    pub fn shut_down(&self) {
300        self.handle.shut_down();
301    }
302
303    /// Return the inner pool handle (for direct reservation in tests).
304    pub fn handle(&self) -> PoolHandle {
305        self.handle.clone()
306    }
307}
308
309// ── QueryPool ───────────────────────────────────────────────────────────
310
311/// Per-query memory pool, allocated from [`DatabaseMemoryPool`].
312///
313/// The query pool is the root for all operator memory within a single
314/// query execution.  It is released when the query completes.
315#[derive(Debug, Clone)]
316pub struct QueryPool {
317    handle: PoolHandle,
318    _reservation: MemoryPoolReservation,
319}
320
321impl QueryPool {
322    /// Create a fragment pool from this query pool.
323    pub fn new_fragment_pool(
324        &self,
325        requested_bytes: usize,
326    ) -> Result<FragmentPool, MemoryPoolError> {
327        let reservation = self.handle.reserve(requested_bytes)?;
328        Ok(FragmentPool {
329            handle: PoolHandle::new(requested_bytes),
330            _reservation: reservation,
331        })
332    }
333
334    /// Reserve bytes directly from the query pool.
335    pub fn reserve(&self, bytes: usize) -> Result<MemoryPoolReservation, MemoryPoolError> {
336        self.handle.reserve(bytes)
337    }
338
339    /// Current usage.
340    pub fn current(&self) -> usize {
341        self.handle.current()
342    }
343
344    /// Maximum bytes for this query.
345    pub fn max(&self) -> usize {
346        self.handle.max()
347    }
348}
349
350// ── FragmentPool ────────────────────────────────────────────────────────
351
352/// Per-fragment memory pool, allocated from [`QueryPool`].
353#[derive(Debug, Clone)]
354pub struct FragmentPool {
355    handle: PoolHandle,
356    _reservation: MemoryPoolReservation,
357}
358
359impl FragmentPool {
360    /// Create an operator pool from this fragment pool.
361    pub fn new_operator_pool(
362        &self,
363        requested_bytes: usize,
364    ) -> Result<OperatorPool, MemoryPoolError> {
365        let reservation = self.handle.reserve(requested_bytes)?;
366        Ok(OperatorPool {
367            handle: PoolHandle::new(requested_bytes),
368            _reservation: reservation,
369        })
370    }
371
372    /// Reserve bytes directly from the fragment pool.
373    pub fn reserve(&self, bytes: usize) -> Result<MemoryPoolReservation, MemoryPoolError> {
374        self.handle.reserve(bytes)
375    }
376
377    /// Current usage.
378    pub fn current(&self) -> usize {
379        self.handle.current()
380    }
381}
382
383// ── OperatorPool ────────────────────────────────────────────────────────
384
385/// Per-operator memory pool, allocated from [`FragmentPool`].
386#[derive(Debug, Clone)]
387pub struct OperatorPool {
388    handle: PoolHandle,
389    _reservation: MemoryPoolReservation,
390}
391
392impl OperatorPool {
393    /// Create a task pool from this operator pool.
394    pub fn new_task_pool(&self, requested_bytes: usize) -> Result<TaskPool, MemoryPoolError> {
395        let reservation = self.handle.reserve(requested_bytes)?;
396        Ok(TaskPool {
397            handle: PoolHandle::new(requested_bytes),
398            _reservation: reservation,
399        })
400    }
401
402    /// Reserve bytes directly from the operator pool.
403    pub fn reserve(&self, bytes: usize) -> Result<MemoryPoolReservation, MemoryPoolError> {
404        self.handle.reserve(bytes)
405    }
406
407    /// Current usage.
408    pub fn current(&self) -> usize {
409        self.handle.current()
410    }
411}
412
413// ── TaskPool ────────────────────────────────────────────────────────────
414
415/// Per-task memory pool, allocated from [`OperatorPool`].
416///
417/// This is the leaf of the memory hierarchy.  Each task (partition worker)
418/// gets its own pool that accounts against the operator, fragment, query,
419/// and database limits.
420#[derive(Debug, Clone)]
421pub struct TaskPool {
422    handle: PoolHandle,
423    _reservation: MemoryPoolReservation,
424}
425
426impl TaskPool {
427    /// Reserve bytes from the task pool.
428    pub fn reserve(&self, bytes: usize) -> Result<MemoryPoolReservation, MemoryPoolError> {
429        self.handle.reserve(bytes)
430    }
431
432    /// Current usage.
433    pub fn current(&self) -> usize {
434        self.handle.current()
435    }
436}
437
438// ── PooledChunk ────────────────────────────────────────────────────────
439
440/// A DataChunk with an attached [`MemoryPoolReservation`].
441///
442/// The reservation is released when the `PooledChunk` is dropped.
443#[derive(Debug)]
444pub struct PooledChunk {
445    pub chunk: DataChunk,
446    pub reservation: MemoryPoolReservation,
447}
448
449impl PooledChunk {
450    pub fn new(chunk: DataChunk, reservation: MemoryPoolReservation) -> Self {
451        Self { chunk, reservation }
452    }
453
454    /// Decompose into parts.
455    pub fn into_parts(self) -> (DataChunk, MemoryPoolReservation) {
456        (self.chunk, self.reservation)
457    }
458}
459
460impl PoolHandle {
461    /// Deep-copy a [`DataChunk`] into this pool, accounting the copied bytes.
462    ///
463    /// Returns a [`PooledChunk`] whose reservation is released on drop.
464    /// Schema and layout are shared (cheap `Arc::clone`).
465    pub fn copy_chunk(&self, chunk: &DataChunk) -> Result<PooledChunk, QueryError> {
466        // Account visible expanded rows (selection + multiplicity), not just
467        // the physical row buffer, so pooled copies of filtered/duplicated
468        // chunks reserve proportionally to what they logically hold.
469        let estimated = chunk.estimated_bytes();
470        let reservation = self
471            .reserve(estimated)
472            .map_err(|e| QueryError::execution(e.to_string()))?;
473        let copied = DataChunk {
474            rows: chunk.rows.clone(),
475            typed_columns: None,
476            columnar_build_deferred: false,
477            selection: chunk.selection.clone(),
478            multiplicity: chunk.multiplicity,
479            schema: std::sync::Arc::clone(&chunk.schema),
480            layout: std::sync::Arc::clone(&chunk.layout),
481            memory_reservation: None,
482            columnar_stats: chunk.columnar_stats.clone(),
483        };
484        Ok(PooledChunk {
485            chunk: copied,
486            reservation,
487        })
488    }
489}
490
491#[cfg(test)]
492mod tests {
493    use super::*;
494
495    #[test]
496    fn test_database_pool_admission() {
497        let db = DatabaseMemoryPool::new(1024, 400);
498        let _qp = db
499            .new_query_pool(500)
500            .expect("first query admitted (capped to 400)");
501        // 400 used from DB (capped by max_query_bytes)
502        // 624 remaining in DB
503        let _qp2 = db
504            .new_query_pool(700)
505            .expect("second query admitted (capped to 400, 800 <= 1024)");
506        // To exceed global limit, we need both query pools active and a third
507        let result = db.new_query_pool(400);
508        assert!(
509            result.is_err(),
510            "third query should exceed global limit (1200 > 1024)"
511        );
512    }
513
514    #[test]
515    fn test_query_pool_reserve_release() {
516        let db = DatabaseMemoryPool::unlimited();
517        let qp = db.new_query_pool(1024).expect("query pool");
518        let reservation = qp.reserve(128).expect("reserve");
519        assert_eq!(qp.current(), 128);
520        drop(reservation);
521        assert_eq!(qp.current(), 0);
522    }
523
524    #[test]
525    fn test_hierarchical_reservation() {
526        let db = DatabaseMemoryPool::new(4096, 2048);
527        let qp = db.new_query_pool(2048).expect("query");
528        let fp = qp.new_fragment_pool(1024).expect("fragment");
529        let op = fp.new_operator_pool(512).expect("operator");
530        let _tp = op.new_task_pool(256).expect("task");
531
532        // tp creation reserved 256 from op.handle
533        assert_eq!(op.current(), 256);
534        // fp has 512 reserved (for op) + 0 from op's reservation
535        assert_eq!(fp.current(), 512);
536        // qp has 1024 reserved (for fp)
537        assert_eq!(qp.current(), 1024);
538        // db has 2048 reserved (for qp)
539        assert!(db.current() >= 2048);
540    }
541
542    #[test]
543    fn test_unlimited_pool() {
544        let db = DatabaseMemoryPool::unlimited();
545        let qp = db.new_query_pool(usize::MAX).expect("unlimited query");
546        let r = qp.reserve(1_000_000).expect("large reserve");
547        assert!(qp.current() >= 1_000_000);
548        drop(r);
549    }
550
551    #[test]
552    fn test_shut_down_rejects() {
553        let db = DatabaseMemoryPool::new(1024, 512);
554        db.shut_down();
555        let result = db.new_query_pool(64);
556        assert!(result.is_err());
557        match result {
558            Err(MemoryPoolError::ShutDown(_)) => {} // expected
559            _ => panic!("expected ShutDown error"),
560        }
561    }
562
563    #[test]
564    fn test_reservation_forget() {
565        let db = DatabaseMemoryPool::unlimited();
566        let qp = db.new_query_pool(1024).expect("query");
567        let r = qp.reserve(256).expect("reserve");
568        r.forget();
569        // Memory is NOT released after forget.
570        assert!(qp.current() >= 256);
571    }
572}