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

ben-manes / caffeine / #5594

06 Jul 2026 06:08PM UTC coverage: 99.713% (-0.2%) from 99.892%
#5594

push

github

ben-manes
Match ConcurrentHashMap for containsAll on the map views

keySet()/values().containsAll inherited AbstractCollection's loop,
which threw NullPointerException on a null element and iterated even
for a self-check. CHM and Guava both skip a null element (false) and
short-circuit containsAll(self) to true; Caffeine's removeAll and
entrySet were already lenient, leaving these two views the holdout.
Override containsAll across the bounded, unbounded, and async views.
The CaffeinatedGuava facade's override (a workaround for the old NPE)
is now redundant, so drop it and delegate to the view.

4106 of 4132 branches covered (99.37%)

42 of 42 new or added lines in 3 files covered. (100.0%)

17 existing lines in 5 files now uncovered.

8350 of 8374 relevant lines covered (99.71%)

1.0 hits per line

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

99.79
/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.lang.invoke.ConstantBootstraps.fieldVarHandle;
29
import static java.util.Locale.US;
30
import static java.util.Objects.requireNonNull;
31
import static java.util.Spliterator.DISTINCT;
32
import static java.util.Spliterator.IMMUTABLE;
33
import static java.util.Spliterator.NONNULL;
34
import static java.util.Spliterator.ORDERED;
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.CompletionException;
70
import java.util.concurrent.ConcurrentHashMap;
71
import java.util.concurrent.ConcurrentMap;
72
import java.util.concurrent.Executor;
73
import java.util.concurrent.ForkJoinPool;
74
import java.util.concurrent.ForkJoinTask;
75
import java.util.concurrent.ThreadLocalRandom;
76
import java.util.concurrent.TimeUnit;
77
import java.util.concurrent.TimeoutException;
78
import java.util.concurrent.locks.ReentrantLock;
79
import java.util.function.BiConsumer;
80
import java.util.function.BiFunction;
81
import java.util.function.Consumer;
82
import java.util.function.Function;
83
import java.util.function.Predicate;
84
import java.util.stream.Stream;
85
import java.util.stream.StreamSupport;
86

87
import org.jspecify.annotations.NonNull;
88
import org.jspecify.annotations.Nullable;
89

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

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

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

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

210
  /** The number of CPUs */
211
  static final int NCPU = Runtime.getRuntime().availableProcessors();
1✔
212
  /** The initial capacity of the write buffer. */
213
  static final int WRITE_BUFFER_MIN = 4;
214
  /** The maximum capacity of the write buffer. */
215
  static final int WRITE_BUFFER_MAX = 128 * ceilingPowerOfTwo(NCPU);
1✔
216
  /** The maximum weighted capacity of the map. */
217
  static final long MAXIMUM_CAPACITY = Long.MAX_VALUE - Integer.MAX_VALUE;
218
  /** The initial percent of the maximum weighted capacity dedicated to the main space. */
219
  static final double PERCENT_MAIN = 0.99d;
220
  /** The percent of the maximum weighted capacity dedicated to the main's protected space. */
221
  static final double PERCENT_MAIN_PROTECTED = 0.80d;
222
  /** The difference in hit rates that restarts the climber. */
223
  static final double HILL_CLIMBER_RESTART_THRESHOLD = 0.05d;
224
  /** The percent of the total size to adapt the window by. */
225
  static final double HILL_CLIMBER_STEP_PERCENT = 0.0625d;
226
  /** The rate to decrease the step size to adapt by. */
227
  static final double HILL_CLIMBER_STEP_DECAY_RATE = 0.98d;
228
  /** Lower bound on the initial step size so that small caches have an opportunity to adapt. */
229
  static final double HILL_CLIMBER_MIN_INITIAL_STEP = 2.0d;
230
  /** The threshold below which the climber's sample period grows as its step size decays. */
231
  static final long SMALL_CACHE_THRESHOLD = 512L;
232
  /** Maximum factor by which the climber's sample period may grow. */
233
  static final double SMALL_CACHE_SAMPLE_RATIO_CAP = 4.0d;
234
  /** The step decay rate for small caches, slower to keep the step large enough for restarts. */
235
  static final double SMALL_CACHE_STEP_DECAY_RATE = 0.995d;
236
  /** The minimum popularity for allowing randomized admission. */
237
  static final int ADMIT_HASHDOS_THRESHOLD = 6;
238
  /** The maximum number of entries that can be transferred between queues. */
239
  static final int QUEUE_TRANSFER_THRESHOLD = 1_000;
240
  /** The maximum number of entries that can be expired per maintenance cycle. */
241
  static final int EXPIRATION_THRESHOLD = 1_000;
242
  /** The maximum time window between touches for expiration updates. */
243
  static final long EXPIRE_TOLERANCE = TimeUnit.SECONDS.toNanos(1);
1✔
244
  /** The maximum duration before an entry expires. */
245
  static final long MAXIMUM_EXPIRY = (Long.MAX_VALUE >> 1); // 150 years
246
  /** The duration to wait on the eviction lock before warning of a possible misuse. */
247
  static final long WARN_AFTER_LOCK_WAIT_NANOS = TimeUnit.SECONDS.toNanos(30);
1✔
248
  /** The number of retries before computing to validate the entry's integrity; pow2 modulus. */
249
  static final int MAX_PUT_SPIN_WAIT_ATTEMPTS = 1024 - 1;
250
  /** The handle for the in-flight refresh operations. */
251
  static final VarHandle REFRESHES = fieldVarHandle(MethodHandles.lookup(),
1✔
252
      "refreshes", VarHandle.class, BoundedLocalCache.class, ConcurrentMap.class);
253

254
  final @Nullable RemovalListener<K, V> evictionListener;
255
  final @Nullable AsyncCacheLoader<K, V> cacheLoader;
256

257
  final MpscGrowableArrayQueue<Runnable> writeBuffer;
258
  final ConcurrentHashMap<Object, Node<K, V>> data;
259
  final PerformCleanupTask drainBuffersTask;
260
  final Consumer<Node<K, V>> accessPolicy;
261
  final Buffer<Node<K, V>> readBuffer;
262
  final NodeFactory<K, V> nodeFactory;
263
  final ReentrantLock evictionLock;
264
  final Weigher<K, V> weigher;
265
  final Executor executor;
266

267
  final boolean isWeighted;
268
  final boolean isAsync;
269

270
  @Nullable Set<K> keySet;
271
  @Nullable Collection<V> values;
272
  @Nullable Set<Entry<K, V>> entrySet;
273
  volatile @Nullable ConcurrentMap<Object, CompletableFuture<?>> refreshes;
274

275
  /** Creates an instance based on the builder's configuration. */
276
  @SuppressWarnings("GuardedBy")
277
  protected BoundedLocalCache(Caffeine<K, V> builder,
278
      @Nullable AsyncCacheLoader<K, V> cacheLoader, boolean isAsync) {
1✔
279
    this.isAsync = isAsync;
1✔
280
    this.cacheLoader = cacheLoader;
1✔
281
    executor = builder.getExecutor();
1✔
282
    isWeighted = builder.isWeighted();
1✔
283
    evictionLock = new ReentrantLock();
1✔
284
    weigher = builder.getWeigher(isAsync);
1✔
285
    drainBuffersTask = new PerformCleanupTask(this);
1✔
286
    nodeFactory = NodeFactory.newFactory(builder, isAsync);
1✔
287
    evictionListener = builder.getEvictionListener(isAsync);
1✔
288
    data = new ConcurrentHashMap<>(builder.getInitialCapacity());
1✔
289
    readBuffer = evicts() || collectKeys() || collectValues() || expiresAfterAccess()
1✔
290
        ? new BoundedBuffer<>()
1✔
291
        : Buffer.disabled();
1✔
292
    accessPolicy = (evicts() || expiresAfterAccess()) ? this::onAccess : e -> {};
1✔
293
    writeBuffer = new MpscGrowableArrayQueue<>(WRITE_BUFFER_MIN, WRITE_BUFFER_MAX);
1✔
294

295
    if (evicts()) {
1✔
296
      setMaximumSize(builder.getMaximum());
1✔
297
    }
298
  }
1✔
299

300
  /** Ensures that the node is alive during the map operation. */
301
  void requireIsAlive(Object key, Node<?, ?> node) {
302
    if (!node.isAlive()) {
1✔
303
      throw new IllegalStateException(brokenEqualityMessage(key, node));
1✔
304
    }
305
  }
1✔
306

307
  /** Logs if the node cannot be found in the map but is still alive. */
308
  void logIfAlive(Node<?, ?> node) {
309
    if (node.isAlive()) {
1✔
310
      String message = brokenEqualityMessage(node.getKeyReference(), node);
1✔
311
      logger.log(Level.ERROR, message, new IllegalStateException());
1✔
312
    }
313
  }
1✔
314

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

324
  /** Returns the exception as unchecked, wrapping checked exceptions in a CompletionException. */
325
  static RuntimeException toUncheckedException(Throwable t) {
326
    if (t instanceof Error) {
1✔
327
      throw (Error) t;
1✔
328
    }
329
    return (t instanceof RuntimeException) ? (RuntimeException) t : new CompletionException(t);
1✔
330
  }
331

332
  /* --------------- Shared --------------- */
333

334
  @Override
335
  public boolean isAsync() {
336
    return isAsync;
1✔
337
  }
338

339
  /** Returns if the node's value is currently being computed asynchronously. */
340
  final boolean isComputingAsync(@Nullable V value) {
341
    return isAsync && !Async.isReady((CompletableFuture<?>) value);
1✔
342
  }
343

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

349
  @GuardedBy("evictionLock")
350
  protected AccessOrderDeque<Node<K, V>> accessOrderProbationDeque() {
351
    throw new UnsupportedOperationException();
1✔
352
  }
353

354
  @GuardedBy("evictionLock")
355
  protected AccessOrderDeque<Node<K, V>> accessOrderProtectedDeque() {
356
    throw new UnsupportedOperationException();
1✔
357
  }
358

359
  @GuardedBy("evictionLock")
360
  protected WriteOrderDeque<Node<K, V>> writeOrderDeque() {
361
    throw new UnsupportedOperationException();
1✔
362
  }
363

364
  @Override
365
  public final Executor executor() {
366
    return executor;
1✔
367
  }
368

369
  @Override
370
  public ConcurrentMap<Object, CompletableFuture<?>> refreshes() {
371
    @Var var pending = refreshes;
1✔
372
    if (pending == null) {
1✔
373
      pending = new ConcurrentHashMap<>();
1✔
374
      if (!REFRESHES.compareAndSet(this, null, pending)) {
1✔
375
        pending = requireNonNull(refreshes);
1✔
376
      }
377
    }
378
    return pending;
1✔
379
  }
380

381
  /** Invalidate the in-flight refresh. */
382
  @SuppressWarnings("RedundantCollectionOperation")
383
  void discardRefresh(Object keyReference) {
384
    var pending = refreshes;
1✔
385
    if ((pending != null) && pending.containsKey(keyReference)) {
1✔
386
      pending.remove(keyReference);
1✔
387
    }
388
  }
1✔
389

390
  @Override
391
  public Object referenceKey(K key) {
392
    return nodeFactory.newLookupKey(key);
1✔
393
  }
394

395
  @Override
396
  public boolean isPendingEviction(K key) {
397
    Node<K, V> node = data.get(nodeFactory.newLookupKey(key));
1✔
398
    if (node == null) {
1✔
399
      return false;
1✔
400
    }
401
    V value = node.getValue();
1✔
402
    return (value == null) || hasExpired(node, expirationTicker().read(), value);
1✔
403
  }
404

405
  /* --------------- Stats Support --------------- */
406

407
  @Override
408
  public boolean isRecordingStats() {
409
    return false;
1✔
410
  }
411

412
  @Override
413
  public StatsCounter statsCounter() {
414
    return StatsCounter.disabledStatsCounter();
1✔
415
  }
416

417
  @Override
418
  public Ticker statsTicker() {
419
    return Ticker.disabledTicker();
1✔
420
  }
421

422
  /* --------------- Removal Listener Support --------------- */
423

424
  @Override
425
  public @Nullable RemovalListener<K, V> removalListener() {
426
    return null;
1✔
427
  }
428

429
  @Override
430
  public void notifyRemoval(@Nullable K key, @Nullable V value, RemovalCause cause) {
431
    var removalListener = removalListener();
1✔
432
    if (removalListener == null) {
1✔
433
      return;
1✔
434
    }
435
    Runnable task = () -> {
1✔
436
      try {
437
        removalListener.onRemoval(key, value, cause);
1✔
438
      } catch (Throwable t) {
1✔
439
        logger.log(Level.WARNING, "Exception thrown by removal listener", t);
1✔
440
      }
1✔
441
    };
1✔
442
    try {
443
      executor.execute(task);
1✔
444
    } catch (Throwable t) {
1✔
445
      logger.log(Level.ERROR, "Exception thrown when submitting removal listener", t);
1✔
446
      task.run();
1✔
447
    }
1✔
448
  }
1✔
449

450
  /* --------------- Eviction Listener Support --------------- */
451

452
  void notifyEviction(@Nullable K key, @Nullable V value, RemovalCause cause) {
453
    if (evictionListener == null) {
1✔
454
      return;
1✔
455
    }
456
    try {
457
      evictionListener.onRemoval(key, value, cause);
1✔
458
    } catch (Throwable t) {
1✔
459
      logger.log(Level.WARNING, "Exception thrown by eviction listener", t);
1✔
460
    }
1✔
461
  }
1✔
462

463
  /* --------------- Reference Support --------------- */
464

465
  @Override
466
  public boolean collectKeys() {
467
    return false;
1✔
468
  }
469

470
  /** Returns if the values are weak or soft reference garbage collected. */
471
  protected boolean collectValues() {
472
    return false;
1✔
473
  }
474

475
  @SuppressWarnings({"DataFlowIssue", "NullAway"})
476
  protected ReferenceQueue<K> keyReferenceQueue() {
477
    return null;
1✔
478
  }
479

480
  @SuppressWarnings({"DataFlowIssue", "NullAway"})
481
  protected ReferenceQueue<V> valueReferenceQueue() {
482
    return null;
1✔
483
  }
484

485
  /* --------------- Expiration Support --------------- */
486

487
  /** Returns the {@link Pacer} used to schedule the maintenance task. */
488
  protected @Nullable Pacer pacer() {
489
    return null;
1✔
490
  }
491

492
  /** Returns if the cache expires entries after a variable time threshold. */
493
  protected boolean expiresVariable() {
494
    return false;
1✔
495
  }
496

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

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

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

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

516
  /** Returns how long after the last write to an entry the map will retain that entry. */
517
  protected long expiresAfterWriteNanos() {
518
    throw new UnsupportedOperationException();
1✔
519
  }
520

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

525
  /** Returns if the cache refreshes entries after a write time threshold. */
526
  protected boolean refreshAfterWrite() {
527
    return false;
1✔
528
  }
529

530
  /** Returns how long after the last write an entry becomes a candidate for refresh. */
531
  protected long refreshAfterWriteNanos() {
532
    throw new UnsupportedOperationException();
1✔
533
  }
534

535
  protected void setRefreshAfterWriteNanos(long refreshAfterWriteNanos) {
536
    throw new UnsupportedOperationException();
1✔
537
  }
538

539
  @Override
540
  @SuppressWarnings({"DataFlowIssue", "NullAway"})
541
  public Expiry<K, V> expiry() {
542
    return null;
1✔
543
  }
544

545
  /** Returns the {@link Ticker} used by this cache for expiration. */
546
  public Ticker expirationTicker() {
547
    return Ticker.disabledTicker();
1✔
548
  }
549

550
  protected TimerWheel<K, V> timerWheel() {
551
    throw new UnsupportedOperationException();
1✔
552
  }
553

554
  /* --------------- Eviction Support --------------- */
555

556
  /** Returns if the cache evicts entries due to a maximum size or weight threshold. */
557
  protected boolean evicts() {
558
    return false;
1✔
559
  }
560

561
  /** Returns if entries may be assigned different weights. */
562
  protected boolean isWeighted() {
563
    return (weigher != Weigher.singletonWeigher());
1✔
564
  }
565

566
  protected FrequencySketch frequencySketch() {
567
    throw new UnsupportedOperationException();
1✔
568
  }
569

570
  /** Returns if an access to an entry can skip notifying the eviction policy. */
571
  protected boolean fastpath() {
572
    return false;
1✔
573
  }
574

575
  /** Returns the maximum weighted size. */
576
  protected long maximum() {
577
    throw new UnsupportedOperationException();
1✔
578
  }
579

580
  /** Returns the maximum weighted size. */
581
  protected long maximumAcquire() {
582
    throw new UnsupportedOperationException();
1✔
583
  }
584

585
  /** Returns the maximum weighted size of the window space. */
586
  protected long windowMaximum() {
587
    throw new UnsupportedOperationException();
1✔
588
  }
589

590
  /** Returns the maximum weighted size of the main's protected space. */
591
  protected long mainProtectedMaximum() {
592
    throw new UnsupportedOperationException();
1✔
593
  }
594

595
  @GuardedBy("evictionLock")
596
  protected void setMaximum(long maximum) {
597
    throw new UnsupportedOperationException();
1✔
598
  }
599

600
  @GuardedBy("evictionLock")
601
  protected void setWindowMaximum(long maximum) {
602
    throw new UnsupportedOperationException();
1✔
603
  }
604

605
  @GuardedBy("evictionLock")
606
  protected void setMainProtectedMaximum(long maximum) {
607
    throw new UnsupportedOperationException();
1✔
608
  }
609

610
  /** Returns the combined weight of the values in the cache (may be negative). */
611
  protected long weightedSize() {
612
    throw new UnsupportedOperationException();
1✔
613
  }
614

615
  /** Returns the combined weight of the values in the cache (may be negative). */
616
  protected long weightedSizeAcquire() {
617
    throw new UnsupportedOperationException();
1✔
618
  }
619

620
  /** Returns the uncorrected combined weight of the values in the window space. */
621
  protected long windowWeightedSize() {
622
    throw new UnsupportedOperationException();
1✔
623
  }
624

625
  /** Returns the uncorrected combined weight of the values in the main's protected space. */
626
  protected long mainProtectedWeightedSize() {
627
    throw new UnsupportedOperationException();
1✔
628
  }
629

630
  @GuardedBy("evictionLock")
631
  protected void setWeightedSize(long weightedSize) {
632
    throw new UnsupportedOperationException();
1✔
633
  }
634

635
  @GuardedBy("evictionLock")
636
  protected void setWindowWeightedSize(long weightedSize) {
637
    throw new UnsupportedOperationException();
1✔
638
  }
639

640
  @GuardedBy("evictionLock")
641
  protected void setMainProtectedWeightedSize(long weightedSize) {
642
    throw new UnsupportedOperationException();
1✔
643
  }
644

645
  protected long hitsInSample() {
646
    throw new UnsupportedOperationException();
1✔
647
  }
648

649
  protected long missesInSample() {
650
    throw new UnsupportedOperationException();
1✔
651
  }
652

653
  protected double stepSize() {
654
    throw new UnsupportedOperationException();
1✔
655
  }
656

657
  protected double previousSampleHitRate() {
658
    throw new UnsupportedOperationException();
1✔
659
  }
660

661
  protected long adjustment() {
662
    throw new UnsupportedOperationException();
1✔
663
  }
664

665
  @GuardedBy("evictionLock")
666
  protected void setHitsInSample(long hitCount) {
667
    throw new UnsupportedOperationException();
1✔
668
  }
669

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

675
  @GuardedBy("evictionLock")
676
  protected void setStepSize(double stepSize) {
677
    throw new UnsupportedOperationException();
1✔
678
  }
679

680
  @GuardedBy("evictionLock")
681
  protected void setPreviousSampleHitRate(double hitRate) {
682
    throw new UnsupportedOperationException();
1✔
683
  }
684

685
  @GuardedBy("evictionLock")
686
  protected void setAdjustment(long amount) {
687
    throw new UnsupportedOperationException();
1✔
688
  }
689

690
  /**
691
   * Sets the maximum weighted size of the cache. The caller may need to perform a maintenance cycle
692
   * to eagerly evicts entries until the cache shrinks to the appropriate size.
693
   */
694
  @GuardedBy("evictionLock")
695
  @SuppressWarnings({"ConstantValue", "Varifier"})
696
  void setMaximumSize(long maximum) {
697
    requireArgument(maximum >= 0, "maximum must not be negative");
1✔
698
    if (maximum == maximum()) {
1✔
699
      return;
1✔
700
    }
701

702
    long max = Math.min(maximum, MAXIMUM_CAPACITY);
1✔
703
    long window = max - (long) (PERCENT_MAIN * max);
1✔
704
    long mainProtected = (long) (PERCENT_MAIN_PROTECTED * (max - window));
1✔
705
    double stepSize = Math.max(HILL_CLIMBER_STEP_PERCENT * max, HILL_CLIMBER_MIN_INITIAL_STEP);
1✔
706

707
    setMaximum(max);
1✔
708
    setWindowMaximum(window);
1✔
709
    setMainProtectedMaximum(mainProtected);
1✔
710

711
    setHitsInSample(0);
1✔
712
    setMissesInSample(0);
1✔
713
    setStepSize((max <= SMALL_CACHE_THRESHOLD) ? stepSize : -stepSize);
1✔
714

715
    if ((frequencySketch() != null) && !isWeighted() && (weightedSize() >= (max >>> 1))) {
1✔
716
      // Lazily initialize when close to the maximum size
717
      frequencySketch().ensureCapacity(max);
1✔
718
    }
719
  }
1✔
720

721
  /** Evicts entries if the cache exceeds the maximum. */
722
  @GuardedBy("evictionLock")
723
  void evictEntries() {
724
    if (!evicts()) {
1✔
725
      return;
1✔
726
    }
727
    var candidate = evictFromWindow();
1✔
728
    evictFromMain(candidate);
1✔
729
  }
1✔
730

731
  /**
732
   * Evicts entries from the window space into the main space while the window size exceeds a
733
   * maximum.
734
   *
735
   * @return the first candidate promoted into the probation space
736
   */
737
  @GuardedBy("evictionLock")
738
  @Nullable Node<K, V> evictFromWindow() {
739
    @Var Node<K, V> first = null;
1✔
740
    @Var Node<K, V> node = accessOrderWindowDeque().peekFirst();
1✔
741
    while (windowWeightedSize() > windowMaximum()) {
1✔
742
      // The pending operations will adjust the size to reflect the correct weight
743
      if (node == null) {
1✔
744
        break;
1✔
745
      }
746

747
      Node<K, V> next = node.getNextInAccessOrder();
1✔
748
      if (node.getPolicyWeight() != 0) {
1✔
749
        node.makeMainProbation();
1✔
750
        accessOrderWindowDeque().remove(node);
1✔
751
        accessOrderProbationDeque().offerLast(node);
1✔
752
        if (first == null) {
1✔
753
          first = node;
1✔
754
        }
755

756
        setWindowWeightedSize(windowWeightedSize() - node.getPolicyWeight());
1✔
757
      }
758
      node = next;
1✔
759
    }
1✔
760

761
    return first;
1✔
762
  }
763

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

793
      // Try evicting from the protected and window queues
794
      if ((candidate == null) && (victim == null)) {
1✔
795
        if (victimQueue == PROBATION) {
1✔
796
          victim = accessOrderProtectedDeque().peekFirst();
1✔
797
          victimQueue = PROTECTED;
1✔
798
          continue;
1✔
799
        } else if (victimQueue == PROTECTED) {
1✔
800
          victim = accessOrderWindowDeque().peekFirst();
1✔
801
          victimQueue = WINDOW;
1✔
802
          continue;
1✔
803
        }
804

805
        // The pending operations will adjust the size to reflect the correct weight
806
        break;
807
      }
808

809
      // Skip over entries with zero weight
810
      if ((victim != null) && (victim.getPolicyWeight() == 0)) {
1✔
811
        victim = victim.getNextInAccessOrder();
1✔
812
        continue;
1✔
813
      } else if ((candidate != null) && (candidate.getPolicyWeight() == 0)) {
1✔
814
        candidate = candidate.getNextInAccessOrder();
1✔
815
        continue;
1✔
816
      }
817

818
      // Evict immediately if only one of the entries is present
819
      if (victim == null) {
1✔
820
        requireNonNull(candidate);
1✔
821
        Node<K, V> previous = candidate.getNextInAccessOrder();
1✔
822
        Node<K, V> evict = candidate;
1✔
823
        candidate = previous;
1✔
824
        evictEntry(evict, RemovalCause.SIZE, 0L);
1✔
825
        continue;
1✔
826
      } else if (candidate == null) {
1✔
827
        Node<K, V> evict = victim;
1✔
828
        victim = victim.getNextInAccessOrder();
1✔
829
        evictEntry(evict, RemovalCause.SIZE, 0L);
1✔
830
        continue;
1✔
831
      }
832

833
      // Evict immediately if both selected the same entry
834
      if (candidate == victim) {
1✔
835
        victim = victim.getNextInAccessOrder();
1✔
836
        evictEntry(candidate, RemovalCause.SIZE, 0L);
1✔
837
        candidate = null;
1✔
838
        continue;
1✔
839
      }
840

841
      // Evict immediately if an entry was collected
842
      var victimKeyRef = victim.getKeyReferenceOrNull();
1✔
843
      var candidateKeyRef = candidate.getKeyReferenceOrNull();
1✔
844
      if (victimKeyRef == null) {
1✔
845
        Node<K, V> evict = victim;
1✔
846
        victim = victim.getNextInAccessOrder();
1✔
847
        evictEntry(evict, RemovalCause.COLLECTED, 0L);
1✔
848
        continue;
1✔
849
      } else if (candidateKeyRef == null) {
1✔
850
        Node<K, V> evict = candidate;
1✔
851
        candidate = candidate.getNextInAccessOrder();
1✔
852
        evictEntry(evict, RemovalCause.COLLECTED, 0L);
1✔
853
        continue;
1✔
854
      }
855

856
      // Evict immediately if an entry was removed
857
      if (!victim.isAlive()) {
1✔
858
        Node<K, V> evict = victim;
1✔
859
        victim = victim.getNextInAccessOrder();
1✔
860
        evictEntry(evict, RemovalCause.SIZE, 0L);
1✔
861
        continue;
1✔
862
      } else if (!candidate.isAlive()) {
1✔
863
        Node<K, V> evict = candidate;
1✔
864
        candidate = candidate.getNextInAccessOrder();
1✔
865
        evictEntry(evict, RemovalCause.SIZE, 0L);
1✔
866
        continue;
1✔
867
      }
868

869
      // Evict immediately if the candidate's weight exceeds the maximum
870
      if (candidate.getPolicyWeight() > maximum()) {
1✔
871
        Node<K, V> evict = candidate;
1✔
872
        candidate = candidate.getNextInAccessOrder();
1✔
873
        evictEntry(evict, RemovalCause.SIZE, 0L);
1✔
874
        continue;
1✔
875
      }
876

877
      // Evict the entry with the lowest frequency
878
      if (admit(candidateKeyRef, victimKeyRef)) {
1✔
879
        Node<K, V> evict = victim;
1✔
880
        victim = victim.getNextInAccessOrder();
1✔
881
        evictEntry(evict, RemovalCause.SIZE, 0L);
1✔
882
        candidate = candidate.getNextInAccessOrder();
1✔
883
      } else {
1✔
884
        Node<K, V> evict = candidate;
1✔
885
        candidate = candidate.getNextInAccessOrder();
1✔
886
        evictEntry(evict, RemovalCause.SIZE, 0L);
1✔
887
      }
