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

ben-manes / caffeine / #5326

15 Mar 2026 08:00PM UTC coverage: 99.861% (-0.08%) from 99.937%
#5326

push

github

ben-manes
minor edge case handling

Avoid slight bias in the frequency sketch from sentinel keys. If the
access reordering is applied after the node was deleted, while other
operations no-op'd the popularity sketch would still increment for the
key. The key was replaced by a sentinel, so this inflates those hash
locations, which by collisions could inflate the popularity of another
entry.

Better timer wheel rollback on exception handling for the current
timestamp. In that case it would have simply forced a delayed full
sweep, so considered benign. More so, since the cache is not supposed
to throw any exceptions, it only acted as a failsafe in case of a bug
and therefore perfect handling was not necessary. Yet because code
walkthroughs keep flagging it, it's best to fix as the previous
approach was too subtle.

Previously, a failed computation could result in duplicate eviction
notifications. This occurred because the old entry was not removed
before the mapping function's / weigher's / Expiry's exception was
propagated. A repeatedly broken computation might notify the listener
multiple times until the entry was removed by the maintenance cycle.
As the listener is generally used for resource clean up, which is
usually idempotent, this double notification was likely fine. The
notification is still handled prior to running the mapping function to
ensure linearizability, e.g. removing a file prior to recreating it.

3847 of 3862 branches covered (99.61%)

65 of 70 new or added lines in 2 files covered. (92.86%)

1 existing line in 1 file now uncovered.

7903 of 7914 relevant lines covered (99.86%)

1.0 hits per line

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

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

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

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

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

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

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

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

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

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

241
  final @Nullable RemovalListener<K, V> evictionListener;
242
  final @Nullable AsyncCacheLoader<K, V> cacheLoader;
243

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

254
  final boolean isWeighted;
255
  final boolean isAsync;
256

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

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

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

287
  /** Ensures that the node is alive during the map operation. */
288
  void requireIsAlive(Object key, Node<?, ?> node) {
289
    if (!node.isAlive()) {
1✔
290
      throw new IllegalStateException(brokenEqualityMessage(key, node));
1✔
291
    }
292
  }
1✔
293

294
  /** Logs if the node cannot be found in the map but is still alive. */
295
  void logIfAlive(Node<?, ?> node) {
296
    if (node.isAlive()) {
1✔
297
      String message = brokenEqualityMessage(node.getKeyReference(), node);
1✔
298
      logger.log(Level.ERROR, message, new IllegalStateException());
1✔
299
    }
300
  }
1✔
301

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

311
  /** Throws the exception, wrapping checked exceptions in a {@link CompletionException}. */
312
  static void throwException(Throwable t) {
313
    if (t instanceof RuntimeException) {
1!
314
      throw (RuntimeException) t;
1✔
NEW
315
    } else if (t instanceof Error) {
×
NEW
316
      throw (Error) t;
×
317
    }
NEW
318
    throw new CompletionException(t);
×
319
  }
320

321
  /* --------------- Shared --------------- */
322

323
  @Override
324
  public boolean isAsync() {
325
    return isAsync;
1✔
326
  }
327

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

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

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

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

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

353
  @Override
354
  public final Executor executor() {
355
    return executor;
1✔
356
  }
357

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

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

379
  @Override
380
  public Object referenceKey(K key) {
381
    return nodeFactory.newLookupKey(key);
1✔
382
  }
383

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

391
  /* --------------- Stats Support --------------- */
392

393
  @Override
394
  public boolean isRecordingStats() {
395
    return false;
1✔
396
  }
397

398
  @Override
399
  public StatsCounter statsCounter() {
400
    return StatsCounter.disabledStatsCounter();
1✔
401
  }
402

403
  @Override
404
  public Ticker statsTicker() {
405
    return Ticker.disabledTicker();
1✔
406
  }
407

408
  /* --------------- Removal Listener Support --------------- */
409

410
  protected @Nullable RemovalListener<K, V> removalListener() {
411
    return null;
1✔
412
  }
413

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

435
  /* --------------- Eviction Listener Support --------------- */
436

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

448
  /* --------------- Reference Support --------------- */
449

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

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

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

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

470
  /* --------------- Expiration Support --------------- */
471

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

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

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

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

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

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

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

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

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

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

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

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

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

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

539
  /* --------------- Eviction Support --------------- */
540

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

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

551
  protected FrequencySketch frequencySketch() {
552
    throw new UnsupportedOperationException();
1✔
553
  }
554

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

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

565
  /** Returns the maximum weighted size. */
566
  protected long maximumAcquire() {
567
    throw new UnsupportedOperationException();
1✔
568
  }
569

570
  /** Returns the maximum weighted size of the window space. */
571
  protected long windowMaximum() {
572
    throw new UnsupportedOperationException();
1✔
573
  }
574

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

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

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

590
  @GuardedBy("evictionLock")
591
  protected void setMainProtectedMaximum(long maximum) {
592
    throw new UnsupportedOperationException();
1✔
593
  }
594

595
  /** Returns the combined weight of the values in the cache (may be negative). */
596
  protected long weightedSize() {
597
    throw new UnsupportedOperationException();
1✔
598
  }
599

600
  /** Returns the combined weight of the values in the cache (may be negative). */
601
  protected long weightedSizeAcquire() {
602
    throw new UnsupportedOperationException();
1✔
603
  }
604

605
  /** Returns the uncorrected combined weight of the values in the window space. */
606
  protected long windowWeightedSize() {
607
    throw new UnsupportedOperationException();
1✔
608
  }
609

610
  /** Returns the uncorrected combined weight of the values in the main's protected space. */
611
  protected long mainProtectedWeightedSize() {
612
    throw new UnsupportedOperationException();
1✔
613
  }
614

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

620
  @GuardedBy("evictionLock")
621
  protected void setWindowWeightedSize(long weightedSize) {
622
    throw new UnsupportedOperationException();
1✔
623
  }
624

625
  @GuardedBy("evictionLock")
626
  protected void setMainProtectedWeightedSize(long weightedSize) {
627
    throw new UnsupportedOperationException();
1✔
628
  }
629

630
  protected int hitsInSample() {
631
    throw new UnsupportedOperationException();
1✔
632
  }
633

634
  protected int missesInSample() {
635
    throw new UnsupportedOperationException();
1✔
636
  }
637

638
  protected double stepSize() {
639
    throw new UnsupportedOperationException();
1✔
640
  }
641

642
  protected double previousSampleHitRate() {
643
    throw new UnsupportedOperationException();
1✔
644
  }
645

646
  protected long adjustment() {
647
    throw new UnsupportedOperationException();
1✔
648
  }
649

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

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

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

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

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

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

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

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

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

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

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

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

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

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

745
    return first;
1✔
746
  }
747

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1115
    return true;
1✔
1116
  }
1117

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1405
        if (cause[0] != null) {
1✔
1406
          notifyRemoval(key, value, cause[0]);
1✔
1407
        }
1408
        if (newValue == null) {
1✔
1409
          statsCounter().recordLoadFailure(loadTime);
1✔
1410
        } else {
1411
          statsCounter().recordLoadSuccess(loadTime);
1✔
1412
        }
1413
        return result;
1✔
1414
      });
1415
      return Async.getIfReady(refreshed);
1✔
1416
    }
1417

1418
    return null;
1✔
1419
  }
1420

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

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

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

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

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

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

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

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

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

1525
  /** Returns if the entry's write time would exceed the minimum expiration reorder threshold. */
1526
  boolean exceedsWriteTimeTolerance(Node<K, V> node, long varTime, long now) {
1527
    long variableTime = node.getVariableTime();
1✔
1528
    long tolerance = EXPIRE_WRITE_TOLERANCE;
1✔
1529
    long writeTime = node.getWriteTime();
1✔
1530
    return
1✔
1531
        (expiresAfterWrite()
1✔
1532
            && ((expiresAfterWriteNanos() <= tolerance) || (Math.abs(now - writeTime) > tolerance)))
1✔
1533
        || (refreshAfterWrite()
1✔
1534
            && ((refreshAfterWriteNanos() <= tolerance) || (Math.abs(now - writeTime) > tolerance)))
1✔
1535
        || (expiresVariable() && (Math.abs(varTime - variableTime) > tolerance));
1✔
1536
  }
1537

1538
  /**
1539
   * Performs the post-processing work required after a write.
1540
   *
1541
   * @param task the pending operation to be applied
1542
   */
