This is an automated email from the ASF dual-hosted git repository.
sadanand48 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 2b5874b743b HDDS-16226. Tool to obtain snapshot trapped bytes for a
bucket. (#11122)
2b5874b743b is described below
commit 2b5874b743b0faba5c4cc4f125030ca4c3af1469
Author: Sadanand Shenoy <[email protected]>
AuthorDate: Wed Sep 30 13:55:18 2026 +0530
HDDS-16226. Tool to obtain snapshot trapped bytes for a bucket. (#11122)
---
.../admin/om/BucketDeletedBytesSubCommand.java | 101 ++++++
.../org/apache/hadoop/ozone/admin/om/OMAdmin.java | 1 +
.../main/java/org/apache/hadoop/ozone/OmUtils.java | 2 +
.../ozone/om/helpers/BucketDeletedBytes.java | 79 +++++
.../ozone/om/protocol/OzoneManagerProtocol.java | 8 +
...OzoneManagerProtocolClientSideTranslatorPB.java | 25 ++
.../java/org/apache/hadoop/ozone/TestOmUtils.java | 10 +
.../src/main/proto/OmClientProtocol.proto | 16 +
.../org/apache/hadoop/ozone/om/OzoneManager.java | 43 +++
.../trapped/BucketDeletedDataCalculator.java | 373 +++++++++++++++++++++
.../ozone/om/snapshot/trapped/package-info.java | 21 ++
.../protocolPB/OzoneManagerRequestHandler.java | 23 ++
.../trapped/TestBucketDeletedDataCalculator.java | 243 ++++++++++++++
13 files changed, 945 insertions(+)
diff --git
a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/ozone/admin/om/BucketDeletedBytesSubCommand.java
b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/ozone/admin/om/BucketDeletedBytesSubCommand.java
new file mode 100644
index 00000000000..db1360b03db
--- /dev/null
+++
b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/ozone/admin/om/BucketDeletedBytesSubCommand.java
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.admin.om;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import org.apache.hadoop.hdds.cli.HddsVersionProvider;
+import org.apache.hadoop.hdds.server.JsonUtils;
+import org.apache.hadoop.ozone.om.helpers.BucketDeletedBytes;
+import org.apache.hadoop.ozone.om.protocol.OzoneManagerProtocol;
+import picocli.CommandLine;
+
+/**
+ * Handler of ozone admin om bucket-deleted-bytes command.
+ */
[email protected](
+ name = "bucket-deleted-bytes",
+ aliases = {"bdb"},
+ description = "Shows snapshot-trapped vs purgeable deleted bytes for a
bucket.",
+ mixinStandardHelpOptions = true,
+ versionProvider = HddsVersionProvider.class)
+public class BucketDeletedBytesSubCommand implements Callable<Void> {
+
+ @CommandLine.Mixin
+ private OmAddressOptions.OptionalServiceIdOrHostMixin omAddressOptions;
+
+ @CommandLine.Parameters(index = "0", arity = "1",
+ description = "Bucket path in volume/bucket format")
+ private String bucketPath;
+
+ @CommandLine.Option(names = { "--json" }, defaultValue = "false",
+ description = "Print output in JSON format")
+ private boolean json;
+
+ @Override
+ public Void call() throws Exception {
+ try (OzoneManagerProtocol omClient = omAddressOptions.newClient()) {
+ BucketDeletedBytes result = omClient.getBucketDeletedBytes(bucketPath);
+ if (json) {
+ printJson(result);
+ } else {
+ printText(result);
+ }
+ }
+ return null;
+ }
+
+ private void printText(BucketDeletedBytes result) {
+ long totalDeletedBytes =
+ result.getSnapshotTrappedBytes() + result.getPurgeableBytes();
+ long totalDeletedKeys =
+ result.getSnapshotTrappedKeys() + result.getPurgeableKeys();
+ long totalDeletedDirs =
+ result.getSnapshotTrappedDirs() + result.getPurgeableDirs();
+ System.out.println("Bucket: " + bucketPath);
+ System.out.println("SnapshotTrappedBytes: " +
result.getSnapshotTrappedBytes());
+ System.out.println("PurgeableBytes: " + result.getPurgeableBytes());
+ System.out.println("SnapshotTrappedKeys: " +
result.getSnapshotTrappedKeys());
+ System.out.println("PurgeableKeys: " + result.getPurgeableKeys());
+ System.out.println("SnapshotTrappedDirs: " +
result.getSnapshotTrappedDirs());
+ System.out.println("PurgeableDirs: " + result.getPurgeableDirs());
+ System.out.println("TotalDeletedBytes: " + totalDeletedBytes);
+ System.out.println("TotalDeletedKeys: " + totalDeletedKeys);
+ System.out.println("TotalDeletedDirs: " + totalDeletedDirs);
+ }
+
+ private void printJson(BucketDeletedBytes result) throws Exception {
+ Map<String, Object> output = new LinkedHashMap<>();
+ output.put("bucketPath", bucketPath);
+ output.put("snapshotTrappedBytes", result.getSnapshotTrappedBytes());
+ output.put("purgeableBytes", result.getPurgeableBytes());
+ output.put("snapshotTrappedKeys", result.getSnapshotTrappedKeys());
+ output.put("purgeableKeys", result.getPurgeableKeys());
+ output.put("snapshotTrappedDirs", result.getSnapshotTrappedDirs());
+ output.put("purgeableDirs", result.getPurgeableDirs());
+ output.put("totalDeletedBytes",
+ result.getSnapshotTrappedBytes() + result.getPurgeableBytes());
+ output.put("totalDeletedKeys",
+ result.getSnapshotTrappedKeys() + result.getPurgeableKeys());
+ output.put("totalDeletedDirs",
+ result.getSnapshotTrappedDirs() + result.getPurgeableDirs());
+ System.out.println(JsonUtils.toJsonStringWithDefaultPrettyPrinter(output));
+ }
+}
+
diff --git
a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/ozone/admin/om/OMAdmin.java
b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/ozone/admin/om/OMAdmin.java
index c31477e3d35..5eae7fde42a 100644
---
a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/ozone/admin/om/OMAdmin.java
+++
b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/ozone/admin/om/OMAdmin.java
@@ -50,6 +50,7 @@
mixinStandardHelpOptions = true,
versionProvider = HddsVersionProvider.class,
subcommands = {
+ BucketDeletedBytesSubCommand.class,
FinalizeUpgradeSubCommand.class,
ListOpenFilesSubCommand.class,
GetServiceRolesSubcommand.class,
diff --git
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/OmUtils.java
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/OmUtils.java
index 617e1636440..e118ef68b41 100644
--- a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/OmUtils.java
+++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/OmUtils.java
@@ -264,6 +264,7 @@ public static boolean isReadOnly(OMRequest omRequest) {
case GetSnapshotInfo:
case GetObjectTagging:
case GetBucketTagging:
+ case GetBucketDeletedBytes:
return true;
case GetQuotaRepairStatus:
case StartQuotaRepair:
@@ -398,6 +399,7 @@ public static boolean shouldSendToFollower(OMRequest
omRequest) {
case GetObjectTagging:
case GetLifecycleConfiguration:
case GetLifecycleServiceStatus:
+ case GetBucketDeletedBytes:
return true;
case GetBucketTagging:
return true;
diff --git
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/BucketDeletedBytes.java
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/BucketDeletedBytes.java
new file mode 100644
index 00000000000..36967b25eca
--- /dev/null
+++
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/BucketDeletedBytes.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.om.helpers;
+
+/**
+ * Bucket-level deleted-data split for a bucket at query time.
+ * <p>
+ * {@code snapshotTrappedBytes + purgeableBytes} represents the replicated size
+ * currently visible to the on-demand calculator for deleted keys and files
+ * discovered in deleted directory traversal for that bucket.
+ * <p>
+ * These values are not the same as OM global metrics such as
+ * {@code totalPendingDeletionBytes} / {@code totalPendingDeletionKeyCount}
+ * because those are cluster-wide service counters with different timing and
+ * accounting scope.
+ */
+public final class BucketDeletedBytes {
+ private final long snapshotTrappedBytes;
+ private final long purgeableBytes;
+ private final long snapshotTrappedKeys;
+ private final long purgeableKeys;
+ private final long snapshotTrappedDirs;
+ private final long purgeableDirs;
+
+ public BucketDeletedBytes(
+ long snapshotTrappedBytes,
+ long purgeableBytes,
+ long snapshotTrappedKeys,
+ long purgeableKeys,
+ long snapshotTrappedDirs,
+ long purgeableDirs) {
+ this.snapshotTrappedBytes = snapshotTrappedBytes;
+ this.purgeableBytes = purgeableBytes;
+ this.snapshotTrappedKeys = snapshotTrappedKeys;
+ this.purgeableKeys = purgeableKeys;
+ this.snapshotTrappedDirs = snapshotTrappedDirs;
+ this.purgeableDirs = purgeableDirs;
+ }
+
+ public long getSnapshotTrappedBytes() {
+ return snapshotTrappedBytes;
+ }
+
+ public long getPurgeableBytes() {
+ return purgeableBytes;
+ }
+
+ public long getSnapshotTrappedKeys() {
+ return snapshotTrappedKeys;
+ }
+
+ public long getPurgeableKeys() {
+ return purgeableKeys;
+ }
+
+ public long getSnapshotTrappedDirs() {
+ return snapshotTrappedDirs;
+ }
+
+ public long getPurgeableDirs() {
+ return purgeableDirs;
+ }
+}
+
diff --git
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/OzoneManagerProtocol.java
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/OzoneManagerProtocol.java
index 71c2d6b2294..e76ec341e77 100644
---
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/OzoneManagerProtocol.java
+++
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocol/OzoneManagerProtocol.java
@@ -30,6 +30,7 @@
import org.apache.hadoop.ozone.om.OMConfigKeys;
import org.apache.hadoop.ozone.om.exceptions.OMException;
import org.apache.hadoop.ozone.om.helpers.AssumeRoleResponseInfo;
+import org.apache.hadoop.ozone.om.helpers.BucketDeletedBytes;
import org.apache.hadoop.ozone.om.helpers.CallerIdentityInfo;
import org.apache.hadoop.ozone.om.helpers.DBUpdates;
import org.apache.hadoop.ozone.om.helpers.DeleteTenantState;
@@ -447,6 +448,13 @@ List<OmBucketInfo> listBuckets(String volumeName,
ListOpenFilesResult listOpenFiles(String path, int maxKeys, String contToken)
throws IOException;
+ /**
+ * Calculate bucket-level deleted bytes split by reclaimability.
+ * @param bucketPath bucket path in volume/bucket format.
+ */
+ BucketDeletedBytes getBucketDeletedBytes(String bucketPath)
+ throws IOException;
+
/**
* Transfer the raft leadership.
*
diff --git
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
index a6f8c7053c1..835b5010a2f 100644
---
a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
+++
b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/protocolPB/OzoneManagerProtocolClientSideTranslatorPB.java
@@ -61,6 +61,7 @@
import org.apache.hadoop.ozone.om.exceptions.OMException;
import org.apache.hadoop.ozone.om.helpers.AssumeRoleResponseInfo;
import org.apache.hadoop.ozone.om.helpers.BasicOmKeyInfo;
+import org.apache.hadoop.ozone.om.helpers.BucketDeletedBytes;
import org.apache.hadoop.ozone.om.helpers.CallerIdentityInfo;
import org.apache.hadoop.ozone.om.helpers.DBUpdates;
import org.apache.hadoop.ozone.om.helpers.DeleteTenantState;
@@ -143,6 +144,8 @@
import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.FinalizeUpgradeResponse;
import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.GetAclRequest;
import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.GetAclResponse;
+import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.GetBucketDeletedBytesRequest;
+import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.GetBucketDeletedBytesResponse;
import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.GetBucketTaggingRequest;
import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.GetBucketTaggingResponse;
import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.GetDelegationTokenResponseProto;
@@ -2067,6 +2070,28 @@ public ListOpenFilesResult listOpenFiles(String path,
resp.getKeyInfoList());
}
+ @Override
+ public BucketDeletedBytes getBucketDeletedBytes(String bucketPath)
+ throws IOException {
+ GetBucketDeletedBytesRequest request = GetBucketDeletedBytesRequest
+ .newBuilder()
+ .setBucketPath(bucketPath)
+ .build();
+ OMRequest omRequest = createOMRequest(Type.GetBucketDeletedBytes)
+ .setGetBucketDeletedBytesRequest(request)
+ .build();
+
+ GetBucketDeletedBytesResponse response =
handleError(submitRequest(omRequest))
+ .getGetBucketDeletedBytesResponse();
+ return new BucketDeletedBytes(
+ response.getSnapshotTrappedBytes(),
+ response.getPurgeableBytes(),
+ response.getSnapshotTrappedKeys(),
+ response.getPurgeableKeys(),
+ response.getSnapshotTrappedDirs(),
+ response.getPurgeableDirs());
+ }
+
@Override
public void transferLeadership(String newLeaderId)
throws IOException {
diff --git
a/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/TestOmUtils.java
b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/TestOmUtils.java
index 1765ad55523..8abfba48a13 100644
--- a/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/TestOmUtils.java
+++ b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/TestOmUtils.java
@@ -437,6 +437,16 @@ public void testShouldSendToFollowerImpliesIsReadOnly() {
}
}
+ @Test
+ public void
testGetBucketDeletedBytesIsClassifiedAsReadOnlyAndFollowerEligible() {
+ OMRequest request = OMRequest.newBuilder()
+ .setCmdType(OzoneManagerProtocolProtos.Type.GetBucketDeletedBytes)
+ .setClientId(UUID.randomUUID().toString())
+ .build();
+ assertTrue(OmUtils.isReadOnly(request));
+ assertTrue(OmUtils.shouldSendToFollower(request));
+ }
+
@Test
void testOmAddressesBracketIPv6Literals() {
OzoneConfiguration conf = new OzoneConfiguration();
diff --git
a/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
b/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
index 9af165bbcd0..ddc4cd80aab 100644
--- a/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
+++ b/hadoop-ozone/interface-client/src/main/proto/OmClientProtocol.proto
@@ -171,6 +171,7 @@ enum Type {
DeleteRevokedSTSTokens = 155;
GetCallerIdentity = 156;
SnapshotCount = 157;
+ GetBucketDeletedBytes = 158;
}
enum SafeMode {
@@ -337,6 +338,7 @@ message OMRequest {
optional UpdateAssumeRoleRequest updateAssumeRoleRequest =
157;
optional GetCallerIdentityRequest getCallerIdentityRequest =
158;
optional SnapshotCountRequest snapshotCountRequest = 159;
+ optional GetBucketDeletedBytesRequest getBucketDeletedBytesRequest =
160;
}
message OMResponse {
@@ -487,6 +489,7 @@ message OMResponse {
optional DeleteRevokedSTSTokensResponse deleteRevokedSTSTokensResponse =
155;
optional GetCallerIdentityResponse getCallerIdentityResponse =
156;
optional SnapshotCountResponse snapshotCountResponse = 157;
+ optional GetBucketDeletedBytesResponse getBucketDeletedBytesResponse =
158;
}
enum Status {
@@ -1767,6 +1770,19 @@ message ListOpenFilesResponse {
repeated KeyInfo keyInfo = 5;
}
+message GetBucketDeletedBytesRequest {
+ required string bucketPath = 1;
+}
+
+message GetBucketDeletedBytesResponse {
+ required uint64 snapshotTrappedBytes = 1;
+ required uint64 purgeableBytes = 2;
+ optional uint64 snapshotTrappedKeys = 3;
+ optional uint64 purgeableKeys = 4;
+ optional uint64 snapshotTrappedDirs = 5;
+ optional uint64 purgeableDirs = 6;
+}
+
message ServicePort {
enum Type {
RPC = 1;
diff --git
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
index fdc6f95b48d..be952b3f6be 100644
---
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
+++
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OzoneManager.java
@@ -261,6 +261,7 @@
import org.apache.hadoop.ozone.om.ha.OMHANodeDetails;
import org.apache.hadoop.ozone.om.ha.OMServiceManager;
import org.apache.hadoop.ozone.om.helpers.BasicOmKeyInfo;
+import org.apache.hadoop.ozone.om.helpers.BucketDeletedBytes;
import org.apache.hadoop.ozone.om.helpers.BucketLayout;
import org.apache.hadoop.ozone.om.helpers.DBUpdates;
import org.apache.hadoop.ozone.om.helpers.KeyInfoWithVolumeContext;
@@ -312,6 +313,7 @@
import org.apache.hadoop.ozone.om.service.QuotaRepairTask;
import org.apache.hadoop.ozone.om.service.RevokedSTSTokenCleanupService;
import org.apache.hadoop.ozone.om.snapshot.defrag.SnapshotDefragService;
+import org.apache.hadoop.ozone.om.snapshot.trapped.BucketDeletedDataCalculator;
import org.apache.hadoop.ozone.om.upgrade.OMLayoutFeature;
import org.apache.hadoop.ozone.om.upgrade.OMLayoutVersionManager;
import org.apache.hadoop.ozone.om.upgrade.OMUpgradeFinalizer;
@@ -1924,6 +1926,47 @@ public OmSnapshotManager getOmSnapshotManager() {
return omSnapshotManager;
}
+ /**
+ * Compute deleted bytes split for a bucket.
+ *
+ * @return stats containing snapshot-trapped bytes and purgeable bytes.
+ */
+ public BucketDeletedDataCalculator.BucketDeletedBytesStats
+ calculateDeletedBytesForBucket(String volume, String bucket)
+ throws IOException {
+ return new BucketDeletedDataCalculator(this).calculate(volume, bucket);
+ }
+
+ /**
+ * Compute deleted bytes split for a bucket path in {@code volume/bucket}
+ * format.
+ */
+ public BucketDeletedDataCalculator.BucketDeletedBytesStats
+ calculateDeletedBytesForBucket(String bucketPath)
+ throws IOException {
+ String[] tokens = bucketPath.split("/", 2);
+ if (tokens.length != 2 || tokens[0].isEmpty() || tokens[1].isEmpty()) {
+ throw new OMException("bucketPath must be in volume/bucket format: " +
bucketPath, INVALID_PATH);
+ }
+ ResolvedBucket resolvedBucket = resolveBucketLink(Pair.of(tokens[0],
tokens[1]));
+ return calculateDeletedBytesForBucket(resolvedBucket.realVolume(),
resolvedBucket.realBucket());
+ }
+
+ @Override
+ public BucketDeletedBytes getBucketDeletedBytes(String bucketPath)
+ throws IOException {
+ checkAdminUserPrivilege("get bucket deleted bytes.");
+ BucketDeletedDataCalculator.BucketDeletedBytesStats stats =
+ calculateDeletedBytesForBucket(bucketPath);
+ return new BucketDeletedBytes(
+ stats.getSnapshotTrappedBytes(),
+ stats.getPurgeableBytes(),
+ stats.getSnapshotTrappedKeys(),
+ stats.getPurgeableKeys(),
+ stats.getSnapshotTrappedDirs(),
+ stats.getPurgeableDirs());
+ }
+
/**
* Get metadata manager.
*
diff --git
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/snapshot/trapped/BucketDeletedDataCalculator.java
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/snapshot/trapped/BucketDeletedDataCalculator.java
new file mode 100644
index 00000000000..5fc2b144bee
--- /dev/null
+++
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/snapshot/trapped/BucketDeletedDataCalculator.java
@@ -0,0 +1,373 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.om.snapshot.trapped;
+
+import java.io.IOException;
+import java.util.ArrayDeque;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Queue;
+import java.util.Set;
+import java.util.UUID;
+import org.apache.hadoop.hdds.utils.db.Table;
+import org.apache.hadoop.hdds.utils.db.Table.KeyValue;
+import org.apache.hadoop.hdds.utils.db.TableIterator;
+import org.apache.hadoop.ozone.om.KeyManager;
+import org.apache.hadoop.ozone.om.OMMetadataManager;
+import org.apache.hadoop.ozone.om.OmMetadataManagerImpl;
+import org.apache.hadoop.ozone.om.OmSnapshot;
+import org.apache.hadoop.ozone.om.OmSnapshotManager;
+import org.apache.hadoop.ozone.om.OzoneManager;
+import org.apache.hadoop.ozone.om.SnapshotChainManager;
+import org.apache.hadoop.ozone.om.helpers.OmKeyInfo;
+import org.apache.hadoop.ozone.om.helpers.RepeatedOmKeyInfo;
+import org.apache.hadoop.ozone.om.helpers.SnapshotInfo;
+import org.apache.hadoop.ozone.om.snapshot.SnapshotUtils;
+import org.apache.hadoop.ozone.om.snapshot.filter.ReclaimableDirFilter;
+import org.apache.hadoop.ozone.om.snapshot.filter.ReclaimableKeyFilter;
+import org.apache.ratis.util.function.UncheckedAutoCloseableSupplier;
+
+/**
+ * Computes bucket-level deleted bytes split into purgeable and
snapshot-trapped.
+ * <p>
+ * This calculator follows KDS/DDS reclaimability semantics across active OM DB
+ * and all active snapshots in the bucket chain.
+ */
+public class BucketDeletedDataCalculator {
+
+ private final OzoneManager ozoneManager;
+ private final OmSnapshotManager omSnapshotManager;
+ private final SnapshotChainManager snapshotChainManager;
+
+ public BucketDeletedDataCalculator(OzoneManager ozoneManager) {
+ this.ozoneManager = ozoneManager;
+ this.omSnapshotManager = ozoneManager.getOmSnapshotManager();
+ this.snapshotChainManager =
+ ((OmMetadataManagerImpl) ozoneManager.getMetadataManager())
+ .getSnapshotChainManager();
+ }
+
+ public BucketDeletedBytesStats calculate(String volume, String bucket)
+ throws IOException {
+ BucketDeletedBytesStats totals = new BucketDeletedBytesStats();
+ processStore(volume, bucket, null,
+ requireKeyManager(ozoneManager.getKeyManager(), "active store"),
+ totals, false);
+
+ Iterator<UUID> iterator = snapshotChainManager.iterator(true);
+ while (iterator.hasNext()) {
+ UUID snapshotId = iterator.next();
+ SnapshotInfo snapshotInfo = SnapshotUtils.getSnapshotInfo(
+ ozoneManager, snapshotChainManager, snapshotId);
+ if (snapshotInfo == null
+ || snapshotInfo.getSnapshotStatus() !=
SnapshotInfo.SnapshotStatus.SNAPSHOT_ACTIVE
+ || !snapshotInfo.getVolumeName().equals(volume)
+ || !snapshotInfo.getBucketName().equals(bucket)
+ || !OmSnapshotManager.areSnapshotChangesFlushedToDB(
+ ozoneManager.getMetadataManager(), snapshotInfo)) {
+ continue;
+ }
+ // Not deep-cleaned snapshots are still expanded and counted, but treated
+ // as trapped because reclaimability is not final yet.
+ boolean forceSnapshotTrapped = !snapshotInfo.isDeepCleanedDeletedDir();
+ try (UncheckedAutoCloseableSupplier<OmSnapshot> snapshot =
+ omSnapshotManager.getActiveSnapshot(volume, bucket,
snapshotInfo.getName())) {
+ processStore(volume, bucket, snapshotInfo,
+ requireKeyManager(snapshot.get().getKeyManager(),
+ "snapshot " + snapshotInfo.getTableKey()),
+ totals, forceSnapshotTrapped);
+ }
+ }
+ return totals;
+ }
+
+ private static KeyManager requireKeyManager(KeyManager keyManager, String
scope)
+ throws IOException {
+ if (keyManager == null) {
+ throw new IOException("KeyManager is not available for " + scope
+ + ". Ensure OM is fully initialized before running the
calculation.");
+ }
+ return keyManager;
+ }
+
+ private void processStore(
+ String volume,
+ String bucket,
+ SnapshotInfo currentSnapshotInfo,
+ KeyManager keyManager,
+ BucketDeletedBytesStats totals,
+ boolean forceSnapshotTrapped) throws IOException {
+ if (forceSnapshotTrapped) {
+ processDeletedKeysAsTrapped(volume, bucket, keyManager, totals);
+ processDeletedDirsAsTrapped(volume, bucket, keyManager, totals);
+ return;
+ }
+ try (ReclaimableKeyFilter reclaimableKeyFilter = new ReclaimableKeyFilter(
+ ozoneManager,
+ omSnapshotManager,
+ snapshotChainManager,
+ currentSnapshotInfo,
+ keyManager,
+ ozoneManager.getMetadataManager().getLock());
+ ReclaimableDirFilter reclaimableDirFilter = new ReclaimableDirFilter(
+ ozoneManager,
+ omSnapshotManager,
+ snapshotChainManager,
+ currentSnapshotInfo,
+ keyManager,
+ ozoneManager.getMetadataManager().getLock())) {
+
+ processDeletedKeys(volume, bucket, keyManager, reclaimableKeyFilter,
totals);
+ processDeletedDirs(volume, bucket, keyManager, reclaimableKeyFilter,
+ reclaimableDirFilter, totals);
+ }
+ }
+
+ private void processDeletedKeysAsTrapped(
+ String volume,
+ String bucket,
+ KeyManager keyManager,
+ BucketDeletedBytesStats totals) throws IOException {
+ OMMetadataManager metadataManager = keyManager.getMetadataManager();
+ Table<String, RepeatedOmKeyInfo> deletedTable =
metadataManager.getDeletedTable();
+ String deletedTablePrefix = metadataManager.getTableBucketPrefix(
+ deletedTable.getName(), volume, bucket);
+ try (TableIterator<String, ? extends KeyValue<String, RepeatedOmKeyInfo>>
itr =
+ deletedTable.iterator(deletedTablePrefix)) {
+ while (itr.hasNext()) {
+ KeyValue<String, ? extends RepeatedOmKeyInfo> entry = itr.next();
+ for (OmKeyInfo omKeyInfo : entry.getValue().getOmKeyInfoList()) {
+ totals.addSnapshotTrappedKey(omKeyInfo.getReplicatedSize());
+ }
+ }
+ }
+ }
+
+ private void processDeletedKeys(
+ String volume,
+ String bucket,
+ KeyManager keyManager,
+ ReclaimableKeyFilter reclaimableKeyFilter,
+ BucketDeletedBytesStats totals) throws IOException {
+ OMMetadataManager metadataManager = keyManager.getMetadataManager();
+ Table<String, RepeatedOmKeyInfo> deletedTable =
metadataManager.getDeletedTable();
+ String deletedTablePrefix = metadataManager.getTableBucketPrefix(
+ deletedTable.getName(), volume, bucket);
+ try (TableIterator<String, ? extends KeyValue<String, RepeatedOmKeyInfo>>
itr =
+ deletedTable.iterator(deletedTablePrefix)) {
+ while (itr.hasNext()) {
+ KeyValue<String, ? extends RepeatedOmKeyInfo> entry = itr.next();
+ for (OmKeyInfo omKeyInfo : entry.getValue().getOmKeyInfoList()) {
+ if (reclaimableKeyFilter.apply(Table.newKeyValue(entry.getKey(),
omKeyInfo))) {
+ totals.addPurgeableKey(omKeyInfo.getReplicatedSize());
+ } else {
+ totals.addSnapshotTrappedKey(omKeyInfo.getReplicatedSize());
+ }
+ }
+ }
+ }
+ }
+
+ private void processDeletedDirs(
+ String volume,
+ String bucket,
+ KeyManager keyManager,
+ ReclaimableKeyFilter reclaimableKeyFilter,
+ ReclaimableDirFilter reclaimableDirFilter,
+ BucketDeletedBytesStats totals) throws IOException {
+ OMMetadataManager metadataManager = ozoneManager.getMetadataManager();
+ long volumeId = metadataManager.getVolumeId(volume);
+ long bucketId = metadataManager.getBucketId(volume, bucket);
+
+ try (TableIterator<String, Table.KeyValue<String, OmKeyInfo>>
deletedDirItr =
+ keyManager.getDeletedDirEntries(volume, bucket)) {
+ while (deletedDirItr.hasNext()) {
+ Table.KeyValue<String, OmKeyInfo> rootEntry = deletedDirItr.next();
+ traverseDeletedDirSubtree(
+ keyManager, reclaimableKeyFilter, reclaimableDirFilter,
+ volumeId, bucketId, rootEntry, totals);
+ }
+ }
+ }
+
+ private void processDeletedDirsAsTrapped(
+ String volume,
+ String bucket,
+ KeyManager keyManager,
+ BucketDeletedBytesStats totals) throws IOException {
+ OMMetadataManager metadataManager = ozoneManager.getMetadataManager();
+ long volumeId = metadataManager.getVolumeId(volume);
+ long bucketId = metadataManager.getBucketId(volume, bucket);
+
+ try (TableIterator<String, Table.KeyValue<String, OmKeyInfo>>
deletedDirItr =
+ keyManager.getDeletedDirEntries(volume, bucket)) {
+ while (deletedDirItr.hasNext()) {
+ Table.KeyValue<String, OmKeyInfo> rootEntry = deletedDirItr.next();
+ traverseDeletedDirSubtreeAsTrapped(
+ keyManager, volumeId, bucketId, rootEntry, totals);
+ }
+ }
+ }
+
+ private void traverseDeletedDirSubtree(
+ KeyManager keyManager,
+ ReclaimableKeyFilter reclaimableKeyFilter,
+ ReclaimableDirFilter reclaimableDirFilter,
+ long volumeId,
+ long bucketId,
+ Table.KeyValue<String, OmKeyInfo> rootEntry,
+ BucketDeletedBytesStats totals) throws IOException {
+ Queue<OmKeyInfo> queue = new ArrayDeque<>();
+ Set<Long> visitedDirIds = new HashSet<>();
+ Set<Long> visitedFileIds = new HashSet<>();
+
+ queue.add(rootEntry.getValue());
+ visitedDirIds.add(rootEntry.getValue().getObjectID());
+
+ while (!queue.isEmpty()) {
+ OmKeyInfo currentDir = queue.poll();
+ boolean currentDirReclaimable =
+ reclaimableDirFilter.apply(Table.newKeyValue("", currentDir));
+ if (currentDirReclaimable) {
+ totals.addPurgeableDir();
+ } else {
+ totals.addSnapshotTrappedDir();
+ }
+
+ for (OmKeyInfo subFile : keyManager.getPendingDeletionSubFiles(
+ volumeId, bucketId, currentDir, keyValue -> true,
Integer.MAX_VALUE).getKeysToDelete()) {
+ if (!visitedFileIds.add(subFile.getObjectID())) {
+ continue;
+ }
+ // Keep parity with DirectoryDeletingService where purgeable parent
+ // directory implies purgeable descendant files.
+ boolean fileReclaimable = currentDirReclaimable
+ || reclaimableKeyFilter.apply(Table.newKeyValue("", subFile));
+ if (fileReclaimable) {
+ totals.addPurgeableKey(subFile.getReplicatedSize());
+ } else {
+ totals.addSnapshotTrappedKey(subFile.getReplicatedSize());
+ }
+ }
+
+ for (OmKeyInfo subDir : keyManager.getPendingDeletionSubDirs(
+ volumeId, bucketId, currentDir, keyValue -> true,
Integer.MAX_VALUE).getKeysToDelete()) {
+ if (visitedDirIds.add(subDir.getObjectID())) {
+ queue.add(subDir);
+ }
+ }
+ }
+ }
+
+ private void traverseDeletedDirSubtreeAsTrapped(
+ KeyManager keyManager,
+ long volumeId,
+ long bucketId,
+ Table.KeyValue<String, OmKeyInfo> rootEntry,
+ BucketDeletedBytesStats totals) throws IOException {
+ Queue<OmKeyInfo> queue = new ArrayDeque<>();
+ Set<Long> visitedDirIds = new HashSet<>();
+ Set<Long> visitedFileIds = new HashSet<>();
+
+ queue.add(rootEntry.getValue());
+ visitedDirIds.add(rootEntry.getValue().getObjectID());
+
+ while (!queue.isEmpty()) {
+ OmKeyInfo currentDir = queue.poll();
+ totals.addSnapshotTrappedDir();
+
+ for (OmKeyInfo subFile : keyManager.getPendingDeletionSubFiles(
+ volumeId, bucketId, currentDir, keyValue -> true,
Integer.MAX_VALUE).getKeysToDelete()) {
+ if (!visitedFileIds.add(subFile.getObjectID())) {
+ continue;
+ }
+ totals.addSnapshotTrappedKey(subFile.getReplicatedSize());
+ }
+
+ for (OmKeyInfo subDir : keyManager.getPendingDeletionSubDirs(
+ volumeId, bucketId, currentDir, keyValue -> true,
Integer.MAX_VALUE).getKeysToDelete()) {
+ if (visitedDirIds.add(subDir.getObjectID())) {
+ queue.add(subDir);
+ }
+ }
+ }
+ }
+
+ /**
+ * Aggregated on-demand deleted-bytes view for one bucket.
+ */
+ public static final class BucketDeletedBytesStats {
+ private long snapshotTrappedBytes;
+ private long purgeableBytes;
+ private long snapshotTrappedKeys;
+ private long purgeableKeys;
+ private long snapshotTrappedDirs;
+ private long purgeableDirs;
+
+ public long getSnapshotTrappedBytes() {
+ return snapshotTrappedBytes;
+ }
+
+ public long getPurgeableBytes() {
+ return purgeableBytes;
+ }
+
+ public long getSnapshotTrappedKeys() {
+ return snapshotTrappedKeys;
+ }
+
+ public long getPurgeableKeys() {
+ return purgeableKeys;
+ }
+
+ public long getSnapshotTrappedDirs() {
+ return snapshotTrappedDirs;
+ }
+
+ public long getPurgeableDirs() {
+ return purgeableDirs;
+ }
+
+ public void addSnapshotTrappedBytes(long value) {
+ snapshotTrappedBytes += value;
+ }
+
+ public void addPurgeableBytes(long value) {
+ purgeableBytes += value;
+ }
+
+ public void addSnapshotTrappedKey(long bytes) {
+ addSnapshotTrappedBytes(bytes);
+ snapshotTrappedKeys++;
+ }
+
+ public void addPurgeableKey(long bytes) {
+ addPurgeableBytes(bytes);
+ purgeableKeys++;
+ }
+
+ public void addSnapshotTrappedDir() {
+ snapshotTrappedDirs++;
+ }
+
+ public void addPurgeableDir() {
+ purgeableDirs++;
+ }
+ }
+}
+
diff --git
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/snapshot/trapped/package-info.java
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/snapshot/trapped/package-info.java
new file mode 100644
index 00000000000..b4fb62c8cd8
--- /dev/null
+++
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/snapshot/trapped/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Helpers for bucket-level deleted-data split and snapshot-trapped accounting.
+ */
+package org.apache.hadoop.ozone.om.snapshot.trapped;
diff --git
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/protocolPB/OzoneManagerRequestHandler.java
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/protocolPB/OzoneManagerRequestHandler.java
index 2fc6000abe6..2e234ac76ec 100644
---
a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/protocolPB/OzoneManagerRequestHandler.java
+++
b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/protocolPB/OzoneManagerRequestHandler.java
@@ -68,6 +68,7 @@
import org.apache.hadoop.ozone.om.exceptions.OMException;
import org.apache.hadoop.ozone.om.execution.flowcontrol.ExecutionContext;
import org.apache.hadoop.ozone.om.helpers.BasicOmKeyInfo;
+import org.apache.hadoop.ozone.om.helpers.BucketDeletedBytes;
import org.apache.hadoop.ozone.om.helpers.BucketLayout;
import org.apache.hadoop.ozone.om.helpers.DBUpdates;
import org.apache.hadoop.ozone.om.helpers.KeyInfoWithVolumeContext;
@@ -114,6 +115,8 @@
import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.EchoRPCResponse;
import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.FinalizeUpgradeProgressRequest;
import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.FinalizeUpgradeProgressResponse;
+import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.GetBucketDeletedBytesRequest;
+import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.GetBucketDeletedBytesResponse;
import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.GetBucketTaggingRequest;
import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.GetBucketTaggingResponse;
import
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.GetCallerIdentityResponse;
@@ -267,6 +270,12 @@ public OMResponse handleReadRequest(OMRequest request) {
request.getListOpenFilesRequest(), request.getVersion());
responseBuilder.setListOpenFilesResponse(listOpenFilesResponse);
break;
+ case GetBucketDeletedBytes:
+ GetBucketDeletedBytesResponse getBucketDeletedBytesResponse =
+ getBucketDeletedBytes(request.getGetBucketDeletedBytesRequest());
+ responseBuilder.setGetBucketDeletedBytesResponse(
+ getBucketDeletedBytesResponse);
+ break;
case ServiceList:
ServiceListResponse serviceListResponse = getServiceList(
request.getServiceListRequest());
@@ -989,6 +998,20 @@ private ListOpenFilesResponse
listOpenFiles(ListOpenFilesRequest req,
return resp.build();
}
+ private GetBucketDeletedBytesResponse getBucketDeletedBytes(
+ GetBucketDeletedBytesRequest request) throws IOException {
+ BucketDeletedBytes bytes = impl.getBucketDeletedBytes(
+ request.getBucketPath());
+ return GetBucketDeletedBytesResponse.newBuilder()
+ .setSnapshotTrappedBytes(bytes.getSnapshotTrappedBytes())
+ .setPurgeableBytes(bytes.getPurgeableBytes())
+ .setSnapshotTrappedKeys(bytes.getSnapshotTrappedKeys())
+ .setPurgeableKeys(bytes.getPurgeableKeys())
+ .setSnapshotTrappedDirs(bytes.getSnapshotTrappedDirs())
+ .setPurgeableDirs(bytes.getPurgeableDirs())
+ .build();
+ }
+
private ServiceListResponse getServiceList(ServiceListRequest request)
throws IOException {
ServiceListResponse.Builder resp = ServiceListResponse.newBuilder();
diff --git
a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/snapshot/trapped/TestBucketDeletedDataCalculator.java
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/snapshot/trapped/TestBucketDeletedDataCalculator.java
new file mode 100644
index 00000000000..a3b8e1334b7
--- /dev/null
+++
b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/snapshot/trapped/TestBucketDeletedDataCalculator.java
@@ -0,0 +1,243 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hadoop.ozone.om.snapshot.trapped;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.util.Collections;
+import org.apache.hadoop.hdds.client.RatisReplicationConfig;
+import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
+import org.apache.hadoop.hdds.utils.db.BatchOperation;
+import org.apache.hadoop.hdds.utils.db.Table;
+import org.apache.hadoop.ozone.om.BucketManager;
+import org.apache.hadoop.ozone.om.DeleteKeysResult;
+import org.apache.hadoop.ozone.om.KeyManager;
+import org.apache.hadoop.ozone.om.OMMetadataManager;
+import org.apache.hadoop.ozone.om.OmSnapshot;
+import org.apache.hadoop.ozone.om.helpers.BucketLayout;
+import org.apache.hadoop.ozone.om.helpers.OmBucketInfo;
+import org.apache.hadoop.ozone.om.helpers.OmKeyInfo;
+import org.apache.hadoop.ozone.om.helpers.OmVolumeArgs;
+import org.apache.hadoop.ozone.om.helpers.RepeatedOmKeyInfo;
+import org.apache.hadoop.ozone.om.helpers.SnapshotInfo;
+import org.apache.hadoop.ozone.om.request.OMRequestTestUtils;
+import org.apache.hadoop.ozone.om.snapshot.SnapshotRequestAndResponseTests;
+import org.apache.ratis.util.function.UncheckedAutoCloseableSupplier;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for {@link BucketDeletedDataCalculator}.
+ */
+public class TestBucketDeletedDataCalculator extends
SnapshotRequestAndResponseTests {
+
+ private KeyManager mockActiveKeyManager(String volume, String bucket) throws
IOException {
+ KeyManager keyManager = mock(KeyManager.class);
+ when(keyManager.getMetadataManager()).thenReturn(getOmMetadataManager());
+ when(keyManager.getDeletedDirEntries(volume,
bucket)).thenAnswer(invocation -> {
+ Table<String, OmKeyInfo> deletedDirTable =
getOmMetadataManager().getDeletedDirTable();
+ String prefix = getOmMetadataManager().getTableBucketPrefix(
+ deletedDirTable.getName(), volume, bucket);
+ return deletedDirTable.iterator(prefix);
+ });
+ when(getOzoneManager().getKeyManager()).thenReturn(keyManager);
+ return keyManager;
+ }
+
+ private void mockBucketManager(String volume, String bucket) throws
IOException {
+ BucketManager bucketManager = mock(BucketManager.class);
+ String bucketDbKey = getOmMetadataManager().getBucketKey(volume, bucket);
+ OmBucketInfo bucketInfo =
getOmMetadataManager().getBucketTable().get(bucketDbKey);
+ when(bucketManager.getBucketInfo(volume, bucket)).thenReturn(bucketInfo);
+ when(getOzoneManager().getBucketManager()).thenReturn(bucketManager);
+ }
+
+ private OmKeyInfo newOmKeyInfo(String volume, String bucket, String keyName,
long objectId) {
+ return OMRequestTestUtils.createOmKeyInfo(
+ volume, bucket, keyName,
+
RatisReplicationConfig.getInstance(HddsProtos.ReplicationFactor.THREE))
+ .setObjectID(objectId)
+ .setUpdateID(objectId)
+ .build();
+ }
+
+ private void putDeletedKey(
+ OMMetadataManager metadataManager, String volume, String bucket,
OmKeyInfo keyInfo) throws Exception {
+ long bucketId = getOmMetadataManager().getBucketId(volume, bucket);
+ RepeatedOmKeyInfo repeated = new RepeatedOmKeyInfo(keyInfo, bucketId);
+ String deletedDbKey = metadataManager.getOzoneKey(volume, bucket,
keyInfo.getKeyName());
+ try (BatchOperation batch =
metadataManager.getStore().initBatchOperation()) {
+ metadataManager.getDeletedTable().putWithBatch(batch, deletedDbKey,
repeated);
+ metadataManager.getStore().commitBatchOperation(batch);
+ }
+ }
+
+ private SnapshotInfo setDeepCleanedDeletedDir(
+ String volume, String bucket, String snapshotName, boolean
deepCleanedDeletedDir) throws Exception {
+ String snapshotTableKey = SnapshotInfo.getTableKey(volume, bucket,
snapshotName);
+ SnapshotInfo snapshotInfo =
getOmMetadataManager().getSnapshotInfoTable().get(snapshotTableKey);
+ snapshotInfo.setDeepCleanedDeletedDir(deepCleanedDeletedDir);
+ getOmMetadataManager().getSnapshotInfoTable()
+ .addCacheEntry(snapshotTableKey, snapshotInfo,
System.currentTimeMillis());
+ return snapshotInfo;
+ }
+
+ private void ensureBucketContextInSnapshot(
+ OMMetadataManager snapshotMetadataManager, String volume, String bucket)
throws Exception {
+ OmVolumeArgs volumeArgs =
getOmMetadataManager().getVolumeTable().get(getOmMetadataManager().getVolumeKey(volume));
+ OmBucketInfo bucketInfo = getOmMetadataManager().getBucketTable()
+ .get(getOmMetadataManager().getBucketKey(volume, bucket));
+ if (volumeArgs != null) {
+
snapshotMetadataManager.getVolumeTable().put(snapshotMetadataManager.getVolumeKey(volume),
volumeArgs);
+ }
+ if (bucketInfo != null) {
+
snapshotMetadataManager.getBucketTable().put(snapshotMetadataManager.getBucketKey(volume,
bucket), bucketInfo);
+ }
+ }
+
+ @Test
+ public void testAosDeletedKeyReportedAsPurgeable() throws Exception {
+ String volume = getVolumeName();
+ String bucket = getBucketName();
+ mockActiveKeyManager(volume, bucket);
+ mockBucketManager(volume, bucket);
+ OmKeyInfo keyInfo = newOmKeyInfo(volume, bucket, "key-a", 101L);
+ putDeletedKey(getOmMetadataManager(), volume, bucket, keyInfo);
+
+ BucketDeletedDataCalculator.BucketDeletedBytesStats stats =
+ new BucketDeletedDataCalculator(getOzoneManager())
+ .calculate(volume, bucket);
+
+ assertEquals(0L, stats.getSnapshotTrappedBytes());
+ assertEquals(0L, stats.getSnapshotTrappedKeys());
+ assertEquals(0L, stats.getSnapshotTrappedDirs());
+ assertTrue(stats.getPurgeableBytes() > 0L);
+ assertEquals(1L, stats.getPurgeableKeys());
+ assertEquals(0L, stats.getPurgeableDirs());
+ }
+
+ @Test
+ public void
testSnapshotDeletedKeysCountedAsTrappedUntilDeletedDirDeepClean() throws
Exception {
+ String volume = getVolumeName();
+ String bucket = getBucketName();
+ mockActiveKeyManager(volume, bucket);
+ mockBucketManager(volume, bucket);
+
+ String snapshotName = "snap-skip";
+ createSnapshotCheckpoint(volume, bucket, snapshotName);
+ setDeepCleanedDeletedDir(volume, bucket, snapshotName, false);
+
+ OmKeyInfo keyInfo = newOmKeyInfo(volume, bucket, "key-in-snapshot", 201L);
+ try (UncheckedAutoCloseableSupplier<OmSnapshot> snapshot =
+ getOmSnapshotManager().getActiveSnapshot(volume, bucket,
snapshotName)) {
+ OMMetadataManager snapshotMetadataManager =
snapshot.get().getMetadataManager();
+ ensureBucketContextInSnapshot(snapshotMetadataManager, volume, bucket);
+ putDeletedKey(snapshotMetadataManager, volume, bucket, keyInfo);
+ }
+
+ BucketDeletedDataCalculator.BucketDeletedBytesStats stats =
+ new BucketDeletedDataCalculator(getOzoneManager()).calculate(volume,
bucket);
+
+ assertEquals(keyInfo.getReplicatedSize(), stats.getSnapshotTrappedBytes());
+ assertEquals(0L, stats.getPurgeableBytes());
+ assertEquals(1L, stats.getSnapshotTrappedKeys());
+ assertEquals(0L, stats.getPurgeableKeys());
+ assertEquals(0L, stats.getSnapshotTrappedDirs());
+ assertEquals(0L, stats.getPurgeableDirs());
+ }
+
+ @Test
+ public void testSnapshotDeletedKeyReportedAsSnapshotTrapped() throws
Exception {
+ String volume = getVolumeName();
+ String bucket = getBucketName();
+ mockActiveKeyManager(volume, bucket);
+ mockBucketManager(volume, bucket);
+
+ String snapshotOne = "snap-1";
+ String snapshotTwo = "snap-2";
+ createSnapshotCheckpoint(volume, bucket, snapshotOne);
+ createSnapshotCheckpoint(volume, bucket, snapshotTwo);
+ setDeepCleanedDeletedDir(volume, bucket, snapshotOne, true);
+ setDeepCleanedDeletedDir(volume, bucket, snapshotTwo, true);
+
+ OmKeyInfo keyInfo = newOmKeyInfo(volume, bucket, "key-trapped", 301L);
+ try (UncheckedAutoCloseableSupplier<OmSnapshot> snapshot =
+ getOmSnapshotManager().getActiveSnapshot(volume, bucket,
snapshotOne)) {
+ OMMetadataManager snapshotMetadataManager =
snapshot.get().getMetadataManager();
+ ensureBucketContextInSnapshot(snapshotMetadataManager, volume, bucket);
+ String keyDbKey = snapshotMetadataManager.getOzoneKey(volume, bucket,
keyInfo.getKeyName());
+ snapshotMetadataManager.getKeyTable(BucketLayout.LEGACY).put(keyDbKey,
keyInfo);
+ }
+ try (UncheckedAutoCloseableSupplier<OmSnapshot> snapshot =
+ getOmSnapshotManager().getActiveSnapshot(volume, bucket,
snapshotTwo)) {
+ OMMetadataManager snapshotMetadataManager =
snapshot.get().getMetadataManager();
+ ensureBucketContextInSnapshot(snapshotMetadataManager, volume, bucket);
+ putDeletedKey(snapshotMetadataManager, volume, bucket, keyInfo);
+ }
+
+ BucketDeletedDataCalculator.BucketDeletedBytesStats stats =
+ new BucketDeletedDataCalculator(getOzoneManager()).calculate(volume,
bucket);
+
+ assertEquals(keyInfo.getReplicatedSize(), stats.getSnapshotTrappedBytes());
+ assertEquals(0L, stats.getPurgeableBytes());
+ assertEquals(1L, stats.getSnapshotTrappedKeys());
+ assertEquals(0L, stats.getPurgeableKeys());
+ assertEquals(0L, stats.getSnapshotTrappedDirs());
+ assertEquals(0L, stats.getPurgeableDirs());
+ }
+
+ @Test
+ public void testAosDeletedDirTraversalReportedAsPurgeable() throws Exception
{
+ String volume = getVolumeName();
+ String bucket = getBucketName();
+ KeyManager keyManager = mockActiveKeyManager(volume, bucket);
+ mockBucketManager(volume, bucket);
+
+ Table<String, OmKeyInfo> deletedDirTable =
getOmMetadataManager().getDeletedDirTable();
+ String deletedDirPrefix = getOmMetadataManager().getTableBucketPrefix(
+ deletedDirTable.getName(), volume, bucket);
+ OmKeyInfo rootDirInfo = newOmKeyInfo(volume, bucket, "dir-root", 401L);
+ try (BatchOperation batch =
getOmMetadataManager().getStore().initBatchOperation()) {
+ deletedDirTable.putWithBatch(batch, deletedDirPrefix + "/dir-root",
rootDirInfo);
+ getOmMetadataManager().getStore().commitBatchOperation(batch);
+ }
+
+ OmKeyInfo subFileInfo = newOmKeyInfo(volume, bucket, "dir-root/file-a",
402L);
+ when(keyManager.getPendingDeletionSubFiles(anyLong(), anyLong(),
any(OmKeyInfo.class), any(), anyInt()))
+ .thenReturn(new
DeleteKeysResult(Collections.singletonList(subFileInfo), true));
+ when(keyManager.getPendingDeletionSubDirs(anyLong(), anyLong(),
any(OmKeyInfo.class), any(), anyInt()))
+ .thenReturn(new DeleteKeysResult(Collections.emptyList(), true));
+
+ BucketDeletedDataCalculator.BucketDeletedBytesStats stats =
+ new BucketDeletedDataCalculator(getOzoneManager()).calculate(volume,
bucket);
+
+ assertEquals(0L, stats.getSnapshotTrappedBytes());
+ assertEquals(subFileInfo.getReplicatedSize(), stats.getPurgeableBytes());
+ assertEquals(0L, stats.getSnapshotTrappedKeys());
+ assertEquals(1L, stats.getPurgeableKeys());
+ assertEquals(0L, stats.getSnapshotTrappedDirs());
+ assertEquals(1L, stats.getPurgeableDirs());
+ }
+}
+
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]