Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions api/src/main/java/org/apache/iceberg/io/FileRange.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* 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.io;

import java.nio.ByteBuffer;
import java.util.concurrent.CompletableFuture;

public class FileRange {
private final CompletableFuture<ByteBuffer> byteBuffer;
private final long offset;
private final int length;

public FileRange(CompletableFuture<ByteBuffer> byteBuffer, long offset, int length) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the parquet implementation, I don't think you can pass the byteBuffer future in like this. I believe this is intended to be set by the implementation so that it can be returned to the invoker.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We not passing the bytebuffer in here right, we passing a future that completes with a byte buffer, we need a way to map the futures in Iceberg to the future's we are setting in Parquet,
So when we call parquetFileRange.setDataReadFuture(future); we need to have a way of tracking that future in Iceberg and that's what this gives us.

this.byteBuffer = byteBuffer;
this.offset = offset;
this.length = length;
}

public CompletableFuture<ByteBuffer> byteBuffer() {
return byteBuffer;
}

public long offset() {
return offset;
}

public int length() {
return length;
}
}
41 changes: 41 additions & 0 deletions api/src/main/java/org/apache/iceberg/io/RangeReadable.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@

import java.io.Closeable;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.function.IntFunction;
import org.apache.iceberg.util.VectoredReadUtils;

/**
* {@code RangeReadable} is an interface that allows for implementations of {@link InputFile}
Expand Down Expand Up @@ -77,4 +81,41 @@ default void readFully(long position, byte[] buffer) throws IOException {
default int readTail(byte[] buffer) throws IOException {
return readTail(buffer, 0, buffer.length);
}

/**
* Is the {@link #readVectored(List, IntFunction)} method available?
*
* @param allocate the allocator to use for allocating ByteBuffers
* @return True if the operation is considered available for this allocator.
*/
default boolean readVectoredAvailable(IntFunction<ByteBuffer> allocate) {
Comment thread
danielcweeks marked this conversation as resolved.
Outdated
Comment thread
stubz151 marked this conversation as resolved.
Outdated
return true;
}

/**
* Read fully a list of file ranges asynchronously from this file. As a result of the call, each
* range will have FileRange.setData(CompletableFuture) called with a future that when complete
* will have a ByteBuffer with the data from the file's range.
*
* <p>The position returned by getPos() after readVectored() is undefined.
*
* <p>If a file is changed while the readVectored() operation is in progress, the output is
* undefined. Some ranges may have old data, some may have new and some may have both.
*
* <p>While a readVectored() operation is in progress, normal read api calls may block.
*
* @param ranges the byte ranges to read
* @param allocate the function to allocate ByteBuffer
* @throws IOException any IOE.
* @throws IllegalArgumentException if the any of ranges are invalid, or they overlap.
*/
default void readVectored(List<FileRange> ranges, IntFunction<ByteBuffer> allocate)
throws IOException {
List<FileRange> validatedRanges = VectoredReadUtils.validateAndSortRanges(ranges);
for (FileRange range : validatedRanges) {
ByteBuffer buffer = allocate.apply(range.length());
readFully(range.offset(), buffer.array());
range.byteBuffer().complete(buffer);
}
}
}
120 changes: 120 additions & 0 deletions api/src/main/java/org/apache/iceberg/util/VectoredReadUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* 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.util;

import java.io.EOFException;
import java.util.Comparator;
import java.util.List;
import org.apache.iceberg.io.FileRange;
import org.apache.iceberg.relocated.com.google.common.base.Preconditions;
import org.apache.iceberg.relocated.com.google.common.collect.Lists;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Utils class for vectoredReads, to help with things like range validation. Most of the code in
* this class is written by @mukundthakur, and taken from
* /hadoop-common/src/main/java/org/apache/hadoop/fs/VectoredReadUtils.java (thank you!).
*/
public final class VectoredReadUtils {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't feel like we need this class. There are three things this does, but it should probalby be just one. The validateRangeRequest should just be handled in the constructor of the FileRange (we currently don't have any validation there). The sortRangeList is a subset of validateAndSortRanges which seems duplicative.

