-
Notifications
You must be signed in to change notification settings - Fork 3k
AWS: Use executor service by default when performing batch deletion of files #5379
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,7 +22,9 @@ | |
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Set; | ||
| import java.util.concurrent.ExecutionException; | ||
| import java.util.concurrent.ExecutorService; | ||
| import java.util.concurrent.Future; | ||
| import java.util.concurrent.atomic.AtomicBoolean; | ||
| import java.util.stream.Collectors; | ||
| import org.apache.iceberg.aws.AwsClientFactories; | ||
|
|
@@ -182,41 +184,53 @@ public void deleteFiles(Iterable<String> paths) throws BulkDeletionFailureExcept | |
| .run(path -> tagFileToDelete(path, awsProperties.s3DeleteTags())); | ||
| } | ||
|
|
||
| if (!awsProperties.isS3DeleteEnabled()) { | ||
| return; | ||
| } | ||
| if (awsProperties.isS3DeleteEnabled()) { | ||
| SetMultimap<String, String> bucketToObjects = | ||
| Multimaps.newSetMultimap(Maps.newHashMap(), Sets::newHashSet); | ||
| List<Future<List<String>>> deletionTasks = Lists.newArrayList(); | ||
| for (String path : paths) { | ||
| S3URI location = new S3URI(path, awsProperties.s3BucketToAccessPointMapping()); | ||
| String bucket = location.bucket(); | ||
| String objectKey = location.key(); | ||
| bucketToObjects.get(bucket).add(objectKey); | ||
| if (bucketToObjects.get(bucket).size() == awsProperties.s3FileIoDeleteBatchSize()) { | ||
| Set<String> keys = Sets.newHashSet(bucketToObjects.get(bucket)); | ||
| Future<List<String>> deletionTask = | ||
| executorService().submit(() -> deleteBatch(bucket, keys)); | ||
| deletionTasks.add(deletionTask); | ||
| bucketToObjects.removeAll(bucket); | ||
| } | ||
| } | ||
|
|
||
| SetMultimap<String, String> bucketToObjects = | ||
| Multimaps.newSetMultimap(Maps.newHashMap(), Sets::newHashSet); | ||
| int numberOfFailedDeletions = 0; | ||
| for (String path : paths) { | ||
| S3URI location = new S3URI(path, awsProperties.s3BucketToAccessPointMapping()); | ||
| String bucket = location.bucket(); | ||
| String objectKey = location.key(); | ||
| Set<String> objectsInBucket = bucketToObjects.get(bucket); | ||
| if (objectsInBucket.size() == awsProperties.s3FileIoDeleteBatchSize()) { | ||
| List<String> failedDeletionsForBatch = deleteObjectsInBucket(bucket, objectsInBucket); | ||
| numberOfFailedDeletions += failedDeletionsForBatch.size(); | ||
| failedDeletionsForBatch.forEach( | ||
| failedPath -> LOG.warn("Failed to delete object at path {}", failedPath)); | ||
| bucketToObjects.removeAll(bucket); | ||
| // Delete the remainder | ||
| for (Map.Entry<String, Collection<String>> bucketToObjectsEntry : | ||
| bucketToObjects.asMap().entrySet()) { | ||
| String bucket = bucketToObjectsEntry.getKey(); | ||
| Collection<String> keys = bucketToObjectsEntry.getValue(); | ||
| Future<List<String>> deletionTask = | ||
| executorService().submit(() -> deleteBatch(bucket, keys)); | ||
| deletionTasks.add(deletionTask); | ||
| } | ||
| bucketToObjects.get(bucket).add(objectKey); | ||
| } | ||
|
|
||
| // Delete the remainder | ||
| for (Map.Entry<String, Collection<String>> bucketToObjectsEntry : | ||
| bucketToObjects.asMap().entrySet()) { | ||
| final String bucket = bucketToObjectsEntry.getKey(); | ||
| final Collection<String> objects = bucketToObjectsEntry.getValue(); | ||
| List<String> failedDeletions = deleteObjectsInBucket(bucket, objects); | ||
| failedDeletions.forEach( | ||
| failedPath -> LOG.warn("Failed to delete object at path {}", failedPath)); | ||
| numberOfFailedDeletions += failedDeletions.size(); | ||
| } | ||
| int totalFailedDeletions = 0; | ||
|
|
||
| for (Future<List<String>> deletionTask : deletionTasks) { | ||
| try { | ||
| List<String> failedDeletions = deletionTask.get(); | ||
| failedDeletions.forEach(path -> LOG.warn("Failed to delete object at path {}", path)); | ||
| totalFailedDeletions += failedDeletions.size(); | ||
| } catch (ExecutionException e) { | ||
| LOG.warn("Caught unexpected exception during batch deletion: ", e.getCause()); | ||
| } catch (InterruptedException e) { | ||
| Thread.currentThread().interrupt(); | ||
| deletionTasks.stream().filter(task -> !task.isDone()).forEach(task -> task.cancel(true)); | ||
| throw new RuntimeException("Interrupted when waiting for deletions to complete", e); | ||
amogh-jahagirdar marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
|
|
||
| if (numberOfFailedDeletions > 0) { | ||
| throw new BulkDeletionFailureException(numberOfFailedDeletions); | ||
| if (totalFailedDeletions > 0) { | ||
| throw new BulkDeletionFailureException(totalFailedDeletions); | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -244,26 +258,33 @@ private void tagFileToDelete(String path, Set<Tag> deleteTags) throws S3Exceptio | |
| client().putObjectTagging(putObjectTaggingRequest); | ||
| } | ||
|
|
||
| private List<String> deleteObjectsInBucket(String bucket, Collection<String> objects) { | ||
| if (!objects.isEmpty()) { | ||
| List<ObjectIdentifier> objectIds = | ||
| objects.stream() | ||
| .map(objectKey -> ObjectIdentifier.builder().key(objectKey).build()) | ||
| .collect(Collectors.toList()); | ||
| DeleteObjectsRequest deleteObjectsRequest = | ||
| DeleteObjectsRequest.builder() | ||
| .bucket(bucket) | ||
| .delete(Delete.builder().objects(objectIds).build()) | ||
| .build(); | ||
| DeleteObjectsResponse response = client().deleteObjects(deleteObjectsRequest); | ||
| if (response.hasErrors()) { | ||
| return response.errors().stream() | ||
| .map(error -> String.format("s3://%s/%s", bucket, error.key())) | ||
| private List<String> deleteBatch(String bucket, Collection<String> keysToDelete) { | ||
| List<ObjectIdentifier> objectIds = | ||
| keysToDelete.stream() | ||
| .map(key -> ObjectIdentifier.builder().key(key).build()) | ||
| .collect(Collectors.toList()); | ||
| DeleteObjectsRequest request = | ||
| DeleteObjectsRequest.builder() | ||
| .bucket(bucket) | ||
| .delete(Delete.builder().objects(objectIds).build()) | ||
| .build(); | ||
| List<String> failures = Lists.newArrayList(); | ||
| try { | ||
| DeleteObjectsResponse response = client().deleteObjects(request); | ||
| if (response.hasErrors()) { | ||
| failures.addAll( | ||
| response.errors().stream() | ||
| .map(error -> String.format("s3://%s/%s", request.bucket(), error.key())) | ||
| .collect(Collectors.toList())); | ||
| } | ||
| } catch (Exception e) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [doubt] Any reason we are catching a generic exception here ?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah I think in case of any failure we should surface a BulkDeletionFailure at the end. So catching the generic exception allows us to handle any failure, treat it as a failure to delete the entire batch, and add that to the failed files list. I'm not sure of any other case where we want to surface something else. We're logging the specific exception so that folks can debug. |
||
| LOG.warn("Encountered failure when deleting batch", e); | ||
| failures.addAll( | ||
| request.delete().objects().stream() | ||
| .map(obj -> String.format("s3://%s/%s", request.bucket(), obj.key())) | ||
| .collect(Collectors.toList())); | ||
| } | ||
|
|
||
| return Lists.newArrayList(); | ||
| return failures; | ||
| } | ||
|
|
||
| @Override | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.