This is an automated email from the ASF dual-hosted git repository.
Samrat002 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/flink.git
The following commit(s) were added to refs/heads/master by this push:
new cda19d472d9 [FLINK-40595][s3] Preserve multipart uploads during
recoverable stream disposal
cda19d472d9 is described below
commit cda19d472d9b3cabe3eed97107674f7fa5540f6d
Author: Mate Czagany <[email protected]>
AuthorDate: Mon Sep 28 13:55:18 2026 +0200
[FLINK-40595][s3] Preserve multipart uploads during recoverable stream
disposal
---
docs/content.zh/docs/deployment/filesystems/s3.md | 6 +++
docs/content/docs/deployment/filesystems/s3.md | 6 +++
flink-filesystems/flink-s3-fs-native/README.md | 6 +++
.../NativeS3RecoverableFsDataOutputStream.java | 40 +++++++-------
.../NativeS3RecoverableFsDataOutputStreamTest.java | 62 +++-------------------
5 files changed, 48 insertions(+), 72 deletions(-)
diff --git a/docs/content.zh/docs/deployment/filesystems/s3.md
b/docs/content.zh/docs/deployment/filesystems/s3.md
index 45f87ec9c51..27d4a77bde2 100644
--- a/docs/content.zh/docs/deployment/filesystems/s3.md
+++ b/docs/content.zh/docs/deployment/filesystems/s3.md
@@ -153,6 +153,12 @@ The legacy configuration key `s3.path.style.access` is
still supported as a fall
The Native S3 FileSystem is a pure-Java implementation built on the AWS SDK v2
completely removing the dependency on Hadoop. It is registered under the
schemes *s3://* and *s3a://*. It provides a drop-in replacement for the Presto
and Hadoop implementations, supporting checkpointing, the [FileSink]({{< ref
"docs/connectors/datastream/filesystem" >}}) (via `RecoverableWriter`),
server-side encryption (SSE-S3, SSE-KMS), cross-account access via IAM role
assumption, entropy injection, and b [...]
+#### Cleaning Up Unfinished Uploads
+
+The Native S3 FileSystem never aborts a multipart upload on its own. An upload
that is in progress may be referenced by a checkpoint or savepoint, and Flink
cannot tell whether that is the case when a stream is closed, cancelled, or
fails to commit. Aborting such an upload would make the referencing checkpoint
or savepoint unrecoverable. Uploads that are no longer needed (for example
after a job is cancelled and never restored, or after a failed commit)
therefore remain in the bucket and [...]
+
+**You must configure an S3 lifecycle rule that aborts incomplete multipart
uploads** on every bucket written through this file system. Without such a
rule, abandoned uploads accumulate indefinitely. See the [S3 documentation on
aborting incomplete multipart
uploads](https://docs.aws.amazon.com/AmazonS3/latest/userguide/mpu-abort-incomplete-mpu-lifecycle-config.html)
for how to configure the rule. Choose a retention period long enough for
uploads to finish and for jobs to recover, includi [...]
+
#### Setup
To use the Native S3 FileSystem, copy the JAR file from the `opt` directory to
the `plugins` directory:
diff --git a/docs/content/docs/deployment/filesystems/s3.md
b/docs/content/docs/deployment/filesystems/s3.md
index 9f84f9e6349..40756c5d95c 100644
--- a/docs/content/docs/deployment/filesystems/s3.md
+++ b/docs/content/docs/deployment/filesystems/s3.md
@@ -153,6 +153,12 @@ The legacy configuration key `s3.path.style.access` is
still supported as a fall
The Native S3 FileSystem is a pure-Java implementation built on the AWS SDK v2
completely removing the dependency on Hadoop. It is registered under the
schemes *s3://* and *s3a://*. It provides a drop-in replacement for the Presto
and Hadoop implementations, supporting checkpointing, the [FileSink]({{< ref
"docs/connectors/datastream/filesystem" >}}) (via `RecoverableWriter`),
server-side encryption (SSE-S3, SSE-KMS), cross-account access via IAM role
assumption, entropy injection, and b [...]
+#### Cleaning Up Unfinished Uploads
+
+The Native S3 FileSystem never aborts a multipart upload on its own. An upload
that is in progress may be referenced by a checkpoint or savepoint, and Flink
cannot tell whether that is the case when a stream is closed, cancelled, or
fails to commit. Aborting such an upload would make the referencing checkpoint
or savepoint unrecoverable. Uploads that are no longer needed (for example
after a job is cancelled and never restored, or after a failed commit)
therefore remain in the bucket and [...]
+
+**You must configure an S3 lifecycle rule that aborts incomplete multipart
uploads** on every bucket written through this file system. Without such a
rule, abandoned uploads accumulate indefinitely. See the [S3 documentation on
aborting incomplete multipart
uploads](https://docs.aws.amazon.com/AmazonS3/latest/userguide/mpu-abort-incomplete-mpu-lifecycle-config.html)
for how to configure the rule. Choose a retention period long enough for
uploads to finish and for jobs to recover, includi [...]
+
#### Setup
To use the Native S3 FileSystem, copy the JAR file from the `opt` directory to
the `plugins` directory:
diff --git a/flink-filesystems/flink-s3-fs-native/README.md
b/flink-filesystems/flink-s3-fs-native/README.md
index 7f45ffe13ab..4686186099e 100644
--- a/flink-filesystems/flink-s3-fs-native/README.md
+++ b/flink-filesystems/flink-s3-fs-native/README.md
@@ -6,6 +6,12 @@ This module provides a native S3 filesystem implementation for
Apache Flink usin
The Native S3 FileSystem is a direct implementation of Flink's FileSystem
interface using AWS SDK v2, without Hadoop dependencies. It provides
exactly-once semantics for checkpointing and file sinks through S3 multipart
uploads.
+### Cleaning Up Unfinished Uploads
+
+The Native S3 FileSystem never aborts a multipart upload on its own. An upload
that is in progress may be referenced by a checkpoint or savepoint, and Flink
cannot tell whether that is the case when a stream is closed, cancelled, or
fails to commit. Aborting such an upload would make the referencing checkpoint
or savepoint unrecoverable. Uploads that are no longer needed (for example
after a job is cancelled and never restored, or after a failed commit)
therefore remain in the bucket and [...]
+
+**You must configure an S3 lifecycle rule that aborts incomplete multipart
uploads** on every bucket written through this file system. Without such a
rule, abandoned uploads accumulate indefinitely. See the [S3 documentation on
aborting incomplete multipart
uploads](https://docs.aws.amazon.com/AmazonS3/latest/userguide/mpu-abort-incomplete-mpu-lifecycle-config.html)
for how to configure the rule. Choose a retention period long enough for
uploads to finish and for jobs to recover, includi [...]
+
## Supported URI Schemes
This module supports both `s3://` and `s3a://` URI schemes:
diff --git
a/flink-filesystems/flink-s3-fs-native/src/main/java/org/apache/flink/fs/s3native/writer/NativeS3RecoverableFsDataOutputStream.java
b/flink-filesystems/flink-s3-fs-native/src/main/java/org/apache/flink/fs/s3native/writer/NativeS3RecoverableFsDataOutputStream.java
index 4afe81c3a9c..79084c23eab 100644
---
a/flink-filesystems/flink-s3-fs-native/src/main/java/org/apache/flink/fs/s3native/writer/NativeS3RecoverableFsDataOutputStream.java
+++
b/flink-filesystems/flink-s3-fs-native/src/main/java/org/apache/flink/fs/s3native/writer/NativeS3RecoverableFsDataOutputStream.java
@@ -51,6 +51,12 @@ import java.util.concurrent.locks.ReentrantLock;
* {@link ReentrantLock} guards the critical sections in {@link #close()},
{@link
* #closeForCommit()}, and {@link #persist()} to ensure safe cleanup of local
resources without
* corrupting S3 state.
+ *
+ * <p>This stream never aborts its multipart upload. The upload may be
referenced by a {@link
+ * NativeS3Recoverable} in a retained checkpoint or savepoint, and the stream
cannot know whether
+ * that is the case. Aborting it would make the referencing snapshot
unrecoverable. Abandoned
+ * uploads are expected to be removed by an S3 lifecycle rule for incomplete
multipart uploads, as
+ * with the Hadoop-based S3 file system.
*/
@NotThreadSafe
class NativeS3RecoverableFsDataOutputStream extends
RecoverableFsDataOutputStream {
@@ -199,7 +205,7 @@ class NativeS3RecoverableFsDataOutputStream extends
RecoverableFsDataOutputStrea
// Do not delete the temp file if uploadPart fails: propagate the
original exception
// unmasked and let the cleanup path (close() or the closeForCommit()
failure handler)
- // delete it and abort the upload. nextPartNumber is only advanced on
success.
+ // delete it. nextPartNumber is only advanced on success.
NativeS3ObjectOperations.UploadPartResult result =
s3AccessHelper.uploadPart(
key, uploadId, nextPartNumber, currentTempFile,
currentPartSize);
@@ -233,11 +239,20 @@ class NativeS3RecoverableFsDataOutputStream extends
RecoverableFsDataOutputStrea
new NativeS3Recoverable(
key, uploadId, new
ArrayList<>(completedParts), numBytesInParts);
} catch (IOException e) {
- // The commit failed after the multipart upload had been
created and parts may
- // already have been uploaded. Abort it so it does not leak as
an orphan upload.
+ // Only local resources are released. The upload is
deliberately left open: a
+ // previous persist() may have handed it out in a recoverable
that a completed
+ // checkpoint references, and aborting it would break recovery
from that
+ // checkpoint. See the class-level Javadoc.
closed = true;
+ LOG.warn(
+ "Failed to close multipart upload for commit (key={},
uploadId={}). The "
+ + "upload is left open because a checkpoint
may still reference "
+ + "it; an S3 lifecycle rule is expected to
remove it if unused.",
+ key,
+ uploadId,
+ e);
try {
- tryAbortUploadAndReleaseResources();
+ releaseLocalResources();
} catch (IOException cleanup) {
e.addSuppressed(cleanup);
}
@@ -285,15 +300,15 @@ class NativeS3RecoverableFsDataOutputStream extends
RecoverableFsDataOutputStrea
try {
if (!closed) {
closed = true;
- tryAbortUploadAndReleaseResources();
+ releaseLocalResources();
}
} finally {
unlock();
}
}
- /** Aborts the multipart upload and releases local resources on the best
effort basis. */
- private void tryAbortUploadAndReleaseResources() throws IOException {
+ /** Closes the local buffer and deletes only the local temp file. */
+ private void releaseLocalResources() throws IOException {
IOException collected = null;
if (currentOutputStream != null) {
try {
@@ -309,17 +324,6 @@ class NativeS3RecoverableFsDataOutputStream extends
RecoverableFsDataOutputStrea
collected = ExceptionUtils.firstOrSuppressed(e, collected);
}
}
- try {
- s3AccessHelper.abortMultiPartUpload(key, uploadId);
- } catch (IOException e) {
- LOG.warn(
- "Failed to abort multipart upload (key={}, uploadId={});
it may be left as an "
- + "orphan upload in S3. Propagating the failure to
the caller.",
- key,
- uploadId,
- e);
- collected = ExceptionUtils.firstOrSuppressed(e, collected);
- }
if (collected != null) {
throw collected;
}
diff --git
a/flink-filesystems/flink-s3-fs-native/src/test/java/org/apache/flink/fs/s3native/writer/NativeS3RecoverableFsDataOutputStreamTest.java
b/flink-filesystems/flink-s3-fs-native/src/test/java/org/apache/flink/fs/s3native/writer/NativeS3RecoverableFsDataOutputStreamTest.java
index 99a6824b9af..c2d63ac9c32 100644
---
a/flink-filesystems/flink-s3-fs-native/src/test/java/org/apache/flink/fs/s3native/writer/NativeS3RecoverableFsDataOutputStreamTest.java
+++
b/flink-filesystems/flink-s3-fs-native/src/test/java/org/apache/flink/fs/s3native/writer/NativeS3RecoverableFsDataOutputStreamTest.java
@@ -61,7 +61,7 @@ class NativeS3RecoverableFsDataOutputStreamTest {
}
@Test
- void closeForCommitAbortsMultipartUploadWhenPartUploadFails() throws
Exception {
+ void closeForCommitKeepsMultipartUploadWhenPartUploadFails() throws
Exception {
s3.failUploadPart = true;
assertThatThrownBy(stream::closeForCommit)
@@ -69,56 +69,19 @@ class NativeS3RecoverableFsDataOutputStreamTest {
.hasMessageContaining("injected uploadPart failure");
assertThat(s3.abortAttempts)
- .as("closeForCommit must abort the upload on failure")
- .isEqualTo(1);
- assertThat(s3.openMultipartUploads)
- .as("the multipart upload must not leak after a failed commit")
- .doesNotContainKey(uploadId);
- assertThat(countLocalFilesIn(tmp)).as("the local temp file must be
cleaned up").isZero();
- }
-
- @Test
- void closeForCommitSurfacesAbortFailureWhenBothUploadAndAbortFail() throws
Exception {
- s3.failUploadPart = true;
- s3.failAbortMultiPartUpload = true;
-
- assertThatThrownBy(stream::closeForCommit)
- .isInstanceOf(IOException.class)
- .hasMessageContaining("injected uploadPart failure")
- .satisfies(
- t ->
- assertThat(t.getSuppressed())
- .as("the abort failure must be
surfaced, not swallowed")
- .anySatisfy(
- s ->
- assertThat(s)
-
.hasMessageContaining(
-
"injected abort failure")));
-
- assertThat(s3.abortAttempts).isEqualTo(1);
- }
-
- @Test
- void closeSurfacesAbortFailureInsteadOfSwallowingIt() throws Exception {
- s3.failAbortMultiPartUpload = true;
-
- assertThatThrownBy(stream::close)
- .isInstanceOf(IOException.class)
- .hasMessageContaining("injected abort failure");
-
- assertThat(s3.abortAttempts).isEqualTo(1);
- assertThat(countLocalFilesIn(tmp))
- .as("local resources are still released even when the abort
fails")
+ .as("closeForCommit must not abort the upload, a snapshot may
reference it")
.isZero();
+ assertThat(s3.openMultipartUploads).containsKey(uploadId);
+ assertThat(countLocalFilesIn(tmp)).as("the local temp file must be
cleaned up").isZero();
}
- /** An abnormal {@code close()} aborts the upload and releases local
state. */
+ /** An abnormal {@code close()} releases local state but keeps the upload.
*/
@Test
- void closeAbortsMultipartUploadOnAbnormalClose() throws Exception {
+ void closeKeepsMultipartUploadOnAbnormalClose() throws Exception {
stream.close();
- assertThat(s3.abortAttempts).isEqualTo(1);
- assertThat(s3.openMultipartUploads).doesNotContainKey(uploadId);
+ assertThat(s3.abortAttempts).isZero();
+ assertThat(s3.openMultipartUploads).containsKey(uploadId);
assertThat(countLocalFilesIn(tmp)).isZero();
}
@@ -130,10 +93,6 @@ class NativeS3RecoverableFsDataOutputStreamTest {
assertThatThrownBy(failingStream::close)
.isInstanceOf(IOException.class)
.hasMessageContaining("injected temp-file delete failure");
-
- assertThat(s3.abortAttempts)
- .as("abort is still attempted despite delete failure")
- .isEqualTo(1);
}
@Test
@@ -261,7 +220,6 @@ class NativeS3RecoverableFsDataOutputStreamTest {
racingStream.close();
- assertThat(s3.abortAttempts).isEqualTo(1);
assertThat(countLocalFilesIn(dir)).isZero();
}
@@ -314,7 +272,6 @@ class NativeS3RecoverableFsDataOutputStreamTest {
final Map<String, Map<Integer, byte[]>> openMultipartUploads = new
HashMap<>();
boolean failUploadPart = false;
- boolean failAbortMultiPartUpload = false;
boolean deletePartFileAfterUpload = false;
int abortAttempts = 0;
int uploadPartAttempts = 0;
@@ -393,9 +350,6 @@ class NativeS3RecoverableFsDataOutputStreamTest {
@Override
public void abortMultiPartUpload(String key, String uploadId) throws
IOException {
abortAttempts++;
- if (failAbortMultiPartUpload) {
- throw new IOException("injected abort failure for uploadId: "
+ uploadId);
- }
openMultipartUploads.remove(uploadId);
}
}