• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ben-manes / caffeine / #4711

13 Jan 2025 10:09PM UTC coverage: 99.046% (-0.02%) from 99.068%
#4711

push

github

ben-manes
wip

7682 of 7756 relevant lines covered (99.05%)

0.99 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.31
/caffeine/src/main/java/com/github/benmanes/caffeine/cache/BoundedLocalCache.java
1
/*
2
 * Copyright 2014 Ben Manes. All Rights Reserved.
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *     http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16
package com.github.benmanes.caffeine.cache;
17

18
import static com.github.benmanes.caffeine.cache.Async.ASYNC_EXPIRY;
19
import static com.github.benmanes.caffeine.cache.Caffeine.calculateHashMapCapacity;
20
import static com.github.benmanes.caffeine.cache.Caffeine.ceilingPowerOfTwo;
21
import static com.github.benmanes.caffeine.cache.Caffeine.requireArgument;
22
import static com.github.benmanes.caffeine.cache.Caffeine.toNanosSaturated;
23
import static com.github.benmanes.caffeine.cache.LocalLoadingCache.newBulkMappingFunction;
24
import static com.github.benmanes.caffeine.cache.LocalLoadingCache.newMappingFunction;
25
import static com.github.benmanes.caffeine.cache.Node.PROBATION;
26
import static com.github.benmanes.caffeine.cache.Node.PROTECTED;
27
import static com.github.benmanes.caffeine.cache.Node.WINDOW;
28
import static java.util.Locale.US;
29
import static java.util.Objects.requireNonNull;
30
import static java.util.Spliterator.DISTINCT;
31
import static java.util.Spliterator.IMMUTABLE;
32
import static java.util.Spliterator.NONNULL;
33
import static java.util.Spliterator.ORDERED;
34
import static java.util.function.Function.identity;
35

36
import java.io.InvalidObjectException;
37
import java.io.ObjectInputStream;
38
import java.io.Serializable;
39
import java.lang.System.Logger;
40
import java.lang.System.Logger.Level;
41
import java.lang.invoke.MethodHandles;
42
import java.lang.invoke.VarHandle;
43
import java.lang.ref.Reference;
44
import java.lang.ref.ReferenceQueue;
45
import java.lang.ref.WeakReference;
46
import java.time.Duration;
47
import java.util.AbstractCollection;
48
import java.util.AbstractSet;
49
import java.util.ArrayDeque;
50
import java.util.Collection;
51
import java.util.Collections;
52
import java.util.Comparator;
53
import java.util.Deque;
54
import java.util.HashMap;
55
import java.util.IdentityHashMap;
56
import java.util.Iterator;
57
import java.util.LinkedHashMap;
58
import java.util.Map;
59
import java.util.NoSuchElementException;
60
import java.util.Objects;
61
import java.util.Optional;
62
import java.util.OptionalInt;
63
import java.util.OptionalLong;
64
import java.util.Set;
65
import java.util.Spliterator;
66
import java.util.Spliterators;
67
import java.util.concurrent.CancellationException;
68
import java.util.concurrent.CompletableFuture;
69
import java.util.concurrent.ConcurrentHashMap;
70
import java.util.concurrent.ConcurrentMap;
71
import java.util.concurrent.Executor;
72
import java.util.concurrent.ForkJoinPool;
73
import java.util.concurrent.ForkJoinTask;
74
import java.util.concurrent.ThreadLocalRandom;
75
import java.util.concurrent.TimeUnit;
76
import java.util.concurrent.TimeoutException;
77
import java.util.concurrent.locks.ReentrantLock;
78
import java.util.function.BiConsumer;
79
import java.util.function.BiFunction;
80
import java.util.function.Consumer;
81
import java.util.function.Function;
82
import java.util.function.Predicate;
83
import java.util.stream.Stream;
84
import java.util.stream.StreamSupport;
85

86
import org.jspecify.annotations.Nullable;
87

88
import com.github.benmanes.caffeine.cache.Async.AsyncExpiry;
89
import com.github.benmanes.caffeine.cache.LinkedDeque.PeekingIterator;
90
import com.github.benmanes.caffeine.cache.Policy.CacheEntry;
91
import com.github.benmanes.caffeine.cache.References.InternalReference;
92
import com.github.benmanes.caffeine.cache.stats.StatsCounter;
93
import com.google.errorprone.annotations.CanIgnoreReturnValue;
94
import com.google.errorprone.annotations.Var;
95
import com.google.errorprone.annotations.concurrent.GuardedBy;
96

97
/**
98
 * An in-memory cache implementation that supports full concurrency of retrievals, a high expected
99
 * concurrency for updates, and multiple ways to bound the cache.
100
 * <p>
101
 * This class is abstract and code generated subclasses provide the complete implementation for a
102
 * particular configuration. This is to ensure that only the fields and execution paths necessary
103
 * for a given configuration are used.
104
 *
105
 * @author ben.manes@gmail.com (Ben Manes)
106
 * @param <K> the type of keys maintained by this cache
107
 * @param <V> the type of mapped values
108
 */