888
    }
1✔
889
  }
1✔
890

891
  /**
892
   * Determines if the candidate should be accepted into the main space, as determined by its
893
   * frequency relative to the victim. A small amount of randomness is used to protect against hash
894
   * collision attacks, where the victim's frequency is artificially raised so that no new entries
895
   * are admitted.
896
   *
897
   * @param candidateKeyRef the keyRef for the entry being proposed for long term retention
898
   * @param victimKeyRef the keyRef for the entry chosen by the eviction policy for replacement
899
   * @return if the candidate should be admitted and the victim ejected
900
   */
901
  @GuardedBy("evictionLock")
902
  boolean admit(Object candidateKeyRef, Object victimKeyRef) {
903
    int candidateFreq = frequencySketch().frequency(candidateKeyRef);
1✔
904
    int victimFreq = frequencySketch().frequency(victimKeyRef);
1✔
905
    if (candidateFreq > victimFreq) {
1✔
906
      return true;
1✔
907
    } else if (candidateFreq >= ADMIT_HASHDOS_THRESHOLD) {
1✔
908
      // The maximum frequency is 15 and halved to 7 after a reset to age the history. An attack
909
      // exploits that a hot candidate is rejected in favor of a hot victim. The threshold of a warm
910
      // candidate reduces the number of random acceptances to minimize the impact on the hit rate.
911
      int random = ThreadLocalRandom.current().nextInt();
1✔
912
      return ((random & 127) == 0);
1✔
913
    }
914
    return false;
1✔
915
  }
916

917
  /** Expires entries that have expired by access, write, or variable. */
918
  @GuardedBy("evictionLock")
919
  void expireEntries() {
920
    long now = expirationTicker().read();
1✔
921
    expireAfterAccessEntries(now);
1✔
922
    expireAfterWriteEntries(now);
1✔
923
    expireVariableEntries(now);
1✔
924

925
    Pacer pacer = pacer();
1✔
926
    if (pacer != null) {
1✔
927
      long delay = getExpirationDelay(now);
1✔
928
      if (delay == Long.MAX_VALUE) {
1✔
929
        pacer.cancel();
1✔
930
      } else {
931
        pacer.schedule(executor, drainBuffersTask, now, delay);
1✔
932
      }
933
    }
934
  }
1✔
935

936
  /** Expires entries in the access-order queue. */
937
  @GuardedBy("evictionLock")
938
  void expireAfterAccessEntries(long now) {
939
    if (!expiresAfterAccess()) {
1✔
940
      return;
1✔
941
    }
942

943
    @Var int remaining = EXPIRATION_THRESHOLD;
1✔
944
    remaining = expireAfterAccessEntries(now, accessOrderWindowDeque(), remaining);
1✔
945
    if (evicts()) {
1✔
946
      remaining = expireAfterAccessEntries(now, accessOrderProbationDeque(), remaining);
1✔
947
      remaining = expireAfterAccessEntries(now, accessOrderProtectedDeque(), remaining);
1✔
948
    }
949
    if (remaining == 0) {
1✔
950
      setDrainStatusOpaque(PROCESSING_TO_REQUIRED);
1✔
951
    }
952
  }
1✔
953

954
  /**
955
   * Expires entries in an access-order queue, up to the {@code remaining} budget, and returns the
956
   * unused budget. When exhausted the caller re-arms maintenance to process the backlog.
957
   */
958
  @GuardedBy("evictionLock")
959
  int expireAfterAccessEntries(long now,
960
      AccessOrderDeque<Node<K, V>> accessOrderDeque, @Var int remaining) {
961
    var head = accessOrderDeque.peekFirst();
1✔
962
    if (head == null) {
1✔
963
      return remaining;
1✔
964
    }
965
    long duration = expiresAfterAccessNanos();
1✔
966
    var last = requireNonNull(accessOrderDeque.peekLast());
1✔
967
    for (var node = head; (node != null) && (remaining > 0);) {
1✔
968
      var next = (node == last) ? null : node.getNextInAccessOrder();
1✔
969
      if ((now - node.getAccessTime()) < duration) {
1✔
970
        boolean stalePosition = ((last.getAccessTime() - node.getAccessTime()) < 0);
1✔
971
        if (stalePosition || isComputingAsync(node.getValue())) {
1✔
972
          accessOrderDeque.moveToBack(node);
1✔
973
          node = next;
1✔
974
          continue;
1✔
975
        }
976
        return remaining;
1✔
977
      }
978
      evictEntry(node, RemovalCause.EXPIRED, now);
1✔
979
      remaining--;
1✔
980
      node = next;
1✔
981
    }
1✔
982
    return remaining;
1✔
983
  }
984

985
  /** Expires entries on the write-order queue. */
986
  @GuardedBy("evictionLock")
987
  void expireAfterWriteEntries(long now) {
988
    if (!expiresAfterWrite()) {
1✔
989
      return;
1✔
990
    }
991

992
    var head = writeOrderDeque().peekFirst();
1✔
993
    if (head == null) {
1✔
994
      return;
1✔
995
    }
996
    long duration = expiresAfterWriteNanos();
1✔
997
    @Var int remaining = EXPIRATION_THRESHOLD;
1✔
998
    var last = requireNonNull(writeOrderDeque().peekLast());
1✔
999
    for (var node = head; (node != null) && (remaining > 0);) {
1✔
1000
      var next = (node == last) ? null : node.getNextInWriteOrder();
1✔
1001
      if ((now - node.getWriteTime()) < duration) {
1✔
1002
        boolean stalePosition = ((last.getWriteTime() - node.getWriteTime()) < 0);
1✔
1003
        if (stalePosition || isComputingAsync(node.getValue())) {
1✔
1004
          writeOrderDeque().moveToBack(node);
1✔
1005
          node = next;
1✔
1006
          continue;
1✔
1007
        }
1008
        return;
1✔
1009
      }
1010
      evictEntry(node, RemovalCause.EXPIRED, now);
1✔
1011
      remaining--;
1✔
1012
      node = next;
1✔
1013
    }
1✔
1014
    if (remaining == 0) {
1✔
1015
      setDrainStatusOpaque(PROCESSING_TO_REQUIRED);
1✔
1016
    }
1017
  }
1✔
1018

1019
  /** Expires entries in the timer wheel. */
1020
  @GuardedBy("evictionLock")
1021
  void expireVariableEntries(long now) {
1022
    if (expiresVariable() && (timerWheel().advance(this, now, EXPIRATION_THRESHOLD) == 0)) {
1✔
1023
      setDrainStatusOpaque(PROCESSING_TO_REQUIRED);
1✔
1024
    }
1025
  }
1✔
1026

1027
  /** Returns the duration until the next item expires, or {@link Long#MAX_VALUE} if none. */
1028
  @GuardedBy("evictionLock")
1029
  long getExpirationDelay(long now) {
1030
    @Var long delay = Long.MAX_VALUE;
1✔
1031
    if (expiresAfterAccess()) {
1✔
1032
      @Var Node<K, V> node = accessOrderWindowDeque().peekFirst();
1✔
1033
      if (node != null) {
1✔
1034
        long age = Math.max(0, now - node.getAccessTime());
1✔
1035
        delay = Math.min(delay, expiresAfterAccessNanos() - age);
1✔
1036
      }
1037
      if (evicts()) {
1✔
1038
        node = accessOrderProbationDeque().peekFirst();
1✔
1039
        if (node != null) {
1✔
1040
          long age = Math.max(0, now - node.getAccessTime());
1✔
1041
          delay = Math.min(delay, expiresAfterAccessNanos() - age);
1✔
1042
        }
1043
        node = accessOrderProtectedDeque().peekFirst();
1✔
1044
        if (node != null) {
1✔
1045
          long age = Math.max(0, now - node.getAccessTime());
1✔
1046
          delay = Math.min(delay, expiresAfterAccessNanos() - age);
1✔
1047
        }
1048
      }
1049
    }
1050
    if (expiresAfterWrite()) {
1✔
1051
      Node<K, V> node = writeOrderDeque().peekFirst();
1✔
1052
      if (node != null) {
1✔
1053
        long age = Math.max(0, now - node.getWriteTime());
1✔
1054
        delay = Math.min(delay, expiresAfterWriteNanos() - age);
1✔
1055
      }
1056
    }
1057
    if (expiresVariable()) {
1✔
1058
      delay = Math.min(delay, timerWheel().getExpirationDelay());
1✔
1059
    }
1060
    return delay;
1✔
1061
  }
1062

1063
  /** Returns if the entry has expired. */
1064
  @SuppressWarnings("ShortCircuitBoolean")
1065
  boolean hasExpired(Node<K, V> node, long now, V value) {
1066
    if (isComputingAsync(value)) {
1✔
1067
      return false;
1✔
1068
    }
1069
    return (expiresAfterAccess() && (now - node.getAccessTime() >= expiresAfterAccessNanos()))
1✔
1070
        | (expiresAfterWrite() && (now - node.getWriteTime() >= expiresAfterWriteNanos()))
1✔
1071
        | (expiresVariable() && (now - node.getVariableTime() >= 0));
1✔
1072
  }
1073

1074
  /**
1075
   * Attempts to evict the entry based on the given removal cause. A removal may be ignored if the
1076
   * entry was updated and is no longer eligible for eviction.
1077
   *
1078
   * @param node the entry to evict
1079
   * @param cause the reason to evict
1080
   * @param now the current time, used only if expiring
1081
   * @return if the entry was evicted
1082
   */
1083
  @GuardedBy("evictionLock")
1084
  @SuppressWarnings({"GuardedByChecker", "SynchronizationOnLocalVariableOrMethodParameter"})
1085
  boolean evictEntry(Node<K, V> node, RemovalCause cause, long now) {
1086
    K key = node.getKey();
1✔
1087
    var ctx = new EvictContext<V>();
1✔
1088
    var keyReference = node.getKeyReference();
1✔
1089

1090
    data.computeIfPresent(keyReference, (k, n) -> {
1✔
1091
      if (n != node) {
1✔
1092
        return n;
1✔
1093
      }
1094
      synchronized (node) {
1✔
1095
        ctx.value = node.getValue();
1✔
1096

1097
        if ((key == null) || (ctx.value == null)) {
1✔
1098
          ctx.cause = RemovalCause.COLLECTED;
1✔
1099
        } else if (cause == RemovalCause.COLLECTED) {
1✔
1100
          ctx.resurrect = true;
1✔
1101
          return node;
1✔
1102
        } else {
1103
          ctx.cause = cause;
1✔
1104
        }
1105

1106
        if (ctx.cause == RemovalCause.EXPIRED) {
1✔
1107
          @Var boolean expired = false;
1✔
1108
          if (expiresAfterAccess()) {
1✔
1109
            expired |= ((now - node.getAccessTime()) >= expiresAfterAccessNanos());
1✔
1110
          }
1111
          if (expiresAfterWrite()) {
1✔
1112
            expired |= ((now - node.getWriteTime()) >= expiresAfterWriteNanos());
1✔
1113
          }
1114
          if (expiresVariable()) {
1✔
1115
            expired |= ((now - node.getVariableTime()) >= 0);
1✔
1116
          }
1117
          if (expired) {
1✔
1118
            if (isComputingAsync(ctx.value)) {
1✔
1119
              long sentinel = (now + ASYNC_EXPIRY);
1✔
1120
              setVariableTime(node, sentinel);
1✔
1121
              setAccessTime(node, sentinel);
1✔
1122
              setWriteTime(node, sentinel);
1✔
1123
              ctx.resurrect = true;
1✔
1124
              return node;
1✔
1125
            }
1126
          } else {
1127
            ctx.resurrect = true;
1✔
1128
            return node;
1✔
1129
          }
1130
        } else if (ctx.cause == RemovalCause.SIZE) {
1✔
1131
          int weight = node.getWeight();
1✔
1132
          if (weight == 0) {
1✔
1133
            ctx.resurrect = true;
1✔
1134
            return node;
1✔
1135
          }
1136
        }
1137

1138
        notifyEviction(key, ctx.value, ctx.cause);
1✔
1139
        discardRefresh(keyReference);
1✔
1140
        ctx.removed = true;
1✔
1141
        node.retire();
1✔
1142
        return null;
1✔
1143
      }
1144
    });
1145

1146
    // The entry is no longer eligible for eviction
1147
    if (ctx.resurrect) {
1✔
1148
      return false;
1✔
1149
    }
1150

1151
    // If the eviction fails due to a concurrent removal of the victim, that removal may cancel out
1152
    // the addition that triggered this eviction. The victim is eagerly unlinked and the size
1153
    // decremented before the removal task so that if an eviction is still required then a new
1154
    // victim will be chosen for removal.
1155
    if (node.inWindow() && (evicts() || expiresAfterAccess())) {
1✔
1156
      accessOrderWindowDeque().remove(node);
1✔
1157
    } else if (evicts()) {
1✔
1158
      if (node.inMainProbation()) {
1✔
1159
        accessOrderProbationDeque().remove(node);
1✔
1160
      } else {
1161
        accessOrderProtectedDeque().remove(node);
1✔
1162
      }
1163
    }
1164
    if (expiresAfterWrite()) {
1✔
1165
      writeOrderDeque().remove(node);
1✔
1166
    } else if (expiresVariable()) {
1✔
1167
      timerWheel().deschedule(node);
1✔
1168
    }
1169

1170
    synchronized (node) {
1✔
1171
      logIfAlive(node);
1✔
1172
      makeDead(node);
1✔
1173
    }
1✔
1174

1175
    if (ctx.removed) {
1✔
1176
      var removeCause = requireNonNull(ctx.cause);
1✔
1177
      statsCounter().recordEviction(node.getWeight(), removeCause);
1✔
1178
      notifyRemoval(key, ctx.value, removeCause);
1✔
1179
    }
1180

1181
    return true;
1✔
1182
  }
1183

1184
  /** Adapts the eviction policy to towards the optimal recency / frequency configuration. */
1185
  @GuardedBy("evictionLock")
1186
  @SuppressWarnings("UnnecessaryReturnStatement")
1187
  void climb() {
1188
    if (!evicts()) {
1✔
1189
      return;
1✔
1190
    }
1191
    determineAdjustment();
1✔
1192
    demoteFromMainProtected();
1✔
1193
    long amount = adjustment();
1✔
1194
    if (amount == 0) {
1✔
1195
      return;
1✔
1196
    } else if (amount > 0) {
1✔
1197
      increaseWindow();
1✔
1198
    } else {
1199
      decreaseWindow();
1✔
1200
    }
1201
  }
1✔
1202

1203
  /** Calculates the amount to adapt the window by and sets {@link #adjustment()} accordingly. */
1204
  @GuardedBy("evictionLock")
1205
  @SuppressWarnings("MathClampDouble")
1206
  void determineAdjustment() {
1207
    if (frequencySketch().isNotInitialized()) {
1✔
1208
      setPreviousSampleHitRate(0.0);
1✔
1209
      setMissesInSample(0);
1✔
1210
      setHitsInSample(0);
1✔
1211
      return;
1✔
1212
    }
1213

1214
    long requestCount = hitsInSample() + missesInSample();
1✔
1215
    @Var double stepDecayRate = HILL_CLIMBER_STEP_DECAY_RATE;
1✔
1216
    @Var long effectiveSampleSize = frequencySketch().sampleSize;
1✔
1217
    if (maximum() <= SMALL_CACHE_THRESHOLD) {
1✔
1218
      // Grows the sample period as the step size decays to avoid converging near the initial ratio
1219
      double initialStep = HILL_CLIMBER_STEP_PERCENT * maximum();
1✔
1220
      double magnitude = Math.max(initialStep / SMALL_CACHE_SAMPLE_RATIO_CAP, Math.abs(stepSize()));
1✔
1221
      double ratio = (magnitude == 0.0)
1✔
1222
          ? 1.0
1✔
1223
          : Math.max(1.0, Math.min(SMALL_CACHE_SAMPLE_RATIO_CAP, initialStep / magnitude));
1✔
1224
      effectiveSampleSize = (long) (effectiveSampleSize * ratio);
1✔
1225
      stepDecayRate = SMALL_CACHE_STEP_DECAY_RATE;
1✔
1226
    }
1227
    if (requestCount < effectiveSampleSize) {
1✔
1228
      return;
1✔
1229
    }
1230

1231
    double hitRate = (double) hitsInSample() / requestCount;
1✔
1232
    double hitRateChange = hitRate - previousSampleHitRate();
1✔
1233
    double amount = (hitRateChange >= 0) ? stepSize() : -stepSize();
1✔
1234
    double nextStepSize = (Math.abs(hitRateChange) >= HILL_CLIMBER_RESTART_THRESHOLD)
1✔
1235
        ? Math.copySign(
1✔
1236
            Math.max(HILL_CLIMBER_STEP_PERCENT * maximum(), HILL_CLIMBER_MIN_INITIAL_STEP), amount)
1✔
1237
        : (stepDecayRate * amount);
1✔
1238
    setPreviousSampleHitRate(hitRate);
1✔
1239
    setAdjustment((long) amount);
1✔
1240
    setStepSize(nextStepSize);
1✔
1241
    setMissesInSample(0);
1✔
1242
    setHitsInSample(0);
1✔
1243
  }
1✔
1244

1245
  /**
1246
   * Increases the size of the admission window by shrinking the portion allocated to the main
1247
   * space. As the main space is partitioned into probation and protected regions (80% / 20%), for
1248
   * simplicity only the protected is reduced. If the regions exceed their maximums, this may cause
1249
   * protected items to be demoted to the probation region and probation items to be demoted to the
1250
   * admission window.
1251
   */
1252
  @GuardedBy("evictionLock")
1253
  void increaseWindow() {
1254
    if (mainProtectedMaximum() == 0) {
1✔
1255
      return;
1✔
1256
    }
1257

1258
    @SuppressWarnings("MathClampLong")
1259
    @Var long quota = Math.min(adjustment(), mainProtectedMaximum());
1✔
1260
    setMainProtectedMaximum(mainProtectedMaximum() - quota);
1✔
1261
    setWindowMaximum(windowMaximum() + quota);
1✔
1262
    demoteFromMainProtected();
1✔
1263

1264
    for (int i = 0; i < QUEUE_TRANSFER_THRESHOLD; i++) {
1✔
1265
      @Var Node<K, V> candidate = accessOrderProbationDeque().peekFirst();
1✔
1266
      @Var boolean probation = true;
1✔
1267
      if ((candidate == null) || (quota < candidate.getPolicyWeight())) {
1✔
1268
        candidate = accessOrderProtectedDeque().peekFirst();
1✔
1269
        probation = false;
1✔
1270
      }
1271
      if (candidate == null) {
1✔
1272
        break;
1✔
1273
      }
1274

1275
      int weight = candidate.getPolicyWeight();
1✔
1276
      if (quota < weight) {
1✔
1277
        break;
1✔
1278
      }
1279

1280
      quota -= weight;
1✔
1281
      if (probation) {
1✔
1282
        accessOrderProbationDeque().remove(candidate);
1✔
1283
      } else {
1284
        setMainProtectedWeightedSize(mainProtectedWeightedSize() - weight);
1✔
1285
        accessOrderProtectedDeque().remove(candidate);
1✔
1286
      }
1287
      setWindowWeightedSize(windowWeightedSize() + weight);
1✔
1288
      accessOrderWindowDeque().offerLast(candidate);
1✔
1289
      candidate.makeWindow();
1✔
1290
    }
1291

1292
    setMainProtectedMaximum(mainProtectedMaximum() + quota);
1✔
1293
    setWindowMaximum(windowMaximum() - quota);
1✔
1294
    setAdjustment(quota);
1✔
1295
  }
1✔
1296

1297
  /** Decreases the size of the admission window and increases the main's protected region. */
1298
  @GuardedBy("evictionLock")
1299
  void decreaseWindow() {
1300
    if (windowMaximum() <= 1) {
1✔
1301
      return;
1✔
1302
    }
1303

1304
    @SuppressWarnings("MathClampLong")
1305
    @Var long quota = Math.min(-adjustment(), Math.max(0, windowMaximum() - 1));
1✔
1306
    setMainProtectedMaximum(mainProtectedMaximum() + quota);
1✔
1307
    setWindowMaximum(windowMaximum() - quota);
1✔
1308

1309
    for (int i = 0; i < QUEUE_TRANSFER_THRESHOLD; i++) {
1✔
1310
      Node<K, V> candidate = accessOrderWindowDeque().peekFirst();
1✔
1311
      if (candidate == null) {
1✔
1312
        break;
1✔
1313
      }
1314

1315
      int weight = candidate.getPolicyWeight();
1✔
1316
      if (quota < weight) {
1✔
1317
        break;
1✔
1318
      }
1319

1320
      quota -= weight;
1✔
1321
      setWindowWeightedSize(windowWeightedSize() - weight);
1✔
1322
      accessOrderWindowDeque().remove(candidate);
1✔
1323
      accessOrderProbationDeque().offerLast(candidate);
1✔
1324
      candidate.makeMainProbation();
1✔
1325
    }
1326

1327
    setMainProtectedMaximum(mainProtectedMaximum() - quota);
1✔
1328
    setWindowMaximum(windowMaximum() + quota);
1✔
1329
    setAdjustment(-quota);
1✔
1330
  }
1✔
1331

1332
  /** Transfers the nodes from the protected to the probation region if it exceeds the maximum. */
1333
  @GuardedBy("evictionLock")
1334
  void demoteFromMainProtected() {
1335
    long mainProtectedMaximum = mainProtectedMaximum();
1✔
1336
    @Var long mainProtectedWeightedSize = mainProtectedWeightedSize();
1✔
1337
    if (mainProtectedWeightedSize <= mainProtectedMaximum) {
1✔
1338
      return;
1✔
1339
    }
1340

1341
    for (int i = 0; i < QUEUE_TRANSFER_THRESHOLD; i++) {
1✔
1342
      if (mainProtectedWeightedSize <= mainProtectedMaximum) {
1✔
1343
        break;
1✔
1344
      }
1345

1346
      Node<K, V> demoted = accessOrderProtectedDeque().pollFirst();
1✔
1347
      if (demoted == null) {
1✔
1348
        break;
1✔
1349
      }
1350
      demoted.makeMainProbation();
1✔
1351
      accessOrderProbationDeque().offerLast(demoted);
1✔
1352
      mainProtectedWeightedSize -= demoted.getPolicyWeight();
1✔
1353
    }
1354
    setMainProtectedWeightedSize(mainProtectedWeightedSize);
1✔
1355
  }
1✔
1356

1357
  /**
1358
   * Performs the post-processing work required after a read.
1359
   *
1360
   * @param node the entry in the page replacement policy
1361
   * @param now the current time, in nanoseconds
1362
   * @param recordHit if the hit count should be incremented
1363
   * @return the refreshed value if immediately loaded, else null
1364
   */
1365
  @Nullable V afterRead(Node<K, V> node, long now, boolean recordHit) {
1366
    if (recordHit) {
1✔
1367
      statsCounter().recordHits(1);
1✔
1368
    }
1369

1370
    boolean delayable = skipReadBuffer() || (readBuffer.offer(node) != Buffer.FULL);
1✔
1371
    if (shouldDrainBuffers(delayable)) {
1✔
1372
      scheduleDrainBuffers();
1✔
1373
    }
1374
    return refreshIfNeeded(node, now);
1✔
1375
  }
1376

1377
  /** Returns if the cache should bypass the read buffer. */
1378
  boolean skipReadBuffer() {
1379
    return fastpath() && frequencySketch().isNotInitialized();
1✔
1380
  }
1381

1382
  /**
1383
   * Asynchronously refreshes the entry if eligible.
1384
   *
1385
   * @param node the entry in the cache to refresh
1386
   * @param now the current time, in nanoseconds
1387
   * @return the refreshed value if immediately loaded, else null
1388
   */
1389
  @SuppressWarnings("FutureReturnValueIgnored")
