> innerReader) {
95 | this.innerReader = innerReader;
96 | }
97 |
98 | public V get(K key) {
99 | this.innerReader.incrementEpoch();
100 | try {
101 | return this.innerReader.currentDsState().get(key);
102 | }
103 | finally {
104 | this.innerReader.incrementEpoch();
105 | }
106 | }
107 |
108 | public V getOrDefault(K key, V defaultValue) {
109 | return this.innerReader.read(map -> map.getOrDefault(key, defaultValue));
110 | }
111 |
112 | public boolean containsKey(K key) {
113 | return this.innerReader.readBool(map -> map.containsKey(key));
114 | }
115 |
116 | public void forEach(BiConsumer super K, ? super V> action) {
117 | this.innerReader.readVoid(map -> map.forEach(action));
118 | }
119 |
120 | public int size() {
121 | return this.innerReader.readInt(HashMap::size);
122 | }
123 |
124 | public boolean isEmpty() {
125 | return this.innerReader.readBool(HashMap::isEmpty);
126 | }
127 |
128 | public boolean containsValue(V value) {
129 | return this.innerReader.readBool(map -> map.containsValue(value));
130 | }
131 | }
132 |
133 | /**
134 | * A Thread Safe reader into the Map.
135 | *
136 | * All the methods on this class should be safe to call from any Thread.
137 | * Uses {@link ThreadLocal} to give each thread its own Reader.
138 | */
139 | public static final class ThreadSafeReader {
140 | private final ThreadLocal> localReader;
141 |
142 | private ThreadSafeReader(ReaderFactory innerFactory) {
143 | this.localReader = ThreadLocal.withInitial(innerFactory::createReader);
144 | }
145 |
146 | public V get(K key) {
147 | return this.localReader.get().get(key);
148 | }
149 |
150 | public V getOrDefault(K key, V defaultValue) {
151 | return this.localReader.get().getOrDefault(key, defaultValue);
152 | }
153 |
154 | public boolean containsKey(K key) {
155 | return this.localReader.get().containsKey(key);
156 | }
157 |
158 | public void forEach(BiConsumer super K, ? super V> action) {
159 | this.localReader.get().forEach(action);
160 | }
161 |
162 | public int size() {
163 | return this.localReader.get().size();
164 | }
165 |
166 | public boolean isEmpty() {
167 | return this.localReader.get().isEmpty();
168 | }
169 |
170 | public boolean containsValue(V value) {
171 | return this.localReader.get().containsValue(value);
172 | }
173 | }
174 |
175 |
176 | /**
177 | * Insert a value into the map.
178 | */
179 | static final class Put implements LeftRight.Operation, V> {
180 | private final K key;
181 | private final V value;
182 |
183 | public Put(K key, V value) {
184 | this.key = key;
185 | this.value = value;
186 | }
187 |
188 | @Override
189 | public V perform(HashMap map) {
190 | return map.put(key, value);
191 | }
192 | }
193 |
194 | /**
195 | * Insert a value into the map if its not already there.
196 | */
197 | static final class PutIfAbsent implements LeftRight.Operation, V> {
198 | private final K key;
199 | private final V value;
200 |
201 | public PutIfAbsent(K key, V value) {
202 | this.key = key;
203 | this.value = value;
204 | }
205 |
206 | @Override
207 | public V perform(HashMap map) {
208 | return map.putIfAbsent(key, value);
209 | }
210 | }
211 |
212 | /**
213 | * Remove some key from the map.
214 | */
215 | static final class Remove implements LeftRight.Operation, V> {
216 | private final K key;
217 |
218 | public Remove(K key) {
219 | this.key = key;
220 | }
221 |
222 | @Override
223 | public V perform(HashMap map) {
224 | return map.remove(key);
225 | }
226 | }
227 |
228 | /**
229 | * Remove some key from the map if it has the matching value.
230 | */
231 | static final class RemoveWithValue implements LeftRight.Operation, Boolean> {
232 | private final K key;
233 | private final V value;
234 |
235 | public RemoveWithValue(K key, V value) {
236 | this.key = key;
237 | this.value = value;
238 | }
239 |
240 | @Override
241 | public Boolean perform(HashMap map) {
242 | return map.remove(key, value);
243 | }
244 | }
245 |
246 | /**
247 | * Clears all entries from the map.
248 | */
249 | static final class Clear implements LeftRight.Operation, Void> {
250 | private static final Clear, ?> INSTANCE = new Clear<>();
251 |
252 | private Clear() {}
253 |
254 | @SuppressWarnings("unchecked")
255 | public static Clear getInstance() {
256 | return (Clear) INSTANCE;
257 | }
258 |
259 | @Override
260 | public Void perform(HashMap map) {
261 | map.clear();
262 | return null;
263 | }
264 | }
265 |
266 |
267 | /**
268 | * A Writer into the Map.
269 | *
270 | * This is not thread safe, so either a single thread needs to have ownership of the writer
271 | * or access to the writer needs to be coordinated via some other mechanism.
272 | *
273 | *
All writes done are only propagated to readers when {@link Writer#refresh()}
274 | * or {@link Writer#close()} are called.
275 | *
276 | *
Any reads done via the writer will by definition always get the most up to date state of the map.
277 | */
278 | public static final class Writer implements Closeable {
279 | private final LeftRight.Writer> innerWriter;
280 |
281 | private Writer(LeftRight.Writer> innerWriter) {
282 | this.innerWriter = innerWriter;
283 | }
284 |
285 | public V put(K key, V value) {
286 | return this.innerWriter.write(new Put<>(key, value));
287 | }
288 |
289 | public V putIfAbsent(K key, V value) {
290 | return this.innerWriter.write(new PutIfAbsent<>(key, value));
291 | }
292 |
293 | public V remove(K key) {
294 | return this.innerWriter.write(new Remove<>(key));
295 | }
296 |
297 | public boolean remove(K key, V value) {
298 | return this.innerWriter.write(new RemoveWithValue<>(key, value));
299 | }
300 |
301 | public void clear() {
302 | this.innerWriter.write(Clear.getInstance());
303 | }
304 |
305 | public int size() {
306 | return this.innerWriter.readInt(HashMap::size);
307 | }
308 |
309 | public boolean isEmpty() {
310 | return this.innerWriter.readBool(HashMap::isEmpty);
311 | }
312 |
313 | public boolean containsValue(V value) {
314 | return this.innerWriter.readBool(map -> map.containsValue(value));
315 | }
316 |
317 | public V get(K key) {
318 | return this.innerWriter.read(map -> map.get(key));
319 | }
320 |
321 | public V getOrDefault(K key, V defaultValue) {
322 | return this.innerWriter.read(map -> map.getOrDefault(key, defaultValue));
323 | }
324 |
325 | public boolean containsKey(K key) {
326 | return this.innerWriter.readBool(map -> map.containsKey(key));
327 | }
328 |
329 | public void forEach(BiConsumer super K, ? super V> action) {
330 | this.innerWriter.readVoid(map -> map.forEach(action));
331 | }
332 |
333 | /**
334 | * Propagates writes to readers.
335 | */
336 | public void refresh() {
337 | this.innerWriter.refresh();
338 | }
339 |
340 | /**
341 | * A close() implementation that calls refresh() for convenient use with try-with-resources.
342 | *
343 | *
344 | * @code
345 | * {
346 | * final var map = LeftRightMap.create();
347 | * try (final var writer = map.writer()) { // Writes will be propagated at the end of scope.
348 | * int key = 0;
349 | * if (writer.containsKey(1)) {
350 | * writer.put(writer.get(1) + 1);
351 | * }
352 | * else {
353 | * writer.put(1, 0);
354 | * }
355 | * }
356 | *
357 | */
358 | @Override
359 | public void close() {
360 | this.refresh();
361 | }
362 | }
363 | }
364 |
--------------------------------------------------------------------------------
/src/main/java/module-info.java:
--------------------------------------------------------------------------------
1 | module dev.mccue.left_right {
2 | exports dev.mccue.left_right;
3 | }
--------------------------------------------------------------------------------
/src/test/java/LeftRightMapTests.java:
--------------------------------------------------------------------------------
1 | import dev.mccue.left_right.LeftRightMap;
2 | import java.util.ArrayList;
3 | import java.util.List;
4 | import java.util.Set;
5 | import java.util.concurrent.Executors;
6 | import java.util.concurrent.Future;
7 | import java.util.stream.Collectors;
8 | import org.junit.Test;
9 |
10 | import static org.junit.Assert.assertEquals;
11 | import static org.junit.Assert.assertNull;
12 |
13 |
14 | public class LeftRightMapTests {
15 | @Test
16 | public void writesOnlyPropagateOnRefresh() {
17 | final var map = LeftRightMap.create();
18 | final var reader = map.threadSafeReader();
19 | final var writer = map.writer();
20 |
21 | assertNull(reader.get("a"));
22 | writer.put("a", "b");
23 | assertNull(reader.get("a"));
24 | writer.refresh();
25 | assertEquals(reader.get("a"), "b");
26 | }
27 |
28 | @Test
29 | public void tryWithResourcesWillRefresh() {
30 | final var map = LeftRightMap.create();
31 | final var reader = map.threadSafeReader();
32 |
33 | try (final var writer = map.writer()) {
34 | writer.put("a", "b");
35 | assertNull(reader.get("a"));
36 | }
37 | assertEquals(reader.get("a"), "b");
38 | }
39 |
40 | @Test
41 | public void everyReaderHandleSeesChangesAfterRefresh() {
42 | final var map = LeftRightMap.create();
43 | final var readers = List.of(
44 | map.readerFactory().createReader(),
45 | map.readerFactory().createReader(),
46 | map.readerFactory().createReader(),
47 | map.readerFactory().createReader()
48 | );
49 |
50 | for (final var reader : readers) {
51 | assertNull(reader.get("a"));
52 | }
53 |
54 | try (final var writer = map.writer()) {
55 | writer.put("a", "b");
56 | }
57 |
58 | for (final var reader : readers) {
59 | assertEquals(reader.get("a"), "b");
60 | }
61 | }
62 |
63 | @Test
64 | public void readersOnDifferentThreadsSeeResults() {
65 | final var executor = Executors.newFixedThreadPool(8);
66 | final var map = LeftRightMap.create();
67 |
68 |
69 | try (final var writer = map.writer()) {
70 | writer.put("a", "b");
71 | }
72 |
73 | final List> readResults = new ArrayList<>();
74 | for (int i = 0; i < 8; i++) {
75 | readResults.add(executor.submit(() -> map.threadSafeReader().get("a")));
76 | }
77 |
78 | assertEquals(
79 | List.of("b", "b", "b", "b", "b", "b", "b", "b"),
80 | readResults.stream()
81 | .map(res -> {
82 | try {
83 | return res.get();
84 | } catch (Exception e) {
85 | throw new RuntimeException(e);
86 | }
87 | })
88 | .collect(Collectors.toList())
89 | );
90 |
91 | executor.shutdownNow();
92 | }
93 |
94 | @Test
95 | public void writerSeesChangesImmediately() {
96 | final var map = LeftRightMap.create();
97 |
98 | try (final var writer = map.writer()) {
99 | writer.put("a", "b");
100 | writer.put("b", "c");
101 | if (writer.get("a") != null) {
102 | writer.put("e", "f");
103 | }
104 |
105 | assertEquals(writer.get("a"), "b");
106 | assertEquals(writer.get("b"), "c");
107 | assertEquals(writer.get("e"), "f");
108 | }
109 | }
110 |
111 | @Test
112 | public void differentOperationsAreAppliedInOrder() {
113 | final var map = LeftRightMap.create();
114 | final var reader = map.threadSafeReader();
115 | final var writer = map.writer();
116 | writer.put("a", "b");
117 | writer.clear();
118 | writer.put("c", "d");
119 | writer.remove("c");
120 | writer.put("e", "f");
121 | writer.refresh();
122 |
123 | assertEquals(reader.size(), 1);
124 | assertEquals(reader.get("e"), "f");
125 | }
126 |
127 | @Test
128 | public void noIntermediateResultsAreSeenByReaders() {
129 | for (int time = 0; time < 5; time++) {
130 | final var map = LeftRightMap.create();
131 | final var writer = map.writer();
132 | writer.put("a", "b");
133 | writer.refresh();
134 | writer.put("a", "c");
135 |
136 | final var executor = Executors.newFixedThreadPool(8);
137 | final List> readResults = new ArrayList<>();
138 | for (int i = 0; i < 1000000; i++) {
139 | final var reader = map.threadSafeReader();
140 | readResults.add(executor.submit(() -> reader.get("a")));
141 | }
142 |
143 | try { // Pause to give the tasks enough time to see the bad value.
144 | Thread.sleep(10);
145 | }
146 | catch (InterruptedException e) {
147 | throw new RuntimeException(e);
148 | }
149 |
150 | writer.put("a", "d");
151 | writer.refresh();
152 |
153 | assertEquals(
154 | Set.of("b", "d"), // spawning the futures should always take long enough to see the final state.
155 | readResults.stream()
156 | .map(res -> {
157 | try {
158 | return res.get();
159 | } catch (Exception e) {
160 | throw new RuntimeException(e);
161 | }
162 | })
163 | .collect(Collectors.toSet())
164 | );
165 |
166 | executor.shutdownNow();
167 | }
168 | }
169 |
170 |
171 | }
172 |
--------------------------------------------------------------------------------