View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
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   * Tests failure retention, cache retention, and computation lock scope.
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         // Characterize retention over a bounded sample without exhausting memory or inspecting the backing map.
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 }