1390
  @Nullable V refreshIfNeeded(Node<K, V> node, long now) {
1391
    if (!refreshAfterWrite()) {
1✔
1392
      return null;
1✔
1393
    }
1394

1395
    K key;
1396
    V oldValue;
1397
    Object keyReference;
1398
    long writeTime = node.getWriteTime();
1✔
1399
    long refreshWriteTime = writeTime | 1L;
1✔
1400
    ConcurrentMap<Object, CompletableFuture<?>> refreshes;
1401
    if (((now - writeTime) > refreshAfterWriteNanos())
1✔
1402
        && ((key = node.getKey()) != null) && ((oldValue = node.getValue()) != null)
1✔
1403
        && !isComputingAsync(oldValue) && ((writeTime & 1L) == 0L)
1✔
1404
        && !(refreshes = refreshes()).containsKey(keyReference = node.getKeyReference())
1✔
1405
        && node.isAlive() && node.casWriteTime(writeTime, refreshWriteTime)) {
1✔
1406
      long[] startTime = new long[1];
1✔
1407
      @SuppressWarnings({"rawtypes", "unchecked"})
1408
      @Nullable CompletableFuture<? extends @Nullable V>[] refreshFuture = new CompletableFuture[1];
1✔
1409
      try {
1410
        refreshes.computeIfAbsent(keyReference, k -> {
1✔
1411
          if (!node.isAlive() || (node.getWriteTime() != refreshWriteTime)) {
1✔
1412
            return null;
1✔
1413
          }
1414
          try {
1415
            startTime[0] = statsTicker().read();
1✔
1416
            if (isAsync) {
1✔
1417
              @SuppressWarnings("unchecked")
1418
              var future = (CompletableFuture<V>) oldValue;
1✔
1419
              if (Async.isReady(future)) {
1✔
1420
                requireNonNull(cacheLoader);
1✔
1421
                var refresh = cacheLoader.asyncReload(key, future.join(), executor);
1✔
1422
                refreshFuture[0] = requireNonNull(refresh, "Null future");
1✔
1423
              } else {
1✔
1424
                // no-op if the future's completion state was modified (e.g. obtrude methods)
1425
                return null;
1✔
1426
              }
1427
            } else {
1✔
1428
              requireNonNull(cacheLoader);
1✔
1429
              var refresh = cacheLoader.asyncReload(key, oldValue, executor);
1✔
1430
              refreshFuture[0] = requireNonNull(refresh, "Null future");
1✔
1431
            }
1432
            return refreshFuture[0];
1✔
1433
          } catch (InterruptedException e) {
1✔
1434
            Thread.currentThread().interrupt();
1✔
1435
            logger.log(Level.WARNING, "Exception thrown when submitting refresh task", e);
1✔
1436
            return null;
1✔
1437
          } catch (Throwable e) {
1✔
1438
            logger.log(Level.WARNING, "Exception thrown when submitting refresh task", e);
1✔
1439
            return null;
1✔
1440
          }
1441
        });
1442
      } finally {
1443
        node.casWriteTime(refreshWriteTime, writeTime);
1✔
1444
      }
1445

1446
      if (refreshFuture[0] == null) {
1✔
1447
        return null;
1✔
1448
      }
1449

1450
      var refreshed = refreshFuture[0].handle((newValue, error) -> {
1✔
1451
        long loadTime = statsTicker().read() - startTime[0];
1✔
1452
        if (error != null) {
1✔
1453
          if (!(error instanceof CancellationException) && !(error instanceof TimeoutException)) {
1✔
1454
            logger.log(Level.WARNING, "Exception thrown during refresh", error);
1✔
1455
          }
1456
          refreshes.remove(keyReference, refreshFuture[0]);
1✔
1457
          statsCounter().recordLoadFailure(loadTime);
1✔
1458
          return null;
1✔
1459
        }
1460

1461
        @SuppressWarnings("unchecked")
1462
        V value = (isAsync && (newValue != null)) ? (V) refreshFuture[0] : newValue;
1✔
1463
        @Nullable RemovalCause[] cause = new RemovalCause[1];
1✔
1464
        var hints = new RemapHints();
1✔
1465
        @Nullable V result;
1466
        try {
1467
          result = compute(key, (K k, @Nullable V currentValue) -> {
1✔
1468
            // Keep the refresh registered until the write clears it to avoid readers from
1469
            // prematurely scheduling another reload
1470
            if (currentValue == null) {
1✔
1471
              // If the entry is absent then discard the refresh and maybe notify the listener
1472
              if (value != null) {
1✔
1473
                cause[0] = RemovalCause.EXPLICIT;
1✔
1474
              }
1475
              return null;
1✔
1476
            } else if (currentValue == value) {
1✔
1477
              // If the reloaded value is the same instance then no-op
1478
              return currentValue;
1✔
1479
            } else if (isAsync &&
1✔
1480
                (newValue == Async.getIfReady((CompletableFuture<?>) currentValue))) {
1✔
1481
              // If the completed futures hold the same value instance then no-op
1482
              return currentValue;
1✔
1483
            } else if ((currentValue == oldValue) && ((node.getWriteTime() & ~1L) == writeTime)
1✔
1484
                && (refreshes.get(keyReference) == refreshFuture[0])) {
1✔
1485
              // If the entry was not modified while in-flight (no ABA) then replace
1486
              return value;
1✔
1487
            }
1488
            // Otherwise the refresh is discarded. If a concurrent write changed the value or
1489
            // writeTime, preserve those timestamps so the refresh rejection does not stomp on
1490
            // the user's write. An external discard with no concurrent write falls through to the
1491
            // normal update, which debounces the next refresh attempt.
1492
            if (value != null) {
1✔
1493
              cause[0] = RemovalCause.REPLACED;
1✔
1494
            }
1495
            if ((currentValue != oldValue) || ((node.getWriteTime() & ~1L) != writeTime)) {
1✔
1496
              hints.preserveTimestamps = true;
1✔
1497
            }
1498
            return currentValue;
1✔
1499
          }, expiry(), /* recordLoad= */ false, /* recordLoadFailure= */ true, hints);
1✔
1500
        } catch (Throwable t) {
1✔
1501
          logger.log(Level.WARNING, "Exception thrown during refresh", t);
1✔
1502
          statsCounter().recordLoadFailure(loadTime);
1✔
1503
          return null;
1✔
1504
        }
1✔
1505

1506
        if (cause[0] != null) {
1✔
1507
          notifyRemoval(key, value, cause[0]);
1✔
1508
        }
1509
        if (newValue == null) {
1✔
1510
          statsCounter().recordLoadFailure(loadTime);
1✔
1511
        } else {
1512
          statsCounter().recordLoadSuccess(loadTime);
1✔
1513
        }
1514
        return result;
1✔
1515
      });
1516
      return Async.getIfReady(refreshed);
1✔
1517
    }
1518

1519
    return null;
1✔
1520
  }
1521

1522
  /**
1523
   * Returns the expiration time for the entry after being created.
1524
   *
1525
   * @param key the key of the entry that was created
1526
   * @param value the value of the entry that was created
1527
   * @param expiry the calculator for the expiration time
1528
   * @param now the current time, in nanoseconds
1529
   * @return the expiration time
1530
   */
1531
  long expireAfterCreate(K key, V value, @Nullable Expiry<? super K, ? super V> expiry, long now) {
1532
    if (expiresVariable()) {
1✔
1533
      requireNonNull(expiry);
1✔
1534
      long duration = Math.max(0L, expiry.expireAfterCreate(key, value, now));
1✔
1535
      return isAsync ? (now + duration) : (now + Math.min(duration, MAXIMUM_EXPIRY));
1✔
1536
    }
1537
    return 0L;
1✔
1538
  }
1539

1540
  /**
1541
   * Returns the expiration time for the entry after being updated.
1542
   *
1543
   * @param node the entry in the page replacement policy
1544
   * @param key the key of the entry that was updated
1545
   * @param value the value of the entry that was updated
1546
   * @param expiry the calculator for the expiration time
1547
   * @param now the current time, in nanoseconds
1548
   * @return the expiration time
1549
   */
1550
  long expireAfterUpdate(Node<K, V> node, K key, V value,
1551
      @Nullable Expiry<? super K, ? super V> expiry, long now) {
1552
    if (expiresVariable()) {
1✔
1553
      requireNonNull(expiry);
1✔
1554
      long currentDuration = Math.max(1, node.getVariableTime() - now);
1✔
1555
      long duration = Math.max(0L, expiry.expireAfterUpdate(key, value, now, currentDuration));
1✔
1556
      return isAsync ? (now + duration) : (now + Math.min(duration, MAXIMUM_EXPIRY));
1✔
1557
    }
1558
    return 0L;
1✔
1559
  }
1560

1561
  /**
1562
   * Returns the access time for the entry after a read.
1563
   *
1564
   * @param node the entry in the page replacement policy
1565
   * @param key the key of the entry that was read
1566
   * @param value the value of the entry that was read
1567
   * @param expiry the calculator for the expiration time
1568
   * @param now the current time, in nanoseconds
1569
   * @return the expiration time
1570
   */
1571
  long expireAfterRead(Node<K, V> node, K key, V value, Expiry<K, V> expiry, long now) {
1572
    if (expiresVariable()) {
1✔
1573
      long currentDuration = Math.max(0L, node.getVariableTime() - now);
1✔
1574
      long duration = Math.max(0L, expiry.expireAfterRead(key, value, now, currentDuration));
1✔
1575
      return isAsync ? (now + duration) : (now + Math.min(duration, MAXIMUM_EXPIRY));
1✔
1576
    }
1577
    return 0L;
1✔
1578
  }
1579

1580
  /**
1581
   * Attempts to update the access time for the entry after a read.
1582
   *
1583
   * @param node the entry in the page replacement policy
1584
   * @param key the key of the entry that was read
1585
   * @param value the value of the entry that was read
1586
   * @param expiry the calculator for the expiration time
1587
   * @param now the current time, in nanoseconds
1588
   */
1589
  void tryExpireAfterRead(Node<K, V> node, K key, V value, Expiry<K, V> expiry, long now) {
1590
    if (!expiresVariable()) {
1✔
1591
      return;
1✔
1592
    }
1593

1594
    long variableTime = node.getVariableTime();
1✔
1595
    long currentDuration = Math.max(1, variableTime - now);
1✔
1596
    if (isAsync && (currentDuration > MAXIMUM_EXPIRY)) {
1✔
1597
      // expireAfterCreate has not yet set the duration after completion
1598
      return;
1✔
1599
    }
1600

1601
    long tolerance = EXPIRE_TOLERANCE;
1✔
1602
    long duration = Math.max(0L, expiry.expireAfterRead(key, value, now, currentDuration));
1✔
1603
    long expirationTime = isAsync ? (now + duration) : (now + Math.min(duration, MAXIMUM_EXPIRY));
1✔
1604
    if (((duration <= tolerance) || (Math.abs(expirationTime - variableTime) > tolerance))
1✔
1605
        && (node.getValue() == value)) {
1✔
1606
      node.casVariableTime(variableTime, expirationTime);
1✔
1607
    }
1608
  }
1✔
1609

1610
  void setVariableTime(Node<K, V> node, long expirationTime) {
1611
    if (expiresVariable()) {
1✔
1612
      node.setVariableTime(expirationTime);
1✔
1613
    }
1614
  }
1✔
1615

1616
  void setWriteTime(Node<K, V> node, long now) {
1617
    if (expiresAfterWrite() || refreshAfterWrite()) {
1✔
1618
      node.setWriteTime(now & ~1L);
1✔
1619
    }
1620
  }
1✔
1621

1622
  void setAccessTime(Node<K, V> node, long now) {
1623
    if (!expiresAfterAccess()) {
1✔
1624
      return;
1✔
1625
    }
1626
    long tolerance = EXPIRE_TOLERANCE;
1✔
1627
    long accessTime = node.getAccessTime();
1✔
1628
    if ((expiresAfterAccessNanos() <= tolerance) || (Math.abs(now - accessTime) > tolerance)) {
1✔
1629
      node.setAccessTime(now);
1✔
1630
    }
1631
  }
1✔
1632

1633
  /** Returns if the entry's write time would exceed the minimum expiration reorder threshold. */
1634
  boolean exceedsWriteTimeTolerance(Node<K, V> node, long varTime, long now) {
1635
    long variableTime = node.getVariableTime();
1✔
1636
    long writeTime = node.getWriteTime();
1✔
1637
    long tolerance = EXPIRE_TOLERANCE;
1✔
1638
    return
1✔
1639
        (expiresAfterWrite()
1✔
1640
            && ((expiresAfterWriteNanos() <= tolerance) || (Math.abs(now - writeTime) > tolerance)))
1✔
1641
        || (refreshAfterWrite()
1✔
1642
            && ((refreshAfterWriteNanos() <= tolerance) || (Math.abs(now - writeTime) > tolerance)))
1✔
1643
        || (expiresVariable() && (Math.abs(varTime - variableTime) > tolerance));
1✔
1644
  }
1645

1646
  /**
1647
   * Performs the post-processing work required after a write.
1648
   *
1649
   * @param task the pending operation to be applied
1650
   */
1651
  void afterWrite(Runnable task) {
1652
    if (writeBuffer.offer(task)) {
1✔
1653
      scheduleAfterWrite();
1✔
1654
      return;
1✔
1655
    }
1656

1657
    // In scenarios where the writing threads cannot make progress then they attempt to provide
1658
    // assistance by performing the eviction work directly. This can resolve cases where the
1659
    // maintenance task is scheduled but not running. That might occur due to all of the executor's
1660
    // threads being busy (perhaps writing into this cache), the write rate greatly exceeds the
1661
    // consuming rate, priority inversion, or if the executor silently discarded the maintenance
1662
    // task. Unfortunately this cannot resolve when the eviction is blocked waiting on a long-
1663
    // running computation due to an eviction listener, the victim is being computed on by a writer,
1664
    // or the victim residing in the same hash bin as a computing entry. In those cases a warning is
1665
    // logged to encourage the application to decouple these computations from the map operations.
1666
    lock();
1✔
1667
    try {
1668
      maintenance(task);
1✔
1669
    } catch (RuntimeException e) {
1✔
1670
      logger.log(Level.ERROR, "Exception thrown when performing the maintenance task", e);
1✔
1671
    } finally {
1672
      evictionLock.unlock();
1✔
1673
    }
1674
    rescheduleCleanUpIfIncomplete();
1✔
1675
  }
1✔
1676

1677
  /** Acquires the eviction lock. */
1678
  void lock() {
1679
    @Var long remainingNanos = WARN_AFTER_LOCK_WAIT_NANOS;
1✔
1680
    long end = System.nanoTime() + remainingNanos;
1✔
1681
    @Var boolean interrupted = false;
1✔
1682
    try {
1683
      for (;;) {
1684
        try {
1685
          if (evictionLock.tryLock(remainingNanos, TimeUnit.NANOSECONDS)) {
1✔
1686
            return;
1✔
1687
          }
1688
          logger.log(Level.WARNING, "The cache is experiencing excessive wait times for acquiring "
1✔
1689
              + "the eviction lock. This may indicate that a long-running computation has halted "
1690
              + "eviction when trying to remove the victim entry. Consider using AsyncCache to "
1691
              + "decouple the computation from the map operation.", new TimeoutException());
1692
          evictionLock.lock();
1✔
1693
          return;
1✔
1694
        } catch (InterruptedException e) {
1✔
1695
          remainingNanos = end - System.nanoTime();
1✔
1696
          interrupted = true;
1✔
1697
        }
1✔
1698
      }
1699
    } finally {
1700
      if (interrupted) {
1✔
1701
        Thread.currentThread().interrupt();
1✔
1702
      }
1703
    }
1704
  }
1705

1706
  /**
1707
   * Conditionally schedules the asynchronous maintenance task after a write operation. If the
1708
   * task status was IDLE or REQUIRED then the maintenance task is scheduled immediately. If it
1709
   * is already processing then it is set to transition to REQUIRED upon completion so that a new
1710
   * execution is triggered by the next operation.
1711
   */
1712
  void scheduleAfterWrite() {
1713
    @Var int drainStatus = drainStatusOpaque();
1✔
1714
    for (;;) {
1715
      switch (drainStatus) {
1✔
1716
        case IDLE:
1717
          casDrainStatus(IDLE, REQUIRED);
1✔
1718
          scheduleDrainBuffers();
1✔
1719
          return;
1✔
1720
        case REQUIRED:
1721
          scheduleDrainBuffers();
1✔
1722
          return;
1✔
1723
        case PROCESSING_TO_IDLE:
1724
          if (casDrainStatus(PROCESSING_TO_IDLE, PROCESSING_TO_REQUIRED)) {
1✔
1725
            return;
1✔
1726
          }
1727
          drainStatus = drainStatusAcquire();
1✔
1728
          continue;
1✔
1729
        case PROCESSING_TO_REQUIRED:
1730
          return;
1✔
1731
        default:
1732
          throw new IllegalStateException("Invalid drain status: " + drainStatus);
1✔
1733
      }
1734
    }
1735
  }
1736

1737
  /**
1738
   * Attempts to schedule an asynchronous task to apply the pending operations to the page
1739
   * replacement policy. If the executor rejects the task then it is run directly.
1740
   */
1741
  void scheduleDrainBuffers() {
1742
    if (drainStatusOpaque() >= PROCESSING_TO_IDLE) {
1✔
1743
      return;
1✔
1744
    }
1745
    if (evictionLock.tryLock()) {
1✔
1746
      try {
1747
        int drainStatus = drainStatusOpaque();
1✔
1748
        if (drainStatus >= PROCESSING_TO_IDLE) {
1✔
1749
          return;
1✔
1750
        }
1751
        setDrainStatusRelease(PROCESSING_TO_IDLE);
1✔
1752
        executor.execute(drainBuffersTask);
1✔
1753
      } catch (Throwable t) {
1✔
1754
        logger.log(Level.WARNING, "Exception thrown when submitting maintenance task", t);
1✔
1755
        maintenance(/* ignored */ null);
1✔
1756
      } finally {
1757
        evictionLock.unlock();
1✔
1758
      }
1759
    }
1760
  }
1✔
1761

1762
  @Override
1763
  public void cleanUp() {
1764
    try {
1765
      performCleanUp(/* ignored */ null);
1✔
1766
    } catch (RuntimeException e) {
1✔
1767
      logger.log(Level.ERROR, "Exception thrown when performing the maintenance task", e);
1✔
1768
    }
1✔
1769
  }
1✔
1770

1771
  /**
1772
   * Performs the maintenance work, blocking until the lock is acquired.
1773
   *
1774
   * @param task an additional pending task to run, or {@code null} if not present
1775
   */
1776
  void performCleanUp(@Nullable Runnable task) {
1777
    evictionLock.lock();
1✔
1778
    try {
1779
      maintenance(task);
1✔
1780
    } finally {
1781
      evictionLock.unlock();
1✔
1782
    }
1783
    rescheduleCleanUpIfIncomplete();
1✔
1784
  }
1✔
1785

1786
  /**
1787
   * If there remains pending operations that were not handled by the prior clean up then try to
1788
   * schedule an asynchronous maintenance task. This may occur due to a concurrent write after the
1789
   * maintenance work had started or if the amortized threshold of work per clean up was reached.
1790
   */
1791
  @SuppressWarnings("resource")
1792
  void rescheduleCleanUpIfIncomplete() {
1793
    if (drainStatusOpaque() != REQUIRED) {
1✔
1794
      return;
1✔
1795
    }
1796

1797
    // An immediate scheduling cannot be performed on a custom executor because it may use a
1798
    // caller-runs policy. This could cause the caller's penalty to exceed the amortized threshold,
1799
    // e.g. repeated concurrent writes could result in a retry loop.
1800
    if (executor == ForkJoinPool.commonPool()) {
1✔
1801
      scheduleDrainBuffers();
1✔
1802
      return;
1✔
1803
    }
1804

1805
    // If a scheduler was configured then the maintenance can be deferred onto the custom executor
1806
    // and run in the near future. Otherwise, it will be handled due to other cache activity.
1807
    var pacer = pacer();
1✔
1808
    if ((pacer != null) && !pacer.isScheduled() && evictionLock.tryLock()) {
1✔
1809
      try {
1810
        if ((drainStatusOpaque() == REQUIRED) && !pacer.isScheduled()) {
1✔
1811
          pacer.schedule(executor, drainBuffersTask, expirationTicker().read(), Pacer.TOLERANCE);
1✔
1812
        }
1813
      } finally {
1814
        evictionLock.unlock();
1✔
1815
      }
1816
    }
1817
  }
1✔
1818

1819
  /**
1820
   * Performs the pending maintenance work and sets the state flags during processing to avoid
1821
   * excess scheduling attempts. The read buffer, write buffer, and reference queues are drained,
1822
   * followed by expiration, and size-based eviction.
1823
   *
1824
   * @param task an additional pending task to run, or {@code null} if not present
1825
   */
1826
  @GuardedBy("evictionLock")
1827
  void maintenance(@Nullable Runnable task) {
1828
    setDrainStatusRelease(PROCESSING_TO_IDLE);
1✔
1829

1830
    try {
1831
      try {
1832
        drainReadBuffer();
1✔
1833
        drainWriteBuffer();
1✔
1834
      } finally {
1835
        if (task != null) {
1✔
1836
          task.run();
1✔
1837
        }
1838
      }
1839

1840
      drainKeyReferences();
1✔
1841
      drainValueReferences();
1✔
1842

1843
      expireEntries();
1✔
1844
      evictEntries();
1✔
1845

1846
      climb();
1✔
1847
    } finally {
1848
      if ((drainStatusOpaque() != PROCESSING_TO_IDLE)
1✔
1849
          || !casDrainStatus(PROCESSING_TO_IDLE, IDLE)) {
1✔
1850
        setDrainStatusOpaque(REQUIRED);
1✔
1851
      }
1852
    }
1853
  }
1✔
1854

1855
  /** Drains the weak key references queue. */
1856
  @GuardedBy("evictionLock")
1857
  void drainKeyReferences() {
1858
    if (!collectKeys()) {
1✔
1859
      return;
1✔
1860
    }
1861
    @Var Reference<? extends K> keyRef;
1862
    while ((keyRef = keyReferenceQueue().poll()) != null) {
1✔
1863
      Node<K, V> node = data.get(keyRef);
1✔
1864
      if (node != null) {
1✔
1865
        evictEntry(node, RemovalCause.COLLECTED, 0L);
1✔
1866
      }
1867
    }
1✔
1868
  }
1✔
1869

1870
  /** Drains the weak / soft value references queue. */
1871
  @GuardedBy("evictionLock")
1872
  void drainValueReferences() {
1873
    if (!collectValues()) {
1✔
1874
      return;
1✔
1875
    }
1876
    @Var Reference<? extends V> valueRef;
1877
    while ((valueRef = valueReferenceQueue().poll()) != null) {
1✔
1878
      @SuppressWarnings("unchecked")
1879
      var ref = (InternalReference<V>) valueRef;
1✔
1880
      Node<K, V> node = data.get(ref.getKeyReference());
1✔
1881
      if ((node != null) && (valueRef == node.getValueReference())) {
1✔
1882
        evictEntry(node, RemovalCause.COLLECTED, 0L);
1✔
1883
      }
1884
    }
1✔
1885
  }
1✔
1886

1887
  /** Drains the read buffer. */
1888
  @GuardedBy("evictionLock")
1889
  void drainReadBuffer() {
1890
    if (!skipReadBuffer()) {
1✔
1891
      readBuffer.drainTo(accessPolicy);
1✔
1892
    }
1893
  }
1✔
1894

1895
  /** Updates the node's location in the page replacement policy. */
1896
  @GuardedBy("evictionLock")
1897
  void onAccess(Node<K, V> node) {
1898
    if (evicts()) {
1✔
1899
      var keyRef = node.getKeyReferenceOrNull();
1✔
1900
      if ((keyRef == null) || !node.isAlive()) {
1✔
1901
        return;
1✔
1902
      }
1903
      frequencySketch().increment(keyRef);
1✔
1904
      if (node.inWindow()) {
1✔
1905
        reorder(accessOrderWindowDeque(), node);
1✔
1906
      } else if (node.inMainProbation()) {
1✔
1907
        reorderProbation(node);
1✔
1908
      } else {
1909
        reorder(accessOrderProtectedDeque(), node);
1✔
1910
      }
1911
      setHitsInSample(hitsInSample() + 1);
1✔
1912
    } else if (expiresAfterAccess()) {
1✔
1913
      reorder(accessOrderWindowDeque(), node);
1✔
1914
    }
1915
    if (expiresVariable()) {
1✔
1916
      timerWheel().reschedule(node);
1✔
1917
    }
1918
  }
1✔
1919

1920
  /** Promote the node from probation to protected on an access. */
1921
  @GuardedBy("evictionLock")
1922
  void reorderProbation(Node<K, V> node) {
1923
    if (!accessOrderProbationDeque().contains(node)) {
1✔
1924
      // Ignore stale accesses for an entry that is no longer present
1925
      return;
1✔
1926
    } else if (node.getPolicyWeight() > mainProtectedMaximum()) {
1✔
1927
      reorder(accessOrderProbationDeque(), node);
1✔
1928
      return;
1✔
1929
    }
1930

1931
    // If the protected space exceeds its maximum, the LRU items are demoted to the probation space.
1932
    // This is deferred to the adaption phase at the end of the maintenance cycle.
1933
    setMainProtectedWeightedSize(mainProtectedWeightedSize() + node.getPolicyWeight());
1✔
1934
    accessOrderProbationDeque().remove(node);
1✔
1935
    accessOrderProtectedDeque().offerLast(node);
1✔
1936
    node.makeMainProtected();
1✔
1937
  }
1✔
1938

1939
  /** Updates the node's location in the policy's deque. */
1940
  static <K, V> void reorder(LinkedDeque<Node<K, V>> deque, Node<K, V> node) {
1941
    // An entry may be scheduled for reordering despite having been removed. This can occur when the
1942
    // entry was concurrently read while a writer was removing it. If the entry is no longer linked
1943
    // then it does not need to be processed.
1944
    if (deque.contains(node)) {
1✔
1945
      deque.moveToBack(node);
1✔
1946
    }
1947
  }
1✔
1948

1949
  /** Drains the write buffer. */
1950
  @GuardedBy("evictionLock")
1951
  void drainWriteBuffer() {
1952
    for (int i = 0; i <= WRITE_BUFFER_MAX; i++) {
1✔
1953
      Runnable task = writeBuffer.poll();
1✔
1954
      if (task == null) {
1✔
1955
        return;
1✔
1956
      }
1957
      task.run();
1✔
1958
    }
1959
    setDrainStatusOpaque(PROCESSING_TO_REQUIRED);
1✔
1960
  }
1✔
1961

1962
  /**
1963
   * Atomically transitions the node to the <code>dead</code> state and decrements the
1964
   * <code>weightedSize</code>.
1965
   *
1966
   * @param node the entry in the page replacement policy
1967
   */
1968
  @GuardedBy("evictionLock")
1969
  @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
1970
  void makeDead(Node<K, V> node) {
1971
    synchronized (node) {
1✔
1972
      if (node.isDead()) {
1✔
1973
        return;
1✔
1974
      }
1975
      if (evicts()) {
1✔
1976
        // The node's policy weight may be out of sync due to a pending update waiting to be
1977
        // processed. At this point the node's weight is finalized, so the weight can be safely
1978
        // taken from the node's perspective and the sizes will be adjusted correctly.
1979
        if (node.inWindow()) {
1✔
1980
          setWindowWeightedSize(windowWeightedSize() - node.getWeight());
1✔
1981
        } else if (node.inMainProtected()) {
1✔
1982
          setMainProtectedWeightedSize(mainProtectedWeightedSize() - node.getWeight());
1✔
1983
        }
1984
        setWeightedSize(weightedSize() - node.getWeight());
1✔
1985
      }
1986
      node.die();
1✔
1987
    }
1✔
1988
  }
1✔
1989

1990
  /** Adds the node to the page replacement policy. */
1991
  final class AddTask implements Runnable {
1992
    final Node<K, V> node;
1993
    final int weight;
1994

1995
    AddTask(Node<K, V> node, int weight) {
1✔
1996
      this.weight = weight;
1✔
1997
      this.node = node;
1✔
1998
    }
1✔
1999

2000
    @Override
2001
    @GuardedBy("evictionLock")
2002
    public void run() {
2003
      if (evicts()) {
1✔
2004
        setWeightedSize(weightedSize() + weight);
1✔
2005
        setWindowWeightedSize(windowWeightedSize() + weight);
1✔
2006
        node.setPolicyWeight(node.getPolicyWeight() + weight);
1✔
2007

2008
        long maximum = maximum();
1✔
2009
        if (weightedSize() >= (maximum >>> 1)) {
1✔
2010
          if (weightedSize() > MAXIMUM_CAPACITY) {
1✔
2011
            evictEntries();
1✔
2012
          } else {
2013
            // Lazily initialize when close to the maximum
2014
            long capacity = isWeighted() ? data.mappingCount() : maximum;
1✔
2015
            frequencySketch().ensureCapacity(capacity);
1✔
2016
          }
2017
        }
2018

2019
        setMissesInSample(missesInSample() + 1);
1✔
2020
      }
2021

2022
      // ignore out-of-order write operations
2023
      boolean isAlive;
2024
      synchronized (node) {
1✔
2025
        isAlive = node.isAlive();
1✔
2026
      }
1✔
2027
      if (isAlive) {
1✔
2028
        if (expiresAfterWrite()) {
1✔
2029
          writeOrderDeque().offerLast(node);
1✔
2030
        }
2031
        if (expiresVariable()) {
1✔
2032
          timerWheel().schedule(node);
1✔
2033
        }
2034
        if (evicts()) {
1✔
2035
          var keyRef = node.getKeyReferenceOrNull();
1✔
2036
          if ((keyRef != null) && node.isAlive()) {
1✔
2037
            frequencySketch().increment(keyRef);
1✔
2038
          }
2039
          if (weight > maximum()) {
1✔
2040
            evictEntry(node, RemovalCause.SIZE, expirationTicker().read());
1✔
2041
          } else if (weight > windowMaximum()) {
1✔
2042
            accessOrderWindowDeque().offerFirst(node);
1✔
2043
          } else {
2044
            accessOrderWindowDeque().offerLast(node);
1✔
2045
          }
2046
        } else if (expiresAfterAccess()) {
1✔
2047
          accessOrderWindowDeque().offerLast(node);
1✔
2048
        }
2049
      }
2050
    }
1✔
2051
  }