1543
  void afterWrite(Runnable task) {
1544
    for (int i = 0; i < WRITE_BUFFER_RETRIES; i++) {
1✔
1545
      if (writeBuffer.offer(task)) {
1✔
1546
        scheduleAfterWrite();
1✔
1547
        return;
1✔
1548
      }
1549
      scheduleDrainBuffers();
1✔
1550
      Thread.onSpinWait();
1✔
1551
    }
1552

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

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

1602
  /**
1603
   * Conditionally schedules the asynchronous maintenance task after a write operation. If the
1604
   * task status was IDLE or REQUIRED then the maintenance task is scheduled immediately. If it
1605
   * is already processing then it is set to transition to REQUIRED upon completion so that a new
1606
   * execution is triggered by the next operation.
1607
   */
1608
  void scheduleAfterWrite() {
1609
    @Var int drainStatus = drainStatusOpaque();
1✔
1610
    for (;;) {
1611
      switch (drainStatus) {
1✔
1612
        case IDLE:
1613
          casDrainStatus(IDLE, REQUIRED);
1✔
1614
          scheduleDrainBuffers();
1✔
1615
          return;
1✔
1616
        case REQUIRED:
1617
          scheduleDrainBuffers();
1✔
1618
          return;
1✔
1619
        case PROCESSING_TO_IDLE:
1620
          if (casDrainStatus(PROCESSING_TO_IDLE, PROCESSING_TO_REQUIRED)) {
1✔
1621
            return;
1✔
1622
          }
1623
          drainStatus = drainStatusAcquire();
1✔
1624
          continue;
1✔
1625
        case PROCESSING_TO_REQUIRED:
1626
          return;
1✔
1627
        default:
1628
          throw new IllegalStateException("Invalid drain status: " + drainStatus);
1✔
1629
      }
1630
    }
1631
  }
1632

1633
  /**
1634
   * Attempts to schedule an asynchronous task to apply the pending operations to the page
1635
   * replacement policy. If the executor rejects the task then it is run directly.
1636
   */
1637
  void scheduleDrainBuffers() {
1638
    if (drainStatusOpaque() >= PROCESSING_TO_IDLE) {
1✔
1639
      return;
1✔
1640
    }
1641
    if (evictionLock.tryLock()) {
1✔
1642
      try {
1643
        int drainStatus = drainStatusOpaque();
1✔
1644
        if (drainStatus >= PROCESSING_TO_IDLE) {
1✔
1645
          return;
1✔
1646
        }
1647
        setDrainStatusRelease(PROCESSING_TO_IDLE);
1✔
1648
        executor.execute(drainBuffersTask);
1✔
1649
      } catch (Throwable t) {
1✔
1650
        logger.log(Level.WARNING, "Exception thrown when submitting maintenance task", t);
1✔
1651
        maintenance(/* ignored */ null);
1✔
1652
      } finally {
1653
        evictionLock.unlock();
1✔
1654
      }
1655
    }
1656
  }
1✔
1657

1658
  @Override
1659
  public void cleanUp() {
1660
    try {
1661
      performCleanUp(/* ignored */ null);
1✔
1662
    } catch (RuntimeException e) {
1✔
1663
      logger.log(Level.ERROR, "Exception thrown when performing the maintenance task", e);
1✔
1664
    }
1✔
1665
  }
1✔
1666

1667
  /**
1668
   * Performs the maintenance work, blocking until the lock is acquired.
1669
   *
1670
   * @param task an additional pending task to run, or {@code null} if not present
1671
   */
1672
  void performCleanUp(@Nullable Runnable task) {
1673
    evictionLock.lock();
1✔
1674
    try {
1675
      maintenance(task);
1✔
1676
    } finally {
1677
      evictionLock.unlock();
1✔
1678
    }
1679
    rescheduleCleanUpIfIncomplete();
1✔
1680
  }
1✔
1681

1682
  /**
1683
   * If there remains pending operations that were not handled by the prior clean up then try to
1684
   * schedule an asynchronous maintenance task. This may occur due to a concurrent write after the
1685
   * maintenance work had started or if the amortized threshold of work per clean up was reached.
1686
   */
1687
  @SuppressWarnings("resource")
1688
  void rescheduleCleanUpIfIncomplete() {
1689
    if (drainStatusOpaque() != REQUIRED) {
1✔
1690
      return;
1✔
1691
    }
1692

1693
    // An immediate scheduling cannot be performed on a custom executor because it may use a
1694
    // caller-runs policy. This could cause the caller's penalty to exceed the amortized threshold,
1695
    // e.g. repeated concurrent writes could result in a retry loop.
1696
    if (executor == ForkJoinPool.commonPool()) {
1✔
1697
      scheduleDrainBuffers();
1✔
1698
      return;
1✔
1699
    }
1700

1701
    // If a scheduler was configured then the maintenance can be deferred onto the custom executor
1702
    // and run in the near future. Otherwise, it will be handled due to other cache activity.
1703
    var pacer = pacer();
1✔
1704
    if ((pacer != null) && !pacer.isScheduled() && evictionLock.tryLock()) {
1✔
1705
      try {
1706
        if ((drainStatusOpaque() == REQUIRED) && !pacer.isScheduled()) {
1✔
1707
          pacer.schedule(executor, drainBuffersTask, expirationTicker().read(), Pacer.TOLERANCE);
1✔
1708
        }
1709
      } finally {
1710
        evictionLock.unlock();
1✔
1711
      }
1712
    }
1713
  }
1✔
1714

1715
  /**
1716
   * Performs the pending maintenance work and sets the state flags during processing to avoid
1717
   * excess scheduling attempts. The read buffer, write buffer, and reference queues are drained,
1718
   * followed by expiration, and size-based eviction.
1719
   *
1720
   * @param task an additional pending task to run, or {@code null} if not present
1721
   */
1722
  @GuardedBy("evictionLock")
1723
  void maintenance(@Nullable Runnable task) {
1724
    setDrainStatusRelease(PROCESSING_TO_IDLE);
1✔
1725

1726
    try {
1727
      drainReadBuffer();
1✔
1728

1729
      drainWriteBuffer();
1✔
1730
      if (task != null) {
1✔
1731
        task.run();
1✔
1732
      }
1733

1734
      drainKeyReferences();
1✔
1735
      drainValueReferences();
1✔
1736

1737
      expireEntries();
1✔
1738
      evictEntries();
1✔
1739

1740
      climb();
1✔
1741
    } finally {
1742
      if ((drainStatusOpaque() != PROCESSING_TO_IDLE)
1✔
1743
          || !casDrainStatus(PROCESSING_TO_IDLE, IDLE)) {
1✔
1744
        setDrainStatusOpaque(REQUIRED);
1✔
1745
      }
1746
    }
1747
  }
1✔
1748

1749
  /** Drains the weak key references queue. */
1750
  @GuardedBy("evictionLock")
1751
  void drainKeyReferences() {
1752
    if (!collectKeys()) {
1✔
1753
      return;
1✔
1754
    }
1755
    @Var Reference<? extends K> keyRef;
1756
    while ((keyRef = keyReferenceQueue().poll()) != null) {
1✔
1757
      Node<K, V> node = data.get(keyRef);
1✔
1758
      if (node != null) {
1✔
1759
        evictEntry(node, RemovalCause.COLLECTED, 0L);
1✔
1760
      }
1761
    }
1✔
1762
  }
1✔
1763

1764
  /** Drains the weak / soft value references queue. */
1765
  @GuardedBy("evictionLock")
1766
  void drainValueReferences() {
1767
    if (!collectValues()) {
1✔
1768
      return;
1✔
1769
    }
1770
    @Var Reference<? extends V> valueRef;
1771
    while ((valueRef = valueReferenceQueue().poll()) != null) {
1✔
1772
      @SuppressWarnings("unchecked")
1773
      var ref = (InternalReference<V>) valueRef;
1✔
1774
      Node<K, V> node = data.get(ref.getKeyReference());
1✔
1775
      if ((node != null) && (valueRef == node.getValueReference())) {
1✔
1776
        evictEntry(node, RemovalCause.COLLECTED, 0L);
1✔
1777
      }
1778
    }
1✔
1779
  }
1✔
1780

1781
  /** Drains the read buffer. */
1782
  @GuardedBy("evictionLock")
1783
  void drainReadBuffer() {
1784
    if (!skipReadBuffer()) {
1✔
1785
      readBuffer.drainTo(accessPolicy);
1✔
1786
    }
1787
  }
1✔
1788

1789
  /** Updates the node's location in the page replacement policy. */
1790
  @GuardedBy("evictionLock")
1791
  void onAccess(Node<K, V> node) {
1792
    if (evicts()) {
1✔
1793
      var keyRef = node.getKeyReferenceOrNull();
1✔
1794
      if ((keyRef == null) || !node.isAlive()) {
1✔
1795
        return;
1✔
1796
      }
1797
      frequencySketch().increment(keyRef);
1✔
1798
      if (node.inWindow()) {
1✔
1799
        reorder(accessOrderWindowDeque(), node);
1✔
1800
      } else if (node.inMainProbation()) {
1✔
1801
        reorderProbation(node);
1✔
1802
      } else {
1803
        reorder(accessOrderProtectedDeque(), node);
1✔
1804
      }
1805
      setHitsInSample(hitsInSample() + 1);
1✔
1806
    } else if (expiresAfterAccess()) {
1✔
1807
      reorder(accessOrderWindowDeque(), node);
1✔
1808
    }
1809
    if (expiresVariable()) {
1✔
1810
      timerWheel().reschedule(node);
1✔
1811
    }
1812
  }
1✔
1813

1814
  /** Promote the node from probation to protected on an access. */
1815
  @GuardedBy("evictionLock")
1816
  void reorderProbation(Node<K, V> node) {
1817
    if (!accessOrderProbationDeque().contains(node)) {
1!
1818
      // Ignore stale accesses for an entry that is no longer present
UNCOV
1819
      return;
×
1820
    } else if (node.getPolicyWeight() > mainProtectedMaximum()) {
1✔
1821
      reorder(accessOrderProbationDeque(), node);
1✔
1822
      return;
1✔
1823
    }
1824

1825
    // If the protected space exceeds its maximum, the LRU items are demoted to the probation space.
1826
    // This is deferred to the adaption phase at the end of the maintenance cycle.
1827
    setMainProtectedWeightedSize(mainProtectedWeightedSize() + node.getPolicyWeight());
1✔
1828
    accessOrderProbationDeque().remove(node);
1✔
1829
    accessOrderProtectedDeque().offerLast(node);
1✔
1830
    node.makeMainProtected();
1✔
1831
  }
1✔
1832

1833
  /** Updates the node's location in the policy's deque. */
1834
  static <K, V> void reorder(LinkedDeque<Node<K, V>> deque, Node<K, V> node) {
1835
    // An entry may be scheduled for reordering despite having been removed. This can occur when the
1836
    // entry was concurrently read while a writer was removing it. If the entry is no longer linked
1837
    // then it does not need to be processed.
1838
    if (deque.contains(node)) {
1✔
1839
      deque.moveToBack(node);
1✔
1840
    }
1841
  }
1✔
1842

1843
  /** Drains the write buffer. */
1844
  @GuardedBy("evictionLock")
1845
  void drainWriteBuffer() {
1846
    for (int i = 0; i <= WRITE_BUFFER_MAX; i++) {
1✔
1847
      Runnable task = writeBuffer.poll();
1✔
1848
      if (task == null) {
1✔
1849
        return;
1✔
1850
      }
1851
      task.run();
1✔
1852
    }
1853
    setDrainStatusOpaque(PROCESSING_TO_REQUIRED);
1✔
1854
  }
1✔
1855

1856
  /**
1857
   * Atomically transitions the node to the <code>dead</code> state and decrements the
1858
   * <code>weightedSize</code>.
1859
   *
1860
   * @param node the entry in the page replacement policy
1861
   */
1862
  @GuardedBy("evictionLock")
1863
  @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
1864
  void makeDead(Node<K, V> node) {
1865
    synchronized (node) {
1✔
1866
      if (node.isDead()) {
1✔
1867
        return;
1✔
1868
      }
1869
      if (evicts()) {
1✔
1870
        // The node's policy weight may be out of sync due to a pending update waiting to be
1871
        // processed. At this point the node's weight is finalized, so the weight can be safely
1872
        // taken from the node's perspective and the sizes will be adjusted correctly.
1873
        if (node.inWindow()) {
1✔
1874
          setWindowWeightedSize(windowWeightedSize() - node.getWeight());
1✔
1875
        } else if (node.inMainProtected()) {
1✔
1876
          setMainProtectedWeightedSize(mainProtectedWeightedSize() - node.getWeight());
1✔
1877
        }
1878
        setWeightedSize(weightedSize() - node.getWeight());
1✔
1879
      }
1880
      node.die();
1✔
1881
    }
1✔
1882
  }
1✔
1883

1884
  /** Adds the node to the page replacement policy. */
1885
  final class AddTask implements Runnable {
1886
    final Node<K, V> node;
1887
    final int weight;
1888

1889
    AddTask(Node<K, V> node, int weight) {
1✔
1890
      this.weight = weight;
1✔
1891
      this.node = node;
1✔
1892
    }
1✔
1893

1894
    @Override
1895
    @GuardedBy("evictionLock")
1896
    public void run() {
1897
      if (evicts()) {
1✔
1898
        setWeightedSize(weightedSize() + weight);
1✔
1899
        setWindowWeightedSize(windowWeightedSize() + weight);
1✔
1900
        node.setPolicyWeight(node.getPolicyWeight() + weight);
1✔
1901

1902
        long maximum = maximum();
1✔
1903
        if (weightedSize() >= (maximum >>> 1)) {
1✔
1904
          if (weightedSize() > MAXIMUM_CAPACITY) {
1✔
1905
            evictEntries();
1✔
1906
          } else {
1907
            // Lazily initialize when close to the maximum
1908
            long capacity = isWeighted() ? data.mappingCount() : maximum;
1✔
1909
            frequencySketch().ensureCapacity(capacity);
1✔
1910
          }
1911
        }
1912

1913
        var keyRef = node.getKeyReferenceOrNull();
1✔
1914
        if (keyRef != null) {
1✔
1915
          frequencySketch().increment(keyRef);
1✔
1916
        }
1917

1918
        setMissesInSample(missesInSample() + 1);
1✔
1919
      }
1920

1921
      // ignore out-of-order write operations
1922
      boolean isAlive;
1923
      synchronized (node) {
1✔
1924
        isAlive = node.isAlive();
1✔
1925
      }
1✔
1926
      if (isAlive) {
1✔
1927
        if (expiresAfterWrite()) {
1✔
1928
          writeOrderDeque().offerLast(node);
1✔
1929
        }
1930
        if (expiresVariable()) {
1✔
1931
          timerWheel().schedule(node);
1✔
1932
        }
1933
        if (evicts()) {
1✔
1934
          if (weight > maximum()) {
1✔
1935
            evictEntry(node, RemovalCause.SIZE, expirationTicker().read());
1✔
1936
          } else if (weight > windowMaximum()) {
1✔
1937
            accessOrderWindowDeque().offerFirst(node);
1✔
1938
          } else {
1939
            accessOrderWindowDeque().offerLast(node);
1✔
1940
          }
1941
        } else if (expiresAfterAccess()) {
1✔
1942
          accessOrderWindowDeque().offerLast(node);
1✔
1943
        }
1944
      }
1945
    }
1✔
1946
  }
1947

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

1952
    RemovalTask(Node<K, V> node) {
1✔
1953
      this.node = node;
1✔
1954
    }
1✔
1955

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

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

1983
    public UpdateTask(Node<K, V> node, int weightDifference) {
1✔
1984
      this.weightDifference = weightDifference;
1✔
1985
      this.node = node;
1✔
1986
    }
1✔
1987

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

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

2033
  /* --------------- Concurrent Map Support --------------- */
2034

2035
  @Override
2036
  public boolean isEmpty() {
2037
    return data.isEmpty();
1✔
2038
  }
2039

2040
  @Override
2041
  public int size() {
2042
    return data.size();
1✔
2043
  }
2044

2045
  @Override
2046
  public long estimatedSize() {
2047
    return data.mappingCount();
1✔
2048
  }
2049

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

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

2064
      // Cancel the scheduled cleanup
2065
      Pacer pacer = pacer();
1✔
2066
      if (pacer != null) {
1✔
2067
        pacer.cancel();
1✔
2068
      }
2069

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

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

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

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

2112
        if ((key == null) || (value[0] == null)) {
1✔
2113
          cause[0] = RemovalCause.COLLECTED;
1✔
2114
        } else if (hasExpired(node, now)) {
1✔
2115
          cause[0] = RemovalCause.EXPIRED;
1✔
2116
        } else {
2117
          cause[0] = RemovalCause.EXPLICIT;
1✔
2118
        }
2119

2120
        if (cause[0].wasEvicted()) {
1✔
2121
          notifyEviction(key, value[0], cause[0]);
1✔
2122
        }
2123

2124
        discardRefresh(node.getKeyReference());
1✔
2125
        node.retire();
1✔
2126
        return null;
1✔
2127
      }
2128
    });
2129

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

2145
    synchronized (node) {
1✔
2146
      logIfAlive(node);
1✔
2147
      makeDead(node);
1✔
2148
    }
1✔
2149

2150
    if (cause[0] != null) {
1✔
2151
      notifyRemoval(key, value[0], cause[0]);
1✔
2152
    }
2153
  }
1✔
2154

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

2162
  @Override
2163
  public boolean containsValue(Object value) {
2164
    requireNonNull(value);
1✔
2165

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

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

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

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

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

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

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

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

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

2269
    @SuppressWarnings("NullableProblems")
2270
    Map<K, V> unmodifiable = Collections.unmodifiableMap(result);
1✔
2271
    return unmodifiable;
1✔
2272
  }
2273

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

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

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

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

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

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

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

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

2397
        long expirationTime = isComputingAsync(value) ? (now + ASYNC_EXPIRY) : now;
1✔
2398
        if (mayUpdate) {
1✔
2399
          exceedsTolerance = exceedsWriteTimeTolerance(prior, varTime, now);
1✔
2400
          if (expired || exceedsTolerance) {
1✔
2401
            setWriteTime(prior, isComputingAsync(value) ? (now + ASYNC_EXPIRY) : now);
1✔
2402
          }
2403

2404
          prior.setValue(value, valueReferenceQueue());
1✔
2405
          prior.setWeight(newWeight);
1✔
2406

2407
          discardRefresh(prior.getKeyReference());
1✔
2408
        }
2409

2410
        setVariableTime(prior, varTime);
1✔
2411
        setAccessTime(prior, expirationTime);
1✔
2412
      }
1✔
2413

2414
      if (expired) {
1✔
2415
        notifyRemoval(key, oldValue, RemovalCause.EXPIRED);
1✔
2416
      } else if (oldValue == null) {
1✔
2417
        notifyRemoval(key, /* value= */ null, RemovalCause.COLLECTED);
1✔
2418
      } else if (mayUpdate) {
1✔
2419
        notifyOnReplace(key, oldValue, value);
1✔
2420
      }
2421

2422
      int weightedDifference = mayUpdate ? (newWeight - oldWeight) : 0;
1✔
2423
      if ((oldValue == null) || (weightedDifference != 0) || expired) {
1✔
2424
        afterWrite(new UpdateTask(prior, weightedDifference));
1✔
2425
      } else if (!onlyIfAbsent && exceedsTolerance) {
1✔
2426
        afterWrite(new UpdateTask(prior, weightedDifference));
1✔
2427
      } else {
2428
        afterRead(prior, now, /* recordHit= */ false);
1✔
2429
      }
2430

2431
      return expired ? null : oldValue;
1✔
2432
    }
2433
  }
2434

2435
  @Override
2436
  @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
2437
  public @Nullable V remove(Object key) {
2438
    @SuppressWarnings({"rawtypes", "unchecked"})
2439
    Node<K, V>[] node = new Node[1];
1✔
2440
    @SuppressWarnings({"unchecked", "Varifier"})
2441
    @Nullable K[] oldKey = (K[]) new Object[1];
1✔
2442
    @SuppressWarnings({"unchecked", "Varifier"})
2443
    @Nullable V[] oldValue = (V[]) new Object[1];
1✔
2444
    @Nullable RemovalCause[] cause = new RemovalCause[1];
1✔
2445
    Object lookupKey = nodeFactory.newLookupKey(key);
1✔
2446

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

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

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

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

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

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

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

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

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

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

2558
        long expirationTime = isComputingAsync(value) ? (now[0] + ASYNC_EXPIRY) : now[0];
1✔
2559
        exceedsTolerance[0] = exceedsWriteTimeTolerance(n, varTime, expirationTime);
1✔
2560
        if (exceedsTolerance[0]) {
1✔
2561
          setWriteTime(n, expirationTime);
1✔
2562
        }
2563
        setAccessTime(n, expirationTime);
1✔
2564
        setVariableTime(n, varTime);
1✔
2565
        discardRefresh(k);
1✔
2566
        return n;
1✔
2567
      }
