|
18 | 18 | import static software.amazon.awssdk.core.interceptor.SdkInternalExecutionAttribute.SDK_HTTP_EXECUTION_ATTRIBUTES; |
19 | 19 | import static software.amazon.awssdk.services.s3.crt.S3CrtSdkHttpExecutionAttribute.CRT_PROGRESS_LISTENER; |
20 | 20 | import static software.amazon.awssdk.services.s3.crt.S3CrtSdkHttpExecutionAttribute.METAREQUEST_PAUSE_OBSERVABLE; |
| 21 | +import static software.amazon.awssdk.services.s3.internal.crt.DefaultS3CrtAsyncClient.RESPONSE_FILE_OPTION; |
| 22 | +import static software.amazon.awssdk.services.s3.internal.crt.DefaultS3CrtAsyncClient.RESPONSE_FILE_PATH; |
21 | 23 | import static software.amazon.awssdk.services.s3.internal.crt.S3InternalSdkHttpExecutionAttribute.CRT_PAUSE_RESUME_TOKEN; |
| 24 | +import static software.amazon.awssdk.transfer.s3.internal.utils.ResumableRequestConverter.canResumeDownload; |
| 25 | +import static software.amazon.awssdk.transfer.s3.internal.utils.ResumableRequestConverter.toCrtDownloadFileRequest; |
22 | 26 |
|
23 | 27 | import java.util.concurrent.CompletableFuture; |
24 | 28 | import java.util.function.Consumer; |
25 | 29 | import software.amazon.awssdk.annotations.SdkInternalApi; |
26 | 30 | import software.amazon.awssdk.awscore.AwsRequestOverrideConfiguration; |
27 | 31 | import software.amazon.awssdk.core.async.AsyncRequestBody; |
| 32 | +import software.amazon.awssdk.core.async.AsyncResponseTransformer; |
28 | 33 | import software.amazon.awssdk.crt.s3.ResumeToken; |
| 34 | +import software.amazon.awssdk.crt.s3.S3MetaRequestOptions.ResponseFileOption; |
29 | 35 | import software.amazon.awssdk.http.SdkHttpExecutionAttributes; |
30 | 36 | import software.amazon.awssdk.services.s3.S3AsyncClient; |
| 37 | +import software.amazon.awssdk.services.s3.internal.crt.CrtResponseFileResponseTransformer; |
31 | 38 | import software.amazon.awssdk.services.s3.internal.crt.S3MetaRequestPauseObservable; |
| 39 | +import software.amazon.awssdk.services.s3.model.GetObjectRequest; |
| 40 | +import software.amazon.awssdk.services.s3.model.GetObjectResponse; |
| 41 | +import software.amazon.awssdk.services.s3.model.HeadObjectResponse; |
32 | 42 | import software.amazon.awssdk.services.s3.model.PutObjectRequest; |
33 | 43 | import software.amazon.awssdk.services.s3.model.PutObjectResponse; |
34 | 44 | import software.amazon.awssdk.transfer.s3.S3TransferManager; |
| 45 | +import software.amazon.awssdk.transfer.s3.internal.model.CrtFileDownload; |
35 | 46 | import software.amazon.awssdk.transfer.s3.internal.model.CrtFileUpload; |
36 | 47 | import software.amazon.awssdk.transfer.s3.internal.model.DefaultUpload; |
| 48 | +import software.amazon.awssdk.transfer.s3.internal.progress.ResumeTransferProgress; |
37 | 49 | import software.amazon.awssdk.transfer.s3.internal.progress.TransferProgressUpdater; |
| 50 | +import software.amazon.awssdk.transfer.s3.model.CompletedFileDownload; |
38 | 51 | import software.amazon.awssdk.transfer.s3.model.CompletedFileUpload; |
39 | 52 | import software.amazon.awssdk.transfer.s3.model.CompletedUpload; |
| 53 | +import software.amazon.awssdk.transfer.s3.model.DownloadFileRequest; |
| 54 | +import software.amazon.awssdk.transfer.s3.model.FileDownload; |
40 | 55 | import software.amazon.awssdk.transfer.s3.model.FileUpload; |
| 56 | +import software.amazon.awssdk.transfer.s3.model.ResumableFileDownload; |
41 | 57 | import software.amazon.awssdk.transfer.s3.model.ResumableFileUpload; |
42 | 58 | import software.amazon.awssdk.transfer.s3.model.Upload; |
43 | 59 | import software.amazon.awssdk.transfer.s3.model.UploadFileRequest; |
44 | 60 | import software.amazon.awssdk.transfer.s3.model.UploadRequest; |
| 61 | +import software.amazon.awssdk.transfer.s3.progress.TransferProgress; |
45 | 62 | import software.amazon.awssdk.utils.CompletableFutureUtils; |
| 63 | +import software.amazon.awssdk.utils.Logger; |
46 | 64 | import software.amazon.awssdk.utils.Validate; |
47 | 65 |
|
48 | 66 | /** |
49 | 67 | * An implementation of {@link S3TransferManager} that uses CRT-based S3 client under the hood. |
50 | 68 | */ |
51 | 69 | @SdkInternalApi |
52 | 70 | class CrtS3TransferManager extends GenericS3TransferManager { |
| 71 | + private static final Logger log = Logger.loggerFor(S3TransferManager.class); |
| 72 | + |
53 | 73 | private final S3AsyncClient s3AsyncClient; |
54 | 74 |
|
55 | 75 | CrtS3TransferManager(TransferManagerConfiguration transferConfiguration, S3AsyncClient s3AsyncClient, |
@@ -137,6 +157,137 @@ FileUpload doResumeUpload(ResumableFileUpload resumableFileUpload) { |
137 | 157 | .build()); |
138 | 158 | } |
139 | 159 |
|
| 160 | + /** |
| 161 | + * Downloads the object by handing the destination file to CRT, which writes the response body to it directly rather than |
| 162 | + * streaming the body back through the SDK. |
| 163 | + */ |
| 164 | + @Override |
| 165 | + FileDownload doDownloadFile(DownloadFileRequest downloadRequest) { |
| 166 | + S3MetaRequestPauseObservable observable = new S3MetaRequestPauseObservable(); |
| 167 | + TransferProgressUpdater progressUpdater = new TransferProgressUpdater(downloadRequest, null); |
| 168 | + |
| 169 | + DownloadFileRequest crtDownloadRequest = crtDownloadFileRequest(downloadRequest, observable, progressUpdater, |
| 170 | + ResponseFileOption.CREATE_OR_REPLACE); |
| 171 | + |
| 172 | + CompletableFuture<CompletedFileDownload> returnFuture = new CompletableFuture<>(); |
| 173 | + initiateCrtDownload(crtDownloadRequest, progressUpdater, returnFuture); |
| 174 | + |
| 175 | + return new CrtFileDownload(returnFuture, progressUpdater.progress(), observable, () -> crtDownloadRequest, null); |
| 176 | + } |
| 177 | + |
| 178 | + @Override |
| 179 | + FileDownload doResumeDownloadFile(ResumableFileDownload resumableFileDownload) { |
| 180 | + DownloadFileRequest originalDownloadRequest = resumableFileDownload.downloadFileRequest(); |
| 181 | + GetObjectRequest getObjectRequest = originalDownloadRequest.getObjectRequest(); |
| 182 | + |
| 183 | + CompletableFuture<CompletedFileDownload> returnFuture = new CompletableFuture<>(); |
| 184 | + CompletableFuture<TransferProgress> progressFuture = new CompletableFuture<>(); |
| 185 | + CompletableFuture<DownloadFileRequest> newDownloadFileRequestFuture = new CompletableFuture<>(); |
| 186 | + S3MetaRequestPauseObservable observable = new S3MetaRequestPauseObservable(); |
| 187 | + |
| 188 | + CompletableFuture<HeadObjectResponse> headFuture = |
| 189 | + s3AsyncClient.headObject(b -> b.bucket(getObjectRequest.bucket()).key(getObjectRequest.key())); |
| 190 | + |
| 191 | + // Ensure cancellations are forwarded to the head future |
| 192 | + CompletableFutureUtils.forwardExceptionTo(returnFuture, headFuture); |
| 193 | + |
| 194 | + headFuture.thenAccept(headObjectResponse -> { |
| 195 | + boolean restartFromBeginning = !canResumeDownload(resumableFileDownload, headObjectResponse) |
| 196 | + || hasCompletedParts(resumableFileDownload); |
| 197 | + |
| 198 | + DownloadFileRequest newDownloadFileRequest = toCrtDownloadFileRequest(resumableFileDownload, headObjectResponse, |
| 199 | + originalDownloadRequest, |
| 200 | + restartFromBeginning); |
| 201 | + |
| 202 | + // CRT appends to whatever the destination file already holds, so appending is only correct when the download is |
| 203 | + // genuinely being continued. Otherwise the file has to be replaced. |
| 204 | + ResponseFileOption responseFileOption = restartFromBeginning ? ResponseFileOption.CREATE_OR_REPLACE |
| 205 | + : ResponseFileOption.CREATE_OR_APPEND; |
| 206 | + |
| 207 | + TransferProgressUpdater progressUpdater = new TransferProgressUpdater(newDownloadFileRequest, null); |
| 208 | + DownloadFileRequest crtDownloadRequest = crtDownloadFileRequest(newDownloadFileRequest, observable, |
| 209 | + progressUpdater, responseFileOption); |
| 210 | + |
| 211 | + newDownloadFileRequestFuture.complete(crtDownloadRequest); |
| 212 | + log.debug(() -> "Sending downloadFileRequest " + crtDownloadRequest); |
| 213 | + |
| 214 | + initiateCrtDownload(crtDownloadRequest, progressUpdater, returnFuture); |
| 215 | + progressFuture.complete(progressUpdater.progress()); |
| 216 | + }).exceptionally(throwable -> { |
| 217 | + handleException(returnFuture, progressFuture, newDownloadFileRequestFuture, throwable); |
| 218 | + return null; |
| 219 | + }); |
| 220 | + |
| 221 | + return new CrtFileDownload(returnFuture, |
| 222 | + new ResumeTransferProgress(progressFuture), |
| 223 | + observable, |
| 224 | + () -> newOrOriginalRequestForPause(newDownloadFileRequestFuture, |
| 225 | + originalDownloadRequest), |
| 226 | + resumableFileDownload); |
| 227 | + } |
| 228 | + |
| 229 | + /** |
| 230 | + * A download that was paused while fetching individual parts can leave gaps in the destination file, because parts are |
| 231 | + * written at their own offsets rather than in order. The CRT-based client only ever appends to the end of the file, so |
| 232 | + * such a download cannot be continued and has to start over. This only arises when a {@link ResumableFileDownload} |
| 233 | + * produced by the Java-based transfer manager is resumed with the CRT-based one. |
| 234 | + */ |
| 235 | + private static boolean hasCompletedParts(ResumableFileDownload resumableFileDownload) { |
| 236 | + if (resumableFileDownload.completedParts().isEmpty()) { |
| 237 | + return false; |
| 238 | + } |
| 239 | + log.debug(() -> "The paused download had completed individual parts, which the CRT-based S3 client cannot continue " |
| 240 | + + "from. The SDK will download the S3 object from the beginning."); |
| 241 | + return true; |
| 242 | + } |
| 243 | + |
| 244 | + private void initiateCrtDownload(DownloadFileRequest downloadRequest, |
| 245 | + TransferProgressUpdater progressUpdater, |
| 246 | + CompletableFuture<CompletedFileDownload> returnFuture) { |
| 247 | + try { |
| 248 | + progressUpdater.transferInitiated(); |
| 249 | + AsyncResponseTransformer<GetObjectResponse, GetObjectResponse> responseTransformer = |
| 250 | + progressUpdater.wrapCrtResponseFileTransformer(new CrtResponseFileResponseTransformer<>()); |
| 251 | + progressUpdater.registerCompletion(returnFuture); |
| 252 | + |
| 253 | + assertNotUnsupportedArn(downloadRequest.getObjectRequest().bucket(), "download"); |
| 254 | + |
| 255 | + CompletableFuture<GetObjectResponse> crtFuture = |
| 256 | + s3AsyncClient.getObject(downloadRequest.getObjectRequest(), responseTransformer); |
| 257 | + |
| 258 | + // Forward download cancellation to CRT future |
| 259 | + CompletableFutureUtils.forwardExceptionTo(returnFuture, crtFuture); |
| 260 | + |
| 261 | + CompletableFutureUtils.forwardTransformedResultTo(crtFuture, returnFuture, |
| 262 | + res -> CompletedFileDownload.builder() |
| 263 | + .response(res) |
| 264 | + .build()); |
| 265 | + } catch (Throwable throwable) { |
| 266 | + returnFuture.completeExceptionally(throwable); |
| 267 | + } |
| 268 | + } |
| 269 | + |
| 270 | + /** |
| 271 | + * Attaches the execution attributes that tell the CRT-based client to write the response body straight to the destination |
| 272 | + * file, together with the progress listener and pause observable needed to report progress on and pause that transfer. |
| 273 | + */ |
| 274 | + private DownloadFileRequest crtDownloadFileRequest(DownloadFileRequest downloadRequest, |
| 275 | + S3MetaRequestPauseObservable observable, |
| 276 | + TransferProgressUpdater progressUpdater, |
| 277 | + ResponseFileOption responseFileOption) { |
| 278 | + GetObjectRequest getObjectRequest = attachSdkAttribute( |
| 279 | + downloadRequest.getObjectRequest(), |
| 280 | + b -> b.putExecutionAttribute(RESPONSE_FILE_PATH, downloadRequest.destination()) |
| 281 | + .putExecutionAttribute(RESPONSE_FILE_OPTION, responseFileOption)); |
| 282 | + |
| 283 | + GetObjectRequest crtGetObjectRequest = attachCrtSdkAttribute( |
| 284 | + getObjectRequest, |
| 285 | + b -> b.put(METAREQUEST_PAUSE_OBSERVABLE, observable) |
| 286 | + .put(CRT_PROGRESS_LISTENER, progressUpdater.crtProgressListener())); |
| 287 | + |
| 288 | + return downloadRequest.copy(r -> r.getObjectRequest(crtGetObjectRequest)); |
| 289 | + } |
| 290 | + |
140 | 291 | private static ResumeToken crtResumeToken(ResumableFileUpload resumableFileUpload) { |
141 | 292 | return new ResumeToken(new ResumeToken.PutResumeTokenBuilder() |
142 | 293 | .withNumPartsCompleted(resumableFileUpload.transferredParts().orElse(0L)) |
@@ -166,4 +317,20 @@ private PutObjectRequest attachCrtSdkAttribute(PutObjectRequest putObjectRequest |
166 | 317 | .overrideConfiguration(modifiedRequestOverrideConfig) |
167 | 318 | .build(); |
168 | 319 | } |
| 320 | + |
| 321 | + private GetObjectRequest attachCrtSdkAttribute(GetObjectRequest getObjectRequest, |
| 322 | + Consumer<SdkHttpExecutionAttributes.Builder> builderMutation) { |
| 323 | + SdkHttpExecutionAttributes existingAttributes = |
| 324 | + getObjectRequest.overrideConfiguration() |
| 325 | + .map(o -> o.executionAttributes().getAttribute(SDK_HTTP_EXECUTION_ATTRIBUTES)) |
| 326 | + .orElse(null); |
| 327 | + |
| 328 | + SdkHttpExecutionAttributes modifiedAttributes = |
| 329 | + (existingAttributes == null ? SdkHttpExecutionAttributes.builder() : existingAttributes.toBuilder()) |
| 330 | + .applyMutation(builderMutation) |
| 331 | + .build(); |
| 332 | + |
| 333 | + return attachSdkAttribute(getObjectRequest, |
| 334 | + b -> b.putExecutionAttribute(SDK_HTTP_EXECUTION_ATTRIBUTES, modifiedAttributes)); |
| 335 | + } |
169 | 336 | } |
0 commit comments