blob: e620b23267962b490c3137b85b29fffe8e914f59 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//! [`MemoryPool`] for memory management during query execution, [`proxy`] for
//! help with allocation accounting.
use datafusion_common::{internal_err, Result};
use std::hash::{Hash, Hasher};
use std::{cmp::Ordering, sync::atomic, sync::Arc};
mod pool;
pub mod proxy {
pub use datafusion_common::utils::proxy::{
HashTableAllocExt, RawTableAllocExt, VecAllocExt,
};
}
pub use pool::*;
/// Tracks and potentially limits memory use across operators during execution.
///
/// # Memory Management Overview
///
/// DataFusion is a streaming query engine, processing most queries without
/// buffering the entire input. Most operators require a fixed amount of memory
/// based on the schema and target batch size. However, certain operations such
/// as sorting and grouping/joining, require buffering intermediate results,
/// which can require memory proportional to the number of input rows.
///
/// Rather than tracking all allocations, DataFusion takes a pragmatic approach:
/// Intermediate memory used as data streams through the system is not accounted
/// (it assumed to be "small") but the large consumers of memory must register
/// and constrain their use. This design trades off the additional code
/// complexity of memory tracking with limiting resource usage.
///
/// When limiting memory with a `MemoryPool` you should typically reserve some
/// overhead (e.g. 10%) for the "small" memory allocations that are not tracked.
///
/// # Memory Management Design
///
/// As explained above, DataFusion's design ONLY limits operators that require
/// "large" amounts of memory (proportional to number of input rows), such as
/// `GroupByHashExec`. It does NOT track and limit memory used internally by
/// other operators such as `DataSourceExec` or the `RecordBatch`es that flow
/// between operators. Furthermore, operators should not reserve memory for the
/// batches they produce. Instead, if a consumer operator needs to hold batches
/// from its producers in memory for an extended period, it is the consumer
/// operator's responsibility to reserve the necessary memory for those batches.
///
/// In order to avoid allocating memory until the OS or the container system
/// kills the process, DataFusion `ExecutionPlan`s (operators) that consume
/// large amounts of memory must first request their desired allocation from a
/// [`MemoryPool`] before allocating more. The request is typically managed via
/// a [`MemoryReservation`] and [`MemoryConsumer`].
///
/// If the allocation is successful, the operator should proceed and allocate
/// the desired memory. If the allocation fails, the operator must either first
/// free memory (e.g. by spilling to local disk) and try again, or error.
///
/// Note that a `MemoryPool` can be shared by concurrently executing plans,
/// which can be used to control memory usage in a multi-tenant system.
///
/// # How MemoryPool works by example
///
/// Scenario 1:
/// For `Filter` operator, `RecordBatch`es will stream through it, so it
/// don't have to keep track of memory usage through [`MemoryPool`].
///
/// Scenario 2:
/// For `CrossJoin` operator, if the input size gets larger, the intermediate
/// state will also grow. So `CrossJoin` operator will use [`MemoryPool`] to
/// limit the memory usage.
/// 2.1 `CrossJoin` operator has read a new batch, asked memory pool for
/// additional memory. Memory pool updates the usage and returns success.
/// 2.2 `CrossJoin` has read another batch, and tries to reserve more memory
/// again, memory pool does not have enough memory. Since `CrossJoin` operator
/// has not implemented spilling, it will stop execution and return an error.
///
/// Scenario 3:
/// For `Aggregate` operator, its intermediate states will also accumulate as
/// the input size gets larger, but with spilling capability. When it tries to
/// reserve more memory from the memory pool, and the memory pool has already
/// reached the memory limit, it will return an error. Then, `Aggregate`
/// operator will spill the intermediate buffers to disk, and release memory
/// from the memory pool, and continue to retry memory reservation.
///
/// # Related Structs
///
/// To better understand memory management in DataFusion, here are the key structs
/// and their relationships:
///
/// - [`MemoryConsumer`]: A named allocation traced by a particular operator. If an
/// execution is parallelized, and there are multiple partitions of the same
/// operator, each partition will have a separate `MemoryConsumer`.
/// - `SharedRegistration`: A registration of a `MemoryConsumer` with a `MemoryPool`.
/// `SharedRegistration` and `MemoryPool` have a many-to-one relationship. `MemoryPool`
/// implementation can decide how to allocate memory based on the registered consumers.
/// (e.g. `FairSpillPool` will try to share available memory evenly among all registered
/// consumers)
/// - [`MemoryReservation`]: Each `MemoryConsumer`/operator can have multiple
/// `MemoryReservation`s for different internal data structures. The relationship
/// between `MemoryConsumer` and `MemoryReservation` is one-to-many. This design
/// enables cleaner operator implementations:
/// - Different `MemoryReservation`s can be used for different purposes
/// - `MemoryReservation` follows RAII principles - to release a reservation,
/// simply drop the `MemoryReservation` object. When all `MemoryReservation`s
/// for a `SharedRegistration` are dropped, the `SharedRegistration` is dropped
/// when its reference count reaches zero, automatically unregistering the
/// `MemoryConsumer` from the `MemoryPool`.
///
/// ## Relationship Diagram
///
/// ```text
/// ┌──────────────────┐ ┌──────────────────┐
/// │MemoryReservation │ │MemoryReservation │
/// └───┬──────────────┘ └──────────────────┘ ......
/// │belongs to │
/// │ ┌───────────────────────┘ │ │
/// │ │ │ │
/// ▼ ▼ ▼ ▼
/// ┌────────────────────────┐ ┌────────────────────────┐
/// │ SharedRegistration │ │ SharedRegistration │
/// │ ┌────────────────┐ │ │ ┌────────────────┐ │
/// │ │ │ │ │ │ │ │
/// │ │ MemoryConsumer │ │ │ │ MemoryConsumer │ │
/// │ │ │ │ │ │ │ │
/// │ └────────────────┘ │ │ └────────────────┘ │
/// └────────────┬───────────┘ └────────────┬───────────┘
/// │ │
/// │ register│into
/// │ │
/// └─────────────┐ ┌──────────────┘
/// │ │
/// ▼ ▼
/// ╔═══════════════════════════════════════════════════╗
/// ║ ║
/// ║ MemoryPool ║
/// ║ ║
/// ╚═══════════════════════════════════════════════════╝
/// ```
///
/// For example, there are two parallel partitions of an operator X: each partition
/// corresponds to a `MemoryConsumer` in the above diagram. Inside each partition of
/// operator X, there are typically several `MemoryReservation`s - one for each
/// internal data structure that needs memory tracking (e.g., 1 reservation for the hash
/// table, and 1 reservation for buffered input, etc.).
///
/// # Implementing `MemoryPool`
///
/// You can implement a custom allocation policy by implementing the
/// [`MemoryPool`] trait and configuring a `SessionContext` appropriately.
/// However, DataFusion comes with the following simple memory pool implementations that
/// handle many common cases:
///
/// * [`UnboundedMemoryPool`]: no memory limits (the default)
///
/// * [`GreedyMemoryPool`]: Limits memory usage to a fixed size using a "first
/// come first served" policy
///
/// * [`FairSpillPool`]: Limits memory usage to a fixed size, allocating memory
/// to all spilling operators fairly
///
/// * [`TrackConsumersPool`]: Wraps another [`MemoryPool`] and tracks consumers,
/// providing better error messages on the largest memory users.
pub trait MemoryPool: Send + Sync + std::fmt::Debug {
/// Registers a new [`MemoryConsumer`]
///
/// Note: Subsequent calls to [`Self::grow`] must be made to reserve memory
fn register(&self, _consumer: &MemoryConsumer) {}
/// Records the destruction of a [`MemoryReservation`] with [`MemoryConsumer`]
///
/// Note: Prior calls to [`Self::shrink`] must be made to free any reserved memory
fn unregister(&self, _consumer: &MemoryConsumer) {}
/// Infallibly grow the provided `reservation` by `additional` bytes
///
/// This must always succeed
fn grow(&self, reservation: &MemoryReservation, additional: usize);
/// Infallibly shrink the provided `reservation` by `shrink` bytes
fn shrink(&self, reservation: &MemoryReservation, shrink: usize);
/// Attempt to grow the provided `reservation` by `additional` bytes
///
/// On error the `allocation` will not be increased in size
fn try_grow(&self, reservation: &MemoryReservation, additional: usize) -> Result<()>;
/// Return the total amount of memory reserved
fn reserved(&self) -> usize;
/// Return the memory limit of the pool
///
/// The default implementation of `MemoryPool::memory_limit`
/// will return `MemoryLimit::Unknown`.
/// If you are using your custom memory pool, but have the requirement to
/// know the memory usage limit of the pool, please implement this method
/// to return it(`Memory::Finite(limit)`).
fn memory_limit(&self) -> MemoryLimit {
MemoryLimit::Unknown
}
}
/// Memory limit of `MemoryPool`
pub enum MemoryLimit {
Infinite,
/// Bounded memory limit in bytes.
Finite(usize),
Unknown,
}
/// A memory consumer is a named allocation traced by a particular
/// [`MemoryReservation`] in a [`MemoryPool`]. All allocations are registered to
/// a particular `MemoryConsumer`;
///
/// Each `MemoryConsumer` is identifiable by a process-unique id, and is therefor not cloneable,
/// If you want a clone of a `MemoryConsumer`, you should look into [`MemoryConsumer::clone_with_new_id`],
/// but note that this `MemoryConsumer` may be treated as a separate entity based on the used pool,
/// and is only guaranteed to share the name and inner properties.
///
/// For help with allocation accounting, see the [`proxy`] module.
///
/// [proxy]: datafusion_common::utils::proxy
#[derive(Debug)]
pub struct MemoryConsumer {
name: String,
can_spill: bool,
id: usize,
}
impl PartialEq for MemoryConsumer {
fn eq(&self, other: &Self) -> bool {
let is_same_id = self.id == other.id;
#[cfg(debug_assertions)]
if is_same_id {
assert_eq!(self.name, other.name);
assert_eq!(self.can_spill, other.can_spill);
}
is_same_id
}
}
impl Eq for MemoryConsumer {}
impl Hash for MemoryConsumer {
fn hash<H: Hasher>(&self, state: &mut H) {
self.id.hash(state);
self.name.hash(state);
self.can_spill.hash(state);
}
}
impl MemoryConsumer {
fn new_unique_id() -> usize {
static ID: atomic::AtomicUsize = atomic::AtomicUsize::new(0);
ID.fetch_add(1, atomic::Ordering::Relaxed)
}
/// Create a new empty [`MemoryConsumer`] that can be grown using [`MemoryReservation`]
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
can_spill: false,
id: Self::new_unique_id(),
}
}
/// Returns a clone of this [`MemoryConsumer`] with a new unique id,
/// which can be registered with a [`MemoryPool`],
/// This new consumer is separate from the original.
pub fn clone_with_new_id(&self) -> Self {
Self {
name: self.name.clone(),
can_spill: self.can_spill,
id: Self::new_unique_id(),
}
}
/// Return the unique id of this [`MemoryConsumer`]
pub fn id(&self) -> usize {
self.id
}
/// Set whether this allocation can be spilled to disk
pub fn with_can_spill(self, can_spill: bool) -> Self {
Self { can_spill, ..self }
}
/// Returns true if this allocation can spill to disk
pub fn can_spill(&self) -> bool {
self.can_spill
}
/// Returns the name associated with this allocation
pub fn name(&self) -> &str {
&self.name
}
/// Registers this [`MemoryConsumer`] with the provided [`MemoryPool`] returning
/// a [`MemoryReservation`] that can be used to grow or shrink the memory reservation
pub fn register(self, pool: &Arc<dyn MemoryPool>) -> MemoryReservation {
pool.register(&self);
MemoryReservation {
registration: Arc::new(SharedRegistration {
pool: Arc::clone(pool),
consumer: self,
}),
size: 0,
}
}
}
/// A registration of a [`MemoryConsumer`] with a [`MemoryPool`].
///
/// Calls [`MemoryPool::unregister`] on drop to return any memory to
/// the underlying pool.
#[derive(Debug)]
struct SharedRegistration {
pool: Arc<dyn MemoryPool>,
consumer: MemoryConsumer,
}
impl Drop for SharedRegistration {
fn drop(&mut self) {
self.pool.unregister(&self.consumer);
}
}
/// A [`MemoryReservation`] tracks an individual reservation of a
/// number of bytes of memory in a [`MemoryPool`] that is freed back
/// to the pool on drop.
///
/// The reservation can be grown or shrunk over time.
#[derive(Debug)]
pub struct MemoryReservation {
registration: Arc<SharedRegistration>,
size: usize,
}
impl MemoryReservation {
/// Returns the size of this reservation in bytes
pub fn size(&self) -> usize {
self.size
}
/// Returns [MemoryConsumer] for this [MemoryReservation]
pub fn consumer(&self) -> &MemoryConsumer {
&self.registration.consumer
}
/// Frees all bytes from this reservation back to the underlying
/// pool, returning the number of bytes freed.
pub fn free(&mut self) -> usize {
let size = self.size;
if size != 0 {
self.shrink(size)
}
size
}
/// Frees `capacity` bytes from this reservation
///
/// # Panics
///
/// Panics if `capacity` exceeds [`Self::size`]
pub fn shrink(&mut self, capacity: usize) {
let new_size = self.size.checked_sub(capacity).unwrap();
self.registration.pool.shrink(self, capacity);
self.size = new_size
}
/// Tries to free `capacity` bytes from this reservation
/// if `capacity` does not exceed [`Self::size`]
/// Returns new reservation size
/// or error if shrinking capacity is more than allocated size
pub fn try_shrink(&mut self, capacity: usize) -> Result<usize> {
if let Some(new_size) = self.size.checked_sub(capacity) {
self.registration.pool.shrink(self, capacity);
self.size = new_size;
Ok(new_size)
} else {
internal_err!(
"Cannot free the capacity {capacity} out of allocated size {}",
self.size
)
}
}
/// Sets the size of this reservation to `capacity`
pub fn resize(&mut self, capacity: usize) {
match capacity.cmp(&self.size) {
Ordering::Greater => self.grow(capacity - self.size),
Ordering::Less => self.shrink(self.size - capacity),
_ => {}
}
}
/// Try to set the size of this reservation to `capacity`
pub fn try_resize(&mut self, capacity: usize) -> Result<()> {
match capacity.cmp(&self.size) {
Ordering::Greater => self.try_grow(capacity - self.size)?,
Ordering::Less => self.shrink(self.size - capacity),
_ => {}
};
Ok(())
}
/// Increase the size of this reservation by `capacity` bytes
pub fn grow(&mut self, capacity: usize) {
self.registration.pool.grow(self, capacity);
self.size += capacity;
}
/// Try to increase the size of this reservation by `capacity`
/// bytes, returning error if there is insufficient capacity left
/// in the pool.
pub fn try_grow(&mut self, capacity: usize) -> Result<()> {
self.registration.pool.try_grow(self, capacity)?;
self.size += capacity;
Ok(())
}
/// Splits off `capacity` bytes from this [`MemoryReservation`]
/// into a new [`MemoryReservation`] with the same
/// [`MemoryConsumer`].
///
/// This can be useful to free part of this reservation with RAAI
/// style dropping
///
/// # Panics
///
/// Panics if `capacity` exceeds [`Self::size`]
pub fn split(&mut self, capacity: usize) -> MemoryReservation {
self.size = self.size.checked_sub(capacity).unwrap();
Self {
size: capacity,
registration: Arc::clone(&self.registration),
}
}
/// Returns a new empty [`MemoryReservation`] with the same [`MemoryConsumer`]
pub fn new_empty(&self) -> Self {
Self {
size: 0,
registration: Arc::clone(&self.registration),
}
}
/// Splits off all the bytes from this [`MemoryReservation`] into
/// a new [`MemoryReservation`] with the same [`MemoryConsumer`]
pub fn take(&mut self) -> MemoryReservation {
self.split(self.size)
}
}
impl Drop for MemoryReservation {
fn drop(&mut self) {
self.free();
}
}
pub mod units {
pub const TB: u64 = 1 << 40;
pub const GB: u64 = 1 << 30;
pub const MB: u64 = 1 << 20;
pub const KB: u64 = 1 << 10;
}
/// Present size in human-readable form
pub fn human_readable_size(size: usize) -> String {
use units::*;
let size = size as u64;
let (value, unit) = {
if size >= 2 * TB {
(size as f64 / TB as f64, "TB")
} else if size >= 2 * GB {
(size as f64 / GB as f64, "GB")
} else if size >= 2 * MB {
(size as f64 / MB as f64, "MB")
} else if size >= 2 * KB {
(size as f64 / KB as f64, "KB")
} else {
(size as f64, "B")
}
};
format!("{value:.1} {unit}")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_id_uniqueness() {
let mut ids = std::collections::HashSet::new();
for _ in 0..100 {
let consumer = MemoryConsumer::new("test");
assert!(ids.insert(consumer.id())); // Ensures unique insertion
}
}
#[test]
fn test_memory_pool_underflow() {
let pool = Arc::new(GreedyMemoryPool::new(50)) as _;
let mut a1 = MemoryConsumer::new("a1").register(&pool);
assert_eq!(pool.reserved(), 0);
a1.grow(100);
assert_eq!(pool.reserved(), 100);
assert_eq!(a1.free(), 100);
assert_eq!(pool.reserved(), 0);
a1.try_grow(100).unwrap_err();
assert_eq!(pool.reserved(), 0);
a1.try_grow(30).unwrap();
assert_eq!(pool.reserved(), 30);
let mut a2 = MemoryConsumer::new("a2").register(&pool);
a2.try_grow(25).unwrap_err();
assert_eq!(pool.reserved(), 30);
drop(a1);
assert_eq!(pool.reserved(), 0);
a2.try_grow(25).unwrap();
assert_eq!(pool.reserved(), 25);
}
#[test]
fn test_split() {
let pool = Arc::new(GreedyMemoryPool::new(50)) as _;
let mut r1 = MemoryConsumer::new("r1").register(&pool);
r1.try_grow(20).unwrap();
assert_eq!(r1.size(), 20);
assert_eq!(pool.reserved(), 20);
// take 5 from r1, should still have same reservation split
let r2 = r1.split(5);
assert_eq!(r1.size(), 15);
assert_eq!(r2.size(), 5);
assert_eq!(pool.reserved(), 20);
// dropping r1 frees 15 but retains 5 as they have the same consumer
drop(r1);
assert_eq!(r2.size(), 5);
assert_eq!(pool.reserved(), 5);
}
#[test]
fn test_new_empty() {
let pool = Arc::new(GreedyMemoryPool::new(50)) as _;
let mut r1 = MemoryConsumer::new("r1").register(&pool);
r1.try_grow(20).unwrap();
let mut r2 = r1.new_empty();
r2.try_grow(5).unwrap();
assert_eq!(r1.size(), 20);
assert_eq!(r2.size(), 5);
assert_eq!(pool.reserved(), 25);
}
#[test]
fn test_take() {
let pool = Arc::new(GreedyMemoryPool::new(50)) as _;
let mut r1 = MemoryConsumer::new("r1").register(&pool);
r1.try_grow(20).unwrap();
let mut r2 = r1.take();
r2.try_grow(5).unwrap();
assert_eq!(r1.size(), 0);
assert_eq!(r2.size(), 25);
assert_eq!(pool.reserved(), 25);
// r1 can still grow again
r1.try_grow(3).unwrap();
assert_eq!(r1.size(), 3);
assert_eq!(r2.size(), 25);
assert_eq!(pool.reserved(), 28);
}
}