Skip to content

Commit 4c43e6d

Browse files
jsenkoEricWittmann
andauthored
fix: redirect configuration (#2955)
* fix: redirect filter is applied only to '/' * fix: mark filter as supporting async, to work with ibmcompat API * fix: attempt to avoid CheckPeriodCache error [1], add eviction [1]: java.lang.IllegalStateException: Recursive update at java.base/java.util.concurrent.ConcurrentHashMap.compute(ConcurrentHashMap.java:1991) at io.apicurio.registry.utils.CheckPeriodCache.compute(CheckPeriodCache.java:36) at io.apicurio.registry.mt.TenantContextLoader.loadRequestContext(TenantContextLoader.java:81) * fix: add eviction check to all methods that potentially increase the size * Update pom.xml Co-authored-by: Eric Wittmann <eric.wittmann@gmail.com>
1 parent d59314b commit 4c43e6d

File tree

7 files changed

+171
-39
lines changed

7 files changed

+171
-39
lines changed

app/src/main/java/io/apicurio/registry/mt/TenantContextLoader.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import io.quarkus.runtime.StartupEvent;
2626
import org.eclipse.microprofile.config.inject.ConfigProperty;
2727

28+
import java.time.Duration;
2829
import javax.enterprise.context.ApplicationScoped;
2930
import javax.enterprise.event.Observes;
3031
import javax.inject.Inject;
@@ -54,7 +55,7 @@ public class TenantContextLoader {
5455
Long cacheCheckPeriod;
5556

5657
public void onStart(@Observes StartupEvent ev) {
57-
contextsCache = new CheckPeriodCache<>(cacheCheckPeriod);
58+
contextsCache = new CheckPeriodCache<>(Duration.ofMillis(cacheCheckPeriod));
5859
}
5960

6061
/**

app/src/main/java/io/apicurio/registry/storage/metrics/StorageMetricsStore.java

+11-11
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,21 @@
1616

1717
package io.apicurio.registry.storage.metrics;
1818

19-
import java.util.concurrent.atomic.AtomicLong;
20-
21-
import javax.enterprise.context.ApplicationScoped;
22-
import javax.enterprise.event.Observes;
23-
import javax.inject.Inject;
24-
25-
import org.eclipse.microprofile.config.inject.ConfigProperty;
26-
import org.slf4j.Logger;
2719
import io.apicurio.common.apps.config.Info;
2820
import io.apicurio.registry.mt.TenantContext;
2921
import io.apicurio.registry.storage.RegistryStorage;
3022
import io.apicurio.registry.types.Current;
3123
import io.apicurio.registry.utils.CheckPeriodCache;
3224
import io.quarkus.runtime.StartupEvent;
3325
import lombok.EqualsAndHashCode;
26+
import org.eclipse.microprofile.config.inject.ConfigProperty;
27+
import org.slf4j.Logger;
28+
29+
import java.time.Duration;
30+
import java.util.concurrent.atomic.AtomicLong;
31+
import javax.enterprise.context.ApplicationScoped;
32+
import javax.enterprise.event.Observes;
33+
import javax.inject.Inject;
3434

3535
/**
3636
* This class provides a set of per-tenant counters. Counters such as "number of artifacts"
@@ -75,9 +75,9 @@ private static class ArtifactVersionKey {
7575
}
7676

7777
public void onStart(@Observes StartupEvent ev) {
78-
totalSchemasCounters = new CheckPeriodCache<>(limitsCheckPeriod);
79-
artifactsCounters = new CheckPeriodCache<>(limitsCheckPeriod);
80-
artifactVersionsCounters = new CheckPeriodCache<>(limitsCheckPeriod);
78+
totalSchemasCounters = new CheckPeriodCache<>(Duration.ofMillis(limitsCheckPeriod));
79+
artifactsCounters = new CheckPeriodCache<>(Duration.ofMillis(limitsCheckPeriod));
80+
artifactVersionsCounters = new CheckPeriodCache<>(Duration.ofMillis(limitsCheckPeriod));
8181
}
8282

8383
public long getOrInitializeTotalSchemasCounter() {

app/src/main/resources-unfiltered/META-INF/web.xml

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@
77
<filter>
88
<filter-name>RedirectFilter</filter-name>
99
<filter-class>io.apicurio.registry.ui.servlets.RedirectFilter</filter-class>
10+
<async-supported>true</async-supported>
1011
</filter>
1112
<filter-mapping>
1213
<filter-name>RedirectFilter</filter-name>
13-
<url-pattern>/</url-pattern>
14+
<url-pattern>/*</url-pattern>
1415
</filter-mapping>
1516

1617
<filter>

common/pom.xml

+6
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@
5050
<scope>compile</scope>
5151
</dependency>
5252

53+
<dependency>
54+
<groupId>org.junit.jupiter</groupId>
55+
<artifactId>junit-jupiter</artifactId>
56+
<scope>test</scope>
57+
</dependency>
58+
5359
</dependencies>
5460

5561

common/src/main/java/io/apicurio/registry/utils/CheckPeriodCache.java

+86-26
Original file line numberDiff line numberDiff line change
@@ -16,56 +16,106 @@
1616

1717
package io.apicurio.registry.utils;
1818

19+
import org.slf4j.Logger;
20+
import org.slf4j.LoggerFactory;
21+
22+
import java.time.Duration;
23+
import java.time.Instant;
1924
import java.util.Map;
25+
import java.util.Map.Entry;
2026
import java.util.concurrent.ConcurrentHashMap;
27+
import java.util.concurrent.atomic.AtomicBoolean;
2128
import java.util.function.Function;
29+
import java.util.stream.Collectors;
2230

2331
/**
2432
* @author Fabian Martinez
33+
* @author Jakub Senko <m@jsenko.net>
2534
*/
2635
public class CheckPeriodCache<K, V> {
2736

28-
private Map<K, CheckValue<V>> cache = new ConcurrentHashMap<>();
29-
private long checkPeriodMillis = 0;
37+
private static final Logger log = LoggerFactory.getLogger(CheckPeriodCache.class);
38+
39+
private final ConcurrentHashMap<K, CheckValue<V>> cache = new ConcurrentHashMap<>();
40+
41+
private final Duration checkPeriod;
42+
43+
private final AtomicBoolean evicting = new AtomicBoolean(false);
44+
private final int evictionThreshold;
45+
46+
public CheckPeriodCache(Duration checkPeriod, int evictionThreshold) {
47+
this.checkPeriod = checkPeriod;
48+
this.evictionThreshold = evictionThreshold;
49+
}
50+
51+
public CheckPeriodCache(Duration checkPeriod) {
52+
this.checkPeriod = checkPeriod;
53+
this.evictionThreshold = 1000;
54+
}
3055

31-
public CheckPeriodCache(long checkPeriodMillis) {
32-
this.checkPeriodMillis = checkPeriodMillis;
56+
private boolean isExpired(CheckValue<?> checkedValue) {
57+
return Instant.now().isAfter(checkedValue.lastUpdate.plus(checkPeriod));
58+
}
59+
60+
private void checkEviction() {
61+
final int currentSize = cache.size();
62+
if (currentSize > evictionThreshold) {
63+
if (evicting.compareAndSet(false, true)) {
64+
try {
65+
// This thread gets to evict
66+
log.debug("Thread {} is evicting the cache. Current size is {}, threshold is {}.",
67+
Thread.currentThread().getName(), currentSize, evictionThreshold);
68+
var toEvict = cache.entrySet().stream()
69+
.filter(entry -> isExpired(entry.getValue()))
70+
.map(Entry::getKey)
71+
.collect(Collectors.toList());
72+
73+
log.debug("Thread {} is evicting the cache. Found {} candidates for eviction.",
74+
Thread.currentThread().getName(), toEvict.size());
75+
toEvict.forEach(k -> {
76+
cache.compute(k, (key, value) -> {
77+
if (value == null || isExpired(value)) {
78+
return null;
79+
} else {
80+
return value;
81+
}
82+
});
83+
});
84+
85+
log.debug("Thread {} has finished evicting the cache. The new size is {}.",
86+
Thread.currentThread().getName(), cache.size());
87+
} finally {
88+
evicting.set(false);
89+
}
90+
}
91+
}
3392
}
3493

3594
public V compute(K k, Function<K, V> remappingFunction) {
95+
checkEviction();
3696
CheckValue<V> returnValue = cache.compute(k, (key, checkedValue) -> {
37-
long now = System.currentTimeMillis();
38-
if (checkedValue == null) {
97+
if (checkedValue == null || isExpired(checkedValue)) {
98+
// Only execute if expired, but do it first in case an exception is thrown
3999
V value = remappingFunction.apply(key);
40-
return new CheckValue<>(now, value);
100+
return new CheckValue<>(Instant.now(), value);
41101
} else {
42-
if (checkedValue.lastUpdate + checkPeriodMillis < now) {
43-
V value = remappingFunction.apply(key);
44-
checkedValue.lastUpdate = now;
45-
checkedValue.value = value;
46-
}
47102
return checkedValue;
48103
}
49104
});
50105
return returnValue.value;
51106
}
52107

53108
public void put(K k, V v) {
54-
cache.put(k, new CheckValue<>(System.currentTimeMillis(), v));
109+
checkEviction();
110+
cache.put(k, new CheckValue<>(Instant.now(), v));
55111
}
56112

57113
public V get(K k) {
58114
CheckValue<V> value = cache.compute(k, (key, checkedValue) -> {
59-
if (checkedValue == null) {
115+
if (checkedValue == null || isExpired(checkedValue)) {
60116
return null;
61117
} else {
62-
long now = System.currentTimeMillis();
63-
if (checkedValue.lastUpdate + checkPeriodMillis < now) {
64-
//value expired
65-
return null;
66-
} else {
67-
return checkedValue;
68-
}
118+
return checkedValue;
69119
}
70120
});
71121
return value == null ? null : value.value;
@@ -79,15 +129,25 @@ public void clear() {
79129
cache.clear();
80130
}
81131

82-
private static class CheckValue<V> {
132+
/**
133+
* USE FOR TEST ONLY
134+
*/
135+
Map<K, CheckValue<V>> getInternal() {
136+
return cache;
137+
}
138+
139+
int size() {
140+
return cache.size();
141+
}
83142

84-
CheckValue(long ts, V value) {
85-
this.lastUpdate = ts;
143+
static class CheckValue<V> {
144+
145+
CheckValue(Instant lastUpdate, V value) {
146+
this.lastUpdate = lastUpdate;
86147
this.value = value;
87148
}
88149

89-
long lastUpdate;
150+
Instant lastUpdate;
90151
V value;
91152
}
92-
93153
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package io.apicurio.registry.utils;
2+
3+
import org.junit.jupiter.api.Assertions;
4+
import org.junit.jupiter.api.Test;
5+
6+
import java.time.Duration;
7+
import java.util.stream.Collectors;
8+
import java.util.stream.IntStream;
9+
10+
class CheckPeriodCacheTest {
11+
12+
@Test
13+
void testEviction() throws InterruptedException {
14+
for (int attempt = 0; attempt < 5; attempt++) {
15+
16+
var cache = new CheckPeriodCache<Integer, Integer>(Duration.ofSeconds(1), 10);
17+
var t1 = new Thread(() -> {
18+
for (int i = 1; i <= 1000; i++) {
19+
cache.compute(i, k -> k);
20+
}
21+
Assertions.assertEquals(1000, cache.size());
22+
Assertions.assertEquals(cache.getInternal().size(), cache.size());
23+
try {
24+
Thread.sleep(1000);
25+
} catch (InterruptedException e) {
26+
throw new RuntimeException(e);
27+
}
28+
for (int i = 1001; i <= 1010; i++) {
29+
cache.compute(i, k -> k);
30+
}
31+
});
32+
var t2 = new Thread(() -> {
33+
for (int i = 1; i <= 1000; i++) {
34+
cache.compute(i, k -> k);
35+
}
36+
Assertions.assertEquals(1000, cache.size());
37+
Assertions.assertEquals(cache.getInternal().size(), cache.size());
38+
try {
39+
Thread.sleep(1000);
40+
} catch (InterruptedException e) {
41+
throw new RuntimeException(e);
42+
}
43+
for (int i = 1001; i <= 1010; i++) {
44+
cache.compute(i, k -> k);
45+
}
46+
});
47+
t1.start();
48+
t2.start();
49+
t1.join();
50+
t2.join();
51+
52+
Assertions.assertEquals(10, cache.size());
53+
var actual = cache.getInternal().values().stream().map(c -> c.value).collect(Collectors.toSet());
54+
var expected = IntStream.range(1001, 1011).boxed().collect(Collectors.toSet());
55+
Assertions.assertEquals(expected, actual);
56+
}
57+
}
58+
}

integration-tests/testsuite/pom.xml

+6
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,12 @@
7777
<scope>provided</scope>
7878
</dependency>
7979

80+
<dependency>
81+
<groupId>org.jboss.slf4j</groupId>
82+
<artifactId>slf4j-jboss-logging</artifactId>
83+
<version>${jboss-slf4j.version}</version>
84+
</dependency>
85+
8086
<!-- Test Only -->
8187
<dependency>
8288
<groupId>io.confluent</groupId>

0 commit comments

Comments
 (0)