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

JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git


The following commit(s) were added to refs/heads/master by this push:
     new 5a702aca45 [format] Close the ParquetFileReader when post-construction 
setup fails (#9576)
5a702aca45 is described below

commit 5a702aca45cf747834abbd199bfac18f06d85900
Author: YangJie <[email protected]>
AuthorDate: Fri Sep 4 03:24:12 2026 -0400

    [format] Close the ParquetFileReader when post-construction setup fails 
(#9576)
---
 .../format/parquet/ParquetReaderFactory.java       | 108 ++++++++--------
 .../parquet/ParquetReaderFactoryLeakTest.java      | 139 +++++++++++++++++++++
 2 files changed, 198 insertions(+), 49 deletions(-)

diff --git 
a/paimon-format/src/main/java/org/apache/paimon/format/parquet/ParquetReaderFactory.java
 
b/paimon-format/src/main/java/org/apache/paimon/format/parquet/ParquetReaderFactory.java
index 2874849bb5..ab7d277a62 100644
--- 
a/paimon-format/src/main/java/org/apache/paimon/format/parquet/ParquetReaderFactory.java
+++ 
b/paimon-format/src/main/java/org/apache/paimon/format/parquet/ParquetReaderFactory.java
@@ -151,57 +151,67 @@ public class ParquetReaderFactory implements 
FormatReaderFactory {
             throw t;
         }
 
-        ShreddingReadPlan readPlan =
-                ShreddingReadPlanFactories.createReadPlan(
-                        readType,
-                        readFieldMetadata(reader),
-                        fileSchema,
-                        shreddingReadPlanFactories(readType));
-        DataField[] physicalReadFields = 
readFields(readPlan.physicalRowType());
-        RequestedSchema requestedSchema =
-                readPlan.isIdentity()
-                        ? getOrCreateRequestedSchema(fileSchema)
-                        : createRequestedSchema(fileSchema, 
physicalReadFields);
-
-        if (LOG.isDebugEnabled()) {
-            LOG.debug(
-                    "Create reader of the parquet file {}, the fileSchema is 
{}, the requestedSchema is {}.",
-                    context.filePath(),
-                    fileSchema,
-                    requestedSchema.messageType);
-        }
-
-        int configuredBatchSize = computeBatchSize(reader, 
requestedSchema.messageType);
-        Preconditions.checkArgument(
-                configuredBatchSize > 0,
-                "Parquet read batch size should be positive: %s",
-                configuredBatchSize);
-        ReadBatchSizer readBatchSizer = context.readBatchSizer();
-        int initialBatchSize =
-                readBatchSizer == null
-                        ? configuredBatchSize
-                        : 
readBatchSizer.batchSize().orElse(configuredBatchSize);
-        reader.setRequestedSchema(requestedSchema.messageType);
-        WritableColumnVector[] writableVectors =
-                createWritableVectors(initialBatchSize, physicalReadFields);
-        IntFunction<WritableColumnVector[]> vectorFactory =
-                size -> createWritableVectors(size, physicalReadFields);
-
-        VectorizedParquetRecordReader parquetReader =
-                new VectorizedParquetRecordReader(
+        // The reader owns the open stream from here, so close it if the setup 
below fails.
+        try {
+            ShreddingReadPlan readPlan =
+                    ShreddingReadPlanFactories.createReadPlan(
+                            readType,
+                            readFieldMetadata(reader),
+                            fileSchema,
+                            shreddingReadPlanFactories(readType));
+            DataField[] physicalReadFields = 
readFields(readPlan.physicalRowType());
+            RequestedSchema requestedSchema =
+                    readPlan.isIdentity()
+                            ? getOrCreateRequestedSchema(fileSchema)
+                            : createRequestedSchema(fileSchema, 
physicalReadFields);
+
+            if (LOG.isDebugEnabled()) {
+                LOG.debug(
+                        "Create reader of the parquet file {}, the fileSchema 
is {}, the requestedSchema is {}.",
                         context.filePath(),
-                        reader,
                         fileSchema,
-                        requestedSchema.fields,
-                        writableVectors,
-                        initialBatchSize,
-                        configuredBatchSize,
-                        context.fileIO(),
-                        readBatchSizer,
-                        vectorFactory);
-        return readPlan.isIdentity()
-                ? parquetReader
-                : new ShreddingFormatReader(parquetReader, readPlan);
+                        requestedSchema.messageType);
+            }
+
+            int configuredBatchSize = computeBatchSize(reader, 
requestedSchema.messageType);
+            Preconditions.checkArgument(
+                    configuredBatchSize > 0,
+                    "Parquet read batch size should be positive: %s",
+                    configuredBatchSize);
+            ReadBatchSizer readBatchSizer = context.readBatchSizer();
+            int initialBatchSize =
+                    readBatchSizer == null
+                            ? configuredBatchSize
+                            : 
readBatchSizer.batchSize().orElse(configuredBatchSize);
+            reader.setRequestedSchema(requestedSchema.messageType);
+            WritableColumnVector[] writableVectors =
+                    createWritableVectors(initialBatchSize, 
physicalReadFields);
+            IntFunction<WritableColumnVector[]> vectorFactory =
+                    size -> createWritableVectors(size, physicalReadFields);
+
+            VectorizedParquetRecordReader parquetReader =
+                    new VectorizedParquetRecordReader(
+                            context.filePath(),
+                            reader,
+                            fileSchema,
+                            requestedSchema.fields,
+                            writableVectors,
+                            initialBatchSize,
+                            configuredBatchSize,
+                            context.fileIO(),
+                            readBatchSizer,
+                            vectorFactory);
+            return readPlan.isIdentity()
+                    ? parquetReader
+                    : new ShreddingFormatReader(parquetReader, readPlan);
+        } catch (Throwable t) {
+            try {
+                reader.close();
+            } catch (Throwable closeFailure) {
+                t.addSuppressed(closeFailure);
+            }
+            throw t;
+        }
     }
 
     private RequestedSchema getOrCreateRequestedSchema(MessageType fileSchema) 
{
diff --git 
a/paimon-format/src/test/java/org/apache/paimon/format/parquet/ParquetReaderFactoryLeakTest.java
 
b/paimon-format/src/test/java/org/apache/paimon/format/parquet/ParquetReaderFactoryLeakTest.java
new file mode 100644
index 0000000000..6898203ba6
--- /dev/null
+++ 
b/paimon-format/src/test/java/org/apache/paimon/format/parquet/ParquetReaderFactoryLeakTest.java
@@ -0,0 +1,139 @@
+/*
+ * 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.paimon.format.parquet;
+
+import org.apache.paimon.data.GenericRow;
+import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.format.FileFormat;
+import org.apache.paimon.format.FormatReaderContext;
+import org.apache.paimon.format.FormatWriter;
+import org.apache.paimon.fs.Path;
+import org.apache.paimon.fs.PositionOutputStream;
+import org.apache.paimon.fs.SeekableInputStream;
+import org.apache.paimon.fs.local.LocalFileIO;
+import org.apache.paimon.options.Options;
+import org.apache.paimon.reader.FileRecordReader;
+import org.apache.paimon.types.DataTypes;
+import org.apache.paimon.types.RowType;
+
+import org.apache.parquet.hadoop.ParquetFileReader;
+import org.apache.parquet.schema.MessageType;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.IOException;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/** Tests that {@link ParquetReaderFactory#createReader} closes the file 
exactly once. */
+class ParquetReaderFactoryLeakTest {
+
+    @TempDir java.nio.file.Path tempDir;
+
+    private final RowType rowType = RowType.of(DataTypes.INT());
+
+    @Test
+    void testSetupFailureAfterReaderExistsClosesFile() throws IOException {
+        Path file = writeParquet();
+        CountingFileIO fileIO = new CountingFileIO();
+
+        // computeBatchSize is called after the ParquetFileReader has been 
constructed, so a
+        // zero batch size fails the check inside the region this test is 
about.
+        ParquetReaderFactory factory =
+                new ParquetReaderFactory(new Options(), rowType, 1024, null) {
+                    @Override
+                    protected int computeBatchSize(
+                            ParquetFileReader reader, MessageType 
requestedSchema) {
+                        return 0;
+                    }
+                };
+
+        assertThatThrownBy(() -> factory.createReader(context(fileIO, file)))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("Parquet read batch size should be 
positive");
+        assertThat(fileIO.closed).hasValue(1);
+    }
+
+    @Test
+    void testSuccessfulReadClosesFileOnce() throws IOException {
+        Path file = writeParquet();
+        CountingFileIO fileIO = new CountingFileIO();
+
+        ParquetReaderFactory factory = new ParquetReaderFactory(new Options(), 
rowType, 1024, null);
+        try (FileRecordReader<InternalRow> reader = 
factory.createReader(context(fileIO, file))) {
+            assertThat(reader.readBatch().next().getInt(0)).isEqualTo(1);
+        }
+        assertThat(fileIO.closed).hasValue(1);
+    }
+
+    private FormatReaderContext context(CountingFileIO fileIO, Path file) 
throws IOException {
+        return new FormatReaderContext(fileIO, file, fileIO.getFileSize(file), 
null, null);
+    }
+
+    private Path writeParquet() throws IOException {
+        Path file = new Path(tempDir.toUri().toString(), "a.parquet");
+        FileFormat format = FileFormat.fromIdentifier("parquet", new 
Options());
+        try (PositionOutputStream out = 
LocalFileIO.create().newOutputStream(file, false)) {
+            FormatWriter writer = 
format.createWriterFactory(rowType).create(out, "zstd");
+            writer.addElement(GenericRow.of(1));
+            writer.close();
+        }
+        return file;
+    }
+
+    private static class CountingFileIO extends LocalFileIO {
+
+        private final AtomicInteger closed = new AtomicInteger();
+
+        @Override
+        public SeekableInputStream newInputStream(Path path) throws 
IOException {
+            SeekableInputStream inner = super.newInputStream(path);
+            return new SeekableInputStream() {
+
+                @Override
+                public void seek(long desired) throws IOException {
+                    inner.seek(desired);
+                }
+
+                @Override
+                public long getPos() throws IOException {
+                    return inner.getPos();
+                }
+
+                @Override
+                public int read() throws IOException {
+                    return inner.read();
+                }
+
+                @Override
+                public int read(byte[] b, int off, int len) throws IOException 
{
+                    return inner.read(b, off, len);
+                }
+
+                @Override
+                public void close() throws IOException {
+                    closed.incrementAndGet();
+                    inner.close();
+                }
+            };
+        }
+    }
+}

Reply via email to