Skip to content

Commit 5a702ac

Browse files
authored
[format] Close the ParquetFileReader when post-construction setup fails (#9576)
1 parent 32a0ed5 commit 5a702ac

2 files changed

Lines changed: 198 additions & 49 deletions

File tree

paimon-format/src/main/java/org/apache/paimon/format/parquet/ParquetReaderFactory.java

Lines changed: 59 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -151,57 +151,67 @@ public FileRecordReader<InternalRow> createReader(FormatReaderFactory.Context co
151151
throw t;
152152
}
153153

154-
ShreddingReadPlan readPlan =
155-
ShreddingReadPlanFactories.createReadPlan(
156-
readType,
157-
readFieldMetadata(reader),
158-
fileSchema,
159-
shreddingReadPlanFactories(readType));
160-
DataField[] physicalReadFields = readFields(readPlan.physicalRowType());
161-
RequestedSchema requestedSchema =
162-
readPlan.isIdentity()
163-
? getOrCreateRequestedSchema(fileSchema)
164-
: createRequestedSchema(fileSchema, physicalReadFields);
165-
166-
if (LOG.isDebugEnabled()) {
167-
LOG.debug(
168-
"Create reader of the parquet file {}, the fileSchema is {}, the requestedSchema is {}.",
169-
context.filePath(),
170-
fileSchema,
171-
requestedSchema.messageType);
172-
}
173-
174-
int configuredBatchSize = computeBatchSize(reader, requestedSchema.messageType);
175-
Preconditions.checkArgument(
176-
configuredBatchSize > 0,
177-
"Parquet read batch size should be positive: %s",
178-
configuredBatchSize);
179-
ReadBatchSizer readBatchSizer = context.readBatchSizer();
180-
int initialBatchSize =
181-
readBatchSizer == null
182-
? configuredBatchSize
183-
: readBatchSizer.batchSize().orElse(configuredBatchSize);
184-
reader.setRequestedSchema(requestedSchema.messageType);
185-
WritableColumnVector[] writableVectors =
186-
createWritableVectors(initialBatchSize, physicalReadFields);
187-
IntFunction<WritableColumnVector[]> vectorFactory =
188-
size -> createWritableVectors(size, physicalReadFields);
189-
190-
VectorizedParquetRecordReader parquetReader =
191-
new VectorizedParquetRecordReader(
154+
// The reader owns the open stream from here, so close it if the setup below fails.
155+
try {
156+
ShreddingReadPlan readPlan =
157+
ShreddingReadPlanFactories.createReadPlan(
158+
readType,
159+
readFieldMetadata(reader),
160+
fileSchema,
161+
shreddingReadPlanFactories(readType));
162+
DataField[] physicalReadFields = readFields(readPlan.physicalRowType());
163+
RequestedSchema requestedSchema =
164+
readPlan.isIdentity()
165+
? getOrCreateRequestedSchema(fileSchema)
166+
: createRequestedSchema(fileSchema, physicalReadFields);
167+
168+
if (LOG.isDebugEnabled()) {
169+
LOG.debug(
170+
"Create reader of the parquet file {}, the fileSchema is {}, the requestedSchema is {}.",
192171
context.filePath(),
193-
reader,
194172
fileSchema,
195-
requestedSchema.fields,
196-
writableVectors,
197-
initialBatchSize,
198-
configuredBatchSize,
199-
context.fileIO(),
200-
readBatchSizer,
201-
vectorFactory);
202-
return readPlan.isIdentity()
203-
? parquetReader
204-
: new ShreddingFormatReader(parquetReader, readPlan);
173+
requestedSchema.messageType);
174+
}
175+
176+
int configuredBatchSize = computeBatchSize(reader, requestedSchema.messageType);
177+
Preconditions.checkArgument(
178+
configuredBatchSize > 0,
179+
"Parquet read batch size should be positive: %s",
180+
configuredBatchSize);
181+
ReadBatchSizer readBatchSizer = context.readBatchSizer();
182+
int initialBatchSize =
183+
readBatchSizer == null
184+
? configuredBatchSize
185+
: readBatchSizer.batchSize().orElse(configuredBatchSize);
186+
reader.setRequestedSchema(requestedSchema.messageType);
187+
WritableColumnVector[] writableVectors =
188+
createWritableVectors(initialBatchSize, physicalReadFields);
189+
IntFunction<WritableColumnVector[]> vectorFactory =
190+
size -> createWritableVectors(size, physicalReadFields);
191+
192+
VectorizedParquetRecordReader parquetReader =
193+
new VectorizedParquetRecordReader(
194+
context.filePath(),
195+
reader,
196+
fileSchema,
197+
requestedSchema.fields,
198+
writableVectors,
199+
initialBatchSize,
200+
configuredBatchSize,
201+
context.fileIO(),
202+
readBatchSizer,
203+
vectorFactory);
204+
return readPlan.isIdentity()
205+
? parquetReader
206+
: new ShreddingFormatReader(parquetReader, readPlan);
207+
} catch (Throwable t) {
208+
try {
209+
reader.close();
210+
} catch (Throwable closeFailure) {
211+
t.addSuppressed(closeFailure);
212+
}
213+
throw t;
214+
}
205215
}
206216

