Skip to content

Commit 499368a

Browse files
committed
HADOOP-17244 directory marker deletion and s3guard
making sure the multi-object-delete unwinding process only removes files and empty dir markers. Testing is incomplete here, as the delete changes split file and dir deletes in half, and only exec the dir delete requests after file deletes go through. so: need to add a test which creates a partially writeable dir tree with only dir entries in. Not yet done. Change-Id: If7ab07edaa57660b3fc57a567ae50ed4a82e9041
1 parent 9b4615c commit 499368a

File tree

7 files changed

+98
-35
lines changed

7 files changed

+98
-35
lines changed

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2081,6 +2081,7 @@ protected void deleteObject(String key)
20812081
DELETE_CONSIDERED_IDEMPOTENT,
20822082
()-> {
20832083
incrementStatistic(OBJECT_DELETE_REQUESTS);
2084+
incrementStatistic(OBJECT_DELETE_OBJECTS);
20842085
s3.deleteObject(bucket, key);
20852086
return null;
20862087
});
@@ -2127,9 +2128,14 @@ private void blockRootDelete(String key) throws InvalidRequestException {
21272128
}
21282129

21292130
/**
2130-
* Perform a bulk object delete operation.
2131+
* Perform a bulk object delete operation against S3; leaves S3Guard
2132+
* alone.
21312133
* Increments the {@code OBJECT_DELETE_REQUESTS} and write
2132-
* operation statistics.
2134+
* operation statistics
2135+
* <p></p>
2136+
* {@code OBJECT_DELETE_OBJECTS} is updated with the actual number
2137+
* of objects deleted in the request.
2138+
* <p></p>
21332139
* Retry policy: retry untranslated; delete considered idempotent.
21342140
* If the request is throttled, this is logged in the throttle statistics,
21352141
* with the counter set to the number of keys, rather than the number

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -531,7 +531,8 @@ private void asyncDeleteAction(
531531
throws IOException {
532532
List<DeleteObjectsResult.DeletedObject> deletedObjects = new ArrayList<>();
533533
try (DurationInfo ignored =
534-
new DurationInfo(LOG, false, "Delete page of keys")) {
534+
new DurationInfo(LOG, false,
535+
"Delete page of %d keys", keyList.size())) {
535536
DeleteObjectsResult result = null;
536537
List<Path> undeletedObjects = new ArrayList<>();
537538
if (!keyList.isEmpty()) {
@@ -541,10 +542,10 @@ private void asyncDeleteAction(
541542
.map(e -> e.keyVersion)
542543
.collect(Collectors.toList());
543544
LOG.debug("Deleting of {} file objects", files.size());
544-
result = Invoker.once("Remove S3 Keys",
545+
result = Invoker.once("Remove S3 Files",
545546
status.getPath().toString(),
546547
() -> callbacks.removeKeys(
547-
keyList,
548+
files,
548549
false,
549550
undeletedObjects,
550551
state,
@@ -578,7 +579,7 @@ private void asyncDeleteAction(
578579
// (HADOOP-17244)
579580
metadataStore.deletePaths(pathList, state);
580581
}
581-
if (auditDeletedKeys && result != null) {
582+
if (auditDeletedKeys) {
582583
// audit the deleted keys
583584
if (deletedObjects.size() != keyList.size()) {
584585
// size mismatch

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -136,9 +136,9 @@ public Pair<List<KeyPath>, List<KeyPath>> splitUndeletedKeys(
136136
deleteException.getDeletedObjects().size());
137137
// convert the collection of keys being deleted into paths
138138
final List<KeyPath> pathsBeingDeleted = keysToKeyPaths(keysToDelete);
139-
// Take this is list of paths
139+
// Take this ist of paths
140140
// extract all undeleted entries contained in the exception and
141-
// then removes them from the original list.
141+
// then remove them from the original list.
142142
List<KeyPath> undeleted = removeUndeletedPaths(deleteException,
143143
pathsBeingDeleted,
144144
getStoreContext()::keyToPath);
@@ -209,15 +209,16 @@ public static List<KeyPath> convertToKeyPaths(
209209
List<Path> deletedPaths = new ArrayList<>();
210210
List<KeyPath> undeleted = outcome.getLeft();
211211
retainedMarkers.clear();
212-
List<Path> undeletedPaths = toPathList(
213-
(List<KeyPath>) undeleted);
212+
List<Path> undeletedPaths = toPathList((List<KeyPath>) undeleted);
214213
// sort shorter keys first,
215214
// so that if the left key is longer than the first it is considered
216215
// smaller, so appears in the list first.
217216
// thus when we look for a dir being empty, we know it holds
218217
deleted.sort((l, r) -> r.getKey().length() - l.getKey().length());
219218

220-
// now go through and delete
219+
// now go through and delete from S3Guard all paths listed in
220+
// the result which are either files or directories with
221+
// no children.
221222
deleted.forEach(kp -> {
222223
Path path = kp.getPath();
223224
try{

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ private void completeActiveCopies(String reason) throws IOException {
220220
* </li>
221221
* </ol>
222222
* This method must only be called from the primary thread.
223-
* @param path path to the object
223+
* @param path path to the object.
224224
* @param key key of the object.
225225
*/
226226
private void queueToDelete(Path path, String key) {
@@ -411,6 +411,7 @@ protected void recursiveDirectoryRename() throws IOException {
411411
destStatus.getPath());
412412
// Although the dir marker policy doesn't always need to do this,
413413
// it's simplest just to be consistent here.
414+
// note: updates the metastore as well a S3.
414415
callbacks.deleteObjectAtPath(destStatus.getPath(), dstKey, false, null);
415416
}
416417