2052

2053
  /** Removes a node from the page replacement policy. */
2054
  final class RemovalTask implements Runnable {
2055
    final Node<K, V> node;
2056

2057
    RemovalTask(Node<K, V> node) {
1✔
2058
      this.node = node;
1✔
2059
    }
1✔
2060

2061
    @Override
2062
    @GuardedBy("evictionLock")
2063
    public void run() {
2064
      // add may not have been processed yet
2065
      if (node.inWindow() && (evicts() || expiresAfterAccess())) {
1✔
2066
        accessOrderWindowDeque().remove(node);
1✔
2067
      } else if (evicts()) {
1✔
2068
        if (node.inMainProbation()) {
1✔
2069
          accessOrderProbationDeque().remove(node);
1✔
2070
        } else {
2071
          accessOrderProtectedDeque().remove(node);
1✔
2072
        }
2073
      }
2074
      if (expiresAfterWrite()) {
1✔
2075
        writeOrderDeque().remove(node);
1✔
2076
      } else if (expiresVariable()) {
1✔
2077
        timerWheel().deschedule(node);
1✔
2078
      }
2079
      makeDead(node);
1✔
2080
    }
1✔
2081
  }
2082

2083
  /** Updates the weighted size. */
2084
  final class UpdateTask implements Runnable {
2085
    final int weightDifference;
2086
    final Node<K, V> node;
2087

2088
    public UpdateTask(Node<K, V> node, int weightDifference) {
1✔
2089
      this.weightDifference = weightDifference;
1✔
2090
      this.node = node;
1✔
2091
    }
1✔
2092

2093
    @Override
2094
    @GuardedBy("evictionLock")
2095
    public void run() {
2096
      if (expiresAfterWrite()) {
1✔
2097
        reorder(writeOrderDeque(), node);
1✔
2098
      } else if (expiresVariable()) {
1✔
2099
        timerWheel().reschedule(node);
1✔
2100
      }
2101
      if (evicts()) {
1✔
2102
        int oldWeightedSize = node.getPolicyWeight();
1✔
2103
        node.setPolicyWeight(oldWeightedSize + weightDifference);
1✔
2104
        if (node.inWindow()) {
1✔
2105
          setWindowWeightedSize(windowWeightedSize() + weightDifference);
1✔
2106
          if (node.getPolicyWeight() > maximum()) {
1✔
2107
            evictEntry(node, RemovalCause.SIZE, expirationTicker().read());
1✔
2108
          } else if (node.getPolicyWeight() <= windowMaximum()) {
1✔
2109
            onAccess(node);
1✔
2110
          } else if (accessOrderWindowDeque().contains(node)) {
1✔
2111
            accessOrderWindowDeque().moveToFront(node);
1✔
2112
          }
2113
        } else if (node.inMainProbation()) {
1✔
2114
            if (node.getPolicyWeight() <= maximum()) {
1✔
2115
              onAccess(node);
1✔
2116
            } else {
2117
              evictEntry(node, RemovalCause.SIZE, expirationTicker().read());
1✔
2118
            }
2119
        } else {
2120
          setMainProtectedWeightedSize(mainProtectedWeightedSize() + weightDifference);
1✔
2121
          if (node.getPolicyWeight() <= maximum()) {
1✔
2122
            onAccess(node);
1✔
2123
          } else {
2124
            evictEntry(node, RemovalCause.SIZE, expirationTicker().read());
1✔
2125
          }
2126
        }
2127

2128
        setWeightedSize(weightedSize() + weightDifference);
1✔
2129
        if (weightedSize() > MAXIMUM_CAPACITY) {
1✔
2130
          evictEntries();
1✔
2131
        }
2132
      } else if (expiresAfterAccess()) {
1✔
2133
        onAccess(node);
1✔
2134
      }
2135
    }
1✔
2136
  }
2137

2138
  /* --------------- Concurrent Map Support --------------- */
2139

2140
  @Override
2141
  public boolean isEmpty() {
2142
    return data.isEmpty();
1✔
2143
  }
2144

2145
  @Override
2146
  public int size() {
2147
    return data.size();
1✔
2148
  }
2149

2150
  @Override
2151
  public long estimatedSize() {
2152
    return data.mappingCount();
1✔
2153
  }
2154

2155
  @Override
2156
  public void clear() {
2157
    Deque<Node<K, V>> entries;
2158
    evictionLock.lock();
1✔
2159
    try {
2160
      // Discard all pending reads
2161
      readBuffer.drainTo(e -> {});
1✔
2162

2163
      // Apply all pending writes
2164
      @Var Runnable task;
2165
      while ((task = writeBuffer.poll()) != null) {
1✔
2166
        task.run();
1✔
2167
      }
2168

2169
      // Cancel the scheduled cleanup
2170
      Pacer pacer = pacer();
1✔
2171
      if (pacer != null) {
1✔
2172
        pacer.cancel();
1✔
2173
      }
2174

2175
      // Discard all entries, falling back to one-by-one to avoid excessive lock hold times
2176
      long now = expirationTicker().read();
1✔
2177
      int threshold = (WRITE_BUFFER_MAX / 2);
1✔
2178
      entries = new ArrayDeque<>(data.values());
1✔
2179
      while (!entries.isEmpty() && (writeBuffer.size() < threshold)) {
1✔
2180
        removeNode(entries.pollFirst(), now);
1✔
2181
      }
2182
    } finally {
2183
      evictionLock.unlock();
1✔
2184
    }
2185

2186
    // Remove any stragglers if released early to more aggressively flush incoming writes
2187
    @Var boolean cleanUp = false;
1✔
2188
    for (var node : entries) {
1✔
2189
      @Nullable K key = node.getKey();
1✔
2190
      if (key == null) {
1✔
2191
        cleanUp = true;
1✔
2192
      } else {
2193
        remove(key);
1✔
2194
      }
2195
    }
1✔
2196
    if (collectKeys() && cleanUp) {
1✔
2197
      cleanUp();
1✔
2198
    } else if (entries.isEmpty()) {
1✔
2199
      rescheduleCleanUpIfIncomplete();
1✔
2200
    }
2201
  }
1✔
2202

2203
  @GuardedBy("evictionLock")
2204
  @SuppressWarnings({"GuardedByChecker", "SynchronizationOnLocalVariableOrMethodParameter"})
2205
  void removeNode(Node<K, V> node, long now) {
2206
    K key = node.getKey();
1✔
2207
    var ctx = new EvictContext<V>();
1✔
2208
    var keyReference = node.getKeyReference();
1✔
2209

2210
    data.computeIfPresent(keyReference, (k, n) -> {
1✔
2211
      if (n != node) {
1✔
2212
        return n;
1✔
2213
      }
2214
      synchronized (node) {
1✔
2215
        ctx.value = node.getValue();
1✔
2216
        ctx.oldWeight = node.getWeight();
1✔
2217

2218
        if ((key == null) || (ctx.value == null)) {
1✔
2219
          ctx.cause = RemovalCause.COLLECTED;
1✔
2220
        } else if (hasExpired(node, now, ctx.value)) {
1✔
2221
          ctx.cause = RemovalCause.EXPIRED;
1✔
2222
        } else {
2223
          ctx.cause = RemovalCause.EXPLICIT;
1✔
2224
        }
2225

2226
        if (ctx.cause.wasEvicted()) {
1✔
2227
          notifyEviction(key, ctx.value, ctx.cause);
1✔
2228
        }
2229

2230
        discardRefresh(node.getKeyReference());
1✔
2231
        node.retire();
1✔
2232
        return null;
1✔
2233
      }
2234
    });
2235

2236
    if (node.inWindow() && (evicts() || expiresAfterAccess())) {
1✔
2237
      accessOrderWindowDeque().remove(node);
1✔
2238
    } else if (evicts()) {
1✔
2239
      if (node.inMainProbation()) {
1✔
2240
        accessOrderProbationDeque().remove(node);
1✔
2241
      } else {
2242
        accessOrderProtectedDeque().remove(node);
1✔
2243
      }
2244
    }
2245
    if (expiresAfterWrite()) {
1✔
2246
      writeOrderDeque().remove(node);
1✔
2247
    } else if (expiresVariable()) {
1✔
2248
      timerWheel().deschedule(node);
1✔
2249
    }
2250

2251
    synchronized (node) {
1✔
2252
      logIfAlive(node);
1✔
2253
      makeDead(node);
1✔
2254
    }
1✔
2255

2256
    if (ctx.cause != null) {
1✔
2257
      if (ctx.cause.wasEvicted()) {
1✔
2258
        statsCounter().recordEviction(ctx.oldWeight, ctx.cause);
1✔
2259
      }
2260
      notifyRemoval(key, ctx.value, ctx.cause);
1✔
2261
    }
2262
  }
1✔
2263

2264
  @Override
2265
  public boolean containsKey(Object key) {
2266
    Node<K, V> node = data.get(nodeFactory.newLookupKey(key));
1✔
2267
    if (node == null) {
1✔
2268
      return false;
1✔
2269
    }
2270
    V value = node.getValue();
1✔
2271
    if ((value == null) || hasExpired(node, expirationTicker().read(), value)) {
1✔
2272
      scheduleDrainBuffers();
1✔
2273
      return false;
1✔
2274
    }
2275
    return true;
1✔
2276
  }
2277

2278
  @Override
2279
  public boolean containsValue(Object value) {
2280
    requireNonNull(value);
1✔
2281

2282
    long now = expirationTicker().read();
1✔
2283
    for (Node<K, V> node : data.values()) {
1✔
2284
      V nodeValue = node.getValue();
1✔
2285
      if ((node.getKey() == null) || (nodeValue == null) || hasExpired(node, now, nodeValue)) {
1✔
2286
        scheduleDrainBuffers();
1✔
2287
      } else if (node.isAlive() && node.containsValue(value)) {
1✔
2288
        return true;
1✔
2289
      }
2290
    }
1✔
2291
    return false;
1✔
2292
  }
2293

2294
  @Override
2295
  public @Nullable V get(Object key) {
2296
    return getIfPresent(key, /* recordStats= */ false);
1✔
2297
  }
2298

2299
  @Override
2300
  public @Nullable V getIfPresent(Object key, boolean recordStats) {
2301
    Node<K, V> node = data.get(nodeFactory.newLookupKey(key));
1✔
2302
    if (node == null) {
1✔
2303
      if (recordStats) {
1✔
2304
        statsCounter().recordMisses(1);
1✔
2305
      }
2306
      if (drainStatusOpaque() == REQUIRED) {
1✔
2307
        scheduleDrainBuffers();
1✔
2308
      }
2309
      return null;
1✔
2310
    }
2311

2312
    V value = node.getValue();
1✔
2313
    long now = expirationTicker().read();
1✔
2314
    if ((value == null) || hasExpired(node, now, value)) {
1✔
2315
      if (recordStats) {
1✔
2316
        statsCounter().recordMisses(1);
1✔
2317
      }
2318
      scheduleDrainBuffers();
1✔
2319
      return null;
1✔
2320
    }
2321

2322
    if (!isComputingAsync(value)) {
1✔
2323
      @SuppressWarnings("unchecked")
2324
      var castedKey = (K) key;
1✔
2325
      setAccessTime(node, now);
1✔
2326
      tryExpireAfterRead(node, castedKey, value, expiry(), now);
1✔
2327
    }
2328
    V refreshed = afterRead(node, now, recordStats);
1✔
2329
    return (refreshed == null) ? value : refreshed;
1✔
2330
  }
2331

2332
  @Override
2333
  public @Nullable V getIfPresentQuietly(Object key) {
2334
    V value;
2335
    Node<K, V> node = data.get(nodeFactory.newLookupKey(key));
1✔
2336
    if ((node == null) || ((value = node.getValue()) == null)
1✔
2337
        || hasExpired(node, expirationTicker().read(), value)) {
1✔
2338
      return null;
1✔
2339
    }
2340
    return value;
1✔
2341
  }
2342

2343
  /**
2344
   * Returns the key associated with the mapping in this cache, or {@code null} if there is none.
2345
   *
2346
   * @param key the key whose canonical instance is to be returned
2347
   * @return the key used by the mapping, or {@code null} if this cache does not contain a mapping
2348
   *         for the key
2349
   * @throws NullPointerException if the specified key is null
2350
   */
2351
  public @Nullable K getKey(K key) {
2352
    Node<K, V> node = data.get(nodeFactory.newLookupKey(key));
1✔
2353
    if (node == null) {
1✔
2354
      if (drainStatusOpaque() == REQUIRED) {
1✔
2355
        scheduleDrainBuffers();
1✔
2356
      }
2357
      return null;
1✔
2358
    }
2359
    afterRead(node, /* now= */ 0L, /* recordHit= */ false);
1✔
2360
    return node.getKey();
1✔
2361
  }
2362

2363
  @Override
2364
  public Map<K, V> getAllPresent(Iterable<? extends K> keys) {
2365
    var result = new LinkedHashMap<K, @Nullable V>(calculateHashMapCapacity(keys));
1✔
2366
    for (K key : keys) {
1✔
2367
      result.put(key, null);
1✔
2368
    }
1✔
2369

2370
    @Var boolean drain = false;
1✔
2371
    int uniqueKeys = result.size();
1✔
2372
    long now = expirationTicker().read();
1✔
2373
    for (var iter = result.entrySet().iterator(); iter.hasNext();) {
1✔
2374
      V value;
2375
      var entry = iter.next();
1✔
2376
      Node<K, V> node = data.get(nodeFactory.newLookupKey(entry.getKey()));
1✔
2377
      if (node == null) {
1✔
2378
        iter.remove();
1✔
2379
      } else if (((value = node.getValue()) == null) || hasExpired(node, now, value)) {
1✔
2380
        iter.remove();
1✔
2381
        drain = true;
1✔
2382
      } else {
2383
        setAccessTime(node, now);
1✔
2384
        tryExpireAfterRead(node, entry.getKey(), value, expiry(), now);
1✔
2385
        V refreshed = afterRead(node, now, /* recordHit= */ false);
1✔
2386
        entry.setValue((refreshed == null) ? value : refreshed);
1✔
2387
      }
2388
    }
1✔
2389
    if (drain) {
1✔
2390
      scheduleDrainBuffers();
1✔
2391
    }
2392
    statsCounter().recordHits(result.size());
1✔
2393
    statsCounter().recordMisses(uniqueKeys - result.size());
1✔
2394

2395
    @SuppressWarnings("NullableProblems")
2396
    Map<K, V> unmodifiable = Collections.unmodifiableMap(result);
1✔
2397
    return unmodifiable;
1✔
2398
  }
2399

2400
  @Override
2401
  public void putAll(Map<? extends K, ? extends V> map) {
2402
    map.forEach(this::put);
1✔
2403
  }
1✔
2404

2405
  @Override
2406
  public @Nullable V put(K key, V value) {
2407
    return put(key, value, expiry(), /* onlyIfAbsent= */ false);
1✔
2408
  }
2409

2410
  @Override
2411
  public @Nullable V putIfAbsent(K key, V value) {
2412
    return put(key, value, expiry(), /* onlyIfAbsent= */ true);
1✔
2413
  }
2414

2415
  /**
2416
   * Adds a node to the policy and the data store. If an existing node is found, then its value is
2417
   * updated if allowed.
2418
   *
2419
   * @param key key with which the specified value is to be associated
2420
   * @param value value to be associated with the specified key
2421
   * @param expiry the calculator for the write expiration time
2422
   * @param onlyIfAbsent a write is performed only if the key is not already associated with a value
2423
   * @return the prior value in or null if no mapping was found
2424
   */
2425
  @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
2426
  @Nullable V put(K key, V value, Expiry<K, V> expiry, boolean onlyIfAbsent) {
2427
    requireNonNull(key);
1✔
2428
    requireNonNull(value);
1✔
2429

2430
    @Var int newWeight = -1;
1✔
2431
    @Var Object keyRef = null;
1✔
2432
    @Var Node<K, V> node = null;
1✔
2433
    Object lookupKey = nodeFactory.newLookupKey(key);
1✔
2434
    for (int attempts = 1; ; attempts++) {
1✔
2435
      @Var Node<K, V> prior = data.get(lookupKey);
1✔
2436
      if (prior == null) {
1✔
2437
        if (node == null) {
1✔
2438
          if (newWeight < 0) {
1✔
2439
            newWeight = weigher.weigh(key, value);
1✔
2440
          }
2441
          long now = expirationTicker().read();
1✔
2442
          keyRef = nodeFactory.newReferenceKey(key, keyReferenceQueue());
1✔
2443
          node = nodeFactory.newNode(keyRef, value, valueReferenceQueue(), newWeight, now);
1✔
2444
          long expirationTime = isComputingAsync(value) ? (now + ASYNC_EXPIRY) : now;
1✔
2445
          setVariableTime(node, expireAfterCreate(key, value, expiry, now));
1✔
2446
          setAccessTime(node, expirationTime);
1✔
2447
          setWriteTime(node, expirationTime);
1✔
2448
        }
2449
        var newNode = node;
1✔
2450
        prior = (cacheLoader == null)
1✔
2451
            ? data.putIfAbsent(keyRef, newNode)
1✔
2452
            : data.computeIfAbsent(keyRef, k -> {
1✔
2453
                discardRefresh(k);
1✔
2454
                return newNode;
1✔
2455
              });
2456
        if ((prior == null) || (prior == node)) {
1✔
2457
          afterWrite(new AddTask(node, newWeight));
1✔
2458
          return null;
1✔
2459
        } else if (onlyIfAbsent) {
1✔
2460
          // An optimistic fast path to avoid unnecessary locking
2461
          V currentValue = prior.getValue();
1✔
2462
          long now = expirationTicker().read();
1✔
2463
          if ((currentValue != null) && !hasExpired(prior, now, currentValue)) {
1✔
2464
            if (!isComputingAsync(currentValue)) {
1✔
2465
              tryExpireAfterRead(prior, key, currentValue, expiry, now);
1✔
2466
              setAccessTime(prior, now);
1✔
2467
            }
2468
            afterRead(prior, now, /* recordHit= */ false);
1✔
2469
            return currentValue;
1✔
2470
          }
2471
        }
2472
      } else if (onlyIfAbsent) {
1✔
2473
        // An optimistic fast path to avoid unnecessary locking
2474
        V currentValue = prior.getValue();
1✔
2475
        long now = expirationTicker().read();
1✔
2476
        if ((currentValue != null) && !hasExpired(prior, now, currentValue)) {
1✔
2477
          if (!isComputingAsync(currentValue)) {
1✔
2478
            tryExpireAfterRead(prior, key, currentValue, expiry, now);
1✔
2479
            setAccessTime(prior, now);
1✔
2480
          }
2481
          afterRead(prior, now, /* recordHit= */ false);
1✔
2482
          return currentValue;
1✔
2483
        }
2484
      }
2485

2486
      // A read may race with the entry's removal, so that after the entry is acquired it may no
2487
      // longer be usable. A retry will reread from the map and either find an absent mapping, a
2488
      // new entry, or a stale entry.
2489
      if (!prior.isAlive()) {
1✔
2490
        // A reread of the stale entry may occur if the state transition occurred but the map
2491
        // removal was delayed by a context switch, so that this thread spin waits until resolved.
2492
        if ((attempts & MAX_PUT_SPIN_WAIT_ATTEMPTS) != 0) {
1✔
2493
          Thread.onSpinWait();
1✔
2494
          continue;
1✔
2495
        }
2496

2497
        // If the spin wait attempts are exhausted then fallback to a map computation in order to
2498
        // deschedule this thread until the entry's removal completes. If the key was modified
2499
        // while in the map so that its equals or hashCode changed then the contents may be
2500
        // corrupted, where the cache holds an evicted (dead) entry that could not be removed.
2501
        // That is a violation of the Map contract, so we check that the mapping is in the "alive"
2502
        // state while in the computation.
2503
        data.computeIfPresent(lookupKey, (k, n) -> {
1✔
2504
          requireIsAlive(key, n);
1✔
2505
          return n;
1✔
2506
        });
2507
        continue;
1✔
2508
      }
2509

2510
      long now;
2511
      V oldValue;
2512
      long varTime;
2513
      int oldWeight;
2514
      @Var boolean expired = false;
1✔
2515
      @Var boolean mayUpdate = true;
1✔
2516
      @Var boolean exceedsTolerance = false;
1✔
2517
      if (newWeight < 0) {
1✔
2518
        newWeight = weigher.weigh(key, value);
1✔
2519
      }
2520
      synchronized (prior) {
1✔
2521
        if (!prior.isAlive()) {
1✔
2522
          continue;
1✔
2523
        }
2524
        oldValue = prior.getValue();
1✔
2525
        oldWeight = prior.getWeight();
1✔
2526
        now = expirationTicker().read();
1✔
2527
        if (oldValue == null) {
1✔
2528
          varTime = expireAfterCreate(key, value, expiry, now);
1✔
2529
          notifyEviction(key, null, RemovalCause.COLLECTED);
1✔
2530
        } else if (hasExpired(prior, now, oldValue)) {
1✔
2531
          expired = true;
1✔
2532
          varTime = expireAfterCreate(key, value, expiry, now);
1✔
2533
          notifyEviction(key, oldValue, RemovalCause.EXPIRED);
1✔
2534
        } else if (onlyIfAbsent) {
1✔
2535
          mayUpdate = false;
1✔
2536
          varTime = expireAfterRead(prior, key, oldValue, expiry, now);
1✔
2537
        } else {
2538
          varTime = expireAfterUpdate(prior, key, value, expiry, now);
1✔
2539
        }
2540

2541
        long expirationTime = isComputingAsync(mayUpdate ? value : oldValue)
1✔
2542
            ? (now + ASYNC_EXPIRY)
1✔
2543
            : now;
1✔
2544
        if (mayUpdate) {
1✔
2545
          exceedsTolerance = exceedsWriteTimeTolerance(prior, varTime, expirationTime);
1✔
2546
          if (expired || exceedsTolerance) {
1✔
2547
            setWriteTime(prior, expirationTime);
1✔
2548
          }
2549

2550
          prior.setValue(value, valueReferenceQueue());
1✔
2551
          prior.setWeight(newWeight);
1✔
2552

2553
          discardRefresh(prior.getKeyReference());
1✔
2554
        }
2555

2556
        setVariableTime(prior, varTime);
1✔
2557
        setAccessTime(prior, expirationTime);
1✔
2558
      }
1✔
2559

2560
      if (expired) {
1✔
2561
        statsCounter().recordEviction(oldWeight, RemovalCause.EXPIRED);
1✔
2562
        notifyRemoval(key, oldValue, RemovalCause.EXPIRED);
1✔
2563
      } else if (oldValue == null) {
1✔
2564
        statsCounter().recordEviction(oldWeight, RemovalCause.COLLECTED);
1✔
2565
        notifyRemoval(key, /* value= */ null, RemovalCause.COLLECTED);
1✔
2566
      } else if (mayUpdate) {
1✔
2567
        notifyOnReplace(key, oldValue, value);
1✔
2568
      }
2569

2570
      int weightedDifference = mayUpdate ? (newWeight - oldWeight) : 0;
1✔
2571
      if ((oldValue == null) || (weightedDifference != 0) || expired) {
1✔
2572
        afterWrite(new UpdateTask(prior, weightedDifference));
1✔
2573
      } else if (!onlyIfAbsent && exceedsTolerance) {
1✔
2574
        afterWrite(new UpdateTask(prior, weightedDifference));
1✔
2575
      } else {
2576
        afterRead(prior, now, /* recordHit= */ false);
1✔
2577
      }
2578

2579
      return expired ? null : oldValue;
1✔
2580
    }
2581
  }
2582

2583
  @Override
2584
  @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
2585
  public @Nullable V remove(Object key) {
2586
    var ctx = new RemoveContext<K, V>();
1✔
2587
    Object lookupKey = nodeFactory.newLookupKey(key);
1✔
2588
    data.computeIfPresent(lookupKey, (k, n) -> {
1✔
2589
      synchronized (n) {
1✔
2590
        requireIsAlive(key, n);
1✔
2591
        ctx.oldKey = n.getKey();
1✔
2592
        ctx.oldValue = n.getValue();
1✔
2593
        ctx.oldWeight = n.getWeight();
1✔
2594
        RemovalCause actualCause;
2595
        if ((ctx.oldKey == null) || (ctx.oldValue == null)) {
1✔
2596
          actualCause = RemovalCause.COLLECTED;
1✔
2597
        } else if (hasExpired(n, expirationTicker().read(), ctx.oldValue)) {
1✔
2598
          actualCause = RemovalCause.EXPIRED;
1✔
2599
        } else {
2600
          actualCause = RemovalCause.EXPLICIT;
1✔
2601
        }
2602
        if (actualCause.wasEvicted()) {
1✔
2603
          notifyEviction(ctx.oldKey, ctx.oldValue, actualCause);
1✔
2604
        }
2605
        ctx.cause = actualCause;
1✔
2606
        discardRefresh(k);
1✔
2607
        ctx.node = n;
1✔
2608
        n.retire();
1✔
2609
        return null;
1✔
2610
      }
2611
    });
2612

2613
    if (ctx.cause != null) {
1✔
2614
      afterWrite(new RemovalTask(requireNonNull(ctx.node)));
1✔
2615
      if (ctx.cause.wasEvicted()) {
1✔
2616
        statsCounter().recordEviction(ctx.oldWeight, ctx.cause);
1✔
2617
      }
2618
      notifyRemoval(ctx.oldKey, ctx.oldValue, ctx.cause);
1✔
2619
    }
2620
    return (ctx.cause == RemovalCause.EXPLICIT) ? ctx.oldValue : null;
1✔
2621
  }
2622

2623
  @Override
2624
  @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
2625
  public boolean remove(Object key, @Nullable Object value) {
2626
    requireNonNull(key);
1✔
2627
    if (value == null) {
1✔
2628
      return false;
1✔
2629
    }
2630

2631
    var ctx = new RemoveContext<K, V>();
1✔
2632
    Object lookupKey = nodeFactory.newLookupKey(key);
1✔
2633
    data.computeIfPresent(lookupKey, (kR, node) -> {
1✔
2634
      synchronized (node) {
1✔
2635
        requireIsAlive(key, node);
1✔
2636
        ctx.oldKey = node.getKey();
1✔
2637
        ctx.oldValue = node.getValue();
1✔
2638
        ctx.oldWeight = node.getWeight();
1✔
2639
        if ((ctx.oldKey == null) || (ctx.oldValue == null)) {
1✔
2640
          ctx.cause = RemovalCause.COLLECTED;
1✔
2641
        } else if (hasExpired(node, expirationTicker().read(), ctx.oldValue)) {
1✔
2642
          ctx.cause = RemovalCause.EXPIRED;
1✔
2643
        } else if (node.containsValue(value)) {
1✔
2644
          ctx.cause = RemovalCause.EXPLICIT;
1✔
2645
        } else {
2646
          return node;
1✔
2647
        }
2648
        if (ctx.cause.wasEvicted()) {
1✔
2649
          notifyEviction(ctx.oldKey, ctx.oldValue, ctx.cause);
1✔
2650
        }
2651
        discardRefresh(kR);
1✔
2652
        ctx.node = node;
1✔
2653
        node.retire();
1✔
2654
        return null;
1✔
2655
      }
2656
    });
2657

2658
    if (ctx.node == null) {
1✔
2659
      return false;
1✔
2660
    }
2661
    var removeCause = requireNonNull(ctx.cause);
1✔
2662
    afterWrite(new RemovalTask(ctx.node));
1✔
2663
    if (removeCause.wasEvicted()) {
1✔
2664
      statsCounter().recordEviction(ctx.oldWeight, removeCause);
1✔
2665
    }
2666
    notifyRemoval(ctx.oldKey, ctx.oldValue, removeCause);
1✔
2667

2668
    return (removeCause == RemovalCause.EXPLICIT);
1✔
2669
  }
