Skip to content

Commit be722fd

Browse files
GH-2375 - Make LiteralReplacement-Cache threadsafe.
Fixes #2375.
1 parent 7cb4da9 commit be722fd

File tree

2 files changed

+110
-7
lines changed

2 files changed

+110
-7
lines changed

src/main/java/org/springframework/data/neo4j/repository/query/Neo4jSpelSupport.java

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import java.util.LinkedHashMap;
1919
import java.util.Map;
20+
import java.util.concurrent.locks.StampedLock;
2021

2122
import org.apiguardian.api.API;
2223
import org.springframework.data.domain.Pageable;
@@ -111,12 +112,39 @@ protected boolean removeEldestEntry(Map.Entry<String, LiteralReplacement> eldest
111112
}
112113
};
113114

115+
private static final StampedLock LOCK = new StampedLock();
116+
114117
static LiteralReplacement withTargetAndValue(LiteralReplacement.Target target, @Nullable String value) {
115118

116119
String valueUsed = value == null ? "" : value;
117-
StringBuilder key = new StringBuilder(target.name()).append("_").append(valueUsed);
118-
119-
return INSTANCES.computeIfAbsent(key.toString(), k -> new StringBasedLiteralReplacement(target, valueUsed));
120+
String key = new StringBuilder(target.name()).append("_").append(valueUsed).toString();
121+
122+
long stamp = LOCK.tryOptimisticRead();
123+
if (LOCK.validate(stamp) && INSTANCES.containsKey(key)) {
124+
return INSTANCES.get(key);
125+
}
126+
try {
127+
stamp = LOCK.readLock();
128+
LiteralReplacement replacement = null;
129+
while (replacement == null) {
130+
if (INSTANCES.containsKey(key)) {
131+
replacement = INSTANCES.get(key);
132+
} else {
133+
long writeStamp = LOCK.tryConvertToWriteLock(stamp);
134+
if (LOCK.validate(writeStamp)) {
135+
replacement = new StringBasedLiteralReplacement(target, valueUsed);
136+
stamp = writeStamp;
137+
INSTANCES.put(key, replacement);
138+
} else {
139+
LOCK.unlockRead(stamp);
140+
stamp = LOCK.writeLock();
141+
}
142+
}
143+
}
144+
return replacement;
145+
} finally {
146+
LOCK.unlock(stamp);
147+
}
120148
}
121149

122150
private final Target target;

src/test/java/org/springframework/data/neo4j/repository/query/Neo4jSpelSupportTest.java

Lines changed: 79 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,26 @@
1717

1818
import static org.assertj.core.api.Assertions.assertThat;
1919
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
20+
import static org.assertj.core.api.Assumptions.assumeThat;
21+
22+
import java.lang.reflect.Field;
23+
import java.util.ArrayList;
24+
import java.util.Collection;
25+
import java.util.IdentityHashMap;
26+
import java.util.Map;
27+
import java.util.concurrent.Callable;
28+
import java.util.concurrent.ExecutionException;
29+
import java.util.concurrent.ExecutorService;
30+
import java.util.concurrent.Executors;
31+
import java.util.concurrent.Future;
32+
import java.util.concurrent.atomic.AtomicBoolean;
33+
import java.util.concurrent.atomic.AtomicInteger;
2034

2135
import org.junit.jupiter.api.Test;
2236
import org.springframework.data.domain.PageRequest;
2337
import org.springframework.data.domain.Sort;
2438
import org.springframework.data.neo4j.repository.query.Neo4jSpelSupport.LiteralReplacement;
39+
import org.springframework.util.ReflectionUtils;
2540

2641
/**
2742
* @author Michael J. Simons
@@ -65,16 +80,76 @@ void orderByShouldWork() {
6580
.withMessageMatching(".+is not a valid order criteria.");
6681
}
6782

83+
private Map<?, ?> getCacheInstance() throws ClassNotFoundException, IllegalAccessException {
84+
Class<?> type = Class.forName(
85+
"org.springframework.data.neo4j.repository.query.Neo4jSpelSupport$StringBasedLiteralReplacement");
86+
Field cacheField = ReflectionUtils.findField(type, "INSTANCES");
87+
cacheField.setAccessible(true);
88+
return (Map<?, ?>) cacheField.get(null);
89+
}
90+
91+
private void flushLiteralCache() {
92+
try {
93+
Map<?, ?> cache = getCacheInstance();
94+
cache.clear();
95+
} catch (Exception e) {
96+
throw new RuntimeException(e);
97+
}
98+
}
99+
100+
private int getCacheSize() {
101+
try {
102+
Map<?, ?> cache = getCacheInstance();
103+
return cache.size();
104+
} catch (Exception e) {
105+
throw new RuntimeException(e);
106+
}
107+
}
108+
68109
@Test // DATAGRAPH-1454
69110
void cacheShouldWork() {
70111

71-
// Make sure we flush this before...
72-
for (int i = 0; i < 16; ++i) {
73-
LiteralReplacement literalReplacement = Neo4jSpelSupport.literal("y" + i);
74-
}
112+
flushLiteralCache();
75113

76114
LiteralReplacement literalReplacement1 = Neo4jSpelSupport.literal("x");
77115
LiteralReplacement literalReplacement2 = Neo4jSpelSupport.literal("x");
78116
assertThat(literalReplacement1).isSameAs(literalReplacement2);
79117
}
118+
119+
@Test // GH-2375
120+
void cacheShouldBeThreadSafe() throws ExecutionException, InterruptedException {
121+
122+
flushLiteralCache();
123+
124+
int numThreads = Runtime.getRuntime().availableProcessors();
125+
ExecutorService executor = Executors.newWorkStealingPool();
126+
127+
AtomicBoolean running = new AtomicBoolean();
128+
AtomicInteger overlaps = new AtomicInteger();
129+
130+
Collection<Callable<LiteralReplacement>> getReplacementCalls = new ArrayList<>();
131+
for (int t = 0; t < numThreads; ++t) {
132+
getReplacementCalls.add(() -> {
133+
if (!running.compareAndSet(false, true)) {
134+
overlaps.incrementAndGet();
135+
}
136+
Thread.sleep(100); // Make the chances of overlapping a bit bigger
137+
LiteralReplacement d = Neo4jSpelSupport.literal("x");
138+
running.compareAndSet(true, false);
139+
return d;
140+
});
141+
}
142+
143+
Map<LiteralReplacement, Integer> replacements = new IdentityHashMap<>();
144+
for (Future<LiteralReplacement> getDriverFuture : executor.invokeAll(getReplacementCalls)) {
145+
replacements.put(getDriverFuture.get(), 1);
146+
}
147+
executor.shutdown();
148+
149+
// Assume things actually had been concurrent
150+
assumeThat(overlaps.get()).isGreaterThan(0);
151+
152+
assertThat(getCacheSize()).isEqualTo(1);
153+
assertThat(replacements).hasSize(1);
154+
}
80155
}

0 commit comments

Comments
 (0)