@@ -422,7 +423,7 @@ protected void recursiveDirectoryRename() throws IOException {
422423
false);
423424

424425
final RemoteIterator<S3ALocatedFileStatus> iterator =
425-
callbacks.listFilesAndEmptyDirectories(parentPath,
426+
callbacks.listFilesAndDirectoryMarkers(parentPath,
426427
sourceStatus,
427428
true,
428429
true);

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import com.amazonaws.services.s3.model.DeleteObjectsRequest;
2222
import com.amazonaws.services.s3.model.MultiObjectDeleteException;
2323
import com.google.common.collect.Lists;
24+
import org.assertj.core.api.Assertions;
2425
import org.junit.Assume;
2526

2627
import org.apache.hadoop.conf.Configuration;
@@ -144,9 +145,10 @@ public void testMultiObjectDeleteNoPermissions() throws Throwable {
144145
Pair<List<KeyPath>, List<KeyPath>> pair =
145146
new MultiObjectDeleteSupport(fs.createStoreContext(), null)
146147
.splitUndeletedKeys(ex, keys);
147-
assertEquals(undeleted, pair.getLeft());
148+
assertEquals(undeleted, toPathList(pair.getLeft()));
148149
List<KeyPath> right = pair.getRight();
149-
assertEquals("Wrong size for " + join(right), 1, right.size());
150+
Assertions.assertThat(right)
151+
.hasSize(1);
150152
assertEquals(markerPath, right.get(0).getPath());
151153
}
152154

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

Lines changed: 71 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@
4242
import org.slf4j.LoggerFactory;
4343

4444
import org.apache.hadoop.conf.Configuration;
45-
import org.apache.hadoop.fs.FileStatus;
4645
import org.apache.hadoop.fs.FileSystem;
4746
import org.apache.hadoop.fs.Path;
4847
import org.apache.hadoop.fs.contract.ContractTestUtils;
@@ -58,6 +57,7 @@
5857
import static org.apache.hadoop.fs.s3a.S3ATestUtils.*;
5958
import static org.apache.hadoop.fs.s3a.S3AUtils.applyLocatedFiles;
6059
import static org.apache.hadoop.fs.s3a.Statistic.FILES_DELETE_REJECTED;
60+
import static org.apache.hadoop.fs.s3a.Statistic.OBJECT_DELETE_OBJECTS;
6161
import static org.apache.hadoop.fs.s3a.Statistic.OBJECT_DELETE_REQUESTS;
6262
import static org.apache.hadoop.fs.s3a.auth.RoleModel.Effects;
6363
import static org.apache.hadoop.fs.s3a.auth.RoleModel.Statement;
@@ -333,7 +333,8 @@ protected Configuration createConfiguration() {
333333
MAX_THREADS,
334334
MAXIMUM_CONNECTIONS,
335335
S3GUARD_DDB_BACKGROUND_SLEEP_MSEC_KEY,
336-
DIRECTORY_MARKER_POLICY);
336+
DIRECTORY_MARKER_POLICY,
337+
BULK_DELETE_PAGE_SIZE);
337338
conf.setInt(MAX_THREADS, EXECUTOR_THREAD_COUNT);
338339
conf.setInt(MAXIMUM_CONNECTIONS, EXECUTOR_THREAD_COUNT * 2);
339340
// turn off prune delays, so as to stop scale tests creating
@@ -342,6 +343,10 @@ protected Configuration createConfiguration() {
342343
// use the keep policy to ensure that surplus markers exist
343344
// to complicate failures
344345
conf.set(DIRECTORY_MARKER_POLICY, DIRECTORY_MARKER_POLICY_KEEP);
346+
// set the delete page size to its maximum to ensure that all
347+
// entries are included in the same large delete, even on
348+
// scale runs. This is needed for assertions on the result.
349+
conf.setInt(BULK_DELETE_PAGE_SIZE, 1_000);
345350
return conf;
346351
}
347352

