this component handles concurrency and asynchronous execution of the DB migration job when called from Java
--- /dev/null
+/*
+ * 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();
+
+}
--- /dev/null
+/*
+ * 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;
+ }
+}
--- /dev/null
+/*
+ * 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 {
+}
--- /dev/null
+/*
+ * 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());
+ }
+}
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.*;
ServerMetadataPersister.class,
DefaultHttpDownloader.class,
UriReader.class,
- ServerIdGenerator.class
+ ServerIdGenerator.class,
+
+ PlatformDatabaseMigrationExecutorServiceImpl.class,
+ PlatformDatabaseMigration.class
);
}
--- /dev/null
+/*
+ * 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();
+ }
+}
--- /dev/null
+/*
+ * 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();
+ }
+ }
+}
--- /dev/null
+/*
+ * 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();
+ }
+}
--- /dev/null
+/*
+ * 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();
+ }
+}