diff options
author | Sébastien Lesaint <sebastien.lesaint@sonarsource.com> | 2019-09-03 12:46:13 +0200 |
---|---|---|
committer | SonarTech <sonartech@sonarsource.com> | 2019-09-16 20:21:02 +0200 |
commit | bb7e74da6d8a3fdd27317af2e92c4ec106e92956 (patch) | |
tree | e3033886ae66599a47c37cf374f13417bf2ea2ed /server/sonar-webserver-api | |
parent | 4d950b9bb39dab261ea5da4d719cf8fd2c9d8d38 (diff) | |
download | sonarqube-bb7e74da6d8a3fdd27317af2e92c4ec106e92956.tar.gz sonarqube-bb7e74da6d8a3fdd27317af2e92c4ec106e92956.zip |
SONAR-12398 make schedule refresh of porfolios work
Diffstat (limited to 'server/sonar-webserver-api')
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); + } +} |