]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-12007 & SONAR-12008 tasks can be executed by any node of the cluster
authorPierre Guillot <50145663+pierre-guillot-sonarsource@users.noreply.github.com>
Wed, 26 Jun 2019 15:42:26 +0000 (17:42 +0200)
committersonartech <sonartech@sonarsource.com>
Fri, 28 Jun 2019 06:45:55 +0000 (08:45 +0200)
SONAR-12007 & SONAR-12008 tasks can be executed by any node of the cluster

server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel1.java
server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel4.java
server/sonar-server/src/main/java/org/sonar/server/telemetry/TelemetryDaemon.java
server/sonar-server/src/main/java/org/sonar/server/util/GlobalLockManager.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/telemetry/TelemetryDaemonTest.java
server/sonar-server/src/test/java/org/sonar/server/util/GlobalLockManagerTest.java [new file with mode: 0644]

index a706b694741250ad58c0a2b2603dc96ba623e7b2..a9082abcdc336a5e23055cdd1e618fca29522010 100644 (file)
@@ -61,6 +61,7 @@ import org.sonar.server.rule.index.RuleIndex;
 import org.sonar.server.setting.ThreadLocalSettings;
 import org.sonar.server.user.SystemPasscodeImpl;
 import org.sonar.server.user.ThreadLocalUserSession;
+import org.sonar.server.util.GlobalLockManager;
 import org.sonar.server.util.OkHttpClientProvider;
 
 import static org.sonar.core.extension.CoreExtensionsInstaller.noAdditionalSideFilter;
@@ -128,6 +129,8 @@ public class PlatformLevel1 extends PlatformLevel {
       // issues
       IssueIndex.class,
 
+      GlobalLockManager.class,
+
       new OkHttpClientProvider(),
 
       CoreExtensionRepositoryImpl.class,
index d49d50bc62cd395b23cfc72641fcb15e9b9b616a..8e850fa4449cd16a61ddd5702c50f778845a1bbd 100644 (file)
@@ -519,11 +519,14 @@ public class PlatformLevel4 extends PlatformLevel {
       HttpRequestIdModule.class,
 
       RecoveryIndexer.class,
-      ProjectIndexersImpl.class);
+      ProjectIndexersImpl.class,
 
-    // telemetry
-    add(TelemetryDataLoader.class);
-    addIfStartupLeader(TelemetryDaemon.class, TelemetryClient.class);
+      // telemetry
+      TelemetryDataLoader.class,
+      TelemetryDaemon.class,
+      TelemetryClient.class
+
+    );
 
     // system info
     add(WebSystemInfoModule.class);
index fc430877ef983b0955c3a66a57c7afa47604b087..394a9bc27a3984395ad6a21ecec8196924079f69 100644 (file)
@@ -36,6 +36,7 @@ import org.sonar.api.utils.log.Logger;
 import org.sonar.api.utils.log.Loggers;
 import org.sonar.api.utils.text.JsonWriter;
 import org.sonar.server.property.InternalProperties;
+import org.sonar.server.util.GlobalLockManager;
 
 import static org.sonar.api.utils.DateUtils.formatDate;
 import static org.sonar.api.utils.DateUtils.parseDate;
