use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use crate::error::{Error, Result};
#[derive(Debug, Clone)]
pub struct Memory {
inner: Arc<Budget>,
}
#[derive(Debug)]
struct Budget {
used: AtomicU64,
limit: Option<u64>,
}
impl Default for Memory {
fn default() -> Self {
Self::unlimited()
}
}
impl Memory {
#[must_use]
pub fn unlimited() -> Self {
Self::new(None)
}
#[must_use]
pub fn with_limit(bytes: u64) -> Self {
Self::new(Some(bytes))
}
#[must_use]
pub fn new(limit: Option<u64>) -> Self {
Self { inner: Arc::new(Budget { used: AtomicU64::new(0), limit }) }
}
#[must_use]
pub fn limit(&self) -> Option<u64> {
self.inner.limit
}
#[must_use]
pub fn used(&self) -> u64 {
self.inner.used.load(Ordering::Relaxed)
}
#[must_use]
pub fn reservation(&self) -> Reservation {
Reservation { memory: self.clone(), bytes: 0 }
}
pub fn reserve(&self, bytes: u64) -> Result<Reservation> {
self.take(bytes)?;
Ok(Reservation { memory: self.clone(), bytes })
}
fn take(&self, bytes: u64) -> Result<()> {
let Some(limit) = self.inner.limit else {
self.inner.used.fetch_add(bytes, Ordering::Relaxed);
return Ok(());
};
let mut used = self.inner.used.load(Ordering::Relaxed);
loop {
let wanted = used.saturating_add(bytes);
if wanted > limit {
return Err(Error::out_of_memory(format!(
"could not allocate {} ({}/{} used)",
human(bytes),
human(used),
human(limit)
)));
}
match self.inner.used.compare_exchange_weak(
used,
wanted,
Ordering::Relaxed,
Ordering::Relaxed,
) {
Ok(_) => return Ok(()),
Err(now) => used = now,
}
}
}
fn give(&self, bytes: u64) {
self.inner.used.fetch_sub(bytes, Ordering::Relaxed);
}
}
#[derive(Debug)]
pub struct Reservation {
memory: Memory,
bytes: u64,
}
impl Reservation {
#[must_use]
pub fn bytes(&self) -> u64 {
self.bytes
}
pub fn grow(&mut self, bytes: u64) -> Result<()> {
self.memory.take(bytes)?;
self.bytes += bytes;
Ok(())
}
pub fn release(&mut self) {
self.memory.give(self.bytes);
self.bytes = 0;
}
}
impl Drop for Reservation {
fn drop(&mut self) {
self.memory.give(self.bytes);
}
}
fn human(bytes: u64) -> String {
#[expect(clippy::cast_precision_loss, reason = "a rounded size is the point of this function")]
let mut size = bytes as f64;
for unit in ["bytes", "KiB", "MiB", "GiB", "TiB"] {
if size < 1024.0 || unit == "TiB" {
return if unit == "bytes" {
format!("{bytes} bytes")
} else {
format!("{size:.1} {unit}")
};
}
size /= 1024.0;
}
unreachable!("the loop returns on its last unit")
}
#[cfg(test)]
mod tests {
use super::{Memory, human};
#[test]
fn an_unlimited_budget_refuses_nothing_and_still_counts() {
let memory = Memory::unlimited();
assert_eq!(memory.limit(), None);
let held = memory.reserve(1 << 30).expect("nothing is refused");
assert_eq!(memory.used(), 1 << 30);
assert_eq!(held.bytes(), 1 << 30);
}
#[test]
fn a_reservation_gives_its_bytes_back_when_it_is_dropped() {
let memory = Memory::with_limit(1024);
{
let _held = memory.reserve(1000).expect("a thousand of a thousand and twenty four");
assert_eq!(memory.used(), 1000);
}
assert_eq!(memory.used(), 0);
memory.reserve(1000).expect("the room is back");
}
#[test]
fn passing_the_limit_is_an_out_of_memory_error_that_says_the_numbers() {
let memory = Memory::with_limit(10 * 1024 * 1024);
let _held = memory.reserve(9 * 1024 * 1024).expect("nine of ten");
let error = memory.reserve(2 * 1024 * 1024).expect_err("eleven of ten");
assert_eq!(error.code().duckdb_name(), "Out of Memory Error");
assert_eq!(error.message(), "could not allocate 2.0 MiB (9.0 MiB/10.0 MiB used)");
}
#[test]
fn a_refused_reservation_takes_nothing() {
let memory = Memory::with_limit(100);
memory.reserve(200).expect_err("twice the limit");
assert_eq!(memory.used(), 0);
memory.reserve(100).expect("the limit is still all there");
}
#[test]
fn a_reservation_grows_until_it_cannot() {
let memory = Memory::with_limit(100);
let mut held = memory.reserve(0).expect("nothing is always available");
held.grow(60).expect("sixty of a hundred");
held.grow(40).expect("and the other forty");
held.grow(1).expect_err("there is no more");
assert_eq!(held.bytes(), 100, "the refused growth changed nothing");
assert_eq!(memory.used(), 100);
}
#[test]
fn releasing_early_frees_the_room_before_the_scope_ends() {
let memory = Memory::with_limit(100);
let mut held = memory.reserve(100).expect("all of it");
held.release();
assert_eq!(memory.used(), 0);
assert_eq!(held.bytes(), 0);
drop(held);
assert_eq!(memory.used(), 0);
}
#[test]
fn two_handles_on_one_budget_are_held_to_it_between_them() {
let memory = Memory::with_limit(100);
let other = memory.clone();
let _held = memory.reserve(60).expect("sixty");
other.reserve(60).expect_err("the other sixty does not fit beside it");
}
#[test]
fn a_size_reads_the_way_duckdb_writes_one() {
assert_eq!(human(0), "0 bytes");
assert_eq!(human(512), "512 bytes");
assert_eq!(human(256 * 1024), "256.0 KiB");
assert_eq!(human(10 * 1024 * 1024), "10.0 MiB");
assert_eq!(human(9_751_000), "9.3 MiB");
assert_eq!(human(3 * 1024 * 1024 * 1024), "3.0 GiB");
assert_eq!(human(5 * 1024u64.pow(5)), "5120.0 TiB");
}
}