109
@SuppressWarnings("serial")
110
abstract class BoundedLocalCache<K, V> extends BLCHeader.DrainStatusRef
111
    implements LocalCache<K, V> {
112

113
  /*
114
   * This class performs a best-effort bounding of a ConcurrentHashMap using a page-replacement
115
   * algorithm to determine which entries to evict when the capacity is exceeded.
116
   *
117
   * Concurrency:
118
   * ------------
119
   * The page replacement algorithms are kept eventually consistent with the map. An update to the
120
   * map and recording of reads may not be immediately reflected in the policy's data structures.
121
   * These structures are guarded by a lock, and operations are applied in batches to avoid lock
122
   * contention. The penalty of applying the batches is spread across threads, so that the amortized
123
   * cost is slightly higher than performing just the ConcurrentHashMap operation [1].
124
   *
125
   * A memento of the reads and writes that were performed on the map is recorded in buffers. These
126
   * buffers are drained at the first opportunity after a write or when a read buffer is full. The
127
   * reads are offered to a buffer that will reject additions if contended on or if it is full. Due
128
   * to the concurrent nature of the read and write operations, a strict policy ordering is not
129
   * possible, but it may be observably strict when single-threaded. The buffers are drained
130
   * asynchronously to minimize the request latency and uses a state machine to determine when to
131
   * schedule this work on an executor.
132
   *
133
   * Due to a lack of a strict ordering guarantee, a task can be executed out-of-order, such as a
134
   * removal followed by its addition. The state of the entry is encoded using the key field to
135
   * avoid additional memory usage. An entry is "alive" if it is in both the hash table and the page
136
   * replacement policy. It is "retired" if it is not in the hash table and is pending removal from
137
   * the page replacement policy. Finally, an entry transitions to the "dead" state when it is
138
   * neither in the hash table nor the page replacement policy. Both the retired and dead states are
139
   * represented by a sentinel key that should not be used for map operations.
140
   *
141
   * Eviction:
142
   * ---------
143
   * Maximum size is implemented using the Window TinyLfu policy [2] due to its high hit rate, O(1)
144
   * time complexity, and small footprint. A new entry starts in the admission window and remains
145
   * there as long as it has high temporal locality (recency). Eventually an entry will slip from
146
   * the window into the main space. If the main space is already full, then a historic frequency
147
   * filter determines whether to evict the newly admitted entry or the victim entry chosen by the
148
   * eviction policy. This process ensures that the entries in the window were very recently used,
149
   * while entries in the main space are accessed very frequently and remain moderately recent. The
150
   * windowing allows the policy to have a high hit rate when entries exhibit a bursty access
151
   * pattern, while the filter ensures that popular items are retained. The admission window uses
152
   * LRU and the main space uses Segmented LRU.
153
   *
154
   * The optimal size of the window vs. main spaces is workload dependent [3]. A large admission
155
   * window is favored by recency-biased workloads, while a small one favors frequency-biased
156
   * workloads. When the window is too small, then recent arrivals are prematurely evicted, but when
157
   * it is too large, then they pollute the cache and force the eviction of more popular entries.
158
   * The optimal configuration is dynamically determined by using hill climbing to walk the hit rate
159
   * curve. This is achieved by sampling the hit rate and adjusting the window size in the direction
160
   * that is improving (making positive or negative steps). At each interval, the step size is
161
   * decreased until the hit rate climber converges at the optimal setting. The process is restarted
162
   * when the hit rate changes over a threshold, indicating that the workload altered, and a new
163
   * setting may be required.
164
   *
165
   * The historic usage is retained in a compact popularity sketch, which uses hashing to
166
   * probabilistically estimate an item's frequency. This exposes a flaw where an adversary could
167
   * use hash flooding [4] to artificially raise the frequency of the main space's victim and cause
168
   * all candidates to be rejected. In the worst case, by exploiting hash collisions, an attacker
169
   * could cause the cache to never hit and hold only worthless items, resulting in a
170
   * denial-of-service attack against the underlying resource. This is mitigated by introducing
171
   * jitter, allowing candidates that are at least moderately popular to have a small, random chance
172
   * of being admitted. This causes the victim to be evicted, but in a way that marginally impacts
173
   * the hit rate.
174
   *
175
   * Expiration:
176
   * -----------
177
   * Expiration is implemented in O(1) time complexity. The time-to-idle policy uses an access-order
178
   * queue, the time-to-live policy uses a write-order queue, and variable expiration uses a
179
   * hierarchical timer wheel [5]. The queuing policies allow for peeking at the oldest entry to
180
   * determine if it has expired. If it has not, then the younger entries must not have expired
181
   * either. If a maximum size is set, then expiration will share the queues, minimizing the
182
   * per-entry footprint. The timer wheel based policy uses hashing and cascading in a manner that
183
   * amortizes the penalty of sorting to achieve a similar algorithmic cost.
184
   *
185
   * The expiration updates are applied in a best effort fashion. The reordering of variable or
186
   * access-order expiration may be discarded by the read buffer if it is full or contended.
187
   * Similarly, the reordering of write expiration may be ignored for an entry if the last update
188
   * was within a short time window. This is done to avoid overwhelming the write buffer.
189
   *
190
   * [1] BP-Wrapper: A Framework Making Any Replacement Algorithms (Almost) Lock Contention Free
191
   * http://web.cse.ohio-state.edu/hpcs/WWW/HTML/publications/papers/TR-09-1.pdf
192
   * [2] TinyLFU: A Highly Efficient Cache Admission Policy
193
   * https://dl.acm.org/citation.cfm?id=3149371
194
   * [3] Adaptive Software Cache Management
195
   * https://dl.acm.org/citation.cfm?id=3274816
196
   * [4] Denial of Service via Algorithmic Complexity Attack
197
   * https://www.usenix.org/legacy/events/sec03/tech/full_papers/crosby/crosby.pdf
198
   * [5] Hashed and Hierarchical Timing Wheels
199
   * http://www.cs.columbia.edu/~nahum/w6998/papers/ton97-timing-wheels.pdf
200
   */
201

202
  static final Logger logger = System.getLogger(BoundedLocalCache.class.getName());
1✔
203

204
  /** The number of CPUs */
205
  static final int NCPU = Runtime.getRuntime().availableProcessors();
1✔
206
  /** The initial capacity of the write buffer. */
207
  static final int WRITE_BUFFER_MIN = 4;
208
  /** The maximum capacity of the write buffer. */
209
  static final int WRITE_BUFFER_MAX = 128 * ceilingPowerOfTwo(NCPU);
1✔
210
  /** The number of attempts to insert into the write buffer before yielding. */
211
  static final int WRITE_BUFFER_RETRIES = 100;
212
  /** The maximum weighted capacity of the map. */
213
  static final long MAXIMUM_CAPACITY = Long.MAX_VALUE - Integer.MAX_VALUE;
214
  /** The initial percent of the maximum weighted capacity dedicated to the main space. */
215
  static final double PERCENT_MAIN = 0.99d;
216
  /** The percent of the maximum weighted capacity dedicated to the main's protected space. */
217
  static final double PERCENT_MAIN_PROTECTED = 0.80d;
218
  /** The difference in hit rates that restarts the climber. */
219
  static final double HILL_CLIMBER_RESTART_THRESHOLD = 0.05d;
220
  /** The percent of the total size to adapt the window by. */
221
  static final double HILL_CLIMBER_STEP_PERCENT = 0.0625d;
222
  /** The rate to decrease the step size to adapt by. */
223
  static final double HILL_CLIMBER_STEP_DECAY_RATE = 0.98d;
224
  /** The minimum popularity for allowing randomized admission. */
225
  static final int ADMIT_HASHDOS_THRESHOLD = 6;
226
  /** The maximum number of entries that can be transferred between queues. */
227
  static final int QUEUE_TRANSFER_THRESHOLD = 1_000;
228
  /** The maximum time window between entry updates before the expiration must be reordered. */
229
  static final long EXPIRE_WRITE_TOLERANCE = TimeUnit.SECONDS.toNanos(1);
1✔
230
  /** The maximum duration before an entry expires. */
231
  static final long MAXIMUM_EXPIRY = (Long.MAX_VALUE >> 1); // 150 years
232
  /** The duration to wait on the eviction lock before warning of a possible misuse. */
233
  static final long WARN_AFTER_LOCK_WAIT_NANOS = TimeUnit.SECONDS.toNanos(30);
1✔
234
  /** The number of retries before computing to validate the entry's integrity; pow2 modulus. */
235
  static final int MAX_PUT_SPIN_WAIT_ATTEMPTS = 1024 - 1;
236
  /** The handle for the in-flight refresh operations. */
237
  static final VarHandle REFRESHES;
238

239
  final @Nullable RemovalListener<K, V> evictionListener;
240
  final @Nullable AsyncCacheLoader<K, V> cacheLoader;
241

242
  final MpscGrowableArrayQueue<Runnable> writeBuffer;
243
  final ConcurrentHashMap<Object, Node<K, V>> data;
244
  final PerformCleanupTask drainBuffersTask;
245
  final Consumer<Node<K, V>> accessPolicy;
246
  final Buffer<Node<K, V>> readBuffer;
247
  final NodeFactory<K, V> nodeFactory;
248
  final ReentrantLock evictionLock;
249
  final Weigher<K, V> weigher;
250
  final Executor executor;
251

252
  final boolean isWeighted;
253
  final boolean isAsync;
254

255
  @Nullable Set<K> keySet;
256
  @Nullable Collection<V> values;
257
  @Nullable Set<Entry<K, V>> entrySet;
258
  volatile @Nullable ConcurrentMap<Object, CompletableFuture<?>> refreshes;
259

260
  /** Creates an instance based on the builder's configuration. */
261
  @SuppressWarnings("GuardedBy")
262
  protected BoundedLocalCache(Caffeine<K, V> builder,
263
      @Nullable AsyncCacheLoader<K, V> cacheLoader, boolean isAsync) {
1✔
264
    this.isAsync = isAsync;
1✔
265
    this.cacheLoader = cacheLoader;
1✔
266
    executor = builder.getExecutor();
1✔
267
    isWeighted = builder.isWeighted();
1✔
268
    evictionLock = new ReentrantLock();
1✔
269
    weigher = builder.getWeigher(isAsync);
1✔
270
    drainBuffersTask = new PerformCleanupTask(this);
1✔
271
    nodeFactory = NodeFactory.newFactory(builder, isAsync);
1✔
272
    evictionListener = builder.getEvictionListener(isAsync);
1✔
273
    data = new ConcurrentHashMap<>(builder.getInitialCapacity());
1✔
274
    readBuffer = evicts() || collectKeys() || collectValues() || expiresAfterAccess()
1✔
275
        ? new BoundedBuffer<>()
1✔
276
        : Buffer.disabled();
1✔
277
    accessPolicy = (evicts() || expiresAfterAccess()) ? this::onAccess : e -> {};
1✔
278
    writeBuffer = new MpscGrowableArrayQueue<>(WRITE_BUFFER_MIN, WRITE_BUFFER_MAX);
1✔
279

280
    if (evicts()) {
1✔
281
      setMaximumSize(builder.getMaximum());
1✔
282
    }
283
  }
1✔
284

285
  static {
286
    try {
287
      REFRESHES = MethodHandles.lookup()
1✔
288
          .findVarHandle(BoundedLocalCache.class, "refreshes", ConcurrentMap.class);
1✔
289
    } catch (ReflectiveOperationException e) {
×
290
      throw new ExceptionInInitializerError(e);
×
291
    }
1✔
292
  }
1✔
293

294
  /** Ensures that the node is alive during the map operation. */
295
  void requireIsAlive(Object key, Node<?, ?> node) {
296
    if (!node.isAlive()) {
1✔
297
      throw new IllegalStateException(brokenEqualityMessage(key, node));
1✔
298
    }
299
  }
1✔
300

301
  /** Logs if the node cannot be found in the map but is still alive. */
302
  void logIfAlive(Node<?, ?> node) {
303
    if (node.isAlive()) {
1✔
304
      String message = brokenEqualityMessage(node.getKeyReference(), node);
1✔
305
      logger.log(Level.ERROR, message, new IllegalStateException());
1✔
306
    }
307
  }
1✔
308

309
  /** Returns the formatted broken equality error message. */
310
  String brokenEqualityMessage(Object key, Node<?, ?> node) {
311
    return String.format(US, "An invalid state was detected, occurring when the key's equals or "
1✔
312
        + "hashCode was modified while residing in the cache. This violation of the Map "
313
        + "contract can lead to non-deterministic behavior (key: %s, key type: %s, "
314
        + "node type: %s, cache type: %s).", key, key.getClass().getName(),
1✔
315
        node.getClass().getSimpleName(), getClass().getSimpleName());
1✔
316
  }
317

318
  /* --------------- Shared --------------- */
319

320
  @Override
321
  public boolean isAsync() {
322
    return isAsync;
1✔
323
  }
324

325
  /** Returns if the node's value is currently being computed asynchronously. */
326
  final boolean isComputingAsync(@Nullable V value) {
327
    return isAsync && !Async.isReady((CompletableFuture<?>) value);
1✔
328
  }
329

330
  @GuardedBy("evictionLock")
331
  protected AccessOrderDeque<Node<K, V>> accessOrderWindowDeque() {
332
    throw new UnsupportedOperationException();
1✔
333
  }
334

335
  @GuardedBy("evictionLock")
336
  protected AccessOrderDeque<Node<K, V>> accessOrderProbationDeque() {
337
    throw new UnsupportedOperationException();
1✔
338
  }
339

340
  @GuardedBy("evictionLock")
341
  protected AccessOrderDeque<Node<K, V>> accessOrderProtectedDeque() {
342
    throw new UnsupportedOperationException();
1✔
343
  }
344

345
  @GuardedBy("evictionLock")
346
  protected WriteOrderDeque<Node<K, V>> writeOrderDeque() {
347
    throw new UnsupportedOperationException();
1✔
348
  }
349

350
  @Override
351
  public final Executor executor() {
352
    return executor;
1✔
353
  }
354

355
  @Override
356
  @SuppressWarnings("NullAway")
357
  public ConcurrentMap<Object, CompletableFuture<?>> refreshes() {
358
    @Var var pending = refreshes;
1✔
359
    if (pending == null) {
1✔
360
      pending = new ConcurrentHashMap<>();
1✔
361
      if (!REFRESHES.compareAndSet(this, null, pending)) {
1✔
362
        pending = refreshes;
×
363
      }
364
    }
365
    return pending;
1✔
366
  }
367

368
  /** Invalidate the in-flight refresh. */
369
  void discardRefresh(Object keyReference) {
370
    var pending = refreshes;
1✔
371
    if ((pending != null) && pending.containsKey(keyReference)) {
1✔
372
      pending.remove(keyReference);
1✔
373
    }
374
  }
1✔
375

376
  @Override
377
  public Object referenceKey(K key) {
378
    return nodeFactory.newLookupKey(key);
1✔
379
  }
380

381
  @Override
382
  public boolean isPendingEviction(K key) {
383
    Node<K, V> node = data.get(nodeFactory.newLookupKey(key));
1✔
384
    return (node != null)
1✔
385
        && ((node.getValue() == null) || hasExpired(node, expirationTicker().read()));
1✔
386
  }
387

388
  /* --------------- Stats Support --------------- */
389

390
  @Override
391
  public boolean isRecordingStats() {
392
    return false;
1✔
393
  }
394

395
  @Override
396
  public StatsCounter statsCounter() {
397
    return StatsCounter.disabledStatsCounter();
1✔
398
  }
399

400
  @Override
401
  public Ticker statsTicker() {
402
    return Ticker.disabledTicker();
1✔
403
  }
404

405
  /* --------------- Removal Listener Support --------------- */
406

407
  @SuppressWarnings("NullAway")
408
  protected RemovalListener<K, V> removalListener() {
409
    return null;
1✔
410
  }
411

412
  protected boolean hasRemovalListener() {
413
    return false;
1✔
414
  }
415

416
  @Override
417
  public void notifyRemoval(@Nullable K key, @Nullable V value, RemovalCause cause) {
418
    if (!hasRemovalListener()) {
1✔
419
      return;
1✔
420
    }
421
    Runnable task = () -> {
1✔
422
      try {
423
        removalListener().onRemoval(key, value, cause);
1✔
424
      } catch (Throwable t) {
1✔
425
        logger.log(Level.WARNING, "Exception thrown by removal listener", t);
1✔
426
      }
1✔
427
    };
1✔
428
    try {
429
      executor.execute(task);
1✔
430
    } catch (Throwable t) {
1✔
431
      logger.log(Level.ERROR, "Exception thrown when submitting removal listener", t);
1✔
432
      task.run();
1✔
433
    }
1✔
434
  }
1✔
435

436
  /* --------------- Eviction Listener Support --------------- */
437

438
  void notifyEviction(@Nullable K key, @Nullable V value, RemovalCause cause) {
439
    if (evictionListener == null) {
1✔
440
      return;
1✔
441
    }
442
    try {
443
      evictionListener.onRemoval(key, value, cause);
1✔
444
    } catch (Throwable t) {
1✔
445
      logger.log(Level.WARNING, "Exception thrown by eviction listener", t);
1✔
446
    }
1✔
447
  }
1✔
448

449
  /* --------------- Reference Support --------------- */
450

451
  /** Returns if the keys are weak reference garbage collected. */
452
  protected boolean collectKeys() {
453
    return false;
1✔
454
  }
455

456
  /** Returns if the values are weak or soft reference garbage collected. */
457
  protected boolean collectValues() {
458
    return false;
1✔
459
  }
460

461
  @SuppressWarnings("NullAway")
462
  protected ReferenceQueue<K> keyReferenceQueue() {
463
    return null;
1✔
464
  }
465

466
  @SuppressWarnings("NullAway")
467
  protected ReferenceQueue<V> valueReferenceQueue() {
468
    return null;
1✔
469
  }
470

471
  /* --------------- Expiration Support --------------- */
472

473
  /** Returns the {@link Pacer} used to schedule the maintenance task. */
474
  protected @Nullable Pacer pacer() {
475
    return null;
1✔
476
  }
477

478
  /** Returns if the cache expires entries after a variable time threshold. */
479
  protected boolean expiresVariable() {
480
    return false;
1✔
481
  }
482

483
  /** Returns if the cache expires entries after an access time threshold. */
484
  protected boolean expiresAfterAccess() {
485
    return false;
1✔
486
  }
487

488
  /** Returns how long after the last access to an entry the map will retain that entry. */
489
  protected long expiresAfterAccessNanos() {
490
    throw new UnsupportedOperationException();
1✔
491
  }
492

493
  protected void setExpiresAfterAccessNanos(long expireAfterAccessNanos) {
494
    throw new UnsupportedOperationException();
1✔
495
  }
496

497
  /** Returns if the cache expires entries after a write time threshold. */
498
  protected boolean expiresAfterWrite() {
499
    return false;
1✔
500
  }
501

502
  /** Returns how long after the last write to an entry the map will retain that entry. */
503
  protected long expiresAfterWriteNanos() {
504
    throw new UnsupportedOperationException();
1✔
505
  }
506

507
  protected void setExpiresAfterWriteNanos(long expireAfterWriteNanos) {
508
    throw new UnsupportedOperationException();
1✔
509
  }
510

511
  /** Returns if the cache refreshes entries after a write time threshold. */
512
  protected boolean refreshAfterWrite() {
513
    return false;
1✔
514
  }
515

516
  /** Returns how long after the last write an entry becomes a candidate for refresh. */
517
  protected long refreshAfterWriteNanos() {
518
    throw new UnsupportedOperationException();
1✔
519
  }
520

521
  protected void setRefreshAfterWriteNanos(long refreshAfterWriteNanos) {
522
    throw new UnsupportedOperationException();
1✔
523
  }
524

525
  @Override
526
  @SuppressWarnings("NullAway")
527
  public Expiry<K, V> expiry() {
528
    return null;
1✔
529
  }
530

531
  /** Returns the {@link Ticker} used by this cache for expiration. */
532
  public Ticker expirationTicker() {
533
    return Ticker.disabledTicker();
1✔
534
  }
535

536
  protected TimerWheel<K, V> timerWheel() {
537
    throw new UnsupportedOperationException();
1✔
538
  }
539

540
  /* --------------- Eviction Support --------------- */
541

542
  /** Returns if the cache evicts entries due to a maximum size or weight threshold. */
543
  protected boolean evicts() {
544
    return false;
1✔
545
  }
546

547
  /** Returns if entries may be assigned different weights. */
548
  protected boolean isWeighted() {
549
    return (weigher != Weigher.singletonWeigher());
1✔
550
  }
551

552
  protected FrequencySketch<K> frequencySketch() {
553
    throw new UnsupportedOperationException();
1✔
554
  }
555

556
  /** Returns if an access to an entry can skip notifying the eviction policy. */
557
  protected boolean fastpath() {
558
    return false;
1✔
559
  }
560

561
  /** Returns the maximum weighted size. */
562
  protected long maximum() {
563
    throw new UnsupportedOperationException();
1✔
564
  }
565

566
  /** Returns the maximum weighted size of the window space. */
567
  protected long windowMaximum() {
568
    throw new UnsupportedOperationException();
1✔
569
  }
570

571
  /** Returns the maximum weighted size of the main's protected space. */
572
  protected long mainProtectedMaximum() {
573
    throw new UnsupportedOperationException();
1✔
574
  }
575

576
  @GuardedBy("evictionLock")
577
  protected void setMaximum(long maximum) {
578
    throw new UnsupportedOperationException();
1✔
579
  }
580

581
  @GuardedBy("evictionLock")
582
  protected void setWindowMaximum(long maximum) {
583
    throw new UnsupportedOperationException();
1✔
584
  }
585

586
  @GuardedBy("evictionLock")
587
  protected void setMainProtectedMaximum(long maximum) {
588
    throw new UnsupportedOperationException();
1✔
589
  }
590

591
  /** Returns the combined weight of the values in the cache (may be negative). */
592
  protected long weightedSize() {
593
    throw new UnsupportedOperationException();
1✔
594
  }
595

596
  /** Returns the uncorrected combined weight of the values in the window space. */
597
  protected long windowWeightedSize() {
598
    throw new UnsupportedOperationException();
1✔
599
  }
600

601
  /** Returns the uncorrected combined weight of the values in the main's protected space. */
602
  protected long mainProtectedWeightedSize() {
603
    throw new UnsupportedOperationException();
1✔
604
  }
605

606
  @GuardedBy("evictionLock")
607
  protected void setWeightedSize(long weightedSize) {
608
    throw new UnsupportedOperationException();
1✔
609
  }
610

611
  @GuardedBy("evictionLock")
612
  protected void setWindowWeightedSize(long weightedSize) {
613
    throw new UnsupportedOperationException();
1✔
614
  }
615

616
  @GuardedBy("evictionLock")
617
  protected void setMainProtectedWeightedSize(long weightedSize) {
618
    throw new UnsupportedOperationException();
1✔
619
  }
620

621
  protected int hitsInSample() {
622
    throw new UnsupportedOperationException();
1✔
623
  }
624

625
  protected int missesInSample() {
626
    throw new UnsupportedOperationException();
1✔
627
  }
628

629
  protected int sampleCount() {
630
    throw new UnsupportedOperationException();
1✔
631
  }
632

633
  protected double stepSize() {
634
    throw new UnsupportedOperationException();
1✔
635
  }
636

637
  protected double previousSampleHitRate() {
638
    throw new UnsupportedOperationException();
1✔
639
  }
640

641
  protected long adjustment() {
642
    throw new UnsupportedOperationException();
1✔
643
  }
644

645
  @GuardedBy("evictionLock")
646
  protected void setHitsInSample(int hitCount) {
647
    throw new UnsupportedOperationException();
1✔
648
  }
649

650
  @GuardedBy("evictionLock")
651
  protected void setMissesInSample(int missCount) {
652
    throw new UnsupportedOperationException();
1✔
653
  }
654

655
  @GuardedBy("evictionLock")
656
  protected void setSampleCount(int sampleCount) {
657
    throw new UnsupportedOperationException();
1✔
658
  }
659

660
  @GuardedBy("evictionLock")
661
  protected void setStepSize(double stepSize) {
662
    throw new UnsupportedOperationException();
1✔
663
  }
664

665
  @GuardedBy("evictionLock")
666
  protected void setPreviousSampleHitRate(double hitRate) {
667
    throw new UnsupportedOperationException();
1✔
668
  }
669

670
  @GuardedBy("evictionLock")
671
  protected void setAdjustment(long amount) {
672
    throw new UnsupportedOperationException();
1✔
673
  }
674

675
  /**
676
   * Sets the maximum weighted size of the cache. The caller may need to perform a maintenance cycle
677
   * to eagerly evicts entries until the cache shrinks to the appropriate size.
678
   */
679
  @GuardedBy("evictionLock")
680
  @SuppressWarnings("Varifier")
681
  void setMaximumSize(long maximum) {
682
    requireArgument(maximum >= 0, "maximum must not be negative");
1✔
683
    if (maximum == maximum()) {
1✔
684
      return;
1✔
685
    }
686

687
    long max = Math.min(maximum, MAXIMUM_CAPACITY);
1✔
688
    long window = max - (long) (PERCENT_MAIN * max);
1✔
689
    long mainProtected = (long) (PERCENT_MAIN_PROTECTED * (max - window));
1✔
690

691
    setMaximum(max);
1✔
692
    setWindowMaximum(window);
1✔
693
    setMainProtectedMaximum(mainProtected);
1✔
694

695
    setHitsInSample(0);
1✔
696
    setMissesInSample(0);
1✔
697
    setStepSize(-HILL_CLIMBER_STEP_PERCENT * max);
1✔
698

699
    if ((frequencySketch() != null) && !isWeighted() && (weightedSize() >= (max >>> 1))) {
1✔
700
      // Lazily initialize when close to the maximum size
701
      frequencySketch().ensureCapacity(max);
1✔
702
    }
703
  }
1✔
704

705
  /** Evicts entries if the cache exceeds the maximum. */
706
  @GuardedBy("evictionLock")
707
  void evictEntries() {
708
    if (!evicts()) {
1✔
709
      return;
1✔
710
    }
711
    var candidate = evictFromWindow();
1✔
712
    evictFromMain(candidate);
1✔
713
  }
1✔
714

715
  /**
716
   * Evicts entries from the window space into the main space while the window size exceeds a
717
   * maximum.
718
   *
719
   * @return the first candidate promoted into the probation space
720
   */
721
  @GuardedBy("evictionLock")
722
  @Nullable Node<K, V> evictFromWindow() {
723
    @Var Node<K, V> first = null;
1✔
724
    @Var Node<K, V> node = accessOrderWindowDeque().peekFirst();
1✔
725
    while (windowWeightedSize() > windowMaximum()) {
1✔
726
      // The pending operations will adjust the size to reflect the correct weight
727
      if (node == null) {
1✔
728
        break;
1✔
729
      }
730

731
      Node<K, V> next = node.getNextInAccessOrder();
1✔
732
      if (node.getPolicyWeight() != 0) {
1✔
733
        node.makeMainProbation();
1✔
734
        accessOrderWindowDeque().remove(node);
1✔
735
        accessOrderProbationDeque().offerLast(node);
1✔
736
        if (first == null) {
1✔
737
          first = node;
1✔
738
        }
739

740
        setWindowWeightedSize(windowWeightedSize() - node.getPolicyWeight());
1✔
741
      }
742
      node = next;
1✔
743
    }
1✔
744

745
    return first;
1✔
746
  }
747

748
  /**
749
   * Evicts entries from the main space if the cache exceeds the maximum capacity. The main space
750
   * determines whether admitting an entry (coming from the window space) is preferable to retaining
751
   * the eviction policy's victim. This decision is made using a frequency filter so that the
752
   * least frequently used entry is removed.
753
   * <p>
754
   * The window space's candidates were previously promoted to the probation space at its MRU
755
   * position and the eviction policy's victim starts at the LRU position. The candidates are
756
   * evaluated in promotion order while an eviction is required, and if exhausted then additional
757
   * entries are retrieved from the window space. Likewise, if the victim selection exhausts the
758
   * probation space then additional entries are retrieved the protected space. The queues are
759
   * consumed in LRU order and the evicted entry is the one with a lower relative frequency, where
760
   * the preference is to retain the main space's victims versus the window space's candidates on a
761
   * tie.
762
   *
763
   * @param candidate the first candidate promoted into the probation space
764
   */
765
  @GuardedBy("evictionLock")
766
  void evictFromMain(@Var @Nullable Node<K, V> candidate) {
767
    @Var int victimQueue = PROBATION;
1✔
768
    @Var int candidateQueue = PROBATION;
1✔
769
    @Var Node<K, V> victim = accessOrderProbationDeque().peekFirst();
1✔
770
    while (weightedSize() > maximum()) {
1✔
771
      // Search the admission window for additional candidates
772
      if ((candidate == null) && (candidateQueue == PROBATION)) {
1✔
773
        candidate = accessOrderWindowDeque().peekFirst();
1✔
774
        candidateQueue = WINDOW;
1✔
775
      }
776

777
      // Try evicting from the protected and window queues
778
      if ((candidate == null) && (victim == null)) {
1✔
779
        if (victimQueue == PROBATION) {
1✔
780
          victim = accessOrderProtectedDeque().peekFirst();
1✔
781
          victimQueue = PROTECTED;
1✔
782
          continue;
1✔
783
        } else if (victimQueue == PROTECTED) {
1✔
784
          victim = accessOrderWindowDeque().peekFirst();
1✔
785
          victimQueue = WINDOW;
1✔
786
          continue;
1✔
787
        }
788

789
        // The pending operations will adjust the size to reflect the correct weight
790
        break;
791
      }
792

793
      // Skip over entries with zero weight
794
      if ((victim != null) && (victim.getPolicyWeight() == 0)) {
1✔
795
        victim = victim.getNextInAccessOrder();
1✔
796
        continue;
1✔
797
      } else if ((candidate != null) && (candidate.getPolicyWeight() == 0)) {
1✔
798
        candidate = candidate.getNextInAccessOrder();
1✔
799
        continue;
1✔
800
      }
801

802
      // Evict immediately if only one of the entries is present
803
      if (victim == null) {
1✔
804
        @SuppressWarnings("NullAway")
805
        Node<K, V> previous = candidate.getNextInAccessOrder();
1✔
806
        Node<K, V> evict = candidate;
1✔
807
        candidate = previous;
1✔
808
        evictEntry(evict, RemovalCause.SIZE, 0L);
1✔
809
        continue;
1✔
810
      } else if (candidate == null) {
1✔
811
        Node<K, V> evict = victim;
1✔
812
        victim = victim.getNextInAccessOrder();
1✔
813
        evictEntry(evict, RemovalCause.SIZE, 0L);
1✔
814
        continue;
1✔
815
      }
816

817
      // Evict immediately if both selected the same entry
818
      if (candidate == victim) {
1✔
819
        victim = victim.getNextInAccessOrder();
1✔
820
        evictEntry(candidate, RemovalCause.SIZE, 0L);
1✔
821
        candidate = null;
1✔
822
        continue;
1✔
823
      }
824

825
      // Evict immediately if an entry was collected
826
      K victimKey = victim.getKey();
1✔
827
      K candidateKey = candidate.getKey();
1✔
828
      if (victimKey == null) {
1✔
829
        Node<K, V> evict = victim;
1✔
830
        victim = victim.getNextInAccessOrder();
1✔
831
        evictEntry(evict, RemovalCause.COLLECTED, 0L);
1✔
832
        continue;
1✔
833
      } else if (candidateKey == null) {
1✔
834
        Node<K, V> evict = candidate;
1✔
835
        candidate = candidate.getNextInAccessOrder();
1✔
836
        evictEntry(evict, RemovalCause.COLLECTED, 0L);
1✔
837
        continue;
1✔
838
      }
839

840
      // Evict immediately if an entry was removed
841
      if (!victim.isAlive()) {
1✔
842
        Node<K, V> evict = victim;
1✔
843
        victim = victim.getNextInAccessOrder();
1✔
844
        evictEntry(evict, RemovalCause.SIZE, 0L);
1✔
845
        continue;
1✔
846
      } else if (!candidate.isAlive()) {
1✔
847
        Node<K, V> evict = candidate;
1✔
848
        candidate = candidate.getNextInAccessOrder();
1✔
849
        evictEntry(evict, RemovalCause.SIZE, 0L);
1✔
850
        continue;
1✔
851
      }
852

853
      // Evict immediately if the candidate's weight exceeds the maximum
854
      if (candidate.getPolicyWeight() > maximum()) {
1✔
855
        Node<K, V> evict = candidate;
1✔
856
        candidate = candidate.getNextInAccessOrder();
1✔
857
        evictEntry(evict, RemovalCause.SIZE, 0L);
1✔
858
        continue;
1✔
859
      }
860

861
      // Evict the entry with the lowest frequency
862
      if (admit(candidateKey, victimKey)) {
1✔
863
        Node<K, V> evict = victim;
1✔
864
        victim = victim.getNextInAccessOrder();
1✔
865
        evictEntry(evict, RemovalCause.SIZE, 0L);
1✔
866
        candidate = candidate.getNextInAccessOrder();
1✔
867
      } else {
1✔
868
        Node<K, V> evict = candidate;
1✔
869
        candidate = candidate.getNextInAccessOrder();
1✔
870
        evictEntry(evict, RemovalCause.SIZE, 0L);
1✔
871
      }
872
    }
1✔
873
  }
1✔
874

875
  /**
876
   * Determines if the candidate should be accepted into the main space, as determined by its
877
   * frequency relative to the victim. A small amount of randomness is used to protect against hash
878
   * collision attacks, where the victim's frequency is artificially raised so that no new entries
879
   * are admitted.
880
   *
881
   * @param candidateKey the key for the entry being proposed for long term retention
882
   * @param victimKey the key for the entry chosen by the eviction policy for replacement
883
   * @return if the candidate should be admitted and the victim ejected
884
   */
885
  @GuardedBy("evictionLock")
886
  boolean admit(K candidateKey, K victimKey) {
887
    int victimFreq = frequencySketch().frequency(victimKey);
1✔
888
    int candidateFreq = frequencySketch().frequency(candidateKey);
1✔
889
    if (candidateFreq > victimFreq) {
1✔
890
      return true;
1✔
891
    } else if (candidateFreq >= ADMIT_HASHDOS_THRESHOLD) {
1✔
892
      // The maximum frequency is 15 and halved to 7 after a reset to age the history. An attack
893
      // exploits that a hot candidate is rejected in favor of a hot victim. The threshold of a warm
894
      // candidate reduces the number of random acceptances to minimize the impact on the hit rate.
895
      int random = ThreadLocalRandom.current().nextInt();
1✔
896
      return ((random & 127) == 0);
1✔
897
    }
898
    return false;
1✔
899
  }
900

901
  /** Expires entries that have expired by access, write, or variable. */
902
  @GuardedBy("evictionLock")
903
  void expireEntries() {
904
    long now = expirationTicker().read();
1✔
905
    expireAfterAccessEntries(now);
1✔
906
    expireAfterWriteEntries(now);
1✔
907
    expireVariableEntries(now);
1✔
908

909
    Pacer pacer = pacer();
1✔
910
    if (pacer != null) {
1✔
911
      long delay = getExpirationDelay(now);
1✔
912
      if (delay == Long.MAX_VALUE) {
1✔
913
        pacer.cancel();
1✔
914
      } else {
915
        pacer.schedule(executor, drainBuffersTask, now, delay);
1✔
916
      }
917
    }
918
  }
1✔
919

920
  /** Expires entries in the access-order queue. */
921
  @GuardedBy("evictionLock")
922
  void expireAfterAccessEntries(long now) {
923
    if (!expiresAfterAccess()) {
1✔
924
      return;
1✔
925
    }
926

927
    expireAfterAccessEntries(now, accessOrderWindowDeque());
1✔
928
    if (evicts()) {
1✔
929
      expireAfterAccessEntries(now, accessOrderProbationDeque());
1✔
930
      expireAfterAccessEntries(now, accessOrderProtectedDeque());
1✔
931
    }
932
  }
1✔
933

934
  /** Expires entries in an access-order queue. */
935
  @GuardedBy("evictionLock")
936
  void expireAfterAccessEntries(long now, AccessOrderDeque<Node<K, V>> accessOrderDeque) {
937
    long duration = expiresAfterAccessNanos();
1✔
938
    for (;;) {
939
      Node<K, V> node = accessOrderDeque.peekFirst();
1✔
940
      if ((node == null) || ((now - node.getAccessTime()) < duration)
1✔
941
          || !evictEntry(node, RemovalCause.EXPIRED, now)) {
1✔
942
        return;
1✔
943
      }
944
    }
1✔
945
  }
946

947
  /** Expires entries on the write-order queue. */
948
  @GuardedBy("evictionLock")
949
  void expireAfterWriteEntries(long now) {
950
    if (!expiresAfterWrite()) {
1✔
951
      return;
1✔
952
    }
953
    long duration = expiresAfterWriteNanos();
1✔
954
    for (;;) {
955
      Node<K, V> node = writeOrderDeque().peekFirst();
1✔
956
      if ((node == null) || ((now - node.getWriteTime()) < duration)
1✔
957
          || !evictEntry(node, RemovalCause.EXPIRED, now)) {
1✔
958
        break;
1✔
959
      }
960
    }
1✔
961
  }
1✔
962

963
  /** Expires entries in the timer wheel. */
964
  @GuardedBy("evictionLock")
965
  void expireVariableEntries(long now) {
966
    if (expiresVariable()) {
1✔
967
      timerWheel().advance(this, now);
1✔
968
    }
969
  }
1✔
970

971
  /** Returns the duration until the next item expires, or {@link Long#MAX_VALUE} if none. */
972
  @GuardedBy("evictionLock")
973
  long getExpirationDelay(long now) {
974
    @Var long delay = Long.MAX_VALUE;
1✔
975
    if (expiresAfterAccess()) {
1✔
976
      @Var Node<K, V> node = accessOrderWindowDeque().peekFirst();
1✔
977
      if (node != null) {
1✔
978
        delay = Math.min(delay, expiresAfterAccessNanos() - (now - node.getAccessTime()));
1✔
979
      }
980
      if (evicts()) {
1✔
981
        node = accessOrderProbationDeque().peekFirst();
1✔
982
        if (node != null) {
1✔
983
          delay = Math.min(delay, expiresAfterAccessNanos() - (now - node.getAccessTime()));
1✔
984
        }
985
        node = accessOrderProtectedDeque().peekFirst();
1✔
986
        if (node != null) {
1✔
987
          delay = Math.min(delay, expiresAfterAccessNanos() - (now - node.getAccessTime()));
1✔
988
        }
989
      }
990
    }
991
    if (expiresAfterWrite()) {
1✔
992
      Node<K, V> node = writeOrderDeque().peekFirst();
1✔
993
      if (node != null) {
1✔
994
        delay = Math.min(delay, expiresAfterWriteNanos() - (now - node.getWriteTime()));
1✔
995
      }
996
    }
997
    if (expiresVariable()) {
1✔
998
      delay = Math.min(delay, timerWheel().getExpirationDelay());
1✔
999
    }
1000
    return delay;
1✔
1001
  }
1002

1003
  /** Returns if the entry has expired. */
1004
  @SuppressWarnings("ShortCircuitBoolean")
1005
  boolean hasExpired(Node<K, V> node, long now) {
1006
    if (isComputingAsync(node.getValue())) {
1✔
1007
      return false;
1✔
1008
    }
1009
    return (expiresAfterAccess() && (now - node.getAccessTime() >= expiresAfterAccessNanos()))
1✔
1010
        | (expiresAfterWrite() && (now - node.getWriteTime() >= expiresAfterWriteNanos()))
1✔
1011
        | (expiresVariable() && (now - node.getVariableTime() >= 0));
1✔
1012
  }
1013

1014
  /**
1015
   * Attempts to evict the entry based on the given removal cause. A removal may be ignored if the
1016
   * entry was updated and is no longer eligible for eviction.
1017
   *
1018
   * @param node the entry to evict
1019
   * @param cause the reason to evict
1020
   * @param now the current time, used only if expiring
1021
   * @return if the entry was evicted
1022
   */
1023
  @GuardedBy("evictionLock")
1024
  @SuppressWarnings({"GuardedByChecker", "NullAway", "PMD.CollapsibleIfStatements"})
1025
  boolean evictEntry(Node<K, V> node, RemovalCause cause, long now) {
1026
    K key = node.getKey();
1✔
1027
    @SuppressWarnings("unchecked")
1028
    var value = (V[]) new Object[1];
1✔
1029
    var removed = new boolean[1];
1✔
1030
    var resurrect = new boolean[1];
1✔
1031
    var actualCause = new RemovalCause[1];
1✔
1032
    var keyReference = node.getKeyReference();
1✔
1033

1034
    data.computeIfPresent(keyReference, (k, n) -> {
1✔
1035
      if (n != node) {
1✔
1036
        return n;
1✔
1037
      }
1038
      synchronized (n) {
1✔
1039
        value[0] = n.getValue();
1✔
1040

1041
        if ((key == null) || (value[0] == null)) {
1✔
1042
          actualCause[0] = RemovalCause.COLLECTED;
1✔
1043
        } else if (cause == RemovalCause.COLLECTED) {
1✔
1044
          resurrect[0] = true;
1✔
1045
          return n;
1✔
1046
        } else {
1047
          actualCause[0] = cause;
1✔
1048
        }
1049

1050
        if (actualCause[0] == RemovalCause.EXPIRED) {
1✔
1051
          @Var boolean expired = false;
1✔
1052
          if (expiresAfterAccess()) {
1✔
1053
            expired |= ((now - n.getAccessTime()) >= expiresAfterAccessNanos());
1✔
1054
          }
1055
          if (expiresAfterWrite()) {
1✔
1056
            expired |= ((now - n.getWriteTime()) >= expiresAfterWriteNanos());
1✔
1057
          }
1058
          if (expiresVariable()) {
1✔
1059
            expired |= ((now - node.getVariableTime()) >= 0);
1✔
1060
          }
1061
          if (!expired) {
1✔
1062
            resurrect[0] = true;
1✔
1063
            return n;
1✔
1064
          }
1065
        } else if (actualCause[0] == RemovalCause.SIZE) {
1✔
1066
          int weight = node.getWeight();
1✔
1067
          if (weight == 0) {
1✔
1068
            resurrect[0] = true;
1✔
1069
            return n;
1✔
1070
          }
1071
        }
1072

1073
        notifyEviction(key, value[0], actualCause[0]);
1✔
1074
        discardRefresh(keyReference);
1✔
1075
        removed[0] = true;
1✔
1076
        node.retire();
1✔
1077
      }
1✔
1078
      return null;
1✔
1079
    });
1080

1081
    // The entry is no longer eligible for eviction
1082
    if (resurrect[0]) {
1✔
1083
      return false;
1✔
1084
    }
1085

1086
    // If the eviction fails due to a concurrent removal of the victim, that removal may cancel out
1087
    // the addition that triggered this eviction. The victim is eagerly unlinked and the size
1088
    // decremented before the removal task so that if an eviction is still required then a new
1089
    // victim will be chosen for removal.
1090
    if (node.inWindow() && (evicts() || expiresAfterAccess())) {
1✔
1091
      accessOrderWindowDeque().remove(node);
1✔
1092
    } else if (evicts()) {
1✔
1093
      if (node.inMainProbation()) {
1✔
1094
        accessOrderProbationDeque().remove(node);
1✔
1095
      } else {
1096
        accessOrderProtectedDeque().remove(node);
1✔
1097
      }
1098
    }
1099
    if (expiresAfterWrite()) {
1✔
1100
      writeOrderDeque().remove(node);
1✔
1101
    } else if (expiresVariable()) {
1✔
1102
      timerWheel().deschedule(node);
1✔
1103
    }
1104

1105
    synchronized (node) {
1✔
1106
      logIfAlive(node);
1✔
1107
      makeDead(node);
1✔
1108
    }
1✔
1109

1110
    if (removed[0]) {
1✔
1111
      statsCounter().recordEviction(node.getWeight(), actualCause[0]);
1✔
1112
      notifyRemoval(key, value[0], actualCause[0]);
1✔
1113
    }
1114

1115
    return true;
1✔
1116
  }
1117

1118
  /** Adapts the eviction policy to towards the optimal recency / frequency configuration. */
1119
  @GuardedBy("evictionLock")
1120
  void climb() {
1121
    if (!evicts()) {
1✔
1122
      return;
1✔
1123
    }
1124

1125
    determineAdjustment();
1✔
1126
    demoteFromMainProtected();
1✔
1127
    long amount = adjustment();
1✔
1128
    if (amount == 0) {
1✔
1129
      return;
1✔
1130
    } else if (amount > 0) {
1✔
1131
      increaseWindow();
1✔
1132
    } else {
1133
      decreaseWindow();
1✔
1134
    }
1135
  }
1✔
1136

1137
  /** Calculates the amount to adapt the window by and sets {@link #adjustment()} accordingly. */
1138
  @GuardedBy("evictionLock")
1139
  void determineAdjustment() {
1140
    if (frequencySketch().isNotInitialized()) {
1✔
1141
      setPreviousSampleHitRate(0.0);
1✔
1142
      setMissesInSample(0);
1✔
1143
      setHitsInSample(0);
1✔
1144
      return;
1✔
1145
    }
1146

1147
    int requestCount = hitsInSample() + missesInSample();
1✔
1148
    if (requestCount < frequencySketch().sampleSize) {
1✔
1149
      return;
1✔
1150
    }
1151

1152
    double hitRate = (double) hitsInSample() / requestCount;
1✔
1153
    double hitRateChange = hitRate - previousSampleHitRate();
1✔
1154
    double amount = (hitRateChange >= 0) ? stepSize() : -stepSize();
1✔
1155
    double nextStepSize = (Math.abs(hitRateChange) >= HILL_CLIMBER_RESTART_THRESHOLD)
1✔
1156
        ? HILL_CLIMBER_STEP_PERCENT * maximum() * (amount >= 0 ? 1 : -1)
1✔
1157
        : HILL_CLIMBER_STEP_DECAY_RATE * amount;
1✔
1158
    setPreviousSampleHitRate(hitRate);
1✔
1159
    setAdjustment((long) amount);
1✔
1160
    setStepSize(nextStepSize);
1✔
1161
    setMissesInSample(0);
1✔
1162
    setHitsInSample(0);
1✔
1163
  }
1✔
1164

1165
  /**
1166
   * Increases the size of the admission window by shrinking the portion allocated to the main
1167
   * space. As the main space is partitioned into probation and protected regions (80% / 20%), for
1168
   * simplicity only the protected is reduced. If the regions exceed their maximums, this may cause
1169
   * protected items to be demoted to the probation region and probation items to be demoted to the
1170
   * admission window.
1171
   */
1172
  @GuardedBy("evictionLock")
1173
  void increaseWindow() {
1174
    if (mainProtectedMaximum() == 0) {
1✔
1175
      return;
1✔
1176
    }
1177

1178
    @Var long quota = Math.min(adjustment(), mainProtectedMaximum());
1✔
1179
    setMainProtectedMaximum(mainProtectedMaximum() - quota);
1✔
1180
    setWindowMaximum(windowMaximum() + quota);
1✔
1181
    demoteFromMainProtected();
1✔
1182

1183
    for (int i = 0; i < QUEUE_TRANSFER_THRESHOLD; i++) {
1✔
1184
      @Var Node<K, V> candidate = accessOrderProbationDeque().peekFirst();
1✔
1185
      @Var boolean probation = true;
1✔
1186
      if ((candidate == null) || (quota < candidate.getPolicyWeight())) {
1✔
1187
        candidate = accessOrderProtectedDeque().peekFirst();
1✔
1188
        probation = false;
1✔
1189
      }
1190
      if (candidate == null) {
1✔
1191
        break;
1✔
1192
      }
1193

1194
      int weight = candidate.getPolicyWeight();
1✔
1195
      if (quota < weight) {
1✔
1196
        break;
1✔
1197
      }
1198

1199
      quota -= weight;
1✔
1200
      if (probation) {
1✔
1201
        accessOrderProbationDeque().remove(candidate);
1✔
1202
      } else {
1203
        setMainProtectedWeightedSize(mainProtectedWeightedSize() - weight);
1✔
1204
        accessOrderProtectedDeque().remove(candidate);
1✔
1205
      }
1206
      setWindowWeightedSize(windowWeightedSize() + weight);
1✔
1207
      accessOrderWindowDeque().offerLast(candidate);
1✔
1208
      candidate.makeWindow();
1✔
1209
    }
1210

1211
    setMainProtectedMaximum(mainProtectedMaximum() + quota);
1✔
1212
    setWindowMaximum(windowMaximum() - quota);
1✔
1213
    setAdjustment(quota);
1✔
1214
  }
1✔
1215

1216
  /** Decreases the size of the admission window and increases the main's protected region. */
1217
  @GuardedBy("evictionLock")
1218
  void decreaseWindow() {
1219
    if (windowMaximum() <= 1) {
1✔
1220
      return;
1✔
1221
    }
1222

1223
    @Var long quota = Math.min(-adjustment(), Math.max(0, windowMaximum() - 1));
1✔
1224
    setMainProtectedMaximum(mainProtectedMaximum() + quota);
1✔
1225
    setWindowMaximum(windowMaximum() - quota);
1✔
1226

1227
    for (int i = 0; i < QUEUE_TRANSFER_THRESHOLD; i++) {
1✔
1228
      Node<K, V> candidate = accessOrderWindowDeque().peekFirst();
1✔
1229
      if (candidate == null) {
1✔
1230
        break;
1✔
1231
      }
1232

1233
      int weight = candidate.getPolicyWeight();
1✔
1234
      if (quota < weight) {
1✔
1235
        break;
1✔
1236
      }
1237

1238
      quota -= weight;
1✔
1239
      setWindowWeightedSize(windowWeightedSize() - weight);
1✔
1240
      accessOrderWindowDeque().remove(candidate);
1✔
1241
      accessOrderProbationDeque().offerLast(candidate);
1✔
1242
      candidate.makeMainProbation();
1✔
1243
    }
1244

1245
    setMainProtectedMaximum(mainProtectedMaximum() - quota);
1✔
1246
    setWindowMaximum(windowMaximum() + quota);
1✔
1247
    setAdjustment(-quota);
1✔
1248
  }
1✔
1249

1250
  /** Transfers the nodes from the protected to the probation region if it exceeds the maximum. */
1251
  @GuardedBy("evictionLock")
1252
  void demoteFromMainProtected() {
1253
    long mainProtectedMaximum = mainProtectedMaximum();
1✔
1254
    @Var long mainProtectedWeightedSize = mainProtectedWeightedSize();
1✔
1255
    if (mainProtectedWeightedSize <= mainProtectedMaximum) {
1✔
1256
      return;
1✔
1257
    }
1258

1259
    for (int i = 0; i < QUEUE_TRANSFER_THRESHOLD; i++) {
1✔
1260
      if (mainProtectedWeightedSize <= mainProtectedMaximum) {
1✔
1261
        break;
1✔
1262
      }
1263

1264
      Node<K, V> demoted = accessOrderProtectedDeque().poll();
1✔
1265
      if (demoted == null) {
1✔
1266
        break;
1✔
1267
      }
1268
      demoted.makeMainProbation();
1✔
1269
      accessOrderProbationDeque().offerLast(demoted);
1✔
1270
      mainProtectedWeightedSize -= demoted.getPolicyWeight();
1✔
1271
    }
1272
    setMainProtectedWeightedSize(mainProtectedWeightedSize);
1✔
1273
  }
1✔
1274

1275
  /**
1276
   * Performs the post-processing work required after a read.
1277
   *
1278
   * @param node the entry in the page replacement policy
1279
   * @param now the current time, in nanoseconds
1280
   * @param recordHit if the hit count should be incremented
1281
   * @return the refreshed value if immediately loaded, else null
1282
   */
1283
  @Nullable V afterRead(Node<K, V> node, long now, boolean recordHit) {
1284
    if (recordHit) {
1✔
1285
      statsCounter().recordHits(1);
1✔
1286
    }
1287

1288
    boolean delayable = skipReadBuffer() || (readBuffer.offer(node) != Buffer.FULL);
1✔
1289
    if (shouldDrainBuffers(delayable)) {
1✔
1290
      scheduleDrainBuffers();
1✔
1291
    }
1292
    return refreshIfNeeded(node, now);
1✔
1293
  }
1294

1295
  /** Returns if the cache should bypass the read buffer. */
1296
  boolean skipReadBuffer() {
1297
    return fastpath() && frequencySketch().isNotInitialized();
1✔
1298
  }
1299

1300
  /**
1301
   * Asynchronously refreshes the entry if eligible.
1302
   *
1303
   * @param node the entry in the cache to refresh
1304
   * @param now the current time, in nanoseconds
1305
   * @return the refreshed value if immediately loaded, else null
1306
   */
1307
  @SuppressWarnings("FutureReturnValueIgnored")
1308
  @Nullable V refreshIfNeeded(Node<K, V> node, long now) {
1309
    if (!refreshAfterWrite()) {
1✔
1310
      return null;
1✔
1311
    }
1312

1313
    K key;
1314
    V oldValue;
1315
    long writeTime = node.getWriteTime();
1✔
1316
    long refreshWriteTime = writeTime | 1L;
1✔
1317
    Object keyReference = node.getKeyReference();
1✔
1318
    ConcurrentMap<Object, CompletableFuture<?>> refreshes;
1319
    if (((now - writeTime) > refreshAfterWriteNanos()) && (keyReference != null)
1✔
1320
        && ((key = node.getKey()) != null) && ((oldValue = node.getValue()) != null)
1✔
1321
        && !isComputingAsync(oldValue) && ((writeTime & 1L) == 0L)
1✔
1322
        && !(refreshes = refreshes()).containsKey(keyReference)
1✔
1323
        && node.isAlive() && node.casWriteTime(writeTime, refreshWriteTime)) {
1✔
1324
      long[] startTime = new long[1];
1✔
1325
      @SuppressWarnings({"rawtypes", "unchecked"})
1326
      CompletableFuture<? extends V>[] refreshFuture = new CompletableFuture[1];
1✔
1327
      try {
1328
        refreshes.computeIfAbsent(keyReference, k -> {
1✔
1329
          try {
1330
            startTime[0] = statsTicker().read();
1✔
1331
            if (isAsync) {
1✔
1332
              @SuppressWarnings("unchecked")
1333
              var future = (CompletableFuture<V>) oldValue;
1✔
1334
              if (Async.isReady(future)) {
1✔
1335
                @SuppressWarnings("NullAway")
1336
                var refresh = cacheLoader.asyncReload(key, future.join(), executor);
1✔
1337
                refreshFuture[0] = requireNonNull(refresh, "Null future");
1✔
1338
              } else {
1✔
1339
                // no-op if the future's completion state was modified (e.g. obtrude methods)
1340
                return null;
1✔
1341
              }
1342
            } else {
1✔
1343
              @SuppressWarnings("NullAway")
1344
              var refresh = cacheLoader.asyncReload(key, oldValue, executor);
1✔
1345
              refreshFuture[0] = requireNonNull(refresh, "Null future");
1✔
1346
            }
1347
            return refreshFuture[0];
1✔
1348
          } catch (InterruptedException e) {
1✔
1349
            Thread.currentThread().interrupt();
1✔
1350
            logger.log(Level.WARNING, "Exception thrown when submitting refresh task", e);
1✔
1351
            return null;
1✔
1352
          } catch (Throwable e) {
1✔
1353
            logger.log(Level.WARNING, "Exception thrown when submitting refresh task", e);
1✔
1354
            return null;
1✔
1355
          }
1356
        });
1357
      } finally {
1358
        node.casWriteTime(refreshWriteTime, writeTime);
1✔
1359
      }
1360

1361
      if (refreshFuture[0] == null) {
1✔
1362
        return null;
1✔
1363
      }
1364

1365
      var refreshed = refreshFuture[0].handle((newValue, error) -> {
1✔
1366
        long loadTime = statsTicker().read() - startTime[0];
1✔
1367
        if (error != null) {
1✔
1368
          if (!(error instanceof CancellationException) && !(error instanceof TimeoutException)) {
1✔
1369
            logger.log(Level.WARNING, "Exception thrown during refresh", error);
1✔
1370
          }
1371
          refreshes.remove(keyReference, refreshFuture[0]);
1✔
1372
          statsCounter().recordLoadFailure(loadTime);
1✔
1373
          return null;
1✔
1374
        }
1375

1376
        @SuppressWarnings("unchecked")
1377
        V value = (isAsync && (newValue != null)) ? (V) refreshFuture[0] : newValue;
1✔
1378

1379
        RemovalCause[] cause = new RemovalCause[1];
1✔
1380
        V result = compute(key, (k, currentValue) -> {
1✔
1381
          if (currentValue == null) {
1✔
1382
            // If the entry is absent then discard the refresh and maybe notifying the listener
1383
            if (value != null) {
1✔
1384
              cause[0] = RemovalCause.EXPLICIT;
1✔
1385
            }
1386
            return null;
1✔
1387
          } else if (currentValue == value) {
1✔
1388
            // If the reloaded value is the same instance then no-op
1389
            return currentValue;
1✔
1390
          } else if (isAsync &&
1✔
1391
              (newValue == Async.getIfReady((CompletableFuture<?>) currentValue))) {
1✔
1392
            // If the completed futures hold the same value instance then no-op
1393
            return currentValue;
1✔
1394
          } else if ((currentValue == oldValue) && (node.getWriteTime() == writeTime)) {
1✔
1395
            // If the entry was not modified while in-flight (no ABA) then replace
1396
            return value;
1✔
1397
          }
1398
          // Otherwise, a write invalidated the refresh so discard it and notify the listener
1399
          cause[0] = RemovalCause.REPLACED;
1✔
1400
          return currentValue;
1✔
1401
        }, expiry(), /* recordLoad= */ false, /* recordLoadFailure= */ true);
1✔
1402

1403
        if (cause[0] != null) {
1✔
1404
          notifyRemoval(key, value, cause[0]);
1✔
1405
        }
1406
        if (newValue == null) {
1✔
1407
          statsCounter().recordLoadFailure(loadTime);
1✔
1408
        } else {
1409
          statsCounter().recordLoadSuccess(loadTime);
1✔
1410
        }
1411

1412
        refreshes.remove(keyReference, refreshFuture[0]);
1✔
1413
        return result;
1✔
1414
      });
1415
      return Async.getIfReady(refreshed);
1✔
1416
    }
1417

1418
    return null;
1✔
1419
  }
1420

1421
  /**
1422
   * Returns the expiration time for the entry after being created.
1423
   *
1424
   * @param key the key of the entry that was created
1425
   * @param value the value of the entry that was created
1426
   * @param expiry the calculator for the expiration time
1427
   * @param now the current time, in nanoseconds
1428
   * @return the expiration time
1429
   */
1430
  long expireAfterCreate(@Nullable K key, @Nullable V value,
1431
      Expiry<? super K, ? super V> expiry, long now) {
1432
    if (expiresVariable() && (key != null) && (value != null)) {
1✔
1433
      long duration = expiry.expireAfterCreate(key, value, now);
1✔
1434
      return isAsync ? (now + duration) : (now + Math.min(duration, MAXIMUM_EXPIRY));
1✔
1435
    }
1436
    return 0L;
1✔
1437
  }
1438

1439
  /**
1440
   * Returns the expiration time for the entry after being updated.
1441
   *
1442
   * @param node the entry in the page replacement policy
1443
   * @param key the key of the entry that was updated
1444
   * @param value the value of the entry that was updated
1445
   * @param expiry the calculator for the expiration time
1446
   * @param now the current time, in nanoseconds
1447
   * @return the expiration time
1448
   */
1449
  long expireAfterUpdate(Node<K, V> node, @Nullable K key,
1450
      @Nullable V value, Expiry<? super K, ? super V> expiry, long now) {
1451
    if (expiresVariable() && (key != null) && (value != null)) {
1✔
1452
      long currentDuration = Math.max(1, node.getVariableTime() - now);
1✔
1453
      long duration = expiry.expireAfterUpdate(key, value, now, currentDuration);
1✔
1454
      return isAsync ? (now + duration) : (now + Math.min(duration, MAXIMUM_EXPIRY));
1✔
1455
    }
1456
    return 0L;
1✔
1457
  }
1458

1459
  /**
1460
   * Returns the access time for the entry after a read.
1461
   *
1462
   * @param node the entry in the page replacement policy
1463
   * @param key the key of the entry that was read
1464
   * @param value the value of the entry that was read
1465
   * @param expiry the calculator for the expiration time
1466
   * @param now the current time, in nanoseconds
1467
   * @return the expiration time
1468
   */
1469
  long expireAfterRead(Node<K, V> node, @Nullable K key,
1470
      @Nullable V value, Expiry<K, V> expiry, long now) {
1471
    if (expiresVariable() && (key != null) && (value != null)) {
1✔
1472
      long currentDuration = Math.max(1, node.getVariableTime() - now);
1✔
1473
      long duration = expiry.expireAfterRead(key, value, now, currentDuration);
1✔
1474
      return isAsync ? (now + duration) : (now + Math.min(duration, MAXIMUM_EXPIRY));
1✔
1475
    }
1476
    return 0L;
1✔
1477
  }
1478

1479
  /**
1480
   * Attempts to update the access time for the entry after a read.
1481
   *
1482
   * @param node the entry in the page replacement policy
1483
   * @param key the key of the entry that was read
1484
   * @param value the value of the entry that was read
1485
   * @param expiry the calculator for the expiration time
1486
   * @param now the current time, in nanoseconds
1487
   */
1488
  void tryExpireAfterRead(Node<K, V> node, @Nullable K key,
1489
      @Nullable V value, Expiry<K, V> expiry, long now) {
1490
    if (!expiresVariable() || (key == null) || (value == null)) {
1✔
1491
      return;
1✔
1492
    }
1493

1494
    long variableTime = node.getVariableTime();
1✔
1495
    long currentDuration = Math.max(1, variableTime - now);
1✔
1496
    if (isAsync && (currentDuration > MAXIMUM_EXPIRY)) {
1✔
1497
      // expireAfterCreate has not yet set the duration after completion
1498
      return;
1✔
1499
    }
1500

1501
    long duration = expiry.expireAfterRead(key, value, now, currentDuration);
1✔
1502
    if (duration != currentDuration) {
1✔
1503
      long expirationTime = isAsync ? (now + duration) : (now + Math.min(duration, MAXIMUM_EXPIRY));
1✔
1504
      node.casVariableTime(variableTime, expirationTime);
1✔
1505
    }
1506
  }
1✔
1507

1508
  void setVariableTime(Node<K, V> node, long expirationTime) {
1509
    if (expiresVariable()) {
1✔
1510
      node.setVariableTime(expirationTime);
1✔
1511
    }
1512
  }
1✔
1513

1514
  void setWriteTime(Node<K, V> node, long now) {
1515
    if (expiresAfterWrite() || refreshAfterWrite()) {
1✔
1516
      node.setWriteTime(now & ~1L);
1✔
1517
    }
1518
  }
1✔
1519

1520
  void setAccessTime(Node<K, V> node, long now) {
1521
    if (expiresAfterAccess()) {
1✔
1522
      node.setAccessTime(now);
1✔
1523
    }
1524
  }
1✔
1525

1526
  /**
1527
   * Performs the post-processing work required after a write.
1528
   *
1529
   * @param task the pending operation to be applied
1530
   */
1531
  void afterWrite(Runnable task) {
1532
    for (int i = 0; i < WRITE_BUFFER_RETRIES; i++) {
1✔
1533
      if (writeBuffer.offer(task)) {
1✔
1534
        scheduleAfterWrite();
1✔
1535
        return;
1✔
1536
      }
1537
      scheduleDrainBuffers();
1✔
1538
      Thread.onSpinWait();
1✔
1539
    }
1540

1541
    // In scenarios where the writing threads cannot make progress then they attempt to provide
1542
    // assistance by performing the eviction work directly. This can resolve cases where the
1543
    // maintenance task is scheduled but not running. That might occur due to all of the executor's
1544
    // threads being busy (perhaps writing into this cache), the write rate greatly exceeds the
1545
    // consuming rate, priority inversion, or if the executor silently discarded the maintenance
1546
    // task. Unfortunately this cannot resolve when the eviction is blocked waiting on a long-
1547
    // running computation due to an eviction listener, the victim is being computed on by a writer,
1548
    // or the victim residing in the same hash bin as a computing entry. In those cases a warning is
1549
    // logged to encourage the application to decouple these computations from the map operations.
1550
    lock();
1✔
1551
    try {
1552
      maintenance(task);
1✔
1553
    } catch (RuntimeException e) {
1✔
1554
      logger.log(Level.ERROR, "Exception thrown when performing the maintenance task", e);
1✔
1555
    } finally {
1556
      evictionLock.unlock();
1✔
1557
    }
1558
    rescheduleCleanUpIfIncomplete();
1✔
1559
  }
1✔
1560

1561
  /** Acquires the eviction lock. */
1562
  void lock() {
1563
    @Var long remainingNanos = WARN_AFTER_LOCK_WAIT_NANOS;
1✔
1564
    long end = System.nanoTime() + remainingNanos;
1✔
1565
    @Var boolean interrupted = false;
1✔
1566
    try {
1567
      for (;;) {
1568
        try {
1569
          if (evictionLock.tryLock(remainingNanos, TimeUnit.NANOSECONDS)) {
1✔
1570
            return;
1✔
1571
          }
1572
          logger.log(Level.WARNING, "The cache is experiencing excessive wait times for acquiring "
1✔
1573
              + "the eviction lock. This may indicate that a long-running computation has halted "
1574
              + "eviction when trying to remove the victim entry. Consider using AsyncCache to "
1575
              + "decouple the computation from the map operation.", new TimeoutException());
1576
          evictionLock.lock();
1✔
1577
          return;
1✔
1578
        } catch (InterruptedException e) {
1✔
1579
          remainingNanos = end - System.nanoTime();
1✔
1580
          interrupted = true;
1✔
1581
        }
1✔
1582
      }
1583
    } finally {
1584
      if (interrupted) {
1✔
1585
        Thread.currentThread().interrupt();
1✔
1586
      }
1587
    }
1588
  }
1589

1590
  /**
1591
   * Conditionally schedules the asynchronous maintenance task after a write operation. If the
1592
   * task status was IDLE or REQUIRED then the maintenance task is scheduled immediately. If it
1593
   * is already processing then it is set to transition to REQUIRED upon completion so that a new
1594
   * execution is triggered by the next operation.
1595
   */
1596
  void scheduleAfterWrite() {
1597
    @Var int drainStatus = drainStatusOpaque();
1✔
1598
    for (;;) {
1599
      switch (drainStatus) {
1✔
1600
        case IDLE:
1601
          casDrainStatus(IDLE, REQUIRED);
1✔
1602
          scheduleDrainBuffers();
1✔
1603
          return;
1✔
1604
        case REQUIRED:
1605
          scheduleDrainBuffers();
1✔
1606
          return;
1✔
1607
        case PROCESSING_TO_IDLE:
1608
          if (casDrainStatus(PROCESSING_TO_IDLE, PROCESSING_TO_REQUIRED)) {
1✔
1609
            return;
1✔
1610
          }
1611
          drainStatus = drainStatusAcquire();
1✔
1612
          continue;
1✔
1613
        case PROCESSING_TO_REQUIRED:
1614
          return;
1✔
1615
        default:
1616
          throw new IllegalStateException("Invalid drain status: " + drainStatus);
1✔
1617
      }
1618
    }
1619
  }
1620

1621
  /**
1622
   * Attempts to schedule an asynchronous task to apply the pending operations to the page
1623
   * replacement policy. If the executor rejects the task then it is run directly.
1624
   */
1625
  void scheduleDrainBuffers() {
1626
    if (drainStatusOpaque() >= PROCESSING_TO_IDLE) {
1✔
1627
      return;
1✔
1628
    }
1629
    if (evictionLock.tryLock()) {
1✔
1630
      try {
1631
        int drainStatus = drainStatusOpaque();
1✔
1632
        if (drainStatus >= PROCESSING_TO_IDLE) {
1✔
1633
          return;
1✔
1634
        }
1635
        setDrainStatusRelease(PROCESSING_TO_IDLE);
1✔
1636
        executor.execute(drainBuffersTask);
1✔
1637
      } catch (Throwable t) {
1✔
1638
        logger.log(Level.WARNING, "Exception thrown when submitting maintenance task", t);
1✔
1639
        maintenance(/* ignored */ null);
1✔
1640
      } finally {
1641
        evictionLock.unlock();
1✔
1642
      }
1643
    }
1644
  }
1✔
1645

1646
  @Override
1647
  public void cleanUp() {
1648
    try {
1649
      performCleanUp(/* ignored */ null);
1✔
1650
    } catch (RuntimeException e) {
1✔
1651
      logger.log(Level.ERROR, "Exception thrown when performing the maintenance task", e);
1✔
1652
    }
1✔
1653
  }
1✔
1654

1655
  /**
1656
   * Performs the maintenance work, blocking until the lock is acquired.
1657
   *
1658
   * @param task an additional pending task to run, or {@code null} if not present
1659
   */
1660
  void performCleanUp(@Nullable Runnable task) {
1661
    evictionLock.lock();
1✔
1662
    try {
1663
      maintenance(task);
1✔
1664
    } finally {
1665
      evictionLock.unlock();
1✔
1666
    }
1667
    rescheduleCleanUpIfIncomplete();
1✔
1668
  }
1✔
1669

1670
  /**
1671
   * If there remains pending operations that were not handled by the prior clean up then try to
1672
   * schedule an asynchronous maintenance task. This may occur due to a concurrent write after the
1673
   * maintenance work had started or if the amortized threshold of work per clean up was reached.
1674
   */
1675
  @SuppressWarnings("resource")
1676
  void rescheduleCleanUpIfIncomplete() {
1677
    if (drainStatusOpaque() != REQUIRED) {
1✔
1678
      return;
1✔
1679
    }
1680

1681
    // An immediate scheduling cannot be performed on a custom executor because it may use a
1682
    // caller-runs policy. This could cause the caller's penalty to exceed the amortized threshold,
1683
    // e.g. repeated concurrent writes could result in a retry loop.
1684
    if (executor == ForkJoinPool.commonPool()) {
1✔
1685
      scheduleDrainBuffers();
1✔
1686
      return;
1✔
1687
    }
1688

1689
    // If a scheduler was configured then the maintenance can be deferred onto the custom executor
1690
    // and run in the near future. Otherwise, it will be handled due to other cache activity.
1691
    var pacer = pacer();
1✔
1692
    if ((pacer != null) && !pacer.isScheduled() && evictionLock.tryLock()) {
1✔
1693
      try {
1694
        if ((drainStatusOpaque() == REQUIRED) && !pacer.isScheduled()) {
1✔
1695
          pacer.schedule(executor, drainBuffersTask, expirationTicker().read(), Pacer.TOLERANCE);
1✔
1696
        }
1697
      } finally {
1698
        evictionLock.unlock();
1✔
1699
      }
1700
    }
1701
  }
1✔
1702

1703
  /**
1704
   * Performs the pending maintenance work and sets the state flags during processing to avoid
1705
   * excess scheduling attempts. The read buffer, write buffer, and reference queues are drained,
1706
   * followed by expiration, and size-based eviction.
1707
   *
1708
   * @param task an additional pending task to run, or {@code null} if not present
1709
   */
1710
  @GuardedBy("evictionLock")
1711
  void maintenance(@Nullable Runnable task) {
1712
    setDrainStatusRelease(PROCESSING_TO_IDLE);
1✔
1713

1714
    try {
1715
      drainReadBuffer();
1✔
1716

1717
      drainWriteBuffer();
1✔
1718
      if (task != null) {
1✔
1719
        task.run();
1✔
1720
      }
1721

1722
      drainKeyReferences();
1✔
1723
      drainValueReferences();
1✔
1724

1725
      expireEntries();
1✔
1726
      evictEntries();
1✔
1727

1728
      climb();
1✔
1729
    } finally {
1730
      if ((drainStatusOpaque() != PROCESSING_TO_IDLE)
1✔
1731
          || !casDrainStatus(PROCESSING_TO_IDLE, IDLE)) {
1✔
1732
        setDrainStatusOpaque(REQUIRED);
1✔
1733
      }
1734
    }
1735
  }
1✔
1736

1737
  /** Drains the weak key references queue. */
1738
  @GuardedBy("evictionLock")
1739
  void drainKeyReferences() {
1740
    if (!collectKeys()) {
1✔
1741
      return;
1✔
1742
    }
1743
    @Var Reference<? extends K> keyRef;
1744
    while ((keyRef = keyReferenceQueue().poll()) != null) {
1✔
1745
      Node<K, V> node = data.get(keyRef);
1✔
1746
      if (node != null) {
1✔
1747
        evictEntry(node, RemovalCause.COLLECTED, 0L);
1✔
1748
      }
1749
    }
1✔
1750
  }
1✔
1751

1752
  /** Drains the weak / soft value references queue. */
1753
  @GuardedBy("evictionLock")
1754
  void drainValueReferences() {
1755
    if (!collectValues()) {
1✔
1756
      return;
1✔
1757
    }
1758
    @Var Reference<? extends V> valueRef;
1759
    while ((valueRef = valueReferenceQueue().poll()) != null) {
1✔
1760
      @SuppressWarnings({"RedundantCast", "unchecked"})
1761
      var ref = (InternalReference<V>) (Object) valueRef;
1✔
1762
      Node<K, V> node = data.get(ref.getKeyReference());
1✔
1763
      if ((node != null) && (valueRef == node.getValueReference())) {
1✔
1764
        evictEntry(node, RemovalCause.COLLECTED, 0L);
1✔
1765
      }
1766
    }
1✔
1767
  }
1✔
1768

1769
  /** Drains the read buffer. */
1770
  @GuardedBy("evictionLock")
1771
  void drainReadBuffer() {
1772
    if (!skipReadBuffer()) {
1✔
1773
      readBuffer.drainTo(accessPolicy);
1✔
1774
    }
1775
  }
1✔
1776

1777
  /** Updates the node's location in the page replacement policy. */
1778
  @GuardedBy("evictionLock")
1779
  void onAccess(Node<K, V> node) {
1780
    if (evicts()) {
1✔
1781
      K key = node.getKey();
1✔
1782
      if (key == null) {
1✔
1783
        return;
1✔
1784
      }
1785
      frequencySketch().increment(key);
1✔
1786
      if (node.inWindow()) {
1✔
1787
        reorder(accessOrderWindowDeque(), node);
1✔
1788
      } else if (node.inMainProbation()) {
1✔
1789
        reorderProbation(node);
1✔
1790
      } else {
1791
        reorder(accessOrderProtectedDeque(), node);
1✔
1792
      }
1793
      setHitsInSample(hitsInSample() + 1);
1✔
1794
    } else if (expiresAfterAccess()) {
1✔
1795
      reorder(accessOrderWindowDeque(), node);
1✔
1796
    }
1797
    if (expiresVariable()) {
1✔
1798
      timerWheel().reschedule(node);
1✔
1799
    }
1800
  }
1✔
1801

1802
  /** Promote the node from probation to protected on an access. */
1803
  @GuardedBy("evictionLock")
1804
  void reorderProbation(Node<K, V> node) {
1805
    if (!accessOrderProbationDeque().contains(node)) {
1✔
1806
      // Ignore stale accesses for an entry that is no longer present
1807
      return;
1✔
1808
    } else if (node.getPolicyWeight() > mainProtectedMaximum()) {
1✔
1809
      reorder(accessOrderProbationDeque(), node);
1✔
1810
      return;
1✔
1811
    }
1812

1813
    // If the protected space exceeds its maximum, the LRU items are demoted to the probation space.
1814
    // This is deferred to the adaption phase at the end of the maintenance cycle.
1815
    setMainProtectedWeightedSize(mainProtectedWeightedSize() + node.getPolicyWeight());
1✔
1816
    accessOrderProbationDeque().remove(node);
1✔
1817
    accessOrderProtectedDeque().offerLast(node);
1✔
1818
    node.makeMainProtected();
1✔
1819
  }
1✔
1820

1821
  /** Updates the node's location in the policy's deque. */
1822
  static <K, V> void reorder(LinkedDeque<Node<K, V>> deque, Node<K, V> node) {
1823
    // An entry may be scheduled for reordering despite having been removed. This can occur when the
1824
    // entry was concurrently read while a writer was removing it. If the entry is no longer linked
1825
    // then it does not need to be processed.
1826
    if (deque.contains(node)) {
1✔
1827
      deque.moveToBack(node);
1✔
1828
    }
1829
  }
1✔
1830

1831
  /** Drains the write buffer. */
1832
  @GuardedBy("evictionLock")
1833
  void drainWriteBuffer() {
1834
    for (int i = 0; i <= WRITE_BUFFER_MAX; i++) {
1✔
1835
      Runnable task = writeBuffer.poll();
1✔
1836
      if (task == null) {
1✔
1837
        return;
1✔
1838
      }
1839
      task.run();
1✔
1840
    }
1841
    setDrainStatusOpaque(PROCESSING_TO_REQUIRED);
1✔
1842
  }
1✔
1843

1844
  /**
1845
   * Atomically transitions the node to the <code>dead</code> state and decrements the
1846
   * <code>weightedSize</code>.
1847
   *
1848
   * @param node the entry in the page replacement policy
1849
   */
1850
  @GuardedBy("evictionLock")
1851
  void makeDead(Node<K, V> node) {
1852
    synchronized (node) {
1✔
1853
      if (node.isDead()) {
1✔
1854
        return;
1✔
1855
      }
1856
      if (evicts()) {
1✔
1857
        // The node's policy weight may be out of sync due to a pending update waiting to be
1858
        // processed. At this point the node's weight is finalized, so the weight can be safely
1859
        // taken from the node's perspective and the sizes will be adjusted correctly.
1860
        if (node.inWindow()) {
1✔
1861
          setWindowWeightedSize(windowWeightedSize() - node.getWeight());
1✔
1862
        } else if (node.inMainProtected()) {
1✔
1863
          setMainProtectedWeightedSize(mainProtectedWeightedSize() - node.getWeight());
1✔
1864
        }
1865
        setWeightedSize(weightedSize() - node.getWeight());
1✔
1866
      }
1867
      node.die();
1✔
1868
    }
1✔
1869
  }
1✔
1870

1871
  /** Adds the node to the page replacement policy. */
1872
  final class AddTask implements Runnable {
1873
    final Node<K, V> node;
1874
    final int weight;
1875

1876
    AddTask(Node<K, V> node, int weight) {
1✔
1877
      this.weight = weight;
1✔
1878
      this.node = node;
1✔
1879
    }
1✔
1880

1881
    @Override
1882
    @GuardedBy("evictionLock")
1883
    public void run() {
1884
      if (evicts()) {
1✔
1885
        setWeightedSize(weightedSize() + weight);
1✔
1886
        setWindowWeightedSize(windowWeightedSize() + weight);
1✔
1887
        node.setPolicyWeight(node.getPolicyWeight() + weight);
1✔
1888

1889
        long maximum = maximum();
1✔
1890
        if (weightedSize() >= (maximum >>> 1)) {
1✔
1891
          if (weightedSize() > MAXIMUM_CAPACITY) {
1✔
1892
            evictEntries();
1✔
1893
          } else {
1894
            // Lazily initialize when close to the maximum
1895
            long capacity = isWeighted() ? data.mappingCount() : maximum;
1✔
1896
            frequencySketch().ensureCapacity(capacity);
1✔
1897
          }
1898
        }
1899

1900
        K key = node.getKey();
1✔
1901
        if (key != null) {
1✔
1902
          frequencySketch().increment(key);
1✔
1903
        }
1904

1905
        setMissesInSample(missesInSample() + 1);
1✔
1906
      }
1907

1908
      // ignore out-of-order write operations
1909
      boolean isAlive;
1910
      synchronized (node) {
1✔
1911
        isAlive = node.isAlive();
1✔
1912
      }
1✔
1913
      if (isAlive) {
1✔
1914
        if (expiresAfterWrite()) {
1✔
1915
          writeOrderDeque().offerLast(node);
1✔
1916
        }
1917
        if (expiresVariable()) {
1✔
1918
          timerWheel().schedule(node);
1✔
1919
        }
1920
        if (evicts()) {
1✔
1921
          if (weight > maximum()) {
1✔
1922
            evictEntry(node, RemovalCause.SIZE, expirationTicker().read());
1✔
1923
          } else if (weight > windowMaximum()) {
1✔
1924
            accessOrderWindowDeque().offerFirst(node);
1✔
1925
          } else {
1926
            accessOrderWindowDeque().offerLast(node);
1✔
1927
          }
1928
        } else if (expiresAfterAccess()) {
1✔
1929
          accessOrderWindowDeque().offerLast(node);
1✔
1930
        }
1931
      }
1932

1933
      // Ensure that in-flight async computation cannot expire (reset on a completion callback)
1934
      if (isComputingAsync(node.getValue())) {
1✔
1935
        synchronized (node) {
1✔
1936
          if (!Async.isReady((CompletableFuture<?>) node.getValue())) {
1✔
1937
            long expirationTime = expirationTicker().read() + ASYNC_EXPIRY;
1✔
1938
            setAccessTime(node, expirationTime);
1✔
1939
          }
1940
        }
1✔
1941
      }
1942
    }
1✔
1943
  }
1944

1945
  /** Removes a node from the page replacement policy. */
1946
  final class RemovalTask implements Runnable {
1947
    final Node<K, V> node;
1948

1949
    RemovalTask(Node<K, V> node) {
1✔
1950
      this.node = node;
1✔
1951
    }
1✔
1952

1953
    @Override
1954
    @GuardedBy("evictionLock")
1955
    public void run() {
1956
      // add may not have been processed yet
1957
      if (node.inWindow() && (evicts() || expiresAfterAccess())) {
1✔
1958
        accessOrderWindowDeque().remove(node);
1✔
1959
      } else if (evicts()) {
1✔
1960
        if (node.inMainProbation()) {
1✔
1961
          accessOrderProbationDeque().remove(node);
1✔
1962
        } else {
1963
          accessOrderProtectedDeque().remove(node);
1✔
1964
        }
1965
      }
1966
      if (expiresAfterWrite()) {
1✔
1967
        writeOrderDeque().remove(node);
1✔
1968
      } else if (expiresVariable()) {
1✔
1969
        timerWheel().deschedule(node);
1✔
1970
      }
1971
      makeDead(node);
1✔
1972
    }
1✔
1973
  }
1974

1975
  /** Updates the weighted size. */
1976
  final class UpdateTask implements Runnable {
1977
    final int weightDifference;
1978
    final Node<K, V> node;
1979

1980
    public UpdateTask(Node<K, V> node, int weightDifference) {
1✔
1981
      this.weightDifference = weightDifference;
1✔
1982
      this.node = node;
1✔
1983
    }
1✔
1984

1985
    @Override
1986
    @GuardedBy("evictionLock")
1987
    public void run() {
1988
      if (expiresAfterWrite()) {
1✔
1989
        reorder(writeOrderDeque(), node);
1✔
1990
      } else if (expiresVariable()) {
1✔
1991
        timerWheel().reschedule(node);
1✔
1992
      }
1993
      if (evicts()) {
1✔
1994
        int oldWeightedSize = node.getPolicyWeight();
1✔
1995
        node.setPolicyWeight(oldWeightedSize + weightDifference);
1✔
1996
        if (node.inWindow()) {
1✔
1997
          setWindowWeightedSize(windowWeightedSize() + weightDifference);
1✔
1998
          if (node.getPolicyWeight() > maximum()) {
1✔
1999
            evictEntry(node, RemovalCause.SIZE, expirationTicker().read());
1✔
2000
          } else if (node.getPolicyWeight() <= windowMaximum()) {
1✔
2001
            onAccess(node);
1✔
2002
          } else if (accessOrderWindowDeque().contains(node)) {
1✔
2003
            accessOrderWindowDeque().moveToFront(node);
1✔
2004
          }
2005
        } else if (node.inMainProbation()) {
1✔
2006
            if (node.getPolicyWeight() <= maximum()) {
1✔
2007
              onAccess(node);
1✔
2008
            } else {
2009
              evictEntry(node, RemovalCause.SIZE, expirationTicker().read());
1✔
2010
            }
2011
        } else if (node.inMainProtected()) {
1✔
2012
          setMainProtectedWeightedSize(mainProtectedWeightedSize() + weightDifference);
1✔
2013
          if (node.getPolicyWeight() <= maximum()) {
1✔
2014
            onAccess(node);
1✔
2015
          } else {
2016
            evictEntry(node, RemovalCause.SIZE, expirationTicker().read());
1✔
2017
          }
2018
        }
2019

2020
        setWeightedSize(weightedSize() + weightDifference);
1✔
2021
        if (weightedSize() > MAXIMUM_CAPACITY) {
1✔
2022
          evictEntries();
1✔
2023
        }
2024
      } else if (expiresAfterAccess()) {
1✔
2025
        onAccess(node);
1✔
2026
      }
2027
    }
1✔
2028
  }
2029

2030
  /* --------------- Concurrent Map Support --------------- */
2031

2032
  @Override
2033
  public boolean isEmpty() {
2034
    return data.isEmpty();
1✔
2035
  }
2036

2037
  @Override
2038
  public int size() {
2039
    return data.size();
1✔
2040
  }
2041

2042
  @Override
2043
  public long estimatedSize() {
2044
    return data.mappingCount();
1✔
2045
  }
2046

2047
  @Override
2048
  public void clear() {
2049
    Deque<Node<K, V>> entries;
2050
    evictionLock.lock();
1✔
2051
    try {
2052
      // Discard all pending reads
2053
      readBuffer.drainTo(e -> {});
1✔
2054

2055
      // Apply all pending writes
2056
      @Var Runnable task;
2057
      while ((task = writeBuffer.poll()) != null) {
1✔
2058
        task.run();
1✔
2059
      }
2060

2061
      // Cancel the scheduled cleanup
2062
      Pacer pacer = pacer();
1✔
2063
      if (pacer != null) {
1✔
2064
        pacer.cancel();
1✔
2065
      }
2066

2067
      // Discard all entries, falling back to one-by-one to avoid excessive lock hold times
2068
      long now = expirationTicker().read();
1✔
2069
      int threshold = (WRITE_BUFFER_MAX / 2);
1✔
2070
      entries = new ArrayDeque<>(data.values());
1✔
2071
      while (!entries.isEmpty() && (writeBuffer.size() < threshold)) {
1✔
2072
        removeNode(entries.poll(), now);
1✔
2073
      }
2074
    } finally {
2075
      evictionLock.unlock();
1✔
2076
    }
2077

2078
    // Remove any stragglers if released early to more aggressively flush incoming writes
2079
    @Var boolean cleanUp = false;
1✔
2080
    for (var node : entries) {
1✔
2081
      var key = node.getKey();
1✔
2082
      if (key == null) {
1✔
2083
        cleanUp = true;
1✔
2084
      } else {
2085
        remove(key);
1✔
2086
      }
2087
    }
1✔
2088
    if (collectKeys() && cleanUp) {
1✔
2089
      cleanUp();
1✔
2090
    }
2091
  }
1✔
2092

2093
  @GuardedBy("evictionLock")
2094
  @SuppressWarnings("GuardedByChecker")
2095
  void removeNode(Node<K, V> node, long now) {
2096
    K key = node.getKey();
1✔
2097
    var cause = new RemovalCause[1];
1✔
2098
    var keyReference = node.getKeyReference();
1✔
2099
    @SuppressWarnings({"unchecked", "Varifier"})
2100
    @Nullable V[] value = (V[]) new Object[1];
1✔
2101

2102
    data.computeIfPresent(keyReference, (k, n) -> {
1✔
2103
      if (n != node) {
1✔
2104
        return n;
1✔
2105
      }
2106
      synchronized (n) {
1✔
2107
        value[0] = n.getValue();
1✔
2108

2109
        if ((key == null) || (value[0] == null)) {
1✔
2110
          cause[0] = RemovalCause.COLLECTED;
1✔
2111
        } else if (hasExpired(n, now)) {
1✔
2112
          cause[0] = RemovalCause.EXPIRED;
1✔
2113
        } else {
2114
          cause[0] = RemovalCause.EXPLICIT;
1✔
2115
        }
2116

2117
        if (cause[0].wasEvicted()) {
1✔
2118
          notifyEviction(key, value[0], cause[0]);
1✔
2119
        }
2120

2121
        discardRefresh(node.getKeyReference());
1✔
2122
        node.retire();
1✔
2123
        return null;
1✔
2124
      }
2125
    });
2126

2127
    if (node.inWindow() && (evicts() || expiresAfterAccess())) {
1✔
2128
      accessOrderWindowDeque().remove(node);
1✔
2129
    } else if (evicts()) {
1✔
2130
      if (node.inMainProbation()) {
1✔
2131
        accessOrderProbationDeque().remove(node);
1✔
2132
      } else {
2133
        accessOrderProtectedDeque().remove(node);
1✔
2134
      }
2135
    }
2136
    if (expiresAfterWrite()) {
1✔
2137
      writeOrderDeque().remove(node);
1✔
2138
    } else if (expiresVariable()) {
1✔
2139
      timerWheel().deschedule(node);
1✔
2140
    }
2141

2142
    synchronized (node) {
1✔
2143
      logIfAlive(node);
1✔
2144
      makeDead(node);
1✔
2145
    }
1✔
2146

2147
    if (cause[0] != null) {
1✔
2148
      notifyRemoval(key, value[0], cause[0]);
1✔
2149
    }
2150
  }
1✔
2151

2152
  @Override
2153
  public boolean containsKey(Object key) {
2154
    Node<K, V> node = data.get(nodeFactory.newLookupKey(key));
1✔
2155
    return (node != null) && (node.getValue() != null)
1✔
2156
        && !hasExpired(node, expirationTicker().read());
1✔
2157
  }
2158

2159
  @Override
2160
  @SuppressWarnings("SuspiciousMethodCalls")
2161
  public boolean containsValue(Object value) {
2162
    requireNonNull(value);
1✔
2163

2164
    long now = expirationTicker().read();
1✔
2165
    for (Node<K, V> node : data.values()) {
1✔
2166
      if (node.containsValue(value) && !hasExpired(node, now) && (node.getKey() != null)) {
1✔
2167
        return true;
1✔
2168
      }
2169
    }
1✔
2170
    return false;
1✔
2171
  }
2172

2173
  @Override
2174
  public @Nullable V get(Object key) {
2175
    return getIfPresent(key, /* recordStats= */ false);
1✔
2176
  }
2177

2178
  @Override
2179
  public @Nullable V getIfPresent(Object key, boolean recordStats) {
2180
    Node<K, V> node = data.get(nodeFactory.newLookupKey(key));
1✔
2181
    if (node == null) {
1✔
2182
      if (recordStats) {
1✔
2183
        statsCounter().recordMisses(1);
1✔
2184
      }
2185
      if (drainStatusOpaque() == REQUIRED) {
1✔
2186
        scheduleDrainBuffers();
1✔
2187
      }
2188
      return null;
1✔
2189
    }
2190

2191
    V value = node.getValue();
1✔
2192
    long now = expirationTicker().read();
1✔
2193
    if (hasExpired(node, now) || (collectValues() && (value == null))) {
1✔
2194
      if (recordStats) {
1✔
2195
        statsCounter().recordMisses(1);
1✔
2196
      }
2197
      scheduleDrainBuffers();
1✔
2198
      return null;
1✔
2199
    }
2200

2201
    if (!isComputingAsync(value)) {
1✔
2202
      @SuppressWarnings("unchecked")
2203
      var castedKey = (K) key;
1✔
2204
      setAccessTime(node, now);
1✔
2205
      tryExpireAfterRead(node, castedKey, value, expiry(), now);
1✔
2206
    }
2207
    V refreshed = afterRead(node, now, recordStats);
1✔
2208
    return (refreshed == null) ? value : refreshed;
1✔
2209
  }
2210

2211
  @Override
2212
  public @Nullable V getIfPresentQuietly(Object key) {
2213
    V value;
2214
    Node<K, V> node = data.get(nodeFactory.newLookupKey(key));
1✔
2215
    if ((node == null) || ((value = node.getValue()) == null)
1✔
2216
        || hasExpired(node, expirationTicker().read())) {
1✔
2217
      return null;
1✔
2218
    }
2219
    return value;
1✔
2220
  }
2221

2222
  /**
2223
   * Returns the key associated with the mapping in this cache, or {@code null} if there is none.
2224
   *
2225
   * @param key the key whose canonical instance is to be returned
2226
   * @return the key used by the mapping, or {@code null} if this cache does not contain a mapping
2227
   *         for the key
2228
   * @throws NullPointerException if the specified key is null
2229
   */
2230
  public @Nullable K getKey(K key) {
2231
    Node<K, V> node = data.get(nodeFactory.newLookupKey(key));
1✔
2232
    if (node == null) {
1✔
2233
      if (drainStatusOpaque() == REQUIRED) {
1✔
2234
        scheduleDrainBuffers();
1✔
2235
      }
2236
      return null;
1✔
2237
    }
2238
    afterRead(node, /* now= */ 0L, /* recordHit= */ false);
1✔
2239
    return node.getKey();
1✔
2240
  }
2241

2242
  @Override
2243
  public Map<K, V> getAllPresent(Iterable<? extends K> keys) {
2244
    var result = new LinkedHashMap<K, V>(calculateHashMapCapacity(keys));
1✔
2245
    for (K key : keys) {
1✔
2246
      result.put(key, null);
1✔
2247
    }
1✔
2248

2249
    int uniqueKeys = result.size();
1✔
2250
    long now = expirationTicker().read();
1✔
2251
    for (var iter = result.entrySet().iterator(); iter.hasNext();) {
1✔
2252
      V value;
2253
      var entry = iter.next();
1✔
2254
      Node<K, V> node = data.get(nodeFactory.newLookupKey(entry.getKey()));
1✔
2255
      if ((node == null) || ((value = node.getValue()) == null) || hasExpired(node, now)) {
1✔
2256
        iter.remove();
1✔
2257
      } else {
2258
        if (!isComputingAsync(value)) {
1✔
2259
          tryExpireAfterRead(node, entry.getKey(), value, expiry(), now);
1✔
2260
          setAccessTime(node, now);
1✔
2261
        }
2262
        V refreshed = afterRead(node, now, /* recordHit= */ false);
1✔
2263
        entry.setValue((refreshed == null) ? value : refreshed);
1✔
2264
      }
2265
    }
1✔
2266
    statsCounter().recordHits(result.size());
1✔
2267
    statsCounter().recordMisses(uniqueKeys - result.size());
1✔
2268

2269
    return Collections.unmodifiableMap(result);
1✔
2270
  }
2271

2272
  @Override
2273
  public void putAll(Map<? extends K, ? extends V> map) {
2274
    map.forEach(this::put);
1✔
2275
  }
1✔
2276

2277
  @Override
2278
  public @Nullable V put(K key, V value) {
2279
    return put(key, value, expiry(), /* onlyIfAbsent= */ false);
1✔
2280
  }
2281

2282
  @Override
2283
  public @Nullable V putIfAbsent(K key, V value) {
2284
    return put(key, value, expiry(), /* onlyIfAbsent= */ true);
1✔
2285
  }
2286

2287
  /**
2288
   * Adds a node to the policy and the data store. If an existing node is found, then its value is
2289
   * updated if allowed.
2290
   *
2291
   * @param key key with which the specified value is to be associated
2292
   * @param value value to be associated with the specified key
2293
   * @param expiry the calculator for the write expiration time
2294
   * @param onlyIfAbsent a write is performed only if the key is not already associated with a value
2295
   * @return the prior value in or null if no mapping was found
2296
   */
2297
  @Nullable V put(K key, V value, Expiry<K, V> expiry, boolean onlyIfAbsent) {
2298
    requireNonNull(key);
1✔
2299
    requireNonNull(value);
1✔
2300

2301
    @Var Node<K, V> node = null;
1✔
2302
    long now = expirationTicker().read();
1✔
2303
    int newWeight = weigher.weigh(key, value);
1✔
2304
    Object lookupKey = nodeFactory.newLookupKey(key);
1✔
2305
    for (int attempts = 1; ; attempts++) {
1✔
2306
      @Var Node<K, V> prior = data.get(lookupKey);
1✔
2307
      if (prior == null) {
1✔
2308
        if (node == null) {
1✔
2309
          node = nodeFactory.newNode(key, keyReferenceQueue(),
1✔
2310
              value, valueReferenceQueue(), newWeight, now);
1✔
2311
          setVariableTime(node, expireAfterCreate(key, value, expiry, now));
1✔
2312
          if (isComputingAsync(value)) {
1✔
2313
            long expirationTime = now + ASYNC_EXPIRY;
1✔
2314
            setAccessTime(node, expirationTime);
1✔
2315
            setWriteTime(node, expirationTime);
1✔
2316
          } else {
1✔
2317
            setAccessTime(node, now);
1✔
2318
            setWriteTime(node, now);
1✔
2319
          }
2320
        }
2321
        prior = data.putIfAbsent(node.getKeyReference(), node);
1✔
2322
        if (prior == null) {
1✔
2323
          afterWrite(new AddTask(node, newWeight));
1✔
2324
          return null;
1✔
2325
        } else if (onlyIfAbsent) {
1✔
2326
          // An optimistic fast path to avoid unnecessary locking
2327
          V currentValue = prior.getValue();
1✔
2328
          if ((currentValue != null) && !hasExpired(prior, now)) {
1✔
2329
            if (!isComputingAsync(currentValue)) {
1✔
2330
              tryExpireAfterRead(prior, key, currentValue, expiry(), now);
1✔
2331
              setAccessTime(prior, now);
1✔
2332
            }
2333
            afterRead(prior, now, /* recordHit= */ false);
1✔
2334
            return currentValue;
1✔
2335
          }
2336
        }
1✔
2337
      } else if (onlyIfAbsent) {
1✔
2338
        // An optimistic fast path to avoid unnecessary locking
2339
        V currentValue = prior.getValue();
1✔
2340
        if ((currentValue != null) && !hasExpired(prior, now)) {
1✔
2341
          if (!isComputingAsync(currentValue)) {
1✔
2342
            tryExpireAfterRead(prior, key, currentValue, expiry(), now);
1✔
2343
            setAccessTime(prior, now);
1✔
2344
          }
2345
          afterRead(prior, now, /* recordHit= */ false);
1✔
2346
          return currentValue;
1✔
2347
        }
2348
      }
2349

2350
      // A read may race with the entry's removal, so that after the entry is acquired it may no
2351
      // longer be usable. A retry will reread from the map and either find an absent mapping, a
2352
      // new entry, or a stale entry.
2353
      if (!prior.isAlive()) {
1✔
2354
        // A reread of the stale entry may occur if the state transition occurred but the map
2355
        // removal was delayed by a context switch, so that this thread spin waits until resolved.
2356
        if ((attempts & MAX_PUT_SPIN_WAIT_ATTEMPTS) != 0) {
1✔
2357
          Thread.onSpinWait();
1✔
2358
          continue;
1✔
2359
        }
2360

2361
        // If the spin wait attempts are exhausted then fallback to a map computation in order to
2362
        // deschedule this thread until the entry's removal completes. If the key was modified
2363
        // while in the map so that its equals or hashCode changed then the contents may be
2364
        // corrupted, where the cache holds an evicted (dead) entry that could not be removed.
2365
        // That is a violation of the Map contract, so we check that the mapping is in the "alive"
2366
        // state while in the computation.
2367
        data.computeIfPresent(lookupKey, (k, n) -> {
1✔
2368
          requireIsAlive(key, n);
×
2369
          return n;
×
2370
        });
2371
        continue;
1✔
2372
      }
2373

2374
      V oldValue;
2375
      long varTime;
2376
      int oldWeight;
2377
      @Var boolean expired = false;
1✔
2378
      @Var boolean mayUpdate = true;
1✔
2379
      @Var boolean exceedsTolerance = false;
1✔
2380
      synchronized (prior) {
1✔
2381
        if (!prior.isAlive()) {
1✔
2382
          continue;
1✔
2383
        }
2384
        oldValue = prior.getValue();
1✔
2385
        oldWeight = prior.getWeight();
1✔
2386
        if (oldValue == null) {
1✔
2387
          varTime = expireAfterCreate(key, value, expiry, now);
1✔
2388
          notifyEviction(key, null, RemovalCause.COLLECTED);
1✔
2389
        } else if (hasExpired(prior, now)) {
1✔
2390
          expired = true;
1✔
2391
          varTime = expireAfterCreate(key, value, expiry, now);
1✔
2392
          notifyEviction(key, oldValue, RemovalCause.EXPIRED);
1✔
2393
        } else if (onlyIfAbsent) {
1✔
2394
          mayUpdate = false;
1✔
2395
          varTime = expireAfterRead(prior, key, value, expiry, now);
1✔
2396
        } else {
2397
          varTime = expireAfterUpdate(prior, key, value, expiry, now);
1✔
2398
        }
2399

2400
        if (mayUpdate) {
1✔
2401
          exceedsTolerance =
1✔
2402
              (expiresAfterWrite() && (now - prior.getWriteTime()) > EXPIRE_WRITE_TOLERANCE)
1✔
2403
              || (expiresVariable()
1✔
2404
                  && Math.abs(varTime - prior.getVariableTime()) > EXPIRE_WRITE_TOLERANCE)
1✔
2405
              || (refreshAfterWrite() && refreshes().containsKey(prior.getKeyReference()));
1✔
2406
          if (expired || exceedsTolerance) {
1✔
2407
            setWriteTime(prior, isComputingAsync(value) ? (now + ASYNC_EXPIRY) : now);
1✔
2408
          }
2409

2410
          prior.setValue(value, valueReferenceQueue());
1✔
2411
          prior.setWeight(newWeight);
1✔
2412

2413
          discardRefresh(prior.getKeyReference());
1✔
2414
        }
2415

2416
        setVariableTime(prior, varTime);
1✔
2417
        setAccessTime(prior, now);
1✔
2418
      }
1✔
2419

2420
      if (expired) {
1✔
2421
        notifyRemoval(key, oldValue, RemovalCause.EXPIRED);
1✔
2422
      } else if (oldValue == null) {
1✔
2423
        notifyRemoval(key, /* value= */ null, RemovalCause.COLLECTED);
1✔
2424
      } else if (mayUpdate) {
1✔
2425
        notifyOnReplace(key, oldValue, value);
1✔
2426
      }
2427

2428
      int weightedDifference = mayUpdate ? (newWeight - oldWeight) : 0;
1✔
2429
      if ((oldValue == null) || (weightedDifference != 0) || expired) {
1✔
2430
        afterWrite(new UpdateTask(prior, weightedDifference));
1✔
2431
      } else if (!onlyIfAbsent && exceedsTolerance) {
1✔
2432
        afterWrite(new UpdateTask(prior, weightedDifference));
1✔
2433
      } else {
2434
        afterRead(prior, now, /* recordHit= */ false);
1✔
2435
      }
2436

2437
      return expired ? null : oldValue;
1✔
2438
    }
2439
  }
2440

2441
  @Override
2442
  public @Nullable V remove(Object key) {
2443
    @SuppressWarnings("unchecked")
2444
    var castKey = (K) key;
1✔
2445
    @SuppressWarnings({"rawtypes", "unchecked"})
2446
    Node<K, V>[] node = new Node[1];
1✔
2447
    @SuppressWarnings({"unchecked", "Varifier"})
2448
    @Nullable V[] oldValue = (V[]) new Object[1];
1✔
2449
    RemovalCause[] cause = new RemovalCause[1];
1✔
2450
    Object lookupKey = nodeFactory.newLookupKey(key);
1✔
2451

2452
    data.computeIfPresent(lookupKey, (k, n) -> {
1✔
2453
      synchronized (n) {
1✔
2454
        requireIsAlive(key, n);
1✔
2455
        oldValue[0] = n.getValue();
1✔
2456
        if (oldValue[0] == null) {
1✔
2457
          cause[0] = RemovalCause.COLLECTED;
1✔
2458
        } else if (hasExpired(n, expirationTicker().read())) {
1✔
2459
          cause[0] = RemovalCause.EXPIRED;
1✔
2460
        } else {
2461
          cause[0] = RemovalCause.EXPLICIT;
1✔
2462
        }
2463
        if (cause[0].wasEvicted()) {
1✔
2464
          notifyEviction(castKey, oldValue[0], cause[0]);
1✔
2465
        }
2466
        discardRefresh(lookupKey);
1✔
2467
        node[0] = n;
1✔
2468
        n.retire();
1✔
2469
      }
1✔
2470
      return null;
1✔
2471
    });
2472

2473
    if (cause[0] != null) {
1✔
2474
      afterWrite(new RemovalTask(node[0]));
1✔
2475
      notifyRemoval(castKey, oldValue[0], cause[0]);
1✔
2476
    }
2477
    return (cause[0] == RemovalCause.EXPLICIT) ? oldValue[0] : null;
1✔
2478
  }
2479

2480
  @Override
2481
  public boolean remove(Object key, Object value) {
2482
    requireNonNull(key);
1✔
2483
    if (value == null) {
1✔
2484
      return false;
1✔
2485
    }
2486

2487
    @SuppressWarnings({"rawtypes", "unchecked"})
2488
    Node<K, V>[] removed = new Node[1];
1✔
2489
    @SuppressWarnings({"unchecked", "Varifier"})
2490
    @Nullable K[] oldKey = (K[]) new Object[1];
1✔
2491
    @SuppressWarnings({"unchecked", "Varifier"})
2492
    @Nullable V[] oldValue = (V[]) new Object[1];
1✔
2493
    RemovalCause[] cause = new RemovalCause[1];
1✔
2494
    Object lookupKey = nodeFactory.newLookupKey(key);
1✔
2495

2496
    data.computeIfPresent(lookupKey, (kR, node) -> {
1✔
2497
      synchronized (node) {
1✔
2498
        requireIsAlive(key, node);
1✔
2499
        oldKey[0] = node.getKey();
1✔
2500
        oldValue[0] = node.getValue();
1✔
2501
        if ((oldKey[0] == null) || (oldValue[0] == null)) {
1✔
2502
          cause[0] = RemovalCause.COLLECTED;
1✔
2503
        } else if (hasExpired(node, expirationTicker().read())) {
1✔
2504
          cause[0] = RemovalCause.EXPIRED;
1✔
2505
        } else if (node.containsValue(value)) {
1✔
2506
          cause[0] = RemovalCause.EXPLICIT;
1✔
2507
        } else {
2508
          return node;
1✔
2509
        }
2510
        if (cause[0].wasEvicted()) {
1✔
2511
          notifyEviction(oldKey[0], oldValue[0], cause[0]);
1✔
2512
        }
2513
        discardRefresh(lookupKey);
1✔
2514
        removed[0] = node;
1✔
2515
        node.retire();
1✔
2516
        return null;
1✔
2517
      }
2518
    });
2519

2520
    if (removed[0] == null) {
1✔
2521
      return false;
1✔
2522
    }
2523
    afterWrite(new RemovalTask(removed[0]));
1✔
2524
    notifyRemoval(oldKey[0], oldValue[0], cause[0]);
1✔
2525

2526
    return (cause[0] == RemovalCause.EXPLICIT);
1✔
2527
  }
2528

2529
  @Override
2530
  public @Nullable V replace(K key, V value) {
2531
    requireNonNull(key);
1✔
2532
    requireNonNull(value);
1✔
2533

2534
    long[] now = new long[1];
1✔
2535
    var oldWeight = new int[1];
1✔
2536
    @SuppressWarnings({"unchecked", "Varifier"})
2537
    @Nullable K[] nodeKey = (K[]) new Object[1];
1✔
2538
    @SuppressWarnings({"unchecked", "Varifier"})
2539
    @Nullable V[] oldValue = (V[]) new Object[1];
1✔
2540
    int weight = weigher.weigh(key, value);
1✔
2541
    Node<K, V> node = data.computeIfPresent(nodeFactory.newLookupKey(key), (k, n) -> {
1✔
2542
      synchronized (n) {
1✔
2543
        requireIsAlive(key, n);
1✔
2544
        nodeKey[0] = n.getKey();
1✔
2545
        oldValue[0] = n.getValue();
1✔
2546
        oldWeight[0] = n.getWeight();
1✔
2547
        if ((nodeKey[0] == null) || (oldValue[0] == null)
1✔
2548
            || hasExpired(n, now[0] = expirationTicker().read())) {
1✔
2549
          oldValue[0] = null;
1✔
2550
          return n;
1✔
2551
        }
2552

2553
        long varTime = expireAfterUpdate(n, key, value, expiry(), now[0]);
1✔
2554
        n.setValue(value, valueReferenceQueue());
1✔
2555
        n.setWeight(weight);
1✔
2556

2557
        setVariableTime(n, varTime);
1✔
2558

2559
        if (isComputingAsync(value)) {
1✔
2560
          long expirationTime = now[0] + ASYNC_EXPIRY;
1✔
2561
          setAccessTime(n, expirationTime);
1✔
2562
          setWriteTime(n, expirationTime);
1✔
2563
        } else {
1✔
2564
          setAccessTime(n, now[0]);
1✔
2565
          setWriteTime(n, now[0]);
1✔
2566
        }
2567
        discardRefresh(k);
1✔
2568
        return n;
1✔
2569
      }
2570
    });
2571

2572
    if ((nodeKey[0] == null) || (oldValue[0] == null)) {
1✔
2573
      return null;
1✔
2574
    }
2575

2576
    int weightedDifference = (weight - oldWeight[0]);
1✔
2577
    if (expiresAfterWrite() || (weightedDifference != 0)) {
1✔
2578
      afterWrite(new UpdateTask(node, weightedDifference));
1✔
2579
    } else {
2580
      afterRead(node, now[0], /* recordHit= */ false);
1✔
2581
    }
2582

2583
    notifyOnReplace(nodeKey[0], oldValue[0], value);
1✔
2584
    return oldValue[0];
1✔
2585
  }
2586

2587
  @Override
2588
  public boolean replace(K key, V oldValue, V newValue) {
2589
    return replace(key, oldValue, newValue, /* shouldDiscardRefresh= */ true);
1✔
2590
  }
2591

2592
  @Override
2593
  public boolean replace(K key, V oldValue, V newValue, boolean shouldDiscardRefresh) {
2594
    requireNonNull(key);
1✔
2595
    requireNonNull(oldValue);
1✔
2596
    requireNonNull(newValue);
1✔
2597

2598
    int weight = weigher.weigh(key, newValue);
1✔
2599
    @SuppressWarnings({"unchecked", "Varifier"})
2600
    @Nullable K[] nodeKey = (K[]) new Object[1];
1✔
2601
    @SuppressWarnings({"unchecked", "Varifier"})
2602
    @Nullable V[] prevValue = (V[]) new Object[1];
1✔
2603
    int[] oldWeight = new int[1];
1✔
2604
    long[] now = new long[1];
1✔
2605
    Node<K, V> node = data.computeIfPresent(nodeFactory.newLookupKey(key), (k, n) -> {
1✔
2606
      synchronized (n) {
1✔
2607
        requireIsAlive(key, n);
1✔
2608
        nodeKey[0] = n.getKey();
1✔
2609
        prevValue[0] = n.getValue();
1✔
2610
        oldWeight[0] = n.getWeight();
1✔
2611
        if ((nodeKey[0] == null) || (prevValue[0] == null) || !n.containsValue(oldValue)
1✔
2612
            || hasExpired(n, now[0] = expirationTicker().read())) {
1✔
2613
          prevValue[0] = null;
1✔
2614
          return n;
1✔
2615
        }
2616

2617
        long varTime = expireAfterUpdate(n, key, newValue, expiry(), now[0]);
1✔
2618
        n.setValue(newValue, valueReferenceQueue());
1✔
2619
        n.setWeight(weight);
1✔
2620

2621
        setVariableTime(n, varTime);
1✔
2622
        setAccessTime(n, now[0]);
1✔
2623
        setWriteTime(n, now[0]);
1✔
2624

2625
        if (shouldDiscardRefresh) {
1✔
2626
          discardRefresh(k);
1✔
2627
        }
2628
      }
1✔
2629
      return n;
1✔
2630
    });
2631

2632
    if ((nodeKey[0] == null) || (prevValue[0] == null)) {
1✔
2633
      return false;
1✔
2634
    }
2635

2636
    int weightedDifference = (weight - oldWeight[0]);
1✔
2637
    if (expiresAfterWrite() || (weightedDifference != 0)) {
1✔
2638
      afterWrite(new UpdateTask(node, weightedDifference));
1✔
2639
    } else {
2640
      afterRead(node, now[0], /* recordHit= */ false);
1✔
2641
    }
2642

2643
    notifyOnReplace(nodeKey[0], prevValue[0], newValue);
1✔
2644
    return true;
1✔
2645
  }
2646

2647
  @Override
2648
  public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
2649
    requireNonNull(function);
1✔
2650

2651
    BiFunction<K, V, V> remappingFunction = (key, oldValue) ->
1✔
2652
        requireNonNull(function.apply(key, oldValue));
1✔
2653
    for (K key : keySet()) {
1✔
2654
      long[] now = { expirationTicker().read() };
1✔
2655
      Object lookupKey = nodeFactory.newLookupKey(key);
1✔
2656
      remap(key, lookupKey, remappingFunction, expiry(), now, /* computeIfAbsent= */ false);
1✔
2657
    }
1✔
2658
  }
