Skip to content

Commit e87fe34

Browse files
authored
feat. Async pause (#1008)
1 parent 2e982fc commit e87fe34

14 files changed

Lines changed: 753 additions & 40 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ concurrency:
1212
cancel-in-progress: true
1313

1414
env:
15-
BUILDER_VERSION: v0.9.96
15+
BUILDER_VERSION: latest
1616
BUILDER_SOURCE: releases
1717
BUILDER_HOST: https://d19elf31gohf1l.cloudfront.net
1818
PACKAGE_NAME: aws-crt-java

crt/aws-c-sdkutils

src/main/java/software/amazon/awssdk/crt/s3/ResumeToken.java

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,17 @@ public ResumeToken build() {
6464
private long numPartsCompleted;
6565
private String uploadId;
6666

67+
/* Download specific fields, populated by native code. */
68+
private String etag;
69+
private String versionId;
70+
private String s3ObjectLastModified;
71+
private long objectSize;
72+
private long objectRangeStart;
73+
private long objectRangeEnd;
74+
private long continuesDownloadedBytes;
75+
private long totalDownloadedBytes;
76+
private long fileLastModifiedEpochNs;
77+
6778
public ResumeToken(PutResumeTokenBuilder builder) {
6879
this.nativeType = S3MetaRequestOptions.MetaRequestType.PUT_OBJECT.getNativeValue();
6980
this.partSize = builder.partSize;
@@ -122,4 +133,122 @@ public String getUploadId() {
122133

123134
return uploadId;
124135
}
136+
137+
/******
138+
* Download Specific fields.
139+
******/
140+
141+
private void validateDownloadToken(String field) {
142+
if (getType() != S3MetaRequestOptions.MetaRequestType.GET_OBJECT) {
143+
throw new IllegalArgumentException(
144+
"ResumeToken - " + field + " is only defined for Get Object Resume tokens");
145+
}
146+
}
147+
148+
/**
149+
* ETag of the S3 object being downloaded, captured from the first response.
150+
* May be null/empty if the download was paused before the first response arrived.
151+
*
152+
* @return etag of the object
153+
*/
154+
public String getEtag() {
155+
validateDownloadToken("etag");
156+
return etag;
157+
}
158+
159+
/**
160+
* Version ID of the S3 object being downloaded.
161+
* Optional: null/empty when the bucket is unversioned or the version id was not captured.
162+
*
163+
* @return version id of the object
164+
*/
165+
public String getVersionId() {
166+
validateDownloadToken("version id");
167+
return versionId;
168+
}
169+
170+
/**
171+
* Last-Modified of the S3 object being downloaded, in HTTP-date format
172+
* (RFC 9110 5.6.7, "Wed, 09 Oct 2024 22:28:00 GMT"), captured from the first
173+
* response. The exact string in the response header.
174+
* Optional: null/empty if the value was not captured before the pause.
175+
*
176+
* @return Last-Modified of the object as an HTTP-date string
177+
*/
178+
public String getS3ObjectLastModified() {
179+
validateDownloadToken("s3 object last modified");
180+
return s3ObjectLastModified;
181+
}
182+
183+
/**
184+
* Total size of the S3 object being downloaded, regardless of any Range header
185+
* on the request (for a ranged download this is larger than the range being
186+
* fetched). 0 if the download was paused before the object size was discovered.
187+
*
188+
* @return total object size in bytes
189+
*/
190+
public long getObjectSize() {
191+
validateDownloadToken("object size");
192+
return objectSize;
193+
}
194+
195+
/**
196+
* Absolute byte offset in the object where the download's range starts.
197+
* 0 for a download without a Range header.
198+
*
199+
* @return range start offset in bytes
200+
*/
201+
public long getObjectRangeStart() {
202+
validateDownloadToken("object range start");
203+
return objectRangeStart;
204+
}
205+
206+
/**
207+
* Absolute byte offset in the object where the download's range ends (inclusive).
208+
* For a download without a Range header this is object size - 1.
209+
* 0 if the download was paused before the object size was discovered.
210+
*
211+
* @return range end offset in bytes (inclusive)
212+
*/
213+
public long getObjectRangeEnd() {
214+
validateDownloadToken("object range end");
215+
return objectRangeEnd;
216+
}
217+
218+
/**
219+
* Number of bytes downloaded continuously from the start of the range, with no
220+
* gaps. Everything before this offset (relative to the object range start) has
221+
* been downloaded.
222+
*
223+
* @return continuously downloaded bytes
224+
*/
225+
public long getContinuesDownloadedBytes() {
226+
validateDownloadToken("continues downloaded bytes");
227+
return continuesDownloadedBytes;
228+
}
229+
230+
/**
231+
* Total number of bytes downloaded before the pause. May be greater than
232+
* {@link #getContinuesDownloadedBytes()} when parts completed out of order,
233+
* leaving gaps. Equals it when delivery was strictly in order.
234+
*
235+
* @return total downloaded bytes
236+
*/
237+
public long getTotalDownloadedBytes() {
238+
validateDownloadToken("total downloaded bytes");
239+
return totalDownloadedBytes;
240+
}
241+
242+
/**
243+
* Last-modified time of the local receive file (nanoseconds since the Unix
244+
* epoch), captured after the file handle was closed during the pause.
245+
* Only set when the download was writing to a file; 0 for downloads that
246+
* deliver via body callback or when the timestamp could not be queried.
247+
*
248+
* @return local receive file last-modified time in epoch nanoseconds
249+
*/
250+
public long getFileLastModifiedEpochNs() {
251+
validateDownloadToken("file last modified epoch ns");
252+
return fileLastModifiedEpochNs;
253+
}
125254
}

src/main/java/software/amazon/awssdk/crt/s3/S3MetaRequest.java

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
package software.amazon.awssdk.crt.s3;
66

77
import java.util.concurrent.CompletableFuture;
8+
import software.amazon.awssdk.crt.CRT;
89
import software.amazon.awssdk.crt.CrtResource;
10+
import software.amazon.awssdk.crt.CrtRuntimeException;
911

1012
public class S3MetaRequest extends CrtResource {
1113

@@ -21,6 +23,23 @@ private void onShutdownComplete() {
2123
this.shutdownComplete.complete(null);
2224
}
2325

26+
/**
27+
* Called from native when an async pause completes. The resume token is null when no
28+
* resumable state was captured, which is not an error.
29+
*
30+
* @param future the future to complete with the pause result
31+
* @param errorCode 0 on success, otherwise the CRT error code that made the pause fail
32+
* @param resumeToken the resume token, or null if no resumable state was captured
33+
*/
34+
private static void onPauseComplete(
35+
CompletableFuture<ResumeToken> future, int errorCode, ResumeToken resumeToken) {
36+
if (errorCode != CRT.AWS_CRT_SUCCESS) {
37+
future.completeExceptionally(new CrtRuntimeException(errorCode));
38+
return;
39+
}
40+
future.complete(resumeToken);
41+
}
42+
2443
/**
2544
* Determines whether a resource releases its dependencies at the same time the
2645
* native handle is released or if it waits. Resources that wait are responsible
@@ -75,6 +94,38 @@ public ResumeToken pause() {
7594
return s3MetaRequestPause(getNativeHandle());
7695
}
7796

97+
/**
98+
* Asynchronously pause the meta request. Works for both uploads (PUT) and downloads (GET).
99+
* The returned future completes once all in-flight work has finished (in-flight parts for
100+
* uploads, file writes for downloads) and the resume token is ready.
101+
* <p>
102+
* For PutObject resume, input stream should always start at the beginning,
103+
* already uploaded parts will be skipped, but checksums on those will be verified if
104+
* the request specified a checksum algorithm.
105+
* <p>
106+
* Note: consuming a download (GET) resume token to resume via meta request options is not
107+
* supported yet. To resume a download, issue a new ranged GET starting at
108+
* {@link ResumeToken#getContinuesDownloadedBytes()} (offset from
109+
* {@link ResumeToken#getObjectRangeStart()}) through the end of the original download.
110+
*
111+
* @return future completed with the resume token once the pause completes. The token may be
112+
* null if the request had not progressed far enough to produce one (equivalent to
113+
* restarting the transfer). Completed exceptionally with a CrtRuntimeException if
114+
* the pause failed.
115+
*/
116+
public CompletableFuture<ResumeToken> pauseAsync() {
117+
if (isNull()) {
118+
throw new IllegalStateException("S3MetaRequest has been closed.");
119+
}
120+
CompletableFuture<ResumeToken> future = new CompletableFuture<>();
121+
try {
122+
s3MetaRequestPauseAsync(getNativeHandle(), future);
123+
} catch (Exception e) {
124+
future.completeExceptionally(e);
125+
}
126+
return future;
127+
}
128+
78129
/**
79130
* Increment the flow-control window, so that response data continues downloading.
80131
* <p>
@@ -114,5 +165,7 @@ public void incrementReadWindow(long bytes) {
114165

115166
private static native ResumeToken s3MetaRequestPause(long s3MetaRequest);
116167

168+
private static native void s3MetaRequestPauseAsync(long s3MetaRequest, CompletableFuture<ResumeToken> future);
169+
117170
private static native void s3MetaRequestIncrementReadWindow(long s3MetaRequest, long bytes);
118171
}

src/main/java/software/amazon/awssdk/crt/s3/S3MetaRequestResponseHandler.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,25 @@ default void onProgress(final S3MetaRequestProgress progress) {
7979
*/
8080
default void onTelemetry(S3RequestMetrics requestMetrics) {
8181
}
82+
83+
/**
84+
* Invoked with a resume token when the meta request fails unexpectedly.
85+
* Allows persisting state for later resume without re-transferring completed parts.
86+
* Supported for both upload (PUT) and download (GET) meta requests.
87+
* Always invoked exactly once on unexpected failure; not invoked on success or
88+
* explicit pause (use {@link S3MetaRequest#pauseAsync} for that).
89+
* The token is null when no resumable state was captured.
90+
* <p>
91+
* WARNING: for a file download with
92+
* {@link S3MetaRequestOptions#withResponseFileDeleteOnFailure} set true, the deletion
93+
* is respected — the partial file is deleted on error, leaving nothing to resume on,
94+
* and this callback fires with a null token. Do not set responseFileDeleteOnFailure
95+
* if you intend to resume from this callback's token.
96+
*
97+
* @param errorCode the CRT error code that caused the meta request to fail
98+
* @param resumeToken resumable state captured at failure time, null if no
99+
* resumable state was captured
100+
*/
101+
default void onErrorResumeToken(final int errorCode, final ResumeToken resumeToken) {
102+
}
82103
}

src/main/java/software/amazon/awssdk/crt/s3/S3MetaRequestResponseHandlerNativeAdapter.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,8 @@ void onProgress(final S3MetaRequestProgress progress) {
3636
void onTelemetry(final S3RequestMetrics requestMetrics) {
3737
responseHandler.onTelemetry(requestMetrics);
3838
}
39+
40+
void onErrorResumeToken(final int errorCode, final ResumeToken resumeToken) {
41+
responseHandler.onErrorResumeToken(errorCode, resumeToken);
42+
}
3943
}

src/main/resources/META-INF/native-image/software.amazon.awssdk/crt/aws-crt/jni-config.json

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2124,20 +2124,47 @@
21242124
{
21252125
"name": "software.amazon.awssdk.crt.s3.ResumeToken",
21262126
"fields": [
2127+
{
2128+
"name": "continuesDownloadedBytes"
2129+
},
2130+
{
2131+
"name": "etag"
2132+
},
2133+
{
2134+
"name": "fileLastModifiedEpochNs"
2135+
},
21272136
{
21282137
"name": "nativeType"
21292138
},
21302139
{
21312140
"name": "numPartsCompleted"
21322141
},
2142+
{
2143+
"name": "objectRangeEnd"
2144+
},
2145+
{
2146+
"name": "objectRangeStart"
2147+
},
2148+
{
2149+
"name": "objectSize"
2150+
},
21332151
{
21342152
"name": "partSize"
21352153
},
2154+
{
2155+
"name": "s3ObjectLastModified"
2156+
},
2157+
{
2158+
"name": "totalDownloadedBytes"
2159+
},
21362160
{
21372161
"name": "totalNumParts"
21382162
},
21392163
{
21402164
"name": "uploadId"
2165+
},
2166+
{
2167+
"name": "versionId"
21412168
}
21422169
],
21432170
"methods": [
@@ -2204,6 +2231,14 @@
22042231
{
22052232
"name": "software.amazon.awssdk.crt.s3.S3MetaRequest",
22062233
"methods": [
2234+
{
2235+
"name": "onPauseComplete",
2236+
"parameterTypes": [
2237+
"java.util.concurrent.CompletableFuture",
2238+
"int",
2239+
"software.amazon.awssdk.crt.s3.ResumeToken"
2240+
]
2241+
},
22072242
{
22082243
"name": "onShutdownComplete",
22092244
"parameterTypes": []
@@ -2364,6 +2399,13 @@
23642399
{
23652400
"name": "software.amazon.awssdk.crt.s3.S3MetaRequestResponseHandlerNativeAdapter",
23662401
"methods": [
2402+
{
2403+
"name": "onErrorResumeToken",
2404+
"parameterTypes": [
2405+
"int",
2406+
"software.amazon.awssdk.crt.s3.ResumeToken"
2407+
]
2408+
},
23672409
{
23682410
"name": "onFinished",
23692411
"parameterTypes": [

0 commit comments

Comments
 (0)