2568
    });
2569

2570
    if ((node == null) || (nodeKey[0] == null) || (oldValue[0] == null)) {
1✔
2571
      return null;
1✔
2572
    }
2573

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

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

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

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

2597
    var now = new long[1];
1✔
2598
    var oldWeight = new int[1];
1✔
2599
    var exceedsTolerance = new boolean[1];
1✔
2600
    @SuppressWarnings({"unchecked", "Varifier"})
2601
    @Nullable K[] nodeKey = (K[]) new Object[1];
1✔
2602
    @SuppressWarnings({"unchecked", "Varifier"})
2603
    @Nullable V[] prevValue = (V[]) new Object[1];
1✔
2604

2605
    int weight = weigher.weigh(key, newValue);
1✔
2606
    Node<K, V> node = data.computeIfPresent(nodeFactory.newLookupKey(key), (k, n) -> {
1✔
2607
      synchronized (n) {
1✔
2608
        requireIsAlive(key, n);
1✔
2609
        nodeKey[0] = n.getKey();
1✔
2610
        prevValue[0] = n.getValue();
1✔
2611
        oldWeight[0] = n.getWeight();
1✔
2612
        if ((nodeKey[0] == null) || (prevValue[0] == null) || !n.containsValue(oldValue)
1✔
2613
            || hasExpired(n, now[0] = expirationTicker().read())) {
1✔
2614
          prevValue[0] = null;
1✔
2615
          return n;
1✔
2616
        }
2617

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

2622
        long expirationTime = isComputingAsync(newValue) ? (now[0] + ASYNC_EXPIRY) : now[0];
1✔
2623
        exceedsTolerance[0] = exceedsWriteTimeTolerance(n, varTime, expirationTime);
1✔
2624
        if (exceedsTolerance[0]) {
1✔
2625
          setWriteTime(n, expirationTime);
1✔
2626
        }
2627
        setAccessTime(n, expirationTime);
1✔
2628
        setVariableTime(n, varTime);
1✔
2629

2630
        if (shouldDiscardRefresh) {
1✔
2631
          discardRefresh(k);
1✔
2632
        }
2633
      }
1✔
2634
      return n;
1✔
2635
    });
2636

2637
    if ((node == null) || (nodeKey[0] == null) || (prevValue[0] == null)) {
1✔
2638
      return false;
1✔
2639
    }
2640

2641
    int weightedDifference = (weight - oldWeight[0]);
1✔
2642
    if (exceedsTolerance[0] || (weightedDifference != 0)) {
1✔
2643
      afterWrite(new UpdateTask(node, weightedDifference));
1✔
2644
    } else {
2645
      afterRead(node, now[0], /* recordHit= */ false);
1✔
2646
    }
2647

2648
    notifyOnReplace(nodeKey[0], prevValue[0], newValue);
1✔
2649
    return true;
1✔
2650
  }
2651

2652
  @Override
2653
  public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
2654
    requireNonNull(function);
1✔
2655

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

2665
  @Override
2666
  public @Nullable V computeIfAbsent(K key,
2667
      @Var Function<? super K, ? extends @Nullable V> mappingFunction,
2668
      boolean recordStats, boolean recordLoad) {
2669
    requireNonNull(key);
1✔
2670
    requireNonNull(mappingFunction);
1✔
2671
    long now = expirationTicker().read();
1✔
2672

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

2693
  /** Returns the current value from a computeIfAbsent invocation. */
2694
  @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
2695
  @Nullable V doComputeIfAbsent(K key, Object keyRef,
2696
      Function<? super K, ? extends @Nullable V> mappingFunction, long[/* 1 */] now,
2697
      boolean recordStats) {
2698
    @SuppressWarnings({"unchecked", "Varifier"})
2699
    @Nullable V[] oldValue = (V[]) new Object[1];
1✔
2700
    @SuppressWarnings({"unchecked", "Varifier"})
2701
    @Nullable V[] newValue = (V[]) new Object[1];
1✔
2702
    @SuppressWarnings({"unchecked", "Varifier"})
2703
    @Nullable K[] nodeKey = (K[]) new Object[1];
1✔
2704
    @SuppressWarnings({"rawtypes", "unchecked"})
2705
    @Nullable Node<K, V>[] removed = new Node[1];
1✔
2706
    @Nullable Throwable[] exception = new Throwable[1];
1✔
2707

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

2729
      synchronized (n) {
1✔
2730
        requireIsAlive(key, n);
1✔
2731
        nodeKey[0] = n.getKey();
1✔
2732
        weight[0] = n.getWeight();
1✔
2733
        oldValue[0] = n.getValue();
1✔
2734
        RemovalCause actualCause;
2735
        if ((nodeKey[0] == null) || (oldValue[0] == null)) {
1✔
2736
          actualCause = RemovalCause.COLLECTED;
1✔
2737
        } else if (hasExpired(n, now[0])) {
1✔
2738
          actualCause = RemovalCause.EXPIRED;
1✔
2739
        } else {
2740
          return n;
1✔
2741
        }
2742

2743
        cause[0] = actualCause;
1✔
2744
        notifyEviction(nodeKey[0], oldValue[0], actualCause);
1✔
2745

2746
        try {
2747
          newValue[0] = mappingFunction.apply(key);
1✔
2748
          if (newValue[0] == null) {
1✔
2749
            discardRefresh(k);
1✔
2750
            removed[0] = n;
1✔
2751
            n.retire();
1✔
2752
            return null;
1✔
2753
          }
2754
          now[0] = expirationTicker().read();
1✔
2755
          weight[1] = weigher.weigh(key, newValue[0]);
1✔
2756
          long varTime = expireAfterCreate(key, newValue[0], expiry(), now[0]);
1✔
2757

2758
          n.setValue(newValue[0], valueReferenceQueue());
1✔
2759
          n.setWeight(weight[1]);
1✔
2760

2761
          long expirationTime = isComputingAsync(newValue[0]) ? (now[0] + ASYNC_EXPIRY) : now[0];
1✔
2762
          setAccessTime(n, expirationTime);
1✔
2763
          setWriteTime(n, expirationTime);
1✔
2764
          setVariableTime(n, varTime);
1✔
2765
          discardRefresh(k);
1✔
2766
          return n;
1✔
2767
        } catch (Throwable e) {
1✔
2768
          newValue[0] = null;
1✔
2769
          discardRefresh(k);
1✔
2770
          exception[0] = e;
1✔
2771
          removed[0] = n;
1✔
2772
          n.retire();
1✔
2773
          return null;
1✔
2774
        }
2775
      }
2776
    });
2777

2778
    if (cause[0] != null) {
1✔
2779
      statsCounter().recordEviction(weight[0], cause[0]);
1✔
2780
      notifyRemoval(nodeKey[0], oldValue[0], cause[0]);
1✔
2781
    }
2782
    if (node == null) {
1✔
2783
      if (removed[0] != null) {
1✔
2784
        afterWrite(new RemovalTask(removed[0]));
1✔
2785
      }
2786
      if (exception[0] != null) {
1✔
NEW
2787
        throwException(exception[0]);
×
2788
      }
2789
      return null;
1✔
2790
    }
2791
    if ((oldValue[0] != null) && (newValue[0] == null)) {
1✔
2792
      if (!isComputingAsync(oldValue[0])) {
1✔
2793
        tryExpireAfterRead(node, key, oldValue[0], expiry(), now[0]);
1✔
2794
        setAccessTime(node, now[0]);
1✔
2795
      }
2796

2797
      afterRead(node, now[0], /* recordHit= */ recordStats);
1✔
2798
      return oldValue[0];
1✔
2799
    }
2800
    if ((oldValue[0] == null) && (cause[0] == null)) {
1✔
2801
      afterWrite(new AddTask(node, weight[1]));
1✔
2802
    } else {
2803
      int weightedDifference = (weight[1] - weight[0]);
1✔
2804
      afterWrite(new UpdateTask(node, weightedDifference));
1✔
2805
    }
2806

2807
    return newValue[0];
1✔
2808
  }
2809

2810
  @Override
2811
  public @Nullable V computeIfPresent(K key,
2812
      BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
2813
    requireNonNull(key);
1✔
2814
    requireNonNull(remappingFunction);
1✔
2815

2816
    // An optimistic fast path to avoid unnecessary locking
2817
    Object lookupKey = nodeFactory.newLookupKey(key);
1✔
2818
    @Nullable Node<K, V> node = data.get(lookupKey);
1✔
2819
    long now;
2820
    if (node == null) {
1✔
2821
      return null;
1✔
2822
    } else if ((node.getValue() == null) || hasExpired(node, (now = expirationTicker().read()))) {
1✔
2823
      scheduleDrainBuffers();
1✔
2824
      return null;
1✔
2825
    }
2826

2827
    BiFunction<? super K, ? super V, ? extends V> statsAwareRemappingFunction =
1✔
2828
        statsAware(remappingFunction, /* recordLoad= */ true, /* recordLoadFailure= */ true);
1✔
2829
    return remap(key, lookupKey, statsAwareRemappingFunction,
1✔
2830
        expiry(), new long[] { now }, /* computeIfAbsent= */ false);
1✔
2831
  }
2832

2833
  @Override
2834
  public @Nullable V compute(K key,
2835
      BiFunction<? super K, ? super V, ? extends @Nullable V> remappingFunction,
2836
      @Nullable Expiry<? super K, ? super V> expiry, boolean recordLoad,
2837
      boolean recordLoadFailure) {
2838
    requireNonNull(key);
1✔
2839
    requireNonNull(remappingFunction);
1✔
2840

2841
    long[] now = { expirationTicker().read() };
1✔
2842
    Object keyRef = nodeFactory.newReferenceKey(key, keyReferenceQueue());
1✔
2843
    BiFunction<? super K, ? super V, ? extends V> statsAwareRemappingFunction =
1✔
2844
        statsAware(remappingFunction, recordLoad, recordLoadFailure);
1✔
2845
    return remap(key, keyRef, statsAwareRemappingFunction,
1✔
2846
        expiry, now, /* computeIfAbsent= */ true);
2847
  }
2848

2849
  @Override
2850
  public @Nullable V merge(K key, V value,
2851
      BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
2852
    requireNonNull(key);
1✔
2853
    requireNonNull(value);
1✔
2854
    requireNonNull(remappingFunction);
1✔
2855

2856
    long[] now = { expirationTicker().read() };
1✔
2857
    Object keyRef = nodeFactory.newReferenceKey(key, keyReferenceQueue());
1✔
2858
    BiFunction<? super K, ? super @Nullable V, ? extends @Nullable V> mergeFunction =
1✔
2859
        (k, oldValue) -> (oldValue == null)
1✔
2860
          ? value
1✔
2861
          : statsAware(remappingFunction).apply(oldValue, value);
1✔
2862
    return remap(key, keyRef, mergeFunction, expiry(), now, /* computeIfAbsent= */ true);
1✔
2863
  }
2864

2865
  /**
2866
   * Attempts to compute a mapping for the specified key and its current mapped value (or
2867
   * {@code null} if there is no current mapping).
2868
   * <p>
2869
   * An entry that has expired or been reference collected is evicted and the computation continues
2870
   * as if the entry had not been present. This method does not pre-screen and does not wrap the
2871
   * remappingFunction to be statistics aware.
2872
   *
2873
   * @param key key with which the specified value is to be associated
2874
   * @param keyRef the key to associate with or a lookup only key if not {@code computeIfAbsent}
2875
   * @param remappingFunction the function to compute a value
2876
   * @param expiry the calculator for the expiration time
2877
   * @param now the current time, according to the ticker
2878
   * @param computeIfAbsent if an absent entry can be computed
2879
   * @return the new value associated with the specified key, or null if none
2880
   */
2881
  @SuppressWarnings({"StatementWithEmptyBody", "SynchronizationOnLocalVariableOrMethodParameter"})