1✔
2659

2660
  @Override
2661
  public @Nullable V computeIfAbsent(K key, @Var Function<? super K, ? extends V> mappingFunction,
2662
      boolean recordStats, boolean recordLoad) {
2663
    requireNonNull(key);
1✔
2664
    requireNonNull(mappingFunction);
1✔
2665
    long now = expirationTicker().read();
1✔
2666

2667
    // An optimistic fast path to avoid unnecessary locking
2668
    Node<K, V> node = data.get(nodeFactory.newLookupKey(key));
1✔
2669
    if (node != null) {
1✔
2670
      V value = node.getValue();
1✔
2671
      if ((value != null) && !hasExpired(node, now)) {
1✔
2672
        if (!isComputingAsync(value)) {
1✔
2673
          tryExpireAfterRead(node, key, value, expiry(), now);
1✔
2674
          setAccessTime(node, now);
1✔
2675
        }
2676
        var refreshed = afterRead(node, now, /* recordHit= */ recordStats);
1✔
2677
        return (refreshed == null) ? value : refreshed;
1✔
2678
      }
2679
    }
2680
    if (recordStats) {
1✔
2681
      mappingFunction = statsAware(mappingFunction, recordLoad);
1✔
2682
    }
2683
    Object keyRef = nodeFactory.newReferenceKey(key, keyReferenceQueue());
1✔
2684
    return doComputeIfAbsent(key, keyRef, mappingFunction, new long[] { now }, recordStats);
1✔
2685
  }
