This is an automated email from the ASF dual-hosted git repository.
sarvekshayr 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 36b90e5dde8 HDDS-15456. Add SCM DB lookup and identify orphan(wrt SCM)
and deleted-but-present containers (#10547)
36b90e5dde8 is described below
commit 36b90e5dde80d24d8e44b422a30538af2de536d3
Author: sreejasahithi <[email protected]>
AuthorDate: Wed Jul 15 11:59:50 2026 +0530
HDDS-15456. Add SCM DB lookup and identify orphan(wrt SCM) and
deleted-but-present containers (#10547)
---
.../container/analyze/AnalyzeSubcommand.java | 234 +++++++++++++++++----
.../analyze/ScmContainerMetadataReader.java | 155 ++++++++++++++
.../analyze/ContainerAnalyzeTestHelper.java | 37 ++++
.../container/analyze/TestAnalyzeSubcommand.java | 188 ++++++++++++++++-
.../analyze/TestScmContainerMetadataReader.java | 100 +++++++++
5 files changed, 670 insertions(+), 44 deletions(-)
diff --git
a/hadoop-ozone/cli-debug/src/main/java/org/apache/hadoop/ozone/debug/datanode/container/analyze/AnalyzeSubcommand.java
b/hadoop-ozone/cli-debug/src/main/java/org/apache/hadoop/ozone/debug/datanode/container/analyze/AnalyzeSubcommand.java
index 12a0c67bd87..ce04cda487b 100644
---
a/hadoop-ozone/cli-debug/src/main/java/org/apache/hadoop/ozone/debug/datanode/container/analyze/AnalyzeSubcommand.java
+++
b/hadoop-ozone/cli-debug/src/main/java/org/apache/hadoop/ozone/debug/datanode/container/analyze/AnalyzeSubcommand.java
@@ -17,12 +17,28 @@
package org.apache.hadoop.ozone.debug.datanode.container.analyze;
+import java.io.File;
import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
+import java.util.Properties;
+import java.util.Set;
import java.util.concurrent.Callable;
+import java.util.stream.Stream;
import org.apache.hadoop.hdds.cli.AbstractSubcommand;
import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.hdds.utils.HddsServerUtil;
+import org.apache.hadoop.hdfs.server.datanode.StorageLocation;
+import org.apache.hadoop.ozone.container.common.helpers.DatanodeVersionFile;
+import org.apache.hadoop.ozone.container.common.utils.StorageVolumeUtil;
+import org.apache.hadoop.ozone.container.common.volume.HddsVolume;
+import org.apache.hadoop.ozone.shell.ListLimitOptions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import picocli.CommandLine;
import picocli.CommandLine.Command;
@@ -34,65 +50,209 @@
*/
@Command(
name = "analyze",
- description = "Analyze container consistency between on-disk container " +
- "directories on this DataNode and SCM metadata. Must be run
locally on a DataNode.")
+ description = {
+ "Analyze container consistency between on-disk container directories
on this DataNode and SCM metadata.",
+ "Must be run locally on a DataNode.",
+ "",
+ "Reports:",
+ " Duplicate container directories: same containerID found on more
than one volume.",
+ " Orphan containers (requires --scm-db): present on disk but not
present in SCM metadata.",
+ " Containers marked DELETED in SCM but present on disk (requires
--scm-db).",
+ "",
+ "Each reported occurrence includes container directory path(s), size
and an on-disk metadata status:",
+ " MISSING_METADATA: metadata/{containerId}.container does not exist.",
+ " INVALID_METADATA: metadata file exists but cannot be parsed, or the
containerID in the",
+ " file does not match the directory name.",
+ " VALID: metadata file is present, parses correctly, and its
containerID matches the directory name."
+ })
public class AnalyzeSubcommand extends AbstractSubcommand implements
Callable<Void> {
+ @Deprecated
@CommandLine.Option(names = {"--count"},
- defaultValue = "20",
- description = "Number of containers to display")
- private int count;
+ hidden = true,
+ description = "Number of containers to display")
+ private Integer count;
+
+ @CommandLine.Mixin
+ private ListLimitOptions listOptions;
+
+ private static final Logger LOG =
LoggerFactory.getLogger(AnalyzeSubcommand.class);
+
+ @CommandLine.Option(names = {"--scm-db"},
+ description = "Path to an offline scm.db directory, or its parent
metadata directory.")
+ private File scmDb;
@Override
public Void call() throws Exception {
- if (count < 1) {
- throw new IOException("Count must be an integer greater than 0.");
- }
+ validateOptions();
OzoneConfiguration conf = getOzoneConf();
ContainerScanResult scanResult = ContainerDirectoryScanner.scan(conf);
Map<Long, List<ContainerDiskOccurrence>> enrichedDuplicates =
ContainerDirectoryScanner.enrichDuplicates(scanResult.getDuplicates());
- // TODO: SCM metadata lookup from --scm-db when provided.
- // TODO: For each id in scanResult.getSingles().keySet() classified
NOT_IN_SCM or DELETED:
- // enrichOccurrence(id, scanResult.getSingles().get(id)) and report.
- // TODO: For each id in enrichedDuplicates.keySet() classified NOT_IN_SCM
or DELETED:
- // enrichedDuplicates.get(id) is already enriched — just report.
+ if (scmDb != null && checkClusterIdConsistency(conf)) {
+ try {
+ findOrphanAndDeletedButPresentContainers(conf, scanResult,
enrichedDuplicates);
+ } catch (IOException e) {
+ err().printf("SCM container consistency checks were skipped: %s%n",
e.getMessage());
+ }
+ } else if (scmDb == null) {
+ out().println("To identify orphan containers (wrt SCM) and containers
that are marked as DELETED in SCM but"
+ + " exist in the datanode's current directory, provide the SCM
database path using the --scm-db option."
+ );
+ }
printDuplicates(enrichedDuplicates);
printVolumeScanErrors(scanResult.getVolumeScanErrors());
return null;
}
- private void printDuplicates(Map<Long, List<ContainerDiskOccurrence>>
duplicates) {
- long totalDuplicateIds = duplicates.size();
- out().printf("Number of containers with duplicate container directories on
this DataNode: %d%n", totalDuplicateIds);
+ /**
+ * Validate CLI options before starting the on-disk DN scan.
+ * {@link #getDisplayLimit()} is also called from
+ * {@link #printContainerOccurrenceReport(String, Map)}, but validating here
fails fast
+ * before the DN volume scan and SCM DB lookup.
+ */
+ private void validateOptions() {
+ getDisplayLimit();
+ }
+
+ private int getDisplayLimit() {
+ if (count != null) {
+ if (count < 1) {
+ throw new IllegalArgumentException("Count must be an integer greater
than 0.");
+ }
+ return count;
+ }
+ return listOptions.getLimit();
+ }
+
+ private boolean displayAll() {
+ return count == null && listOptions.isAll();
+ }
+
+ private boolean checkClusterIdConsistency(OzoneConfiguration conf) {
+ File resolvedScmDb;
+ try {
+ resolvedScmDb = ScmContainerMetadataReader.resolveScmDbDirectory(scmDb);
+ } catch (IOException e) {
+ err().printf("SCM container consistency checks were skipped: %s%n",
e.getMessage());
+ return false;
+ }
+
+ String scmClusterId =
ScmContainerMetadataReader.readScmClusterId(resolvedScmDb);
+
+ if (scmClusterId == null) {
+ err().printf("Warning: could not determine the SCM cluster ID from the
VERSION file next to %s. "
+ + "Cluster ID comparison with DataNode volume cluster ID was
skipped. "
+ + "Verify --scm-db is from the same cluster as this
DataNode.%n", resolvedScmDb);
+ return true;
+ }
+
+ String dnClusterId = readFirstDnClusterId(conf);
+ if (dnClusterId == null) {
+ err().println("Warning: could not determine the DataNode cluster ID from
configured volumes.");
+ return true;
+ } else if (!dnClusterId.equals(scmClusterId)) {
+ err().printf("Warning: cluster ID mismatch. DataNode volume cluster ID
[%s]"
+ + " does not match SCM database cluster ID [%s] at %s."
+ + " Verify --scm-db is from the same cluster as this
DataNode.%n",
+ dnClusterId, scmClusterId, resolvedScmDb);
+ return false;
+ }
+ return true;
+ }
- if (totalDuplicateIds == 0) {
+ private String readFirstDnClusterId(OzoneConfiguration conf) {
+ for (String storageDir : HddsServerUtil.getDatanodeStorageDirs(conf)) {
+ try {
+ String volumeRoot =
StorageLocation.parse(storageDir).getUri().getPath();
+ File hddsRoot = new File(volumeRoot, HddsVolume.HDDS_VOLUME_DIR);
+ File versionFile = StorageVolumeUtil.getVersionFile(hddsRoot);
+ Properties props = DatanodeVersionFile.readFrom(versionFile);
+ if (!props.isEmpty()) {
+ return StorageVolumeUtil.getClusterID(props, versionFile, null);
+ }
+ } catch (IOException e) {
+ LOG.debug("Could not read cluster ID from volume {}: {}", storageDir,
e.getMessage());
+ }
+ }
+ return null;
+ }
+
+ private void findOrphanAndDeletedButPresentContainers(OzoneConfiguration
conf, ContainerScanResult scanResult,
+ Map<Long, List<ContainerDiskOccurrence>> enrichedDuplicates) throws
IOException {
+ Map<Long, List<ContainerDiskOccurrence>> enrichedOrphanContainers = new
HashMap<>();
+ Map<Long, List<ContainerDiskOccurrence>> enrichedDeletedButPresent = new
HashMap<>();
+
+ try (ScmContainerMetadataReader reader = new
ScmContainerMetadataReader(conf, scmDb)) {
+ Set<Long> containerIds = new HashSet<>(scanResult.getSingles().keySet());
+ containerIds.addAll(enrichedDuplicates.keySet());
+
+ for (long containerId : containerIds) {
+ Optional<ScmContainerMetadataReader.ScmContainerClassification>
classification = reader.classify(containerId);
+ if (!classification.isPresent()) {
+ continue;
+ }
+ List<ContainerDiskOccurrence> occurrences =
enrichedDuplicates.get(containerId);
+ if (occurrences == null) {
+ String path = scanResult.getSingles().get(containerId);
+ occurrences =
Collections.singletonList(ContainerDirectoryScanner.enrichOccurrence(containerId,
path));
+ }
+ if (classification.get() ==
ScmContainerMetadataReader.ScmContainerClassification.NOT_IN_SCM) {
+ enrichedOrphanContainers.put(containerId, occurrences);
+ } else {
+ enrichedDeletedButPresent.put(containerId, occurrences);
+ }
+ }
+ }
+
+ printContainerOccurrenceReport("Number of orphan containers(wrt SCM) on
this DataNode: %d%n",
+ enrichedOrphanContainers);
+ printContainerOccurrenceReport(
+ "Number of containers marked DELETED in SCM but present on disk on
this DataNode: %d%n",
+ enrichedDeletedButPresent);
+ }
+
+ private void printContainerOccurrenceReport(String countFormat,
+ Map<Long, List<ContainerDiskOccurrence>> containersById) {
+ long total = containersById.size();
+ out().printf(countFormat, total);
+ if (total == 0) {
return;
}
- if (totalDuplicateIds > count) {
- out().printf("Showing first %d:%n", count);
+ Stream<Map.Entry<Long, List<ContainerDiskOccurrence>>> stream =
+ containersById.entrySet().stream().sorted(Map.Entry.comparingByKey());
+ if (!displayAll()) {
+ int limit = getDisplayLimit();
+ if (total > limit) {
+ out().printf("Showing first %d:%n", limit);
+ }
+ stream = stream.limit(limit);
}
+ stream.forEach(entry -> printContainerEntry(entry.getKey(),
entry.getValue()));
+ }
- duplicates.entrySet().stream()
- .sorted(Map.Entry.comparingByKey())
- .limit(count)
- .forEach(entry -> {
- long containerId = entry.getKey();
- List<ContainerDiskOccurrence> occurrences = entry.getValue();
- out().printf("Container %d (%d occurrences):%n", containerId,
occurrences.size());
- for (ContainerDiskOccurrence o : occurrences) {
- out().printf(" path=%s%n", o.getContainerPath());
- if (o.isSizeKnown()) {
- out().printf(" status=%s size=%d bytes%n", o.getStatus(),
o.getSizeBytes());
- } else {
- out().printf(" status=%s size=unavailable (failed to compute
directory size)%n",
- o.getStatus());
- }
- out().println();
- }
- });
+ private void printContainerEntry(long containerId,
List<ContainerDiskOccurrence> occurrences) {
+ out().printf("Container %d (%d occurrence%s):%n",
+ containerId,
+ occurrences.size(),
+ occurrences.size() == 1 ? "" : "s");
+ for (ContainerDiskOccurrence occurrence : occurrences) {
+ out().printf(" path=%s%n", occurrence.getContainerPath());
+ if (occurrence.isSizeKnown()) {
+ out().printf(" status=%s size=%d bytes%n", occurrence.getStatus(),
occurrence.getSizeBytes());
+ } else {
+ out().printf(" status=%s size=unavailable (failed to compute
directory size)%n", occurrence.getStatus());
+ }
+ out().println();
+ }
+ }
+
+ private void printDuplicates(Map<Long, List<ContainerDiskOccurrence>>
duplicates) {
+ printContainerOccurrenceReport(
+ "Number of containers with duplicate container directories on this
DataNode: %d%n",
+ duplicates);
}
private void printVolumeScanErrors(List<String> volumeScanErrors) {
diff --git
a/hadoop-ozone/cli-debug/src/main/java/org/apache/hadoop/ozone/debug/datanode/container/analyze/ScmContainerMetadataReader.java
b/hadoop-ozone/cli-debug/src/main/java/org/apache/hadoop/ozone/debug/datanode/container/analyze/ScmContainerMetadataReader.java
new file mode 100644
index 00000000000..9ec01c3d69e
--- /dev/null
+++
b/hadoop-ozone/cli-debug/src/main/java/org/apache/hadoop/ozone/debug/datanode/container/analyze/ScmContainerMetadataReader.java
@@ -0,0 +1,155 @@
+/*
+ * 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.debug.datanode.container.analyze;
+
+import static org.apache.hadoop.hdds.scm.metadata.SCMDBDefinition.CONTAINERS;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Properties;
+import org.apache.hadoop.hdds.conf.ConfigurationSource;
+import org.apache.hadoop.hdds.scm.container.ContainerID;
+import org.apache.hadoop.hdds.scm.container.ContainerInfo;
+import org.apache.hadoop.hdds.scm.metadata.SCMDBDefinition;
+import org.apache.hadoop.hdds.utils.db.CodecException;
+import org.apache.hadoop.hdds.utils.db.DBStore;
+import org.apache.hadoop.hdds.utils.db.DBStoreBuilder;
+import org.apache.hadoop.hdds.utils.db.RocksDatabaseException;
+import org.apache.hadoop.hdds.utils.db.Table;
+import org.apache.hadoop.hdds.utils.db.cache.TableCache.CacheType;
+import org.apache.hadoop.ozone.OzoneConsts;
+import org.apache.hadoop.ozone.common.Storage;
+import org.apache.hadoop.ozone.container.common.helpers.DatanodeVersionFile;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Read-only lookup of container metadata from {@code scm.db}.
+ */
+public final class ScmContainerMetadataReader implements AutoCloseable {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(ScmContainerMetadataReader.class);
+ private final DBStore dbStore;
+ private final Table<ContainerID, ContainerInfo> containerTable;
+
+ public ScmContainerMetadataReader(ConfigurationSource conf, File scmDbPath)
+ throws IOException {
+ File scmDbDir = resolveScmDbDirectory(scmDbPath);
+ File parentDir = scmDbDir.getParentFile();
+ if (parentDir == null) {
+ throw new IOException("SCM database directory has no parent path: " +
scmDbDir);
+ }
+ try {
+ this.dbStore = DBStoreBuilder.newBuilder(conf, SCMDBDefinition.get(),
scmDbDir.getName(),
+ parentDir.toPath())
+ .setOpenReadOnly(true)
+ .build();
+ } catch (RocksDatabaseException e) {
+ throw new IOException("Failed to open SCM database at " + scmDbDir, e);
+ }
+ try {
+ this.containerTable = CONTAINERS.getTable(dbStore, CacheType.NO_CACHE);
+ } catch (RocksDatabaseException | CodecException e) {
+ dbStore.close();
+ throw new IOException("Failed to open scm.db containers column family at
" + scmDbDir, e);
+ }
+ }
+
+ /**
+ * Classify a container ID against scm.db {@code containers}.
+ *
+ * @return {@link Optional#empty()} when the container is present in SCM
with a
+ * non-DELETED lifecycle state
+ */
+ public Optional<ScmContainerClassification> classify(long containerId)
throws IOException {
+ try {
+ ContainerInfo info =
containerTable.get(ContainerID.valueOf(containerId));
+ if (info == null) {
+ return Optional.of(ScmContainerClassification.NOT_IN_SCM);
+ }
+ if (info.isDeleted()) {
+ return Optional.of(ScmContainerClassification.DELETED);
+ }
+ return Optional.empty();
+ } catch (RocksDatabaseException | CodecException e) {
+ throw new IOException("Failed to read container " + containerId + " from
scm.db", e);
+ }
+ }
+
+ /**
+ * Read the cluster ID from the SCM VERSION file adjacent to {@code
scmDbDir}.
+ *
+ * <p>The VERSION file is expected at
+ * {@code {scmDbDir.parent}/{@value
OzoneConsts#STORAGE_DIR}/current/VERSION}.
+ *
+ * @return the cluster ID string, or null if the file does not exist or
could not be read.
+ */
+ static String readScmClusterId(File scmDbDir) {
+ File parentDir = scmDbDir.getParentFile();
+ if (parentDir == null) {
+ return null;
+ }
+ File versionFile = new File(new File(new File(parentDir,
OzoneConsts.STORAGE_DIR),
+ Storage.STORAGE_DIR_CURRENT), Storage.STORAGE_FILE_VERSION);
+ if (!versionFile.exists()) {
+ return null;
+ }
+ try {
+ Properties props = DatanodeVersionFile.readFrom(versionFile);
+ return props.getProperty(OzoneConsts.CLUSTER_ID);
+ } catch (IOException e) {
+ LOG.debug("Could not read SCM cluster ID from {}: {}", versionFile,
e.getMessage());
+ return null;
+ }
+ }
+
+ static File resolveScmDbDirectory(File path) throws IOException {
+ Objects.requireNonNull(path, "scmDbPath");
+ File absolutePath = path.getAbsoluteFile();
+ File scmDbDir = absolutePath;
+ if (!OzoneConsts.SCM_DB_NAME.equals(absolutePath.getName())) {
+ File child = new File(absolutePath, OzoneConsts.SCM_DB_NAME);
+ if (child.isDirectory()) {
+ scmDbDir = child;
+ }
+ }
+ if (!scmDbDir.isDirectory()) {
+ throw new IOException("SCM database directory not found: " + path);
+ }
+ return scmDbDir;
+ }
+
+ @Override
+ public void close() {
+ if (dbStore != null) {
+ dbStore.close();
+ }
+ }
+
+ /**
+ * SCM-side classification for an on-disk container directory.
+ */
+ enum ScmContainerClassification {
+ /** No record for this container ID in scm.db {@code containers}. */
+ NOT_IN_SCM,
+ /** Record exists and {@link ContainerInfo} state is DELETED. */
+ DELETED
+ }
+}
diff --git
a/hadoop-ozone/cli-debug/src/test/java/org/apache/hadoop/ozone/debug/datanode/container/analyze/ContainerAnalyzeTestHelper.java
b/hadoop-ozone/cli-debug/src/test/java/org/apache/hadoop/ozone/debug/datanode/container/analyze/ContainerAnalyzeTestHelper.java
index c9d3e01483d..e5aab1e5c5f 100644
---
a/hadoop-ozone/cli-debug/src/test/java/org/apache/hadoop/ozone/debug/datanode/container/analyze/ContainerAnalyzeTestHelper.java
+++
b/hadoop-ozone/cli-debug/src/test/java/org/apache/hadoop/ozone/debug/datanode/container/analyze/ContainerAnalyzeTestHelper.java
@@ -21,9 +21,19 @@
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.Map;
import java.util.UUID;
import org.apache.hadoop.conf.StorageUnit;
+import org.apache.hadoop.hdds.client.RatisReplicationConfig;
import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
+import org.apache.hadoop.hdds.scm.container.ContainerID;
+import org.apache.hadoop.hdds.scm.container.ContainerInfo;
+import org.apache.hadoop.hdds.scm.metadata.SCMDBDefinition;
+import org.apache.hadoop.hdds.utils.db.DBStore;
+import org.apache.hadoop.hdds.utils.db.DBStoreBuilder;
+import org.apache.hadoop.hdds.utils.db.Table;
+import org.apache.hadoop.ozone.OzoneConsts;
import org.apache.hadoop.ozone.common.Storage;
import org.apache.hadoop.ozone.container.common.helpers.ContainerUtils;
import org.apache.hadoop.ozone.container.common.impl.ContainerDataYaml;
@@ -110,4 +120,31 @@ void corruptVersionFile(File volumeRoot) throws
IOException {
File versionFile = StorageVolumeUtil.getVersionFile(hddsRoot);
Files.write(versionFile.toPath(), new byte[0]);
}
+
+ /**
+ * Creates an offline {@code scm.db} with the given container states.
+ *
+ * @return path to the {@code scm.db} directory
+ */
+ File createScmDb(Map<Long, HddsProtos.LifeCycleState> containerStates)
throws IOException {
+ Path scmRoot = tempDir.resolve("scm-metadata");
+ Files.createDirectories(scmRoot);
+ DBStore dbStore = DBStoreBuilder.newBuilder(conf, SCMDBDefinition.get(),
OzoneConsts.SCM_DB_NAME, scmRoot).build();
+ try {
+ Table<ContainerID, ContainerInfo> containerTable =
SCMDBDefinition.CONTAINERS.getTable(dbStore);
+ for (Map.Entry<Long, HddsProtos.LifeCycleState> entry :
containerStates.entrySet()) {
+ long containerId = entry.getKey();
+ ContainerInfo containerInfo = new ContainerInfo.Builder()
+ .setContainerID(containerId)
+ .setState(entry.getValue())
+ .setOwner("test")
+
.setReplicationConfig(RatisReplicationConfig.getInstance(HddsProtos.ReplicationFactor.THREE))
+ .build();
+ containerTable.put(ContainerID.valueOf(containerId), containerInfo);
+ }
+ } finally {
+ dbStore.close();
+ }
+ return scmRoot.resolve(OzoneConsts.SCM_DB_NAME).toFile();
+ }
}
diff --git
a/hadoop-ozone/cli-debug/src/test/java/org/apache/hadoop/ozone/debug/datanode/container/analyze/TestAnalyzeSubcommand.java
b/hadoop-ozone/cli-debug/src/test/java/org/apache/hadoop/ozone/debug/datanode/container/analyze/TestAnalyzeSubcommand.java
index 0d3da9f45a7..54c780b897a 100644
---
a/hadoop-ozone/cli-debug/src/test/java/org/apache/hadoop/ozone/debug/datanode/container/analyze/TestAnalyzeSubcommand.java
+++
b/hadoop-ozone/cli-debug/src/test/java/org/apache/hadoop/ozone/debug/datanode/container/analyze/TestAnalyzeSubcommand.java
@@ -18,6 +18,7 @@
package org.apache.hadoop.ozone.debug.datanode.container.analyze;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.params.provider.Arguments.arguments;
import java.io.File;
import java.io.PrintWriter;
@@ -25,14 +26,21 @@
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.UUID;
+import java.util.stream.Stream;
import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
import org.apache.hadoop.hdds.scm.ScmConfigKeys;
import org.apache.hadoop.ozone.debug.OzoneDebug;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
import picocli.CommandLine;
/**
@@ -84,7 +92,7 @@ public void testAnalyzeRespectsCount() throws Exception {
}
executeAnalyze(volumeRoot1.getAbsolutePath() + "," +
volumeRoot2.getAbsolutePath(),
- "--count", "2");
+ "--length", "2");
String output = outWriter.toString();
assertThat(output).contains("Number of containers with duplicate container
directories on this DataNode: 3");
@@ -97,10 +105,10 @@ public void testAnalyzeRespectsCount() throws Exception {
@Test
public void testAnalyzeInvalidCount() {
- executeAnalyze(tempDir.toString(), "--count", "0");
+ executeAnalyze(tempDir.toString(), "--length", "0");
String combined = outWriter.toString() + errWriter.toString();
- assertThat(combined).contains("Count must be an integer greater than 0.");
+ assertThat(combined).contains("List length should be a positive number");
}
@Test
@@ -164,16 +172,182 @@ public void
testAnalyzeDuplicateValidAndInvalidEmptyFile() throws Exception {
assertDuplicateReport(volumeRoot1, volumeRoot2, containerId,
"INVALID_METADATA");
}
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("scmOrphanOrDeletedScenarios")
+ public void testAnalyzeScmOrphanOrDeletedSingleVolume(String scenarioName,
long containerId,
+ HddsProtos.LifeCycleState scmState, boolean metadataFilePresent, long
metadataContainerId, String expectedStatus)
+ throws Exception {
+ File volumeRoot = testHelper.formatVolume("volume0");
+ testHelper.createContainerDirectory(volumeRoot, containerId,
metadataFilePresent, metadataContainerId);
+
+ Map<Long, HddsProtos.LifeCycleState> scmContainers = new HashMap<>();
+ if (scmState != null) {
+ scmContainers.put(containerId, scmState);
+ }
+ File scmDb = testHelper.createScmDb(scmContainers);
+
+ executeAnalyze(volumeRoot.getAbsolutePath(), "--scm-db",
scmDb.getAbsolutePath());
+
+ String output = outWriter.toString();
+ assertScmCounts(output, scmState == null ? 1 : 0, scmState ==
HddsProtos.LifeCycleState.DELETED ? 1 : 0);
+ assertThat(output).contains("Container " + containerId + " (1
occurrence):");
+ assertOccurrenceStatus(output, volumeRoot, containerId, expectedStatus);
+ }
+
+ @ParameterizedTest(name = "{0}")
+ @MethodSource("scmOrphanOrDeletedScenarios")
+ public void testAnalyzeScmOrphanOrDeletedOnTwoVolumes(String scenarioName,
long containerId,
+ HddsProtos.LifeCycleState scmState, boolean metadataFilePresent, long
metadataContainerId, String expectedStatus)
+ throws Exception {
+ File volumeRoot1 = testHelper.formatVolume("volume0");
+ File volumeRoot2 = testHelper.formatVolume("volume1");
+ testHelper.createContainerDirectory(volumeRoot1, containerId,
metadataFilePresent, metadataContainerId);
+ testHelper.createContainerDirectory(volumeRoot2, containerId,
metadataFilePresent, metadataContainerId);
+
+ Map<Long, HddsProtos.LifeCycleState> scmContainers = new HashMap<>();
+ if (scmState != null) {
+ scmContainers.put(containerId, scmState);
+ }
+ File scmDb = testHelper.createScmDb(scmContainers);
+
+ executeAnalyze(volumeRoot1.getAbsolutePath() + "," +
volumeRoot2.getAbsolutePath(),
+ "--scm-db", scmDb.getAbsolutePath());
+
+ String output = outWriter.toString();
+ assertScmCounts(output, scmState == null ? 1 : 0, scmState ==
HddsProtos.LifeCycleState.DELETED ? 1 : 0);
+ assertThat(output).contains("Container " + containerId + " (2
occurrences):");
+ assertOccurrenceStatus(output, volumeRoot1, containerId, expectedStatus);
+ assertOccurrenceStatus(output, volumeRoot2, containerId, expectedStatus);
+ }
+
+ @Test
+ public void testAnalyzeScmOmitsHealthyContainer() throws Exception {
+ File volumeRoot = testHelper.formatVolume("volume0");
+ long containerId = 8020L;
+ testHelper.createContainerDirectory(volumeRoot, containerId, true,
containerId);
+
+ Map<Long, HddsProtos.LifeCycleState> scmContainers = new HashMap<>();
+ scmContainers.put(containerId, HddsProtos.LifeCycleState.CLOSED);
+ File scmDb = testHelper.createScmDb(scmContainers);
+
+ executeAnalyze(volumeRoot.getAbsolutePath(), "--scm-db",
scmDb.getAbsolutePath());
+
+ String output = outWriter.toString();
+ assertThat(output).contains("Number of orphan containers(wrt SCM) on this
DataNode: 0");
+ assertThat(output).contains(
+ "Number of containers marked DELETED in SCM but present on disk on
this DataNode: 0");
+ }
+
+ @Test
+ public void testAnalyzeScmMixedOrphanDeletedHealthy() throws Exception {
+ File volumeRoot = testHelper.formatVolume("volume0");
+ long orphanId = 8101L;
+ long deletedId = 8102L;
+ long healthyId = 8103L;
+ testHelper.createContainerDirectory(volumeRoot, orphanId, true, orphanId);
+ testHelper.createContainerDirectory(volumeRoot, deletedId, true,
deletedId);
+ testHelper.createContainerDirectory(volumeRoot, healthyId, true,
healthyId);
+
+ Map<Long, HddsProtos.LifeCycleState> scmContainers = new HashMap<>();
+ scmContainers.put(deletedId, HddsProtos.LifeCycleState.DELETED);
+ scmContainers.put(healthyId, HddsProtos.LifeCycleState.CLOSED);
+ File scmDb = testHelper.createScmDb(scmContainers);
+
+ executeAnalyze(volumeRoot.getAbsolutePath(), "--scm-db",
scmDb.getAbsolutePath());
+
+ String output = outWriter.toString();
+ assertThat(output).contains("Number of orphan containers(wrt SCM) on this
DataNode: 1");
+ assertThat(output).contains(
+ "Number of containers marked DELETED in SCM but present on disk on
this DataNode: 1");
+ assertThat(output).contains("Container " + orphanId + " (1 occurrence):");
+ assertOccurrenceStatus(output, volumeRoot, orphanId, "VALID");
+ assertThat(output).contains("Container " + deletedId + " (1 occurrence):");
+ assertOccurrenceStatus(output, volumeRoot, deletedId, "VALID");
+ assertThat(output).doesNotContain("Container " + healthyId);
+ }
+
+ @Test
+ public void testAnalyzeScmMixedOrphanDeletedDuplicate() throws Exception {
+ File volumeRoot1 = testHelper.formatVolume("volume0");
+ File volumeRoot2 = testHelper.formatVolume("volume1");
+ long orphanId = 8201L;
+ long deletedId = 8202L;
+ long duplicateId = 8203L;
+ testHelper.createContainerDirectory(volumeRoot1, orphanId, true, orphanId);
+ testHelper.createContainerDirectory(volumeRoot1, deletedId, true,
deletedId);
+ testHelper.createContainerDirectory(volumeRoot1, duplicateId, true,
duplicateId);
+ testHelper.createContainerDirectory(volumeRoot2, duplicateId, true,
duplicateId);
+
+ Map<Long, HddsProtos.LifeCycleState> scmContainers = new HashMap<>();
+ scmContainers.put(deletedId, HddsProtos.LifeCycleState.DELETED);
+ scmContainers.put(duplicateId, HddsProtos.LifeCycleState.CLOSED);
+ File scmDb = testHelper.createScmDb(scmContainers);
+
+ executeAnalyze(volumeRoot1.getAbsolutePath() + "," +
volumeRoot2.getAbsolutePath(),
+ "--scm-db", scmDb.getAbsolutePath());
+
+ String output = outWriter.toString();
+ assertThat(output).contains("Number of orphan containers(wrt SCM) on this
DataNode: 1");
+ assertThat(output).contains("Container " + orphanId + " (1 occurrence):");
+ assertOccurrenceStatus(output, volumeRoot1, orphanId, "VALID");
+ assertThat(output).contains(
+ "Number of containers marked DELETED in SCM but present on disk on
this DataNode: 1");
+ assertThat(output).contains("Container " + deletedId + " (1 occurrence):");
+ assertOccurrenceStatus(output, volumeRoot1, deletedId, "VALID");
+ assertThat(output).contains("Number of containers with duplicate container
directories on this DataNode: 1");
+ assertThat(output).contains("Container " + duplicateId + " (2
occurrences):");
+ assertOccurrenceStatus(output, volumeRoot1, duplicateId, "VALID");
+ assertOccurrenceStatus(output, volumeRoot2, duplicateId, "VALID");
+ }
+
+ @Test
+ public void testAnalyzeWithoutScmDb() throws Exception {
+ File volumeRoot = testHelper.formatVolume("volume0");
+ long containerId = 8301L;
+ testHelper.createContainerDirectory(volumeRoot, containerId, true,
containerId);
+
+ executeAnalyze(volumeRoot.getAbsolutePath());
+
+ String output = outWriter.toString();
+ assertThat(output).contains("provide the SCM database path using the
--scm-db option");
+ assertThat(output).doesNotContain("Number of orphan containers(wrt SCM) on
this DataNode:");
+ assertThat(output).doesNotContain(
+ "Number of containers marked DELETED in SCM but present on disk on
this DataNode:");
+ assertThat(output).contains("Number of containers with duplicate container
directories on this DataNode: 0");
+ }
+
+ private static Stream<Arguments> scmOrphanOrDeletedScenarios() {
+ return Stream.of(
+ arguments("orphan-valid", 8008L, null, true, 8008L, "VALID"),
+ arguments("deleted-but-present-valid", 8030L,
HddsProtos.LifeCycleState.DELETED, true, 8030L, "VALID"),
+ arguments("orphan-missing-metadata", 8401L, null, false, 8401L,
"MISSING_METADATA"),
+ arguments("deleted-but-present-missing-metadata", 8402L,
HddsProtos.LifeCycleState.DELETED, false, 8402L,
+ "MISSING_METADATA"),
+ arguments("orphan-invalid-metadata", 8403L, null, true, 9999L,
"INVALID_METADATA"),
+ arguments("deleted-but-present-invalid-metadata", 8404L,
HddsProtos.LifeCycleState.DELETED, true, 9999L,
+ "INVALID_METADATA"));
+ }
+
+ private void assertScmCounts(String output, int expectedOrphans, int
expectedDeleted) {
+ assertThat(output).contains(
+ "Number of orphan containers(wrt SCM) on this DataNode: " +
expectedOrphans);
+ assertThat(output).contains(
+ "Number of containers marked DELETED in SCM but present on disk on
this DataNode: " + expectedDeleted);
+ }
+
private void assertDuplicateReport(File volumeRoot1, File volumeRoot2, long
containerId,
String volume2ExpectedStatus) {
executeAnalyze(volumeRoot1.getAbsolutePath() + "," +
volumeRoot2.getAbsolutePath());
- String path1 = testHelper.containerPath(volumeRoot1, containerId);
- String path2 = testHelper.containerPath(volumeRoot2, containerId);
String output = outWriter.toString();
assertThat(output).contains("Container " + containerId + " (2
occurrences):");
- assertThat(output).contains("path=" + path1 + "\n status=" + "VALID");
- assertThat(output).contains("path=" + path2 + "\n status=" +
volume2ExpectedStatus);
+ assertOccurrenceStatus(output, volumeRoot1, containerId, "VALID");
+ assertOccurrenceStatus(output, volumeRoot2, containerId,
volume2ExpectedStatus);
+ }
+
+ private void assertOccurrenceStatus(String output, File volumeRoot, long
containerId, String expectedStatus) {
+ assertThat(output).contains(String.format("path=%s%n status=%s",
+ testHelper.containerPath(volumeRoot, containerId), expectedStatus));
}
private void executeAnalyze(String datanodeDirs, String... extraArgs) {
diff --git
a/hadoop-ozone/cli-debug/src/test/java/org/apache/hadoop/ozone/debug/datanode/container/analyze/TestScmContainerMetadataReader.java
b/hadoop-ozone/cli-debug/src/test/java/org/apache/hadoop/ozone/debug/datanode/container/analyze/TestScmContainerMetadataReader.java
new file mode 100644
index 00000000000..78878b1f2c7
--- /dev/null
+++
b/hadoop-ozone/cli-debug/src/test/java/org/apache/hadoop/ozone/debug/datanode/container/analyze/TestScmContainerMetadataReader.java
@@ -0,0 +1,100 @@
+/*
+ * 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.debug.datanode.container.analyze;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+/**
+ * Unit tests for {@link ScmContainerMetadataReader}.
+ */
+public class TestScmContainerMetadataReader {
+
+ @TempDir
+ private Path tempDir;
+
+ private OzoneConfiguration conf;
+ private ContainerAnalyzeTestHelper testHelper;
+
+ @BeforeEach
+ public void setup() {
+ conf = new OzoneConfiguration();
+ testHelper = new ContainerAnalyzeTestHelper(tempDir, conf,
+ UUID.randomUUID().toString(), UUID.randomUUID().toString());
+ }
+
+ @Test
+ public void testClassifyNotInScm() throws Exception {
+ File scmDb = testHelper.createScmDb(Collections.emptyMap());
+ try (ScmContainerMetadataReader reader = new
ScmContainerMetadataReader(conf, scmDb)) {
+ Optional<ScmContainerMetadataReader.ScmContainerClassification> result =
reader.classify(1001L);
+ assertTrue(result.isPresent());
+
assertEquals(ScmContainerMetadataReader.ScmContainerClassification.NOT_IN_SCM,
result.get());
+ }
+ }
+
+ @Test
+ public void testClassifyDeleted() throws Exception {
+ Map<Long, HddsProtos.LifeCycleState> containers = new HashMap<>();
+ containers.put(1002L, HddsProtos.LifeCycleState.DELETED);
+ File scmDb = testHelper.createScmDb(containers);
+
+ try (ScmContainerMetadataReader reader = new
ScmContainerMetadataReader(conf, scmDb.getParentFile())) {
+ Optional<ScmContainerMetadataReader.ScmContainerClassification> result =
reader.classify(1002L);
+ assertTrue(result.isPresent());
+
assertEquals(ScmContainerMetadataReader.ScmContainerClassification.DELETED,
result.get());
+ }
+ }
+
+ @Test
+ public void testClassifyOmitOther() throws Exception {
+ Map<Long, HddsProtos.LifeCycleState> containers = new HashMap<>();
+ containers.put(1003L, HddsProtos.LifeCycleState.CLOSED);
+ containers.put(1004L, HddsProtos.LifeCycleState.OPEN);
+ File scmDb = testHelper.createScmDb(containers);
+
+ try (ScmContainerMetadataReader reader = new
ScmContainerMetadataReader(conf, scmDb)) {
+ assertFalse(reader.classify(1003L).isPresent());
+ assertFalse(reader.classify(1004L).isPresent());
+ }
+ }
+
+ @Test
+ public void testResolveScmDbDirectoryReturnsAbsolutePathWithParent() throws
Exception {
+ File scmDb = testHelper.createScmDb(Collections.emptyMap());
+ File resolved = ScmContainerMetadataReader.resolveScmDbDirectory(scmDb);
+ assertTrue(resolved.isAbsolute());
+ assertNotNull(resolved.getParentFile());
+ assertEquals(scmDb.getAbsolutePath(), resolved.getAbsolutePath());
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]