blob: 35a28c08b2c4ed8117f8269efaaa4958ae89377b [file] [log] [blame]
// Copyright (C) 2018 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.server.cache;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import com.google.gerrit.server.cache.PerThreadCache.Key;
import com.google.gerrit.server.cache.PerThreadCache.ReadonlyRequestWindow;
import com.google.gerrit.server.git.RefCache;
import com.google.gerrit.util.http.testutil.FakeHttpServletRequest;
import java.io.IOException;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import org.eclipse.jgit.lib.ObjectId;
import org.junit.Test;
public class PerThreadCacheTest {
@Test
public void key_respectsClass() {
assertThat(PerThreadCache.Key.create(String.class))
.isEqualTo(PerThreadCache.Key.create(String.class));
assertThat(PerThreadCache.Key.create(String.class))
.isNotEqualTo(PerThreadCache.Key.create(Integer.class));
}
@Test
public void key_respectsIdentifiers() {
assertThat(PerThreadCache.Key.create(String.class, "id1"))
.isEqualTo(PerThreadCache.Key.create(String.class, "id1"));
assertThat(PerThreadCache.Key.create(String.class, "id1"))
.isNotEqualTo(PerThreadCache.Key.create(String.class, "id2"));
}
@Test
public void endToEndCache() {
try (PerThreadCache ignored = PerThreadCache.create(null)) {
PerThreadCache cache = PerThreadCache.get();
PerThreadCache.Key<String> key1 = PerThreadCache.Key.create(String.class);
assertThat(cache.getWithLoader(key1, () -> "value1", null)).hasValue("value1");
Supplier<String> neverCalled =
() -> {
throw new IllegalStateException("this method must not be called");
};
assertThat(cache.getWithLoader(key1, neverCalled, null)).hasValue("value1");
}
}
@Test
public void cleanUp() {
PerThreadCache.Key<String> key = PerThreadCache.Key.create(String.class);
try (PerThreadCache ignored = PerThreadCache.create(null)) {
PerThreadCache cache = PerThreadCache.get();
assertThat(cache.getWithLoader(key, () -> "value1", null)).hasValue("value1");
}
// Create a second cache and assert that it is not connected to the first one.
// This ensures that the cleanup is actually working.
try (PerThreadCache ignored = PerThreadCache.create(null)) {
PerThreadCache cache = PerThreadCache.get();
assertThat(cache.getWithLoader(key, () -> "value2", null)).hasValue("value2");
}
}
@Test
public void unloaderCalledUponCleanup() {
AtomicBoolean unloaderCalled = new AtomicBoolean();
PerThreadCache.Key<String> key = PerThreadCache.Key.create(String.class);
try (PerThreadCache ignored = PerThreadCache.create(null)) {
PerThreadCache cache = PerThreadCache.get();
cache.getWithLoader(key, () -> "value1", v -> unloaderCalled.set(true));
assertThat(unloaderCalled.get()).isFalse();
}
assertThat(unloaderCalled.get()).isTrue();
}
@Test
public void doubleInstantiationFails() {
try (PerThreadCache ignored = PerThreadCache.create(null)) {
IllegalStateException thrown =
assertThrows(IllegalStateException.class, () -> PerThreadCache.create(null));
assertThat(thrown).hasMessageThat().contains("called create() twice on the same request");
}
}
@Test
public void isAssociatedWithHttpReadonlyRequest() {
HttpServletRequest getRequest = new FakeHttpServletRequest();
try (PerThreadCache cache = PerThreadCache.create(getRequest)) {
assertThat(cache.allowRefCache()).isTrue();
}
}
@Test
public void isAssociatedWithHttpWriteRequest() {
HttpServletRequest putRequest =
new HttpServletRequestWrapper(new FakeHttpServletRequest()) {
@Override
public String getMethod() {
return "PUT";
}
};
try (PerThreadCache cache = PerThreadCache.create(putRequest)) {
assertThat(cache.allowRefCache()).isFalse();
}
}
@Test
public void isNotAssociatedWithHttpRequest() {
try (PerThreadCache cache = PerThreadCache.create(null)) {
assertThat(cache.allowRefCache()).isFalse();
}
}
@Test
public void isAssociatedWithReadonlyRequest() {
try (PerThreadCache cache = PerThreadCache.createReadOnly()) {
assertThat(cache.allowRefCache()).isTrue();
}
}
@Test
public void openNestedTemporaryReadonlyWindows() throws Exception {
try (PerThreadCache cache = PerThreadCache.create(null)) {
assertThat(cache.allowRefCache()).isFalse();
try (ReadonlyRequestWindow outerWindow = PerThreadCache.openReadonlyRequestWindow()) {
assertThat(cache.allowRefCache()).isTrue();
try (ReadonlyRequestWindow innerWindow = PerThreadCache.openReadonlyRequestWindow()) {
assertThat(cache.allowRefCache()).isTrue();
}
assertThat(cache.allowRefCache()).isTrue();
}
assertThat(cache.allowRefCache()).isFalse();
}
}
static class TestCacheValue implements RefCache {
public static final String STALENESS_FAILED_MESSAGE = "Staleness check failed";
private final String value;
public TestCacheValue(String value) {
this.value = value;
}
@Override
public String toString() {
return value;
}
@Override
public boolean equals(Object obj) {
return obj != null
&& (obj instanceof TestCacheValue)
&& ((TestCacheValue) obj).value.equals(value);
}
@Override
public int hashCode() {
return value.hashCode();
}
public void checkStaleness() throws IllegalStateException {
throw new IllegalStateException(STALENESS_FAILED_MESSAGE);
}
@Override
public Optional<ObjectId> get(String refName) throws IOException {
return Optional.empty();
}
@Override
public void close() {}
}
@Test
public void clearOutStaleEntriesAfterReadonlyWindow() throws Exception {
Key<TestCacheValue> key = PerThreadCache.Key.create(TestCacheValue.class, "key");
TestCacheValue cachedValue = new TestCacheValue("cached value");
TestCacheValue updatedValue = new TestCacheValue("updated value");
try (PerThreadCache cache = PerThreadCache.create(null)) {
try (ReadonlyRequestWindow outerWindow = PerThreadCache.openReadonlyRequestWindow()) {
assertThat(PerThreadCache.get(key, () -> cachedValue, v -> {})).hasValue(cachedValue);
assertThat(PerThreadCache.get(key, () -> updatedValue, v -> {})).hasValue(cachedValue);
}
assertThat(PerThreadCache.get(key, () -> updatedValue, v -> {})).hasValue(updatedValue);
}
}
@Test
public void checkForStaleEntriesAfterReadonlyWindow() {
Key<TestCacheValue> key = PerThreadCache.Key.create(TestCacheValue.class, "key");
try (PerThreadCache cache = PerThreadCache.create(null)) {
IllegalStateException thrown =
assertThrows(
IllegalStateException.class,
() -> {
try (ReadonlyRequestWindow outerWindow =
PerThreadCache.openReadonlyRequestWindow()) {
PerThreadCache.get(
key, () -> new TestCacheValue(""), TestCacheValue::checkStaleness);
}
});
assertThat(thrown).hasMessageThat().isEqualTo(TestCacheValue.STALENESS_FAILED_MESSAGE);
}
}
@Test
public void allowStaleEntriesAfterReadonlyWindow() {
Key<TestCacheValue> key = PerThreadCache.Key.create(TestCacheValue.class, "key");
TestCacheValue value = new TestCacheValue("");
Optional<TestCacheValue> cachedValue;
try (PerThreadCache cache = PerThreadCache.create(null)) {
try (ReadonlyRequestWindow window = PerThreadCache.openReadonlyRequestWindow()) {
cachedValue = PerThreadCache.get(key, () -> value, null);
}
}
assertThat(cachedValue).hasValue(value);
}
@SuppressWarnings("deprecation")
@Test
public void disableCachingForSpecificTypes() {
System.setProperty(
PerThreadCache.PER_THREAD_CACHE_DISABLED_TYPES_PROPERTY,
String.class.getCanonicalName() + "," + Integer.class.getCanonicalName());
try {
try (PerThreadCache cache = PerThreadCache.createReadOnly()) {
assertThat(
PerThreadCache.get(
PerThreadCache.Key.create(String.class, "key"), () -> "oldValue", null))
.isEmpty();
assertThat(
PerThreadCache.getOrCompute(
PerThreadCache.Key.create(String.class, "key"), () -> "newValue"))
.isEqualTo("newValue");
assertThat(
PerThreadCache.get(PerThreadCache.Key.create(Integer.class, "key"), () -> 1, null))
.isEmpty();
assertThat(
PerThreadCache.getOrCompute(
PerThreadCache.Key.create(Integer.class, "key"), () -> 2))
.isEqualTo(2);
assertThat(PerThreadCache.get(PerThreadCache.Key.create(Long.class, "key"), () -> 1L, null))
.hasValue(1L);
assertThat(
PerThreadCache.getOrCompute(PerThreadCache.Key.create(Long.class, "key"), () -> 2L))
.isEqualTo(1L);
}
} finally {
System.setProperty(PerThreadCache.PER_THREAD_CACHE_DISABLED_TYPES_PROPERTY, "");
}
}
@SuppressWarnings("deprecation")
@Test
public void enforceMaxSize() {
try (PerThreadCache cache = PerThreadCache.create(null)) {
fillTheCache(cache);
// Assert that the value was not persisted
PerThreadCache.Key<String> key = PerThreadCache.Key.create(String.class, 1000);
assertThat(cache.getWithLoader(key, () -> "new value", null)).isEmpty();
String value = cache.get(key, () -> "directly served");
assertThat(value).isEqualTo("directly served");
}
}
@Test
public void returnEmptyWhenCacheIsFull() {
try (PerThreadCache cache = PerThreadCache.create(null)) {
fillTheCache(cache);
PerThreadCache.Key<String> key = PerThreadCache.Key.create(String.class, 1000);
assertThat(cache.getWithLoader(key, () -> "new value", null)).isEmpty();
}
}
@Test
public void unloaderNotCalledUponCleanupIfCacheWasFull() {
AtomicBoolean unloaderCalled = new AtomicBoolean();
PerThreadCache.Key<String> key = PerThreadCache.Key.create(String.class);
try (PerThreadCache ignored = PerThreadCache.create(null)) {
PerThreadCache cache = PerThreadCache.get();
fillTheCache(cache);
cache.getWithLoader(key, () -> "value1", v -> unloaderCalled.set(true));
}
assertThat(unloaderCalled.get()).isFalse();
}
private void fillTheCache(PerThreadCache cache) {
for (int i = 0; i < 50; i++) {
PerThreadCache.Key<String> key = PerThreadCache.Key.create(String.class, i);
cache.getWithLoader(key, () -> "cached value", null);
}
}
}