]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-6366 add DatabaseMigration component
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Tue, 7 Apr 2015 09:02:21 +0000 (11:02 +0200)
committerSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Fri, 17 Apr 2015 13:15:00 +0000 (15:15 +0200)
this component handles concurrency and asynchronous execution of the DB migration job when called from Java

server/sonar-server/src/main/java/org/sonar/server/db/migrations/DatabaseMigration.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/db/migrations/PlatformDatabaseMigration.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/db/migrations/PlatformDatabaseMigrationExecutorService.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/db/migrations/PlatformDatabaseMigrationExecutorServiceImpl.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/platform/ServerComponents.java
server/sonar-server/src/test/java/org/sonar/server/db/migrations/PlatformDatabaseMigrationAsynchronousTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/db/migrations/PlatformDatabaseMigrationConcurrentAccessTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/db/migrations/PlatformDatabaseMigrationExecutorServiceAdaptor.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/db/migrations/PlatformDatabaseMigrationTest.java [new file with mode: 0644]

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 (file)
index 0000000..e7e5a56
--- /dev/null
@@ -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.
+   * <p>
+   * 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.
+   * </p>
+   * <p>
+   * <strong>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</strong>
+   * </p>
+   */
+  void startIt();
+
+  /**
+   * The time and day the last migration was started.
+   * <p>
+   * If no migration was ever started, the returned date is {@code null}. This value is reset when {@link #startIt()} is
+   * called.
+   * </p>
+   *
+   * @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.
+   * <p>
+   * This value is reset when {@link #startIt()} is called.
+   * </p>
+   * @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 (file)
index 0000000..3425c28
--- /dev/null
@@ -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.
+   * <p>
+   * 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.
+   * </p>
+   */
+  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 (file)
index 0000000..6efa645
--- /dev/null
@@ -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 (file)
index 0000000..e105610
--- /dev/null
@@ -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());
+  }
+}
index 1209df754c440b13d32cbc4c93cbe0b55e0ed7a9..32e598f6b8014325d73744b0a92e2318acb11177 100644 (file)
@@ -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 (file)
index 0000000..1eb35d1
--- /dev/null
@@ -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 (file)
index 0000000..6f6cfa1
--- /dev/null
@@ -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 (file)
index 0000000..21f6b42
--- /dev/null
@@ -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<Runnable> 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 <T> Future<T> submit(Callable<T> task) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public <T> Future<T> submit(Runnable task, T result) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public Future<?> submit(Runnable task) {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public <T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public <T> T invokeAny(Collection<? extends Callable<T>> 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 (file)
index 0000000..7000c36
--- /dev/null
@@ -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();
+  }
+}