This is an automated email from the ASF dual-hosted git repository.

sumitagrawal pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ozone.git


The following commit(s) were added to refs/heads/master by this push:
     new feed38e736 HDDS-7366. Coordinate on demand and background container 
scanners. (#4726)
feed38e736 is described below

commit feed38e73690952e8073a5e5986d8c89e50eb6a0
Author: Ethan Rose <[email protected]>
AuthorDate: Thu May 25 01:41:13 2023 -0700

    HDDS-7366. Coordinate on demand and background container scanners. (#4726)
    
    * Use timestamps to throttle scans
    
    * Updated unit test for on demand scanner support of timestamps
    
    * Unit tests pass after refactor and additions
    
    * Revert findbugs
    
    * Change variable to static final
    
    * Combine log messages to helper methods
    
    * Checkstyle
    
    * Restore findbugs exclude from master and include renames
---
 .../dev-support/findbugsExcludeFile.xml            |   6 +-
 .../container/common/helpers/ContainerUtils.java   |  27 +++
 .../container/common/impl/HddsDispatcher.java      |   4 +-
 .../container/keyvalue/KeyValueContainer.java      |  12 +-
 ...ava => AbstractBackgroundContainerScanner.java} |   7 +-
 ...er.java => BackgroundContainerDataScanner.java} |  35 ++--
 ...ava => BackgroundContainerMetadataScanner.java} |  20 +-
 .../ozoneimpl/ContainerScannerConfiguration.java   |  28 +++
 ...nner.java => OnDemandContainerDataScanner.java} |  46 +++--
 .../ozone/container/ozoneimpl/OzoneContainer.java  |  17 +-
 .../ozone/container/common/ContainerTestUtils.java |   2 +-
 .../TestBackgroundContainerDataScanner.java        | 134 +++++++++++++
 .../TestBackgroundContainerMetadataScanner.java    | 114 +++++++++++
 .../TestContainerScannerConfiguration.java         |   9 +
 .../ozoneimpl/TestContainerScannerMetrics.java     | 199 -------------------
 .../ozoneimpl/TestContainerScannersAbstract.java   | 155 +++++++++++++++
 .../TestOnDemandContainerDataScanner.java          | 212 +++++++++++++++++++++
 .../ozoneimpl/TestOnDemandContainerScanner.java    | 157 ---------------
 ...Scanner.java => TestContainerDataScanners.java} |   8 +-
 19 files changed, 781 insertions(+), 411 deletions(-)

diff --git a/hadoop-hdds/container-service/dev-support/findbugsExcludeFile.xml 
b/hadoop-hdds/container-service/dev-support/findbugsExcludeFile.xml
index bf72793b9a..2cca8b90e2 100644
--- a/hadoop-hdds/container-service/dev-support/findbugsExcludeFile.xml
+++ b/hadoop-hdds/container-service/dev-support/findbugsExcludeFile.xml
@@ -82,7 +82,11 @@
     <Bug pattern="NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE" />
   </Match>
   <Match>
-    <Class 
name="org.apache.hadoop.ozone.container.ozoneimpl.TestContainerScannerMetrics"/>
+    <Class 
name="org.apache.hadoop.ozone.container.ozoneimpl.TestBackgroundContainerDataScanner"/>
+    <Bug pattern="RU_INVOKE_RUN" />
+  </Match>
+  <Match>
+    <Class 
name="org.apache.hadoop.ozone.container.ozoneimpl.TestBackgroundContainerMetadataScanner"/>
     <Bug pattern="RU_INVOKE_RUN" />
   </Match>
 </FindBugsFilter>
diff --git 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/helpers/ContainerUtils.java
 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/helpers/ContainerUtils.java
index 45a38c1618..ff974b9cf4 100644
--- 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/helpers/ContainerUtils.java
+++ 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/helpers/ContainerUtils.java
@@ -33,6 +33,9 @@ import java.io.IOException;
 import java.nio.file.Paths;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Optional;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -49,6 +52,7 @@ import org.apache.hadoop.ozone.OzoneConsts;
 import org.apache.hadoop.ozone.container.common.impl.ContainerData;
 import org.apache.hadoop.ozone.container.common.impl.ContainerDataYaml;
 import org.apache.hadoop.ozone.container.common.impl.ContainerSet;
+import org.apache.hadoop.ozone.container.common.interfaces.Container;
 import org.apache.hadoop.ozone.container.keyvalue.KeyValueContainerData;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -232,6 +236,29 @@ public final class ContainerUtils {
     }
   }
 
+  public static boolean recentlyScanned(Container<?> container,
+      long minScanGap, Logger log) {
+    Optional<Instant> lastScanTime =
+        container.getContainerData().lastDataScanTime();
+    Instant now = Instant.now();
+    // Container is considered recently scanned if it was scanned within the
+    // configured time frame. If the optional is empty, the container was
+    // never scanned.
+    boolean recentlyScanned = lastScanTime.map(scanInstant ->
+        Duration.between(now, scanInstant).abs()
+            .compareTo(Duration.ofMillis(minScanGap)) < 0)
+        .orElse(false);
+
+    if (recentlyScanned && log.isDebugEnabled()) {
+      log.debug("Skipping scan for container {} which was last " +
+              "scanned at {}. Current time is {}.",
+          container.getContainerData().getContainerID(), lastScanTime.get(),
+          now);
+    }
+
+    return recentlyScanned;
+  }
+
   /**
    * Get the .container file from the containerBaseDir.
    * @param containerBaseDir container base directory. The name of this
diff --git 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/impl/HddsDispatcher.java
 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/impl/HddsDispatcher.java
index ef5bae7999..3a11dccf47 100644
--- 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/impl/HddsDispatcher.java
+++ 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/common/impl/HddsDispatcher.java
@@ -57,8 +57,8 @@ import 
org.apache.hadoop.ozone.container.common.statemachine.StateContext;
 import 
org.apache.hadoop.ozone.container.common.transport.server.ratis.DispatcherContext;
 import org.apache.hadoop.ozone.container.common.volume.HddsVolume;
 import org.apache.hadoop.ozone.container.common.volume.VolumeSet;
+import 
org.apache.hadoop.ozone.container.ozoneimpl.OnDemandContainerDataScanner;
 import org.apache.hadoop.ozone.container.common.volume.VolumeUsage;
-import org.apache.hadoop.ozone.container.ozoneimpl.OnDemandContainerScanner;
 import org.apache.hadoop.security.UserGroupInformation;
 import org.apache.hadoop.util.Time;
 import org.apache.ratis.statemachine.StateMachine;
@@ -386,7 +386,7 @@ public class HddsDispatcher implements ContainerDispatcher, 
Auditor {
         // Create a specific exception that signals for on demand scanning
         // and move this general scan to where it is more appropriate.
         // Add integration tests to test the full functionality.
-        OnDemandContainerScanner.scanContainer(container);
+        OnDemandContainerDataScanner.scanContainer(container);
         audit(action, eventType, params, AuditEventStatus.FAILURE,
             new Exception(responseProto.getMessage()));
       }
diff --git 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/keyvalue/KeyValueContainer.java
 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/keyvalue/KeyValueContainer.java
index 3253fa3588..1900d28b29 100644
--- 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/keyvalue/KeyValueContainer.java
+++ 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/keyvalue/KeyValueContainer.java
@@ -859,8 +859,14 @@ public class KeyValueContainer implements 
Container<KeyValueContainerData> {
 
   @Override
   public boolean shouldScanData() {
-    return containerData.getState() == ContainerDataProto.State.CLOSED
+    boolean shouldScan =
+        containerData.getState() == ContainerDataProto.State.CLOSED
         || containerData.getState() == ContainerDataProto.State.QUASI_CLOSED;
+    if (!shouldScan && LOG.isDebugEnabled()) {
+      LOG.debug("Container {} in state {} should not have its data scanned.",
+          containerData.getContainerID(), containerData.getState());
+    }
+    return shouldScan;
   }
 
   @Override
@@ -879,10 +885,6 @@ public class KeyValueContainer implements 
Container<KeyValueContainerData> {
     return checker.fullCheck(throttler, canceler);
   }
 
-  private enum ContainerCheckLevel {
-    NO_CHECK, FAST_CHECK, FULL_CHECK
-  }
-
   /**
    * Creates a temporary file.
    * @param file
diff --git 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/ozoneimpl/AbstractContainerScanner.java
 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/ozoneimpl/AbstractBackgroundContainerScanner.java
similarity index 94%
rename from 
hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/ozoneimpl/AbstractContainerScanner.java
rename to 
hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/ozoneimpl/AbstractBackgroundContainerScanner.java
index 7877f5aaf4..c956e9a032 100644
--- 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/ozoneimpl/AbstractContainerScanner.java
+++ 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/ozoneimpl/AbstractBackgroundContainerScanner.java
@@ -31,9 +31,9 @@ import java.util.concurrent.TimeUnit;
 /**
  * Base class for scheduled scanners on a Datanode.
  */
-public abstract class AbstractContainerScanner extends Thread {
+public abstract class AbstractBackgroundContainerScanner extends Thread {
   public static final Logger LOG =
-      LoggerFactory.getLogger(AbstractContainerScanner.class);
+      LoggerFactory.getLogger(AbstractBackgroundContainerScanner.class);
 
   private final long dataScanInterval;
 
@@ -43,7 +43,8 @@ public abstract class AbstractContainerScanner extends Thread 
{
    */
   private volatile boolean stopping = false;
 
-  public AbstractContainerScanner(String name, long dataScanInterval) {
+  public AbstractBackgroundContainerScanner(String name,
+      long dataScanInterval) {
     this.dataScanInterval = dataScanInterval;
     setName(name);
     setDaemon(true);
diff --git 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/ozoneimpl/ContainerDataScanner.java
 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/ozoneimpl/BackgroundContainerDataScanner.java
similarity index 79%
rename from 
hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/ozoneimpl/ContainerDataScanner.java
rename to 
hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/ozoneimpl/BackgroundContainerDataScanner.java
index 91f9d95533..dd9ba8212e 100644
--- 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/ozoneimpl/ContainerDataScanner.java
+++ 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/ozoneimpl/BackgroundContainerDataScanner.java
@@ -20,6 +20,7 @@ package org.apache.hadoop.ozone.container.ozoneimpl;
 import com.google.common.annotations.VisibleForTesting;
 import org.apache.hadoop.hdfs.util.Canceler;
 import org.apache.hadoop.hdfs.util.DataTransferThrottler;
+import org.apache.hadoop.ozone.container.common.helpers.ContainerUtils;
 import org.apache.hadoop.ozone.container.common.impl.ContainerData;
 import org.apache.hadoop.ozone.container.common.interfaces.Container;
 import org.apache.hadoop.ozone.container.common.volume.HddsVolume;
@@ -34,9 +35,10 @@ import java.util.Optional;
 /**
  * Data scanner that full checks a volume. Each volume gets a separate thread.
  */
-public class ContainerDataScanner extends AbstractContainerScanner {
+public class BackgroundContainerDataScanner extends
+    AbstractBackgroundContainerScanner {
   public static final Logger LOG =
-      LoggerFactory.getLogger(ContainerDataScanner.class);
+      LoggerFactory.getLogger(BackgroundContainerDataScanner.class);
 
   /**
    * The volume that we're scanning.
@@ -47,21 +49,28 @@ public class ContainerDataScanner extends 
AbstractContainerScanner {
   private final Canceler canceler;
   private static final String NAME_FORMAT = "ContainerDataScanner(%s)";
   private final ContainerDataScannerMetrics metrics;
+  private final long minScanGap;
 
-  public ContainerDataScanner(ContainerScannerConfiguration conf,
-                              ContainerController controller,
-                              HddsVolume volume) {
+  public BackgroundContainerDataScanner(ContainerScannerConfiguration conf,
+                                        ContainerController controller,
+                                        HddsVolume volume) {
     super(String.format(NAME_FORMAT, volume), conf.getDataScanInterval());
     this.controller = controller;
     this.volume = volume;
     throttler = new HddsDataTransferThrottler(conf.getBandwidthPerVolume());
     canceler = new Canceler();
     this.metrics = ContainerDataScannerMetrics.create(volume.toString());
+    this.minScanGap = conf.getContainerScanMinGap();
+  }
+
+  private boolean shouldScan(Container<?> container) {
+    return container.shouldScanData() &&
+        !ContainerUtils.recentlyScanned(container, minScanGap, LOG);
   }
 
   @Override
   public void scanContainer(Container<?> c) throws IOException {
-    if (!c.shouldScanData()) {
+    if (!shouldScan(c)) {
       return;
     }
     ContainerData containerData = c.getContainerData();
@@ -70,12 +79,12 @@ public class ContainerDataScanner extends 
AbstractContainerScanner {
     if (!c.scanData(throttler, canceler)) {
       metrics.incNumUnHealthyContainers();
       controller.markContainerUnhealthy(containerId);
-    } else {
-      Instant now = Instant.now();
-      logScanCompleted(containerData, now);
-      controller.updateDataScanTimestamp(containerId, now);
     }
+
     metrics.incNumContainersScanned();
+    Instant now = Instant.now();
+    logScanCompleted(containerData, now);
+    controller.updateDataScanTimestamp(containerId, now);
   }
 
   @Override
@@ -125,13 +134,15 @@ public class ContainerDataScanner extends 
AbstractContainerScanner {
 
     @Override
     public synchronized void throttle(long numOfBytes) {
-      ContainerDataScanner.this.metrics.incNumBytesScanned(numOfBytes);
+      BackgroundContainerDataScanner.this.metrics.incNumBytesScanned(
+          numOfBytes);
       super.throttle(numOfBytes);
     }
 
     @Override
     public synchronized void throttle(long numOfBytes, Canceler c) {
-      ContainerDataScanner.this.metrics.incNumBytesScanned(numOfBytes);
+      BackgroundContainerDataScanner.this.metrics.incNumBytesScanned(
+          numOfBytes);
       super.throttle(numOfBytes, c);
     }
   }
diff --git 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/ozoneimpl/ContainerMetadataScanner.java
 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/ozoneimpl/BackgroundContainerMetadataScanner.java
similarity index 72%
rename from 
hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/ozoneimpl/ContainerMetadataScanner.java
rename to 
hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/ozoneimpl/BackgroundContainerMetadataScanner.java
index b1c3e66e7e..cd321dd3b7 100644
--- 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/ozoneimpl/ContainerMetadataScanner.java
+++ 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/ozoneimpl/BackgroundContainerMetadataScanner.java
@@ -18,6 +18,7 @@
 package org.apache.hadoop.ozone.container.ozoneimpl;
 
 import com.google.common.annotations.VisibleForTesting;
+import org.apache.hadoop.ozone.container.common.helpers.ContainerUtils;
 import org.apache.hadoop.ozone.container.common.interfaces.Container;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -30,18 +31,21 @@ import java.util.Iterator;
  * containers.
  * Only one thread will be responsible for scanning all volumes.
  */
-public class ContainerMetadataScanner extends AbstractContainerScanner {
+public class BackgroundContainerMetadataScanner extends
+    AbstractBackgroundContainerScanner {
   public static final Logger LOG =
-      LoggerFactory.getLogger(ContainerMetadataScanner.class);
+      LoggerFactory.getLogger(BackgroundContainerMetadataScanner.class);
 
   private final ContainerMetadataScannerMetrics metrics;
   private final ContainerController controller;
+  private final long minScanGap;
 
-  public ContainerMetadataScanner(ContainerScannerConfiguration conf,
-                                  ContainerController controller) {
+  public BackgroundContainerMetadataScanner(ContainerScannerConfiguration conf,
+                                            ContainerController controller) {
     super("ContainerMetadataScanner", conf.getMetadataScanInterval());
     this.controller = controller;
     this.metrics = ContainerMetadataScannerMetrics.create();
+    this.minScanGap = conf.getContainerScanMinGap();
   }
 
   @Override
@@ -52,6 +56,14 @@ public class ContainerMetadataScanner extends 
AbstractContainerScanner {
   @VisibleForTesting
   @Override
   public void scanContainer(Container<?> container) throws IOException {
+    // Full data scan also does a metadata scan. If a full data scan was done
+    // recently, we can skip this metadata scan.
+    if (ContainerUtils.recentlyScanned(container, minScanGap, LOG)) {
+      return;
+    }
+
+    // Do not update the scan timestamp since this was just a metadata scan,
+    // not a full scan.
     if (!container.scanMetaData()) {
       metrics.incNumUnHealthyContainers();
       controller.markContainerUnhealthy(
diff --git 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/ozoneimpl/ContainerScannerConfiguration.java
 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/ozoneimpl/ContainerScannerConfiguration.java
index 20c519bd6e..599b15ab8f 100644
--- 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/ozoneimpl/ContainerScannerConfiguration.java
+++ 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/ozoneimpl/ContainerScannerConfiguration.java
@@ -28,6 +28,8 @@ import org.slf4j.LoggerFactory;
 
 import java.time.Duration;
 
+import static org.apache.hadoop.hdds.conf.ConfigTag.DATANODE;
+
 /**
  * This class defines configuration parameters for the container scanners.
  **/
@@ -48,6 +50,11 @@ public class ContainerScannerConfiguration {
       "hdds.container.scrub.volume.bytes.per.second";
   public static final String ON_DEMAND_VOLUME_BYTES_PER_SECOND_KEY =
       "hdds.container.scrub.on.demand.volume.bytes.per.second";
+  public static final String CONTAINER_SCAN_MIN_GAP =
+      "hdds.container.scrub.min.gap";
+
+  static final long CONTAINER_SCAN_MIN_GAP_DEFAULT =
+      Duration.ofMinutes(15).toMillis();
 
   public static final long METADATA_SCAN_INTERVAL_DEFAULT =
       Duration.ofHours(3).toMillis();
@@ -101,6 +108,16 @@ public class ContainerScannerConfiguration {
   private long onDemandBandwidthPerVolume
       = ON_DEMAND_BANDWIDTH_PER_VOLUME_DEFAULT;
 
+  @Config(key = "min.gap",
+      defaultValue = "15m",
+      type = ConfigType.TIME,
+      tags = { DATANODE },
+      description = "The minimum gap between two successive scans of the same"
+          + " container. Unit could be defined with"
+          + " postfix (ns,ms,s,m,h,d)."
+  )
+  private long containerScanMinGap = CONTAINER_SCAN_MIN_GAP_DEFAULT;
+
   @PostConstruct
   public void validate() {
     if (metadataScanInterval < 0) {
@@ -117,6 +134,13 @@ public class ContainerScannerConfiguration {
       dataScanInterval = DATA_SCAN_INTERVAL_DEFAULT;
     }
 
+    if (containerScanMinGap < 0) {
+      LOG.warn(CONTAINER_SCAN_MIN_GAP +
+              " must be >= 0 and was set to {}. Defaulting to {}",
+          containerScanMinGap, CONTAINER_SCAN_MIN_GAP);
+      containerScanMinGap = CONTAINER_SCAN_MIN_GAP_DEFAULT;
+    }
+
     if (bandwidthPerVolume < 0) {
       LOG.warn(VOLUME_BYTES_PER_SECOND_KEY +
               " must be >= 0 and was set to {}. Defaulting to {}",
@@ -162,4 +186,8 @@ public class ContainerScannerConfiguration {
   public long getOnDemandBandwidthPerVolume() {
     return onDemandBandwidthPerVolume;
   }
+
+  public long getContainerScanMinGap() {
+    return containerScanMinGap;
+  }
 }
diff --git 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/ozoneimpl/OnDemandContainerScanner.java
 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/ozoneimpl/OnDemandContainerDataScanner.java
similarity index 82%
rename from 
hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/ozoneimpl/OnDemandContainerScanner.java
rename to 
hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/ozoneimpl/OnDemandContainerDataScanner.java
index 69c8ba3fee..f0bf31257e 100644
--- 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/ozoneimpl/OnDemandContainerScanner.java
+++ 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/ozoneimpl/OnDemandContainerDataScanner.java
@@ -20,6 +20,7 @@ package org.apache.hadoop.ozone.container.ozoneimpl;
 import com.google.common.annotations.VisibleForTesting;
 import org.apache.hadoop.hdfs.util.Canceler;
 import org.apache.hadoop.hdfs.util.DataTransferThrottler;
+import org.apache.hadoop.ozone.container.common.helpers.ContainerUtils;
 import org.apache.hadoop.ozone.container.common.impl.ContainerData;
 import org.apache.hadoop.ozone.container.common.interfaces.Container;
 import org.slf4j.Logger;
@@ -37,11 +38,11 @@ import java.util.concurrent.TimeUnit;
 /**
  * Class for performing on demand scans of containers.
  */
-public final class OnDemandContainerScanner {
+public final class OnDemandContainerDataScanner {
   public static final Logger LOG =
-      LoggerFactory.getLogger(OnDemandContainerScanner.class);
+      LoggerFactory.getLogger(OnDemandContainerDataScanner.class);
 
-  private static volatile OnDemandContainerScanner instance;
+  private static volatile OnDemandContainerDataScanner instance;
 
   private final ExecutorService scanExecutor;
   private final ContainerController containerController;
@@ -50,8 +51,9 @@ public final class OnDemandContainerScanner {
   private final ConcurrentHashMap
       .KeySetView<Long, Boolean> containerRescheduleCheckSet;
   private final OnDemandScannerMetrics metrics;
+  private final long minScanGap;
 
-  private OnDemandContainerScanner(
+  private OnDemandContainerDataScanner(
       ContainerScannerConfiguration conf, ContainerController controller) {
     containerController = controller;
     throttler = new DataTransferThrottler(
@@ -60,6 +62,7 @@ public final class OnDemandContainerScanner {
     metrics = OnDemandScannerMetrics.create();
     scanExecutor = Executors.newSingleThreadExecutor();
     containerRescheduleCheckSet = ConcurrentHashMap.newKeySet();
+    minScanGap = conf.getContainerScanMinGap();
   }
 
   public static synchronized void init(
@@ -69,20 +72,29 @@ public final class OnDemandContainerScanner {
           " a second time on a datanode.");
       return;
     }
-    instance = new OnDemandContainerScanner(conf, controller);
+    instance = new OnDemandContainerDataScanner(conf, controller);
+  }
+
+  private static boolean shouldScan(Container<?> container) {
+    if (instance == null) {
+      LOG.debug("Skipping on demand scan for container {} since scanner was " +
+          "not initialized.", container.getContainerData().getContainerID());
+    }
+    return instance != null &&
+        container.shouldScanData() &&
+        !ContainerUtils.recentlyScanned(container, instance.minScanGap, LOG);
   }
 
   public static Optional<Future<?>> scanContainer(Container<?> container) {
-    if (instance == null || !container.shouldScanData()) {
+    if (!shouldScan(container)) {
       return Optional.empty();
     }
+
     Future<?> resultFuture = null;
     long containerId = container.getContainerData().getContainerID();
     if (addContainerToScheduledContainers(containerId)) {
       resultFuture = instance.scanExecutor.submit(() -> {
-        if (container.shouldScanData()) {
-          performOnDemandScan(container);
-        }
+        performOnDemandScan(container);
         removeContainerFromScheduledContainers(containerId);
       });
     }
@@ -99,19 +111,23 @@ public final class OnDemandContainerScanner {
   }
 
   private static void performOnDemandScan(Container<?> container) {
+    if (!shouldScan(container)) {
+      return;
+    }
+
     long containerId = container.getContainerData().getContainerID();
     try {
       ContainerData containerData = container.getContainerData();
       logScanStart(containerData);
-      if (container.scanData(instance.throttler, instance.canceler)) {
-        Instant now = Instant.now();
-        logScanCompleted(containerData, now);
-        instance.containerController.updateDataScanTimestamp(containerId, now);
-      } else {
-        instance.containerController.markContainerUnhealthy(containerId);
+      if (!container.scanData(instance.throttler, instance.canceler)) {
         instance.metrics.incNumUnHealthyContainers();
+        instance.containerController.markContainerUnhealthy(containerId);
       }
+
       instance.metrics.incNumContainersScanned();
+      Instant now = Instant.now();
+      logScanCompleted(containerData, now);
+      instance.containerController.updateDataScanTimestamp(containerId, now);
     } catch (IOException e) {
       LOG.warn("Unexpected exception while scanning container "
           + containerId, e);
diff --git 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/ozoneimpl/OzoneContainer.java
 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/ozoneimpl/OzoneContainer.java
index d276b617d5..bc2461b3ff 100644
--- 
a/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/ozoneimpl/OzoneContainer.java
+++ 
b/hadoop-hdds/container-service/src/main/java/org/apache/hadoop/ozone/container/ozoneimpl/OzoneContainer.java
@@ -106,8 +106,8 @@ public class OzoneContainer {
   private final XceiverServerSpi writeChannel;
   private final XceiverServerSpi readChannel;
   private final ContainerController controller;
-  private ContainerMetadataScanner metadataScanner;
-  private List<ContainerDataScanner> dataScanners;
+  private BackgroundContainerMetadataScanner metadataScanner;
+  private List<BackgroundContainerDataScanner> dataScanners;
   private final BlockDeletingService blockDeletingService;
   private final StaleRecoveringContainerScrubbingService
       recoveringContainerScrubbingService;
@@ -328,8 +328,8 @@ public class OzoneContainer {
     }
     dataScanners = new ArrayList<>();
     for (StorageVolume v : volumeSet.getVolumesList()) {
-      ContainerDataScanner s = new ContainerDataScanner(c, controller,
-          (HddsVolume) v);
+      BackgroundContainerDataScanner s =
+          new BackgroundContainerDataScanner(c, controller, (HddsVolume) v);
       s.start();
       dataScanners.add(s);
     }
@@ -337,7 +337,8 @@ public class OzoneContainer {
 
   private void initMetadataScanner(ContainerScannerConfiguration c) {
     if (this.metadataScanner == null) {
-      this.metadataScanner = new ContainerMetadataScanner(c, controller);
+      this.metadataScanner =
+          new BackgroundContainerMetadataScanner(c, controller);
     }
     this.metadataScanner.start();
   }
@@ -348,7 +349,7 @@ public class OzoneContainer {
           "so the on-demand container data scanner will not start.");
       return;
     }
-    OnDemandContainerScanner.init(c, controller);
+    OnDemandContainerDataScanner.init(c, controller);
   }
 
   /**
@@ -364,10 +365,10 @@ public class OzoneContainer {
     if (dataScanners == null) {
       return;
     }
-    for (ContainerDataScanner s : dataScanners) {
+    for (BackgroundContainerDataScanner s : dataScanners) {
       s.shutdown();
     }
-    OnDemandContainerScanner.shutdown();
+    OnDemandContainerDataScanner.shutdown();
   }
 
   /**
diff --git 
a/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/common/ContainerTestUtils.java
 
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/common/ContainerTestUtils.java
index 31d5000acb..2c912b277a 100644
--- 
a/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/common/ContainerTestUtils.java
+++ 
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/common/ContainerTestUtils.java
@@ -176,7 +176,7 @@ public final class ContainerTestUtils {
       boolean scanMetaDataSuccess, boolean scanDataSuccess,
       AtomicLong containerIdSeq) {
     setupMockContainer(c, shouldScanData, scanDataSuccess, containerIdSeq);
-    when(c.scanMetaData()).thenReturn(scanMetaDataSuccess);
+    Mockito.lenient().when(c.scanMetaData()).thenReturn(scanMetaDataSuccess);
   }
 
   public static void setupMockContainer(
diff --git 
a/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/ozoneimpl/TestBackgroundContainerDataScanner.java
 
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/ozoneimpl/TestBackgroundContainerDataScanner.java
new file mode 100644
index 0000000000..dc39ee2a9d
--- /dev/null
+++ 
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/ozoneimpl/TestBackgroundContainerDataScanner.java
@@ -0,0 +1,134 @@
+/*
+ * 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.container.ozoneimpl;
+
+import org.apache.hadoop.metrics2.lib.DefaultMetricsSystem;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.BeforeEach;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.never;
+
+/**
+ * Unit tests for the background container data scanner.
+ */
+@MockitoSettings(strictness = Strictness.LENIENT)
+public class TestBackgroundContainerDataScanner extends
+    TestContainerScannersAbstract {
+
+  private BackgroundContainerDataScanner scanner;
+
+  @BeforeEach
+  public void setup() {
+    super.setup();
+    scanner = new BackgroundContainerDataScanner(conf, controller, vol);
+  }
+
+  @Test
+  @Override
+  public void testRecentlyScannedContainerIsSkipped() {
+    setScannedTimestampRecent(healthy);
+    scanner.runIteration();
+    Mockito.verify(healthy, never()).scanData(any(), any());
+  }
+
+  @Test
+  @Override
+  public void testPreviouslyScannedContainerIsScanned() {
+    // If the last scan time is before than the configured gap, the container
+    // should be scanned.
+    setScannedTimestampOld(healthy);
+    scanner.runIteration();
+    Mockito.verify(healthy, atLeastOnce()).scanData(any(), any());
+  }
+
+  @Test
+  @Override
+  public void testUnscannedContainerIsScanned() {
+    // If there is no last scanned time, the container should be scanned.
+    Mockito.when(healthy.getContainerData().lastDataScanTime())
+        .thenReturn(Optional.empty());
+    scanner.runIteration();
+    Mockito.verify(healthy, atLeastOnce()).scanData(any(), any());
+  }
+
+  @Test
+  @Override
+  public void testScannerMetrics() {
+    scanner.runIteration();
+
+    ContainerDataScannerMetrics metrics = scanner.getMetrics();
+    assertEquals(1, metrics.getNumScanIterations());
+    assertEquals(2, metrics.getNumContainersScanned());
+    assertEquals(1, metrics.getNumUnHealthyContainers());
+  }
+
+  @Test
+  @Override
+  public void testScannerMetricsUnregisters() {
+    String name = scanner.getMetrics().getName();
+
+    assertNotNull(DefaultMetricsSystem.instance().getSource(name));
+
+    scanner.shutdown();
+    scanner.run();
+
+    assertNull(DefaultMetricsSystem.instance().getSource(name));
+  }
+
+  @Test
+  @Override
+  public void testUnhealthyContainersDetected() throws Exception {
+    scanner.runIteration();
+    verifyContainerMarkedUnhealthy(healthy, never());
+    verifyContainerMarkedUnhealthy(corruptData, atLeastOnce());
+    verifyContainerMarkedUnhealthy(openCorruptMetadata, never());
+    verifyContainerMarkedUnhealthy(openContainer, never());
+  }
+
+  @Test
+  public void testScanTimestampUpdated() throws Exception {
+    scanner.runIteration();
+    // Open containers should not be scanned.
+    Mockito.verify(controller, never())
+        .updateDataScanTimestamp(
+            eq(openContainer.getContainerData().getContainerID()), any());
+    Mockito.verify(controller, never())
+        .updateDataScanTimestamp(
+            eq(openCorruptMetadata.getContainerData().getContainerID()), 
any());
+    // All other containers should have been scanned.
+    Mockito.verify(controller, atLeastOnce())
+        .updateDataScanTimestamp(
+            eq(healthy.getContainerData().getContainerID()), any());
+    Mockito.verify(controller, atLeastOnce())
+        .updateDataScanTimestamp(
+            eq(corruptData.getContainerData().getContainerID()), any());
+  }
+}
diff --git 
a/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/ozoneimpl/TestBackgroundContainerMetadataScanner.java
 
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/ozoneimpl/TestBackgroundContainerMetadataScanner.java
new file mode 100644
index 0000000000..8ae61a1a27
--- /dev/null
+++ 
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/ozoneimpl/TestBackgroundContainerMetadataScanner.java
@@ -0,0 +1,114 @@
+/*
+ * 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.container.ozoneimpl;
+
+import org.apache.hadoop.metrics2.lib.DefaultMetricsSystem;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.BeforeEach;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.never;
+
+/**
+ * Unit tests for the background container metadata scanner.
+ */
+@MockitoSettings(strictness = Strictness.LENIENT)
+public class TestBackgroundContainerMetadataScanner extends
+    TestContainerScannersAbstract {
+
+  private BackgroundContainerMetadataScanner scanner;
+
+  @BeforeEach
+  public void setup() {
+    super.setup();
+    scanner = new BackgroundContainerMetadataScanner(conf, controller);
+  }
+
+  @Test
+  @Override
+  public void testRecentlyScannedContainerIsSkipped() {
+    // If the last scan time is before than the configured gap, the container
+    // should be scanned.
+    setScannedTimestampRecent(healthy);
+    scanner.runIteration();
+    Mockito.verify(healthy, never()).scanMetaData();
+  }
+
+  @Test
+  @Override
+  public void testPreviouslyScannedContainerIsScanned() {
+    setScannedTimestampOld(healthy);
+    scanner.runIteration();
+    Mockito.verify(healthy, atLeastOnce()).scanMetaData();
+  }
+
+  @Test
+  @Override
+  public void testUnscannedContainerIsScanned() throws Exception {
+    // If there is no last scanned time, the container should be scanned.
+    Mockito.when(healthy.getContainerData().lastDataScanTime())
+        .thenReturn(Optional.empty());
+    scanner.runIteration();
+    Mockito.verify(healthy, atLeastOnce()).scanMetaData();
+  }
+
+  @Test
+  @Override
+  public void testScannerMetrics() {
+    scanner.runIteration();
+
+    ContainerMetadataScannerMetrics metrics = scanner.getMetrics();
+    assertEquals(1, metrics.getNumScanIterations());
+    assertEquals(3, metrics.getNumContainersScanned());
+    assertEquals(1, metrics.getNumUnHealthyContainers());
+  }
+
+  @Test
+  @Override
+  public void testScannerMetricsUnregisters() {
+    String name = scanner.getMetrics().getName();
+
+    assertNotNull(DefaultMetricsSystem.instance().getSource(name));
+
+    scanner.shutdown();
+    scanner.run();
+
+    assertNull(DefaultMetricsSystem.instance().getSource(name));
+  }
+
+  @Test
+  @Override
+  public void testUnhealthyContainersDetected() throws Exception {
+    scanner.runIteration();
+    verifyContainerMarkedUnhealthy(healthy, never());
+    // Metadata scanner cannot detect data corruption.
+    verifyContainerMarkedUnhealthy(corruptData, never());
+    verifyContainerMarkedUnhealthy(openCorruptMetadata, atLeastOnce());
+    verifyContainerMarkedUnhealthy(openContainer, never());
+  }
+}
diff --git 
a/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/ozoneimpl/TestContainerScannerConfiguration.java
 
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/ozoneimpl/TestContainerScannerConfiguration.java
index 077acbb9d9..f11a7f5522 100644
--- 
a/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/ozoneimpl/TestContainerScannerConfiguration.java
+++ 
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/ozoneimpl/TestContainerScannerConfiguration.java
@@ -25,6 +25,8 @@ import org.junit.jupiter.api.Test;
 import java.time.Duration;
 
 import static 
org.apache.hadoop.ozone.container.ozoneimpl.ContainerScannerConfiguration.BANDWIDTH_PER_VOLUME_DEFAULT;
+import static 
org.apache.hadoop.ozone.container.ozoneimpl.ContainerScannerConfiguration.CONTAINER_SCAN_MIN_GAP;
+import static 
org.apache.hadoop.ozone.container.ozoneimpl.ContainerScannerConfiguration.CONTAINER_SCAN_MIN_GAP_DEFAULT;
 import static 
org.apache.hadoop.ozone.container.ozoneimpl.ContainerScannerConfiguration.DATA_SCAN_INTERVAL_DEFAULT;
 import static 
org.apache.hadoop.ozone.container.ozoneimpl.ContainerScannerConfiguration.DATA_SCAN_INTERVAL_KEY;
 import static 
org.apache.hadoop.ozone.container.ozoneimpl.ContainerScannerConfiguration.METADATA_SCAN_INTERVAL_DEFAULT;
@@ -55,6 +57,7 @@ public class TestContainerScannerConfiguration {
 
     conf.setLong(METADATA_SCAN_INTERVAL_KEY, validInterval);
     conf.setLong(DATA_SCAN_INTERVAL_KEY, validInterval);
+    conf.setLong(CONTAINER_SCAN_MIN_GAP, validInterval);
     conf.setLong(VOLUME_BYTES_PER_SECOND_KEY, validBandwidth);
     conf.setLong(ON_DEMAND_VOLUME_BYTES_PER_SECOND_KEY, 
validOnDemandBandwidth);
 
@@ -63,6 +66,7 @@ public class TestContainerScannerConfiguration {
 
     assertEquals(validInterval, csConf.getMetadataScanInterval());
     assertEquals(validInterval, csConf.getDataScanInterval());
+    assertEquals(validInterval, csConf.getContainerScanMinGap());
     assertEquals(validBandwidth, csConf.getBandwidthPerVolume());
     assertEquals(validOnDemandBandwidth,
         csConf.getOnDemandBandwidthPerVolume());
@@ -75,6 +79,7 @@ public class TestContainerScannerConfiguration {
 
     conf.setLong(METADATA_SCAN_INTERVAL_KEY, invalidInterval);
     conf.setLong(DATA_SCAN_INTERVAL_KEY, invalidInterval);
+    conf.setLong(CONTAINER_SCAN_MIN_GAP, invalidInterval);
     conf.setLong(VOLUME_BYTES_PER_SECOND_KEY, invalidBandwidth);
     conf.setLong(ON_DEMAND_VOLUME_BYTES_PER_SECOND_KEY, invalidBandwidth);
 
@@ -85,6 +90,8 @@ public class TestContainerScannerConfiguration {
         csConf.getMetadataScanInterval());
     assertEquals(DATA_SCAN_INTERVAL_DEFAULT,
         csConf.getDataScanInterval());
+    assertEquals(CONTAINER_SCAN_MIN_GAP_DEFAULT,
+        csConf.getContainerScanMinGap());
     assertEquals(BANDWIDTH_PER_VOLUME_DEFAULT,
         csConf.getBandwidthPerVolume());
     assertEquals(ON_DEMAND_BANDWIDTH_PER_VOLUME_DEFAULT,
@@ -105,5 +112,7 @@ public class TestContainerScannerConfiguration {
         csConf.getBandwidthPerVolume());
     assertEquals(ON_DEMAND_BANDWIDTH_PER_VOLUME_DEFAULT,
         csConf.getOnDemandBandwidthPerVolume());
+    assertEquals(CONTAINER_SCAN_MIN_GAP_DEFAULT,
+        csConf.getContainerScanMinGap());
   }
 }
diff --git 
a/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/ozoneimpl/TestContainerScannerMetrics.java
 
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/ozoneimpl/TestContainerScannerMetrics.java
deleted file mode 100644
index 6d9eb31db1..0000000000
--- 
a/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/ozoneimpl/TestContainerScannerMetrics.java
+++ /dev/null
@@ -1,199 +0,0 @@
-/*
- * 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
- * <p>
- * http://www.apache.org/licenses/LICENSE-2.0
- * <p>
- * 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.container.ozoneimpl;
-
-import org.apache.commons.compress.utils.Lists;
-import org.apache.hadoop.metrics2.lib.DefaultMetricsSystem;
-import org.apache.hadoop.ozone.container.common.ContainerTestUtils;
-import org.apache.hadoop.ozone.container.common.impl.ContainerData;
-import org.apache.hadoop.ozone.container.common.interfaces.Container;
-import org.apache.hadoop.ozone.container.common.volume.HddsVolume;
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.Mock;
-import org.mockito.junit.jupiter.MockitoExtension;
-import org.mockito.junit.jupiter.MockitoSettings;
-import org.mockito.quality.Strictness;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Optional;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Future;
-import java.util.concurrent.atomic.AtomicLong;
-
-import static org.apache.hadoop.hdds.conf.OzoneConfiguration.newInstanceOf;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertNull;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-/**
- * This test verifies the container scanner metrics functionality.
- */
-@ExtendWith(MockitoExtension.class)
-@MockitoSettings(strictness = Strictness.LENIENT)
-public class TestContainerScannerMetrics {
-
-  private final AtomicLong containerIdSeq = new AtomicLong(100);
-
-  @Mock
-  private Container<ContainerData> healthy;
-
-  @Mock
-  private Container<ContainerData> corruptMetadata;
-
-  @Mock
-  private Container<ContainerData> corruptData;
-
-  @Mock
-  private HddsVolume vol;
-
-  private ContainerScannerConfiguration conf;
-  private ContainerController controller;
-
-  @BeforeEach
-  public void setup() {
-    conf = newInstanceOf(ContainerScannerConfiguration.class);
-    conf.setMetadataScanInterval(0);
-    conf.setDataScanInterval(0);
-    conf.setEnabled(true);
-    controller = mockContainerController();
-  }
-
-  @AfterEach
-  public void tearDown() {
-    OnDemandContainerScanner.shutdown();
-  }
-
-  @Test
-  public void testContainerMetaDataScannerMetrics() {
-    ContainerMetadataScanner subject =
-        new ContainerMetadataScanner(conf, controller);
-    subject.runIteration();
-
-    ContainerMetadataScannerMetrics metrics = subject.getMetrics();
-    assertEquals(1, metrics.getNumScanIterations());
-    assertEquals(3, metrics.getNumContainersScanned());
-    assertEquals(1, metrics.getNumUnHealthyContainers());
-  }
-
-  @Test
-  public void testContainerMetaDataScannerMetricsUnregisters() {
-    ContainerMetadataScanner subject =
-        new ContainerMetadataScanner(conf, controller);
-    String name = subject.getMetrics().getName();
-
-    assertNotNull(DefaultMetricsSystem.instance().getSource(name));
-
-    subject.shutdown();
-    subject.run();
-
-    assertNull(DefaultMetricsSystem.instance().getSource(name));
-  }
-
-  @Test
-  public void testContainerDataScannerMetrics() {
-    ContainerDataScanner subject =
-        new ContainerDataScanner(conf, controller, vol);
-    subject.runIteration();
-
-    ContainerDataScannerMetrics metrics = subject.getMetrics();
-    assertEquals(1, metrics.getNumScanIterations());
-    assertEquals(2, metrics.getNumContainersScanned());
-    assertEquals(1, metrics.getNumUnHealthyContainers());
-  }
-
-  @Test
-  public void testContainerDataScannerMetricsUnregisters() throws IOException {
-    HddsVolume volume = new HddsVolume.Builder("/").failedVolume(true).build();
-    ContainerDataScanner subject =
-        new ContainerDataScanner(conf, controller, volume);
-    String name = subject.getMetrics().getName();
-
-    assertNotNull(DefaultMetricsSystem.instance().getSource(name));
-
-    subject.shutdown();
-    subject.run();
-
-    assertNull(DefaultMetricsSystem.instance().getSource(name));
-  }
-
-  @Test
-  public void testOnDemandScannerMetrics() throws Exception {
-    OnDemandContainerScanner.init(conf, controller);
-    ArrayList<Optional<Future<?>>> resultFutureList = Lists.newArrayList();
-    resultFutureList.add(OnDemandContainerScanner.scanContainer(corruptData));
-    resultFutureList.add(
-        OnDemandContainerScanner.scanContainer(corruptMetadata));
-    resultFutureList.add(OnDemandContainerScanner.scanContainer(healthy));
-    waitOnScannerToFinish(resultFutureList);
-    OnDemandScannerMetrics metrics = OnDemandContainerScanner.getMetrics();
-    //Containers with shouldScanData = false shouldn't increase
-    // the number of scanned containers
-    assertEquals(1, metrics.getNumUnHealthyContainers());
-    assertEquals(2, metrics.getNumContainersScanned());
-  }
-
-  private void waitOnScannerToFinish(
-      ArrayList<Optional<Future<?>>> resultFutureList)
-      throws ExecutionException, InterruptedException {
-    for (Optional<Future<?>> future : resultFutureList) {
-      if (future.isPresent()) {
-        future.get().get();
-      }
-    }
-  }
-
-  @Test
-  public void testOnDemandScannerMetricsUnregisters() {
-    OnDemandContainerScanner.init(conf, controller);
-    String metricsName = OnDemandContainerScanner.getMetrics().getName();
-    assertNotNull(DefaultMetricsSystem.instance().getSource(metricsName));
-    OnDemandContainerScanner.shutdown();
-    OnDemandContainerScanner.scanContainer(healthy);
-    assertNull(DefaultMetricsSystem.instance().getSource(metricsName));
-  }
-
-  private ContainerController mockContainerController() {
-    // healthy container
-    ContainerTestUtils.setupMockContainer(healthy,
-        true, true, true, containerIdSeq);
-
-    // unhealthy container (corrupt data)
-    ContainerTestUtils.setupMockContainer(corruptData,
-        true, true, false, containerIdSeq);
-
-    // unhealthy container (corrupt metadata)
-    ContainerTestUtils.setupMockContainer(corruptMetadata,
-        false, false, false, containerIdSeq);
-
-    Collection<Container<?>> containers = Arrays.asList(
-        healthy, corruptData, corruptMetadata);
-    ContainerController mock = mock(ContainerController.class);
-    when(mock.getContainers(vol)).thenReturn(containers.iterator());
-    when(mock.getContainers()).thenReturn(containers.iterator());
-
-    return mock;
-  }
-}
diff --git 
a/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/ozoneimpl/TestContainerScannersAbstract.java
 
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/ozoneimpl/TestContainerScannersAbstract.java
new file mode 100644
index 0000000000..4e00d3327b
--- /dev/null
+++ 
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/ozoneimpl/TestContainerScannersAbstract.java
@@ -0,0 +1,155 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * 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.container.ozoneimpl;
+
+import org.apache.hadoop.ozone.container.common.ContainerTestUtils;
+import org.apache.hadoop.ozone.container.common.impl.ContainerData;
+import org.apache.hadoop.ozone.container.common.interfaces.Container;
+import org.apache.hadoop.ozone.container.common.volume.HddsVolume;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+import org.mockito.verification.VerificationMode;
+
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicLong;
+
+import static org.apache.hadoop.hdds.conf.OzoneConfiguration.newInstanceOf;
+import static 
org.apache.hadoop.ozone.container.ozoneimpl.ContainerScannerConfiguration.CONTAINER_SCAN_MIN_GAP_DEFAULT;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * General testing guidelines for the various container scanners whose tests
+ * subclass this one.
+ */
+@MockitoSettings(strictness = Strictness.LENIENT)
+@SuppressWarnings("checkstyle:VisibilityModifier")
+public abstract class TestContainerScannersAbstract {
+
+  private static final AtomicLong CONTAINER_SEQ_ID = new AtomicLong(100);
+
+  @Mock
+  protected Container<ContainerData> healthy;
+
+  @Mock
+  protected Container<ContainerData> openContainer;
+
+  @Mock
+  protected Container<ContainerData> openCorruptMetadata;
+
+  @Mock
+  protected Container<ContainerData> corruptData;
+
+  @Mock
+  protected HddsVolume vol;
+
+  protected ContainerScannerConfiguration conf;
+  protected ContainerController controller;
+
+  public void setup() {
+    conf = newInstanceOf(ContainerScannerConfiguration.class);
+    conf.setMetadataScanInterval(0);
+    conf.setDataScanInterval(0);
+    conf.setEnabled(true);
+    controller = mockContainerController();
+  }
+
+  // ALL SCANNERS SHOULD TEST THESE THINGS
+
+  @Test
+  public abstract void testRecentlyScannedContainerIsSkipped() throws 
Exception;
+
+  @Test
+  public abstract void testPreviouslyScannedContainerIsScanned()
+      throws Exception;
+
+  @Test
+  public abstract void testUnscannedContainerIsScanned() throws Exception;
+
+  @Test
+  public abstract void testUnhealthyContainersDetected() throws Exception;
+
+  @Test
+  public abstract void testScannerMetrics() throws Exception;
+
+  @Test
+  public abstract void testScannerMetricsUnregisters() throws Exception;
+
+  // HELPER METHODS
+
+  protected void setScannedTimestampOld(Container<ContainerData> container) {
+    // If the last scan time is before than the configured gap, the container
+    // should be scanned.
+    Instant oldLastScanTime = Instant.now()
+        .minus(CONTAINER_SCAN_MIN_GAP_DEFAULT, ChronoUnit.MILLIS)
+        .minus(10, ChronoUnit.MINUTES);
+    Mockito.when(container.getContainerData().lastDataScanTime())
+        .thenReturn(Optional.of(oldLastScanTime));
+  }
+
+  protected void setScannedTimestampRecent(Container<ContainerData> container) 
{
+    // If the last scan time is within the configured gap, the container
+    // should be skipped.
+    Instant recentLastScanTime = Instant.now()
+        .minus(CONTAINER_SCAN_MIN_GAP_DEFAULT, ChronoUnit.MILLIS)
+        .plus(1, ChronoUnit.MINUTES);
+    Mockito.when(container.getContainerData().lastDataScanTime())
+        .thenReturn(Optional.of(recentLastScanTime));
+  }
+
+  protected void verifyContainerMarkedUnhealthy(
+      Container<ContainerData> container, VerificationMode invocationTimes)
+      throws Exception {
+    Mockito.verify(controller, invocationTimes).markContainerUnhealthy(
+        container.getContainerData().getContainerID());
+  }
+
+  private ContainerController mockContainerController() {
+    // healthy container
+    ContainerTestUtils.setupMockContainer(healthy,
+        true, true, true, CONTAINER_SEQ_ID);
+
+    // Open container (only metadata can be scanned)
+    ContainerTestUtils.setupMockContainer(openContainer,
+        false, true, false, CONTAINER_SEQ_ID);
+
+    // unhealthy container (corrupt data)
+    ContainerTestUtils.setupMockContainer(corruptData,
+        true, true, false, CONTAINER_SEQ_ID);
+
+    // unhealthy container (corrupt metadata). To simulate container still
+    // being open while metadata is corrupted, shouldScanData will return 
false.
+    ContainerTestUtils.setupMockContainer(openCorruptMetadata,
+        false, false, false, CONTAINER_SEQ_ID);
+
+    Collection<Container<?>> containers = Arrays.asList(
+        healthy, corruptData, openCorruptMetadata);
+    ContainerController mock = mock(ContainerController.class);
+    when(mock.getContainers(vol)).thenReturn(containers.iterator());
+    when(mock.getContainers()).thenReturn(containers.iterator());
+
+    return mock;
+  }
+}
diff --git 
a/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/ozoneimpl/TestOnDemandContainerDataScanner.java
 
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/ozoneimpl/TestOnDemandContainerDataScanner.java
new file mode 100644
index 0000000000..033688a693
--- /dev/null
+++ 
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/ozoneimpl/TestOnDemandContainerDataScanner.java
@@ -0,0 +1,212 @@
+/*
+ * 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.container.ozoneimpl;
+
+import org.apache.commons.compress.utils.Lists;
+import org.apache.hadoop.metrics2.lib.DefaultMetricsSystem;
+import org.apache.hadoop.ozone.container.common.impl.ContainerData;
+import org.apache.hadoop.ozone.container.common.interfaces.Container;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+import org.mockito.stubbing.Answer;
+
+import java.util.ArrayList;
+import java.util.Optional;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.never;
+
+/**
+ * Unit tests for the on-demand container scanner.
+ */
+@MockitoSettings(strictness = Strictness.LENIENT)
+public class TestOnDemandContainerDataScanner extends
+    TestContainerScannersAbstract {
+
+  @BeforeEach
+  public void setup() {
+    super.setup();
+  }
+
+  @Test
+  @Override
+  public void testRecentlyScannedContainerIsSkipped() throws Exception {
+    setScannedTimestampRecent(healthy);
+    scanContainer(healthy);
+    Mockito.verify(healthy, never()).scanData(any(), any());
+  }
+
+  @Test
+  @Override
+  public void testPreviouslyScannedContainerIsScanned() throws Exception {
+    // If the last scan time is before than the configured gap, the container
+    // should be scanned.
+    setScannedTimestampOld(healthy);
+    scanContainer(healthy);
+    Mockito.verify(healthy, atLeastOnce()).scanData(any(), any());
+  }
+
+  @Test
+  @Override
+  public void testUnscannedContainerIsScanned() throws Exception {
+    // If there is no last scanned time, the container should be scanned.
+    Mockito.when(healthy.getContainerData().lastDataScanTime())
+        .thenReturn(Optional.empty());
+    scanContainer(healthy);
+    Mockito.verify(healthy, atLeastOnce()).scanData(any(), any());
+  }
+
+  @AfterEach
+  public void tearDown() {
+    OnDemandContainerDataScanner.shutdown();
+  }
+
+  @Test
+  public void testScanTimestampUpdated() throws Exception {
+    OnDemandContainerDataScanner.init(conf, controller);
+    Optional<Future<?>> scanFuture =
+        OnDemandContainerDataScanner.scanContainer(healthy);
+    Assertions.assertTrue(scanFuture.isPresent());
+    scanFuture.get().get();
+    Mockito.verify(controller, atLeastOnce())
+        .updateDataScanTimestamp(
+            eq(healthy.getContainerData().getContainerID()), any());
+  }
+
+  @Test
+  public void testContainerScannerMultipleInitsAndShutdowns() throws Exception 
{
+    OnDemandContainerDataScanner.init(conf, controller);
+    OnDemandContainerDataScanner.init(conf, controller);
+    OnDemandContainerDataScanner.shutdown();
+    OnDemandContainerDataScanner.shutdown();
+    //There shouldn't be an interaction after shutdown:
+    OnDemandContainerDataScanner.scanContainer(corruptData);
+    verifyContainerMarkedUnhealthy(corruptData, never());
+  }
+
+  @Test
+  public void testSameContainerQueuedMultipleTimes() throws Exception {
+    OnDemandContainerDataScanner.init(conf, controller);
+    //Given a container that has not finished scanning
+    CountDownLatch latch = new CountDownLatch(1);
+    Mockito.lenient().when(corruptData.scanData(
+            OnDemandContainerDataScanner.getThrottler(),
+            OnDemandContainerDataScanner.getCanceler()))
+        .thenAnswer((Answer<Boolean>) invocation -> {
+          latch.await();
+          return false;
+        });
+    Optional<Future<?>> onGoingScan = OnDemandContainerDataScanner
+        .scanContainer(corruptData);
+    Assertions.assertTrue(onGoingScan.isPresent());
+    Assertions.assertFalse(onGoingScan.get().isDone());
+    //When scheduling the same container again
+    Optional<Future<?>> secondScan = OnDemandContainerDataScanner
+        .scanContainer(corruptData);
+    //Then the second scan is not scheduled and the first scan can still finish
+    Assertions.assertFalse(secondScan.isPresent());
+    latch.countDown();
+    onGoingScan.get().get();
+    Mockito.verify(controller, atLeastOnce()).
+        
markContainerUnhealthy(corruptData.getContainerData().getContainerID());
+  }
+
+  @Test
+  @Override
+  public void testScannerMetrics() throws Exception {
+    OnDemandContainerDataScanner.init(conf, controller);
+    ArrayList<Optional<Future<?>>> resultFutureList = Lists.newArrayList();
+    resultFutureList.add(OnDemandContainerDataScanner.scanContainer(
+        corruptData));
+    resultFutureList.add(
+        OnDemandContainerDataScanner.scanContainer(openContainer));
+    resultFutureList.add(
+        OnDemandContainerDataScanner.scanContainer(openCorruptMetadata));
+    resultFutureList.add(OnDemandContainerDataScanner.scanContainer(healthy));
+    waitOnScannerToFinish(resultFutureList);
+    OnDemandScannerMetrics metrics = OnDemandContainerDataScanner.getMetrics();
+    //Containers with shouldScanData = false shouldn't increase
+    // the number of scanned containers
+    assertEquals(1, metrics.getNumUnHealthyContainers());
+    assertEquals(2, metrics.getNumContainersScanned());
+  }
+
+  @Test
+  @Override
+  public void testScannerMetricsUnregisters() {
+    OnDemandContainerDataScanner.init(conf, controller);
+    String metricsName = OnDemandContainerDataScanner.getMetrics().getName();
+    assertNotNull(DefaultMetricsSystem.instance().getSource(metricsName));
+    OnDemandContainerDataScanner.shutdown();
+    OnDemandContainerDataScanner.scanContainer(healthy);
+    assertNull(DefaultMetricsSystem.instance().getSource(metricsName));
+  }
+
+  @Test
+  @Override
+  public void testUnhealthyContainersDetected() throws Exception {
+    // Without initialization,
+    // there shouldn't be interaction with containerController
+    OnDemandContainerDataScanner.scanContainer(corruptData);
+    Mockito.verifyZeroInteractions(controller);
+
+    scanContainer(healthy);
+    verifyContainerMarkedUnhealthy(healthy, never());
+    scanContainer(corruptData);
+    verifyContainerMarkedUnhealthy(corruptData, atLeastOnce());
+    scanContainer(openCorruptMetadata);
+    verifyContainerMarkedUnhealthy(openCorruptMetadata, never());
+    scanContainer(openContainer);
+    verifyContainerMarkedUnhealthy(openContainer, never());
+  }
+
+  private void scanContainer(Container<ContainerData> container)
+      throws Exception {
+    OnDemandContainerDataScanner.init(conf, controller);
+    Optional<Future<?>> scanFuture =
+        OnDemandContainerDataScanner.scanContainer(container);
+    if (scanFuture.isPresent()) {
+      scanFuture.get().get();
+    }
+  }
+
+  private void waitOnScannerToFinish(
+      ArrayList<Optional<Future<?>>> resultFutureList)
+      throws ExecutionException, InterruptedException {
+    for (Optional<Future<?>> future : resultFutureList) {
+      if (future.isPresent()) {
+        future.get().get();
+      }
+    }
+  }
+}
diff --git 
a/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/ozoneimpl/TestOnDemandContainerScanner.java
 
b/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/ozoneimpl/TestOnDemandContainerScanner.java
deleted file mode 100644
index c2686b559c..0000000000
--- 
a/hadoop-hdds/container-service/src/test/java/org/apache/hadoop/ozone/container/ozoneimpl/TestOnDemandContainerScanner.java
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- * 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.container.ozoneimpl;
-
-import org.apache.hadoop.ozone.container.common.ContainerTestUtils;
-import org.apache.hadoop.ozone.container.common.impl.ContainerData;
-import org.apache.hadoop.ozone.container.common.interfaces.Container;
-import org.junit.After;
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.Mockito;
-import org.mockito.junit.MockitoJUnitRunner;
-import org.mockito.stubbing.Answer;
-import org.mockito.verification.VerificationMode;
-
-import java.io.IOException;
-import java.util.Optional;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Future;
-import java.util.concurrent.atomic.AtomicLong;
-
-import static org.apache.hadoop.hdds.conf.OzoneConfiguration.newInstanceOf;
-import static org.mockito.Mockito.atLeastOnce;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-
-/**
- * Unit tests for the on-demand container scanner.
- */
-@RunWith(MockitoJUnitRunner.class)
-public class TestOnDemandContainerScanner {
-
-  private final AtomicLong containerIdSeq = new AtomicLong(100);
-
-  @Mock
-  private Container<ContainerData> healthy;
-
-  @Mock
-  private Container<ContainerData> openContainer;
-
-  @Mock
-  private Container<ContainerData> corruptData;
-
-  private ContainerScannerConfiguration conf;
-  private ContainerController controller;
-
-  @Before
-  public void setup() {
-    conf = newInstanceOf(ContainerScannerConfiguration.class);
-    conf.setMetadataScanInterval(0);
-    conf.setDataScanInterval(0);
-    controller = mockContainerController();
-  }
-
-  @After
-  public void tearDown() {
-    OnDemandContainerScanner.shutdown();
-  }
-
-  @Test
-  public void testOnDemandContainerScanner() throws Exception {
-    //Without initialization,
-    // there shouldn't be interaction with containerController
-    OnDemandContainerScanner.scanContainer(corruptData);
-    Mockito.verifyZeroInteractions(controller);
-    OnDemandContainerScanner.init(conf, controller);
-    testContainerMarkedUnhealthy(healthy, never());
-    testContainerMarkedUnhealthy(corruptData, atLeastOnce());
-    testContainerMarkedUnhealthy(openContainer, never());
-  }
-
-  @Test
-  public void testContainerScannerMultipleInitsAndShutdowns() throws Exception 
{
-    OnDemandContainerScanner.init(conf, controller);
-    OnDemandContainerScanner.init(conf, controller);
-    OnDemandContainerScanner.shutdown();
-    OnDemandContainerScanner.shutdown();
-    //There shouldn't be an interaction after shutdown:
-    testContainerMarkedUnhealthy(corruptData, never());
-  }
-
-  @Test
-  public void testSameContainerQueuedMultipleTimes() throws Exception {
-    OnDemandContainerScanner.init(conf, controller);
-    //Given a container that has not finished scanning
-    CountDownLatch latch = new CountDownLatch(1);
-    Mockito.lenient().when(corruptData.scanData(
-            OnDemandContainerScanner.getThrottler(),
-            OnDemandContainerScanner.getCanceler()))
-        .thenAnswer((Answer<Boolean>) invocation -> {
-          latch.await();
-          return false;
-        });
-    Optional<Future<?>> onGoingScan = OnDemandContainerScanner
-        .scanContainer(corruptData);
-    Assert.assertTrue(onGoingScan.isPresent());
-    Assert.assertFalse(onGoingScan.get().isDone());
-    //When scheduling the same container again
-    Optional<Future<?>> secondScan = OnDemandContainerScanner
-        .scanContainer(corruptData);
-    //Then the second scan is not scheduled and the first scan can still finish
-    Assert.assertFalse(secondScan.isPresent());
-    latch.countDown();
-    onGoingScan.get().get();
-    Mockito.verify(controller, atLeastOnce()).
-        
markContainerUnhealthy(corruptData.getContainerData().getContainerID());
-  }
-
-  private void testContainerMarkedUnhealthy(
-      Container<?> container, VerificationMode invocationTimes)
-      throws InterruptedException, ExecutionException, IOException {
-    Optional<Future<?>> result =
-        OnDemandContainerScanner.scanContainer(container);
-    if (result.isPresent()) {
-      result.get().get();
-    }
-    Mockito.verify(controller, invocationTimes).markContainerUnhealthy(
-        container.getContainerData().getContainerID());
-  }
-
-  private ContainerController mockContainerController() {
-    // healthy container
-    ContainerTestUtils.setupMockContainer(healthy,
-        true, true, containerIdSeq);
-
-    // unhealthy container (corrupt data)
-    ContainerTestUtils.setupMockContainer(corruptData,
-        true, false, containerIdSeq);
-
-    // unhealthy container (corrupt metadata)
-    ContainerTestUtils.setupMockContainer(openContainer,
-        false, false, containerIdSeq);
-
-    return mock(ContainerController.class);
-  }
-}
diff --git 
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/dn/scanner/TestDataScanner.java
 
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/dn/scanner/TestContainerDataScanners.java
similarity index 97%
rename from 
hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/dn/scanner/TestDataScanner.java
rename to 
hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/dn/scanner/TestContainerDataScanners.java
index f0ad740c45..16519a0001 100644
--- 
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/dn/scanner/TestDataScanner.java
+++ 
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/dn/scanner/TestContainerDataScanners.java
@@ -46,7 +46,7 @@ import org.apache.hadoop.ozone.container.ContainerTestHelper;
 import org.apache.hadoop.ozone.container.TestHelper;
 import org.apache.hadoop.ozone.container.common.impl.ContainerSet;
 import org.apache.hadoop.ozone.container.common.interfaces.Container;
-import org.apache.hadoop.ozone.container.ozoneimpl.ContainerMetadataScanner;
+import 
org.apache.hadoop.ozone.container.ozoneimpl.BackgroundContainerMetadataScanner;
 import 
org.apache.hadoop.ozone.container.ozoneimpl.ContainerScannerConfiguration;
 import org.apache.hadoop.ozone.container.ozoneimpl.OzoneContainer;
 import org.junit.AfterClass;
@@ -71,7 +71,7 @@ import static 
org.apache.hadoop.hdds.protocol.proto.StorageContainerDatanodeProt
 /**
  * This class tests the data scanner functionality.
  */
-public class TestDataScanner {
+public class TestContainerDataScanners {
 
   /**
    * Set a timeout for each test.
@@ -168,8 +168,8 @@ public class TestDataScanner {
 
     ContainerScannerConfiguration conf = ozoneConfig.getObject(
         ContainerScannerConfiguration.class);
-    ContainerMetadataScanner sb = new ContainerMetadataScanner(conf,
-        oc.getController());
+    BackgroundContainerMetadataScanner sb =
+        new BackgroundContainerMetadataScanner(conf, oc.getController());
     //Scan the open container and trigger on-demand scan for the closed one
     sb.scanContainer(openContainer);
     tryReadKeyWithMissingChunksDir(bucket, keyNameInClosedContainer);


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to