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 deb56f03629 HDDS-16939. Recon drops replica history for departed
datanodes on OM full reprocess (#11494)
deb56f03629 is described below
commit deb56f036298ffbc7ae26cf2fec0908254eeab6e
Author: Devesh Kumar Singh <[email protected]>
AuthorDate: Wed Oct 14 12:10:08 2026 +0530
HDDS-16939. Recon drops replica history for departed datanodes on OM full
reprocess (#11494)
---
hadoop-hdds/docs/content/concept/Recon.md | 14 +-
.../hadoop/ozone/debug/DBDefinitionFactory.java | 3 +-
.../ozone/debug/TestDBDefinitionFactory.java | 26 ++
hadoop-ozone/integration-test-recon/pom.xml | 5 +
.../ozone/recon/TestReconWithOzoneManager.java | 125 +++++++++-
.../apache/hadoop/ozone/recon/ReconConstants.java | 2 +
.../hadoop/ozone/recon/ReconControllerModule.java | 2 +
.../org/apache/hadoop/ozone/recon/ReconServer.java | 13 +
.../ozone/recon/scm/ReconContainerManager.java | 26 +-
.../impl/ReconContainerMetadataManagerImpl.java | 32 ++-
.../ozone/recon/spi/impl/ReconDBDefinition.java | 2 +-
.../spi/impl/ReconReplicaHistoryDBDefinition.java | 50 ++++
.../spi/impl/ReconReplicaHistoryDBProvider.java | 98 ++++++++
.../hadoop/ozone/recon/upgrade/ReconVersion.java | 1 +
.../upgrade/ReplicaHistoryDBUpgradeAction.java | 40 ++++
.../hadoop/ozone/recon/ReconTestInjector.java | 2 +
.../ozone/recon/api/TestContainerEndpoint.java | 54 +++++
.../scm/AbstractReconContainerManagerTest.java | 8 +-
.../ozone/recon/scm/TestReconContainerManager.java | 90 +++++++
.../TestReconStorageContainerManagerFacade.java | 8 +
.../impl/TestReconReplicaHistoryDBProvider.java | 262 +++++++++++++++++++++
.../recon/upgrade/TestReconVersionManager.java | 124 ++++++++++
22 files changed, 954 insertions(+), 33 deletions(-)
diff --git a/hadoop-hdds/docs/content/concept/Recon.md
b/hadoop-hdds/docs/content/concept/Recon.md
index 608a37c39b4..e84fb81b591 100644
--- a/hadoop-hdds/docs/content/concept/Recon.md
+++ b/hadoop-hdds/docs/content/concept/Recon.md
@@ -124,6 +124,10 @@ to the Prometheus endpoint like
`ozone.recon.prometheus.http.endpoint=http://pro
* Stores the mapping (container, key) -> count
* ContainerKeyCount table
* Stores containerID -> no. of keys count within the container
+ * Container replica history is stored in `recon-container-replica-history.db`
+ under `ozone.recon.db.dir`. This database is independent of the databases
+ replaced during OM reprocess and SCM snapshot synchronization, so history
for
+ departed datanodes survives those operations.
* The following data is stored in the configured SQL database (default is
Derby):
* GlobalStats table
@@ -134,14 +138,18 @@ to the Prometheus endpoint like
`ozone.recon.prometheus.http.endpoint=http://pro
* ReconTaskStatus table
* Keeps track of the status and last run timestamp of the registered OM
and SCM
db tasks in the [Recon Task Framework](#task-framework)
- * ContainerHistory table
- * Stores ContainerReplica -> Datanode mapping with last known
timestamp. This
- is used to determine the last known datanodes when a container is
reported missing
* UnhealthyContainers table
* Keeps track of all the Unhealthy Containers (MISSING,
UNDER_REPLICATED,
OVER_REPLICATED, MIS_REPLICATED) in the cluster at any given time
+On upgrade, Recon's versioned upgrade action migrates existing replica history
from
+`recon-container-key.db` before finalizing the new version and starting its
services.
+The migration streams the history through a temporary
+SST file and retains the source table. A durable completion marker prevents
migration
+on subsequent starts; an incomplete migration is retried on the next start.
Allow
+disk space for the new history database and the temporary SST file during
migration.
+
## Notable configurations
key | default | <div style="width: 300px;">description</div>
diff --git
a/hadoop-ozone/cli-debug/src/main/java/org/apache/hadoop/ozone/debug/DBDefinitionFactory.java
b/hadoop-ozone/cli-debug/src/main/java/org/apache/hadoop/ozone/debug/DBDefinitionFactory.java
index ad366b02421..d22f18e3b8b 100644
---
a/hadoop-ozone/cli-debug/src/main/java/org/apache/hadoop/ozone/debug/DBDefinitionFactory.java
+++
b/hadoop-ozone/cli-debug/src/main/java/org/apache/hadoop/ozone/debug/DBDefinitionFactory.java
@@ -39,6 +39,7 @@
import org.apache.hadoop.ozone.om.codec.OMDBDefinition;
import org.apache.hadoop.ozone.recon.scm.ReconSCMDBDefinition;
import org.apache.hadoop.ozone.recon.spi.impl.ReconDBDefinition;
+import org.apache.hadoop.ozone.recon.spi.impl.ReconReplicaHistoryDBDefinition;
/**
* Utility class to get appropriate DBDefinition.
@@ -51,7 +52,7 @@ public final class DBDefinitionFactory {
static {
final Map<String, DBDefinition> map = new HashMap<>();
Arrays.asList(SCMDBDefinition.get(), OMDBDefinition.get(),
ReconSCMDBDefinition.get(),
- WitnessedContainerDBDefinition.get())
+ WitnessedContainerDBDefinition.get(), new
ReconReplicaHistoryDBDefinition())
.forEach(dbDefinition -> map.put(dbDefinition.getName(),
dbDefinition));
DB_MAP = Collections.unmodifiableMap(map);
}
diff --git
a/hadoop-ozone/cli-debug/src/test/java/org/apache/hadoop/ozone/debug/TestDBDefinitionFactory.java
b/hadoop-ozone/cli-debug/src/test/java/org/apache/hadoop/ozone/debug/TestDBDefinitionFactory.java
index d12ea50123c..a4537024a23 100644
---
a/hadoop-ozone/cli-debug/src/test/java/org/apache/hadoop/ozone/debug/TestDBDefinitionFactory.java
+++
b/hadoop-ozone/cli-debug/src/test/java/org/apache/hadoop/ozone/debug/TestDBDefinitionFactory.java
@@ -19,19 +19,26 @@
import static
org.apache.hadoop.ozone.recon.ReconConstants.RECON_CONTAINER_KEY_DB;
import static
org.apache.hadoop.ozone.recon.ReconConstants.RECON_OM_SNAPSHOT_DB;
+import static
org.apache.hadoop.ozone.recon.ReconConstants.RECON_REPLICA_HISTORY_DB;
+import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.apache.hadoop.hdds.conf.OzoneConfiguration;
import org.apache.hadoop.hdds.scm.metadata.SCMDBDefinition;
+import org.apache.hadoop.hdds.utils.db.DBColumnFamilyDefinition;
import org.apache.hadoop.hdds.utils.db.DBDefinition;
import
org.apache.hadoop.ozone.container.metadata.DatanodeSchemaOneDBDefinition;
import
org.apache.hadoop.ozone.container.metadata.DatanodeSchemaThreeDBDefinition;
import
org.apache.hadoop.ozone.container.metadata.DatanodeSchemaTwoDBDefinition;
import org.apache.hadoop.ozone.om.codec.OMDBDefinition;
+import org.apache.hadoop.ozone.recon.scm.ContainerReplicaHistoryList;
import org.apache.hadoop.ozone.recon.scm.ReconSCMDBDefinition;
import org.apache.hadoop.ozone.recon.spi.impl.ReconDBDefinition;
+import org.apache.hadoop.ozone.recon.spi.impl.ReconReplicaHistoryDBDefinition;
import org.junit.jupiter.api.Test;
/**
@@ -39,6 +46,25 @@
*/
public class TestDBDefinitionFactory {
+ @Test
+ public void testGetReconReplicaHistoryDefinition() {
+ DBDefinition definition =
DBDefinitionFactory.getDefinition(RECON_REPLICA_HISTORY_DB);
+ assertInstanceOf(ReconReplicaHistoryDBDefinition.class, definition);
+ assertEquals(RECON_REPLICA_HISTORY_DB, definition.getName());
+ assertSame(definition, DBDefinitionFactory.getDefinition(
+ Paths.get("/tmp", RECON_REPLICA_HISTORY_DB), new
OzoneConfiguration()));
+
+ DBColumnFamilyDefinition<?, ?> history =
definition.getColumnFamily("replica_history_v2");
+ assertNotNull(history);
+ assertEquals(Long.class, history.getKeyType());
+ assertEquals(ContainerReplicaHistoryList.class, history.getValueType());
+
+ DBColumnFamilyDefinition<?, ?> metadata =
definition.getColumnFamily("metadata");
+ assertNotNull(metadata);
+ assertEquals(String.class, metadata.getKeyType());
+ assertEquals(String.class, metadata.getValueType());
+ }
+
@Test
public void testGetDefinition() {
DBDefinition definition =
DBDefinitionFactory.getDefinition(OMDBDefinition.get().getName());
diff --git a/hadoop-ozone/integration-test-recon/pom.xml
b/hadoop-ozone/integration-test-recon/pom.xml
index ca2fae43bf0..33e1b93493d 100644
--- a/hadoop-ozone/integration-test-recon/pom.xml
+++ b/hadoop-ozone/integration-test-recon/pom.xml
@@ -41,6 +41,11 @@
<artifactId>guava</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>com.google.inject</groupId>
+ <artifactId>guice</artifactId>
+ <scope>test</scope>
+ </dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
diff --git
a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconWithOzoneManager.java
b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconWithOzoneManager.java
index 8896f74c415..44e8c8f2296 100644
---
a/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconWithOzoneManager.java
+++
b/hadoop-ozone/integration-test-recon/src/test/java/org/apache/hadoop/ozone/recon/TestReconWithOzoneManager.java
@@ -19,7 +19,6 @@
import static java.net.HttpURLConnection.HTTP_CREATED;
import static java.net.HttpURLConnection.HTTP_OK;
-import static
org.apache.hadoop.hdds.recon.ReconConfigKeys.OZONE_RECON_HTTP_ADDRESS_KEY;
import static
org.apache.hadoop.ozone.recon.ReconServerConfigKeys.OZONE_RECON_OM_CONNECTION_REQUEST_TIMEOUT;
import static
org.apache.hadoop.ozone.recon.ReconServerConfigKeys.OZONE_RECON_OM_CONNECTION_REQUEST_TIMEOUT_DEFAULT;
import static
org.apache.hadoop.ozone.recon.ReconServerConfigKeys.OZONE_RECON_OM_CONNECTION_TIMEOUT;
@@ -29,14 +28,26 @@
import static
org.apache.hadoop.ozone.recon.ReconServerConfigKeys.RECON_OM_DELTA_UPDATE_LIMIT;
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.apache.hadoop.ozone.recon.spi.impl.ReconDBDefinition.REPLICA_HISTORY_V2;
+import static
org.apache.hadoop.ozone.recon.spi.impl.ReconReplicaHistoryDBDefinition.METADATA;
+import static
org.apache.ozone.recon.codegen.JooqCodeGenerator.RECON_SCHEMA_NAME;
+import static
org.apache.ozone.recon.schema.SchemaVersionTableDefinition.SCHEMA_VERSION_TABLE_NAME;
import static org.assertj.core.api.Assertions.assertThat;
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.junit.jupiter.api.Assertions.assertTrue;
import static org.slf4j.event.Level.INFO;
import java.io.IOException;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -46,9 +57,13 @@
import org.apache.hadoop.hdds.client.BlockID;
import org.apache.hadoop.hdds.client.StandaloneReplicationConfig;
import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.hdds.protocol.DatanodeID;
import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
import org.apache.hadoop.hdds.scm.HddsTestUtils;
+import org.apache.hadoop.hdds.scm.container.ContainerChecksums;
import org.apache.hadoop.hdds.scm.pipeline.Pipeline;
+import org.apache.hadoop.hdds.utils.db.DBStore;
+import org.apache.hadoop.hdds.utils.db.DBStoreBuilder;
import org.apache.hadoop.hdds.utils.db.RDBStore;
import org.apache.hadoop.ozone.MiniOzoneCluster;
import org.apache.hadoop.ozone.om.OMMetadataManager;
@@ -59,7 +74,13 @@
import org.apache.hadoop.ozone.om.helpers.OmKeyLocationInfoGroup;
import org.apache.hadoop.ozone.om.helpers.OmVolumeArgs;
import org.apache.hadoop.ozone.recon.metrics.OzoneManagerSyncMetrics;
+import org.apache.hadoop.ozone.recon.persistence.DataSourceConfiguration;
+import org.apache.hadoop.ozone.recon.scm.ContainerReplicaHistory;
+import org.apache.hadoop.ozone.recon.scm.ContainerReplicaHistoryList;
import org.apache.hadoop.ozone.recon.spi.impl.OzoneManagerServiceProviderImpl;
+import org.apache.hadoop.ozone.recon.spi.impl.ReconDBDefinition;
+import org.apache.hadoop.ozone.recon.spi.impl.ReconReplicaHistoryDBDefinition;
+import org.apache.hadoop.ozone.recon.upgrade.ReconVersion;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.config.RequestConfig;
@@ -72,6 +93,7 @@
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
+import org.slf4j.LoggerFactory;
/**
* Test Ozone Recon.
@@ -82,6 +104,7 @@ public class TestReconWithOzoneManager {
private static MiniOzoneCluster cluster = null;
private static OMMetadataManager metadataManager;
private static CloseableHttpClient httpClient;
+ private static String reconHttpURL;
private static String taskStatusURL;
private static ReconService recon;
@@ -124,8 +147,9 @@ public static void init() throws Exception {
cluster.getStorageContainerManager().exitSafeMode();
- String reconHTTPAddress = conf.get(OZONE_RECON_HTTP_ADDRESS_KEY);
- taskStatusURL = "http://" + reconHTTPAddress + "/api/v1/task/status";
+ // Keep requests on loopback when the HTTP server advertises the machine
hostname.
+ reconHttpURL = "http://127.0.0.1:" +
recon.getReconServer().getHttpServer().getHttpAddress().getPort();
+ taskStatusURL = reconHttpURL + "/api/v1/task/status";
// initialize HTTPClient
httpClient = HttpClientBuilder
@@ -433,8 +457,7 @@ public void testManualOMDBRebuild() throws Exception {
assertThat(reconLoadedSeqNumber).isGreaterThanOrEqualTo(omLatestSeqNumber);
// 6. POST reinit
- String triggerUrl = "http://" +
cluster.getConf().get(OZONE_RECON_HTTP_ADDRESS_KEY) +
- "/api/v1/triggerdbsync/om/reinit";
+ String triggerUrl = reconHttpURL + "/api/v1/triggerdbsync/om/reinit";
org.apache.http.client.methods.HttpPost httpPost = new
org.apache.http.client.methods.HttpPost(triggerUrl);
HttpResponse response = httpClient.execute(httpPost);
assertEquals(202, response.getStatusLine().getStatusCode());
@@ -481,6 +504,98 @@ public void testManualOMDBRebuild() throws Exception {
}, 1000, 30000);
}
+ @Test
+ public void testReplicaHistoryMigrationOnUpgradeAndRestart() throws
Exception {
+ OzoneConfiguration conf = cluster.getConf();
+ String jdbcUrl = ReconGuiceServletContextListener.getGlobalInjector()
+ .getInstance(DataSourceConfiguration.class).getJdbcUrl();
+ long containerID = 987654321L;
+ Map<DatanodeID, ContainerReplicaHistory> expected = new HashMap<>();
+ for (int i = 0; i < 3; i++) {
+ DatanodeID id = DatanodeID.randomID();
+ expected.put(id, new ContainerReplicaHistory(id, 100L + i, 200L + i,
300L + i,
+ "CLOSED", ContainerChecksums.of(1234L + i, 0L)));
+ }
+
+ try (LogCapturer logs =
LogCapturer.captureLogs(LoggerFactory.getLogger("org.apache.hadoop.ozone.recon")))
{
+ recon.stop();
+ // Seed unmigrated history while all Recon writers are stopped,
retaining other test data.
+ String migrationMessage;
+ try (DBStore legacy = DBStoreBuilder.createDBStore(conf,
+ new ReconDBDefinition(ReconConstants.RECON_CONTAINER_KEY_DB));
+ DBStore history = DBStoreBuilder.createDBStore(conf, new
ReconReplicaHistoryDBDefinition())) {
+ migrationMessage = "Migrated container replica history to " +
history.getDbLocation();
+ REPLICA_HISTORY_V2.getTable(legacy).put(containerID,
+ new ContainerReplicaHistoryList(new
ArrayList<>(expected.values())));
+ assertNull(REPLICA_HISTORY_V2.getTable(history).get(containerID));
+ METADATA.getTable(history).delete("replicaHistoryMigrated");
+ legacy.flushLog(true);
+ history.flushLog(true);
+ }
+ try (Connection connection = DriverManager.getConnection(jdbcUrl,
RECON_SCHEMA_NAME, "");
+ PreparedStatement statement = connection.prepareStatement(
+ "UPDATE " + SCHEMA_VERSION_TABLE_NAME + " SET
\"version_number\" = ?")) {
+ statement.setInt(1,
ReconVersion.UNHEALTHY_CONTAINERS_STATE_CONTAINER_ID_INDEX.serialize());
+ assertEquals(1, statement.executeUpdate());
+ }
+
assertEquals(ReconVersion.UNHEALTHY_CONTAINERS_STATE_CONTAINER_ID_INDEX.serialize(),
+ getReconSchemaVersion(jdbcUrl));
+
+ logs.clearOutput();
+ recon.start(conf);
+ assertEquals(ReconVersion.SOFTWARE_VERSION.serialize(),
getReconSchemaVersion(jdbcUrl));
+ assertReplicaHistory(containerID, expected);
+ String startupLogs = logs.getOutput();
+ int migration = startupLogs.indexOf(migrationMessage);
+ int versionPersisted = startupLogs.indexOf(
+ "Updated apparent version to '" +
ReconVersion.REPLICA_HISTORY_DB.serialize() + "'.");
+ assertThat(migration).isGreaterThanOrEqualTo(0);
+ assertThat(versionPersisted).isGreaterThan(migration);
+
+ recon.stop();
+ // A second copy would overwrite the migrated record, making repeated
migration observable.
+ try (DBStore legacy = DBStoreBuilder.createDBStore(conf,
+ new ReconDBDefinition(ReconConstants.RECON_CONTAINER_KEY_DB));
+ DBStore history = DBStoreBuilder.createDBStore(conf, new
ReconReplicaHistoryDBDefinition())) {
+ REPLICA_HISTORY_V2.getTable(legacy).put(containerID, new
ContainerReplicaHistoryList(Collections.emptyList()));
+ assertEquals("true",
METADATA.getTable(history).get("replicaHistoryMigrated"));
+ }
+ logs.clearOutput();
+ recon.start(conf);
+ assertEquals(ReconVersion.SOFTWARE_VERSION.serialize(),
getReconSchemaVersion(jdbcUrl));
+ assertReplicaHistory(containerID, expected);
+ assertThat(logs.getOutput()).doesNotContain(migrationMessage);
+ } finally {
+ if (recon.getReconServer() == null) {
+ recon.start(conf);
+ }
+ }
+ }
+
+ private int getReconSchemaVersion(String jdbcUrl) throws SQLException {
+ try (Connection connection = DriverManager.getConnection(jdbcUrl,
RECON_SCHEMA_NAME, "");
+ PreparedStatement statement = connection.prepareStatement(
+ "SELECT \"version_number\" FROM " + SCHEMA_VERSION_TABLE_NAME);
+ ResultSet result = statement.executeQuery()) {
+ assertTrue(result.next());
+ return result.getInt(1);
+ }
+ }
+
+ private void assertReplicaHistory(long containerID, Map<DatanodeID,
ContainerReplicaHistory> expected)
+ throws IOException {
+ Map<DatanodeID, ContainerReplicaHistory> actual =
+
recon.getReconServer().getReconContainerMetadataManager().getContainerReplicaHistory(containerID);
+ assertEquals(expected.size(), actual.size());
+ expected.forEach((id, history) -> {
+ assertNotNull(actual.get(id));
+ assertEquals(history.toProto(), actual.get(id).toProto());
+ });
+ List<Map<String, Object>> response =
JsonTestUtils.readTreeAsListOfMaps(makeHttpCall(
+ reconHttpURL + "/api/v1/containers/" + containerID +
"/replicaHistory"));
+ assertEquals(expected.size(), response.size());
+ }
+
private static void refreshOmMetadataManager() {
metadataManager = cluster.getOzoneManager().getMetadataManager();
}
diff --git
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconConstants.java
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconConstants.java
index 0320a8c8b2a..c96723c1011 100644
---
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconConstants.java
+++
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconConstants.java
@@ -26,6 +26,8 @@ public final class ReconConstants {
public static final String RECON_CONTAINER_KEY_DB = "recon-container-key.db";
+ public static final String RECON_REPLICA_HISTORY_DB =
"recon-container-replica-history.db";
+
public static final String CONTAINER_COUNT_KEY = "containerCount";
public static final String RECON_OM_SNAPSHOT_DB = "om.snapshot.db";
diff --git
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconControllerModule.java
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconControllerModule.java
index cdfdf416561..f5d9186306d 100644
---
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconControllerModule.java
+++
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconControllerModule.java
@@ -64,6 +64,7 @@
import org.apache.hadoop.ozone.recon.spi.impl.ReconFileMetadataManagerImpl;
import org.apache.hadoop.ozone.recon.spi.impl.ReconGlobalStatsManagerImpl;
import org.apache.hadoop.ozone.recon.spi.impl.ReconNamespaceSummaryManagerImpl;
+import org.apache.hadoop.ozone.recon.spi.impl.ReconReplicaHistoryDBProvider;
import
org.apache.hadoop.ozone.recon.spi.impl.StorageContainerServiceProviderImpl;
import org.apache.hadoop.ozone.recon.tasks.ContainerKeyMapperTaskFSO;
import org.apache.hadoop.ozone.recon.tasks.ContainerKeyMapperTaskOBS;
@@ -108,6 +109,7 @@ protected void configure() {
bind(ReconHttpServer.class).in(Singleton.class);
bind(ReconStorageConfig.class).in(Singleton.class);
bind(ReconDBProvider.class).in(Singleton.class);
+ bind(ReconReplicaHistoryDBProvider.class).in(Singleton.class);
bind(ReconOMMetadataManager.class)
.to(ReconOmMetadataManagerImpl.class);
bind(OMMetadataManager.class).to(ReconOmMetadataManagerImpl.class);
diff --git
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconServer.java
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconServer.java
index 28b0e60609a..deb9791b152 100644
---
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconServer.java
+++
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconServer.java
@@ -58,6 +58,7 @@
import org.apache.hadoop.ozone.recon.spi.ReconNamespaceSummaryManager;
import org.apache.hadoop.ozone.recon.spi.StorageContainerServiceProvider;
import org.apache.hadoop.ozone.recon.spi.impl.ReconDBProvider;
+import org.apache.hadoop.ozone.recon.spi.impl.ReconReplicaHistoryDBProvider;
import org.apache.hadoop.ozone.recon.tasks.ReconTaskController;
import org.apache.hadoop.ozone.recon.upgrade.ReconVersionManager;
import org.apache.hadoop.ozone.upgrade.UpgradeException;
@@ -85,6 +86,7 @@ public class ReconServer extends GenericCli implements
Callable<Void> {
private ReconContainerMetadataManager reconContainerMetadataManager;
private OzoneManagerServiceProvider ozoneManagerServiceProvider;
private ReconDBProvider reconDBProvider;
+ private ReconReplicaHistoryDBProvider replicaHistoryDBProvider;
private ReconNamespaceSummaryManager reconNamespaceSummaryManager;
private ReconStorageContainerManagerFacade reconStorageContainerManager;
private OzoneConfiguration configuration;
@@ -153,6 +155,7 @@ public Void call() throws Exception {
}
jvmPauseMonitor = newJvmPauseMonitor("Recon");
this.reconDBProvider = injector.getInstance(ReconDBProvider.class);
+ this.replicaHistoryDBProvider =
injector.getInstance(ReconReplicaHistoryDBProvider.class);
this.reconContainerMetadataManager =
injector.getInstance(ReconContainerMetadataManager.class);
this.reconNamespaceSummaryManager =
@@ -183,6 +186,9 @@ public Void call() throws Exception {
finalizeUpgrade();
+ // Fresh SQL installations are initialized at the software version and
do not run upgrade actions.
+
replicaHistoryDBProvider.migrateReplicaHistory(reconDBProvider.getDbStore());
+
LOG.debug("Now starting all services of Recon...");
// Start all services
start();
@@ -372,6 +378,13 @@ public void stop() {
}
}
}
+ if (replicaHistoryDBProvider != null) {
+ try {
+ replicaHistoryDBProvider.close();
+ } catch (IOException ex) {
+ LOG.error("Recon replica history DB close failed", ex);
+ }
+ }
if (certClient != null) {
try {
certClient.close();
diff --git
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/scm/ReconContainerManager.java
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/scm/ReconContainerManager.java
index 230ed8a5f7d..85bcad55da0 100644
---
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/scm/ReconContainerManager.java
+++
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/scm/ReconContainerManager.java
@@ -375,9 +375,9 @@ public void removeContainerReplica(ContainerID containerID,
final ContainerReplicaHistory ts = replicaLastSeenMap.get(id);
if (ts != null) {
// Flush to DB, then remove from in-memory map
- upsertContainerHistory(cid, id, ts.getLastSeenTime(), ts.getBcsId(),
- state, ts.getChecksums());
- replicaLastSeenMap.remove(id);
+ if (upsertContainerHistory(cid, id, ts.getLastSeenTime(),
ts.getBcsId(), state, ts.getChecksums())) {
+ replicaLastSeenMap.remove(id);
+ }
}
}
}
@@ -462,18 +462,20 @@ public void flushReplicaHistoryMapToDB(boolean clearMap) {
synchronized (replicaHistoryMap) {
try {
cdbServiceProvider.batchStoreContainerReplicaHistory(replicaHistoryMap);
+ if (clearMap) {
+ replicaHistoryMap.clear();
+ }
} catch (IOException e) {
- LOG.debug("Error flushing container replica history to DB. {}",
- e.getMessage());
- }
- if (clearMap) {
- replicaHistoryMap.clear();
+ LOG.warn("Error flushing container replica history to DB.", e);
}
}
}
- public void upsertContainerHistory(long containerID, DatanodeID id, long
time,
- long bcsId, String state,
ContainerChecksums checksums) {
+ /**
+ * Persist replica history and report whether it is safe to discard the
in-memory entry.
+ */
+ public boolean upsertContainerHistory(long containerID, DatanodeID id, long
time,
+ long bcsId, String state,
ContainerChecksums checksums) {
Map<DatanodeID, ContainerReplicaHistory> tsMap;
try {
tsMap = cdbServiceProvider.getContainerReplicaHistory(containerID);
@@ -489,8 +491,10 @@ public void upsertContainerHistory(long containerID,
DatanodeID id, long time,
ts.setChecksums(checksums);
}
cdbServiceProvider.storeContainerReplicaHistory(containerID, tsMap);
+ return true;
} catch (IOException e) {
- LOG.debug("Error on DB operations. {}", e.getMessage());
+ LOG.warn("Unable to persist replica history for container {} on datanode
{}", containerID, id, e);
+ return false;
}
}
diff --git
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/spi/impl/ReconContainerMetadataManagerImpl.java
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/spi/impl/ReconContainerMetadataManagerImpl.java
index 0e7510d5777..610541f8c9c 100644
---
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/spi/impl/ReconContainerMetadataManagerImpl.java
+++
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/spi/impl/ReconContainerMetadataManagerImpl.java
@@ -21,7 +21,6 @@
import static
org.apache.hadoop.ozone.recon.spi.impl.ReconDBDefinition.CONTAINER_KEY;
import static
org.apache.hadoop.ozone.recon.spi.impl.ReconDBDefinition.CONTAINER_KEY_COUNT;
import static
org.apache.hadoop.ozone.recon.spi.impl.ReconDBDefinition.KEY_CONTAINER;
-import static
org.apache.hadoop.ozone.recon.spi.impl.ReconDBDefinition.REPLICA_HISTORY_V2;
import jakarta.annotation.Nonnull;
import java.io.IOException;
@@ -76,11 +75,12 @@ public class ReconContainerMetadataManagerImpl
private Table<ContainerKeyPrefix, Integer> containerKeyTable;
private Table<KeyPrefixContainer, Integer> keyContainerTable;
private Table<Long, Long> containerKeyCountTable;
- private Table<Long, ContainerReplicaHistoryList>
+ private final Table<Long, ContainerReplicaHistoryList>
containerReplicaHistoryTable;
private GlobalStatsDao globalStatsDao;
private DBStore containerDbStore;
+ private final ReconReplicaHistoryDBProvider replicaHistoryDBProvider;
private Configuration sqlConfiguration;
@@ -88,13 +88,17 @@ public class ReconContainerMetadataManagerImpl
@Inject
public ReconContainerMetadataManagerImpl(
- ReconDBProvider reconDBProvider, Configuration sqlConfiguration,
ReconOMMetadataManager omMetadataManager) {
- this(reconDBProvider.getDbStore(), sqlConfiguration, omMetadataManager);
+ ReconDBProvider reconDBProvider, ReconReplicaHistoryDBProvider
replicaHistoryDBProvider,
+ Configuration sqlConfiguration, ReconOMMetadataManager
omMetadataManager) {
+ this(reconDBProvider.getDbStore(), replicaHistoryDBProvider,
sqlConfiguration, omMetadataManager);
}
private ReconContainerMetadataManagerImpl(
- DBStore reconDBStore, Configuration sqlConfiguration,
ReconOMMetadataManager omMetadataManager) {
+ DBStore reconDBStore, ReconReplicaHistoryDBProvider
replicaHistoryDBProvider,
+ Configuration sqlConfiguration, ReconOMMetadataManager
omMetadataManager) {
containerDbStore = reconDBStore;
+ this.replicaHistoryDBProvider = replicaHistoryDBProvider;
+ containerReplicaHistoryTable =
replicaHistoryDBProvider.getReplicaHistoryTable();
globalStatsDao = new GlobalStatsDao(sqlConfiguration);
this.sqlConfiguration = sqlConfiguration;
this.omMetadataManager = omMetadataManager;
@@ -104,7 +108,8 @@ private ReconContainerMetadataManagerImpl(
@Override
public ReconContainerMetadataManager getStagedReconContainerMetadataManager(
DBStore stagedReconDbStore) {
- return new ReconContainerMetadataManagerImpl(stagedReconDbStore,
sqlConfiguration, omMetadataManager);
+ return new ReconContainerMetadataManagerImpl(
+ stagedReconDbStore, replicaHistoryDBProvider, sqlConfiguration,
omMetadataManager);
}
@Override
@@ -167,8 +172,6 @@ private void initializeTables() {
}
this.containerKeyCountTable =
CONTAINER_KEY_COUNT.getTable(containerDbStore);
- this.containerReplicaHistoryTable =
- REPLICA_HISTORY_V2.getTable(containerDbStore);
} catch (IOException e) {
LOG.error("Unable to create Container Key tables.", e);
}
@@ -241,11 +244,18 @@ public void batchStoreContainerReplicaHistory(
Map<Long, Map<DatanodeID, ContainerReplicaHistory>> replicaHistoryMap)
throws IOException {
try (BatchOperation batchOperation =
- containerDbStore.initBatchOperation()) {
+ replicaHistoryDBProvider.getDbStore().initBatchOperation()) {
for (Map.Entry<Long, Map<DatanodeID, ContainerReplicaHistory>> entry :
replicaHistoryMap.entrySet()) {
final long containerId = entry.getKey();
- final Map<DatanodeID, ContainerReplicaHistory> tsMap =
entry.getValue();
+ // The in-memory map contains only current replicas; preserve departed
datanodes on shutdown.
+ final Map<DatanodeID, ContainerReplicaHistory> tsMap =
getContainerReplicaHistory(containerId);
+ entry.getValue().forEach((id, inMemory) -> tsMap.merge(id, inMemory,
(stored, current) -> {
+ // Prefer the in-memory record on equal timestamps, as before.
+ ContainerReplicaHistory latest = stored.getLastSeenTime() >
current.getLastSeenTime() ? stored : current;
+ return new ContainerReplicaHistory(id,
Math.min(stored.getFirstSeenTime(), current.getFirstSeenTime()),
+ latest.getLastSeenTime(), latest.getBcsId(), latest.getState(),
latest.getChecksums());
+ }));
List<ContainerReplicaHistory> tsList = new ArrayList<>();
for (Map.Entry<DatanodeID, ContainerReplicaHistory> e :
tsMap.entrySet()) {
@@ -256,7 +266,7 @@ public void batchStoreContainerReplicaHistory(
new ContainerReplicaHistoryList(tsList));
}
- containerDbStore.commitBatchOperation(batchOperation);
+
replicaHistoryDBProvider.getDbStore().commitBatchOperation(batchOperation);
}
}
diff --git
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/spi/impl/ReconDBDefinition.java
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/spi/impl/ReconDBDefinition.java
index 3fe489da79d..92482a71db9 100644
---
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/spi/impl/ReconDBDefinition.java
+++
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/spi/impl/ReconDBDefinition.java
@@ -73,7 +73,7 @@ public class ReconDBDefinition extends DBDefinition.WithMap {
LongCodec.get(),
NSSummaryCodec.get());
- // Container Replica History with bcsId tracking.
+ // Retained in the legacy DB for migration to
ReconReplicaHistoryDBDefinition.
public static final DBColumnFamilyDefinition
<Long, ContainerReplicaHistoryList> REPLICA_HISTORY_V2 =
new DBColumnFamilyDefinition<Long, ContainerReplicaHistoryList>(
diff --git
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/spi/impl/ReconReplicaHistoryDBDefinition.java
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/spi/impl/ReconReplicaHistoryDBDefinition.java
new file mode 100644
index 00000000000..f8c0f36e7be
--- /dev/null
+++
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/spi/impl/ReconReplicaHistoryDBDefinition.java
@@ -0,0 +1,50 @@
+/*
+ * 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
org.apache.hadoop.ozone.recon.ReconConstants.RECON_REPLICA_HISTORY_DB;
+import static
org.apache.hadoop.ozone.recon.spi.impl.ReconDBDefinition.REPLICA_HISTORY_V2;
+
+import org.apache.hadoop.hdds.utils.db.DBColumnFamilyDefinition;
+import org.apache.hadoop.hdds.utils.db.DBDefinition;
+import org.apache.hadoop.hdds.utils.db.StringCodec;
+import org.apache.hadoop.ozone.recon.ReconServerConfigKeys;
+
+/**
+ * Replica history is independent of the OM-derived tables rebuilt during
reprocess.
+ */
+public class ReconReplicaHistoryDBDefinition extends DBDefinition.WithMap {
+ // Stores the migration completion marker in the destination DB so a retry
can skip copying history
+ // if the copy succeeded but persisting Recon's upgrade version failed.
+ public static final DBColumnFamilyDefinition<String, String> METADATA =
+ new DBColumnFamilyDefinition<>("metadata", StringCodec.get(),
StringCodec.get());
+
+ public ReconReplicaHistoryDBDefinition() {
+ super(DBColumnFamilyDefinition.newUnmodifiableMap(REPLICA_HISTORY_V2,
METADATA));
+ }
+
+ @Override
+ public String getName() {
+ return RECON_REPLICA_HISTORY_DB;
+ }
+
+ @Override
+ public String getLocationConfigKey() {
+ return ReconServerConfigKeys.OZONE_RECON_DB_DIR;
+ }
+}
diff --git
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/spi/impl/ReconReplicaHistoryDBProvider.java
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/spi/impl/ReconReplicaHistoryDBProvider.java
new file mode 100644
index 00000000000..a6328d00011
--- /dev/null
+++
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/spi/impl/ReconReplicaHistoryDBProvider.java
@@ -0,0 +1,98 @@
+/*
+ * 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
org.apache.hadoop.ozone.recon.spi.impl.ReconDBDefinition.REPLICA_HISTORY_V2;
+import static
org.apache.hadoop.ozone.recon.spi.impl.ReconReplicaHistoryDBDefinition.METADATA;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.hdds.utils.db.DBStore;
+import org.apache.hadoop.hdds.utils.db.DBStoreBuilder;
+import org.apache.hadoop.hdds.utils.db.Table;
+import org.apache.hadoop.ozone.recon.scm.ContainerReplicaHistoryList;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Owns the replica history DB, which is never replaced by an OM or SCM
snapshot.
+ */
+@Singleton
+public class ReconReplicaHistoryDBProvider implements Closeable {
+ private static final Logger LOG =
LoggerFactory.getLogger(ReconReplicaHistoryDBProvider.class);
+ private static final String MIGRATION_COMPLETE = "replicaHistoryMigrated";
+ private final DBStore dbStore;
+ private final Table<Long, ContainerReplicaHistoryList> replicaHistoryTable;
+
+ @Inject
+ public ReconReplicaHistoryDBProvider(OzoneConfiguration configuration)
throws IOException {
+ dbStore = DBStoreBuilder.createDBStore(configuration, new
ReconReplicaHistoryDBDefinition());
+ try {
+ replicaHistoryTable = REPLICA_HISTORY_V2.getTable(dbStore);
+ } catch (IOException | RuntimeException e) {
+ try {
+ dbStore.close();
+ } catch (RuntimeException closeException) {
+ e.addSuppressed(closeException);
+ }
+ throw e;
+ }
+ }
+
+ /**
+ * Migrate legacy history before services start. Safe to retry if version
persistence fails after the copy.
+ */
+ public void migrateReplicaHistory(DBStore legacyStore) throws IOException {
+ Table<String, String> metadata = METADATA.getTable(dbStore);
+ File migrationFile = new File(dbStore.getDbLocation(),
"replica-history-migration.sst");
+ try {
+ if (metadata.isExist(MIGRATION_COMPLETE)) {
+ return;
+ }
+ // Retry incomplete migrations from the unchanged source before any
services start.
+ Files.deleteIfExists(migrationFile.toPath());
+
legacyStore.getTable(REPLICA_HISTORY_V2.getName()).dumpToFileWithPrefix(migrationFile,
null);
+
dbStore.getTable(REPLICA_HISTORY_V2.getName()).loadFromFile(migrationFile);
+ metadata.put(MIGRATION_COMPLETE, "true");
+ dbStore.flushLog(true);
+ LOG.info("Migrated container replica history to {}",
dbStore.getDbLocation());
+ } finally {
+ if (migrationFile.exists() && !migrationFile.delete()) {
+ LOG.warn("Unable to delete replica history migration file {}",
migrationFile);
+ }
+ }
+ }
+
+ public DBStore getDbStore() {
+ return dbStore;
+ }
+
+ public Table<Long, ContainerReplicaHistoryList> getReplicaHistoryTable() {
+ return replicaHistoryTable;
+ }
+
+ @Override
+ public void close() throws IOException {
+ dbStore.close();
+ }
+}
diff --git
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReconVersion.java
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReconVersion.java
index 0e79f2a2a86..f85d835e303 100644
---
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReconVersion.java
+++
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReconVersion.java
@@ -43,6 +43,7 @@ public enum ReconVersion implements ComponentVersion {
REPLICATED_SIZE_OF_FILES(4, "Adds replicatedSizeOfFiles to NSSummary"),
UNHEALTHY_CONTAINERS_STATE_CONTAINER_ID_INDEX(5,
"Adds idx_state_container_id index on UNHEALTHY_CONTAINERS for
upgrades"),
+ REPLICA_HISTORY_DB(6, "Stores container replica history in separate
recon-container-replica-history.db"),
UNKNOWN_VERSION(-1, "Used when a version cannot be deserialized to any
version recognized by this"
+ " component, which may indicate it came from a component in a newer
version");
diff --git
a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReplicaHistoryDBUpgradeAction.java
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReplicaHistoryDBUpgradeAction.java
new file mode 100644
index 00000000000..bed07dfaa66
--- /dev/null
+++
b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReplicaHistoryDBUpgradeAction.java
@@ -0,0 +1,40 @@
+/*
+ * 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.upgrade;
+
+import com.google.inject.Injector;
+import javax.sql.DataSource;
+import org.apache.hadoop.ozone.recon.ReconGuiceServletContextListener;
+import org.apache.hadoop.ozone.recon.spi.impl.ReconDBProvider;
+import org.apache.hadoop.ozone.recon.spi.impl.ReconReplicaHistoryDBProvider;
+
+/**
+ * Preserve legacy container replica history before finalizing the separate
history DB layout.
+ */
+@ReconUpgradeActionForVersion(version = ReconVersion.REPLICA_HISTORY_DB)
+public class ReplicaHistoryDBUpgradeAction implements ReconUpgradeAction {
+ @Override
+ public void execute(DataSource source) throws Exception {
+ Injector injector = ReconGuiceServletContextListener.getGlobalInjector();
+ if (injector == null) {
+ throw new IllegalStateException("Guice injector not initialized. Replica
history migration cannot proceed.");
+ }
+ injector.getInstance(ReconReplicaHistoryDBProvider.class)
+
.migrateReplicaHistory(injector.getInstance(ReconDBProvider.class).getDbStore());
+ }
+}
diff --git
a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/ReconTestInjector.java
b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/ReconTestInjector.java
index e93e4d71f52..c634ebeb388 100644
---
a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/ReconTestInjector.java
+++
b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/ReconTestInjector.java
@@ -51,6 +51,7 @@
import org.apache.hadoop.ozone.recon.spi.impl.ReconFileMetadataManagerImpl;
import org.apache.hadoop.ozone.recon.spi.impl.ReconGlobalStatsManagerImpl;
import org.apache.hadoop.ozone.recon.spi.impl.ReconNamespaceSummaryManagerImpl;
+import org.apache.hadoop.ozone.recon.spi.impl.ReconReplicaHistoryDBProvider;
import org.apache.ratis.util.Preconditions;
/**
@@ -187,6 +188,7 @@ protected void configure() {
.to(ReconNamespaceSummaryManagerImpl.class)
.in(Singleton.class);
bind(ReconDBProvider.class).in(Singleton.class);
+ bind(ReconReplicaHistoryDBProvider.class).in(Singleton.class);
}
for (Map.Entry<Class, Object> entry :
diff --git
a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/api/TestContainerEndpoint.java
b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/api/TestContainerEndpoint.java
index 505f22c7c8e..68485d05ab9 100644
---
a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/api/TestContainerEndpoint.java
+++
b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/api/TestContainerEndpoint.java
@@ -65,9 +65,11 @@
import org.apache.hadoop.hdds.protocol.DatanodeID;
import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
import org.apache.hadoop.hdds.protocol.proto.HddsProtos.ReplicationFactor;
+import
org.apache.hadoop.hdds.protocol.proto.StorageContainerDatanodeProtocolProtos.ContainerReplicaProto;
import org.apache.hadoop.hdds.scm.container.ContainerChecksums;
import org.apache.hadoop.hdds.scm.container.ContainerID;
import org.apache.hadoop.hdds.scm.container.ContainerInfo;
+import org.apache.hadoop.hdds.scm.container.ContainerReplica;
import org.apache.hadoop.hdds.scm.container.ContainerStateManager;
import
org.apache.hadoop.hdds.scm.container.common.helpers.ContainerWithPipeline;
import org.apache.hadoop.hdds.scm.pipeline.Pipeline;
@@ -100,6 +102,7 @@
import org.apache.hadoop.ozone.recon.persistence.ContainerHealthSchemaManager;
import org.apache.hadoop.ozone.recon.persistence.ContainerHistory;
import org.apache.hadoop.ozone.recon.recovery.ReconOMMetadataManager;
+import org.apache.hadoop.ozone.recon.scm.ContainerReplicaHistory;
import org.apache.hadoop.ozone.recon.scm.ReconContainerManager;
import org.apache.hadoop.ozone.recon.scm.ReconPipelineManager;
import org.apache.hadoop.ozone.recon.scm.ReconStorageContainerManagerFacade;
@@ -107,6 +110,7 @@
import org.apache.hadoop.ozone.recon.spi.ReconNamespaceSummaryManager;
import org.apache.hadoop.ozone.recon.spi.StorageContainerServiceProvider;
import org.apache.hadoop.ozone.recon.spi.impl.OzoneManagerServiceProviderImpl;
+import org.apache.hadoop.ozone.recon.spi.impl.ReconDBProvider;
import
org.apache.hadoop.ozone.recon.spi.impl.StorageContainerServiceProviderImpl;
import org.apache.hadoop.ozone.recon.tasks.ContainerKeyMapperHelper;
import org.apache.hadoop.ozone.recon.tasks.ContainerKeyMapperTaskFSO;
@@ -138,6 +142,7 @@ public class TestContainerEndpoint {
private ContainerStateManager containerStateManager;
private ReconPipelineManager reconPipelineManager;
private ReconContainerMetadataManager reconContainerMetadataManager;
+ private ReconDBProvider reconDBProvider;
private ContainerEndpoint containerEndpoint;
private boolean isSetupDone = false;
private ContainerHealthSchemaManager containerHealthSchemaManager;
@@ -208,6 +213,7 @@ private void initializeInjector() throws Exception {
ozoneStorageContainerManager.getPipelineManager();
reconContainerMetadataManager =
reconTestInjector.getInstance(ReconContainerMetadataManager.class);
+ reconDBProvider = reconTestInjector.getInstance(ReconDBProvider.class);
containerEndpoint = reconTestInjector.getInstance(ContainerEndpoint.class);
containerHealthSchemaManager =
reconTestInjector.getInstance(ContainerHealthSchemaManager.class);
@@ -1069,6 +1075,54 @@ public void testUnhealthyContainersPaging()
assertEquals(6L, records.get(2).getContainerID());
}
+ @Test
+ public void testReplicaHistorySurvivesOmReprocess() throws Exception {
+
reconContainerManager.addNewContainer(getTestContainer(HddsProtos.LifeCycleState.CLOSED,
1L));
+ List<ContainerReplica> replicas = new ArrayList<>();
+ for (int i = 1; i <= 4; i++) {
+ DatanodeID id = newDatanode("host" + i, "127.0.0." + i);
+ ContainerReplica replica = ContainerReplica.newBuilder()
+ .setContainerID(containerID)
+ .setContainerState(ContainerReplicaProto.State.CLOSED)
+ .setDatanodeDetails(reconContainerManager.getNodeDB().get(id))
+ .setSequenceId(100L)
+ .setChecksums(ContainerChecksums.of(1234L, 0L))
+ .build();
+ reconContainerManager.updateContainerReplica(containerID, replica);
+ replicas.add(replica);
+ }
+ reconContainerManager.removeContainerReplica(containerID, replicas.get(3));
+ assertEquals(3,
reconContainerManager.getReplicaHistoryMap().get(1L).size());
+ Map<DatanodeID, ContainerReplicaHistory> expected =
+ reconContainerMetadataManager.getContainerReplicaHistory(1L);
+ assertEquals(4, expected.size());
+ long expectedKeyCount =
reconContainerMetadataManager.getKeyCountForContainer(1L);
+
+ for (int i = 0; i < 2; i++) {
+ ReconConstants.resetTableTruncatedFlags();
+ ReconDBProvider stagedProvider =
reconDBProvider.getStagedReconDBProvider();
+ ReconContainerMetadataManager stagedManager =
+
reconContainerMetadataManager.getStagedReconContainerMetadataManager(stagedProvider.getDbStore());
+ assertTrue(new ContainerKeyMapperTaskOBS(stagedManager, omConfiguration)
+ .reprocess(reconOMMetadataManager).isTaskSuccess());
+ reconDBProvider.replaceStagedDb(stagedProvider);
+ reconContainerMetadataManager.reinitialize(reconDBProvider);
+ assertEquals(expectedKeyCount,
reconContainerMetadataManager.getKeyCountForContainer(1L));
+
+ List<ContainerHistory> histories = (List<ContainerHistory>)
+ containerEndpoint.getReplicaHistoryForContainer(1L).getEntity();
+ assertEquals(4, histories.size());
+ assertTrue(histories.stream().anyMatch(history ->
+
history.getDatanodeUuid().equals(replicas.get(3).getDatanodeDetails().getID().toString())));
+ Map<DatanodeID, ContainerReplicaHistory> actual =
reconContainerMetadataManager.getContainerReplicaHistory(1L);
+ expected.forEach((id, history) -> assertEquals(history.toProto(),
actual.get(id).toProto()));
+ }
+
+ // A shutdown flush must preserve the departed datanode after reprocess as
well.
+ reconContainerManager.flushReplicaHistoryMapToDB(true);
+ assertEquals(4, reconContainerManager.getAllContainerHistory(1L).size());
+ }
+
@Test
public void testGetReplicaHistoryForContainer() throws IOException {
// Add container history for container id 1
diff --git
a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/scm/AbstractReconContainerManagerTest.java
b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/scm/AbstractReconContainerManagerTest.java
index a395d4a3c6b..d02c9a821c4 100644
---
a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/scm/AbstractReconContainerManagerTest.java
+++
b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/scm/AbstractReconContainerManagerTest.java
@@ -73,6 +73,7 @@ public class AbstractReconContainerManagerTest {
private OzoneConfiguration conf;
private ReconPipelineManager pipelineManager;
private ReconContainerManager containerManager;
+ private ReconContainerMetadataManager reconContainerMetadataManager;
private DBStore store;
@BeforeEach
@@ -102,6 +103,7 @@ public void setUp(@TempDir File tempDir) throws Exception {
ContainerReplicaPendingOps pendingOps = new ContainerReplicaPendingOps(
Clock.system(ZoneId.systemDefault()), null);
+ reconContainerMetadataManager = mock(ReconContainerMetadataManager.class);
containerManager = new ReconContainerManager(
conf,
store,
@@ -109,7 +111,7 @@ public void setUp(@TempDir File tempDir) throws Exception {
pipelineManager,
getScmServiceProvider(),
mock(ContainerHealthSchemaManager.class),
- mock(ReconContainerMetadataManager.class),
+ reconContainerMetadataManager,
scmhaManager,
sequenceIdGen,
pendingOps);
@@ -133,6 +135,10 @@ protected ReconContainerManager getContainerManager() {
return containerManager;
}
+ protected ReconContainerMetadataManager getReconContainerMetadataManager() {
+ return reconContainerMetadataManager;
+ }
+
private StorageContainerServiceProvider getScmServiceProvider()
throws IOException, TimeoutException {
Pipeline pipeline = getRandomPipeline();
diff --git
a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/scm/TestReconContainerManager.java
b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/scm/TestReconContainerManager.java
index bd59c52a003..365fa3bcf19 100644
---
a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/scm/TestReconContainerManager.java
+++
b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/scm/TestReconContainerManager.java
@@ -25,8 +25,14 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyMap;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
@@ -51,8 +57,11 @@
import
org.apache.hadoop.hdds.scm.container.common.helpers.ContainerWithPipeline;
import org.apache.hadoop.hdds.scm.ha.SCMHAManager;
import org.apache.hadoop.hdds.scm.pipeline.Pipeline;
+import org.apache.hadoop.ozone.recon.spi.ReconContainerMetadataManager;
import org.apache.ozone.test.GenericTestUtils;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
/**
* Test Recon Container Manager.
@@ -384,6 +393,87 @@ public void testUpdateAndRemoveContainerReplica()
repHistMap.get(cIDlong1).keySet().iterator().next());
}
+ @ParameterizedTest
+ @ValueSource(booleans = {true, false})
+ void testRemoveContainerReplicaRetainsHistoryOnPersistenceFailure(boolean
readFailure) throws Exception {
+ ContainerReplica replica = addContainerReplicaWithHistory();
+ ReconContainerManager manager = getContainerManager();
+ ContainerID containerID = replica.getContainerID();
+ DatanodeID id = replica.getDatanodeDetails().getID();
+ ContainerReplicaHistory history =
manager.getReplicaHistoryMap().get(containerID.getId()).get(id);
+ ReconContainerMetadataManager metadataManager =
getReconContainerMetadataManager();
+ if (readFailure) {
+ doThrow(new IOException("History read
failed")).when(metadataManager).getContainerReplicaHistory(anyLong());
+ } else {
+ doThrow(new IOException("History write failed"))
+ .when(metadataManager).storeContainerReplicaHistory(anyLong(),
anyMap());
+ }
+
+ manager.removeContainerReplica(containerID, replica);
+
+ assertTrue(manager.getContainerReplicas(containerID).isEmpty());
+ assertSame(history,
manager.getReplicaHistoryMap().get(containerID.getId()).get(id));
+ assertEquals(1,
manager.getAllContainerHistory(containerID.getId()).size());
+ doAnswer(invocation -> {
+ Map<Long, Map<DatanodeID, ContainerReplicaHistory>> pending =
invocation.getArgument(0);
+ assertSame(history, pending.get(containerID.getId()).get(id));
+ return null;
+ }).when(metadataManager).batchStoreContainerReplicaHistory(anyMap());
+ manager.flushReplicaHistoryMapToDB(true);
+ assertTrue(manager.getReplicaHistoryMap().isEmpty());
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {true, false})
+ void testFailedShutdownFlushRetainsHistoryForRetry(boolean clearMap) throws
Exception {
+ ContainerReplica replica = addContainerReplicaWithHistory();
+ ReconContainerManager manager = getContainerManager();
+ long containerID = replica.getContainerID().getId();
+ DatanodeID id = replica.getDatanodeDetails().getID();
+ ContainerReplicaHistory history =
manager.getReplicaHistoryMap().get(containerID).get(id);
+ ReconContainerMetadataManager metadataManager =
getReconContainerMetadataManager();
+ doThrow(new IOException("History batch write failed"))
+ .when(metadataManager).batchStoreContainerReplicaHistory(anyMap());
+
+ manager.flushReplicaHistoryMapToDB(clearMap);
+
+ assertSame(history,
manager.getReplicaHistoryMap().get(containerID).get(id));
+
doNothing().when(metadataManager).batchStoreContainerReplicaHistory(anyMap());
+ manager.flushReplicaHistoryMapToDB(clearMap);
+ if (clearMap) {
+ assertTrue(manager.getReplicaHistoryMap().isEmpty());
+ } else {
+ assertSame(history,
manager.getReplicaHistoryMap().get(containerID).get(id));
+ }
+ }
+
+ @Test
+ void testUpsertContainerHistoryReportsPersistenceStatus() throws Exception {
+ ReconContainerManager manager = getContainerManager();
+ DatanodeID id = DatanodeID.randomID();
+ ContainerChecksums checksums = ContainerChecksums.of(1234L, 0L);
+ assertTrue(manager.upsertContainerHistory(1L, id, 10L, 20L, "OPEN",
checksums));
+ ReconContainerMetadataManager metadataManager =
getReconContainerMetadataManager();
+ doThrow(new IOException("History write failed")).doNothing()
+ .when(metadataManager).storeContainerReplicaHistory(anyLong(),
anyMap());
+ assertFalse(manager.upsertContainerHistory(1L, id, 10L, 20L, "OPEN",
checksums));
+ assertTrue(manager.upsertContainerHistory(1L, id, 10L, 20L, "OPEN",
checksums));
+ }
+
+ private ContainerReplica addContainerReplicaWithHistory() throws
IOException, TimeoutException {
+ ContainerWithPipeline container = getTestContainer(LifeCycleState.OPEN);
+ getContainerManager().addNewContainer(container);
+ ContainerReplica replica = ContainerReplica.newBuilder()
+ .setContainerID(container.getContainerInfo().containerID())
+ .setContainerState(State.OPEN)
+ .setDatanodeDetails(randomDatanodeDetails())
+ .setSequenceId(1001L)
+ .setChecksums(ContainerChecksums.of(1234L, 0L))
+ .build();
+ getContainerManager().updateContainerReplica(replica.getContainerID(),
replica);
+ return replica;
+ }
+
@Test
public void testAddNewContainerWithMissingPipeline()
throws IOException, TimeoutException {
diff --git
a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/scm/TestReconStorageContainerManagerFacade.java
b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/scm/TestReconStorageContainerManagerFacade.java
index 42fd25b36c7..b8bfe929a05 100644
---
a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/scm/TestReconStorageContainerManagerFacade.java
+++
b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/scm/TestReconStorageContainerManagerFacade.java
@@ -34,6 +34,8 @@
import java.util.ArrayList;
import java.util.List;
import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.hdds.protocol.DatanodeID;
+import org.apache.hadoop.hdds.scm.container.ContainerChecksums;
import org.apache.hadoop.hdds.scm.server.OzoneStorageContainerManager;
import org.apache.hadoop.hdds.utils.db.DBCheckpoint;
import org.apache.hadoop.hdds.utils.db.DBStore;
@@ -90,6 +92,10 @@ void testScmSnapshotDbIsOpenedAtCanonicalReconPath() throws
Exception {
ReconStorageContainerManagerFacade reconScm =
injector.getInstance(ReconStorageContainerManagerFacade.class);
+ ReconContainerManager containerManager = (ReconContainerManager)
reconScm.getContainerManager();
+ DatanodeID historyDatanode = DatanodeID.randomID();
+ containerManager.upsertContainerHistory(1L, historyDatanode, 10L, 20L,
+ "CLOSED", ContainerChecksums.of(1234L, 0L));
metricsToUnregister.add(getContainerSyncMetrics(reconScm));
OzoneConfiguration conf = injector.getInstance(OzoneConfiguration.class);
File checkpointDir =
@@ -113,6 +119,8 @@ void testScmSnapshotDbIsOpenedAtCanonicalReconPath() throws
Exception {
reconScm.getScmDBStore().getDbLocation().getCanonicalFile());
assertTrue(canonicalReconScmDb.exists());
assertFalse(checkpointDir.exists());
+ assertEquals(1, containerManager.getAllContainerHistory(1L).size());
+ assertEquals(historyDatanode.toString(),
containerManager.getAllContainerHistory(1L).get(0).getDatanodeUuid());
}
@Test
diff --git
a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/spi/impl/TestReconReplicaHistoryDBProvider.java
b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/spi/impl/TestReconReplicaHistoryDBProvider.java
new file mode 100644
index 00000000000..7bbe3e62b71
--- /dev/null
+++
b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/spi/impl/TestReconReplicaHistoryDBProvider.java
@@ -0,0 +1,262 @@
+/*
+ * 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
org.apache.hadoop.ozone.recon.ReconServerConfigKeys.OZONE_RECON_DB_DIR;
+import static
org.apache.hadoop.ozone.recon.spi.impl.ReconDBDefinition.REPLICA_HISTORY_V2;
+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.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Map;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.hdds.protocol.DatanodeID;
+import org.apache.hadoop.hdds.scm.container.ContainerChecksums;
+import org.apache.hadoop.hdds.utils.db.DBStore;
+import org.apache.hadoop.hdds.utils.db.DBStoreBuilder;
+import org.apache.hadoop.hdds.utils.db.RDBBatchOperation;
+import org.apache.hadoop.hdds.utils.db.Table;
+import org.apache.hadoop.ozone.recon.ReconUtils;
+import org.apache.hadoop.ozone.recon.recovery.ReconOMMetadataManager;
+import org.apache.hadoop.ozone.recon.scm.ContainerReplicaHistory;
+import org.apache.hadoop.ozone.recon.scm.ContainerReplicaHistoryList;
+import org.apache.hadoop.ozone.recon.spi.ReconContainerMetadataManager;
+import org.jooq.impl.DefaultConfiguration;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+/**
+ * Tests startup migration and the independent lifetime of container replica
history.
+ */
+class TestReconReplicaHistoryDBProvider {
+ @TempDir
+ private Path temporaryFolder;
+
+ private OzoneConfiguration configuration;
+ private ReconDBProvider reconDBProvider;
+ private ReconReplicaHistoryDBProvider historyDBProvider;
+
+ @BeforeEach
+ void setUp() {
+ configuration = new OzoneConfiguration();
+ configuration.set(OZONE_RECON_DB_DIR, temporaryFolder.toString());
+ reconDBProvider = new ReconDBProvider(configuration, new ReconUtils());
+ }
+
+ @AfterEach
+ void tearDown() throws Exception {
+ if (historyDBProvider != null) {
+ historyDBProvider.close();
+ }
+ reconDBProvider.close();
+ }
+
+ @Test
+ void testMigratesLegacyHistoryWithoutChangingSource() throws Exception {
+ Table<Long, ContainerReplicaHistoryList> legacyTable =
REPLICA_HISTORY_V2.getTable(reconDBProvider.getDbStore());
+ ContainerReplicaHistoryList history = new
ContainerReplicaHistoryList(Arrays.asList(newHistory(), newHistory()));
+ for (long id = 1; id <= 2000; id++) {
+ legacyTable.put(id, history);
+ }
+
+ openHistoryDB();
+ for (long id = 1; id <= 2000; id++) {
+ assertEquals(history.toProto(),
historyDBProvider.getReplicaHistoryTable().get(id).toProto());
+ assertEquals(history.toProto(), legacyTable.get(id).toProto());
+ }
+ assertFalse(historyDBProvider.getDbStore().getTableNames()
+ .containsValue(ReconDBDefinition.CONTAINER_KEY.getName()));
+ }
+
+ @Test
+ void testFreshDatabaseAndRestartSkipMigration() throws Exception {
+ openHistoryDB();
+ assertTrue(historyDBProvider.getReplicaHistoryTable().isEmpty());
+ ContainerReplicaHistoryList history = new
ContainerReplicaHistoryList(Collections.singletonList(newHistory()));
+ historyDBProvider.getReplicaHistoryTable().put(1L, history);
+ historyDBProvider.close();
+
+ DBStore sourceStore = mock(DBStore.class);
+ historyDBProvider = new ReconReplicaHistoryDBProvider(configuration);
+ historyDBProvider.migrateReplicaHistory(sourceStore);
+ verifyNoInteractions(sourceStore);
+ assertEquals(history.toProto(),
historyDBProvider.getReplicaHistoryTable().get(1L).toProto());
+ }
+
+ @Test
+ void testRetriesPartiallyMigratedDatabase() throws Exception {
+ ContainerReplicaHistoryList history = new
ContainerReplicaHistoryList(Collections.singletonList(newHistory()));
+ Table<Long, ContainerReplicaHistoryList> source =
REPLICA_HISTORY_V2.getTable(reconDBProvider.getDbStore());
+ source.put(1L, history);
+ source.put(2L, history);
+ try (DBStore partialStore = DBStoreBuilder.createDBStore(configuration,
new ReconReplicaHistoryDBDefinition())) {
+ REPLICA_HISTORY_V2.getTable(partialStore).put(1L,
+ new
ContainerReplicaHistoryList(Collections.singletonList(newHistory())));
+ }
+
+ openHistoryDB();
+ assertEquals(history.toProto(),
historyDBProvider.getReplicaHistoryTable().get(1L).toProto());
+ assertEquals(history.toProto(),
historyDBProvider.getReplicaHistoryTable().get(2L).toProto());
+ }
+
+ @Test
+ void testFailedMigrationCanBeRetriedAfterRestart() throws Exception {
+ ContainerReplicaHistoryList history = new
ContainerReplicaHistoryList(Collections.singletonList(newHistory()));
+ REPLICA_HISTORY_V2.getTable(reconDBProvider.getDbStore()).put(1L, history);
+ Table<byte[], byte[]> sourceTable =
reconDBProvider.getDbStore().getTable(REPLICA_HISTORY_V2.getName());
+ Table<byte[], byte[]> failingTable = mock(Table.class);
+ doAnswer(invocation -> {
+ sourceTable.dumpToFileWithPrefix(invocation.getArgument(0), null);
+ throw new IOException("Interrupted migration");
+ }).when(failingTable).dumpToFileWithPrefix(any(), isNull());
+ DBStore failingSource = mock(DBStore.class);
+ when(failingSource.getTable(anyString())).thenReturn(failingTable);
+ historyDBProvider = new ReconReplicaHistoryDBProvider(configuration);
+ assertThrows(IOException.class, () ->
historyDBProvider.migrateReplicaHistory(failingSource));
+ historyDBProvider.close();
+ try (DBStore destination = DBStoreBuilder.createDBStore(configuration, new
ReconReplicaHistoryDBDefinition())) {
+
assertTrue(ReconReplicaHistoryDBDefinition.METADATA.getTable(destination).isEmpty());
+ assertTrue(REPLICA_HISTORY_V2.getTable(destination).isEmpty());
+ }
+ assertEquals(history.toProto(),
REPLICA_HISTORY_V2.getTable(reconDBProvider.getDbStore()).get(1L).toProto());
+
+ openHistoryDB();
+ assertEquals(history.toProto(),
historyDBProvider.getReplicaHistoryTable().get(1L).toProto());
+ }
+
+ @Test
+ void testHistorySurvivesRepeatedStagedDbReplacement() throws Exception {
+ openHistoryDB();
+ ReconContainerMetadataManager manager = newManager();
+ ContainerReplicaHistory history = newHistory();
+ manager.storeContainerReplicaHistory(1L,
Collections.singletonMap(history.getId(), history));
+ DBStore historyStore = historyDBProvider.getDbStore();
+ Table<Long, ContainerReplicaHistoryList> historyTable =
historyDBProvider.getReplicaHistoryTable();
+
+ for (int i = 0; i < 3; i++) {
+ ReconDBProvider stagedProvider =
reconDBProvider.getStagedReconDBProvider();
+ ReconContainerMetadataManager stagedManager =
manager.getStagedReconContainerMetadataManager(
+ stagedProvider.getDbStore());
+ try (RDBBatchOperation batch = RDBBatchOperation.newAtomicOperation()) {
+ stagedManager.batchStoreContainerKeyCounts(batch, 1L, (long) i + 1);
+ stagedManager.commitBatchOperation(batch);
+ }
+ // History writes continue while OM-derived data is being rebuilt in
staging.
+ ContainerReplicaHistory anotherHistory = newHistory();
+ Map<DatanodeID, ContainerReplicaHistory> histories =
manager.getContainerReplicaHistory(1L);
+ histories.put(anotherHistory.getId(), anotherHistory);
+ manager.storeContainerReplicaHistory(1L, histories);
+
assertTrue(REPLICA_HISTORY_V2.getTable(stagedProvider.getDbStore()).isEmpty());
+ reconDBProvider.replaceStagedDb(stagedProvider);
+ manager.reinitialize(reconDBProvider);
+
+ assertSame(historyStore, historyDBProvider.getDbStore());
+ assertSame(historyTable, historyDBProvider.getReplicaHistoryTable());
+ assertFalse(historyStore.isClosed());
+ assertEquals(i + 1, manager.getKeyCountForContainer(1L));
+ assertEquals(i + 2, manager.getContainerReplicaHistory(1L).size());
+ assertEquals(history.toProto(),
manager.getContainerReplicaHistory(1L).get(history.getId()).toProto());
+ }
+ historyDBProvider.close();
+ openHistoryDB();
+ assertEquals(4, newManager().getContainerReplicaHistory(1L).size());
+ }
+
+ @Test
+ void testShutdownBatchPreservesDepartedReplicas() throws Exception {
+ openHistoryDB();
+ ReconContainerMetadataManager manager = newManager();
+ ContainerReplicaHistory departed = newHistory();
+ ContainerReplicaHistory current = newHistory();
+ manager.storeContainerReplicaHistory(1L,
Collections.singletonMap(departed.getId(), departed));
+ manager.batchStoreContainerReplicaHistory(Collections.singletonMap(
+ 1L, Collections.singletonMap(current.getId(), current)));
+ historyDBProvider.close();
+ openHistoryDB();
+
+ Map<DatanodeID, ContainerReplicaHistory> history =
newManager().getContainerReplicaHistory(1L);
+ assertEquals(2, history.size());
+ assertEquals(departed.toProto(), history.get(departed.getId()).toProto());
+ assertEquals(current.toProto(), history.get(current.getId()).toProto());
+ assertNull(historyDBProvider.getReplicaHistoryTable().get(2L));
+ }
+
+ @ParameterizedTest
+ @CsvSource({
+ "10, 20, 30, 40, 10, 40, true",
+ "30, 40, 10, 20, 10, 40, false",
+ "10, 40, 30, 40, 10, 40, true"
+ })
+ void testShutdownBatchPreservesFirstSeenAndLatestReplicaInfo(long
storedFirstSeen, long storedLastSeen,
+ long currentFirstSeen, long currentLastSeen, long expectedFirstSeen,
long expectedLastSeen,
+ boolean currentFieldsExpected) throws Exception {
+ openHistoryDB();
+ ReconContainerMetadataManager manager = newManager();
+ DatanodeID id = DatanodeID.randomID();
+ ContainerReplicaHistory stored = new ContainerReplicaHistory(id,
storedFirstSeen, storedLastSeen, 100L,
+ "OPEN", ContainerChecksums.of(1111L, 0L));
+ ContainerReplicaHistory current = new ContainerReplicaHistory(id,
currentFirstSeen, currentLastSeen, 200L,
+ "CLOSED", ContainerChecksums.of(2222L, 0L));
+ manager.storeContainerReplicaHistory(1L, Collections.singletonMap(id,
stored));
+ manager.batchStoreContainerReplicaHistory(Collections.singletonMap(1L,
Collections.singletonMap(id, current)));
+ assertEquals(currentFirstSeen, current.getFirstSeenTime());
+ assertEquals(currentLastSeen, current.getLastSeenTime());
+ historyDBProvider.close();
+ openHistoryDB();
+
+ ContainerReplicaHistory expected = new ContainerReplicaHistory(id,
expectedFirstSeen, expectedLastSeen,
+ currentFieldsExpected ? 200L : 100L, currentFieldsExpected ? "CLOSED"
: "OPEN",
+ ContainerChecksums.of(currentFieldsExpected ? 2222L : 1111L, 0L));
+ Map<DatanodeID, ContainerReplicaHistory> history =
newManager().getContainerReplicaHistory(1L);
+ assertEquals(1, history.size());
+ assertEquals(expected.toProto(), history.get(id).toProto());
+ }
+
+ private void openHistoryDB() throws IOException {
+ historyDBProvider = new ReconReplicaHistoryDBProvider(configuration);
+ historyDBProvider.migrateReplicaHistory(reconDBProvider.getDbStore());
+ }
+
+ private ReconContainerMetadataManager newManager() {
+ return new ReconContainerMetadataManagerImpl(reconDBProvider,
historyDBProvider,
+ new DefaultConfiguration(), mock(ReconOMMetadataManager.class));
+ }
+
+ private static ContainerReplicaHistory newHistory() {
+ return new ContainerReplicaHistory(DatanodeID.randomID(), 10L, 20L, 30L,
+ "CLOSED", ContainerChecksums.of(1234L, 0L));
+ }
+}
diff --git
a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestReconVersionManager.java
b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestReconVersionManager.java
index 4017c09b24c..bd9e1715aa9 100644
---
a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestReconVersionManager.java
+++
b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestReconVersionManager.java
@@ -17,6 +17,8 @@
package org.apache.hadoop.ozone.recon.upgrade;
+import static
org.apache.hadoop.ozone.recon.ReconServerConfigKeys.OZONE_RECON_DB_DIR;
+import static
org.apache.hadoop.ozone.recon.spi.impl.ReconDBDefinition.REPLICA_HISTORY_V2;
import static
org.apache.ozone.recon.schema.SchemaVersionTableDefinition.SCHEMA_VERSION_TABLE_NAME;
import static org.jooq.impl.DSL.name;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -26,22 +28,29 @@
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Provider;
+import com.google.inject.Singleton;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.sql.SQLException;
import java.util.Arrays;
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -49,9 +58,18 @@
import java.util.stream.Stream;
import javax.sql.DataSource;
import org.apache.hadoop.hdds.ComponentVersion;
+import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.hdds.protocol.DatanodeID;
+import org.apache.hadoop.hdds.scm.container.ContainerChecksums;
+import org.apache.hadoop.hdds.utils.db.DBStore;
+import org.apache.hadoop.ozone.recon.ReconGuiceServletContextListener;
import org.apache.hadoop.ozone.recon.persistence.AbstractReconSqlDBTest;
import org.apache.hadoop.ozone.recon.persistence.DataSourceConfiguration;
import org.apache.hadoop.ozone.recon.persistence.JooqPersistenceModule;
+import org.apache.hadoop.ozone.recon.scm.ContainerReplicaHistory;
+import org.apache.hadoop.ozone.recon.scm.ContainerReplicaHistoryList;
+import org.apache.hadoop.ozone.recon.spi.impl.ReconDBProvider;
+import org.apache.hadoop.ozone.recon.spi.impl.ReconReplicaHistoryDBProvider;
import org.apache.hadoop.ozone.upgrade.AbstractComponentVersionManagerTest;
import org.apache.hadoop.ozone.upgrade.ComponentUpgradeActionProvider;
import org.apache.hadoop.ozone.upgrade.ComponentVersionManager;
@@ -63,6 +81,7 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.provider.Arguments;
+import org.mockito.MockedStatic;
/**
* Tests for {@link ReconVersionManager}.
@@ -150,6 +169,8 @@ public void testClasspathScanDiscoversUpgradeActions()
throws Exception {
ReconUpgradeAction taskStatusAction =
versionManager.getUpgradeActionsForTesting()
.get(ReconVersion.TASK_STATUS_STATISTICS);
assertInstanceOf(ReconTaskStatusTableUpgradeAction.class,
taskStatusAction);
+ assertInstanceOf(ReplicaHistoryDBUpgradeAction.class,
+
versionManager.getUpgradeActionsForTesting().get(ReconVersion.REPLICA_HISTORY_DB));
}
try (ReconVersionManager versionManager = createManager(
@@ -158,6 +179,8 @@ public void testClasspathScanDiscoversUpgradeActions()
throws Exception {
ReconUpgradeAction taskStatusAction =
versionManager.getUpgradeActionsForTesting()
.get(ReconVersion.TASK_STATUS_STATISTICS);
assertInstanceOf(ReconTaskStatusTableUpgradeAction.class,
taskStatusAction);
+ assertInstanceOf(ReplicaHistoryDBUpgradeAction.class,
+
versionManager.getUpgradeActionsForTesting().get(ReconVersion.REPLICA_HISTORY_DB));
}
}
@@ -188,6 +211,107 @@ public void testFinalizeRunsSuppliedUpgradeAction()
throws Exception {
}
}
+ @Test
+ void testReplicaHistoryMigrationBeforeVersionFinalization() throws Exception
{
+ Injector reconInjector = createReplicaHistoryInjector();
+ try (DBStore legacyStore =
reconInjector.getInstance(ReconDBProvider.class).getDbStore();
+ ReconReplicaHistoryDBProvider historyProvider =
reconInjector.getInstance(ReconReplicaHistoryDBProvider.class);
+ ReconVersionManager versionManager = createManager(
+
ReconVersion.UNHEALTHY_CONTAINERS_STATE_CONTAINER_ID_INDEX.serialize(), new
ReconUpgradeActionProvider());
+ MockedStatic<ReconGuiceServletContextListener> listener =
mockStatic(ReconGuiceServletContextListener.class)) {
+ ContainerReplicaHistoryList history = legacyReplicaHistory();
+ REPLICA_HISTORY_V2.getTable(legacyStore).put(1L, history);
+ assertTrue(historyProvider.getReplicaHistoryTable().isEmpty());
+
listener.when(ReconGuiceServletContextListener::getGlobalInjector).thenReturn(reconInjector);
+
+ versionManager.finalizeUpgrade();
+
+ assertEquals(history.toProto(),
historyProvider.getReplicaHistoryTable().get(1L).toProto());
+ assertEquals(history.toProto(),
REPLICA_HISTORY_V2.getTable(legacyStore).get(1L).toProto());
+ assertEquals(ReconVersion.REPLICA_HISTORY_DB,
versionManager.getApparentVersion());
+ assertEquals(ReconVersion.REPLICA_HISTORY_DB.serialize(),
versionManager.getPersistedApparentVersion());
+ }
+ }
+
+ @Test
+ void testReplicaHistoryMigrationFailureDoesNotAdvanceVersion() throws
Exception {
+ Injector reconInjector = mock(Injector.class);
+ ReconDBProvider sourceProvider = mock(ReconDBProvider.class);
+ when(sourceProvider.getDbStore()).thenReturn(mock(DBStore.class));
+ ReconReplicaHistoryDBProvider historyProvider =
mock(ReconReplicaHistoryDBProvider.class);
+
when(reconInjector.getInstance(ReconDBProvider.class)).thenReturn(sourceProvider);
+
when(reconInjector.getInstance(ReconReplicaHistoryDBProvider.class)).thenReturn(historyProvider);
+ doThrow(new IOException("Interrupted replica history
migration")).doNothing()
+ .when(historyProvider).migrateReplicaHistory(any());
+
+ try (ReconVersionManager versionManager = createManager(
+
ReconVersion.UNHEALTHY_CONTAINERS_STATE_CONTAINER_ID_INDEX.serialize(), new
ReconUpgradeActionProvider());
+ MockedStatic<ReconGuiceServletContextListener> listener =
mockStatic(ReconGuiceServletContextListener.class)) {
+
listener.when(ReconGuiceServletContextListener::getGlobalInjector).thenReturn(reconInjector);
+ UpgradeException thrown = assertThrows(UpgradeException.class,
versionManager::finalizeUpgrade);
+
assertEquals(UpgradeException.ResultCodes.FINALIZE_UPGRADE_ACTION_FAILED,
thrown.getResult());
+ assertEquals(ReconVersion.UNHEALTHY_CONTAINERS_STATE_CONTAINER_ID_INDEX,
versionManager.getApparentVersion());
+
assertEquals(ReconVersion.UNHEALTHY_CONTAINERS_STATE_CONTAINER_ID_INDEX.serialize(),
+ versionManager.getPersistedApparentVersion());
+
+ versionManager.finalizeUpgrade();
+ assertEquals(ReconVersion.REPLICA_HISTORY_DB.serialize(),
versionManager.getPersistedApparentVersion());
+ verify(historyProvider, times(2)).migrateReplicaHistory(any());
+ }
+ }
+
+ @Test
+ void testReplicaHistoryMigrationNotRepeatedAfterVersionPersistenceFailure()
throws Exception {
+ Injector reconInjector = createReplicaHistoryInjector();
+ ReconDBProvider sourceProvider =
spy(reconInjector.getInstance(ReconDBProvider.class));
+ DBStore sourceStore = spy(sourceProvider.getDbStore());
+ when(sourceProvider.getDbStore()).thenReturn(sourceStore);
+ Injector migrationInjector = mock(Injector.class);
+
when(migrationInjector.getInstance(ReconDBProvider.class)).thenReturn(sourceProvider);
+
+ try (DBStore legacyStore = sourceStore;
+ ReconReplicaHistoryDBProvider historyProvider =
reconInjector.getInstance(ReconReplicaHistoryDBProvider.class);
+ ReconVersionManager versionManager = spy(createManager(
+
ReconVersion.UNHEALTHY_CONTAINERS_STATE_CONTAINER_ID_INDEX.serialize(), new
ReconUpgradeActionProvider()));
+ MockedStatic<ReconGuiceServletContextListener> listener =
mockStatic(ReconGuiceServletContextListener.class)) {
+
when(migrationInjector.getInstance(ReconReplicaHistoryDBProvider.class)).thenReturn(historyProvider);
+
listener.when(ReconGuiceServletContextListener::getGlobalInjector).thenReturn(migrationInjector);
+ ContainerReplicaHistoryList history = legacyReplicaHistory();
+ REPLICA_HISTORY_V2.getTable(legacyStore).put(1L, history);
+ doThrow(new IOException("Version persistence failed")).doCallRealMethod()
+
.when(versionManager).persistApparentVersion(ReconVersion.REPLICA_HISTORY_DB);
+
+ UpgradeException thrown = assertThrows(UpgradeException.class,
versionManager::finalizeUpgrade);
+
assertEquals(UpgradeException.ResultCodes.APPARENT_VERSION_UPDATE_FAILED,
thrown.getResult());
+ assertEquals(history.toProto(),
historyProvider.getReplicaHistoryTable().get(1L).toProto());
+
assertEquals(ReconVersion.UNHEALTHY_CONTAINERS_STATE_CONTAINER_ID_INDEX.serialize(),
+ versionManager.getPersistedApparentVersion());
+
+ clearInvocations(sourceStore);
+ versionManager.finalizeUpgrade();
+ verifyNoInteractions(sourceStore);
+ assertEquals(ReconVersion.REPLICA_HISTORY_DB.serialize(),
versionManager.getPersistedApparentVersion());
+ }
+ }
+
+ private Injector createReplicaHistoryInjector() throws IOException {
+ OzoneConfiguration configuration = new OzoneConfiguration();
+ configuration.set(OZONE_RECON_DB_DIR,
Files.createDirectory(tempDir.resolve("replica-history")).toString());
+ return Guice.createInjector(new AbstractModule() {
+ @Override
+ protected void configure() {
+ bind(OzoneConfiguration.class).toInstance(configuration);
+ bind(ReconDBProvider.class).in(Singleton.class);
+ bind(ReconReplicaHistoryDBProvider.class).in(Singleton.class);
+ }
+ });
+ }
+
+ private static ContainerReplicaHistoryList legacyReplicaHistory() {
+ return new ContainerReplicaHistoryList(Collections.singletonList(new
ContainerReplicaHistory(
+ DatanodeID.randomID(), 10L, 20L, 30L, "CLOSED",
ContainerChecksums.of(1234L, 0L))));
+ }
+
@Override
@Test
public void testUpgradeActionFailureAbortsFinalize() throws Exception {
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]