Skip to content

Commit 944a889

Browse files
authored
perf(ehcache): avoid coarse locking (#187)
* perf(ehcache): avoid synchronization * perf: specify ExpiryPolicy.NO_EXPIRY when expiryTime is null * chore: remove todo comment * refactor: simplify long addition * perf: avoid some gets by using putIfAbsent return value * chore: deprecate LockedAbstractCache
1 parent 9b58d0b commit 944a889

File tree

3 files changed

+146
-32
lines changed

3 files changed

+146
-32
lines changed

core/src/main/java/io/github/xanthic/cache/core/LockedAbstractCache.java

+2
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@
2727
*
2828
* @param <K> The type of keys that form the cache
2929
* @param <V> The type of values contained in the cache
30+
* @deprecated no longer used by Xanthic to implement any canonical cache provider
3031
*/
32+
@Deprecated
3133
public abstract class LockedAbstractCache<K, V> implements Cache<K, V> {
3234

3335
protected final ReadWriteLock lock = new ReentrantReadWriteLock();
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,67 @@
11
package io.github.xanthic.cache.provider.ehcache;
22

3-
import io.github.xanthic.cache.core.LockedAbstractCache;
4-
import lombok.EqualsAndHashCode;
3+
import io.github.xanthic.cache.api.Cache;
54
import lombok.Value;
5+
import org.jetbrains.annotations.ApiStatus;
66
import org.jetbrains.annotations.NotNull;
7+
import org.jetbrains.annotations.Nullable;
78

89
import java.util.Map;
910
import java.util.function.BiConsumer;
11+
import java.util.function.BiFunction;
12+
import java.util.function.Function;
1013

1114
@Value
12-
@EqualsAndHashCode(callSuper = false)
15+
@ApiStatus.Internal
1316
@SuppressWarnings("unchecked")
14-
public class EhcacheDelegate<K, V> extends LockedAbstractCache<K, V> {
17+
public class EhcacheDelegate<K, V> implements Cache<K, V> {
1518
org.ehcache.Cache<Object, Object> cache;
1619

1720
@Override
18-
protected V getUnlocked(@NotNull K key) {
21+
public @Nullable V get(@NotNull K key) {
1922
return (V) cache.get(key);
2023
}
2124

2225
@Override
23-
protected void putUnlocked(@NotNull K key, @NotNull V value) {
24-
cache.put(key, value);
26+
public @Nullable V put(@NotNull K key, @NotNull V value) {
27+
Object prev = cache.get(key);
28+
while (true) {
29+
if (prev == null) {
30+
Object latest = cache.putIfAbsent(key, value);
31+
if (latest == null) {
32+
return null;
33+
}
34+
prev = latest;
35+
} else {
36+
if (cache.replace(key, prev, value)) {
37+
return (V) prev;
38+
}
39+
prev = cache.get(key);
40+
}
41+
}
2542
}
2643

2744
@Override
28-
protected void removeUnlocked(@NotNull K key) {
29-
cache.remove(key);
45+
public @Nullable V remove(@NotNull K key) {
46+
while (true) {
47+
Object prev = cache.get(key);
48+
if (prev == null) {
49+
return null;
50+
}
51+
52+
if (cache.remove(key, prev)) {
53+
return (V) prev;
54+
}
55+
}
3056
}
3157

3258
@Override
33-
protected void clearUnlocked() {
59+
public void clear() {
3460
cache.clear();
3561
}
3662

3763
@Override
38-
protected long sizeUnlocked() {
64+
public long size() {
3965
long n = 0;
4066
for (org.ehcache.Cache.Entry<Object, Object> ignored : cache) {
4167
n++;
@@ -44,35 +70,116 @@ protected long sizeUnlocked() {
4470
}
4571

4672
@Override
47-
public V putIfAbsent(@NotNull K key, @NotNull V value) {
48-
return read(() -> (V) cache.putIfAbsent(key, value));
73+
public @Nullable V compute(@NotNull K key, @NotNull BiFunction<? super K, ? super V, ? extends V> computeFunc) {
74+
Object old = cache.get(key);
75+
while (true) {
76+
V computed = computeFunc.apply(key, (V) old);
77+
if (computed == null) {
78+
if (old == null || cache.remove(key, old)) {
79+
return null;
80+
}
81+
} else {
82+
if (old == null) {
83+
Object latest = cache.putIfAbsent(key, computed);
84+
if (latest == null) {
85+
return computed;
86+
} else {
87+
old = latest;
88+
continue;
89+
}
90+
} else {
91+
if (cache.replace(key, old, computed)) {
92+
return computed;
93+
}
94+
}
95+
}
96+
old = cache.get(key);
97+
}
4998
}
5099

51100
@Override
52-
public void putAll(@NotNull Map<? extends K, ? extends V> map) {
53-
read(() -> {
54-
cache.putAll(map);
55-
return Void.TYPE;
56-
});
101+
public V computeIfAbsent(@NotNull K key, @NotNull Function<K, V> computeFunc) {
102+
Object initial = cache.get(key);
103+
if (initial != null) {
104+
return (V) initial;
105+
}
106+
107+
V computed = computeFunc.apply(key);
108+
if (computed == null) {
109+
return null;
110+
}
111+
112+
Object previous = cache.putIfAbsent(key, computed);
113+
if (previous == null) {
114+
return computed;
115+
} else {
116+
return (V) previous;
117+
}
118+
}
119+
120+
@Override
121+
public @Nullable V computeIfPresent(@NotNull K key, @NotNull BiFunction<? super K, ? super V, ? extends V> computeFunc) {
122+
while (true) {
123+
Object oldValue = cache.get(key);
124+
if (oldValue == null) {
125+
return null;
126+
}
127+
V computed = computeFunc.apply(key, (V) oldValue);
128+
if (computed == null) {
129+
if (cache.remove(key, oldValue))
130+
return null;
131+
} else {
132+
if (cache.replace(key, oldValue, computed)) {
133+
return computed;
134+
}
135+
}
136+
}
137+
}
138+
139+
@Override
140+
public @Nullable V putIfAbsent(@NotNull K key, @NotNull V value) {
141+
return (V) cache.putIfAbsent(key, value);
142+
}
143+
144+
@Override
145+
public V merge(@NotNull K key, @NotNull V value, @NotNull BiFunction<V, V, V> mergeFunc) {
146+
Object oldValue = cache.get(key);
147+
while (true) {
148+
if (oldValue == null) {
149+
Object latest = cache.putIfAbsent(key, value);
150+
if (latest == null) {
151+
return value;
152+
} else {
153+
oldValue = latest;
154+
}
155+
} else {
156+
V merged = mergeFunc.apply((V) oldValue, value);
157+
if (cache.replace(key, oldValue, merged)) {
158+
return merged;
159+
} else {
160+
oldValue = cache.get(key);
161+
}
162+
}
163+
}
57164
}
58165

59166
@Override
60167
public boolean replace(@NotNull K key, @NotNull V value) {
61-
return read(() -> cache.replace(key, value) != null);
168+
return cache.replace(key, value) != null;
62169
}
63170

64171
@Override
65172
public boolean replace(@NotNull K key, @NotNull V oldValue, @NotNull V newValue) {
66-
return read(() -> cache.replace(key, oldValue, newValue));
173+
return cache.replace(key, oldValue, newValue);
174+
}
175+
176+
@Override
177+
public void putAll(@NotNull Map<? extends K, ? extends V> map) {
178+
cache.putAll(map);
67179
}
68180

69181
@Override
70182
public void forEach(@NotNull BiConsumer<? super K, ? super V> action) {
71-
// We can't guarantee that users won't attempt to mutate the cache from within the action
72-
// So, we incur a performance penalty to acquire a write (rather than read) lock in order to avoid deadlocking
73-
write(() -> {
74-
cache.forEach(e -> action.accept((K) e.getKey(), (V) e.getValue()));
75-
return Void.TYPE;
76-
});
183+
cache.forEach(e -> action.accept((K) e.getKey(), (V) e.getValue()));
77184
}
78185
}

provider-ehcache/src/main/java/io/github/xanthic/cache/provider/ehcache/EhcacheProvider.java

+11-6
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import org.ehcache.config.builders.ResourcePoolsBuilder;
1414
import org.ehcache.core.spi.time.TickingTimeSource;
1515
import org.ehcache.event.EventType;
16+
import org.ehcache.expiry.ExpiryPolicy;
1617
import org.ehcache.impl.internal.TimeSourceConfiguration;
1718

1819
import java.time.Duration;
@@ -46,12 +47,16 @@ public <K, V> Cache<K, V> build(ICacheSpec<K, V> spec) {
4647
)
4748
};
4849

49-
handleExpiration(spec.expiryTime(), spec.expiryType(), (time, type) -> {
50-
if (type == ExpiryType.POST_WRITE)
51-
builder[0] = builder[0].withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(time));
52-
else
53-
builder[0] = builder[0].withExpiry(ExpiryPolicyBuilder.timeToIdleExpiration(time));
54-
});
50+
if (spec.expiryTime() == null) {
51+
builder[0].withExpiry(ExpiryPolicy.NO_EXPIRY);
52+
} else {
53+
handleExpiration(spec.expiryTime(), spec.expiryType(), (time, type) -> {
54+
if (type == ExpiryType.POST_WRITE)
55+
builder[0] = builder[0].withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(time));
56+
else
57+
builder[0] = builder[0].withExpiry(ExpiryPolicyBuilder.timeToIdleExpiration(time));
58+
});
59+
}
5560

5661
if (spec.removalListener() != null) {
5762
builder[0] = builder[0].withService(

0 commit comments

Comments
 (0)