2670

2671
  @Override
2672
  @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
2673
  public @Nullable V replace(K key, V value) {
2674
    requireNonNull(key);
1✔
2675
    requireNonNull(value);
1✔
2676
    var ctx = new ReplaceContext<K, V>();
1✔
2677
    int weight = weigher.weigh(key, value);
1✔
2678
    Node<K, V> node = data.computeIfPresent(nodeFactory.newLookupKey(key), (k, n) -> {
1✔
2679
      synchronized (n) {
1✔
2680
        requireIsAlive(key, n);
1✔
2681
        ctx.nodeKey = n.getKey();
1✔
2682
        ctx.oldValue = n.getValue();
1✔
2683
        ctx.oldWeight = n.getWeight();
1✔
2684
        if ((ctx.nodeKey == null) || (ctx.oldValue == null)
1✔
2685
            || hasExpired(n, ctx.now = expirationTicker().read(), ctx.oldValue)) {
1✔
2686
          ctx.oldValue = null;
1✔
2687
          return n;
1✔
2688
        }
2689

2690
        long varTime = expireAfterUpdate(n, key, value, expiry(), ctx.now);
1✔
2691
        n.setValue(value, valueReferenceQueue());
1✔
2692
        n.setWeight(weight);
1✔
2693

2694
        long expirationTime = isComputingAsync(value) ? (ctx.now + ASYNC_EXPIRY) : ctx.now;
1✔
2695
        ctx.exceedsTolerance = exceedsWriteTimeTolerance(n, varTime, expirationTime);
1✔
2696
        if (ctx.exceedsTolerance) {
1✔
2697
          setWriteTime(n, expirationTime);
1✔
2698
        }
2699
        setAccessTime(n, expirationTime);
1✔
2700
        setVariableTime(n, varTime);
1✔
2701
        discardRefresh(k);
1✔
2702
        return n;
1✔
2703
      }
2704
    });
2705

2706
    if ((node == null) || (ctx.nodeKey == null) || (ctx.oldValue == null)) {
1✔
2707
      if (node != null) {
1✔
2708
        scheduleDrainBuffers();
1✔
2709
      }
2710
      return null;
1✔
2711
    }
2712

2713
    int weightedDifference = (weight - ctx.oldWeight);
1✔
2714
    if (ctx.exceedsTolerance || (weightedDifference != 0)) {
1✔
2715
      afterWrite(new UpdateTask(node, weightedDifference));
1✔
2716
    } else {
2717
      afterRead(node, ctx.now, /* recordHit= */ false);
1✔
2718
    }
2719

2720
    notifyOnReplace(ctx.nodeKey, ctx.oldValue, value);
1✔
2721
    return ctx.oldValue;
1✔
2722
  }
2723

2724
  @Override
2725
  public boolean replace(K key, V oldValue, V newValue) {
2726
    return replace(key, oldValue, newValue, /* shouldDiscardRefresh= */ true);
1✔
2727
  }
2728

2729
  @Override
2730
  @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
2731
  public boolean replace(K key, V oldValue, V newValue, boolean shouldDiscardRefresh) {
2732
    requireNonNull(key);
1✔
2733
    requireNonNull(oldValue);
1✔
2734
    requireNonNull(newValue);
1✔
2735
    var ctx = new ReplaceContext<K, V>();
1✔
2736
    int weight = weigher.weigh(key, newValue);
1✔
2737
    Node<K, V> node = data.computeIfPresent(nodeFactory.newLookupKey(key), (k, n) -> {
1✔
2738
      synchronized (n) {
1✔
2739
        requireIsAlive(key, n);
1✔
2740
        ctx.nodeKey = n.getKey();
1✔
2741
        ctx.oldValue = n.getValue();
1✔
2742
        ctx.oldWeight = n.getWeight();
1✔
2743
        if ((ctx.nodeKey == null) || (ctx.oldValue == null) || !n.containsValue(oldValue)
1✔
2744
            || hasExpired(n, ctx.now = expirationTicker().read(), ctx.oldValue)) {
1✔
2745
          ctx.oldValue = null;
1✔
2746
          return n;
1✔
2747
        }
2748

2749
        long varTime = expireAfterUpdate(n, key, newValue, expiry(), ctx.now);
1✔
2750
        n.setValue(newValue, valueReferenceQueue());
1✔
2751
        n.setWeight(weight);
1✔
2752

2753
        long expirationTime = isComputingAsync(newValue) ? (ctx.now + ASYNC_EXPIRY) : ctx.now;
1✔
2754
        ctx.exceedsTolerance = exceedsWriteTimeTolerance(n, varTime, expirationTime);
1✔
2755
        if (ctx.exceedsTolerance) {
1✔
2756
          setWriteTime(n, expirationTime);
1✔
2757
        }
2758
        setAccessTime(n, expirationTime);
1✔
2759
        setVariableTime(n, varTime);
1✔
2760

2761
        if (shouldDiscardRefresh) {
1✔
2762
          discardRefresh(k);
1✔
2763
        }
2764
      }
1✔
2765
      return n;
1✔
2766
    });
2767

2768
    if ((node == null) || (ctx.nodeKey == null) || (ctx.oldValue == null)) {
1✔
2769
      if (node != null) {
1✔
2770
        scheduleDrainBuffers();
1✔
2771
      }
2772
      return false;
1✔
2773
    }
2774

2775
    int weightedDifference = (weight - ctx.oldWeight);
1✔
2776
    if (ctx.exceedsTolerance || (weightedDifference != 0)) {
1✔
2777
      afterWrite(new UpdateTask(node, weightedDifference));
1✔
2778
    } else {
2779
      afterRead(node, ctx.now, /* recordHit= */ false);
1✔
2780
    }
2781

2782
    notifyOnReplace(ctx.nodeKey, ctx.oldValue, newValue);
1✔
2783
    return true;
1✔
2784
  }
2785

2786
  @Override
2787
  public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
2788
    requireNonNull(function);
1✔
2789

2790
    BiFunction<K, V, V> remappingFunction = (key, oldValue) ->
1✔
2791
        requireNonNull(function.apply(key, oldValue));
1✔
2792
    for (K key : keySet()) {
1✔
2793
      Object lookupKey = nodeFactory.newLookupKey(key);
1✔
2794
      remap(key, lookupKey, remappingFunction, expiry(),
1✔
2795
          new ComputeContext<>(expirationTicker().read()), /* computeIfAbsent= */ false);
1✔
2796
    }
1✔
2797
  }
1✔
2798

2799
  @Override
2800
  public @Nullable V computeIfAbsent(K key,
2801
      @Var Function<? super K, ? extends @Nullable V> mappingFunction,
2802
      boolean recordStats, boolean recordLoad) {
2803
    requireNonNull(key);
1✔
2804
    requireNonNull(mappingFunction);
1✔
2805

2806
    // An optimistic fast path to avoid unnecessary locking
2807
    Node<K, V> node = data.get(nodeFactory.newLookupKey(key));
1✔
2808
    long now = expirationTicker().read();
1✔
2809
    if (node != null) {
1✔
2810
      V value = node.getValue();
1✔
2811
      if ((value != null) && !hasExpired(node, now, value)) {
1✔
2812
        if (!isComputingAsync(value)) {
1✔
2813
          tryExpireAfterRead(node, key, value, expiry(), now);
1✔
2814
          setAccessTime(node, now);
1✔
2815
        }
2816
        @Nullable V refreshed = afterRead(node, now, /* recordHit= */ recordStats);
1✔
2817
        return (refreshed == null) ? value : refreshed;
1✔
2818
      }
2819
    }
2820
    if (recordStats) {
1✔
2821
      mappingFunction = statsAware(mappingFunction, recordLoad);
1✔
2822
    }
2823
    Object keyRef = nodeFactory.newReferenceKey(key, keyReferenceQueue());
1✔
2824
    return doComputeIfAbsent(key, keyRef, mappingFunction,
1✔
2825
        new ComputeContext<>(now), recordStats);
2826
  }
2827

2828
  /** Returns the current value from a computeIfAbsent invocation. */
2829
  @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
2830
  @Nullable V doComputeIfAbsent(K key, Object keyRef,
2831
      Function<? super K, ? extends @Nullable V> mappingFunction,
2832
      ComputeContext<K, V> ctx, boolean recordStats) {
2833
    Node<K, V> node = data.compute(keyRef, (k, n) -> {
1✔
2834
      if (n == null) {
1✔
2835
        ctx.newValue = mappingFunction.apply(key);
1✔
2836
        if (ctx.newValue == null) {
1✔
2837
          discardRefresh(k);
1✔
2838
          return null;
1✔
2839
        }
2840
        ctx.now = expirationTicker().read();
1✔
2841
        ctx.newWeight = weigher.weigh(key, ctx.newValue);
1✔
2842
        var created = nodeFactory.newNode(k, ctx.newValue,
1✔
2843
            valueReferenceQueue(), ctx.newWeight, ctx.now);
1✔
2844
        long expirationTime = isComputingAsync(ctx.newValue)
1✔
2845
            ? ctx.now + ASYNC_EXPIRY
1✔
2846
            : ctx.now;
1✔
2847
        setVariableTime(created, expireAfterCreate(key, ctx.newValue, expiry(), ctx.now));
1✔
2848
        setAccessTime(created, expirationTime);
1✔
2849
        setWriteTime(created, expirationTime);
1✔
2850
        discardRefresh(k);
1✔
2851
        return created;
1✔
2852
      }
2853

2854
      synchronized (n) {
1✔
2855
        requireIsAlive(key, n);
1✔
2856
        ctx.nodeKey = n.getKey();
1✔
2857
        ctx.oldValue = n.getValue();
1✔
2858
        ctx.oldWeight = n.getWeight();
1✔
2859
        RemovalCause actualCause;
2860
        if ((ctx.nodeKey == null) || (ctx.oldValue == null)) {
1✔
2861
          actualCause = RemovalCause.COLLECTED;
1✔
2862
        } else if (hasExpired(n, ctx.now = expirationTicker().read(), ctx.oldValue)) {
1✔
2863
          actualCause = RemovalCause.EXPIRED;
1✔
2864
        } else {
2865
          return n;
1✔
2866
        }
2867

2868
        ctx.cause = actualCause;
1✔
2869
        notifyEviction(ctx.nodeKey, ctx.oldValue, actualCause);
1✔
2870

2871
        try {
2872
          ctx.newValue = mappingFunction.apply(key);
1✔
2873
          if (ctx.newValue == null) {
1✔
2874
            discardRefresh(k);
1✔
2875
            ctx.removed = n;
1✔
2876
            n.retire();
1✔
2877
            return null;
1✔
2878
          }
2879
          ctx.now = expirationTicker().read();
1✔
2880
          ctx.newWeight = weigher.weigh(key, ctx.newValue);
1✔
2881
          long varTime = expireAfterCreate(key, ctx.newValue, expiry(), ctx.now);
1✔
2882

2883
          n.setValue(ctx.newValue, valueReferenceQueue());
1✔
2884
          n.setWeight(ctx.newWeight);
1✔
2885

2886
          long expirationTime = isComputingAsync(ctx.newValue)
1!
2887
              ? (ctx.now + ASYNC_EXPIRY) : ctx.now;
1✔
2888
          setAccessTime(n, expirationTime);
1✔
2889
          setWriteTime(n, expirationTime);
1✔
2890
          setVariableTime(n, varTime);
1✔
2891
          discardRefresh(k);
1✔
2892
          return n;
1✔
2893
        } catch (Throwable e) {
1✔
2894
          ctx.newValue = null;
1✔
2895
          discardRefresh(k);
1✔
2896
          ctx.exception = e;
1✔
2897
          ctx.removed = n;
1✔
2898
          n.retire();
1✔
2899
          return null;
1✔
2900
        }
2901
      }
2902
    });
2903

2904
    if (ctx.cause != null) {
1✔
2905
      statsCounter().recordEviction(ctx.oldWeight, ctx.cause);
1✔
2906
      notifyRemoval(ctx.nodeKey, ctx.oldValue, ctx.cause);
1✔
2907
    }
2908
    if (node == null) {
1✔
2909
      if (ctx.removed != null) {
1✔
2910
        afterWrite(new RemovalTask(ctx.removed));
1✔
2911
      }
2912
      if (ctx.exception != null) {
1✔
2913
        throw toUncheckedException(ctx.exception);
1✔
2914
      }
2915
      return null;
1✔
2916
    }
2917
    if ((ctx.oldValue != null) && (ctx.newValue == null)) {
1✔
2918
      if (!isComputingAsync(ctx.oldValue)) {
1✔
2919
        tryExpireAfterRead(node, key, ctx.oldValue, expiry(), ctx.now);
1✔
2920
        setAccessTime(node, ctx.now);
1✔
2921
      }
2922

2923
      @Nullable V refreshed = afterRead(node, ctx.now, /* recordHit= */ recordStats);
1✔
2924
      return (refreshed == null) ? ctx.oldValue : refreshed;
1✔
2925
    }
2926
    if ((ctx.oldValue == null) && (ctx.cause == null)) {
1✔
2927
      afterWrite(new AddTask(node, ctx.newWeight));
1✔
2928
    } else {
2929
      int weightedDifference = (ctx.newWeight - ctx.oldWeight);
1✔
2930
      afterWrite(new UpdateTask(node, weightedDifference));
1✔
2931
    }
2932

2933
    return ctx.newValue;
1✔
2934
  }
2935

2936
  @Override
2937
  public @Nullable V computeIfPresent(K key,
2938
      BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
2939
    requireNonNull(key);
1✔
2940
    requireNonNull(remappingFunction);
1✔
2941

2942
    // An optimistic fast path to avoid unnecessary locking
2943
    Object lookupKey = nodeFactory.newLookupKey(key);
1✔
2944
    @Nullable Node<K, V> node = data.get(lookupKey);
1✔
2945
    long now;
2946
    if (node == null) {
1✔
2947
      return null;
1✔
2948
    }
2949
    V value = node.getValue();
1✔
2950
    if ((value == null) || hasExpired(node, now = expirationTicker().read(), value)) {
1✔
2951
      scheduleDrainBuffers();
1✔
2952
      return null;
1✔
2953
    }
2954

2955
    BiFunction<? super K, ? super V, ? extends V> statsAwareRemappingFunction =
1✔
2956
        statsAware(remappingFunction, /* recordLoad= */ true, /* recordLoadFailure= */ true);
1✔
2957
    return remap(key, lookupKey, statsAwareRemappingFunction,
1✔
2958
        expiry(), new ComputeContext<>(now), /* computeIfAbsent= */ false);
1✔
2959
  }
2960

2961
  @Override
2962
  public @Nullable V compute(K key,
2963
      BiFunction<? super K, ? super V, ? extends @Nullable V> remappingFunction,
2964
      @Nullable Expiry<? super K, ? super V> expiry, boolean recordLoad, boolean recordLoadFailure,
2965
      @Nullable RemapHints hints) {
2966
    requireNonNull(key);
1✔
2967
    requireNonNull(remappingFunction);
1✔
2968

2969
    Object keyRef = nodeFactory.newReferenceKey(key, keyReferenceQueue());
1✔
2970
    BiFunction<? super K, ? super V, ? extends V> statsAwareRemappingFunction =
1✔
2971
        statsAware(remappingFunction, recordLoad, recordLoadFailure);
1✔
2972
    var ctx = new ComputeContext<K, V>(expirationTicker().read());
1✔
2973
    ctx.hints = hints;
1✔
2974
    return remap(key, keyRef, statsAwareRemappingFunction,
1✔
2975
        expiry, ctx, /* computeIfAbsent= */ true);
2976
  }
2977

2978
  @Override
2979
  public @Nullable V merge(K key, V value,
2980
      BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
2981
    requireNonNull(key);
1✔
2982
    requireNonNull(value);
1✔
2983
    requireNonNull(remappingFunction);
1✔
2984

2985
    Object keyRef = nodeFactory.newReferenceKey(key, keyReferenceQueue());
1✔
2986
    BiFunction<? super V, ? super V, ? extends V> f = statsAware(remappingFunction);
1✔
2987
    BiFunction<? super K, ? super @Nullable V, ? extends @Nullable V> mergeFunction =
1✔
2988
        (k, oldValue) -> (oldValue == null) ? value : f.apply(oldValue, value);
1✔
2989
    return remap(key, keyRef, mergeFunction, expiry(),
1✔
2990
        new ComputeContext<>(expirationTicker().read()), /* computeIfAbsent= */ true);
1✔
2991
  }
2992

2993
  /**
2994
   * Attempts to compute a mapping for the specified key and its current mapped value (or
2995
   * {@code null} if there is no current mapping).
2996
   * <p>
2997
   * An entry that has expired or been reference collected is evicted and the computation continues
2998
   * as if the entry had not been present. This method does not pre-screen and does not wrap the
2999
   * remappingFunction to be statistics aware.
3000
   *
3001
   * @param key key with which the specified value is to be associated
3002
   * @param keyRef the key to associate with or a lookup only key if not {@code computeIfAbsent}
3003
   * @param remappingFunction the function to compute a value
3004
   * @param expiry the calculator for the expiration time
3005
   * @param ctx the mutable context for passing state to and from the {@link ConcurrentHashMap}
3006
   *        compute lambda, with {@link ComputeContext#now} set to the current ticker time
3007
   * @param computeIfAbsent if an absent entry can be computed
3008
   * @return the new value associated with the specified key, or null if none
3009
   */
3010
  @SuppressWarnings({"StatementWithEmptyBody", "SynchronizationOnLocalVariableOrMethodParameter"})
3011
  @Nullable V remap(K key, Object keyRef,
3012
      BiFunction<? super K, ? super V, ? extends @Nullable V> remappingFunction,
3013
      @Nullable Expiry<? super K, ? super V> expiry,
3014
      ComputeContext<K, V> ctx, boolean computeIfAbsent) {
3015
    Node<K, V> node = data.compute(keyRef, (kr, n) -> {
1✔
3016
      if (n == null) {
1✔
3017
        if (!computeIfAbsent) {
1✔
3018
          return null;
1✔
3019
        }
3020
        ctx.newValue = remappingFunction.apply(key, null);
1✔
3021
        if (ctx.newValue == null) {
1✔
3022
          discardRefresh(kr);
1✔
3023
          return null;
1✔
3024
        }
3025
        try {
3026
          ctx.now = expirationTicker().read();
1✔
3027
          ctx.newWeight = weigher.weigh(key, ctx.newValue);
1✔
3028
          long varTime = expireAfterCreate(key, ctx.newValue, expiry, ctx.now);
1✔
3029
          var created = nodeFactory.newNode(keyRef, ctx.newValue,
1✔
3030
              valueReferenceQueue(), ctx.newWeight, ctx.now);
1✔
3031

3032
          long expirationTime = isComputingAsync(ctx.newValue)
1✔
3033
              ? ctx.now + ASYNC_EXPIRY
1✔
3034
              : ctx.now;
1✔
3035
          setAccessTime(created, expirationTime);
1✔
3036
          setWriteTime(created, expirationTime);
1✔
3037
          setVariableTime(created, varTime);
1✔
3038
          return created;
1✔
3039
        } finally {
3040
          discardRefresh(kr);
1✔
3041
        }
3042
      }
3043

3044
      synchronized (n) {
1✔
3045
        requireIsAlive(key, n);
1✔
3046
        ctx.nodeKey = n.getKey();
1✔
3047
        ctx.oldValue = n.getValue();
1✔
3048
        ctx.oldWeight = n.getWeight();
1✔
3049
        if ((ctx.nodeKey == null) || (ctx.oldValue == null)) {
1✔
3050
          ctx.cause = RemovalCause.COLLECTED;
1✔
3051
        } else if (hasExpired(n, expirationTicker().read(), ctx.oldValue)) {
1✔
3052
          ctx.cause = RemovalCause.EXPIRED;
1✔
3053
        }
3054
        if (ctx.cause != null) {
1✔
3055
          notifyEviction(ctx.nodeKey, ctx.oldValue, ctx.cause);
1✔
3056
          if (!computeIfAbsent) {
1✔
3057
            discardRefresh(kr);
1✔
3058
            ctx.removed = n;
1✔
3059
            n.retire();
1✔
3060
            return null;
1✔
3061
          }
3062
        }
3063

3064
        boolean wasEvicted = (ctx.cause != null);
1✔
3065
        try {
3066
          ctx.newValue = remappingFunction.apply(key,
1✔
3067
              (ctx.cause == null) ? ctx.oldValue : null);
1✔
3068

3069
          if (ctx.newValue == null) {
1✔
3070
            if (ctx.cause == null) {
1✔
3071
              ctx.cause = RemovalCause.EXPLICIT;
1✔
3072
            }
3073
            discardRefresh(kr);
1✔
3074
            ctx.removed = n;
1✔
3075
            n.retire();
1✔
3076
            return null;
1✔
3077
          }
3078

3079
          // If the caller flagged a same-instance return as a no-op (e.g., a refresh was rejected
3080
          // and should not touch the entry), skip the metadata updates below.
3081
          if ((ctx.hints != null) && ctx.hints.preserveTimestamps
1✔
3082
              && (ctx.newValue == ctx.oldValue) && (ctx.cause == null)) {
3083
            // Skip for query-style callers whose no-op path must leave any in-flight refresh intact
3084
            if (!ctx.hints.preserveRefresh) {
1✔
3085
              discardRefresh(kr);
1✔
3086
            }
3087
            return n;
1✔
3088
          }
3089

3090
          long varTime;
3091
          ctx.newWeight = weigher.weigh(key, ctx.newValue);
1✔
3092
          ctx.now = expirationTicker().read();
1✔
3093
          if (ctx.cause == null) {
1✔
3094
            if (ctx.newValue != ctx.oldValue) {
1✔
3095
              ctx.cause = RemovalCause.REPLACED;
1✔
3096
            }
3097
            varTime = expireAfterUpdate(n, key, ctx.newValue, expiry, ctx.now);
1✔
3098
          } else {
3099
            varTime = expireAfterCreate(key, ctx.newValue, expiry, ctx.now);
1✔
3100
          }
3101

3102
          if (ctx.newValue != ctx.oldValue) {
1✔
3103
            n.setValue(ctx.newValue, valueReferenceQueue());
1✔
3104
          }
3105
          n.setWeight(ctx.newWeight);
1✔
3106

3107
          long expirationTime = isComputingAsync(ctx.newValue)
1✔
3108
              ? ctx.now + ASYNC_EXPIRY
1✔
3109
              : ctx.now;
1✔
3110
          ctx.exceedsTolerance = exceedsWriteTimeTolerance(n, varTime, expirationTime);
1✔
3111
          if (((ctx.cause != null) && ctx.cause.wasEvicted()) || ctx.exceedsTolerance) {
1✔
3112
            setWriteTime(n, expirationTime);
1✔
3113
          }
3114
          setAccessTime(n, expirationTime);
1✔
3115
          setVariableTime(n, varTime);
1✔
3116
          discardRefresh(kr);
1✔
3117
          return n;
1✔
3118
        } catch (Throwable e) {
1✔
3119
          discardRefresh(kr);
1✔
3120
          if (!wasEvicted) {
1✔
3121
            throw e;
1✔
3122
          }
3123
          ctx.newValue = null;
1✔
3124
          ctx.exception = e;
1✔
3125
          ctx.removed = n;
1✔
3126
          n.retire();
1✔
3127
          return null;
1✔
3128
        }
3129
      }
3130
    });
3131

3132
    if (ctx.cause != null) {
1✔
3133
      if (ctx.cause == RemovalCause.REPLACED) {
1✔
3134
        requireNonNull(ctx.newValue);
1✔
3135
        notifyOnReplace(key, ctx.oldValue, ctx.newValue);
1✔
3136
      } else {
3137
        if (ctx.cause.wasEvicted()) {
1✔
3138
          statsCounter().recordEviction(ctx.oldWeight, ctx.cause);
1✔
3139
        }
3140
        notifyRemoval(ctx.nodeKey, ctx.oldValue, ctx.cause);
1✔
3141
      }
3142
    }
3143

3144
    if (ctx.removed != null) {
1✔
3145
      afterWrite(new RemovalTask(ctx.removed));
1✔
3146
    } else if (node == null) {
1✔
3147
      // absent and not computable
3148
    } else if ((ctx.hints != null) && ctx.hints.preserveTimestamps) {
1✔
3149
      // The remapping was a signaled no-op; the node was not modified
3150
    } else if ((ctx.oldValue == null) && (ctx.cause == null)) {
1✔
3151
      afterWrite(new AddTask(node, ctx.newWeight));
1✔
3152
    } else {
3153
      int weightedDifference = ctx.newWeight - ctx.oldWeight;
1✔
3154
      if (ctx.exceedsTolerance || (weightedDifference != 0)) {
1✔
3155
        afterWrite(new UpdateTask(node, weightedDifference));
1✔
3156
      } else {
3157
        afterRead(node, ctx.now, /* recordHit= */ false);
1✔
3158
        if ((ctx.cause != null) && ctx.cause.wasEvicted()) {
1✔
3159
          scheduleDrainBuffers();
1✔
3160
        }
3161
      }
3162
    }
3163

3164
    if (ctx.exception != null) {
1✔
3165
      throw toUncheckedException(ctx.exception);
1✔
3166
    }
3167
    return ctx.newValue;
1✔
3168
  }
3169

3170
  @Override
3171
  public void forEach(BiConsumer<? super K, ? super V> action) {
3172
    requireNonNull(action);
1✔
3173

3174
    for (var iterator = new EntryIterator<>(this); iterator.hasNext();) {
1✔
3175
      action.accept(iterator.key, iterator.value);
1✔
3176
      iterator.advance();
1✔
3177
    }
3178
  }
1✔
3179

3180
  @Override
3181
  public Set<K> keySet() {
3182
    Set<K> ks = keySet;
1✔
3183
    return (ks == null) ? (keySet = new KeySetView<>(this)) : ks;
1✔
3184
  }
3185

3186
  @Override
3187
  public Collection<V> values() {
3188
    Collection<V> vs = values;
1✔
3189
    return (vs == null) ? (values = new ValuesView<>(this)) : vs;
1✔
3190
  }
3191

3192
  @Override
3193
  public Set<Entry<K, V>> entrySet() {
3194
    Set<Entry<K, V>> es = entrySet;
1✔
3195
    return (es == null) ? (entrySet = new EntrySetView<>(this)) : es;
1✔
3196
  }
3197

