Skip to content

Commit de780c3

Browse files
authored
Fix async getObject(request, Path) deleting a pre-existing file when the download fails before writing (#7353)
1 parent 6c9201a commit de780c3

3 files changed

Lines changed: 93 additions & 4 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"type": "bugfix",
3+
"category": "AWS SDK for Java v2",
4+
"contributor": "",
5+
"description": "Prevent `AsyncResponseTransformer.toFile` from deleting a pre-existing destination when a request fails before the SDK opens the file. Calls using `CREATE_NEW` now preserve the existing file and surface `FileAlreadyExistsException` instead of allowing a retry to overwrite it."
6+
}

core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/async/FileAsyncResponseTransformer.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ private AsynchronousFileChannel createChannel(Path path) throws IOException {
139139

140140
@Override
141141
public CompletableFuture<ResponseT> prepare() {
142+
fileChannel = null;
142143
cf = new CompletableFuture<>();
143144
cf.whenComplete((r, t) -> {
144145
if (t != null && fileChannel != null) {
@@ -169,14 +170,16 @@ public void onStream(SdkPublisher<ByteBuffer> publisher) {
169170

170171
@Override
171172
public void exceptionOccurred(Throwable throwable) {
173+
AsynchronousFileChannel currentFileChannel = fileChannel;
174+
fileChannel = null;
172175
try {
173-
if (fileChannel != null) {
176+
if (currentFileChannel != null) {
174177
runAndLogError(log.logger(),
175178
String.format("Failed to close the file %s, resource may be leaked", path),
176-
() -> fileChannel.close());
179+
currentFileChannel::close);
177180
}
178181
} finally {
179-
if (configuration.failureBehavior() == FailureBehavior.DELETE) {
182+
if (currentFileChannel != null && configuration.failureBehavior() == FailureBehavior.DELETE) {
180183
runAndLogError(log.logger(),
181184
String.format("Failed to delete the file %s", path),
182185
() -> Files.deleteIfExists(path));

core/sdk-core/src/test/java/software/amazon/awssdk/core/internal/async/FileAsyncResponseTransformerTest.java

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,8 @@ public void synchronousPublisher_shouldNotHang() throws Exception {
121121
@Test
122122
void noConfiguration_fileAlreadyExists_shouldThrowException() throws Exception {
123123
Path testPath = testFs.getPath("test_file.txt");
124-
Files.write(testPath, RandomStringUtils.random(1000).getBytes(StandardCharsets.UTF_8));
124+
String existingContent = RandomStringUtils.randomAlphanumeric(1000);
125+
Files.write(testPath, existingContent.getBytes(StandardCharsets.UTF_8));
125126
assertThat(testPath).exists();
126127

127128
String content = RandomStringUtils.randomAlphanumeric(30000);
@@ -131,6 +132,7 @@ void noConfiguration_fileAlreadyExists_shouldThrowException() throws Exception {
131132
transformer.onResponse("foobar");
132133
transformer.onStream(testPublisher(content));
133134
assertThatThrownBy(() -> future.join()).hasRootCauseInstanceOf(FileAlreadyExistsException.class);
135+
assertThat(testPath).hasContent(existingContent);
134136
}
135137

136138
@Test
@@ -186,6 +188,73 @@ void createOrAppendExisting_fileExists_shouldAppend() throws Exception {
186188
assertThat(testPath).hasContent(existingString + content);
187189
}
188190

191+
@ParameterizedTest
192+
@MethodSource("deleteConfigurations")
193+
void exceptionOccurred_beforeFileOpened_shouldPreserveExistingFile(FileTransformerConfiguration configuration)
194+
throws Exception {
195+
Path testPath = testFs.getPath("test_file.txt");
196+
String existingContent = RandomStringUtils.randomAlphanumeric(1000);
197+
Files.write(testPath, existingContent.getBytes(StandardCharsets.UTF_8));
198+
199+
FileAsyncResponseTransformer<String> transformer = new FileAsyncResponseTransformer<>(testPath, configuration);
200+
CompletableFuture<String> future = transformer.prepare();
201+
RuntimeException exception = new RuntimeException("oops");
202+
transformer.exceptionOccurred(exception);
203+
204+
assertThat(future).failsWithin(1, TimeUnit.SECONDS)
205+
.withThrowableOfType(ExecutionException.class)
206+
.withCause(exception);
207+
assertThat(testPath).hasContent(existingContent);
208+
}
209+
210+
@Test
211+
void exceptionOccurred_beforeFileOpenedOnRetry_shouldPreserveExistingFile() throws Exception {
212+
Path testPath = testFs.getPath("test_file.txt");
213+
FileAsyncResponseTransformer<String> transformer = new FileAsyncResponseTransformer<>(testPath);
214+
215+
stubException(transformer);
216+
assertThat(testPath).doesNotExist();
217+
218+
String existingContent = RandomStringUtils.randomAlphanumeric(1000);
219+
Files.write(testPath, existingContent.getBytes(StandardCharsets.UTF_8));
220+
CompletableFuture<String> future = transformer.prepare();
221+
RuntimeException exception = new RuntimeException("oops");
222+
transformer.exceptionOccurred(exception);
223+
224+
assertThat(future).failsWithin(1, TimeUnit.SECONDS)
225+
.withThrowableOfType(ExecutionException.class)
226+
.withCause(exception);
227+
assertThat(testPath).hasContent(existingContent);
228+
}
229+
230+
@Test
231+
void exceptionOccurred_afterFileOpened_shouldAllowRetry() throws Exception {
232+
Path testPath = testFs.getPath("test_file.txt");
233+
FileAsyncResponseTransformer<String> transformer = new FileAsyncResponseTransformer<>(testPath);
234+
235+
stubException(transformer);
236+
assertThat(testPath).doesNotExist();
237+
238+
String content = RandomStringUtils.randomAlphanumeric(1000);
239+
stubSuccessfulStreaming(content, transformer);
240+
assertThat(testPath).hasContent(content);
241+
}
242+
243+
@Test
244+
void exceptionOccurred_calledTwice_shouldNotDeleteReplacement() throws Exception {
245+
Path testPath = testFs.getPath("test_file.txt");
246+
FileAsyncResponseTransformer<String> transformer = new FileAsyncResponseTransformer<>(testPath);
247+
248+
stubException(transformer);
249+
assertThat(testPath).doesNotExist();
250+
251+
String replacementContent = RandomStringUtils.randomAlphanumeric(1000);
252+
Files.write(testPath, replacementContent.getBytes(StandardCharsets.UTF_8));
253+
transformer.exceptionOccurred(new RuntimeException("second callback"));
254+
255+
assertThat(testPath).hasContent(replacementContent);
256+
}
257+
189258
@ParameterizedTest
190259
@MethodSource("configurations")
191260
void exceptionOccurred_deleteFileBehavior(FileTransformerConfiguration configuration) throws Exception {
@@ -203,6 +272,17 @@ void exceptionOccurred_deleteFileBehavior(FileTransformerConfiguration configura
203272
}
204273
}
205274

275+
private static List<FileTransformerConfiguration> deleteConfigurations() {
276+
List<FileTransformerConfiguration> conf = new ArrayList<>();
277+
for (FileWriteOption fileWriteOption : FileWriteOption.values()) {
278+
conf.add(FileTransformerConfiguration.builder()
279+
.fileWriteOption(fileWriteOption)
280+
.failureBehavior(DELETE)
281+
.build());
282+
}
283+
return conf;
284+
}
285+
206286
private static List<FileTransformerConfiguration> configurations() {
207287
List<FileTransformerConfiguration> conf = new ArrayList<>();
208288
conf.add(FileTransformerConfiguration.defaultCreateNew());

0 commit comments

Comments
 (0)