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}