3198
  /**
3199
   * Object equality requires reflexive, symmetric, transitive, and consistency properties. Of
3200
   * these, symmetry and consistency require further clarification for how they are upheld.
3201
   * <p>
3202
   * The <i>consistency</i> property between invocations requires that the results are the same if
3203
   * there are no modifications to the information used. Therefore, usages should expect that this
3204
   * operation may return misleading results if either the maps or the data held by them is modified
3205
   * during the execution of this method. This characteristic allows for comparing the map sizes and
3206
   * assuming stable mappings, as done by {@link java.util.AbstractMap}-based maps.
3207
   * <p>
3208
   * The <i>symmetric</i> property requires that the result is the same for all implementations of
3209
   * {@link Map#equals(Object)}. That contract is defined in terms of the stable mappings provided
3210
   * by {@link #entrySet()}, meaning that the {@link #size()} optimization forces that the count is
3211
   * consistent with the mappings when used for an equality check.
3212
   * <p>
3213
   * The cache's {@link #size()} method may include entries that have expired or have been reference
3214
   * collected, but have not yet been removed from the backing map. An iteration over the map may
3215
   * trigger the removal of these dead entries when skipped over during traversal. To ensure
3216
   * consistency and symmetry, usages should call {@link #cleanUp()} before this method while no
3217
   * other concurrent operations are being performed on this cache. This is not done implicitly by
3218
   * {@link #size()} as many usages assume it to be instantaneous and lock-free. As a postcondition
3219
   * the iteration count is verified against the prescreened {@link #size()} so that a concurrent
3220
   * maintenance pass that drops dead entries during traversal is detected and reported as not
3221
   * equal, rather than silently returning {@code true} on the surviving subset.
3222
   */
3223
  @Override
3224
  public boolean equals(@Nullable Object o) {
3225
    if (o == this) {
1✔
3226
      return true;
1✔
3227
    } else if (!(o instanceof Map)) {
1✔
3228
      return false;
1✔
3229
    }
3230

3231
    var map = (Map<?, ?>) o;
1✔
3232
    int expectedSize = size();
1✔
3233
    if (map.size() != expectedSize) {
1✔
3234
      return false;
1✔
3235
    }
3236

3237
    long now = expirationTicker().read();
1✔
3238
    @Var int count = 0;
1✔
3239
    try {
3240
      for (var node : data.values()) {
1✔
3241
        K key = node.getKey();
1✔
3242
        V value = node.getValue();
1✔
3243
        if ((key == null) || (value == null)
1✔
3244
            || !node.isAlive() || hasExpired(node, now, value)) {
1✔
3245
          scheduleDrainBuffers();
1✔
3246
          return false;
1✔
3247
        } else {
3248
          var val = map.get(key);
1✔
3249
          if ((val == null) || ((val != value) && !val.equals(value))) {
1✔
3250
            return false;
1✔
3251
          }
3252
        }
3253
        count++;
1✔
3254
      }
1✔
3255
    } catch (ClassCastException | NullPointerException ignored) {
1✔
3256
      return false;
1✔
3257
    }
1✔
3258
    return (count == expectedSize);
1✔
3259
  }
3260

3261
  @Override
3262
  public int hashCode() {
3263
    @Var int hash = 0;
1✔
3264
    @Var boolean drain = false;
1✔
3265
    long now = expirationTicker().read();
1✔
3266
    for (var node : data.values()) {
1✔
3267
      K key = node.getKey();
1✔
3268
      V value = node.getValue();
1✔
3269
      if ((key == null) || (value == null)
1✔
3270
          || !node.isAlive() || hasExpired(node, now, value)) {
1✔
3271
        drain = true;
1✔
3272
      } else {
3273
        hash += key.hashCode() ^ value.hashCode();
1✔
3274
      }
3275
    }
1✔
3276
    if (drain) {
1✔
3277
      scheduleDrainBuffers();
1✔
3278
    }
3279
    return hash;
1✔
3280
  }
3281

3282
  @Override
3283
  public String toString() {
3284
    @Var boolean drain = false;
1✔
3285
    long now = expirationTicker().read();
1✔
3286
    var result = new StringBuilder().append('{');
1✔
3287
    for (var node : data.values()) {
1✔
3288
      K key = node.getKey();
1✔
3289
      V value = node.getValue();
1✔
3290
      if ((key == null) || (value == null)
1!
3291
          || !node.isAlive() || hasExpired(node, now, value)) {
1✔
3292
        drain = true;
1✔
3293
      } else {
3294
        if (result.length() != 1) {
1✔
3295
          result.append(',').append(' ');
1✔
3296
        }
3297
        result.append((key == this) ? "(this Map)" : key);
1✔
3298
        result.append('=');
1✔
3299
        result.append((value == this) ? "(this Map)" : value);
1✔
3300
      }
3301
    }
1✔
3302
    if (drain) {
1✔
3303
      scheduleDrainBuffers();
1✔
3304
    }
3305
    return result.append('}').toString();
1✔
3306
  }
3307

3308
  /**
3309
   * Returns the computed result from the ordered traversal of the cache entries.
3310
   *
3311
   * @param hottest the coldest or hottest iteration order
3312
   * @param transformer a function that unwraps the value
3313
   * @param mappingFunction the mapping function to compute a value
3314
   * @return the computed value
3315
   */
3316
  @SuppressWarnings("GuardedByChecker")
3317
  <T> T evictionOrder(boolean hottest, Function<@Nullable V, @Nullable V> transformer,
3318
      Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
3319
    Comparator<Node<K, V>> comparator = Comparator.comparingInt(node -> {
1✔
3320
      var keyRef = node.getKeyReferenceOrNull();
1✔
3321
      return ((keyRef == null) || !node.isAlive()) ? 0 : frequencySketch().frequency(keyRef);
1!
3322
    });
3323
    Iterable<Node<K, V>> iterable;
3324
    if (hottest) {
1✔
3325
      iterable = () -> {
1✔
3326
        var secondary = PeekingIterator.comparing(
1✔
3327
            accessOrderProbationDeque().descendingIterator(),
1✔
3328
            accessOrderWindowDeque().descendingIterator(), comparator);
1✔
3329
        return PeekingIterator.concat(
1✔
3330
            accessOrderProtectedDeque().descendingIterator(), secondary);
1✔
3331
      };
3332
    } else {
3333
      iterable = () -> {
1✔
3334
        var primary = PeekingIterator.comparing(
1✔
3335
            accessOrderWindowDeque().iterator(), accessOrderProbationDeque().iterator(),
1✔
3336
            comparator.reversed());
1✔
3337
        return PeekingIterator.concat(primary, accessOrderProtectedDeque().iterator());
1✔
3338
      };
3339
    }
3340
    return snapshot(iterable, transformer, mappingFunction);
1✔
3341
  }
3342

3343
  /**
3344
   * Returns the computed result from the ordered traversal of the cache entries.
3345
   *
3346
   * @param oldest the youngest or oldest iteration order
3347
   * @param transformer a function that unwraps the value
3348
   * @param mappingFunction the mapping function to compute a value
3349
   * @return the computed value
3350
   */
3351
  @SuppressWarnings("GuardedByChecker")
3352
  <T> T expireAfterAccessOrder(boolean oldest, Function<@Nullable V, @Nullable V> transformer,
3353
      Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
3354
    Iterable<Node<K, V>> iterable;
3355
    if (evicts()) {
1✔
3356
      iterable = () -> {
1✔
3357
        @Var Comparator<Node<K, V>> comparator = Comparator.comparingLong(Node::getAccessTime);
1✔
3358
        PeekingIterator<Node<K, V>> first;
3359
        PeekingIterator<Node<K, V>> second;
3360
        PeekingIterator<Node<K, V>> third;
3361
        if (oldest) {
1✔
3362
          comparator = comparator.reversed();
1✔
3363
          first = accessOrderWindowDeque().iterator();
1✔
3364
          second = accessOrderProbationDeque().iterator();
1✔
3365
          third = accessOrderProtectedDeque().iterator();
1✔
3366
        } else {
3367
          first = accessOrderWindowDeque().descendingIterator();
1✔
3368
          second = accessOrderProbationDeque().descendingIterator();
1✔
3369
          third = accessOrderProtectedDeque().descendingIterator();
1✔
3370
        }
3371
        return PeekingIterator.comparing(
1✔
3372
            PeekingIterator.comparing(first, second, comparator), third, comparator);
1✔
3373
      };
3374
    } else {
3375
      iterable = oldest
1✔
3376
          ? accessOrderWindowDeque()
1✔
3377
          : accessOrderWindowDeque()::descendingIterator;
1✔
3378
    }
3379
    return snapshot(iterable, transformer, mappingFunction);
1✔
3380
  }
3381

3382
  /**
3383
   * Returns the computed result from the ordered traversal of the cache entries.
3384
   *
3385
   * @param iterable the supplier of the entries in the cache
3386
   * @param transformer a function that unwraps the value
3387
   * @param mappingFunction the mapping function to compute a value
3388
   * @return the computed value
3389
   */
3390
  <T> T snapshot(Iterable<Node<K, V>> iterable, Function<@Nullable V, @Nullable V> transformer,
3391
      Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
3392
    requireNonNull(mappingFunction);
1✔
3393
    requireNonNull(transformer);
1✔
3394
    requireNonNull(iterable);
1✔
3395

3396
    evictionLock.lock();
1✔
3397
    try {
3398
      maintenance(/* ignored */ null);
1✔
3399

3400
      // Obtain the iterator as late as possible for modification count checking
3401
      try (var stream = StreamSupport.stream(Spliterators.spliteratorUnknownSize(
1✔
3402
           iterable.iterator(), DISTINCT | ORDERED | NONNULL | IMMUTABLE), /* parallel= */ false)) {
1✔
3403
        return mappingFunction.apply(stream
1✔
3404
            .map(node -> nodeToCacheEntry(node, transformer, node.getPolicyWeight()))
1✔
3405
            .filter(Objects::nonNull));
1✔
3406
      }
3407
    } finally {
3408
      evictionLock.unlock();
1✔
3409
      rescheduleCleanUpIfIncomplete();
1✔
3410
    }
3411
  }
3412

3413
  /**
3414
   * Returns an entry for the given node if it can be used externally, else null. The weight is
3415
   * caller-supplied: snapshot callers hold evictionLock and read policyWeight (in sync with the
3416
   * drain thread); unlocked readers read weight, which may be stale for a concurrent in-place
3417
   * update (acceptable for a point-in-time CacheEntry).
3418
   */
3419
  @Nullable CacheEntry<K, V> nodeToCacheEntry(
3420
      Node<K, V> node, Function<@Nullable V, @Nullable V> transformer, int weight) {
3421
    V rawValue = node.getValue();
1✔
3422
    if (rawValue == null) {
1✔
3423
      return null;
1✔
3424
    }
3425
    V value = transformer.apply(rawValue);
1✔
3426
    K key = node.getKey();
1✔
3427
    long now;
3428
    if ((key == null) || (value == null) || !node.isAlive()
1✔
3429
        || hasExpired(node, (now = expirationTicker().read()), rawValue)) {
1✔
3430
      return null;
1✔
3431
    }
3432

3433
    @Var long expiresAfter = Long.MAX_VALUE;
1✔
3434
    if (expiresAfterAccess()) {
1✔
3435
      expiresAfter = Math.min(expiresAfter,
1✔
3436
          expiresAfterAccessNanos() - (now - node.getAccessTime()));
1✔
3437
    }
3438
    if (expiresAfterWrite()) {
1✔
3439
      expiresAfter = Math.min(expiresAfter,
1✔
3440
          expiresAfterWriteNanos() - ((now & ~1L) - (node.getWriteTime() & ~1L)));
1✔
3441
    }
3442
    if (expiresVariable()) {
1✔
3443
      expiresAfter = node.getVariableTime() - now;
1✔
3444
    }
3445

3446
    long refreshableAt = refreshAfterWrite()
1✔
3447
        ? (node.getWriteTime() & ~1L) + refreshAfterWriteNanos()
1✔
3448
        : now + Long.MAX_VALUE;
1✔
3449
    return SnapshotEntry.forEntry(key, value, now, weight, now + expiresAfter, refreshableAt);
1✔
3450
  }
3451

3452
  /** Mutable context for passing state between a lambda and the caller. */
3453
  static final class EvictContext<V> {
1✔
3454
    @Nullable RemovalCause cause;
3455
    @Nullable V value;
3456
    boolean resurrect;
3457
    boolean removed;
3458
    int oldWeight;
3459
  }
3460

3461
  /** Mutable context for passing state between a lambda and the caller. */
3462
  static final class RemoveContext<K, V> {
1✔
3463
    @Nullable K oldKey;
3464
    @Nullable V oldValue;
3465
    @Nullable Node<K, V> node;
3466
    @Nullable RemovalCause cause;
3467
    int oldWeight;
3468
  }
3469

3470
  /** Mutable context for passing state between a lambda and the caller. */
3471
  static final class ReplaceContext<K, V> {
1✔
3472
    @Nullable K nodeKey;
3473
    @Nullable V oldValue;
3474

3475
    long now;
3476
    int oldWeight;
3477
    boolean exceedsTolerance;
3478
  }
3479

3480
  /** Mutable context for passing state between a lambda and the caller. */
3481
  static final class ComputeContext<K, V> {
3482
    @Nullable K nodeKey;
3483
    @Nullable V oldValue;
3484
    @Nullable V newValue;
3485
    @Nullable Node<K, V> removed;
3486
    @Nullable RemovalCause cause;
3487
    @Nullable Throwable exception;
3488
    @Nullable RemapHints hints;
3489

3490
    long now;
3491
    int oldWeight;
3492
    int newWeight;
3493
    boolean exceedsTolerance;
3494

3495
    ComputeContext(long now) {
1✔
3496
      this.now = now;
1✔
3497
    }
1✔
3498
  }
3499

3500
  /** A function that produces an unmodifiable map up to the limit in stream order. */
3501
  static final class SizeLimiter<K, V> implements Function<Stream<CacheEntry<K, V>>, Map<K, V>> {
3502
    private final int expectedSize;
3503
    private final long limit;
3504

3505
    SizeLimiter(int expectedSize, long limit) {
1✔
3506
      requireArgument(limit >= 0);
1✔
3507
      this.expectedSize = expectedSize;
1✔
3508
      this.limit = limit;
1✔
3509
    }
1✔
3510

3511
    @Override
3512
    public Map<K, V> apply(Stream<CacheEntry<K, V>> stream) {
3513
      var map = new LinkedHashMap<K, V>(calculateHashMapCapacity(expectedSize));
1✔
3514
      stream.limit(limit).forEach(entry -> map.put(entry.getKey(), entry.getValue()));
1✔
3515
      return Collections.unmodifiableMap(map);
1✔
3516
    }
3517
  }
3518

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

3523
    private long weightedSize;
3524

3525
    WeightLimiter(long weightLimit) {
1✔
3526
      requireArgument(weightLimit >= 0);
1✔
3527
      this.weightLimit = weightLimit;
1✔
3528
    }
1✔
3529

3530
    @Override
3531
    public Map<K, V> apply(Stream<CacheEntry<K, V>> stream) {
3532
      var map = new LinkedHashMap<K, V>();
1✔
3533
      stream.takeWhile(entry -> {
1✔
3534
        weightedSize = Math.addExact(weightedSize, entry.weight());
1✔
3535
        return (weightedSize <= weightLimit);
1✔
3536
      }).forEach(entry -> map.put(entry.getKey(), entry.getValue()));
1✔
3537
      return Collections.unmodifiableMap(map);
1✔
3538
    }
3539
  }
3540

3541
  /** An adapter to safely externalize the keys. */
3542
  static final class KeySetView<K, V> extends AbstractSet<K> {
3543
    final BoundedLocalCache<K, V> cache;
3544

3545
    KeySetView(BoundedLocalCache<K, V> cache) {
1✔
3546
      this.cache = requireNonNull(cache);
1✔
3547
    }
1✔
3548

3549
    @Override
3550
    public int size() {
3551
      return cache.size();
1✔
3552
    }
3553

3554
    @Override
3555
    public void clear() {
3556
      cache.clear();
1✔
3557
    }
1✔
3558

3559
    @Override
3560
    @SuppressWarnings("SuspiciousMethodCalls")
3561
    public boolean contains(Object o) {
3562
      return cache.containsKey(o);
1✔
3563
    }
3564

3565
    @Override
3566
    public boolean containsAll(Collection<?> collection) {
3567
      requireNonNull(collection);
1✔
3568
      if (collection != this) {
1✔
3569
        for (Object o : collection) {
1✔
3570
          if ((o == null) || !contains(o)) {
1✔
3571
            return false;
1✔
3572
          }
3573
        }
1✔
3574
      }
3575
      return true;
1✔
3576
    }
3577

3578
    @Override
3579
    public boolean removeAll(Collection<?> collection) {
3580
      requireNonNull(collection);
1✔
3581
      @Var boolean modified = false;
1✔
3582
      if (cache.collectKeys() || ((collection instanceof Set<?>) && (collection.size() > size()))) {
1✔
3583
        for (K key : this) {
1✔
3584
          if (collection.contains(key)) {
1✔
3585
            modified |= remove(key);
1✔
3586
          }
3587
        }
1✔
3588
      } else {
3589
        for (var item : collection) {
1✔
3590
          modified |= (item != null) && remove(item);
1✔
3591
        }
1✔
3592
      }
3593
      return modified;
1✔
3594
    }
3595

3596
    @Override
3597
    public boolean remove(Object o) {
3598
      return (cache.remove(o) != null);
1✔
3599
    }
3600

3601
    @Override
3602
    public boolean removeIf(Predicate<? super K> filter) {
3603
      requireNonNull(filter);
1✔
3604
      @Var boolean modified = false;
1✔
3605
      for (K key : this) {
1✔
3606
        if (filter.test(key) && remove(key)) {
1✔
3607
          modified = true;
1✔
3608
        }
3609
      }
1✔
3610
      return modified;
1✔
3611
    }
3612

3613
    @Override
3614
    public boolean retainAll(Collection<?> collection) {
3615
      requireNonNull(collection);
1✔
3616
      @Var boolean modified = false;
1✔
3617
      for (K key : this) {
1✔
3618
        if (!collection.contains(key) && remove(key)) {
1✔
3619
          modified = true;
1✔
3620
        }
3621
      }
1✔
3622
      return modified;
1✔
3623
    }
3624

3625
    @Override
3626
    public Iterator<K> iterator() {
3627
      return new KeyIterator<>(cache);
1✔
3628
    }
3629

3630
    @Override
3631
    public Spliterator<K> spliterator() {
3632
      return new KeySpliterator<>(cache);
1✔
3633
    }
3634
  }
3635

3636
  /** An adapter to safely externalize the key iterator. */
3637
  static final class KeyIterator<K, V> implements Iterator<K> {
3638
    final EntryIterator<K, V> iterator;
3639

3640
    KeyIterator(BoundedLocalCache<K, V> cache) {
1✔
3641
      this.iterator = new EntryIterator<>(cache);
1✔
3642
    }
1✔
3643

3644
    @Override
3645
    public boolean hasNext() {
3646
      return iterator.hasNext();
1✔
3647
    }
3648

3649
    @Override
3650
    public K next() {
3651
      return iterator.nextKey();
1✔
3652
    }
3653

3654
    @Override
3655
    public void remove() {
3656
      iterator.remove();
1✔
3657
    }
1✔
3658
  }
3659

3660
  /** An adapter to safely externalize the key spliterator. */
3661
  static final class KeySpliterator<K, V> implements Spliterator<K> {
3662
    final Spliterator<Node<K, V>> spliterator;
3663
    final BoundedLocalCache<K, V> cache;
3664

3665
    KeySpliterator(BoundedLocalCache<K, V> cache) {
3666
      this(cache, cache.data.values().spliterator());
1✔
3667
    }
1✔
3668

3669
    KeySpliterator(BoundedLocalCache<K, V> cache, Spliterator<Node<K, V>> spliterator) {
1✔
3670
      this.spliterator = requireNonNull(spliterator);
1✔
3671
      this.cache = requireNonNull(cache);
1✔
3672
    }
1✔
3673

3674
    @Override
3675
    public void forEachRemaining(Consumer<? super K> action) {
3676
      requireNonNull(action);
1✔
3677
      Consumer<Node<K, V>> consumer = node -> {
1✔
3678
        K key = node.getKey();
1✔
3679
        V value = node.getValue();
1✔
3680
        long now = cache.expirationTicker().read();
1✔
3681
        if ((key == null) || (value == null) || cache.hasExpired(node, now, value)) {
1✔
3682
          cache.scheduleDrainBuffers();
1✔
3683
        } else if (node.isAlive()) {
1✔
3684
          action.accept(key);
1✔
3685
        }
3686
      };
1✔
3687
      spliterator.forEachRemaining(consumer);
1✔
3688
    }
1✔
3689

3690
    @Override
3691
    public boolean tryAdvance(Consumer<? super K> action) {
3692
      requireNonNull(action);
1✔
3693
      boolean[] advanced = { false };
1✔
3694
      Consumer<Node<K, V>> consumer = node -> {
1✔
3695
        K key = node.getKey();
1✔
3696
        V value = node.getValue();
1✔
3697
        long now = cache.expirationTicker().read();
1✔
3698
        if ((key == null) || (value == null) || cache.hasExpired(node, now, value)) {
1✔
3699
          cache.scheduleDrainBuffers();
1✔
3700
        } else if (node.isAlive()) {
1✔
3701
          action.accept(key);
1✔
3702
          advanced[0] = true;
1✔
3703
        }
3704
      };
1✔
3705
      while (spliterator.tryAdvance(consumer)) {
1✔
3706
        if (advanced[0]) {
1✔
3707
          return true;
1✔
3708
        }
3709
      }
3710
      return false;
1✔
3711
    }
3712

3713
    @Override
3714
    public @Nullable Spliterator<K> trySplit() {
3715
      Spliterator<Node<K, V>> split = spliterator.trySplit();
1✔
3716
      return (split == null) ? null : new KeySpliterator<>(cache, split);
1✔
3717
    }
3718

3719
    @Override
3720
    public long estimateSize() {
3721
      return spliterator.estimateSize();
1✔
3722
    }
3723

3724
    @Override
3725
    public int characteristics() {
3726
      return DISTINCT | CONCURRENT | NONNULL;
1✔
3727
    }
3728
  }
3729

3730
  /** An adapter to safely externalize the values. */
3731
  static final class ValuesView<K, V> extends AbstractCollection<V> {
3732
    final BoundedLocalCache<K, V> cache;
3733

3734
    ValuesView(BoundedLocalCache<K, V> cache) {
1✔
3735
      this.cache = requireNonNull(cache);
1✔
3736
    }
1✔
3737

3738
    @Override
3739
    public int size() {
3740
      return cache.size();
1✔
3741
    }
3742

3743
    @Override
3744
    public void clear() {
3745
      cache.clear();
1✔
3746
    }
1✔
3747

3748
    @Override
3749
    @SuppressWarnings("SuspiciousMethodCalls")
3750
    public boolean contains(Object o) {
3751
      return cache.containsValue(o);
1✔
3752
    }
3753

3754
    @Override
3755
    public boolean containsAll(Collection<?> collection) {
3756
      requireNonNull(collection);
1✔
3757
      if (collection != this) {
1✔
3758
        for (Object o : collection) {
1✔
3759
          if ((o == null) || !contains(o)) {
1✔
3760
            return false;
1✔
3761
          }
3762
        }
1✔
3763
      }
3764
      return true;
1✔
3765
    }
3766

3767
    @Override
3768
    public boolean removeAll(Collection<?> collection) {
3769
      requireNonNull(collection);
1✔
3770
      @Var boolean modified = false;
1✔
3771
      for (var iterator = new EntryIterator<>(cache); iterator.hasNext();) {
1✔
3772
        var key = requireNonNull(iterator.key);
1✔
3773
        var value = requireNonNull(iterator.value);
1✔
3774
        if (collection.contains(value) && cache.remove(key, value)) {
1✔
3775
          modified = true;
1✔
3776
        }
3777
        iterator.advance();
1✔
3778
      }
1✔
3779
      return modified;
1✔
3780
    }
3781

3782
    @Override
3783
    public boolean remove(@Nullable Object o) {
3784
      if (o == null) {
1✔
3785
        return false;
1✔
3786
      }
3787
      for (var iterator = new EntryIterator<>(cache); iterator.hasNext();) {
1✔
3788
        var key = requireNonNull(iterator.key);
1✔
3789
        var node = requireNonNull(iterator.next);
1✔
3790
        var value = requireNonNull(iterator.value);
1✔
3791
        if (node.containsValue(o) && cache.remove(key, value)) {
1✔
3792
          return true;
1✔
3793
        }
3794
        iterator.advance();
1✔
3795
      }
1✔
3796
      return false;
1✔
3797
    }
3798

3799
    @Override
3800
    public boolean removeIf(Predicate<? super V> filter) {
3801
      requireNonNull(filter);
1✔
3802
      @Var boolean modified = false;
1✔
3803
      for (var iterator = new EntryIterator<>(cache); iterator.hasNext();) {
1✔
3804
        var value = requireNonNull(iterator.value);
1✔
3805
        if (filter.test(value)) {
1✔
3806
          var key = requireNonNull(iterator.key);
1✔
3807
          modified |= cache.remove(key, value);
1✔
3808
        }
3809
        iterator.advance();
1✔
3810
      }
1✔
3811
      return modified;
1✔
3812
    }
3813

3814
    @Override
3815
    public boolean retainAll(Collection<?> collection) {
3816
      requireNonNull(collection);
1✔
3817
      @Var boolean modified = false;
1✔
3818
      for (var iterator = new EntryIterator<>(cache); iterator.hasNext();) {
1✔
3819
        var key = requireNonNull(iterator.key);
1✔
3820
        var value = requireNonNull(iterator.value);
1✔
3821
        if (!collection.contains(value) && cache.remove(key, value)) {
1✔
3822
          modified = true;
1✔
3823
        }
3824
        iterator.advance();
1✔
3825
      }
1✔
3826
      return modified;
1✔
3827
    }
3828

3829
    @Override
3830
    public Iterator<V> iterator() {
3831
      return new ValueIterator<>(cache);
1✔
3832
    }
3833

3834
    @Override
3835
    public Spliterator<V> spliterator() {
3836
      return new ValueSpliterator<>(cache);
1✔
3837
    }
3838
  }
3839

3840
  /** An adapter to safely externalize the value iterator. */
3841
  static final class ValueIterator<K, V> implements Iterator<V> {
3842
    final EntryIterator<K, V> iterator;
3843

3844
    ValueIterator(BoundedLocalCache<K, V> cache) {
1✔
3845
      this.iterator = new EntryIterator<>(cache);
1✔
3846
    }
1✔
3847

3848
    @Override
3849
    public boolean hasNext() {
3850
      return iterator.hasNext();
1✔
3851
    }
3852

3853
    @Override
3854
    public V next() {
3855
      return iterator.nextValue();
1✔
3856
    }
3857

3858
    @Override
3859
    public void remove() {
3860
      iterator.remove();
1✔
3861
    }
1✔
3862
  }
3863

3864
  /** An adapter to safely externalize the value spliterator. */
