public CompletableFuture<Void> deleteAsync(List<MessageInfo> infos) {
    try {
      checkStarted();
      checkDuplicates(infos);
      List<CompletableFuture<Boolean>> operationFutures = new ArrayList<>();

      // Delete each message
      for (MessageInfo msgInfo : infos) {
        BlobId blobId = (BlobId) msgInfo.getStoreKey();
        // TODO: Add retries similar to synchronous operation
        operationFutures.add(deleteAsyncIfNeeded(blobId, msgInfo.getOperationTimeMs(), msgInfo.getLifeVersion()));
      }

      // Complete the result once all the individual delete operations complete. If any of the operation completes
      // exceptionally, complete the result exceptionally as well.
      return CompletableFuture.allOf(operationFutures.toArray(new CompletableFuture<?>[0])).exceptionally(throwable -> {
        unwrapCompletionExceptionAndThrowStoreException(throwable, CloudStorageException.class,
            this::mapToStoreException);
        return null;
      });
    } catch (StoreException e) {
      return FutureUtils.completedExceptionally(e);
    }
  }
Deletes all the messages in the list from the store asynchronously. When the lifeVersion is
{@link MessageInfo#LIFE_VERSION_FROM_FRONTEND}, this method is invoked by the responding to the frontend request.
Otherwise, it's invoked in the replication thread.
@param infos The list of messages that need to be deleted. Only the StoreKey, OperationTime, LifeVersion
                     should be used in this method.
@return a {@link CompletableFuture} that will eventually complete successfully when all the messages are deleted
        from store or will contain the {@link StoreException} if an error occurred
ambry-cloud/src/main/java/com/github/ambry/cloud/CloudBlobStore.java