diff --git a/hadoop-ozone/recon-codegen/src/main/java/org/hadoop/ozone/recon/codegen/SqlDbUtils.java b/hadoop-ozone/recon-codegen/src/main/java/org/hadoop/ozone/recon/codegen/SqlDbUtils.java index a3675dcbe771..7fb98be4a1da 100644 --- a/hadoop-ozone/recon-codegen/src/main/java/org/hadoop/ozone/recon/codegen/SqlDbUtils.java +++ b/hadoop-ozone/recon-codegen/src/main/java/org/hadoop/ozone/recon/codegen/SqlDbUtils.java @@ -24,8 +24,11 @@ import java.io.OutputStream; import java.sql.Connection; import java.sql.DriverManager; +import java.sql.ResultSet; import java.sql.SQLException; import java.util.function.BiPredicate; +import java.util.ArrayList; +import java.util.List; import org.jooq.exception.DataAccessException; import org.jooq.impl.DSL; @@ -95,4 +98,23 @@ public void write(int b) throws IOException { LOG.info("{} table already exists, skipping creation.", tableName); return true; }; + + /** + * Utility method to list all user-defined tables in the database. + * + * @param connection The database connection to use. + * @return A list of table names (user-defined tables only). + * @throws SQLException If there is an issue accessing the database metadata. + */ + public static List listAllTables(Connection connection) throws SQLException { + List tableNames = new ArrayList<>(); + try (ResultSet resultSet = connection.getMetaData().getTables(null, null, null, new String[]{"TABLE"})) { + while (resultSet.next()) { + String tableName = resultSet.getString("TABLE_NAME"); + tableNames.add(tableName); + } + } + LOG.debug("Found {} user-defined tables in the database: {}", tableNames.size(), tableNames); + return tableNames; + } } diff --git a/hadoop-ozone/recon-codegen/src/main/java/org/hadoop/ozone/recon/schema/SchemaVersionTableDefinition.java b/hadoop-ozone/recon-codegen/src/main/java/org/hadoop/ozone/recon/schema/SchemaVersionTableDefinition.java index f7e538f31ad8..6545a5390387 100644 --- a/hadoop-ozone/recon-codegen/src/main/java/org/hadoop/ozone/recon/schema/SchemaVersionTableDefinition.java +++ b/hadoop-ozone/recon-codegen/src/main/java/org/hadoop/ozone/recon/schema/SchemaVersionTableDefinition.java @@ -24,12 +24,16 @@ import org.jooq.DSLContext; import org.jooq.impl.DSL; import org.jooq.impl.SQLDataType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; import static org.hadoop.ozone.recon.codegen.SqlDbUtils.TABLE_EXISTS_CHECK; +import static org.hadoop.ozone.recon.codegen.SqlDbUtils.listAllTables; +import static org.jooq.impl.DSL.name; /** * Class for managing the schema of the SchemaVersion table. @@ -37,9 +41,11 @@ @Singleton public class SchemaVersionTableDefinition implements ReconSchemaDefinition { + private static final Logger LOG = LoggerFactory.getLogger(SchemaVersionTableDefinition.class); + public static final String SCHEMA_VERSION_TABLE_NAME = "RECON_SCHEMA_VERSION"; private final DataSource dataSource; - private DSLContext dslContext; + private int latestSLV; @Inject public SchemaVersionTableDefinition(DataSource dataSource) { @@ -48,22 +54,56 @@ public SchemaVersionTableDefinition(DataSource dataSource) { @Override public void initializeSchema() throws SQLException { - Connection conn = dataSource.getConnection(); - dslContext = DSL.using(conn); + try (Connection conn = dataSource.getConnection()) { + DSLContext localDslContext = DSL.using(conn); + + if (!TABLE_EXISTS_CHECK.test(conn, SCHEMA_VERSION_TABLE_NAME)) { + // If the RECON_SCHEMA_VERSION table does not exist, check for other tables + // to identify if it is a fresh install + boolean isFreshInstall = listAllTables(conn).isEmpty(); + createSchemaVersionTable(localDslContext); - if (!TABLE_EXISTS_CHECK.test(conn, SCHEMA_VERSION_TABLE_NAME)) { - createSchemaVersionTable(); + if (isFreshInstall) { + // Fresh install: Set the SLV to the latest version + insertInitialSLV(localDslContext, latestSLV); + } + } } } /** * Create the Schema Version table. + * + * @param dslContext The DSLContext to use for the operation. */ - private void createSchemaVersionTable() throws SQLException { + private void createSchemaVersionTable(DSLContext dslContext) { dslContext.createTableIfNotExists(SCHEMA_VERSION_TABLE_NAME) .column("version_number", SQLDataType.INTEGER.nullable(false)) .column("applied_on", SQLDataType.TIMESTAMP.defaultValue(DSL.currentTimestamp())) .execute(); } + /** + * Inserts the initial SLV into the Schema Version table. + * + * @param dslContext The DSLContext to use for the operation. + * @param slv The initial SLV value. + */ + private void insertInitialSLV(DSLContext dslContext, int slv) { + dslContext.insertInto(DSL.table(SCHEMA_VERSION_TABLE_NAME)) + .columns(DSL.field(name("version_number")), + DSL.field(name("applied_on"))) + .values(slv, DSL.currentTimestamp()) + .execute(); + LOG.info("Inserted initial SLV '{}' into SchemaVersion table.", slv); + } + + /** + * Set the latest SLV. + * + * @param slv The latest Software Layout Version. + */ + public void setLatestSLV(int slv) { + this.latestSLV = slv; + } } diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconSchemaManager.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconSchemaManager.java index 253e37d75abe..66f1212d3f84 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconSchemaManager.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconSchemaManager.java @@ -22,7 +22,9 @@ import java.util.HashSet; import java.util.Set; +import org.apache.hadoop.ozone.recon.upgrade.ReconLayoutFeature; import org.hadoop.ozone.recon.schema.ReconSchemaDefinition; +import org.hadoop.ozone.recon.schema.SchemaVersionTableDefinition; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -45,13 +47,46 @@ public ReconSchemaManager(Set reconSchemaDefinitions) { @VisibleForTesting public void createReconSchema() { - reconSchemaDefinitions.forEach(reconSchemaDefinition -> { - try { - reconSchemaDefinition.initializeSchema(); - } catch (SQLException e) { - LOG.error("Error creating Recon schema {}.", - reconSchemaDefinition.getClass().getSimpleName(), e); - } - }); + // Calculate the latest SLV from ReconLayoutFeature + int latestSLV = calculateLatestSLV(); + + try { + // Initialize the schema version table first + reconSchemaDefinitions.stream() + .filter(SchemaVersionTableDefinition.class::isInstance) + .findFirst() + .ifPresent(schemaDefinition -> { + SchemaVersionTableDefinition schemaVersionTable = (SchemaVersionTableDefinition) schemaDefinition; + schemaVersionTable.setLatestSLV(latestSLV); + try { + schemaVersionTable.initializeSchema(); + } catch (SQLException e) { + LOG.error("Error initializing SchemaVersionTableDefinition.", e); + } + }); + + // Initialize all other tables + reconSchemaDefinitions.stream() + .filter(definition -> !(definition instanceof SchemaVersionTableDefinition)) + .forEach(definition -> { + try { + definition.initializeSchema(); + } catch (SQLException e) { + LOG.error("Error initializing schema: {}.", definition.getClass().getSimpleName(), e); + } + }); + + } catch (Exception e) { + LOG.error("Error creating Recon schema.", e); + } + } + + /** + * Calculate the latest SLV by iterating over ReconLayoutFeature. + * + * @return The latest SLV. + */ + private int calculateLatestSLV() { + return ReconLayoutFeature.determineSLV(); } } diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReconLayoutFeature.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReconLayoutFeature.java index 96969c9f3d22..52739efe1a6d 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReconLayoutFeature.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReconLayoutFeature.java @@ -21,6 +21,7 @@ import org.reflections.Reflections; +import java.util.Arrays; import java.util.EnumMap; import java.util.Optional; import java.util.Set; @@ -92,6 +93,17 @@ public static void registerUpgradeActions() { } } + /** + * Determines the Software Layout Version (SLV) based on the latest feature version. + * @return The Software Layout Version (SLV). + */ + public static int determineSLV() { + return Arrays.stream(ReconLayoutFeature.values()) + .mapToInt(ReconLayoutFeature::getVersion) + .max() + .orElse(0); // Default to 0 if no features are defined + } + /** * Returns the list of all layout feature values. * diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReconLayoutVersionManager.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReconLayoutVersionManager.java index a595b6a0c107..050127fb751f 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReconLayoutVersionManager.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReconLayoutVersionManager.java @@ -73,7 +73,7 @@ private int determineSLV() { return Arrays.stream(ReconLayoutFeature.values()) .mapToInt(ReconLayoutFeature::getVersion) .max() - .orElse(0); // Default to 0 if no features are defined + .orElse(0); } /** diff --git a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/persistence/AbstractReconSqlDBTest.java b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/persistence/AbstractReconSqlDBTest.java index d007fbb1cf7b..efd32693ee9c 100644 --- a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/persistence/AbstractReconSqlDBTest.java +++ b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/persistence/AbstractReconSqlDBTest.java @@ -126,6 +126,10 @@ protected Connection getConnection() throws SQLException { return injector.getInstance(DataSource.class).getConnection(); } + protected DataSource getDataSource() { + return injector.getInstance(DataSource.class); + } + protected DSLContext getDslContext() { return dslContext; } diff --git a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/persistence/TestSchemaVersionTableDefinition.java b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/persistence/TestSchemaVersionTableDefinition.java index ab3c4f8e6ecb..fad421a929d0 100644 --- a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/persistence/TestSchemaVersionTableDefinition.java +++ b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/persistence/TestSchemaVersionTableDefinition.java @@ -19,9 +19,14 @@ package org.apache.hadoop.ozone.recon.persistence; +import static org.hadoop.ozone.recon.codegen.SqlDbUtils.TABLE_EXISTS_CHECK; +import static org.hadoop.ozone.recon.codegen.SqlDbUtils.listAllTables; +import static org.hadoop.ozone.recon.schema.ContainerSchemaDefinition.UNHEALTHY_CONTAINERS_TABLE_NAME; import static org.hadoop.ozone.recon.schema.SchemaVersionTableDefinition.SCHEMA_VERSION_TABLE_NAME; +import static org.hadoop.ozone.recon.schema.StatsSchemaDefinition.GLOBAL_STATS_TABLE_NAME; import static org.jooq.impl.DSL.name; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; import java.sql.Connection; import java.sql.DatabaseMetaData; @@ -34,9 +39,14 @@ import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.Pair; +import org.apache.hadoop.ozone.recon.ReconContext; +import org.apache.hadoop.ozone.recon.ReconSchemaVersionTableManager; +import org.apache.hadoop.ozone.recon.upgrade.ReconLayoutVersionManager; +import org.hadoop.ozone.recon.schema.SchemaVersionTableDefinition; import org.jooq.DSLContext; import org.jooq.Record1; import org.jooq.impl.DSL; +import org.jooq.impl.SQLDataType; import org.junit.jupiter.api.Test; /** @@ -72,10 +82,18 @@ public void testSchemaVersionTableCreation() throws Exception { assertEquals(expectedPairs, actualPairs, "Column definitions do not match expected values."); } + @Test public void testSchemaVersionCRUDOperations() throws SQLException { Connection connection = getConnection(); + // Ensure no tables exist initially, simulating a fresh installation + dropAllTables(connection); + + // Create the schema version table + createSchemaVersionTable(connection); + + DSLContext dslContext = DSL.using(connection); DatabaseMetaData metaData = connection.getMetaData(); ResultSet resultSet = metaData.getTables(null, null, SCHEMA_VERSION_TABLE_NAME, null); @@ -85,8 +103,6 @@ public void testSchemaVersionCRUDOperations() throws SQLException { resultSet.getString("TABLE_NAME")); } - DSLContext dslContext = DSL.using(connection); - // Insert a new version record dslContext.insertInto(DSL.table(SCHEMA_VERSION_TABLE_NAME)) .columns(DSL.field(name("version_number")), DSL.field(name("applied_on"))) @@ -120,4 +136,176 @@ public void testSchemaVersionCRUDOperations() throws SQLException { int count = dslContext.fetchCount(DSL.table(SCHEMA_VERSION_TABLE_NAME)); assertEquals(0, count, "The table should be empty after deletion."); } + + /** + * Scenario: + * - A fresh installation of the cluster, where no tables exist initially. + * - All tables, including the schema version table, are created during initialization. + * + * Expected Outcome: + * - The schema version table is created during initialization. + * - The MLV is set to the latest SLV (Software Layout Version), indicating the schema is up-to-date. + * - No upgrade actions are triggered as all tables are already at the latest version. + */ + @Test + public void testFreshInstallScenario() throws Exception { + Connection connection = getConnection(); + + // Ensure no tables exist initially, simulating a fresh installation + dropAllTables(connection); + + // Initialize the schema + SchemaVersionTableDefinition schemaVersionTable = new SchemaVersionTableDefinition(getDataSource()); + schemaVersionTable.setLatestSLV(3); // Assuming the latest SLV = 3 + schemaVersionTable.initializeSchema(); + + // Verify that the SchemaVersionTable is created + boolean tableExists = TABLE_EXISTS_CHECK.test(connection, SCHEMA_VERSION_TABLE_NAME); + assertEquals(true, tableExists, "The Schema Version Table should be created."); + + // Initialize ReconSchemaVersionTableManager and ReconLayoutVersionManager + ReconSchemaVersionTableManager schemaVersionTableManager = new ReconSchemaVersionTableManager(getDataSource()); + ReconLayoutVersionManager layoutVersionManager = + new ReconLayoutVersionManager(schemaVersionTableManager, mock(ReconContext.class)); + + // Fetch and verify the current MLV + int mlv = layoutVersionManager.getCurrentMLV(); + assertEquals(3, mlv, "For a fresh install, MLV should be set to the latest SLV value."); + } + + /** + * Scenario: + * - The cluster was running without a schema version framework in an older version. + * - After the upgrade, the schema version table is introduced while other tables already exist. + * + * Expected Outcome: + * - The schema version table is created during initialization. + * - The MLV is set to -1, indicating the starting point of the schema version framework. + * - Ensures only necessary upgrades are executed, avoiding redundant updates. + */ + @Test + public void testPreUpgradedClusterScenario() throws Exception { + Connection connection = getConnection(); + + // Simulate the cluster by creating other tables but not the schema version table + dropTable(connection, SCHEMA_VERSION_TABLE_NAME); + if (listAllTables(connection).isEmpty()) { + createTable(connection, GLOBAL_STATS_TABLE_NAME); + createTable(connection, UNHEALTHY_CONTAINERS_TABLE_NAME); + } + + // Initialize the schema + SchemaVersionTableDefinition schemaVersionTable = new SchemaVersionTableDefinition(getDataSource()); + schemaVersionTable.initializeSchema(); + + // Verify SchemaVersionTable is created + boolean tableExists = TABLE_EXISTS_CHECK.test(connection, SCHEMA_VERSION_TABLE_NAME); + assertEquals(true, tableExists, "The Schema Version Table should be created."); + + // Initialize ReconSchemaVersionTableManager and ReconLayoutVersionManager + ReconSchemaVersionTableManager schemaVersionTableManager = new ReconSchemaVersionTableManager(getDataSource()); + ReconLayoutVersionManager layoutVersionManager = + new ReconLayoutVersionManager(schemaVersionTableManager, mock(ReconContext.class)); + + // Fetch and verify the current MLV + int mlv = layoutVersionManager.getCurrentMLV(); + assertEquals(-1, mlv, "For a pre-upgraded cluster, MLV should be set to -1."); + } + + /*** + * Scenario: + * - This simulates a cluster where the schema version table already exists, + * indicating the schema version framework is in place. + * - The schema version table contains a previously finalized Metadata Layout Version (MLV). + * + * Expected Outcome: + * - The MLV stored in the schema version table (2) is correctly read by the ReconLayoutVersionManager. + * - The MLV is retained and not overridden by the SLV value (3) during schema initialization. + * - This ensures no unnecessary upgrades are triggered and the existing MLV remains consistent. + */ + @Test + public void testUpgradedClusterScenario() throws Exception { + Connection connection = getConnection(); + + // Simulate a cluster with an existing schema version framework + dropAllTables(connection); // Ensure no previous data exists + if (listAllTables(connection).isEmpty()) { + // Create necessary tables to simulate the cluster state + createTable(connection, GLOBAL_STATS_TABLE_NAME); + createTable(connection, UNHEALTHY_CONTAINERS_TABLE_NAME); + // Create the schema version table + createSchemaVersionTable(connection); + } + + // Insert a single existing MLV (e.g., version 2) into the Schema Version Table + DSLContext dslContext = DSL.using(connection); + dslContext.insertInto(DSL.table(SCHEMA_VERSION_TABLE_NAME)) + .columns(DSL.field(name("version_number")), + DSL.field(name("applied_on"))) + .values(2, new Timestamp(System.currentTimeMillis())) + .execute(); + + // Initialize the schema + SchemaVersionTableDefinition schemaVersionTable = new SchemaVersionTableDefinition(getDataSource()); + schemaVersionTable.setLatestSLV(3); // Assuming the latest SLV = 3 + schemaVersionTable.initializeSchema(); + + // Initialize managers to interact with schema version framework + ReconSchemaVersionTableManager schemaVersionTableManager = new ReconSchemaVersionTableManager(getDataSource()); + ReconLayoutVersionManager layoutVersionManager = + new ReconLayoutVersionManager(schemaVersionTableManager, mock(ReconContext.class)); + + // Fetch and verify the current MLV stored in the database + int mlv = layoutVersionManager.getCurrentMLV(); + + // Assert that the MLV stored in the DB is retained and not overridden by the SLV value + // when running initializeSchema() before upgrade takes place + assertEquals(2, mlv, "For a cluster with an existing schema version framework, " + + "the MLV should match the value stored in the DB."); + } + + /** + * Utility method to create the schema version table. + */ + private void createSchemaVersionTable(Connection connection) throws SQLException { + DSLContext dslContext = DSL.using(connection); + dslContext.createTableIfNotExists(SCHEMA_VERSION_TABLE_NAME) + .column("version_number", SQLDataType.INTEGER.nullable(false)) + .column("applied_on", SQLDataType.TIMESTAMP.defaultValue(DSL.currentTimestamp())) + .execute(); + } + + /** + * Utility method to create a mock table. + */ + private void createTable(Connection connection, String tableName) throws SQLException { + DSLContext dslContext = DSL.using(connection); + dslContext.createTableIfNotExists(tableName) + .column("id", SQLDataType.INTEGER.nullable(false)) + .column("data", SQLDataType.VARCHAR(255)) + .execute(); + } + + /** + * Utility method to drop all tables (simulating a fresh environment). + */ + private void dropAllTables(Connection connection) throws SQLException { + DSLContext dslContext = DSL.using(connection); + List tableNames = listAllTables(connection); + if (tableNames.isEmpty()) { + return; + } + for (String tableName : tableNames) { + dslContext.dropTableIfExists(tableName).execute(); + } + } + + /** + * Utility method to drop one table. + */ + private void dropTable(Connection connection, String tableName) throws SQLException { + DSLContext dslContext = DSL.using(connection); + dslContext.dropTableIfExists(tableName).execute(); + } + }