I'd suggest moving validateAndSort to the RangeReadable interface as a static utility that can be used by implementors and avoid creating this util class.

private VectoredReadUtils() {}

private static final Logger LOG = LoggerFactory.getLogger(VectoredReadUtils.class);

/**
* Validate a list of ranges (including overlapping checks) and return the sorted list.
*
* <p>Two ranges overlap when the start offset of second is less than the end offset of first. End
* offset is calculated as start offset + length.
*
* @param input input list
* @return a new sorted list.
* @throws IllegalArgumentException if there are overlapping ranges or a range element is invalid
* (other than with negative offset)
* @throws EOFException if the last range extends beyond the end of the file supplied or a range
* offset is negative
*/
public static List<FileRange> validateAndSortRanges(final List<FileRange> input)
throws EOFException {

Preconditions.checkNotNull(input, "Null input list");

if (input.isEmpty()) {
// this may seem a pathological case, but it was valid
// before and somehow Spark can call it through parquet.
LOG.debug("Empty input list");
return input;
}

final List<FileRange> sortedRanges;

if (input.size() == 1) {
validateRangeRequest(input.get(0));
sortedRanges = input;
} else {
sortedRanges = sortRangeList(input);
FileRange prev = null;
for (final FileRange current : sortedRanges) {
validateRangeRequest(current);
if (prev != null) {
Preconditions.checkArgument(
current.offset() >= prev.offset() + prev.length(),
"Overlapping ranges %s and %s",
prev,
current);
}
prev = current;
}
}

return sortedRanges;
}

/**
* Validate a single range.
*
* @param range range to validate.
* @return the range.
* @throws IllegalArgumentException the range length is negative or other invalid condition is met
* other than the those which raise EOFException or NullPointerException.
* @throws EOFException the range offset is negative
* @throws NullPointerException if the range is null.
*/
public static FileRange validateRangeRequest(FileRange range) throws EOFException {
Preconditions.checkNotNull(range, "range is null");

Preconditions.checkArgument(range.length() >= 0, "length is negative in %s", range);
if (range.offset() < 0) {
throw new EOFException("position is negative in range " + range);
}
return range;
}

/**
* Sort the input ranges by offset; no validation is done.
*
* @param input input ranges.
* @return a new list of the ranges, sorted by offset.
*/
public static List<FileRange> sortRangeList(List<FileRange> input) {
final List<FileRange> l = Lists.newArrayList(input);
l.sort(Comparator.comparingLong(FileRange::offset));
return l;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1340,6 +1340,7 @@ public <D> CloseableIterable<D> build() {
optionsBuilder.withDecryption(fileDecryptionProperties);
}

optionsBuilder.withUseHadoopVectoredIo(true);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There were some efforts to allow Iceberg working without Hadoop on the classpath.
I'm not sure how far away these efforts went, and also not sure how this change will effect that effort.

Could you please help me understand the consequences of always using withUseHadoopVectoredIo?

Thanks,
Peter

@stubz151 stubz151 Sep 23, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For part 1 about the effort to reduce the dependencies on Hadoop I don't think that was ever completed I do see a TODO comment about wanting to do it. I am probably making the effort more complicated as I am adding 2 new imports from Hadoop but I don't think that is a big risk.

for 2) withUseHadoopVectoredIo is used in the file reader in conjunction with readVectoredAvailable() so moving to always using readVector doesn't change anything unless the stream also supports readVectored.
https://github.com/apache/parquet-java/blob/f50dd6cb4b526cf4b585993c1b69a838cd8151f3/parquet-hadoop/src/main/java/org/apache/parquet/hadoop/ParquetFileReader.java#L1303

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the naming of this option is a little misleading. The withUseHadoopVectoredIo doesn't necessarily depend on hadoop as @stubz151 mentions, but rather enables the vectored io behavior in Parquet.

ParquetReadOptions options = optionsBuilder.build();

