modified ruby scripts used by RubyBridge to define and instance a class instead of only define a method alone because mapping such script to a Java interface works only for the first script (at least in unit tests)
split upgrade_and_start from Ruby into two seperate Ruby tasks (trigger ActiveRecord migration on one side and web route (re)creation on the other) and a pure Java task to restart the container
*/
package org.sonar.server.db.migrations;
-import org.sonar.api.utils.log.Logger;
-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;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.api.utils.log.Profiler;
+import org.sonar.server.platform.Platform;
+import org.sonar.server.ruby.RubyBridge;
+
/**
* Handles concurrency to make sure only one DB migration can run at a time.
*/
* ExecutorService implements threads management.
*/
private final PlatformDatabaseMigrationExecutorService executorService;
+ private final Platform platform;
/**
* This lock implements thread safety from concurrent calls of method {@link #startIt()}
*/
@Nullable
private Throwable failureError;
- public PlatformDatabaseMigration(RubyBridge rubyBridge, PlatformDatabaseMigrationExecutorService executorService) {
+ public PlatformDatabaseMigration(RubyBridge rubyBridge, PlatformDatabaseMigrationExecutorService executorService, Platform platform) {
this.rubyBridge = rubyBridge;
this.executorService = executorService;
+ this.platform = platform;
}
@Override
status = Status.RUNNING;
startDate = new Date();
failureError = null;
+ Profiler profiler = Profiler.create(LOGGER);
try {
- LOGGER.info("Starting DB Migration at {}", startDate);
- rubyBridge.databaseMigration().trigger();
- LOGGER.info("DB Migration ended successfully at {}", new Date());
+ profiler.startInfo("Starting DB Migration");
+ upgradeDb();
+ restartContainer();
+ recreateWebRoutes();
status = Status.SUCCEEDED;
+ profiler.stopInfo("DB Migration ended successfully");
} catch (Throwable t) {
- LOGGER.error("DB Migration failed and ended at " + startDate + " with an exception", t);
+ profiler.stopInfo("DB migration failed");
+ LOGGER.error(
+ "DB Migration or container restart failed. Process ended with an exception", t
+ );
status = Status.FAILED;
failureError = t;
} finally {
running.getAndSet(false);
}
}
+
+ private void upgradeDb() {
+ Profiler profiler = Profiler.createIfTrace(LOGGER);
+ profiler.startTrace("Starting DB Migration");
+ rubyBridge.databaseMigration().trigger();
+ profiler.stopTrace("DB Migration ended");
+ }
+
+ private void restartContainer() {
+ Profiler profiler = Profiler.createIfTrace(LOGGER);
+ profiler.startTrace("Restarting container");
+ platform.doStart();
+ profiler.stopTrace("Container restarted successfully");
+ }
+
+ private void recreateWebRoutes() {
+ Profiler profiler = Profiler.createIfTrace(LOGGER);
+ profiler.startTrace("Recreating web routes");
+ rubyBridge.railsRoutes().recreate();
+ profiler.startTrace("Routes recreated successfully");
+ }
});
}
--- /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.ruby;
+
+/**
+ * Interface which must be top-level public class to be used by the Ruby engine but that hides name of the Ruby method
+ * in the Ruby script from the rest of the platform (only {@link RubyDatabaseMigration} is known to the platform).
+ */
+public interface CallDatabaseVersionUpgrade {
+
+ /**
+ * Java method that calls the upgrade_and_start method defined in the {@code call_databaseversion_upgrade.rb} script.
+ */
+ void callUpgrade();
+}
--- /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.ruby;
+
+/**
+ * Interface which must be a top-level public class to be used by the Ruby engine but that hides name of the Ruby
+ * method in the Ruby script from the rest of the platform (only {@link RubyRailsRoutes} is known to the platform).
+ */
+public interface CallLoadJavaWebServices {
+ /**
+ * Java method that calls the call_upgrade_and_start method defined in the {@code call_load_java_web_services.rb} script.
+ */
+ void callLoadJavaWebServices();
+}
*/
package org.sonar.server.ruby;
+import java.io.IOException;
+import java.io.InputStream;
+import javax.annotation.Nullable;
import org.jruby.Ruby;
import org.jruby.RubyNil;
import org.jruby.RubyRuntimeAdapter;
import org.jruby.javasupport.JavaUtil;
import org.jruby.runtime.builtin.IRubyObject;
-import java.io.IOException;
-import java.io.InputStream;
-
public class PlatformRubyBridge implements RubyBridge {
- private static final String CALL_UPGRADE_AND_START_RB_FILENAME = "call_upgrade_and_start.rb";
+ private static final String CALL_UPGRADE_AND_START_RB_FILENAME = "call_databaseversion_upgrade.rb";
+ private static final String CALL_LOAD_JAVA_WEB_SERVICES_RB_FILENAME = "call_load_java_web_services.rb";
private final RackBridge rackBridge;
private final RubyRuntimeAdapter adapter = JavaEmbedUtils.newRuntimeAdapter();
@Override
public RubyDatabaseMigration databaseMigration() {
- final CallUpgradeAndStart callUpgradeAndStart = parseMethodScriptToInterface(
- CALL_UPGRADE_AND_START_RB_FILENAME, CallUpgradeAndStart.class
+ final CallDatabaseVersionUpgrade callDatabaseVersionUpgrade = parseMethodScriptToInterface(
+ CALL_UPGRADE_AND_START_RB_FILENAME, CallDatabaseVersionUpgrade.class
);
return new RubyDatabaseMigration() {
@Override
public void trigger() {
- callUpgradeAndStart.callUpgradeAndStart();
+ callDatabaseVersionUpgrade.callUpgrade();
+ }
+ };
+ }
+
+ @Override
+ public RubyRailsRoutes railsRoutes() {
+ final CallLoadJavaWebServices callLoadJavaWebServices = parseMethodScriptToInterface(
+ CALL_LOAD_JAVA_WEB_SERVICES_RB_FILENAME, CallLoadJavaWebServices.class
+ );
+
+ return new RubyRailsRoutes() {
+ @Override
+ public void recreate() {
+ callLoadJavaWebServices.callLoadJavaWebServices();
}
};
}
private <T> T parseMethodScriptToInterface(String fileName, Class<T> clazz) {
try (InputStream in = getClass().getResourceAsStream(fileName)) {
Ruby rubyRuntime = rackBridge.getRubyRuntime();
- JavaEmbedUtils.EvalUnit evalUnit = adapter.parse(rubyRuntime, in, fileName, 0);
+ JavaEmbedUtils.EvalUnit evalUnit = JavaEmbedUtils.newRuntimeAdapter().parse(rubyRuntime, in, fileName, 0);
IRubyObject rubyObject = evalUnit.run();
Object receiver = JavaEmbedUtils.rubyToJava(rubyObject);
T wrapper = getInstance(rubyRuntime, receiver, clazz);
* Fork of method {@link org.jruby.embed.internal.EmbedRubyInterfaceAdapterImpl#getInstance(Object, Class)}
*/
@SuppressWarnings("unchecked")
- public <T> T getInstance(Ruby runtime, Object receiver, Class<T> clazz) {
+ public <T> T getInstance(Ruby runtime, @Nullable Object receiver, @Nullable Class<T> clazz) {
if (clazz == null || !clazz.isInterface()) {
return null;
}
}
}
- /**
- * Interface which must be public to be used by the Ruby engine but that hides name of the Ruby method in the Ruby
- * script from the rest of the platform (only {@link RubyDatabaseMigration} is known to the platform).
- */
- public interface CallUpgradeAndStart {
-
- /**
- * Java method that calls the upgrade_and_start method defined in the {@code call_upgrade_and_start.rb} script.
- */
- void callUpgradeAndStart();
- }
}
* @return a {@link RubyDatabaseMigration}
*/
RubyDatabaseMigration databaseMigration();
+
+ /**
+ * Returns a class that allows calling the (re)creation of web routes in Rails.
+ *
+ * @return a {@link RubyRailsRoutes}
+ */
+ RubyRailsRoutes railsRoutes();
}
--- /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.ruby;
+
+public interface RubyRailsRoutes {
+ /**
+ * Triggers the (re)creation of web route in Ruby On Rails.
+ * <strong>This is not thread safe!</strong>
+ */
+ void recreate();
+}
--- /dev/null
+# this script defines a method which calls the class method "upgrade" of the DatabaseVersion class defined
+# in /server/sonar-web/src/main/webapp/WEB-INF/lib/database_version.rb
+
+require 'database_version'
+
+class RbCallUpgrade
+ include Java::org.sonar.server.ruby.CallDatabaseVersionUpgrade
+ def call_upgrade
+ DatabaseVersion.upgrade
+ end
+end
+RbCallUpgrade.new
\ No newline at end of file
--- /dev/null
+# this script defines a method which calls the class method "load_java_web_services" of the DatabaseVersion class
+# definedin /server/sonar-web/src/main/webapp/WEB-INF/lib/database_version.rb
+
+require 'database_version'
+
+class RbCallLoadJavaWebServices
+ include Java::org.sonar.server.ruby.CallLoadJavaWebServices
+ def call_load_java_web_services
+ DatabaseVersion.load_java_web_services
+ end
+end
+RbCallLoadJavaWebServices.new
\ No newline at end of file
+++ /dev/null
-# this script defines a method which calls the class method "upgrade_and_start" of the DatabaseVersion class defined
-# in /server/sonar-web/src/main/webapp/WEB-INF/lib/database_version.rb
-
-require 'database_version'
-
-def call_upgrade_and_start
- DatabaseVersion.upgrade_and_start
-end
\ No newline at end of file
package org.sonar.server.db.migrations;
import com.google.common.base.Throwables;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
import org.junit.After;
-import org.junit.Before;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
+import org.sonar.server.platform.Platform;
import org.sonar.server.ruby.RubyBridge;
import org.sonar.server.ruby.RubyDatabaseMigration;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
+import static org.mockito.Mockito.mock;
public class PlatformDatabaseMigrationAsynchronousTest {
});
}
};
- @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);
- }
+ private RubyDatabaseMigration rubyDatabaseMigration = mock(RubyDatabaseMigration.class);
+ private RubyBridge rubyBridge = mock(RubyBridge.class);
+ private Platform platform = mock(Platform.class);
+ private PlatformDatabaseMigration underTest = new PlatformDatabaseMigration(rubyBridge, executorService, platform);
@After
public void tearDown() throws Exception {
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.After;
import org.junit.Test;
+import org.sonar.server.platform.Platform;
import org.sonar.server.ruby.RubyBridge;
import org.sonar.server.ruby.RubyDatabaseMigration;
+import org.sonar.server.ruby.RubyRailsRoutes;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
}
};
private RubyBridge rubyBridge = mock(RubyBridge.class);
- private PlatformDatabaseMigration underTest = new PlatformDatabaseMigration(rubyBridge, executorService);
+ private Platform platform = mock(Platform.class);
+ private RubyRailsRoutes railsRoutes = mock(RubyRailsRoutes.class);
+ private PlatformDatabaseMigration underTest = new PlatformDatabaseMigration(rubyBridge, executorService, platform);
@After
public void tearDown() throws Exception {
@Test
public void two_concurrent_calls_to_startit_call_trigger_only_once() throws Exception {
when(rubyBridge.databaseMigration()).thenReturn(rubyDatabaseMigration);
+ when(rubyBridge.railsRoutes()).thenReturn(railsRoutes);
pool.submit(new CallStartit());
pool.submit(new CallStartit());
*/
package org.sonar.server.db.migrations;
-import org.junit.Before;
+import java.util.Date;
import org.junit.Test;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
+import org.mockito.InOrder;
+import org.sonar.server.platform.Platform;
import org.sonar.server.ruby.RubyBridge;
import org.sonar.server.ruby.RubyDatabaseMigration;
-
-import java.util.Date;
+import org.sonar.server.ruby.RubyRailsRoutes;
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.inOrder;
+import static org.mockito.Mockito.mock;
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();
+ private static final Throwable AN_ERROR = new RuntimeException("runtime exception created on purpose");
/**
* Implementation of execute runs Runnable synchronously.
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);
- }
+ private RubyBridge rubyBridge = mock(RubyBridge.class);
+ private RubyDatabaseMigration rubyDatabaseMigration = mock(RubyDatabaseMigration.class);
+ private RubyRailsRoutes rubyRailsRoutes = mock(RubyRailsRoutes.class);
+ private Platform platform = mock(Platform.class);
+ private InOrder inOrder = inOrder(rubyDatabaseMigration, rubyBridge, rubyRailsRoutes, platform);
+
+ private PlatformDatabaseMigration underTest = new PlatformDatabaseMigration(rubyBridge, executorService, platform);
@Test
public void status_is_NONE_when_component_is_created() throws Exception {
@Test
public void startit_calls_databasemigration_trigger_in_a_separate_thread() throws Exception {
when(rubyBridge.databaseMigration()).thenReturn(rubyDatabaseMigration);
+ when(rubyBridge.railsRoutes()).thenReturn(rubyRailsRoutes);
underTest.startIt();
- verify(rubyBridge).databaseMigration();
- verify(rubyDatabaseMigration).trigger();
+ inOrder.verify(rubyBridge).databaseMigration();
+ inOrder.verify(rubyDatabaseMigration).trigger();
+ inOrder.verify(platform).doStart();
+ inOrder.verify(rubyBridge).railsRoutes();
+ inOrder.verify(rubyRailsRoutes).recreate();
+ inOrder.verifyNoMoreInteractions();
}
@Test
public void status_is_SUCCEEDED_and_failure_is_null_when_trigger_runs_without_an_exception() throws Exception {
when(rubyBridge.databaseMigration()).thenReturn(rubyDatabaseMigration);
+ when(rubyBridge.railsRoutes()).thenReturn(rubyRailsRoutes);
underTest.startIt();
private void mockTriggerThrowsError() {
when(rubyBridge.databaseMigration()).thenReturn(rubyDatabaseMigration);
doThrow(AN_ERROR).when(rubyDatabaseMigration).trigger();
+ when(rubyBridge.railsRoutes()).thenReturn(rubyRailsRoutes);
}
private void mockTriggerDoesNothing() {
when(rubyBridge.databaseMigration()).thenReturn(rubyDatabaseMigration);
doNothing().when(rubyDatabaseMigration).trigger();
+ when(rubyBridge.railsRoutes()).thenReturn(rubyRailsRoutes);
}
}
package org.sonar.server.ruby;
import com.google.common.collect.ImmutableList;
+import java.io.File;
+import java.net.URISyntaxException;
+import java.net.URL;
import org.jruby.embed.LocalContextScope;
import org.jruby.embed.ScriptingContainer;
import org.jruby.exceptions.RaiseException;
import org.junit.Before;
import org.junit.Test;
-import java.io.File;
-import java.net.URISyntaxException;
-import java.net.URL;
-
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
*/
private static ScriptingContainer setupScriptingContainer() {
try {
- ScriptingContainer container = new ScriptingContainer(LocalContextScope.CONCURRENT);
+ ScriptingContainer container = new ScriptingContainer(LocalContextScope.THREADSAFE);
URL resource = PlatformRubyBridge.class.getResource("database_version.rb");
String dirPath = new File(resource.toURI()).getParentFile().getPath();
container.setLoadPaths(ImmutableList.of(dirPath));
}
}
+ /**
+ * unit test only makes sure the wrapping and method forwarding provided by JRuby works so building the
+ * RubyRailsRoutes object and calling its trigger method is enough as it would otherwise raise an exception
+ */
+ @Test
+ public void testRailsRoutes() {
+ try {
+ underTest.railsRoutes().recreate();
+ } catch (RaiseException e) {
+ e.printStackTrace();
+ throw new RuntimeException("Loading error with container loadPath " + container.getLoadPaths(), e);
+ }
+ }
+
}
# it would otherwise raise an exception
class DatabaseVersion
- def self.upgrade_and_start
+ def self.upgrade
+
+ end
+
+ def self.load_java_web_services
end
$uptodate
end
+ def self.upgrade
+ ActiveRecord::Migrator.migrate(migrations_path)
+ end
+
def self.upgrade_and_start
ActiveRecord::Migrator.migrate(migrations_path)
Java::OrgSonarServerPlatform::Platform.getInstance().doStart()