JingsongLi commented on code in PR #73:
URL: https://github.com/apache/paimon-mosaic/pull/73#discussion_r3789633890


##########
java/src/main/java/org/apache/paimon/mosaic/MosaicWriter.java:
##########
@@ -69,19 +73,153 @@ public MosaicWriter(OutputStream outputStream, Schema 
arrowSchema, WriterOptions
         }
     }
 
+    /**
+     * Writes an Arrow batch synchronously.
+     *
+     * <p>All top-level and nested field vectors must share one allocator 
root. That root may be
+     * independent from the writer allocator root; temporary Arrow C Data 
metadata remains charged
+     * to the writer allocator supplied at construction time. The caller 
retains ownership of the
+     * batch and must keep it open until this method returns.
+     *
+     * @param root batch to write
+     * @throws IllegalArgumentException if field vectors use different 
allocator roots
+     * @throws IllegalStateException if the writer is closed
+     */
     public void write(VectorSchemaRoot root) {
         if (closed || handle == 0) {
             throw new IllegalStateException("writer is closed");
         }
+        boolean sameAllocatorRoot = sharesWriterAllocatorRoot(root);
         try (ArrowArray arrowArray = ArrowArray.allocateNew(allocator);
              ArrowSchema arrowSchema = ArrowSchema.allocateNew(allocator)) {
             try {
-                Data.exportVectorSchemaRoot(allocator, root, null, arrowArray, 
arrowSchema);
+                if (sameAllocatorRoot) {
+                    Data.exportVectorSchemaRoot(
+                            allocator, root, null, arrowArray, arrowSchema);
+                } else {
+                    Data.exportSchema(allocator, root.getSchema(), null, 
arrowSchema);
+                    exportCrossRootArray(allocator, root, arrowArray);
+                }
                 NativeLib.nativeWriterWriteBatch(handle, 
arrowArray.memoryAddress(), arrowSchema.memoryAddress());
             } finally {
                 releaseExported(arrowArray);
                 releaseExported(arrowSchema);
             }
+        } catch (Throwable failure) {
+            throw propagateNativeFailure("write batch failed", failure);
+        }
+    }
+
+    private static void exportCrossRootArray(
+            BufferAllocator exportAllocator, VectorSchemaRoot root, ArrowArray 
arrowArray) {
+        // Data.exportVectorSchemaRoot reloads every field into a temporary 
StructVector. If that
+        // reload fails before the root ArrowArray owns a release callback, 
Arrow 15 can retain
+        // already-associated input buffers. Export each child directly 
instead: input buffers are
+        // retained without being associated with the writer allocator, while 
temporary C Data
+        // metadata remains charged to that allocator.
+        RootArrayPrivateData privateData = new RootArrayPrivateData();
+        try {
+            privateData.bufferPointers = exportAllocator.buffer(Long.BYTES);
+            privateData.bufferPointers.writeLong(0L);
+
+            List<FieldVector> vectors = root.getFieldVectors();
+            if (!vectors.isEmpty()) {
+                privateData.childPointers =
+                        exportAllocator.buffer((long) vectors.size() * 
Long.BYTES);
+                for (int i = 0; i < vectors.size(); i++) {
+                    ArrowArray child = ArrowArray.allocateNew(exportAllocator);
+                    privateData.children.add(child);
+                    privateData.childPointers.writeLong(child.memoryAddress());
+                }
+                for (int i = 0; i < vectors.size(); i++) {
+                    Data.exportVector(
+                            exportAllocator, vectors.get(i), null, 
privateData.children.get(i));
+                }
+            }
+
+            ArrowArray.Snapshot snapshot = new ArrowArray.Snapshot();
+            snapshot.length = root.getRowCount();
+            snapshot.null_count = 0;
+            snapshot.offset = 0;
+            snapshot.n_buffers = 1;
+            snapshot.n_children = vectors.size();
+            snapshot.buffers = privateData.bufferPointers.memoryAddress();
+            snapshot.children =
+                    privateData.childPointers == null
+                            ? 0
+                            : privateData.childPointers.memoryAddress();
+            snapshot.dictionary = 0;
+            snapshot.release = 0;
+            arrowArray.save(snapshot);
+            JniWrapper.get().exportArray(arrowArray.memoryAddress(), 
privateData);
+        } catch (RuntimeException | Error failure) {
+            privateData.abort(failure);

Review Comment:
   [P1] Release the registered root callback before aborting child metadata
   
   `JniWrapper.exportArray` can return with an `OutOfMemoryError` after Arrow 
15 has already installed the root `ArrowArray` release callback: its JNI code 
does not check the result of `NewGlobalRef(privateData)` before assigning 
`private_data` and `release`. In that state, `privateData.abort()` frees 
`childPointers` and the child structs, but the outer `finally` then invokes the 
still-registered root callback, whose first step is to walk `root.children`. 
That dereferences freed native memory and can crash or corrupt the JVM under 
memory pressure.
   
   Please branch on root ownership here: when `arrowArray.snapshot().release != 
0`, release the root first while its child pointer table is still valid, then 
call the idempotent `privateData.close()` as a fallback. Use the current manual 
`abort()` path only while the root release callback is still zero. A 
fault-injection test that throws after root callback registration would cover 
this window.



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

Reply via email to