peterxcli commented on code in PR #5493:
URL: https://github.com/apache/datafusion-comet/pull/5493#discussion_r3879394575


##########
spark/src/main/java/org/apache/spark/shuffle/comet/CometBoundedShuffleMemoryAllocator.java:
##########
@@ -112,6 +131,96 @@ public synchronized MemoryBlock allocate(long required) {
     return allocateMemoryBlock(size);
   }
 
+  /**
+   * Like {@link #allocate(long)}, but waits for other tasks of this shared 
pool to free memory,
+   * mirroring how Spark's unified memory manager blocks a task until memory 
becomes available.
+   * Callers must only use this after spilling their own buffered data. The 
wait fails fast when it
+   * can never succeed: when the request does not fit next to the memory this 
thread itself still
+   * retains (e.g. the sorter's pointer array), or when all allocated memory 
is retained by threads
+   * that are themselves blocked here and none of their requests fits in the 
free pool. Interrupting
+   * the task (e.g. task kill) aborts the wait.
+   */
+  @Override
+  public synchronized MemoryBlock allocateBlocking(long required) {
+    long size = Math.max(pageSize, required);
+    Thread self = Thread.currentThread();
+    long waitStart = 0;
+    long lastLog = 0;
+    try {
+      while (true) {
+        try {
+          return allocateMemoryBlock(size);
+        } catch (SparkOutOfMemoryError e) {
+          if (waitingThreads.put(self, size) == null) {
+            // Wake existing waiters so they re-evaluate the deadlock check 
against the enlarged
+            // waiting set.
+            notifyAll();
+          }
+          // This thread cannot free what it retains while it waits, so a 
request that does not
+          // fit next to its own retained memory can never be satisfied.
+          if (size > totalMemory - retainedMemory.getOrDefault(self, 0L)) {
+            throw e;
+          }
+          // The allocation just failed, so the request does not fit in the 
unallocated pool.
+          // Waiting can only succeed while some thread can still free memory: 
either a thread
+          // outside the waiting set retains pool memory, or another waiter's 
request fits in the
+          // free pool, in which case that waiter can proceed and eventually 
free what it retains.
+          if (allocatedMemory <= retainedByWaitingThreads() && 
!anyWaiterCanProceed()) {

Review Comment:
   Follow-up addressed in 4b592b9c2. The constructor cleanup now covers the 
whole tail of `SpillSorter`'s constructor — `serializeSchema` and the `Native` 
library load included — not just the pointer-array allocation. An `adopted` 
flag keeps ownership single: before `expandPointerArray` adopts the array it is 
freed directly (the sorter still owns only its initial one-entry array), and 
after adoption `inMemSorter.free()` releases it — avoiding a double free, which 
would trip `TaskMemoryManager`'s already-freed assertion in off-heap mode. 
Extended the constructor-leak regression test with your exact trigger: a nested 
field with `parquet.field.id = 2147483648` makes `ParquetUtils.getFieldId` 
throw `IllegalArgumentException` after the array is adopted, and the test then 
asserts a full-pool 1,048,576-byte allocation succeeds — which fails while the 
adopted 32-KiB array leaks. Also hardened the bypass writer's failure cleanup 
to skip null slots in case a mid-loop writer constructio
 n failure leaves the tail of `partitionWriters` unassigned.



-- 
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