3865
  static final class ValueSpliterator<K, V> implements Spliterator<V> {
3866
    final Spliterator<Node<K, V>> spliterator;
3867
    final BoundedLocalCache<K, V> cache;
3868

3869
    ValueSpliterator(BoundedLocalCache<K, V> cache) {
3870
      this(cache, cache.data.values().spliterator());
1✔
3871
    }
1✔
3872

3873
    ValueSpliterator(BoundedLocalCache<K, V> cache, Spliterator<Node<K, V>> spliterator) {
1✔
3874
      this.spliterator = requireNonNull(spliterator);
1✔
3875
      this.cache = requireNonNull(cache);
1✔
3876
    }
1✔
3877

3878
    @Override
3879
    public void forEachRemaining(Consumer<? super V> action) {
3880
      requireNonNull(action);
1✔
3881
      Consumer<Node<K, V>> consumer = node -> {
1✔
3882
        K key = node.getKey();
1✔
3883
        V value = node.getValue();
1✔
3884
        long now = cache.expirationTicker().read();
1✔
3885
        if ((key == null) || (value == null) || cache.hasExpired(node, now, value)) {
1✔
3886
          cache.scheduleDrainBuffers();
1✔
3887
        } else if (node.isAlive()) {
1✔
3888
          action.accept(value);
1✔
3889
        }
3890
      };
1✔
3891
      spliterator.forEachRemaining(consumer);
1✔
3892
    }
1✔
3893

3894
    @Override
3895
    public boolean tryAdvance(Consumer<? super V> action) {
3896
      requireNonNull(action);
1✔
3897
      boolean[] advanced = { false };
1✔
3898
      Consumer<Node<K, V>> consumer = node -> {
1✔
3899
        K key = node.getKey();
1✔
3900
        V value = node.getValue();
1✔
3901
        long now = cache.expirationTicker().read();
1✔
3902
        if ((key == null) || (value == null) || cache.hasExpired(node, now, value)) {
1✔
3903
          cache.scheduleDrainBuffers();
1✔
3904
        } else if (node.isAlive()) {
1✔
3905
          action.accept(value);
1✔
3906
          advanced[0] = true;
1✔
3907
        }
3908
      };
1✔
3909
      while (spliterator.tryAdvance(consumer)) {
1✔
3910
        if (advanced[0]) {
1✔
3911
          return true;
1✔
3912
        }
3913
      }
3914
      return false;
1✔
3915
    }
3916

3917
    @Override
3918
    public @Nullable Spliterator<V> trySplit() {
3919
      Spliterator<Node<K, V>> split = spliterator.trySplit();
1✔
3920
      return (split == null) ? null : new ValueSpliterator<>(cache, split);
1✔
3921
    }
3922

3923
    @Override
3924
    public long estimateSize() {
3925
      return spliterator.estimateSize();
1✔
3926
    }
3927

3928
    @Override
3929
    public int characteristics() {
3930
      return CONCURRENT | NONNULL;
1✔
3931
    }
3932
  }
3933

3934
  /** An adapter to safely externalize the entries. */
3935
  static final class EntrySetView<K, V> extends AbstractSet<Entry<K, V>> {
3936
    final BoundedLocalCache<K, V> cache;
3937

3938
    EntrySetView(BoundedLocalCache<K, V> cache) {
1✔
3939
      this.cache = requireNonNull(cache);
1✔
3940
    }
1✔
3941

3942
    @Override
3943
    public int size() {
3944
      return cache.size();
1✔
3945
    }
3946

3947
    @Override
3948
    public void clear() {
3949
      cache.clear();
1✔
3950
    }
1✔
3951

3952
    @Override
3953
    public boolean contains(Object o) {
3954
      if (!(o instanceof Entry<?, ?>)) {
1✔
3955
        return false;
1✔
3956
      }
3957
      var entry = (Entry<?, ?>) o;
1✔
3958
      var key = entry.getKey();
1✔
3959
      var value = entry.getValue();
1✔
3960
      if ((key == null) || (value == null)) {
1✔
3961
        return false;
1✔
3962
      }
3963
      Node<K, V> node = cache.data.get(cache.nodeFactory.newLookupKey(key));
1✔
3964
      if (node == null) {
1✔
3965
        return false;
1✔
3966
      }
3967
      V nodeValue = node.getValue();
1✔
3968
      return (nodeValue != null) && node.containsValue(value)
1✔
3969
          && !cache.hasExpired(node, cache.expirationTicker().read(), nodeValue);
1✔
3970
    }
3971

3972
    @Override
3973
    public boolean removeAll(Collection<?> collection) {
3974
      requireNonNull(collection);
1✔
3975
      @Var boolean modified = false;
1✔
3976
      if (cache.collectKeys() || ((collection instanceof Set<?>) && (collection.size() > size()))) {
1✔
3977
        for (var entry : this) {
1✔
3978
          if (collection.contains(entry)) {
1✔
3979
            modified |= remove(entry);
1✔
3980
          }
3981
        }
1✔
3982
      } else {
3983
        for (var item : collection) {
1✔
3984
          modified |= (item != null) && remove(item);
1✔
3985
        }
1✔
3986
      }
3987
      return modified;
1✔
3988
    }
3989

3990
    @Override
3991
    @SuppressWarnings("SuspiciousMethodCalls")
3992
    public boolean remove(Object o) {
3993
      if (!(o instanceof Entry<?, ?>)) {
1✔
3994
        return false;
1✔
3995
      }
3996
      var entry = (Entry<?, ?>) o;
1✔
3997
      var key = entry.getKey();
1✔
3998
      return (key != null) && cache.remove(key, entry.getValue());
1✔
3999
    }
4000

4001
    @Override
4002
    public boolean removeIf(Predicate<? super Entry<K, V>> filter) {
4003
      requireNonNull(filter);
1✔
4004
      @Var boolean modified = false;
1✔
4005
      for (var iterator = new EntryIterator<>(cache); iterator.hasNext();) {
1✔
4006
        var key = requireNonNull(iterator.key);
1✔
4007
        var value = requireNonNull(iterator.value);
1✔
4008
        if (filter.test(Map.entry(key, value))) {
1✔
4009
          modified |= cache.remove(key, value);
1✔
4010
        }
4011
        iterator.advance();
1✔
4012
      }
1✔
4013
      return modified;
1✔
4014
    }
4015

4016
    @Override
4017
    public boolean retainAll(Collection<?> collection) {
4018
      requireNonNull(collection);
1✔
4019
      @Var boolean modified = false;
1✔
4020
      for (var entry : this) {
1✔
4021
        if (!collection.contains(entry) && remove(entry)) {
1✔
4022
          modified = true;
1✔
4023
        }
4024
      }
1✔
4025
      return modified;
1✔
4026
    }
4027

4028
    @Override
4029
    public Iterator<Entry<K, V>> iterator() {
4030
      return new EntryIterator<>(cache);
1✔
4031
    }
4032

4033
    @Override
4034
    public Spliterator<Entry<K, V>> spliterator() {
4035
      return new EntrySpliterator<>(cache);
1✔
4036
    }
4037
  }
4038

4039
  /** An adapter to safely externalize the entry iterator. */
4040
  static final class EntryIterator<K, V> implements Iterator<Entry<K, V>> {
4041
    final BoundedLocalCache<K, V> cache;
4042
    final Iterator<Node<K, V>> iterator;
4043

4044
    @Nullable K key;
4045
    @Nullable V value;
4046
    @Nullable K removalKey;
4047
    @Nullable Node<K, V> next;
4048

4049
    EntryIterator(BoundedLocalCache<K, V> cache) {
1✔
4050
      this.iterator = cache.data.values().iterator();
1✔
4051
      this.cache = cache;
1✔
4052
    }
1✔
4053

4054
    @Override
4055
    public boolean hasNext() {
4056
      if (next != null) {
1✔
4057
        return true;
1✔
4058
      }
4059

4060
      long now = cache.expirationTicker().read();
1✔
4061
      while (iterator.hasNext()) {
1✔
4062
        next = iterator.next();
1✔
4063
        value = next.getValue();
1✔
4064
        key = next.getKey();
1✔
4065

4066
        boolean evictable = (key == null) || (value == null) || cache.hasExpired(next, now, value);
1✔
4067
        if (evictable || !next.isAlive()) {
1✔
4068
          if (evictable) {
1✔
4069
            cache.scheduleDrainBuffers();
1✔
4070
          }
4071
          advance();
1✔
4072
          continue;
1✔
4073
        }
4074
        return true;
1✔
4075
      }
4076
      return false;
1✔
4077
    }
4078

4079
    /** Invalidates the current position so that the iterator may compute the next position. */
4080
    void advance() {
4081
      value = null;
1✔
4082
      next = null;
1✔
4083
      key = null;
1✔
4084
    }
1✔
4085

4086
    K nextKey() {
4087
      if (!hasNext()) {
1✔
4088
        throw new NoSuchElementException();
1✔
4089
      }
4090
      removalKey = key;
1✔
4091
      advance();
1✔
4092
      return requireNonNull(removalKey);
1✔
4093
    }
4094

4095
    V nextValue() {
4096
      if (!hasNext()) {
1✔
4097
        throw new NoSuchElementException();
1✔
4098
      }
4099
      removalKey = key;
1✔
4100
      V val = value;
1✔
4101
      advance();
1✔
4102
      return requireNonNull(val);
1✔
4103
    }
4104

4105
    @Override
4106
    public Entry<K, V> next() {
4107
      if (!hasNext()) {
1✔
4108
        throw new NoSuchElementException();
1✔
4109
      }
4110
      var entry = new WriteThroughEntry<K, @NonNull V>(
1✔
4111
          cache, requireNonNull(key), requireNonNull(value));
1✔
4112
      removalKey = key;
1✔
4113
      advance();
1✔
4114
      return entry;
1✔
4115
    }
4116

4117
    @Override
4118
    public void remove() {
4119
      if (removalKey == null) {
1✔
4120
        throw new IllegalStateException();
1✔
4121
      }
4122
      cache.remove(removalKey);
1✔
4123
      removalKey = null;
1✔
4124
    }
1✔
4125
  }
4126

4127
  /** An adapter to safely externalize the entry spliterator. */
4128
  static final class EntrySpliterator<K, V> implements Spliterator<Entry<K, V>> {
4129
    final Spliterator<Node<K, V>> spliterator;
4130
    final BoundedLocalCache<K, V> cache;
4131

4132
    EntrySpliterator(BoundedLocalCache<K, V> cache) {
4133
      this(cache, cache.data.values().spliterator());
1✔
4134
    }
1✔
4135

4136
    EntrySpliterator(BoundedLocalCache<K, V> cache, Spliterator<Node<K, V>> spliterator) {
1✔
4137
      this.spliterator = requireNonNull(spliterator);
1✔
4138
      this.cache = requireNonNull(cache);
1✔
4139
    }
1✔
4140

4141
    @Override
4142
    public void forEachRemaining(Consumer<? super Entry<K, V>> action) {
4143
      requireNonNull(action);
1✔
4144
      Consumer<Node<K, V>> consumer = node -> {
1✔
4145
        K key = node.getKey();
1✔
4146
        V value = node.getValue();
1✔
4147
        long now = cache.expirationTicker().read();
1✔
4148
        if ((key == null) || (value == null) || cache.hasExpired(node, now, value)) {
1✔
4149
          cache.scheduleDrainBuffers();
1✔
4150
        } else if (node.isAlive()) {
1✔
4151
          action.accept(new WriteThroughEntry<>(cache, key, value));
1✔
4152
        }
4153
      };
1✔
4154
      spliterator.forEachRemaining(consumer);
1✔
4155
    }
1✔
4156

4157
    @Override
4158
    public boolean tryAdvance(Consumer<? super Entry<K, V>> action) {
4159
      requireNonNull(action);
1✔
4160
      boolean[] advanced = { false };
1✔
4161
      Consumer<Node<K, V>> consumer = node -> {
1✔
4162
        K key = node.getKey();
1✔
4163
        V value = node.getValue();
1✔
4164
        long now = cache.expirationTicker().read();
1✔
4165
        if ((key == null) || (value == null) || cache.hasExpired(node, now, value)) {
1✔
4166
          cache.scheduleDrainBuffers();
1✔
4167
        } else if (node.isAlive()) {
1✔
4168
          action.accept(new WriteThroughEntry<>(cache, key, value));
1✔
4169
          advanced[0] = true;
1✔
4170
        }
4171
      };
1✔
4172
      while (spliterator.tryAdvance(consumer)) {
1✔
4173
        if (advanced[0]) {
1✔
4174
          return true;
1✔
4175
        }
4176
      }
4177
      return false;
1✔
4178
    }
4179

4180
    @Override
4181
    public @Nullable Spliterator<Entry<K, V>> trySplit() {
4182
      Spliterator<Node<K, V>> split = spliterator.trySplit();
1✔
4183
      return (split == null) ? null : new EntrySpliterator<>(cache, split);
1✔
4184
    }
4185

4186
    @Override
4187
    public long estimateSize() {
4188
      return spliterator.estimateSize();
1✔
4189
    }
4190

4191
    @Override
4192
    public int characteristics() {
4193
      return DISTINCT | CONCURRENT | NONNULL;
1✔
4194
    }
4195
  }
4196

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

4201
    final WeakReference<BoundedLocalCache<?, ?>> reference;
4202

4203
    PerformCleanupTask(BoundedLocalCache<?, ?> cache) {
1✔
4204
      reference = new WeakReference<>(cache);
1✔
4205
    }
1✔
4206

4207
    @Override
4208
    protected boolean exec() {
4209
      try {
4210
        run();
1✔
4211
      } catch (Throwable t) {
1✔
4212
        logger.log(Level.ERROR, "Exception thrown when performing the maintenance task", t);
1✔
4213
      }
1✔
4214

4215
      // Indicates that the task has not completed to allow subsequent submissions to execute
4216
      return false;
1✔
4217
    }
4218

4219
    @Override
4220
    public void run() {
4221
      BoundedLocalCache<?, ?> cache = reference.get();
1✔
4222
      if (cache != null) {
1✔
4223
        cache.performCleanUp(/* ignored */ null);
1✔
4224
      }
4225
    }
1✔
4226

4227
    /**
4228
     * This method cannot be ignored due to being final, so a hostile user supplied Executor could
4229
     * forcibly complete the task and halt future executions. There are easier ways to intentionally
4230
     * harm a system, so this is assumed to not happen in practice.
4231
     */
4232
    // public final void quietlyComplete() {}
4233

UNCOV
4234
    @Override public void complete(@Nullable Void value) {}
×
UNCOV
4235
    @Override public void setRawResult(@Nullable Void value) {}
×
UNCOV
4236
    @Override public @Nullable Void getRawResult() { return null; }
×
UNCOV
4237
    @Override public void completeExceptionally(@Nullable Throwable t) {}
×
UNCOV
4238
    @Override public boolean cancel(boolean mayInterruptIfRunning) { return false; }
×
4239
  }
4240

4241
  /** Creates a serialization proxy based on the common configuration shared by all cache types. */
4242
  static <K, V> SerializationProxy<K, V> makeSerializationProxy(BoundedLocalCache<?, ?> cache) {
4243
    var proxy = new SerializationProxy<K, V>();
1✔
4244
    proxy.weakKeys = cache.collectKeys();
1✔
4245
    proxy.weakValues = cache.nodeFactory.weakValues();
1✔
4246
    proxy.softValues = cache.nodeFactory.softValues();
1✔
4247
    proxy.isRecordingStats = cache.isRecordingStats();
1✔
4248
    proxy.evictionListener = cache.evictionListener;
1✔
4249
    proxy.removalListener = cache.removalListener();
1✔
4250
    proxy.ticker = cache.expirationTicker();
1✔
4251
    if (cache.expiresAfterAccess()) {
1✔
4252
      proxy.expiresAfterAccessNanos = cache.expiresAfterAccessNanos();
1✔
4253
    }
4254
    if (cache.expiresAfterWrite()) {
1✔
4255
      proxy.expiresAfterWriteNanos = cache.expiresAfterWriteNanos();
1✔
4256
    }
4257
    if (cache.expiresVariable()) {
1✔
4258
      proxy.expiry = cache.expiry();
1✔
4259
    }
4260
    if (cache.refreshAfterWrite()) {
1✔
4261
      proxy.refreshAfterWriteNanos = cache.refreshAfterWriteNanos();
1✔
4262
    }
4263
    if (cache.evicts()) {
1✔
4264
      if (cache.isWeighted) {
1✔
4265
        proxy.weigher = cache.weigher;
1✔
4266
        proxy.maximumWeight = cache.maximum();
1✔
4267
      } else {
4268
        proxy.maximumSize = cache.maximum();
1✔
4269
      }
4270
    }
4271
    proxy.cacheLoader = cache.cacheLoader;
1✔
4272
    proxy.async = cache.isAsync;
1✔
4273
    return proxy;
1✔
4274
  }
4275

4276
  /* --------------- Manual Cache --------------- */
4277

4278
  static class BoundedLocalManualCache<K, V> implements LocalManualCache<K, V>, Serializable {
4279
    private static final long serialVersionUID = 1;
4280

4281
    final BoundedLocalCache<K, V> cache;
4282

4283
    @Nullable Policy<K, V> policy;
4284

4285
    BoundedLocalManualCache(Caffeine<K, V> builder) {
4286
      this(builder, null);
1✔
4287
    }
1✔
4288

4289
    BoundedLocalManualCache(Caffeine<K, V> builder, @Nullable CacheLoader<? super K, V> loader) {
1✔
4290
      cache = LocalCacheFactory.newBoundedLocalCache(builder, loader, /* isAsync= */ false);
1✔
4291
    }
1✔
4292

4293
    @Override
4294
    public final BoundedLocalCache<K, V> cache() {
4295
      return cache;
1✔
4296
    }
4297

4298
    @Override
4299
    public final Policy<K, V> policy() {
4300
      if (policy == null) {
1✔
4301
        Function<@Nullable V, @Nullable V> identity = v -> v;
1✔
4302
        policy = new BoundedPolicy<>(cache, identity, cache.isWeighted);
1✔
4303
      }
4304
      return policy;
1✔
4305
    }
4306

4307
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
4308
      throw new InvalidObjectException("Proxy required");
1✔
4309
    }
4310

4311
    private Object writeReplace() {
4312
      return makeSerializationProxy(cache);
1✔
4313
    }
4314
  }
4315

4316
  @SuppressWarnings({"NullableOptional",
4317
    "OptionalAssignedToNull", "OptionalUsedAsFieldOrParameterType"})
