diff --git a/.asf.yaml b/.asf.yaml index 9edce0b5eb2..94c18f0c581 100644 --- a/.asf.yaml +++ b/.asf.yaml @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. github: - description: "Scalable, redundant, and distributed object store for Apache Hadoop" + description: "Scalable, reliable, distributed storage system optimized for data analytics and object store workloads." homepage: https://ozone.apache.org labels: - hadoop diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a8854aa6971..dff554c18e1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ on: env: FAIL_FAST: ${{ github.event_name == 'pull_request' }} # Minimum required Java version for running Ozone is defined in pom.xml (javac.version). - TEST_JAVA_VERSION: 17 # JDK version used by CI build and tests; should match the JDK version in apache/ozone-runner image + TEST_JAVA_VERSION: 21 # JDK version used by CI build and tests; should match the JDK version in apache/ozone-runner image MAVEN_OPTS: -Dhttp.keepAlive=false -Dmaven.wagon.http.pool=false -Dmaven.wagon.http.retryHandler.class=standard -Dmaven.wagon.http.retryHandler.count=3 HADOOP_IMAGE: ghcr.io/apache/hadoop OZONE_IMAGE: ghcr.io/apache/ozone @@ -257,7 +257,7 @@ jobs: key: maven-repo-${{ hashFiles('**/pom.xml') }} restore-keys: | maven-repo- - if: ${{ !contains('author,bats,docs', matrix.check) }} + if: ${{ !contains('author,bats', matrix.check) }} - name: Download Ratis repo if: ${{ inputs.ratis_args != '' }} uses: actions/download-artifact@v4 @@ -343,6 +343,15 @@ jobs: uses: actions/checkout@v4 with: ref: ${{ needs.build-info.outputs.sha }} + - name: Cache for maven dependencies + uses: actions/cache/restore@v4 + with: + path: | + ~/.m2/repository/*/*/* + !~/.m2/repository/org/apache/ozone + key: maven-repo-${{ hashFiles('**/pom.xml') }} + restore-keys: | + maven-repo- - name: Download compiled Ozone binaries uses: actions/download-artifact@v4 with: @@ -486,6 +495,15 @@ jobs: uses: actions/checkout@v4 with: ref: ${{ needs.build-info.outputs.sha }} + - name: Cache for maven dependencies + uses: actions/cache/restore@v4 + with: + path: | + ~/.m2/repository/*/*/* + !~/.m2/repository/org/apache/ozone + key: maven-repo-${{ hashFiles('**/pom.xml') }} + restore-keys: | + maven-repo- - name: Download compiled Ozone binaries uses: actions/download-artifact@v4 with: @@ -530,6 +548,15 @@ jobs: uses: actions/checkout@v4 with: ref: ${{ needs.build-info.outputs.sha }} + - name: Cache for maven dependencies + uses: actions/cache/restore@v4 + with: + path: | + ~/.m2/repository/*/*/* + !~/.m2/repository/org/apache/ozone + key: maven-repo-${{ hashFiles('**/pom.xml') }} + restore-keys: | + maven-repo- - name: Download compiled Ozone binaries uses: actions/download-artifact@v4 with: diff --git a/.github/workflows/populate-cache.yml b/.github/workflows/populate-cache.yml index 5ab9f63118e..94f2ccfe52d 100644 --- a/.github/workflows/populate-cache.yml +++ b/.github/workflows/populate-cache.yml @@ -73,7 +73,7 @@ jobs: - name: Fetch dependencies if: steps.restore-cache.outputs.cache-hit != 'true' - run: mvn --batch-mode --no-transfer-progress --show-version -Pgo-offline -Pdist clean verify + run: mvn --batch-mode --no-transfer-progress --show-version -Pgo-offline -Pdist -Drocks_tools_native clean verify - name: Delete Ozone jars from repo if: steps.restore-cache.outputs.cache-hit != 'true' diff --git a/hadoop-hdds/pom.xml b/hadoop-hdds/pom.xml index 0237210d2fc..707e9852898 100644 --- a/hadoop-hdds/pom.xml +++ b/hadoop-hdds/pom.xml @@ -54,18 +54,6 @@ https://maven.apache.org/xsd/maven-4.0.0.xsd"> rocks-native - - - apache.snapshots.https - https://repository.apache.org/content/repositories/snapshots - - - - - apache.snapshots.https - https://repository.apache.org/content/repositories/snapshots - - diff --git a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/safemode/ContainerSafeModeRule.java b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/safemode/ContainerSafeModeRule.java index accd805602e..bdd7160de4c 100644 --- a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/safemode/ContainerSafeModeRule.java +++ b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/safemode/ContainerSafeModeRule.java @@ -68,6 +68,12 @@ public class ContainerSafeModeRule extends private AtomicLong ecContainerWithMinReplicas = new AtomicLong(0); private final ContainerManager containerManager; + public ContainerSafeModeRule(String ruleName, EventQueue eventQueue, + ConfigurationSource conf, + ContainerManager containerManager, SCMSafeModeManager manager) { + this(ruleName, eventQueue, conf, containerManager.getContainers(), containerManager, manager); + } + public ContainerSafeModeRule(String ruleName, EventQueue eventQueue, ConfigurationSource conf, List containers, diff --git a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/safemode/SCMSafeModeManager.java b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/safemode/SCMSafeModeManager.java index 39530de16b6..78ce994af73 100644 --- a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/safemode/SCMSafeModeManager.java +++ b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/safemode/SCMSafeModeManager.java @@ -23,6 +23,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; import org.apache.commons.lang3.tuple.Pair; import org.apache.hadoop.hdds.HddsConfigKeys; @@ -90,7 +91,7 @@ public class SCMSafeModeManager implements SafeModeManager { private AtomicBoolean preCheckComplete = new AtomicBoolean(false); private AtomicBoolean forceExitSafeMode = new AtomicBoolean(false); - private Map exitRules = new HashMap(1); + private Map exitRules = new HashMap<>(1); private Set preCheckRules = new HashSet<>(1); private ConfigurationSource config; private static final String CONT_EXIT_RULE = "ContainerSafeModeRule"; @@ -110,6 +111,8 @@ public class SCMSafeModeManager implements SafeModeManager { private final SafeModeMetrics safeModeMetrics; + + // TODO: Remove allContainers argument. (HDDS-11795) public SCMSafeModeManager(ConfigurationSource conf, List allContainers, ContainerManager containerManager, PipelineManager pipelineManager, @@ -126,30 +129,17 @@ public SCMSafeModeManager(ConfigurationSource conf, if (isSafeModeEnabled) { this.safeModeMetrics = SafeModeMetrics.create(); - ContainerSafeModeRule containerSafeModeRule = - new ContainerSafeModeRule(CONT_EXIT_RULE, eventQueue, config, - allContainers, containerManager, this); - DataNodeSafeModeRule dataNodeSafeModeRule = - new DataNodeSafeModeRule(DN_EXIT_RULE, eventQueue, config, this); - exitRules.put(CONT_EXIT_RULE, containerSafeModeRule); - exitRules.put(DN_EXIT_RULE, dataNodeSafeModeRule); - preCheckRules.add(DN_EXIT_RULE); - if (conf.getBoolean( - HddsConfigKeys.HDDS_SCM_SAFEMODE_PIPELINE_AVAILABILITY_CHECK, - HddsConfigKeys.HDDS_SCM_SAFEMODE_PIPELINE_AVAILABILITY_CHECK_DEFAULT) - && pipelineManager != null) { - HealthyPipelineSafeModeRule healthyPipelineSafeModeRule = - new HealthyPipelineSafeModeRule(HEALTHY_PIPELINE_EXIT_RULE, - eventQueue, pipelineManager, - this, config, scmContext); - OneReplicaPipelineSafeModeRule oneReplicaPipelineSafeModeRule = - new OneReplicaPipelineSafeModeRule( - ATLEAST_ONE_DATANODE_REPORTED_PIPELINE_EXIT_RULE, eventQueue, - pipelineManager, this, conf); - exitRules.put(HEALTHY_PIPELINE_EXIT_RULE, healthyPipelineSafeModeRule); - exitRules.put(ATLEAST_ONE_DATANODE_REPORTED_PIPELINE_EXIT_RULE, - oneReplicaPipelineSafeModeRule); - } + + // TODO: Remove the cyclic ("this") dependency (HDDS-11797) + SafeModeRuleFactory.initialize(config, scmContext, eventQueue, + this, pipelineManager, containerManager); + SafeModeRuleFactory factory = SafeModeRuleFactory.getInstance(); + + exitRules = factory.getSafeModeRules().stream().collect( + Collectors.toMap(SafeModeExitRule::getRuleName, rule -> rule)); + + preCheckRules = factory.getPreCheckRules().stream() + .map(SafeModeExitRule::getRuleName).collect(Collectors.toSet()); } else { this.safeModeMetrics = null; exitSafeMode(eventQueue, true); diff --git a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/safemode/SafeModeRuleFactory.java b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/safemode/SafeModeRuleFactory.java new file mode 100644 index 00000000000..8e75f51b962 --- /dev/null +++ b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/safemode/SafeModeRuleFactory.java @@ -0,0 +1,131 @@ +/* + * 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.hdds.scm.safemode; + + +import org.apache.hadoop.hdds.HddsConfigKeys; +import org.apache.hadoop.hdds.conf.ConfigurationSource; +import org.apache.hadoop.hdds.scm.container.ContainerManager; +import org.apache.hadoop.hdds.scm.ha.SCMContext; +import org.apache.hadoop.hdds.scm.pipeline.PipelineManager; +import org.apache.hadoop.hdds.server.events.EventQueue; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +/** + * Factory to create SafeMode rules. + */ +public final class SafeModeRuleFactory { + + + private static final Logger LOG = LoggerFactory.getLogger(SafeModeRuleFactory.class); + + // TODO: Move the rule names to respective rules. (HDDS-11798) + private static final String CONT_EXIT_RULE = "ContainerSafeModeRule"; + private static final String DN_EXIT_RULE = "DataNodeSafeModeRule"; + private static final String HEALTHY_PIPELINE_EXIT_RULE = + "HealthyPipelineSafeModeRule"; + private static final String ATLEAST_ONE_DATANODE_REPORTED_PIPELINE_EXIT_RULE = + "AtleastOneDatanodeReportedRule"; + + private final ConfigurationSource config; + private final SCMContext scmContext; + private final EventQueue eventQueue; + + // TODO: Remove dependency on safeModeManager (HDDS-11797) + private final SCMSafeModeManager safeModeManager; + private final PipelineManager pipelineManager; + private final ContainerManager containerManager; + + private final List> safeModeRules; + private final List> preCheckRules; + + private static SafeModeRuleFactory instance; + + private SafeModeRuleFactory(final ConfigurationSource config, + final SCMContext scmContext, + final EventQueue eventQueue, + final SCMSafeModeManager safeModeManager, + final PipelineManager pipelineManager, + final ContainerManager containerManager) { + this.config = config; + this.scmContext = scmContext; + this.eventQueue = eventQueue; + this.safeModeManager = safeModeManager; + this.pipelineManager = pipelineManager; + this.containerManager = containerManager; + this.safeModeRules = new ArrayList<>(); + this.preCheckRules = new ArrayList<>(); + loadRules(); + } + + private void loadRules() { + // TODO: Use annotation to load the rules. (HDDS-11730) + safeModeRules.add(new ContainerSafeModeRule(CONT_EXIT_RULE, eventQueue, config, + containerManager, safeModeManager)); + SafeModeExitRule dnRule = new DataNodeSafeModeRule(DN_EXIT_RULE, eventQueue, config, safeModeManager); + safeModeRules.add(dnRule); + preCheckRules.add(dnRule); + + // TODO: Move isRuleEnabled check to the Rule implementation. (HDDS-11799) + if (config.getBoolean( + HddsConfigKeys.HDDS_SCM_SAFEMODE_PIPELINE_AVAILABILITY_CHECK, + HddsConfigKeys.HDDS_SCM_SAFEMODE_PIPELINE_AVAILABILITY_CHECK_DEFAULT) + && pipelineManager != null) { + + safeModeRules.add(new HealthyPipelineSafeModeRule(HEALTHY_PIPELINE_EXIT_RULE, + eventQueue, pipelineManager, safeModeManager, config, scmContext)); + safeModeRules.add(new OneReplicaPipelineSafeModeRule( + ATLEAST_ONE_DATANODE_REPORTED_PIPELINE_EXIT_RULE, eventQueue, + pipelineManager, safeModeManager, config)); + } + + } + + public static synchronized SafeModeRuleFactory getInstance() { + if (instance != null) { + return instance; + } + throw new IllegalStateException("SafeModeRuleFactory not initialized," + + " call initialize method before getInstance."); + } + + // TODO: Refactor and reduce the arguments. (HDDS-11800) + public static synchronized void initialize( + final ConfigurationSource config, + final SCMContext scmContext, + final EventQueue eventQueue, + final SCMSafeModeManager safeModeManager, + final PipelineManager pipelineManager, + final ContainerManager containerManager) { + instance = new SafeModeRuleFactory(config, scmContext, eventQueue, + safeModeManager, pipelineManager, containerManager); + } + + public List> getSafeModeRules() { + return safeModeRules; + } + + public List> getPreCheckRules() { + return preCheckRules; + } +} diff --git a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/pipeline/TestPipelineManagerImpl.java b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/pipeline/TestPipelineManagerImpl.java index dd994f35b64..1dfbfd32785 100644 --- a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/pipeline/TestPipelineManagerImpl.java +++ b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/pipeline/TestPipelineManagerImpl.java @@ -358,7 +358,8 @@ public void testClosePipelineShouldFailOnFollower() throws Exception { public void testPipelineReport() throws Exception { try (PipelineManagerImpl pipelineManager = createPipelineManager(true)) { SCMSafeModeManager scmSafeModeManager = - new SCMSafeModeManager(conf, new ArrayList<>(), null, pipelineManager, + new SCMSafeModeManager(conf, new ArrayList<>(), + mock(ContainerManager.class), pipelineManager, new EventQueue(), serviceManager, scmContext); Pipeline pipeline = pipelineManager .createPipeline(RatisReplicationConfig @@ -469,7 +470,7 @@ public void testPipelineOpenOnlyWhenLeaderReported() throws Exception { SCMSafeModeManager scmSafeModeManager = new SCMSafeModeManager(new OzoneConfiguration(), new ArrayList<>(), - null, pipelineManager, new EventQueue(), + mock(ContainerManager.class), pipelineManager, new EventQueue(), serviceManager, scmContext); PipelineReportHandler pipelineReportHandler = new PipelineReportHandler(scmSafeModeManager, pipelineManager, diff --git a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/safemode/TestHealthyPipelineSafeModeRule.java b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/safemode/TestHealthyPipelineSafeModeRule.java index 98f16394902..13eb4be724c 100644 --- a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/safemode/TestHealthyPipelineSafeModeRule.java +++ b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/safemode/TestHealthyPipelineSafeModeRule.java @@ -31,6 +31,7 @@ import org.apache.hadoop.hdds.protocol.proto.HddsProtos.ReplicationFactor; import org.apache.hadoop.hdds.scm.HddsTestUtils; import org.apache.hadoop.hdds.scm.container.ContainerInfo; +import org.apache.hadoop.hdds.scm.container.ContainerManager; import org.apache.hadoop.hdds.scm.container.MockNodeManager; import org.apache.hadoop.hdds.scm.events.SCMEvents; import org.apache.hadoop.hdds.scm.ha.SCMHAManagerStub; @@ -50,6 +51,8 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** * This class tests HealthyPipelineSafeMode rule. @@ -69,6 +72,8 @@ public void testHealthyPipelineSafeModeRuleWithNoPipelines() OzoneConfiguration config = new OzoneConfiguration(); MockNodeManager nodeManager = new MockNodeManager(true, 0); + ContainerManager containerManager = mock(ContainerManager.class); + when(containerManager.getContainers()).thenReturn(containers); config.set(HddsConfigKeys.OZONE_METADATA_DIRS, tempFile.getPath()); // enable pipeline check config.setBoolean( @@ -94,7 +99,7 @@ public void testHealthyPipelineSafeModeRuleWithNoPipelines() pipelineManager.setPipelineProvider(HddsProtos.ReplicationType.RATIS, mockRatisProvider); SCMSafeModeManager scmSafeModeManager = new SCMSafeModeManager( - config, containers, null, pipelineManager, eventQueue, + config, containers, containerManager, pipelineManager, eventQueue, serviceManager, scmContext); HealthyPipelineSafeModeRule healthyPipelineSafeModeRule = @@ -121,6 +126,8 @@ public void testHealthyPipelineSafeModeRuleWithPipelines() throws Exception { // stale and last one is dead, and this repeats. So for a 12 node, 9 // healthy, 2 stale and one dead. MockNodeManager nodeManager = new MockNodeManager(true, 12); + ContainerManager containerManager = mock(ContainerManager.class); + when(containerManager.getContainers()).thenReturn(containers); config.set(HddsConfigKeys.OZONE_METADATA_DIRS, tempFile.getPath()); // enable pipeline check config.setBoolean( @@ -172,7 +179,7 @@ public void testHealthyPipelineSafeModeRuleWithPipelines() throws Exception { MockRatisPipelineProvider.markPipelineHealthy(pipeline3); SCMSafeModeManager scmSafeModeManager = new SCMSafeModeManager( - config, containers, null, pipelineManager, eventQueue, + config, containers, containerManager, pipelineManager, eventQueue, serviceManager, scmContext); HealthyPipelineSafeModeRule healthyPipelineSafeModeRule = @@ -215,6 +222,8 @@ public void testHealthyPipelineSafeModeRuleWithMixedPipelines() // stale and last one is dead, and this repeats. So for a 12 node, 9 // healthy, 2 stale and one dead. MockNodeManager nodeManager = new MockNodeManager(true, 12); + ContainerManager containerManager = mock(ContainerManager.class); + when(containerManager.getContainers()).thenReturn(containers); config.set(HddsConfigKeys.OZONE_METADATA_DIRS, tempFile.getPath()); // enable pipeline check config.setBoolean( @@ -266,7 +275,7 @@ public void testHealthyPipelineSafeModeRuleWithMixedPipelines() MockRatisPipelineProvider.markPipelineHealthy(pipeline3); SCMSafeModeManager scmSafeModeManager = new SCMSafeModeManager( - config, containers, null, pipelineManager, eventQueue, + config, containers, containerManager, pipelineManager, eventQueue, serviceManager, scmContext); HealthyPipelineSafeModeRule healthyPipelineSafeModeRule = diff --git a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/safemode/TestOneReplicaPipelineSafeModeRule.java b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/safemode/TestOneReplicaPipelineSafeModeRule.java index e070a2b6036..76bafa8b1fb 100644 --- a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/safemode/TestOneReplicaPipelineSafeModeRule.java +++ b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/safemode/TestOneReplicaPipelineSafeModeRule.java @@ -35,6 +35,7 @@ import org.apache.hadoop.hdds.protocol.proto.StorageContainerDatanodeProtocolProtos.PipelineReport; import org.apache.hadoop.hdds.scm.HddsTestUtils; import org.apache.hadoop.hdds.scm.container.ContainerInfo; +import org.apache.hadoop.hdds.scm.container.ContainerManager; import org.apache.hadoop.hdds.scm.container.MockNodeManager; import org.apache.hadoop.hdds.scm.events.SCMEvents; import org.apache.hadoop.hdds.scm.ha.SCMHAManagerStub; @@ -58,6 +59,8 @@ import org.slf4j.LoggerFactory; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** * This class tests OneReplicaPipelineSafeModeRule. @@ -86,7 +89,8 @@ private void setup(int nodes, int pipelineFactorThreeCount, List containers = new ArrayList<>(); containers.addAll(HddsTestUtils.getContainerInfo(1)); mockNodeManager = new MockNodeManager(true, nodes); - + ContainerManager containerManager = mock(ContainerManager.class); + when(containerManager.getContainers()).thenReturn(containers); eventQueue = new EventQueue(); serviceManager = new SCMServiceManager(); scmContext = SCMContext.emptyContext(); @@ -116,7 +120,7 @@ private void setup(int nodes, int pipelineFactorThreeCount, HddsProtos.ReplicationFactor.ONE); SCMSafeModeManager scmSafeModeManager = - new SCMSafeModeManager(ozoneConfiguration, containers, null, + new SCMSafeModeManager(ozoneConfiguration, containers, containerManager, pipelineManager, eventQueue, serviceManager, scmContext); rule = scmSafeModeManager.getOneReplicaPipelineSafeModeRule(); diff --git a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/safemode/TestSCMSafeModeManager.java b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/safemode/TestSCMSafeModeManager.java index 05e23177659..fc8ec9c1912 100644 --- a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/safemode/TestSCMSafeModeManager.java +++ b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/safemode/TestSCMSafeModeManager.java @@ -81,6 +81,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.params.provider.Arguments.arguments; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** Test class for SCMSafeModeManager. */ @@ -123,12 +124,6 @@ public void testSafeModeState(int numContainers) throws Exception { testSafeMode(numContainers); } - @Test - public void testSafeModeStateWithNullContainers() { - new SCMSafeModeManager(config, Collections.emptyList(), - null, null, queue, serviceManager, scmContext); - } - private void testSafeMode(int numContainers) throws Exception { containers = new ArrayList<>(); containers.addAll(HddsTestUtils.getContainerInfo(numContainers)); @@ -138,8 +133,10 @@ private void testSafeMode(int numContainers) throws Exception { container.setState(HddsProtos.LifeCycleState.CLOSED); container.setNumberOfKeys(10); } + ContainerManager containerManager = mock(ContainerManager.class); + when(containerManager.getContainers()).thenReturn(containers); scmSafeModeManager = new SCMSafeModeManager( - config, containers, null, null, queue, + config, containers, containerManager, null, queue, serviceManager, scmContext); assertTrue(scmSafeModeManager.getInSafeMode()); @@ -175,8 +172,10 @@ public void testSafeModeExitRule() throws Exception { container.setState(HddsProtos.LifeCycleState.CLOSED); container.setNumberOfKeys(10); } + ContainerManager containerManager = mock(ContainerManager.class); + when(containerManager.getContainers()).thenReturn(containers); scmSafeModeManager = new SCMSafeModeManager( - config, containers, null, null, queue, + config, containers, containerManager, null, queue, serviceManager, scmContext); long cutOff = (long) Math.ceil(numContainers * config.getDouble( @@ -242,8 +241,11 @@ public void testHealthyPipelinePercentWithIncorrectValue(double healthyPercent, scmContext, serviceManager, Clock.system(ZoneOffset.UTC)); + ContainerManager containerManager = mock(ContainerManager.class); + when(containerManager.getContainers()).thenReturn(containers); IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, - () -> new SCMSafeModeManager(conf, containers, null, pipelineManager, queue, serviceManager, scmContext)); + () -> new SCMSafeModeManager(conf, containers, containerManager, + pipelineManager, queue, serviceManager, scmContext)); assertThat(exception).hasMessageEndingWith("value should be >= 0.0 and <= 1.0"); } @@ -305,8 +307,11 @@ public void testSafeModeExitRuleWithPipelineAvailabilityCheck( container.setState(HddsProtos.LifeCycleState.CLOSED); } + ContainerManager containerManager = mock(ContainerManager.class); + when(containerManager.getContainers()).thenReturn(containers); + scmSafeModeManager = new SCMSafeModeManager( - conf, containers, null, pipelineManager, queue, serviceManager, + conf, containers, containerManager, pipelineManager, queue, serviceManager, scmContext); assertTrue(scmSafeModeManager.getInSafeMode()); @@ -439,8 +444,10 @@ public void testDisableSafeMode() { OzoneConfiguration conf = new OzoneConfiguration(config); conf.setBoolean(HddsConfigKeys.HDDS_SCM_SAFEMODE_ENABLED, false); PipelineManager pipelineManager = mock(PipelineManager.class); + ContainerManager containerManager = mock(ContainerManager.class); + when(containerManager.getContainers()).thenReturn(containers); scmSafeModeManager = new SCMSafeModeManager( - conf, containers, null, pipelineManager, queue, serviceManager, + conf, containers, containerManager, pipelineManager, queue, serviceManager, scmContext); assertFalse(scmSafeModeManager.getInSafeMode()); } @@ -478,8 +485,11 @@ public void testContainerSafeModeRule() throws Exception { container.setNumberOfKeys(0); } + ContainerManager containerManager = mock(ContainerManager.class); + when(containerManager.getContainers()).thenReturn(containers); + scmSafeModeManager = new SCMSafeModeManager( - config, containers, null, null, queue, serviceManager, scmContext); + config, containers, containerManager, null, queue, serviceManager, scmContext); assertTrue(scmSafeModeManager.getInSafeMode()); @@ -575,8 +585,10 @@ public void testContainerSafeModeRuleEC(int data, int parity) throws Exception { private void testSafeModeDataNodes(int numOfDns) throws Exception { OzoneConfiguration conf = new OzoneConfiguration(config); conf.setInt(HddsConfigKeys.HDDS_SCM_SAFEMODE_MIN_DATANODE, numOfDns); + ContainerManager containerManager = mock(ContainerManager.class); + when(containerManager.getContainers()).thenReturn(containers); scmSafeModeManager = new SCMSafeModeManager( - conf, containers, null, null, queue, + conf, containers, containerManager, null, queue, serviceManager, scmContext); // Assert SCM is in Safe mode. @@ -686,9 +698,11 @@ public void testSafeModePipelineExitRule() throws Exception { pipeline = pipelineManager.getPipeline(pipeline.getId()); MockRatisPipelineProvider.markPipelineHealthy(pipeline); + ContainerManager containerManager = mock(ContainerManager.class); + when(containerManager.getContainers()).thenReturn(containers); scmSafeModeManager = new SCMSafeModeManager( - config, containers, null, pipelineManager, queue, serviceManager, + config, containers, containerManager, pipelineManager, queue, serviceManager, scmContext); SCMDatanodeProtocolServer.NodeRegistrationContainerReport nodeRegistrationContainerReport = @@ -739,8 +753,11 @@ public void testPipelinesNotCreatedUntilPreCheckPasses() throws Exception { pipelineManager.setPipelineProvider(HddsProtos.ReplicationType.RATIS, mockRatisProvider); + ContainerManager containerManager = mock(ContainerManager.class); + when(containerManager.getContainers()).thenReturn(containers); + scmSafeModeManager = new SCMSafeModeManager( - config, containers, null, pipelineManager, queue, serviceManager, + config, containers, containerManager, pipelineManager, queue, serviceManager, scmContext); // Assert SCM is in Safe mode. diff --git a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/safemode/TestSafeModeRuleFactory.java b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/safemode/TestSafeModeRuleFactory.java new file mode 100644 index 00000000000..837012429be --- /dev/null +++ b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/safemode/TestSafeModeRuleFactory.java @@ -0,0 +1,85 @@ +/* + * 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.hdds.scm.safemode; + +import org.apache.hadoop.hdds.conf.OzoneConfiguration; +import org.apache.hadoop.hdds.scm.container.ContainerManager; +import org.apache.hadoop.hdds.scm.ha.SCMContext; +import org.apache.hadoop.hdds.scm.pipeline.PipelineManager; +import org.apache.hadoop.hdds.server.events.EventQueue; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Field; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class TestSafeModeRuleFactory { + + @Test + public void testIllegalState() { + // If the initialization is already done by different test, we have to reset it. + try { + final Field instance = SafeModeRuleFactory.class.getDeclaredField("instance"); + instance.setAccessible(true); + instance.set(null, null); + } catch (Exception e) { + throw new RuntimeException(); + } + assertThrows(IllegalStateException.class, SafeModeRuleFactory::getInstance); + } + + @Test + public void testLoadedSafeModeRules() { + initializeSafeModeRuleFactory(); + final SafeModeRuleFactory factory = SafeModeRuleFactory.getInstance(); + + // Currently we assert the total count against hardcoded value + // as the rules are hardcoded in SafeModeRuleFactory. + + // This will be fixed once we load rules using annotation. + assertEquals(4, factory.getSafeModeRules().size(), + "The total safemode rules count doesn't match"); + + } + + @Test + public void testLoadedPreCheckRules() { + initializeSafeModeRuleFactory(); + final SafeModeRuleFactory factory = SafeModeRuleFactory.getInstance(); + + // Currently we assert the total count against hardcoded value + // as the rules are hardcoded in SafeModeRuleFactory. + + // This will be fixed once we load rules using annotation. + assertEquals(1, factory.getPreCheckRules().size(), + "The total safemode rules count doesn't match"); + + } + + private void initializeSafeModeRuleFactory() { + final SCMSafeModeManager safeModeManager = mock(SCMSafeModeManager.class); + when(safeModeManager.getSafeModeMetrics()).thenReturn(mock(SafeModeMetrics.class)); + SafeModeRuleFactory.initialize(new OzoneConfiguration(), + SCMContext.emptyContext(), new EventQueue(), safeModeManager, mock( + PipelineManager.class), mock(ContainerManager.class)); + } + +} diff --git a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/OMConfigKeys.java b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/OMConfigKeys.java index 2cf5e4ced3e..880fe8614b2 100644 --- a/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/OMConfigKeys.java +++ b/hadoop-ozone/common/src/main/java/org/apache/hadoop/ozone/om/OMConfigKeys.java @@ -421,6 +421,11 @@ private OMConfigKeys() { // resulting 24MB public static final int OZONE_PATH_DELETING_LIMIT_PER_TASK_DEFAULT = 6000; + public static final String OZONE_THREAD_NUMBER_DIR_DELETION = + "ozone.thread.number.dir.deletion"; + + public static final int OZONE_THREAD_NUMBER_DIR_DELETION_DEFAULT = 10; + public static final String SNAPSHOT_SST_DELETING_LIMIT_PER_TASK = "ozone.snapshot.filtering.limit.per.task"; public static final int SNAPSHOT_SST_DELETING_LIMIT_PER_TASK_DEFAULT = 2; diff --git a/hadoop-ozone/dev-support/checks/_lib.sh b/hadoop-ozone/dev-support/checks/_lib.sh index 48108f2e72b..632aecb8296 100644 --- a/hadoop-ozone/dev-support/checks/_lib.sh +++ b/hadoop-ozone/dev-support/checks/_lib.sh @@ -161,7 +161,7 @@ download_hadoop_aws() { if [[ ! -e "${dir}" ]] || [[ ! -d "${dir}"/src/test/resources ]]; then mkdir -p "${dir}" if [[ ! -f "${dir}.tar.gz" ]]; then - local url="https://archive.apache.org/dist/hadoop/common/hadoop-${HADOOP_VERSION}/hadoop-${HADOOP_VERSION}-src.tar.gz" + local url="https://www.apache.org/dyn/closer.lua?action=download&filename=hadoop/common/hadoop-${HADOOP_VERSION}/hadoop-${HADOOP_VERSION}-src.tar.gz" echo "Downloading Hadoop from ${url}" curl -LSs --fail -o "${dir}.tar.gz" "$url" || return 1 fi diff --git a/hadoop-ozone/dist/pom.xml b/hadoop-ozone/dist/pom.xml index 20c28b6368b..a3548acdedd 100644 --- a/hadoop-ozone/dist/pom.xml +++ b/hadoop-ozone/dist/pom.xml @@ -30,7 +30,7 @@ true apache/ozone -rocky - 20241108-jdk17-1 + 20241119-1-jdk21 ghcr.io/apache/ozone-testkrb5:20241129-1 true diff --git a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/fs/ozone/TestDirectoryDeletingServiceWithFSO.java b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/fs/ozone/TestDirectoryDeletingServiceWithFSO.java index 8d161dedeb3..78fb4c66fc1 100644 --- a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/fs/ozone/TestDirectoryDeletingServiceWithFSO.java +++ b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/fs/ozone/TestDirectoryDeletingServiceWithFSO.java @@ -529,7 +529,7 @@ public void testAOSKeyDeletingWithSnapshotCreateParallelExecution() when(ozoneManager.getOmSnapshotManager()).thenAnswer(i -> omSnapshotManager); DirectoryDeletingService service = Mockito.spy(new DirectoryDeletingService(1000, TimeUnit.MILLISECONDS, 1000, ozoneManager, - cluster.getConf())); + cluster.getConf(), 1)); service.shutdown(); final int initialSnapshotCount = (int) cluster.getOzoneManager().getMetadataManager().countRowsInTable(snapshotInfoTable); @@ -563,7 +563,7 @@ public void testAOSKeyDeletingWithSnapshotCreateParallelExecution() } return i.callRealMethod(); }).when(service).optimizeDirDeletesAndSubmitRequest(anyLong(), anyLong(), anyLong(), - anyLong(), anyList(), anyList(), eq(null), anyLong(), anyInt(), Mockito.any(), any()); + anyLong(), anyList(), anyList(), eq(null), anyLong(), anyInt(), Mockito.any(), any(), anyLong()); Mockito.doAnswer(i -> { store.createSnapshot(testVolumeName, testBucketName, snap2); diff --git a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/TestOzoneConfigurationFields.java b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/TestOzoneConfigurationFields.java index 8a219514d34..1fbfc1f1f70 100644 --- a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/TestOzoneConfigurationFields.java +++ b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/TestOzoneConfigurationFields.java @@ -126,6 +126,7 @@ private void addPropertiesNotInXml() { OMConfigKeys.OZONE_RANGER_HTTPS_ADDRESS_KEY, OMConfigKeys.OZONE_OM_RANGER_HTTPS_ADMIN_API_USER, OMConfigKeys.OZONE_OM_RANGER_HTTPS_ADMIN_API_PASSWD, + OMConfigKeys.OZONE_THREAD_NUMBER_DIR_DELETION, ScmConfigKeys.OZONE_SCM_PIPELINE_PLACEMENT_IMPL_KEY, ScmConfigKeys.OZONE_SCM_HA_PREFIX, S3GatewayConfigKeys.OZONE_S3G_FSO_DIRECTORY_CREATION_ENABLED, diff --git a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/om/snapshot/TestSnapshotDeletingServiceIntegrationTest.java b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/om/snapshot/TestSnapshotDeletingServiceIntegrationTest.java index a9e5faa041f..c3a58a1a211 100644 --- a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/om/snapshot/TestSnapshotDeletingServiceIntegrationTest.java +++ b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/om/snapshot/TestSnapshotDeletingServiceIntegrationTest.java @@ -480,22 +480,22 @@ public void testSnapshotWithFSO() throws Exception { private DirectoryDeletingService getMockedDirectoryDeletingService(AtomicBoolean dirDeletionWaitStarted, AtomicBoolean dirDeletionStarted) - throws InterruptedException, TimeoutException { + throws InterruptedException, TimeoutException, IOException { OzoneManager ozoneManager = Mockito.spy(om); om.getKeyManager().getDirDeletingService().shutdown(); GenericTestUtils.waitFor(() -> om.getKeyManager().getDirDeletingService().getThreadCount() == 0, 1000, 100000); DirectoryDeletingService directoryDeletingService = Mockito.spy(new DirectoryDeletingService(10000, - TimeUnit.MILLISECONDS, 100000, ozoneManager, cluster.getConf())); + TimeUnit.MILLISECONDS, 100000, ozoneManager, cluster.getConf(), 1)); directoryDeletingService.shutdown(); GenericTestUtils.waitFor(() -> directoryDeletingService.getThreadCount() == 0, 1000, 100000); - when(ozoneManager.getMetadataManager()).thenAnswer(i -> { + doAnswer(i -> { // Wait for SDS to reach DDS wait block before processing any deleted directories. GenericTestUtils.waitFor(dirDeletionWaitStarted::get, 1000, 100000); dirDeletionStarted.set(true); return i.callRealMethod(); - }); + }).when(directoryDeletingService).getPendingDeletedDirInfo(); return directoryDeletingService; } diff --git a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/repair/om/TestFSORepairTool.java b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/repair/om/TestFSORepairTool.java new file mode 100644 index 00000000000..4006ec6e822 --- /dev/null +++ b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/repair/om/TestFSORepairTool.java @@ -0,0 +1,569 @@ +/* + * 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.repair.om; + +import org.apache.commons.io.IOUtils; +import org.apache.hadoop.fs.CommonConfigurationKeysPublic; +import org.apache.hadoop.fs.FSDataOutputStream; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.fs.contract.ContractTestUtils; +import org.apache.hadoop.hdds.conf.OzoneConfiguration; +import org.apache.hadoop.hdds.utils.db.Table; +import org.apache.hadoop.hdds.utils.db.TableIterator; +import org.apache.hadoop.ozone.MiniOzoneCluster; +import org.apache.hadoop.ozone.client.BucketArgs; +import org.apache.hadoop.ozone.client.ObjectStore; +import org.apache.hadoop.ozone.client.OzoneClient; +import org.apache.hadoop.ozone.client.OzoneClientFactory; +import org.apache.hadoop.ozone.client.io.OzoneOutputStream; +import org.apache.hadoop.ozone.om.OMStorage; +import org.apache.hadoop.ozone.om.helpers.BucketLayout; +import org.apache.hadoop.ozone.om.helpers.OmDirectoryInfo; +import org.apache.hadoop.ozone.repair.OzoneRepair; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import picocli.CommandLine; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.apache.hadoop.ozone.OzoneConsts.OM_DB_NAME; +import static org.apache.hadoop.ozone.OzoneConsts.OZONE_OFS_URI_SCHEME; +import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_ADDRESS_KEY; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * FSORepairTool test cases. + */ +public class TestFSORepairTool { + public static final Logger LOG = LoggerFactory.getLogger(TestFSORepairTool.class); + private final ByteArrayOutputStream out = new ByteArrayOutputStream(); + private final ByteArrayOutputStream err = new ByteArrayOutputStream(); + private static final PrintStream OLD_OUT = System.out; + private static final PrintStream OLD_ERR = System.err; + private static final String DEFAULT_ENCODING = UTF_8.name(); + private MiniOzoneCluster cluster; + private FileSystem fs; + private OzoneClient client; + private OzoneConfiguration conf = null; + + @BeforeEach + public void init() throws Exception { + // Set configs. + conf = new OzoneConfiguration(); + + // Build cluster. + cluster = MiniOzoneCluster.newBuilder(conf).build(); + cluster.waitForClusterToBeReady(); + + // Init ofs. + final String rootPath = String.format("%s://%s/", OZONE_OFS_URI_SCHEME, conf.get(OZONE_OM_ADDRESS_KEY)); + conf.set(CommonConfigurationKeysPublic.FS_DEFAULT_NAME_KEY, rootPath); + fs = FileSystem.get(conf); + client = OzoneClientFactory.getRpcClient(conf); + + System.setOut(new PrintStream(out, false, DEFAULT_ENCODING)); + System.setErr(new PrintStream(err, false, DEFAULT_ENCODING)); + } + + @AfterEach + public void reset() throws IOException { + // reset stream after each unit test + out.reset(); + err.reset(); + + // restore system streams + System.setOut(OLD_OUT); + System.setErr(OLD_ERR); + + if (cluster != null) { + cluster.shutdown(); + } + if (client != null) { + client.close(); + } + IOUtils.closeQuietly(fs); + } + + @Test + public void testConnectedTreeOneBucket() throws Exception { + CommandLine cmd = new OzoneRepair().getCmd(); + String dbPath = new File(OMStorage.getOmDbDir(conf) + "/" + OM_DB_NAME).getPath(); + + FSORepairTool.Report expectedReport = buildConnectedTree("vol1", "bucket1"); + String expectedOutput = serializeReport(expectedReport); + + // Test the connected tree in debug mode. + cluster.getOzoneManager().stop(); + + String[] args = new String[] {"om", "fso-tree", "--db", dbPath}; + int exitCode = cmd.execute(args); + assertEquals(0, exitCode); + + String cliOutput = out.toString(DEFAULT_ENCODING); + String reportOutput = extractRelevantSection(cliOutput); + Assertions.assertEquals(expectedOutput, reportOutput); + + out.reset(); + err.reset(); + + // Running again in repair mode should give same results since the tree is connected. + String[] args1 = new String[] {"om", "fso-tree", "--db", dbPath, "--repair"}; + int exitCode1 = cmd.execute(args1); + assertEquals(0, exitCode1); + + String cliOutput1 = out.toString(DEFAULT_ENCODING); + String reportOutput1 = extractRelevantSection(cliOutput1); + Assertions.assertEquals(expectedOutput, reportOutput1); + + cluster.getOzoneManager().restart(); + } + + @Test + public void testReportedDataSize() throws Exception { + CommandLine cmd = new OzoneRepair().getCmd(); + String dbPath = new File(OMStorage.getOmDbDir(conf) + "/" + OM_DB_NAME).getPath(); + + FSORepairTool.Report report1 = buildDisconnectedTree("vol1", "bucket1", 10); + FSORepairTool.Report report2 = buildConnectedTree("vol1", "bucket2", 10); + FSORepairTool.Report expectedReport = new FSORepairTool.Report(report1, report2); + String expectedOutput = serializeReport(expectedReport); + + cluster.getOzoneManager().stop(); + + String[] args = new String[] {"om", "fso-tree", "--db", dbPath, "--repair"}; + int exitCode = cmd.execute(args); + assertEquals(0, exitCode); + + String cliOutput = out.toString(DEFAULT_ENCODING); + String reportOutput = extractRelevantSection(cliOutput); + + Assertions.assertEquals(expectedOutput, reportOutput); + cluster.getOzoneManager().restart(); + } + + /** + * Test to verify how the tool processes the volume and bucket + * filters. + */ + @Test + public void testVolumeAndBucketFilter() throws Exception { + CommandLine cmd = new OzoneRepair().getCmd(); + String dbPath = new File(OMStorage.getOmDbDir(conf) + "/" + OM_DB_NAME).getPath(); + + FSORepairTool.Report report1 = buildDisconnectedTree("vol1", "bucket1", 10); + FSORepairTool.Report report2 = buildConnectedTree("vol2", "bucket2", 10); + FSORepairTool.Report expectedReport1 = new FSORepairTool.Report(report1); + FSORepairTool.Report expectedReport2 = new FSORepairTool.Report(report2); + + cluster.getOzoneManager().stop(); + + // When volume filter is passed + String[] args1 = new String[]{"om", "fso-tree", "--db", dbPath, "--volume", "/vol1"}; + int exitCode1 = cmd.execute(args1); + assertEquals(0, exitCode1); + + String cliOutput1 = out.toString(DEFAULT_ENCODING); + String reportOutput1 = extractRelevantSection(cliOutput1); + String expectedOutput1 = serializeReport(expectedReport1); + Assertions.assertEquals(expectedOutput1, reportOutput1); + + out.reset(); + err.reset(); + + // When both volume and bucket filters are passed + String[] args2 = new String[]{"om", "fso-tree", "--db", dbPath, "--volume", "/vol2", + "--bucket", "bucket2"}; + int exitCode2 = cmd.execute(args2); + assertEquals(0, exitCode2); + + String cliOutput2 = out.toString(DEFAULT_ENCODING); + String reportOutput2 = extractRelevantSection(cliOutput2); + String expectedOutput2 = serializeReport(expectedReport2); + Assertions.assertEquals(expectedOutput2, reportOutput2); + + out.reset(); + err.reset(); + + // When a non-existent bucket filter is passed + String[] args3 = new String[]{"om", "fso-tree", "--db", dbPath, "--volume", "/vol1", + "--bucket", "bucket2"}; + int exitCode3 = cmd.execute(args3); + assertEquals(0, exitCode3); + String cliOutput3 = out.toString(DEFAULT_ENCODING); + Assertions.assertTrue(cliOutput3.contains("Bucket 'bucket2' does not exist in volume '/vol1'.")); + + out.reset(); + err.reset(); + + // When a non-existent volume filter is passed + String[] args4 = new String[]{"om", "fso-tree", "--db", dbPath, "--volume", "/vol3"}; + int exitCode4 = cmd.execute(args4); + assertEquals(0, exitCode4); + String cliOutput4 = out.toString(DEFAULT_ENCODING); + Assertions.assertTrue(cliOutput4.contains("Volume '/vol3' does not exist.")); + + out.reset(); + err.reset(); + + // When bucket filter is passed without the volume filter. + String[] args5 = new String[]{"om", "fso-tree", "--db", dbPath, "--bucket", "bucket1"}; + int exitCode5 = cmd.execute(args5); + assertEquals(0, exitCode5); + String cliOutput5 = out.toString(DEFAULT_ENCODING); + Assertions.assertTrue(cliOutput5.contains("--bucket flag cannot be used without specifying --volume.")); + + cluster.getOzoneManager().restart(); + } + + @Test + public void testMultipleBucketsAndVolumes() throws Exception { + CommandLine cmd = new OzoneRepair().getCmd(); + String dbPath = new File(OMStorage.getOmDbDir(conf) + "/" + OM_DB_NAME).getPath(); + + FSORepairTool.Report report1 = buildConnectedTree("vol1", "bucket1"); + FSORepairTool.Report report2 = buildDisconnectedTree("vol2", "bucket2"); + FSORepairTool.Report expectedAggregateReport = new FSORepairTool.Report(report1, report2); + String expectedOutput = serializeReport(expectedAggregateReport); + + cluster.getOzoneManager().stop(); + + String[] args = new String[] {"om", "fso-tree", "--db", dbPath, "--repair"}; + int exitCode = cmd.execute(args); + assertEquals(0, exitCode); + + String cliOutput = out.toString(DEFAULT_ENCODING); + String reportOutput = extractRelevantSection(cliOutput); + Assertions.assertEquals(expectedOutput, reportOutput); + + cluster.getOzoneManager().restart(); + } + + /** + * Tests having multiple entries in the deleted file and directory tables + * for the same objects. + */ + @Test + public void testDeleteOverwrite() throws Exception { + CommandLine cmd = new OzoneRepair().getCmd(); + String dbPath = new File(OMStorage.getOmDbDir(conf) + "/" + OM_DB_NAME).getPath(); + + // Create files and dirs under dir1. To make sure they are added to the + // delete table, the keys must have data. + buildConnectedTree("vol1", "bucket1", 10); + // Move soon to be disconnected objects to the deleted table. + fs.delete(new Path("/vol1/bucket1/dir1/dir2/file3"), true); + fs.delete(new Path("/vol1/bucket1/dir1/dir2"), true); + fs.delete(new Path("/vol1/bucket1/dir1/file1"), true); + fs.delete(new Path("/vol1/bucket1/dir1/file2"), true); + + // Recreate deleted objects, then disconnect dir1. + // This means after the repair runs, these objects will be + // the deleted tables multiple times. Some will have the same dir1 parent ID + // in their key name too. + ContractTestUtils.touch(fs, new Path("/vol1/bucket1/dir1/dir2/file3")); + ContractTestUtils.touch(fs, new Path("/vol1/bucket1/dir1/file1")); + ContractTestUtils.touch(fs, new Path("/vol1/bucket1/dir1/file2")); + disconnectDirectory("dir1"); + + cluster.getOzoneManager().stop(); + + String[] args = new String[]{"om", "fso-tree", "--db", dbPath, "--repair"}; + int exitCode = cmd.execute(args); + assertEquals(0, exitCode); + + String cliOutput = out.toString(DEFAULT_ENCODING); + Assertions.assertTrue(cliOutput.contains("Unreferenced:\n\tDirectories: 1\n\tFiles: 3")); + + cluster.getOzoneManager().restart(); + } + + @Test + public void testEmptyFileTrees() throws Exception { + CommandLine cmd = new OzoneRepair().getCmd(); + String dbPath = new File(OMStorage.getOmDbDir(conf) + "/" + OM_DB_NAME).getPath(); + + FSORepairTool.Report emptyReport = buildEmptyTree(); + String expectedOutput = serializeReport(emptyReport); + + cluster.getOzoneManager().stop(); + + // Run when there are no file trees. + String[] args = new String[] {"om", "fso-tree", "--db", dbPath, "--repair"}; + int exitCode = cmd.execute(args); + assertEquals(0, exitCode); + + String cliOutput = out.toString(DEFAULT_ENCODING); + String reportOutput = extractRelevantSection(cliOutput); + Assertions.assertEquals(expectedOutput, reportOutput); + + out.reset(); + err.reset(); + cluster.getOzoneManager().restart(); + + // Create an empty volume and bucket. + fs.mkdirs(new Path("/vol1")); + fs.mkdirs(new Path("/vol2/bucket1")); + + cluster.getOzoneManager().stop(); + + // Run on an empty volume and bucket. + String[] args1 = new String[] {"om", "fso-tree", "--db", dbPath, "--repair"}; + int exitCode1 = cmd.execute(args1); + assertEquals(0, exitCode1); + + String cliOutput2 = out.toString(DEFAULT_ENCODING); + String reportOutput2 = extractRelevantSection(cliOutput2); + Assertions.assertEquals(expectedOutput, reportOutput2); + + cluster.getOzoneManager().restart(); + } + + @Test + public void testNonFSOBucketsSkipped() throws Exception { + ObjectStore store = client.getObjectStore(); + + // Create legacy and OBS buckets. + store.createVolume("vol1"); + store.getVolume("vol1").createBucket("obs-bucket", + BucketArgs.newBuilder().setBucketLayout(BucketLayout.OBJECT_STORE) + .build()); + store.getVolume("vol1").createBucket("legacy-bucket", + BucketArgs.newBuilder().setBucketLayout(BucketLayout.LEGACY) + .build()); + + // Put a key in the legacy and OBS buckets. + OzoneOutputStream obsStream = store.getVolume("vol1") + .getBucket("obs-bucket") + .createKey("prefix/test-key", 3); + obsStream.write(new byte[]{1, 1, 1}); + obsStream.close(); + + OzoneOutputStream legacyStream = store.getVolume("vol1") + .getBucket("legacy-bucket") + .createKey("prefix/test-key", 3); + legacyStream.write(new byte[]{1, 1, 1}); + legacyStream.close(); + + CommandLine cmd = new OzoneRepair().getCmd(); + String dbPath = new File(OMStorage.getOmDbDir(conf) + "/" + OM_DB_NAME).getPath(); + + // Add an FSO bucket with data. + FSORepairTool.Report connectReport = buildConnectedTree("vol1", "fso-bucket"); + + cluster.getOzoneManager().stop(); + + // Even in repair mode there should be no action. legacy and obs buckets + // will be skipped and FSO tree is connected. + String[] args = new String[] {"om", "fso-tree", "--db", dbPath, "--repair"}; + int exitCode = cmd.execute(args); + assertEquals(0, exitCode); + + String cliOutput = out.toString(DEFAULT_ENCODING); + String reportOutput = extractRelevantSection(cliOutput); + String expectedOutput = serializeReport(connectReport); + + Assertions.assertEquals(expectedOutput, reportOutput); + Assertions.assertTrue(cliOutput.contains("Skipping non-FSO bucket /vol1/obs-bucket")); + Assertions.assertTrue(cliOutput.contains("Skipping non-FSO bucket /vol1/legacy-bucket")); + + cluster.getOzoneManager().restart(); + } + + private FSORepairTool.Report buildConnectedTree(String volume, String bucket) throws Exception { + return buildConnectedTree(volume, bucket, 0); + } + + private String extractRelevantSection(String cliOutput) { + int startIndex = cliOutput.indexOf("Reachable:"); + if (startIndex == -1) { + throw new AssertionError("Output does not contain 'Reachable' section."); + } + return cliOutput.substring(startIndex).trim(); + } + + private String serializeReport(FSORepairTool.Report report) { + return String.format( + "Reachable:%n\tDirectories: %d%n\tFiles: %d%n\tBytes: %d%n" + + "Unreachable:%n\tDirectories: %d%n\tFiles: %d%n\tBytes: %d%n" + + "Unreferenced:%n\tDirectories: %d%n\tFiles: %d%n\tBytes: %d", + report.getReachable().getDirs(), + report.getReachable().getFiles(), + report.getReachable().getBytes(), + report.getUnreachable().getDirs(), + report.getUnreachable().getFiles(), + report.getUnreachable().getBytes(), + report.getUnreferenced().getDirs(), + report.getUnreferenced().getFiles(), + report.getUnreferenced().getBytes() + ); + } + + /** + * Creates a tree with 3 reachable directories and 4 reachable files. + */ + private FSORepairTool.Report buildConnectedTree(String volume, String bucket, int fileSize) throws Exception { + Path bucketPath = new Path("/" + volume + "/" + bucket); + Path dir1 = new Path(bucketPath, "dir1"); + Path file1 = new Path(dir1, "file1"); + Path file2 = new Path(dir1, "file2"); + + Path dir2 = new Path(bucketPath, "dir1/dir2"); + Path file3 = new Path(dir2, "file3"); + + Path dir3 = new Path(bucketPath, "dir3"); + Path file4 = new Path(bucketPath, "file4"); + + fs.mkdirs(dir1); + fs.mkdirs(dir2); + fs.mkdirs(dir3); + + // Content to put in every file. + String data = new String(new char[fileSize]); + + FSDataOutputStream stream = fs.create(file1); + stream.write(data.getBytes(StandardCharsets.UTF_8)); + stream.close(); + stream = fs.create(file2); + stream.write(data.getBytes(StandardCharsets.UTF_8)); + stream.close(); + stream = fs.create(file3); + stream.write(data.getBytes(StandardCharsets.UTF_8)); + stream.close(); + stream = fs.create(file4); + stream.write(data.getBytes(StandardCharsets.UTF_8)); + stream.close(); + + assertConnectedTreeReadable(volume, bucket); + + FSORepairTool.ReportStatistics reachableCount = + new FSORepairTool.ReportStatistics(3, 4, fileSize * 4L); + return new FSORepairTool.Report.Builder() + .setReachable(reachableCount) + .build(); + } + + private FSORepairTool.Report buildEmptyTree() { + FSORepairTool.ReportStatistics reachableCount = + new FSORepairTool.ReportStatistics(0, 0, 0); + FSORepairTool.ReportStatistics unreachableCount = + new FSORepairTool.ReportStatistics(0, 0, 0); + FSORepairTool.ReportStatistics unreferencedCount = + new FSORepairTool.ReportStatistics(0, 0, 0); + return new FSORepairTool.Report.Builder() + .setReachable(reachableCount) + .setUnreachable(unreachableCount) + .setUnreferenced(unreferencedCount) + .build(); + } + + private void assertConnectedTreeReadable(String volume, String bucket) throws IOException { + Path bucketPath = new Path("/" + volume + "/" + bucket); + Path dir1 = new Path(bucketPath, "dir1"); + Path file1 = new Path(dir1, "file1"); + Path file2 = new Path(dir1, "file2"); + + Path dir2 = new Path(bucketPath, "dir1/dir2"); + Path file3 = new Path(dir2, "file3"); + + Path dir3 = new Path(bucketPath, "dir3"); + Path file4 = new Path(bucketPath, "file4"); + + Assertions.assertTrue(fs.exists(dir1)); + Assertions.assertTrue(fs.exists(dir2)); + Assertions.assertTrue(fs.exists(dir3)); + Assertions.assertTrue(fs.exists(file1)); + Assertions.assertTrue(fs.exists(file2)); + Assertions.assertTrue(fs.exists(file3)); + Assertions.assertTrue(fs.exists(file4)); + } + + private FSORepairTool.Report buildDisconnectedTree(String volume, String bucket) throws Exception { + return buildDisconnectedTree(volume, bucket, 0); + } + + /** + * Creates a tree with 1 reachable directory, 1 reachable file, 1 + * unreachable directory, and 3 unreachable files. + */ + private FSORepairTool.Report buildDisconnectedTree(String volume, String bucket, int fileSize) throws Exception { + buildConnectedTree(volume, bucket, fileSize); + + // Manually remove dir1. This should disconnect 3 of the files and 1 of + // the directories. + disconnectDirectory("dir1"); + + assertDisconnectedTreePartiallyReadable(volume, bucket); + + // dir1 does not count towards the unreachable directories the tool + // will see. It was deleted completely so the tool will never see it. + FSORepairTool.ReportStatistics reachableCount = + new FSORepairTool.ReportStatistics(1, 1, fileSize); + FSORepairTool.ReportStatistics unreferencedCount = + new FSORepairTool.ReportStatistics(1, 3, fileSize * 3L); + return new FSORepairTool.Report.Builder() + .setReachable(reachableCount) + .setUnreferenced(unreferencedCount) + .build(); + } + + private void disconnectDirectory(String dirName) throws Exception { + Table dirTable = cluster.getOzoneManager().getMetadataManager().getDirectoryTable(); + try (TableIterator> iterator = dirTable.iterator()) { + while (iterator.hasNext()) { + Table.KeyValue entry = iterator.next(); + String key = entry.getKey(); + if (key.contains(dirName)) { + dirTable.delete(key); + break; + } + } + } + } + + private void assertDisconnectedTreePartiallyReadable(String volume, String bucket) throws Exception { + Path bucketPath = new Path("/" + volume + "/" + bucket); + Path dir1 = new Path(bucketPath, "dir1"); + Path file1 = new Path(dir1, "file1"); + Path file2 = new Path(dir1, "file2"); + + Path dir2 = new Path(bucketPath, "dir1/dir2"); + Path file3 = new Path(dir2, "file3"); + + Path dir3 = new Path(bucketPath, "dir3"); + Path file4 = new Path(bucketPath, "file4"); + + Assertions.assertFalse(fs.exists(dir1)); + Assertions.assertFalse(fs.exists(dir2)); + Assertions.assertTrue(fs.exists(dir3)); + Assertions.assertFalse(fs.exists(file1)); + Assertions.assertFalse(fs.exists(file2)); + Assertions.assertFalse(fs.exists(file3)); + Assertions.assertTrue(fs.exists(file4)); + } +} diff --git a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v1/TestS3SDKV1WithRatisStreaming.java b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v1/TestS3SDKV1WithRatisStreaming.java new file mode 100644 index 00000000000..571d4c64908 --- /dev/null +++ b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/s3/awssdk/v1/TestS3SDKV1WithRatisStreaming.java @@ -0,0 +1,55 @@ +/* + * 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.s3.awssdk.v1; + +import org.apache.hadoop.hdds.conf.OzoneConfiguration; +import org.apache.hadoop.hdds.scm.ScmConfigKeys; +import org.apache.hadoop.ozone.OzoneConfigKeys; +import org.apache.hadoop.ozone.om.OMConfigKeys; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Timeout; + +import java.io.IOException; + +/** + * Tests the AWS S3 SDK basic operations with OM Ratis enabled and Streaming Write Pipeline. + */ +@Timeout(300) +public class TestS3SDKV1WithRatisStreaming extends AbstractS3SDKV1Tests { + + @BeforeAll + public static void init() throws Exception { + OzoneConfiguration conf = new OzoneConfiguration(); + conf.setBoolean(ScmConfigKeys.OZONE_SCM_PIPELINE_AUTO_CREATE_FACTOR_ONE, + false); + conf.setBoolean(OMConfigKeys.OZONE_OM_RATIS_ENABLE_KEY, true); + conf.setBoolean(OzoneConfigKeys.OZONE_NETWORK_TOPOLOGY_AWARE_READ_KEY, + true); + conf.setBoolean(OzoneConfigKeys.HDDS_CONTAINER_RATIS_DATASTREAM_ENABLED, true); + conf.setBoolean(OzoneConfigKeys.OZONE_FS_DATASTREAM_ENABLED, true); + // Ensure that all writes use datastream + conf.set(OzoneConfigKeys.OZONE_FS_DATASTREAM_AUTO_THRESHOLD, "0MB"); + startCluster(conf); + } + + @AfterAll + public static void shutdown() throws IOException { + shutdownCluster(); + } +} diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/KeyManagerImpl.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/KeyManagerImpl.java index 7532cf8b324..ccda21efc93 100644 --- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/KeyManagerImpl.java +++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/KeyManagerImpl.java @@ -153,6 +153,8 @@ import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_SNAPSHOT_DIRECTORY_SERVICE_TIMEOUT_DEFAULT; import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_SNAPSHOT_SST_FILTERING_SERVICE_INTERVAL; import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_SNAPSHOT_SST_FILTERING_SERVICE_INTERVAL_DEFAULT; +import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_THREAD_NUMBER_DIR_DELETION; +import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_THREAD_NUMBER_DIR_DELETION_DEFAULT; import static org.apache.hadoop.ozone.om.OzoneManagerUtils.getBucketLayout; import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.BUCKET_NOT_FOUND; import static org.apache.hadoop.ozone.om.exceptions.OMException.ResultCodes.FILE_NOT_FOUND; @@ -257,8 +259,16 @@ public void start(OzoneConfiguration configuration) { OZONE_BLOCK_DELETING_SERVICE_TIMEOUT, OZONE_BLOCK_DELETING_SERVICE_TIMEOUT_DEFAULT, TimeUnit.MILLISECONDS); - dirDeletingService = new DirectoryDeletingService(dirDeleteInterval, - TimeUnit.MILLISECONDS, serviceTimeout, ozoneManager, configuration); + int dirDeletingServiceCorePoolSize = + configuration.getInt(OZONE_THREAD_NUMBER_DIR_DELETION, + OZONE_THREAD_NUMBER_DIR_DELETION_DEFAULT); + if (dirDeletingServiceCorePoolSize <= 0) { + dirDeletingServiceCorePoolSize = 1; + } + dirDeletingService = + new DirectoryDeletingService(dirDeleteInterval, TimeUnit.MILLISECONDS, + serviceTimeout, ozoneManager, configuration, + dirDeletingServiceCorePoolSize); dirDeletingService.start(); } @@ -2052,7 +2062,7 @@ public List getPendingDeletionSubDirs(long volumeId, long bucketId, parentInfo.getObjectID(), ""); long countEntries = 0; - Table dirTable = metadataManager.getDirectoryTable(); + Table dirTable = metadataManager.getDirectoryTable(); try (TableIterator> iterator = dirTable.iterator()) { diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/service/AbstractKeyDeletingService.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/service/AbstractKeyDeletingService.java index 76c16232e39..0ac6c986606 100644 --- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/service/AbstractKeyDeletingService.java +++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/service/AbstractKeyDeletingService.java @@ -280,7 +280,7 @@ private void addToMap(Map, List> map, String object protected void submitPurgePaths(List requests, String snapTableKey, - UUID expectedPreviousSnapshotId) { + UUID expectedPreviousSnapshotId, long rnCnt) { OzoneManagerProtocolProtos.PurgeDirectoriesRequest.Builder purgeDirRequest = OzoneManagerProtocolProtos.PurgeDirectoriesRequest.newBuilder(); @@ -305,7 +305,7 @@ protected void submitPurgePaths(List requests, // Submit Purge paths request to OM try { - OzoneManagerRatisUtils.submitRequest(ozoneManager, omRequest, clientId, runCount.get()); + OzoneManagerRatisUtils.submitRequest(ozoneManager, omRequest, clientId, rnCnt); } catch (ServiceException e) { LOG.error("PurgePaths request failed. Will retry at next run.", e); } @@ -400,7 +400,7 @@ public long optimizeDirDeletesAndSubmitRequest(long remainNum, List purgePathRequestList, String snapTableKey, long startTime, int remainingBufLimit, KeyManager keyManager, - UUID expectedPreviousSnapshotId) { + UUID expectedPreviousSnapshotId, long rnCnt) { // Optimization to handle delete sub-dir and keys to remove quickly // This case will be useful to handle when depth of directory is high @@ -442,7 +442,7 @@ public long optimizeDirDeletesAndSubmitRequest(long remainNum, } if (!purgePathRequestList.isEmpty()) { - submitPurgePaths(purgePathRequestList, snapTableKey, expectedPreviousSnapshotId); + submitPurgePaths(purgePathRequestList, snapTableKey, expectedPreviousSnapshotId, rnCnt); } if (dirNum != 0 || subDirNum != 0 || subFileNum != 0) { @@ -455,7 +455,7 @@ public long optimizeDirDeletesAndSubmitRequest(long remainNum, "DeletedDirectoryTable, iteration elapsed: {}ms," + " totalRunCount: {}", dirNum, subdirDelNum, subFileNum, (subDirNum - subdirDelNum), - Time.monotonicNow() - startTime, getRunCount()); + Time.monotonicNow() - startTime, rnCnt); } return remainNum; } diff --git a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/service/DirectoryDeletingService.java b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/service/DirectoryDeletingService.java index 09f4a8f8a3d..a8270f92f2b 100644 --- a/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/service/DirectoryDeletingService.java +++ b/hadoop-ozone/ozone-manager/src/main/java/org/apache/hadoop/ozone/om/service/DirectoryDeletingService.java @@ -23,6 +23,7 @@ import org.apache.hadoop.hdds.utils.BackgroundTask; import org.apache.hadoop.hdds.utils.BackgroundTaskQueue; import org.apache.hadoop.hdds.utils.BackgroundTaskResult; +import org.apache.hadoop.hdds.utils.IOUtils; 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; @@ -49,6 +50,7 @@ import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_PATH_DELETING_LIMIT_PER_TASK; import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_PATH_DELETING_LIMIT_PER_TASK_DEFAULT; @@ -74,10 +76,10 @@ public class DirectoryDeletingService extends AbstractKeyDeletingService { public static final Logger LOG = LoggerFactory.getLogger(DirectoryDeletingService.class); - // Use only a single thread for DirDeletion. Multiple threads would read - // or write to same tables and can send deletion requests for same key - // multiple times. - private static final int DIR_DELETING_CORE_POOL_SIZE = 1; + // Using multi thread for DirDeletion. Multiple threads would read + // from parent directory info from deleted directory table concurrently + // and send deletion requests. + private final int dirDeletingCorePoolSize; private static final int MIN_ERR_LIMIT_PER_TASK = 1000; // Number of items(dirs/files) to be batched in an iteration. @@ -86,11 +88,15 @@ public class DirectoryDeletingService extends AbstractKeyDeletingService { private final AtomicBoolean suspended; private AtomicBoolean isRunningOnAOS; + private final DeletedDirSupplier deletedDirSupplier; + + private AtomicInteger taskCount = new AtomicInteger(0); + public DirectoryDeletingService(long interval, TimeUnit unit, long serviceTimeout, OzoneManager ozoneManager, - OzoneConfiguration configuration) { + OzoneConfiguration configuration, int dirDeletingServiceCorePoolSize) { super(DirectoryDeletingService.class.getSimpleName(), interval, unit, - DIR_DELETING_CORE_POOL_SIZE, serviceTimeout, ozoneManager, null); + dirDeletingServiceCorePoolSize, serviceTimeout, ozoneManager, null); this.pathLimitPerTask = configuration .getInt(OZONE_PATH_DELETING_LIMIT_PER_TASK, OZONE_PATH_DELETING_LIMIT_PER_TASK_DEFAULT); @@ -102,6 +108,9 @@ public DirectoryDeletingService(long interval, TimeUnit unit, this.ratisByteLimit = (int) (limit * 0.9); this.suspended = new AtomicBoolean(false); this.isRunningOnAOS = new AtomicBoolean(false); + this.dirDeletingCorePoolSize = dirDeletingServiceCorePoolSize; + deletedDirSupplier = new DeletedDirSupplier(); + taskCount.set(0); } private boolean shouldRun() { @@ -116,6 +125,10 @@ public boolean isRunningOnAOS() { return isRunningOnAOS.get(); } + public AtomicInteger getTaskCount() { + return taskCount; + } + /** * Suspend the service. */ @@ -135,10 +148,55 @@ public void resume() { @Override public BackgroundTaskQueue getTasks() { BackgroundTaskQueue queue = new BackgroundTaskQueue(); - queue.add(new DirectoryDeletingService.DirDeletingTask(this)); + if (taskCount.get() > 0) { + LOG.info("{} Directory deleting task(s) already in progress.", + taskCount.get()); + return queue; + } + try { + deletedDirSupplier.reInitItr(); + } catch (IOException ex) { + LOG.error("Unable to get the iterator.", ex); + return queue; + } + taskCount.set(dirDeletingCorePoolSize); + for (int i = 0; i < dirDeletingCorePoolSize; i++) { + queue.add(new DirectoryDeletingService.DirDeletingTask(this)); + } return queue; } + @Override + public void shutdown() { + super.shutdown(); + deletedDirSupplier.closeItr(); + } + + private final class DeletedDirSupplier { + private TableIterator> + deleteTableIterator; + + private synchronized Table.KeyValue get() + throws IOException { + if (deleteTableIterator.hasNext()) { + return deleteTableIterator.next(); + } + return null; + } + + private synchronized void closeItr() { + IOUtils.closeQuietly(deleteTableIterator); + deleteTableIterator = null; + } + + private synchronized void reInitItr() throws IOException { + closeItr(); + deleteTableIterator = + getOzoneManager().getMetadataManager().getDeletedDirTable() + .iterator(); + } + } + private final class DirDeletingTask implements BackgroundTask { private final DirectoryDeletingService directoryDeletingService; @@ -153,89 +211,93 @@ public int getPriority() { @Override public BackgroundTaskResult call() { - if (shouldRun()) { - if (LOG.isDebugEnabled()) { - LOG.debug("Running DirectoryDeletingService"); - } - isRunningOnAOS.set(true); - getRunCount().incrementAndGet(); - long dirNum = 0L; - long subDirNum = 0L; - long subFileNum = 0L; - long remainNum = pathLimitPerTask; - int consumedSize = 0; - List purgePathRequestList = new ArrayList<>(); - List> allSubDirList - = new ArrayList<>((int) remainNum); - - Table.KeyValue pendingDeletedDirInfo; - - try (TableIterator> - deleteTableIterator = getOzoneManager().getMetadataManager(). - getDeletedDirTable().iterator()) { + try { + if (shouldRun()) { + if (LOG.isDebugEnabled()) { + LOG.debug("Running DirectoryDeletingService"); + } + isRunningOnAOS.set(true); + long rnCnt = getRunCount().incrementAndGet(); + long dirNum = 0L; + long subDirNum = 0L; + long subFileNum = 0L; + long remainNum = pathLimitPerTask; + int consumedSize = 0; + List purgePathRequestList = new ArrayList<>(); + List> allSubDirList = + new ArrayList<>((int) remainNum); + + Table.KeyValue pendingDeletedDirInfo; // This is to avoid race condition b/w purge request and snapshot chain updation. For AOS taking the global // snapshotId since AOS could process multiple buckets in one iteration. - UUID expectedPreviousSnapshotId = - ((OmMetadataManagerImpl)getOzoneManager().getMetadataManager()).getSnapshotChainManager() - .getLatestGlobalSnapshotId(); - - long startTime = Time.monotonicNow(); - while (remainNum > 0 && deleteTableIterator.hasNext()) { - pendingDeletedDirInfo = deleteTableIterator.next(); - // Do not reclaim if the directory is still being referenced by - // the previous snapshot. - if (previousSnapshotHasDir(pendingDeletedDirInfo)) { - continue; - } + try { + UUID expectedPreviousSnapshotId = + ((OmMetadataManagerImpl) getOzoneManager().getMetadataManager()).getSnapshotChainManager() + .getLatestGlobalSnapshotId(); - PurgePathRequest request = prepareDeleteDirRequest( - remainNum, pendingDeletedDirInfo.getValue(), - pendingDeletedDirInfo.getKey(), allSubDirList, - getOzoneManager().getKeyManager()); - if (isBufferLimitCrossed(ratisByteLimit, consumedSize, - request.getSerializedSize())) { - if (purgePathRequestList.size() != 0) { - // if message buffer reaches max limit, avoid sending further - remainNum = 0; + long startTime = Time.monotonicNow(); + while (remainNum > 0) { + pendingDeletedDirInfo = getPendingDeletedDirInfo(); + if (pendingDeletedDirInfo == null) { break; } - // if directory itself is having a lot of keys / files, - // reduce capacity to minimum level - remainNum = MIN_ERR_LIMIT_PER_TASK; - request = prepareDeleteDirRequest( - remainNum, pendingDeletedDirInfo.getValue(), + // Do not reclaim if the directory is still being referenced by + // the previous snapshot. + if (previousSnapshotHasDir(pendingDeletedDirInfo)) { + continue; + } + + PurgePathRequest request = prepareDeleteDirRequest(remainNum, + pendingDeletedDirInfo.getValue(), pendingDeletedDirInfo.getKey(), allSubDirList, getOzoneManager().getKeyManager()); + if (isBufferLimitCrossed(ratisByteLimit, consumedSize, + request.getSerializedSize())) { + if (purgePathRequestList.size() != 0) { + // if message buffer reaches max limit, avoid sending further + remainNum = 0; + break; + } + // if directory itself is having a lot of keys / files, + // reduce capacity to minimum level + remainNum = MIN_ERR_LIMIT_PER_TASK; + request = prepareDeleteDirRequest(remainNum, + pendingDeletedDirInfo.getValue(), + pendingDeletedDirInfo.getKey(), allSubDirList, + getOzoneManager().getKeyManager()); + } + consumedSize += request.getSerializedSize(); + purgePathRequestList.add(request); + // reduce remain count for self, sub-files, and sub-directories + remainNum = remainNum - 1; + remainNum = remainNum - request.getDeletedSubFilesCount(); + remainNum = remainNum - request.getMarkDeletedSubDirsCount(); + // Count up the purgeDeletedDir, subDirs and subFiles + if (request.getDeletedDir() != null && !request.getDeletedDir() + .isEmpty()) { + dirNum++; + } + subDirNum += request.getMarkDeletedSubDirsCount(); + subFileNum += request.getDeletedSubFilesCount(); } - consumedSize += request.getSerializedSize(); - purgePathRequestList.add(request); - // reduce remain count for self, sub-files, and sub-directories - remainNum = remainNum - 1; - remainNum = remainNum - request.getDeletedSubFilesCount(); - remainNum = remainNum - request.getMarkDeletedSubDirsCount(); - // Count up the purgeDeletedDir, subDirs and subFiles - if (request.getDeletedDir() != null - && !request.getDeletedDir().isEmpty()) { - dirNum++; - } - subDirNum += request.getMarkDeletedSubDirsCount(); - subFileNum += request.getDeletedSubFilesCount(); - } + optimizeDirDeletesAndSubmitRequest(remainNum, dirNum, subDirNum, + subFileNum, allSubDirList, purgePathRequestList, null, + startTime, ratisByteLimit - consumedSize, + getOzoneManager().getKeyManager(), expectedPreviousSnapshotId, + rnCnt); - optimizeDirDeletesAndSubmitRequest( - remainNum, dirNum, subDirNum, subFileNum, - allSubDirList, purgePathRequestList, null, startTime, - ratisByteLimit - consumedSize, - getOzoneManager().getKeyManager(), expectedPreviousSnapshotId); - - } catch (IOException e) { - LOG.error("Error while running delete directories and files " + - "background task. Will retry at next run.", e); - } - isRunningOnAOS.set(false); - synchronized (directoryDeletingService) { - this.directoryDeletingService.notify(); + } catch (IOException e) { + LOG.error( + "Error while running delete directories and files " + "background task. Will retry at next run.", + e); + } + isRunningOnAOS.set(false); + synchronized (directoryDeletingService) { + this.directoryDeletingService.notify(); + } } + } finally { + taskCount.getAndDecrement(); } // place holder by returning empty results of this call back. return BackgroundTaskResult.EmptyTaskResult.newResult(); @@ -301,4 +363,9 @@ private boolean previousSnapshotHasDir( } } + public KeyValue getPendingDeletedDirInfo() + throws IOException { + return deletedDirSupplier.get(); + } + } diff --git a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/service/TestDirectoryDeletingService.java b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/service/TestDirectoryDeletingService.java index 04e8efa7b79..681b24b8e42 100644 --- a/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/service/TestDirectoryDeletingService.java +++ b/hadoop-ozone/ozone-manager/src/test/java/org/apache/hadoop/ozone/om/service/TestDirectoryDeletingService.java @@ -51,6 +51,9 @@ import static org.apache.hadoop.hdds.protocol.proto.HddsProtos.ReplicationFactor.ONE; import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_DIR_DELETING_SERVICE_INTERVAL; import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_PATH_DELETING_LIMIT_PER_TASK_DEFAULT; +import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_PATH_DELETING_LIMIT_PER_TASK; +import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_THREAD_NUMBER_DIR_DELETION_DEFAULT; +import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_THREAD_NUMBER_DIR_DELETION; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -202,14 +205,19 @@ public void testDeleteDirectoryFlatDirsHavingNoChilds() throws Exception { .setReplicationConfig(StandaloneReplicationConfig.getInstance(ONE)) .setDataSize(0).setRecursive(true).build(); writeClient.deleteKey(delArgs); + int pathDelLimit = conf.getInt(OZONE_PATH_DELETING_LIMIT_PER_TASK, + OZONE_PATH_DELETING_LIMIT_PER_TASK_DEFAULT); + int numThread = conf.getInt(OZONE_THREAD_NUMBER_DIR_DELETION, + OZONE_THREAD_NUMBER_DIR_DELETION_DEFAULT); // check if difference between each run should not cross the directory deletion limit // and wait till all dir is removed GenericTestUtils.waitFor(() -> { delDirCnt[1] = dirDeletingService.getDeletedDirsCount(); - assertTrue(delDirCnt[1] - delDirCnt[0] <= OZONE_PATH_DELETING_LIMIT_PER_TASK_DEFAULT, + assertTrue( + delDirCnt[1] - delDirCnt[0] <= ((long) pathDelLimit * numThread), "base: " + delDirCnt[0] + ", new: " + delDirCnt[1]); - delDirCnt[0] = delDirCnt[1]; + delDirCnt[0] = delDirCnt[1]; return dirDeletingService.getDeletedDirsCount() >= dirCreatesCount; }, 500, 300000); } diff --git a/hadoop-ozone/pom.xml b/hadoop-ozone/pom.xml index a8b32c686a0..22cd10085dd 100644 --- a/hadoop-ozone/pom.xml +++ b/hadoop-ozone/pom.xml @@ -49,13 +49,6 @@ s3-secret-store - - - apache.snapshots.https - https://repository.apache.org/content/repositories/snapshots - - - diff --git a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectEndpoint.java b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectEndpoint.java index 0a928981613..9311fb7fa4b 100644 --- a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectEndpoint.java +++ b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectEndpoint.java @@ -323,7 +323,7 @@ public Response put( perf.appendStreamMode(); Pair keyWriteResult = ObjectEndpointStreaming .put(bucket, keyPath, length, replicationConfig, chunkSize, - customMetadata, digestInputStream, perf); + customMetadata, tags, digestInputStream, perf); eTag = keyWriteResult.getKey(); putLength = keyWriteResult.getValue(); } else { diff --git a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectEndpointStreaming.java b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectEndpointStreaming.java index cb9499aa20d..f5d185fc76b 100644 --- a/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectEndpointStreaming.java +++ b/hadoop-ozone/s3gateway/src/main/java/org/apache/hadoop/ozone/s3/endpoint/ObjectEndpointStreaming.java @@ -61,12 +61,13 @@ public static Pair put( OzoneBucket bucket, String keyPath, long length, ReplicationConfig replicationConfig, int chunkSize, Map keyMetadata, + Map tags, DigestInputStream body, PerformanceStringBuilder perf) throws IOException, OS3Exception { try { return putKeyWithStream(bucket, keyPath, - length, chunkSize, replicationConfig, keyMetadata, body, perf); + length, chunkSize, replicationConfig, keyMetadata, tags, body, perf); } catch (IOException ex) { LOG.error("Exception occurred in PutObject", ex); if (ex instanceof OMException) { @@ -97,13 +98,14 @@ public static Pair putKeyWithStream( int bufferSize, ReplicationConfig replicationConfig, Map keyMetadata, + Map tags, DigestInputStream body, PerformanceStringBuilder perf) throws IOException { long startNanos = Time.monotonicNowNanos(); long writeLen; String eTag; try (OzoneDataStreamOutput streamOutput = bucket.createStreamKey(keyPath, - length, replicationConfig, keyMetadata)) { + length, replicationConfig, keyMetadata, tags)) { long metadataLatencyNs = METRICS.updatePutKeyMetadataStats(startNanos); writeLen = writeToStreamOutput(streamOutput, body, bufferSize, length); eTag = DatatypeConverter.printHexBinary(body.getMessageDigest().digest()) diff --git a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/repair/om/FSORepairCLI.java b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/repair/om/FSORepairCLI.java new file mode 100644 index 00000000000..5a217e9f2de --- /dev/null +++ b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/repair/om/FSORepairCLI.java @@ -0,0 +1,78 @@ +/* + * 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.repair.om; + +import picocli.CommandLine; + +import java.util.concurrent.Callable; + +/** + * Parser for scm.db file. + */ +@CommandLine.Command( + name = "fso-tree", + description = "Identify and repair a disconnected FSO tree by marking unreferenced entries for deletion. " + + "OM should be stopped while this tool is run." +) +public class FSORepairCLI implements Callable { + + @CommandLine.Option(names = {"--db"}, + required = true, + description = "Path to OM RocksDB") + private String dbPath; + + @CommandLine.Option(names = {"-r", "--repair"}, + defaultValue = "false", + description = "Run in repair mode to move unreferenced files and directories to deleted tables.") + private boolean repair; + + @CommandLine.Option(names = {"-v", "--volume"}, + description = "Filter by volume name. Add '/' before the volume name.") + private String volume; + + @CommandLine.Option(names = {"-b", "--bucket"}, + description = "Filter by bucket name") + private String bucket; + + @CommandLine.Option(names = {"--verbose"}, + description = "Verbose output. Show all intermediate steps and deleted keys info.") + private boolean verbose; + + @Override + public Void call() throws Exception { + if (repair) { + System.out.println("FSO Repair Tool is running in repair mode"); + } else { + System.out.println("FSO Repair Tool is running in debug mode"); + } + try { + FSORepairTool + repairTool = new FSORepairTool(dbPath, repair, volume, bucket, verbose); + repairTool.run(); + } catch (Exception ex) { + throw new IllegalArgumentException("FSO repair failed: " + ex.getMessage()); + } + + if (verbose) { + System.out.println("FSO repair finished."); + } + + return null; + } +} diff --git a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/repair/om/FSORepairTool.java b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/repair/om/FSORepairTool.java new file mode 100644 index 00000000000..7e0fb23f5aa --- /dev/null +++ b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/repair/om/FSORepairTool.java @@ -0,0 +1,710 @@ +/* + * 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.repair.om; + +import org.apache.commons.io.FileUtils; +import org.apache.hadoop.hdds.conf.ConfigurationSource; +import org.apache.hadoop.hdds.conf.OzoneConfiguration; +import org.apache.hadoop.hdds.utils.db.Table; +import org.apache.hadoop.hdds.utils.db.DBStore; +import org.apache.hadoop.hdds.utils.db.DBStoreBuilder; +import org.apache.hadoop.hdds.utils.db.TableIterator; +import org.apache.hadoop.hdds.utils.db.BatchOperation; +import org.apache.hadoop.ozone.OmUtils; +import org.apache.hadoop.ozone.om.OmMetadataManagerImpl; +import org.apache.hadoop.ozone.om.helpers.BucketLayout; +import org.apache.hadoop.ozone.om.helpers.OmBucketInfo; +import org.apache.hadoop.ozone.om.helpers.OmDirectoryInfo; +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.helpers.WithObjectID; +import org.apache.hadoop.ozone.om.request.file.OMFileRequest; +import org.apache.ratis.util.Preconditions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Objects; +import java.util.Stack; + +import static org.apache.hadoop.ozone.OzoneConsts.OM_KEY_PREFIX; + +/** + * Base Tool to identify and repair disconnected FSO trees across all buckets. + * This tool logs information about reachable, unreachable and unreferenced files and directories in debug mode + * and moves these unreferenced files and directories to the deleted tables in repair mode. + + * If deletes are still in progress (the deleted directory table is not empty), the tool + * reports that the tree is unreachable, even though pending deletes would fix the issue. + * If not, the tool reports them as unreferenced and deletes them in repair mode. + + * Before using the tool, make sure all OMs are stopped, and that all Ratis logs have been flushed to the OM DB. + * This can be done using `ozone admin prepare` before running the tool, and `ozone admin + * cancelprepare` when done. + + * The tool will run a DFS from each bucket, and save all reachable directories as keys in a new temporary RocksDB + * instance called "reachable.db" in the same directory as om.db. + * It will then scan the entire file and directory tables for each bucket to see if each object's parent is in the + * reachable table of reachable.db. The reachable table will be dropped and recreated for each bucket. + * The tool is idempotent. reachable.db will not be deleted automatically when the tool finishes, + * in case users want to manually inspect it. It can be safely deleted once the tool finishes. + */ +public class FSORepairTool { + public static final Logger LOG = LoggerFactory.getLogger(FSORepairTool.class); + + private final String omDBPath; + private final DBStore store; + private final Table volumeTable; + private final Table bucketTable; + private final Table directoryTable; + private final Table fileTable; + private final Table deletedDirectoryTable; + private final Table deletedTable; + private final Table snapshotInfoTable; + private final String volumeFilter; + private final String bucketFilter; + private static final String REACHABLE_TABLE = "reachable"; + private DBStore reachableDB; + private final ReportStatistics reachableStats; + private final ReportStatistics unreachableStats; + private final ReportStatistics unreferencedStats; + private final boolean repair; + private final boolean verbose; + + public FSORepairTool(String dbPath, boolean repair, String volume, String bucket, boolean verbose) + throws IOException { + this(getStoreFromPath(dbPath), dbPath, repair, volume, bucket, verbose); + } + + /** + * Allows passing RocksDB instance from a MiniOzoneCluster directly to this class for testing. + */ + public FSORepairTool(DBStore dbStore, String dbPath, boolean repair, String volume, String bucket, boolean verbose) + throws IOException { + this.reachableStats = new ReportStatistics(0, 0, 0); + this.unreachableStats = new ReportStatistics(0, 0, 0); + this.unreferencedStats = new ReportStatistics(0, 0, 0); + + this.store = dbStore; + this.omDBPath = dbPath; + this.repair = repair; + this.volumeFilter = volume; + this.bucketFilter = bucket; + this.verbose = verbose; + volumeTable = store.getTable(OmMetadataManagerImpl.VOLUME_TABLE, + String.class, + OmVolumeArgs.class); + bucketTable = store.getTable(OmMetadataManagerImpl.BUCKET_TABLE, + String.class, + OmBucketInfo.class); + directoryTable = store.getTable(OmMetadataManagerImpl.DIRECTORY_TABLE, + String.class, + OmDirectoryInfo.class); + fileTable = store.getTable(OmMetadataManagerImpl.FILE_TABLE, + String.class, + OmKeyInfo.class); + deletedDirectoryTable = store.getTable(OmMetadataManagerImpl.DELETED_DIR_TABLE, + String.class, + OmKeyInfo.class); + deletedTable = store.getTable(OmMetadataManagerImpl.DELETED_TABLE, + String.class, + RepeatedOmKeyInfo.class); + snapshotInfoTable = store.getTable(OmMetadataManagerImpl.SNAPSHOT_INFO_TABLE, + String.class, + SnapshotInfo.class); + } + + protected static DBStore getStoreFromPath(String dbPath) throws IOException { + File omDBFile = new File(dbPath); + if (!omDBFile.exists() || !omDBFile.isDirectory()) { + throw new IOException(String.format("Specified OM DB instance %s does " + + "not exist or is not a RocksDB directory.", dbPath)); + } + // Load RocksDB and tables needed. + return OmMetadataManagerImpl.loadDB(new OzoneConfiguration(), new File(dbPath).getParentFile(), -1); + } + + public FSORepairTool.Report run() throws Exception { + try { + if (bucketFilter != null && volumeFilter == null) { + System.out.println("--bucket flag cannot be used without specifying --volume."); + return null; + } + + if (volumeFilter != null) { + OmVolumeArgs volumeArgs = volumeTable.getIfExist(volumeFilter); + if (volumeArgs == null) { + System.out.println("Volume '" + volumeFilter + "' does not exist."); + return null; + } + } + + // Iterate all volumes or a specific volume if specified + try (TableIterator> + volumeIterator = volumeTable.iterator()) { + try { + openReachableDB(); + } catch (IOException e) { + System.out.println("Failed to open reachable database: " + e.getMessage()); + throw e; + } + while (volumeIterator.hasNext()) { + Table.KeyValue volumeEntry = volumeIterator.next(); + String volumeKey = volumeEntry.getKey(); + + if (volumeFilter != null && !volumeFilter.equals(volumeKey)) { + continue; + } + + System.out.println("Processing volume: " + volumeKey); + + if (bucketFilter != null) { + OmBucketInfo bucketInfo = bucketTable.getIfExist(volumeKey + "/" + bucketFilter); + if (bucketInfo == null) { + //Bucket does not exist in the volume + System.out.println("Bucket '" + bucketFilter + "' does not exist in volume '" + volumeKey + "'."); + return null; + } + + if (bucketInfo.getBucketLayout() != BucketLayout.FILE_SYSTEM_OPTIMIZED) { + System.out.println("Skipping non-FSO bucket " + bucketFilter); + continue; + } + + processBucket(volumeEntry.getValue(), bucketInfo); + } else { + + // Iterate all buckets in the volume. + try (TableIterator> + bucketIterator = bucketTable.iterator()) { + bucketIterator.seek(volumeKey); + while (bucketIterator.hasNext()) { + Table.KeyValue bucketEntry = bucketIterator.next(); + String bucketKey = bucketEntry.getKey(); + OmBucketInfo bucketInfo = bucketEntry.getValue(); + + if (bucketInfo.getBucketLayout() != BucketLayout.FILE_SYSTEM_OPTIMIZED) { + System.out.println("Skipping non-FSO bucket " + bucketKey); + continue; + } + + // Stop this loop once we have seen all buckets in the current + // volume. + if (!bucketKey.startsWith(volumeKey)) { + break; + } + + processBucket(volumeEntry.getValue(), bucketInfo); + } + } + } + } + } + } catch (IOException e) { + System.out.println("An error occurred while processing" + e.getMessage()); + throw e; + } finally { + closeReachableDB(); + store.close(); + } + + return buildReportAndLog(); + } + + private boolean checkIfSnapshotExistsForBucket(String volumeName, String bucketName) throws IOException { + if (snapshotInfoTable == null) { + return false; + } + + try (TableIterator> iterator = + snapshotInfoTable.iterator()) { + while (iterator.hasNext()) { + SnapshotInfo snapshotInfo = iterator.next().getValue(); + String snapshotPath = (volumeName + "/" + bucketName).replaceFirst("^/", ""); + if (snapshotInfo.getSnapshotPath().equals(snapshotPath)) { + return true; + } + } + } + return false; + } + + private void processBucket(OmVolumeArgs volume, OmBucketInfo bucketInfo) throws IOException { + System.out.println("Processing bucket: " + volume.getVolume() + "/" + bucketInfo.getBucketName()); + if (checkIfSnapshotExistsForBucket(volume.getVolume(), bucketInfo.getBucketName())) { + if (!repair) { + System.out.println( + "Snapshot detected in bucket '" + volume.getVolume() + "/" + bucketInfo.getBucketName() + "'. "); + } else { + System.out.println( + "Skipping repair for bucket '" + volume.getVolume() + "/" + bucketInfo.getBucketName() + "' " + + "due to snapshot presence."); + return; + } + } + markReachableObjectsInBucket(volume, bucketInfo); + handleUnreachableAndUnreferencedObjects(volume, bucketInfo); + } + + private Report buildReportAndLog() { + Report report = new Report.Builder() + .setReachable(reachableStats) + .setUnreachable(unreachableStats) + .setUnreferenced(unreferencedStats) + .build(); + + System.out.println("\n" + report); + return report; + } + + private void markReachableObjectsInBucket(OmVolumeArgs volume, OmBucketInfo bucket) throws IOException { + // Only put directories in the stack. + // Directory keys should have the form /volumeID/bucketID/parentID/name. + Stack dirKeyStack = new Stack<>(); + + // Since the tool uses parent directories to check for reachability, add + // a reachable entry for the bucket as well. + addReachableEntry(volume, bucket, bucket); + // Initialize the stack with all immediate child directories of the + // bucket, and mark them all as reachable. + Collection childDirs = getChildDirectoriesAndMarkAsReachable(volume, bucket, bucket); + dirKeyStack.addAll(childDirs); + + while (!dirKeyStack.isEmpty()) { + // Get one directory and process its immediate children. + String currentDirKey = dirKeyStack.pop(); + OmDirectoryInfo currentDir = directoryTable.get(currentDirKey); + if (currentDir == null) { + System.out.println("Directory key" + currentDirKey + "to be processed was not found in the directory table."); + continue; + } + + // TODO revisit this for a more memory efficient implementation, + // possibly making better use of RocksDB iterators. + childDirs = getChildDirectoriesAndMarkAsReachable(volume, bucket, currentDir); + dirKeyStack.addAll(childDirs); + } + } + + private boolean isDirectoryInDeletedDirTable(String dirKey) throws IOException { + return deletedDirectoryTable.isExist(dirKey); + } + + private boolean isFileKeyInDeletedTable(String fileKey) throws IOException { + return deletedTable.isExist(fileKey); + } + + private void handleUnreachableAndUnreferencedObjects(OmVolumeArgs volume, OmBucketInfo bucket) throws IOException { + // Check for unreachable and unreferenced directories in the bucket. + String bucketPrefix = OM_KEY_PREFIX + + volume.getObjectID() + + OM_KEY_PREFIX + + bucket.getObjectID(); + + try (TableIterator> dirIterator = + directoryTable.iterator()) { + dirIterator.seek(bucketPrefix); + while (dirIterator.hasNext()) { + Table.KeyValue dirEntry = dirIterator.next(); + String dirKey = dirEntry.getKey(); + + // Only search directories in this bucket. + if (!dirKey.startsWith(bucketPrefix)) { + break; + } + + if (!isReachable(dirKey)) { + if (!isDirectoryInDeletedDirTable(dirKey)) { + System.out.println("Found unreferenced directory: " + dirKey); + unreferencedStats.addDir(); + + if (!repair) { + if (verbose) { + System.out.println("Marking unreferenced directory " + dirKey + " for deletion."); + } + } else { + System.out.println("Deleting unreferenced directory " + dirKey); + OmDirectoryInfo dirInfo = dirEntry.getValue(); + markDirectoryForDeletion(volume.getVolume(), bucket.getBucketName(), dirKey, dirInfo); + } + } else { + unreachableStats.addDir(); + } + } + } + } + + // Check for unreachable and unreferenced files + try (TableIterator> + fileIterator = fileTable.iterator()) { + fileIterator.seek(bucketPrefix); + while (fileIterator.hasNext()) { + Table.KeyValue fileEntry = fileIterator.next(); + String fileKey = fileEntry.getKey(); + // Only search files in this bucket. + if (!fileKey.startsWith(bucketPrefix)) { + break; + } + + OmKeyInfo fileInfo = fileEntry.getValue(); + if (!isReachable(fileKey)) { + if (!isFileKeyInDeletedTable(fileKey)) { + System.out.println("Found unreferenced file: " + fileKey); + unreferencedStats.addFile(fileInfo.getDataSize()); + + if (!repair) { + if (verbose) { + System.out.println("Marking unreferenced file " + fileKey + " for deletion." + fileKey); + } + } else { + System.out.println("Deleting unreferenced file " + fileKey); + markFileForDeletion(fileKey, fileInfo); + } + } else { + unreachableStats.addFile(fileInfo.getDataSize()); + } + } else { + // NOTE: We are deserializing the proto of every reachable file + // just to log it's size. If we don't need this information we could + // save time by skipping this step. + reachableStats.addFile(fileInfo.getDataSize()); + } + } + } + } + + protected void markFileForDeletion(String fileKey, OmKeyInfo fileInfo) throws IOException { + try (BatchOperation batch = store.initBatchOperation()) { + fileTable.deleteWithBatch(batch, fileKey); + + RepeatedOmKeyInfo originalRepeatedKeyInfo = deletedTable.get(fileKey); + RepeatedOmKeyInfo updatedRepeatedOmKeyInfo = OmUtils.prepareKeyForDelete( + fileInfo, fileInfo.getUpdateID(), true); + // NOTE: The FSO code seems to write the open key entry with the whole + // path, using the object's names instead of their ID. This would only + // be possible when the file is deleted explicitly, and not part of a + // directory delete. It is also not possible here if the file's parent + // is gone. The name of the key does not matter so just use IDs. + deletedTable.putWithBatch(batch, fileKey, updatedRepeatedOmKeyInfo); + if (verbose) { + System.out.println("Added entry " + fileKey + " to open key table: " + updatedRepeatedOmKeyInfo); + } + store.commitBatchOperation(batch); + } + } + + protected void markDirectoryForDeletion(String volumeName, String bucketName, + String dirKeyName, OmDirectoryInfo dirInfo) throws IOException { + try (BatchOperation batch = store.initBatchOperation()) { + directoryTable.deleteWithBatch(batch, dirKeyName); + // HDDS-7592: Make directory entries in deleted dir table unique. + String deleteDirKeyName = dirKeyName + OM_KEY_PREFIX + dirInfo.getObjectID(); + + // Convert the directory to OmKeyInfo for deletion. + OmKeyInfo dirAsKeyInfo = OMFileRequest.getOmKeyInfo(volumeName, bucketName, dirInfo, dirInfo.getName()); + deletedDirectoryTable.putWithBatch(batch, deleteDirKeyName, dirAsKeyInfo); + + store.commitBatchOperation(batch); + } + } + + private Collection getChildDirectoriesAndMarkAsReachable(OmVolumeArgs volume, OmBucketInfo bucket, + WithObjectID currentDir) throws IOException { + + Collection childDirs = new ArrayList<>(); + + try (TableIterator> + dirIterator = directoryTable.iterator()) { + String dirPrefix = buildReachableKey(volume, bucket, currentDir); + // Start searching the directory table at the current directory's + // prefix to get its immediate children. + dirIterator.seek(dirPrefix); + while (dirIterator.hasNext()) { + Table.KeyValue childDirEntry = dirIterator.next(); + String childDirKey = childDirEntry.getKey(); + // Stop processing once we have seen all immediate children of this + // directory. + if (!childDirKey.startsWith(dirPrefix)) { + break; + } + // This directory was reached by search. + addReachableEntry(volume, bucket, childDirEntry.getValue()); + childDirs.add(childDirKey); + reachableStats.addDir(); + } + } + + return childDirs; + } + + /** + * Add the specified object to the reachable table, indicating it is part + * of the connected FSO tree. + */ + private void addReachableEntry(OmVolumeArgs volume, OmBucketInfo bucket, WithObjectID object) throws IOException { + String reachableKey = buildReachableKey(volume, bucket, object); + // No value is needed for this table. + reachableDB.getTable(REACHABLE_TABLE, String.class, byte[].class).put(reachableKey, new byte[]{}); + } + + /** + * Build an entry in the reachable table for the current object, which + * could be a bucket, file or directory. + */ + private static String buildReachableKey(OmVolumeArgs volume, OmBucketInfo bucket, WithObjectID object) { + return OM_KEY_PREFIX + + volume.getObjectID() + + OM_KEY_PREFIX + + bucket.getObjectID() + + OM_KEY_PREFIX + + object.getObjectID(); + } + + /** + * + * @param fileOrDirKey The key of a file or directory in RocksDB. + * @return true if the entry's parent is in the reachable table. + */ + protected boolean isReachable(String fileOrDirKey) throws IOException { + String reachableParentKey = buildReachableParentKey(fileOrDirKey); + + return reachableDB.getTable(REACHABLE_TABLE, String.class, byte[].class).get(reachableParentKey) != null; + } + + /** + * Build an entry in the reachable table for the current object's parent + * object. The object could be a file or directory. + */ + private static String buildReachableParentKey(String fileOrDirKey) { + String[] keyParts = fileOrDirKey.split(OM_KEY_PREFIX); + // Should be /volID/bucketID/parentID/name + // The first part will be blank since key begins with a slash. + Preconditions.assertTrue(keyParts.length >= 4); + String volumeID = keyParts[1]; + String bucketID = keyParts[2]; + String parentID = keyParts[3]; + + return OM_KEY_PREFIX + + volumeID + + OM_KEY_PREFIX + + bucketID + + OM_KEY_PREFIX + + parentID; + } + + private void openReachableDB() throws IOException { + File reachableDBFile = new File(new File(omDBPath).getParentFile(), "reachable.db"); + System.out.println("Creating database of reachable directories at " + reachableDBFile); + // Delete the DB from the last run if it exists. + if (reachableDBFile.exists()) { + FileUtils.deleteDirectory(reachableDBFile); + } + + ConfigurationSource conf = new OzoneConfiguration(); + reachableDB = DBStoreBuilder.newBuilder(conf) + .setName("reachable.db") + .setPath(reachableDBFile.getParentFile().toPath()) + .addTable(REACHABLE_TABLE) + .build(); + } + + private void closeReachableDB() throws IOException { + if (reachableDB != null) { + reachableDB.close(); + } + File reachableDBFile = new File(new File(omDBPath).getParentFile(), "reachable.db"); + if (reachableDBFile.exists()) { + FileUtils.deleteDirectory(reachableDBFile); + } + } + + /** + * Define a Report to be created. + */ + public static class Report { + private final ReportStatistics reachable; + private final ReportStatistics unreachable; + private final ReportStatistics unreferenced; + + /** + * Builds one report that is the aggregate of multiple others. + */ + public Report(FSORepairTool.Report... reports) { + reachable = new ReportStatistics(); + unreachable = new ReportStatistics(); + unreferenced = new ReportStatistics(); + + for (FSORepairTool.Report report : reports) { + reachable.add(report.reachable); + unreachable.add(report.unreachable); + unreferenced.add(report.unreferenced); + } + } + + private Report(FSORepairTool.Report.Builder builder) { + this.reachable = builder.reachable; + this.unreachable = builder.unreachable; + this.unreferenced = builder.unreferenced; + } + + public ReportStatistics getReachable() { + return reachable; + } + + public ReportStatistics getUnreachable() { + return unreachable; + } + + public ReportStatistics getUnreferenced() { + return unreferenced; + } + + public String toString() { + return "Reachable:" + reachable + "\nUnreachable:" + unreachable + "\nUnreferenced:" + unreferenced; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + FSORepairTool.Report report = (FSORepairTool.Report) other; + + // Useful for testing. + System.out.println("Comparing reports\nExpect:\n" + this + "\nActual:\n" + report); + + return reachable.equals(report.reachable) && unreachable.equals(report.unreachable) && + unreferenced.equals(report.unreferenced); + } + + @Override + public int hashCode() { + return Objects.hash(reachable, unreachable, unreferenced); + } + + /** + * Builder class for a Report. + */ + public static final class Builder { + private ReportStatistics reachable = new ReportStatistics(); + private ReportStatistics unreachable = new ReportStatistics(); + private ReportStatistics unreferenced = new ReportStatistics(); + + public Builder() { + } + + public Builder setReachable(ReportStatistics reachable) { + this.reachable = reachable; + return this; + } + + public Builder setUnreachable(ReportStatistics unreachable) { + this.unreachable = unreachable; + return this; + } + + public Builder setUnreferenced(ReportStatistics unreferenced) { + this.unreferenced = unreferenced; + return this; + } + + public Report build() { + return new Report(this); + } + } + } + + /** + * Represents the statistics of reachable and unreachable data. + * This gives the count of dirs, files and bytes. + */ + + public static class ReportStatistics { + private long dirs; + private long files; + private long bytes; + + public ReportStatistics() { } + + public ReportStatistics(long dirs, long files, long bytes) { + this.dirs = dirs; + this.files = files; + this.bytes = bytes; + } + + public void add(ReportStatistics other) { + this.dirs += other.dirs; + this.files += other.files; + this.bytes += other.bytes; + } + + public long getDirs() { + return dirs; + } + + public long getFiles() { + return files; + } + + public long getBytes() { + return bytes; + } + + @Override + public String toString() { + return "\n\tDirectories: " + dirs + + "\n\tFiles: " + files + + "\n\tBytes: " + bytes; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + ReportStatistics stats = (ReportStatistics) other; + + return bytes == stats.bytes && files == stats.files && dirs == stats.dirs; + } + + @Override + public int hashCode() { + return Objects.hash(bytes, files, dirs); + } + + public void addDir() { + dirs++; + } + + public void addFile(long size) { + files++; + bytes += size; + } + } +} diff --git a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/repair/om/OMRepair.java b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/repair/om/OMRepair.java new file mode 100644 index 00000000000..56d42d23f49 --- /dev/null +++ b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/repair/om/OMRepair.java @@ -0,0 +1,47 @@ +/* + * 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.repair.om; + +import org.apache.hadoop.hdds.cli.GenericCli; +import org.apache.hadoop.hdds.cli.RepairSubcommand; +import org.kohsuke.MetaInfServices; +import picocli.CommandLine; + +import java.util.concurrent.Callable; + +/** + * Ozone Repair CLI for OM. + */ +@CommandLine.Command(name = "om", + subcommands = { + FSORepairCLI.class, + }, + description = "Operational tool to repair OM.") +@MetaInfServices(RepairSubcommand.class) +public class OMRepair implements Callable, RepairSubcommand { + + @CommandLine.Spec + private CommandLine.Model.CommandSpec spec; + + @Override + public Void call() { + GenericCli.missingSubcommand(spec); + return null; + } +} diff --git a/pom.xml b/pom.xml index 869afebf493..6f27c486c15 100644 --- a/pom.xml +++ b/pom.xml @@ -44,9 +44,28 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xs - ${distMgmtSnapshotsId} - ${distMgmtSnapshotsName} - ${distMgmtSnapshotsUrl} + apache.snapshots + https://repository.apache.org/snapshots + + false + never + + + false + never + + + + apache.snapshots.https + https://repository.apache.org/content/repositories/snapshots + + false + never + + + false + never + @@ -277,7 +296,7 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xs 4.2.2 0.45.1 3.5.0 - 2.4.0 + 2.5.0 1.0-beta-1 1.0-M1 3.6.0 @@ -1974,7 +1993,7 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xs package - makeAggregateBom + makeBom