2882
  @Nullable V remap(K key, Object keyRef,
2883
      BiFunction<? super K, ? super V, ? extends @Nullable V> remappingFunction,
2884
      @Nullable Expiry<? super K, ? super V> expiry, long[/* 1 */] now, boolean computeIfAbsent) {
2885
    @SuppressWarnings({"unchecked", "Varifier"})
2886
    @Nullable K[] nodeKey = (K[]) new Object[1];
1✔
2887
    @SuppressWarnings({"unchecked", "Varifier"})
2888
    @Nullable V[] oldValue = (V[]) new Object[1];
1✔
2889
    @SuppressWarnings({"unchecked", "Varifier"})
2890
    @Nullable V[] newValue = (V[]) new Object[1];
1✔
2891
    @SuppressWarnings({"rawtypes", "unchecked"})
2892
    @Nullable Node<K, V>[] removed = new Node[1];
1✔
2893
    @Nullable Throwable[] exception = new Throwable[1];
1✔
2894

2895
    var weight = new int[2]; // old, new
1✔
2896
    var cause = new RemovalCause[1];
1✔
2897
    var exceedsTolerance = new boolean[1];
1✔
2898

2899
    Node<K, V> node = data.compute(keyRef, (kr, n) -> {
1✔
2900
      if (n == null) {
1✔
2901
        if (!computeIfAbsent) {
1✔
2902
          return null;
1✔
2903
        }
2904
        newValue[0] = remappingFunction.apply(key, null);
1✔
2905
        if (newValue[0] == null) {
1✔
2906
          return null;
1✔
2907
        }
2908
        now[0] = expirationTicker().read();
1✔
2909
        weight[1] = weigher.weigh(key, newValue[0]);
1✔
2910
        long varTime = expireAfterCreate(key, newValue[0], expiry, now[0]);
1✔
2911
        var created = nodeFactory.newNode(keyRef, newValue[0],
1✔
2912
            valueReferenceQueue(), weight[1], now[0]);
1✔
2913

2914
        long expirationTime = isComputingAsync(newValue[0]) ? (now[0] + ASYNC_EXPIRY) : now[0];
1✔
2915
        setAccessTime(created, expirationTime);
1✔
2916
        setWriteTime(created, expirationTime);
1✔
2917
        setVariableTime(created, varTime);
1✔
2918
        discardRefresh(kr);
1✔
2919
        return created;
1✔
2920
      }
2921

2922
      synchronized (n) {
1✔
2923
        requireIsAlive(key, n);
1✔
2924
        nodeKey[0] = n.getKey();
1✔
2925
        oldValue[0] = n.getValue();
1✔
2926
        if ((nodeKey[0] == null) || (oldValue[0] == null)) {
1✔
2927
          cause[0] = RemovalCause.COLLECTED;
1✔
2928
        } else if (hasExpired(n, expirationTicker().read())) {
1✔
2929
          cause[0] = RemovalCause.EXPIRED;
1✔
2930
        }
2931
        if (cause[0] != null) {
1✔
2932
          notifyEviction(nodeKey[0], oldValue[0], cause[0]);
1✔
2933
          if (!computeIfAbsent) {
1✔
2934
            removed[0] = n;
1✔
2935
            n.retire();
1✔
2936
            return null;
1✔
2937
          }
2938
        }
2939

2940
        boolean wasEvicted = (cause[0] != null);
1✔
2941
        try {
2942
          newValue[0] = remappingFunction.apply(nodeKey[0],
1✔
2943
              (cause[0] == null) ? oldValue[0] : null);
1✔
2944

2945
          if (newValue[0] == null) {
1✔
2946
            if (cause[0] == null) {
1✔
2947
              cause[0] = RemovalCause.EXPLICIT;
1✔
2948
              discardRefresh(kr);
1✔
2949
            }
2950
            removed[0] = n;
1✔
2951
            n.retire();
1✔
2952
            return null;
1✔
2953
          }
2954

2955
          long varTime;
2956
          weight[0] = n.getWeight();
1✔
2957
          weight[1] = weigher.weigh(key, newValue[0]);
1✔
2958
          now[0] = expirationTicker().read();
1✔
2959
          if (cause[0] == null) {
1✔
2960
            if (newValue[0] != oldValue[0]) {
1✔
2961
              cause[0] = RemovalCause.REPLACED;
1✔
2962
            }
2963
            varTime = expireAfterUpdate(n, key, newValue[0], expiry, now[0]);
1✔
2964
          } else {
2965
            varTime = expireAfterCreate(key, newValue[0], expiry, now[0]);
1✔
2966
          }
2967

2968
          n.setValue(newValue[0], valueReferenceQueue());
1✔
2969
          n.setWeight(weight[1]);
1✔
2970

2971
          long expirationTime = isComputingAsync(newValue[0]) ? (now[0] + ASYNC_EXPIRY) : now[0];
1✔
2972
          exceedsTolerance[0] = exceedsWriteTimeTolerance(n, varTime, expirationTime);
1✔
2973
          if (((cause[0] != null) && cause[0].wasEvicted()) || exceedsTolerance[0]) {
1✔
2974
            setWriteTime(n, expirationTime);
1✔
2975
          }
2976
          setAccessTime(n, expirationTime);
1✔
2977
          setVariableTime(n, varTime);
1✔
2978
          discardRefresh(kr);
1✔
2979
          return n;
1✔
2980
        } catch (Throwable e) {
1✔
2981
          if (!wasEvicted) {
1✔
2982
            throw e;
1✔
2983
          }
2984
          newValue[0] = null;
1✔
2985
          exception[0] = e;
1✔
2986
          removed[0] = n;
1✔
2987
          n.retire();
1✔
2988
          return null;
1✔
2989
        }
2990
      }
2991
    });
2992

2993
    if (cause[0] != null) {
1✔
2994
      if (cause[0] == RemovalCause.REPLACED) {
1✔
2995
        requireNonNull(newValue[0]);
1✔
2996
        notifyOnReplace(key, oldValue[0], newValue[0]);
1✔
2997
      } else {
2998
        if (cause[0].wasEvicted()) {
1✔
2999
          statsCounter().recordEviction(weight[0], cause[0]);
1✔
3000
        }
3001
        notifyRemoval(nodeKey[0], oldValue[0], cause[0]);
1✔
3002
      }
3003
    }
3004

3005
    if (removed[0] != null) {
1✔
3006
      afterWrite(new RemovalTask(removed[0]));
1✔
3007
    } else if (node == null) {
1✔
3008
      // absent and not computable
3009
    } else if ((oldValue[0] == null) && (cause[0] == null)) {
1✔
3010
      afterWrite(new AddTask(node, weight[1]));
1✔
3011
    } else {
3012
      int weightedDifference = weight[1] - weight[0];
1✔
3013
      if (exceedsTolerance[0] || (weightedDifference != 0)) {
1✔
3014
        afterWrite(new UpdateTask(node, weightedDifference));
1✔
3015
      } else {
3016
        afterRead(node, now[0], /* recordHit= */ false);
1✔
3017
        if ((cause[0] != null) && cause[0].wasEvicted()) {
1✔
3018
          scheduleDrainBuffers();
1✔
3019
        }
3020
      }
3021
    }
3022

3023
    if (exception[0] != null) {
1✔
NEW
3024
      throwException(exception[0]);
×
3025
    }
3026
    return newValue[0];
1✔
3027
  }
3028

3029
  @Override
3030
  public void forEach(BiConsumer<? super K, ? super V> action) {
3031
    requireNonNull(action);
1✔
3032

3033
    for (var iterator = new EntryIterator<>(this); iterator.hasNext();) {
1✔
3034
      action.accept(iterator.key, iterator.value);
1✔
3035
      iterator.advance();
1✔
3036
    }
3037
  }
1✔
3038

3039
  @Override
3040
  public Set<K> keySet() {
3041
    Set<K> ks = keySet;
1✔
3042
    return (ks == null) ? (keySet = new KeySetView<>(this)) : ks;
1✔
3043
  }
3044

3045
  @Override
3046
  public Collection<V> values() {
3047
    Collection<V> vs = values;
1✔
3048
    return (vs == null) ? (values = new ValuesView<>(this)) : vs;
1✔
3049
  }
3050

3051
  @Override
3052
  public Set<Entry<K, V>> entrySet() {
3053
    Set<Entry<K, V>> es = entrySet;
1✔
3054
    return (es == null) ? (entrySet = new EntrySetView<>(this)) : es;
1✔
3055
  }
3056

3057
  /**
3058
   * Object equality requires reflexive, symmetric, transitive, and consistency properties. Of
3059
   * these, symmetry and consistency require further clarification for how they are upheld.
3060
   * <p>
3061
   * The <i>consistency</i> property between invocations requires that the results are the same if
3062
   * there are no modifications to the information used. Therefore, usages should expect that this
3063
   * operation may return misleading results if either the maps or the data held by them is modified
3064
   * during the execution of this method. This characteristic allows for comparing the map sizes and
3065
   * assuming stable mappings, as done by {@link java.util.AbstractMap}-based maps.
3066
   * <p>
3067
   * The <i>symmetric</i> property requires that the result is the same for all implementations of
3068
   * {@link Map#equals(Object)}. That contract is defined in terms of the stable mappings provided
3069
   * by {@link #entrySet()}, meaning that the {@link #size()} optimization forces that the count is
3070
   * consistent with the mappings when used for an equality check.
3071
   * <p>
3072
   * The cache's {@link #size()} method may include entries that have expired or have been reference
3073
   * collected, but have not yet been removed from the backing map. An iteration over the map may
3074
   * trigger the removal of these dead entries when skipped over during traversal. To ensure
3075
   * consistency and symmetry, usages should call {@link #cleanUp()} before this method while no
3076
   * other concurrent operations are being performed on this cache. This is not done implicitly by
3077
   * {@link #size()} as many usages assume it to be instantaneous and lock-free.
3078
   */
3079
  @Override
3080
  public boolean equals(@Nullable Object o) {
3081
    if (o == this) {
1✔
3082
      return true;
1✔
3083
    } else if (!(o instanceof Map)) {
1✔
3084
      return false;
1✔
3085
    }
3086

3087
    var map = (Map<?, ?>) o;
1✔
3088
    if (size() != map.size()) {
1✔
3089
      return false;
1✔
3090
    }
3091

3092
    long now = expirationTicker().read();
1✔
3093
    for (var node : data.values()) {
1✔
3094
      K key = node.getKey();
1✔
3095
      V value = node.getValue();
1✔
3096
      if ((key == null) || (value == null)
1✔
3097
          || !node.isAlive() || hasExpired(node, now)) {
1✔
3098
        scheduleDrainBuffers();
1✔
3099
        return false;
1✔
3100
      } else {
3101
        var val = map.get(key);
1✔
3102
        if ((val == null) || ((val != value) && !val.equals(value))) {
1✔
3103
          return false;
1✔
3104
        }
3105
      }
3106
    }
1✔
3107
    return true;
1✔
3108
  }
3109

3110
  @Override
3111
  public int hashCode() {
3112
    @Var int hash = 0;
1✔
3113
    long now = expirationTicker().read();
1✔
3114
    for (var node : data.values()) {
1✔
3115
      K key = node.getKey();
1✔
3116
      V value = node.getValue();
1✔
3117
      if ((key == null) || (value == null)
1✔
3118
          || !node.isAlive() || hasExpired(node, now)) {
1✔
3119
        scheduleDrainBuffers();
1✔
3120
      } else {
3121
        hash += key.hashCode() ^ value.hashCode();
1✔
3122
      }
3123
    }
1✔
3124
    return hash;
1✔
3125
  }
3126

3127
  @Override
3128
  public String toString() {
3129
    var result = new StringBuilder().append('{');
1✔
3130
    long now = expirationTicker().read();
1✔
3131
    for (var node : data.values()) {
1✔
3132
      K key = node.getKey();
1✔
3133
      V value = node.getValue();
1✔
3134
      if ((key == null) || (value == null)
1✔
3135
          || !node.isAlive() || hasExpired(node, now)) {
1✔
3136
        scheduleDrainBuffers();
1✔
3137
      } else {
3138
        if (result.length() != 1) {
1✔
3139
          result.append(',').append(' ');
1✔
3140
        }
3141
        result.append((key == this) ? "(this Map)" : key);
1✔
3142
        result.append('=');
1✔
3143
        result.append((value == this) ? "(this Map)" : value);
1✔
3144
      }
3145
    }
1✔
3146
    return result.append('}').toString();
1✔
3147
  }
3148

3149
  /**
3150
   * Returns the computed result from the ordered traversal of the cache entries.
3151
   *
3152
   * @param hottest the coldest or hottest iteration order
3153
   * @param transformer a function that unwraps the value
3154
   * @param mappingFunction the mapping function to compute a value
3155
   * @return the computed value
3156
   */
3157
  @SuppressWarnings("GuardedByChecker")
3158
  <T> T evictionOrder(boolean hottest, Function<@Nullable V, @Nullable V> transformer,
3159
      Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
3160
    Comparator<Node<K, V>> comparator = Comparator.comparingInt(node -> {
1✔
3161
      var keyRef = node.getKeyReferenceOrNull();
1✔
3162
      return (keyRef == null) ? 0 : frequencySketch().frequency(keyRef);
1✔
3163
    });
3164
    Iterable<Node<K, V>> iterable;
3165
    if (hottest) {
1✔
3166
      iterable = () -> {
1✔
3167
        var secondary = PeekingIterator.comparing(
1✔
3168
            accessOrderProbationDeque().descendingIterator(),
1✔
3169
            accessOrderWindowDeque().descendingIterator(), comparator);
1✔
3170
        return PeekingIterator.concat(
1✔
3171
            accessOrderProtectedDeque().descendingIterator(), secondary);
1✔
3172
      };
3173
    } else {
3174
      iterable = () -> {
1✔
3175
        var primary = PeekingIterator.comparing(
1✔
3176
            accessOrderWindowDeque().iterator(), accessOrderProbationDeque().iterator(),
1✔
3177
            comparator.reversed());
1✔
3178
        return PeekingIterator.concat(primary, accessOrderProtectedDeque().iterator());
1✔
3179
      };
3180
    }
3181
    return snapshot(iterable, transformer, mappingFunction);
1✔
3182
  }
3183

3184
  /**
3185
   * Returns the computed result from the ordered traversal of the cache entries.
3186
   *
3187
   * @param oldest the youngest or oldest iteration order
3188
   * @param transformer a function that unwraps the value
3189
   * @param mappingFunction the mapping function to compute a value
3190
   * @return the computed value
3191
   */
3192
  @SuppressWarnings("GuardedByChecker")
3193
  <T> T expireAfterAccessOrder(boolean oldest, Function<@Nullable V, @Nullable V> transformer,
3194
      Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
3195
    Iterable<Node<K, V>> iterable;
3196
    if (evicts()) {
1✔
3197
      iterable = () -> {
1✔
3198
        @Var Comparator<Node<K, V>> comparator = Comparator.comparingLong(Node::getAccessTime);
1✔
3199
        PeekingIterator<Node<K, V>> first;
3200
        PeekingIterator<Node<K, V>> second;
3201
        PeekingIterator<Node<K, V>> third;
3202
        if (oldest) {
1✔
3203
          first = accessOrderWindowDeque().iterator();
1✔
3204
          second = accessOrderProbationDeque().iterator();
1✔
3205
          third = accessOrderProtectedDeque().iterator();
1✔
3206
        } else {
3207
          comparator = comparator.reversed();
1✔
3208
          first = accessOrderWindowDeque().descendingIterator();
1✔
3209
          second = accessOrderProbationDeque().descendingIterator();
1✔
3210
          third = accessOrderProtectedDeque().descendingIterator();
1✔
3211
        }
3212
        return PeekingIterator.comparing(
1✔
3213
            PeekingIterator.comparing(first, second, comparator), third, comparator);
1✔
3214
      };
3215
    } else {
3216
      iterable = oldest
1✔
3217
          ? accessOrderWindowDeque()
1✔
3218
          : accessOrderWindowDeque()::descendingIterator;
1✔
3219
    }
3220
    return snapshot(iterable, transformer, mappingFunction);
1✔
3221
  }
3222

3223
  /**
3224
   * Returns the computed result from the ordered traversal of the cache entries.
3225
   *
3226
   * @param iterable the supplier of the entries in the cache
3227
   * @param transformer a function that unwraps the value
3228
   * @param mappingFunction the mapping function to compute a value
3229
   * @return the computed value
3230
   */