207217
private RequestedSchema getOrCreateRequestedSchema(MessageType fileSchema) {
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package org.apache.paimon.format.parquet;
20+
21+
import org.apache.paimon.data.GenericRow;
22+
import org.apache.paimon.data.InternalRow;
23+
import org.apache.paimon.format.FileFormat;
24+
import org.apache.paimon.format.FormatReaderContext;
25+
import org.apache.paimon.format.FormatWriter;
26+
import org.apache.paimon.fs.Path;
27+
import org.apache.paimon.fs.PositionOutputStream;
28+
import org.apache.paimon.fs.SeekableInputStream;
29+
import org.apache.paimon.fs.local.LocalFileIO;
30+
import org.apache.paimon.options.Options;
31+
import org.apache.paimon.reader.FileRecordReader;
32+
import org.apache.paimon.types.DataTypes;
33+
import org.apache.paimon.types.RowType;
34+
35+
import org.apache.parquet.hadoop.ParquetFileReader;
36+
import org.apache.parquet.schema.MessageType;
37+
import org.junit.jupiter.api.Test;
38+
import org.junit.jupiter.api.io.TempDir;
39+
40+
import java.io.IOException;
41+
import java.util.concurrent.atomic.AtomicInteger;
42+
43+
import static org.assertj.core.api.Assertions.assertThat;
44+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
45+
46+
/** Tests that {@link ParquetReaderFactory#createReader} closes the file exactly once. */
47+
class ParquetReaderFactoryLeakTest {
48+
49+
@TempDir java.nio.file.Path tempDir;
50+
51+
private final RowType rowType = RowType.of(DataTypes.INT());
52+
53+
@Test
54+
void testSetupFailureAfterReaderExistsClosesFile() throws IOException {
55+
Path file = writeParquet();
56+
CountingFileIO fileIO = new CountingFileIO();
57+
58+
// computeBatchSize is called after the ParquetFileReader has been constructed, so a
59+
// zero batch size fails the check inside the region this test is about.
60+
ParquetReaderFactory factory =
61+
new ParquetReaderFactory(new Options(), rowType, 1024, null) {
62+
@Override
63+
protected int computeBatchSize(
64+
ParquetFileReader reader, MessageType requestedSchema) {
65+
return 0;
66+
}
67+
};
68+
69+
assertThatThrownBy(() -> factory.createReader(context(fileIO, file)))
70+
.isInstanceOf(IllegalArgumentException.class)
71+
.hasMessageContaining("Parquet read batch size should be positive");
72+
assertThat(fileIO.closed).hasValue(1);
73+
}
74+
75+
@Test
76+
void testSuccessfulReadClosesFileOnce() throws IOException {
77+
Path file = writeParquet();
78+
CountingFileIO fileIO = new CountingFileIO();
79+
80+
ParquetReaderFactory factory = new ParquetReaderFactory(new Options(), rowType, 1024, null);
81+
try (FileRecordReader<InternalRow> reader = factory.createReader(context(fileIO, file))) {
82+
assertThat(reader.readBatch().next().getInt(0)).isEqualTo(1);
83+
}
84+
assertThat(fileIO.closed).hasValue(1);
85+
}
86+
87+
private FormatReaderContext context(CountingFileIO fileIO, Path file) throws IOException {
88+
return new FormatReaderContext(fileIO, file, fileIO.getFileSize(file), null, null);
89+
}
90+
91+
private Path writeParquet() throws IOException {
92+
Path file = new Path(tempDir.toUri().toString(), "a.parquet");
93+
FileFormat format = FileFormat.fromIdentifier("parquet", new Options());
94+
try (PositionOutputStream out = LocalFileIO.create().newOutputStream(file, false)) {
95+
FormatWriter writer = format.createWriterFactory(rowType).create(out, "zstd");
96+
writer.addElement(GenericRow.of(1));
97+
writer.close();
98+
}
99+
return file;
100+
}
101+
102+
private static class CountingFileIO extends LocalFileIO {
103+
104+
private final AtomicInteger closed = new AtomicInteger();
105+
106+
@Override
107+
public SeekableInputStream newInputStream(Path path) throws IOException {
108+
SeekableInputStream inner = super.newInputStream(path);
109+
return new SeekableInputStream() {
110+
111+
@Override
112+
public void seek(long desired) throws IOException {
113+
inner.seek(desired);
114+
}
115+
116+
@Override
117+
public long getPos() throws IOException {
118+
return inner.getPos();
119+
}
120+
121+
@Override
122+
public int read() throws IOException {
123+
return inner.read();
124+
}
125+
126+
@Override
127+
public int read(byte[] b, int off, int len) throws IOException {
128+
return inner.read(b, off, len);
129+
}
130+
131+
@Override
132+
public void close() throws IOException {
133+
closed.incrementAndGet();
134+
inner.close();
135+
}
136+
};
137+
}
138+
}
139+
}

0 commit comments

Comments
 (0)