From 74d69a407524f52b5194264918654fcf68f3e34e Mon Sep 17 00:00:00 2001 From: Simon Brandhof Date: Tue, 15 Oct 2013 09:50:37 +0200 Subject: [PATCH] SONAR-4777 Asynchronous web service to upgrade database --- .../app/controllers/api/server_controller.rb | 39 ++++++-- .../app/models/database_migration_manager.rb | 14 +-- .../java/org/sonar/wsclient/SonarClient.java | 6 ++ .../org/sonar/wsclient/system/Migration.java | 43 ++++++++ .../sonar/wsclient/system/SystemClient.java | 36 +++++++ .../system/internal/DefaultMigration.java | 38 +++++++ .../system/internal/DefaultSystemClient.java | 76 ++++++++++++++ .../system/internal/package-info.java | 22 +++++ .../sonar/wsclient/system/package-info.java | 22 +++++ .../org/sonar/wsclient/SonarClientTest.java | 2 + .../internal/DefaultSystemClientTest.java | 98 +++++++++++++++++++ 11 files changed, 379 insertions(+), 17 deletions(-) create mode 100644 sonar-ws-client/src/main/java/org/sonar/wsclient/system/Migration.java create mode 100644 sonar-ws-client/src/main/java/org/sonar/wsclient/system/SystemClient.java create mode 100644 sonar-ws-client/src/main/java/org/sonar/wsclient/system/internal/DefaultMigration.java create mode 100644 sonar-ws-client/src/main/java/org/sonar/wsclient/system/internal/DefaultSystemClient.java create mode 100644 sonar-ws-client/src/main/java/org/sonar/wsclient/system/internal/package-info.java create mode 100644 sonar-ws-client/src/main/java/org/sonar/wsclient/system/package-info.java create mode 100644 sonar-ws-client/src/test/java/org/sonar/wsclient/system/internal/DefaultSystemClientTest.java diff --git a/sonar-server/src/main/webapp/WEB-INF/app/controllers/api/server_controller.rb b/sonar-server/src/main/webapp/WEB-INF/app/controllers/api/server_controller.rb index c2f2d5a9bb4..24cdfb7edfa 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/controllers/api/server_controller.rb +++ b/sonar-server/src/main/webapp/WEB-INF/app/controllers/api/server_controller.rb @@ -25,7 +25,6 @@ class Api::ServerController < Api::ApiController before_filter :set_cache_buster, :only => 'index' # execute database setup - verify :method => :post, :only => [:setup, :index_projects] skip_before_filter :check_database_version, :setup def key @@ -65,24 +64,43 @@ class Api::ServerController < Api::ApiController end def setup + verify_post_request + manager=DatabaseMigrationManager.instance begin # Ask the DB migration manager to start the migration # => No need to check for authorizations (actually everybody can run the upgrade) - # nor concurrent calls (this is handled directly by DatabaseMigrationManager) - DatabaseMigrationManager.instance.start_migration - - current_status = DatabaseMigrationManager.instance.is_sonar_access_allowed? ? "ok" : "ko" - - hash={:status => current_status, - :migration_status => DatabaseMigrationManager.instance.status, - :message => DatabaseMigrationManager.instance.message} + # nor concurrent calls (this is handled directly by DatabaseMigrationManager) + manager.start_migration + + operational=manager.is_sonar_access_allowed? + current_status = operational ? "ok" : "ko" + hash={ + # deprecated fields + :status => current_status, + :migration_status => manager.status, + + # correct fields + :operational => operational, + :state => manager.status + } + hash[:message]=manager.message if manager.message + hash[:startedAt]=manager.migration_start_time if manager.migration_start_time + respond_to do |format| format.json{ render :json => jsonp(hash) } format.xml { render :xml => hash.to_xml(:skip_types => true, :root => 'setup') } format.text { render :text => hash[:status] } end rescue => e - hash={:status => 'ko', :msg => e.message} + hash={ + # deprecated fields + :status => 'ko', + :msg => e.message, + + # correct fields + :message => e.message, + :state => manager.status + } respond_to do |format| format.json{ render :json => jsonp(hash) } format.xml { render :xml => hash.to_xml(:skip_types => true, :root => 'setup') } @@ -92,6 +110,7 @@ class Api::ServerController < Api::ApiController end def index_projects + verify_post_request access_denied unless has_role?(:admin) logger.info 'Indexing projects' Java::OrgSonarServerUi::JRubyFacade.getInstance().indexProjects() diff --git a/sonar-server/src/main/webapp/WEB-INF/app/models/database_migration_manager.rb b/sonar-server/src/main/webapp/WEB-INF/app/models/database_migration_manager.rb index 4b0e7b4e4ee..82191c86f3a 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/models/database_migration_manager.rb +++ b/sonar-server/src/main/webapp/WEB-INF/app/models/database_migration_manager.rb @@ -93,21 +93,21 @@ class DatabaseMigrationManager def start_migration # Use an exclusive block of code to ensure that only 1 thread will be able to proceed with the migration - can_start_migration = false + requires_migration = false Thread.exclusive do - if requires_migration? - @status = MIGRATION_RUNNING - @message = "Database migration is running" - can_start_migration = true - end + requires_migration = requires_migration? end - if can_start_migration + if requires_migration Thread.new do begin + @status = MIGRATION_RUNNING + @message = "Database migration is running" Thread.current[:name] = "Database Upgrade" @start_time = Time.now + DatabaseVersion.upgrade_and_start + @status = MIGRATION_SUCCEEDED @message = "Migration succeeded." rescue Exception => e diff --git a/sonar-ws-client/src/main/java/org/sonar/wsclient/SonarClient.java b/sonar-ws-client/src/main/java/org/sonar/wsclient/SonarClient.java index 04d4c1b7964..aefd3e382b1 100644 --- a/sonar-ws-client/src/main/java/org/sonar/wsclient/SonarClient.java +++ b/sonar-ws-client/src/main/java/org/sonar/wsclient/SonarClient.java @@ -28,6 +28,8 @@ import org.sonar.wsclient.permissions.PermissionClient; import org.sonar.wsclient.permissions.internal.DefaultPermissionClient; import org.sonar.wsclient.project.ProjectClient; import org.sonar.wsclient.project.internal.DefaultProjectClient; +import org.sonar.wsclient.system.SystemClient; +import org.sonar.wsclient.system.internal.DefaultSystemClient; import org.sonar.wsclient.user.UserClient; import org.sonar.wsclient.user.internal.DefaultUserClient; @@ -101,6 +103,10 @@ public class SonarClient { return new DefaultProjectClient(requestFactory); } + public SystemClient systemClient() { + return new DefaultSystemClient(requestFactory); + } + /** * Create a builder of {@link SonarClient}s. */ diff --git a/sonar-ws-client/src/main/java/org/sonar/wsclient/system/Migration.java b/sonar-ws-client/src/main/java/org/sonar/wsclient/system/Migration.java new file mode 100644 index 00000000000..8891d96f939 --- /dev/null +++ b/sonar-ws-client/src/main/java/org/sonar/wsclient/system/Migration.java @@ -0,0 +1,43 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 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.wsclient.system; + +import javax.annotation.Nullable; +import java.util.Date; + +/** + * @since 4.0 + */ +public interface Migration { + enum Status { + MIGRATION_NEEDED, MIGRATION_RUNNING, MIGRATION_FAILED, + MIGRATION_SUCCEEDED, NO_MIGRATION + } + + boolean operationalWebapp(); + + Status status(); + + @Nullable + String message(); + + @Nullable + Date startedAt(); +} diff --git a/sonar-ws-client/src/main/java/org/sonar/wsclient/system/SystemClient.java b/sonar-ws-client/src/main/java/org/sonar/wsclient/system/SystemClient.java new file mode 100644 index 00000000000..d14377d3f32 --- /dev/null +++ b/sonar-ws-client/src/main/java/org/sonar/wsclient/system/SystemClient.java @@ -0,0 +1,36 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 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.wsclient.system; + +/** + * @since 4.0 + */ +public interface SystemClient { + /** + * Asynchronously start a database migration. No effect if executed + * several times. + */ + Migration migrate(); + + /** + * Synchronously start a database migration. + */ + Migration migrate(long timeoutInSeconds, long rateInSeconds); +} diff --git a/sonar-ws-client/src/main/java/org/sonar/wsclient/system/internal/DefaultMigration.java b/sonar-ws-client/src/main/java/org/sonar/wsclient/system/internal/DefaultMigration.java new file mode 100644 index 00000000000..2aa90de9b94 --- /dev/null +++ b/sonar-ws-client/src/main/java/org/sonar/wsclient/system/internal/DefaultMigration.java @@ -0,0 +1,38 @@ +package org.sonar.wsclient.system.internal; + +import org.sonar.wsclient.system.Migration; +import org.sonar.wsclient.unmarshallers.JsonUtils; + +import javax.annotation.Nullable; +import java.util.Date; +import java.util.Map; + +public class DefaultMigration implements Migration { + + private final Map json; + + public DefaultMigration(Map json) { + this.json = json; + } + + @Override + public boolean operationalWebapp() { + return JsonUtils.getBoolean(json, "operational"); + } + + @Override + public Status status() { + return Status.valueOf(JsonUtils.getString(json, "state")); + } + + @Override + public String message() { + return JsonUtils.getString(json, "message"); + } + + @Override + @Nullable + public Date startedAt() { + return JsonUtils.getDateTime(json, "startedAt"); + } +} diff --git a/sonar-ws-client/src/main/java/org/sonar/wsclient/system/internal/DefaultSystemClient.java b/sonar-ws-client/src/main/java/org/sonar/wsclient/system/internal/DefaultSystemClient.java new file mode 100644 index 00000000000..86781a7cdfd --- /dev/null +++ b/sonar-ws-client/src/main/java/org/sonar/wsclient/system/internal/DefaultSystemClient.java @@ -0,0 +1,76 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 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.wsclient.system.internal; + +import org.json.simple.JSONValue; +import org.sonar.wsclient.internal.HttpRequestFactory; +import org.sonar.wsclient.system.Migration; +import org.sonar.wsclient.system.SystemClient; + +import java.util.HashMap; +import java.util.Map; + +public class DefaultSystemClient implements SystemClient { + + private final HttpRequestFactory requestFactory; + + public DefaultSystemClient(HttpRequestFactory requestFactory) { + this.requestFactory = requestFactory; + } + + @Override + public Migration migrate() { + String json = requestFactory.post("/api/server/setup", new HashMap()); + return jsonToMigration(json); + } + + @Override + public Migration migrate(long timeoutInMs, long rateInMs) { + if (rateInMs >= timeoutInMs) { + throw new IllegalArgumentException("Timeout must be greater than rate"); + } + Migration migration = null; + boolean running = true; + long endAt = System.currentTimeMillis() + timeoutInMs; + while (running && System.currentTimeMillis() < endAt) { + migration = migrate(); + if (migration.status() == Migration.Status.MIGRATION_NEEDED || + migration.status() == Migration.Status.MIGRATION_RUNNING) { + sleepQuietly(rateInMs); + } else { + running = false; + } + } + return migration; + } + + private void sleepQuietly(long rateInMs) { + try { + Thread.sleep(rateInMs); + } catch (InterruptedException e) { + throw new IllegalStateException("Fail to sleep!", e); + } + } + + private Migration jsonToMigration(String json) { + Map jsonRoot = (Map) JSONValue.parse(json); + return new DefaultMigration(jsonRoot); + } +} diff --git a/sonar-ws-client/src/main/java/org/sonar/wsclient/system/internal/package-info.java b/sonar-ws-client/src/main/java/org/sonar/wsclient/system/internal/package-info.java new file mode 100644 index 00000000000..3cb9011d7f8 --- /dev/null +++ b/sonar-ws-client/src/main/java/org/sonar/wsclient/system/internal/package-info.java @@ -0,0 +1,22 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 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. + */ + +@javax.annotation.ParametersAreNonnullByDefault +package org.sonar.wsclient.system.internal; diff --git a/sonar-ws-client/src/main/java/org/sonar/wsclient/system/package-info.java b/sonar-ws-client/src/main/java/org/sonar/wsclient/system/package-info.java new file mode 100644 index 00000000000..433b6d9424b --- /dev/null +++ b/sonar-ws-client/src/main/java/org/sonar/wsclient/system/package-info.java @@ -0,0 +1,22 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 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. + */ + +@javax.annotation.ParametersAreNonnullByDefault +package org.sonar.wsclient.system; diff --git a/sonar-ws-client/src/test/java/org/sonar/wsclient/SonarClientTest.java b/sonar-ws-client/src/test/java/org/sonar/wsclient/SonarClientTest.java index cb1516a9db9..92741d41d26 100644 --- a/sonar-ws-client/src/test/java/org/sonar/wsclient/SonarClientTest.java +++ b/sonar-ws-client/src/test/java/org/sonar/wsclient/SonarClientTest.java @@ -24,6 +24,7 @@ import org.sonar.wsclient.issue.internal.DefaultActionPlanClient; import org.sonar.wsclient.issue.internal.DefaultIssueClient; import org.sonar.wsclient.permissions.internal.DefaultPermissionClient; import org.sonar.wsclient.project.internal.DefaultProjectClient; +import org.sonar.wsclient.system.internal.DefaultSystemClient; import org.sonar.wsclient.user.internal.DefaultUserClient; import static org.fest.assertions.Assertions.assertThat; @@ -38,6 +39,7 @@ public class SonarClientTest { assertThat(client.userClient()).isNotNull().isInstanceOf(DefaultUserClient.class); assertThat(client.permissionClient()).isNotNull().isInstanceOf(DefaultPermissionClient.class); assertThat(client.projectClient()).isNotNull().isInstanceOf(DefaultProjectClient.class); + assertThat(client.systemClient()).isNotNull().isInstanceOf(DefaultSystemClient.class); } @Test diff --git a/sonar-ws-client/src/test/java/org/sonar/wsclient/system/internal/DefaultSystemClientTest.java b/sonar-ws-client/src/test/java/org/sonar/wsclient/system/internal/DefaultSystemClientTest.java new file mode 100644 index 00000000000..fffc6c83a88 --- /dev/null +++ b/sonar-ws-client/src/test/java/org/sonar/wsclient/system/internal/DefaultSystemClientTest.java @@ -0,0 +1,98 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 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.wsclient.system.internal; + +import org.junit.Rule; +import org.junit.Test; +import org.sonar.wsclient.MockHttpServerInterceptor; +import org.sonar.wsclient.internal.HttpRequestFactory; +import org.sonar.wsclient.system.Migration; + +import static org.fest.assertions.Assertions.assertThat; +import static org.fest.assertions.Fail.fail; +import static org.mockito.Matchers.anyMap; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class DefaultSystemClientTest { + + static final String RUNNING_JSON = "{\"status\": \"KO\", \"state\": \"MIGRATION_RUNNING\", " + + "\"operational\": false, \"startedAt\": \"2013-12-20T12:34:56+0100\"}"; + + static final String DONE_JSON = "{\"status\": \"OK\", \"state\": \"MIGRATION_SUCCEEDED\", " + + "\"operational\": true, \"message\": \"done\"}"; + + @Rule + public MockHttpServerInterceptor httpServer = new MockHttpServerInterceptor(); + + @Test + public void start_migration_asynchronously() { + HttpRequestFactory requestFactory = new HttpRequestFactory(httpServer.url()); + httpServer.stubResponseBody(RUNNING_JSON); + + DefaultSystemClient client = new DefaultSystemClient(requestFactory); + Migration migration = client.migrate(); + + assertThat(httpServer.requestedPath()).isEqualTo("/api/server/setup"); + assertThat(migration.status()).isEqualTo(Migration.Status.MIGRATION_RUNNING); + assertThat(migration.operationalWebapp()).isFalse(); + assertThat(migration.startedAt().getYear()).isEqualTo(113);//2013 = nb of years since 1900 + } + + @Test + public void fail_if_rate_is_greater_than_timeout() throws Exception { + try { + DefaultSystemClient client = new DefaultSystemClient(mock(HttpRequestFactory.class)); + client.migrate(5L, 50L); + fail(); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage()).isEqualTo("Timeout must be greater than rate"); + } + } + + @Test + public void stop_synchronous_migration_on_timeout() { + HttpRequestFactory requestFactory = new HttpRequestFactory(httpServer.url()); + httpServer.stubResponseBody(RUNNING_JSON); + + DefaultSystemClient client = new DefaultSystemClient(requestFactory); + Migration migration = client.migrate(50L, 5L); + + assertThat(migration.status()).isEqualTo(Migration.Status.MIGRATION_RUNNING); + assertThat(migration.operationalWebapp()).isFalse(); + } + + @Test + public void return_result_before_timeout_of_synchronous_migration() { + HttpRequestFactory requestFactory = mock(HttpRequestFactory.class); + when(requestFactory.post(eq("/api/server/setup"), anyMap())).thenReturn( + RUNNING_JSON, DONE_JSON + ); + + DefaultSystemClient client = new DefaultSystemClient(requestFactory); + Migration migration = client.migrate(50L, 5L); + + assertThat(migration.status()).isEqualTo(Migration.Status.MIGRATION_SUCCEEDED); + assertThat(migration.operationalWebapp()).isTrue(); + assertThat(migration.message()).isEqualTo("done"); + assertThat(migration.startedAt()).isNull(); + } +} -- 2.39.5