LuciferYang opened a new issue, #12716:
URL: https://github.com/apache/gluten/issues/12716

   ### Backend
   
   VL (Velox). The code is in `gluten-core` and is shared by every append site.
   
   ### Bug description
   
   `Spillers.AppendableSpillerList` holds a plain `ArrayList` that `append` 
mutates and `spill` iterates, with no lock on either side. The two run on 
different threads.
   
   `NativeMemoryManager` hands the list to `ReservationListeners`, which 
registers it as a node of the task's memory tree, before anything is appended 
to it. The shuffle writers then append their own spiller on the first non-empty 
batch, which is well after `records.next()` started driving the upstream 
pipeline. On the other side `TreeMemoryTargets#spillTree` walks every consumer 
of the task by design, per its own comment in `MemoryTargets`: "Spill from root 
node so other consumers also get spilled". So a spill triggered by any consumer 
reaches every other consumer's list, across runtime boundaries. The triggering 
thread need not be the task thread either: an allocation on a Velox io thread 
goes through that runtime's `ReservationListener`, so an async split prefetch 
can fail to reserve and start a root-level walk while the task thread is back 
in Java appending. The walk stays inside the loop across a JNI shrink or 
reclaim call, so the window is milliseconds rather than one instructi
 on.
   
   Two symptoms follow, with different likelihoods.
   
   The common one is a silent skip. `ArrayList`'s `size` and `modCount` are not 
volatile and `add` is unsynchronized, so with no happens-before edge the 
walking thread may observe neither write, end the walk early, and 
under-reclaim. `ThrowOnOomMemoryTarget.borrow` retries the reservation and each 
retry re-walks the tree, so this defers one round of reclaim rather than losing 
it.
   
   The louder one is a `ConcurrentModificationException`. Once the append is 
observed the CME is certain rather than a narrow race: the append raises 
`size`, so `hasNext` stays true, so `next()` runs, and its first act is the 
`modCount` check. It surfaces through the JNI boundary as `Error during calling 
Java code from native code: java.util.ConcurrentModificationException`.
   
   GLUTEN-11509 was this same race one field over, on 
`TreeMemoryConsumer#children`, with a production stack trace from the Delta 
stats writer thread. Its fix (#11553) switched that map to `ConcurrentHashMap` 
and left this list alone. That issue noted the main branch had no asynchronous 
use of the memory tree yet; the per-runtime hooked executor for Velox io 
threads (#11882, #12302) and the Delta native statistics writer (#11419) both 
supply one now.
   
   Five call sites append to such a list: `NativeMemoryManager.scala:59` (in 
the constructor), `NativePlanEvaluator.java:94`, and the three shuffle writers 
(`ColumnarShuffleWriter.scala:194`, 
`VeloxCelebornColumnarShuffleWriter.scala:184`, 
`VeloxUniffleColumnarShuffleWriter.java:195`). The first two append while their 
`NativeMemoryManager` is still being constructed, so no other thread of the 
task is allocating yet. The shuffle writers are the reachable ones, because 
their list is registered when the writer is built and appended to only on the 
first non-empty batch.
   
   ### Gluten version
   
   main (1.8.0-SNAPSHOT)
   
   ### Spark version
   
   Version-agnostic (applies to spark-3.3 / 3.4 / 3.5 / 4.0 / 4.1).
   
   ### Spark configurations
   
   All defaults. A multi-core executor so `taskSlots > 1` keeps the 
`RetryOnOomMemoryTarget` root-level walk in play, 
`spark.gluten.memory.isolation=false` (default), 
`spark.memory.offHeap.enabled=true`, and Velox io threads default to 
`numTaskSlotsPerExecutor`. The query needs a columnar shuffle over a file 
source so the io threads are prefetching while the writer appends, plus enough 
memory pressure to trigger a spill.
   
   ### System information
   
   Not applicable. The affected code is 
`gluten-core/src/main/java/org/apache/gluten/memory/memtarget/Spillers.java`, 
and does not depend on OS or hardware.
   
   ### Relevant logs
   
   From GLUTEN-11509, the same walk on the sibling field, for the shape of the 
failure:
   
   ```text
   org.apache.gluten.exception.GlutenException: Error during calling Java code 
from native code: java.util.ConcurrentModificationException
     at java.base/java.util.HashMap$HashIterator.nextNode(HashMap.java:1597)
     at 
org.apache.gluten.memory.memtarget.TreeMemoryTargets.spillTree(TreeMemoryTargets.java:63)
     at 
org.apache.gluten.memory.memtarget.spark.TreeMemoryConsumer.spill(TreeMemoryConsumer.java:116)
     at 
org.apache.spark.memory.TaskMemoryManager.trySpillAndAcquire(TaskMemoryManager.java:228)
     at 
org.apache.spark.memory.TaskMemoryManager.acquireExecutionMemory(TaskMemoryManager.java:191)
   ```
   
   ### Fix direction
   
   Use `CopyOnWriteArrayList`. The two sides cannot be brought under one lock 
cheaply, iteration is held open across a JNI spill, and appends are two per 
list per task while walks are on the reclaim path, so the copy is cheap. 
Snapshot iteration also stays correct if a spiller ever appends during its own 
spill, which locking `append` would not cover. Declaring the field as the 
concrete type makes a revert to `ArrayList` a compile error, and `@ThreadSafe` 
records the guarantee since `MemoryTarget` documents the opposite default.
   
   Note what this does not change: a copy-on-write iterator is a snapshot, so 
an append landing mid-walk still misses that round. An index walk would pick it 
up and be equally thread-safe, but would let a spiller that appends during its 
own spill extend a single walk without bound. Snapshot iteration fixes the work 
per round and leans on the existing retry.
   


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