aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-webserver-api
diff options
context:
space:
mode:
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>2019-09-03 12:46:13 +0200
committerSonarTech <sonartech@sonarsource.com>2019-09-16 20:21:02 +0200
commitbb7e74da6d8a3fdd27317af2e92c4ec106e92956 (patch)
treee3033886ae66599a47c37cf374f13417bf2ea2ed /server/sonar-webserver-api
parent4d950b9bb39dab261ea5da4d719cf8fd2c9d8d38 (diff)
downloadsonarqube-bb7e74da6d8a3fdd27317af2e92c4ec106e92956.tar.gz
sonarqube-bb7e74da6d8a3fdd27317af2e92c4ec106e92956.zip
SONAR-12398 make schedule refresh of porfolios work
Diffstat (limited to 'server/sonar-webserver-api')
-rw-r--r--server/sonar-webserver-api/build.gradle4
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/util/GlobalLockManager.java45
-rw-r--r--server/sonar-webserver-api/src/main/java/org/sonar/server/util/GlobalLockManagerImpl.java63
-rw-r--r--server/sonar-webserver-api/src/test/java/org/sonar/server/util/GlobalLockManagerImplTest.java189
-rw-r--r--server/sonar-webserver-api/src/test/java/org/sonar/server/util/GlobalLockManagerTest.java76
-rw-r--r--server/sonar-webserver-api/src/testFixtures/java/org/sonar/server/util/GlobalLockManagerRule.java66
6 files changed, 337 insertions, 106 deletions
diff --git a/server/sonar-webserver-api/build.gradle b/server/sonar-webserver-api/build.gradle
index 561fd3957d5..bdf336d1874 100644
--- a/server/sonar-webserver-api/build.gradle
+++ b/server/sonar-webserver-api/build.gradle
@@ -37,4 +37,8 @@ dependencies {
testCompile 'org.mockito:mockito-core'
testCompile testFixtures(project(':server:sonar-server-common'))
testCompile project(':sonar-testing-harness')
+
+ testFixturesApi 'junit:junit'
+
+ testFixturesCompileOnly 'com.google.code.findbugs:jsr305'
}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/util/GlobalLockManager.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/util/GlobalLockManager.java
index 81f13b22a1d..debbc539f7d 100644
--- a/server/sonar-webserver-api/src/main/java/org/sonar/server/util/GlobalLockManager.java
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/util/GlobalLockManager.java
@@ -19,41 +19,26 @@
*/
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;
+import org.sonar.db.property.InternalPropertiesDao;
-/**
- * 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 interface GlobalLockManager {
- public GlobalLockManager(DbClient dbClient) {
- this.dbClient = dbClient;
- }
+ int LOCK_NAME_MAX_LENGTH = InternalPropertiesDao.LOCK_NAME_MAX_LENGTH;
+ int DEFAULT_LOCK_DURATION_SECONDS = 180;
/**
- * Try to acquire a lock on the given name in the default namespace,
+ * Try to acquire a lock on the given name for the {@link #DEFAULT_LOCK_DURATION_SECONDS default duration},
* using the generic locking mechanism of {@see org.sonar.db.property.InternalPropertiesDao}.
+ *
+ * @throws IllegalArgumentException if name's length is > {@link #LOCK_NAME_MAX_LENGTH} or empty
*/
- public boolean tryLock(String name) {
- return tryLock(name, DEFAULT_LOCK_DURATION_SECONDS);
- }
+ boolean tryLock(String name);
- 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;
- }
- }
+ /**
+ * Try to acquire a lock on the given name for the specified duration,
+ * using the generic locking mechanism of {@see org.sonar.db.property.InternalPropertiesDao}.
+ *
+ * @throws IllegalArgumentException if name's length is > {@link #LOCK_NAME_MAX_LENGTH} or empty
+ */
+ boolean tryLock(String name, int durationSecond);
}
diff --git a/server/sonar-webserver-api/src/main/java/org/sonar/server/util/GlobalLockManagerImpl.java b/server/sonar-webserver-api/src/main/java/org/sonar/server/util/GlobalLockManagerImpl.java
new file mode 100644
index 00000000000..7611475cd2d
--- /dev/null
+++ b/server/sonar-webserver-api/src/main/java/org/sonar/server/util/GlobalLockManagerImpl.java
@@ -0,0 +1,63 @@
+/*
+ * 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;
+import org.sonar.db.property.InternalPropertiesDao;
+
+import static org.sonar.api.utils.Preconditions.checkArgument;
+
+/**
+ * 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 GlobalLockManagerImpl implements GlobalLockManager {
+
+ private final DbClient dbClient;
+
+ public GlobalLockManagerImpl(DbClient dbClient) {
+ this.dbClient = dbClient;
+ }
+
+ @Override
+ public boolean tryLock(String name) {
+ return tryLock(name, DEFAULT_LOCK_DURATION_SECONDS);
+ }
+
+ @Override
+ public boolean tryLock(String name, int durationSecond) {
+ checkArgument(
+ !name.isEmpty() && name.length() <= LOCK_NAME_MAX_LENGTH,
+ "name's length must be > 0 and <= %s: '%s'", LOCK_NAME_MAX_LENGTH, name);
+ checkArgument(durationSecond > 0, "duration must be > 0: %s", durationSecond);
+
+ try (DbSession dbSession = dbClient.openSession(false)) {
+ boolean success = dbClient.internalPropertiesDao().tryLock(dbSession, name, durationSecond);
+ dbSession.commit();
+ return success;
+ }
+ }
+}
diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/util/GlobalLockManagerImplTest.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/util/GlobalLockManagerImplTest.java
new file mode 100644
index 00000000000..3abb897d49c
--- /dev/null
+++ b/server/sonar-webserver-api/src/test/java/org/sonar/server/util/GlobalLockManagerImplTest.java
@@ -0,0 +1,189 @@
+/*
+ * 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 com.tngtech.java.junit.dataprovider.DataProvider;
+import com.tngtech.java.junit.dataprovider.DataProviderRunner;
+import com.tngtech.java.junit.dataprovider.UseDataProvider;
+import java.util.Random;
+import org.apache.commons.lang.RandomStringUtils;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.property.InternalPropertiesDao;
+
+import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+import static org.sonar.server.util.GlobalLockManager.DEFAULT_LOCK_DURATION_SECONDS;
+
+@RunWith(DataProviderRunner.class)
+public class GlobalLockManagerImplTest {
+
+ private static final int LOCK_NAME_MAX_LENGTH = 15;
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ private final DbClient dbClient = mock(DbClient.class);
+ private final InternalPropertiesDao internalPropertiesDao = mock(InternalPropertiesDao.class);
+ private final DbSession dbSession = mock(DbSession.class);
+ private final GlobalLockManager underTest = new GlobalLockManagerImpl(dbClient);
+
+ @Before
+ public void wire_db_mocks() {
+ when(dbClient.openSession(false)).thenReturn(dbSession);
+ when(dbClient.internalPropertiesDao()).thenReturn(internalPropertiesDao);
+ }
+
+ @Test
+ public void tryLock_fails_with_IAE_if_name_is_empty() {
+ String badLockName = "";
+
+ expectBadLockNameIAE(badLockName);
+
+ underTest.tryLock(badLockName);
+ }
+
+ @Test
+ public void tryLock_fails_with_IAE_if_name_length_is_16_or_more() {
+ String badLockName = RandomStringUtils.random(LOCK_NAME_MAX_LENGTH + 1 + new Random().nextInt(96));
+
+ expectBadLockNameIAE(badLockName);
+
+ underTest.tryLock(badLockName);
+ }
+
+ @Test
+ public void tryLock_accepts_name_with_length_15_or_less() {
+ for (int i = 1; i <= LOCK_NAME_MAX_LENGTH; i++) {
+ underTest.tryLock(RandomStringUtils.random(i));
+ }
+ }
+
+ @Test
+ @UseDataProvider("randomValidLockName")
+ public void tryLock_delegates_to_internalPropertiesDao_and_commits(String randomValidLockName) {
+ boolean expected = new Random().nextBoolean();
+ when(internalPropertiesDao.tryLock(dbSession, randomValidLockName, DEFAULT_LOCK_DURATION_SECONDS))
+ .thenReturn(expected);
+
+ assertThat(underTest.tryLock(randomValidLockName)).isEqualTo(expected);
+
+ verify(dbClient).openSession(false);
+ verify(internalPropertiesDao).tryLock(dbSession, randomValidLockName, DEFAULT_LOCK_DURATION_SECONDS);
+ verify(dbSession).commit();
+ verifyNoMoreInteractions(internalPropertiesDao);
+ }
+
+ @Test
+ @UseDataProvider("randomValidDuration")
+ public void tryLock_with_duration_fails_with_IAE_if_name_is_empty(int randomValidDuration) {
+ String badLockName = "";
+
+ expectBadLockNameIAE(badLockName);
+
+ underTest.tryLock(badLockName, randomValidDuration);
+ }
+
+ @Test
+ @UseDataProvider("randomValidDuration")
+ public void tryLock_with_duration_accepts_name_with_length_15_or_less(int randomValidDuration) {
+ for (int i = 1; i <= 15; i++) {
+ underTest.tryLock(RandomStringUtils.random(i), randomValidDuration);
+ }
+ }
+
+ @Test
+ @UseDataProvider("randomValidDuration")
+ public void tryLock_with_duration_fails_with_IAE_if_name_length_is_16_or_more(int randomValidDuration) {
+ String badLockName = RandomStringUtils.random(LOCK_NAME_MAX_LENGTH + 1 + new Random().nextInt(65));
+
+ expectBadLockNameIAE(badLockName);
+
+ underTest.tryLock(badLockName, randomValidDuration);
+ }
+
+ @Test
+ @UseDataProvider("randomValidLockName")
+ public void tryLock_with_duration_fails_with_IAE_if_duration_is_0(String randomValidLockName) {
+ expectBadDuration(0);
+
+ underTest.tryLock(randomValidLockName, 0);
+ }
+
+ @Test
+ @UseDataProvider("randomValidLockName")
+ public void tryLock_with_duration_fails_with_IAE_if_duration_is_less_than_0(String randomValidLockName) {
+ int negativeDuration = -1 - new Random().nextInt(100);
+
+ expectBadDuration(negativeDuration);
+
+ underTest.tryLock(randomValidLockName, negativeDuration);
+ }
+
+ @Test
+ @UseDataProvider("randomValidDuration")
+ public void tryLock_with_duration_delegates_to_InternalPropertiesDao_and_commits(int randomValidDuration) {
+ String lockName = "foo";
+ boolean expected = new Random().nextBoolean();
+ when(internalPropertiesDao.tryLock(dbSession, lockName, randomValidDuration))
+ .thenReturn(expected);
+
+ assertThat(underTest.tryLock(lockName, randomValidDuration)).isEqualTo(expected);
+
+ verify(dbClient).openSession(false);
+ verify(internalPropertiesDao).tryLock(dbSession, lockName, randomValidDuration);
+ verify(dbSession).commit();
+ verifyNoMoreInteractions(internalPropertiesDao);
+ }
+
+ @DataProvider
+ public static Object[][] randomValidLockName() {
+ return new Object[][] {
+ {randomAlphabetic(1 + new Random().nextInt(LOCK_NAME_MAX_LENGTH))}
+ };
+ }
+
+ @DataProvider
+ public static Object[][] randomValidDuration() {
+ return new Object[][] {
+ {1+ new Random().nextInt(2_00)}
+ };
+ }
+
+ private void expectBadLockNameIAE(String badLockName) {
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("name's length must be > 0 and <= " + LOCK_NAME_MAX_LENGTH + ": '" + badLockName + "'");
+ }
+
+ private void expectBadDuration(int badDuration) {
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("duration must be > 0: " + badDuration);
+ }
+
+}
diff --git a/server/sonar-webserver-api/src/test/java/org/sonar/server/util/GlobalLockManagerTest.java b/server/sonar-webserver-api/src/test/java/org/sonar/server/util/GlobalLockManagerTest.java
deleted file mode 100644
index 8a069e1eae2..00000000000
--- a/server/sonar-webserver-api/src/test/java/org/sonar/server/util/GlobalLockManagerTest.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * 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();
- }
-
-}
diff --git a/server/sonar-webserver-api/src/testFixtures/java/org/sonar/server/util/GlobalLockManagerRule.java b/server/sonar-webserver-api/src/testFixtures/java/org/sonar/server/util/GlobalLockManagerRule.java
new file mode 100644
index 00000000000..86d5b53b96c
--- /dev/null
+++ b/server/sonar-webserver-api/src/testFixtures/java/org/sonar/server/util/GlobalLockManagerRule.java
@@ -0,0 +1,66 @@
+/*
+ * 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 java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.Test;
+import org.junit.rules.ExternalResource;
+
+import static org.sonar.api.utils.Preconditions.checkArgument;
+
+/**
+ * {@link org.junit.Rule} implementing {@link GlobalLockManager} to test against this interface without consideration
+ * of time, only attempts to acquire a given lock.
+ */
+public class GlobalLockManagerRule extends ExternalResource implements GlobalLockManager {
+ private final Map<String, Deque<Boolean>> lockAttemptsByLockName = new HashMap<>();
+
+ @Test
+ public GlobalLockManagerRule addAttempt(String lockName, boolean success) {
+ lockAttemptsByLockName.compute(lockName, (k, v) -> {
+ Deque<Boolean> queue = v == null ? new ArrayDeque<>() : v;
+ queue.push(success);
+ return queue;
+ });
+ return this;
+ }
+
+ @Override
+ public boolean tryLock(String name) {
+ checkArgument(!name.isEmpty() && name.length() <= LOCK_NAME_MAX_LENGTH, "invalid lock name");
+
+ Deque<Boolean> deque = lockAttemptsByLockName.get(name);
+ Boolean res = deque == null ? null : deque.pop();
+ if (res == null) {
+ throw new IllegalStateException("No more attempt value available");
+ }
+ return res;
+ }
+
+ @Override
+ public boolean tryLock(String name, int durationSecond) {
+ checkArgument(durationSecond > 0, "negative duration not allowed");
+
+ return tryLock(name);
+ }
+}