@@ -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); | |||
} | |||
} |
@@ -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(); | |||
} |
@@ -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(); | |||
} | |||
} |
@@ -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); | |||
} |
@@ -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); | |||
} |
@@ -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(); |
@@ -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); | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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); | |||
} |
@@ -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) { |
@@ -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 | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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) { | |||
} | |||
} |
@@ -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; | |||
} | |||
} | |||
} |
@@ -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"); |
@@ -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) { |
@@ -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()); | |||
} | |||
} | |||
@@ -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"); | |||
} | |||
@@ -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"); | |||
} | |||
@@ -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); |
@@ -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()); | |||
} | |||
} |
@@ -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); | |||
} | |||
} | |||
@@ -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); |