This is an automated email from the ASF dual-hosted git repository.
adoroszlai pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ozone.git
The following commit(s) were added to refs/heads/master by this push:
new 7b68fcd3414 HDDS-16282. Avoid O(n^2) key removal in S3 DeleteObjects
(#11126)
7b68fcd3414 is described below
commit 7b68fcd3414278f5ee5930edb7cd1d2a4827d6e9
Author: rjgoyln <[email protected]>
AuthorDate: Thu Sep 24 12:59:56 2026 +0800
HDDS-16282. Avoid O(n^2) key removal in S3 DeleteObjects (#11126)
---
.../hadoop/ozone/s3/endpoint/BucketEndpoint.java | 11 +--
.../ozone/s3/endpoint/TestObjectMultiDelete.java | 84 ++++++++++++++++++++++
2 files changed, 91 insertions(+), 4 deletions(-)
diff --git
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketEndpoint.java
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketEndpoint.java
index c833d5f22f4..497f4efc76b 100644
---
a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketEndpoint.java
+++
b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/BucketEndpoint.java
@@ -362,6 +362,7 @@ public MultiDeleteResponse multiDelete(
}
MultiDeleteResponse result = new MultiDeleteResponse();
List<String> deleteKeys = new ArrayList<>();
+ List<String> failedDeletes = new ArrayList<>();
if (request.getObjects() != null) {
Map<String, ErrorInfo> undeletedKeyResultMap;
@@ -378,20 +379,22 @@ public MultiDeleteResponse multiDelete(
// if the key is not found, it is assumed to be successfully
deleted
ResultCodes.KEY_NOT_FOUND.name().equals(error.getCode());
if (deleted) {
- deleteKeys.remove(d.getKey());
if (!request.isQuiet()) {
result.addDeleted(new DeletedObject(d.getKey()));
}
} else {
+ failedDeletes.add(d.getKey());
result.addError(new Error(d.getKey(), error.getCode(),
error.getMessage()));
}
}
getMetrics().updateDeleteKeySuccessStats(startNanos);
} catch (IOException ex) {
getMetrics().updateDeleteKeyFailureStats(startNanos);
+ // the batch delete failed as a whole, so no key is reported as deleted
+ failedDeletes.addAll(deleteKeys);
final OMException omEx = (OMException)
HddsClientUtils.containsException(ex, OMException.class);
if (omEx != null) {
- auditMultiDeleteFailure(context, deleteKeys, omEx);
+ auditMultiDeleteFailure(context, failedDeletes, omEx);
throw newError(bucketName, omEx);
}
LOG.error("Delete key failed: {}", ex.getMessage());
@@ -402,10 +405,10 @@ public MultiDeleteResponse multiDelete(
}
if (!result.getErrors().isEmpty()) {
- auditMultiDeleteFailure(context, deleteKeys, new Exception("MultiDelete
Exception"));
+ auditMultiDeleteFailure(context, failedDeletes, new
Exception("MultiDelete Exception"));
} else {
AuditMessage.Builder message = auditMessageFor(context.getAction());
- message.getParams().put("failedDeletes", deleteKeys.toString());
+ message.getParams().put("failedDeletes", failedDeletes.toString());
AUDIT.logWriteSuccess(message.withResult(AuditEventStatus.SUCCESS).build());
}
diff --git
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestObjectMultiDelete.java
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestObjectMultiDelete.java
index 1d657460346..f4316152ce8 100644
---
a/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestObjectMultiDelete.java
+++
b/hadoop-ozone/s3gateway/src/test/java/org/apache/hadoop/ozone/s3/endpoint/TestObjectMultiDelete.java
@@ -17,23 +17,39 @@
package org.apache.hadoop.ozone.s3.endpoint;
+import static java.util.Arrays.asList;
import static java.util.Collections.singleton;
import static
org.apache.hadoop.ozone.s3.endpoint.EndpointTestUtils.assertErrorResponse;
import static org.apache.hadoop.ozone.s3.exception.S3ErrorTable.MALFORMED_XML;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyBoolean;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
import com.google.common.collect.Sets;
import java.io.IOException;
+import java.util.HashMap;
import java.util.HashSet;
+import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import javax.xml.bind.JAXBException;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.ozone.client.ObjectStore;
import org.apache.hadoop.ozone.client.OzoneBucket;
import org.apache.hadoop.ozone.client.OzoneClient;
import org.apache.hadoop.ozone.client.OzoneClientStub;
import org.apache.hadoop.ozone.client.OzoneKey;
+import org.apache.hadoop.ozone.client.OzoneVolume;
+import org.apache.hadoop.ozone.client.protocol.ClientProtocol;
+import org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes;
+import org.apache.hadoop.ozone.om.helpers.ErrorInfo;
import org.apache.hadoop.ozone.s3.endpoint.MultiDeleteRequest.DeleteObject;
+import org.apache.hadoop.ozone.s3.endpoint.MultiDeleteResponse.DeletedObject;
import org.apache.hadoop.ozone.s3.exception.OS3Exception;
+import org.apache.hadoop.ozone.s3.metrics.S3GatewayMetrics;
import org.apache.hadoop.ozone.s3.util.S3Consts;
import org.junit.jupiter.api.Test;
@@ -140,6 +156,74 @@ public void multiDeleteAllowsMaxKeysPerRequest() throws
Exception {
assertEquals(3, Sets.newHashSet(bucket.listKeys("")).size());
}
+ @Test
+ public void multiDeleteAuditsOnlyFailedKeys() throws Exception {
+ OzoneBucket bucket = mock(OzoneBucket.class);
+ Map<String, ErrorInfo> undeletedKeys = new HashMap<>();
+ undeletedKeys.put("key2", new ErrorInfo("ACCESS_DENIED", "ACL check
failed"));
+ undeletedKeys.put("key3", new ErrorInfo(ResultCodes.KEY_NOT_FOUND.name(),
"Key does not exist"));
+ when(bucket.deleteKeys(any(), anyBoolean())).thenReturn(undeletedKeys);
+
+ Map<String, String> auditParams = new HashMap<>();
+ MultiDeleteResponse response =
+ newEndpointFor(bucket, auditParams).multiDelete("b1", "",
threeKeyRequest());
+
+ assertEquals(asList("key1", "key3"), response.getDeletedObjects().stream()
+ .map(DeletedObject::getKey)
+ .collect(Collectors.toList()));
+ assertEquals(1, response.getErrors().size());
+ assertEquals("key2", response.getErrors().get(0).getKey());
+ assertEquals("[key2]", auditParams.get("failedDeletes"));
+ }
+
+ @Test
+ public void multiDeleteAuditsAllKeysWhenDeleteFails() throws Exception {
+ OzoneBucket bucket = mock(OzoneBucket.class);
+ when(bucket.deleteKeys(any(), anyBoolean())).thenThrow(new
IOException("Ozone Manager is unavailable"));
+
+ Map<String, String> auditParams = new HashMap<>();
+ MultiDeleteResponse response =
+ newEndpointFor(bucket, auditParams).multiDelete("b1", "",
threeKeyRequest());
+
+ assertEquals(0, response.getDeletedObjects().size());
+ assertEquals(1, response.getErrors().size());
+ assertEquals("ALL", response.getErrors().get(0).getKey());
+ assertEquals("InternalError", response.getErrors().get(0).getCode());
+ assertEquals("[key1, key2, key3]", auditParams.get("failedDeletes"));
+ }
+
+ private MultiDeleteRequest threeKeyRequest() {
+ MultiDeleteRequest mdr = new MultiDeleteRequest();
+ mdr.getObjects().add(new DeleteObject("key1"));
+ mdr.getObjects().add(new DeleteObject("key2"));
+ mdr.getObjects().add(new DeleteObject("key3"));
+ return mdr;
+ }
+
+ private BucketEndpoint newEndpointFor(OzoneBucket bucket, Map<String,
String> auditParams) throws IOException {
+ OzoneConfiguration conf = new OzoneConfiguration();
+ S3GatewayMetrics.create(conf);
+
+ OzoneClient client = mock(OzoneClient.class);
+ ObjectStore objectStore = mock(ObjectStore.class);
+ OzoneVolume volume = mock(OzoneVolume.class);
+ when(client.getObjectStore()).thenReturn(objectStore);
+ when(client.getConfiguration()).thenReturn(conf);
+ when(objectStore.getClientProxy()).thenReturn(mock(ClientProtocol.class));
+ when(objectStore.getS3Volume()).thenReturn(volume);
+ when(volume.getBucket(anyString())).thenReturn(bucket);
+
+ return EndpointBuilder.newBucketEndpointBuilder()
+ .setBase(new BucketEndpoint() {
+ @Override
+ protected Map<String, String> getAuditParameters() {
+ return auditParams;
+ }
+ })
+ .setClient(client)
+ .build();
+ }
+
private OzoneBucket initTestData(OzoneClient client) throws IOException {
client.getObjectStore().createS3Bucket("b1");
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]