andygrove commented on code in PR #5539:
URL: https://github.com/apache/datafusion-comet/pull/5539#discussion_r3886912606


##########
spark/src/main/java/org/apache/arrow/c/ArrowImporter.java:
##########
@@ -50,10 +51,21 @@ Field importField(ArrowSchema schema, 
CDataDictionaryProvider provider) {
 
   public FieldVector importVector(
       ArrowArray array, ArrowSchema schema, CDataDictionaryProvider provider) {
-    Field field = importField(schema, provider);
-    FieldVector vector = field.createVector(allocator);
-    ArrayImporter importer = new ArrayImporter(allocator, vector, provider);
-    importer.importArray(array);
-    return vector;
+    FieldVector vector = null;
+    try {
+      Field field = importField(schema, provider);
+      vector = field.createVector(allocator);
+      ArrayImporter importer = new ArrayImporter(allocator, vector, provider);
+      importer.importArray(array);
+      return vector;
+    } catch (RuntimeException | Error failure) {
+      if (vector != null) {
+        AutoCloseables.close(failure, vector);
+      }
+      if (!array.isClosed()) {

Review Comment:
   `isClosed()` is package-private and annotated `@VisibleForTesting` in Arrow. 
It works here because this class shares Arrow's package, and I do not think 
there is a better signal available.
   
   Could you add a short comment saying what it distinguishes, namely that 
`ArrayImporter.importArray` closes the source array before it can fail in 
`doImport`? Without that, a future Arrow upgrade that drops the method is 
likely to get fixed by deleting the guard, and that turns this into a double 
release rather than a leak.



##########
spark/src/main/scala/org/apache/comet/vector/NativeUtil.scala:
##########
@@ -253,16 +254,33 @@ class NativeUtil {
    */
   def importVector(arrays: Array[ArrowArray], schemas: Array[ArrowSchema]): 
Seq[CometVector] = {
     val arrayVectors = mutable.ArrayBuffer.empty[CometVector]
+    var firstUnconsumed = 0
 
-    (0 until arrays.length).foreach { i =>
-      val arrowSchema = schemas(i)
-      val arrowArray = arrays(i)
-
-      arrayVectors += CometVector.getVector(
-        importer.importVector(arrowArray, arrowSchema, dictionaryProvider),
-        dictionaryProvider)
+    try {
+      (0 until arrays.length).foreach { i =>
+        val arrowSchema = schemas(i)
+        val arrowArray = arrays(i)
+
+        firstUnconsumed = i + 1

Review Comment:
   Setting this before the import is the right call, but it is worth a comment 
saying why. It relies on `importer.importVector` consuming both the schema and 
the array on every exit, success or failure. The schema goes through 
`importField`'s `finally` and the array through the new catch in 
`ArrowImporter`. That is what makes it safe to exclude column `i` from 
`releaseArrowStructs`.
   
   Right now that invariant lives entirely in `ArrowImporter` and there is 
nothing here pointing at it. Getting it wrong in either direction gives you a 
leak or a double release.



##########
spark/src/test/scala/org/apache/comet/vector/NativeUtilSuite.scala:
##########
@@ -99,6 +103,99 @@ class NativeUtilSuite extends CometTestBase {
     }
   }
 
+  test("getNextBatch releases imported vectors and Arrow structs when vector 
import fails") {
+    withIsolatedStructAllocator { (nativeUtil, allocator, _) =>
+      Using.resource(new IntVector("value", allocator)) { vector =>
+        vector.allocateNew(4)
+        vector.setSafe(0, 42)
+        vector.setValueCount(1)
+        val failure = intercept[IllegalStateException] {
+          nativeUtil.getNextBatch(
+            2,
+            (arrays, schemas) => {
+              Data.exportVector(
+                allocator,
+                vector,
+                null,
+                ArrowArray.wrap(arrays(0)),
+                ArrowSchema.wrap(schemas(0)))
+              vector.close()
+              1L
+            })
+        }
+        assert(failure.getMessage == "Cannot import released ArrowSchema")
+        assert(failure.getSuppressed.isEmpty)
+        assert(allocator.getAllocatedMemory == 0)
+      }
+    }
+  }
+
+  test("getNextBatch releases an imported vector when its Comet wrapper 
rejects the type") {
+    withIsolatedStructAllocator { (nativeUtil, allocator, _) =>
+      Using.resource(new UInt4Vector("value", allocator)) { vector =>
+        vector.allocateNew(1)
+        vector.setSafe(0, 42)
+        vector.setValueCount(1)
+        val failure = intercept[UnsupportedOperationException] {
+          nativeUtil.getNextBatch(
+            2,
+            (arrays, schemas) => {
+              Data.exportVector(
+                allocator,
+                vector,
+                null,
+                ArrowArray.wrap(arrays(0)),
+                ArrowSchema.wrap(schemas(0)))
+              vector.close()
+              1L
+            })
+        }
+        assert(failure.getSuppressed.isEmpty)
+        assert(allocator.getAllocatedMemory == 0)
+      }
+    }
+  }
+
+  test("importVector releases partially imported vectors when Arrow array 
import fails") {
+    withIsolatedStructAllocator { (nativeUtil, allocator, _) =>
+      val intType = FieldType.nullable(new ArrowType.Int(32, true))
+      val stringType = FieldType.nullable(new ArrowType.Utf8())
+      val intStruct = StructVector.empty("int_struct", allocator)
+      val firstInt = intStruct.addOrGet("first", intType, classOf[IntVector])
+      val secondInt = intStruct.addOrGet("second", intType, classOf[IntVector])
+      intStruct.allocateNew()
+      intStruct.setIndexDefined(0)
+      firstInt.setSafe(0, 1)
+      secondInt.setSafe(0, 2)
+      intStruct.setValueCount(1)
+
+      val stringStruct = StructVector.empty("string_struct", allocator)
+      stringStruct.addOrGet("first", intType, classOf[IntVector])
+      stringStruct.addOrGet("second", stringType, classOf[VarCharVector])
+      stringStruct.allocateNew()
+      stringStruct.setValueCount(1)
+
+      val (arrays, schemas) = nativeUtil.allocateArrowStructs(2)
+      Data.exportVector(allocator, intStruct, null, arrays(0), schemas(0))
+      Data.exportVector(allocator, stringStruct, null, arrays(1), schemas(1))
+      intStruct.close()
+      stringStruct.close()

Review Comment:
   `intStruct` and `stringStruct` are closed unconditionally before the `try`, 
so if either `Data.exportVector` throws they leak and the failure surfaces as 
an allocator complaint rather than the real error. You use `Using.resource` in 
the two tests above. Could this one do the same?



##########
spark/src/main/scala/org/apache/comet/vector/NativeUtil.scala:
##########
@@ -253,16 +254,33 @@ class NativeUtil {
    */
   def importVector(arrays: Array[ArrowArray], schemas: Array[ArrowSchema]): 
Seq[CometVector] = {
     val arrayVectors = mutable.ArrayBuffer.empty[CometVector]
+    var firstUnconsumed = 0
 
-    (0 until arrays.length).foreach { i =>
-      val arrowSchema = schemas(i)
-      val arrowArray = arrays(i)
-
-      arrayVectors += CometVector.getVector(
-        importer.importVector(arrowArray, arrowSchema, dictionaryProvider),
-        dictionaryProvider)
+    try {
+      (0 until arrays.length).foreach { i =>
+        val arrowSchema = schemas(i)
+        val arrowArray = arrays(i)
+
+        firstUnconsumed = i + 1
+        val arrowVector = importer.importVector(arrowArray, arrowSchema, 
dictionaryProvider)
+        var cometVector: CometVector = null
+        try {
+          cometVector = CometVector.getVector(arrowVector, dictionaryProvider)
+          arrayVectors += cometVector
+        } catch {
+          case failure: Throwable =>
+            val rollback = if (cometVector == null) arrowVector else 
cometVector

Review Comment:
   The `var` plus null check plus `rollback` makes the reader reason about a 
case that cannot happen, since `arrayVectors += cometVector` will not throw. 
This is equivalent and drops both:
   
   ```scala
   val cometVector =
     try CometVector.getVector(arrowVector, dictionaryProvider)
     catch {
       case failure: Throwable =>
         AutoCloseables.close(failure, arrowVector)
         throw failure
     }
   arrayVectors += cometVector
   ```
   
   In code whose whole job is memory ownership, the fewer branches a reader has 
to hold in their head the better.



##########
spark/src/main/java/org/apache/arrow/c/ArrowImporter.java:
##########
@@ -50,10 +51,21 @@ Field importField(ArrowSchema schema, 
CDataDictionaryProvider provider) {
 
   public FieldVector importVector(
       ArrowArray array, ArrowSchema schema, CDataDictionaryProvider provider) {
-    Field field = importField(schema, provider);
-    FieldVector vector = field.createVector(allocator);
-    ArrayImporter importer = new ArrayImporter(allocator, vector, provider);
-    importer.importArray(array);
-    return vector;
+    FieldVector vector = null;
+    try {
+      Field field = importField(schema, provider);
+      vector = field.createVector(allocator);
+      ArrayImporter importer = new ArrayImporter(allocator, vector, provider);
+      importer.importArray(array);
+      return vector;
+    } catch (RuntimeException | Error failure) {
+      if (vector != null) {
+        AutoCloseables.close(failure, vector);

Review Comment:
   For a dictionary-encoded column, `ArrayImporter.doImport` loads the 
dictionary values into the provider's vector before it loads the main data, and 
those buffers reference the same `ReferenceCountedArrowArray` as the column 
itself. If the main data import then fails, closing `vector` here does not drop 
those references, so the C release callback only fires when 
`NativeUtil.close()` closes the provider. The same applies when 
`CometVector.getVector` throws on a dictionary column, since the rollback there 
is only the indices vector.
   
   Does that match your reading? It is a deferred release rather than a 
permanent leak, and it is bounded by task lifetime. But the trigger this is 
aimed at is allocator pressure, which is exactly when holding a native batch 
until end of task hurts most. Would clearing the failing column's dictionary 
vector be enough? A test with a dictionary-encoded column would pin the 
behavior down either way.



##########
spark/src/main/scala/org/apache/comet/vector/NativeUtil.scala:
##########
@@ -253,16 +254,33 @@ class NativeUtil {
    */
   def importVector(arrays: Array[ArrowArray], schemas: Array[ArrowSchema]): 
Seq[CometVector] = {

Review Comment:
   Could you extend the scaladoc to say that on failure this method releases 
everything it was given? `getNextBatch` does its own cleanup on the other two 
exits, so the next person reading it could reasonably wrap this call in cleanup 
too and cause a double release.



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