2686

2687
  /** Returns the current value from a computeIfAbsent invocation. */
2688
  @Nullable V doComputeIfAbsent(K key, Object keyRef,
2689
      Function<? super K, ? extends @Nullable V> mappingFunction, long[/* 1 */] now,
2690
      boolean recordStats) {
2691
    @SuppressWarnings({"unchecked", "Varifier"})
2692
    @Nullable V[] oldValue = (V[]) new Object[1];
1✔
2693
    @SuppressWarnings({"unchecked", "Varifier"})
2694
    @Nullable V[] newValue = (V[]) new Object[1];
1✔
2695
    @SuppressWarnings({"unchecked", "Varifier"})
2696
    @Nullable K[] nodeKey = (K[]) new Object[1];
1✔
2697
    @SuppressWarnings({"rawtypes", "unchecked"})
2698
    Node<K, V>[] removed = new Node[1];
1✔
2699

2700
    int[] weight = new int[2]; // old, new
1✔
2701
    RemovalCause[] cause = new RemovalCause[1];
1✔
2702
    Node<K, V> node = data.compute(keyRef, (k, n) -> {
1✔
2703
      if (n == null) {
1✔
2704
        newValue[0] = mappingFunction.apply(key);
1✔
2705
        if (newValue[0] == null) {
1✔
2706
          return null;
1✔
2707
        }
2708
        now[0] = expirationTicker().read();
1✔
2709
        weight[1] = weigher.weigh(key, newValue[0]);
1✔
2710
        var created = nodeFactory.newNode(key, keyReferenceQueue(),
1✔
2711
            newValue[0], valueReferenceQueue(), weight[1], now[0]);
1✔
2712
        setVariableTime(created, expireAfterCreate(key, newValue[0], expiry(), now[0]));
1✔
2713
        if (isComputingAsync(newValue[0])) {
1✔
2714
          long expirationTime = now[0] + ASYNC_EXPIRY;
1✔
2715
          setAccessTime(created, expirationTime);
1✔
2716
          setWriteTime(created, expirationTime);
1✔
2717
        } else {
1✔
2718
          setAccessTime(created, now[0]);
1✔
2719
          setWriteTime(created, now[0]);
1✔
2720
        }
2721
        return created;
1✔
2722
      }
2723

2724
      synchronized (n) {
1✔
2725
        requireIsAlive(key, n);
1✔
2726
        nodeKey[0] = n.getKey();
1✔
2727
        weight[0] = n.getWeight();
1✔
2728
        oldValue[0] = n.getValue();
1✔
2729
        if ((nodeKey[0] == null) || (oldValue[0] == null)) {
1✔
2730
          cause[0] = RemovalCause.COLLECTED;
1✔
2731
        } else if (hasExpired(n, now[0])) {
1✔
2732
          cause[0] = RemovalCause.EXPIRED;
1✔
2733
        } else {
2734
          return n;
1✔
2735
        }
2736

2737
        if (cause[0].wasEvicted()) {
1✔
2738
          notifyEviction(nodeKey[0], oldValue[0], cause[0]);
1✔
2739
        }
2740
        newValue[0] = mappingFunction.apply(key);
1✔
2741
        if (newValue[0] == null) {
1✔
2742
          removed[0] = n;
1✔
2743
          n.retire();
1✔
2744
          return null;
1✔
2745
        }
2746
        now[0] = expirationTicker().read();
1✔
2747
        weight[1] = weigher.weigh(key, newValue[0]);
1✔
2748
        long varTime = expireAfterCreate(key, newValue[0], expiry(), now[0]);
1✔
2749

2750
        n.setValue(newValue[0], valueReferenceQueue());
1✔
2751
        n.setWeight(weight[1]);
1✔
2752

2753
        setVariableTime(n, varTime);
1✔
2754
        if (isComputingAsync(newValue[0])) {
1✔
2755
          long expirationTime = now[0] + ASYNC_EXPIRY;
1✔
2756
          setAccessTime(n, expirationTime);
1✔
2757
          setWriteTime(n, expirationTime);
1✔
2758
        } else {
1✔
2759
          setAccessTime(n, now[0]);
1✔
2760
          setWriteTime(n, now[0]);
1✔
2761
        }
2762
        discardRefresh(k);
1✔
2763
        return n;
1✔
2764
      }
2765
    });
2766

2767
    if (cause[0] != null) {
1✔
2768
      if (cause[0].wasEvicted()) {
1✔
2769
        statsCounter().recordEviction(weight[0], cause[0]);
1✔
2770
      }
2771
      notifyRemoval(nodeKey[0], oldValue[0], cause[0]);
1✔
2772
    }
2773
    if (node == null) {
1✔
2774
      if (removed[0] != null) {
1✔
2775
        afterWrite(new RemovalTask(removed[0]));
1✔
2776
      }
2777
      return null;
1✔
2778
    }
2779
    if (newValue[0] == null) {
1✔
2780
      if (!isComputingAsync(oldValue[0])) {
1✔
2781
        tryExpireAfterRead(node, key, oldValue[0], expiry(), now[0]);
1✔
2782
        setAccessTime(node, now[0]);
1✔
2783
      }
2784

2785
      afterRead(node, now[0], /* recordHit= */ recordStats);
1✔
2786
      return oldValue[0];
1✔
2787
    }
2788
    if ((oldValue[0] == null) && (cause[0] == null)) {
1✔
2789
      afterWrite(new AddTask(node, weight[1]));
1✔
2790
    } else {
2791
      int weightedDifference = (weight[1] - weight[0]);
1✔
2792
      afterWrite(new UpdateTask(node, weightedDifference));
1✔
2793
    }
2794

2795
    return newValue[0];
1✔
2796
  }
