-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathBoundedLocalCache.java
More file actions
363 lines (326 loc) · 12.7 KB
/
Copy pathBoundedLocalCache.java
File metadata and controls
363 lines (326 loc) · 12.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
package io.github.czhaodev.tinylfu.internal;
import io.github.czhaodev.tinylfu.Cache;
import io.github.czhaodev.tinylfu.Ticker;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;
/**
* The concurrent engine: keeps {@link #getIfPresent} effectively lock-free
* by never touching the eviction policy or timer wheel inline. Reads are
* recorded into a {@link StripedReadBuffer} (droppable, advisory-only);
* writes update the backing map immediately (visible right away) and enqueue
* a bookkeeping event onto a {@link WriteBuffer} (never dropped). A single
* {@link DrainStatus} state machine ensures at most one thread drains at a
* time. <strong>The map is always right, everything else is eventually
* right</strong> -- {@code get}/{@code put}/{@code invalidate} are visible
* the instant they return, regardless of whether any bookkeeping has run.
*
* <p>Unlike the Rust sibling's {@code DashMap}-backed design, this class
* never needs to release a map guard before triggering a drain:
* {@link ConcurrentHashMap#compute} holds its per-bin lock as an ordinary
* (reentrant) Java monitor for the callback's entire duration, so a nested,
* same-thread call back into the map -- even one that happens to hash to
* the same bin -- cannot deadlock the way a non-reentrant lock would. This
* also means an {@code UPDATE} event can never be drained before its own
* node's {@code ADD} event: a second writer's {@code compute()} call for the
* same key cannot even begin until the first writer's callback (which
* already enqueued the {@code ADD}) has returned. The staleness checks
* mirroring the Rust source's races are kept anyway, as defense in depth.
*/
public final class BoundedLocalCache<K, V> implements Cache<K, V> {
private final Ticker ticker;
private final long expireAfterWriteNanos; // -1 if not configured
private final long expireAfterAccessNanos; // -1 if not configured
private final ConcurrentHashMap<K, Node<K, V>> data = new ConcurrentHashMap<>();
private final StripedReadBuffer<K, V> readBuffer = new StripedReadBuffer<>();
private final WriteBuffer<K, V> writeBuffer = new WriteBuffer<>();
private final DrainStatus drainStatus = new DrainStatus();
/** Guards {@link #policy} and {@link #timerWheel}; only ever acquired via tryLock, except in {@link #cleanUp}. */
private final ReentrantLock evictionLock = new ReentrantLock();
private final WindowTinyLfuPolicy<K, V> policy;
private final TimerWheel<K, V> timerWheel;
public BoundedLocalCache(long maximumSize, Ticker ticker, long expireAfterWriteNanos, long expireAfterAccessNanos) {
this.ticker = ticker;
this.expireAfterWriteNanos = expireAfterWriteNanos;
this.expireAfterAccessNanos = expireAfterAccessNanos;
this.policy = new WindowTinyLfuPolicy<>(maximumSize);
this.timerWheel = new TimerWheel<>(ticker.read());
}
@Override
public Optional<V> getIfPresent(K key) {
Node<K, V> node = data.get(key);
if (node == null || isExpired(node)) {
return Optional.empty();
}
afterRead(node);
return Optional.of(node.value);
}
@Override
public Optional<V> getWith(K key, Function<? super K, ? extends V> mappingFunction) {
Node<K, V> existing = data.get(key);
if (existing != null && !isExpired(existing)) {
afterRead(existing);
return Optional.of(existing.value);
}
long now = ticker.read();
AtomicReference<V> result = new AtomicReference<>();
AtomicReference<Node<K, V>> removedNode = new AtomicReference<>();
AtomicReference<Node<K, V>> addedNode = new AtomicReference<>();
data.compute(key, (k, current) -> {
if (current != null && !isExpired(current)) {
result.set(current.value);
return current;
}
if (current != null) {
// An expired entry occupied this key; it's discarded either way. Never
// reused, since it may still be referenced by an in-flight buffered
// event or scheduled in the timer wheel with a stale deadline.
removedNode.set(current);
}
V computed = mappingFunction.apply(key);
if (computed == null) {
return null;
}
Node<K, V> fresh = new Node<>(key, computed);
applyWriteExpiration(fresh, now);
addedNode.set(fresh);
result.set(computed);
return fresh;
});
// afterRemove/afterAdd (and the synchronous drain they may trigger) must run
// only once compute() has returned and released this key's bin lock: compute()
// reinstalls its return value unconditionally when the callback exits, so a
// nested drain that touches this same key while still inside the callback
// would race against that reinstallation.
if (removedNode.get() != null) {
afterRemove(removedNode.get());
}
if (addedNode.get() != null) {
afterAdd(addedNode.get());
}
return Optional.ofNullable(result.get());
}
@Override
public void put(K key, V value) {
rawPut(key, value);
}
@Override
public void invalidate(K key) {
rawInvalidate(key);
}
@Override
public void invalidateAll() {
data.clear();
writeBuffer.add(WriteEvent.clear());
scheduleDrain();
}
@Override
public void invalidateEntries(Iterable<? extends K> keys) {
for (K key : keys) {
rawInvalidate(key);
}
}
@Override
public long estimatedSize() {
return data.mappingCount();
}
@Override
public Map<K, V> asMapSnapshot() {
Map<K, V> snapshot = new HashMap<>();
for (Map.Entry<K, Node<K, V>> entry : data.entrySet()) {
Node<K, V> node = entry.getValue();
if (!isExpired(node)) {
snapshot.put(entry.getKey(), node.value);
}
}
return snapshot;
}
@Override
public void cleanUp() {
evictionLock.lock();
try {
drainStatus.set(DrainStatus.State.PROCESSING);
performDrain();
} finally {
drainStatus.set(DrainStatus.State.IDLE);
evictionLock.unlock();
}
}
private V rawPut(K key, V value) {
long now = ticker.read();
AtomicReference<V> previous = new AtomicReference<>();
AtomicReference<Node<K, V>> addedNode = new AtomicReference<>();
AtomicReference<Node<K, V>> updatedNode = new AtomicReference<>();
data.compute(key, (k, existing) -> {
if (existing != null) {
previous.set(existing.value);
existing.value = value;
applyWriteExpiration(existing, now);
updatedNode.set(existing);
return existing;
}
Node<K, V> node = new Node<>(key, value);
applyWriteExpiration(node, now);
addedNode.set(node);
return node;
});
// See getWith's comment: afterAdd/afterUpdate must run only after compute()
// has returned, so a nested drain never races against compute()'s own
// (unconditional, on-return) installation of this key's mapping.
if (addedNode.get() != null) {
afterAdd(addedNode.get());
} else if (updatedNode.get() != null) {
afterUpdate(updatedNode.get());
}
return previous.get();
}
private V rawInvalidate(K key) {
Node<K, V> removed = data.remove(key);
if (removed == null) {
return null;
}
afterRemove(removed);
return removed.value;
}
private boolean hasExpiration() {
return expireAfterWriteNanos >= 0 || expireAfterAccessNanos >= 0;
}
private boolean isExpired(Node<K, V> node) {
return hasExpiration() && node.expirationTimeNanos <= ticker.read();
}
private void applyWriteExpiration(Node<K, V> node, long now) {
if (!hasExpiration()) {
return;
}
if (expireAfterWriteNanos >= 0) {
node.writeExpirationTimeNanos = now + expireAfterWriteNanos;
}
node.expirationTimeNanos = effectiveDeadline(node, now);
}
private long effectiveDeadline(Node<K, V> node, long now) {
long deadline = Long.MAX_VALUE;
if (expireAfterWriteNanos >= 0) {
deadline = Math.min(deadline, node.writeExpirationTimeNanos);
}
if (expireAfterAccessNanos >= 0) {
deadline = Math.min(deadline, now + expireAfterAccessNanos);
}
return deadline;
}
private void afterRead(Node<K, V> node) {
if (readBuffer.recordRead(node)) {
scheduleDrain();
}
}
private void afterAdd(Node<K, V> node) {
writeBuffer.add(WriteEvent.add(node));
scheduleDrain();
}
private void afterUpdate(Node<K, V> node) {
writeBuffer.add(WriteEvent.update(node));
scheduleDrain();
}
private void afterRemove(Node<K, V> node) {
writeBuffer.add(WriteEvent.remove(node));
scheduleDrain();
}
private void scheduleDrain() {
if (drainStatus.compareAndSet(DrainStatus.State.IDLE, DrainStatus.State.REQUIRED)) {
tryDrain();
}
}
private void tryDrain() {
if (!drainStatus.compareAndSet(DrainStatus.State.REQUIRED, DrainStatus.State.PROCESSING)) {
return;
}
if (!evictionLock.tryLock()) {
drainStatus.compareAndSet(DrainStatus.State.PROCESSING, DrainStatus.State.REQUIRED);
return;
}
try {
performDrain();
} finally {
drainStatus.set(DrainStatus.State.IDLE);
evictionLock.unlock();
}
}
/** Must run only while holding {@link #evictionLock}. */
private void performDrain() {
// Writes drain before reads: a buffered read can only ever reference a node
// whose ADD event is already enqueued, so this ordering guarantees every
// node a buffered read names has already been linked by the time reads
// replay.
WriteEvent<K, V> event;
while ((event = writeBuffer.poll()) != null) {
applyWriteEvent(event);
}
long now = ticker.read();
readBuffer.drainTo(node -> {
policy.recordAccess(node);
if (node.queueType != null && expireAfterAccessNanos >= 0) {
long newDeadline = effectiveDeadline(node, now);
if (newDeadline != node.expirationTimeNanos) {
timerWheel.deschedule(node);
node.expirationTimeNanos = newDeadline;
timerWheel.schedule(node, newDeadline);
}
}
});
if (hasExpiration()) {
List<Node<K, V>> expired = new ArrayList<>();
timerWheel.advance(now, expired);
for (Node<K, V> node : expired) {
if (data.remove(node.key, node)) {
policy.remove(node);
}
}
}
for (Node<K, V> evicted : policy.evict()) {
if (hasExpiration()) {
timerWheel.deschedule(evicted);
}
data.remove(evicted.key, evicted);
}
}
private void applyWriteEvent(WriteEvent<K, V> event) {
Node<K, V> node = event.node;
switch (event.type) {
case ADD -> {
policy.recordAdd(node);
if (hasExpiration()) {
timerWheel.schedule(node, node.expirationTimeNanos);
}
}
case UPDATE -> {
if (node.queueType == null) {
return; // this node's own ADD hasn't drained yet -- safe no-op.
}
policy.recordAccess(node);
if (hasExpiration()) {
timerWheel.deschedule(node);
timerWheel.schedule(node, node.expirationTimeNanos);
}
}
case REMOVE -> {
if (node.queueType != null) {
policy.remove(node);
}
if (hasExpiration()) {
timerWheel.deschedule(node);
}
}
case CLEAR -> {
policy.clear();
if (hasExpiration()) {
timerWheel.clear();
}
}
}
}
}