diff --git a/hadoop-ozone/recon-codegen/src/main/java/org/hadoop/ozone/recon/codegen/ReconSchemaGenerationModule.java b/hadoop-ozone/recon-codegen/src/main/java/org/hadoop/ozone/recon/codegen/ReconSchemaGenerationModule.java index 8272c2bd6da..d59ab8acd6b 100644 --- a/hadoop-ozone/recon-codegen/src/main/java/org/hadoop/ozone/recon/codegen/ReconSchemaGenerationModule.java +++ b/hadoop-ozone/recon-codegen/src/main/java/org/hadoop/ozone/recon/codegen/ReconSchemaGenerationModule.java @@ -22,6 +22,7 @@ import org.hadoop.ozone.recon.schema.ReconSchemaDefinition; import org.hadoop.ozone.recon.schema.StatsSchemaDefinition; import org.hadoop.ozone.recon.schema.UtilizationSchemaDefinition; +import org.hadoop.ozone.recon.schema.SchemaVersionTableDefinition; import com.google.inject.AbstractModule; import com.google.inject.multibindings.Multibinder; @@ -40,5 +41,6 @@ protected void configure() { schemaBinder.addBinding().to(ContainerSchemaDefinition.class); schemaBinder.addBinding().to(ReconTaskSchemaDefinition.class); schemaBinder.addBinding().to(StatsSchemaDefinition.class); + schemaBinder.addBinding().to(SchemaVersionTableDefinition.class); } } 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 new file mode 100644 index 00000000000..f7e538f31ad --- /dev/null +++ b/hadoop-ozone/recon-codegen/src/main/java/org/hadoop/ozone/recon/schema/SchemaVersionTableDefinition.java @@ -0,0 +1,69 @@ +/* + * 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.hadoop.ozone.recon.schema; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import org.jooq.DSLContext; +import org.jooq.impl.DSL; +import org.jooq.impl.SQLDataType; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; + +import static org.hadoop.ozone.recon.codegen.SqlDbUtils.TABLE_EXISTS_CHECK; + +/** + * Class for managing the schema of the SchemaVersion table. + */ +@Singleton +public class SchemaVersionTableDefinition implements ReconSchemaDefinition { + + public static final String SCHEMA_VERSION_TABLE_NAME = "RECON_SCHEMA_VERSION"; + private final DataSource dataSource; + private DSLContext dslContext; + + @Inject + public SchemaVersionTableDefinition(DataSource dataSource) { + this.dataSource = dataSource; + } + + @Override + public void initializeSchema() throws SQLException { + Connection conn = dataSource.getConnection(); + dslContext = DSL.using(conn); + + if (!TABLE_EXISTS_CHECK.test(conn, SCHEMA_VERSION_TABLE_NAME)) { + createSchemaVersionTable(); + } + } + + /** + * Create the Schema Version table. + */ + private void createSchemaVersionTable() throws SQLException { + dslContext.createTableIfNotExists(SCHEMA_VERSION_TABLE_NAME) + .column("version_number", SQLDataType.INTEGER.nullable(false)) + .column("applied_on", SQLDataType.TIMESTAMP.defaultValue(DSL.currentTimestamp())) + .execute(); + } + +} diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconContext.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconContext.java index c9875cb826b..a98603a7e9c 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconContext.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconContext.java @@ -128,7 +128,10 @@ public enum ErrorCode { Arrays.asList("Overview (OM Data)", "OM DB Insights")), GET_SCM_DB_SNAPSHOT_FAILED( "SCM DB Snapshot sync failed !!!", - Arrays.asList("Containers", "Pipelines")); + Arrays.asList("Containers", "Pipelines")), + UPGRADE_FAILURE( + "Schema upgrade failed. Recon encountered an issue while finalizing the layout upgrade.", + Arrays.asList("Recon startup", "Metadata Layout Version")); private final String message; private final List impacts; diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconSchemaVersionTableManager.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconSchemaVersionTableManager.java new file mode 100644 index 00000000000..d7c3c65f2c1 --- /dev/null +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/ReconSchemaVersionTableManager.java @@ -0,0 +1,108 @@ +/* + * 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; + +import com.google.inject.Inject; +import org.jooq.DSLContext; +import org.jooq.impl.DSL; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.sql.DataSource; +import java.sql.SQLException; + +import static org.jooq.impl.DSL.name; + +/** + * Manager for handling the Recon Schema Version table. + * This class provides methods to get and update the current schema version. + */ +public class ReconSchemaVersionTableManager { + + private static final Logger LOG = LoggerFactory.getLogger(ReconSchemaVersionTableManager.class); + public static final String RECON_SCHEMA_VERSION_TABLE_NAME = "RECON_SCHEMA_VERSION"; + private final DSLContext dslContext; + private final DataSource dataSource; + + @Inject + public ReconSchemaVersionTableManager(DataSource dataSource) throws SQLException { + this.dataSource = dataSource; + this.dslContext = DSL.using(dataSource.getConnection()); + } + + /** + * Get the current schema version from the RECON_SCHEMA_VERSION table. + * If the table is empty, or if it does not exist, it will return 0. + * @return The current schema version. + */ + public int getCurrentSchemaVersion() throws SQLException { + try { + return dslContext.select(DSL.field(name("version_number"))) + .from(DSL.table(RECON_SCHEMA_VERSION_TABLE_NAME)) + .fetchOptional() + .map(record -> record.get( + DSL.field(name("version_number"), Integer.class))) + .orElse(-1); // Return -1 if no version is found + } catch (Exception e) { + LOG.error("Failed to fetch the current schema version.", e); + throw new SQLException("Unable to read schema version from the table.", e); + } + } + + /** + * Update the schema version in the RECON_SCHEMA_VERSION table after all tables are upgraded. + * + * @param newVersion The new version to set. + */ + public void updateSchemaVersion(int newVersion) throws SQLException { + try { + boolean recordExists = dslContext.fetchExists(dslContext.selectOne() + .from(DSL.table(RECON_SCHEMA_VERSION_TABLE_NAME))); + + if (recordExists) { + // Update the existing schema version record + dslContext.update(DSL.table(RECON_SCHEMA_VERSION_TABLE_NAME)) + .set(DSL.field(name("version_number")), newVersion) + .set(DSL.field(name("applied_on")), DSL.currentTimestamp()) + .execute(); + LOG.info("Updated schema version to '{}'.", newVersion); + } else { + // Insert a new schema version record + dslContext.insertInto(DSL.table(RECON_SCHEMA_VERSION_TABLE_NAME)) + .columns(DSL.field(name("version_number")), + DSL.field(name("applied_on"))) + .values(newVersion, DSL.currentTimestamp()) + .execute(); + LOG.info("Inserted new schema version '{}'.", newVersion); + } + } catch (Exception e) { + LOG.error("Failed to update schema version to '{}'.", newVersion, e); + throw new SQLException("Unable to update schema version in the table.", e); + } + } + + /** + * Provides the data source used by this manager. + * @return The DataSource instance. + */ + public DataSource getDataSource() { + return dataSource; + } +} 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 3295eb4524c..7c9564c23b1 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 @@ -42,6 +42,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.upgrade.ReconLayoutVersionManager; import org.apache.hadoop.ozone.util.OzoneNetUtils; import org.apache.hadoop.ozone.util.OzoneVersionInfo; import org.apache.hadoop.ozone.util.ShutdownHookManager; @@ -105,6 +106,7 @@ public Void call() throws Exception { ReconServer.class, originalArgs, LOG, configuration); ConfigurationProvider.setConfiguration(configuration); + injector = Guice.createInjector(new ReconControllerModule(), new ReconRestServletModule(configuration), new ReconSchemaGenerationModule()); @@ -136,8 +138,11 @@ public Void call() throws Exception { this.reconNamespaceSummaryManager = injector.getInstance(ReconNamespaceSummaryManager.class); + ReconContext reconContext = injector.getInstance(ReconContext.class); + ReconSchemaManager reconSchemaManager = injector.getInstance(ReconSchemaManager.class); + LOG.info("Creating Recon Schema."); reconSchemaManager.createReconSchema(); LOG.debug("Recon schema creation done."); @@ -153,6 +158,15 @@ public Void call() throws Exception { this.reconTaskStatusMetrics = injector.getInstance(ReconTaskStatusMetrics.class); + // Handle Recon Schema Versioning + ReconSchemaVersionTableManager versionTableManager = + injector.getInstance(ReconSchemaVersionTableManager.class); + + ReconLayoutVersionManager layoutVersionManager = + new ReconLayoutVersionManager(versionTableManager, reconContext); + // Run the upgrade framework to finalize layout features if needed + layoutVersionManager.finalizeLayoutFeatures(); + LOG.info("Initializing support of Recon Features..."); FeatureProvider.initFeatureSupport(configuration); 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 new file mode 100644 index 00000000000..96969c9f3d2 --- /dev/null +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReconLayoutFeature.java @@ -0,0 +1,103 @@ +/* + * 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 org.reflections.Reflections; + +import java.util.EnumMap; +import java.util.Optional; +import java.util.Set; + +/** + * Enum representing Recon layout features with their version, description, + * and associated upgrade action to be executed during an upgrade. + */ +public enum ReconLayoutFeature { + // Represents the starting point for Recon's layout versioning system. + INITIAL_VERSION(0, "Recon Layout Versioning Introduction"); + + private final int version; + private final String description; + private final EnumMap actions = + new EnumMap<>(ReconUpgradeAction.UpgradeActionType.class); + + ReconLayoutFeature(final int version, String description) { + this.version = version; + this.description = description; + } + + public int getVersion() { + return version; + } + + public String getDescription() { + return description; + } + + /** + * Retrieves the upgrade action for the specified {@link ReconUpgradeAction.UpgradeActionType}. + * + * @param type The type of the upgrade action (e.g., FINALIZE). + * @return An {@link Optional} containing the upgrade action if present. + */ + public Optional getAction(ReconUpgradeAction.UpgradeActionType type) { + return Optional.ofNullable(actions.get(type)); + } + + /** + * Associates a given upgrade action with a specific upgrade phase for this feature. + * + * @param type The phase/type of the upgrade action. + * @param action The upgrade action to associate with this feature. + */ + public void addAction(ReconUpgradeAction.UpgradeActionType type, ReconUpgradeAction action) { + actions.put(type, action); + } + + /** + * Scans the classpath for all classes annotated with {@link UpgradeActionRecon} + * and registers their upgrade actions for the corresponding feature and phase. + * This method dynamically loads and registers all upgrade actions based on their + * annotations. + */ + public static void registerUpgradeActions() { + Reflections reflections = new Reflections("org.apache.hadoop.ozone.recon.upgrade"); + Set> actionClasses = reflections.getTypesAnnotatedWith(UpgradeActionRecon.class); + + for (Class actionClass : actionClasses) { + try { + ReconUpgradeAction action = (ReconUpgradeAction) actionClass.getDeclaredConstructor().newInstance(); + UpgradeActionRecon annotation = actionClass.getAnnotation(UpgradeActionRecon.class); + annotation.feature().addAction(annotation.type(), action); + } catch (Exception e) { + throw new RuntimeException("Failed to register upgrade action: " + actionClass.getSimpleName(), e); + } + } + } + + /** + * Returns the list of all layout feature values. + * + * @return An array of all {@link ReconLayoutFeature} values. + */ + public static ReconLayoutFeature[] getValues() { + return ReconLayoutFeature.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 new file mode 100644 index 00000000000..b646f6d9a86 --- /dev/null +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReconLayoutVersionManager.java @@ -0,0 +1,141 @@ +/** + * 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 org.apache.hadoop.ozone.recon.ReconContext; +import org.apache.hadoop.ozone.recon.ReconSchemaVersionTableManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.SQLException; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; + +/** + * ReconLayoutVersionManager is responsible for managing the layout version of the Recon service. + * It determines the current Metadata Layout Version (MLV) and Software Layout Version (SLV) of the + * Recon service, and finalizes the layout features that need to be upgraded. + */ +public class ReconLayoutVersionManager { + + private static final Logger LOG = LoggerFactory.getLogger(ReconLayoutVersionManager.class); + + private final ReconSchemaVersionTableManager schemaVersionTableManager; + private final ReconContext reconContext; + + // Metadata Layout Version (MLV) of the Recon Metadata on disk + private int currentMLV; + + public ReconLayoutVersionManager(ReconSchemaVersionTableManager schemaVersionTableManager, + ReconContext reconContext) + throws SQLException { + this.schemaVersionTableManager = schemaVersionTableManager; + this.currentMLV = determineMLV(); + this.reconContext = reconContext; + ReconLayoutFeature.registerUpgradeActions(); // Register actions via annotation + } + + /** + * Determines the current Metadata Layout Version (MLV) from the version table. + * @return The current Metadata Layout Version (MLV). + */ + private int determineMLV() throws SQLException { + return schemaVersionTableManager.getCurrentSchemaVersion(); + } + + /** + * Determines the Software Layout Version (SLV) based on the latest feature version. + * @return The Software Layout Version (SLV). + */ + private int determineSLV() { + return Arrays.stream(ReconLayoutFeature.values()) + .mapToInt(ReconLayoutFeature::getVersion) + .max() + .orElse(0); // Default to 0 if no features are defined + } + + /** + * Finalizes the layout features that need to be upgraded, by executing the upgrade action for each + * feature that is registered for finalization. + */ + public void finalizeLayoutFeatures() { + // Get features that need finalization, sorted by version + List featuresToFinalize = getRegisteredFeatures(); + + for (ReconLayoutFeature feature : featuresToFinalize) { + try { + // Fetch only the FINALIZE action for the feature + Optional action = feature.getAction(ReconUpgradeAction.UpgradeActionType.FINALIZE); + if (action.isPresent()) { + // Execute the upgrade action & update the schema version in the DB + action.get().execute(); + updateSchemaVersion(feature.getVersion()); + LOG.info("Feature versioned {} finalized successfully.", feature.getVersion()); + } + } catch (Exception e) { + // Log the error to both logs and ReconContext + LOG.error("Failed to finalize feature {}: {}", feature.getVersion(), e.getMessage()); + reconContext.updateErrors(ReconContext.ErrorCode.UPGRADE_FAILURE); + reconContext.updateHealthStatus(new AtomicBoolean(false)); + // Stop further upgrades as an error occurred + throw new RuntimeException("Recon failed to finalize layout feature. Startup halted."); + } + } + } + + /** + * Returns a list of ReconLayoutFeature objects that are registered for finalization. + */ + protected List getRegisteredFeatures() { + List allFeatures = + Arrays.asList(ReconLayoutFeature.values()); + + LOG.info("Current MLV: {}. SLV: {}. Checking features for registration...", currentMLV, determineSLV()); + + List registeredFeatures = allFeatures.stream() + .filter(feature -> feature.getVersion() > currentMLV) + .sorted((a, b) -> Integer.compare(a.getVersion(), b.getVersion())) // Sort by version in ascending order + .collect(Collectors.toList()); + + return registeredFeatures; + } + + /** + * Updates the Metadata Layout Version (MLV) in the database after finalizing a feature. + * @param newVersion The new Metadata Layout Version (MLV) to set. + */ + private void updateSchemaVersion(int newVersion) throws SQLException { + schemaVersionTableManager.updateSchemaVersion(newVersion); + this.currentMLV = newVersion; + LOG.info("MLV updated to: " + newVersion); + } + + public int getCurrentMLV() { + return currentMLV; + } + + public int getCurrentSLV() { + return determineSLV(); + } + +} diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReconUpgradeAction.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReconUpgradeAction.java new file mode 100644 index 00000000000..f09cdf8e1f2 --- /dev/null +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReconUpgradeAction.java @@ -0,0 +1,49 @@ +/* + * 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; + +/** + * ReconUpgradeAction is an interface for executing upgrade actions in Recon. + */ +public interface ReconUpgradeAction { + + /** + * Defines the different phases during which upgrade actions can be executed. + * Each action type corresponds to a specific point in the upgrade process: + * + * - FINALIZE: This action is executed automatically during the startup + * of Recon when it finalizes the layout upgrade. It ensures that all necessary + * upgrades or schema changes are applied to bring the system in sync with + * the latest version. + */ + enum UpgradeActionType { + FINALIZE + } + + /** + * Execute the upgrade action. + */ + void execute() throws Exception; + + /** + * Provides the type of upgrade phase (e.g., FINALIZE). + */ + UpgradeActionType getType(); +} diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/UpgradeActionRecon.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/UpgradeActionRecon.java new file mode 100644 index 00000000000..749e5f5c067 --- /dev/null +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/UpgradeActionRecon.java @@ -0,0 +1,70 @@ +/* + * 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 java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * The {@code UpgradeActionRecon} annotation is used to specify + * upgrade actions that should be executed during particular phases + * of the Recon service layout upgrade process. + * + *

This annotation can be used to associate an upgrade action + * class with a specific layout feature and upgrade phase. The + * framework will dynamically discover these annotated upgrade + * actions and execute them based on the feature's version and + * the defined action type (e.g., {@link ReconUpgradeAction.UpgradeActionType#FINALIZE}). + * + *

The annotation is retained at runtime, allowing the reflection-based + * mechanism to scan for annotated classes, register the associated actions, + * and execute them as necessary during the layout upgrade process. + * + * Example usage: + * + *

+ * {@code
+ *
+ * @UpgradeActionRecon(feature = FEATURE_NAME, type = FINALIZE)
+ *  public class FeatureNameUpgradeAction implements ReconUpgradeAction {
+ *     @Override
+ *     public void execute() throws Exception {
+ *       // Custom upgrade logic for FEATURE_1
+ *     }
+ *  }
+ * }
+ * 
+ */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface UpgradeActionRecon { + + /** + * Defines the layout feature this upgrade action is associated with. + */ + ReconLayoutFeature feature(); + + /** + * Defines the type of upgrade phase during which the action should be executed. + */ + ReconUpgradeAction.UpgradeActionType type(); +} diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/package-info.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/package-info.java new file mode 100644 index 00000000000..56a94b1f84a --- /dev/null +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/package-info.java @@ -0,0 +1,29 @@ +/* + * 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 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. + */ + +/** + * This package contains classes and interfaces for handling + * upgrade actions in Apache Ozone Recon. + * + * The main interface {@link org.apache.hadoop.ozone.recon.upgrade.ReconUpgradeAction} + * defines the structure for actions that need to be executed during an upgrade + * process in Recon. The actions can be triggered automatically + * during startup to ensure the correct version of the schema or + * layout is applied. + */ +package org.apache.hadoop.ozone.recon.upgrade; 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 new file mode 100644 index 00000000000..ab3c4f8e6ec --- /dev/null +++ b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/persistence/TestSchemaVersionTableDefinition.java @@ -0,0 +1,123 @@ +/* + * 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.persistence; + +import static org.hadoop.ozone.recon.schema.SchemaVersionTableDefinition.SCHEMA_VERSION_TABLE_NAME; +import static org.jooq.impl.DSL.name; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.sql.Types; +import java.util.ArrayList; +import java.util.List; + +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.apache.commons.lang3.tuple.Pair; +import org.jooq.DSLContext; +import org.jooq.Record1; +import org.jooq.impl.DSL; +import org.junit.jupiter.api.Test; + +/** + * Test class for SchemaVersionTableDefinition. + */ +public class TestSchemaVersionTableDefinition extends AbstractReconSqlDBTest { + + public TestSchemaVersionTableDefinition() { + super(); + } + + @Test + public void testSchemaVersionTableCreation() throws Exception { + Connection connection = getConnection(); + // Verify table definition + DatabaseMetaData metaData = connection.getMetaData(); + ResultSet resultSet = metaData.getColumns(null, null, + SCHEMA_VERSION_TABLE_NAME, null); + + List> expectedPairs = new ArrayList<>(); + + expectedPairs.add(new ImmutablePair<>("version_number", Types.INTEGER)); + expectedPairs.add(new ImmutablePair<>("applied_on", Types.TIMESTAMP)); + + List> actualPairs = new ArrayList<>(); + + while (resultSet.next()) { + actualPairs.add(new ImmutablePair<>(resultSet.getString("COLUMN_NAME"), + resultSet.getInt("DATA_TYPE"))); + } + + assertEquals(2, actualPairs.size(), "Unexpected number of columns"); + assertEquals(expectedPairs, actualPairs, "Column definitions do not match expected values."); + } + + @Test + public void testSchemaVersionCRUDOperations() throws SQLException { + Connection connection = getConnection(); + + DatabaseMetaData metaData = connection.getMetaData(); + ResultSet resultSet = metaData.getTables(null, null, + SCHEMA_VERSION_TABLE_NAME, null); + + while (resultSet.next()) { + assertEquals(SCHEMA_VERSION_TABLE_NAME, + 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"))) + .values(1, new Timestamp(System.currentTimeMillis())) + .execute(); + + // Read the inserted record + Record1 result = dslContext.select(DSL.field(name("version_number"), Integer.class)) + .from(DSL.table(SCHEMA_VERSION_TABLE_NAME)) + .fetchOne(); + + assertEquals(1, result.value1(), "The version number does not match the expected value."); + + // Update the version record + dslContext.update(DSL.table(SCHEMA_VERSION_TABLE_NAME)) + .set(DSL.field(name("version_number")), 2) + .execute(); + + // Read the updated record + result = dslContext.select(DSL.field(name("version_number"), Integer.class)) + .from(DSL.table(SCHEMA_VERSION_TABLE_NAME)) + .fetchOne(); + + assertEquals(2, result.value1(), "The updated version number does not match the expected value."); + + // Delete the version record + dslContext.deleteFrom(DSL.table(SCHEMA_VERSION_TABLE_NAME)) + .execute(); + + // Verify deletion + int count = dslContext.fetchCount(DSL.table(SCHEMA_VERSION_TABLE_NAME)); + assertEquals(0, count, "The table should be empty after deletion."); + } +} diff --git a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestReconLayoutVersionManager.java b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestReconLayoutVersionManager.java new file mode 100644 index 00000000000..1da4c48a94c --- /dev/null +++ b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestReconLayoutVersionManager.java @@ -0,0 +1,288 @@ +/* + * 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 org.apache.hadoop.ozone.recon.ReconContext; +import org.apache.hadoop.ozone.recon.ReconSchemaVersionTableManager; +import org.mockito.InOrder; +import org.mockito.MockedStatic; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.AfterEach; + +import java.sql.SQLException; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.inOrder; + + +/** + * Tests for ReconLayoutVersionManager. + */ +public class TestReconLayoutVersionManager { + + private ReconSchemaVersionTableManager schemaVersionTableManager; + private ReconLayoutVersionManager layoutVersionManager; + private MockedStatic mockedEnum; + private MockedStatic mockedEnumUpgradeActionType; + + @BeforeEach + public void setUp() throws SQLException { + schemaVersionTableManager = mock(ReconSchemaVersionTableManager.class); + when(schemaVersionTableManager.getCurrentSchemaVersion()).thenReturn(0); + + // Mocking ReconLayoutFeature.values() to return custom enum instances + mockedEnum = mockStatic(ReconLayoutFeature.class); + mockedEnumUpgradeActionType = mockStatic(ReconUpgradeAction.UpgradeActionType.class); + + ReconLayoutFeature feature1 = mock(ReconLayoutFeature.class); + when(feature1.getVersion()).thenReturn(1); + ReconUpgradeAction action1 = mock(ReconUpgradeAction.class); + when(feature1.getAction(ReconUpgradeAction.UpgradeActionType.FINALIZE)) + .thenReturn(Optional.of(action1)); + + ReconLayoutFeature feature2 = mock(ReconLayoutFeature.class); + when(feature2.getVersion()).thenReturn(2); + ReconUpgradeAction action2 = mock(ReconUpgradeAction.class); + when(feature2.getAction(ReconUpgradeAction.UpgradeActionType.FINALIZE)) + .thenReturn(Optional.of(action2)); + + // Define the custom features to be returned + mockedEnum.when(ReconLayoutFeature::values).thenReturn(new ReconLayoutFeature[]{feature1, feature2}); + + layoutVersionManager = new ReconLayoutVersionManager(schemaVersionTableManager, mock(ReconContext.class)); + } + + @AfterEach + public void tearDown() { + // Close the static mock after each test to deregister it + mockedEnum.close(); + if (mockedEnumUpgradeActionType != null) { + mockedEnumUpgradeActionType.close(); + } + } + + /** + * Tests the initialization of layout version manager to ensure + * that the MLV (Metadata Layout Version) is set correctly to 0, + * and SLV (Software Layout Version) reflects the maximum available version. + */ + @Test + public void testInitializationWithMockedValues() { + assertEquals(0, layoutVersionManager.getCurrentMLV()); + assertEquals(2, layoutVersionManager.getCurrentSLV()); + } + + /** + * Tests the finalization of layout features and ensure that the updateSchemaVersion for + * the schemaVersionTable is triggered for each feature version. + */ + @Test + public void testFinalizeLayoutFeaturesWithMockedValues() throws SQLException { + layoutVersionManager.finalizeLayoutFeatures(); + + // Verify that schema versions are updated for our custom features + verify(schemaVersionTableManager, times(1)).updateSchemaVersion(1); + verify(schemaVersionTableManager, times(1)).updateSchemaVersion(2); + } + + /** + * Tests the retrieval of registered features to ensure that the correct + * layout features are returned according to the mocked values. + */ + @Test + public void testGetRegisteredFeaturesWithMockedValues() { + // Fetch the registered features + List registeredFeatures = layoutVersionManager.getRegisteredFeatures(); + + // Verify that the registered features match the mocked ones + ReconLayoutFeature feature1 = ReconLayoutFeature.values()[0]; + ReconLayoutFeature feature2 = ReconLayoutFeature.values()[1]; + List expectedFeatures = Arrays.asList(feature1, feature2); + assertEquals(expectedFeatures, registeredFeatures); + } + + /** + * Tests the scenario where no layout features are present. Ensures that no schema + * version updates are attempted when there are no features to finalize. + */ + @Test + public void testNoLayoutFeatures() throws SQLException { + mockedEnum.when(ReconLayoutFeature::values).thenReturn(new ReconLayoutFeature[]{}); + layoutVersionManager.finalizeLayoutFeatures(); + verify(schemaVersionTableManager, never()).updateSchemaVersion(anyInt()); + } + + /** + * Tests the scenario where an upgrade action fails. Ensures that if an upgrade action + * throws an exception, the schema version is not updated. + */ + @Test + public void testUpgradeActionFailure() throws Exception { + // Reset existing mocks and set up new features for this specific test + mockedEnum.reset(); + + // Mock ReconLayoutFeature instances + ReconLayoutFeature feature1 = mock(ReconLayoutFeature.class); + when(feature1.getVersion()).thenReturn(1); + ReconUpgradeAction action1 = mock(ReconUpgradeAction.class); + + // Simulate an exception being thrown during the upgrade action execution + doThrow(new RuntimeException("Upgrade failed")).when(action1).execute(); + when(feature1.getAction(ReconUpgradeAction.UpgradeActionType.FINALIZE)) + .thenReturn(Optional.of(action1)); + + // Mock the static values method to return the custom feature + mockedEnum.when(ReconLayoutFeature::values).thenReturn(new ReconLayoutFeature[]{feature1}); + + // Execute the layout feature finalization + try { + layoutVersionManager.finalizeLayoutFeatures(); + } catch (Exception e) { + } + // Verify that schema version update was never called due to the exception + verify(schemaVersionTableManager, never()).updateSchemaVersion(anyInt()); + } + + /** + * Tests the order of execution for the upgrade actions to ensure that + * they are executed sequentially according to their version numbers. + */ + @Test + public void testUpgradeActionExecutionOrder() throws Exception { + // Reset the existing static mock for this specific test + mockedEnum.reset(); + + // Mock ReconLayoutFeature instances + ReconLayoutFeature feature1 = mock(ReconLayoutFeature.class); + when(feature1.getVersion()).thenReturn(1); + ReconUpgradeAction action1 = mock(ReconUpgradeAction.class); + when(feature1.getAction(ReconUpgradeAction.UpgradeActionType.FINALIZE)) + .thenReturn(Optional.of(action1)); + + ReconLayoutFeature feature2 = mock(ReconLayoutFeature.class); + when(feature2.getVersion()).thenReturn(2); + ReconUpgradeAction action2 = mock(ReconUpgradeAction.class); + when(feature2.getAction(ReconUpgradeAction.UpgradeActionType.FINALIZE)) + .thenReturn(Optional.of(action2)); + + ReconLayoutFeature feature3 = mock(ReconLayoutFeature.class); + when(feature3.getVersion()).thenReturn(3); + ReconUpgradeAction action3 = mock(ReconUpgradeAction.class); + when(feature3.getAction(ReconUpgradeAction.UpgradeActionType.FINALIZE)) + .thenReturn(Optional.of(action3)); + + // Mock the static values method to return custom features in a jumbled order + mockedEnum.when(ReconLayoutFeature::values).thenReturn(new ReconLayoutFeature[]{feature2, feature3, feature1}); + + // Execute the layout feature finalization + layoutVersionManager.finalizeLayoutFeatures(); + + // Verify that the actions were executed in the correct order using InOrder + InOrder inOrder = inOrder(action1, action2, action3); + inOrder.verify(action1).execute(); // Should be executed first + inOrder.verify(action2).execute(); // Should be executed second + inOrder.verify(action3).execute(); // Should be executed third + } + + /** + * Tests the scenario where no upgrade actions are needed. Ensures that if the current + * schema version matches the maximum layout version, no upgrade actions are executed. + */ + @Test + public void testNoUpgradeActionsNeeded() throws SQLException { + when(schemaVersionTableManager.getCurrentSchemaVersion()).thenReturn(2); + layoutVersionManager = new ReconLayoutVersionManager(schemaVersionTableManager, mock(ReconContext.class)); + layoutVersionManager.finalizeLayoutFeatures(); + + verify(schemaVersionTableManager, never()).updateSchemaVersion(anyInt()); + } + + /** + * Tests the scenario where the first two features are finalized, + * and then a third feature is introduced. Ensures that only the + * newly introduced feature is finalized while the previously + * finalized features are skipped. + */ + @Test + public void testFinalizingNewFeatureWithoutReFinalizingPreviousFeatures() throws Exception { + // Step 1: Finalize the first two features. + when(schemaVersionTableManager.getCurrentSchemaVersion()).thenReturn(0); + + // Mock the first two features. + ReconLayoutFeature feature1 = mock(ReconLayoutFeature.class); + when(feature1.getVersion()).thenReturn(1); + ReconUpgradeAction action1 = mock(ReconUpgradeAction.class); + when(feature1.getAction(ReconUpgradeAction.UpgradeActionType.FINALIZE)) + .thenReturn(Optional.of(action1)); + + ReconLayoutFeature feature2 = mock(ReconLayoutFeature.class); + when(feature2.getVersion()).thenReturn(2); + ReconUpgradeAction action2 = mock(ReconUpgradeAction.class); + when(feature2.getAction(ReconUpgradeAction.UpgradeActionType.FINALIZE)) + .thenReturn(Optional.of(action2)); + + mockedEnum.when(ReconLayoutFeature::values).thenReturn(new ReconLayoutFeature[]{feature1, feature2}); + + // Finalize the first two features. + layoutVersionManager.finalizeLayoutFeatures(); + + // Verify that the schema versions for the first two features were updated. + verify(schemaVersionTableManager, times(1)).updateSchemaVersion(1); + verify(schemaVersionTableManager, times(1)).updateSchemaVersion(2); + + // Step 2: Introduce a new feature (Feature 3). + ReconLayoutFeature feature3 = mock(ReconLayoutFeature.class); + when(feature3.getVersion()).thenReturn(3); + ReconUpgradeAction action3 = mock(ReconUpgradeAction.class); + when(feature3.getAction(ReconUpgradeAction.UpgradeActionType.FINALIZE)) + .thenReturn(Optional.of(action3)); + + mockedEnum.when(ReconLayoutFeature::values).thenReturn(new ReconLayoutFeature[]{feature1, feature2, feature3}); + + // Update schema version to simulate that features 1 and 2 have already been finalized. + when(schemaVersionTableManager.getCurrentSchemaVersion()).thenReturn(2); + + // Finalize again, but only feature 3 should be finalized. + layoutVersionManager.finalizeLayoutFeatures(); + + // Verify that the schema version for feature 3 was updated. + verify(schemaVersionTableManager, times(1)).updateSchemaVersion(3); + + // Verify that action1 and action2 were not executed again. + verify(action1, times(1)).execute(); // Still should have been executed only once + verify(action2, times(1)).execute(); // Still should have been executed only once + + // Verify that the upgrade action for feature 3 was executed. + verify(action3, times(1)).execute(); + } + +}