3231
  <T> T snapshot(Iterable<Node<K, V>> iterable, Function<@Nullable V, @Nullable V> transformer,
3232
      Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
3233
    requireNonNull(mappingFunction);
1✔
3234
    requireNonNull(transformer);
1✔
3235
    requireNonNull(iterable);
1✔
3236

3237
    evictionLock.lock();
1✔
3238
    try {
3239
      maintenance(/* ignored */ null);
1✔
3240

3241
      // Obtain the iterator as late as possible for modification count checking
3242
      try (var stream = StreamSupport.stream(Spliterators.spliteratorUnknownSize(
1✔
3243
           iterable.iterator(), DISTINCT | ORDERED | NONNULL | IMMUTABLE), /* parallel= */ false)) {
1✔
3244
        return mappingFunction.apply(stream
1✔
3245
            .map(node -> nodeToCacheEntry(node, transformer))
1✔
3246
            .filter(Objects::nonNull));
1✔
3247
      }
3248
    } finally {
3249
      evictionLock.unlock();
1✔
3250
      rescheduleCleanUpIfIncomplete();
1✔
3251
    }
3252
  }
3253

3254
  /** Returns an entry for the given node if it can be used externally, else null. */
3255
  @Nullable CacheEntry<K, V> nodeToCacheEntry(
3256
      Node<K, V> node, Function<@Nullable V, @Nullable V> transformer) {
3257
    V value = transformer.apply(node.getValue());
1✔
3258
    K key = node.getKey();
1✔
3259
    long now;
3260
    if ((key == null) || (value == null) || !node.isAlive()
1✔
3261
        || hasExpired(node, (now = expirationTicker().read()))) {
1✔
3262
      return null;
1✔
3263
    }
3264

3265
    @Var long expiresAfter = Long.MAX_VALUE;
1✔
3266
    if (expiresAfterAccess()) {
1✔
3267
      expiresAfter = Math.min(expiresAfter,
1✔
3268
          expiresAfterAccessNanos() - (now - node.getAccessTime()));
1✔
3269
    }
3270
    if (expiresAfterWrite()) {
1✔
3271
      expiresAfter = Math.min(expiresAfter,
1✔
3272
          expiresAfterWriteNanos() - ((now & ~1L) - (node.getWriteTime() & ~1L)));
1✔
3273
    }
3274
    if (expiresVariable()) {
1✔
3275
      expiresAfter = node.getVariableTime() - now;
1✔
3276
    }
3277

3278
    long refreshableAt = refreshAfterWrite()
1✔
3279
        ? (node.getWriteTime() & ~1L) + refreshAfterWriteNanos()
1✔
3280
        : now + Long.MAX_VALUE;
1✔
3281
    int weight = node.getPolicyWeight();
1✔
3282
    return SnapshotEntry.forEntry(key, value, now, weight, now + expiresAfter, refreshableAt);
1✔
3283
  }
3284

3285
  /** A function that produces an unmodifiable map up to the limit in stream order. */
3286
  static final class SizeLimiter<K, V> implements Function<Stream<CacheEntry<K, V>>, Map<K, V>> {
3287
    private final int expectedSize;
3288
    private final long limit;
3289

3290
    SizeLimiter(int expectedSize, long limit) {
1✔
3291
      requireArgument(limit >= 0);
1✔
3292
      this.expectedSize = expectedSize;
1✔
3293
      this.limit = limit;
1✔
3294
    }
1✔
3295

3296
    @Override
3297
    public Map<K, V> apply(Stream<CacheEntry<K, V>> stream) {
3298
      var map = new LinkedHashMap<K, V>(calculateHashMapCapacity(expectedSize));
1✔
3299
      stream.limit(limit).forEach(entry -> map.put(entry.getKey(), entry.getValue()));
1✔
3300
      return Collections.unmodifiableMap(map);
1✔
3301
    }
3302
  }
3303

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

3308
    private long weightedSize;
3309

3310
    WeightLimiter(long weightLimit) {
1✔
3311
      requireArgument(weightLimit >= 0);
1✔
3312
      this.weightLimit = weightLimit;
1✔
3313
    }
1✔
3314

3315
    @Override
3316
    public Map<K, V> apply(Stream<CacheEntry<K, V>> stream) {
3317
      var map = new LinkedHashMap<K, V>();
1✔
3318
      stream.takeWhile(entry -> {
1✔
3319
        weightedSize = Math.addExact(weightedSize, entry.weight());
1✔
3320
        return (weightedSize <= weightLimit);
1✔
3321
      }).forEach(entry -> map.put(entry.getKey(), entry.getValue()));
1✔
3322
      return Collections.unmodifiableMap(map);
1✔
3323
    }
3324
  }
3325

3326
  /** An adapter to safely externalize the keys. */
3327
  static final class KeySetView<K, V> extends AbstractSet<K> {
3328
    final BoundedLocalCache<K, V> cache;
3329

3330
    KeySetView(BoundedLocalCache<K, V> cache) {
1✔
3331
      this.cache = requireNonNull(cache);
1✔
3332
    }
1✔
3333

3334
    @Override
3335
    public int size() {
3336
      return cache.size();
1✔
3337
    }
3338

3339
    @Override
3340
    public void clear() {
3341
      cache.clear();
1✔
3342
    }
1✔
3343

3344
    @Override
3345
    @SuppressWarnings("SuspiciousMethodCalls")
3346
    public boolean contains(Object o) {
3347
      return cache.containsKey(o);
1✔
3348
    }
3349

3350
    @Override
3351
    public boolean removeAll(Collection<?> collection) {
3352
      requireNonNull(collection);
1✔
3353
      @Var boolean modified = false;
1✔
3354
      if ((collection instanceof Set<?>) && (collection.size() > size())) {
1✔
3355
        for (K key : this) {
1✔
3356
          if (collection.contains(key)) {
1✔
3357
            modified |= remove(key);
1✔
3358
          }
3359
        }
1✔
3360
      } else {
3361
        for (var item : collection) {
1✔
3362
          modified |= (item != null) && remove(item);
1✔
3363
        }
1✔
3364
      }
3365
      return modified;
1✔
3366
    }
3367

3368
    @Override
3369
    public boolean remove(Object o) {
3370
      return (cache.remove(o) != null);
1✔
3371
    }
3372

3373
    @Override
3374
    public boolean removeIf(Predicate<? super K> filter) {
3375
      requireNonNull(filter);
1✔
3376
      @Var boolean modified = false;
1✔
3377
      for (K key : this) {
1✔
3378
        if (filter.test(key) && remove(key)) {
1✔
3379
          modified = true;
1✔
3380
        }
3381
      }
1✔
3382
      return modified;
1✔
3383
    }
3384

3385
    @Override
3386
    public boolean retainAll(Collection<?> collection) {
3387
      requireNonNull(collection);
1✔
3388
      @Var boolean modified = false;
1✔
3389
      for (K key : this) {
1✔
3390
        if (!collection.contains(key) && remove(key)) {
1✔
3391
          modified = true;
1✔
3392
        }
3393
      }
1✔
3394
      return modified;
1✔
3395
    }
3396

3397
    @Override
3398
    public Iterator<K> iterator() {
3399
      return new KeyIterator<>(cache);
1✔
3400
    }
3401

3402
    @Override
3403
    public Spliterator<K> spliterator() {
3404
      return new KeySpliterator<>(cache);
1✔
3405
    }
3406
  }
3407

3408
  /** An adapter to safely externalize the key iterator. */
3409
  static final class KeyIterator<K, V> implements Iterator<K> {
3410
    final EntryIterator<K, V> iterator;
3411

3412
    KeyIterator(BoundedLocalCache<K, V> cache) {
1✔
3413
      this.iterator = new EntryIterator<>(cache);
1✔
3414
    }
1✔
3415

3416
    @Override
3417
    public boolean hasNext() {
3418
      return iterator.hasNext();
1✔
3419
    }
3420

3421
    @Override
3422
    public K next() {
3423
      return iterator.nextKey();
1✔
3424
    }
3425

3426
    @Override
3427
    public void remove() {
3428
      iterator.remove();
1✔
3429
    }
1✔
3430
  }
3431

3432
  /** An adapter to safely externalize the key spliterator. */
3433
  static final class KeySpliterator<K, V> implements Spliterator<K> {
3434
    final Spliterator<Node<K, V>> spliterator;
3435
    final BoundedLocalCache<K, V> cache;
3436

3437
    KeySpliterator(BoundedLocalCache<K, V> cache) {
3438
      this(cache, cache.data.values().spliterator());
1✔
3439
    }
1✔
3440

3441
    KeySpliterator(BoundedLocalCache<K, V> cache, Spliterator<Node<K, V>> spliterator) {
1✔
3442
      this.spliterator = requireNonNull(spliterator);
1✔
3443
      this.cache = requireNonNull(cache);
1✔
3444
    }
1✔
3445

3446
    @Override
3447
    public void forEachRemaining(Consumer<? super K> action) {
3448
      requireNonNull(action);
1✔
3449
      Consumer<Node<K, V>> consumer = node -> {
1✔
3450
        K key = node.getKey();
1✔
3451
        V value = node.getValue();
1✔
3452
        long now = cache.expirationTicker().read();
1✔
3453
        if ((key != null) && (value != null) && node.isAlive() && !cache.hasExpired(node, now)) {
1✔
3454
          action.accept(key);
1✔
3455
        }
3456
      };
1✔
3457
      spliterator.forEachRemaining(consumer);
1✔
3458
    }
1✔
3459

3460
    @Override
3461
    public boolean tryAdvance(Consumer<? super K> action) {
3462
      requireNonNull(action);
1✔
3463
      boolean[] advanced = { false };
1✔
3464
      Consumer<Node<K, V>> consumer = node -> {
1✔
3465
        K key = node.getKey();
1✔
3466
        V value = node.getValue();
1✔
3467
        long now = cache.expirationTicker().read();
1✔
3468
        if ((key != null) && (value != null) && node.isAlive() && !cache.hasExpired(node, now)) {
1✔
3469
          action.accept(key);
1✔
3470
          advanced[0] = true;
1✔
3471
        }
3472
      };
1✔
3473
      while (spliterator.tryAdvance(consumer)) {
1✔
3474
        if (advanced[0]) {
1✔
3475
          return true;
1✔
3476
        }
3477
      }
3478
      return false;
1✔
3479
    }
3480

3481
    @Override
3482
    public @Nullable Spliterator<K> trySplit() {
3483
      Spliterator<Node<K, V>> split = spliterator.trySplit();
1✔
3484
      return (split == null) ? null : new KeySpliterator<>(cache, split);
1✔
3485
    }
3486

3487
    @Override
3488
    public long estimateSize() {
3489
      return spliterator.estimateSize();
1✔
3490
    }
3491

3492
    @Override
3493
    public int characteristics() {
3494
      return DISTINCT | CONCURRENT | NONNULL;
1✔
3495
    }
3496
  }
3497

3498
  /** An adapter to safely externalize the values. */
3499
  static final class ValuesView<K, V> extends AbstractCollection<V> {
3500
    final BoundedLocalCache<K, V> cache;
3501

3502
    ValuesView(BoundedLocalCache<K, V> cache) {
1✔
3503
      this.cache = requireNonNull(cache);
1✔
3504
    }
1✔
3505

3506
    @Override
3507
    public int size() {
3508
      return cache.size();
1✔
3509
    }
3510

3511
    @Override
3512
    public void clear() {
3513
      cache.clear();
1✔
3514
    }
1✔
3515

3516
    @Override
3517
    @SuppressWarnings("SuspiciousMethodCalls")
3518
    public boolean contains(Object o) {
3519
      return cache.containsValue(o);
1✔
3520
    }
3521

3522
    @Override
3523
    public boolean removeAll(Collection<?> collection) {
3524
      requireNonNull(collection);
1✔
3525
      @Var boolean modified = false;
1✔
3526
      for (var iterator = new EntryIterator<>(cache); iterator.hasNext();) {
1✔
3527
        var key = requireNonNull(iterator.key);
1✔
3528
        var value = requireNonNull(iterator.value);
1✔
3529
        if (collection.contains(value) && cache.remove(key, value)) {
1✔
3530
          modified = true;
1✔
3531
        }
3532
        iterator.advance();
1✔
3533
      }
1✔
3534
      return modified;
1✔
3535
    }
3536

3537
    @Override
3538
    public boolean remove(@Nullable Object o) {
3539
      if (o == null) {
1✔
3540
        return false;
1✔
3541
      }
3542
      for (var iterator = new EntryIterator<>(cache); iterator.hasNext();) {
1✔
3543
        var key = requireNonNull(iterator.key);
1✔
3544
        var value = requireNonNull(iterator.value);
1✔
3545
        if (o.equals(value) && cache.remove(key, value)) {
1✔
3546
          return true;
1✔
3547
        }
3548
        iterator.advance();
1✔
3549
      }
1✔
3550
      return false;
1✔
3551
    }
3552

3553
    @Override
3554
    public boolean removeIf(Predicate<? super V> filter) {
3555
      requireNonNull(filter);
1✔
3556
      @Var boolean modified = false;
1✔
3557
      for (var iterator = new EntryIterator<>(cache); iterator.hasNext();) {
1✔
3558
        var value = requireNonNull(iterator.value);
1✔
3559
        if (filter.test(value)) {
1✔
3560
          var key = requireNonNull(iterator.key);
1✔
3561
          modified |= cache.remove(key, value);
1✔
3562
        }
3563
        iterator.advance();
1✔
3564
      }
1✔
3565
      return modified;
1✔
3566
    }
3567

3568
    @Override
3569
    public boolean retainAll(Collection<?> collection) {
3570
      requireNonNull(collection);
1✔
3571
      @Var boolean modified = false;
1✔
3572
      for (var iterator = new EntryIterator<>(cache); iterator.hasNext();) {
1✔
3573
        var key = requireNonNull(iterator.key);
1✔
3574
        var value = requireNonNull(iterator.value);
1✔
3575
        if (!collection.contains(value) && cache.remove(key, value)) {
1✔
3576
          modified = true;
1✔
3577
        }
3578
        iterator.advance();
1✔
3579
      }
1✔
3580
      return modified;
1✔
3581
    }
3582

3583
    @Override
3584
    public Iterator<V> iterator() {
3585
      return new ValueIterator<>(cache);
1✔
3586
    }
3587

3588
    @Override
3589
    public Spliterator<V> spliterator() {
3590
      return new ValueSpliterator<>(cache);
1✔
3591
    }
3592
  }