4318
  static final class BoundedPolicy<K, V> implements Policy<K, V> {
4319
    final Function<@Nullable V, @Nullable V> transformer;
4320
    final BoundedLocalCache<K, V> cache;
4321
    final boolean isWeighted;
4322

4323
    @Nullable Optional<Eviction<K, V>> eviction;
4324
    @Nullable Optional<FixedRefresh<K, V>> refreshes;
4325
    @Nullable Optional<FixedExpiration<K, V>> afterWrite;
4326
    @Nullable Optional<FixedExpiration<K, V>> afterAccess;
4327
    @Nullable Optional<VarExpiration<K, V>> variable;
4328

4329
    BoundedPolicy(BoundedLocalCache<K, V> cache,
4330
        Function<@Nullable V, @Nullable V> transformer, boolean isWeighted) {
1✔
4331
      this.transformer = transformer;
1✔
4332
      this.isWeighted = isWeighted;
1✔
4333
      this.cache = cache;
1✔
4334
    }
1✔
4335

4336
    @Override public boolean isRecordingStats() {
4337
      return cache.isRecordingStats();
1✔
4338
    }
4339
    @Override public @Nullable V getIfPresentQuietly(K key) {
4340
      return transformer.apply(cache.getIfPresentQuietly(key));
1✔
4341
    }
4342
    @SuppressWarnings("GuardedByChecker")
4343
    @Override public @Nullable CacheEntry<K, V> getEntryIfPresentQuietly(K key) {
4344
      Node<K, V> node = cache.data.get(cache.nodeFactory.newLookupKey(key));
1✔
4345
      return (node == null) ? null : cache.nodeToCacheEntry(node, transformer, node.getWeight());
1✔
4346
    }
4347
    @SuppressWarnings("Java9CollectionFactory")
4348
    @Override public Map<K, CompletableFuture<V>> refreshes() {
4349
      var refreshes = cache.refreshes;
1✔
4350
      if ((refreshes == null) || refreshes.isEmpty()) {
1✔
4351
        @SuppressWarnings({"ImmutableMapOf", "RedundantUnmodifiable"})
4352
        Map<K, CompletableFuture<V>> emptyMap = Collections.unmodifiableMap(Collections.emptyMap());
1✔
4353
        return emptyMap;
1✔
4354
      } else if (cache.collectKeys()) {
1✔
4355
        var inFlight = new IdentityHashMap<K, CompletableFuture<V>>(refreshes.size());
1✔
4356
        for (var entry : refreshes.entrySet()) {
1✔
4357
          @SuppressWarnings("unchecked")
4358
          @Nullable K key = ((InternalReference<K>) entry.getKey()).get();
1✔
4359
          @SuppressWarnings("unchecked")
4360
          var future = (CompletableFuture<V>) entry.getValue();
1✔
4361
          if (key != null) {
1✔
4362
            inFlight.put(key, future);
1✔
4363
          }
4364
        }
1✔
4365
        return Collections.unmodifiableMap(inFlight);
1✔
4366
      }
4367
      @SuppressWarnings("unchecked")
4368
      var castedRefreshes = (Map<K, CompletableFuture<V>>) (Object) refreshes;
1✔
4369
      return Collections.unmodifiableMap(new HashMap<>(castedRefreshes));
1✔
4370
    }
4371
    @Override public Optional<Eviction<K, V>> eviction() {
4372
      return cache.evicts()
1✔
4373
          ? (eviction == null) ? (eviction = Optional.of(new BoundedEviction())) : eviction
1✔
4374
          : Optional.empty();
1✔
4375
    }
4376
    @Override public Optional<FixedExpiration<K, V>> expireAfterAccess() {
4377
      if (!cache.expiresAfterAccess()) {
1✔
4378
        return Optional.empty();
1✔
4379
      }
4380
      return (afterAccess == null)
1✔
4381
          ? (afterAccess = Optional.of(new BoundedExpireAfterAccess()))
1✔
4382
          : afterAccess;
1✔
4383
    }
4384
    @Override public Optional<FixedExpiration<K, V>> expireAfterWrite() {
4385
      if (!cache.expiresAfterWrite()) {
1✔
4386
        return Optional.empty();
1✔
4387
      }
4388
      return (afterWrite == null)
1✔
4389
          ? (afterWrite = Optional.of(new BoundedExpireAfterWrite()))
1✔
4390
          : afterWrite;
1✔
4391
    }
4392
    @Override public Optional<VarExpiration<K, V>> expireVariably() {
4393
      if (!cache.expiresVariable()) {
1✔
4394
        return Optional.empty();
1✔
4395
      }
4396
      return (variable == null)
1✔
4397
          ? (variable = Optional.of(new BoundedVarExpiration()))
1✔
4398
          : variable;
1✔
4399
    }
4400
    @Override public Optional<FixedRefresh<K, V>> refreshAfterWrite() {
4401
      if (!cache.refreshAfterWrite()) {
1✔
4402
        return Optional.empty();
1✔
4403
      }
4404
      return (refreshes == null)
1✔
4405
          ? (refreshes = Optional.of(new BoundedRefreshAfterWrite()))
1✔
4406
          : refreshes;
1✔
4407
    }
4408

4409
    final class BoundedEviction implements Eviction<K, V> {
1✔
4410
      @Override public boolean isWeighted() {
4411
        return isWeighted;
1✔
4412
      }
4413
      @Override public OptionalInt weightOf(K key) {
4414
        requireNonNull(key);
1✔
4415
        if (!isWeighted) {
1✔
4416
          return OptionalInt.empty();
1✔
4417
        }
4418
        Node<K, V> node = cache.data.get(cache.nodeFactory.newLookupKey(key));
1✔
4419
        if (node == null) {
1✔
4420
          return OptionalInt.empty();
1✔
4421
        }
4422
        V value = node.getValue();
1✔
4423
        if ((value == null) || cache.hasExpired(node, cache.expirationTicker().read(), value)) {
1✔
4424
          return OptionalInt.empty();
1✔
4425
        }
4426
        synchronized (node) {
1✔
4427
          return node.isAlive() ? OptionalInt.of(node.getWeight()) : OptionalInt.empty();
1✔
4428
        }
4429
      }
4430
      @Override public OptionalLong weightedSize() {
4431
        return isWeighted
1✔
4432
            ? OptionalLong.of(Math.max(0, cache.weightedSizeAcquire()))
1✔
4433
            : OptionalLong.empty();
1✔
4434
      }
4435
      @Override public long getMaximum() {
4436
        return cache.maximumAcquire();
1✔
4437
      }
4438
      @Override public void setMaximum(long maximum) {
4439
        cache.evictionLock.lock();
1✔
4440
        try {
4441
          cache.setMaximumSize(maximum);
1✔
4442
          cache.maintenance(/* ignored */ null);
1✔
4443
        } finally {
4444
          cache.evictionLock.unlock();
1✔
4445
          cache.rescheduleCleanUpIfIncomplete();
1✔
4446
        }
4447
      }
1✔
4448
      @Override public Map<K, V> coldest(int limit) {
4449
        int expectedSize = Math.min(limit, cache.size());
1✔
4450
        var limiter = new SizeLimiter<K, V>(expectedSize, limit);
1✔
4451
        return cache.evictionOrder(/* hottest= */ false, transformer, limiter);
1✔
4452
      }
4453
      @Override public Map<K, V> coldestWeighted(long weightLimit) {
4454
        var limiter = isWeighted()
1✔
4455
            ? new WeightLimiter<K, V>(weightLimit)
1✔
4456
            : new SizeLimiter<K, V>((int) Math.min(weightLimit, cache.size()), weightLimit);
1✔
4457
        return cache.evictionOrder(/* hottest= */ false, transformer, limiter);
1✔
4458
      }
4459
      @Override
4460
      public <T> T coldest(Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4461
        requireNonNull(mappingFunction);
1✔
4462
        return cache.evictionOrder(/* hottest= */ false, transformer, mappingFunction);
1✔
4463
      }
4464
      @Override public Map<K, V> hottest(int limit) {
4465
        int expectedSize = Math.min(limit, cache.size());
1✔
4466
        var limiter = new SizeLimiter<K, V>(expectedSize, limit);
1✔
4467
        return cache.evictionOrder(/* hottest= */ true, transformer, limiter);
1✔
4468
      }
4469
      @Override public Map<K, V> hottestWeighted(long weightLimit) {
4470
        var limiter = isWeighted()
1✔
4471
            ? new WeightLimiter<K, V>(weightLimit)
1✔
4472
            : new SizeLimiter<K, V>((int) Math.min(weightLimit, cache.size()), weightLimit);
1✔
4473
        return cache.evictionOrder(/* hottest= */ true, transformer, limiter);
1✔
4474
      }
4475
      @Override
4476
      public <T> T hottest(Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4477
        requireNonNull(mappingFunction);
1✔
4478
        return cache.evictionOrder(/* hottest= */ true, transformer, mappingFunction);
1✔
4479
      }
4480
    }
4481

4482
    @SuppressWarnings("PreferJavaTimeOverload")
4483
    final class BoundedExpireAfterAccess implements FixedExpiration<K, V> {
1✔
4484
      @Override public OptionalLong ageOf(K key, TimeUnit unit) {
4485
        requireNonNull(key);
1✔
4486
        requireNonNull(unit);
1✔
4487
        Object lookupKey = cache.nodeFactory.newLookupKey(key);
1✔
4488
        Node<K, V> node = cache.data.get(lookupKey);
1✔
4489
        if (node == null) {
1✔
4490
          return OptionalLong.empty();
1✔
4491
        }
4492
        V value = node.getValue();
1✔
4493
        if (value == null) {
1✔
4494
          return OptionalLong.empty();
1✔
4495
        }
4496
        long now = cache.expirationTicker().read();
1✔
4497
        return cache.hasExpired(node, now, value)
1✔
4498
            ? OptionalLong.empty()
1✔
4499
            : OptionalLong.of(unit.convert(now - node.getAccessTime(), TimeUnit.NANOSECONDS));
1✔
4500
      }
4501
      @Override public long getExpiresAfter(TimeUnit unit) {
4502
        return unit.convert(cache.expiresAfterAccessNanos(), TimeUnit.NANOSECONDS);
1✔
4503
      }
4504
      @Override public void setExpiresAfter(long duration, TimeUnit unit) {
4505
        requireArgument(duration >= 0);
1!
4506
        cache.setExpiresAfterAccessNanos(unit.toNanos(duration));
1✔
4507
        cache.scheduleAfterWrite();
1✔
4508
      }
1✔
4509
      @Override public Map<K, V> oldest(int limit) {
4510
        return oldest(new SizeLimiter<>(Math.min(limit, cache.size()), limit));
1✔
4511
      }
4512
      @Override public <T> T oldest(Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4513
        return cache.expireAfterAccessOrder(/* oldest= */ true, transformer, mappingFunction);
1✔
4514
      }
4515
      @Override public Map<K, V> youngest(int limit) {
4516
        return youngest(new SizeLimiter<>(Math.min(limit, cache.size()), limit));
1✔
4517
      }
4518
      @Override public <T> T youngest(Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4519
        return cache.expireAfterAccessOrder(/* oldest= */ false, transformer, mappingFunction);
1✔
4520
      }
4521
    }
4522

4523
    @SuppressWarnings("PreferJavaTimeOverload")
4524
    final class BoundedExpireAfterWrite implements FixedExpiration<K, V> {
1✔
4525
      @Override public OptionalLong ageOf(K key, TimeUnit unit) {
4526
        requireNonNull(key);
1✔
4527
        requireNonNull(unit);
1✔
4528
        Object lookupKey = cache.nodeFactory.newLookupKey(key);
1✔
4529
        Node<K, V> node = cache.data.get(lookupKey);
1✔
4530
        if (node == null) {
1✔
4531
          return OptionalLong.empty();
1✔
4532
        }
4533
        V value = node.getValue();
1✔
4534
        if (value == null) {
1✔
4535
          return OptionalLong.empty();
1✔
4536
        }
4537
        long now = cache.expirationTicker().read();
1✔
4538
        return cache.hasExpired(node, now, value)
1✔
4539
            ? OptionalLong.empty()
1✔
4540
            : OptionalLong.of(unit.convert(
1✔
4541
                (now & ~1L) - (node.getWriteTime() & ~1L), TimeUnit.NANOSECONDS));
1✔
4542
      }
4543
      @Override public long getExpiresAfter(TimeUnit unit) {
4544
        return unit.convert(cache.expiresAfterWriteNanos(), TimeUnit.NANOSECONDS);
1✔
4545
      }
4546
      @Override public void setExpiresAfter(long duration, TimeUnit unit) {
4547
        requireArgument(duration >= 0);
1✔
4548
        cache.setExpiresAfterWriteNanos(unit.toNanos(duration));
1✔
4549
        cache.scheduleAfterWrite();
1✔
4550
      }
1✔
4551
      @Override public Map<K, V> oldest(int limit) {
4552
        return oldest(new SizeLimiter<>(Math.min(limit, cache.size()), limit));
1✔
4553
      }
4554
      @SuppressWarnings("GuardedByChecker")
4555
      @Override public <T> T oldest(Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4556
        return cache.snapshot(cache.writeOrderDeque(), transformer, mappingFunction);
1✔
4557
      }
4558
      @Override public Map<K, V> youngest(int limit) {
4559
        return youngest(new SizeLimiter<>(Math.min(limit, cache.size()), limit));
1✔
4560
      }
4561
      @SuppressWarnings("GuardedByChecker")
4562
      @Override public <T> T youngest(Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4563
        return cache.snapshot(cache.writeOrderDeque()::descendingIterator,
1✔
4564
            transformer, mappingFunction);
4565
      }
4566
    }
4567

4568
    @SuppressWarnings("PreferJavaTimeOverload")
4569
    final class BoundedVarExpiration implements VarExpiration<K, V> {
1✔
4570
      @Override public OptionalLong getExpiresAfter(K key, TimeUnit unit) {
4571
        requireNonNull(key);
1✔
4572
        requireNonNull(unit);
1✔
4573
        Object lookupKey = cache.nodeFactory.newLookupKey(key);
1✔
4574
        Node<K, V> node = cache.data.get(lookupKey);
1✔
4575
        if (node == null) {
1✔
4576
          return OptionalLong.empty();
1✔
4577
        }
4578
        V value = node.getValue();
1✔
4579
        if (value == null) {
1✔
4580
          return OptionalLong.empty();
1✔
4581
        }
4582
        long now = cache.expirationTicker().read();
1✔
4583
        return cache.hasExpired(node, now, value)
1✔
4584
            ? OptionalLong.empty()
1✔
4585
            : OptionalLong.of(unit.convert(node.getVariableTime() - now, TimeUnit.NANOSECONDS));
1✔
4586
      }
4587
      @Override public void setExpiresAfter(K key, long duration, TimeUnit unit) {
4588
        requireNonNull(key);
1✔
4589
        requireNonNull(unit);
1✔
4590
        requireArgument(duration >= 0);
1✔
4591
        Object lookupKey = cache.nodeFactory.newLookupKey(key);
1✔
4592
        Node<K, V> node = cache.data.get(lookupKey);
1✔
4593
        if (node != null) {
1✔
4594
          long now;
4595
          long durationNanos = TimeUnit.NANOSECONDS.convert(duration, unit);
1✔
4596
          synchronized (node) {
1✔
4597
            now = cache.expirationTicker().read();
1✔
4598
            V value = node.getValue();
1✔
4599
            if ((value == null) || cache.isComputingAsync(value)
1✔
4600
                || cache.hasExpired(node, now, value)) {
1✔
4601
              return;
1✔
4602
            }
4603
            node.setVariableTime(now + Math.min(durationNanos, MAXIMUM_EXPIRY));
1✔
4604
          }
1✔
4605
          cache.afterRead(node, now, /* recordHit= */ false);
1✔
4606
        }
4607
      }
1✔
4608
      @Override public @Nullable V put(K key, V value, long duration, TimeUnit unit) {
4609
        requireNonNull(unit);
1✔
4610
        requireNonNull(value);
1✔
4611
        requireArgument(duration >= 0);
1✔
4612
        return cache.isAsync
1✔
4613
            ? putAsync(key, value, duration, unit)
1✔
4614
            : putSync(key, value, duration, unit, /* onlyIfAbsent= */ false);
1✔
4615
      }
4616
      @Override public @Nullable V putIfAbsent(K key, V value, long duration, TimeUnit unit) {
4617
        requireNonNull(unit);
1✔
4618
        requireNonNull(value);
1✔
4619
        requireArgument(duration >= 0);
1✔
4620
        return cache.isAsync
1✔
4621
            ? putIfAbsentAsync(key, value, duration, unit)
1✔
4622
            : putSync(key, value, duration, unit, /* onlyIfAbsent= */ true);
1✔
4623
      }
4624
      @Nullable V putSync(K key, V value, long duration, TimeUnit unit, boolean onlyIfAbsent) {
4625
        var expiry = new FixedExpireAfterWrite<K, V>(duration, unit);
1✔
4626
        return cache.put(key, value, expiry, onlyIfAbsent);
1✔
4627
      }
4628
      @SuppressWarnings("unchecked")
4629
      @Nullable V putIfAbsentAsync(K key, V value, long duration, TimeUnit unit) {
4630
        // Keep in sync with LocalAsyncCache.AsMapView#putIfAbsent(key, value)
4631
        var expiry = (Expiry<K, V>) new AsyncExpiry<>(new FixedExpireAfterWrite<>(duration, unit));
1✔
4632
        var asyncValue = (V) CompletableFuture.completedFuture(value);
1✔
4633

4634
        @Var CompletableFuture<V> priorFuture = null;
1✔
4635
        for (;;) {
4636
          priorFuture = (CompletableFuture<V>) ((priorFuture == null)
1✔
4637
              ? cache.getIfPresent(key, /* recordStats= */ false)
1✔
4638
              : cache.getIfPresentQuietly(key));
1✔
4639
          if (priorFuture != null) {
1✔
4640
            if (!priorFuture.isDone()) {
1✔
4641
              Async.getWhenSuccessful(priorFuture);
1✔
4642
              continue;
1✔
4643
            }
4644

4645
            V prior = Async.getWhenSuccessful(priorFuture);
1✔
4646
            if (prior != null) {
1✔
4647
              return prior;
1✔
4648
            }
4649
          }
4650

4651
          boolean[] added = { false };
1✔
4652
          var hints = new LocalCache.RemapHints();
1✔
4653
          var computed = (CompletableFuture<V>) cache.compute(key, (K k, @Nullable V oldValue) -> {
1✔
4654
            var oldValueFuture = (CompletableFuture<V>) oldValue;
1✔
4655
            added[0] = (oldValueFuture == null)
1✔
4656
                || (oldValueFuture.isDone() && (Async.getIfReady(oldValueFuture) == null));
1✔
4657
            if (added[0]) {
1✔
4658
              return asyncValue;
1✔
4659
            }
4660
            hints.preserveTimestamps = true;
1✔
4661
            hints.preserveRefresh = true;
1✔
4662
            return oldValue;
1✔
4663
          }, expiry, /* recordLoad= */ false, /* recordLoadFailure= */ false, hints);
4664

4665
          if (added[0]) {
1✔
4666
            return null;
1✔
4667
          } else {
4668
            V prior = Async.getWhenSuccessful(computed);
1✔
4669
            if (prior != null) {
1✔
4670
              return prior;
1✔
4671
            }
4672
          }
4673
        }
1✔
4674
      }
4675
      @SuppressWarnings("unchecked")
4676
      @Nullable V putAsync(K key, V value, long duration, TimeUnit unit) {
4677
        var expiry = (Expiry<K, V>) new AsyncExpiry<>(new FixedExpireAfterWrite<>(duration, unit));
1✔
4678
        var asyncValue = (V) CompletableFuture.completedFuture(value);
1✔
4679

4680
        var oldValueFuture = (CompletableFuture<V>) cache.put(
1✔
4681
            key, asyncValue, expiry, /* onlyIfAbsent= */ false);
4682
        return Async.getWhenSuccessful(oldValueFuture);
1✔
4683
      }
4684
      @Override public @Nullable V compute(K key,
4685
          BiFunction<? super K, ? super V, ? extends V> remappingFunction,
4686
          Duration duration) {
4687
        requireNonNull(key);
1✔
4688
        requireNonNull(duration);
1✔
4689
        requireNonNull(remappingFunction);
1✔
4690
        requireArgument(!duration.isNegative(), "duration cannot be negative: %s", duration);
1✔
4691
        var expiry = new FixedExpireAfterWrite<K, V>(
1✔
4692
            toNanosSaturated(duration), TimeUnit.NANOSECONDS);
1✔
4693

4694
        return cache.isAsync
1✔
4695
            ? computeAsync(key, remappingFunction, expiry)
1✔
4696
            : cache.compute(key, remappingFunction, expiry,
1✔
4697
                /* recordLoad= */ true, /* recordLoadFailure= */ true);
4698
      }
4699
      @Nullable V computeAsync(K key,
4700
          BiFunction<? super K, ? super V, ? extends V> remappingFunction,
4701
          Expiry<? super K, ? super V> expiry) {
4702
        // Keep in sync with LocalAsyncCache.AsMapView#compute(key, remappingFunction)
4703
        @SuppressWarnings("unchecked")
4704
        var delegate = (LocalCache<K, CompletableFuture<V>>) cache;
1✔
4705

4706
        @SuppressWarnings({"rawtypes", "unchecked", "Varifier"})
4707
        @Nullable V[] newValue = (@Nullable V[]) new Object[1];
1✔
4708
        for (;;) {
4709
          Async.getWhenSuccessful(delegate.getIfPresentQuietly(key));
1✔
4710

4711
          var hints = new LocalCache.RemapHints();
1✔
4712
          CompletableFuture<V> valueFuture = delegate.compute(
1✔
4713
              key, (K k, @Nullable CompletableFuture<V> oldValueFuture) -> {
4714
                if ((oldValueFuture != null) && !oldValueFuture.isDone()) {
1✔
4715
                  hints.preserveTimestamps = true;
1✔
4716
                  hints.preserveRefresh = true;
1✔
4717
                  return oldValueFuture;
1✔
4718
                }
4719

4720
                V oldValue = Async.getIfReady(oldValueFuture);
1✔
4721
                BiFunction<? super K, ? super V, ? extends @Nullable V> function =
1✔
4722
                    delegate.statsAware(remappingFunction,
1✔
4723
                        /* recordLoad= */ true, /* recordLoadFailure= */ true);
4724
                newValue[0] = function.apply(key, oldValue);
1✔
4725
                return (newValue[0] == null) ? null
1✔
4726
                    : CompletableFuture.completedFuture(newValue[0]);
1✔
4727
              }, new AsyncExpiry<>(expiry), /* recordLoad= */ false,
4728
              /* recordLoadFailure= */ false, hints);
4729

4730
          if (newValue[0] != null) {
1✔
4731
            return newValue[0];
1✔
4732
          } else if (valueFuture == null) {
1✔
4733
            return null;
1✔
4734
          }
4735
        }
1✔
4736
      }
4737
      @Override public Map<K, V> oldest(int limit) {
4738
        return oldest(new SizeLimiter<>(Math.min(limit, cache.size()), limit));
1✔
4739
      }
4740
      @Override public <T> T oldest(Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4741
        return cache.snapshot(cache.timerWheel(), transformer, mappingFunction);
1✔
4742
      }
4743
      @Override public Map<K, V> youngest(int limit) {
4744
        return youngest(new SizeLimiter<>(Math.min(limit, cache.size()), limit));
1✔
4745
      }
4746
      @Override public <T> T youngest(Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4747
        return cache.snapshot(cache.timerWheel()::descendingIterator, transformer, mappingFunction);
1✔
4748
      }
4749
    }
4750

4751
    static final class FixedExpireAfterWrite<K, V> implements Expiry<K, V> {
4752
      final long duration;
4753
      final TimeUnit unit;
4754

4755
      FixedExpireAfterWrite(long duration, TimeUnit unit) {
1✔
4756
        this.duration = duration;
1✔
4757
        this.unit = unit;
1✔
4758
      }
1✔
4759
      @Override public long expireAfterCreate(K key, V value, long currentTime) {
4760
        return unit.toNanos(duration);
1✔
4761
      }
4762
      @Override public long expireAfterUpdate(
4763
          K key, V value, long currentTime, long currentDuration) {
4764
        return unit.toNanos(duration);
1✔
4765
      }
4766
      @CanIgnoreReturnValue
4767
      @Override public long expireAfterRead(
4768
          K key, V value, long currentTime, long currentDuration) {
4769
        return currentDuration;
1✔
4770
      }
4771
    }
4772

4773
    @SuppressWarnings("PreferJavaTimeOverload")
4774
    final class BoundedRefreshAfterWrite implements FixedRefresh<K, V> {
1✔
4775
      @Override public OptionalLong ageOf(K key, TimeUnit unit) {
4776
        requireNonNull(key);
1✔
4777
        requireNonNull(unit);
1✔
4778
        Object lookupKey = cache.nodeFactory.newLookupKey(key);
1✔
4779
        Node<K, V> node = cache.data.get(lookupKey);
1✔
4780
        if (node == null) {
1✔
4781
          return OptionalLong.empty();
1✔
4782
        }
4783
        V value = node.getValue();
1✔
4784
        if (value == null) {
1✔
4785
          return OptionalLong.empty();
1✔
4786
        }
4787
        long now = cache.expirationTicker().read();
1✔
4788
        return cache.hasExpired(node, now, value)
1✔
4789
            ? OptionalLong.empty()
1✔
4790
            : OptionalLong.of(unit.convert(
1✔
4791
                (now & ~1L) - (node.getWriteTime() & ~1L), TimeUnit.NANOSECONDS));
1✔
4792
      }
4793
      @Override public long getRefreshesAfter(TimeUnit unit) {
4794
        return unit.convert(cache.refreshAfterWriteNanos(), TimeUnit.NANOSECONDS);
1✔
4795
      }
4796
      @Override public void setRefreshesAfter(long duration, TimeUnit unit) {
4797
        requireNonNull(unit);
1✔
4798
        requireArgument(duration > 0, "duration must be positive: %s %s", duration, unit);
1✔
4799
        cache.setRefreshAfterWriteNanos(unit.toNanos(duration));
1✔
4800
        cache.scheduleAfterWrite();
1✔
4801
      }
1✔
4802
    }
4803
  }
4804

4805
  /* --------------- Loading Cache --------------- */
4806

4807
  static final class BoundedLocalLoadingCache<K, V>
4808
      extends BoundedLocalManualCache<K, V> implements LocalLoadingCache<K, V> {
4809
    private static final long serialVersionUID = 1;
4810

4811
    final Function<K, @Nullable V> mappingFunction;
4812
    final @Nullable Function<Set<? extends K>, Map<K, V>> bulkMappingFunction;
4813

4814
    BoundedLocalLoadingCache(Caffeine<K, V> builder, CacheLoader<? super K, V> loader) {
4815
      super(builder, loader);
1✔
4816
      requireNonNull(loader);
1✔
4817
      mappingFunction = newMappingFunction(loader);
1✔
4818
      bulkMappingFunction = newBulkMappingFunction(loader);
1✔
4819
    }
1✔
4820

4821
    @Override
4822
    @SuppressWarnings({"DataFlowIssue", "NullAway"})
4823
    public AsyncCacheLoader<? super K, V> cacheLoader() {
4824
      return cache.cacheLoader;
1✔
4825
    }
4826

4827
    @Override
4828
    public Function<K, @Nullable V> mappingFunction() {
4829
      return mappingFunction;
1✔
4830
    }
4831

4832
    @Override
4833
    public @Nullable Function<Set<? extends K>, Map<K, V>> bulkMappingFunction() {
4834
      return bulkMappingFunction;
1✔
4835
    }
4836

4837
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
4838
      throw new InvalidObjectException("Proxy required");
1✔
4839
    }
4840

4841
    private Object writeReplace() {
4842
      return makeSerializationProxy(cache);
1✔
4843
    }
4844
  }
4845

4846
  /* --------------- Async Cache --------------- */
4847

4848
  static final class BoundedLocalAsyncCache<K, V> implements LocalAsyncCache<K, V>, Serializable {
4849
    private static final long serialVersionUID = 1;
4850

4851
    final BoundedLocalCache<K, CompletableFuture<V>> cache;
4852
    final boolean isWeighted;
4853

4854
    @Nullable ConcurrentMap<K, CompletableFuture<V>> mapView;
4855
    @Nullable CacheView<K, V> cacheView;
4856
    @Nullable Policy<K, V> policy;
4857

4858
    @SuppressWarnings("unchecked")
4859
    BoundedLocalAsyncCache(Caffeine<K, V> builder) {
1✔
4860
      cache = (BoundedLocalCache<K, CompletableFuture<V>>) LocalCacheFactory
1✔
4861
          .newBoundedLocalCache(builder, /* cacheLoader= */ null, /* isAsync= */ true);
1✔
4862
      isWeighted = builder.isWeighted();
1✔
4863
    }
1✔
4864

4865
    @Override
4866
    public BoundedLocalCache<K, CompletableFuture<V>> cache() {
4867
      return cache;
1✔
4868
    }
4869

4870
    @Override
4871
    public ConcurrentMap<K, CompletableFuture<V>> asMap() {
4872
      return (mapView == null) ? (mapView = new AsyncAsMapView<>(this)) : mapView;
1✔
4873
    }
4874

4875
    @Override
4876
    public Cache<K, V> synchronous() {
4877
      return (cacheView == null) ? (cacheView = new CacheView<>(this)) : cacheView;
1✔
4878
    }
4879

4880
    @Override
4881
    public Policy<K, V> policy() {
4882
      if (policy == null) {
1✔
4883
        @SuppressWarnings("unchecked")
4884
        var castCache = (BoundedLocalCache<K, V>) cache;
1✔
4885
        Function<CompletableFuture<V>, @Nullable V> transformer = Async::getIfReady;
1✔
4886
        @SuppressWarnings("unchecked")
4887
        var castTransformer = (Function<@Nullable V, @Nullable V>) transformer;
1✔
4888
        policy = new BoundedPolicy<>(castCache, castTransformer, isWeighted);
1✔
4889
      }
4890
      return policy;
1✔
4891
    }
4892

4893
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
4894
      throw new InvalidObjectException("Proxy required");
1✔
4895
    }
4896

4897
    private Object writeReplace() {
4898
      return makeSerializationProxy(cache);
1✔
4899
    }
4900
  }
4901

4902
  /* --------------- Async Loading Cache --------------- */
4903

4904
  static final class BoundedLocalAsyncLoadingCache<K, V>
4905
      extends LocalAsyncLoadingCache<K, V> implements Serializable {
4906
    private static final long serialVersionUID = 1;
4907

4908
    final BoundedLocalCache<K, CompletableFuture<V>> cache;
4909
    final boolean isWeighted;
4910

4911
    @Nullable ConcurrentMap<K, CompletableFuture<V>> mapView;
4912
    @Nullable Policy<K, V> policy;
4913

4914
    @SuppressWarnings("unchecked")
4915
    BoundedLocalAsyncLoadingCache(Caffeine<K, V> builder, AsyncCacheLoader<? super K, V> loader) {
4916
      super(loader);
1✔
4917
      isWeighted = builder.isWeighted();
1✔
4918
      cache = (BoundedLocalCache<K, CompletableFuture<V>>) LocalCacheFactory
1✔
4919
          .newBoundedLocalCache(builder, loader, /* isAsync= */ true);
1✔
4920
    }
1✔
4921

4922
    @Override
4923
    public BoundedLocalCache<K, CompletableFuture<V>> cache() {
4924
      return cache;
1✔
4925
    }
4926

4927
    @Override
4928
    public ConcurrentMap<K, CompletableFuture<V>> asMap() {
4929
      return (mapView == null) ? (mapView = new AsyncAsMapView<>(this)) : mapView;
1✔
4930
    }
4931

4932
    @Override
4933
    public Policy<K, V> policy() {
4934
      if (policy == null) {
1✔
4935
        @SuppressWarnings("unchecked")
4936
        var castCache = (BoundedLocalCache<K, V>) cache;
1✔
4937
        Function<CompletableFuture<V>, @Nullable V> transformer = Async::getIfReady;
1✔
4938
        @SuppressWarnings("unchecked")
4939
        var castTransformer = (Function<@Nullable V, @Nullable V>) transformer;
1✔
4940
        policy = new BoundedPolicy<>(castCache, castTransformer, isWeighted);
1✔
4941
      }
4942
      return policy;
1✔
4943
    }
4944

4945
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
4946
      throw new InvalidObjectException("Proxy required");
1✔
4947
    }
4948

4949
    private Object writeReplace() {
4950
      return makeSerializationProxy(cache);
1✔
4951
    }
4952
  }
4953
}
4954

4955
/** The namespace for field padding through inheritance. */
4956
@SuppressWarnings({"IdentifierName", "MultiVariableDeclaration"})
4957
final class BLCHeader {
4958

4959
  private BLCHeader() {}
4960

4961
  @SuppressWarnings("unused")
4962
  static class PadDrainStatus {
1✔
4963
    byte p000, p001, p002, p003, p004, p005, p006, p007;
4964
    byte p008, p009, p010, p011, p012, p013, p014, p015;
4965
    byte p016, p017, p018, p019, p020, p021, p022, p023;
4966
    byte p024, p025, p026, p027, p028, p029, p030, p031;
4967
    byte p032, p033, p034, p035, p036, p037, p038, p039;
4968
    byte p040, p041, p042, p043, p044, p045, p046, p047;
4969
    byte p048, p049, p050, p051, p052, p053, p054, p055;
4970
    byte p056, p057, p058, p059, p060, p061, p062, p063;
4971
    byte p064, p065, p066, p067, p068, p069, p070, p071;
4972
    byte p072, p073, p074, p075, p076, p077, p078, p079;
4973
    byte p080, p081, p082, p083, p084, p085, p086, p087;
4974
    byte p088, p089, p090, p091, p092, p093, p094, p095;
4975
    byte p096, p097, p098, p099, p100, p101, p102, p103;
4976
    byte p104, p105, p106, p107, p108, p109, p110, p111;
4977
    byte p112, p113, p114, p115, p116, p117, p118, p119;
4978
  }
4979

4980
  /** Enforces a memory layout to avoid false sharing by padding the drain status. */
4981
  abstract static class DrainStatusRef extends PadDrainStatus {
1✔
4982
    static final VarHandle DRAIN_STATUS = fieldVarHandle(MethodHandles.lookup(),
1✔
4983
        "drainStatus", VarHandle.class, DrainStatusRef.class, int.class);
4984

4985
    /** A drain is not taking place. */
4986
    static final int IDLE = 0;
4987
    /** A drain is required due to a pending write modification. */
4988
    static final int REQUIRED = 1;
4989
    /** A drain is in progress and will transition to idle. */
4990
    static final int PROCESSING_TO_IDLE = 2;
4991
    /** A drain is in progress and will transition to required. */
4992
    static final int PROCESSING_TO_REQUIRED = 3;
4993

4994
    /** The draining status of the buffers. */
4995
    volatile int drainStatus = IDLE;
1✔
4996

4997
    /**
4998
     * Returns whether maintenance work is needed.
4999
     *
5000
     * @param delayable if draining the read buffer can be delayed
5001
     */
5002
    @SuppressWarnings("StatementSwitchToExpressionSwitch")
5003
    boolean shouldDrainBuffers(boolean delayable) {
5004
      switch (drainStatusOpaque()) {
1✔
5005
        case IDLE:
5006
          return !delayable;
1✔
5007
        case REQUIRED:
5008
          return true;
1✔
5009
        case PROCESSING_TO_IDLE:
5010
        case PROCESSING_TO_REQUIRED:
5011
          return false;
1✔
5012
        default:
5013
          throw new IllegalStateException("Invalid drain status: " + drainStatus);
1✔
5014
      }
5015
    }
5016

5017
    int drainStatusOpaque() {
5018
      return (int) DRAIN_STATUS.getOpaque(this);
1✔
5019
    }
5020

5021
    int drainStatusAcquire() {
5022
      return (int) DRAIN_STATUS.getAcquire(this);
1✔
5023
    }
5024

5025
    void setDrainStatusOpaque(int drainStatus) {
5026
      DRAIN_STATUS.setOpaque(this, drainStatus);
1✔
5027
    }
1✔
5028

5029
    void setDrainStatusRelease(int drainStatus) {
5030
      DRAIN_STATUS.setRelease(this, drainStatus);
1✔
5031
    }
1✔
5032

5033
    boolean casDrainStatus(int expect, int update) {
5034
      return DRAIN_STATUS.compareAndSet(this, expect, update);
1✔
5035
    }
5036
  }
5037
}
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