@@ -482,8 +487,11 @@ public void testRenameDirFailsInDelete() throws Throwable {
482487

483488
// create a set of files
484489
// this is done in parallel as it is 10x faster on a long-haul test run.
485-
List<Path> createdFiles = createFiles(fs, readOnlyDir, dirDepth, fileCount,
486-
dirCount);
490+
List<Path> dirs = new ArrayList<>(dirCount);
491+
List<Path> createdFiles = createDirsAndFiles(fs, readOnlyDir, dirDepth,
492+
fileCount, dirCount,
493+
new ArrayList<>(fileCount),
494+
dirs);
487495
// are they all there?
488496
int expectedFileCount = createdFiles.size();
489497
assertFileCount("files ready to rename", roleFS,
@@ -500,26 +508,36 @@ public void testRenameDirFailsInDelete() throws Throwable {
500508
MultiObjectDeleteException.class, deniedException);
501509
final List<Path> undeleted
502510
= extractUndeletedPaths(mde, fs::keyToQualifiedPath);
511+
512+
List<Path> expectedUndeletedFiles = new ArrayList<>(createdFiles);
513+
if (getFileSystem().getDirectoryMarkerPolicy()
514+
.keepDirectoryMarkers(readOnlyDir)) {
515+
// directory markers are being retained,
516+
// so will also be in the list of undeleted files
517+
expectedUndeletedFiles.addAll(dirs);
518+
}
503519
Assertions.assertThat(undeleted)
504520
.as("files which could not be deleted")
505-
.hasSize(expectedFileCount)
506-
.containsAll(createdFiles)
507-
.containsExactlyInAnyOrderElementsOf(createdFiles);
521+
.containsExactlyInAnyOrderElementsOf(expectedUndeletedFiles);
508522
}
509523
LOG.info("Result of renaming read-only files is as expected",
510524
deniedException);
511525
assertFileCount("files in the source directory", roleFS,
512526
readOnlyDir, expectedFileCount);
513527
// now lets look at the destination.
514-
// even with S3Guard on, we expect the destination to match that of our
528+
// even with S3Guard on, we expect the destination to match that of
515529
// the remote state.
516530
// the test will exist
517531
describe("Verify destination directory exists");
518-
FileStatus st = roleFS.getFileStatus(writableDir);
519-
assertTrue("Not a directory: " + st,
520-
st.isDirectory());
532+
assertIsDirectory(writableDir);
521533
assertFileCount("files in the dest directory", roleFS,
522534
writableDir, expectedFileCount);
535+
// all directories in the source tree must still exist,
536+
// which for S3Guard means no tombstone markers were added
537+
LOG.info("Verifying all directories still exist");
538+
for (Path dir : dirs) {
539+
assertIsDirectory(dir);
540+
}
523541
}
524542

