--- /dev/null
+/*
+ * 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);
+ }
+
+}
*/
@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();
+
}
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() {
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();
+ }
+
}
void setStartedAt(@Nullable Date startedAt);
void setError(@Nullable Throwable error);
+
+ void incrementCompletedMigrations();
+
+ void setTotalMigrations(int completedMigrations);
}
*/
package org.sonar.server.platform.db.migration.engine;
+import org.sonar.server.platform.db.migration.step.MigrationStatusListener;
+
/**
* This class is responsible for:
* <ul>
* </ul>
*/
public interface MigrationEngine {
- void execute();
+ void execute(MigrationStatusListener listener);
}
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;
}
@Override
- public void execute() {
+ public void execute(MigrationStatusListener listener) {
MigrationContainer migrationContainer = new MigrationContainerImpl(serverContainer, MigrationStepsExecutorImpl.class);
try {
MigrationStepsExecutor stepsExecutor = migrationContainer.getComponentByType(MigrationStepsExecutor.class);
.map(i -> migrationSteps.readFrom(i + 1))
.orElse(migrationSteps.readAll());
-
- stepsExecutor.execute(steps);
+ listener.onMigrationsStart(steps.size());
+ stepsExecutor.execute(steps, listener);
} finally {
migrationContainer.cleanup();
--- /dev/null
+/*
+ * 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);
+
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
/**
* @throws MigrationStepExecutionException at the first failing migration step execution
*/
- void execute(List<RegisteredMigrationStep> steps);
+ void execute(List<RegisteredMigrationStep> steps, MigrationStatusListener listener);
}
}
@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) {
--- /dev/null
+/*
+ * 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
+ }
+}
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);
}
@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);
+ }
+
}
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;
private static class NoOpExecutor implements MigrationStepsExecutor {
@Override
- public void execute(List<RegisteredMigrationStep> steps) {
+ public void execute(List<RegisteredMigrationStep> steps, MigrationStatusListener listener) {
}
}
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;
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);
}
@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;
- }
- }
}
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=");
((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);
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);
((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");
((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");
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;
}
@Override
- public void execute(List<RegisteredMigrationStep> steps) {
+ public void execute(List<RegisteredMigrationStep> steps, MigrationStatusListener listener) {
steps.forEach(step -> execute(step, container));
}
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) {
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;
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());
}
}
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;
/**
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");
}
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;
underTest.start();
- verify(migrationEngine).execute();
+ verify(migrationEngine).execute(any());
verifyInfoLog();
}
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");
}
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;
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);
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;
/**
* 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();
}
}
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());
}
}
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;
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" +
}
- 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);
}
}
*/
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;
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);
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
content().json("{\"status\":\"MIGRATION_SUCCEEDED\",\"message\":\"Migration succeeded.\"}"));
}
-
@Test
void getStatus_whenMigrationRequired_returnMigrationRequired() throws Exception {
when(databaseVersion.getStatus()).thenReturn(DatabaseVersion.Status.REQUIRES_UPGRADE);