This is an automated email from the ASF dual-hosted git repository.
danielcweeks 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 2ebe838e31 AWS: Fix zero-length S3 tail reads (#18329)
2ebe838e31 is described below
commit 2ebe838e314e5f1ad8c5fb16662e7434fc11de38
Author: Alejandro Morgante <[email protected]>
AuthorDate: Thu Oct 29 14:12:20 2026 -0300
AWS: Fix zero-length S3 tail reads (#18329)
* AWS: Avoid S3 requests for zero-length range reads
---
.../apache/iceberg/aws/s3/TestS3InputStream.java | 29 ++++++++++++++++++++++
.../org/apache/iceberg/aws/s3/S3InputStream.java | 4 +++
.../apache/iceberg/aws/s3/TestS3InputStream.java | 20 ++++++++++++++-
3 files changed, 52 insertions(+), 1 deletion(-)
diff --git
a/aws/src/integration/java/org/apache/iceberg/aws/s3/TestS3InputStream.java
b/aws/src/integration/java/org/apache/iceberg/aws/s3/TestS3InputStream.java
index ba5ce25a3d..e40e0be08e 100644
--- a/aws/src/integration/java/org/apache/iceberg/aws/s3/TestS3InputStream.java
+++ b/aws/src/integration/java/org/apache/iceberg/aws/s3/TestS3InputStream.java
@@ -157,6 +157,35 @@ public class TestS3InputStream {
testRangeRead(s3);
}
+ @Test
+ void zeroLengthReadTailSucceedsForNonEmptyObject() throws Exception {
+ S3URI uri = new S3URI("s3://bucket/path/to/zero-tail-nonempty.dat");
+ writeS3Data(uri, new byte[] {1});
+
+ try (RangeReadable in = newInputStream(s3, uri)) {
+ assertThat(in.readTail(new byte[0], 0, 0)).isZero();
+ }
+ }
+
+ @Test
+ void zeroLengthReadTailSucceedsForEmptyObject() throws Exception {
+ S3URI uri = new S3URI("s3://bucket/path/to/zero-tail-empty.dat");
+ writeS3Data(uri, new byte[0]);
+
+ try (RangeReadable in = newInputStream(s3, uri)) {
+ assertThat(in.readTail(new byte[0], 0, 0)).isZero();
+ }
+ }
+
+ @Test
+ void zeroLengthReadTailSucceedsForMissingObject() throws Exception {
+ S3URI uri = new S3URI("s3://bucket/path/to/missing-read-tail.dat");
+
+ try (RangeReadable in = newInputStream(s3, uri)) {
+ assertThat(in.readTail(new byte[0], 0, 0)).isZero();
+ }
+ }
+
protected void testRangeRead(S3Client s3Client) throws Exception {
S3URI uri = new S3URI("s3://bucket/path/to/range-read.dat");
int dataSize = 1024 * 1024 * 10;
diff --git a/aws/src/main/java/org/apache/iceberg/aws/s3/S3InputStream.java
b/aws/src/main/java/org/apache/iceberg/aws/s3/S3InputStream.java
index ae0a4498a4..a6ba1da47d 100644
--- a/aws/src/main/java/org/apache/iceberg/aws/s3/S3InputStream.java
+++ b/aws/src/main/java/org/apache/iceberg/aws/s3/S3InputStream.java
@@ -186,6 +186,10 @@ class S3InputStream extends SeekableInputStream implements
RangeReadable {
public int readTail(byte[] buffer, int offset, int length) throws
IOException {
Preconditions.checkPositionIndexes(offset, offset + length, buffer.length);
+ if (length == 0) {
+ return 0;
+ }
+
String range = String.format("bytes=-%s", length);
try (InputStream rangeStream = readRange(range)) {
diff --git a/aws/src/test/java/org/apache/iceberg/aws/s3/TestS3InputStream.java
b/aws/src/test/java/org/apache/iceberg/aws/s3/TestS3InputStream.java
index 5dd6d3dc13..36fd70cf93 100644
--- a/aws/src/test/java/org/apache/iceberg/aws/s3/TestS3InputStream.java
+++ b/aws/src/test/java/org/apache/iceberg/aws/s3/TestS3InputStream.java
@@ -22,6 +22,7 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import java.io.ByteArrayInputStream;
@@ -65,10 +66,12 @@ public final class TestS3InputStream {
@Test
void testReadTailClosesTheStream() throws IOException {
+ byte[] buffer = new byte[1];
when(s3Client.getObject(any(GetObjectRequest.class),
any(ResponseTransformer.class)))
.thenReturn(inputStream);
+ when(inputStream.read(buffer, 0, buffer.length)).thenReturn(buffer.length);
- s3InputStream.readTail(new byte[0], 0, 0);
+ s3InputStream.readTail(buffer, 0, buffer.length);
verify(inputStream).close();
}
@@ -111,6 +114,21 @@ public final class TestS3InputStream {
}
}
+ @Test
+ void zeroLengthReadTailReturnsZeroWithoutChangingBuffer() throws IOException
{
+ byte[] buffer = new byte[] {2};
+
+ assertThat(s3InputStream.readTail(buffer, 0, 0)).isZero();
+ assertThat(buffer).containsExactly((byte) 2);
+ }
+
+ @Test
+ void zeroLengthReadTailDoesNotCallS3() throws IOException {
+ s3InputStream.readTail(new byte[0], 0, 0);
+
+ verifyNoInteractions(s3Client);
+ }
+
@Test
void testReadTailTracksMetrics() throws IOException {
byte[] data = new byte[] {1, 2, 3, 4};