2797

2798
  @Override
2799
  public @Nullable V computeIfPresent(K key,
2800
      BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
2801
    requireNonNull(key);
1✔
2802
    requireNonNull(remappingFunction);
1✔
2803

2804
    // An optimistic fast path to avoid unnecessary locking
2805
    Object lookupKey = nodeFactory.newLookupKey(key);
1✔
2806
    @Nullable Node<K, V> node = data.get(lookupKey);
1✔
2807
    long now;
2808
    if (node == null) {
1✔
2809
      return null;
1✔
2810
    } else if ((node.getValue() == null) || hasExpired(node, (now = expirationTicker().read()))) {
1✔
2811
      scheduleDrainBuffers();
1✔
2812
      return null;
1✔
2813
    }
2814

2815
    BiFunction<? super K, ? super V, ? extends V> statsAwareRemappingFunction =
1✔
2816
        statsAware(remappingFunction, /* recordLoad= */ true, /* recordLoadFailure= */ true);
1✔
2817
    return remap(key, lookupKey, statsAwareRemappingFunction,
1✔
2818
        expiry(), new long[] { now }, /* computeIfAbsent= */ false);
1✔
2819
  }
2820

2821
  @Override
2822
  @SuppressWarnings("NullAway")
2823
  public @Nullable V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction,
2824
      @Nullable Expiry<? super K, ? super V> expiry, boolean recordLoad,
2825
      boolean recordLoadFailure) {
2826
    requireNonNull(key);
1✔
2827
    requireNonNull(remappingFunction);
1✔
2828

2829
    long[] now = { expirationTicker().read() };
1✔
2830
    Object keyRef = nodeFactory.newReferenceKey(key, keyReferenceQueue());
1✔
2831
    BiFunction<? super K, ? super V, ? extends V> statsAwareRemappingFunction =
1✔
2832
        statsAware(remappingFunction, recordLoad, recordLoadFailure);
1✔
2833
    return remap(key, keyRef, statsAwareRemappingFunction,
1✔
2834
        expiry, now, /* computeIfAbsent= */ true);
2835
  }
2836

2837
  @Override
2838
  public @Nullable V merge(K key, V value,
2839
      BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
2840
    requireNonNull(key);
1✔
2841
    requireNonNull(value);
1✔
2842
    requireNonNull(remappingFunction);
1✔
2843

2844
    long[] now = { expirationTicker().read() };
1✔
2845
    Object keyRef = nodeFactory.newReferenceKey(key, keyReferenceQueue());
1✔
2846
    BiFunction<? super K, ? super V, ? extends V> mergeFunction = (k, oldValue) ->
1✔
2847
        (oldValue == null) ? value : statsAware(remappingFunction).apply(oldValue, value);
1✔
2848
    return remap(key, keyRef, mergeFunction, expiry(), now, /* computeIfAbsent= */ true);
1✔
2849
  }
2850

2851
  /**
2852
   * Attempts to compute a mapping for the specified key and its current mapped value (or
2853
   * {@code null} if there is no current mapping).
2854
   * <p>
2855
   * An entry that has expired or been reference collected is evicted and the computation continues
2856
   * as if the entry had not been present. This method does not pre-screen and does not wrap the
2857
   * remappingFunction to be statistics aware.
2858
   *
2859
   * @param key key with which the specified value is to be associated
2860
   * @param keyRef the key to associate with or a lookup only key if not {@code computeIfAbsent}
2861
   * @param remappingFunction the function to compute a value
2862
   * @param expiry the calculator for the expiration time
2863
   * @param now the current time, according to the ticker
2864
   * @param computeIfAbsent if an absent entry can be computed
2865
   * @return the new value associated with the specified key, or null if none
2866
   */
2867
  @SuppressWarnings("PMD.EmptyControlStatement")
2868
  @Nullable V remap(K key, Object keyRef,
2869
      BiFunction<? super K, ? super V, ? extends @Nullable V> remappingFunction,
2870
      Expiry<? super K, ? super V> expiry, long[/* 1 */] now, boolean computeIfAbsent) {
2871
    @SuppressWarnings({"unchecked", "Varifier"})
2872
    @Nullable K[] nodeKey = (K[]) new Object[1];
1✔
2873
    @SuppressWarnings({"unchecked", "Varifier"})
2874
    @Nullable V[] oldValue = (V[]) new Object[1];
1✔
2875
    @SuppressWarnings({"unchecked", "Varifier"})
2876
    @Nullable V[] newValue = (V[]) new Object[1];
1✔
2877
    @SuppressWarnings({"rawtypes", "unchecked"})
2878
    Node<K, V>[] removed = new Node[1];
1✔
2879

2880
    var weight = new int[2]; // old, new
1✔
2881
    var cause = new RemovalCause[1];
1✔
2882

2883
    Node<K, V> node = data.compute(keyRef, (kr, n) -> {
1✔
2884
      if (n == null) {
1✔
2885
        if (!computeIfAbsent) {
1✔
2886
          return null;
1✔
2887
        }
2888
        newValue[0] = remappingFunction.apply(key, null);
1✔
2889
        if (newValue[0] == null) {
1✔
2890
          return null;
1✔
2891
        }
2892
        now[0] = expirationTicker().read();
1✔
2893
        weight[1] = weigher.weigh(key, newValue[0]);
1✔
2894
        long varTime = expireAfterCreate(key, newValue[0], expiry, now[0]);
1✔
2895
        var created = nodeFactory.newNode(keyRef, newValue[0],
1✔
2896
            valueReferenceQueue(), weight[1], now[0]);
1✔
2897
        setVariableTime(created, varTime);
1✔
2898
        if (isComputingAsync(newValue[0])) {
1✔
2899
          long expirationTime = now[0] + ASYNC_EXPIRY;
1✔
2900
          setAccessTime(created, expirationTime);
1✔
2901
          setWriteTime(created, expirationTime);
1✔
2902
        } else {
1✔
2903
          setAccessTime(created, now[0]);
1✔
2904
          setWriteTime(created, now[0]);
1✔
2905
        }
2906
        discardRefresh(key);
1✔
2907
        return created;
1✔
2908
      }
2909

2910
      synchronized (n) {
1✔
2911
        requireIsAlive(key, n);
1✔
2912
        nodeKey[0] = n.getKey();
1✔
2913
        oldValue[0] = n.getValue();
1✔
2914
        if ((nodeKey[0] == null) || (oldValue[0] == null)) {
1✔
2915
          cause[0] = RemovalCause.COLLECTED;
1✔
2916
        } else if (hasExpired(n, expirationTicker().read())) {
1✔
2917
          cause[0] = RemovalCause.EXPIRED;
1✔
2918
        }
2919
        if (cause[0] != null) {
1✔
2920
          notifyEviction(nodeKey[0], oldValue[0], cause[0]);
1✔
2921
          if (!computeIfAbsent) {
1✔
2922
            removed[0] = n;
1✔
2923
            n.retire();
1✔
2924
            return null;
1✔
2925
          }
2926
        }
2927

2928
        newValue[0] = remappingFunction.apply(nodeKey[0],
1✔
2929
            (cause[0] == null) ? oldValue[0] : null);
1✔
2930
        if (newValue[0] == null) {
1✔
2931
          if (cause[0] == null) {
1✔
2932
            cause[0] = RemovalCause.EXPLICIT;
1✔
2933
            discardRefresh(kr);
1✔
2934
          }
2935
          removed[0] = n;
1✔
2936
          n.retire();
1✔
2937
          return null;
1✔
2938
        }
2939

2940
        long varTime;
2941
        weight[0] = n.getWeight();
1✔
2942
        weight[1] = weigher.weigh(key, newValue[0]);
1✔
2943
        now[0] = expirationTicker().read();
1✔
2944
        if (cause[0] == null) {
1✔
2945
          if (newValue[0] != oldValue[0]) {
1✔
2946
            cause[0] = RemovalCause.REPLACED;
1✔
2947
          }
2948
          varTime = expireAfterUpdate(n, key, newValue[0], expiry, now[0]);
1✔
2949
        } else {
2950
          varTime = expireAfterCreate(key, newValue[0], expiry, now[0]);
1✔
2951
        }
2952

2953
        n.setValue(newValue[0], valueReferenceQueue());
1✔
2954
        n.setWeight(weight[1]);
1✔
2955

2956
        setVariableTime(n, varTime);
1✔
2957
        if (isComputingAsync(newValue[0])) {
1✔
2958
          long expirationTime = now[0] + ASYNC_EXPIRY;
1✔
2959
          setAccessTime(n, expirationTime);
1✔
2960
          setWriteTime(n, expirationTime);
1✔
2961
        } else {
1✔
2962
          setAccessTime(n, now[0]);
1✔
2963
          setWriteTime(n, now[0]);
1✔
2964
        }
2965
        discardRefresh(kr);
1✔
2966
        return n;
1✔
2967
      }
2968
    });
2969

2970
    if (cause[0] != null) {
1✔
2971
      if (cause[0] == RemovalCause.REPLACED) {
1✔
2972
        requireNonNull(newValue[0]);
1✔
2973
        notifyOnReplace(key, oldValue[0], newValue[0]);
1✔
2974
      } else {
2975
        if (cause[0].wasEvicted()) {
1✔
2976
          statsCounter().recordEviction(weight[0], cause[0]);
1✔
2977
        }
2978
        notifyRemoval(nodeKey[0], oldValue[0], cause[0]);
1✔
2979
      }
2980
    }
2981

2982
    if (removed[0] != null) {
1✔
2983
      afterWrite(new RemovalTask(removed[0]));
1✔
2984
    } else if (node == null) {
1✔
2985
      // absent and not computable
2986
    } else if ((oldValue[0] == null) && (cause[0] == null)) {
1✔
2987
      afterWrite(new AddTask(node, weight[1]));
1✔
2988
    } else {
2989
      int weightedDifference = weight[1] - weight[0];
1✔
2990
      if (expiresAfterWrite() || (weightedDifference != 0)) {
1✔
2991
        afterWrite(new UpdateTask(node, weightedDifference));
1✔
2992
      } else {
2993
        afterRead(node, now[0], /* recordHit= */ false);
1✔
2994
        if ((cause[0] != null) && cause[0].wasEvicted()) {
1✔
2995
          scheduleDrainBuffers();
1✔
2996
        }
2997
      }
2998
    }
2999

3000
    return newValue[0];
1✔
3001
  }