3593

3594
  /** An adapter to safely externalize the value iterator. */
3595
  static final class ValueIterator<K, V> implements Iterator<V> {
3596
    final EntryIterator<K, V> iterator;
3597

3598
    ValueIterator(BoundedLocalCache<K, V> cache) {
1✔
3599
      this.iterator = new EntryIterator<>(cache);
1✔
3600
    }
1✔
3601

3602
    @Override
3603
    public boolean hasNext() {
3604
      return iterator.hasNext();
1✔
3605
    }
3606

3607
    @Override
3608
    public V next() {
3609
      return iterator.nextValue();
1✔
3610
    }
3611

3612
    @Override
3613
    public void remove() {
3614
      iterator.remove();
1✔
3615
    }
1✔
3616
  }
3617

3618
  /** An adapter to safely externalize the value spliterator. */
3619
  static final class ValueSpliterator<K, V> implements Spliterator<V> {
3620
    final Spliterator<Node<K, V>> spliterator;
3621
    final BoundedLocalCache<K, V> cache;
3622

3623
    ValueSpliterator(BoundedLocalCache<K, V> cache) {
3624
      this(cache, cache.data.values().spliterator());
1✔
3625
    }
1✔
3626

3627
    ValueSpliterator(BoundedLocalCache<K, V> cache, Spliterator<Node<K, V>> spliterator) {
1✔
3628
      this.spliterator = requireNonNull(spliterator);
1✔
3629
      this.cache = requireNonNull(cache);
1✔
3630
    }
1✔
3631

3632
    @Override
3633
    public void forEachRemaining(Consumer<? super V> action) {
3634
      requireNonNull(action);
1✔
3635
      Consumer<Node<K, V>> consumer = node -> {
1✔
3636
        K key = node.getKey();
1✔
3637
        V value = node.getValue();
1✔
3638
        long now = cache.expirationTicker().read();
1✔
3639
        if ((key != null) && (value != null) && node.isAlive() && !cache.hasExpired(node, now)) {
1✔
3640
          action.accept(value);
1✔
3641
        }
3642
      };
1✔
3643
      spliterator.forEachRemaining(consumer);
1✔
3644
    }
1✔
3645

3646
    @Override
3647
    public boolean tryAdvance(Consumer<? super V> action) {
3648
      requireNonNull(action);
1✔
3649
      boolean[] advanced = { false };
1✔
3650
      Consumer<Node<K, V>> consumer = node -> {
1✔
3651
        K key = node.getKey();
1✔
3652
        V value = node.getValue();
1✔
3653
        long now = cache.expirationTicker().read();
1✔
3654
        if ((key != null) && (value != null) && node.isAlive() && !cache.hasExpired(node, now)) {
1✔
3655
          action.accept(value);
1✔
3656
          advanced[0] = true;
1✔
3657
        }
3658
      };
1✔
3659
      while (spliterator.tryAdvance(consumer)) {
1✔
3660
        if (advanced[0]) {
1✔
3661
          return true;
1✔
3662
        }
3663
      }
3664
      return false;
1✔
3665
    }
3666

3667
    @Override
3668
    public @Nullable Spliterator<V> trySplit() {
3669
      Spliterator<Node<K, V>> split = spliterator.trySplit();
1✔
3670
      return (split == null) ? null : new ValueSpliterator<>(cache, split);
1✔
3671
    }
3672

3673
    @Override
3674
    public long estimateSize() {
3675
      return spliterator.estimateSize();
1✔
3676
    }
3677

3678
    @Override
3679
    public int characteristics() {
3680
      return CONCURRENT | NONNULL;
1✔
3681
    }
3682
  }
3683

3684
  /** An adapter to safely externalize the entries. */
3685
  static final class EntrySetView<K, V> extends AbstractSet<Entry<K, V>> {
3686
    final BoundedLocalCache<K, V> cache;
3687

3688
    EntrySetView(BoundedLocalCache<K, V> cache) {
1✔
3689
      this.cache = requireNonNull(cache);
1✔
3690
    }
1✔
3691

3692
    @Override
3693
    public int size() {
3694
      return cache.size();
1✔
3695
    }
3696

3697
    @Override
3698
    public void clear() {
3699
      cache.clear();
1✔
3700
    }
1✔
3701

3702
    @Override
3703
    public boolean contains(Object o) {
3704
      if (!(o instanceof Entry<?, ?>)) {
1✔
3705
        return false;
1✔
3706
      }
3707
      var entry = (Entry<?, ?>) o;
1✔
3708
      var key = entry.getKey();
1✔
3709
      var value = entry.getValue();
1✔
3710
      if ((key == null) || (value == null)) {
1✔
3711
        return false;
1✔
3712
      }
3713
      Node<K, V> node = cache.data.get(cache.nodeFactory.newLookupKey(key));
1✔
3714
      return (node != null) && node.containsValue(value)
1✔
3715
           && !cache.hasExpired(node, cache.expirationTicker().read());
1✔
3716
    }
3717

3718
    @Override
3719
    public boolean removeAll(Collection<?> collection) {
3720
      requireNonNull(collection);
1✔
3721
      @Var boolean modified = false;
1✔
3722
      if ((collection instanceof Set<?>) && (collection.size() > size())) {
1✔
3723
        for (var entry : this) {
1✔
3724
          if (collection.contains(entry)) {
1✔
3725
            modified |= remove(entry);
1✔
3726
          }
3727
        }
1✔
3728
      } else {
3729
        for (var item : collection) {
1✔
3730
          modified |= (item != null) && remove(item);
1✔
3731
        }
1✔
3732
      }
3733
      return modified;
1✔
3734
    }
3735

3736
    @Override
3737
    @SuppressWarnings("SuspiciousMethodCalls")
3738
    public boolean remove(Object o) {
3739
      if (!(o instanceof Entry<?, ?>)) {
1✔
3740
        return false;
1✔
3741
      }
3742
      var entry = (Entry<?, ?>) o;
1✔
3743
      var key = entry.getKey();
1✔
3744
      return (key != null) && cache.remove(key, entry.getValue());
1✔
3745
    }
3746

3747
    @Override
3748
    public boolean removeIf(Predicate<? super Entry<K, V>> filter) {
3749
      requireNonNull(filter);
1✔
3750
      @Var boolean modified = false;
1✔
3751
      for (Entry<K, V> entry : this) {
1✔
3752
        if (filter.test(entry)) {
1✔
3753
          modified |= cache.remove(entry.getKey(), entry.getValue());
1✔
3754
        }
3755
      }
1✔
3756
      return modified;
1✔
3757
    }
3758

3759
    @Override
3760
    public boolean retainAll(Collection<?> collection) {
3761
      requireNonNull(collection);
1✔
3762
      @Var boolean modified = false;
1✔
3763
      for (var entry : this) {
1✔
3764
        if (!collection.contains(entry) && remove(entry)) {
1✔
3765
          modified = true;
1✔
3766
        }
3767
      }
1✔
3768
      return modified;
1✔
3769
    }
3770

3771
    @Override
3772
    public Iterator<Entry<K, V>> iterator() {
3773
      return new EntryIterator<>(cache);
1✔
3774
    }
3775

3776
    @Override
3777
    public Spliterator<Entry<K, V>> spliterator() {
3778
      return new EntrySpliterator<>(cache);
1✔
3779
    }
3780
  }
3781

3782
  /** An adapter to safely externalize the entry iterator. */
3783
  static final class EntryIterator<K, V> implements Iterator<Entry<K, V>> {
3784
    final BoundedLocalCache<K, V> cache;
3785
    final Iterator<Node<K, V>> iterator;
3786

3787
    @Nullable K key;
3788
    @Nullable V value;
3789
    @Nullable K removalKey;
3790
    @Nullable Node<K, V> next;
3791

3792
    EntryIterator(BoundedLocalCache<K, V> cache) {
1✔
3793
      this.iterator = cache.data.values().iterator();
1✔
3794
      this.cache = cache;
1✔
3795
    }
1✔
3796

3797
    @Override
3798
    public boolean hasNext() {
3799
      if (next != null) {
1✔
3800
        return true;
1✔
3801
      }
3802

3803
      long now = cache.expirationTicker().read();
1✔
3804
      while (iterator.hasNext()) {
1✔
3805
        next = iterator.next();
1✔
3806
        value = next.getValue();
1✔
3807
        key = next.getKey();
1✔
3808

3809
        boolean evictable = (key == null) || (value == null) || cache.hasExpired(next, now);
1✔
3810
        if (evictable || !next.isAlive()) {
1✔
3811
          if (evictable) {
1✔
3812
            cache.scheduleDrainBuffers();
1✔
3813
          }
3814
          advance();
1✔
3815
          continue;
1✔
3816
        }
3817
        return true;
1✔
3818
      }
3819
      return false;
1✔
3820
    }
3821

3822
    /** Invalidates the current position so that the iterator may compute the next position. */
3823
    void advance() {
3824
      value = null;
1✔
3825
      next = null;
1✔
3826
      key = null;
1✔
3827
    }
1✔
3828

3829
    K nextKey() {
3830
      if (!hasNext()) {
1✔
3831
        throw new NoSuchElementException();
1✔
3832
      }
3833
      removalKey = key;
1✔
3834
      advance();
1✔
3835
      return requireNonNull(removalKey);
1✔
3836
    }
3837

3838
    V nextValue() {
3839
      if (!hasNext()) {
1✔
3840
        throw new NoSuchElementException();
1✔
3841
      }
3842
      removalKey = key;
1✔
3843
      V val = value;
1✔
3844
      advance();
1✔
3845
      return requireNonNull(val);
1✔
3846
    }
3847

3848
    @Override
3849
    public Entry<K, V> next() {
3850
      if (!hasNext()) {
1✔
3851
        throw new NoSuchElementException();
1✔
3852
      }
3853
      var entry = new WriteThroughEntry<K, @NonNull V>(
1✔
3854
          cache, requireNonNull(key), requireNonNull(value));
1✔
3855
      removalKey = key;
1✔
3856
      advance();
1✔
3857
      return entry;
1✔
3858
    }
3859

3860
    @Override
3861
    public void remove() {
3862
      if (removalKey == null) {
1✔
3863
        throw new IllegalStateException();
1✔
3864
      }
3865
      cache.remove(removalKey);
1✔
3866
      removalKey = null;
1✔
3867
    }
1✔
3868
  }
3869

3870
  /** An adapter to safely externalize the entry spliterator. */
3871
  static final class EntrySpliterator<K, V> implements Spliterator<Entry<K, V>> {
3872
    final Spliterator<Node<K, V>> spliterator;
3873
    final BoundedLocalCache<K, V> cache;
3874

3875
    EntrySpliterator(BoundedLocalCache<K, V> cache) {
3876
      this(cache, cache.data.values().spliterator());
1✔
3877
    }
1✔
3878

3879
    EntrySpliterator(BoundedLocalCache<K, V> cache, Spliterator<Node<K, V>> spliterator) {
1✔
3880
      this.spliterator = requireNonNull(spliterator);
1✔
3881
      this.cache = requireNonNull(cache);
1✔
3882
    }
1✔
3883

3884
    @Override
3885
    public void forEachRemaining(Consumer<? super Entry<K, V>> action) {
3886
      requireNonNull(action);
1✔
3887
      Consumer<Node<K, V>> consumer = node -> {
1✔
3888
        K key = node.getKey();
1✔
3889
        V value = node.getValue();
1✔
3890
        long now = cache.expirationTicker().read();
1✔
3891
        if ((key != null) && (value != null) && node.isAlive() && !cache.hasExpired(node, now)) {
1✔
3892
          action.accept(new WriteThroughEntry<>(cache, key, value));
1✔
3893
        }
3894
      };
1✔
3895
      spliterator.forEachRemaining(consumer);
1✔
3896
    }
1✔
3897

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

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

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

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

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

3940
    final WeakReference<BoundedLocalCache<?, ?>> reference;
3941

3942
    PerformCleanupTask(BoundedLocalCache<?, ?> cache) {
1✔
3943
      reference = new WeakReference<>(cache);
1✔
3944
    }
1✔
3945

3946
    @Override
3947
    public boolean exec() {
3948
      try {
3949
        run();
1✔
3950
      } catch (Throwable t) {
1✔
3951
        logger.log(Level.ERROR, "Exception thrown when performing the maintenance task", t);
1✔
3952
      }
1✔
3953

3954
      // Indicates that the task has not completed to allow subsequent submissions to execute
3955
      return false;
1✔
3956
    }
3957

3958
    @Override
3959
    public void run() {
3960
      BoundedLocalCache<?, ?> cache = reference.get();
1✔
3961
      if (cache != null) {
1✔
3962
        cache.performCleanUp(/* ignored */ null);
1✔
3963
      }
3964
    }
1✔
3965

3966
    /**
3967
     * This method cannot be ignored due to being final, so a hostile user supplied Executor could
3968
     * forcibly complete the task and halt future executions. There are easier ways to intentionally
3969
     * harm a system, so this is assumed to not happen in practice.
3970
     */
3971
    // public final void quietlyComplete() {}
3972

3973
    @Override public void complete(@Nullable Void value) {}
1✔
3974
    @Override public void setRawResult(@Nullable Void value) {}
1✔
3975
    @Override public @Nullable Void getRawResult() { return null; }
1✔
3976
    @Override public void completeExceptionally(@Nullable Throwable t) {}
1✔
3977
    @Override public boolean cancel(boolean mayInterruptIfRunning) { return false; }
1✔
3978
  }
3979

3980
  /** Creates a serialization proxy based on the common configuration shared by all cache types. */
