@@ -46,9 +46,11 @@ public class InternalPropertiesDao implements Dao { | |||
/** | |||
* A common prefix used by locks. {@see InternalPropertiesDao#tryLock} | |||
*/ | |||
public static final String LOCK_PREFIX = "lock."; | |||
private static final String LOCK_PREFIX = "lock."; | |||
private static final int KEY_MAX_LENGTH = 20; | |||
public static final int LOCK_NAME_MAX_LENGTH = KEY_MAX_LENGTH - LOCK_PREFIX.length(); | |||
static final int KEY_MAX_LENGTH = 20; | |||
private static final int TEXT_VALUE_MAX_LENGTH = 4000; | |||
private static final Optional<String> OPTIONAL_OF_EMPTY_STRING = Optional.of(""); | |||
@@ -190,13 +192,22 @@ public class InternalPropertiesDao implements Dao { | |||
* and the atomic replacement of the timestamp succeeds. | |||
* | |||
* The lock is considered released when the specified duration has elapsed. | |||
* | |||
* @throws IllegalArgumentException if name's length is > {@link #LOCK_NAME_MAX_LENGTH} | |||
* @throws IllegalArgumentException if maxAgeInSeconds is <= 0 | |||
*/ | |||
public boolean tryLock(DbSession dbSession, String name, int maxAgeInSeconds) { | |||
String key = LOCK_PREFIX + '.' + name; | |||
if (key.length() > KEY_MAX_LENGTH) { | |||
if (name.isEmpty()) { | |||
throw new IllegalArgumentException("lock name can't be empty"); | |||
} | |||
if (name.length() > LOCK_NAME_MAX_LENGTH) { | |||
throw new IllegalArgumentException("lock name is too long"); | |||
} | |||
if (maxAgeInSeconds <= 0) { | |||
throw new IllegalArgumentException("maxAgeInSeconds must be > 0"); | |||
} | |||
String key = LOCK_PREFIX + name; | |||
long now = system2.now(); | |||
Optional<String> timestampAsStringOpt = selectByKey(dbSession, key); |
@@ -54,8 +54,6 @@ 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.db.property.InternalPropertiesDao.KEY_MAX_LENGTH; | |||
import static org.sonar.db.property.InternalPropertiesDao.LOCK_PREFIX; | |||
public class InternalPropertiesDaoTest { | |||
@@ -409,7 +407,7 @@ public class InternalPropertiesDaoTest { | |||
when(system2.now()).thenReturn(now); | |||
assertThat(underTest.tryLock(dbSession, A_KEY, 60)).isTrue(); | |||
assertThat(underTest.selectByKey(dbSession, key(A_KEY))).contains(String.valueOf(now)); | |||
assertThat(underTest.selectByKey(dbSession, propertyKeyOf(A_KEY))).contains(String.valueOf(now)); | |||
} | |||
@Test | |||
@@ -424,7 +422,7 @@ public class InternalPropertiesDaoTest { | |||
when(system2.now()).thenReturn(now); | |||
assertThat(underTest.tryLock(dbSession, A_KEY, lockDurationSeconds)).isTrue(); | |||
assertThat(underTest.selectByKey(dbSession, key(A_KEY))).contains(String.valueOf(now)); | |||
assertThat(underTest.selectByKey(dbSession, propertyKeyOf(A_KEY))).contains(String.valueOf(now)); | |||
} | |||
@Test | |||
@@ -434,13 +432,13 @@ public class InternalPropertiesDaoTest { | |||
assertThat(underTest.tryLock(dbSession, A_KEY, 60)).isTrue(); | |||
assertThat(underTest.tryLock(dbSession, A_KEY, 60)).isFalse(); | |||
assertThat(underTest.selectByKey(dbSession, key(A_KEY))).contains(String.valueOf(now)); | |||
assertThat(underTest.selectByKey(dbSession, propertyKeyOf(A_KEY))).contains(String.valueOf(now)); | |||
} | |||
@Test | |||
public void tryLock_fails_if_it_would_insert_concurrently() { | |||
String name = randomAlphabetic(5); | |||
String key = key(name); | |||
String propertyKey = propertyKeyOf(name); | |||
long now = new Random().nextInt(); | |||
when(system2.now()).thenReturn(now); | |||
@@ -449,22 +447,22 @@ public class InternalPropertiesDaoTest { | |||
InternalPropertiesMapper mapperMock = mock(InternalPropertiesMapper.class); | |||
DbSession dbSessionMock = mock(DbSession.class); | |||
when(dbSessionMock.getMapper(InternalPropertiesMapper.class)).thenReturn(mapperMock); | |||
when(mapperMock.selectAsText(ImmutableList.of(key))) | |||
when(mapperMock.selectAsText(ImmutableList.of(propertyKey))) | |||
.thenReturn(ImmutableList.of()); | |||
doThrow(RuntimeException.class).when(mapperMock).insertAsText(eq(key), anyString(), anyLong()); | |||
doThrow(RuntimeException.class).when(mapperMock).insertAsText(eq(propertyKey), anyString(), anyLong()); | |||
assertThat(underTest.tryLock(dbSessionMock, name, 60)).isFalse(); | |||
assertThat(underTest.selectByKey(dbSession, key)).contains(String.valueOf(now)); | |||
assertThat(underTest.selectByKey(dbSession, propertyKey)).contains(String.valueOf(now)); | |||
} | |||
@Test | |||
public void tryLock_fails_if_concurrent_caller_succeeded_first() { | |||
int lockDurationSeconds = 60; | |||
String name = randomAlphabetic(5); | |||
String key = key(name); | |||
String propertyKey = propertyKeyOf(name); | |||
long now = 123456;//new Random().nextInt(); | |||
long now = new Random().nextInt(4_889_989); | |||
long oldTimestamp = now - lockDurationSeconds * 1000; | |||
when(system2.now()).thenReturn(oldTimestamp); | |||
assertThat(underTest.tryLock(dbSession, name, lockDurationSeconds)).isTrue(); | |||
@@ -474,27 +472,36 @@ public class InternalPropertiesDaoTest { | |||
DbSession dbSessionMock = mock(DbSession.class); | |||
when(dbSessionMock.getMapper(InternalPropertiesMapper.class)).thenReturn(mapperMock); | |||
InternalPropertyDto dto = new InternalPropertyDto(); | |||
dto.setKey(key); | |||
dto.setKey(propertyKey); | |||
dto.setValue(String.valueOf(oldTimestamp - 1)); | |||
when(mapperMock.selectAsText(ImmutableList.of(key))) | |||
when(mapperMock.selectAsText(ImmutableList.of(propertyKey))) | |||
.thenReturn(ImmutableList.of(dto)); | |||
assertThat(underTest.tryLock(dbSessionMock, name, lockDurationSeconds)).isFalse(); | |||
assertThat(underTest.selectByKey(dbSession, key)).contains(String.valueOf(oldTimestamp)); | |||
assertThat(underTest.selectByKey(dbSession, propertyKey)).contains(String.valueOf(oldTimestamp)); | |||
} | |||
@Test | |||
public void tryLock_throws_if_lock_name_would_produce_too_long_key() { | |||
String tooLongName = randomAlphabetic(KEY_MAX_LENGTH - LOCK_PREFIX.length()); | |||
public void tryLock_throws_IAE_if_lock_name_is_empty() { | |||
expectedException.expect(IllegalArgumentException.class); | |||
expectedException.expectMessage("lock name can't be empty"); | |||
underTest.tryLock(dbSession, "", 60); | |||
} | |||
@Test | |||
public void tryLock_throws_IAE_if_lock_name_length_is_16_or_more() { | |||
String tooLongName = randomAlphabetic(16 + new Random().nextInt(30)); | |||
expectedException.expect(IllegalArgumentException.class); | |||
expectedException.expectMessage("lock name is too long"); | |||
underTest.tryLock(dbSession, tooLongName, 60); | |||
} | |||
private String key(String name) { | |||
return LOCK_PREFIX + '.' + name; | |||
private static String propertyKeyOf(String lockName) { | |||
return "lock." + lockName; | |||
} | |||
private void expectKeyNullOrEmptyIAE() { |
@@ -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' | |||
} |
@@ -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); | |||
} |
@@ -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; | |||
} | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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(); | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -39,6 +39,7 @@ import org.sonar.server.measure.index.ProjectMeasuresIndexer; | |||
import org.sonar.server.permission.index.PermissionIndexerTester; | |||
import org.sonar.server.permission.index.WebAuthorizationTypeSupport; | |||
import org.sonar.server.util.GlobalLockManager; | |||
import org.sonar.server.util.GlobalLockManagerImpl; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.mockito.ArgumentMatchers.any; | |||
@@ -65,7 +66,7 @@ public class ProjectsInWarningDaemonTest { | |||
private ProjectMeasuresIndex projectMeasuresIndex = new ProjectMeasuresIndex(es.client(), new WebAuthorizationTypeSupport(null), System2.INSTANCE); | |||
private MapSettings settings = new MapSettings(); | |||
private GlobalLockManager lockManager = mock(GlobalLockManager.class); | |||
private GlobalLockManager lockManager = mock(GlobalLockManagerImpl.class); | |||
private ProjectsInWarning projectsInWarning = new ProjectsInWarning(); | |||
private ProjectsInWarningDaemon underTest = new ProjectsInWarningDaemon(db.getDbClient(), projectMeasuresIndex, settings.asConfig(), lockManager, projectsInWarning); |
@@ -51,6 +51,7 @@ 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.server.util.GlobalLockManagerImpl; | |||
import org.sonar.updatecenter.common.Version; | |||
import static java.util.Arrays.asList; | |||
@@ -96,7 +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 final GlobalLockManager lockManager = mock(GlobalLockManagerImpl.class); | |||
private FakeServer server = new FakeServer(); | |||
private PluginRepository pluginRepository = mock(PluginRepository.class); | |||
private TestSystem2 system2 = new TestSystem2().setNow(System.currentTimeMillis()); |
@@ -29,6 +29,7 @@ import org.sonar.api.internal.MetadataLoader; | |||
import org.sonar.api.internal.SonarRuntimeImpl; | |||
import org.sonar.api.utils.System2; | |||
import org.sonar.api.utils.Version; | |||
import org.sonar.server.util.GlobalLockManagerImpl; | |||
import org.sonar.server.util.TempFolderCleaner; | |||
import org.sonar.core.config.CorePropertyDefinitions; | |||
import org.sonar.core.extension.CoreExtensionRepositoryImpl; | |||
@@ -61,7 +62,6 @@ 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; | |||
@@ -129,7 +129,7 @@ public class PlatformLevel1 extends PlatformLevel { | |||
// issues | |||
IssueIndex.class, | |||
GlobalLockManager.class, | |||
GlobalLockManagerImpl.class, | |||
new OkHttpClientProvider(), | |||