1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 package org.apache.commons.lang3.concurrent;
19
20 import static org.junit.jupiter.api.Assertions.assertEquals;
21 import static org.junit.jupiter.api.Assertions.assertSame;
22 import static org.junit.jupiter.api.Assertions.assertThrows;
23 import static org.junit.jupiter.api.Assertions.assertTrue;
24
25 import java.util.ArrayList;
26 import java.util.List;
27 import java.util.concurrent.CountDownLatch;
28 import java.util.concurrent.ExecutorService;
29 import java.util.concurrent.Executors;
30 import java.util.concurrent.Future;
31 import java.util.concurrent.TimeUnit;
32 import java.util.concurrent.atomic.AtomicInteger;
33 import java.util.concurrent.atomic.AtomicReference;
34 import java.util.function.Function;
35
36 import org.apache.commons.lang3.AbstractLangTest;
37 import org.junit.jupiter.params.ParameterizedTest;
38 import org.junit.jupiter.params.provider.ValueSource;
39
40
41
42
43 class MemoizerCacheTest extends AbstractLangTest {
44
45 private static final int TIMEOUT_SECONDS = 10;
46
47 private static void await(final CountDownLatch latch) {
48 try {
49 assertTrue(latch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS), "Timed out waiting for a test worker");
50 } catch (final InterruptedException e) {
51 Thread.currentThread().interrupt();
52 throw new AssertionError(e);
53 }
54 }
55
56 private static <I, O> Memoizer<I, O> newMemoizer(final boolean useFunction, final Function<I, O> function) {
57 return useFunction ? new Memoizer<>(function) : new Memoizer<>((Computable<I, O>) function::apply);
58 }
59
60 private static <I, O> Memoizer<I, O> newMemoizer(final boolean useFunction, final Function<I, O> function, final boolean recalculate) {
61 return useFunction ? new Memoizer<>(function, recalculate) : new Memoizer<>((Computable<I, O>) function::apply, recalculate);
62 }
63
64 private static void shutdown(final ExecutorService executor) throws InterruptedException {
65 executor.shutdownNow();
66 assertTrue(executor.awaitTermination(TIMEOUT_SECONDS, TimeUnit.SECONDS), "Test workers did not terminate");
67 }
68
69 @ParameterizedTest
70 @ValueSource(booleans = { false, true })
71 void testConcurrentCallsForSameKeyComputeOnce(final boolean useFunction) throws Exception {
72 final int callerCount = 8;
73 final AtomicInteger calls = new AtomicInteger();
74 final CountDownLatch ready = new CountDownLatch(callerCount);
75 final CountDownLatch start = new CountDownLatch(1);
76 final CountDownLatch entered = new CountDownLatch(1);
77 final CountDownLatch release = new CountDownLatch(1);
78 final Object result = new Object();
79 final Memoizer<String, Object> memoizer = newMemoizer(useFunction, key -> {
80 calls.incrementAndGet();
81 entered.countDown();
82 await(release);
83 return result;
84 });
85 final ExecutorService executor = Executors.newFixedThreadPool(callerCount);
86 try {
87 final List<Future<Object>> futures = new ArrayList<>();
88 for (int i = 0; i < callerCount; i++) {
89 futures.add(executor.submit(() -> {
90 ready.countDown();
91 await(start);
92 return memoizer.compute("key");
93 }));
94 }
95 await(ready);
96 start.countDown();
97 await(entered);
98 release.countDown();
99 for (final Future<Object> future : futures) {
100 assertSame(result, future.get(TIMEOUT_SECONDS, TimeUnit.SECONDS));
101 }
102 assertSame(result, memoizer.compute("key"));
103 assertEquals(1, calls.get());
104 } finally {
105 start.countDown();
106 release.countDown();
107 shutdown(executor);
108 }
109 }
110
111 @ParameterizedTest
112 @ValueSource(booleans = { false, true })
113 void testDefaultCachesFirstFailure(final boolean useFunction) throws Exception {
114 final AtomicInteger calls = new AtomicInteger();
115 final IllegalStateException failure = new IllegalStateException("Transient failure");
116 final Memoizer<String, String> memoizer = newMemoizer(useFunction, key -> {
117 if (calls.incrementAndGet() == 1) {
118 throw failure;
119 }
120 return key;
121 });
122 for (int i = 0; i < 3; i++) {
123 assertSame(failure, assertThrows(IllegalStateException.class, () -> memoizer.compute("failed")));
124 }
125 assertEquals(1, calls.get(), "A transient failure remains cached by default");
126 assertEquals("other", memoizer.compute("other"));
127 assertSame(failure, assertThrows(IllegalStateException.class, () -> memoizer.compute("failed")));
128 assertEquals(2, calls.get());
129 }
130
131 @ParameterizedTest
132 @ValueSource(booleans = { false, true })
133 void testDistinctKeysRetainCachedResults(final boolean useFunction) throws Exception {
134 final int keyCount = 1024;
135 final AtomicInteger calls = new AtomicInteger();
136 final Memoizer<Integer, Object> memoizer = newMemoizer(useFunction, key -> {
137 calls.incrementAndGet();
138 return new Object();
139 });
140 final List<Object> results = new ArrayList<>();
141 for (int i = 0; i < keyCount; i++) {
142 results.add(memoizer.compute(i));
143 }
144
145 for (int i = 0; i < keyCount; i++) {
146 assertSame(results.get(i), memoizer.compute(i));
147 }
148 assertEquals(keyCount, calls.get(), "Adding distinct keys must not evict earlier results");
149 }
150
151 @ParameterizedTest
152 @ValueSource(booleans = { false, true })
153 void testRecalculateRetriesFailureOnNextCall(final boolean useFunction) throws Exception {
154 final AtomicInteger calls = new AtomicInteger();
155 final IllegalStateException failure = new IllegalStateException("Transient failure");
156 final Object result = new Object();
157 final Memoizer<String, Object> memoizer = newMemoizer(useFunction, key -> {
158 if (calls.incrementAndGet() == 1) {
159 throw failure;
160 }
161 return result;
162 }, true);
163 assertSame(failure, assertThrows(IllegalStateException.class, () -> memoizer.compute("key")));
164 assertEquals(1, calls.get(), "The failing call must propagate its failure without retrying internally");
165 assertSame(result, memoizer.compute("key"));
166 assertSame(result, memoizer.compute("key"));
167 assertEquals(2, calls.get());
168 }
169
170 @ParameterizedTest
171 @ValueSource(booleans = { false, true })
172 void testReentrantComputationForDistinctCollidingKey(final boolean useFunction) throws Exception {
173 assertEquals("Aa".hashCode(), "BB".hashCode());
174 final AtomicInteger calls = new AtomicInteger();
175 final AtomicReference<Memoizer<String, String>> reference = new AtomicReference<>();
176 final Memoizer<String, String> memoizer = newMemoizer(useFunction, key -> {
177 calls.incrementAndGet();
178 if ("Aa".equals(key)) {
179 try {
180 return reference.get().compute("BB");
181 } catch (final InterruptedException e) {
182 Thread.currentThread().interrupt();
183 throw new AssertionError(e);
184 }
185 }
186 return key;
187 });
188 reference.set(memoizer);
189 final ExecutorService executor = Executors.newSingleThreadExecutor();
190 try {
191 assertEquals("BB", executor.submit(() -> memoizer.compute("Aa")).get(TIMEOUT_SECONDS, TimeUnit.SECONDS));
192 assertEquals("BB", memoizer.compute("Aa"));
193 assertEquals("BB", memoizer.compute("BB"));
194 assertEquals(2, calls.get());
195 } finally {
196 shutdown(executor);
197 }
198 }
199
200 @ParameterizedTest
201 @ValueSource(booleans = { false, true })
202 void testSlowComputationDoesNotBlockDistinctCollidingKey(final boolean useFunction) throws Exception {
203 assertEquals("Aa".hashCode(), "BB".hashCode());
204 final CountDownLatch entered = new CountDownLatch(1);
205 final CountDownLatch release = new CountDownLatch(1);
206 final Memoizer<String, String> memoizer = newMemoizer(useFunction, key -> {
207 if ("Aa".equals(key)) {
208 entered.countDown();
209 await(release);
210 }
211 return key;
212 });
213 final ExecutorService executor = Executors.newFixedThreadPool(2);
214 try {
215 final Future<String> slow = executor.submit(() -> memoizer.compute("Aa"));
216 await(entered);
217 final Future<String> other = executor.submit(() -> memoizer.compute("BB"));
218 assertEquals("BB", other.get(TIMEOUT_SECONDS, TimeUnit.SECONDS));
219 assertEquals(1L, release.getCount(), "The colliding key must complete while the first computation is blocked");
220 release.countDown();
221 assertEquals("Aa", slow.get(TIMEOUT_SECONDS, TimeUnit.SECONDS));
222 } finally {
223 release.countDown();
224 shutdown(executor);
225 }
226 }
227 }