3981
  static <K, V> SerializationProxy<K, V> makeSerializationProxy(BoundedLocalCache<?, ?> cache) {
3982
    var proxy = new SerializationProxy<K, V>();
1✔
3983
    proxy.weakKeys = cache.collectKeys();
1✔
3984
    proxy.weakValues = cache.nodeFactory.weakValues();
1✔
3985
    proxy.softValues = cache.nodeFactory.softValues();
1✔
3986
    proxy.isRecordingStats = cache.isRecordingStats();
1✔
3987
    proxy.evictionListener = cache.evictionListener;
1✔
3988
    proxy.removalListener = cache.removalListener();
1✔
3989
    proxy.ticker = cache.expirationTicker();
1✔
3990
    if (cache.expiresAfterAccess()) {
1✔
3991
      proxy.expiresAfterAccessNanos = cache.expiresAfterAccessNanos();
1✔
3992
    }
3993
    if (cache.expiresAfterWrite()) {
1✔
3994
      proxy.expiresAfterWriteNanos = cache.expiresAfterWriteNanos();
1✔
3995
    }
3996
    if (cache.expiresVariable()) {
1✔
3997
      proxy.expiry = cache.expiry();
1✔
3998
    }
3999
    if (cache.refreshAfterWrite()) {
1✔
4000
      proxy.refreshAfterWriteNanos = cache.refreshAfterWriteNanos();
1✔
4001
    }
4002
    if (cache.evicts()) {
1✔
4003
      if (cache.isWeighted) {
1✔
4004
        proxy.weigher = cache.weigher;
1✔
4005
        proxy.maximumWeight = cache.maximum();
1✔
4006
      } else {
4007
        proxy.maximumSize = cache.maximum();
1✔
4008
      }
4009
    }
4010
    proxy.cacheLoader = cache.cacheLoader;
1✔
4011
    proxy.async = cache.isAsync;
1✔
4012
    return proxy;
1✔
4013
  }
4014

4015
  /* --------------- Manual Cache --------------- */
4016

4017
  static class BoundedLocalManualCache<K, V> implements LocalManualCache<K, V>, Serializable {
4018
    private static final long serialVersionUID = 1;
4019

4020
    final BoundedLocalCache<K, V> cache;
4021

4022
    @Nullable Policy<K, V> policy;
4023

4024
    BoundedLocalManualCache(Caffeine<K, V> builder) {
4025
      this(builder, null);
1✔
4026
    }
1✔
4027

4028
    BoundedLocalManualCache(Caffeine<K, V> builder, @Nullable CacheLoader<? super K, V> loader) {
1✔
4029
      cache = LocalCacheFactory.newBoundedLocalCache(builder, loader, /* isAsync= */ false);
1✔
4030
    }
1✔
4031

4032
    @Override
4033
    public final BoundedLocalCache<K, V> cache() {
4034
      return cache;
1✔
4035
    }
4036

4037
    @Override
4038
    public final Policy<K, V> policy() {
4039
      if (policy == null) {
1✔
4040
        Function<@Nullable V, @Nullable V> identity = v -> v;
1✔
4041
        policy = new BoundedPolicy<>(cache, identity, cache.isWeighted);
1✔
4042
      }
4043
      return policy;
1✔
4044
    }
4045

4046
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
4047
      throw new InvalidObjectException("Proxy required");
1✔
4048
    }
4049

4050
    private Object writeReplace() {
4051
      return makeSerializationProxy(cache);
1✔
4052
    }
4053
  }
4054

4055
  @SuppressWarnings({"NullableOptional",
4056
    "OptionalAssignedToNull", "OptionalUsedAsFieldOrParameterType"})
4057
  static final class BoundedPolicy<K, V> implements Policy<K, V> {
4058
    final Function<@Nullable V, @Nullable V> transformer;
4059
    final BoundedLocalCache<K, V> cache;
4060
    final boolean isWeighted;
4061

4062
    @Nullable Optional<Eviction<K, V>> eviction;
4063
    @Nullable Optional<FixedRefresh<K, V>> refreshes;
4064
    @Nullable Optional<FixedExpiration<K, V>> afterWrite;
4065
    @Nullable Optional<FixedExpiration<K, V>> afterAccess;
4066
    @Nullable Optional<VarExpiration<K, V>> variable;
4067

4068
    BoundedPolicy(BoundedLocalCache<K, V> cache,
4069
        Function<@Nullable V, @Nullable V> transformer, boolean isWeighted) {
1✔
4070
      this.transformer = transformer;
1✔
4071
      this.isWeighted = isWeighted;
1✔
4072
      this.cache = cache;
1✔
4073
    }
1✔
4074

4075
    @Override public boolean isRecordingStats() {
4076
      return cache.isRecordingStats();
1✔
4077
    }
4078
    @Override public @Nullable V getIfPresentQuietly(K key) {
4079
      return transformer.apply(cache.getIfPresentQuietly(key));
1✔
4080
    }
4081
    @Override public @Nullable CacheEntry<K, V> getEntryIfPresentQuietly(K key) {
4082
      Node<K, V> node = cache.data.get(cache.nodeFactory.newLookupKey(key));
1✔
4083
      return (node == null) ? null : cache.nodeToCacheEntry(node, transformer);
1✔
4084
    }
4085
    @SuppressWarnings("Java9CollectionFactory")
4086
    @Override public Map<K, CompletableFuture<V>> refreshes() {
4087
      var refreshes = cache.refreshes;
1✔
4088
      if ((refreshes == null) || refreshes.isEmpty()) {
1✔
4089
        @SuppressWarnings({"ImmutableMapOf", "RedundantUnmodifiable"})
4090
        Map<K, CompletableFuture<V>> emptyMap = Collections.unmodifiableMap(Collections.emptyMap());
1✔
4091
        return emptyMap;
1✔
4092
      } else if (cache.collectKeys()) {
1✔
4093
        var inFlight = new IdentityHashMap<K, CompletableFuture<V>>(refreshes.size());
1✔
4094
        for (var entry : refreshes.entrySet()) {
1✔
4095
          @SuppressWarnings("unchecked")
4096
          @Nullable K key = ((InternalReference<K>) entry.getKey()).get();
1✔
4097
          @SuppressWarnings("unchecked")
4098
          var future = (CompletableFuture<V>) entry.getValue();
1✔
4099
          if (key != null) {
1✔
4100
            inFlight.put(key, future);
1✔
4101
          }
4102
        }
1✔
4103
        return Collections.unmodifiableMap(inFlight);
1✔
4104
      }
4105
      @SuppressWarnings("unchecked")
4106
      var castedRefreshes = (Map<K, CompletableFuture<V>>) (Object) refreshes;
1✔
4107
      return Collections.unmodifiableMap(new HashMap<>(castedRefreshes));
1✔
4108
    }
4109
    @Override public Optional<Eviction<K, V>> eviction() {
4110
      return cache.evicts()
1✔
4111
          ? (eviction == null) ? (eviction = Optional.of(new BoundedEviction())) : eviction
1✔
4112
          : Optional.empty();
1✔
4113
    }
4114
    @Override public Optional<FixedExpiration<K, V>> expireAfterAccess() {
4115
      if (!cache.expiresAfterAccess()) {
1✔
4116
        return Optional.empty();
1✔
4117
      }
4118
      return (afterAccess == null)
1✔
4119
          ? (afterAccess = Optional.of(new BoundedExpireAfterAccess()))
1✔
4120
          : afterAccess;
1✔
4121
    }
4122
    @Override public Optional<FixedExpiration<K, V>> expireAfterWrite() {
4123
      if (!cache.expiresAfterWrite()) {
1✔
4124
        return Optional.empty();
1✔
4125
      }
4126
      return (afterWrite == null)
1✔
4127
          ? (afterWrite = Optional.of(new BoundedExpireAfterWrite()))
1✔
4128
          : afterWrite;
1✔
4129
    }
4130
    @Override public Optional<VarExpiration<K, V>> expireVariably() {
4131
      if (!cache.expiresVariable()) {
1✔
4132
        return Optional.empty();
1✔
4133
      }
4134
      return (variable == null)
1✔
4135
          ? (variable = Optional.of(new BoundedVarExpiration()))
1✔
4136
          : variable;
1✔
4137
    }
4138
    @Override public Optional<FixedRefresh<K, V>> refreshAfterWrite() {
4139
      if (!cache.refreshAfterWrite()) {
1✔
4140
        return Optional.empty();
1✔
4141
      }
4142
      return (refreshes == null)
1✔
4143
          ? (refreshes = Optional.of(new BoundedRefreshAfterWrite()))
1✔
4144
          : refreshes;
1✔
4145
    }
4146

4147
    final class BoundedEviction implements Eviction<K, V> {
1✔
4148
      @Override public boolean isWeighted() {
4149
        return isWeighted;
1✔
4150
      }
4151
      @Override public OptionalInt weightOf(K key) {
4152
        requireNonNull(key);
1✔
4153
        if (!isWeighted) {
1✔
4154
          return OptionalInt.empty();
1✔
4155
        }
4156
        Node<K, V> node = cache.data.get(cache.nodeFactory.newLookupKey(key));
1✔
4157
        if ((node == null) || cache.hasExpired(node, cache.expirationTicker().read())) {
1✔
4158
          return OptionalInt.empty();
1✔
4159
        }
4160
        synchronized (node) {
1✔
4161
          return node.isAlive() ? OptionalInt.of(node.getWeight()) : OptionalInt.empty();
1✔
4162
        }
4163
      }
4164
      @Override public OptionalLong weightedSize() {
4165
        return isWeighted
1✔
4166
            ? OptionalLong.of(Math.max(0, cache.weightedSizeAcquire()))
1✔
4167
            : OptionalLong.empty();
1✔
4168
      }
4169
      @Override public long getMaximum() {
4170
        return cache.maximumAcquire();
1✔
4171
      }
4172
      @Override public void setMaximum(long maximum) {
4173
        cache.evictionLock.lock();
1✔
4174
        try {
4175
          cache.setMaximumSize(maximum);
1✔
4176
          cache.maintenance(/* ignored */ null);
1✔
4177
        } finally {
4178
          cache.evictionLock.unlock();
1✔
4179
          cache.rescheduleCleanUpIfIncomplete();
1✔
4180
        }
4181
      }
1✔
4182
      @Override public Map<K, V> coldest(int limit) {
4183
        int expectedSize = Math.min(limit, cache.size());
1✔
4184
        var limiter = new SizeLimiter<K, V>(expectedSize, limit);
1✔
4185
        return cache.evictionOrder(/* hottest= */ false, transformer, limiter);
1✔
4186
      }
4187
      @Override public Map<K, V> coldestWeighted(long weightLimit) {
4188
        var limiter = isWeighted()
1✔
4189
            ? new WeightLimiter<K, V>(weightLimit)
1✔
4190
            : new SizeLimiter<K, V>((int) Math.min(weightLimit, cache.size()), weightLimit);
1✔
4191
        return cache.evictionOrder(/* hottest= */ false, transformer, limiter);
1✔
4192
      }
4193
      @Override
4194
      public <T> T coldest(Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4195
        requireNonNull(mappingFunction);
1✔
4196
        return cache.evictionOrder(/* hottest= */ false, transformer, mappingFunction);
1✔
4197
      }
4198
      @Override public Map<K, V> hottest(int limit) {
4199
        int expectedSize = Math.min(limit, cache.size());
1✔
4200
        var limiter = new SizeLimiter<K, V>(expectedSize, limit);
1✔
4201
        return cache.evictionOrder(/* hottest= */ true, transformer, limiter);
1✔
4202
      }
4203
      @Override public Map<K, V> hottestWeighted(long weightLimit) {
4204
        var limiter = isWeighted()
1✔
4205
            ? new WeightLimiter<K, V>(weightLimit)
1✔
4206
            : new SizeLimiter<K, V>((int) Math.min(weightLimit, cache.size()), weightLimit);
1✔
4207
        return cache.evictionOrder(/* hottest= */ true, transformer, limiter);
1✔
4208
      }
4209
      @Override
4210
      public <T> T hottest(Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4211
        requireNonNull(mappingFunction);
1✔
4212
        return cache.evictionOrder(/* hottest= */ true, transformer, mappingFunction);
1✔
4213
      }
4214
    }
4215

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

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

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

4353
        for (;;) {
4354
          var priorFuture = (CompletableFuture<V>) cache.getIfPresent(
1✔
4355
              key, /* recordStats= */ false);
4356
          if (priorFuture != null) {
1✔
4357
            if (!priorFuture.isDone()) {
1✔
4358
              Async.getWhenSuccessful(priorFuture);
1✔
4359
              continue;
1✔
4360
            }
4361

4362
            V prior = Async.getWhenSuccessful(priorFuture);
1✔
4363
            if (prior != null) {
1✔
4364
              return prior;
1✔
4365
            }
4366
          }
4367

4368
          boolean[] added = { false };
1✔
4369
          var computed = (CompletableFuture<V>) cache.compute(key, (K k, @Nullable V oldValue) -> {
1✔
4370
            var oldValueFuture = (CompletableFuture<V>) oldValue;
1✔
4371
            added[0] = (oldValueFuture == null)
1✔
4372
                || (oldValueFuture.isDone() && (Async.getIfReady(oldValueFuture) == null));
1✔
4373
            return added[0] ? asyncValue : oldValue;
1✔
4374
          }, expiry, /* recordLoad= */ false, /* recordLoadFailure= */ false);
4375

4376
          if (added[0]) {
1✔
4377
            return null;
1✔
4378
          } else {
4379
            V prior = Async.getWhenSuccessful(computed);
1✔
4380
            if (prior != null) {
1✔
4381
              return prior;
1✔
4382
            }
4383
          }
4384
        }
1✔
4385
      }
4386
      @SuppressWarnings("unchecked")
4387
      @Nullable V putAsync(K key, V value, long duration, TimeUnit unit) {
4388
        var expiry = (Expiry<K, V>) new AsyncExpiry<>(new FixedExpireAfterWrite<>(duration, unit));
1✔
4389
        var asyncValue = (V) CompletableFuture.completedFuture(value);
1✔
4390

4391
        var oldValueFuture = (CompletableFuture<V>) cache.put(
1✔
4392
            key, asyncValue, expiry, /* onlyIfAbsent= */ false);
4393
        return Async.getWhenSuccessful(oldValueFuture);
1✔
4394
      }
4395
      @Override public @Nullable V compute(K key,
4396
          BiFunction<? super K, ? super V, ? extends V> remappingFunction,
4397
          Duration duration) {
4398
        requireNonNull(key);
1✔
4399
        requireNonNull(duration);
1✔
4400
        requireNonNull(remappingFunction);
1✔
4401
        requireArgument(!duration.isNegative(), "duration cannot be negative: %s", duration);
1✔
4402
        var expiry = new FixedExpireAfterWrite<K, V>(
1✔
4403
            toNanosSaturated(duration), TimeUnit.NANOSECONDS);
1✔
4404

4405
        return cache.isAsync
1✔
4406
            ? computeAsync(key, remappingFunction, expiry)
1✔
4407
            : cache.compute(key, remappingFunction, expiry,
1✔
4408
                /* recordLoad= */ true, /* recordLoadFailure= */ true);
4409
      }
