Skip to main content

rudb_common/
memory.rs

1//! How much memory a query is allowed to hold.
2
3use std::sync::Arc;
4use std::sync::atomic::{AtomicU64, Ordering};
5
6use crate::error::{Error, Result};
7
8/// A budget shared by everything running against one database.
9///
10/// Cheap to clone, and a clone shares the total with the budget it came from, so two queries running
11/// at once are held to one limit between them rather than to one each. That is what DuckDB's
12/// `memory_limit` means and it is the only reading that is any use: a limit that each query gets a
13/// fresh copy of is not a limit on the process.
14///
15/// # What it counts
16///
17/// What an operator says it is holding. Nothing here hooks the allocator, so the number is the sum
18/// of what the buffering operators reserved and not the resident size of the process. The gap is
19/// real and it is in one direction, since an operator charges for what it asked for and never for
20/// more.
21///
22/// How large the gap is decides whether the limit is any use. Under reporting is the safe direction
23/// only while it is small: a budget that is spent at two fifths of the real footprint is not a
24/// conservative limit, it is a limit that lets a query take two and a half times what it was
25/// allowed and get killed from outside anyway, which is exactly what it was there to prevent. #227
26/// found the aggregate doing that and it is why the operators charge a container for its capacity
27/// rather than its length and add [`ALLOCATION`] per block. [`Memory::peak`] is the accounted side
28/// of that comparison, so the gap can be measured rather than assumed.
29///
30/// The operators that reserve are the ones that buffer without bound, which is sorting, grouping,
31/// duplicate elimination, joining, set operations and the result a query hands back. A streaming
32/// operator holds one chunk and gives it away again, so charging it would be counting the same
33/// megabyte once per level of the tree.
34///
35/// # Why a reservation rather than a pair of calls
36///
37/// [`Memory::reserve`] hands back a [`Reservation`] that releases what it took when it is dropped,
38/// so an operator that fails halfway through, or a query stopped by an interrupt, gives its memory
39/// back without anybody writing the release. A pair of `take` and `give` calls is the version where
40/// the release is missed on the error path, and the error path here is the one that matters, since
41/// running out of memory is itself an error and it unwinds through every operator below.
42#[derive(Debug, Clone)]
43pub struct Memory {
44    inner: Arc<Budget>,
45}
46
47#[derive(Debug)]
48struct Budget {
49    used: AtomicU64,
50    /// The limit, with [`NO_LIMIT`] meaning there is none.
51    ///
52    /// Atomic rather than plain, because `SET memory_limit` changes it while queries are running
53    /// and the budget is shared by every one of them. A query that is already holding more than a
54    /// new limit allows is not stopped: it keeps what it has and is refused the next time it asks
55    /// for more, which is what DuckDB does and is the only behaviour that does not turn a setting
56    /// into a way of killing whatever happens to be running.
57    limit: AtomicU64,
58    /// The most that has ever been held at once, which nothing gives back.
59    ///
60    /// Added for #227, where the question was how far the accounting is from what the process
61    /// actually takes, and the only way to ask it was to run a query under `/usr/bin/time -v` and
62    /// compare by hand. Now the accounted side of that comparison is a number the database will
63    /// say, so a test can assert on it and a benchmark can print it beside the resident set.
64    peak: AtomicU64,
65}
66
67/// What the limit holds when there is no limit.
68///
69/// A sentinel rather than an `Option`, because an `Option<u64>` is not atomic and a lock around the
70/// limit would be a lock taken on every reservation.
71const NO_LIMIT: u64 = u64::MAX;
72
73/// What the allocator takes on top of a block, for every block handed out.
74///
75/// Every general purpose allocator keeps a header beside the block and rounds the size up to an
76/// alignment, and none of them will say by how much. Sixteen is glibc's, an eight byte header and a
77/// sixteen byte alignment, and it is a floor rather than an average, so a caller that adds this per
78/// allocation is still under reporting and is under reporting by much less than one that adds
79/// nothing.
80///
81/// It matters because the things this budget counts are made of small allocations. A hash table of
82/// seventeen million groups is seventeen million blocks, and sixteen bytes apiece is a quarter of a
83/// gigabyte that was invisible before #227.
84pub const ALLOCATION: u64 = 16;
85
86impl Default for Memory {
87    fn default() -> Self {
88        Self::unlimited()
89    }
90}
91
92impl Memory {
93    /// A budget nothing is refused against, which still counts what is held.
94    ///
95    /// The counting is kept because [`Memory::used`] is worth reading whether or not there is a
96    /// limit, and because a query that behaves differently depending on whether a limit is set is a
97    /// query whose limit cannot be tested by setting one.
98    #[must_use]
99    pub fn unlimited() -> Self {
100        Self::new(None)
101    }
102
103    /// A budget of this many bytes.
104    #[must_use]
105    pub fn with_limit(bytes: u64) -> Self {
106        Self::new(Some(bytes))
107    }
108
109    /// A budget of this many bytes, or no limit at all.
110    #[must_use]
111    pub fn new(limit: Option<u64>) -> Self {
112        let limit = AtomicU64::new(limit.unwrap_or(NO_LIMIT));
113        Self { inner: Arc::new(Budget { used: AtomicU64::new(0), limit, peak: AtomicU64::new(0) }) }
114    }
115
116    /// The limit, if there is one.
117    #[must_use]
118    pub fn limit(&self) -> Option<u64> {
119        match self.inner.limit.load(Ordering::Relaxed) {
120            NO_LIMIT => None,
121            limit => Some(limit),
122        }
123    }
124
125    /// Changes the limit, for every query holding this budget.
126    ///
127    /// A limit below what is already held is allowed and refuses the next reservation rather than
128    /// stopping anything, which is what DuckDB does and is the only behaviour that does not turn a
129    /// setting into a way of killing whatever happens to be running.
130    pub fn set_limit(&self, limit: Option<u64>) {
131        self.inner.limit.store(limit.unwrap_or(NO_LIMIT), Ordering::Relaxed);
132    }
133
134    /// How many bytes are held right now.
135    #[must_use]
136    pub fn used(&self) -> u64 {
137        self.inner.used.load(Ordering::Relaxed)
138    }
139
140    /// The most that was ever held at once since the last [`Memory::forget_peak`].
141    ///
142    /// [`Memory::used`] falls back to zero when a query ends, so it answers what is held and never
143    /// what was held, and what was held is the number worth knowing. It is what a query cost, it is
144    /// what has to be compared against the resident set to find out whether the accounting means
145    /// anything, and it is the one to print beside a benchmark row.
146    ///
147    /// It is a property of the budget rather than of a query, so two queries running at once share
148    /// one and it is the peak of the pair.
149    #[must_use]
150    pub fn peak(&self) -> u64 {
151        self.inner.peak.load(Ordering::Relaxed)
152    }
153
154    /// Puts the high water mark back to what is held right now.
155    ///
156    /// Back to what is held rather than to zero, because a mark below the current total would be a
157    /// number that says less was held than is held.
158    pub fn forget_peak(&self) {
159        self.inner.peak.store(self.used(), Ordering::Relaxed);
160    }
161
162    /// A reservation on this budget that is holding nothing yet.
163    ///
164    /// What a buffering operator starts with, because it is built before it has read anything and
165    /// its constructor has no error to report. It grows as the input arrives.
166    #[must_use]
167    pub fn reservation(&self) -> Reservation {
168        Reservation { memory: self.clone(), bytes: 0 }
169    }
170
171    /// Takes `bytes` out of the budget, to be given back when the reservation is dropped.
172    ///
173    /// Reserving nothing always works and is the way an operator gets a handle it can grow later.
174    ///
175    /// # Errors
176    ///
177    /// [`crate::ErrorCode::OutOfMemory`] when the limit is set and this would pass it. Nothing is
178    /// taken in that case, so a caller that carries on after catching it is holding what it held
179    /// before.
180    pub fn reserve(&self, bytes: u64) -> Result<Reservation> {
181        self.take(bytes)?;
182        Ok(Reservation { memory: self.clone(), bytes })
183    }
184
185    /// Adds to the total, or reports that it cannot.
186    ///
187    /// The loop is a compare and exchange rather than a fetch and add with a check afterwards,
188    /// because a fetch and add that has to be undone is a window in which another thread sees a
189    /// total that was never allowed and refuses a query that would have fit.
190    fn take(&self, bytes: u64) -> Result<()> {
191        let Some(limit) = self.limit() else {
192            let was = self.inner.used.fetch_add(bytes, Ordering::Relaxed);
193            self.inner.peak.fetch_max(was + bytes, Ordering::Relaxed);
194            return Ok(());
195        };
196        let mut used = self.inner.used.load(Ordering::Relaxed);
197        loop {
198            let wanted = used.saturating_add(bytes);
199            if wanted > limit {
200                return Err(Error::out_of_memory(format!(
201                    "could not allocate {} ({}/{} used)",
202                    human(bytes),
203                    human(used),
204                    human(limit)
205                )));
206            }
207            match self.inner.used.compare_exchange_weak(
208                used,
209                wanted,
210                Ordering::Relaxed,
211                Ordering::Relaxed,
212            ) {
213                Ok(_) => {
214                    self.inner.peak.fetch_max(wanted, Ordering::Relaxed);
215                    return Ok(());
216                }
217                Err(now) => used = now,
218            }
219        }
220    }
221
222    /// Gives bytes back.
223    fn give(&self, bytes: u64) {
224        self.inner.used.fetch_sub(bytes, Ordering::Relaxed);
225    }
226}
227
228/// Memory one operator is holding, given back when this is dropped.
229///
230/// It starts at whatever [`Memory::reserve`] was asked for and grows from there, which is the shape
231/// a buffering operator wants: it does not know how much it will hold until it has read its input,
232/// and it wants to be told as soon as the answer is too much rather than after the last row.
233#[derive(Debug)]
234pub struct Reservation {
235    memory: Memory,
236    bytes: u64,
237}
238
239impl Reservation {
240    /// How much this reservation is holding.
241    #[must_use]
242    pub fn bytes(&self) -> u64 {
243        self.bytes
244    }
245
246    /// Takes another `bytes` out of the same budget.
247    ///
248    /// # Errors
249    ///
250    /// [`crate::ErrorCode::OutOfMemory`] when the limit is set and this would pass it. The
251    /// reservation is unchanged in that case and still releases what it already held.
252    pub fn grow(&mut self, bytes: u64) -> Result<()> {
253        self.memory.take(bytes)?;
254        self.bytes += bytes;
255        Ok(())
256    }
257
258    /// Gives everything back now rather than at the end of the scope.
259    ///
260    /// For an operator that has finished with its buffer and is about to hand out what it built
261    /// from it, where waiting for the drop would hold two copies against the limit at once.
262    pub fn release(&mut self) {
263        self.memory.give(self.bytes);
264        self.bytes = 0;
265    }
266}
267
268impl Drop for Reservation {
269    fn drop(&mut self) {
270        self.memory.give(self.bytes);
271    }
272}
273
274/// A size the way an error message says one.
275///
276/// The shape DuckDB prints, which is one decimal place and the binary units, so that
277/// `9.3 MiB/9.5 MiB used` in a message from here reads as the same sentence as the one from there.
278/// It rounds, which is why it is not the formatter `--print-config` uses: a configuration dump has
279/// to print a number somebody can compare against what they set, and a message about running out of
280/// memory has to print one somebody can read.
281pub fn human(bytes: u64) -> String {
282    #[expect(clippy::cast_precision_loss, reason = "a rounded size is the point of this function")]
283    let mut size = bytes as f64;
284    for unit in ["bytes", "KiB", "MiB", "GiB", "TiB", "PiB"] {
285        if size < 1024.0 || unit == "PiB" {
286            return if unit == "bytes" {
287                format!("{bytes} bytes")
288            } else {
289                format!("{size:.1} {unit}")
290            };
291        }
292        size /= 1024.0;
293    }
294    unreachable!("the loop returns on its last unit")
295}
296
297#[cfg(test)]
298mod tests {
299    use super::{Memory, human};
300
301    #[test]
302    fn the_peak_remembers_what_used_forgets() {
303        let memory = Memory::with_limit(1 << 20);
304        {
305            let _held = memory.reserve(1000).expect("room for the first");
306            let _more = memory.reserve(2000).expect("room for the second");
307            assert_eq!(memory.used(), 3000);
308            assert_eq!(memory.peak(), 3000);
309        }
310        assert_eq!(memory.used(), 0);
311        assert_eq!(memory.peak(), 3000, "what was held is the number worth knowing");
312        let held = memory.reserve(500).expect("room again");
313        assert_eq!(memory.peak(), 3000, "a smaller total does not move the mark down");
314        memory.forget_peak();
315        assert_eq!(memory.peak(), 500, "forgetting goes back to what is held, not to zero");
316        drop(held);
317    }
318
319    #[test]
320    fn a_budget_with_no_limit_still_has_a_peak() {
321        // The unlimited path is a plain add rather than the compare and exchange loop, so it is a
322        // second place the mark has to be moved and a second place to forget to.
323        let memory = Memory::unlimited();
324        let held = memory.reserve(4096).expect("nothing is refused");
325        drop(held);
326        assert_eq!(memory.used(), 0);
327        assert_eq!(memory.peak(), 4096);
328    }
329
330    #[test]
331    fn a_refused_reservation_does_not_move_the_mark() {
332        let memory = Memory::with_limit(1000);
333        let held = memory.reserve(900).expect("room for this");
334        memory.reserve(200).expect_err("no room for that");
335        assert_eq!(memory.peak(), 900, "what was refused was never held");
336        drop(held);
337    }
338
339    #[test]
340    fn an_unlimited_budget_refuses_nothing_and_still_counts() {
341        let memory = Memory::unlimited();
342        assert_eq!(memory.limit(), None);
343        let held = memory.reserve(1 << 30).expect("nothing is refused");
344        assert_eq!(memory.used(), 1 << 30);
345        assert_eq!(held.bytes(), 1 << 30);
346    }
347
348    #[test]
349    fn a_reservation_gives_its_bytes_back_when_it_is_dropped() {
350        let memory = Memory::with_limit(1024);
351        {
352            let _held = memory.reserve(1000).expect("a thousand of a thousand and twenty four");
353            assert_eq!(memory.used(), 1000);
354        }
355        assert_eq!(memory.used(), 0);
356        memory.reserve(1000).expect("the room is back");
357    }
358
359    #[test]
360    fn passing_the_limit_is_an_out_of_memory_error_that_says_the_numbers() {
361        let memory = Memory::with_limit(10 * 1024 * 1024);
362        let _held = memory.reserve(9 * 1024 * 1024).expect("nine of ten");
363        let error = memory.reserve(2 * 1024 * 1024).expect_err("eleven of ten");
364        assert_eq!(error.code().duckdb_name(), "Out of Memory Error");
365        assert_eq!(error.message(), "could not allocate 2.0 MiB (9.0 MiB/10.0 MiB used)");
366    }
367
368    #[test]
369    fn a_refused_reservation_takes_nothing() {
370        let memory = Memory::with_limit(100);
371        memory.reserve(200).expect_err("twice the limit");
372        assert_eq!(memory.used(), 0);
373        memory.reserve(100).expect("the limit is still all there");
374    }
375
376    #[test]
377    fn a_reservation_grows_until_it_cannot() {
378        let memory = Memory::with_limit(100);
379        let mut held = memory.reserve(0).expect("nothing is always available");
380        held.grow(60).expect("sixty of a hundred");
381        held.grow(40).expect("and the other forty");
382        held.grow(1).expect_err("there is no more");
383        assert_eq!(held.bytes(), 100, "the refused growth changed nothing");
384        assert_eq!(memory.used(), 100);
385    }
386
387    #[test]
388    fn releasing_early_frees_the_room_before_the_scope_ends() {
389        let memory = Memory::with_limit(100);
390        let mut held = memory.reserve(100).expect("all of it");
391        held.release();
392        assert_eq!(memory.used(), 0);
393        assert_eq!(held.bytes(), 0);
394        // And the drop that follows does not take the total below zero.
395        drop(held);
396        assert_eq!(memory.used(), 0);
397    }
398
399    #[test]
400    fn two_handles_on_one_budget_are_held_to_it_between_them() {
401        // A limit each query gets a fresh copy of is not a limit on the process.
402        let memory = Memory::with_limit(100);
403        let other = memory.clone();
404        let _held = memory.reserve(60).expect("sixty");
405        other.reserve(60).expect_err("the other sixty does not fit beside it");
406    }
407
408    #[test]
409    fn a_size_reads_the_way_duckdb_writes_one() {
410        assert_eq!(human(0), "0 bytes");
411        assert_eq!(human(512), "512 bytes");
412        assert_eq!(human(256 * 1024), "256.0 KiB");
413        assert_eq!(human(10 * 1024 * 1024), "10.0 MiB");
414        assert_eq!(human(9_751_000), "9.3 MiB");
415        assert_eq!(human(3 * 1024 * 1024 * 1024), "3.0 GiB");
416        assert_eq!(human(5 * 1024u64.pow(5)), "5.0 PiB");
417        // The last unit runs off the end rather than there being a unit past it, because a size
418        // that big is a bug in whatever asked for it and not a number anybody reads.
419        assert_eq!(human(u64::MAX), "16384.0 PiB");
420    }
421}