3002

3003
  @Override
3004
  public void forEach(BiConsumer<? super K, ? super V> action) {
3005
    requireNonNull(action);
1✔
3006

3007
    for (var iterator = new EntryIterator<>(this); iterator.hasNext();) {
1✔
3008
      action.accept(iterator.key, iterator.value);
1✔
3009
      iterator.advance();
1✔
3010
    }
3011
  }
1✔
3012

3013
  @Override
3014
  public Set<K> keySet() {
3015
    Set<K> ks = keySet;
1✔
3016
    return (ks == null) ? (keySet = new KeySetView<>(this)) : ks;
1✔
3017
  }
3018

3019
  @Override
3020
  public Collection<V> values() {
3021
    Collection<V> vs = values;
1✔
3022
    return (vs == null) ? (values = new ValuesView<>(this)) : vs;
1✔
3023
  }
3024

3025
  @Override
3026
  public Set<Entry<K, V>> entrySet() {
3027
    Set<Entry<K, V>> es = entrySet;
1✔
3028
    return (es == null) ? (entrySet = new EntrySetView<>(this)) : es;
1✔
3029
  }
3030

3031
  /**
3032
   * Object equality requires reflexive, symmetric, transitive, and consistency properties. Of
3033
   * these, symmetry and consistency require further clarification for how they are upheld.
3034
   * <p>
3035
   * The <i>consistency</i> property between invocations requires that the results are the same if
3036
   * there are no modifications to the information used. Therefore, usages should expect that this
3037
   * operation may return misleading results if either the maps or the data held by them is modified
3038
   * during the execution of this method. This characteristic allows for comparing the map sizes and
3039
   * assuming stable mappings, as done by {@link java.util.AbstractMap}-based maps.
3040
   * <p>
3041
   * The <i>symmetric</i> property requires that the result is the same for all implementations of
3042
   * {@link Map#equals(Object)}. That contract is defined in terms of the stable mappings provided
3043
   * by {@link #entrySet()}, meaning that the {@link #size()} optimization forces that the count is
3044
   * consistent with the mappings when used for an equality check.
3045
   * <p>
3046
   * The cache's {@link #size()} method may include entries that have expired or have been reference
3047
   * collected, but have not yet been removed from the backing map. An iteration over the map may
3048
   * trigger the removal of these dead entries when skipped over during traversal. To ensure
3049
   * consistency and symmetry, usages should call {@link #cleanUp()} before this method while no
3050
   * other concurrent operations are being performed on this cache. This is not done implicitly by
3051
   * {@link #size()} as many usages assume it to be instantaneous and lock-free.
3052
   */
3053
  @Override
3054
  public boolean equals(@Nullable Object o) {
3055
    if (o == this) {
1✔
3056
      return true;
1✔
3057
    } else if (!(o instanceof Map)) {
1✔
3058
      return false;
1✔
3059
    }
3060

3061
    var map = (Map<?, ?>) o;
1✔
3062
    if (size() != map.size()) {
1✔
3063
      return false;
1✔
3064
    }
3065

3066
    long now = expirationTicker().read();
1✔
3067
    for (var node : data.values()) {
1✔
3068
      K key = node.getKey();
1✔
3069
      V value = node.getValue();
1✔
3070
      if ((key == null) || (value == null)
1✔
3071
          || !node.isAlive() || hasExpired(node, now)) {
1✔
3072
        scheduleDrainBuffers();
1✔
3073
        return false;
1✔
3074
      } else {
3075
        var val = map.get(key);
1✔
3076
        if ((val == null) || ((val != value) && !val.equals(value))) {
1✔
3077
          return false;
1✔
3078
        }
3079
      }
3080
    }
1✔
3081
    return true;
1✔
3082
  }
3083

3084
  @Override
3085
  @SuppressWarnings("NullAway")
3086
  public int hashCode() {
3087
    @Var int hash = 0;
1✔
3088
    long now = expirationTicker().read();
1✔
3089
    for (var node : data.values()) {
1✔
3090
      K key = node.getKey();
1✔
3091
      V value = node.getValue();
1✔
3092
      if ((key == null) || (value == null)
1✔
3093
          || !node.isAlive() || hasExpired(node, now)) {
1✔
3094
        scheduleDrainBuffers();
1✔
3095
      } else {
3096
        hash += key.hashCode() ^ value.hashCode();
1✔
3097
      }
3098
    }
1✔
3099
    return hash;
1✔
3100
  }
3101

3102
  @Override
3103
  public String toString() {
3104
    var result = new StringBuilder().append('{');
1✔
3105
    long now = expirationTicker().read();
1✔
3106
    for (var node : data.values()) {
1✔
3107
      K key = node.getKey();
1✔
3108
      V value = node.getValue();
1✔
3109
      if ((key == null) || (value == null)
1✔
3110
          || !node.isAlive() || hasExpired(node, now)) {
1✔
3111
        scheduleDrainBuffers();
1✔
3112
      } else {
3113
        if (result.length() != 1) {
1✔
3114
          result.append(',').append(' ');
1✔
3115
        }
3116
        result.append((key == this) ? "(this Map)" : key);
1✔
3117
        result.append('=');
1✔
3118
        result.append((value == this) ? "(this Map)" : value);
1✔
3119
      }
3120
    }
1✔
3121
    return result.append('}').toString();
1✔
3122
  }
3123

3124
  /**
3125
   * Returns the computed result from the ordered traversal of the cache entries.
3126
   *
3127
   * @param hottest the coldest or hottest iteration order
3128
   * @param transformer a function that unwraps the value
3129
   * @param mappingFunction the mapping function to compute a value
3130
   * @return the computed value
3131
   */
3132
  @SuppressWarnings("GuardedByChecker")
3133
  <T> T evictionOrder(boolean hottest, Function<@Nullable V, @Nullable V> transformer,
3134
      Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
3135
    Comparator<Node<K, V>> comparator = Comparator.comparingInt(node -> {
1✔
3136
      K key = node.getKey();
1✔
3137
      return (key == null) ? 0 : frequencySketch().frequency(key);
1✔
3138
    });
3139
    Iterable<Node<K, V>> iterable;
3140
    if (hottest) {
1✔
3141
      iterable = () -> {
1✔
3142
        var secondary = PeekingIterator.comparing(
1✔
3143
            accessOrderProbationDeque().descendingIterator(),
1✔
3144
            accessOrderWindowDeque().descendingIterator(), comparator);
1✔
3145
        return PeekingIterator.concat(
1✔
3146
            accessOrderProtectedDeque().descendingIterator(), secondary);
1✔
3147
      };
3148
    } else {
3149
      iterable = () -> {
1✔
3150
        var primary = PeekingIterator.comparing(
1✔
3151
            accessOrderWindowDeque().iterator(), accessOrderProbationDeque().iterator(),
1✔
3152
            comparator.reversed());
1✔
3153
        return PeekingIterator.concat(primary, accessOrderProtectedDeque().iterator());
1✔
3154
      };
3155
    }
3156
    return snapshot(iterable, transformer, mappingFunction);
1✔
3157
  }
3158

3159
  /**
3160
   * Returns the computed result from the ordered traversal of the cache entries.
3161
   *
3162
   * @param oldest the youngest or oldest iteration order
3163
   * @param transformer a function that unwraps the value
3164
   * @param mappingFunction the mapping function to compute a value
3165
   * @return the computed value
3166
   */
3167
  @SuppressWarnings("GuardedByChecker")
3168
  <T> T expireAfterAccessOrder(boolean oldest, Function<@Nullable V, @Nullable V> transformer,
3169
      Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
3170
    Iterable<Node<K, V>> iterable;
3171
    if (evicts()) {
1✔
3172
      iterable = () -> {
1✔
3173
        @Var Comparator<Node<K, V>> comparator = Comparator.comparingLong(Node::getAccessTime);
1✔
3174
        PeekingIterator<Node<K, V>> first;
3175
        PeekingIterator<Node<K, V>> second;
3176
        PeekingIterator<Node<K, V>> third;
3177
        if (oldest) {
1✔
3178
          first = accessOrderWindowDeque().iterator();
1✔
3179
          second = accessOrderProbationDeque().iterator();
1✔
3180
          third = accessOrderProtectedDeque().iterator();
1✔
3181
        } else {
3182
          comparator = comparator.reversed();
1✔
3183
          first = accessOrderWindowDeque().descendingIterator();
1✔
3184
          second = accessOrderProbationDeque().descendingIterator();
1✔
3185
          third = accessOrderProtectedDeque().descendingIterator();
1✔
3186
        }
3187
        return PeekingIterator.comparing(
1✔
3188
            PeekingIterator.comparing(first, second, comparator), third, comparator);
1✔
3189
      };
3190
    } else {
3191
      iterable = oldest
1✔
3192
          ? accessOrderWindowDeque()
1✔
3193
          : accessOrderWindowDeque()::descendingIterator;
1✔
3194
    }
3195
    return snapshot(iterable, transformer, mappingFunction);
1✔
3196
  }
3197

3198
  /**
3199
   * Returns the computed result from the ordered traversal of the cache entries.
3200
   *
3201
   * @param iterable the supplier of the entries in the cache
3202
   * @param transformer a function that unwraps the value
3203
   * @param mappingFunction the mapping function to compute a value
3204
   * @return the computed value
3205
   */
3206
  @SuppressWarnings("NullAway")
3207
  <T> T snapshot(Iterable<Node<K, V>> iterable, Function<@Nullable V, @Nullable V> transformer,
3208
      Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
3209
    requireNonNull(mappingFunction);
1✔
3210
    requireNonNull(transformer);
1✔
3211
    requireNonNull(iterable);
1✔
3212

3213
    evictionLock.lock();
1✔
3214
    try {
3215
      maintenance(/* ignored */ null);
1✔
3216

3217
      // Obtain the iterator as late as possible for modification count checking
3218
      try (var stream = StreamSupport.stream(Spliterators.spliteratorUnknownSize(
1✔
3219
           iterable.iterator(), DISTINCT | ORDERED | NONNULL | IMMUTABLE), /* parallel= */ false)) {
1✔
3220
        return mappingFunction.apply(stream
1✔
3221
            .map(node -> nodeToCacheEntry(node, transformer))
1✔
3222
            .filter(Objects::nonNull));
1✔
3223
      }
3224
    } finally {
3225
      evictionLock.unlock();
1✔
3226
      rescheduleCleanUpIfIncomplete();
1✔
3227
    }
3228
  }
3229

3230
  /** Returns an entry for the given node if it can be used externally, else null. */
3231
  @Nullable CacheEntry<K, V> nodeToCacheEntry(
3232
      Node<K, V> node, Function<@Nullable V, @Nullable V> transformer) {
3233
    V value = transformer.apply(node.getValue());
1✔
3234
    K key = node.getKey();
1✔
3235
    long now;
3236
    if ((key == null) || (value == null) || !node.isAlive()
1✔
3237
        || hasExpired(node, (now = expirationTicker().read()))) {
1✔
3238
      return null;
1✔
3239
    }
3240

3241
    @Var long expiresAfter = Long.MAX_VALUE;
1✔
3242
    if (expiresAfterAccess()) {
1✔
3243
      expiresAfter = Math.min(expiresAfter, now - node.getAccessTime() + expiresAfterAccessNanos());
1✔
3244
    }
3245
    if (expiresAfterWrite()) {
1✔
3246
      expiresAfter = Math.min(expiresAfter,
1✔
3247
          (now & ~1L) - (node.getWriteTime() & ~1L) + expiresAfterWriteNanos());
1✔
3248
    }
3249
    if (expiresVariable()) {
1✔
3250
      expiresAfter = node.getVariableTime() - now;
1✔
3251
    }
3252

3253
    long refreshableAt = refreshAfterWrite()
1✔
3254
        ? node.getWriteTime() + refreshAfterWriteNanos()
1✔
3255
        : now + Long.MAX_VALUE;
1✔
3256
    int weight = node.getPolicyWeight();
1✔
3257
    return SnapshotEntry.forEntry(key, value, now, weight, now + expiresAfter, refreshableAt);
1✔
3258
  }
3259

3260
  /** A function that produces an unmodifiable map up to the limit in stream order. */
3261
  static final class SizeLimiter<K, V> implements Function<Stream<CacheEntry<K, V>>, Map<K, V>> {
3262
    private final int expectedSize;
3263
    private final long limit;
3264

3265
    SizeLimiter(int expectedSize, long limit) {
1✔
3266
      requireArgument(limit >= 0);
1✔
3267
      this.expectedSize = expectedSize;
1✔
3268
      this.limit = limit;
1✔
3269
    }
1✔
3270

3271
    @Override
3272
    public Map<K, V> apply(Stream<CacheEntry<K, V>> stream) {
3273
      var map = new LinkedHashMap<K, V>(calculateHashMapCapacity(expectedSize));
1✔
3274
      stream.limit(limit).forEach(entry -> map.put(entry.getKey(), entry.getValue()));
1✔
3275
      return Collections.unmodifiableMap(map);
1✔
3276
    }
3277
  }
3278

3279
  /** A function that produces an unmodifiable map up to the weighted limit in stream order. */
3280
  static final class WeightLimiter<K, V> implements Function<Stream<CacheEntry<K, V>>, Map<K, V>> {
3281
    private final long weightLimit;
3282

3283
    private long weightedSize;
3284

3285
    WeightLimiter(long weightLimit) {
1✔
3286
      requireArgument(weightLimit >= 0);
1✔
3287
      this.weightLimit = weightLimit;
1✔
3288
    }
1✔
3289

3290
    @Override
3291
    public Map<K, V> apply(Stream<CacheEntry<K, V>> stream) {
3292
      var map = new LinkedHashMap<K, V>();
1✔
3293
      stream.takeWhile(entry -> {
1✔
3294
        weightedSize = Math.addExact(weightedSize, entry.weight());
1✔
3295
        return (weightedSize <= weightLimit);
1✔
3296
      }).forEach(entry -> map.put(entry.getKey(), entry.getValue()));
1✔
3297
      return Collections.unmodifiableMap(map);
1✔
3298
    }
3299
  }
3300

3301
  /** An adapter to safely externalize the keys. */
3302
  static final class KeySetView<K, V> extends AbstractSet<K> {
3303
    final BoundedLocalCache<K, V> cache;
3304

3305
    KeySetView(BoundedLocalCache<K, V> cache) {
1✔
3306
      this.cache = requireNonNull(cache);
1✔
3307
    }
1✔
3308

3309
    @Override
3310
    public int size() {
3311
      return cache.size();
1✔
3312
    }
3313

3314
    @Override
3315
    public void clear() {
3316
      cache.clear();
1✔
3317
    }
1✔
3318

3319
    @Override
3320
    @SuppressWarnings("SuspiciousMethodCalls")
3321
    public boolean contains(Object o) {
3322
      return cache.containsKey(o);
1✔
3323
    }
3324

3325
    @Override
3326
    public boolean removeAll(Collection<?> collection) {
3327
      requireNonNull(collection);
1✔
3328
      @Var boolean modified = false;
1✔
3329
      if ((collection instanceof Set<?>) && (collection.size() > size())) {
1✔
3330
        for (K key : this) {
1✔
3331
          if (collection.contains(key)) {
1✔
3332
            modified |= remove(key);
1✔
3333
          }
3334
        }
1✔
3335
      } else {
3336
        for (var item : collection) {
1✔
3337
          modified |= (item != null) && remove(item);
1✔
3338
        }
1✔
3339
      }
3340
      return modified;
1✔
3341
    }
3342

3343
    @Override
3344
    public boolean remove(Object o) {
3345
      return (cache.remove(o) != null);
1✔
3346
    }
3347

3348
    @Override
3349
    public boolean removeIf(Predicate<? super K> filter) {
3350
      requireNonNull(filter);
1✔
3351
      @Var boolean modified = false;
1✔
3352
      for (K key : this) {
1✔
3353
        if (filter.test(key) && remove(key)) {
1✔
3354
          modified = true;
1✔
3355
        }
3356
      }
1✔
3357
      return modified;
1✔
3358
    }
3359

3360
    @Override
3361
    public boolean retainAll(Collection<?> collection) {
3362
      requireNonNull(collection);
1✔
3363
      @Var boolean modified = false;
1✔
3364
      for (K key : this) {
1✔
3365
        if (!collection.contains(key) && remove(key)) {
1✔
3366
          modified = true;
1✔
3367
        }
3368
      }
1✔
3369
      return modified;
1✔
3370
    }
3371

3372
    @Override
3373
    public Iterator<K> iterator() {
3374
      return new KeyIterator<>(cache);
1✔
3375
    }
3376

3377
    @Override
3378
    public Spliterator<K> spliterator() {
3379
      return new KeySpliterator<>(cache);
1✔
3380
    }
3381
  }
3382

3383
  /** An adapter to safely externalize the key iterator. */
3384
  static final class KeyIterator<K, V> implements Iterator<K> {
3385
    final EntryIterator<K, V> iterator;
3386

3387
    KeyIterator(BoundedLocalCache<K, V> cache) {
1✔
3388
      this.iterator = new EntryIterator<>(cache);
1✔
3389
    }
1✔
3390

3391
    @Override
3392
    public boolean hasNext() {
3393
      return iterator.hasNext();
1✔
3394
    }
3395

3396
    @Override
3397
    public K next() {
3398
      return iterator.nextKey();
1✔
3399
    }
3400

3401
    @Override
3402
    public void remove() {
3403
      iterator.remove();
1✔
3404
    }
1✔
3405
  }
3406

3407
  /** An adapter to safely externalize the key spliterator. */
3408
  static final class KeySpliterator<K, V> implements Spliterator<K> {
3409
    final Spliterator<Node<K, V>> spliterator;
3410
    final BoundedLocalCache<K, V> cache;
3411

3412
    KeySpliterator(BoundedLocalCache<K, V> cache) {
3413
      this(cache, cache.data.values().spliterator());
1✔
3414
    }
1✔
3415

3416
    KeySpliterator(BoundedLocalCache<K, V> cache, Spliterator<Node<K, V>> spliterator) {
1✔
3417
      this.spliterator = requireNonNull(spliterator);
1✔
3418
      this.cache = requireNonNull(cache);
1✔
3419
    }
1✔
3420

3421
    @Override
3422
    public void forEachRemaining(Consumer<? super K> action) {
3423
      requireNonNull(action);
1✔
3424
      Consumer<Node<K, V>> consumer = node -> {
1✔
3425
        K key = node.getKey();
1✔
3426
        V value = node.getValue();
1✔
3427
        long now = cache.expirationTicker().read();
1✔
3428
        if ((key != null) && (value != null) && node.isAlive() && !cache.hasExpired(node, now)) {
1✔
3429
          action.accept(key);
1✔
3430
        }
3431
      };
1✔
3432
      spliterator.forEachRemaining(consumer);
1✔
3433
    }
1✔
3434

3435
    @Override
3436
    public boolean tryAdvance(Consumer<? super K> action) {
3437
      requireNonNull(action);
1✔
3438
      boolean[] advanced = { false };
1✔
3439
      Consumer<Node<K, V>> consumer = node -> {
1✔
3440
        K key = node.getKey();
1✔
3441
        V value = node.getValue();
1✔
3442
        long now = cache.expirationTicker().read();
1✔
3443
        if ((key != null) && (value != null) && node.isAlive() && !cache.hasExpired(node, now)) {
1✔
3444
          action.accept(key);
1✔
3445
          advanced[0] = true;
1✔
3446
        }
3447
      };
1✔
3448
      while (spliterator.tryAdvance(consumer)) {
1✔
3449
        if (advanced[0]) {
1✔
3450
          return true;
1✔
3451
        }
3452
      }
3453
      return false;
1✔
3454
    }
3455

3456
    @Override
3457
    public @Nullable Spliterator<K> trySplit() {
3458
      Spliterator<Node<K, V>> split = spliterator.trySplit();
1✔
3459
      return (split == null) ? null : new KeySpliterator<>(cache, split);
1✔
3460
    }
3461

3462
    @Override
3463
    public long estimateSize() {
3464
      return spliterator.estimateSize();
1✔
3465
    }
3466

3467
    @Override
3468
    public int characteristics() {
3469
      return DISTINCT | CONCURRENT | NONNULL;
1✔
3470
    }
3471
  }
3472

3473
  /** An adapter to safely externalize the values. */
3474
  static final class ValuesView<K, V> extends AbstractCollection<V> {
3475
    final BoundedLocalCache<K, V> cache;
3476

3477
    ValuesView(BoundedLocalCache<K, V> cache) {
1✔
3478
      this.cache = requireNonNull(cache);
1✔
3479
    }
1✔
3480

3481
    @Override
3482
    public int size() {
3483
      return cache.size();
1✔
3484
    }
3485

3486
    @Override
3487
    public void clear() {
3488
      cache.clear();
1✔
3489
    }
1✔
3490

3491
    @Override
3492
    @SuppressWarnings("SuspiciousMethodCalls")
3493
    public boolean contains(Object o) {
3494
      return cache.containsValue(o);
1✔
3495
    }
3496

3497
    @Override
3498
    @SuppressWarnings("NullAway")
3499
    public boolean removeAll(Collection<?> collection) {
3500
      requireNonNull(collection);
1✔
3501
      @Var boolean modified = false;
1✔
3502
      for (var iterator = new EntryIterator<>(cache); iterator.hasNext();) {
1✔
3503
        if (collection.contains(iterator.value) && cache.remove(iterator.key, iterator.value)) {
1✔
3504
          modified = true;
1✔
3505
        }
3506
        iterator.advance();
1✔
3507
      }
3508
      return modified;
1✔
3509
    }
3510

3511
    @Override
3512
    @SuppressWarnings("NullAway")
3513
    public boolean remove(Object o) {
3514
      if (o == null) {
1✔
3515
        return false;
1✔
3516
      }
3517
      for (var iterator = new EntryIterator<>(cache); iterator.hasNext();) {
1✔
3518
        if (o.equals(iterator.value) && cache.remove(iterator.key, iterator.value)) {
1✔
3519
          return true;
1✔
3520
        }
3521
        iterator.advance();
1✔
3522
      }
3523
      return false;
1✔
3524
    }
3525

3526
    @Override
3527
    @SuppressWarnings("NullAway")
3528
    public boolean removeIf(Predicate<? super V> filter) {
3529
      requireNonNull(filter);
1✔
3530
      @Var boolean modified = false;
1✔
3531
      for (var iterator = new EntryIterator<>(cache); iterator.hasNext();) {
1✔
3532
        if (filter.test(iterator.value)) {
1✔
3533
          modified |= cache.remove(iterator.key, iterator.value);
1✔
3534
        }
3535
        iterator.advance();
1✔
3536
      }
3537
      return modified;
1✔
3538
    }
3539

3540
    @Override
3541
    @SuppressWarnings("NullAway")
3542
    public boolean retainAll(Collection<?> collection) {
3543
      requireNonNull(collection);
1✔
3544
      @Var boolean modified = false;
1✔
3545
      for (var iterator = new EntryIterator<>(cache); iterator.hasNext();) {
1✔
3546
        if (!collection.contains(iterator.value) && cache.remove(iterator.key, iterator.value)) {
1✔
3547
          modified = true;
1✔
3548
        }
3549
        iterator.advance();
1✔
3550
      }
3551
      return modified;
1✔
3552
    }
3553

3554
    @Override
3555
    public Iterator<V> iterator() {
3556
      return new ValueIterator<>(cache);
1✔
3557
    }
3558

3559
    @Override
3560
    public Spliterator<V> spliterator() {
3561
      return new ValueSpliterator<>(cache);
1✔
3562
    }
3563
  }
3564

3565
  /** An adapter to safely externalize the value iterator. */
3566
  static final class ValueIterator<K, V> implements Iterator<V> {
3567
    final EntryIterator<K, V> iterator;
3568

3569
    ValueIterator(BoundedLocalCache<K, V> cache) {
1✔
3570
      this.iterator = new EntryIterator<>(cache);
1✔
3571
    }
1✔
3572

3573
    @Override
3574
    public boolean hasNext() {
3575
      return iterator.hasNext();
1✔
3576
    }
3577

3578
    @Override
3579
    public V next() {
3580
      return iterator.nextValue();
1✔
3581
    }
3582

3583
    @Override
3584
    public void remove() {
3585
      iterator.remove();
1✔
3586
    }
1✔
3587
  }
3588

