From a0a2cbb511cb3bb976c5653cd2403706a8dc2b6a Mon Sep 17 00:00:00 2001 From: Ethan Rose Date: Mon, 3 Aug 2026 15:05:09 -0400 Subject: [PATCH 1/2] Initial addition of combined action and test --- .../InitialConstraintUpgradeAction.java | 39 ---- .../ReconTaskStatusTableUpgradeAction.java | 6 +- .../TestInitialConstraintUpgradeAction.java | 199 ------------------ ...TestReconTaskStatusTableUpgradeAction.java | 29 +++ 4 files changed, 34 insertions(+), 239 deletions(-) delete mode 100644 hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/InitialConstraintUpgradeAction.java delete mode 100644 hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestInitialConstraintUpgradeAction.java diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/InitialConstraintUpgradeAction.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/InitialConstraintUpgradeAction.java deleted file mode 100644 index ffcf89768f21..000000000000 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/InitialConstraintUpgradeAction.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.hadoop.ozone.recon.upgrade; - -import static org.apache.hadoop.ozone.recon.upgrade.ReconVersion.INITIAL_VERSION; - -import java.sql.SQLException; -import javax.sql.DataSource; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Upgrade action for the INITIAL schema version, which manages constraints - * for the UNHEALTHY_CONTAINERS table. - */ -@ReconUpgradeActionForVersion(version = INITIAL_VERSION) -public class InitialConstraintUpgradeAction implements ReconUpgradeAction { - private static final Logger LOG = LoggerFactory.getLogger(InitialConstraintUpgradeAction.class); - - @Override - public void execute(DataSource source) throws SQLException { - ReconUpgradeAction.updateUnhealthyContainerStatesConstraint(source, LOG); - } -} diff --git a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReconTaskStatusTableUpgradeAction.java b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReconTaskStatusTableUpgradeAction.java index f8a5a98a695e..9deabc163ea8 100644 --- a/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReconTaskStatusTableUpgradeAction.java +++ b/hadoop-ozone/recon/src/main/java/org/apache/hadoop/ozone/recon/upgrade/ReconTaskStatusTableUpgradeAction.java @@ -34,9 +34,11 @@ import org.slf4j.LoggerFactory; /** - * Upgrade action for TASK_STATUS_STATISTICS feature layout change, which adds + * Upgrade action for TASK_STATUS_STATISTICS version, which adds * last_task_run_status and current_task_run_status columns to * {@link ReconTaskSchemaDefinition} in case it is missing . + *

+ * It also applies the UNHEALTHY_CONTAINERS check constraint which must be run with Recon's first version increase. */ @ReconUpgradeActionForVersion(version = ReconVersion.TASK_STATUS_STATISTICS) public class ReconTaskStatusTableUpgradeAction implements ReconUpgradeAction { @@ -73,6 +75,8 @@ private void setColumnAsNonNullableIfNeeded(Connection conn, DSLContext dslConte @Override public void execute(DataSource dataSource) throws DataAccessException, SQLException { + ReconUpgradeAction.updateUnhealthyContainerStatesConstraint(dataSource, LOG); + try (Connection conn = dataSource.getConnection()) { if (!TABLE_EXISTS_CHECK.test(conn, RECON_TASK_STATUS_TABLE_NAME)) { return; diff --git a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestInitialConstraintUpgradeAction.java b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestInitialConstraintUpgradeAction.java deleted file mode 100644 index 5b90e50a4ce5..000000000000 --- a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestInitialConstraintUpgradeAction.java +++ /dev/null @@ -1,199 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * 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 static org.apache.ozone.recon.schema.ContainerSchemaDefinition.UNHEALTHY_CONTAINERS_TABLE_NAME; -import static org.apache.ozone.recon.schema.SqlDbUtils.constraintExists; -import static org.jooq.impl.DSL.field; -import static org.jooq.impl.DSL.name; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.sql.Connection; -import java.sql.DatabaseMetaData; -import java.sql.ResultSet; -import java.sql.SQLException; -import javax.sql.DataSource; -import org.apache.hadoop.ozone.recon.persistence.AbstractReconSqlDBTest; -import org.apache.hadoop.ozone.recon.scm.ReconStorageContainerManagerFacade; -import org.apache.ozone.recon.schema.ContainerSchemaDefinition; -import org.jooq.DSLContext; -import org.jooq.impl.DSL; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -/** - * Test class for InitialConstraintUpgradeAction. - */ -public class TestInitialConstraintUpgradeAction extends AbstractReconSqlDBTest { - - private InitialConstraintUpgradeAction upgradeAction; - private DSLContext dslContext; - private ReconStorageContainerManagerFacade mockScmFacade; - - @BeforeEach - public void setUp() throws SQLException { - // Initialize the DSLContext - dslContext = getDslContext(); - - // Initialize the upgrade action - upgradeAction = new InitialConstraintUpgradeAction(); - - // Mock the SCM facade to provide the DataSource - mockScmFacade = mock(ReconStorageContainerManagerFacade.class); - DataSource dataSource = getInjector().getInstance(DataSource.class); - when(mockScmFacade.getDataSource()).thenReturn(dataSource); - - // Check if the table already exists - try (Connection conn = dataSource.getConnection()) { - DatabaseMetaData dbMetaData = conn.getMetaData(); - ResultSet tables = dbMetaData.getTables(null, null, UNHEALTHY_CONTAINERS_TABLE_NAME, null); - if (!tables.next()) { - // Create the initial table if it does not exist - dslContext.createTable(UNHEALTHY_CONTAINERS_TABLE_NAME) - .column("container_id", org.jooq.impl.SQLDataType.BIGINT - .nullable(false)) - .column("container_state", org.jooq.impl.SQLDataType.VARCHAR(16) - .nullable(false)) - .constraint(DSL.constraint("pk_container_id") - .primaryKey("container_id", "container_state")) - .execute(); - } - } - } - - @Test - public void testExecuteIsIdempotent() throws SQLException { - DataSource dataSource = getInjector().getInstance(DataSource.class); - upgradeAction.execute(dataSource); - try (Connection conn = dataSource.getConnection()) { - assertTrue(constraintExists(conn, UNHEALTHY_CONTAINERS_TABLE_NAME, - UNHEALTHY_CONTAINERS_TABLE_NAME + "ck1")); - } - assertDoesNotThrow(() -> upgradeAction.execute(dataSource)); - } - - @Test - public void testUpgradeAppliesConstraintModificationForAllStates() throws SQLException { - // Run the upgrade action - upgradeAction.execute(mockScmFacade.getDataSource()); - - // Iterate over all valid states and insert records - for (ContainerSchemaDefinition.UnHealthyContainerStates state : - ContainerSchemaDefinition.UnHealthyContainerStates.values()) { - dslContext.insertInto(DSL.table(UNHEALTHY_CONTAINERS_TABLE_NAME)) - .columns( - field(name("container_id")), - field(name("container_state")), - field(name("in_state_since")), - field(name("expected_replica_count")), - field(name("actual_replica_count")), - field(name("replica_delta")), - field(name("reason")) - ) - .values( - System.currentTimeMillis(), // Unique container_id for each record - state.name(), System.currentTimeMillis(), 3, 2, 1, "Replica count mismatch" - ) - .execute(); - } - - // Verify that the number of inserted records matches the number of enum values - int count = dslContext.fetchCount(DSL.table(UNHEALTHY_CONTAINERS_TABLE_NAME)); - assertEquals(ContainerSchemaDefinition.UnHealthyContainerStates.values().length, - count, "Expected one record for each valid state"); - - // Try inserting an invalid state (should fail due to constraint) - assertThrows(org.jooq.exception.DataAccessException.class, () -> - dslContext.insertInto(DSL.table(UNHEALTHY_CONTAINERS_TABLE_NAME)) - .columns( - field(name("container_id")), - field(name("container_state")), - field(name("in_state_since")), - field(name("expected_replica_count")), - field(name("actual_replica_count")), - field(name("replica_delta")), - field(name("reason")) - ) - .values(999L, "INVALID_STATE", System.currentTimeMillis(), 3, 2, 1, - "Invalid state test").execute(), - "Inserting an invalid container_state should fail due to the constraint"); - } - - @Test - public void testInsertionWithNullContainerState() { - assertThrows(org.jooq.exception.DataAccessException.class, () -> { - dslContext.insertInto(DSL.table(UNHEALTHY_CONTAINERS_TABLE_NAME)) - .columns( - field(name("container_id")), - field(name("container_state")), - field(name("in_state_since")), - field(name("expected_replica_count")), - field(name("actual_replica_count")), - field(name("replica_delta")), - field(name("reason")) - ) - .values( - 100L, // container_id - null, // container_state is NULL - System.currentTimeMillis(), 3, 2, 1, "Testing NULL state" - ) - .execute(); - }, "Inserting a NULL container_state should fail due to the NOT NULL constraint"); - } - - @Test - public void testDuplicatePrimaryKeyInsertion() throws SQLException { - // Insert the first record - dslContext.insertInto(DSL.table(UNHEALTHY_CONTAINERS_TABLE_NAME)) - .columns( - field(name("container_id")), - field(name("container_state")), - field(name("in_state_since")), - field(name("expected_replica_count")), - field(name("actual_replica_count")), - field(name("replica_delta")), - field(name("reason")) - ) - .values(200L, "MISSING", System.currentTimeMillis(), 3, 2, 1, "First insertion" - ) - .execute(); - - // Try inserting a duplicate record with the same primary key - assertThrows(org.jooq.exception.DataAccessException.class, () -> { - dslContext.insertInto(DSL.table(UNHEALTHY_CONTAINERS_TABLE_NAME)) - .columns( - field(name("container_id")), - field(name("container_state")), - field(name("in_state_since")), - field(name("expected_replica_count")), - field(name("actual_replica_count")), - field(name("replica_delta")), - field(name("reason")) - ) - .values(200L, "MISSING", System.currentTimeMillis(), 3, 2, 1, "Duplicate insertion" - ) - .execute(); - }, "Inserting a duplicate primary key should fail due to the primary key constraint"); - } - -} diff --git a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestReconTaskStatusTableUpgradeAction.java b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestReconTaskStatusTableUpgradeAction.java index 2d98bba6ab19..a7b43a1aeba9 100644 --- a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestReconTaskStatusTableUpgradeAction.java +++ b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestReconTaskStatusTableUpgradeAction.java @@ -17,10 +17,13 @@ package org.apache.hadoop.ozone.recon.upgrade; +import static org.apache.ozone.recon.schema.ContainerSchemaDefinition.UNHEALTHY_CONTAINERS_TABLE_NAME; import static org.apache.ozone.recon.schema.ReconTaskSchemaDefinition.RECON_TASK_STATUS_TABLE_NAME; import static org.apache.ozone.recon.schema.SqlDbUtils.TABLE_EXISTS_CHECK; import static org.apache.ozone.recon.schema.SqlDbUtils.columnExists; +import static org.apache.ozone.recon.schema.SqlDbUtils.constraintExists; import static org.apache.ozone.recon.schema.SqlDbUtils.isColumnNullable; +import static org.jooq.impl.DSL.name; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -42,6 +45,7 @@ public class TestReconTaskStatusTableUpgradeAction extends AbstractReconSqlDBTes private static final String LAST_TASK_RUN_STATUS = "last_task_run_status"; private static final String IS_CURRENT_TASK_RUNNING = "is_current_task_running"; + private static final String UNHEALTHY_CONTAINERS_CONSTRAINT = UNHEALTHY_CONTAINERS_TABLE_NAME + "ck1"; private DSLContext dslContext; private DataSource dataSource; @@ -98,6 +102,17 @@ public void testNoOpWhenTableMissing() throws SQLException { assertDoesNotThrow(() -> upgradeAction.execute(dataSource)); } + @Test + public void testExecuteAppliesUnhealthyContainersConstraint() throws Exception { + createUnhealthyContainersTableWithoutCheckConstraint(); + + upgradeAction.execute(dataSource); + + try (Connection conn = dataSource.getConnection()) { + assertTrue(constraintExists(conn, UNHEALTHY_CONTAINERS_TABLE_NAME, UNHEALTHY_CONTAINERS_CONSTRAINT)); + } + } + private void createLegacyTaskStatusTable() throws SQLException { dropTaskStatusTableIfPresent(); try (Connection conn = dataSource.getConnection()) { @@ -124,4 +139,18 @@ private void dropTaskStatusTableIfPresent() throws SQLException { } } } + + private void createUnhealthyContainersTableWithoutCheckConstraint() throws SQLException { + try (Connection conn = dataSource.getConnection()) { + if (TABLE_EXISTS_CHECK.test(conn, UNHEALTHY_CONTAINERS_TABLE_NAME)) { + dslContext.dropTable(UNHEALTHY_CONTAINERS_TABLE_NAME).execute(); + } + } + dslContext.createTable(UNHEALTHY_CONTAINERS_TABLE_NAME) + .column("container_id", SQLDataType.BIGINT.nullable(false)) + .column("container_state", SQLDataType.VARCHAR(16).nullable(false)) + .constraint(DSL.constraint("pk_container_id") + .primaryKey(name("container_id"), name("container_state"))) + .execute(); + } } From c27054aafae1893943fdd89c60f07828d608f79d Mon Sep 17 00:00:00 2001 From: Ethan Rose Date: Mon, 3 Aug 2026 16:05:10 -0400 Subject: [PATCH 2/2] Restore container state check --- ...healthyContainerReplicaMismatchAction.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestUnhealthyContainerReplicaMismatchAction.java b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestUnhealthyContainerReplicaMismatchAction.java index 3b58d8846095..5f9b1812785c 100644 --- a/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestUnhealthyContainerReplicaMismatchAction.java +++ b/hadoop-ozone/recon/src/test/java/org/apache/hadoop/ozone/recon/upgrade/TestUnhealthyContainerReplicaMismatchAction.java @@ -20,15 +20,20 @@ import static org.apache.ozone.recon.schema.ContainerSchemaDefinition.UNHEALTHY_CONTAINERS_TABLE_NAME; import static org.apache.ozone.recon.schema.SqlDbUtils.TABLE_EXISTS_CHECK; import static org.apache.ozone.recon.schema.SqlDbUtils.constraintExists; +import static org.jooq.impl.DSL.field; import static org.jooq.impl.DSL.name; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.sql.Connection; import java.sql.SQLException; import javax.sql.DataSource; import org.apache.hadoop.ozone.recon.persistence.AbstractReconSqlDBTest; +import org.apache.ozone.recon.schema.ContainerSchemaDefinition.UnHealthyContainerStates; import org.jooq.DSLContext; +import org.jooq.exception.DataAccessException; import org.jooq.impl.DSL; import org.jooq.impl.SQLDataType; import org.junit.jupiter.api.BeforeEach; @@ -68,6 +73,30 @@ public void testNoOpWhenTableMissing() throws SQLException { assertDoesNotThrow(() -> upgradeAction.execute(dataSource)); } + @Test + public void testConstraintAdmitsAllStatesAndRejectsInvalid() throws Exception { + upgradeAction.execute(dataSource); + + UnHealthyContainerStates[] states = UnHealthyContainerStates.values(); + for (int i = 0; i < states.length; i++) { + insertContainerState(i, states[i].name()); + } + assertEquals(states.length, + dslContext.fetchCount(DSL.table(UNHEALTHY_CONTAINERS_TABLE_NAME)), + "Expected one record for each valid state"); + + assertThrows(DataAccessException.class, + () -> insertContainerState(states.length, "INVALID_STATE"), + "Inserting an invalid container_state should fail due to the check constraint"); + } + + private void insertContainerState(long containerId, String containerState) { + dslContext.insertInto(DSL.table(UNHEALTHY_CONTAINERS_TABLE_NAME)) + .columns(field(name("container_id")), field(name("container_state"))) + .values(containerId, containerState) + .execute(); + } + private void createTableWithoutCheckConstraint() throws SQLException { dropTableIfPresent(); dslContext.createTable(UNHEALTHY_CONTAINERS_TABLE_NAME)