diff --git a/hadoop-hdds/framework/src/main/resources/webapps/static/ozone.js b/hadoop-hdds/framework/src/main/resources/webapps/static/ozone.js index aac641e625e1..2835c3633e5e 100644 --- a/hadoop-hdds/framework/src/main/resources/webapps/static/ozone.js +++ b/hadoop-hdds/framework/src/main/resources/webapps/static/ozone.js @@ -256,6 +256,8 @@ ioLinkHref: '@', scanner: '<', scannerLinkHref: '@', + snapshot: '@', + snapshotLinkHref: '@' }, templateUrl: 'static/templates/menu.html', controller: function($http) { diff --git a/hadoop-hdds/framework/src/main/resources/webapps/static/templates/menu.html b/hadoop-hdds/framework/src/main/resources/webapps/static/templates/menu.html index 9a14f356d7a4..1963a6543835 100644 --- a/hadoop-hdds/framework/src/main/resources/webapps/static/templates/menu.html +++ b/hadoop-hdds/framework/src/main/resources/webapps/static/templates/menu.html @@ -58,5 +58,6 @@
  • IO Status
  • Data Scanner
  • +
  • Ozone Snapshot
  • diff --git a/hadoop-hdds/server-scm/src/main/resources/webapps/scm/scm-overview.html b/hadoop-hdds/server-scm/src/main/resources/webapps/scm/scm-overview.html index 7bfe405850e2..bb2f25a1c325 100644 --- a/hadoop-hdds/server-scm/src/main/resources/webapps/scm/scm-overview.html +++ b/hadoop-hdds/server-scm/src/main/resources/webapps/scm/scm-overview.html @@ -391,4 +391,4 @@

    Safemode rules statuses

    {{typestat.value[1]}} - \ No newline at end of file + diff --git a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/SnapshotDiffJob.java b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/SnapshotDiffJob.java index 1e5da689f4bb..714a54cbd9b2 100644 --- a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/SnapshotDiffJob.java +++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/helpers/SnapshotDiffJob.java @@ -94,7 +94,7 @@ public SnapshotDiffJob(long creationTime, this.largestEntryKey = largestEntryKey; } - public static Codec getCodec() { + public static Codec codec() { return CODEC; } @@ -228,10 +228,10 @@ public String toString() { sb.append(", reason: ").append(reason); } if (status.equals(JobStatus.IN_PROGRESS) && subStatus != null) { - sb.append(", subStatus: ").append(status); + sb.append(", subStatus: ").append(subStatus); if (subStatus.equals(SubStatus.OBJECT_ID_MAP_GEN_FSO) || subStatus.equals(SubStatus.OBJECT_ID_MAP_GEN_OBS)) { - sb.append(String.format(", keysProcessedPercent: %.2f", keysProcessedPct)); + sb.append(String.format(", keysProcessedPct: %.2f", keysProcessedPct)); } } return sb.toString(); diff --git a/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmSnapshotDiffJobCodec.java b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmSnapshotDiffJobCodec.java index b24d225d0cc5..d22be5f9090e 100644 --- a/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmSnapshotDiffJobCodec.java +++ b/hadoop-ozone/common/src/test/java/org/apache/hadoop/ozone/om/helpers/TestOmSnapshotDiffJobCodec.java @@ -30,7 +30,7 @@ public class TestOmSnapshotDiffJobCodec { private final OldSnapshotDiffJobCodecForTesting oldCodec = new OldSnapshotDiffJobCodecForTesting(); - private final Codec newCodec = SnapshotDiffJob.getCodec(); + private final Codec newCodec = SnapshotDiffJob.codec(); @Test public void testOldJsonSerializedDataCanBeReadByNewCodec() throws Exception { diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmSnapshotManager.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmSnapshotManager.java index 56603a9f207b..1ee53e076fcc 100644 --- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmSnapshotManager.java +++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/OmSnapshotManager.java @@ -425,7 +425,7 @@ private static CodecRegistry createCodecRegistryForSnapDiff() { // DiffReportEntry codec for Diff Report. registry.addCodec(SnapshotDiffReportOzone.DiffReportEntry.class, SnapshotDiffReportOzone.getDiffReportEntryCodec()); - registry.addCodec(SnapshotDiffJob.class, SnapshotDiffJob.getCodec()); + registry.addCodec(SnapshotDiffJob.class, SnapshotDiffJob.codec()); return registry.build(); } diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/snapshot/SnapshotDiffManager.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/snapshot/SnapshotDiffManager.java index 9dbf546f18d1..c1c61e74226c 100644 --- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/snapshot/SnapshotDiffManager.java +++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/snapshot/SnapshotDiffManager.java @@ -95,9 +95,12 @@ import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.stream.Collectors; +import javax.management.ObjectName; import org.apache.commons.io.file.PathUtils; import org.apache.commons.lang3.tuple.Pair; +import org.apache.hadoop.hdds.HddsUtils; import org.apache.hadoop.hdds.StringUtils; +import org.apache.hadoop.hdds.annotation.InterfaceAudience; import org.apache.hadoop.hdds.conf.OzoneConfiguration; import org.apache.hadoop.hdds.utils.NativeLibraryNotLoadedException; import org.apache.hadoop.hdds.utils.db.CodecRegistry; @@ -111,6 +114,7 @@ import org.apache.hadoop.hdds.utils.db.managed.ManagedRocksDB; import org.apache.hadoop.hdfs.protocol.SnapshotDiffReport.DiffReportEntry; import org.apache.hadoop.hdfs.protocol.SnapshotDiffReport.DiffType; +import org.apache.hadoop.metrics2.util.MBeans; import org.apache.hadoop.ozone.OFSPath; import org.apache.hadoop.ozone.OzoneConsts; import org.apache.hadoop.ozone.om.OMMetadataManager; @@ -147,7 +151,8 @@ /** * Class to generate snapshot diff. */ -public class SnapshotDiffManager implements AutoCloseable { +@InterfaceAudience.Private +public class SnapshotDiffManager implements AutoCloseable, SnapshotDiffManagerMXBean { private static final Logger LOG = LoggerFactory.getLogger(SnapshotDiffManager.class); private static final Map DIFF_TYPE_STRING_MAP = @@ -177,6 +182,7 @@ public class SnapshotDiffManager implements AutoCloseable { */ private final PersistentMap snapDiffJobTable; private final ExecutorService snapDiffExecutor; + private ObjectName snapshotDiffManagerBeanName; /** * Directory to keep hardlinks of SST files for a snapDiff job temporarily. @@ -280,6 +286,7 @@ public SnapshotDiffManager(ManagedRocksDB db, // When we build snapDiff HA aware, we will revisit this. // Details: https://github.com/apache/ozone/pull/4438#discussion_r1149788226 this.loadJobsOnStartUp(); + this.registerMXBean(); } @VisibleForTesting @@ -1514,8 +1521,34 @@ void loadJobsOnStartUp() { } } + @Override + public List getSnapshotDiffJobs() { + List jobs = new ArrayList<>(); + try (ClosableIterator> iterator = + snapDiffJobTable.iterator()) { + while (iterator.hasNext()) { + jobs.add(iterator.next().getValue()); + } + } + return jobs; + } + + private void registerMXBean() { + this.snapshotDiffManagerBeanName = HddsUtils.registerWithJmxProperties( + "OzoneManager", "SnapshotDiffManager", + Collections.emptyMap(), this); + } + + private void unregisterMXBean() { + if (this.snapshotDiffManagerBeanName != null) { + MBeans.unregister(this.snapshotDiffManagerBeanName); + this.snapshotDiffManagerBeanName = null; + } + } + @Override public void close() { + unregisterMXBean(); if (snapDiffExecutor != null) { closeExecutorService(snapDiffExecutor, "SnapDiffExecutor"); } diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/snapshot/SnapshotDiffManagerMXBean.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/snapshot/SnapshotDiffManagerMXBean.java new file mode 100644 index 000000000000..c46689e3cb9c --- /dev/null +++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/snapshot/SnapshotDiffManagerMXBean.java @@ -0,0 +1,34 @@ +/* + * 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; + +import java.util.List; +import org.apache.hadoop.hdds.annotation.InterfaceAudience; +import org.apache.hadoop.ozone.om.helpers.SnapshotDiffJob; + +/** + * JMX interface for SnapshotDiffManager. + */ +@InterfaceAudience.Private +public interface SnapshotDiffManagerMXBean { + /** + * Returns all snapshot diff jobs. + * @return list of snapshot diff jobs + */ + List getSnapshotDiffJobs(); +} diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/snapshot/db/SnapshotDiffDBDefinition.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/snapshot/db/SnapshotDiffDBDefinition.java index 1add663c7f1c..bb09425a7df6 100644 --- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/snapshot/db/SnapshotDiffDBDefinition.java +++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/snapshot/db/SnapshotDiffDBDefinition.java @@ -51,7 +51,7 @@ public final class SnapshotDiffDBDefinition extends DBDefinition.WithMap { public static final String SNAP_DIFF_JOB_TABLE_NAME = "snap-diff-job-table"; public static final DBColumnFamilyDefinition SNAP_DIFF_JOB_TABLE_DEF - = new DBColumnFamilyDefinition<>(SNAP_DIFF_JOB_TABLE_NAME, StringCodec.get(), SnapshotDiffJob.getCodec()); + = new DBColumnFamilyDefinition<>(SNAP_DIFF_JOB_TABLE_NAME, StringCodec.get(), SnapshotDiffJob.codec()); /** * Global table to keep the diff report. Each key is prefixed by the jobId * to improve look up and clean up. JobId comes from snap-diff-job-table. diff --git a/hadoop-ozone/ozone-manager/src/main/resources/webapps/ozoneManager/index.html b/hadoop-ozone/ozone-manager/src/main/resources/webapps/ozoneManager/index.html index 54accf457f34..153000c2c917 100644 --- a/hadoop-ozone/ozone-manager/src/main/resources/webapps/ozoneManager/index.html +++ b/hadoop-ozone/ozone-manager/src/main/resources/webapps/ozoneManager/index.html @@ -49,7 +49,9 @@ Ozone Manager + metrics="{ 'OM metrics' : '#!/metrics/ozoneManager', 'Rpc metrics' : '#!/metrics/rpc'}" + snapshot="true" + snapshot-link-href="#!/snapshots"> diff --git a/hadoop-ozone/ozone-manager/src/main/resources/webapps/ozoneManager/om-snapshots.html b/hadoop-ozone/ozone-manager/src/main/resources/webapps/ozoneManager/om-snapshots.html new file mode 100644 index 000000000000..6884f5a04c1f --- /dev/null +++ b/hadoop-ozone/ozone-manager/src/main/resources/webapps/ozoneManager/om-snapshots.html @@ -0,0 +1,165 @@ + +

    Usage Statistics

    + + + + + + + + + + + + + + + + + + + + + +
    MetricValue
    Number of Active Snapshots{{$ctrl.snapshotUsageMetrics.NumSnapshotActive}}
    Number of Deleted Snapshots{{$ctrl.snapshotUsageMetrics.NumSnapshotDeleted}}
    Snapshot Cache Size{{$ctrl.snapshotUsageMetrics.NumSnapshotCacheSize}}
    + +
    + +

    Snapshot Diff Jobs

    + +
    +
    + + +
    +
    + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + Job ID + + Status + + Volume + + Bucket + + From Snapshot + + To Snapshot + + Progress + + Total Diff Entries + + Creation Time +
    {{job.jobId}} + {{job.status}} +
    + Reason: {{job.reason}} +
    +
    + Sub-status: {{job.subStatus}} +
    +
    {{job.volume}}{{job.bucket}}{{job.fromSnapshot}}{{job.toSnapshot}} + Completed + {{job.keysProcessedPct | number:2}}% + {{job.totalDiffEntries}}{{job.creationTime | date:'yyyy-MM-dd HH:mm:ss'}}
    + +
    +
    + + + of {{lastIndex}}. + + Showing {{getCurrentPageFirstItemIndex()}} to {{getCurrentPageLastItemIndex()}} of the total {{totalItems}} entries. + +
    +
    + +
    +
    + +
    + +

    Snapshot Internal Metrics

    + + + + + + + + + + + + + + +
    Metric NameValue
    {{metric.key}}{{metric.value}}
    diff --git a/hadoop-ozone/ozone-manager/src/main/resources/webapps/ozoneManager/ozoneManager.js b/hadoop-ozone/ozone-manager/src/main/resources/webapps/ozoneManager/ozoneManager.js index 8269b6df0fbb..e98d3f7ba3a7 100644 --- a/hadoop-ozone/ozone-manager/src/main/resources/webapps/ozoneManager/ozoneManager.js +++ b/hadoop-ozone/ozone-manager/src/main/resources/webapps/ozoneManager/ozoneManager.js @@ -27,8 +27,110 @@ $routeProvider .when("/metrics/ozoneManager", { template: "" + }) + .when("/snapshots", { + template: "" }); }); + angular.module('ozoneManager').component('omSnapshots', { + templateUrl: 'om-snapshots.html', + controller: function ($http, $scope) { + var ctrl = this; + ctrl.snapshotMetrics = []; + ctrl.snapshotDiffJobs = []; + ctrl.snapshotUsageMetrics = { + 'NumSnapshotActive': 0, + 'NumSnapshotDeleted': 0, + 'NumSnapshotCacheSize': 0 + }; + $scope.reverse = false; + $scope.columnName = "jobId"; + let snapDiffJobsCopy = []; + $scope.RecordsToDisplay = "10"; + $scope.currentPage = 1; + $scope.lastIndex = 0; + + $http.get("jmx?qry=Hadoop:service=OzoneManager,name=OMMetrics") + .then(function (result) { + if (result.data.beans && result.data.beans.length > 0) { + var metrics = result.data.beans[0]; + ctrl.snapshotUsageMetrics.NumSnapshotActive = metrics.NumSnapshotActive || 0; + ctrl.snapshotUsageMetrics.NumSnapshotDeleted = metrics.NumSnapshotDeleted || 0; + ctrl.snapshotUsageMetrics.NumSnapshotCacheSize = metrics.NumSnapshotCacheSize || 0; + } + }); + + $http.get("jmx?qry=Hadoop:service=OzoneManager,name=OmSnapshotInternalMetrics") + .then(function (result) { + if (result.data.beans && result.data.beans.length > 0) { + var metrics = result.data.beans[0]; + for (var key in metrics) { + if (!isIgnoredJmxKeys(key)) { + ctrl.snapshotMetrics.push({key: key, value: metrics[key]}); + } + } + } + }); + + $http.get("jmx?qry=Hadoop:service=OzoneManager,name=SnapshotDiffManager") + .then(function (result) { + if (result.data.beans && result.data.beans.length > 0) { + snapDiffJobsCopy = result.data.beans[0].SnapshotDiffJobs; + $scope.totalItems = snapDiffJobsCopy.length; + $scope.lastIndex = Math.ceil(snapDiffJobsCopy.length / $scope.RecordsToDisplay); + ctrl.snapshotDiffJobs = snapDiffJobsCopy.slice(0, $scope.RecordsToDisplay); + } + }); + + /*if option is 'All' display all records else display specified record on page*/ + $scope.UpdateRecordsToShow = () => { + if($scope.RecordsToDisplay == 'All') { + $scope.lastIndex = 1; + ctrl.snapshotDiffJobs = snapDiffJobsCopy; + } else { + $scope.lastIndex = Math.ceil(snapDiffJobsCopy.length / $scope.RecordsToDisplay); + ctrl.snapshotDiffJobs = snapDiffJobsCopy.slice(0, $scope.RecordsToDisplay); + } + $scope.currentPage = 1; + } + /* Page Slicing logic */ + $scope.handlePagination = (pageIndex, isDisabled) => { + if(!isDisabled && $scope.RecordsToDisplay != 'All') { + pageIndex = parseInt(pageIndex); + let startIndex = 0, endIndex = 0; + $scope.currentPage = pageIndex; + startIndex = ($scope.currentPage - 1) * parseInt($scope.RecordsToDisplay); + endIndex = startIndex + parseInt($scope.RecordsToDisplay); + ctrl.snapshotDiffJobs = snapDiffJobsCopy.slice(startIndex, endIndex); + } + } + /*column sort logic*/ + $scope.columnSort = (colName) => { + $scope.columnName = colName; + $scope.reverse = !$scope.reverse; + } + /*show page*/ + $scope.getPagesArray = function () { + return Array.from({ length: $scope.lastIndex }, (_, index) => index + 1); + }; + /*show last item index*/ + $scope.getCurrentPageLastItemIndex = () => { + if ($scope.RecordsToDisplay == 'All') { + return $scope.totalItems; + } + + let endIndex = $scope.currentPage * parseInt($scope.RecordsToDisplay); + return Math.min(endIndex, $scope.totalItems); + } + /*show first item index*/ + $scope.getCurrentPageFirstItemIndex = () => { + if ($scope.RecordsToDisplay == 'All') { + return 1; + } + return ($scope.currentPage - 1) * $scope.RecordsToDisplay + 1; + } + } + }); angular.module('ozoneManager').component('omMetrics', { templateUrl: 'om-metrics.html', controller: function ($http) { diff --git a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/service/TestSnapshotDiffCleanupService.java b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/service/TestSnapshotDiffCleanupService.java index 2dc9329601a4..25947fae6454 100644 --- a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/service/TestSnapshotDiffCleanupService.java +++ b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/service/TestSnapshotDiffCleanupService.java @@ -169,7 +169,7 @@ public void init() throws RocksDBException, IOException { // DiffReportEntry codec for Diff Report. b.addCodec(SnapshotDiffReportOzone.DiffReportEntry.class, SnapshotDiffReportOzone.getDiffReportEntryCodec()); - b.addCodec(SnapshotDiffJob.class, SnapshotDiffJob.getCodec()); + b.addCodec(SnapshotDiffJob.class, SnapshotDiffJob.codec()); codecRegistry = b.build(); emptyReportEntry = codecRegistry.asRawData("{}"); diff --git a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/snapshot/TestSnapshotDiffManager.java b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/snapshot/TestSnapshotDiffManager.java index 6327b712dee3..4da22a90ed85 100644 --- a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/snapshot/TestSnapshotDiffManager.java +++ b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/snapshot/TestSnapshotDiffManager.java @@ -220,7 +220,7 @@ public class TestSnapshotDiffManager { public static void initCodecRegistry() { codecRegistry = CodecRegistry.newBuilder() .addCodec(DiffReportEntry.class, getDiffReportEntryCodec()) - .addCodec(SnapshotDiffJob.class, SnapshotDiffJob.getCodec()) + .addCodec(SnapshotDiffJob.class, SnapshotDiffJob.codec()) .build(); } @@ -1365,4 +1365,21 @@ public void testGetSnapshotDiffReportJob() throws Exception { } } } + + @Test + public void testGetSnapshotDiffJobs() throws Exception { + // Initially no jobs + List jobs = snapshotDiffManager.getSnapshotDiffJobs(); + assertEquals(0, jobs.size()); + + // Submit a job + snapshotDiffManager.getSnapshotDiffReport(VOLUME_NAME, BUCKET_NAME, + snapshotNames.get(0), snapshotNames.get(1), + "", 100, false, false); + + jobs = snapshotDiffManager.getSnapshotDiffJobs(); + assertEquals(1, jobs.size()); + assertEquals(snapshotNames.get(0), jobs.get(0).getFromSnapshot()); + assertEquals(snapshotNames.get(1), jobs.get(0).getToSnapshot()); + } } diff --git a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/snapshot/TestSnapshotDiffManagerMXBean.java b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/snapshot/TestSnapshotDiffManagerMXBean.java new file mode 100644 index 000000000000..b9e3419f53c7 --- /dev/null +++ b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/snapshot/TestSnapshotDiffManagerMXBean.java @@ -0,0 +1,153 @@ +/* + * 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; + +import static org.apache.hadoop.hdds.utils.db.DBStoreBuilder.DEFAULT_COLUMN_FAMILY_NAME; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.lang.management.ManagementFactory; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import javax.management.MBeanServer; +import javax.management.ObjectName; +import javax.management.openmbean.CompositeData; +import org.apache.hadoop.hdds.conf.OzoneConfiguration; +import org.apache.hadoop.hdds.utils.db.CodecRegistry; +import org.apache.hadoop.hdds.utils.db.managed.ManagedColumnFamilyOptions; +import org.apache.hadoop.hdds.utils.db.managed.ManagedDBOptions; +import org.apache.hadoop.hdds.utils.db.managed.ManagedRocksDB; +import org.apache.hadoop.ozone.om.OMMetadataManager; +import org.apache.hadoop.ozone.om.OMMetrics; +import org.apache.hadoop.ozone.om.OmSnapshotManager; +import org.apache.hadoop.ozone.om.OzoneManager; +import org.apache.hadoop.ozone.om.helpers.SnapshotDiffJob; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.rocksdb.ColumnFamilyDescriptor; +import org.rocksdb.ColumnFamilyHandle; +import org.rocksdb.RocksDBException; + +/** + * Tests for SnapshotDiffManagerMXBean registration. + */ +public class TestSnapshotDiffManagerMXBean { + + private SnapshotDiffManager snapshotDiffManager; + private ManagedRocksDB db; + private PersistentMap snapDiffJobTable; + private MBeanServer mbs; + + @BeforeEach + public void setUp(@TempDir Path tempDir) throws IOException, RocksDBException { + OzoneConfiguration conf = new OzoneConfiguration(); + ManagedDBOptions dbOptions = new ManagedDBOptions(); + dbOptions.setCreateIfMissing(true); + ManagedColumnFamilyOptions columnFamilyOptions = new ManagedColumnFamilyOptions(); + + List columnFamilyDescriptors = + Collections.singletonList(new ColumnFamilyDescriptor( + DEFAULT_COLUMN_FAMILY_NAME.getBytes(StandardCharsets.UTF_8), + columnFamilyOptions)); + + List columnFamilyHandles = new ArrayList<>(); + + db = ManagedRocksDB.open(dbOptions, tempDir.toAbsolutePath().toString(), + columnFamilyDescriptors, columnFamilyHandles); + + CodecRegistry codecRegistry = CodecRegistry.newBuilder() + .addCodec(SnapshotDiffJob.class, SnapshotDiffJob.codec()) + .build(); + + ColumnFamilyHandle jobCFH = db.get().createColumnFamily( + new ColumnFamilyDescriptor("jobTable".getBytes(StandardCharsets.UTF_8), + columnFamilyOptions)); + ColumnFamilyHandle reportCFH = db.get().createColumnFamily( + new ColumnFamilyDescriptor("reportTable".getBytes(StandardCharsets.UTF_8), + columnFamilyOptions)); + + snapDiffJobTable = new RocksDbPersistentMap<>(db, jobCFH, + codecRegistry, String.class, SnapshotDiffJob.class); + + OzoneManager ozoneManager = mock(OzoneManager.class); + when(ozoneManager.getConfiguration()).thenReturn(conf); + when(ozoneManager.getMetrics()).thenReturn(mock(OMMetrics.class)); + OMMetadataManager omMetadataManager = mock(OMMetadataManager.class); + org.apache.hadoop.hdds.utils.db.RDBStore rdbStore = + mock(org.apache.hadoop.hdds.utils.db.RDBStore.class); + when(rdbStore.getSnapshotMetadataDir()) + .thenReturn(tempDir.toAbsolutePath().toString()); + when(omMetadataManager.getStore()).thenReturn(rdbStore); + when(ozoneManager.getMetadataManager()).thenReturn(omMetadataManager); + when(ozoneManager.getOmSnapshotManager()).thenReturn(mock(OmSnapshotManager.class)); + + snapshotDiffManager = new SnapshotDiffManager(db, ozoneManager, + jobCFH, reportCFH, columnFamilyOptions, codecRegistry); + + mbs = ManagementFactory.getPlatformMBeanServer(); + } + + @AfterEach + public void tearDown() { + if (snapshotDiffManager != null) { + snapshotDiffManager.close(); + } + if (db != null) { + db.close(); + } + } + + @Test + public void testMXBeanRegistration() throws Exception { + ObjectName name = new ObjectName( + "Hadoop:service=OzoneManager,name=SnapshotDiffManager"); + assertTrue(mbs.isRegistered(name), "SnapshotDiffManager MBean should be registered"); + + // Initially 0 jobs + Object jobsAttr = mbs.getAttribute(name, "SnapshotDiffJobs"); + assertTrue(jobsAttr instanceof CompositeData[]); + assertEquals(0, ((CompositeData[]) jobsAttr).length); + + // Add a job directly to the table + SnapshotDiffJob job = new SnapshotDiffJob(System.currentTimeMillis(), "job-1", + org.apache.hadoop.ozone.snapshot.SnapshotDiffResponse.JobStatus.QUEUED, + "vol", "bucket", "snap1", "snap2", + false, false, 0, + org.apache.hadoop.ozone.snapshot.SnapshotDiffResponse.SubStatus.OBJECT_ID_MAP_GEN_FSO, + 0, null); + snapDiffJobTable.put("job-1", job); + + // Verify MXBean method returns the job via JMX + jobsAttr = mbs.getAttribute(name, "SnapshotDiffJobs"); + assertTrue(jobsAttr instanceof CompositeData[]); + CompositeData[] jobs = (CompositeData[]) jobsAttr; + assertEquals(1, jobs.length); + assertEquals("job-1", jobs[0].get("jobId")); + assertEquals("snap1", jobs[0].get("fromSnapshot")); + assertEquals("snap2", jobs[0].get("toSnapshot")); + assertEquals("OBJECT_ID_MAP_GEN_FSO", jobs[0].get("subStatus")); + } +}