3589
  /** An adapter to safely externalize the value spliterator. */
3590
  static final class ValueSpliterator<K, V> implements Spliterator<V> {
3591
    final Spliterator<Node<K, V>> spliterator;
3592
    final BoundedLocalCache<K, V> cache;
3593

3594
    ValueSpliterator(BoundedLocalCache<K, V> cache) {
3595
      this(cache, cache.data.values().spliterator());
1✔
3596
    }
1✔
3597

3598
    ValueSpliterator(BoundedLocalCache<K, V> cache, Spliterator<Node<K, V>> spliterator) {
1✔
3599
      this.spliterator = requireNonNull(spliterator);
1✔
3600
      this.cache = requireNonNull(cache);
1✔
3601
    }
1✔
3602

3603
    @Override
3604
    public void forEachRemaining(Consumer<? super V> action) {
3605
      requireNonNull(action);
1✔
3606
      Consumer<Node<K, V>> consumer = node -> {
1✔
3607
        K key = node.getKey();
1✔
3608
        V value = node.getValue();
1✔
3609
        long now = cache.expirationTicker().read();
1✔
3610
        if ((key != null) && (value != null) && node.isAlive() && !cache.hasExpired(node, now)) {
1✔
3611
          action.accept(value);
1✔
3612
        }
3613
      };
1✔
3614
      spliterator.forEachRemaining(consumer);
1✔
3615
    }
1✔
3616

3617
    @Override
3618
    public boolean tryAdvance(Consumer<? super V> action) {
3619
      requireNonNull(action);
1✔
3620
      boolean[] advanced = { false };
1✔
3621
      long now = cache.expirationTicker().read();
1✔
3622
      Consumer<Node<K, V>> consumer = node -> {
1✔
3623
        K key = node.getKey();
1✔
3624
        V value = node.getValue();
1✔
3625
        if ((key != null) && (value != null) && !cache.hasExpired(node, now) && node.isAlive()) {
1✔
3626
          action.accept(value);
1✔
3627
          advanced[0] = true;
1✔
3628
        }
3629
      };
1✔
3630
      while (spliterator.tryAdvance(consumer)) {
1✔
3631
        if (advanced[0]) {
1✔
3632
          return true;
1✔
3633
        }
3634
      }
3635
      return false;
1✔
3636
    }
3637

3638
    @Override
3639
    public @Nullable Spliterator<V> trySplit() {
3640
      Spliterator<Node<K, V>> split = spliterator.trySplit();
1✔
3641
      return (split == null) ? null : new ValueSpliterator<>(cache, split);
1✔
3642
    }
3643

3644
    @Override
3645
    public long estimateSize() {
3646
      return spliterator.estimateSize();
1✔
3647
    }
3648

3649
    @Override
3650
    public int characteristics() {
3651
      return CONCURRENT | NONNULL;
1✔
3652
    }
3653
  }
3654

3655
  /** An adapter to safely externalize the entries. */
3656
  static final class EntrySetView<K, V> extends AbstractSet<Entry<K, V>> {
3657
    final BoundedLocalCache<K, V> cache;
3658

3659
    EntrySetView(BoundedLocalCache<K, V> cache) {
1✔
3660
      this.cache = requireNonNull(cache);
1✔
3661
    }
1✔
3662

3663
    @Override
3664
    public int size() {
3665
      return cache.size();
1✔
3666
    }
3667

3668
    @Override
3669
    public void clear() {
3670
      cache.clear();
1✔
3671
    }
1✔
3672

3673
    @Override
3674
    public boolean contains(Object o) {
3675
      if (!(o instanceof Entry<?, ?>)) {
1✔
3676
        return false;
1✔
3677
      }
3678
      var entry = (Entry<?, ?>) o;
1✔
3679
      var key = entry.getKey();
1✔
3680
      var value = entry.getValue();
1✔
3681
      if ((key == null) || (value == null)) {
1✔
3682
        return false;
1✔
3683
      }
3684
      Node<K, V> node = cache.data.get(cache.nodeFactory.newLookupKey(key));
1✔
3685
      return (node != null) && node.containsValue(value);
1✔
3686
    }
3687

3688
    @Override
3689
    public boolean removeAll(Collection<?> collection) {
3690
      requireNonNull(collection);
1✔
3691
      @Var boolean modified = false;
1✔
3692
      if ((collection instanceof Set<?>) && (collection.size() > size())) {
1✔
3693
        for (var entry : this) {
1✔
3694
          if (collection.contains(entry)) {
1✔
3695
            modified |= remove(entry);
1✔
3696
          }
3697
        }
1✔
3698
      } else {
3699
        for (var item : collection) {
1✔
3700
          modified |= (item != null) && remove(item);
1✔
3701
        }
1✔
3702
      }
3703
      return modified;
1✔
3704
    }
3705

3706
    @Override
3707
    @SuppressWarnings("SuspiciousMethodCalls")
3708
    public boolean remove(Object o) {
3709
      if (!(o instanceof Entry<?, ?>)) {
1✔
3710
        return false;
1✔
3711
      }
3712
      var entry = (Entry<?, ?>) o;
1✔
3713
      var key = entry.getKey();
1✔
3714
      return (key != null) && cache.remove(key, entry.getValue());
1✔
3715
    }
3716

3717
    @Override
3718
    public boolean removeIf(Predicate<? super Entry<K, V>> filter) {
3719
      requireNonNull(filter);
1✔
3720
      @Var boolean modified = false;
1✔
3721
      for (Entry<K, V> entry : this) {
1✔
3722
        if (filter.test(entry)) {
1✔
3723
          modified |= cache.remove(entry.getKey(), entry.getValue());
1✔
3724
        }
3725
      }
1✔
3726
      return modified;
1✔
3727
    }
3728

3729
    @Override
3730
    public boolean retainAll(Collection<?> collection) {
3731
      requireNonNull(collection);
1✔
3732
      @Var boolean modified = false;
1✔
3733
      for (var entry : this) {
1✔
3734
        if (!collection.contains(entry) && remove(entry)) {
1✔
3735
          modified = true;
1✔
3736
        }
3737
      }
1✔
3738
      return modified;
1✔
3739
    }
3740

3741
    @Override
3742
    public Iterator<Entry<K, V>> iterator() {
3743
      return new EntryIterator<>(cache);
1✔
3744
    }
3745

3746
    @Override
3747
    public Spliterator<Entry<K, V>> spliterator() {
3748
      return new EntrySpliterator<>(cache);
1✔
3749
    }
3750
  }
3751

3752
  /** An adapter to safely externalize the entry iterator. */
3753
  static final class EntryIterator<K, V> implements Iterator<Entry<K, V>> {
3754
    final BoundedLocalCache<K, V> cache;
3755
    final Iterator<Node<K, V>> iterator;
3756

3757
    @Nullable K key;
3758
    @Nullable V value;
3759
    @Nullable K removalKey;
3760
    @Nullable Node<K, V> next;
3761

3762
    EntryIterator(BoundedLocalCache<K, V> cache) {
1✔
3763
      this.iterator = cache.data.values().iterator();
1✔
3764
      this.cache = cache;
1✔
3765
    }
1✔
3766

3767
    @Override
3768
    public boolean hasNext() {
3769
      if (next != null) {
1✔
3770
        return true;
1✔
3771
      }
3772

3773
      long now = cache.expirationTicker().read();
1✔
3774
      while (iterator.hasNext()) {
1✔
3775
        next = iterator.next();
1✔
3776
        value = next.getValue();
1✔
3777
        key = next.getKey();
1✔
3778

3779
        boolean evictable = (key == null) || (value == null) || cache.hasExpired(next, now);
1✔
3780
        if (evictable || !next.isAlive()) {
1✔
3781
          if (evictable) {
1✔
3782
            cache.scheduleDrainBuffers();
1✔
3783
          }
3784
          advance();
1✔
3785
          continue;
1✔
3786
        }
3787
        return true;
1✔
3788
      }
3789
      return false;
1✔
3790
    }
3791

3792
    /** Invalidates the current position so that the iterator may compute the next position. */
3793
    void advance() {
3794
      value = null;
1✔
3795
      next = null;
1✔
3796
      key = null;
1✔
3797
    }
1✔
3798

3799
    @SuppressWarnings("NullAway")
3800
    K nextKey() {
3801
      if (!hasNext()) {
1✔
3802
        throw new NoSuchElementException();
1✔
3803
      }
3804
      removalKey = key;
1✔
3805
      advance();
1✔
3806
      return removalKey;
1✔
3807
    }
3808

3809
    @SuppressWarnings("NullAway")
3810
    V nextValue() {
3811
      if (!hasNext()) {
1✔
3812
        throw new NoSuchElementException();
1✔
3813
      }
3814
      removalKey = key;
1✔
3815
      V val = value;
1✔
3816
      advance();
1✔
3817
      return val;
1✔
3818
    }
3819

3820
    @Override
3821
    public Entry<K, V> next() {
3822
      if (!hasNext()) {
1✔
3823
        throw new NoSuchElementException();
1✔
3824
      }
3825
      @SuppressWarnings("NullAway")
3826
      var entry = new WriteThroughEntry<>(cache, key, value);
1✔
3827
      removalKey = key;
1✔
3828
      advance();
1✔
3829
      return entry;
1✔
3830
    }
3831

3832
    @Override
3833
    public void remove() {
3834
      if (removalKey == null) {
1✔
3835
        throw new IllegalStateException();
1✔
3836
      }
3837
      cache.remove(removalKey);
1✔
3838
      removalKey = null;
1✔
3839
    }
1✔
3840
  }
3841

3842
  /** An adapter to safely externalize the entry spliterator. */
3843
  static final class EntrySpliterator<K, V> implements Spliterator<Entry<K, V>> {
3844
    final Spliterator<Node<K, V>> spliterator;
3845
    final BoundedLocalCache<K, V> cache;
3846

3847
    EntrySpliterator(BoundedLocalCache<K, V> cache) {
3848
      this(cache, cache.data.values().spliterator());
1✔
3849
    }
1✔
3850

3851
    EntrySpliterator(BoundedLocalCache<K, V> cache, Spliterator<Node<K, V>> spliterator) {
1✔
3852
      this.spliterator = requireNonNull(spliterator);
1✔
3853
      this.cache = requireNonNull(cache);
1✔
3854
    }
1✔
3855

3856
    @Override
3857
    public void forEachRemaining(Consumer<? super Entry<K, V>> action) {
3858
      requireNonNull(action);
1✔
3859
      Consumer<Node<K, V>> consumer = node -> {
1✔
3860
        K key = node.getKey();
1✔
3861
        V value = node.getValue();
1✔
3862
        long now = cache.expirationTicker().read();
1✔
3863
        if ((key != null) && (value != null) && node.isAlive() && !cache.hasExpired(node, now)) {
1✔
3864
          action.accept(new WriteThroughEntry<>(cache, key, value));
1✔
3865
        }
3866
      };
1✔
3867
      spliterator.forEachRemaining(consumer);
1✔
3868
    }
1✔
3869

3870
    @Override
3871
    public boolean tryAdvance(Consumer<? super Entry<K, V>> action) {
3872
      requireNonNull(action);
1✔
3873
      boolean[] advanced = { false };
1✔
3874
      Consumer<Node<K, V>> consumer = node -> {
1✔
3875
        K key = node.getKey();
1✔
3876
        V value = node.getValue();
1✔
3877
        long now = cache.expirationTicker().read();
1✔
3878
        if ((key != null) && (value != null) && node.isAlive() && !cache.hasExpired(node, now)) {
1✔
3879
          action.accept(new WriteThroughEntry<>(cache, key, value));
1✔
3880
          advanced[0] = true;
1✔
3881
        }
3882
      };
1✔
3883
      while (spliterator.tryAdvance(consumer)) {
1✔
3884
        if (advanced[0]) {
1✔
3885
          return true;
1✔
3886
        }
3887
      }
3888
      return false;
1✔
3889
    }
3890

3891
    @Override
3892
    public @Nullable Spliterator<Entry<K, V>> trySplit() {
3893
      Spliterator<Node<K, V>> split = spliterator.trySplit();
1✔
3894
      return (split == null) ? null : new EntrySpliterator<>(cache, split);
1✔
3895
    }
3896

3897
    @Override
3898
    public long estimateSize() {
3899
      return spliterator.estimateSize();
1✔
3900
    }
3901

3902
    @Override
3903
    public int characteristics() {
3904
      return DISTINCT | CONCURRENT | NONNULL;
1✔
3905
    }
3906
  }
3907

3908
  /** A reusable task that performs the maintenance work; used to avoid wrapping by ForkJoinPool. */
3909
  static final class PerformCleanupTask extends ForkJoinTask<@Nullable Void> implements Runnable {
3910
    private static final long serialVersionUID = 1L;
3911

3912
    final WeakReference<BoundedLocalCache<?, ?>> reference;
3913

3914
    PerformCleanupTask(BoundedLocalCache<?, ?> cache) {
1✔
3915
      reference = new WeakReference<>(cache);
1✔
3916
    }
1✔
3917

3918
    @Override
3919
    public boolean exec() {
3920
      try {
3921
        run();
1✔
3922
      } catch (Throwable t) {
1✔
3923
        logger.log(Level.ERROR, "Exception thrown when performing the maintenance task", t);
1✔
3924
      }
1✔
3925

3926
      // Indicates that the task has not completed to allow subsequent submissions to execute
3927
      return false;
1✔
3928
    }
3929

3930
    @Override
3931
    public void run() {
3932
      BoundedLocalCache<?, ?> cache = reference.get();
1✔
3933
      if (cache != null) {
1✔
3934
        cache.performCleanUp(/* ignored */ null);
1✔
3935
      }
3936
    }
1✔
3937

3938
    /**
3939
     * This method cannot be ignored due to being final, so a hostile user supplied Executor could
3940
     * forcibly complete the task and halt future executions. There are easier ways to intentionally
3941
     * harm a system, so this is assumed to not happen in practice.
3942
     */
3943
    // public final void quietlyComplete() {}
3944

3945
    @Override public void complete(@Nullable Void value) {}
1✔
3946
    @Override public void setRawResult(@Nullable Void value) {}
1✔
3947
    @Override public @Nullable Void getRawResult() { return null; }
1✔
3948
    @Override public void completeExceptionally(@Nullable Throwable t) {}
1✔
3949
    @Override public boolean cancel(boolean mayInterruptIfRunning) { return false; }
1✔
3950
  }
3951

3952
  /** Creates a serialization proxy based on the common configuration shared by all cache types. */
3953
  static <K, V> SerializationProxy<K, V> makeSerializationProxy(BoundedLocalCache<?, ?> cache) {
3954
    var proxy = new SerializationProxy<K, V>();
1✔
3955
    proxy.weakKeys = cache.collectKeys();
1✔
3956
    proxy.weakValues = cache.nodeFactory.weakValues();
1✔
3957
    proxy.softValues = cache.nodeFactory.softValues();
1✔
3958
    proxy.isRecordingStats = cache.isRecordingStats();
1✔
3959
    proxy.evictionListener = cache.evictionListener;
1✔
3960
    proxy.removalListener = cache.removalListener();
1✔
3961
    proxy.ticker = cache.expirationTicker();
1✔
3962
    if (cache.expiresAfterAccess()) {
1✔
3963
      proxy.expiresAfterAccessNanos = cache.expiresAfterAccessNanos();
1✔
3964
    }
3965
    if (cache.expiresAfterWrite()) {
1✔
3966
      proxy.expiresAfterWriteNanos = cache.expiresAfterWriteNanos();
1✔
3967
    }
3968
    if (cache.expiresVariable()) {
1✔
3969
      proxy.expiry = cache.expiry();
1✔
3970
    }
3971
    if (cache.refreshAfterWrite()) {
1✔
3972
      proxy.refreshAfterWriteNanos = cache.refreshAfterWriteNanos();
1✔
3973
    }
3974
    if (cache.evicts()) {
1✔
3975
      if (cache.isWeighted) {
1✔
3976
        proxy.weigher = cache.weigher;
1✔
3977
        proxy.maximumWeight = cache.maximum();
1✔
3978
      } else {
3979
        proxy.maximumSize = cache.maximum();
1✔
3980
      }
3981
    }
3982
    proxy.cacheLoader = cache.cacheLoader;
1✔
3983
    proxy.async = cache.isAsync;
1✔
3984
    return proxy;
1✔
3985
  }
3986

3987
  /* --------------- Manual Cache --------------- */
3988

3989
  static class BoundedLocalManualCache<K, V> implements LocalManualCache<K, V>, Serializable {
3990
    private static final long serialVersionUID = 1;
3991

3992
    final BoundedLocalCache<K, V> cache;
3993

3994
    @Nullable Policy<K, V> policy;
3995

3996
    BoundedLocalManualCache(Caffeine<K, V> builder) {
3997
      this(builder, null);
1✔
3998
    }
1✔
3999

4000
    BoundedLocalManualCache(Caffeine<K, V> builder, @Nullable CacheLoader<? super K, V> loader) {
1✔
4001
      cache = LocalCacheFactory.newBoundedLocalCache(builder, loader, /* isAsync= */ false);
1✔
4002
    }
1✔
4003

4004
    @Override
4005
    public final BoundedLocalCache<K, V> cache() {
4006
      return cache;
1✔
4007
    }
4008

4009
    @Override
4010
    public final Policy<K, V> policy() {
4011
      if (policy == null) {
1✔
4012
        @SuppressWarnings("NullAway")
4013
        Function<@Nullable V, @Nullable V> identity = identity();
1✔
4014
        policy = new BoundedPolicy<>(cache, identity, cache.isWeighted);
1✔
4015
      }
4016
      return policy;
1✔
4017
    }
4018

4019
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
4020
      throw new InvalidObjectException("Proxy required");
1✔
4021
    }
4022

4023
    private Object writeReplace() {
4024
      return makeSerializationProxy(cache);
1✔
4025
    }
4026
  }
4027

4028
  @SuppressWarnings({"NullableOptional", "OptionalAssignedToNull"})
