Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 41 additions & 12 deletions native/core/src/execution/memory_pools/fair_pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,9 @@ use jni::objects::{Global, JObject};

use crate::{errors::CometResult, jvm_bridge::JVMClasses};
use datafusion::common::resources_err;
use datafusion::execution::memory_pool::MemoryConsumer;
use datafusion::{
common::DataFusionError,
execution::memory_pool::{MemoryPool, MemoryReservation},
execution::memory_pool::{MemoryConsumer, MemoryLimit, MemoryPool, MemoryReservation},
};
use parking_lot::Mutex;

Expand All @@ -44,6 +43,17 @@ struct CometFairPoolState {
num: usize,
}

fn fair_limit_exceeded(
pool_size: usize,
num: usize,
reservation: &MemoryReservation,
additional: usize,
) -> Option<(usize, usize)> {
let used = reservation.size();
let limit = pool_size.checked_div(num).expect("overflow in checked_div");
(limit < used.saturating_add(additional)).then_some((used, limit))
}

impl Debug for CometFairMemoryPool {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
let state = self.state.lock();
Expand Down Expand Up @@ -142,21 +152,15 @@ impl MemoryPool for CometFairMemoryPool {

fn try_grow(
&self,
_reservation: &MemoryReservation,
reservation: &MemoryReservation,
additional: usize,
) -> Result<(), DataFusionError> {
if additional > 0 {
let mut state = self.state.lock();
let num = state.num;
let limit = self
.pool_size
.checked_div(num)
.expect("overflow in checked_div");
// We use state.used instead of reservation.size() because DataFusion 53+
// calls pool.try_grow() before incrementing the reservation's atomic size,
// so reservation.size() would not include prior grows.
let used = state.used;
if limit < used + additional {
if let Some((used, limit)) =
fair_limit_exceeded(self.pool_size, num, reservation, additional)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Preserve the aggregate configured pool limit

This replaces the only aggregate admission bound with a per-reservation check. With spark.memory.offHeap.size=64m and spark.comet.exec.memoryPool.fraction=0.5, pool_size is 32 MiB. A can register and reserve 24 MiB; after B registers, B's request for 16 MiB now passes (0 + 16 <= 32 / 2), leaving 40 MiB reserved against the configured 32 MiB budget. The base rejects B's request. Similarly, new_empty() shares its consumer registration while starting a separate size counter, so sibling reservations can collectively exceed the budget; DataFusion's native sort merge uses this pattern.

Spark's JVM memory manager only applies its larger off-heap budget, not Comet's fraction, so it can grant these requests. This consumes the headroom that the tuning guide explicitly reserves to avoid OOM from untracked allocations. Please retain the corrected requester check and add a separate aggregate state.used versus pool_size bound; this does not require the spillability-policy change in #5465.

Both excess-allocation sequences were reproduced against unchanged head/base pool code in a harness with mocked JNI grants; Spark's larger backing limit was verified separately in source.

{
return resources_err!(
"Failed to acquire {additional} bytes where {used} bytes already reserved and the fair limit is {limit} bytes, {num} registered"
);
Expand Down Expand Up @@ -187,4 +191,29 @@ impl MemoryPool for CometFairMemoryPool {
fn reserved(&self) -> usize {
self.state.lock().used
}

fn memory_limit(&self) -> MemoryLimit {
MemoryLimit::Finite(self.pool_size)
}
}

#[cfg(test)]
mod tests {
use super::*;
use datafusion::execution::memory_pool::UnboundedMemoryPool;

#[test]
fn fair_share_uses_requesting_reservation_and_reports_pool_limit() {
let backing: Arc<dyn MemoryPool> = Arc::new(UnboundedMemoryPool::default());
let other = MemoryConsumer::new("other").register(&backing);
let requesting = MemoryConsumer::new("requesting").register(&backing);
other.grow(10);
requesting.grow(6);

assert_eq!(fair_limit_exceeded(32, 2, &requesting, 10), None);
assert_eq!(fair_limit_exceeded(32, 2, &requesting, 11), Some((6, 16)));

let pool = CometFairMemoryPool::new(Arc::new(Global::null()), 32);
assert!(matches!(pool.memory_limit(), MemoryLimit::Finite(32)));
}
}
Loading