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 `bytes` of it back, or everything if that is more than this is holding.
259 ///
260 /// For an operator that charged several things against one reservation and has dropped one of
261 /// them. The aggregate charges its hash table, its accumulators and its distinct sets together,
262 /// and then hands the keys out of the table into the rows it is building, at which point the
263 /// table is gone and the accumulators are not. Waiting for the whole reservation would charge a
264 /// table that no longer exists for the whole of the conversion, which is exactly the moment the
265 /// operator is holding the most.
266 pub fn shrink(&mut self, bytes: u64) {
267 let given = bytes.min(self.bytes);
268 self.memory.give(given);
269 self.bytes -= given;
270 }
271
272 /// Gives everything back now rather than at the end of the scope.
273 ///
274 /// For an operator that has finished with its buffer and is about to hand out what it built
275 /// from it, where waiting for the drop would hold two copies against the limit at once.
276 pub fn release(&mut self) {
277 self.memory.give(self.bytes);
278 self.bytes = 0;
279 }
280}
281
282impl Drop for Reservation {
283 fn drop(&mut self) {
284 self.memory.give(self.bytes);
285 }
286}
287
288/// A size the way an error message says one.
289///
290/// The shape DuckDB prints, which is one decimal place and the binary units, so that
291/// `9.3 MiB/9.5 MiB used` in a message from here reads as the same sentence as the one from there.
292/// It rounds, which is why it is not the formatter `--print-config` uses: a configuration dump has
293/// to print a number somebody can compare against what they set, and a message about running out of
294/// memory has to print one somebody can read.
295pub fn human(bytes: u64) -> String {
296 #[expect(clippy::cast_precision_loss, reason = "a rounded size is the point of this function")]
297 let mut size = bytes as f64;
298 for unit in ["bytes", "KiB", "MiB", "GiB", "TiB", "PiB"] {
299 if size < 1024.0 || unit == "PiB" {
300 return if unit == "bytes" {
301 format!("{bytes} bytes")
302 } else {
303 format!("{size:.1} {unit}")
304 };
305 }
306 size /= 1024.0;
307 }
308 unreachable!("the loop returns on its last unit")
309}
310
311#[cfg(test)]
312mod tests {
313 use super::{Memory, human};
314
315 #[test]
316 fn the_peak_remembers_what_used_forgets() {
317 let memory = Memory::with_limit(1 << 20);
318 {
319 let _held = memory.reserve(1000).expect("room for the first");
320 let _more = memory.reserve(2000).expect("room for the second");
321 assert_eq!(memory.used(), 3000);
322 assert_eq!(memory.peak(), 3000);
323 }
324 assert_eq!(memory.used(), 0);
325 assert_eq!(memory.peak(), 3000, "what was held is the number worth knowing");
326 let held = memory.reserve(500).expect("room again");
327 assert_eq!(memory.peak(), 3000, "a smaller total does not move the mark down");
328 memory.forget_peak();
329 assert_eq!(memory.peak(), 500, "forgetting goes back to what is held, not to zero");
330 drop(held);
331 }
332
333 #[test]
334 fn a_budget_with_no_limit_still_has_a_peak() {
335 // The unlimited path is a plain add rather than the compare and exchange loop, so it is a
336 // second place the mark has to be moved and a second place to forget to.
337 let memory = Memory::unlimited();
338 let held = memory.reserve(4096).expect("nothing is refused");
339 drop(held);
340 assert_eq!(memory.used(), 0);
341 assert_eq!(memory.peak(), 4096);
342 }
343
344 #[test]
345 fn a_shrink_gives_back_part_and_never_more_than_it_holds() {
346 let memory = Memory::with_limit(1000);
347 let mut held = memory.reserve(800).expect("room for this");
348 held.shrink(300);
349 assert_eq!(held.bytes(), 500);
350 assert_eq!(memory.used(), 500, "the budget got the difference back");
351 memory.reserve(400).expect("which is room for something else");
352 // A reservation that is asked for more than it has gives what it has. The alternative is an
353 // arithmetic overflow in an operator that miscounted, and the operator that miscounted is
354 // the one that would never find out.
355 held.shrink(u64::MAX);
356 assert_eq!(held.bytes(), 0);
357 assert_eq!(memory.used(), 0);
358 }
359
360 #[test]
361 fn a_refused_reservation_does_not_move_the_mark() {
362 let memory = Memory::with_limit(1000);
363 let held = memory.reserve(900).expect("room for this");
364 memory.reserve(200).expect_err("no room for that");
365 assert_eq!(memory.peak(), 900, "what was refused was never held");
366 drop(held);
367 }
368
369 #[test]
370 fn an_unlimited_budget_refuses_nothing_and_still_counts() {
371 let memory = Memory::unlimited();
372 assert_eq!(memory.limit(), None);
373 let held = memory.reserve(1 << 30).expect("nothing is refused");
374 assert_eq!(memory.used(), 1 << 30);
375 assert_eq!(held.bytes(), 1 << 30);
376 }
377
378 #[test]
379 fn a_reservation_gives_its_bytes_back_when_it_is_dropped() {
380 let memory = Memory::with_limit(1024);
381 {
382 let _held = memory.reserve(1000).expect("a thousand of a thousand and twenty four");
383 assert_eq!(memory.used(), 1000);
384 }
385 assert_eq!(memory.used(), 0);
386 memory.reserve(1000).expect("the room is back");
387 }
388
389 #[test]
390 fn passing_the_limit_is_an_out_of_memory_error_that_says_the_numbers() {
391 let memory = Memory::with_limit(10 * 1024 * 1024);
392 let _held = memory.reserve(9 * 1024 * 1024).expect("nine of ten");
393 let error = memory.reserve(2 * 1024 * 1024).expect_err("eleven of ten");
394 assert_eq!(error.code().duckdb_name(), "Out of Memory Error");
395 assert_eq!(error.message(), "could not allocate 2.0 MiB (9.0 MiB/10.0 MiB used)");
396 }
397
398 #[test]
399 fn a_refused_reservation_takes_nothing() {
400 let memory = Memory::with_limit(100);
401 memory.reserve(200).expect_err("twice the limit");
402 assert_eq!(memory.used(), 0);
403 memory.reserve(100).expect("the limit is still all there");
404 }
405
406 #[test]
407 fn a_reservation_grows_until_it_cannot() {
408 let memory = Memory::with_limit(100);
409 let mut held = memory.reserve(0).expect("nothing is always available");
410 held.grow(60).expect("sixty of a hundred");
411 held.grow(40).expect("and the other forty");
412 held.grow(1).expect_err("there is no more");
413 assert_eq!(held.bytes(), 100, "the refused growth changed nothing");
414 assert_eq!(memory.used(), 100);
415 }
416
417 #[test]
418 fn releasing_early_frees_the_room_before_the_scope_ends() {
419 let memory = Memory::with_limit(100);
420 let mut held = memory.reserve(100).expect("all of it");
421 held.release();
422 assert_eq!(memory.used(), 0);
423 assert_eq!(held.bytes(), 0);
424 // And the drop that follows does not take the total below zero.
425 drop(held);
426 assert_eq!(memory.used(), 0);
427 }
428
429 #[test]
430 fn two_handles_on_one_budget_are_held_to_it_between_them() {
431 // A limit each query gets a fresh copy of is not a limit on the process.
432 let memory = Memory::with_limit(100);
433 let other = memory.clone();
434 let _held = memory.reserve(60).expect("sixty");
435 other.reserve(60).expect_err("the other sixty does not fit beside it");
436 }
437
438 #[test]
439 fn a_size_reads_the_way_duckdb_writes_one() {
440 assert_eq!(human(0), "0 bytes");
441 assert_eq!(human(512), "512 bytes");
442 assert_eq!(human(256 * 1024), "256.0 KiB");
443 assert_eq!(human(10 * 1024 * 1024), "10.0 MiB");
444 assert_eq!(human(9_751_000), "9.3 MiB");
445 assert_eq!(human(3 * 1024 * 1024 * 1024), "3.0 GiB");
446 assert_eq!(human(5 * 1024u64.pow(5)), "5.0 PiB");
447 // The last unit runs off the end rather than there being a unit past it, because a size
448 // that big is a bug in whatever asked for it and not a number anybody reads.
449 assert_eq!(human(u64::MAX), "16384.0 PiB");
450 }
451}