Skip to main content

arrow_buffer/
pool.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! This module contains traits for memory pool traits and an implementation
19//! for tracking memory usage.
20//!
21//! The basic traits are [`MemoryPool`] and [`MemoryReservation`]. And default
22//! implementation of [`MemoryPool`] is [`TrackingMemoryPool`]. Their relationship
23//! is as follows:
24//!
25//! ```text
26//!     (pool tracker)                        (resizable)
27//!  ┌──────────────────┐ fn reserve() ┌─────────────────────────┐
28//!  │ trait MemoryPool │─────────────►│ trait MemoryReservation │
29//!  └──────────────────┘              └─────────────────────────┘
30//! ```
31
32use std::fmt::Debug;
33use std::sync::atomic::{AtomicUsize, Ordering};
34use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
35
36/// A memory reservation within a [`MemoryPool`] that is freed on drop
37pub trait MemoryReservation: Debug + Send + Sync {
38    /// Returns the size of this reservation in bytes.
39    fn size(&self) -> usize;
40
41    /// Resize this reservation to a new size in bytes.
42    fn resize(&mut self, new_size: usize);
43}
44
45/// A pool of memory that can be reserved and released.
46///
47/// This is used to accurately track memory usage when buffers are shared
48/// between multiple arrays or other data structures.
49///
50/// For example, assume we have two arrays that share underlying buffer.
51/// It's hard to tell how much memory is used by them because we can't
52/// tell if the buffer is shared or not.
53///
54/// ```text
55///       Array A           Array B
56///    ┌────────────┐    ┌────────────┐
57///    │ slices...  │    │ slices...  │
58///    │────────────│    │────────────│
59///    │ Arc<Bytes> │    │ Arc<Bytes> │ (shared buffer)
60///    └─────▲──────┘    └───────▲────┘
61///          │                   │
62///          │       Bytes       │
63///          │  ┌─────────────┐  │
64///          │  │   data...   │  │
65///          │  │─────────────│  │
66///          └──│   Memory    │──┘   (tracked with a memory pool)
67///             │ Reservation │
68///             └─────────────┘
69/// ```
70///
71/// With a memory pool, we can count the memory usage by the shared buffer
72/// directly.
73pub trait MemoryPool: Debug + Send + Sync {
74    /// Reserves memory from the pool. Infallible.
75    ///
76    /// Returns a reservation of the requested size.
77    fn reserve(&self, size: usize) -> Box<dyn MemoryReservation>;
78
79    /// Returns the current available memory in the pool.
80    ///
81    /// The pool may be overfilled, so this method might return a negative value.
82    fn available(&self) -> isize;
83
84    /// Returns the current used memory from the pool.
85    fn used(&self) -> usize;
86
87    /// Returns the maximum memory that can be reserved from the pool.
88    fn capacity(&self) -> usize;
89}
90
91/// A simple [`MemoryPool`] that reports the total memory usage
92#[derive(Debug, Default)]
93pub struct TrackingMemoryPool(Arc<AtomicUsize>);
94
95impl TrackingMemoryPool {
96    /// Returns the total allocated size
97    pub fn allocated(&self) -> usize {
98        self.0.load(Ordering::Relaxed)
99    }
100}
101
102impl MemoryPool for TrackingMemoryPool {
103    fn reserve(&self, size: usize) -> Box<dyn MemoryReservation> {
104        self.0.fetch_add(size, Ordering::Relaxed);
105        Box::new(Tracker {
106            size,
107            shared: Arc::clone(&self.0),
108        })
109    }
110
111    fn available(&self) -> isize {
112        isize::MAX - self.used() as isize
113    }
114
115    fn used(&self) -> usize {
116        self.0.load(Ordering::Relaxed)
117    }
118
119    fn capacity(&self) -> usize {
120        usize::MAX
121    }
122}
123
124#[derive(Debug)]
125struct Tracker {
126    size: usize,
127    shared: Arc<AtomicUsize>,
128}
129
130impl Drop for Tracker {
131    fn drop(&mut self) {
132        self.shared.fetch_sub(self.size, Ordering::Relaxed);
133    }
134}
135
136impl MemoryReservation for Tracker {
137    fn size(&self) -> usize {
138        self.size
139    }
140
141    fn resize(&mut self, new: usize) {
142        match self.size < new {
143            true => self.shared.fetch_add(new - self.size, Ordering::Relaxed),
144            false => self.shared.fetch_sub(self.size - new, Ordering::Relaxed),
145        };
146        self.size = new;
147    }
148}
149
150/// Lock a memory reservation, recovering from a poisoned lock.
151///
152/// A poisoned lock only means that some other thread panicked. The reservation it
153/// guards is plain size accounting, so there is no broken invariant to protect, and
154/// recovering it is always preferable to panicking.
155pub(crate) fn lock_reservation(
156    reservation: &Mutex<Option<Box<dyn MemoryReservation>>>,
157) -> MutexGuard<'_, Option<Box<dyn MemoryReservation>>> {
158    reservation.lock().unwrap_or_else(PoisonError::into_inner)
159}
160
161/// This is a wrapper for the reservation so we can standardize on changing
162/// and avoid race conditions in memory accounting
163#[derive(Debug, Default)]
164pub(crate) struct TrackedReservation {
165    reservation: Mutex<Option<Box<dyn MemoryReservation>>>,
166}
167
168impl TrackedReservation {
169    /// Claim memory from a pool, replacing the current reservation (if exists).
170    pub fn claim(&self, pool: &dyn MemoryPool, capacity: usize) {
171        // get the existing reservation
172        let mut guard = lock_reservation(&self.reservation);
173
174        // drop it before we reserve the new one
175        drop(guard.take());
176
177        // reserve the new one
178        *guard = Some(pool.reserve(capacity))
179    }
180
181    /// Resize the memory reservation of this buffer
182    ///
183    /// This is a no-op if this buffer doesn't have a reservation.
184    pub fn resize(&self, new_size: usize) {
185        if let Some(reservation) = lock_reservation(&self.reservation).as_mut() {
186            // Resize the reservation
187            reservation.resize(new_size);
188        }
189    }
190
191    /// Takes ownership of the reservation and returns it in a new `TrackedReservation`
192    pub fn take(&self) -> Self {
193        let reservation = lock_reservation(&self.reservation).take();
194
195        Self {
196            reservation: Mutex::new(reservation),
197        }
198    }
199
200    /// Replaces the current tracked reservation with `other`, consuming it.
201    pub fn replace(&self, other: Self) {
202        // get the owned value out, preventing double lock
203        let reservation = other
204            .reservation
205            .into_inner()
206            .unwrap_or_else(PoisonError::into_inner);
207
208        let mut guard = lock_reservation(&self.reservation);
209
210        // drop the old reservation before installing the new one
211        drop(guard.take());
212        *guard = reservation;
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    #[test]
221    fn test_tracking_memory_pool() {
222        let pool = TrackingMemoryPool::default();
223
224        // Reserve 512 bytes
225        let reservation = pool.reserve(512);
226        assert_eq!(reservation.size(), 512);
227        assert_eq!(pool.used(), 512);
228        assert_eq!(pool.available(), isize::MAX - 512);
229
230        // Reserve another 256 bytes
231        let reservation2 = pool.reserve(256);
232        assert_eq!(reservation2.size(), 256);
233        assert_eq!(pool.used(), 768);
234        assert_eq!(pool.available(), isize::MAX - 768);
235
236        // Test resize to increase
237        let mut reservation_mut = reservation;
238        reservation_mut.resize(600);
239        assert_eq!(reservation_mut.size(), 600);
240        assert_eq!(pool.used(), 856); // 600 + 256
241
242        // Test resize to decrease
243        reservation_mut.resize(400);
244        assert_eq!(reservation_mut.size(), 400);
245        assert_eq!(pool.used(), 656); // 400 + 256
246
247        // Drop the first reservation
248        drop(reservation_mut);
249        assert_eq!(pool.used(), 256);
250
251        // Drop the second reservation
252        drop(reservation2);
253        assert_eq!(pool.used(), 0);
254    }
255
256    /// A [`MemoryPool`] that records the peak usage observed at the instant
257    /// each reservation is taken, letting a single-threaded test witness the
258    /// transient double-count that [`TrackedReservation::claim`] must avoid.
259    #[derive(Debug, Default)]
260    struct PeakPool {
261        inner: TrackingMemoryPool,
262        peak: AtomicUsize,
263    }
264
265    impl MemoryPool for PeakPool {
266        fn reserve(&self, size: usize) -> Box<dyn MemoryReservation> {
267            let reservation = self.inner.reserve(size);
268            self.peak.fetch_max(self.inner.used(), Ordering::Relaxed);
269            reservation
270        }
271
272        fn available(&self) -> isize {
273            self.inner.available()
274        }
275
276        fn used(&self) -> usize {
277            self.inner.used()
278        }
279
280        fn capacity(&self) -> usize {
281            self.inner.capacity()
282        }
283    }
284
285    #[test]
286    fn test_claim_reclaims_before_reserving() {
287        let pool = PeakPool::default();
288        let reservation = TrackedReservation::default();
289
290        // Claim 512 bytes.
291        reservation.claim(&pool, 512);
292        assert_eq!(pool.used(), 512);
293
294        // Re-claim the same amount. The old reservation must be released
295        // before the new one is taken, so usage never transiently doubles
296        // (see #10139).
297        reservation.claim(&pool, 512);
298        assert_eq!(pool.used(), 512);
299        assert_eq!(
300            pool.peak.load(Ordering::Relaxed),
301            512,
302            "claim double-counted memory while reclaiming"
303        );
304    }
305}