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};

Reply via email to