NameMapping mapping;
Expand Down
70 changes: 70 additions & 0 deletions parquet/src/main/java/org/apache/iceberg/parquet/ParquetIO.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.function.IntFunction;
import java.util.stream.Collectors;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FSDataOutputStream;
Expand All @@ -29,11 +34,15 @@
import org.apache.iceberg.hadoop.HadoopOutputFile;
import org.apache.iceberg.io.DelegatingInputStream;
import org.apache.iceberg.io.DelegatingOutputStream;
import org.apache.iceberg.io.FileRange;
import org.apache.iceberg.io.RangeReadable;
import org.apache.parquet.bytes.ByteBufferAllocator;
import org.apache.parquet.hadoop.util.HadoopStreams;
import org.apache.parquet.io.DelegatingPositionOutputStream;
import org.apache.parquet.io.DelegatingSeekableInputStream;
import org.apache.parquet.io.InputFile;
import org.apache.parquet.io.OutputFile;
import org.apache.parquet.io.ParquetFileRange;
import org.apache.parquet.io.PositionOutputStream;
import org.apache.parquet.io.SeekableInputStream;

Expand Down Expand Up @@ -91,6 +100,9 @@ static SeekableInputStream stream(org.apache.iceberg.io.SeekableInputStream stre
return HadoopStreams.wrap((FSDataInputStream) wrapped);
}
}
if (stream instanceof RangeReadable) {
Comment thread
stubz151 marked this conversation as resolved.
return new ParquetRangeReadableInputStreamAdapter(stream);
}
Comment thread
stubz151 marked this conversation as resolved.
return new ParquetInputStreamAdapter(stream);
}

Expand Down Expand Up @@ -123,6 +135,64 @@ public void seek(long newPos) throws IOException {
}
}

private static class ParquetRangeReadableInputStreamAdapter<
T extends org.apache.iceberg.io.SeekableInputStream & RangeReadable>
extends DelegatingSeekableInputStream implements RangeReadable {
private final T delegate;

private ParquetRangeReadableInputStreamAdapter(T delegate) {
super(delegate);
this.delegate = delegate;
}

@Override
public long getPos() throws IOException {
return delegate.getPos();
}

@Override
public void seek(long newPos) throws IOException {
delegate.seek(newPos);
}

@Override
public void readFully(long position, byte[] buffer, int offset, int length) throws IOException {
delegate.readFully(position, buffer, offset, length);
}

@Override
public int readTail(byte[] buffer, int offset, int length) throws IOException {
return delegate.readTail(buffer, offset, length);
}

@Override
public boolean readVectoredAvailable(ByteBufferAllocator allocate) {
return true;
}

@Override
public void readVectored(List<ParquetFileRange> ranges, ByteBufferAllocator allocate)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add some tests at the ParquetIO level to validate this? I know we're adding some in S3FileIO, but it would be good to have this interface tested (even if there's a mock implementation)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added in testRangeReadableAdapterReadVectored which does something similar to the tests in S3FileIO, but focused a bit more on checking that the buffers/ranges are being used correctly, I skipped the other operations but can add them in if we want. Let me know

throws IOException {
IntFunction<ByteBuffer> delegateAllocate = (allocate::allocate);
List<FileRange> delegateRange = convertRanges(ranges);
Comment thread
danielcweeks marked this conversation as resolved.
delegate.readVectored(delegateRange, delegateAllocate);
}

private static List<FileRange> convertRanges(List<ParquetFileRange> ranges) {
return ranges.stream()
.map(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this just maps between the internal parquet hadoop range and the new iceberg one.

parquetFileRange -> {
CompletableFuture<ByteBuffer> result = new CompletableFuture<>();
parquetFileRange.setDataReadFuture(result);
Comment thread
stubz151 marked this conversation as resolved.
Outdated
return new FileRange(
parquetFileRange.getDataReadFuture(),
parquetFileRange.getOffset(),
parquetFileRange.getLength());
})
.collect(Collectors.toList());
}
}

private static class ParquetOutputStreamAdapter extends DelegatingPositionOutputStream {
private final org.apache.iceberg.io.PositionOutputStream delegate;

Expand Down