From: Sébastien Lesaint Date: Tue, 7 Apr 2015 09:02:21 +0000 (+0200) Subject: SONAR-6366 add DatabaseMigration component X-Git-Tag: 5.2-RC1~2228 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=31c36d7682f21af45d0029136267f9611b10f067;p=sonarqube.git SONAR-6366 add DatabaseMigration component this component handles concurrency and asynchronous execution of the DB migration job when called from Java --- diff --git a/server/sonar-server/src/main/java/org/sonar/server/db/migrations/DatabaseMigration.java b/server/sonar-server/src/main/java/org/sonar/server/db/migrations/DatabaseMigration.java new file mode 100644 index 00000000000..e7e5a56c27c --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/db/migrations/DatabaseMigration.java @@ -0,0 +1,70 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.db.migrations; + +import javax.annotation.CheckForNull; +import java.util.Date; + +public interface DatabaseMigration { + enum Status { + NONE, RUNNING, FAILED, SUCCEEDED + } + + /** + * Starts the migration status and returns immediately. + *

+ * Migration can not be started twice but calling this method wont raise an error. + * On the other hand, calling this method when no migration is needed will start the process anyway. + *

+ *

+ * This method should be named {@code start} but it can not be because it will be called by the pico container + * and this will cause unwanted behavior + *

+ */ + void startIt(); + + /** + * The time and day the last migration was started. + *

+ * If no migration was ever started, the returned date is {@code null}. This value is reset when {@link #startIt()} is + * called. + *

+ * + * @return a {@link Date} or {@code null} + */ + @CheckForNull + Date startedAt(); + + /** + * Current status of the migration. + */ + Status status(); + + /** + * The error of the last migration if it failed. + *

+ * This value is reset when {@link #startIt()} is called. + *

+ * @return a {@link Throwable} or {@code null} + */ + @CheckForNull + Throwable failureError(); + +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/db/migrations/PlatformDatabaseMigration.java b/server/sonar-server/src/main/java/org/sonar/server/db/migrations/PlatformDatabaseMigration.java new file mode 100644 index 00000000000..3425c284e92 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/db/migrations/PlatformDatabaseMigration.java @@ -0,0 +1,126 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.db.migrations; + +import org.sonar.api.utils.log.Loggers; +import org.sonar.server.ruby.RubyBridge; + +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import java.util.Date; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Handles concurrency to make sure only one DB migration can run at a time. + */ +public class PlatformDatabaseMigration implements DatabaseMigration { + + private final RubyBridge rubyBridge; + /** + * ExecutorService implements threads management. + */ + private final PlatformDatabaseMigrationExecutorService executorService; + /** + * This lock implements thread safety from concurrent calls of method {@link #startIt()} + */ + private final ReentrantLock lock = new ReentrantLock(); + + /** + * This property acts as a semaphore to make sure at most one db migration task is created at a time. + *

+ * It is set to {@code true} by the first thread to execute the {@link #startIt()} method and set to {@code false} + * by the thread executing the db migration. + *

+ */ + private AtomicBoolean running = new AtomicBoolean(false); + private Status status = Status.NONE; + @Nullable + private Date startDate; + @Nullable + private Throwable failureError; + + public PlatformDatabaseMigration(RubyBridge rubyBridge, PlatformDatabaseMigrationExecutorService executorService) { + this.rubyBridge = rubyBridge; + this.executorService = executorService; + } + + @Override + public void startIt() { + if (lock.isLocked() || this.running.get() /* fail-fast if db migration is running */) { + return; + } + + lock.lock(); + try { + startAsynchronousDBMigration(); + } finally { + lock.unlock(); + } + } + + /** + * This method is not thread safe and must be external protected from concurrent executions. + */ + private void startAsynchronousDBMigration() { + if (this.running.get()) { + return; + } + + running.getAndSet(true); + executorService.execute(new Runnable() { + @Override + public void run() { + status = Status.RUNNING; + startDate = new Date(); + failureError = null; + try { + Loggers.get(PlatformDatabaseMigration.class).info("Starting DB Migration at {}", startDate); + rubyBridge.databaseMigration().trigger(); + Loggers.get(PlatformDatabaseMigration.class).info("DB Migration ended successfully at {}", new Date()); + status = Status.SUCCEEDED; + } catch (Throwable t) { + Loggers.get(PlatformDatabaseMigration.class).error("DB Migration failed and ended at " + startDate + " with an exception", t); + status = Status.FAILED; + failureError = t; + } finally { + running.getAndSet(false); + } + } + }); + } + + @Override + @CheckForNull + public Date startedAt() { + return this.startDate; + } + + @Override + public Status status() { + return this.status; + } + + @Override + @CheckForNull + public Throwable failureError() { + return this.failureError; + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/db/migrations/PlatformDatabaseMigrationExecutorService.java b/server/sonar-server/src/main/java/org/sonar/server/db/migrations/PlatformDatabaseMigrationExecutorService.java new file mode 100644 index 00000000000..6efa645018a --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/db/migrations/PlatformDatabaseMigrationExecutorService.java @@ -0,0 +1,29 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.db.migrations; + +import java.util.concurrent.ExecutorService; + +/** + * Flag interface for the ExecutorService to be used by the {@link PlatformDatabaseMigration} + * component. + */ +public interface PlatformDatabaseMigrationExecutorService extends ExecutorService { +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/db/migrations/PlatformDatabaseMigrationExecutorServiceImpl.java b/server/sonar-server/src/main/java/org/sonar/server/db/migrations/PlatformDatabaseMigrationExecutorServiceImpl.java new file mode 100644 index 00000000000..e1056105507 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/db/migrations/PlatformDatabaseMigrationExecutorServiceImpl.java @@ -0,0 +1,37 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.db.migrations; + +import org.sonar.server.util.AbstractStoppableExecutorService; + +import java.util.concurrent.Executors; + +/** + * Since only one DB migration can run at a time, this implementation of PlatformDatabaseMigrationExecutorService + * wraps a single thread executor from the JDK. + */ +public class PlatformDatabaseMigrationExecutorServiceImpl + extends AbstractStoppableExecutorService + implements PlatformDatabaseMigrationExecutorService { + + public PlatformDatabaseMigrationExecutorServiceImpl() { + super(Executors.newSingleThreadExecutor()); + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/ServerComponents.java b/server/sonar-server/src/main/java/org/sonar/server/platform/ServerComponents.java index 1209df754c4..32e598f6b80 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/ServerComponents.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/ServerComponents.java @@ -103,6 +103,8 @@ import org.sonar.server.dashboard.ws.DashboardsWebService; import org.sonar.server.db.DatabaseChecker; import org.sonar.server.db.DbClient; import org.sonar.server.db.EmbeddedDatabaseFactory; +import org.sonar.server.db.migrations.PlatformDatabaseMigration; +import org.sonar.server.db.migrations.PlatformDatabaseMigrationExecutorServiceImpl; import org.sonar.server.db.migrations.MigrationSteps; import org.sonar.server.db.migrations.DatabaseMigrator; import org.sonar.server.debt.*; @@ -359,7 +361,10 @@ class ServerComponents { ServerMetadataPersister.class, DefaultHttpDownloader.class, UriReader.class, - ServerIdGenerator.class + ServerIdGenerator.class, + + PlatformDatabaseMigrationExecutorServiceImpl.class, + PlatformDatabaseMigration.class ); } diff --git a/server/sonar-server/src/test/java/org/sonar/server/db/migrations/PlatformDatabaseMigrationAsynchronousTest.java b/server/sonar-server/src/test/java/org/sonar/server/db/migrations/PlatformDatabaseMigrationAsynchronousTest.java new file mode 100644 index 00000000000..1eb35d1873b --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/db/migrations/PlatformDatabaseMigrationAsynchronousTest.java @@ -0,0 +1,73 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.db.migrations; + +import com.google.common.base.Throwables; +import org.junit.After; +import org.junit.Before; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.sonar.server.ruby.RubyBridge; +import org.sonar.server.ruby.RubyDatabaseMigration; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class PlatformDatabaseMigrationAsynchronousTest { + + private ExecutorService pool = Executors.newFixedThreadPool(2); + private ExecutorService delegate = Executors.newSingleThreadExecutor(); + /** + * Implementation of execute wraps specified Runnable to add a delay of 200 ms before passing it + * to a SingleThread executor to execute asynchronously. + */ + private PlatformDatabaseMigrationExecutorService executorService = new PlatformDatabaseMigrationExecutorServiceAdaptor() { + @Override + public void execute(final Runnable command) { + delegate.execute(new Runnable() { + @Override + public void run() { + try { + Thread.currentThread().wait(200); + } catch (InterruptedException e) { + Throwables.propagate(e); + } + command.run(); + } + }); + } + }; + @Mock + private RubyDatabaseMigration rubyDatabaseMigration; + @Mock + private RubyBridge rubyBridge; + private PlatformDatabaseMigration underTest; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + underTest = new PlatformDatabaseMigration(rubyBridge, executorService); + } + + @After + public void tearDown() throws Exception { + delegate.shutdownNow(); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/db/migrations/PlatformDatabaseMigrationConcurrentAccessTest.java b/server/sonar-server/src/test/java/org/sonar/server/db/migrations/PlatformDatabaseMigrationConcurrentAccessTest.java new file mode 100644 index 00000000000..6f6cfa1b3dd --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/db/migrations/PlatformDatabaseMigrationConcurrentAccessTest.java @@ -0,0 +1,94 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.db.migrations; + +import com.google.common.base.Throwables; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.sonar.server.ruby.RubyBridge; +import org.sonar.server.ruby.RubyDatabaseMigration; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.internal.verification.VerificationModeFactory.times; + +public class PlatformDatabaseMigrationConcurrentAccessTest { + + private ExecutorService pool = Executors.newFixedThreadPool(2); + /** + * Implementation of execute runs Runnable synchronously with a delay of 200ms. + */ + private PlatformDatabaseMigrationExecutorService executorService = new PlatformDatabaseMigrationExecutorServiceAdaptor() { + @Override + public void execute(Runnable command) { + try { + Thread.currentThread().sleep(200); + } catch (InterruptedException e) { + Throwables.propagate(e); + } + command.run(); + } + }; + @Mock + private RubyDatabaseMigration rubyDatabaseMigration; + @Mock + private RubyBridge rubyBridge; + private PlatformDatabaseMigration underTest; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + underTest = new PlatformDatabaseMigration(rubyBridge, executorService); + } + + @After + public void tearDown() throws Exception { + pool.shutdownNow(); + } + + @Test + public void two_concurrent_calls_to_startit_call_trigger_only_once() throws Exception { + when(rubyBridge.databaseMigration()).thenReturn(rubyDatabaseMigration); + + pool.submit(new CallStartit()); + pool.submit(new CallStartit()); + + pool.awaitTermination(3, TimeUnit.SECONDS); + + verify(rubyBridge, times(1)).databaseMigration(); + + assertThat(underTest.status()).isEqualTo(DatabaseMigration.Status.SUCCEEDED); + } + + private class CallStartit implements Runnable { + @Override + public void run() { + underTest.startIt(); + } + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/db/migrations/PlatformDatabaseMigrationExecutorServiceAdaptor.java b/server/sonar-server/src/test/java/org/sonar/server/db/migrations/PlatformDatabaseMigrationExecutorServiceAdaptor.java new file mode 100644 index 00000000000..21f6b425cac --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/db/migrations/PlatformDatabaseMigrationExecutorServiceAdaptor.java @@ -0,0 +1,100 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.db.migrations; + +import java.util.Collection; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * Adaptor for the PlatformDatabaseMigrationExecutorService interface which implementation of methods all throw + * UnsupportedOperationException. + */ +class PlatformDatabaseMigrationExecutorServiceAdaptor implements PlatformDatabaseMigrationExecutorService { + + @Override + public void execute(Runnable command) { + throw new UnsupportedOperationException(); + } + + @Override + public void shutdown() { + throw new UnsupportedOperationException(); + } + + @Override + public List shutdownNow() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isShutdown() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isTerminated() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { + throw new UnsupportedOperationException(); + } + + @Override + public Future submit(Callable task) { + throw new UnsupportedOperationException(); + } + + @Override + public Future submit(Runnable task, T result) { + throw new UnsupportedOperationException(); + } + + @Override + public Future submit(Runnable task) { + throw new UnsupportedOperationException(); + } + + @Override + public List> invokeAll(Collection> tasks) throws InterruptedException { + throw new UnsupportedOperationException(); + } + + @Override + public List> invokeAll(Collection> tasks, long timeout, TimeUnit unit) throws InterruptedException { + throw new UnsupportedOperationException(); + } + + @Override + public T invokeAny(Collection> tasks) throws InterruptedException, ExecutionException { + throw new UnsupportedOperationException(); + } + + @Override + public T invokeAny(Collection> tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + throw new UnsupportedOperationException(); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/db/migrations/PlatformDatabaseMigrationTest.java b/server/sonar-server/src/test/java/org/sonar/server/db/migrations/PlatformDatabaseMigrationTest.java new file mode 100644 index 00000000000..7000c36f841 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/db/migrations/PlatformDatabaseMigrationTest.java @@ -0,0 +1,140 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.db.migrations; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.sonar.server.ruby.RubyBridge; +import org.sonar.server.ruby.RubyDatabaseMigration; + +import java.util.Date; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Unit test for PlatformDatabaseMigration which does not test any of its concurrency management and asynchronous execution code. + */ +public class PlatformDatabaseMigrationTest { + private static final Throwable AN_ERROR = new RuntimeException(); + + /** + * Implementation of execute runs Runnable synchronously. + */ + private PlatformDatabaseMigrationExecutorService executorService = new PlatformDatabaseMigrationExecutorServiceAdaptor() { + @Override + public void execute(Runnable command) { + command.run(); + } + }; + @Mock + private RubyDatabaseMigration rubyDatabaseMigration; + @Mock + private RubyBridge rubyBridge; + private PlatformDatabaseMigration underTest; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + underTest = new PlatformDatabaseMigration(rubyBridge, executorService); + } + + @Test + public void status_is_NONE_when_component_is_created() throws Exception { + assertThat(underTest.status()).isEqualTo(DatabaseMigration.Status.NONE); + } + + @Test + public void startedAt_is_null_when_component_is_created() throws Exception { + assertThat(underTest.startedAt()).isNull(); + } + + @Test + public void failureError_is_null_when_component_is_created() throws Exception { + assertThat(underTest.failureError()).isNull(); + } + + @Test + public void startit_calls_databasemigration_trigger_in_a_separate_thread() throws Exception { + when(rubyBridge.databaseMigration()).thenReturn(rubyDatabaseMigration); + + underTest.startIt(); + + verify(rubyBridge).databaseMigration(); + verify(rubyDatabaseMigration).trigger(); + } + + @Test + public void status_is_SUCCEEDED_and_failure_is_null_when_trigger_runs_without_an_exception() throws Exception { + when(rubyBridge.databaseMigration()).thenReturn(rubyDatabaseMigration); + + underTest.startIt(); + + assertThat(underTest.status()).isEqualTo(DatabaseMigration.Status.SUCCEEDED); + assertThat(underTest.failureError()).isNull(); + assertThat(underTest.startedAt()).isNotNull(); + } + + @Test + public void status_is_FAILED_and_failure_stores_the_exception_when_trigger_throws_an_exception() throws Exception { + mockTriggerThrowsError(); + + underTest.startIt(); + + assertThat(underTest.status()).isEqualTo(DatabaseMigration.Status.FAILED); + assertThat(underTest.failureError()).isSameAs(AN_ERROR); + assertThat(underTest.startedAt()).isNotNull(); + } + + @Test + public void successive_calls_to_startIt_reset_status_startedAt_and_failureError() throws Exception { + mockTriggerThrowsError(); + + underTest.startIt(); + + assertThat(underTest.status()).isEqualTo(DatabaseMigration.Status.FAILED); + assertThat(underTest.failureError()).isSameAs(AN_ERROR); + Date firstStartDate = underTest.startedAt(); + assertThat(firstStartDate).isNotNull(); + + mockTriggerDoesNothing(); + + underTest.startIt(); + + assertThat(underTest.status()).isEqualTo(DatabaseMigration.Status.SUCCEEDED); + assertThat(underTest.failureError()).isNull(); + assertThat(underTest.startedAt()).isNotSameAs(firstStartDate); + } + + private void mockTriggerThrowsError() { + when(rubyBridge.databaseMigration()).thenReturn(rubyDatabaseMigration); + doThrow(AN_ERROR).when(rubyDatabaseMigration).trigger(); + } + + private void mockTriggerDoesNothing() { + when(rubyBridge.databaseMigration()).thenReturn(rubyDatabaseMigration); + doNothing().when(rubyDatabaseMigration).trigger(); + } +}