4029
  static final class BoundedPolicy<K, V> implements Policy<K, V> {
4030
    final Function<@Nullable V, @Nullable V> transformer;
4031
    final BoundedLocalCache<K, V> cache;
4032
    final boolean isWeighted;
4033

4034
    @Nullable Optional<Eviction<K, V>> eviction;
4035
    @Nullable Optional<FixedRefresh<K, V>> refreshes;
4036
    @Nullable Optional<FixedExpiration<K, V>> afterWrite;
4037
    @Nullable Optional<FixedExpiration<K, V>> afterAccess;
4038
    @Nullable Optional<VarExpiration<K, V>> variable;
4039

4040
    BoundedPolicy(BoundedLocalCache<K, V> cache,
4041
        Function<@Nullable V, @Nullable V> transformer, boolean isWeighted) {
1✔
4042
      this.transformer = transformer;
1✔
4043
      this.isWeighted = isWeighted;
1✔
4044
      this.cache = cache;
1✔
4045
    }
1✔
4046

4047
    @Override public boolean isRecordingStats() {
4048
      return cache.isRecordingStats();
1✔
4049
    }
4050
    @Override public @Nullable V getIfPresentQuietly(K key) {
4051
      return transformer.apply(cache.getIfPresentQuietly(key));
1✔
4052
    }
4053
    @SuppressWarnings("NullAway")
4054
    @Override public @Nullable CacheEntry<K, V> getEntryIfPresentQuietly(K key) {
4055
      Node<K, V> node = cache.data.get(cache.nodeFactory.newLookupKey(key));
1✔
4056
      return (node == null) ? null : cache.nodeToCacheEntry(node, transformer);
1✔
4057
    }
4058
    @SuppressWarnings("Java9CollectionFactory")
4059
    @Override public Map<K, CompletableFuture<V>> refreshes() {
4060
      var refreshes = cache.refreshes;
1✔
4061
      if ((refreshes == null) || refreshes.isEmpty()) {
1✔
4062
        @SuppressWarnings("ImmutableMapOf")
4063
        Map<K, CompletableFuture<V>> emptyMap = Collections.unmodifiableMap(Collections.emptyMap());
1✔
4064
        return emptyMap;
1✔
4065
      } else if (cache.collectKeys()) {
1✔
4066
        var inFlight = new IdentityHashMap<K, CompletableFuture<V>>(refreshes.size());
1✔
4067
        for (var entry : refreshes.entrySet()) {
1✔
4068
          @SuppressWarnings("unchecked")
4069
          var key = ((InternalReference<K>) entry.getKey()).get();
1✔
4070
          @SuppressWarnings("unchecked")
4071
          var future = (CompletableFuture<V>) entry.getValue();
1✔
4072
          if (key != null) {
1✔
4073
            inFlight.put(key, future);
1✔
4074
          }
4075
        }
1✔
4076
        return Collections.unmodifiableMap(inFlight);
1✔
4077
      }
4078
      @SuppressWarnings("unchecked")
4079
      var castedRefreshes = (Map<K, CompletableFuture<V>>) (Object) refreshes;
1✔
4080
      return Collections.unmodifiableMap(new HashMap<>(castedRefreshes));
1✔
4081
    }
4082
    @Override public Optional<Eviction<K, V>> eviction() {
4083
      return cache.evicts()
1✔
4084
          ? (eviction == null) ? (eviction = Optional.of(new BoundedEviction())) : eviction
1✔
4085
          : Optional.empty();
1✔
4086
    }
4087
    @Override public Optional<FixedExpiration<K, V>> expireAfterAccess() {
4088
      if (!cache.expiresAfterAccess()) {
1✔
4089
        return Optional.empty();
1✔
4090
      }
4091
      return (afterAccess == null)
1✔
4092
          ? (afterAccess = Optional.of(new BoundedExpireAfterAccess()))
1✔
4093
          : afterAccess;
1✔
4094
    }
4095
    @Override public Optional<FixedExpiration<K, V>> expireAfterWrite() {
4096
      if (!cache.expiresAfterWrite()) {
1✔
4097
        return Optional.empty();
1✔
4098
      }
4099
      return (afterWrite == null)
1✔
4100
          ? (afterWrite = Optional.of(new BoundedExpireAfterWrite()))
1✔
4101
          : afterWrite;
1✔
4102
    }
4103
    @Override public Optional<VarExpiration<K, V>> expireVariably() {
4104
      if (!cache.expiresVariable()) {
1✔
4105
        return Optional.empty();
1✔
4106
      }
4107
      return (variable == null)
1✔
4108
          ? (variable = Optional.of(new BoundedVarExpiration()))
1✔
4109
          : variable;
1✔
4110
    }
4111
    @Override public Optional<FixedRefresh<K, V>> refreshAfterWrite() {
4112
      if (!cache.refreshAfterWrite()) {
1✔
4113
        return Optional.empty();
1✔
4114
      }
4115
      return (refreshes == null)
1✔
4116
          ? (refreshes = Optional.of(new BoundedRefreshAfterWrite()))
1✔
4117
          : refreshes;
1✔
4118
    }
4119

4120
    final class BoundedEviction implements Eviction<K, V> {
1✔
4121
      @Override public boolean isWeighted() {
4122
        return isWeighted;
1✔
4123
      }
4124
      @Override public OptionalInt weightOf(K key) {
4125
        requireNonNull(key);
1✔
4126
        if (!isWeighted) {
1✔
4127
          return OptionalInt.empty();
1✔
4128
        }
4129
        Node<K, V> node = cache.data.get(cache.nodeFactory.newLookupKey(key));
1✔
4130
        if ((node == null) || cache.hasExpired(node, cache.expirationTicker().read())) {
1✔
4131
          return OptionalInt.empty();
1✔
4132
        }
4133
        synchronized (node) {
1✔
4134
          return OptionalInt.of(node.getWeight());
1✔
4135
        }
4136
      }
4137
      @Override public OptionalLong weightedSize() {
4138
        if (cache.evicts() && isWeighted()) {
1✔
4139
          cache.evictionLock.lock();
1✔
4140
          try {
4141
            if (cache.drainStatusOpaque() == REQUIRED) {
1✔
4142
              cache.maintenance(/* ignored */ null);
1✔
4143
            }
4144
            return OptionalLong.of(Math.max(0, cache.weightedSize()));
1✔
4145
          } finally {
4146
            cache.evictionLock.unlock();
1✔
4147
            cache.rescheduleCleanUpIfIncomplete();
1✔
4148
          }
4149
        }
4150
        return OptionalLong.empty();
1✔
4151
      }
4152
      @Override public long getMaximum() {
4153
        cache.evictionLock.lock();
1✔
4154
        try {
4155
          if (cache.drainStatusOpaque() == REQUIRED) {
1✔
4156
            cache.maintenance(/* ignored */ null);
1✔
4157
          }
4158
          return cache.maximum();
1✔
4159
        } finally {
4160
          cache.evictionLock.unlock();
1✔
4161
          cache.rescheduleCleanUpIfIncomplete();
1✔
4162
        }
4163
      }
4164
      @Override public void setMaximum(long maximum) {
4165
        cache.evictionLock.lock();
1✔
4166
        try {
4167
          cache.setMaximumSize(maximum);
1✔
4168
          cache.maintenance(/* ignored */ null);
1✔
4169
        } finally {
4170
          cache.evictionLock.unlock();
1✔
4171
          cache.rescheduleCleanUpIfIncomplete();
1✔
4172
        }
4173
      }
1✔
4174
      @Override public Map<K, V> coldest(int limit) {
4175
        int expectedSize = Math.min(limit, cache.size());
1✔
4176
        var limiter = new SizeLimiter<K, V>(expectedSize, limit);
1✔
4177
        return cache.evictionOrder(/* hottest= */ false, transformer, limiter);
1✔
4178
      }
4179
      @Override public Map<K, V> coldestWeighted(long weightLimit) {
4180
        var limiter = isWeighted()
1✔
4181
            ? new WeightLimiter<K, V>(weightLimit)
1✔
4182
            : new SizeLimiter<K, V>((int) Math.min(weightLimit, cache.size()), weightLimit);
1✔
4183
        return cache.evictionOrder(/* hottest= */ false, transformer, limiter);
1✔
4184
      }
4185
      @Override
4186
      public <T> T coldest(Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4187
        requireNonNull(mappingFunction);
1✔
4188
        return cache.evictionOrder(/* hottest= */ false, transformer, mappingFunction);
1✔
4189
      }
4190
      @Override public Map<K, V> hottest(int limit) {
4191
        int expectedSize = Math.min(limit, cache.size());
1✔
4192
        var limiter = new SizeLimiter<K, V>(expectedSize, limit);
1✔
4193
        return cache.evictionOrder(/* hottest= */ true, transformer, limiter);
1✔
4194
      }
4195
      @Override public Map<K, V> hottestWeighted(long weightLimit) {
4196
        var limiter = isWeighted()
1✔
4197
            ? new WeightLimiter<K, V>(weightLimit)
1✔
4198
            : new SizeLimiter<K, V>((int) Math.min(weightLimit, cache.size()), weightLimit);
1✔
4199
        return cache.evictionOrder(/* hottest= */ true, transformer, limiter);
1✔
4200
      }
4201
      @Override
4202
      public <T> T hottest(Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4203
        requireNonNull(mappingFunction);
1✔
4204
        return cache.evictionOrder(/* hottest= */ true, transformer, mappingFunction);
1✔
4205
      }
4206
    }
4207

4208
    @SuppressWarnings("PreferJavaTimeOverload")
4209
    final class BoundedExpireAfterAccess implements FixedExpiration<K, V> {
1✔
4210
      @Override public OptionalLong ageOf(K key, TimeUnit unit) {
4211
        requireNonNull(key);
1✔
4212
        requireNonNull(unit);
1✔
4213
        Object lookupKey = cache.nodeFactory.newLookupKey(key);
1✔
4214
        Node<K, V> node = cache.data.get(lookupKey);
1✔
4215
        if (node == null) {
1✔
4216
          return OptionalLong.empty();
1✔
4217
        }
4218
        long now = cache.expirationTicker().read();
1✔
4219
        return cache.hasExpired(node, now)
1✔
4220
            ? OptionalLong.empty()
1✔
4221
            : OptionalLong.of(unit.convert(now - node.getAccessTime(), TimeUnit.NANOSECONDS));
1✔
4222
      }
4223
      @Override public long getExpiresAfter(TimeUnit unit) {
4224
        return unit.convert(cache.expiresAfterAccessNanos(), TimeUnit.NANOSECONDS);
1✔
4225
      }
4226
      @Override public void setExpiresAfter(long duration, TimeUnit unit) {
4227
        requireArgument(duration >= 0);
1✔
4228
        cache.setExpiresAfterAccessNanos(unit.toNanos(duration));
1✔
4229
        cache.scheduleAfterWrite();
1✔
4230
      }
1✔
4231
      @Override public Map<K, V> oldest(int limit) {
4232
        return oldest(new SizeLimiter<>(Math.min(limit, cache.size()), limit));
1✔
4233
      }
4234
      @Override public <T> T oldest(Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4235
        return cache.expireAfterAccessOrder(/* oldest= */ true, transformer, mappingFunction);
1✔
4236
      }
4237
      @Override public Map<K, V> youngest(int limit) {
4238
        return youngest(new SizeLimiter<>(Math.min(limit, cache.size()), limit));
1✔
4239
      }
4240
      @Override public <T> T youngest(Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4241
        return cache.expireAfterAccessOrder(/* oldest= */ false, transformer, mappingFunction);
1✔
4242
      }
4243
    }
4244

4245
    @SuppressWarnings("PreferJavaTimeOverload")
4246
    final class BoundedExpireAfterWrite implements FixedExpiration<K, V> {
1✔
4247
      @Override public OptionalLong ageOf(K key, TimeUnit unit) {
4248
        requireNonNull(key);
1✔
4249
        requireNonNull(unit);
1✔
4250
        Object lookupKey = cache.nodeFactory.newLookupKey(key);
1✔
4251
        Node<K, V> node = cache.data.get(lookupKey);
1✔
4252
        if (node == null) {
1✔
4253
          return OptionalLong.empty();
1✔
4254
        }
4255
        long now = cache.expirationTicker().read();
1✔
4256
        return cache.hasExpired(node, now)
1✔
4257
            ? OptionalLong.empty()
1✔
4258
            : OptionalLong.of(unit.convert(now - node.getWriteTime(), TimeUnit.NANOSECONDS));
1✔
4259
      }
4260
      @Override public long getExpiresAfter(TimeUnit unit) {
4261
        return unit.convert(cache.expiresAfterWriteNanos(), TimeUnit.NANOSECONDS);
1✔
4262
      }
4263
      @Override public void setExpiresAfter(long duration, TimeUnit unit) {
4264
        requireArgument(duration >= 0);
1✔
4265
        cache.setExpiresAfterWriteNanos(unit.toNanos(duration));
1✔
4266
        cache.scheduleAfterWrite();
1✔
4267
      }
1✔
4268
      @Override public Map<K, V> oldest(int limit) {
4269
        return oldest(new SizeLimiter<>(Math.min(limit, cache.size()), limit));
1✔
4270
      }
4271
      @SuppressWarnings("GuardedByChecker")
4272
      @Override public <T> T oldest(Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4273
        return cache.snapshot(cache.writeOrderDeque(), transformer, mappingFunction);
1✔
4274
      }
4275
      @Override public Map<K, V> youngest(int limit) {
4276
        return youngest(new SizeLimiter<>(Math.min(limit, cache.size()), limit));
1✔
4277
      }
4278
      @SuppressWarnings("GuardedByChecker")
4279
      @Override public <T> T youngest(Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4280
        return cache.snapshot(cache.writeOrderDeque()::descendingIterator,
1✔
4281
            transformer, mappingFunction);
4282
      }
4283
    }
4284

4285
    @SuppressWarnings("PreferJavaTimeOverload")
4286
    final class BoundedVarExpiration implements VarExpiration<K, V> {
1✔
4287
      @Override public OptionalLong getExpiresAfter(K key, TimeUnit unit) {
4288
        requireNonNull(key);
1✔
4289
        requireNonNull(unit);
1✔
4290
        Object lookupKey = cache.nodeFactory.newLookupKey(key);
1✔
4291
        Node<K, V> node = cache.data.get(lookupKey);
1✔
4292
        if (node == null) {
1✔
4293
          return OptionalLong.empty();
1✔
4294
        }
4295
        long now = cache.expirationTicker().read();
1✔
4296
        return cache.hasExpired(node, now)
1✔
4297
            ? OptionalLong.empty()
1✔
4298
            : OptionalLong.of(unit.convert(node.getVariableTime() - now, TimeUnit.NANOSECONDS));
1✔
4299
      }
4300
      @Override public void setExpiresAfter(K key, long duration, TimeUnit unit) {
4301
        requireNonNull(key);
1✔
4302
        requireNonNull(unit);
1✔
4303
        requireArgument(duration >= 0);
1✔
4304
        Object lookupKey = cache.nodeFactory.newLookupKey(key);
1✔
4305
        Node<K, V> node = cache.data.get(lookupKey);
1✔
4306
        if (node != null) {
1✔
4307
          long now;
4308
          long durationNanos = TimeUnit.NANOSECONDS.convert(duration, unit);
1✔
4309
          synchronized (node) {
1✔
4310
            now = cache.expirationTicker().read();
1✔
4311
            if (cache.hasExpired(node, now)) {
1✔
4312
              return;
1✔
4313
            }
4314
            node.setVariableTime(now + Math.min(durationNanos, MAXIMUM_EXPIRY));
1✔
4315
          }
1✔
4316
          cache.afterRead(node, now, /* recordHit= */ false);
1✔
4317
        }
4318
      }
1✔
4319
      @Override public @Nullable V put(K key, V value, long duration, TimeUnit unit) {
4320
        requireNonNull(unit);
1✔
4321
        requireNonNull(value);
1✔
4322
        requireArgument(duration >= 0);
1✔
4323
        return cache.isAsync
1✔
4324
            ? putAsync(key, value, duration, unit)
1✔
4325
            : putSync(key, value, duration, unit, /* onlyIfAbsent= */ false);
1✔
4326
      }
4327
      @Override public @Nullable V putIfAbsent(K key, V value, long duration, TimeUnit unit) {
4328
        requireNonNull(unit);
1✔
4329
        requireNonNull(value);
1✔
4330
        requireArgument(duration >= 0);
1✔
4331
        return cache.isAsync
1✔
4332
            ? putIfAbsentAsync(key, value, duration, unit)
1✔
4333
            : putSync(key, value, duration, unit, /* onlyIfAbsent= */ true);
1✔
4334
      }
4335
      @Nullable V putSync(K key, V value, long duration, TimeUnit unit, boolean onlyIfAbsent) {
4336
        var expiry = new FixedExpireAfterWrite<K, V>(duration, unit);
1✔
4337
        return cache.put(key, value, expiry, onlyIfAbsent);
1✔
4338
      }
4339
      @SuppressWarnings("unchecked")
4340
      @Nullable V putIfAbsentAsync(K key, V value, long duration, TimeUnit unit) {
4341
        // Keep in sync with LocalAsyncCache.AsMapView#putIfAbsent(key, value)
4342
        var expiry = (Expiry<K, V>) new AsyncExpiry<>(new FixedExpireAfterWrite<>(duration, unit));
1✔
4343
        var asyncValue = (V) CompletableFuture.completedFuture(value);
1✔
4344

4345
        for (;;) {
4346
          var priorFuture = (CompletableFuture<V>) cache.getIfPresent(key, /* recordStats= */ false);
1✔
4347
          if (priorFuture != null) {
1✔
4348
            if (!priorFuture.isDone()) {
1✔
4349
              Async.getWhenSuccessful(priorFuture);
×
4350
              continue;
×
4351
            }
4352

4353
            V prior = Async.getWhenSuccessful(priorFuture);
1✔
4354
            if (prior != null) {
1✔
4355
              return prior;
1✔
4356
            }
4357
          }
4358

4359
          boolean[] added = { false };
1✔
4360
          var computed = (CompletableFuture<V>) cache.compute(key, (k, oldValue) -> {
1✔
4361
            var oldValueFuture = (CompletableFuture<V>) oldValue;
1✔
4362
            added[0] = (oldValueFuture == null)
1✔
4363
                || (oldValueFuture.isDone() && (Async.getIfReady(oldValueFuture) == null));
1✔
4364
            return added[0] ? asyncValue : oldValue;
1✔
4365
          }, expiry, /* recordLoad= */ false, /* recordLoadFailure= */ false);
4366

4367
          if (added[0]) {
1✔
4368
            return null;
1✔
4369
          } else {
4370
            V prior = Async.getWhenSuccessful(computed);
×
4371
            if (prior != null) {
×
4372
              return prior;
×
4373
            }
4374
          }
4375
        }
×
4376
      }
4377
      @SuppressWarnings("unchecked")
4378
      @Nullable V putAsync(K key, V value, long duration, TimeUnit unit) {
4379
        var expiry = (Expiry<K, V>) new AsyncExpiry<>(new FixedExpireAfterWrite<>(duration, unit));
1✔
4380
        var asyncValue = (V) CompletableFuture.completedFuture(value);
1✔
4381

4382
        var oldValueFuture = (CompletableFuture<V>) cache.put(
1✔
4383
            key, asyncValue, expiry, /* onlyIfAbsent= */ false);
4384
        return Async.getWhenSuccessful(oldValueFuture);
1✔
4385
      }
4386
      @SuppressWarnings("NullAway")
4387
      @Override public V compute(K key,
4388
          BiFunction<? super K, ? super V, ? extends V> remappingFunction,
4389
          Duration duration) {
4390
        requireNonNull(key);
1✔
4391
        requireNonNull(duration);
1✔
4392
        requireNonNull(remappingFunction);
1✔
4393
        requireArgument(!duration.isNegative(), "duration cannot be negative: %s", duration);
1✔
4394
        var expiry = new FixedExpireAfterWrite<K, V>(
1✔
4395
            toNanosSaturated(duration), TimeUnit.NANOSECONDS);
1✔
4396

4397
        return cache.isAsync
1✔
4398
            ? computeAsync(key, remappingFunction, expiry)
1✔
4399
            : cache.compute(key, remappingFunction, expiry,
1✔
4400
                /* recordLoad= */ true, /* recordLoadFailure= */ true);
4401
      }
4402
      @Nullable V computeAsync(K key,
4403
          BiFunction<? super K, ? super V, ? extends V> remappingFunction,
4404
          Expiry<? super K, ? super V> expiry) {
4405
        // Keep in sync with LocalAsyncCache.AsMapView#compute(key, remappingFunction)
4406
        @SuppressWarnings("unchecked")
4407
        var delegate = (LocalCache<K, CompletableFuture<V>>) cache;
1✔
4408

4409
        @SuppressWarnings({"rawtypes", "unchecked"})
4410
        var newValue = (V[]) new Object[1];
1✔
4411
        for (;;) {
4412
          Async.getWhenSuccessful(delegate.getIfPresentQuietly(key));
1✔
4413

4414
          CompletableFuture<V> valueFuture = delegate.compute(key, (k, oldValueFuture) -> {
1✔
4415
            if ((oldValueFuture != null) && !oldValueFuture.isDone()) {
1✔
4416
              return oldValueFuture;
×
4417
            }
4418

4419
            V oldValue = Async.getIfReady(oldValueFuture);
1✔
4420
            BiFunction<? super K, ? super V, ? extends V> function = delegate.statsAware(
1✔
4421
                remappingFunction, /* recordLoad= */ true, /* recordLoadFailure= */ true);
4422
            newValue[0] = function.apply(key, oldValue);
1✔
4423
            return (newValue[0] == null) ? null : CompletableFuture.completedFuture(newValue[0]);
1✔
4424
          }, new AsyncExpiry<>(expiry), /* recordLoad= */ false, /* recordLoadFailure= */ false);
4425

4426
          if (newValue[0] != null) {
1✔
4427
            return newValue[0];
1✔
4428
          } else if (valueFuture == null) {
1✔
4429
            return null;
1✔
4430
          }
4431
        }
×
4432
      }
4433
      @Override public Map<K, V> oldest(int limit) {
4434
        return oldest(new SizeLimiter<>(Math.min(limit, cache.size()), limit));
1✔
4435
      }
4436
      @Override public <T> T oldest(Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4437
        return cache.snapshot(cache.timerWheel(), transformer, mappingFunction);
1✔
4438
      }
4439
      @Override public Map<K, V> youngest(int limit) {
4440
        return youngest(new SizeLimiter<>(Math.min(limit, cache.size()), limit));
1✔
4441
      }
4442
      @Override public <T> T youngest(Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4443
        return cache.snapshot(cache.timerWheel()::descendingIterator, transformer, mappingFunction);
1✔
4444
      }
4445
    }
4446

4447
    static final class FixedExpireAfterWrite<K, V> implements Expiry<K, V> {
4448
      final long duration;
4449
      final TimeUnit unit;
4450

4451
      FixedExpireAfterWrite(long duration, TimeUnit unit) {
1✔
4452
        this.duration = duration;
1✔
4453
        this.unit = unit;
1✔
4454
      }
1✔
4455
      @Override public long expireAfterCreate(K key, V value, long currentTime) {
4456
        return unit.toNanos(duration);
1✔
4457
      }
4458
      @Override public long expireAfterUpdate(
4459
          K key, V value, long currentTime, long currentDuration) {
4460
        return unit.toNanos(duration);
1✔
4461
      }
4462
      @CanIgnoreReturnValue
4463
      @Override public long expireAfterRead(
4464
          K key, V value, long currentTime, long currentDuration) {
4465
        return currentDuration;
1✔
4466
      }
4467
    }
4468

4469
    @SuppressWarnings("PreferJavaTimeOverload")
4470
    final class BoundedRefreshAfterWrite implements FixedRefresh<K, V> {
1✔
4471
      @Override public OptionalLong ageOf(K key, TimeUnit unit) {
4472
        requireNonNull(key);
1✔
4473
        requireNonNull(unit);
1✔
4474
        Object lookupKey = cache.nodeFactory.newLookupKey(key);
1✔
4475
        Node<K, V> node = cache.data.get(lookupKey);
1✔
4476
        if (node == null) {
1✔
4477
          return OptionalLong.empty();
1✔
4478
        }
4479
        long now = cache.expirationTicker().read();
1✔
4480
        return cache.hasExpired(node, now)
1✔
4481
            ? OptionalLong.empty()
1✔
4482
            : OptionalLong.of(unit.convert(now - node.getWriteTime(), TimeUnit.NANOSECONDS));
1✔
4483
      }
4484
      @Override public long getRefreshesAfter(TimeUnit unit) {
4485
        return unit.convert(cache.refreshAfterWriteNanos(), TimeUnit.NANOSECONDS);
1✔
4486
      }
4487
      @Override public void setRefreshesAfter(long duration, TimeUnit unit) {
4488
        requireArgument(duration >= 0);
1✔
4489
        cache.setRefreshAfterWriteNanos(unit.toNanos(duration));
1✔
4490
        cache.scheduleAfterWrite();
1✔
4491
      }
1✔
4492
    }
4493
  }
4494

4495
  /* --------------- Loading Cache --------------- */
4496

4497
  static final class BoundedLocalLoadingCache<K, V>
4498
      extends BoundedLocalManualCache<K, V> implements LocalLoadingCache<K, V> {
4499
    private static final long serialVersionUID = 1;
4500

4501
    final Function<K, V> mappingFunction;
4502
    final @Nullable Function<Set<? extends K>, Map<K, V>> bulkMappingFunction;
4503

4504
    BoundedLocalLoadingCache(Caffeine<K, V> builder, CacheLoader<? super K, V> loader) {
4505
      super(builder, loader);
1✔
4506
      requireNonNull(loader);
1✔
4507
      mappingFunction = newMappingFunction(loader);
1✔
4508
      bulkMappingFunction = newBulkMappingFunction(loader);
1✔
4509
    }
1✔
4510

4511
    @Override
4512
    @SuppressWarnings("NullAway")
4513
    public AsyncCacheLoader<? super K, V> cacheLoader() {
4514
      return cache.cacheLoader;
1✔
4515
    }
4516

4517
    @Override
4518
    public Function<K, V> mappingFunction() {
4519
      return mappingFunction;
1✔
4520
    }
4521

4522
    @Override
4523
    public @Nullable Function<Set<? extends K>, Map<K, V>> bulkMappingFunction() {
4524
      return bulkMappingFunction;
1✔
4525
    }
4526

4527
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
4528
      throw new InvalidObjectException("Proxy required");
1✔
4529
    }
4530

4531
    private Object writeReplace() {
4532
      return makeSerializationProxy(cache);
1✔
4533
    }
4534
  }
4535

4536
  /* --------------- Async Cache --------------- */
4537

4538
  static final class BoundedLocalAsyncCache<K, V> implements LocalAsyncCache<K, V>, Serializable {
4539
    private static final long serialVersionUID = 1;
4540

4541
    final BoundedLocalCache<K, CompletableFuture<V>> cache;
4542
    final boolean isWeighted;
4543

4544
    @Nullable ConcurrentMap<K, CompletableFuture<V>> mapView;
4545
    @Nullable CacheView<K, V> cacheView;
4546
    @Nullable Policy<K, V> policy;
4547

4548
    @SuppressWarnings("unchecked")
4549
    BoundedLocalAsyncCache(Caffeine<K, V> builder) {
1✔
4550
      cache = (BoundedLocalCache<K, CompletableFuture<V>>) LocalCacheFactory
1✔
4551
          .newBoundedLocalCache(builder, /* cacheLoader= */ null, /* isAsync= */ true);
1✔
4552
      isWeighted = builder.isWeighted();
1✔
4553
    }
1✔
4554

4555
    @Override
4556
    public BoundedLocalCache<K, CompletableFuture<V>> cache() {
4557
      return cache;
1✔
4558
    }
4559

4560
    @Override
4561
    public ConcurrentMap<K, CompletableFuture<V>> asMap() {
4562
      return (mapView == null) ? (mapView = new AsyncAsMapView<>(this)) : mapView;
1✔
4563
    }
4564

4565
    @Override
4566
    public Cache<K, V> synchronous() {
4567
      return (cacheView == null) ? (cacheView = new CacheView<>(this)) : cacheView;
1✔
4568
    }
4569

4570
    @Override
4571
    public Policy<K, V> policy() {
4572
      if (policy == null) {
1✔
4573
        @SuppressWarnings("unchecked")
4574
        var castCache = (BoundedLocalCache<K, V>) cache;
1✔
4575
        Function<CompletableFuture<V>, @Nullable V> transformer = Async::getIfReady;
1✔
4576
        @SuppressWarnings({"NullAway", "unchecked", "Varifier"})
4577
        Function<@Nullable V, @Nullable V> castTransformer = (Function<V, V>) transformer;
1✔
4578
        policy = new BoundedPolicy<>(castCache, castTransformer, isWeighted);
1✔
4579
      }
4580
      return policy;
1✔
4581
    }
4582

4583
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
4584
      throw new InvalidObjectException("Proxy required");
1✔
4585
    }
4586

4587
    private Object writeReplace() {
4588
      return makeSerializationProxy(cache);
1✔
4589
    }
4590
  }
4591

4592
  /* --------------- Async Loading Cache --------------- */
4593

4594
  static final class BoundedLocalAsyncLoadingCache<K, V>
4595
      extends LocalAsyncLoadingCache<K, V> implements Serializable {
4596
    private static final long serialVersionUID = 1;
4597

4598
    final BoundedLocalCache<K, CompletableFuture<V>> cache;
4599
    final boolean isWeighted;
4600

4601
    @Nullable ConcurrentMap<K, CompletableFuture<V>> mapView;
4602
    @Nullable Policy<K, V> policy;
4603

4604
    @SuppressWarnings("unchecked")
4605
    BoundedLocalAsyncLoadingCache(Caffeine<K, V> builder, AsyncCacheLoader<? super K, V> loader) {
4606
      super(loader);
1✔
4607
      isWeighted = builder.isWeighted();
1✔
4608
      cache = (BoundedLocalCache<K, CompletableFuture<V>>) LocalCacheFactory
1✔
4609
          .newBoundedLocalCache(builder, loader, /* isAsync= */ true);
1✔
4610
    }
1✔
4611

4612
    @Override
4613
    public BoundedLocalCache<K, CompletableFuture<V>> cache() {
4614
      return cache;
1✔
4615
    }
4616

4617
    @Override
4618
    public ConcurrentMap<K, CompletableFuture<V>> asMap() {
4619
      return (mapView == null) ? (mapView = new AsyncAsMapView<>(this)) : mapView;
1✔
4620
    }
4621

4622
    @Override
4623
    public Policy<K, V> policy() {
4624
      if (policy == null) {
1✔
4625
        @SuppressWarnings("unchecked")
4626
        var castCache = (BoundedLocalCache<K, V>) cache;
1✔
4627
        Function<CompletableFuture<V>, @Nullable V> transformer = Async::getIfReady;
1✔
4628
        @SuppressWarnings({"NullAway", "unchecked", "Varifier"})
4629
        Function<@Nullable V, @Nullable V> castTransformer = (Function<V, V>) transformer;
1✔
4630
        policy = new BoundedPolicy<>(castCache, castTransformer, isWeighted);
1✔
4631
      }
4632
      return policy;
1✔
4633
    }
4634

4635
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
4636
      throw new InvalidObjectException("Proxy required");
1✔
4637
    }
4638

4639
    private Object writeReplace() {
4640
      return makeSerializationProxy(cache);
1✔
4641
    }
4642
  }
4643
}
4644

4645
/** The namespace for field padding through inheritance. */
4646
@SuppressWarnings({"MemberName", "MultiVariableDeclaration"})
4647
final class BLCHeader {
4648

4649
  private BLCHeader() {}
4650

4651
  static class PadDrainStatus {
1✔
4652
    byte p000, p001, p002, p003, p004, p005, p006, p007;
4653
    byte p008, p009, p010, p011, p012, p013, p014, p015;
4654
    byte p016, p017, p018, p019, p020, p021, p022, p023;
4655
    byte p024, p025, p026, p027, p028, p029, p030, p031;
4656
    byte p032, p033, p034, p035, p036, p037, p038, p039;
4657
    byte p040, p041, p042, p043, p044, p045, p046, p047;
4658
    byte p048, p049, p050, p051, p052, p053, p054, p055;
4659
    byte p056, p057, p058, p059, p060, p061, p062, p063;
4660
    byte p064, p065, p066, p067, p068, p069, p070, p071;
4661
    byte p072, p073, p074, p075, p076, p077, p078, p079;
4662
    byte p080, p081, p082, p083, p084, p085, p086, p087;
4663
    byte p088, p089, p090, p091, p092, p093, p094, p095;
4664
    byte p096, p097, p098, p099, p100, p101, p102, p103;
4665
    byte p104, p105, p106, p107, p108, p109, p110, p111;
4666
    byte p112, p113, p114, p115, p116, p117, p118, p119;
4667
  }
4668

4669
  /** Enforces a memory layout to avoid false sharing by padding the drain status. */
4670
  abstract static class DrainStatusRef extends PadDrainStatus {
1✔
4671
    static final VarHandle DRAIN_STATUS;
4672

4673
    /** A drain is not taking place. */
4674
    static final int IDLE = 0;
4675
    /** A drain is required due to a pending write modification. */
4676
    static final int REQUIRED = 1;
4677
    /** A drain is in progress and will transition to idle. */
4678
    static final int PROCESSING_TO_IDLE = 2;
4679
    /** A drain is in progress and will transition to required. */
4680
    static final int PROCESSING_TO_REQUIRED = 3;
4681

4682
    /** The draining status of the buffers. */
4683
    volatile int drainStatus = IDLE;
1✔
4684

4685
    /**
4686
     * Returns whether maintenance work is needed.
4687
     *
4688
     * @param delayable if draining the read buffer can be delayed
4689
     */
4690
    boolean shouldDrainBuffers(boolean delayable) {
4691
      switch (drainStatusOpaque()) {
1✔
4692
        case IDLE:
4693
          return !delayable;
1✔
4694
        case REQUIRED:
4695
          return true;
1✔
4696
        case PROCESSING_TO_IDLE:
4697
        case PROCESSING_TO_REQUIRED:
4698
          return false;
1✔
4699
        default:
4700
          throw new IllegalStateException("Invalid drain status: " + drainStatus);
1✔
4701
      }
4702
    }
4703

4704
    int drainStatusOpaque() {
4705
      return (int) DRAIN_STATUS.getOpaque(this);
1✔
4706
    }
4707

4708
    int drainStatusAcquire() {
4709
      return (int) DRAIN_STATUS.getAcquire(this);
1✔
4710
    }
4711

4712
    void setDrainStatusOpaque(int drainStatus) {
4713
      DRAIN_STATUS.setOpaque(this, drainStatus);
1✔
4714
    }
1✔
4715

4716
    void setDrainStatusRelease(int drainStatus) {
4717
      DRAIN_STATUS.setRelease(this, drainStatus);
1✔
4718
    }
1✔
4719

4720
    boolean casDrainStatus(int expect, int update) {
4721
      return DRAIN_STATUS.compareAndSet(this, expect, update);
1✔
4722
    }
4723

4724
    static {
4725
      try {
4726
        DRAIN_STATUS = MethodHandles.lookup()
1✔
4727
            .findVarHandle(DrainStatusRef.class, "drainStatus", int.class);
1✔
4728
      } catch (ReflectiveOperationException e) {
×
4729
        throw new ExceptionInInitializerError(e);
×
4730
      }
1✔
4731
    }
1✔
4732
  }
4733
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc