|
@@ -23,6 +23,7 @@ import java.nio.file.AccessDeniedException;
|
|
|
import java.util.ArrayList;
|
|
|
import java.util.Collection;
|
|
|
import java.util.List;
|
|
|
+import java.util.Objects;
|
|
|
import java.util.function.Function;
|
|
|
import java.util.stream.Collectors;
|
|
|
|
|
@@ -37,8 +38,10 @@ import org.apache.commons.lang3.tuple.Triple;
|
|
|
import org.apache.hadoop.fs.Path;
|
|
|
import org.apache.hadoop.fs.s3a.AWSS3IOException;
|
|
|
import org.apache.hadoop.fs.s3a.S3AFileSystem;
|
|
|
+import org.apache.hadoop.fs.s3a.Tristate;
|
|
|
import org.apache.hadoop.fs.s3a.s3guard.BulkOperationState;
|
|
|
import org.apache.hadoop.fs.s3a.s3guard.MetadataStore;
|
|
|
+import org.apache.hadoop.fs.s3a.s3guard.PathMetadata;
|
|
|
|
|
|
import static com.google.common.base.Preconditions.checkNotNull;
|
|
|
|
|
@@ -84,15 +87,25 @@ public final class MultiObjectDeleteSupport extends AbstractStoreOperation {
|
|
|
public static IOException translateDeleteException(
|
|
|
final String message,
|
|
|
final MultiObjectDeleteException deleteException) {
|
|
|
+ List<MultiObjectDeleteException.DeleteError> errors
|
|
|
+ = deleteException.getErrors();
|
|
|
+ LOG.warn("Bulk delete operation failed to delete all objects;"
|
|
|
+ + " failure count = {}",
|
|
|
+ errors.size());
|
|
|
final StringBuilder result = new StringBuilder(
|
|
|
- deleteException.getErrors().size() * 256);
|
|
|
+ errors.size() * 256);
|
|
|
result.append(message).append(": ");
|
|
|
String exitCode = "";
|
|
|
for (MultiObjectDeleteException.DeleteError error :
|
|
|
deleteException.getErrors()) {
|
|
|
String code = error.getCode();
|
|
|
- result.append(String.format("%s: %s: %s%n", code, error.getKey(),
|
|
|
- error.getMessage()));
|
|
|
+ String item = String.format("%s: %s%s: %s%n", code, error.getKey(),
|
|
|
+ (error.getVersionId() != null
|
|
|
+ ? (" (" + error.getVersionId() + ")")
|
|
|
+ : ""),
|
|
|
+ error.getMessage());
|
|
|
+ LOG.warn(item);
|
|
|
+ result.append(item);
|
|
|
if (exitCode.isEmpty() || ACCESS_DENIED.equals(code)) {
|
|
|
exitCode = code;
|
|
|
}
|
|
@@ -113,7 +126,7 @@ public final class MultiObjectDeleteSupport extends AbstractStoreOperation {
|
|
|
* @param keysToDelete the keys in the delete request
|
|
|
* @return tuple of (undeleted, deleted) paths.
|
|
|
*/
|
|
|
- public Pair<List<Path>, List<Path>> splitUndeletedKeys(
|
|
|
+ public Pair<List<KeyPath>, List<KeyPath>> splitUndeletedKeys(
|
|
|
final MultiObjectDeleteException deleteException,
|
|
|
final Collection<DeleteObjectsRequest.KeyVersion> keysToDelete) {
|
|
|
LOG.debug("Processing delete failure; keys to delete count = {};"
|
|
@@ -122,11 +135,11 @@ public final class MultiObjectDeleteSupport extends AbstractStoreOperation {
|
|
|
deleteException.getErrors().size(),
|
|
|
deleteException.getDeletedObjects().size());
|
|
|
// convert the collection of keys being deleted into paths
|
|
|
- final List<Path> pathsBeingDeleted = keysToPaths(keysToDelete);
|
|
|
- // Take this is list of paths
|
|
|
+ final List<KeyPath> pathsBeingDeleted = keysToKeyPaths(keysToDelete);
|
|
|
+ // Take this ist of paths
|
|
|
// extract all undeleted entries contained in the exception and
|
|
|
- // then removes them from the original list.
|
|
|
- List<Path> undeleted = removeUndeletedPaths(deleteException,
|
|
|
+ // then remove them from the original list.
|
|
|
+ List<KeyPath> undeleted = removeUndeletedPaths(deleteException,
|
|
|
pathsBeingDeleted,
|
|
|
getStoreContext()::keyToPath);
|
|
|
return Pair.of(undeleted, pathsBeingDeleted);
|
|
@@ -139,7 +152,17 @@ public final class MultiObjectDeleteSupport extends AbstractStoreOperation {
|
|
|
*/
|
|
|
public List<Path> keysToPaths(
|
|
|
final Collection<DeleteObjectsRequest.KeyVersion> keysToDelete) {
|
|
|
- return convertToPaths(keysToDelete,
|
|
|
+ return toPathList(keysToKeyPaths(keysToDelete));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Given a list of delete requests, convert them all to keypaths.
|
|
|
+ * @param keysToDelete list of keys for the delete operation.
|
|
|
+ * @return list of keypath entries
|
|
|
+ */
|
|
|
+ public List<KeyPath> keysToKeyPaths(
|
|
|
+ final Collection<DeleteObjectsRequest.KeyVersion> keysToDelete) {
|
|
|
+ return convertToKeyPaths(keysToDelete,
|
|
|
getStoreContext()::keyToPath);
|
|
|
}
|
|
|
|
|
@@ -149,13 +172,17 @@ public final class MultiObjectDeleteSupport extends AbstractStoreOperation {
|
|
|
* @param qualifier path qualifier
|
|
|
* @return the paths.
|
|
|
*/
|
|
|
- public static List<Path> convertToPaths(
|
|
|
+ public static List<KeyPath> convertToKeyPaths(
|
|
|
final Collection<DeleteObjectsRequest.KeyVersion> keysToDelete,
|
|
|
final Function<String, Path> qualifier) {
|
|
|
- return keysToDelete.stream()
|
|
|
- .map((keyVersion) ->
|
|
|
- qualifier.apply(keyVersion.getKey()))
|
|
|
- .collect(Collectors.toList());
|
|
|
+ List<KeyPath> l = new ArrayList<>(keysToDelete.size());
|
|
|
+ for (DeleteObjectsRequest.KeyVersion kv : keysToDelete) {
|
|
|
+ String key = kv.getKey();
|
|
|
+ Path p = qualifier.apply(key);
|
|
|
+ boolean isDir = key.endsWith("/");
|
|
|
+ l.add(new KeyPath(key, p, isDir));
|
|
|
+ }
|
|
|
+ return l;
|
|
|
}
|
|
|
|
|
|
/**
|
|
@@ -164,27 +191,59 @@ public final class MultiObjectDeleteSupport extends AbstractStoreOperation {
|
|
|
* and the original list of files to delete declares to have been deleted.
|
|
|
* @param deleteException the delete exception.
|
|
|
* @param keysToDelete collection of keys which had been requested.
|
|
|
+ * @param retainedMarkers list built up of retained markers.
|
|
|
* @return a tuple of (undeleted, deleted, failures)
|
|
|
*/
|
|
|
public Triple<List<Path>, List<Path>, List<Pair<Path, IOException>>>
|
|
|
processDeleteFailure(
|
|
|
final MultiObjectDeleteException deleteException,
|
|
|
- final List<DeleteObjectsRequest.KeyVersion> keysToDelete) {
|
|
|
+ final List<DeleteObjectsRequest.KeyVersion> keysToDelete,
|
|
|
+ final List<Path> retainedMarkers) {
|
|
|
final MetadataStore metadataStore =
|
|
|
checkNotNull(getStoreContext().getMetadataStore(),
|
|
|
"context metadatastore");
|
|
|
final List<Pair<Path, IOException>> failures = new ArrayList<>();
|
|
|
- final Pair<List<Path>, List<Path>> outcome =
|
|
|
+ final Pair<List<KeyPath>, List<KeyPath>> outcome =
|
|
|
splitUndeletedKeys(deleteException, keysToDelete);
|
|
|
- List<Path> deleted = outcome.getRight();
|
|
|
- List<Path> undeleted = outcome.getLeft();
|
|
|
- // delete the paths but recover
|
|
|
- // TODO: handle the case where a parent path is deleted but not a child.
|
|
|
- // TODO: in a fake object delete, we don't actually want to delete
|
|
|
- // metastore entries
|
|
|
- deleted.forEach(path -> {
|
|
|
- try {
|
|
|
- metadataStore.delete(path, operationState);
|
|
|
+ List<KeyPath> deleted = outcome.getRight();
|
|
|
+ List<Path> deletedPaths = new ArrayList<>();
|
|
|
+ List<KeyPath> undeleted = outcome.getLeft();
|
|
|
+ retainedMarkers.clear();
|
|
|
+ List<Path> undeletedPaths = toPathList((List<KeyPath>) undeleted);
|
|
|
+ // sort shorter keys first,
|
|
|
+ // so that if the left key is longer than the first it is considered
|
|
|
+ // smaller, so appears in the list first.
|
|
|
+ // thus when we look for a dir being empty, we know it holds
|
|
|
+ deleted.sort((l, r) -> r.getKey().length() - l.getKey().length());
|
|
|
+
|
|
|
+ // now go through and delete from S3Guard all paths listed in
|
|
|
+ // the result which are either files or directories with
|
|
|
+ // no children.
|
|
|
+ deleted.forEach(kp -> {
|
|
|
+ Path path = kp.getPath();
|
|
|
+ try{
|
|
|
+ boolean toDelete = true;
|
|
|
+ if (kp.isDirectoryMarker()) {
|
|
|
+ // its a dir marker, which could be an empty dir
|
|
|
+ // (which is then tombstoned), or a non-empty dir, which
|
|
|
+ // is not tombstoned.
|
|
|
+ // for this to be handled, we have to have removed children
|
|
|
+ // from the store first, which relies on the sort
|
|
|
+ PathMetadata pmentry = metadataStore.get(path, true);
|
|
|
+ if (pmentry != null && !pmentry.isDeleted()) {
|
|
|
+ toDelete = pmentry.getFileStatus().isEmptyDirectory()
|
|
|
+ == Tristate.TRUE;
|
|
|
+ } else {
|
|
|
+ toDelete = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (toDelete) {
|
|
|
+ LOG.debug("Removing deleted object from S3Guard Store {}", path);
|
|
|
+ metadataStore.delete(path, operationState);
|
|
|
+ } else {
|
|
|
+ LOG.debug("Retaining S3Guard directory entry {}", path);
|
|
|
+ retainedMarkers.add(path);
|
|
|
+ }
|
|
|
} catch (IOException e) {
|
|
|
// trouble: we failed to delete the far end entry
|
|
|
// try with the next one.
|
|
@@ -192,11 +251,25 @@ public final class MultiObjectDeleteSupport extends AbstractStoreOperation {
|
|
|
LOG.warn("Failed to update S3Guard store with deletion of {}", path);
|
|
|
failures.add(Pair.of(path, e));
|
|
|
}
|
|
|
+ // irrespective of the S3Guard outcome, it is declared as deleted, as
|
|
|
+ // it is no longer in the S3 store.
|
|
|
+ deletedPaths.add(path);
|
|
|
});
|
|
|
if (LOG.isDebugEnabled()) {
|
|
|
undeleted.forEach(p -> LOG.debug("Deleted {}", p));
|
|
|
}
|
|
|
- return Triple.of(undeleted, deleted, failures);
|
|
|
+ return Triple.of(undeletedPaths, deletedPaths, failures);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Given a list of keypaths, convert to a list of paths.
|
|
|
+ * @param keyPaths source list
|
|
|
+ * @return a listg of paths
|
|
|
+ */
|
|
|
+ public static List<Path> toPathList(final List<KeyPath> keyPaths) {
|
|
|
+ return keyPaths.stream()
|
|
|
+ .map(KeyPath::getPath)
|
|
|
+ .collect(Collectors.toList());
|
|
|
}
|
|
|
|
|
|
/**
|
|
@@ -211,8 +284,31 @@ public final class MultiObjectDeleteSupport extends AbstractStoreOperation {
|
|
|
public static List<Path> extractUndeletedPaths(
|
|
|
final MultiObjectDeleteException deleteException,
|
|
|
final Function<String, Path> qualifierFn) {
|
|
|
- return deleteException.getErrors().stream()
|
|
|
- .map((e) -> qualifierFn.apply(e.getKey()))
|
|
|
+ return toPathList(extractUndeletedKeyPaths(deleteException, qualifierFn));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Build a list of undeleted paths from a {@code MultiObjectDeleteException}.
|
|
|
+ * Outside of unit tests, the qualifier function should be
|
|
|
+ * {@link S3AFileSystem#keyToQualifiedPath(String)}.
|
|
|
+ * @param deleteException the delete exception.
|
|
|
+ * @param qualifierFn function to qualify paths
|
|
|
+ * @return the possibly empty list of paths.
|
|
|
+ */
|
|
|
+ @VisibleForTesting
|
|
|
+ public static List<KeyPath> extractUndeletedKeyPaths(
|
|
|
+ final MultiObjectDeleteException deleteException,
|
|
|
+ final Function<String, Path> qualifierFn) {
|
|
|
+
|
|
|
+ List<MultiObjectDeleteException.DeleteError> errors
|
|
|
+ = deleteException.getErrors();
|
|
|
+ return errors.stream()
|
|
|
+ .map((error) -> {
|
|
|
+ String key = error.getKey();
|
|
|
+ Path path = qualifierFn.apply(key);
|
|
|
+ boolean isDir = key.endsWith("/");
|
|
|
+ return new KeyPath(key, path, isDir);
|
|
|
+ })
|
|
|
.collect(Collectors.toList());
|
|
|
}
|
|
|
|
|
@@ -227,12 +323,17 @@ public final class MultiObjectDeleteSupport extends AbstractStoreOperation {
|
|
|
* @return the list of undeleted entries
|
|
|
*/
|
|
|
@VisibleForTesting
|
|
|
- static List<Path> removeUndeletedPaths(
|
|
|
+ static List<KeyPath> removeUndeletedPaths(
|
|
|
final MultiObjectDeleteException deleteException,
|
|
|
- final Collection<Path> pathsBeingDeleted,
|
|
|
+ final Collection<KeyPath> pathsBeingDeleted,
|
|
|
final Function<String, Path> qualifier) {
|
|
|
- List<Path> undeleted = extractUndeletedPaths(deleteException, qualifier);
|
|
|
- pathsBeingDeleted.removeAll(undeleted);
|
|
|
+ // get the undeleted values
|
|
|
+ List<KeyPath> undeleted = extractUndeletedKeyPaths(deleteException,
|
|
|
+ qualifier);
|
|
|
+ // and remove them from the undeleted list, matching on key
|
|
|
+ for (KeyPath undel : undeleted) {
|
|
|
+ pathsBeingDeleted.removeIf(kp -> kp.getPath().equals(undel.getPath()));
|
|
|
+ }
|
|
|
return undeleted;
|
|
|
}
|
|
|
|
|
@@ -247,4 +348,70 @@ public final class MultiObjectDeleteSupport extends AbstractStoreOperation {
|
|
|
final List<DeleteObjectsRequest.KeyVersion> keysToDelete) {
|
|
|
return keysToPaths(keysToDelete);
|
|
|
}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Representation of a (key, path) which couldn't be deleted;
|
|
|
+ * the dir marker flag is inferred from the key suffix.
|
|
|
+ * <p>
|
|
|
+ * Added because Pairs of Lists of Triples was just too complex
|
|
|
+ * for Java code.
|
|
|
+ * </p>
|
|
|
+ */
|
|
|
+ public static final class KeyPath {
|
|
|
+ /** Key in bucket. */
|
|
|
+ private final String key;
|
|
|
+ /** Full path. */
|
|
|
+ private final Path path;
|
|
|
+ /** Is this a directory marker? */
|
|
|
+ private final boolean directoryMarker;
|
|
|
+
|
|
|
+ public KeyPath(final String key,
|
|
|
+ final Path path,
|
|
|
+ final boolean directoryMarker) {
|
|
|
+ this.key = key;
|
|
|
+ this.path = path;
|
|
|
+ this.directoryMarker = directoryMarker;
|
|
|
+ }
|
|
|
+
|
|
|
+ public String getKey() {
|
|
|
+ return key;
|
|
|
+ }
|
|
|
+
|
|
|
+ public Path getPath() {
|
|
|
+ return path;
|
|
|
+ }
|
|
|
+
|
|
|
+ public boolean isDirectoryMarker() {
|
|
|
+ return directoryMarker;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public String toString() {
|
|
|
+ return "KeyPath{" +
|
|
|
+ "key='" + key + '\'' +
|
|
|
+ ", path=" + path +
|
|
|
+ ", directoryMarker=" + directoryMarker +
|
|
|
+ '}';
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Equals test is on key alone.
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public boolean equals(final Object o) {
|
|
|
+ if (this == o) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if (o == null || getClass() != o.getClass()) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ KeyPath keyPath = (KeyPath) o;
|
|
|
+ return key.equals(keyPath.key);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public int hashCode() {
|
|
|
+ return Objects.hash(key);
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|