import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
+import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
+import org.apache.ibatis.exceptions.PersistenceException;
import org.sonar.api.CoreProperties;
import org.sonar.api.ce.ComputeEngineSide;
import org.sonar.api.config.Encryption;
private final Properties systemProps;
private static final ThreadLocal<Map<String, String>> CACHE = new ThreadLocal<>();
+ private Map<String, String> getPropertyDbFailureCache = Collections.emptyMap();
+ private Map<String, String> getPropertiesDbFailureCache = Collections.emptyMap();
private SettingLoader settingLoader;
public ThreadLocalSettings(PropertyDefinitions definitions, Properties props) {
Map<String, String> dbProps = CACHE.get();
// caching is disabled
if (dbProps == null) {
- return Optional.ofNullable(settingLoader.load(key));
+ return Optional.ofNullable(load(key));
}
String loadedValue;
} else {
// cache the effective value (null if the property
// is not persisted)
- loadedValue = settingLoader.load(key);
+ loadedValue = load(key);
dbProps.put(key, loadedValue);
}
return Optional.ofNullable(loadedValue);
}
+ private String load(String key) {
+ try {
+ return settingLoader.load(key);
+ } catch (PersistenceException e) {
+ return getPropertyDbFailureCache.get(key);
+ }
+ }
+
@Override
protected void set(String key, String value) {
requireNonNull(key, "key can't be null");
* Clears the cache specific to the current thread (if any).
*/
public void unload() {
+ Map<String, String> settings = CACHE.get();
CACHE.remove();
+ // update cache of settings to be used in case of DB connectivity error
+ this.getPropertyDbFailureCache = settings;
}
@Override
public Map<String, String> getProperties() {
ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
- settingLoader.loadAll(builder);
- systemProps.entrySet().forEach(entry -> builder.put((String) entry.getKey(), (String) entry.getValue()));
+ loadAll(builder);
+ systemProps.forEach((key, value) -> builder.put((String) key, (String) value));
return builder.build();
}
+
+ private void loadAll(ImmutableMap.Builder<String, String> builder) {
+ try {
+ ImmutableMap.Builder<String, String> cacheBuilder = ImmutableMap.builder();
+ settingLoader.loadAll(cacheBuilder);
+ Map<String, String> cache = cacheBuilder.build();
+ builder.putAll(cache);
+ getPropertiesDbFailureCache = cache;
+ } catch (PersistenceException e) {
+ builder.putAll(getPropertiesDbFailureCache);
+ }
+ }
}
import java.util.Properties;
import java.util.concurrent.CountDownLatch;
import javax.annotation.Nullable;
+import org.apache.ibatis.exceptions.PersistenceException;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
import org.sonar.api.config.PropertyDefinitions;
import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.data.MapEntry.entry;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
public class ThreadLocalSettingsTest {
@Rule
public ExpectedException expectedException = ExpectedException.none();
-
- private MapSettingLoader dbSettingLoader = new MapSettingLoader();
-
@Rule
public TemporaryFolder temp = new TemporaryFolder();
+ private MapSettingLoader dbSettingLoader = new MapSettingLoader();
private ThreadLocalSettings underTest = null;
@After
assertThat(underTest.getProperties()).containsOnly(entry("system", "from system"), entry("db", "from db"), entry("empty", ""));
}
+ @Test
+ public void getProperties_is_not_cached_in_thread_cache() {
+ insertPropertyIntoDb("foo", "bar");
+ underTest = create(Collections.emptyMap());
+ underTest.load();
+
+ assertThat(underTest.getProperties())
+ .containsOnly(entry("foo", "bar"));
+
+ insertPropertyIntoDb("foo2", "bar2");
+ assertThat(underTest.getProperties())
+ .containsOnly(entry("foo", "bar"), entry("foo2", "bar2"));
+
+ underTest.unload();
+
+ assertThat(underTest.getProperties())
+ .containsOnly(entry("foo", "bar"), entry("foo2", "bar2"));
+ }
+
@Test
public void load_creates_a_thread_specific_cache() throws InterruptedException {
insertPropertyIntoDb(A_KEY, "v1");
underTest.unload();
}
+ @Test
+ public void getProperties_return_empty_if_DB_error_on_first_call_ever_out_of_thread_cache() {
+ SettingLoader settingLoaderMock = mock(SettingLoader.class);
+ PersistenceException toBeThrown = new PersistenceException("Faking an error connecting to DB");
+ doThrow(toBeThrown).when(settingLoaderMock).loadAll(any(ImmutableMap.Builder.class));
+ underTest = new ThreadLocalSettings(new PropertyDefinitions(), new Properties(), settingLoaderMock);
+
+ assertThat(underTest.getProperties())
+ .isEmpty();
+ }
+
+ @Test
+ public void getProperties_returns_empty_if_DB_error_on_first_call_ever_in_thread_cache() {
+ SettingLoader settingLoaderMock = mock(SettingLoader.class);
+ PersistenceException toBeThrown = new PersistenceException("Faking an error connecting to DB");
+ doThrow(toBeThrown).when(settingLoaderMock).loadAll(any(ImmutableMap.Builder.class));
+ underTest = new ThreadLocalSettings(new PropertyDefinitions(), new Properties(), settingLoaderMock);
+ underTest.load();
+
+ assertThat(underTest.getProperties())
+ .isEmpty();
+ }
+
+ @Test
+ public void getProperties_return_properties_from_previous_thread_cache_if_DB_error_on_not_first_call() {
+ String key = randomAlphanumeric(3);
+ String value1 = randomAlphanumeric(4);
+ String value2 = randomAlphanumeric(5);
+ SettingLoader settingLoaderMock = mock(SettingLoader.class);
+ PersistenceException toBeThrown = new PersistenceException("Faking an error connecting to DB");
+ doAnswer(invocationOnMock -> {
+ ImmutableMap.Builder<String, String> builder = (ImmutableMap.Builder<String, String>) invocationOnMock.getArguments()[0];
+ builder.put(key, value1);
+ return null;
+ }).doThrow(toBeThrown)
+ .doAnswer(invocationOnMock -> {
+ ImmutableMap.Builder<String, String> builder = (ImmutableMap.Builder<String, String>) invocationOnMock.getArguments()[0];
+ builder.put(key, value2);
+ return null;
+ })
+ .when(settingLoaderMock)
+ .loadAll(any(ImmutableMap.Builder.class));
+ underTest = new ThreadLocalSettings(new PropertyDefinitions(), new Properties(), settingLoaderMock);
+
+ underTest.load();
+ assertThat(underTest.getProperties())
+ .containsOnly(entry(key, value1));
+ underTest.unload();
+
+ underTest.load();
+ assertThat(underTest.getProperties())
+ .containsOnly(entry(key, value1));
+ underTest.unload();
+
+ underTest.load();
+ assertThat(underTest.getProperties())
+ .containsOnly(entry(key, value2));
+ underTest.unload();
+ }
+
+ @Test
+ public void get_returns_empty_if_DB_error_on_first_call_ever_out_of_thread_cache() {
+ SettingLoader settingLoaderMock = mock(SettingLoader.class);
+ PersistenceException toBeThrown = new PersistenceException("Faking an error connecting to DB");
+ String key = randomAlphanumeric(3);
+ doThrow(toBeThrown).when(settingLoaderMock).load(key);
+ underTest = new ThreadLocalSettings(new PropertyDefinitions(), new Properties(), settingLoaderMock);
+
+ assertThat(underTest.get(key)).isEmpty();
+ }
+
+ @Test
+ public void get_returns_empty_if_DB_error_on_first_call_ever_in_thread_cache() {
+ SettingLoader settingLoaderMock = mock(SettingLoader.class);
+ PersistenceException toBeThrown = new PersistenceException("Faking an error connecting to DB");
+ String key = randomAlphanumeric(3);
+ doThrow(toBeThrown).when(settingLoaderMock).load(key);
+ underTest = new ThreadLocalSettings(new PropertyDefinitions(), new Properties(), settingLoaderMock);
+ underTest.load();
+
+ assertThat(underTest.get(key)).isEmpty();
+ }
+
private void insertPropertyIntoDb(String key, @Nullable String value) {
dbSettingLoader.put(key, value);
}