]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-22141 add new fields in the response of migrations-status endpoint
authorMatteo Mara <matteo.mara@sonarsource.com>
Thu, 2 May 2024 15:51:27 +0000 (17:51 +0200)
committersonartech <sonartech@sonarsource.com>
Wed, 8 May 2024 20:02:44 +0000 (20:02 +0000)
23 files changed:
server/sonar-db-migration/src/it/java/org/sonar/server/platform/db/migration/step/MigrationStatusListenerImplTest.java [new file with mode: 0644]
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/DatabaseMigrationState.java
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/DatabaseMigrationStateImpl.java
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/MutableDatabaseMigrationState.java
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/engine/MigrationEngine.java
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/engine/MigrationEngineImpl.java
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/step/MigrationStatusListener.java [new file with mode: 0644]
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/step/MigrationStatusListenerImpl.java [new file with mode: 0644]
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/step/MigrationStepsExecutor.java
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/step/MigrationStepsExecutorImpl.java
server/sonar-db-migration/src/main/java/org/sonar/server/platform/db/migration/step/NoOpMigrationStatusListener.java [new file with mode: 0644]
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/DatabaseMigrationStateImplTest.java
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/engine/MigrationContainerImplTest.java
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/engine/MigrationEngineImplTest.java
server/sonar-db-migration/src/test/java/org/sonar/server/platform/db/migration/step/MigrationStepsExecutorImplTest.java
server/sonar-db-migration/src/testFixtures/java/org/sonar/db/SQDatabase.java
server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/migration/AutoDbMigration.java
server/sonar-webserver-core/src/main/java/org/sonar/server/platform/db/migration/DatabaseMigrationImpl.java
server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/migration/AutoDbMigrationTest.java
server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/migration/DatabaseMigrationImplConcurrentAccessTest.java
server/sonar-webserver-core/src/test/java/org/sonar/server/platform/db/migration/DatabaseMigrationImplTest.java
server/sonar-webserver-webapi-v2/src/main/java/org/sonar/server/v2/api/system/controller/DatabaseMigrationsController.java
server/sonar-webserver-webapi-v2/src/test/java/org/sonar/server/v2/api/system/controller/DatabaseMigrationsControllerTest.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 (file)
index 0000000..b406c27
--- /dev/null
@@ -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);
+  }
+
+}
index 82bb1204e1cae8fe159b05f0d899b3d0de304c1d..ff2f310e6b524fdab30453b2121080e982418390 100644 (file)
@@ -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();
+
 }
index daa970663153697fb856bd1c0e4ef3fffae7e54e..5951e58871b922c4c8f2a15eede847642434a890 100644 (file)
@@ -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();
+  }
+
 }
index f283dfc411bdb28f1cad3f7c651110f36a2bbc74..fb1d64ce71f20922a4b0c009903a09f097ae42c4 100644 (file)
@@ -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);
 }
index 8dcd0fb4b01fed8147625b515b8fd570f542e7b3..cffea5ccab92ca62f6ac2dd51be875447d1306b8 100644 (file)
@@ -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:
  * <ul>
@@ -29,5 +31,5 @@ package org.sonar.server.platform.db.migration.engine;
  * </ul>
  */
 public interface MigrationEngine {
-  void execute();
+  void execute(MigrationStatusListener listener);
 }
index 5dbd6bd974d19ad17336fddcbd5fa40cf92d63c3..7e87116e853451e01bfea1d67228de30973837c3 100644 (file)
@@ -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 (file)
index 0000000..6143052
--- /dev/null
@@ -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 (file)
index 0000000..8d02058
--- /dev/null
@@ -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);
+  }
+}
index 13ef91aa6d80c98c980f352f26c6c3fd3e38f6e4..2e321cbda1b59b3e54d258c024225379079948ab 100644 (file)
@@ -34,5 +34,5 @@ public interface MigrationStepsExecutor {
   /**
    * @throws MigrationStepExecutionException at the first failing migration step execution
    */
-  void execute(List<RegisteredMigrationStep> steps);
+  void execute(List<RegisteredMigrationStep> steps, MigrationStatusListener listener);
 }
index ee5c55d427c816cb111e63e2f03ca7c0402cf29a..e7105c0eff224fc6fa055d5aa6d7f963670d62db 100644 (file)
@@ -44,12 +44,15 @@ public class MigrationStepsExecutorImpl implements MigrationStepsExecutor {
   }
 
   @Override
-  public void execute(List<RegisteredMigrationStep> steps) {
+  public void execute(List<RegisteredMigrationStep> 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 (file)
index 0000000..a8d170d
--- /dev/null
@@ -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
+  }
+}
index 33c39c5e0a024a6dd391bd2ad33b6e64d51c8900..1c2813b4c3a95c0af397e70cc3578b712036d6e9 100644 (file)
 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);
+  }
+  
 }
index 4ccd4b9806972cebc510adce9bef0e40c4fd3588..13e99bb6bbc22b3f9850b46b990dd8e32ac7354b 100644 (file)
@@ -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<RegisteredMigrationStep> steps) {
+    public void execute(List<RegisteredMigrationStep> steps, MigrationStatusListener listener) {
 
     }
   }
index d23648a4082746ae9026abcefb22c2b32de88fac..9e07918615982c6b3e6f47eff53dcd267ae4f228 100644 (file)
@@ -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<RegisteredMigrationStep> 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<RegisteredMigrationStep> 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<RegisteredMigrationStep> 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;
-    }
-  }
 }
index e3aae2873331584f4bbb24025e80143a7f01ed44..7eb31d0472bf4588fdfbc2380bccf3d2d42613cc 100644 (file)
@@ -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");
index fd390854701e16562112dfee908c3ae2284a29e9..29d3c8419d9b73192b28f6cff1addfe782fb9a4b 100644 (file)
@@ -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<RegisteredMigrationStep> steps) {
+    public void execute(List<RegisteredMigrationStep> 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<RegisteredMigrationStep> filterUntilStep(List<RegisteredMigrationStep> steps, Class<? extends MigrationStep> stepClass) {
index e6b10d6dc44e29141ba32dfd621f04d8f2818300..c6b2bef3da827221e95837575bed8fa357c5106c 100644 (file)
@@ -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());
     }
   }
 
index f116466532edf37bb4cfd51b146406b08a58a6f0..9ddd83466044ba511280ac4d047261cf2cf1b1f5 100644 (file)
@@ -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");
   }
 
index 8ac4ba02b10371c741d60efdc8dbe44b5460b772..b100e02b0cd0caffcc7856bfa3dcb2e5f0db2382 100644 (file)
@@ -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");
   }
 
index 1d3aae9e24772b5adccaec85f10f9ed506071cd2..07b055255e5d8411bb576abe8b84e519ae5177a6 100644 (file)
@@ -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);
index 24023f2fed6f63c8dcd183e92e29daae1c95c189..0cab425a9cae68d5ff2902fc783dd5c70ca95032 100644 (file)
@@ -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());
   }
 }
index d585473bc563f9707d13e5f2ec0ae31116849f6a..0bc47b5d54e042eb039e0166a6a5c1b14c8e7624 100644 (file)
 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);
     }
   }
 
index 5b2bc2d5f0558ae67e8d6ee87ab9527065ea956d..dec18c62cb9617d3dabcea2879cf65f38fa36b8b 100644 (file)
  */
 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);