You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iceberg.apache.org by pv...@apache.org on 2026/04/24 13:49:12 UTC

(iceberg) branch main updated: ORC: Fix connection leak in OrcIterable (#16086)

This is an automated email from the ASF dual-hosted git repository.

pvary pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg.git


The following commit(s) were added to refs/heads/main by this push:
     new f475ccb5c5 ORC: Fix connection leak in OrcIterable (#16086)
f475ccb5c5 is described below

commit f475ccb5c579b8a1d4842507b12e854161dea7af
Author: Denys Kuzmenko <dk...@cloudera.com>
AuthorDate: Fri Apr 24 16:49:05 2026 +0300

    ORC: Fix connection leak in OrcIterable (#16086)
---
 .../java/org/apache/iceberg/orc/OrcIterable.java   |   2 +
 .../orc/TestOrcIterableResourceCleanup.java        | 133 +++++++++++++++++++++
 2 files changed, 135 insertions(+)

diff --git a/orc/src/main/java/org/apache/iceberg/orc/OrcIterable.java b/orc/src/main/java/org/apache/iceberg/orc/OrcIterable.java
index 119b3c54f2..0f65f1b65d 100644
--- a/orc/src/main/java/org/apache/iceberg/orc/OrcIterable.java
+++ b/orc/src/main/java/org/apache/iceberg/orc/OrcIterable.java
@@ -103,6 +103,8 @@ class OrcIterable<T> extends CloseableGroup implements CloseableIterable<T> {
 
     VectorizedRowBatchIterator rowBatchIterator =
         newOrcIterator(file, readOrcSchema, start, length, orcFileReader, sarg, recordsPerBatch);
+    addCloseable(rowBatchIterator);
+
     if (batchReaderFunction != null) {
       OrcBatchReader<T> batchReader = (OrcBatchReader<T>) batchReaderFunction.apply(readOrcSchema);
       return CloseableIterator.transform(
diff --git a/orc/src/test/java/org/apache/iceberg/orc/TestOrcIterableResourceCleanup.java b/orc/src/test/java/org/apache/iceberg/orc/TestOrcIterableResourceCleanup.java
new file mode 100644
index 0000000000..6e819af557
--- /dev/null
+++ b/orc/src/test/java/org/apache/iceberg/orc/TestOrcIterableResourceCleanup.java
@@ -0,0 +1,133 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.iceberg.orc;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import org.apache.iceberg.Files;
+import org.apache.iceberg.PartitionSpec;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.data.GenericRecord;
+import org.apache.iceberg.data.Record;
+import org.apache.iceberg.data.orc.GenericOrcReader;
+import org.apache.iceberg.data.orc.GenericOrcWriter;
+import org.apache.iceberg.io.CloseableIterable;
+import org.apache.iceberg.io.CloseableIterator;
+import org.apache.iceberg.io.DataWriter;
+import org.apache.iceberg.io.InputFile;
+import org.apache.iceberg.io.OutputFile;
+import org.apache.iceberg.io.SeekableInputStream;
+import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
+import org.apache.iceberg.relocated.com.google.common.collect.Lists;
+import org.apache.iceberg.types.Types;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.mockito.Mockito;
+
+public class TestOrcIterableResourceCleanup {
+
+  private static final Schema SCHEMA =
+      new Schema(
+          Types.NestedField.required(1, "id", Types.LongType.get()),
+          Types.NestedField.optional(2, "data", Types.StringType.get()));
+
+  @TempDir private File temp;
+
+  @Test
+  public void testClosingIterableClosesAllStreams() throws IOException {
+    List<SeekableInputStream> inputStreams = Lists.newArrayList();
+    InputFile inputFile = spyOnStreams(writeTestOrcFile(), inputStreams);
+
+    try (CloseableIterable<Record> iterable = newOrcIterable(inputFile)) {
+      try (CloseableIterator<Record> iterator = iterable.iterator()) {
+        drain(iterator);
+      }
+    }
+
+    verifyAllStreamsClosed(inputStreams);
+  }
+
+  @Test
+  public void testClosingIterableClosesIteratorResources() throws IOException {
+    List<SeekableInputStream> inputStreams = Lists.newArrayList();
+    InputFile inputFile = spyOnStreams(writeTestOrcFile(), inputStreams);
+
+    // Without addCloseable(rowBatchIterator) in OrcIterable, the VectorizedRowBatchIterator
+    // and its RecordReader are never closed, leaking ORC input streams / file handles.
+    for (int round = 0; round < 5; round++) {
+      try (CloseableIterable<Record> iterable = newOrcIterable(inputFile)) {
+        drain(iterable.iterator());
+      }
+    }
+
+    verifyAllStreamsClosed(inputStreams);
+  }
+
+  private static <T> void drain(CloseableIterator<T> iterator) {
+    while (iterator.hasNext()) {
+      iterator.next();
+    }
+  }
+
+  private InputFile writeTestOrcFile() throws IOException {
+    OutputFile outputFile = Files.localOutput(File.createTempFile("test", ".orc", temp));
+    try (DataWriter<Record> writer =
+        ORC.writeData(outputFile)
+            .schema(SCHEMA)
+            .createWriterFunc(GenericOrcWriter::buildWriter)
+            .overwrite()
+            .withSpec(PartitionSpec.unpartitioned())
+            .build()) {
+      GenericRecord record = GenericRecord.create(SCHEMA);
+      for (int i = 0; i < 10; i++) {
+        writer.write(record.copy(ImmutableMap.of("id", (long) i, "data", "val" + i)));
+      }
+    }
+
+    return outputFile.toInputFile();
+  }
+
+  private static CloseableIterable<Record> newOrcIterable(InputFile input) {
+    return ORC.read(input)
+        .project(SCHEMA)
+        .createReaderFunc(fileSchema -> GenericOrcReader.buildReader(SCHEMA, fileSchema))
+        .build();
+  }
+
+  private static void verifyAllStreamsClosed(List<SeekableInputStream> streams) throws IOException {
+    for (SeekableInputStream stream : streams) {
+      Mockito.verify(stream, Mockito.times(1)).close();
+    }
+  }
+
+  private static InputFile spyOnStreams(InputFile delegate, List<SeekableInputStream> streams) {
+    InputFile inputFile = Mockito.spy(delegate);
+    Mockito.doAnswer(
+            invocation -> {
+              SeekableInputStream real = (SeekableInputStream) invocation.callRealMethod();
+              SeekableInputStream inputStream = Mockito.spy(real);
+              streams.add(inputStream);
+              return inputStream;
+            })
+        .when(inputFile)
+        .newStream();
+    return inputFile;
+  }
+}