4410
      @Nullable V computeAsync(K key,
4411
          BiFunction<? super K, ? super V, ? extends V> remappingFunction,
4412
          Expiry<? super K, ? super V> expiry) {
4413
        // Keep in sync with LocalAsyncCache.AsMapView#compute(key, remappingFunction)
4414
        @SuppressWarnings("unchecked")
4415
        var delegate = (LocalCache<K, CompletableFuture<V>>) cache;
1✔
4416

4417
        @SuppressWarnings({"rawtypes", "unchecked", "Varifier"})
4418
        @Nullable V[] newValue = (@Nullable V[]) new Object[1];
1✔
4419
        for (;;) {
4420
          Async.getWhenSuccessful(delegate.getIfPresentQuietly(key));
1✔
4421

4422
          CompletableFuture<V> valueFuture = delegate.compute(
1✔
4423
              key, (K k, @Nullable CompletableFuture<V> oldValueFuture) -> {
4424
                if ((oldValueFuture != null) && !oldValueFuture.isDone()) {
1✔
4425
                  return oldValueFuture;
1✔
4426
                }
4427

4428
                V oldValue = Async.getIfReady(oldValueFuture);
1✔
4429
                BiFunction<? super K, ? super V, ? extends @Nullable V> function =
1✔
4430
                    delegate.statsAware(remappingFunction,
1✔
4431
                        /* recordLoad= */ true, /* recordLoadFailure= */ true);
4432
                newValue[0] = function.apply(key, oldValue);
1✔
4433
                return (newValue[0] == null) ? null
1✔
4434
                    : CompletableFuture.completedFuture(newValue[0]);
1✔
4435
              }, new AsyncExpiry<>(expiry), /* recordLoad= */ false,
4436
              /* recordLoadFailure= */ false);
4437

4438
          if (newValue[0] != null) {
1✔
4439
            return newValue[0];
1✔
4440
          } else if (valueFuture == null) {
1✔
4441
            return null;
1✔
4442
          }
4443
        }
1✔
4444
      }
4445
      @Override public Map<K, V> oldest(int limit) {
4446
        return oldest(new SizeLimiter<>(Math.min(limit, cache.size()), limit));
1✔
4447
      }
4448
      @Override public <T> T oldest(Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4449
        return cache.snapshot(cache.timerWheel(), transformer, mappingFunction);
1✔
4450
      }
4451
      @Override public Map<K, V> youngest(int limit) {
4452
        return youngest(new SizeLimiter<>(Math.min(limit, cache.size()), limit));
1✔
4453
      }
4454
      @Override public <T> T youngest(Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4455
        return cache.snapshot(cache.timerWheel()::descendingIterator, transformer, mappingFunction);
1✔
4456
      }
4457
    }
4458

4459
    static final class FixedExpireAfterWrite<K, V> implements Expiry<K, V> {
4460
      final long duration;
4461
      final TimeUnit unit;
4462

4463
      FixedExpireAfterWrite(long duration, TimeUnit unit) {
1✔
4464
        this.duration = duration;
1✔
4465
        this.unit = unit;
1✔
4466
      }
1✔
4467
      @Override public long expireAfterCreate(K key, V value, long currentTime) {
4468
        return unit.toNanos(duration);
1✔
4469
      }
4470
      @Override public long expireAfterUpdate(
4471
          K key, V value, long currentTime, long currentDuration) {
4472
        return unit.toNanos(duration);
1✔
4473
      }
4474
      @CanIgnoreReturnValue
4475
      @Override public long expireAfterRead(
4476
          K key, V value, long currentTime, long currentDuration) {
4477
        return currentDuration;
1✔
4478
      }
4479
    }
4480

4481
    @SuppressWarnings("PreferJavaTimeOverload")
4482
    final class BoundedRefreshAfterWrite implements FixedRefresh<K, V> {
1✔
4483
      @Override public OptionalLong ageOf(K key, TimeUnit unit) {
4484
        requireNonNull(key);
1✔
4485
        requireNonNull(unit);
1✔
4486
        Object lookupKey = cache.nodeFactory.newLookupKey(key);
1✔
4487
        Node<K, V> node = cache.data.get(lookupKey);
1✔
4488
        if (node == null) {
1✔
4489
          return OptionalLong.empty();
1✔
4490
        }
4491
        long now = cache.expirationTicker().read();
1✔
4492
        return cache.hasExpired(node, now)
1✔
4493
            ? OptionalLong.empty()
1✔
4494
            : OptionalLong.of(unit.convert(now - node.getWriteTime(), TimeUnit.NANOSECONDS));
1✔
4495
      }
4496
      @Override public long getRefreshesAfter(TimeUnit unit) {
4497
        return unit.convert(cache.refreshAfterWriteNanos(), TimeUnit.NANOSECONDS);
1✔
4498
      }
4499
      @Override public void setRefreshesAfter(long duration, TimeUnit unit) {
4500
        requireArgument(duration >= 0);
1✔
4501
        cache.setRefreshAfterWriteNanos(unit.toNanos(duration));
1✔
4502
        cache.scheduleAfterWrite();
1✔
4503
      }
1✔
4504
    }
4505
  }
4506

4507
  /* --------------- Loading Cache --------------- */
4508

4509
  static final class BoundedLocalLoadingCache<K, V>
4510
      extends BoundedLocalManualCache<K, V> implements LocalLoadingCache<K, V> {
4511
    private static final long serialVersionUID = 1;
4512

4513
    final Function<K, @Nullable V> mappingFunction;
4514
    final @Nullable Function<Set<? extends K>, Map<K, V>> bulkMappingFunction;
4515

4516
    BoundedLocalLoadingCache(Caffeine<K, V> builder, CacheLoader<? super K, V> loader) {
4517
      super(builder, loader);
1✔
4518
      requireNonNull(loader);
1✔
4519
      mappingFunction = newMappingFunction(loader);
1✔
4520
      bulkMappingFunction = newBulkMappingFunction(loader);
1✔
4521
    }
1✔
4522

4523
    @Override
4524
    @SuppressWarnings({"DataFlowIssue", "NullAway"})
4525
    public AsyncCacheLoader<? super K, V> cacheLoader() {
4526
      return cache.cacheLoader;
1✔
4527
    }
4528

4529
    @Override
4530
    public Function<K, @Nullable V> mappingFunction() {
4531
      return mappingFunction;
1✔
4532
    }
4533

4534
    @Override
4535
    public @Nullable Function<Set<? extends K>, Map<K, V>> bulkMappingFunction() {
4536
      return bulkMappingFunction;
1✔
4537
    }
4538

4539
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
4540
      throw new InvalidObjectException("Proxy required");
1✔
4541
    }
4542

4543
    private Object writeReplace() {
4544
      return makeSerializationProxy(cache);
1✔
4545
    }
4546
  }
4547

4548
  /* --------------- Async Cache --------------- */
4549

4550
  static final class BoundedLocalAsyncCache<K, V> implements LocalAsyncCache<K, V>, Serializable {
4551
    private static final long serialVersionUID = 1;
4552

4553
    final BoundedLocalCache<K, CompletableFuture<V>> cache;
4554
    final boolean isWeighted;
4555

4556
    @Nullable ConcurrentMap<K, CompletableFuture<V>> mapView;
4557
    @Nullable CacheView<K, V> cacheView;
4558
    @Nullable Policy<K, V> policy;
4559

4560
    @SuppressWarnings("unchecked")
4561
    BoundedLocalAsyncCache(Caffeine<K, V> builder) {
1✔
4562
      cache = (BoundedLocalCache<K, CompletableFuture<V>>) LocalCacheFactory
1✔
4563
          .newBoundedLocalCache(builder, /* cacheLoader= */ null, /* isAsync= */ true);
1✔
4564
      isWeighted = builder.isWeighted();
1✔
4565
    }
1✔
4566

4567
    @Override
4568
    public BoundedLocalCache<K, CompletableFuture<V>> cache() {
4569
      return cache;
1✔
4570
    }
4571

4572
    @Override
4573
    public ConcurrentMap<K, CompletableFuture<V>> asMap() {
4574
      return (mapView == null) ? (mapView = new AsyncAsMapView<>(this)) : mapView;
1✔
4575
    }
4576

4577
    @Override
4578
    public Cache<K, V> synchronous() {
4579
      return (cacheView == null) ? (cacheView = new CacheView<>(this)) : cacheView;
1✔
4580
    }
4581

4582
    @Override
4583
    public Policy<K, V> policy() {
4584
      if (policy == null) {
1✔
4585
        @SuppressWarnings("unchecked")
4586
        var castCache = (BoundedLocalCache<K, V>) cache;
1✔
4587
        Function<CompletableFuture<V>, @Nullable V> transformer = Async::getIfReady;
1✔
4588
        @SuppressWarnings("unchecked")
4589
        var castTransformer = (Function<@Nullable V, @Nullable V>) transformer;
1✔
4590
        policy = new BoundedPolicy<>(castCache, castTransformer, isWeighted);
1✔
4591
      }
4592
      return policy;
1✔
4593
    }
4594

4595
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
4596
      throw new InvalidObjectException("Proxy required");
1✔
4597
    }
4598

4599
    private Object writeReplace() {
4600
      return makeSerializationProxy(cache);
1✔
4601
    }
4602
  }
4603

4604
  /* --------------- Async Loading Cache --------------- */
4605

4606
  static final class BoundedLocalAsyncLoadingCache<K, V>
4607
      extends LocalAsyncLoadingCache<K, V> implements Serializable {
4608
    private static final long serialVersionUID = 1;
4609

4610
    final BoundedLocalCache<K, CompletableFuture<V>> cache;
4611
    final boolean isWeighted;
4612

4613
    @Nullable ConcurrentMap<K, CompletableFuture<V>> mapView;
4614
    @Nullable Policy<K, V> policy;
4615

4616
    @SuppressWarnings("unchecked")
4617
    BoundedLocalAsyncLoadingCache(Caffeine<K, V> builder, AsyncCacheLoader<? super K, V> loader) {
4618
      super(loader);
1✔
4619
      isWeighted = builder.isWeighted();
1✔
4620
      cache = (BoundedLocalCache<K, CompletableFuture<V>>) LocalCacheFactory
1✔
4621
          .newBoundedLocalCache(builder, loader, /* isAsync= */ true);
1✔
4622
    }
1✔
4623

4624
    @Override
4625
    public BoundedLocalCache<K, CompletableFuture<V>> cache() {
4626
      return cache;
1✔
4627
    }
4628

4629
    @Override
4630
    public ConcurrentMap<K, CompletableFuture<V>> asMap() {
4631
      return (mapView == null) ? (mapView = new AsyncAsMapView<>(this)) : mapView;
1✔
4632
    }
4633

4634
    @Override
4635
    public Policy<K, V> policy() {
4636
      if (policy == null) {
1✔
4637
        @SuppressWarnings("unchecked")
4638
        var castCache = (BoundedLocalCache<K, V>) cache;
1✔
4639
        Function<CompletableFuture<V>, @Nullable V> transformer = Async::getIfReady;
1✔
4640
        @SuppressWarnings("unchecked")
4641
        var castTransformer = (Function<@Nullable V, @Nullable V>) transformer;
1✔
4642
        policy = new BoundedPolicy<>(castCache, castTransformer, isWeighted);
1✔
4643
      }
4644
      return policy;
1✔
4645
    }
4646

4647
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
4648
      throw new InvalidObjectException("Proxy required");
1✔
4649
    }
4650

4651
    private Object writeReplace() {
4652
      return makeSerializationProxy(cache);
1✔
4653
    }
4654
  }
4655
}
4656

4657
/** The namespace for field padding through inheritance. */
4658
@SuppressWarnings({"IdentifierName", "MultiVariableDeclaration"})
4659
final class BLCHeader {
4660

4661
  private BLCHeader() {}
4662

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

4682
  /** Enforces a memory layout to avoid false sharing by padding the drain status. */
4683
  abstract static class DrainStatusRef extends PadDrainStatus {
1✔
4684
    static final VarHandle DRAIN_STATUS = findVarHandle(
1✔
4685
        DrainStatusRef.class, "drainStatus", int.class);
4686

4687
    /** A drain is not taking place. */
4688
    static final int IDLE = 0;
4689
    /** A drain is required due to a pending write modification. */
4690
    static final int REQUIRED = 1;
4691
    /** A drain is in progress and will transition to idle. */
4692
    static final int PROCESSING_TO_IDLE = 2;
4693
    /** A drain is in progress and will transition to required. */
4694
    static final int PROCESSING_TO_REQUIRED = 3;
4695

4696
    /** The draining status of the buffers. */
4697
    volatile int drainStatus = IDLE;
1✔
4698

4699
    /**
4700
     * Returns whether maintenance work is needed.
4701
     *
4702
     * @param delayable if draining the read buffer can be delayed
4703
     */
4704
    @SuppressWarnings("StatementSwitchToExpressionSwitch")
4705
    boolean shouldDrainBuffers(boolean delayable) {
4706
      switch (drainStatusOpaque()) {
1✔
4707
        case IDLE:
4708
          return !delayable;
1✔
4709
        case REQUIRED:
4710
          return true;
1✔
4711
        case PROCESSING_TO_IDLE:
4712
        case PROCESSING_TO_REQUIRED:
4713
          return false;
1✔
4714
        default:
4715
          throw new IllegalStateException("Invalid drain status: " + drainStatus);
1✔
4716
      }
4717
    }
4718

4719
    int drainStatusOpaque() {
4720
      return (int) DRAIN_STATUS.getOpaque(this);
1✔
4721
    }
4722

4723
    int drainStatusAcquire() {
4724
      return (int) DRAIN_STATUS.getAcquire(this);
1✔
4725
    }
4726

4727
    void setDrainStatusOpaque(int drainStatus) {
4728
      DRAIN_STATUS.setOpaque(this, drainStatus);
1✔
4729
    }
1✔
4730

4731
    void setDrainStatusRelease(int drainStatus) {
4732
      DRAIN_STATUS.setRelease(this, drainStatus);
1✔
4733
    }
1✔
4734

4735
    boolean casDrainStatus(int expect, int update) {
4736
      return DRAIN_STATUS.compareAndSet(this, expect, update);
1✔
4737
    }
4738

4739
    static VarHandle findVarHandle(Class<?> recv, String name, Class<?> type) {
4740
      try {
4741
        return MethodHandles.lookup().findVarHandle(recv, name, type);
1✔
4742
      } catch (ReflectiveOperationException e) {
1✔
4743
        throw new ExceptionInInitializerError(e);
1✔
4744
      }
4745
    }
4746
  }
4747
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc