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

devmadhuu 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 c31553bcded HDDS-15766. Make Recon OM DB large tarball transfer 
reliable (#10696)
c31553bcded is described below

commit c31553bcdedbaf72d35de066b539f07dad1a8f64
Author: Devesh Kumar Singh <[email protected]>
AuthorDate: Tue Jul 21 10:00:59 2026 +0530

    HDDS-15766. Make Recon OM DB large tarball transfer reliable (#10696)
    
    Generated-by: Cursor (Opus 4.8)
    Co-authored-by: Devesh Kumar Singh <[email protected]>
---
 .../hadoop/hdds/utils/RDBSnapshotProvider.java     |   2 +-
 .../ozone/recon/TestReconWithOzoneManagerHA.java   |   9 -
 .../spi/impl/OzoneManagerServiceProviderImpl.java  | 202 +++++++--------
 .../recon/spi/impl/ReconRDBSnapshotProvider.java   | 245 ++++++++++++++++++
 .../ozone/recon/api/TestTriggerDBSyncEndpoint.java |   1 +
 .../impl/TestOzoneManagerServiceProviderImpl.java  | 281 +++++++++++----------
 .../spi/impl/TestReconRDBSnapshotProvider.java     | 223 ++++++++++++++++
 7 files changed, 712 insertions(+), 251 deletions(-)

diff --git 
a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/utils/RDBSnapshotProvider.java
 
b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/utils/RDBSnapshotProvider.java
index 1b00571f72e..8253f7bb112 100644
--- 
a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/utils/RDBSnapshotProvider.java
+++ 
b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/utils/RDBSnapshotProvider.java
@@ -126,7 +126,7 @@ public DBCheckpoint downloadDBSnapshotFromLeader(String 
leaderNodeID)
       LOG.info("Successfully untar the downloaded snapshot {} at {}.",
           targetFile, unTarredDb.toAbsolutePath());
       if (ratisSnapshotComplete(unTarredDb)) {
-        LOG.info("Ratis snapshot transfer is complete.");
+        LOG.info("DB snapshot transfer is complete.");
         return getCheckpointFromUntarredDb(unTarredDb);
       }
     }
diff --git 
a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconWithOzoneManagerHA.java
 
b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconWithOzoneManagerHA.java
index 6533f482c8f..c2fca580fda 100644
--- 
a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconWithOzoneManagerHA.java
+++ 
b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconWithOzoneManagerHA.java
@@ -18,7 +18,6 @@
 package org.apache.hadoop.ozone.recon;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
-import static 
org.apache.hadoop.ozone.OzoneConsts.OZONE_DB_CHECKPOINT_HTTP_ENDPOINT;
 import static 
org.apache.hadoop.ozone.recon.ReconOmMetaManagerTestUtils.waitForEventBufferEmpty;
 import static 
org.apache.hadoop.ozone.recon.ReconOmMetaManagerTestUtils.waitUntilReconKeyCounts;
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -120,14 +119,6 @@ public void testReconGetsSnapshotFromLeader() throws 
Exception {
     OzoneManagerServiceProviderImpl impl = (OzoneManagerServiceProviderImpl)
         recon.getReconServer().getOzoneManagerServiceProvider();
 
-    String hostname =
-        ozoneManager.get().getHttpServer().getHttpAddress().getHostName();
-    String expectedUrl = "http://"; +
-        (hostname.equals("0.0.0.0") ? "localhost" : hostname) + ":" +
-        ozoneManager.get().getHttpServer().getHttpAddress().getPort() +
-        OZONE_DB_CHECKPOINT_HTTP_ENDPOINT;
-    String snapshotUrl = impl.getOzoneManagerSnapshotUrl();
-    assertEquals(expectedUrl, snapshotUrl);
     // Write some data
     String keyPrefix = "ratis";
     OzoneOutputStream key = objectStore.getVolume(VOL_NAME)
diff --git 
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/spi/impl/OzoneManagerServiceProviderImpl.java
 
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/spi/impl/OzoneManagerServiceProviderImpl.java
index f5d7e4e0acf..bec49088d27 100644
--- 
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/spi/impl/OzoneManagerServiceProviderImpl.java
+++ 
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/spi/impl/OzoneManagerServiceProviderImpl.java
@@ -18,8 +18,8 @@
 package org.apache.hadoop.ozone.recon.spi.impl;
 
 import static 
org.apache.hadoop.hdds.recon.ReconConfigKeys.OZONE_RECON_DB_DIRS_PERMISSIONS_DEFAULT;
-import static 
org.apache.hadoop.ozone.OzoneConsts.OZONE_DB_CHECKPOINT_HTTP_ENDPOINT;
-import static 
org.apache.hadoop.ozone.OzoneConsts.OZONE_DB_CHECKPOINT_REQUEST_FLUSH;
+import static 
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_DB_CHECKPOINT_USE_INODE_BASED_DEFAULT;
+import static 
org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_DB_CHECKPOINT_USE_INODE_BASED_KEY;
 import static org.apache.hadoop.ozone.om.OMConfigKeys.OZONE_OM_HTTP_AUTH_TYPE;
 import static 
org.apache.hadoop.ozone.recon.ReconConstants.RECON_OM_SNAPSHOT_DB;
 import static org.apache.hadoop.ozone.recon.ReconConstants.STAGING;
@@ -45,10 +45,8 @@
 import com.google.common.util.concurrent.ThreadFactoryBuilder;
 import java.io.File;
 import java.io.IOException;
-import java.io.InputStream;
+import java.io.UncheckedIOException;
 import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
 import java.nio.file.attribute.PosixFilePermission;
 import java.nio.file.attribute.PosixFilePermissions;
 import java.util.Arrays;
@@ -73,23 +71,20 @@
 import org.apache.hadoop.hdds.utils.db.DBCheckpoint;
 import org.apache.hadoop.hdds.utils.db.RDBBatchOperation;
 import org.apache.hadoop.hdds.utils.db.RDBStore;
-import org.apache.hadoop.hdds.utils.db.RocksDBCheckpoint;
 import org.apache.hadoop.hdds.utils.db.RocksDatabase;
 import org.apache.hadoop.hdds.utils.db.Table;
 import org.apache.hadoop.hdds.utils.db.TableIterator;
 import org.apache.hadoop.hdds.utils.db.managed.ManagedWriteBatch;
 import org.apache.hadoop.hdds.utils.db.managed.ManagedWriteOptions;
 import org.apache.hadoop.hdfs.web.URLConnectionFactory;
-import org.apache.hadoop.ozone.om.OMConfigKeys;
 import org.apache.hadoop.ozone.om.OMMetadataManager;
 import org.apache.hadoop.ozone.om.helpers.DBUpdates;
+import org.apache.hadoop.ozone.om.helpers.ServiceInfo;
 import org.apache.hadoop.ozone.om.protocol.OzoneManagerProtocol;
 import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.DBUpdatesRequest;
-import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.ServicePort.Type;
 import org.apache.hadoop.ozone.recon.ReconContext;
 import org.apache.hadoop.ozone.recon.ReconServerConfigKeys;
 import org.apache.hadoop.ozone.recon.ReconUtils;
-import org.apache.hadoop.ozone.recon.TarExtractor;
 import org.apache.hadoop.ozone.recon.metrics.OzoneManagerSyncMetrics;
 import org.apache.hadoop.ozone.recon.metrics.ReconSyncMetrics;
 import org.apache.hadoop.ozone.recon.recovery.ReconOMMetadataManager;
@@ -101,7 +96,6 @@
 import org.apache.hadoop.ozone.recon.tasks.ReconTaskReInitializationEvent;
 import org.apache.hadoop.ozone.recon.tasks.updater.ReconTaskStatusUpdater;
 import 
org.apache.hadoop.ozone.recon.tasks.updater.ReconTaskStatusUpdaterManager;
-import org.apache.hadoop.security.SecurityUtil;
 import org.apache.hadoop.util.Time;
 import org.rocksdb.RocksDBException;
 import org.slf4j.Logger;
@@ -118,9 +112,8 @@ public class OzoneManagerServiceProviderImpl
       LoggerFactory.getLogger(OzoneManagerServiceProviderImpl.class);
   private URLConnectionFactory connectionFactory;
 
-  private File omSnapshotDBParentDir = null;
   private File reconDbDir = null;
-  private String omDBSnapshotUrl;
+  private File omSnapshotDBParentDir = null;
 
   private OzoneManagerProtocol ozoneManagerClient;
   private final OzoneConfiguration configuration;
@@ -140,7 +133,7 @@ public class OzoneManagerServiceProviderImpl
   private ThreadFactory threadFactory;
   private ReconContext reconContext;
   private ReconTaskStatusUpdaterManager taskStatusUpdaterManager;
-  private TarExtractor tarExtractor;
+  private ReconRDBSnapshotProvider reconSnapshotProvider;
 
   /**
    * OM Snapshot related task names.
@@ -179,12 +172,6 @@ public OzoneManagerServiceProviderImpl(
         URLConnectionFactory.newDefaultURLConnectionFactory(connectionTimeout,
             connectionRequestTimeout, configuration);
 
-    String ozoneManagerHttpAddress = configuration.get(OMConfigKeys
-        .OZONE_OM_HTTP_ADDRESS_KEY);
-
-    String ozoneManagerHttpsAddress = configuration.get(OMConfigKeys
-        .OZONE_OM_HTTPS_ADDRESS_KEY);
-
     long deltaUpdateLimits = configuration.getLong(RECON_OM_DELTA_UPDATE_LIMIT,
         RECON_OM_DELTA_UPDATE_LIMIT_DEFAULT);
 
@@ -195,24 +182,23 @@ public OzoneManagerServiceProviderImpl(
 
     HttpConfig.Policy policy = HttpConfig.getHttpPolicy(configuration);
 
-    omDBSnapshotUrl = "http://"; + ozoneManagerHttpAddress +
-        OZONE_DB_CHECKPOINT_HTTP_ENDPOINT;
-
-    if (policy.isHttpsEnabled()) {
-      omDBSnapshotUrl = "https://"; + ozoneManagerHttpsAddress +
-          OZONE_DB_CHECKPOINT_HTTP_ENDPOINT;
-    }
-
     boolean flushParam = configuration.getBoolean(
         OZONE_RECON_OM_SNAPSHOT_TASK_FLUSH_PARAM,
         configuration.getBoolean(
             ReconServerConfigKeys.RECON_OM_SNAPSHOT_TASK_FLUSH_PARAM,
             false)
     );
-
-    if (flushParam) {
-      omDBSnapshotUrl += "?" + OZONE_DB_CHECKPOINT_REQUEST_FLUSH + "=true";
-    }
+    // Same switch OM followers honor: use the inode-based v2 checkpoint 
endpoint
+    // by default, or fall back to the v1 endpoint when disabled (e.g. mixed
+    // versions during an upgrade).
+    // NOTE: this flag is read from Recon's OzoneConfiguration, not OM's. 
During
+    // a mixed-version rollout, operators must set
+    // ozone.om.db.checkpoint.use.inode.based.transfer on Recon to match the
+    // OM cluster's setting; otherwise Recon may hit an endpoint the OM side
+    // does not serve.
+    boolean useV2CheckpointApi = configuration.getBoolean(
+        OZONE_OM_DB_CHECKPOINT_USE_INODE_BASED_KEY,
+        OZONE_OM_DB_CHECKPOINT_USE_INODE_BASED_DEFAULT);
 
     this.reconUtils = reconUtils;
     this.omMetadataManager = omMetadataManager;
@@ -228,13 +214,16 @@ public OzoneManagerServiceProviderImpl(
     this.threadFactory =
         new ThreadFactoryBuilder().setNameFormat(threadNamePrefix + 
"SyncOM-%d")
             .build();
-    // Number of parallel workers
-    int omDBTarProcessorThreadCount = Math.min(64, 
Runtime.getRuntime().availableProcessors());
     this.reconContext = reconContext;
     this.taskStatusUpdaterManager = taskStatusUpdaterManager;
     this.omDBLagThreshold = 
configuration.getLong(RECON_OM_DELTA_UPDATE_LAG_THRESHOLD,
         RECON_OM_DELTA_UPDATE_LAG_THRESHOLD_DEFAULT);
-    this.tarExtractor = new TarExtractor(omDBTarProcessorThreadCount, 
threadNamePrefix);
+    // Download the full OM DB snapshot by reusing OM's checkpoint transfer 
path
+    // (the same one an OM follower uses): POST /v2/dbCheckpoint, or the v1
+    // /dbCheckpoint endpoint when the inode-based transfer is disabled.
+    this.reconSnapshotProvider = new ReconRDBSnapshotProvider(
+        omSnapshotDBParentDir, connectionFactory, isOmSpnegoEnabled(), policy,
+        flushParam, useV2CheckpointApi, this::getLeaderServiceInfo);
   }
 
   @Override
@@ -247,7 +236,6 @@ public void start() {
     LOG.info("Starting Ozone Manager Service Provider.");
     scheduler = Executors.newScheduledThreadPool(1, threadFactory);
     try {
-      tarExtractor.start();
       omMetadataManager.start(configuration);
     } catch (IOException ioEx) {
       LOG.error("Error starting Recon OM Metadata Manager.", ioEx);
@@ -375,7 +363,6 @@ private void stopSyncDataFromOMThread() {
       scheduler.shutdownNow();
       Thread.currentThread().interrupt();
     }
-    tarExtractor.stop();
     LOG.debug("Shutdown the OM DB sync scheduler.");
   }
 
@@ -387,7 +374,6 @@ public boolean triggerSyncDataFromOMImmediately() {
       // immediately.
       stopSyncDataFromOMThread();
       scheduler = Executors.newScheduledThreadPool(1, threadFactory);
-      tarExtractor.start();
       startSyncDataFromOM(0L);
       return true;
     } else {
@@ -402,54 +388,59 @@ public void stop() throws Exception {
     reconTaskController.stop();
     omMetadataManager.stop();
     scheduler.shutdownNow();
-    tarExtractor.stop();
+    reconSnapshotProvider.close();
     metrics.unRegister();
     reconSyncMetrics.unRegister();
     connectionFactory.destroy();
   }
 
+  private boolean isOmSpnegoEnabled() {
+    return configuration.get(OZONE_OM_HTTP_AUTH_TYPE, "simple")
+        .equals("kerberos");
+  }
+
   /**
-   * Find the OM leader's address to get the snapshot from.
+   * Return the current OM leader's {@link ServiceInfo} from the OM service
+   * list. The retained transfer checkpoint is local to the leader, so Recon
+   * always downloads from (and resumes against) the leader.
    */
-  @VisibleForTesting
-  public String getOzoneManagerSnapshotUrl() throws IOException {
-    String omLeaderUrl = omDBSnapshotUrl;
-    List<org.apache.hadoop.ozone.om.helpers.ServiceInfo> serviceList =
-        ozoneManagerClient.getServiceList();
-    HttpConfig.Policy policy = HttpConfig.getHttpPolicy(configuration);
-    if (!serviceList.isEmpty()) {
-      for (org.apache.hadoop.ozone.om.helpers.ServiceInfo info : serviceList) {
-        if (info.getNodeType().equals(HddsProtos.NodeType.OM) &&
-            info.getOmRoleInfo().hasServerRole() &&
-            info.getOmRoleInfo().getServerRole().equals(LEADER.name())) {
-          omLeaderUrl = (policy.isHttpsEnabled() ?
-              "https://"; + info.getServiceAddress(Type.HTTPS) :
-              "http://"; + info.getServiceAddress(Type.HTTP)) +
-              OZONE_DB_CHECKPOINT_HTTP_ENDPOINT;
+  private ServiceInfo getLeaderServiceInfo() {
+    try {
+      List<ServiceInfo> serviceList = ozoneManagerClient.getServiceList();
+      for (ServiceInfo info : serviceList) {
+        if (info.getNodeType().equals(HddsProtos.NodeType.OM)
+            && info.getOmRoleInfo().hasServerRole()
+            && info.getOmRoleInfo().getServerRole().equals(LEADER.name())) {
+          return info;
         }
       }
+    } catch (IOException e) {
+      throw new UncheckedIOException("Failed to fetch OM service list", e);
     }
-    return omLeaderUrl;
+    throw new IllegalStateException("No OM leader found in the OM service 
list.");
   }
 
-  private boolean isOmSpnegoEnabled() {
-    return configuration.get(OZONE_OM_HTTP_AUTH_TYPE, "simple")
-        .equals("kerberos");
+  private String getLeaderNodeId() {
+    return getLeaderServiceInfo().getOmRoleInfo().getNodeId();
   }
 
   /**
-   * Method to obtain current OM DB Snapshot.
-   * @return DBCheckpoint instance.
+   * Obtain the current OM DB snapshot using the same OM-follower bootstrap
+   * mechanism (chunked, resumable {@code POST /v2/dbCheckpoint} with hard-link
+   * dedup on the leader). The returned {@link DBCheckpoint} points at
+   * a stable {@code om.snapshot.db_<ts>} directory ready to be promoted by
+   * {@link #updateReconOmDBWithNewSnapshot()}; returns {@code null} on 
failure,
+   * leaving the current active DB untouched.
+   *
+   * @return DBCheckpoint instance, or {@code null} on failure.
    */
   @VisibleForTesting
   public DBCheckpoint getOzoneManagerDBSnapshot() {
-    String snapshotFileName = RECON_OM_SNAPSHOT_DB + "_" + 
System.currentTimeMillis();
-    Path untarredDbDir = Paths.get(omSnapshotDBParentDir.getAbsolutePath(), 
snapshotFileName);
-
-    // Before fetching full snapshot again and create a new OM DB snapshot 
directory, check and delete
-    // any existing OM DB snapshot directories under recon om db dir location 
and delete all such
-    // om db snapshot dirs including the last known om db snapshot dir 
returned by reconUtils.getLastKnownDB
-    File lastKnownDB = reconUtils.getLastKnownDB(omSnapshotDBParentDir, 
RECON_OM_SNAPSHOT_DB);
+    // Before fetching a new full snapshot, delete the last known OM DB 
snapshot
+    // dir so we don't hold two full copies at once (keeps peak disk ~1x). This
+    // also clears any snapshot dir left over after switching v1/v2 endpoints.
+    File lastKnownDB = reconUtils.getLastKnownDB(omSnapshotDBParentDir,
+        RECON_OM_SNAPSHOT_DB);
     if (lastKnownDB != null) {
       boolean existingOmSnapshotDBDeleted = 
FileUtils.deleteQuietly(lastKnownDB);
       if (existingOmSnapshotDBDeleted) {
@@ -461,56 +452,58 @@ public DBCheckpoint getOzoneManagerDBSnapshot() {
       }
     }
 
-    // Now below cleanup operation will even remove any left over staging dirs 
in recon om db dir location which
-    // may be left due to any previous partial extraction of tar entries and 
during copy sst files process by
-    // tarExtractor.extractTar
-    File[] leftOverStagingDirs = omSnapshotDBParentDir.listFiles(f -> 
f.getName().startsWith(STAGING));
+    // Remove any leftover staging dirs from a previous partial extraction
+    // (for example artifacts left by the v1 tar-extraction path).
+    File[] leftOverStagingDirs =
+        omSnapshotDBParentDir.listFiles(f -> f.getName().startsWith(STAGING));
     if (leftOverStagingDirs != null) {
       for (File stagingDir : leftOverStagingDirs) {
-        LOG.warn("Cleaning up leftover staging folder from failed extraction: 
{}", stagingDir.getAbsolutePath());
-        boolean stagingDirDeleted = FileUtils.deleteQuietly(stagingDir);
-        if (stagingDirDeleted) {
-          LOG.info("Successfully deleted leftover staging folder: {}", 
stagingDir.getAbsolutePath());
+        LOG.warn("Cleaning up leftover staging folder from failed extraction: 
{}",
+            stagingDir.getAbsolutePath());
+        if (FileUtils.deleteQuietly(stagingDir)) {
+          LOG.info("Successfully deleted leftover staging folder: {}",
+              stagingDir.getAbsolutePath());
         } else {
-          LOG.warn("Failed to delete leftover staging folder: {}", 
stagingDir.getAbsolutePath());
+          LOG.warn("Failed to delete leftover staging folder: {}",
+              stagingDir.getAbsolutePath());
         }
       }
     }
 
     try {
-      SecurityUtil.doAsLoginUser(() -> {
-        try (InputStream inputStream = reconUtils.makeHttpCall(
-            connectionFactory, getOzoneManagerSnapshotUrl(), 
isOmSpnegoEnabled()).getInputStream()) {
-          tarExtractor.extractTar(inputStream, untarredDbDir);
-        } catch (IOException | InterruptedException e) {
-          reconContext.updateHealthStatus(new AtomicBoolean(false));
-          
reconContext.updateErrors(ReconContext.ErrorCode.GET_OM_DB_SNAPSHOT_FAILED);
-          throw new RuntimeException("Error while extracting OM DB Snapshot 
TAR.", e);
-        }
-        return null;
-      });
-      // Validate extracted files
-      File[] sstFiles = untarredDbDir.toFile().listFiles((dir, name) -> 
name.endsWith(".sst"));
+      String leaderNodeId = getLeaderNodeId();
+      DBCheckpoint checkpoint =
+          reconSnapshotProvider.downloadDBSnapshotFromLeader(leaderNodeId);
+
+      // Validate the assembled snapshot: log how many SST files it contains.
+      File untarredDbDir = checkpoint.getCheckpointLocation().toFile();
+      File[] sstFiles =
+          untarredDbDir.listFiles((dir, name) -> name.endsWith(".sst"));
       if (sstFiles != null && sstFiles.length > 0) {
-        LOG.info("Number of SST files found in the OM snapshot directory: {} - 
{}", untarredDbDir, sstFiles.length);
+        LOG.info("Number of SST files found in the OM snapshot directory: {} - 
{}",
+            untarredDbDir, sstFiles.length);
+        if (LOG.isDebugEnabled()) {
+          LOG.debug("Valid SST files found: {}", Arrays.stream(sstFiles)
+              .map(File::getName).collect(Collectors.toList()));
+        }
       }
 
-      List<String> sstFileNames = Arrays.stream(sstFiles)
-          .map(File::getName)
-          .collect(Collectors.toList());
-      LOG.debug("Valid SST files found: {}", sstFileNames);
-
-      // Currently, OM DB type is not configurable. Hence, defaulting to
-      // RocksDB.
       reconContext.updateHealthStatus(new AtomicBoolean(true));
-      
reconContext.getErrors().remove(ReconContext.ErrorCode.GET_OM_DB_SNAPSHOT_FAILED);
-      return new RocksDBCheckpoint(untarredDbDir);
-    } catch (IOException e) {
+      reconContext.getErrors()
+          .remove(ReconContext.ErrorCode.GET_OM_DB_SNAPSHOT_FAILED);
+      return checkpoint;
+    } catch (IOException | RuntimeException e) {
       LOG.error("Unable to obtain Ozone Manager DB Snapshot.", e);
       reconContext.updateHealthStatus(new AtomicBoolean(false));
-      
reconContext.updateErrors(ReconContext.ErrorCode.GET_OM_DB_SNAPSHOT_FAILED);
+      reconContext.updateErrors(
+          ReconContext.ErrorCode.GET_OM_DB_SNAPSHOT_FAILED);
+      // Do not wipe the candidate dir here. The shared RDBSnapshotProvider's
+      // checkLeaderConsistency() manages it on the next attempt: kept for the
+      // same leader (so a future batched/chunked transfer can resume the parts
+      // already received), reset on a leader change. This matches how the OM
+      // follower handles a failed download.
+      return null;
     }
-    return null;
   }
 
   /**
@@ -993,8 +986,9 @@ public OzoneManagerSyncMetrics getMetrics() {
   }
 
   @VisibleForTesting
-  public TarExtractor getTarExtractor() {
-    return tarExtractor;
+  public void setReconSnapshotProvider(
+      ReconRDBSnapshotProvider reconSnapshotProvider) {
+    this.reconSnapshotProvider = reconSnapshotProvider;
   }
 
 }
diff --git 
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/spi/impl/ReconRDBSnapshotProvider.java
 
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/spi/impl/ReconRDBSnapshotProvider.java
new file mode 100644
index 00000000000..7241178e9b9
--- /dev/null
+++ 
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/spi/impl/ReconRDBSnapshotProvider.java
@@ -0,0 +1,245 @@
+/*
+ * 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.recon.spi.impl;
+
+import static java.net.HttpURLConnection.HTTP_CREATED;
+import static java.net.HttpURLConnection.HTTP_OK;
+import static org.apache.hadoop.ozone.OzoneConsts.MULTIPART_FORM_DATA_BOUNDARY;
+import static org.apache.hadoop.ozone.OzoneConsts.OM_DB_NAME;
+import static 
org.apache.hadoop.ozone.OzoneConsts.OZONE_DB_CHECKPOINT_HTTP_ENDPOINT;
+import static 
org.apache.hadoop.ozone.OzoneConsts.OZONE_DB_CHECKPOINT_HTTP_ENDPOINT_V2;
+import static 
org.apache.hadoop.ozone.OzoneConsts.OZONE_DB_CHECKPOINT_INCLUDE_SNAPSHOT_DATA;
+import static 
org.apache.hadoop.ozone.OzoneConsts.OZONE_DB_CHECKPOINT_REQUEST_FLUSH;
+import static 
org.apache.hadoop.ozone.recon.ReconConstants.RECON_OM_SNAPSHOT_DB;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Supplier;
+import org.apache.commons.io.FileUtils;
+import org.apache.hadoop.hdds.server.http.HttpConfig;
+import org.apache.hadoop.hdds.utils.HAUtils;
+import org.apache.hadoop.hdds.utils.RDBSnapshotProvider;
+import org.apache.hadoop.hdds.utils.db.DBCheckpoint;
+import org.apache.hadoop.hdds.utils.db.InodeMetadataRocksDBCheckpoint;
+import org.apache.hadoop.hdds.utils.db.RocksDBCheckpoint;
+import org.apache.hadoop.hdfs.web.URLConnectionFactory;
+import org.apache.hadoop.ozone.om.helpers.ServiceInfo;
+import org.apache.hadoop.ozone.om.ratis_snapshot.OmRatisSnapshotProvider;
+import 
org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos.ServicePort.Type;
+import org.apache.hadoop.security.SecurityUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Recon's {@link RDBSnapshotProvider} implementation that downloads the OM DB
+ * checkpoint using the same bootstrap mechanism an OM follower uses: a chunked
+ * {@code POST /v2/dbCheckpoint} request that carries a {@code toExcludeList[]}
+ * of the parts already received so an interrupted transfer can resume, with
+ * hard-link dedup on the leader and a completion sentinel to end the transfer.
+ *
+ * <p>This mirrors {@link OmRatisSnapshotProvider}: it overrides
+ * {@link #downloadSnapshot} to POST to the leader's {@code /v2/dbCheckpoint}
+ * endpoint and {@link #getCheckpointFromUntarredDb} to assemble and promote 
the
+ * downloaded DB into a stable snapshot dir Recon can open. Like the OM 
follower,
+ * it honors {@code ozone.om.db.checkpoint.use.inode.based.transfer}: when 
that is
+ * {@code false} it falls back to the v1 {@code /dbCheckpoint} endpoint.
+ */
+public class ReconRDBSnapshotProvider extends RDBSnapshotProvider {
+
+  private static final Logger LOG =
+      LoggerFactory.getLogger(ReconRDBSnapshotProvider.class);
+
+  private final URLConnectionFactory connectionFactory;
+  private final boolean spnegoEnabled;
+  private final boolean httpsEnabled;
+  private final boolean flushBeforeCheckpoint;
+  private final boolean useV2CheckpointApi;
+  private final Supplier<ServiceInfo> leaderInfoSupplier;
+  // Leader pinned for the duration of a single (possibly multi-part) transfer.
+  private final AtomicReference<ServiceInfo> pinnedLeader =
+      new AtomicReference<>();
+
+  public ReconRDBSnapshotProvider(File snapshotDir,
+      URLConnectionFactory connectionFactory, boolean spnegoEnabled,
+      HttpConfig.Policy httpPolicy, boolean flushBeforeCheckpoint,
+      boolean useV2CheckpointApi,
+      Supplier<ServiceInfo> leaderInfoSupplier) {
+    super(snapshotDir, RECON_OM_SNAPSHOT_DB);
+    this.connectionFactory = connectionFactory;
+    this.spnegoEnabled = spnegoEnabled;
+    this.httpsEnabled = httpPolicy.isHttpsEnabled();
+    this.flushBeforeCheckpoint = flushBeforeCheckpoint;
+    this.useV2CheckpointApi = useV2CheckpointApi;
+    this.leaderInfoSupplier = leaderInfoSupplier;
+  }
+
+  /**
+   * Pin the OM leader for the whole transfer before delegating to the shared
+   * driver loop. The base loop resolves the leader only once (via
+   * {#checkLeaderConsistency}); re-resolving per part would let a
+   * mid-transfer OM failover merge parts from two leaders into the same
+   * candidate dir. Recon's transfer is single-part today (it excludes snapshot
+   * data), but pinning keeps this correct if that ever changes, matching how
+   * {@link OmRatisSnapshotProvider} pins the leader for every part.
+   */
+  @Override
+  public DBCheckpoint downloadDBSnapshotFromLeader(String leaderNodeID)
+      throws IOException {
+    pinnedLeader.set(leaderInfoSupplier.get());
+    try {
+      return super.downloadDBSnapshotFromLeader(leaderNodeID);
+    } finally {
+      pinnedLeader.set(null);
+    }
+  }
+
+  @Override
+  public void downloadSnapshot(String leaderNodeID, File targetFile)
+      throws IOException {
+    // Use the leader pinned for this transfer. The fallback only applies to a
+    // direct downloadSnapshot call outside downloadDBSnapshotFromLeader.
+    ServiceInfo leader = pinnedLeader.get();
+    if (leader == null) {
+      leader = leaderInfoSupplier.get();
+    }
+    URL checkpointUrl = buildCheckpointUrl(leader);
+    LOG.info("Downloading OM DB checkpoint from leader {}. Checkpoint: {}, "
+        + "URL: {}", leaderNodeID, targetFile.getName(), checkpointUrl);
+    SecurityUtil.doAsLoginUser(() -> {
+      HttpURLConnection connection = (HttpURLConnection)
+          connectionFactory.openConnection(checkpointUrl, spnegoEnabled);
+      connection.setRequestMethod("POST");
+      connection.setRequestProperty("Content-Type",
+          "multipart/form-data; boundary=" + MULTIPART_FORM_DATA_BOUNDARY);
+      connection.setDoOutput(true);
+
+      List<String> existingFiles = useV2CheckpointApi
+          ? HAUtils.getExistingFiles(getCandidateDir())
+          : HAUtils.getExistingSstFilesRelativeToDbDir(getCandidateDir());
+      OmRatisSnapshotProvider.writeFormData(connection, existingFiles);
+
+      connection.connect();
+      int errorCode = connection.getResponseCode();
+      if (errorCode != HTTP_OK && errorCode != HTTP_CREATED) {
+        throw new IOException("Unexpected response code " + errorCode
+            + " when downloading OM DB checkpoint from " + checkpointUrl);
+      }
+      try (InputStream inputStream = connection.getInputStream()) {
+        OmRatisSnapshotProvider.downloadFileWithProgress(inputStream,
+            targetFile);
+      } catch (IOException ex) {
+        if (!FileUtils.deleteQuietly(targetFile)) {
+          LOG.error("Failed to delete partial checkpoint file {}", targetFile);
+        }
+        throw ex;
+      } finally {
+        connection.disconnect();
+      }
+      return null;
+    });
+  }
+
+  /**
+   * After the transfer completes, install the leader's hard-link inventory,
+   * normalize the layout to {@code <candidate>/om.db}, then move that DB out 
of
+   * the reused candidate dir into a stable timestamped snapshot dir that Recon
+   * opens as its new live DB. The candidate dir is emptied so the next sync
+   * starts from a clean candidate dir.
+   */
+  @Override
+  public DBCheckpoint getCheckpointFromUntarredDb(Path untarredDbDir)
+      throws IOException {
+    // The base class only calls this once it has seen the leader's
+    // end-of-tarball marker. That marker is named "ratis snapshot complete"
+    // for historical reasons, but it is not Ratis-specific: OM's shared
+    // DBCheckpointServlet appends it to the end of every /v2/dbCheckpoint
+    // response (the same one an OM follower bootstraps from), so here it just
+    // means "the leader finished sending the checkpoint".
+
+    // Installs hard links from hardLinkFile (tolerates a missing/empty file)
+    // and moves root-level DB files into <untarredDbDir>/om.db. 
deleteSourceFiles
+    // follows the endpoint: true for v2 (inode-based), false for the v1 
layout.
+    new InodeMetadataRocksDBCheckpoint(untarredDbDir, useV2CheckpointApi);
+
+    Path omDbDir = untarredDbDir.resolve(OM_DB_NAME);
+    if (!Files.isDirectory(omDbDir)) {
+      throw new IOException("Expected RocksDB directory not found after "
+          + "assembling checkpoint: " + omDbDir);
+    }
+
+    String stableName = RECON_OM_SNAPSHOT_DB + "_" + 
System.currentTimeMillis();
+    Path stablePath = getSnapshotDir().toPath().resolve(stableName);
+    Files.move(omDbDir, stablePath);
+    LOG.info("Assembled OM DB moved from {} to {}", omDbDir, stablePath);
+
+    // Clear residual entries (completion flag, orphan seeded files, empty 
dirs)
+    // so the candidate dir is empty for the next sync cycle.
+    cleanupCandidateDir(untarredDbDir.toFile());
+
+    return new RocksDBCheckpoint(stablePath);
+  }
+
+  URL buildCheckpointUrl(ServiceInfo leader) throws IOException {
+    Type portType = httpsEnabled ? Type.HTTPS : Type.HTTP;
+    String scheme = httpsEnabled ? "https" : "http";
+    String path = useV2CheckpointApi ? OZONE_DB_CHECKPOINT_HTTP_ENDPOINT_V2
+        : OZONE_DB_CHECKPOINT_HTTP_ENDPOINT;
+    // Recon does not need OM's nested snapshot data.
+    String query = OZONE_DB_CHECKPOINT_INCLUDE_SNAPSHOT_DATA + "=false&"
+        + OZONE_DB_CHECKPOINT_REQUEST_FLUSH + "="
+        + (flushBeforeCheckpoint ? "true" : "false");
+    try {
+      return new URI(scheme, null, leader.getHostname(),
+          leader.getPort(portType), path, query, null).toURL();
+    } catch (URISyntaxException | MalformedURLException e) {
+      throw new IOException("Could not build OM DB checkpoint URL", e);
+    }
+  }
+
+  ServiceInfo getPinnedLeader() {
+    return pinnedLeader.get();
+  }
+
+  private void cleanupCandidateDir(File candidate) {
+    File[] entries = candidate.listFiles();
+    if (entries == null) {
+      return;
+    }
+    for (File entry : entries) {
+      if (!FileUtils.deleteQuietly(entry)) {
+        LOG.warn("Failed to clean up candidate dir entry {}", entry);
+      }
+    }
+  }
+
+  @Override
+  public void close() {
+    // The URLConnectionFactory is owned and destroyed by
+    // OzoneManagerServiceProviderImpl; nothing to release here.
+  }
+}
diff --git 
a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/api/TestTriggerDBSyncEndpoint.java
 
b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/api/TestTriggerDBSyncEndpoint.java
index 6e2ad040ce1..5ec99ee9b44 100644
--- 
a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/api/TestTriggerDBSyncEndpoint.java
+++ 
b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/api/TestTriggerDBSyncEndpoint.java
@@ -120,6 +120,7 @@ public void setUp() throws IOException, 
AuthenticationException {
     }
     when(reconUtilsMock.makeHttpCall(any(), anyString(), anyBoolean()))
         .thenReturn(httpURLConnectionMock);
+    when(reconUtilsMock.getReconDbDir(any(), 
anyString())).thenCallRealMethod();
     when(reconUtilsMock.getReconNodeDetails(
         any(OzoneConfiguration.class))).thenReturn(
         ReconTestUtils.getReconNodeDetails());
diff --git 
a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/spi/impl/TestOzoneManagerServiceProviderImpl.java
 
b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/spi/impl/TestOzoneManagerServiceProviderImpl.java
index e6690e37703..c5664934116 100644
--- 
a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/spi/impl/TestOzoneManagerServiceProviderImpl.java
+++ 
b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/spi/impl/TestOzoneManagerServiceProviderImpl.java
@@ -22,21 +22,19 @@
 import static 
org.apache.hadoop.ozone.recon.OMMetadataManagerTestUtils.initializeEmptyOmMetadataManager;
 import static 
org.apache.hadoop.ozone.recon.OMMetadataManagerTestUtils.initializeNewOmMetadataManager;
 import static 
org.apache.hadoop.ozone.recon.OMMetadataManagerTestUtils.writeDataToOm;
+import static 
org.apache.hadoop.ozone.recon.ReconConstants.RECON_OM_SNAPSHOT_DB;
 import static 
org.apache.hadoop.ozone.recon.ReconServerConfigKeys.OZONE_RECON_DB_DIR;
 import static 
org.apache.hadoop.ozone.recon.ReconServerConfigKeys.OZONE_RECON_OM_SNAPSHOT_DB_DIR;
 import static 
org.apache.hadoop.ozone.recon.ReconServerConfigKeys.RECON_OM_DELTA_UPDATE_LAG_THRESHOLD;
 import static 
org.apache.hadoop.ozone.recon.ReconServerConfigKeys.RECON_OM_DELTA_UPDATE_LIMIT;
-import static org.apache.hadoop.ozone.recon.ReconUtils.createTarFile;
 import static 
org.apache.hadoop.ozone.recon.spi.impl.OzoneManagerServiceProviderImpl.OmSnapshotTaskName.OmDeltaRequest;
 import static 
org.apache.hadoop.ozone.recon.spi.impl.OzoneManagerServiceProviderImpl.OmSnapshotTaskName.OmSnapshotRequest;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
-import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.anyBoolean;
 import static org.mockito.Mockito.anyString;
 import static org.mockito.Mockito.doCallRealMethod;
 import static org.mockito.Mockito.doNothing;
@@ -47,13 +45,12 @@
 
 import java.io.File;
 import java.io.IOException;
-import java.io.InputStream;
-import java.net.HttpURLConnection;
-import java.nio.file.Files;
 import java.nio.file.Paths;
+import java.util.Collections;
 import java.util.List;
 import org.apache.commons.io.FileUtils;
 import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
 import org.apache.hadoop.hdds.utils.db.DBCheckpoint;
 import org.apache.hadoop.hdds.utils.db.RDBStore;
 import org.apache.hadoop.hdds.utils.db.RocksDatabase;
@@ -62,6 +59,7 @@
 import org.apache.hadoop.ozone.om.OMMetadataManager;
 import org.apache.hadoop.ozone.om.helpers.BucketLayout;
 import org.apache.hadoop.ozone.om.helpers.DBUpdates;
+import org.apache.hadoop.ozone.om.helpers.ServiceInfo;
 import org.apache.hadoop.ozone.om.protocol.OzoneManagerProtocol;
 import org.apache.hadoop.ozone.protocol.proto.OzoneManagerProtocolProtos;
 import org.apache.hadoop.ozone.recon.ReconContext;
@@ -92,6 +90,7 @@ public class TestOzoneManagerServiceProviderImpl {
   private OzoneConfiguration configuration;
   private OzoneManagerProtocol ozoneManagerProtocol;
   private ReconContext reconContext;
+  private File omSnapshotDbParentDir;
 
   @BeforeEach
   public void setUp(@TempDir File dirReconSnapDB, @TempDir File dirReconDB)
@@ -104,6 +103,8 @@ public void setUp(@TempDir File dirReconSnapDB, @TempDir 
File dirReconDB)
     configuration.set("ozone.om.address", "localhost:9862");
     ozoneManagerProtocol = getMockOzoneManagerClient(new DBUpdates());
     reconContext = new ReconContext(configuration, new ReconUtils());
+    omSnapshotDbParentDir = new ReconUtils()
+        .getReconDbDir(configuration, OZONE_RECON_OM_SNAPSHOT_DB_DIR);
   }
 
   @AfterEach
@@ -130,42 +131,37 @@ public void testUpdateReconOmDBWithNewSnapshot(
 
     DBCheckpoint checkpoint = omMetadataManager.getStore()
         .getCheckpoint(true);
-    File tarFile = createTarFile(checkpoint.getCheckpointLocation());
-    try (InputStream inputStream = Files.newInputStream(tarFile.toPath())) {
-      ReconUtils reconUtilsMock = getMockReconUtils();
-      HttpURLConnection httpURLConnectionMock = mock(HttpURLConnection.class);
-      when(httpURLConnectionMock.getInputStream()).thenReturn(inputStream);
-      when(reconUtilsMock.makeHttpCall(any(), anyString(), anyBoolean()))
-          .thenReturn(httpURLConnectionMock);
-      when(reconUtilsMock.getReconNodeDetails(
-          any(OzoneConfiguration.class))).thenReturn(
-          ReconTestUtils.getReconNodeDetails());
-      ReconTaskController reconTaskController = getMockTaskController();
-
-      OzoneManagerServiceProviderImpl ozoneManagerServiceProvider =
-          new OzoneManagerServiceProviderImpl(configuration,
-              reconOMMetadataManager, reconTaskController, reconUtilsMock, 
ozoneManagerProtocol,
-              reconContext, getMockTaskStatusUpdaterManager());
-
-      assertNull(reconOMMetadataManager.getKeyTable(getBucketLayout())
-          .get("/sampleVol/bucketOne/key_one"));
-      assertNull(reconOMMetadataManager.getKeyTable(getBucketLayout())
-          .get("/sampleVol/bucketOne/key_two"));
-
-      ozoneManagerServiceProvider.getTarExtractor().start();
-      assertTrue(ozoneManagerServiceProvider.updateReconOmDBWithNewSnapshot());
-
-      assertNotNull(reconOMMetadataManager.getKeyTable(getBucketLayout())
-          .get("/sampleVol/bucketOne/key_one"));
-      assertNotNull(reconOMMetadataManager.getKeyTable(getBucketLayout())
-          .get("/sampleVol/bucketOne/key_two"));
-
-      // Verifying if context error GET_OM_DB_SNAPSHOT_FAILED is removed
-      
assertFalse(reconContext.getErrors().contains(ReconContext.ErrorCode.GET_OM_DB_SNAPSHOT_FAILED));
-      omMetadataManager.stop();
-      reconOMMetadataManager.stop();
-      reconTaskController.stop();
-    }
+    ReconUtils reconUtilsMock = getMockReconUtils();
+    when(reconUtilsMock.getReconNodeDetails(
+        any(OzoneConfiguration.class))).thenReturn(
+        ReconTestUtils.getReconNodeDetails());
+    ReconTaskController reconTaskController = getMockTaskController();
+
+    OzoneManagerServiceProviderImpl ozoneManagerServiceProvider =
+        new OzoneManagerServiceProviderImpl(configuration,
+            reconOMMetadataManager, reconTaskController, reconUtilsMock, 
ozoneManagerProtocol,
+            reconContext, getMockTaskStatusUpdaterManager());
+    ozoneManagerServiceProvider.setReconSnapshotProvider(
+        newStubSnapshotProvider(omSnapshotDbParentDir,
+            checkpoint.getCheckpointLocation(), false));
+
+    assertNull(reconOMMetadataManager.getKeyTable(getBucketLayout())
+        .get("/sampleVol/bucketOne/key_one"));
+    assertNull(reconOMMetadataManager.getKeyTable(getBucketLayout())
+        .get("/sampleVol/bucketOne/key_two"));
+
+    assertTrue(ozoneManagerServiceProvider.updateReconOmDBWithNewSnapshot());
+
+    assertNotNull(reconOMMetadataManager.getKeyTable(getBucketLayout())
+        .get("/sampleVol/bucketOne/key_one"));
+    assertNotNull(reconOMMetadataManager.getKeyTable(getBucketLayout())
+        .get("/sampleVol/bucketOne/key_two"));
+
+    // Verifying if context error GET_OM_DB_SNAPSHOT_FAILED is removed
+    
assertFalse(reconContext.getErrors().contains(ReconContext.ErrorCode.GET_OM_DB_SNAPSHOT_FAILED));
+    omMetadataManager.stop();
+    reconOMMetadataManager.stop();
+    reconTaskController.stop();
   }
 
   @Test
@@ -180,9 +176,6 @@ public void testUpdateReconOmDBWithNewSnapshotFailure(
             dirReconMetadata);
 
     ReconUtils reconUtilsMock = getMockReconUtils();
-
-    when(reconUtilsMock.makeHttpCall(any(), anyString(), anyBoolean()))
-        .thenThrow(new IOException("Mocked IOException"));
     when(reconUtilsMock.getReconNodeDetails(
         any(OzoneConfiguration.class))).thenReturn(
         ReconTestUtils.getReconNodeDetails());
@@ -192,12 +185,11 @@ public void testUpdateReconOmDBWithNewSnapshotFailure(
         new OzoneManagerServiceProviderImpl(configuration,
             reconOMMetadataManager, reconTaskController, reconUtilsMock, 
ozoneManagerProtocol,
             reconContext, getMockTaskStatusUpdaterManager());
+    // Stub provider that fails the download; the active DB must be kept.
+    ozoneManagerServiceProvider.setReconSnapshotProvider(
+        newStubSnapshotProvider(omSnapshotDbParentDir, null, true));
 
-    Exception exception = assertThrows(RuntimeException.class, () -> {
-      ozoneManagerServiceProvider.updateReconOmDBWithNewSnapshot();
-    });
-
-    assertTrue(exception.getCause() instanceof IOException);
+    assertFalse(ozoneManagerServiceProvider.updateReconOmDBWithNewSnapshot());
 
     // Verifying if context error GET_OM_DB_SNAPSHOT_FAILED is added
     
assertTrue(reconContext.getErrors().contains(ReconContext.ErrorCode.GET_OM_DB_SNAPSHOT_FAILED));
@@ -219,34 +211,29 @@ public void testUpdateReconOmDBWithNewSnapshotSuccess(
     writeDataToOm(omMetadataManager, "key_two");
 
     DBCheckpoint checkpoint = omMetadataManager.getStore().getCheckpoint(true);
-    File tarFile = createTarFile(checkpoint.getCheckpointLocation());
-    try (InputStream inputStream = Files.newInputStream(tarFile.toPath())) {
-      ReconUtils reconUtilsMock = getMockReconUtils();
-      HttpURLConnection httpURLConnectionMock = mock(HttpURLConnection.class);
-      when(httpURLConnectionMock.getInputStream()).thenReturn(inputStream);
-      when(reconUtilsMock.makeHttpCall(any(), anyString(), anyBoolean()))
-          .thenReturn(httpURLConnectionMock);
-      when(reconUtilsMock.getReconNodeDetails(any(OzoneConfiguration.class)))
-          .thenReturn(ReconTestUtils.getReconNodeDetails());
-      ReconTaskController reconTaskController = getMockTaskController();
-
-      
reconContext.updateErrors(ReconContext.ErrorCode.GET_OM_DB_SNAPSHOT_FAILED);
-
-      OzoneManagerServiceProviderImpl ozoneManagerServiceProvider =
-          new OzoneManagerServiceProviderImpl(configuration,
-              reconOMMetadataManager, reconTaskController, reconUtilsMock, 
ozoneManagerProtocol,
-              reconContext, getMockTaskStatusUpdaterManager());
-
-      
assertTrue(reconContext.getErrors().contains(ReconContext.ErrorCode.GET_OM_DB_SNAPSHOT_FAILED));
-      ozoneManagerServiceProvider.getTarExtractor().start();
-      assertTrue(ozoneManagerServiceProvider.updateReconOmDBWithNewSnapshot());
-      
assertFalse(reconContext.getErrors().contains(ReconContext.ErrorCode.GET_OM_DB_SNAPSHOT_FAILED));
-
-      assertNotNull(reconOMMetadataManager.getKeyTable(getBucketLayout())
-          .get("/sampleVol/bucketOne/key_one"));
-      assertNotNull(reconOMMetadataManager.getKeyTable(getBucketLayout())
-          .get("/sampleVol/bucketOne/key_two"));
-    }
+    ReconUtils reconUtilsMock = getMockReconUtils();
+    when(reconUtilsMock.getReconNodeDetails(any(OzoneConfiguration.class)))
+        .thenReturn(ReconTestUtils.getReconNodeDetails());
+    ReconTaskController reconTaskController = getMockTaskController();
+
+    
reconContext.updateErrors(ReconContext.ErrorCode.GET_OM_DB_SNAPSHOT_FAILED);
+
+    OzoneManagerServiceProviderImpl ozoneManagerServiceProvider =
+        new OzoneManagerServiceProviderImpl(configuration,
+            reconOMMetadataManager, reconTaskController, reconUtilsMock, 
ozoneManagerProtocol,
+            reconContext, getMockTaskStatusUpdaterManager());
+    ozoneManagerServiceProvider.setReconSnapshotProvider(
+        newStubSnapshotProvider(omSnapshotDbParentDir,
+            checkpoint.getCheckpointLocation(), false));
+
+    
assertTrue(reconContext.getErrors().contains(ReconContext.ErrorCode.GET_OM_DB_SNAPSHOT_FAILED));
+    assertTrue(ozoneManagerServiceProvider.updateReconOmDBWithNewSnapshot());
+    
assertFalse(reconContext.getErrors().contains(ReconContext.ErrorCode.GET_OM_DB_SNAPSHOT_FAILED));
+
+    assertNotNull(reconOMMetadataManager.getKeyTable(getBucketLayout())
+        .get("/sampleVol/bucketOne/key_one"));
+    assertNotNull(reconOMMetadataManager.getKeyTable(getBucketLayout())
+        .get("/sampleVol/bucketOne/key_two"));
     omMetadataManager.stop();
     reconOMMetadataManager.stop();
   }
@@ -265,39 +252,30 @@ public void testReconOmDBCloseAndOpenNewSnapshotDb(
 
     DBCheckpoint checkpoint = omMetadataManager.getStore()
         .getCheckpoint(true);
-    File tarFile1 = createTarFile(checkpoint.getCheckpointLocation());
-    File tarFile2 = createTarFile(checkpoint.getCheckpointLocation());
     ReconUtils reconUtilsMock = getMockReconUtils();
+    when(reconUtilsMock.getReconNodeDetails(
+        any(OzoneConfiguration.class))).thenReturn(
+        ReconTestUtils.getReconNodeDetails());
     ReconTaskController reconTaskController = getMockTaskController();
-    try (InputStream inputStream1 = Files.newInputStream(tarFile1.toPath())) {
-      HttpURLConnection httpURLConnectionMock1 = mock(HttpURLConnection.class);
-      when(httpURLConnectionMock1.getInputStream()).thenReturn(inputStream1);
-      when(reconUtilsMock.makeHttpCall(any(), anyString(), anyBoolean()))
-          .thenReturn(httpURLConnectionMock1);
-      when(reconUtilsMock.getReconNodeDetails(
-          any(OzoneConfiguration.class))).thenReturn(
-          ReconTestUtils.getReconNodeDetails());
-
-      OzoneManagerServiceProviderImpl ozoneManagerServiceProvider1 =
-          new OzoneManagerServiceProviderImpl(configuration,
-              reconOMMetadataManager, reconTaskController, reconUtilsMock, 
ozoneManagerProtocol,
-              reconContext, getMockTaskStatusUpdaterManager());
-      ozoneManagerServiceProvider1.getTarExtractor().start();
-      
assertTrue(ozoneManagerServiceProvider1.updateReconOmDBWithNewSnapshot());
-    }
 
-    try (InputStream inputStream2 = Files.newInputStream(tarFile2.toPath())) {
-      HttpURLConnection httpURLConnectionMock2 = mock(HttpURLConnection.class);
-      when(httpURLConnectionMock2.getInputStream()).thenReturn(inputStream2);
-      when(reconUtilsMock.makeHttpCall(any(), anyString(), anyBoolean()))
-          .thenReturn(httpURLConnectionMock2);
-      OzoneManagerServiceProviderImpl ozoneManagerServiceProvider2 =
-          new OzoneManagerServiceProviderImpl(configuration,
-              reconOMMetadataManager, reconTaskController, reconUtilsMock, 
ozoneManagerProtocol,
-              reconContext, getMockTaskStatusUpdaterManager());
-      ozoneManagerServiceProvider2.getTarExtractor().start();
-      
assertTrue(ozoneManagerServiceProvider2.updateReconOmDBWithNewSnapshot());
-    }
+    OzoneManagerServiceProviderImpl ozoneManagerServiceProvider1 =
+        new OzoneManagerServiceProviderImpl(configuration,
+            reconOMMetadataManager, reconTaskController, reconUtilsMock, 
ozoneManagerProtocol,
+            reconContext, getMockTaskStatusUpdaterManager());
+    ozoneManagerServiceProvider1.setReconSnapshotProvider(
+        newStubSnapshotProvider(omSnapshotDbParentDir,
+            checkpoint.getCheckpointLocation(), false));
+    assertTrue(ozoneManagerServiceProvider1.updateReconOmDBWithNewSnapshot());
+
+    OzoneManagerServiceProviderImpl ozoneManagerServiceProvider2 =
+        new OzoneManagerServiceProviderImpl(configuration,
+            reconOMMetadataManager, reconTaskController, reconUtilsMock, 
ozoneManagerProtocol,
+            reconContext, getMockTaskStatusUpdaterManager());
+    ozoneManagerServiceProvider2.setReconSnapshotProvider(
+        newStubSnapshotProvider(omSnapshotDbParentDir,
+            checkpoint.getCheckpointLocation(), false));
+    assertTrue(ozoneManagerServiceProvider2.updateReconOmDBWithNewSnapshot());
+
     omMetadataManager.stop();
     reconOMMetadataManager.stop();
     reconTaskController.stop();
@@ -319,35 +297,29 @@ public void testGetOzoneManagerDBSnapshot(@TempDir File 
dirReconMetadata)
         .toFile();
     FileUtils.write(file2, "File2 Contents", UTF_8);
 
-    //Create test tar file.
-    File tarFile = createTarFile(checkpointDir.toPath());
-    try (InputStream fileInputStream = Files.newInputStream(tarFile.toPath())) 
{
-      ReconUtils reconUtilsMock = getMockReconUtils();
-      HttpURLConnection httpURLConnectionMock = mock(HttpURLConnection.class);
-      when(httpURLConnectionMock.getInputStream()).thenReturn(fileInputStream);
-      when(reconUtilsMock.makeHttpCall(any(), anyString(), anyBoolean()))
-          .thenReturn(httpURLConnectionMock);
-      when(reconUtilsMock.getReconNodeDetails(
-          any(OzoneConfiguration.class))).thenReturn(
-          ReconTestUtils.getReconNodeDetails());
-      ReconOMMetadataManager reconOMMetadataManager =
-          mock(ReconOMMetadataManager.class);
-      ReconTaskController reconTaskController = getMockTaskController();
-      OzoneManagerServiceProviderImpl ozoneManagerServiceProvider =
-          new OzoneManagerServiceProviderImpl(configuration,
-              reconOMMetadataManager, reconTaskController, reconUtilsMock, 
ozoneManagerProtocol,
-              reconContext, getMockTaskStatusUpdaterManager());
-
-      ozoneManagerServiceProvider.getTarExtractor().start();
-      DBCheckpoint checkpoint = ozoneManagerServiceProvider
-          .getOzoneManagerDBSnapshot();
-      assertNotNull(checkpoint);
-      assertTrue(checkpoint.getCheckpointLocation().toFile().isDirectory());
-
-      File[] files = checkpoint.getCheckpointLocation().toFile().listFiles();
-      assertNotNull(files);
-      assertEquals(2, files.length);
-    }
+    ReconUtils reconUtilsMock = getMockReconUtils();
+    when(reconUtilsMock.getReconNodeDetails(
+        any(OzoneConfiguration.class))).thenReturn(
+        ReconTestUtils.getReconNodeDetails());
+    ReconOMMetadataManager reconOMMetadataManager =
+        mock(ReconOMMetadataManager.class);
+    ReconTaskController reconTaskController = getMockTaskController();
+    OzoneManagerServiceProviderImpl ozoneManagerServiceProvider =
+        new OzoneManagerServiceProviderImpl(configuration,
+            reconOMMetadataManager, reconTaskController, reconUtilsMock, 
ozoneManagerProtocol,
+            reconContext, getMockTaskStatusUpdaterManager());
+    ozoneManagerServiceProvider.setReconSnapshotProvider(
+        newStubSnapshotProvider(omSnapshotDbParentDir, checkpointDir.toPath(),
+            false));
+
+    DBCheckpoint checkpoint = ozoneManagerServiceProvider
+        .getOzoneManagerDBSnapshot();
+    assertNotNull(checkpoint);
+    assertTrue(checkpoint.getCheckpointLocation().toFile().isDirectory());
+
+    File[] files = checkpoint.getCheckpointLocation().toFile().listFiles();
+    assertNotNull(files);
+    assertEquals(2, files.length);
   }
 
   static RocksDatabase getRocksDatabase(OMMetadataManager om) {
@@ -636,9 +608,44 @@ private OzoneManagerProtocol getMockOzoneManagerClient(
         mock(OzoneManagerProtocol.class);
     when(ozoneManagerProtocolMock.getDBUpdates(any(OzoneManagerProtocolProtos
         .DBUpdatesRequest.class))).thenReturn(dbUpdatesWrapper);
+    // Provide an OM leader so getLeaderNodeId() can resolve the download 
source.
+    ServiceInfo leaderInfo = mock(ServiceInfo.class);
+    when(leaderInfo.getNodeType()).thenReturn(HddsProtos.NodeType.OM);
+    OzoneManagerProtocolProtos.OMRoleInfo roleInfo =
+        OzoneManagerProtocolProtos.OMRoleInfo.newBuilder()
+            .setNodeId("om1").setServerRole("LEADER").build();
+    when(leaderInfo.getOmRoleInfo()).thenReturn(roleInfo);
+    when(ozoneManagerProtocolMock.getServiceList())
+        .thenReturn(Collections.singletonList(leaderInfo));
     return ozoneManagerProtocolMock;
   }
 
+  /**
+   * A stub {@link ReconRDBSnapshotProvider} that bypasses HTTP/tar and simply
+   * promotes a copy of {@code sourceCheckpoint} into a fresh timestamped
+   * snapshot dir (or fails). Used to drive the provider's install path
+   * deterministically.
+   */
+  private ReconRDBSnapshotProvider newStubSnapshotProvider(
+      File snapshotParent, java.nio.file.Path sourceCheckpoint,
+      boolean fail) {
+    return new ReconRDBSnapshotProvider(snapshotParent, null, false,
+        org.apache.hadoop.hdds.server.http.HttpConfig.Policy.HTTP_ONLY, false,
+        true, () -> null) {
+      @Override
+      public DBCheckpoint downloadDBSnapshotFromLeader(String leaderNodeID)
+          throws IOException {
+        if (fail) {
+          throw new IOException("Mocked download failure");
+        }
+        java.nio.file.Path dest = snapshotParent.toPath().resolve(
+            RECON_OM_SNAPSHOT_DB + "_" + System.nanoTime());
+        FileUtils.copyDirectory(sourceCheckpoint.toFile(), dest.toFile());
+        return new org.apache.hadoop.hdds.utils.db.RocksDBCheckpoint(dest);
+      }
+    };
+  }
+
   // Mock the case of SNNFE
   private OzoneManagerProtocol getMockOzoneManagerClientWithThrow()
       throws IOException {
diff --git 
a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/spi/impl/TestReconRDBSnapshotProvider.java
 
b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/spi/impl/TestReconRDBSnapshotProvider.java
new file mode 100644
index 00000000000..9c93adc4a33
--- /dev/null
+++ 
b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/spi/impl/TestReconRDBSnapshotProvider.java
@@ -0,0 +1,223 @@
+/*
+ * 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.recon.spi.impl;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.hadoop.ozone.OzoneConsts.HARDLINK_SEPARATOR;
+import static 
org.apache.hadoop.ozone.OzoneConsts.OZONE_DB_CHECKPOINT_HTTP_ENDPOINT;
+import static 
org.apache.hadoop.ozone.OzoneConsts.OZONE_DB_CHECKPOINT_HTTP_ENDPOINT_V2;
+import static 
org.apache.hadoop.ozone.recon.ReconConstants.RECON_OM_SNAPSHOT_DB;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Supplier;
+import org.apache.commons.compress.archivers.ArchiveOutputStream;
+import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
+import org.apache.commons.io.FileUtils;
+import org.apache.hadoop.hdds.server.http.HttpConfig;
+import org.apache.hadoop.hdds.utils.Archiver;
+import org.apache.hadoop.hdds.utils.HddsServerUtil;
+import org.apache.hadoop.hdds.utils.db.DBCheckpoint;
+import org.apache.hadoop.hdds.utils.db.InodeMetadataRocksDBCheckpoint;
+import org.apache.hadoop.hdds.utils.db.RocksDBCheckpoint;
+import org.apache.hadoop.ozone.om.helpers.ServiceInfo;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+/**
+ * Tests for {@link ReconRDBSnapshotProvider}: normalization and promotion of
+ * the assembled checkpoint after the transfer completes.
+ */
+public class TestReconRDBSnapshotProvider {
+
+  private static final Supplier<ServiceInfo> NO_LEADER = () -> null;
+
+  private ReconRDBSnapshotProvider newProvider(File snapshotDir) {
+    return newProvider(snapshotDir, true);
+  }
+
+  private ReconRDBSnapshotProvider newProvider(File snapshotDir,
+      boolean useV2CheckpointApi) {
+    return new ReconRDBSnapshotProvider(snapshotDir, null, false,
+        HttpConfig.Policy.HTTP_ONLY, false, useV2CheckpointApi, NO_LEADER);
+  }
+
+  private void writeFile(File dir, String name, String content)
+      throws IOException {
+    FileUtils.write(new File(dir, name), content, UTF_8);
+  }
+
+  @Test
+  public void testGetCheckpointPromotesDbAndClearsCandidate(
+      @TempDir File snapshotDir) throws IOException {
+    ReconRDBSnapshotProvider provider = newProvider(snapshotDir);
+    File candidate = provider.getCandidateDir();
+
+    // Simulate a fully untarred v2 checkpoint: flat DB files at the root plus
+    // the completion sentinel (no hardLinkFile - it should be tolerated).
+    writeFile(candidate, "000010.sst", "data-a");
+    writeFile(candidate, "CURRENT", "current");
+    writeFile(candidate, 
HddsServerUtil.OZONE_RATIS_SNAPSHOT_COMPLETE_FLAG_NAME,
+        "");
+
+    DBCheckpoint checkpoint =
+        provider.getCheckpointFromUntarredDb(candidate.toPath());
+
+    File promoted = checkpoint.getCheckpointLocation().toFile();
+    assertTrue(promoted.getName().startsWith(RECON_OM_SNAPSHOT_DB + "_"),
+        "Promoted DB should be a timestamped snapshot dir");
+    assertEquals(snapshotDir, promoted.getParentFile());
+    assertTrue(new File(promoted, "000010.sst").exists());
+    assertTrue(new File(promoted, "CURRENT").exists());
+    // The completion sentinel must not leak into the DB.
+    assertFalse(new File(promoted,
+        HddsServerUtil.OZONE_RATIS_SNAPSHOT_COMPLETE_FLAG_NAME).exists());
+    // Candidate dir must be emptied so the next sync starts clean.
+    assertEquals(0, candidate.list().length);
+  }
+
+  @Test
+  public void testGetCheckpointInstallsHardLinks(@TempDir File snapshotDir)
+      throws IOException {
+    ReconRDBSnapshotProvider provider = newProvider(snapshotDir);
+    File candidate = provider.getCandidateDir();
+
+    writeFile(candidate, "000001.sst", "shared-content");
+    // hardLinkFile: create 000002.sst as a hard link to 000001.sst.
+    writeFile(candidate, InodeMetadataRocksDBCheckpoint.OM_HARDLINK_FILE,
+        "000002.sst" + HARDLINK_SEPARATOR + "000001.sst" + "\n");
+
+    DBCheckpoint checkpoint =
+        provider.getCheckpointFromUntarredDb(candidate.toPath());
+
+    File promoted = checkpoint.getCheckpointLocation().toFile();
+    File linked = new File(promoted, "000002.sst");
+    assertTrue(linked.exists(), "Hard-linked SST should be materialized");
+    assertEquals("shared-content",
+        FileUtils.readFileToString(linked, UTF_8));
+    assertFalse(new File(promoted,
+        InodeMetadataRocksDBCheckpoint.OM_HARDLINK_FILE).exists(),
+        "hardLinkFile must be consumed, not left in the DB");
+  }
+
+  @Test
+  public void testCandidateDirLocation(@TempDir File snapshotDir) {
+    ReconRDBSnapshotProvider provider = newProvider(snapshotDir);
+    assertEquals(RECON_OM_SNAPSHOT_DB + ".candidate",
+        provider.getCandidateDir().getName());
+    assertEquals(snapshotDir, provider.getCandidateDir().getParentFile());
+  }
+
+  @Test
+  public void testLeaderPinnedForEntireTransfer(@TempDir File snapshotDir)
+      throws IOException {
+    ServiceInfo leaderA = mock(ServiceInfo.class);
+    when(leaderA.getHostname()).thenReturn("om-leader-a");
+    ServiceInfo leaderB = mock(ServiceInfo.class);
+    when(leaderB.getHostname()).thenReturn("om-leader-b");
+
+    // Supplier resolves leaderA first, then leaderB, mimicking an OM failover
+    // during the transfer.
+    AtomicInteger supplierCalls = new AtomicInteger();
+    Supplier<ServiceInfo> supplier = () ->
+        supplierCalls.getAndIncrement() == 0 ? leaderA : leaderB;
+
+    List<String> leadersUsedPerPart = new ArrayList<>();
+
+    ReconRDBSnapshotProvider provider =
+        new ReconRDBSnapshotProvider(snapshotDir, null, false,
+            HttpConfig.Policy.HTTP_ONLY, false, true, supplier) {
+          @Override
+          public void downloadSnapshot(String leaderNodeID, File targetFile)
+              throws IOException {
+            // Mirror the production leader resolution: use the pinned leader,
+            // falling back to the supplier only when nothing is pinned.
+            ServiceInfo leader = getPinnedLeader();
+            if (leader == null) {
+              leader = supplier.get();
+            }
+            leadersUsedPerPart.add(leader.getHostname());
+            // First part is partial; the second carries the completion flag.
+            writeTar(targetFile, leadersUsedPerPart.size() > 1);
+          }
+
+          @Override
+          public DBCheckpoint getCheckpointFromUntarredDb(Path untarredDbDir) {
+            return new RocksDBCheckpoint(untarredDbDir);
+          }
+        };
+
+    provider.downloadDBSnapshotFromLeader("om-leader-a-node-id");
+
+    assertEquals(2, leadersUsedPerPart.size(),
+        "Test should exercise a two-part transfer");
+    assertTrue(leadersUsedPerPart.stream().allMatch("om-leader-a"::equals),
+        "All parts must come from the leader pinned at the start of the "
+            + "transfer, even if the resolved leader changes mid-transfer; "
+            + "leadersUsedPerPart=" + leadersUsedPerPart);
+    assertEquals(1, supplierCalls.get(),
+        "Leader should be resolved exactly once per transfer");
+    assertNull(provider.getPinnedLeader(),
+        "Pinned leader must be cleared after the transfer completes");
+  }
+
+  private void writeTar(File targetFile, boolean complete) throws IOException {
+    try (ArchiveOutputStream<TarArchiveEntry> out =
+             Archiver.tar(Files.newOutputStream(targetFile.toPath()))) {
+      if (complete) {
+        HddsServerUtil.includeRatisSnapshotCompleteFlag(out);
+      } else {
+        File tmp = File.createTempFile("recon-part", ".sst");
+        FileUtils.write(tmp, "partial", UTF_8);
+        Archiver.includeFile(tmp, "000100.sst", out);
+      }
+    }
+  }
+
+  @Test
+  public void testBuildCheckpointUrlHonorsInodeBasedConfig(
+      @TempDir File snapshotDir) throws IOException {
+    ServiceInfo leader = mock(ServiceInfo.class);
+    when(leader.getHostname()).thenReturn("om-host");
+    when(leader.getPort(any())).thenReturn(9874);
+
+    // Inode-based transfer on (default) -> v2 endpoint.
+    URL v2Url = newProvider(snapshotDir, true).buildCheckpointUrl(leader);
+    assertEquals(OZONE_DB_CHECKPOINT_HTTP_ENDPOINT_V2, v2Url.getPath());
+    assertTrue(v2Url.getQuery().contains("includeSnapshotData=false"));
+    assertTrue(v2Url.getQuery().contains("flushBeforeCheckpoint=false"));
+    // Disabled -> fall back to the v1 endpoint.
+    URL v1Url = newProvider(snapshotDir, false).buildCheckpointUrl(leader);
+    assertEquals(OZONE_DB_CHECKPOINT_HTTP_ENDPOINT, v1Url.getPath());
+    assertTrue(v1Url.getQuery().contains("includeSnapshotData=false"));
+    assertTrue(v1Url.getQuery().contains("flushBeforeCheckpoint=false"));
+  }
+}


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

Reply via email to