Skip to content

Commit 0f469be

Browse files
committed
HADOOP-17244: multi object delete failure handling
-improve test, fix production code -transient failure of a root test, likely cause: debugging test runs had got store/ddb inconsistent. Slightly improved error message on assert and temp dirs to include full timestamp to identify when the failure happened. TODO: ITest of dir-under-dir where a parent dir marker is deleted but a child dir is not. S3Guard must still be valid Change-Id: I2c91aa234d333072b2bf7ee9d8d844ebc7d31eb7
1 parent 499368a commit 0f469be

File tree

4 files changed

+57
-17
lines changed

4 files changed

+57
-17
lines changed

hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/contract/AbstractContractRootDirectoryTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ public void testRecursiveRootListing() throws IOException {
254254
fs.listFiles(root, true));
255255
describe("verifying consistency with treewalk's files");
256256
ContractTestUtils.TreeScanResults treeWalk = treeWalk(fs, root);
257-
treeWalk.assertFieldsEquivalent("files", listing,
257+
treeWalk.assertFieldsEquivalent("treewalk vs listFiles(/, true)", listing,
258258
treeWalk.getFiles(),
259259
listing.getFiles());
260260
}

hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/impl/MultiObjectDeleteSupport.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ static List<KeyPath> removeUndeletedPaths(
331331
List<KeyPath> undeleted = extractUndeletedKeyPaths(deleteException, qualifier);
332332
// and remove them from the undeleted list, matching on key
333333
for (KeyPath undel : undeleted) {
334-
pathsBeingDeleted.removeIf(kp -> kp.equals(undel));
334+
pathsBeingDeleted.removeIf(kp -> kp.getPath().equals(undel.getPath()));
335335
}
336336
return undeleted;
337337
}

hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/impl/ITestPartialRenamesDeletes.java

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -352,16 +352,17 @@ protected Configuration createConfiguration() {
352352

353353
/**
354354
* Create a unique path, which includes method name,
355-
* multidelete flag and a random UUID.
355+
* multidelete flag and a timestamp.
356356
* @return a string to use for paths.
357357
* @throws IOException path creation failure.
358358
*/
359359
private Path uniquePath() throws IOException {
360+
long now = System.currentTimeMillis();
360361
return path(
361-
String.format("%s-%s-%04d",
362+
String.format("%s-%s-%06d.%03d",
362363
getMethodName(),
363364
multiDelete ? "multi" : "single",
364-
System.currentTimeMillis() % 10000));
365+
now / 1000, now % 1000));
365366
}
366367

367368
/**
@@ -642,8 +643,6 @@ public void testPartialDirDelete() throws Throwable {
642643
fileCount, dirCount,
643644
new ArrayList<>(fileCount),
644645
dirs);
645-
createFiles(fs, readOnlyDir,
646-
dirDepth, fileCount, dirCount);
647646
List<Path> deletableFiles = createFiles(fs,
648647
writableDir, dirDepth, fileCount, dirCount);
649648

@@ -724,6 +723,25 @@ public void testPartialDirDelete() throws Throwable {
724723
.containsExactlyInAnyOrderElementsOf(readOnlyFiles);
725724
}
726725

726+
/**
727+
* Verifies the logic of handling directory markers in
728+
* delete operations, specifically:
729+
* <ol>
730+
* <li>all markers above empty directories MUST be deleted</li>
731+
* <li>all markers above non-empty directories MUST NOT be deleted</li>
732+
* </ol>
733+
* As the delete list may include subdirectories, we need to work up from
734+
* the bottom of the list of deleted files before probing the parents,
735+
* that being done by a s3guard get(path, need-empty-directory) call.
736+
* <p></p>
737+
* This is pretty sensitive code.
738+
*/
739+
@Test
740+
public void testSubdirDeleteFailures() throws Throwable {
741+
describe("Multiobject delete handling of directorYesFory markers");
742+
assume("Multiobject delete only", multiDelete);
743+
}
744+
727745
/**
728746
* Expect the delete() call to fail.
729747
* @param path path to delete.

hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/impl/TestPartialDeleteFailures.java

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,9 @@ private MultiObjectDeleteException createDeleteException(
137137
}
138138

139139
/**
140-
* From a list of paths, build up the list of keys for a delete request.
140+
* From a list of paths, build up the list of KeyVersion records
141+
* for a delete request.
142+
* All the entries will be files (i.e. no trailing /)
141143
* @param paths path list
142144
* @return a key list suitable for a delete request.
143145
*/
@@ -152,23 +154,40 @@ public static List<DeleteObjectsRequest.KeyVersion> keysToDelete(
152154
.collect(Collectors.toList());
153155
}
154156

157+
/**
158+
* From a list of keys, build up the list of keys for a delete request.
159+
* If a key has a trailing /, that will be retained, so it will be
160+
* considered a directory during multi-object delete failure handling
161+
* @param keys key list
162+
* @return a key list suitable for a delete request.
163+
*/
164+
public static List<DeleteObjectsRequest.KeyVersion> toDeleteRequests(
165+
List<String> keys) {
166+
return keys.stream()
167+
.map(DeleteObjectsRequest.KeyVersion::new)
168+
.collect(Collectors.toList());
169+
}
170+
155171
/**
156172
* Verify that on a partial delete, the S3Guard tables are updated
157173
* with deleted items. And only them.
158174
*/
159175
@Test
160176
public void testProcessDeleteFailure() throws Throwable {
161-
Path pathA = qualifyKey("/a");
162-
Path pathAB = qualifyKey("/a/b");
163-
Path pathAC = qualifyKey("/a/c");
177+
String keyA = "/a/";
178+
String keyAB = "/a/b";
179+
String keyAC = "/a/c";
180+
Path pathA = qualifyKey(keyA);
181+
Path pathAB = qualifyKey(keyAB);
182+
Path pathAC = qualifyKey(keyAC);
183+
List<String> srcKeys = Lists.newArrayList(keyA, keyAB, keyAC);
164184
List<Path> src = Lists.newArrayList(pathA, pathAB, pathAC);
165-
List<DeleteObjectsRequest.KeyVersion> keyList = keysToDelete(src);
185+
List<DeleteObjectsRequest.KeyVersion> keyList = toDeleteRequests(srcKeys);
166186
List<Path> deleteForbidden = Lists.newArrayList(pathAB);
167187
final List<Path> deleteAllowed = Lists.newArrayList(pathA, pathAC);
168-
List<MultiObjectDeleteSupport.KeyPath> forbiddenKP = deleteForbidden.stream()
169-
.map(p ->
170-
new MultiObjectDeleteSupport.KeyPath(toKey(p), p, false))
171-
.collect(Collectors.toList());
188+
List<MultiObjectDeleteSupport.KeyPath> forbiddenKP =
189+
Lists.newArrayList(
190+
new MultiObjectDeleteSupport.KeyPath(keyAB, pathAB, true));
172191
MultiObjectDeleteException ex = createDeleteException(ACCESS_DENIED,
173192
forbiddenKP);
174193
S3ATestUtils.OperationTrackingStore store
@@ -196,7 +215,10 @@ public void testProcessDeleteFailure() throws Throwable {
196215
// because dir marker retention is on, we expect at least one retained marker
197216
Assertions.assertThat(retainedMarkers).
198217
as("Retained Markers")
199-
.isNotEmpty();
218+
.containsExactly(pathA);
219+
Assertions.assertThat(store.getDeleted()).
220+
as("List of tombstoned records")
221+
.doesNotContain(pathA);
200222
}
201223

202224

0 commit comments

Comments
 (0)