525543
@Test
@@ -618,7 +636,13 @@ public void testPartialDirDelete() throws Throwable {
618636
S3AFileSystem fs = getFileSystem();
619637
StoreContext storeContext = fs.createStoreContext();
620638

621-
List<Path> readOnlyFiles = createFiles(fs, readOnlyDir,
639+
List<Path> dirs = new ArrayList<>(dirCount);
640+
List<Path> readOnlyFiles = createDirsAndFiles(
641+
fs, readOnlyDir, dirDepth,
642+
fileCount, dirCount,
643+
new ArrayList<>(fileCount),
644+
dirs);
645+
createFiles(fs, readOnlyDir,
622646
dirDepth, fileCount, dirCount);
623647
List<Path> deletableFiles = createFiles(fs,
624648
writableDir, dirDepth, fileCount, dirCount);
@@ -642,16 +666,19 @@ public void testPartialDirDelete() throws Throwable {
642666
// this set can be deleted by the role FS
643667
MetricDiff rejectionCount = new MetricDiff(roleFS, FILES_DELETE_REJECTED);
644668
MetricDiff deleteVerbCount = new MetricDiff(roleFS, OBJECT_DELETE_REQUESTS);
669+
MetricDiff deleteObjectCount = new MetricDiff(roleFS, OBJECT_DELETE_OBJECTS);
645670

646671
describe("Trying to delete read only directory");
647672
AccessDeniedException ex = expectDeleteForbidden(readOnlyDir);
648673
if (multiDelete) {
649674
// multi-delete status checks
650675
extractCause(MultiObjectDeleteException.class, ex);
676+
deleteVerbCount.assertDiffEquals("Wrong delete request count", 1);
677+
deleteObjectCount.assertDiffEquals("Wrong count of objects in delete request",
678+
readOnlyFiles.size());
651679
rejectionCount.assertDiffEquals("Wrong rejection count",
652680
readOnlyFiles.size());
653-
deleteVerbCount.assertDiffEquals("Wrong delete count", 1);
654-
reset(rejectionCount, deleteVerbCount);
681+
reset(rejectionCount, deleteVerbCount, deleteObjectCount);
655682
}
656683
// all the files are still there? (avoid in scale test due to cost)
657684
if (!scaleTest) {
@@ -669,6 +696,9 @@ public void testPartialDirDelete() throws Throwable {
669696
mde, keyPaths, storeContext::keyToPath);
670697
final List<Path> undeleted = toPathList(
671698
undeletedKeyPaths);
699+
deleteObjectCount.assertDiffEquals(
700+
"Wrong count of objects in delete request",
701+
allFiles.size());
672702
Assertions.assertThat(undeleted)
673703
.as("files which could not be deleted")
674704
.containsExactlyInAnyOrderElementsOf(readOnlyFiles);
@@ -691,7 +721,7 @@ public void testPartialDirDelete() throws Throwable {
691721

692722
Assertions.assertThat(readOnlyListing)
693723
.as("ReadOnly directory " + directoryList)
694-
.containsAll(readOnlyFiles);
724+
.containsExactlyInAnyOrderElementsOf(readOnlyFiles);
695725
}
696726

697727
/**
@@ -785,7 +815,7 @@ private static CompletableFuture<Path> put(FileSystem fs,
785815
}
786816

787817
/**
788-
* Parallel-touch a set of files in the destination directory.
818+
* Build a set of files in a directory tree.
789819
* @param fs filesystem
790820
* @param destDir destination
791821
* @param depth file depth
@@ -798,10 +828,31 @@ public static List<Path> createFiles(final FileSystem fs,
798828
final int depth,
799829
final int fileCount,
800830
final int dirCount) throws IOException {
801-
List<CompletableFuture<Path>> futures = new ArrayList<>(fileCount);
802-
List<Path> paths = new ArrayList<>(fileCount);
803-
List<Path> dirs = new ArrayList<>(dirCount);
831+
return createDirsAndFiles(fs, destDir, depth, fileCount, dirCount,
832+
new ArrayList<Path>(fileCount),
833+
new ArrayList<Path>(dirCount));
834+
}
835+
836+
/**
837+
* Build a set of files in a directory tree.
838+
* @param fs filesystem
839+
* @param destDir destination
840+
* @param depth file depth
841+
* @param fileCount number of files to create.
842+
* @param dirCount number of dirs to create at each level
843+
* @param paths [out] list of file paths created
844+
* @param dirs [out] list of directory paths created.
845+
* @return the list of files created.
846+
*/
847+
public static List<Path> createDirsAndFiles(final FileSystem fs,
848+
final Path destDir,
849+
final int depth,
850+
final int fileCount,
851+
final int dirCount,
852+
final List<Path> paths,
853+
final List<Path> dirs) throws IOException {
804854
buildPaths(paths, dirs, destDir, depth, fileCount, dirCount);
855+
List<CompletableFuture<Path>> futures = new ArrayList<>(paths.size() + dirs.size());
805856

806857
// create directories. With dir marker retention, that adds more entries
807858
// to cause deletion issues
@@ -817,7 +868,7 @@ public static List<Path> createFiles(final FileSystem fs,
817868
}
818869

819870
try (DurationInfo ignore =
820-
new DurationInfo(LOG, "Creating %d files", fileCount)) {
871+
new DurationInfo(LOG, "Creating %d files", paths.size())) {
821872
for (Path path : paths) {
822873
futures.add(put(fs, path, path.getName()));
823874
}

hadoop-tools/hadoop-aws/src/test/java/org/apache/hadoop/fs/s3a/performance/AbstractS3ACostTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ public void setup() throws Exception {
151151
INVOCATION_COPY_FROM_LOCAL_FILE,
152152
OBJECT_COPY_REQUESTS,
153153
OBJECT_DELETE_REQUESTS,
154+
OBJECT_DELETE_OBJECTS,
154155
OBJECT_LIST_REQUESTS,
155156
OBJECT_METADATA_REQUESTS,
156157
OBJECT_PUT_BYTES,

0 commit comments

Comments
 (0)