From c66dbade2186e6b8b8e3de1d0bf3ad4e95c6e093 Mon Sep 17 00:00:00 2001 From: Matteo Mara Date: Thu, 2 May 2024 17:51:27 +0200 Subject: [PATCH] SONAR-22141 add new fields in the response of migrations-status endpoint --- .../step/MigrationStatusListenerImplTest.java | 54 +++++++++++++++++++ .../db/migration/DatabaseMigrationState.java | 16 ++++++ .../migration/DatabaseMigrationStateImpl.java | 41 +++++++++++++- .../MutableDatabaseMigrationState.java | 4 ++ .../db/migration/engine/MigrationEngine.java | 4 +- .../migration/engine/MigrationEngineImpl.java | 7 +-- .../step/MigrationStatusListener.java | 28 ++++++++++ .../step/MigrationStatusListenerImpl.java | 41 ++++++++++++++ .../step/MigrationStepsExecutor.java | 2 +- .../step/MigrationStepsExecutorImpl.java | 7 ++- .../step/NoOpMigrationStatusListener.java | 32 +++++++++++ .../DatabaseMigrationStateImplTest.java | 44 +++++++++++---- .../engine/MigrationContainerImplTest.java | 3 +- .../engine/MigrationEngineImplTest.java | 54 +++++-------------- .../step/MigrationStepsExecutorImplTest.java | 11 ++-- .../java/org/sonar/db/SQDatabase.java | 5 +- .../db/migration/AutoDbMigration.java | 5 +- .../db/migration/DatabaseMigrationImpl.java | 3 +- .../db/migration/AutoDbMigrationTest.java | 5 +- ...baseMigrationImplConcurrentAccessTest.java | 4 +- .../migration/DatabaseMigrationImplTest.java | 19 +++---- .../DatabaseMigrationsController.java | 23 ++++++-- .../DatabaseMigrationsControllerTest.java | 13 ++++- 23 files changed, 337 insertions(+), 88 deletions(-) create mode 100644 server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/step/MigrationStatusListenerImplTest.java create mode 100644 server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/step/MigrationStatusListener.java create mode 100644 server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/step/MigrationStatusListenerImpl.java create mode 100644 server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/step/NoOpMigrationStatusListener.java diff --git a/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/step/MigrationStatusListenerImplTest.java b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/step/MigrationStatusListenerImplTest.java new file mode 100644 index 00000000000..b406c276427 --- /dev/null +++ b/server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/step/MigrationStatusListenerImplTest.java @@ -0,0 +1,54 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.db.migration.step; + +import org.junit.jupiter.api.Test; +import org.sonar.server.platform.db.migration.DatabaseMigrationStateImpl; + +import static org.assertj.core.api.Assertions.assertThat; + +class MigrationStatusListenerImplTest { + + @Test + void onMigrationStepCompleted_incrementsTheStepsInTheState() { + final DatabaseMigrationStateImpl state = new DatabaseMigrationStateImpl(); + final MigrationStatusListenerImpl underTest = new MigrationStatusListenerImpl(state); + + assertThat(state.getCompletedMigrations()).isZero(); + + underTest.onMigrationStepCompleted(); + + assertThat(state.getCompletedMigrations()).isEqualTo(1); + } + + @Test + void onMigrationStart_setsTheCorrectNumberOfTotalSteps() { + final DatabaseMigrationStateImpl state = new DatabaseMigrationStateImpl(); + final MigrationStatusListenerImpl underTest = new MigrationStatusListenerImpl(state); + final int totalMigrations = 10; + + assertThat(state.getTotalMigrations()).isZero(); + + underTest.onMigrationsStart(totalMigrations); + + assertThat(state.getTotalMigrations()).isEqualTo(totalMigrations); + } + +} diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/DatabaseMigrationState.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/DatabaseMigrationState.java index 82bb1204e1c..ff2f310e6b5 100644 --- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/DatabaseMigrationState.java +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/DatabaseMigrationState.java @@ -77,4 +77,20 @@ public interface DatabaseMigrationState { */ @CheckForNull Throwable getError(); + + /** + * The amount of migrations already completed. + */ + int getCompletedMigrations(); + + /** + * The total amount of migrations to be performed. + */ + int getTotalMigrations(); + + /** + * The expected finish timestamp of the migration. + */ + Date getExpectedFinishDate(); + } diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/DatabaseMigrationStateImpl.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/DatabaseMigrationStateImpl.java index daa97066315..5951e58871b 100644 --- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/DatabaseMigrationStateImpl.java +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/DatabaseMigrationStateImpl.java @@ -29,9 +29,13 @@ import javax.annotation.Nullable; public class DatabaseMigrationStateImpl implements MutableDatabaseMigrationState { private Status status = Status.NONE; @Nullable - private Date startedAt; + private Date startedAt = null; @Nullable - private Throwable error; + private Throwable error = null; + private int completedMigrations = 0; + private int totalMigrations = 0; + @Nullable + private Date completionExpectedAt = null; @Override public Status getStatus() { @@ -60,8 +64,41 @@ public class DatabaseMigrationStateImpl implements MutableDatabaseMigrationState return error; } + @Override + public void incrementCompletedMigrations() { + completedMigrations++; + updateExpectedFinishDate(); + } + + @Override + public int getCompletedMigrations() { + return completedMigrations; + } + + @Override + public void setTotalMigrations(int totalMigrations) { + this.totalMigrations = totalMigrations; + } + + @Override + public int getTotalMigrations() { + return totalMigrations; + } + @Override public void setError(@Nullable Throwable error) { this.error = error; } + + @Override + public Date getExpectedFinishDate() { + return completionExpectedAt; + } + + private void updateExpectedFinishDate() { + // Here the logic is to calculate the expected finish date based on the current time and the number of migrations completed and total + // migrations + this.completionExpectedAt = new Date(); + } + } diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/MutableDatabaseMigrationState.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/MutableDatabaseMigrationState.java index f283dfc411b..fb1d64ce71f 100644 --- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/MutableDatabaseMigrationState.java +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/MutableDatabaseMigrationState.java @@ -28,4 +28,8 @@ public interface MutableDatabaseMigrationState extends DatabaseMigrationState { void setStartedAt(@Nullable Date startedAt); void setError(@Nullable Throwable error); + + void incrementCompletedMigrations(); + + void setTotalMigrations(int completedMigrations); } diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/engine/MigrationEngine.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/engine/MigrationEngine.java index 8dcd0fb4b01..cffea5ccab9 100644 --- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/engine/MigrationEngine.java +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/engine/MigrationEngine.java @@ -19,6 +19,8 @@ */ package org.sonar.server.platform.db.migration.engine; +import org.sonar.server.platform.db.migration.step.MigrationStatusListener; + /** * This class is responsible for: * */ public interface MigrationEngine { - void execute(); + void execute(MigrationStatusListener listener); } diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/engine/MigrationEngineImpl.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/engine/MigrationEngineImpl.java index 5dbd6bd974d..7e87116e853 100644 --- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/engine/MigrationEngineImpl.java +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/engine/MigrationEngineImpl.java @@ -23,6 +23,7 @@ import java.util.List; import java.util.Optional; import org.sonar.core.platform.SpringComponentContainer; import org.sonar.server.platform.db.migration.history.MigrationHistory; +import org.sonar.server.platform.db.migration.step.MigrationStatusListener; import org.sonar.server.platform.db.migration.step.MigrationSteps; import org.sonar.server.platform.db.migration.step.MigrationStepsExecutor; import org.sonar.server.platform.db.migration.step.MigrationStepsExecutorImpl; @@ -40,7 +41,7 @@ public class MigrationEngineImpl implements MigrationEngine { } @Override - public void execute() { + public void execute(MigrationStatusListener listener) { MigrationContainer migrationContainer = new MigrationContainerImpl(serverContainer, MigrationStepsExecutorImpl.class); try { MigrationStepsExecutor stepsExecutor = migrationContainer.getComponentByType(MigrationStepsExecutor.class); @@ -50,8 +51,8 @@ public class MigrationEngineImpl implements MigrationEngine { .map(i -> migrationSteps.readFrom(i + 1)) .orElse(migrationSteps.readAll()); - - stepsExecutor.execute(steps); + listener.onMigrationsStart(steps.size()); + stepsExecutor.execute(steps, listener); } finally { migrationContainer.cleanup(); diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/step/MigrationStatusListener.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/step/MigrationStatusListener.java new file mode 100644 index 00000000000..614305283c1 --- /dev/null +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/step/MigrationStatusListener.java @@ -0,0 +1,28 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.db.migration.step; + +public interface MigrationStatusListener { + + void onMigrationStepCompleted(); + + void onMigrationsStart(int totalMigrations); + +} diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/step/MigrationStatusListenerImpl.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/step/MigrationStatusListenerImpl.java new file mode 100644 index 00000000000..8d02058c8b4 --- /dev/null +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/step/MigrationStatusListenerImpl.java @@ -0,0 +1,41 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.db.migration.step; + +import org.sonar.server.platform.db.migration.MutableDatabaseMigrationState; + +public class MigrationStatusListenerImpl implements MigrationStatusListener { + + private final MutableDatabaseMigrationState migrationState; + + public MigrationStatusListenerImpl(MutableDatabaseMigrationState migrationState) { + this.migrationState = migrationState; + } + + @Override + public void onMigrationStepCompleted() { + migrationState.incrementCompletedMigrations(); + } + + @Override + public void onMigrationsStart(int totalMigrations) { + migrationState.setTotalMigrations(totalMigrations); + } +} diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/step/MigrationStepsExecutor.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/step/MigrationStepsExecutor.java index 13ef91aa6d8..2e321cbda1b 100644 --- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/step/MigrationStepsExecutor.java +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/step/MigrationStepsExecutor.java @@ -34,5 +34,5 @@ public interface MigrationStepsExecutor { /** * @throws MigrationStepExecutionException at the first failing migration step execution */ - void execute(List steps); + void execute(List steps, MigrationStatusListener listener); } diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/step/MigrationStepsExecutorImpl.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/step/MigrationStepsExecutorImpl.java index ee5c55d427c..e7105c0eff2 100644 --- a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/step/MigrationStepsExecutorImpl.java +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/step/MigrationStepsExecutorImpl.java @@ -44,12 +44,15 @@ public class MigrationStepsExecutorImpl implements MigrationStepsExecutor { } @Override - public void execute(List steps) { + public void execute(List steps, MigrationStatusListener listener) { Profiler globalProfiler = Profiler.create(LOGGER); globalProfiler.startInfo(GLOBAL_START_MESSAGE); boolean allStepsExecuted = false; try { - steps.forEach(this::execute); + for (RegisteredMigrationStep step : steps) { + this.execute(step); + listener.onMigrationStepCompleted(); + } allStepsExecuted = true; } finally { if (allStepsExecuted) { diff --git a/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/step/NoOpMigrationStatusListener.java b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/step/NoOpMigrationStatusListener.java new file mode 100644 index 00000000000..a8d170da319 --- /dev/null +++ b/server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/step/NoOpMigrationStatusListener.java @@ -0,0 +1,32 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.platform.db.migration.step; + +public class NoOpMigrationStatusListener implements MigrationStatusListener { + @Override + public void onMigrationStepCompleted() { + // no op + } + + @Override + public void onMigrationsStart(int totalMigrations) { + // no op + } +} diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/DatabaseMigrationStateImplTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/DatabaseMigrationStateImplTest.java index 33c39c5e0a0..1c2813b4c3a 100644 --- a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/DatabaseMigrationStateImplTest.java +++ b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/DatabaseMigrationStateImplTest.java @@ -20,35 +20,34 @@ package org.sonar.server.platform.db.migration; import java.util.Date; -import org.junit.Test; +import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; -public class DatabaseMigrationStateImplTest { +class DatabaseMigrationStateImplTest { private DatabaseMigrationStateImpl underTest = new DatabaseMigrationStateImpl(); @Test - public void getStatus_returns_NONE_when_component_is_created() { + void getStatus_whenComponentIsCreated_shouldReturnNONE() { assertThat(underTest.getStatus()).isEqualTo(DatabaseMigrationState.Status.NONE); } @Test - public void getStatus_returns_argument_of_setStatus() { + void getStatus_shouldReturnArgumentOfSetStatus() { for (DatabaseMigrationState.Status status : DatabaseMigrationState.Status.values()) { underTest.setStatus(status); assertThat(underTest.getStatus()).isEqualTo(status); } - } @Test - public void getStartedAt_returns_null_when_component_is_created() { + void getStartedAt_whenComponentIsCreated_shouldReturnNull() { assertThat(underTest.getStartedAt()).isNull(); } @Test - public void getStartedAt_returns_argument_of_setStartedAt() { + void getStartedAt_shouldReturnArgumentOfSetStartedAt() { Date expected = new Date(); underTest.setStartedAt(expected); @@ -56,15 +55,42 @@ public class DatabaseMigrationStateImplTest { } @Test - public void getError_returns_null_when_component_is_created() { + void getError_whenComponentIsCreated_shouldReturnNull() { assertThat(underTest.getError()).isNull(); } @Test - public void getError_returns_argument_of_setError() { + void getError_shouldReturnArgumentOfSetError() { RuntimeException expected = new RuntimeException(); underTest.setError(expected); assertThat(underTest.getError()).isSameAs(expected); } + + @Test + void incrementCompletedMigrations_shouldIncrementCompletedMigrations() { + assertThat(underTest.getCompletedMigrations()).isZero(); + + underTest.incrementCompletedMigrations(); + + assertThat(underTest.getCompletedMigrations()).isEqualTo(1); + } + + @Test + void getTotalMigrations_shouldReturnArgumentOfSetTotalMigrations() { + underTest.setTotalMigrations(10); + + assertThat(underTest.getTotalMigrations()).isEqualTo(10); + } + + @Test + void incrementCompletedMigrations_shouldUpdateExpectedFinishDate() { + Date startDate = new Date(); + + underTest.incrementCompletedMigrations(); + + // At the moment the expected finish date gets update with the timestamp of the last migration completed + assertThat(underTest.getExpectedFinishDate()).isAfterOrEqualTo(startDate); + } + } diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/engine/MigrationContainerImplTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/engine/MigrationContainerImplTest.java index 4ccd4b98069..13e99bb6bbc 100644 --- a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/engine/MigrationContainerImplTest.java +++ b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/engine/MigrationContainerImplTest.java @@ -26,6 +26,7 @@ import org.junit.Test; import org.sonar.api.Startable; import org.sonar.core.platform.SpringComponentContainer; import org.sonar.server.platform.db.migration.step.InternalMigrationStepRegistry; +import org.sonar.server.platform.db.migration.step.MigrationStatusListener; import org.sonar.server.platform.db.migration.step.MigrationStep; import org.sonar.server.platform.db.migration.step.MigrationStepRegistryImpl; import org.sonar.server.platform.db.migration.step.MigrationStepsExecutor; @@ -96,7 +97,7 @@ public class MigrationContainerImplTest { private static class NoOpExecutor implements MigrationStepsExecutor { @Override - public void execute(List steps) { + public void execute(List steps, MigrationStatusListener listener) { } } diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/engine/MigrationEngineImplTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/engine/MigrationEngineImplTest.java index d23648a4082..9e079186159 100644 --- a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/engine/MigrationEngineImplTest.java +++ b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/engine/MigrationEngineImplTest.java @@ -22,15 +22,13 @@ package org.sonar.server.platform.db.migration.engine; import java.sql.SQLException; import java.util.List; import java.util.Optional; -import org.junit.Before; -import org.junit.Test; -import org.sonar.api.config.internal.MapSettings; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.sonar.core.platform.SpringComponentContainer; -import org.sonar.server.platform.db.migration.SupportsBlueGreen; import org.sonar.server.platform.db.migration.history.MigrationHistory; import org.sonar.server.platform.db.migration.step.MigrationStep; import org.sonar.server.platform.db.migration.step.MigrationSteps; -import org.sonar.server.platform.db.migration.step.MigrationStepsExecutor; +import org.sonar.server.platform.db.migration.step.NoOpMigrationStatusListener; import org.sonar.server.platform.db.migration.step.RegisteredMigrationStep; import static java.util.Collections.singletonList; @@ -40,16 +38,15 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -public class MigrationEngineImplTest { +class MigrationEngineImplTest { private final MigrationHistory migrationHistory = mock(MigrationHistory.class); private final SpringComponentContainer serverContainer = new SpringComponentContainer(); private final MigrationSteps migrationSteps = mock(MigrationSteps.class); private final StepRegistry stepRegistry = new StepRegistry(); - private final MapSettings settings = new MapSettings(); private final MigrationEngineImpl underTest = new MigrationEngineImpl(migrationHistory, serverContainer, migrationSteps); - @Before - public void before() { + @BeforeEach + void before() { serverContainer.add(migrationSteps); serverContainer.add(migrationHistory); serverContainer.add(stepRegistry); @@ -57,63 +54,40 @@ public class MigrationEngineImplTest { } @Test - public void execute_execute_all_steps_of_there_is_no_last_migration_number() { + void execute_execute_all_steps_of_there_is_no_last_migration_number() { when(migrationHistory.getLastMigrationNumber()).thenReturn(Optional.empty()); List steps = singletonList(new RegisteredMigrationStep(1, "doo", TestMigrationStep.class)); when(migrationSteps.readAll()).thenReturn(steps); - underTest.execute(); + underTest.execute(new NoOpMigrationStatusListener()); verify(migrationSteps, times(2)).readAll(); assertThat(stepRegistry.stepRan).isTrue(); } @Test - public void execute_execute_steps_from_last_migration_number_plus_1() { + void execute_execute_steps_from_last_migration_number_plus_1() { when(migrationHistory.getLastMigrationNumber()).thenReturn(Optional.of(50L)); List steps = singletonList(new RegisteredMigrationStep(1, "doo", TestMigrationStep.class)); when(migrationSteps.readFrom(51)).thenReturn(steps); when(migrationSteps.readAll()).thenReturn(steps); - underTest.execute(); + underTest.execute(new NoOpMigrationStatusListener()); verify(migrationSteps).readFrom(51); assertThat(stepRegistry.stepRan).isTrue(); } - private static class NoOpExecutor implements MigrationStepsExecutor { - @Override - public void execute(List steps) { - // no op - } - } - private static class StepRegistry { boolean stepRan = false; } - private static class TestMigrationStep implements MigrationStep { - private final StepRegistry registry; + private record TestMigrationStep(StepRegistry registry) implements MigrationStep { - public TestMigrationStep(StepRegistry registry) { - this.registry = registry; - } @Override - public void execute() throws SQLException { - registry.stepRan = true; + public void execute() throws SQLException { + registry.stepRan = true; + } } - } - @SupportsBlueGreen - private static class TestBlueGreenMigrationStep implements MigrationStep { - private final StepRegistry registry; - - public TestBlueGreenMigrationStep(StepRegistry registry) { - this.registry = registry; - } - @Override - public void execute() throws SQLException { - registry.stepRan = true; - } - } } diff --git a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/step/MigrationStepsExecutorImplTest.java b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/step/MigrationStepsExecutorImplTest.java index e3aae287333..7eb31d0472b 100644 --- a/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/step/MigrationStepsExecutorImplTest.java +++ b/server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/step/MigrationStepsExecutorImplTest.java @@ -47,10 +47,11 @@ public class MigrationStepsExecutorImplTest { private MigrationContainer migrationContainer = new SimpleMigrationContainer(); private MigrationHistory migrationHistor = mock(MigrationHistory.class); private MigrationStepsExecutorImpl underTest = new MigrationStepsExecutorImpl(migrationContainer, migrationHistor); + private NoOpMigrationStatusListener noOpMigrationStatusListener = new NoOpMigrationStatusListener(); @Test public void execute_does_not_fail_when_stream_is_empty_and_log_start_stop_INFO() { - underTest.execute(Collections.emptyList()); + underTest.execute(Collections.emptyList(), null); assertThat(logTester.logs()).hasSize(2); assertLogLevel(Level.INFO, "Executing DB migrations...", "Executed DB migrations: success | time="); @@ -62,7 +63,7 @@ public class MigrationStepsExecutorImplTest { ((SpringComponentContainer) migrationContainer).startComponents(); try { - underTest.execute(steps); + underTest.execute(steps, noOpMigrationStatusListener); fail("execute should have thrown a IllegalStateException"); } catch (IllegalStateException e) { assertThat(e).hasMessage("Unable to load component " + MigrationStep1.class); @@ -94,7 +95,7 @@ public class MigrationStepsExecutorImplTest { underTest.execute(asList( registeredStepOf(1, MigrationStep2.class), registeredStepOf(2, MigrationStep1.class), - registeredStepOf(3, MigrationStep3.class))); + registeredStepOf(3, MigrationStep3.class)), new NoOpMigrationStatusListener()); assertThat(SingleCallCheckerMigrationStep.calledSteps) .containsExactly(MigrationStep2.class, MigrationStep1.class, MigrationStep3.class); @@ -124,7 +125,7 @@ public class MigrationStepsExecutorImplTest { ((SpringComponentContainer) migrationContainer).startComponents(); try { - underTest.execute(steps); + underTest.execute(steps, noOpMigrationStatusListener); fail("a MigrationStepExecutionException should have been thrown"); } catch (MigrationStepExecutionException e) { assertThat(e).hasMessage("Execution of migration step #2 '2-SqlExceptionFailingMigrationStep' failed"); @@ -153,7 +154,7 @@ public class MigrationStepsExecutorImplTest { ((SpringComponentContainer) migrationContainer).startComponents(); try { - underTest.execute(steps); + underTest.execute(steps, noOpMigrationStatusListener); fail("should throw MigrationStepExecutionException"); } catch (MigrationStepExecutionException e) { assertThat(e).hasMessage("Execution of migration step #2 '2-RuntimeExceptionFailingMigrationStep' failed"); diff --git a/server/sonar-db-migration/src/testFixtures/java/org/sonar/db/SQDatabase.java b/server/sonar-db-migration/src/testFixtures/java/org/sonar/db/SQDatabase.java index fd390854701..29d3c8419d9 100644 --- a/server/sonar-db-migration/src/testFixtures/java/org/sonar/db/SQDatabase.java +++ b/server/sonar-db-migration/src/testFixtures/java/org/sonar/db/SQDatabase.java @@ -45,6 +45,7 @@ import org.sonar.server.platform.db.migration.MigrationConfigurationModule; import org.sonar.server.platform.db.migration.engine.MigrationContainer; import org.sonar.server.platform.db.migration.engine.MigrationContainerImpl; import org.sonar.server.platform.db.migration.history.MigrationHistoryTableImpl; +import org.sonar.server.platform.db.migration.step.MigrationStatusListener; import org.sonar.server.platform.db.migration.step.MigrationStep; import org.sonar.server.platform.db.migration.step.MigrationStepExecutionException; import org.sonar.server.platform.db.migration.step.MigrationSteps; @@ -144,7 +145,7 @@ public class SQDatabase extends DefaultDatabase { } @Override - public void execute(List steps) { + public void execute(List steps, MigrationStatusListener listener) { steps.forEach(step -> execute(step, container)); } @@ -195,7 +196,7 @@ public class SQDatabase extends DefaultDatabase { if (stepClass != null) { steps = filterUntilStep(steps, stepClass); } - executor.execute(steps); + executor.execute(steps, null); } private static List filterUntilStep(List steps, Class stepClass) { diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/migration/AutoDbMigration.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/migration/AutoDbMigration.java index e6b10d6dc44..c6b2bef3da8 100644 --- a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/migration/AutoDbMigration.java +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/migration/AutoDbMigration.java @@ -23,6 +23,7 @@ import org.sonar.api.Startable; import org.slf4j.LoggerFactory; import org.sonar.server.platform.DefaultServerUpgradeStatus; import org.sonar.server.platform.db.migration.engine.MigrationEngine; +import org.sonar.server.platform.db.migration.step.NoOpMigrationStatusListener; public class AutoDbMigration implements Startable { private final DefaultServerUpgradeStatus serverUpgradeStatus; @@ -37,10 +38,10 @@ public class AutoDbMigration implements Startable { public void start() { if (serverUpgradeStatus.isFreshInstall()) { LoggerFactory.getLogger(getClass()).info("Automatically perform DB migration on fresh install"); - migrationEngine.execute(); + migrationEngine.execute(new NoOpMigrationStatusListener()); } else if (serverUpgradeStatus.isUpgraded() && serverUpgradeStatus.isAutoDbUpgrade()) { LoggerFactory.getLogger(getClass()).info("Automatically perform DB migration, as automatic database upgrade is enabled"); - migrationEngine.execute(); + migrationEngine.execute(new NoOpMigrationStatusListener()); } } diff --git a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/migration/DatabaseMigrationImpl.java b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/migration/DatabaseMigrationImpl.java index f116466532e..9ddd8346604 100644 --- a/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/migration/DatabaseMigrationImpl.java +++ b/server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/migration/DatabaseMigrationImpl.java @@ -27,6 +27,7 @@ import org.sonar.core.util.logs.Profiler; import org.sonar.server.platform.Platform; import org.sonar.server.platform.db.migration.DatabaseMigrationState.Status; import org.sonar.server.platform.db.migration.engine.MigrationEngine; +import org.sonar.server.platform.db.migration.step.MigrationStatusListenerImpl; import org.sonar.server.platform.db.migration.step.MigrationStepExecutionException; /** @@ -102,7 +103,7 @@ public class DatabaseMigrationImpl implements DatabaseMigration { private void doUpgradeDb() { Profiler profiler = Profiler.createIfTrace(LOGGER); profiler.startTrace("Starting DB Migration"); - migrationEngine.execute(); + migrationEngine.execute(new MigrationStatusListenerImpl(migrationState)); profiler.stopTrace("DB Migration ended"); } diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/migration/AutoDbMigrationTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/migration/AutoDbMigrationTest.java index 8ac4ba02b10..b100e02b0cd 100644 --- a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/migration/AutoDbMigrationTest.java +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/migration/AutoDbMigrationTest.java @@ -34,6 +34,7 @@ import org.sonar.server.platform.DefaultServerUpgradeStatus; import org.sonar.server.platform.db.migration.engine.MigrationEngine; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; @@ -74,7 +75,7 @@ public class AutoDbMigrationTest { underTest.start(); - verify(migrationEngine).execute(); + verify(migrationEngine).execute(any()); verifyInfoLog(); } @@ -96,7 +97,7 @@ public class AutoDbMigrationTest { underTest.start(); - verify(migrationEngine).execute(); + verify(migrationEngine).execute(any()); assertThat(logTester.logs(Level.INFO)).contains("Automatically perform DB migration, as automatic database upgrade is enabled"); } diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/migration/DatabaseMigrationImplConcurrentAccessTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/migration/DatabaseMigrationImplConcurrentAccessTest.java index 1d3aae9e247..07b055255e5 100644 --- a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/migration/DatabaseMigrationImplConcurrentAccessTest.java +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/migration/DatabaseMigrationImplConcurrentAccessTest.java @@ -29,6 +29,8 @@ import org.junit.After; import org.junit.Test; import org.sonar.server.platform.Platform; import org.sonar.server.platform.db.migration.engine.MigrationEngine; +import org.sonar.server.platform.db.migration.step.MigrationStatusListener; +import org.sonar.server.platform.db.migration.step.NoOpMigrationStatusListener; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -54,7 +56,7 @@ public class DatabaseMigrationImplConcurrentAccessTest { private AtomicInteger triggerCount = new AtomicInteger(); private MigrationEngine incrementingMigrationEngine = new MigrationEngine() { @Override - public void execute() { + public void execute(MigrationStatusListener listener){ // need execute to consume some time to avoid UT to fail because it ran too fast and threads never executed concurrently try { Thread.sleep(200); diff --git a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/migration/DatabaseMigrationImplTest.java b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/migration/DatabaseMigrationImplTest.java index 24023f2fed6..0cab425a9ca 100644 --- a/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/migration/DatabaseMigrationImplTest.java +++ b/server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/migration/DatabaseMigrationImplTest.java @@ -26,6 +26,7 @@ import org.sonar.server.platform.Platform; import org.sonar.server.platform.db.migration.engine.MigrationEngine; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.inOrder; @@ -40,24 +41,24 @@ public class DatabaseMigrationImplTest { /** * Implementation of execute runs Runnable synchronously. */ - private DatabaseMigrationExecutorService executorService = new DatabaseMigrationExecutorServiceAdaptor() { + private final DatabaseMigrationExecutorService executorService = new DatabaseMigrationExecutorServiceAdaptor() { @Override public void execute(Runnable command) { command.run(); } }; - private MutableDatabaseMigrationState migrationState = new DatabaseMigrationStateImpl(); - private Platform platform = mock(Platform.class); - private MigrationEngine migrationEngine = mock(MigrationEngine.class); - private InOrder inOrder = inOrder(platform, migrationEngine); + private final MutableDatabaseMigrationState migrationState = new DatabaseMigrationStateImpl(); + private final Platform platform = mock(Platform.class); + private final MigrationEngine migrationEngine = mock(MigrationEngine.class); + private final InOrder inOrder = inOrder(platform, migrationEngine); - private DatabaseMigrationImpl underTest = new DatabaseMigrationImpl(executorService, migrationState, migrationEngine, platform); + private final DatabaseMigrationImpl underTest = new DatabaseMigrationImpl(executorService, migrationState, migrationEngine, platform); @Test public void startit_calls_MigrationEngine_execute() { underTest.startIt(); - inOrder.verify(migrationEngine).execute(); + inOrder.verify(migrationEngine).execute(any()); inOrder.verify(platform).doStart(); inOrder.verifyNoMoreInteractions(); } @@ -103,10 +104,10 @@ public class DatabaseMigrationImplTest { } private void mockMigrationThrowsError() { - doThrow(AN_ERROR).when(migrationEngine).execute(); + doThrow(AN_ERROR).when(migrationEngine).execute(any()); } private void mockMigrationDoesNothing() { - doNothing().when(migrationEngine).execute(); + doNothing().when(migrationEngine).execute(any()); } } diff --git a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/system/controller/DatabaseMigrationsController.java b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/system/controller/DatabaseMigrationsController.java index d585473bc56..0bc47b5d54e 100644 --- a/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/system/controller/DatabaseMigrationsController.java +++ b/server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/system/controller/DatabaseMigrationsController.java @@ -20,12 +20,13 @@ package org.sonar.server.v2.api.system.controller; import io.swagger.v3.oas.annotations.Operation; +import java.text.SimpleDateFormat; import java.util.Optional; +import java.util.TimeZone; import javax.annotation.Nullable; import org.sonar.db.Database; import org.sonar.server.platform.db.migration.DatabaseMigrationState; import org.sonar.server.platform.db.migration.version.DatabaseVersion; -import org.sonar.server.v2.common.DateString; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -43,10 +44,13 @@ public class DatabaseMigrationsController { private final DatabaseMigrationState databaseMigrationState; private final Database database; + private static final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); + public DatabaseMigrationsController(DatabaseVersion databaseVersion, DatabaseMigrationState databaseMigrationState, Database database) { this.databaseVersion = databaseVersion; this.databaseMigrationState = databaseMigrationState; this.database = database; + simpleDateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); } @Operation(summary = "Gets the status of ongoing database migrations, if any", description = "Return the detailed status of ongoing database migrations" + @@ -70,16 +74,25 @@ public class DatabaseMigrationsController { } - public record DatabaseMigrationsResponse(String status, @Nullable String startedAt, @Nullable String message) { + public record DatabaseMigrationsResponse( + String status, + @Nullable Integer completedSteps, + @Nullable Integer totalSteps, + @Nullable String startedAt, + @Nullable String message, + @Nullable String expectedFinishTimestamp) { public DatabaseMigrationsResponse(DatabaseMigrationState state) { this(state.getStatus().toString(), - DateString.from(state.getStartedAt()), - state.getError() != null ? state.getError().getMessage() : state.getStatus().getMessage()); + state.getCompletedMigrations(), + state.getTotalMigrations(), + state.getStartedAt() != null ? simpleDateFormat.format(state.getStartedAt()) : null, + state.getError() != null ? state.getError().getMessage() : state.getStatus().getMessage(), + state.getExpectedFinishDate() != null ? simpleDateFormat.format(state.getExpectedFinishDate()) : null); } public DatabaseMigrationsResponse(DatabaseMigrationState.Status status) { - this(status.toString(), null, status.getMessage()); + this(status.toString(), null, null, null, status.getMessage(), null); } } diff --git a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/system/controller/DatabaseMigrationsControllerTest.java b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/system/controller/DatabaseMigrationsControllerTest.java index 5b2bc2d5f05..dec18c62cb9 100644 --- a/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/system/controller/DatabaseMigrationsControllerTest.java +++ b/server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/system/controller/DatabaseMigrationsControllerTest.java @@ -19,8 +19,10 @@ */ package org.sonar.server.v2.api.system.controller; +import java.text.SimpleDateFormat; import java.util.Date; import java.util.Optional; +import java.util.TimeZone; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -44,6 +46,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. class DatabaseMigrationsControllerTest { private static final Date SOME_DATE = new Date(); + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); + private final DatabaseVersion databaseVersion = mock(); private final DatabaseMigrationState migrationState = mock(); private final Dialect dialect = mock(Dialect.class); @@ -98,9 +102,15 @@ class DatabaseMigrationsControllerTest { when(dialect.supportsMigration()).thenReturn(true); when(migrationState.getStatus()).thenReturn(RUNNING); when(migrationState.getStartedAt()).thenReturn(SOME_DATE); + when(migrationState.getExpectedFinishDate()).thenReturn(SOME_DATE); + when(migrationState.getCompletedMigrations()).thenReturn(1); + when(migrationState.getTotalMigrations()).thenReturn(10); + + DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC")); mockMvc.perform(get(DATABASE_MIGRATIONS_ENDPOINT)).andExpectAll(status().isOk(), - content().json("{\"status\":\"MIGRATION_RUNNING\",\"message\":\"Database migration is running.\"}")); + content().json("{\"status\":\"MIGRATION_RUNNING\",\"completedSteps\":1,\"totalSteps\":10," + + "\"message\":\"Database migration is running.\",\"expectedFinishTimestamp\":\""+DATE_FORMAT.format(SOME_DATE)+"\"}")); } @Test @@ -125,7 +135,6 @@ class DatabaseMigrationsControllerTest { content().json("{\"status\":\"MIGRATION_SUCCEEDED\",\"message\":\"Migration succeeded.\"}")); } - @Test void getStatus_whenMigrationRequired_returnMigrationRequired() throws Exception { when(databaseVersion.getStatus()).thenReturn(DatabaseVersion.Status.REQUIRES_UPGRADE); -- 2.39.5