This is an automated email from the ASF dual-hosted git repository.
wgtmac pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/parquet-java.git
The following commit(s) were added to refs/heads/master by this push:
new 2f34ba390 GH-3394: Reuse listed file status during split planning
(#3674)
2f34ba390 is described below
commit 2f34ba390501a6d08b47fec17c507b013b4decb3
Author: Aaron Niskode-Dossett <[email protected]>
AuthorDate: Sat Sep 19 22:31:52 2026 -0500
GH-3394: Reuse listed file status during split planning (#3674)
---
.../apache/parquet/hadoop/ParquetInputFormat.java | 44 ++-
.../hadoop/TestInputFormatFileStatusReuse.java | 295 +++++++++++++++++++++
2 files changed, 333 insertions(+), 6 deletions(-)
diff --git
a/parquet-hadoop/src/main/java/org/apache/parquet/hadoop/ParquetInputFormat.java
b/parquet-hadoop/src/main/java/org/apache/parquet/hadoop/ParquetInputFormat.java
index 8e05d49bd..811e1f383 100644
---
a/parquet-hadoop/src/main/java/org/apache/parquet/hadoop/ParquetInputFormat.java
+++
b/parquet-hadoop/src/main/java/org/apache/parquet/hadoop/ParquetInputFormat.java
@@ -485,15 +485,42 @@ public class ParquetInputFormat<T> extends
FileInputFormat<Void, T> {
if (footer == null) {
// Footer was originally missing, so get it from the cache again
-
footers.add(footersCache.getCurrentValue(footerEntry.getKey()).getFooter());
- } else {
- footers.add(footer);
+ footer =
footersCache.getCurrentValue(footerEntry.getKey()).getFooter();
}
+ footers.add(new FooterWithFileStatus(footer,
footerEntry.getKey().status));
}
return footers;
}
+ static FileStatus getFileStatus(Footer footer) {
+ return footer instanceof FooterWithFileStatus ? ((FooterWithFileStatus)
footer).fileStatus : null;
+ }
+
+ private static List<Footer> withFileStatuses(List<Footer> footers,
Collection<FileStatus> statuses) {
+ Map<Path, FileStatus> statusesByPath = new HashMap<Path,
FileStatus>(statuses.size());
+ for (FileStatus status : statuses) {
+ statusesByPath.put(status.getPath(), status);
+ }
+
+ List<Footer> footersWithStatuses = new ArrayList<Footer>(footers.size());
+ for (Footer footer : footers) {
+ FileStatus status = statusesByPath.get(footer.getFile());
+ footersWithStatuses.add(status == null ? footer : new
FooterWithFileStatus(footer, status));
+ }
+ return footersWithStatuses;
+ }
+
+ /** Carries an already-listed status through split planning without adding
it to Footer's public API. */
+ private static final class FooterWithFileStatus extends Footer {
+ private final FileStatus fileStatus;
+
+ private FooterWithFileStatus(Footer footer, FileStatus fileStatus) {
+ super(footer.getFile(), footer.getParquetMetadata());
+ this.fileStatus = fileStatus;
+ }
+ }
+
public List<Footer> getFooters(Configuration configuration, List<FileStatus>
statuses) throws IOException {
return getFooters(configuration, (Collection<FileStatus>) statuses);
}
@@ -509,7 +536,9 @@ public class ParquetInputFormat<T> extends
FileInputFormat<Void, T> {
public List<Footer> getFooters(Configuration configuration,
Collection<FileStatus> statuses) throws IOException {
LOG.debug("reading {} files", statuses.size());
boolean taskSideMetaData = isTaskSideMetaData(configuration);
- return
ParquetFileReader.readAllFootersInParallelUsingSummaryFiles(configuration,
statuses, taskSideMetaData);
+ List<Footer> footers =
+
ParquetFileReader.readAllFootersInParallelUsingSummaryFiles(configuration,
statuses, taskSideMetaData);
+ return withFileStatuses(footers, statuses);
}
/**
@@ -732,8 +761,6 @@ class ClientSideMetadataSplitStrategy {
for (Footer footer : footers) {
final Path file = footer.getFile();
LOG.debug("{}", file);
- FileSystem fs = file.getFileSystem(configuration);
- FileStatus fileStatus = fs.getFileStatus(file);
ParquetMetadata parquetMetaData = footer.getParquetMetadata();
List<BlockMetaData> blocks = parquetMetaData.getBlocks();
@@ -748,6 +775,11 @@ class ClientSideMetadataSplitStrategy {
continue;
}
+ FileSystem fs = file.getFileSystem(configuration);
+ FileStatus fileStatus = ParquetInputFormat.getFileStatus(footer);
+ if (fileStatus == null) {
+ fileStatus = fs.getFileStatus(file);
+ }
BlockLocation[] fileBlockLocations =
fs.getFileBlockLocations(fileStatus, 0, fileStatus.getLen());
splits.addAll(generateSplits(
filteredBlocks,
diff --git
a/parquet-hadoop/src/test/java/org/apache/parquet/hadoop/TestInputFormatFileStatusReuse.java
b/parquet-hadoop/src/test/java/org/apache/parquet/hadoop/TestInputFormatFileStatusReuse.java
new file mode 100644
index 000000000..84e40a386
--- /dev/null
+++
b/parquet-hadoop/src/test/java/org/apache/parquet/hadoop/TestInputFormatFileStatusReuse.java
@@ -0,0 +1,295 @@
+/*
+ * 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.parquet.hadoop;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.UUID;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.BlockLocation;
+import org.apache.hadoop.fs.FSDataInputStream;
+import org.apache.hadoop.fs.FileStatus;
+import org.apache.hadoop.fs.FileSystem;
+import org.apache.hadoop.fs.Path;
+import org.apache.hadoop.fs.RawLocalFileSystem;
+import org.apache.hadoop.mapreduce.InputSplit;
+import org.apache.hadoop.mapreduce.Job;
+import org.apache.parquet.example.data.Group;
+import org.apache.parquet.example.data.simple.SimpleGroupFactory;
+import org.apache.parquet.hadoop.ParquetOutputFormat.JobSummaryLevel;
+import org.apache.parquet.hadoop.example.ExampleParquetWriter;
+import org.apache.parquet.hadoop.example.GroupReadSupport;
+import org.apache.parquet.hadoop.example.GroupWriteSupport;
+import org.apache.parquet.hadoop.metadata.ParquetMetadata;
+import org.apache.parquet.schema.MessageType;
+import org.apache.parquet.schema.MessageTypeParser;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+public class TestInputFormatFileStatusReuse {
+ private static final String TRACKING_SCHEME = "tracking";
+ private static final MessageType SCHEMA =
MessageTypeParser.parseMessageType("message test { required int32 id; }");
+
+ @Rule
+ public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ @Test
+ public void reusesListedFileStatusAfterReadingAndCachingFooters() throws
Exception {
+ File parquetFile = writeParquetFile();
+ TestContext context = newTestContext(parquetFile);
+ FixedStatusInputFormat inputFormat = new
FixedStatusInputFormat(context.status);
+
+ assertUsesListedStatus(inputFormat, context, context.status);
+ assertEquals(1, inputFormat.getFooterReadCount());
+
+ FileStatus refreshedStatus = new FileStatus(context.status);
+ inputFormat.setStatus(refreshedStatus);
+ context.fileSystem.clearObservations();
+ assertUsesListedStatus(inputFormat, context, refreshedStatus);
+ assertEquals("the second split plan should use the cached footer", 1,
inputFormat.getFooterReadCount());
+ }
+
+ @Test
+ public void reusesListedFileStatusWithSummaryFileFooters() throws Exception {
+ File parquetFile = writeParquetFile();
+ writeSummaryFiles(parquetFile);
+ TestContext context = newTestContext(parquetFile);
+ FixedStatusInputFormat inputFormat = new
FixedStatusInputFormat(context.status);
+
+ assertUsesListedStatus(inputFormat, context, context.status);
+
+ Path metadataPath = new Path(context.status.getPath().getParent(),
ParquetFileWriter.PARQUET_METADATA_FILE);
+ assertTrue("the summary metadata should supply the footer",
context.fileSystem.getOpenCount(metadataPath) > 0);
+ assertEquals(
+ "the data file should not be opened when its footer comes from summary
metadata",
+ 0,
+ context.fileSystem.getOpenCount(context.status.getPath()));
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ public void looksUpFileStatusForFootersWithoutListedStatuses() throws
Exception {
+ File parquetFile = writeParquetFile();
+ TestContext context = newTestContext(parquetFile);
+ Path localPath = new Path(parquetFile.toURI());
+ ParquetMetadata metadata = ParquetFileReader.readFooter(new
Configuration(), localPath);
+ Footer footer = new Footer(context.status.getPath(), metadata);
+ FixedStatusInputFormat inputFormat = new
FixedStatusInputFormat(Collections.emptyList());
+
+ List<ParquetInputSplit> splits =
+ inputFormat.getSplits(context.job.getConfiguration(),
Collections.singletonList(footer));
+
+ assertEquals(1, splits.size());
+ assertEquals(1,
context.fileSystem.getFileStatusCount(context.status.getPath()));
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ public void reusesProvidedFileStatusWithDeprecatedSplitPlanning() throws
Exception {
+ File parquetFile = writeParquetFile();
+ TestContext context = newTestContext(parquetFile);
+ FixedStatusInputFormat inputFormat = new
FixedStatusInputFormat(context.status);
+ List<Footer> footers =
+ inputFormat.getFooters(context.job.getConfiguration(),
Collections.singletonList(context.status));
+
+ context.fileSystem.clearObservations();
+ List<ParquetInputSplit> splits =
inputFormat.getSplits(context.job.getConfiguration(), footers);
+
+ assertEquals(1, splits.size());
+ assertEquals(0,
context.fileSystem.getFileStatusCount(context.status.getPath()));
+ assertSame(context.status, context.fileSystem.getBlockLocationStatus());
+ }
+
+ private void assertUsesListedStatus(
+ FixedStatusInputFormat inputFormat, TestContext context, FileStatus
expectedStatus) throws IOException {
+ List<InputSplit> splits = inputFormat.getSplits(context.job);
+
+ assertEquals(1, splits.size());
+ assertEquals(
+ "split planning should not request status for a file returned by
listStatus",
+ 0,
+ context.fileSystem.getFileStatusCount(context.status.getPath()));
+ assertSame(
+ "block location lookup should use the FileStatus returned by
listStatus",
+ expectedStatus,
+ context.fileSystem.getBlockLocationStatus());
+ }
+
+ private TestContext newTestContext(File parquetFile) throws Exception {
+ Configuration configuration = new Configuration();
+ configuration.setClass("fs." + TRACKING_SCHEME + ".impl",
TrackingFileSystem.class, FileSystem.class);
+ Job job = Job.getInstance(configuration);
+ ParquetInputFormat.setTaskSideMetaData(job, false);
+
+ Path path = new Path(TRACKING_SCHEME, UUID.randomUUID().toString(),
parquetFile.getAbsolutePath());
+ TrackingFileSystem fileSystem = (TrackingFileSystem)
path.getFileSystem(configuration);
+ FileStatus status = fileSystem.getFileStatus(path);
+ fileSystem.clearObservations();
+ return new TestContext(job, fileSystem, status);
+ }
+
+ private File writeParquetFile() throws IOException {
+ File file = new File(temporaryFolder.getRoot(), "part-00000.parquet");
+ Configuration configuration = new Configuration();
+ GroupWriteSupport.setSchema(SCHEMA, configuration);
+
+ try (ParquetWriter<Group> writer = ExampleParquetWriter.builder(new
Path(file.toURI()))
+ .withConf(configuration)
+ .withType(SCHEMA)
+ .build()) {
+ writer.write(new SimpleGroupFactory(SCHEMA).newGroup().append("id", 1));
+ }
+ return file;
+ }
+
+ private void writeSummaryFiles(File parquetFile) throws IOException {
+ Configuration configuration = new Configuration();
+ Path file = new Path(parquetFile.toURI());
+ FileStatus status = file.getFileSystem(configuration).getFileStatus(file);
+ ParquetMetadata metadata = ParquetFileReader.readFooter(configuration,
status);
+ Footer footer = new Footer(file, metadata);
+ ParquetFileWriter.writeMetadataFile(
+ configuration,
+ new Path(parquetFile.getParentFile().toURI()),
+ Collections.singletonList(footer),
+ JobSummaryLevel.ALL);
+ }
+
+ private static final class TestContext {
+ private final Job job;
+ private final TrackingFileSystem fileSystem;
+ private final FileStatus status;
+
+ private TestContext(Job job, TrackingFileSystem fileSystem, FileStatus
status) {
+ this.job = job;
+ this.fileSystem = fileSystem;
+ this.status = status;
+ }
+ }
+
+ private static final class FixedStatusInputFormat extends
ParquetInputFormat<Group> {
+ private List<FileStatus> statuses;
+ private int footerReadCount;
+
+ private FixedStatusInputFormat(FileStatus status) {
+ this(Collections.singletonList(status));
+ }
+
+ private FixedStatusInputFormat(List<FileStatus> statuses) {
+ super(GroupReadSupport.class);
+ this.statuses = statuses;
+ }
+
+ @Override
+ protected List<FileStatus>
listStatus(org.apache.hadoop.mapreduce.JobContext jobContext) {
+ return statuses;
+ }
+
+ @Override
+ public List<Footer> getFooters(Configuration configuration,
Collection<FileStatus> statuses)
+ throws IOException {
+ footerReadCount++;
+ return super.getFooters(configuration, statuses);
+ }
+
+ private int getFooterReadCount() {
+ return footerReadCount;
+ }
+
+ private void setStatus(FileStatus status) {
+ statuses = Collections.singletonList(status);
+ }
+ }
+
+ public static final class TrackingFileSystem extends RawLocalFileSystem {
+ private final RawLocalFileSystem delegate = new RawLocalFileSystem();
+ private final List<Path> fileStatusPaths =
Collections.synchronizedList(new ArrayList<Path>());
+ private final List<Path> openedPaths = Collections.synchronizedList(new
ArrayList<Path>());
+ private URI uri = URI.create(TRACKING_SCHEME + ":///");
+ private volatile FileStatus blockLocationStatus;
+
+ @Override
+ public void initialize(URI name, Configuration configuration) throws
IOException {
+ super.initialize(name, configuration);
+ String authority = name.getAuthority() == null ? "" :
name.getAuthority();
+ uri = URI.create(name.getScheme() + "://" + authority + "/");
+ delegate.initialize(URI.create("file:///"), configuration);
+ }
+
+ @Override
+ public URI getUri() {
+ return uri == null ? URI.create(TRACKING_SCHEME + ":///") : uri;
+ }
+
+ @Override
+ public FSDataInputStream open(Path path, int bufferSize) throws
IOException {
+ openedPaths.add(path);
+ return delegate.open(toLocalPath(path), bufferSize);
+ }
+
+ @Override
+ public FileStatus getFileStatus(Path path) throws IOException {
+ fileStatusPaths.add(path);
+ FileStatus status = new
FileStatus(delegate.getFileStatus(toLocalPath(path)));
+ status.setPath(path);
+ return status;
+ }
+
+ @Override
+ public BlockLocation[] getFileBlockLocations(FileStatus status, long
start, long length) throws IOException {
+ blockLocationStatus = status;
+ return super.getFileBlockLocations(status, start, length);
+ }
+
+ private Path toLocalPath(Path path) {
+ return new Path(path.toUri().getPath());
+ }
+
+ private int getFileStatusCount(Path path) {
+ synchronized (fileStatusPaths) {
+ return Collections.frequency(fileStatusPaths, path);
+ }
+ }
+
+ private int getOpenCount(Path path) {
+ synchronized (openedPaths) {
+ return Collections.frequency(openedPaths, path);
+ }
+ }
+
+ private FileStatus getBlockLocationStatus() {
+ return blockLocationStatus;
+ }
+
+ private void clearObservations() {
+ fileStatusPaths.clear();
+ openedPaths.clear();
+ blockLocationStatus = null;
+ }
+ }
+}