This is an automated email from the ASF dual-hosted git repository.

ivandika3 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ozone.git


The following commit(s) were added to refs/heads/master by this push:
     new 9b917693783 HDDS-9640. S3G Write Exception Handling Improvement 
(#10257)
9b917693783 is described below

commit 9b917693783eba2f144841b8dd75ef769e265970
Author: Peter Lee <[email protected]>
AuthorDate: Thu Jun 4 13:19:24 2026 +0800

    HDDS-9640. S3G Write Exception Handling Improvement (#10257)
---
 .../ozone/s3/endpoint/S3ObjectWriteGuard.java      |  44 ++++-
 .../ozone/s3/endpoint/TestS3ObjectWriteGuard.java  | 218 +++++++++++++++++++++
 2 files changed, 255 insertions(+), 7 deletions(-)

diff --git 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/S3ObjectWriteGuard.java
 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/S3ObjectWriteGuard.java
index 45dcfb15088..ad3f9c2a484 100644
--- 
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/S3ObjectWriteGuard.java
+++ 
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/S3ObjectWriteGuard.java
@@ -33,15 +33,18 @@ class S3ObjectWriteGuard implements AutoCloseable {
 
   private final OutputStream outputStream;
   private final long expectedLength;
+  private final String keyPath;
   private final List<CheckedRunnable<IOException>> preCommits =
       new ArrayList<>();
   private long writtenLength;
+  private Throwable transferFailure;
 
   S3ObjectWriteGuard(
       OzoneOutputStream outputStream, long expectedLength, String keyPath) {
     this.outputStream = outputStream;
     this.expectedLength = expectedLength;
-    addContentLengthValidation(keyPath);
+    this.keyPath = keyPath;
+    addCommitGuard();
     outputStream.setPreCommits(getPreCommits());
   }
 
@@ -49,12 +52,28 @@ protected S3ObjectWriteGuard(
       OutputStream outputStream, long expectedLength, String keyPath) {
     this.outputStream = outputStream;
     this.expectedLength = expectedLength;
-    addContentLengthValidation(keyPath);
+    this.keyPath = keyPath;
+    addCommitGuard();
   }
 
-  private void addContentLengthValidation(String keyPath) {
-    preCommits.add(() -> EndpointBase.validateContentLength(
-        expectedLength, writtenLength, keyPath).run());
+  private void addCommitGuard() {
+    preCommits.add(this::validateBeforeCommit);
+  }
+
+  private void validateBeforeCommit() throws IOException {
+    if (transferFailure != null) {
+      throw new IOException(
+          "S3 object transfer failed before commit: " + keyPath,
+          transferFailure);
+    }
+    EndpointBase.validateContentLength(
+        expectedLength, writtenLength, keyPath).run();
+  }
+
+  private void recordTransferFailure(Throwable failure) {
+    if (transferFailure == null) {
+      transferFailure = failure;
+    }
   }
 
   protected List<CheckedRunnable<IOException>> getPreCommits() {
@@ -70,11 +89,22 @@ long copyFrom(InputStream body, int bufferSize) throws 
IOException {
     while (writtenLength < expectedLength) {
       int toRead = Math.toIntExact(
           Math.min(bufferSize, expectedLength - writtenLength));
-      int readLength = body.read(buffer, 0, toRead);
+      final int readLength;
+      try {
+        readLength = body.read(buffer, 0, toRead);
+      } catch (IOException | RuntimeException ex) {
+        recordTransferFailure(ex);
+        throw ex;
+      }
       if (readLength == -1) {
         break;
       }
-      write(buffer, 0, readLength);
+      try {
+        write(buffer, 0, readLength);
+      } catch (IOException | RuntimeException ex) {
+        recordTransferFailure(ex);
+        throw ex;
+      }
       writtenLength += readLength;
     }
     return writtenLength;
diff --git 
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestS3ObjectWriteGuard.java
 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestS3ObjectWriteGuard.java
new file mode 100644
index 00000000000..bd0c6b96ad4
--- /dev/null
+++ 
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestS3ObjectWriteGuard.java
@@ -0,0 +1,218 @@
+/*
+ * 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.hadoop.ozone.s3.endpoint;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Field;
+import java.nio.ByteBuffer;
+import java.util.Collections;
+import 
org.apache.hadoop.ozone.client.OzoneBucketStub.KeyMetadataAwareByteBufferStreamOutput;
+import 
org.apache.hadoop.ozone.client.OzoneBucketStub.KeyMetadataAwareOutputStream;
+import org.apache.hadoop.ozone.client.io.OzoneDataStreamOutput;
+import org.apache.hadoop.ozone.client.io.OzoneOutputStream;
+import org.apache.hadoop.ozone.s3.exception.OS3Exception;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for close-time commit guards used by S3 object writes.
+ */
+class TestS3ObjectWriteGuard {
+
+  private static final String KEY_PATH = "bucket/key";
+  private static final byte[] CONTENT = "content".getBytes(UTF_8);
+
+  @Test
+  void readIOExceptionBlocksCommitWithCause() {
+    IOException failure = new IOException("read failed");
+    S3ObjectWriteGuard guard = newGuard(CONTENT.length);
+
+    IOException thrown = assertThrows(IOException.class,
+        () -> guard.copyFrom(failingInputStream(failure), 4));
+
+    assertSame(failure, thrown);
+    assertCommitBlockedBy(guard, failure);
+  }
+
+  @Test
+  void readRuntimeExceptionBlocksCommitWithCause() {
+    RuntimeException failure = new IllegalStateException("read failed");
+    S3ObjectWriteGuard guard = newGuard(CONTENT.length);
+
+    RuntimeException thrown = assertThrows(RuntimeException.class,
+        () -> guard.copyFrom(failingInputStream(failure), 4));
+
+    assertSame(failure, thrown);
+    assertCommitBlockedBy(guard, failure);
+  }
+
+  @Test
+  void writeIOExceptionDoesNotAdvanceWrittenLengthAndBlocksCommit()
+      throws ReflectiveOperationException {
+    IOException failure = new IOException("write failed");
+    FailingKeyMetadataAwareOutputStream keyOutputStream =
+        new FailingKeyMetadataAwareOutputStream(failure);
+    S3ObjectWriteGuard guard = new S3ObjectWriteGuard(
+        new OzoneOutputStream(keyOutputStream, null),
+        CONTENT.length, KEY_PATH);
+
+    IOException thrown = assertThrows(IOException.class,
+        () -> guard.copyFrom(new ByteArrayInputStream(CONTENT),
+            CONTENT.length));
+
+    assertSame(failure, thrown);
+    assertThat(keyOutputStream.getWriteAttempts()).isEqualTo(1);
+    assertThat(writtenLength(guard)).isZero();
+    assertCommitBlockedBy(guard, failure);
+  }
+
+  @Test
+  void earlyEofIsBlockedByContentLengthValidation() throws IOException {
+    S3ObjectWriteGuard guard = newGuard(CONTENT.length);
+
+    long copied = guard.copyFrom(new ByteArrayInputStream(new byte[] {1, 2}),
+        4);
+
+    assertThat(copied).isEqualTo(2);
+    OS3Exception failure = assertThrows(OS3Exception.class, guard::close);
+    assertThat(failure.getErrorMessage()).contains(
+        "Request body length 2 does not match expected length "
+            + CONTENT.length);
+    assertThat(failure.getCause()).isNull();
+  }
+
+  @Test
+  void datastreamWriteIOExceptionBlocksCommitWithCause() {
+    IOException failure = new IOException("datastream write failed");
+    FailingKeyMetadataAwareByteBufferStreamOutput keyOutputStream =
+        new FailingKeyMetadataAwareByteBufferStreamOutput(failure);
+    S3ObjectStreamingWriteGuard guard = new S3ObjectStreamingWriteGuard(
+        new OzoneDataStreamOutput(keyOutputStream, null),
+        CONTENT.length, KEY_PATH);
+
+    IOException thrown = assertThrows(IOException.class,
+        () -> guard.copyFrom(new ByteArrayInputStream(CONTENT),
+            CONTENT.length));
+
+    assertSame(failure, thrown);
+    assertThat(keyOutputStream.getWriteAttempts()).isEqualTo(1);
+    assertCommitBlockedBy(guard, failure);
+  }
+
+  private static S3ObjectWriteGuard newGuard(long expectedLength) {
+    KeyMetadataAwareOutputStream keyOutputStream =
+        new KeyMetadataAwareOutputStream(Collections.emptyMap());
+    return new S3ObjectWriteGuard(new OzoneOutputStream(keyOutputStream, null),
+        expectedLength, KEY_PATH);
+  }
+
+  private static InputStream failingInputStream(Throwable failure) {
+    return new InputStream() {
+      @Override
+      public int read() throws IOException {
+        throwFailure(failure);
+        return -1;
+      }
+
+      @Override
+      public int read(byte[] buffer, int offset, int length)
+          throws IOException {
+        throwFailure(failure);
+        return -1;
+      }
+    };
+  }
+
+  private static void throwFailure(Throwable failure) throws IOException {
+    if (failure instanceof IOException) {
+      throw (IOException) failure;
+    }
+    if (failure instanceof RuntimeException) {
+      throw (RuntimeException) failure;
+    }
+    throw new AssertionError(failure);
+  }
+
+  private static void assertCommitBlockedBy(
+      S3ObjectWriteGuard guard, Throwable failure) {
+    IOException commitFailure = assertThrows(IOException.class, guard::close);
+    assertThat(commitFailure.getMessage()).isEqualTo(
+        "S3 object transfer failed before commit: " + KEY_PATH);
+    assertSame(failure, commitFailure.getCause());
+  }
+
+  private static long writtenLength(S3ObjectWriteGuard guard)
+      throws ReflectiveOperationException {
+    Field field = S3ObjectWriteGuard.class.getDeclaredField("writtenLength");
+    field.setAccessible(true);
+    return field.getLong(guard);
+  }
+
+  private static final class FailingKeyMetadataAwareOutputStream
+      extends KeyMetadataAwareOutputStream {
+
+    private final IOException failure;
+    private int writeAttempts;
+
+    private FailingKeyMetadataAwareOutputStream(IOException failure) {
+      super(Collections.emptyMap());
+      this.failure = failure;
+    }
+
+    @Override
+    public void write(byte[] buffer, int offset, int length)
+        throws IOException {
+      writeAttempts++;
+      throw failure;
+    }
+
+    private int getWriteAttempts() {
+      return writeAttempts;
+    }
+  }
+
+  private static final class FailingKeyMetadataAwareByteBufferStreamOutput
+      extends KeyMetadataAwareByteBufferStreamOutput {
+
+    private final IOException failure;
+    private int writeAttempts;
+
+    private FailingKeyMetadataAwareByteBufferStreamOutput(
+        IOException failure) {
+      super(Collections.emptyMap());
+      this.failure = failure;
+    }
+
+    @Override
+    public void write(ByteBuffer buffer, int offset, int length)
+        throws IOException {
+      writeAttempts++;
+      throw failure;
+    }
+
+    private int getWriteAttempts() {
+      return writeAttempts;
+    }
+  }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to