sunchao commented on code in PR #5613:
URL: https://github.com/apache/datafusion-comet/pull/5613#discussion_r4105107272


##########
native/core/src/execution/memory_pools/fair_pool.rs:
##########
@@ -148,37 +295,90 @@ impl MemoryPool for CometFairMemoryPool {
         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 {
-                return resources_err!(
-                    "Failed to acquire {additional} bytes where {used} bytes 
already reserved ({} bytes overcommitted) and the fair limit is {limit} bytes, 
{num} registered",
-                    self.spark.overcommit()
-                );
-            }
-
-            // A partial grant is handed back and refused, which triggers 
spilling in the caller.
-            if let Err(refusal) = self.spark.try_acquire(additional)? {
-                return resources_err!(
-                    "Failed to acquire {} bytes plus {} bytes overcommitted, 
only got {} bytes. Reserved: {} bytes",
-                    additional,
-                    refusal.overcommit,
-                    refusal.granted,
-                    state.used
-                );
-            }
-            state.used = state
-                .used
-                .checked_add(additional)
-                .expect("overflow in checked_add");
+            // Checking the fair limit and reserving the bytes is one atomic 
step, so concurrent
+            // grows can never jointly exceed pool_size / num. The blocking 
JVM calls then run
+            // without any lock held, and the reservation rolls back if the 
JVM does not back it.
+            {
+                let mut state = self.state.lock();
+                let num = state.num;
+                let limit = self
+                    .pool_size
+                    .checked_div(num)
+                    .expect("overflow in checked_div");
+                // The pool tracks one total across every consumer and checks 
the fair limit
+                // against that total, not against this reservation's own size.
+                let used = state.used;
+                match used.checked_add(additional) {
+                    Some(total) if total <= limit => state.used = total,
+                    _ => {
+                        return resources_err!(
+                            "Failed to acquire {additional} bytes where {used} 
bytes already reserved ({} bytes overcommitted) and the fair limit is {limit} 
bytes, {num} registered",
+                            self.spark.overcommit()
+                        );
+                    }
+                }
+            }
+
+            // The anchor comes after the local limit check, so a grow the 
pool rejects itself
+            // never makes a JVM call, and before the real request, so the 
byte is held before
+            // the balance can reach zero. The JVM call can panic inside its 
JNI frame; the
+            // optimistic reservation must not outlive either call, or the 
leaked bytes poison
+            // the task-shared pool for every other consumer.
+            match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
+                self.take_missing_anchor()
+            })) {
+                Ok(Ok(())) => {}
+                Ok(Err(e)) => {
+                    self.settle_acquire(additional, 0);
+                    return Err(e.into());
+                }
+                Err(panic) => {
+                    self.settle_acquire(additional, 0);
+                    std::panic::resume_unwind(panic);
+                }
+            }
+            // Spark is asked for the request plus any outstanding overcommit, 
and a full grant
+            // repays the overcommit. A short grant stays with Spark until 
this pool hands it
+            // back below, so the bytes can stay charged meanwhile.
+            let refusal = match 
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
+                self.spark.try_acquire_leaving_a_short_grant(additional)

Review Comment:
   [P1] [P1] Prevent `showMemoryUsage` from blocking short-grant rollback
   
   Allowing these calls to overlap exposes a JVM lock cycle. 
`CometTaskMemoryManager.acquireMemory` releases Spark's task monitor after 
acquiring memory, then reacquires it in `showMemoryUsage` when the grant is 
short. In a 100-byte executor pool, let other tasks hold 82 and 1 bytes and 
this pool hold its anchor. A 30-byte request receives 16. Before its diagnostic 
reacquires the monitor, release the other task's 1 byte and start a second 
10-byte request. The second request parks below its new 25-byte minimum while 
holding the task monitor. The first blocks in `showMemoryUsage`, so it cannot 
return to Rust and release the 16-byte short grant that would satisfy the 
second request. Both requests fit the local fair limit with two consumers.
   
   Expected behavior is to return the short grant, spill, and let the second 
request proceed. Instead, the task can hang until an unrelated task frees 
memory. The previous fair-pool mutex excluded this interleaving. Could the 
bridge avoid reacquiring the task monitor after obtaining the grant, or cover 
acquisition and diagnostics with one monitor scope while keeping releases 
independent?
   
   Evidence: Bounded reproduction: 
`/tmp/pr5613-bb273df5-jvm-probe/ShortGrantProbe.scala`, compiled with the 
unchanged head's `CometTaskMemoryManager.java` against Spark 4.1.3. A logging 
latch schedules the interleaving and is released before observing the failure. 
Output in `result.log`: `head: first=BLOCKED, second=WAITING, held=16+anchor`. 
Stacks identify `TaskMemoryManager.showMemoryUsage:325` and 
`ExecutionMemoryPool.acquireMemory:142`. Releasing 50 bytes from the unrelated 
task rescues both threads. A control emulating the previous serialization 
completes without that release and ends with zero balance. Source inspection 
confirms the same monitor structure across all five checked Spark versions.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to