@@ -50,21 +51,26 @@ public class TelemetryDaemon implements Startable {
   private static final int SEVEN_DAYS = 7 * 24 * 60 * 60 * 1_000;
   private static final String I_PROP_LAST_PING = "telemetry.lastPing";
   private static final String I_PROP_OPT_OUT = "telemetry.optOut";
+  private static final String LOCK_NAME = "TelemetryStat";
   private static final Logger LOG = Loggers.get(TelemetryDaemon.class);
+  private static final String LOCK_DELAY_SEC = "sonar.telemetry.lock.delay";
 
   private final TelemetryDataLoader dataLoader;
   private final TelemetryClient telemetryClient;
+  private final GlobalLockManager lockManager;
   private final Configuration config;
   private final InternalProperties internalProperties;
   private final System2 system2;
 
   private ScheduledExecutorService executorService;
 
-  public TelemetryDaemon(TelemetryDataLoader dataLoader, TelemetryClient telemetryClient, Configuration config, InternalProperties internalProperties, System2 system2) {
+  public TelemetryDaemon(TelemetryDataLoader dataLoader, TelemetryClient telemetryClient, Configuration config,
+    InternalProperties internalProperties, GlobalLockManager lockManager, System2 system2) {
     this.dataLoader = dataLoader;
     this.telemetryClient = telemetryClient;
     this.config = config;
     this.internalProperties = internalProperties;
+    this.lockManager = lockManager;
     this.system2 = system2;
   }
 
@@ -113,6 +119,11 @@ public class TelemetryDaemon implements Startable {
   private Runnable telemetryCommand() {
     return () -> {
       try {
+
+        if (!lockManager.tryLock(LOCK_NAME, lockDuration())) {
+          return;
+        }
+
         long now = system2.now();
         if (shouldUploadStatistics(now)) {
           uploadStatistics();
@@ -157,4 +168,8 @@ public class TelemetryDaemon implements Startable {
     return config.getInt(SONAR_TELEMETRY_FREQUENCY_IN_SECONDS.getKey())
       .orElseThrow(() -> new IllegalStateException(String.format("Setting '%s' must be provided.", SONAR_TELEMETRY_FREQUENCY_IN_SECONDS)));
   }
+
+  private int lockDuration(){
+    return config.getInt(LOCK_DELAY_SEC).orElse(60);
+  }
 }
diff --git a/server/sonar-server/src/main/java/org/sonar/server/util/GlobalLockManager.java b/server/sonar-server/src/main/java/org/sonar/server/util/GlobalLockManager.java
new file mode 100644 (file)
index 0000000..81f13b2
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.util;
+
+import org.sonar.api.ce.ComputeEngineSide;
+import org.sonar.api.server.ServerSide;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+
+/**
+ * Provide a simple mechanism to manage global locks across multiple nodes running in a cluster.
+ * In the target use case multiple nodes try to execute something at around the same time,
+ * and only the first should succeed, and the rest do nothing.
+ */
+@ComputeEngineSide
+@ServerSide
+public class GlobalLockManager {
+
+  static final int DEFAULT_LOCK_DURATION_SECONDS = 180;
+
+  private final DbClient dbClient;
+
+  public GlobalLockManager(DbClient dbClient) {
+    this.dbClient = dbClient;
+  }
+
+  /**
+   * Try to acquire a lock on the given name in the default namespace,
+   * using the generic locking mechanism of {@see org.sonar.db.property.InternalPropertiesDao}.
+   */
+  public boolean tryLock(String name) {
+    return tryLock(name, DEFAULT_LOCK_DURATION_SECONDS);
+  }
+
+  public boolean tryLock(String name, int durationSecond) {
+    try (DbSession dbSession = dbClient.openSession(false)) {
+      boolean success = dbClient.internalPropertiesDao().tryLock(dbSession, name, durationSecond);
+      dbSession.commit();
+      return success;
+    }
+  }
+}
index 82c1cc48384a6b5807e1f6fdbcefb9dcf83a4f10..227aff02c155e84ec3cbc57b8ef104dee94f6067 100644 (file)
@@ -29,6 +29,7 @@ import org.junit.After;
 import org.junit.Rule;
 import org.junit.Test;
 import org.mockito.ArgumentCaptor;
+import org.mockito.internal.matchers.Any;
 import org.sonar.api.config.internal.MapSettings;
 import org.sonar.api.utils.internal.TestSystem2;
 import org.sonar.api.utils.log.LogTester;
@@ -50,6 +51,7 @@ import org.sonar.server.property.MapInternalProperties;
 import org.sonar.server.tester.UserSessionRule;
 import org.sonar.server.user.index.UserIndex;
 import org.sonar.server.user.index.UserIndexer;
+import org.sonar.server.util.GlobalLockManager;
 import org.sonar.updatecenter.common.Version;
 
 import static java.util.Arrays.asList;
@@ -57,6 +59,8 @@ import static java.util.Collections.emptySet;
 import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
 import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.after;
 import static org.mockito.Mockito.mock;
@@ -93,6 +97,7 @@ public class TelemetryDaemonTest {
 
   private TelemetryClient client = mock(TelemetryClient.class);
   private InternalProperties internalProperties = spy(new MapInternalProperties());
+  private final GlobalLockManager lockManager = mock(GlobalLockManager.class);
   private FakeServer server = new FakeServer();
   private PluginRepository pluginRepository = mock(PluginRepository.class);
   private TestSystem2 system2 = new TestSystem2().setNow(System.currentTimeMillis());
@@ -103,12 +108,12 @@ public class TelemetryDaemonTest {
 
   private final TelemetryDataLoader communityDataLoader = new TelemetryDataLoader(server, db.getDbClient(), pluginRepository, new UserIndex(es.client(), system2),
     new ProjectMeasuresIndex(es.client(), null, system2), editionProvider, new DefaultOrganizationProviderImpl(db.getDbClient()), internalProperties, null);
-  private TelemetryDaemon communityUnderTest = new TelemetryDaemon(communityDataLoader, client, settings.asConfig(), internalProperties, system2);
+  private TelemetryDaemon communityUnderTest = new TelemetryDaemon(communityDataLoader, client, settings.asConfig(), internalProperties, lockManager, system2);
 
   private final LicenseReader licenseReader = mock(LicenseReader.class);
   private final TelemetryDataLoader commercialDataLoader = new TelemetryDataLoader(server, db.getDbClient(), pluginRepository, new UserIndex(es.client(), system2),
     new ProjectMeasuresIndex(es.client(), null, system2), editionProvider, new DefaultOrganizationProviderImpl(db.getDbClient()), internalProperties, licenseReader);
-  private TelemetryDaemon commercialUnderTest = new TelemetryDaemon(commercialDataLoader, client, settings.asConfig(), internalProperties, system2);
+  private TelemetryDaemon commercialUnderTest = new TelemetryDaemon(commercialDataLoader, client, settings.asConfig(), internalProperties, lockManager, system2);
 
   @After
   public void tearDown() {
@@ -124,6 +129,7 @@ public class TelemetryDaemonTest {
     List<PluginInfo> plugins = asList(newPlugin("java", "4.12.0.11033"), newPlugin("scmgit", "1.2"), new PluginInfo("other"));
     when(pluginRepository.getPluginInfos()).thenReturn(plugins);
     when(editionProvider.get()).thenReturn(Optional.of(EditionProvider.Edition.DEVELOPER));
+    when(lockManager.tryLock(any(), anyInt())).thenReturn(true);
 
     IntStream.range(0, 3).forEach(i -> db.users().insertUser());
     db.users().insertUser(u -> u.setActive(false));
@@ -175,6 +181,7 @@ public class TelemetryDaemonTest {
   @Test
   public void take_biggest_long_living_branches() throws IOException {
     initTelemetrySettingsToDefaultValues();
+    when(lockManager.tryLock(any(), anyInt())).thenReturn(true);
     settings.setProperty("sonar.telemetry.frequencyInSeconds", "1");
     server.setId("AU-TpxcB-iU5OvuD2FL7").setVersion("7.5.4");
     MetricDto ncloc = db.measures().insertMetric(m -> m.setKey(NCLOC_KEY));
@@ -197,6 +204,7 @@ public class TelemetryDaemonTest {
   @Test
   public void send_data_via_client_at_startup_after_initial_delay() throws IOException {
     initTelemetrySettingsToDefaultValues();
+    when(lockManager.tryLock(any(), anyInt())).thenReturn(true);
     settings.setProperty("sonar.telemetry.frequencyInSeconds", "1");
     communityUnderTest.start();
 
@@ -206,6 +214,7 @@ public class TelemetryDaemonTest {
   @Test
   public void data_contains_no_license_type_on_community_edition() throws IOException {
     initTelemetrySettingsToDefaultValues();
+    when(lockManager.tryLock(any(), anyInt())).thenReturn(true);
     settings.setProperty("sonar.telemetry.frequencyInSeconds", "1");
 
     communityUnderTest.start();
@@ -219,6 +228,7 @@ public class TelemetryDaemonTest {
     initTelemetrySettingsToDefaultValues();
     settings.setProperty("sonar.telemetry.frequencyInSeconds", "1");
     when(licenseReader.read()).thenReturn(Optional.empty());
+    when(lockManager.tryLock(any(), anyInt())).thenReturn(true);
 
     commercialUnderTest.start();
 
@@ -234,6 +244,7 @@ public class TelemetryDaemonTest {
     LicenseReader.License license = mock(LicenseReader.License.class);
     when(license.getType()).thenReturn(licenseType);
     when(licenseReader.read()).thenReturn(Optional.of(license));
+    when(lockManager.tryLock(any(), anyInt())).thenReturn(true);
 
     commercialUnderTest.start();
 
@@ -246,6 +257,7 @@ public class TelemetryDaemonTest {
   @Test
   public void check_if_should_send_data_periodically() throws IOException {
     initTelemetrySettingsToDefaultValues();
+    when(lockManager.tryLock(any(), anyInt())).thenReturn(true);
     long now = system2.now();
     long sixDaysAgo = now - (ONE_DAY * 6L);
     long sevenDaysAgo = now - (ONE_DAY * 7L);
@@ -261,6 +273,7 @@ public class TelemetryDaemonTest {
   @Test
   public void send_server_id_and_version() throws IOException {
     initTelemetrySettingsToDefaultValues();
+    when(lockManager.tryLock(any(), anyInt())).thenReturn(true);
     settings.setProperty("sonar.telemetry.frequencyInSeconds", "1");
     String id = randomAlphanumeric(40);
     String version = randomAlphanumeric(10);
@@ -275,6 +288,7 @@ public class TelemetryDaemonTest {
   @Test
   public void send_server_installation_date_and_installation_version() throws IOException {
     initTelemetrySettingsToDefaultValues();
+    when(lockManager.tryLock(any(), anyInt())).thenReturn(true);
     settings.setProperty("sonar.telemetry.frequencyInSeconds", "1");
     String installationVersion = "7.9.BEST.LTS.EVER";
     Long installationDate = 1546300800000L; // 2019/01/01
@@ -290,6 +304,7 @@ public class TelemetryDaemonTest {
   @Test
   public void do_not_send_server_installation_details_if_missing_property() throws IOException {
     initTelemetrySettingsToDefaultValues();
+    when(lockManager.tryLock(any(), anyInt())).thenReturn(true);
     settings.setProperty("sonar.telemetry.frequencyInSeconds", "1");
 
     communityUnderTest.start();
@@ -301,6 +316,7 @@ public class TelemetryDaemonTest {
   @Test
   public void do_not_send_data_if_last_ping_earlier_than_one_week_ago() throws IOException {
     initTelemetrySettingsToDefaultValues();
+    when(lockManager.tryLock(any(), anyInt())).thenReturn(true);
     settings.setProperty("sonar.telemetry.frequencyInSeconds", "1");
     long now = system2.now();
     long sixDaysAgo = now - (ONE_DAY * 6L);
@@ -314,6 +330,7 @@ public class TelemetryDaemonTest {
   @Test
   public void send_data_if_last_ping_is_one_week_ago() throws IOException {
     initTelemetrySettingsToDefaultValues();
+    when(lockManager.tryLock(any(), anyInt())).thenReturn(true);
     settings.setProperty("sonar.telemetry.frequencyInSeconds", "1");
     long today = parseDate("2017-08-01").getTime();
     system2.setNow(today + 15 * ONE_HOUR);
@@ -330,6 +347,7 @@ public class TelemetryDaemonTest {
   @Test
   public void opt_out_sent_once() throws IOException {
     initTelemetrySettingsToDefaultValues();
+    when(lockManager.tryLock(any(), anyInt())).thenReturn(true);
     settings.setProperty("sonar.telemetry.frequencyInSeconds", "1");
     settings.setProperty("sonar.telemetry.enable", "false");
     communityUnderTest.start();
diff --git a/server/sonar-server/src/test/java/org/sonar/server/util/GlobalLockManagerTest.java b/server/sonar-server/src/test/java/org/sonar/server/util/GlobalLockManagerTest.java
new file mode 100644 (file)
index 0000000..8a069e1
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2019 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.util;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.utils.System2;
+import org.sonar.db.DbTester;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.server.util.GlobalLockManager.DEFAULT_LOCK_DURATION_SECONDS;
+
+public class GlobalLockManagerTest {
+
+  private final System2 system2 = mock(System2.class);
+
+  @Rule
+  public final DbTester dbTester = DbTester.create(system2);
+
+  private final GlobalLockManager underTest = new GlobalLockManager(dbTester.getDbClient());
+
+  @Test
+  public void tryLock_succeeds_when_created_for_the_first_time() {
+    assertThat(underTest.tryLock("newName")).isTrue();
+  }
+
+  @Test
+  public void tryLock_fails_when_previous_lock_is_too_recent() {
+    String name = "newName";
+    assertThat(underTest.tryLock(name)).isTrue();
+    assertThat(underTest.tryLock(name)).isFalse();
+  }
+
+  @Test
+  public void tryLock_succeeds_when_previous_lock_is_old_enough() {
+    String name = "newName";
+    long firstLock = 0;
+    long longEnoughAfterFirstLock = firstLock + DEFAULT_LOCK_DURATION_SECONDS * 1000;
+    long notLongEnoughAfterFirstLock = longEnoughAfterFirstLock - 1;
+
+    when(system2.now()).thenReturn(firstLock);
+    assertThat(underTest.tryLock(name)).isTrue();
+
+    when(system2.now()).thenReturn(notLongEnoughAfterFirstLock);
+    assertThat(underTest.tryLock(name)).isFalse();
+
+    when(system2.now()).thenReturn(longEnoughAfterFirstLock);
+    assertThat(underTest.tryLock(name)).isTrue();
+  }
+
+  @Test
+  public void locks_with_different_name_are_independent() {
+    assertThat(underTest.tryLock("newName1")).isTrue();
+    assertThat(underTest.tryLock("newName2")).isTrue();
+  }
+
+}