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

ben-manes / caffeine / #5292

13 Feb 2026 05:35AM UTC coverage: 99.987% (-0.01%) from 100.0%
#5292

push

github

ben-manes
perform test sharding at discovery time

3830 of 3838 branches covered (99.79%)

7869 of 7870 relevant lines covered (99.99%)

1.0 hits per line

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

99.95
/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.ConcurrentHashMap;
69
import java.util.concurrent.ConcurrentMap;
70
import java.util.concurrent.Executor;
71
import java.util.concurrent.ForkJoinPool;
72
import java.util.concurrent.ForkJoinTask;
73
import java.util.concurrent.ThreadLocalRandom;
74
import java.util.concurrent.TimeUnit;
75
import java.util.concurrent.TimeoutException;
76
import java.util.concurrent.locks.ReentrantLock;
77
import java.util.function.BiConsumer;
78
import java.util.function.BiFunction;
79
import java.util.function.Consumer;
80
import java.util.function.Function;
81
import java.util.function.Predicate;
82
import java.util.stream.Stream;
83
import java.util.stream.StreamSupport;
84

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

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

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

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

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

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

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

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

253
  final boolean isWeighted;
254
  final boolean isAsync;
255

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

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

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

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

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

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

310
  /* --------------- Shared --------------- */
311

312
  @Override
313
  public boolean isAsync() {
314
    return isAsync;
1✔
315
  }
316

317
  /** Returns if the node's value is currently being computed asynchronously. */
318
  final boolean isComputingAsync(@Nullable V value) {
319
    return isAsync && !Async.isReady((CompletableFuture<?>) value);
1✔
320
  }
321

322
  @GuardedBy("evictionLock")
323
  protected AccessOrderDeque<Node<K, V>> accessOrderWindowDeque() {
324
    throw new UnsupportedOperationException();
1✔
325
  }
326

327
  @GuardedBy("evictionLock")
328
  protected AccessOrderDeque<Node<K, V>> accessOrderProbationDeque() {
329
    throw new UnsupportedOperationException();
1✔
330
  }
331

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

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

342
  @Override
343
  public final Executor executor() {
344
    return executor;
1✔
345
  }
346

347
  @Override
348
  public ConcurrentMap<Object, CompletableFuture<?>> refreshes() {
349
    @Var var pending = refreshes;
1✔
350
    if (pending == null) {
1✔
351
      pending = new ConcurrentHashMap<>();
1✔
352
      if (!REFRESHES.compareAndSet(this, null, pending)) {
1✔
353
        pending = requireNonNull(refreshes);
×
354
      }
355
    }
356
    return pending;
1✔
357
  }
358

359
  /** Invalidate the in-flight refresh. */
360
  @SuppressWarnings("RedundantCollectionOperation")
361
  void discardRefresh(Object keyReference) {
362
    var pending = refreshes;
1✔
363
    if ((pending != null) && pending.containsKey(keyReference)) {
1✔
364
      pending.remove(keyReference);
1✔
365
    }
366
  }
1✔
367

368
  @Override
369
  public Object referenceKey(K key) {
370
    return nodeFactory.newLookupKey(key);
1✔
371
  }
372

373
  @Override
374
  public boolean isPendingEviction(K key) {
375
    Node<K, V> node = data.get(nodeFactory.newLookupKey(key));
1✔
376
    return (node != null)
1✔
377
        && ((node.getValue() == null) || hasExpired(node, expirationTicker().read()));
1✔
378
  }
379

380
  /* --------------- Stats Support --------------- */
381

382
  @Override
383
  public boolean isRecordingStats() {
384
    return false;
1✔
385
  }
386

387
  @Override
388
  public StatsCounter statsCounter() {
389
    return StatsCounter.disabledStatsCounter();
1✔
390
  }
391

392
  @Override
393
  public Ticker statsTicker() {
394
    return Ticker.disabledTicker();
1✔
395
  }
396

397
  /* --------------- Removal Listener Support --------------- */
398

399
  protected @Nullable RemovalListener<K, V> removalListener() {
400
    return null;
1✔
401
  }
402

403
  @Override
404
  public void notifyRemoval(@Nullable K key, @Nullable V value, RemovalCause cause) {
405
    var removalListener = removalListener();
1✔
406
    if (removalListener == null) {
1✔
407
      return;
1✔
408
    }
409
    Runnable task = () -> {
1✔
410
      try {
411
        removalListener.onRemoval(key, value, cause);
1✔
412
      } catch (Throwable t) {
1✔
413
        logger.log(Level.WARNING, "Exception thrown by removal listener", t);
1✔
414
      }
1✔
415
    };
1✔
416
    try {
417
      executor.execute(task);
1✔
418
    } catch (Throwable t) {
1✔
419
      logger.log(Level.ERROR, "Exception thrown when submitting removal listener", t);
1✔
420
      task.run();
1✔
421
    }
1✔
422
  }
1✔
423

424
  /* --------------- Eviction Listener Support --------------- */
425

426
  void notifyEviction(@Nullable K key, @Nullable V value, RemovalCause cause) {
427
    if (evictionListener == null) {
1✔
428
      return;
1✔
429
    }
430
    try {
431
      evictionListener.onRemoval(key, value, cause);
1✔
432
    } catch (Throwable t) {
1✔
433
      logger.log(Level.WARNING, "Exception thrown by eviction listener", t);
1✔
434
    }
1✔
435
  }
1✔
436

437
  /* --------------- Reference Support --------------- */
438

439
  /** Returns if the keys are weak reference garbage collected. */
440
  protected boolean collectKeys() {
441
    return false;
1✔
442
  }
443

444
  /** Returns if the values are weak or soft reference garbage collected. */
445
  protected boolean collectValues() {
446
    return false;
1✔
447
  }
448

449
  @SuppressWarnings({"DataFlowIssue", "NullAway"})
450
  protected ReferenceQueue<K> keyReferenceQueue() {
451
    return null;
1✔
452
  }
453

454
  @SuppressWarnings({"DataFlowIssue", "NullAway"})
455
  protected ReferenceQueue<V> valueReferenceQueue() {
456
    return null;
1✔
457
  }
458

459
  /* --------------- Expiration Support --------------- */
460

461
  /** Returns the {@link Pacer} used to schedule the maintenance task. */
462
  protected @Nullable Pacer pacer() {
463
    return null;
1✔
464
  }
465

466
  /** Returns if the cache expires entries after a variable time threshold. */
467
  protected boolean expiresVariable() {
468
    return false;
1✔
469
  }
470

471
  /** Returns if the cache expires entries after an access time threshold. */
472
  protected boolean expiresAfterAccess() {
473
    return false;
1✔
474
  }
475

476
  /** Returns how long after the last access to an entry the map will retain that entry. */
477
  protected long expiresAfterAccessNanos() {
478
    throw new UnsupportedOperationException();
1✔
479
  }
480

481
  protected void setExpiresAfterAccessNanos(long expireAfterAccessNanos) {
482
    throw new UnsupportedOperationException();
1✔
483
  }
484

485
  /** Returns if the cache expires entries after a write time threshold. */
486
  protected boolean expiresAfterWrite() {
487
    return false;
1✔
488
  }
489

490
  /** Returns how long after the last write to an entry the map will retain that entry. */
491
  protected long expiresAfterWriteNanos() {
492
    throw new UnsupportedOperationException();
1✔
493
  }
494

495
  protected void setExpiresAfterWriteNanos(long expireAfterWriteNanos) {
496
    throw new UnsupportedOperationException();
1✔
497
  }
498

499
  /** Returns if the cache refreshes entries after a write time threshold. */
500
  protected boolean refreshAfterWrite() {
501
    return false;
1✔
502
  }
503

504
  /** Returns how long after the last write an entry becomes a candidate for refresh. */
505
  protected long refreshAfterWriteNanos() {
506
    throw new UnsupportedOperationException();
1✔
507
  }
508

509
  protected void setRefreshAfterWriteNanos(long refreshAfterWriteNanos) {
510
    throw new UnsupportedOperationException();
1✔
511
  }
512

513
  @Override
514
  @SuppressWarnings({"DataFlowIssue", "NullAway"})
515
  public Expiry<K, V> expiry() {
516
    return null;
1✔
517
  }
518

519
  /** Returns the {@link Ticker} used by this cache for expiration. */
520
  public Ticker expirationTicker() {
521
    return Ticker.disabledTicker();
1✔
522
  }
523

524
  protected TimerWheel<K, V> timerWheel() {
525
    throw new UnsupportedOperationException();
1✔
526
  }
527

528
  /* --------------- Eviction Support --------------- */
529

530
  /** Returns if the cache evicts entries due to a maximum size or weight threshold. */
531
  protected boolean evicts() {
532
    return false;
1✔
533
  }
534

535
  /** Returns if entries may be assigned different weights. */
536
  protected boolean isWeighted() {
537
    return (weigher != Weigher.singletonWeigher());
1✔
538
  }
539

540
  protected FrequencySketch frequencySketch() {
541
    throw new UnsupportedOperationException();
1✔
542
  }
543

544
  /** Returns if an access to an entry can skip notifying the eviction policy. */
545
  protected boolean fastpath() {
546
    return false;
1✔
547
  }
548

549
  /** Returns the maximum weighted size. */
550
  protected long maximum() {
551
    throw new UnsupportedOperationException();
1✔
552
  }
553

554
  /** Returns the maximum weighted size. */
555
  protected long maximumAcquire() {
556
    throw new UnsupportedOperationException();
1✔
557
  }
558

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

564
  /** Returns the maximum weighted size of the main's protected space. */
565
  protected long mainProtectedMaximum() {
566
    throw new UnsupportedOperationException();
1✔
567
  }
568

569
  @GuardedBy("evictionLock")
570
  protected void setMaximum(long maximum) {
571
    throw new UnsupportedOperationException();
1✔
572
  }
573

574
  @GuardedBy("evictionLock")
575
  protected void setWindowMaximum(long maximum) {
576
    throw new UnsupportedOperationException();
1✔
577
  }
578

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

584
  /** Returns the combined weight of the values in the cache (may be negative). */
585
  protected long weightedSize() {
586
    throw new UnsupportedOperationException();
1✔
587
  }
588

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

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

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

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

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

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

619
  protected int hitsInSample() {
620
    throw new UnsupportedOperationException();
1✔
621
  }
622

623
  protected int missesInSample() {
624
    throw new UnsupportedOperationException();
1✔
625
  }
626

627
  protected double stepSize() {
628
    throw new UnsupportedOperationException();
1✔
629
  }
630

631
  protected double previousSampleHitRate() {
632
    throw new UnsupportedOperationException();
1✔
633
  }
634

635
  protected long adjustment() {
636
    throw new UnsupportedOperationException();
1✔
637
  }
638

639
  @GuardedBy("evictionLock")
640
  protected void setHitsInSample(int hitCount) {
641
    throw new UnsupportedOperationException();
1✔
642
  }
643

644
  @GuardedBy("evictionLock")
645
  protected void setMissesInSample(int missCount) {
646
    throw new UnsupportedOperationException();
1✔
647
  }
648

649
  @GuardedBy("evictionLock")
650
  protected void setStepSize(double stepSize) {
651
    throw new UnsupportedOperationException();
1✔
652
  }
653

654
  @GuardedBy("evictionLock")
655
  protected void setPreviousSampleHitRate(double hitRate) {
656
    throw new UnsupportedOperationException();
1✔
657
  }
658

659
  @GuardedBy("evictionLock")
660
  protected void setAdjustment(long amount) {
661
    throw new UnsupportedOperationException();
1✔
662
  }
663

664
  /**
665
   * Sets the maximum weighted size of the cache. The caller may need to perform a maintenance cycle
666
   * to eagerly evicts entries until the cache shrinks to the appropriate size.
667
   */
668
  @GuardedBy("evictionLock")
669
  @SuppressWarnings({"ConstantValue", "Varifier"})
670
  void setMaximumSize(long maximum) {
671
    requireArgument(maximum >= 0, "maximum must not be negative");
1✔
672
    if (maximum == maximum()) {
1✔
673
      return;
1✔
674
    }
675

676
    long max = Math.min(maximum, MAXIMUM_CAPACITY);
1✔
677
    long window = max - (long) (PERCENT_MAIN * max);
1✔
678
    long mainProtected = (long) (PERCENT_MAIN_PROTECTED * (max - window));
1✔
679

680
    setMaximum(max);
1✔
681
    setWindowMaximum(window);
1✔
682
    setMainProtectedMaximum(mainProtected);
1✔
683

684
    setHitsInSample(0);
1✔
685
    setMissesInSample(0);
1✔
686
    setStepSize(-HILL_CLIMBER_STEP_PERCENT * max);
1✔
687

688
    if ((frequencySketch() != null) && !isWeighted() && (weightedSize() >= (max >>> 1))) {
1✔
689
      // Lazily initialize when close to the maximum size
690
      frequencySketch().ensureCapacity(max);
1✔
691
    }
692
  }
1✔
693

694
  /** Evicts entries if the cache exceeds the maximum. */
695
  @GuardedBy("evictionLock")
696
  void evictEntries() {
697
    if (!evicts()) {
1✔
698
      return;
1✔
699
    }
700
    var candidate = evictFromWindow();
1✔
701
    evictFromMain(candidate);
1✔
702
  }
1✔
703

704
  /**
705
   * Evicts entries from the window space into the main space while the window size exceeds a
706
   * maximum.
707
   *
708
   * @return the first candidate promoted into the probation space
709
   */
710
  @GuardedBy("evictionLock")
711
  @Nullable Node<K, V> evictFromWindow() {
712
    @Var Node<K, V> first = null;
1✔
713
    @Var Node<K, V> node = accessOrderWindowDeque().peekFirst();
1✔
714
    while (windowWeightedSize() > windowMaximum()) {
1✔
715
      // The pending operations will adjust the size to reflect the correct weight
716
      if (node == null) {
1✔
717
        break;
1✔
718
      }
719

720
      Node<K, V> next = node.getNextInAccessOrder();
1✔
721
      if (node.getPolicyWeight() != 0) {
1✔
722
        node.makeMainProbation();
1✔
723
        accessOrderWindowDeque().remove(node);
1✔
724
        accessOrderProbationDeque().offerLast(node);
1✔
725
        if (first == null) {
1✔
726
          first = node;
1✔
727
        }
728

729
        setWindowWeightedSize(windowWeightedSize() - node.getPolicyWeight());
1✔
730
      }
731
      node = next;
1✔
732
    }
1✔
733

734
    return first;
1✔
735
  }
736

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

766
      // Try evicting from the protected and window queues
767
      if ((candidate == null) && (victim == null)) {
1✔
768
        if (victimQueue == PROBATION) {
1✔
769
          victim = accessOrderProtectedDeque().peekFirst();
1✔
770
          victimQueue = PROTECTED;
1✔
771
          continue;
1✔
772
        } else if (victimQueue == PROTECTED) {
1✔
773
          victim = accessOrderWindowDeque().peekFirst();
1✔
774
          victimQueue = WINDOW;
1✔
775
          continue;
1✔
776
        }
777

778
        // The pending operations will adjust the size to reflect the correct weight
779
        break;
780
      }
781

782
      // Skip over entries with zero weight
783
      if ((victim != null) && (victim.getPolicyWeight() == 0)) {
1✔
784
        victim = victim.getNextInAccessOrder();
1✔
785
        continue;
1✔
786
      } else if ((candidate != null) && (candidate.getPolicyWeight() == 0)) {
1✔
787
        candidate = candidate.getNextInAccessOrder();
1✔
788
        continue;
1✔
789
      }
790

791
      // Evict immediately if only one of the entries is present
792
      if (victim == null) {
1✔
793
        requireNonNull(candidate);
1✔
794
        Node<K, V> previous = candidate.getNextInAccessOrder();
1✔
795
        Node<K, V> evict = candidate;
1✔
796
        candidate = previous;
1✔
797
        evictEntry(evict, RemovalCause.SIZE, 0L);
1✔
798
        continue;
1✔
799
      } else if (candidate == null) {
1✔
800
        Node<K, V> evict = victim;
1✔
801
        victim = victim.getNextInAccessOrder();
1✔
802
        evictEntry(evict, RemovalCause.SIZE, 0L);
1✔
803
        continue;
1✔
804
      }
805

806
      // Evict immediately if both selected the same entry
807
      if (candidate == victim) {
1✔
808
        victim = victim.getNextInAccessOrder();
1✔
809
        evictEntry(candidate, RemovalCause.SIZE, 0L);
1✔
810
        candidate = null;
1✔
811
        continue;
1✔
812
      }
813

814
      // Evict immediately if an entry was collected
815
      var victimKeyRef = victim.getKeyReferenceOrNull();
1✔
816
      var candidateKeyRef = candidate.getKeyReferenceOrNull();
1✔
817
      if (victimKeyRef == null) {
1✔
818
        Node<K, V> evict = victim;
1✔
819
        victim = victim.getNextInAccessOrder();
1✔
820
        evictEntry(evict, RemovalCause.COLLECTED, 0L);
1✔
821
        continue;
1✔
822
      } else if (candidateKeyRef == null) {
1✔
823
        Node<K, V> evict = candidate;
1✔
824
        candidate = candidate.getNextInAccessOrder();
1✔
825
        evictEntry(evict, RemovalCause.COLLECTED, 0L);
1✔
826
        continue;
1✔
827
      }
828

829
      // Evict immediately if an entry was removed
830
      if (!victim.isAlive()) {
1✔
831
        Node<K, V> evict = victim;
1✔
832
        victim = victim.getNextInAccessOrder();
1✔
833
        evictEntry(evict, RemovalCause.SIZE, 0L);
1✔
834
        continue;
1✔
835
      } else if (!candidate.isAlive()) {
1✔
836
        Node<K, V> evict = candidate;
1✔
837
        candidate = candidate.getNextInAccessOrder();
1✔
838
        evictEntry(evict, RemovalCause.SIZE, 0L);
1✔
839
        continue;
1✔
840
      }
841

842
      // Evict immediately if the candidate's weight exceeds the maximum
843
      if (candidate.getPolicyWeight() > maximum()) {
1✔
844
        Node<K, V> evict = candidate;
1✔
845
        candidate = candidate.getNextInAccessOrder();
1✔
846
        evictEntry(evict, RemovalCause.SIZE, 0L);
1✔
847
        continue;
1✔
848
      }
849

850
      // Evict the entry with the lowest frequency
851
      if (admit(candidateKeyRef, victimKeyRef)) {
1✔
852
        Node<K, V> evict = victim;
1✔
853
        victim = victim.getNextInAccessOrder();
1✔
854
        evictEntry(evict, RemovalCause.SIZE, 0L);
1✔
855
        candidate = candidate.getNextInAccessOrder();
1✔
856
      } else {
1✔
857
        Node<K, V> evict = candidate;
1✔
858
        candidate = candidate.getNextInAccessOrder();
1✔
859
        evictEntry(evict, RemovalCause.SIZE, 0L);
1✔
860
      }
861
    }
1✔
862
  }
1✔
863

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

890
  /** Expires entries that have expired by access, write, or variable. */
891
  @GuardedBy("evictionLock")
892
  void expireEntries() {
893
    long now = expirationTicker().read();
1✔
894
    expireAfterAccessEntries(now);
1✔
895
    expireAfterWriteEntries(now);
1✔
896
    expireVariableEntries(now);
1✔
897

898
    Pacer pacer = pacer();
1✔
899
    if (pacer != null) {
1✔
900
      long delay = getExpirationDelay(now);
1✔
901
      if (delay == Long.MAX_VALUE) {
1✔
902
        pacer.cancel();
1✔
903
      } else {
904
        pacer.schedule(executor, drainBuffersTask, now, delay);
1✔
905
      }
906
    }
907
  }
1✔
908

909
  /** Expires entries in the access-order queue. */
910
  @GuardedBy("evictionLock")
911
  void expireAfterAccessEntries(long now) {
912
    if (!expiresAfterAccess()) {
1✔
913
      return;
1✔
914
    }
915

916
    expireAfterAccessEntries(now, accessOrderWindowDeque());
1✔
917
    if (evicts()) {
1✔
918
      expireAfterAccessEntries(now, accessOrderProbationDeque());
1✔
919
      expireAfterAccessEntries(now, accessOrderProtectedDeque());
1✔
920
    }
921
  }
1✔
922

923
  /** Expires entries in an access-order queue. */
924
  @GuardedBy("evictionLock")
925
  void expireAfterAccessEntries(long now, AccessOrderDeque<Node<K, V>> accessOrderDeque) {
926
    long duration = expiresAfterAccessNanos();
1✔
927
    for (;;) {
928
      Node<K, V> node = accessOrderDeque.peekFirst();
1✔
929
      if ((node == null) || ((now - node.getAccessTime()) < duration)
1✔
930
          || !evictEntry(node, RemovalCause.EXPIRED, now)) {
1✔
931
        return;
1✔
932
      }
933
    }
1✔
934
  }
935

936
  /** Expires entries on the write-order queue. */
937
  @GuardedBy("evictionLock")
938
  void expireAfterWriteEntries(long now) {
939
    if (!expiresAfterWrite()) {
1✔
940
      return;
1✔
941
    }
942
    long duration = expiresAfterWriteNanos();
1✔
943
    for (;;) {
944
      Node<K, V> node = writeOrderDeque().peekFirst();
1✔
945
      if ((node == null) || ((now - node.getWriteTime()) < duration)
1✔
946
          || !evictEntry(node, RemovalCause.EXPIRED, now)) {
1✔
947
        break;
1✔
948
      }
949
    }
1✔
950
  }
1✔
951

952
  /** Expires entries in the timer wheel. */
953
  @GuardedBy("evictionLock")
954
  void expireVariableEntries(long now) {
955
    if (expiresVariable()) {
1✔
956
      timerWheel().advance(this, now);
1✔
957
    }
958
  }
1✔
959

960
  /** Returns the duration until the next item expires, or {@link Long#MAX_VALUE} if none. */
961
  @GuardedBy("evictionLock")
962
  long getExpirationDelay(long now) {
963
    @Var long delay = Long.MAX_VALUE;
1✔
964
    if (expiresAfterAccess()) {
1✔
965
      @Var Node<K, V> node = accessOrderWindowDeque().peekFirst();
1✔
966
      if (node != null) {
1✔
967
        delay = Math.min(delay, expiresAfterAccessNanos() - (now - node.getAccessTime()));
1✔
968
      }
969
      if (evicts()) {
1✔
970
        node = accessOrderProbationDeque().peekFirst();
1✔
971
        if (node != null) {
1✔
972
          delay = Math.min(delay, expiresAfterAccessNanos() - (now - node.getAccessTime()));
1✔
973
        }
974
        node = accessOrderProtectedDeque().peekFirst();
1✔
975
        if (node != null) {
1✔
976
          delay = Math.min(delay, expiresAfterAccessNanos() - (now - node.getAccessTime()));
1✔
977
        }
978
      }
979
    }
980
    if (expiresAfterWrite()) {
1✔
981
      Node<K, V> node = writeOrderDeque().peekFirst();
1✔
982
      if (node != null) {
1✔
983
        delay = Math.min(delay, expiresAfterWriteNanos() - (now - node.getWriteTime()));
1✔
984
      }
985
    }
986
    if (expiresVariable()) {
1✔
987
      delay = Math.min(delay, timerWheel().getExpirationDelay());
1✔
988
    }
989
    return delay;
1✔
990
  }
991

992
  /** Returns if the entry has expired. */
993
  @SuppressWarnings("ShortCircuitBoolean")
994
  boolean hasExpired(Node<K, V> node, long now) {
995
    if (isComputingAsync(node.getValue())) {
1✔
996
      return false;
1✔
997
    }
998
    return (expiresAfterAccess() && (now - node.getAccessTime() >= expiresAfterAccessNanos()))
1✔
999
        | (expiresAfterWrite() && (now - node.getWriteTime() >= expiresAfterWriteNanos()))
1✔
1000
        | (expiresVariable() && (now - node.getVariableTime() >= 0));
1✔
1001
  }
1002

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

1023
    data.computeIfPresent(keyReference, (k, n) -> {
1✔
1024
      if (n != node) {
1✔
1025
        return n;
1✔
1026
      }
1027
      synchronized (n) {
1✔
1028
        value[0] = n.getValue();
1✔
1029

1030
        if ((key == null) || (value[0] == null)) {
1✔
1031
          actualCause[0] = RemovalCause.COLLECTED;
1✔
1032
        } else if (cause == RemovalCause.COLLECTED) {
1✔
1033
          resurrect[0] = true;
1✔
1034
          return n;
1✔
1035
        } else {
1036
          actualCause[0] = cause;
1✔
1037
        }
1038

1039
        if (actualCause[0] == RemovalCause.EXPIRED) {
1✔
1040
          @Var boolean expired = false;
1✔
1041
          if (expiresAfterAccess()) {
1✔
1042
            expired |= ((now - n.getAccessTime()) >= expiresAfterAccessNanos());
1✔
1043
          }
1044
          if (expiresAfterWrite()) {
1✔
1045
            expired |= ((now - n.getWriteTime()) >= expiresAfterWriteNanos());
1✔
1046
          }
1047
          if (expiresVariable()) {
1✔
1048
            expired |= ((now - node.getVariableTime()) >= 0);
1✔
1049
          }
1050
          if (!expired) {
1✔
1051
            resurrect[0] = true;
1✔
1052
            return n;
1✔
1053
          }
1054
        } else if (actualCause[0] == RemovalCause.SIZE) {
1✔
1055
          int weight = node.getWeight();
1✔
1056
          if (weight == 0) {
1✔
1057
            resurrect[0] = true;
1✔
1058
            return n;
1✔
1059
          }
1060
        }
1061

1062
        notifyEviction(key, value[0], actualCause[0]);
1✔
1063
        discardRefresh(keyReference);
1✔
1064
        removed[0] = true;
1✔
1065
        node.retire();
1✔
1066
        return null;
1✔
1067
      }
1068
    });
1069

1070
    // The entry is no longer eligible for eviction
1071
    if (resurrect[0]) {
1✔
1072
      return false;
1✔
1073
    }
1074

1075
    // If the eviction fails due to a concurrent removal of the victim, that removal may cancel out
1076
    // the addition that triggered this eviction. The victim is eagerly unlinked and the size
1077
    // decremented before the removal task so that if an eviction is still required then a new
1078
    // victim will be chosen for removal.
1079
    if (node.inWindow() && (evicts() || expiresAfterAccess())) {
1✔
1080
      accessOrderWindowDeque().remove(node);
1✔
1081
    } else if (evicts()) {
1✔
1082
      if (node.inMainProbation()) {
1✔
1083
        accessOrderProbationDeque().remove(node);
1✔
1084
      } else {
1085
        accessOrderProtectedDeque().remove(node);
1✔
1086
      }
1087
    }
1088
    if (expiresAfterWrite()) {
1✔
1089
      writeOrderDeque().remove(node);
1✔
1090
    } else if (expiresVariable()) {
1✔
1091
      timerWheel().deschedule(node);
1✔
1092
    }
1093

1094
    synchronized (node) {
1✔
1095
      logIfAlive(node);
1✔
1096
      makeDead(node);
1✔
1097
    }
1✔
1098

1099
    if (removed[0]) {
1✔
1100
      statsCounter().recordEviction(node.getWeight(), actualCause[0]);
1✔
1101
      notifyRemoval(key, value[0], actualCause[0]);
1✔
1102
    }
1103

1104
    return true;
1✔
1105
  }
1106

1107
  /** Adapts the eviction policy to towards the optimal recency / frequency configuration. */
1108
  @GuardedBy("evictionLock")
1109
  @SuppressWarnings("UnnecessaryReturnStatement")
1110
  void climb() {
1111
    if (!evicts()) {
1✔
1112
      return;
1✔
1113
    }
1114

1115
    determineAdjustment();
1✔
1116
    demoteFromMainProtected();
1✔
1117
    long amount = adjustment();
1✔
1118
    if (amount == 0) {
1✔
1119
      return;
1✔
1120
    } else if (amount > 0) {
1✔
1121
      increaseWindow();
1✔
1122
    } else {
1123
      decreaseWindow();
1✔
1124
    }
1125
  }
1✔
1126

1127
  /** Calculates the amount to adapt the window by and sets {@link #adjustment()} accordingly. */
1128
  @GuardedBy("evictionLock")
1129
  void determineAdjustment() {
1130
    if (frequencySketch().isNotInitialized()) {
1✔
1131
      setPreviousSampleHitRate(0.0);
1✔
1132
      setMissesInSample(0);
1✔
1133
      setHitsInSample(0);
1✔
1134
      return;
1✔
1135
    }
1136

1137
    int requestCount = hitsInSample() + missesInSample();
1✔
1138
    if (requestCount < frequencySketch().sampleSize) {
1✔
1139
      return;
1✔
1140
    }
1141

1142
    double hitRate = (double) hitsInSample() / requestCount;
1✔
1143
    double hitRateChange = hitRate - previousSampleHitRate();
1✔
1144
    double amount = (hitRateChange >= 0) ? stepSize() : -stepSize();
1✔
1145
    double nextStepSize = (Math.abs(hitRateChange) >= HILL_CLIMBER_RESTART_THRESHOLD)
1✔
1146
        ? HILL_CLIMBER_STEP_PERCENT * maximum() * (amount >= 0 ? 1 : -1)
1✔
1147
        : HILL_CLIMBER_STEP_DECAY_RATE * amount;
1✔
1148
    setPreviousSampleHitRate(hitRate);
1✔
1149
    setAdjustment((long) amount);
1✔
1150
    setStepSize(nextStepSize);
1✔
1151
    setMissesInSample(0);
1✔
1152
    setHitsInSample(0);
1✔
1153
  }
1✔
1154

1155
  /**
1156
   * Increases the size of the admission window by shrinking the portion allocated to the main
1157
   * space. As the main space is partitioned into probation and protected regions (80% / 20%), for
1158
   * simplicity only the protected is reduced. If the regions exceed their maximums, this may cause
1159
   * protected items to be demoted to the probation region and probation items to be demoted to the
1160
   * admission window.
1161
   */
1162
  @GuardedBy("evictionLock")
1163
  void increaseWindow() {
1164
    if (mainProtectedMaximum() == 0) {
1✔
1165
      return;
1✔
1166
    }
1167

1168
    @Var long quota = Math.min(adjustment(), mainProtectedMaximum());
1✔
1169
    setMainProtectedMaximum(mainProtectedMaximum() - quota);
1✔
1170
    setWindowMaximum(windowMaximum() + quota);
1✔
1171
    demoteFromMainProtected();
1✔
1172

1173
    for (int i = 0; i < QUEUE_TRANSFER_THRESHOLD; i++) {
1✔
1174
      @Var Node<K, V> candidate = accessOrderProbationDeque().peekFirst();
1✔
1175
      @Var boolean probation = true;
1✔
1176
      if ((candidate == null) || (quota < candidate.getPolicyWeight())) {
1✔
1177
        candidate = accessOrderProtectedDeque().peekFirst();
1✔
1178
        probation = false;
1✔
1179
      }
1180
      if (candidate == null) {
1✔
1181
        break;
1✔
1182
      }
1183

1184
      int weight = candidate.getPolicyWeight();
1✔
1185
      if (quota < weight) {
1✔
1186
        break;
1✔
1187
      }
1188

1189
      quota -= weight;
1✔
1190
      if (probation) {
1✔
1191
        accessOrderProbationDeque().remove(candidate);
1✔
1192
      } else {
1193
        setMainProtectedWeightedSize(mainProtectedWeightedSize() - weight);
1✔
1194
        accessOrderProtectedDeque().remove(candidate);
1✔
1195
      }
1196
      setWindowWeightedSize(windowWeightedSize() + weight);
1✔
1197
      accessOrderWindowDeque().offerLast(candidate);
1✔
1198
      candidate.makeWindow();
1✔
1199
    }
1200

1201
    setMainProtectedMaximum(mainProtectedMaximum() + quota);
1✔
1202
    setWindowMaximum(windowMaximum() - quota);
1✔
1203
    setAdjustment(quota);
1✔
1204
  }
1✔
1205

1206
  /** Decreases the size of the admission window and increases the main's protected region. */
1207
  @GuardedBy("evictionLock")
1208
  void decreaseWindow() {
1209
    if (windowMaximum() <= 1) {
1✔
1210
      return;
1✔
1211
    }
1212

1213
    @Var long quota = Math.min(-adjustment(), Math.max(0, windowMaximum() - 1));
1✔
1214
    setMainProtectedMaximum(mainProtectedMaximum() + quota);
1✔
1215
    setWindowMaximum(windowMaximum() - quota);
1✔
1216

1217
    for (int i = 0; i < QUEUE_TRANSFER_THRESHOLD; i++) {
1✔
1218
      Node<K, V> candidate = accessOrderWindowDeque().peekFirst();
1✔
1219
      if (candidate == null) {
1✔
1220
        break;
1✔
1221
      }
1222

1223
      int weight = candidate.getPolicyWeight();
1✔
1224
      if (quota < weight) {
1✔
1225
        break;
1✔
1226
      }
1227

1228
      quota -= weight;
1✔
1229
      setWindowWeightedSize(windowWeightedSize() - weight);
1✔
1230
      accessOrderWindowDeque().remove(candidate);
1✔
1231
      accessOrderProbationDeque().offerLast(candidate);
1✔
1232
      candidate.makeMainProbation();
1✔
1233
    }
1234

1235
    setMainProtectedMaximum(mainProtectedMaximum() - quota);
1✔
1236
    setWindowMaximum(windowMaximum() + quota);
1✔
1237
    setAdjustment(-quota);
1✔
1238
  }
1✔
1239

1240
  /** Transfers the nodes from the protected to the probation region if it exceeds the maximum. */
1241
  @GuardedBy("evictionLock")
1242
  void demoteFromMainProtected() {
1243
    long mainProtectedMaximum = mainProtectedMaximum();
1✔
1244
    @Var long mainProtectedWeightedSize = mainProtectedWeightedSize();
1✔
1245
    if (mainProtectedWeightedSize <= mainProtectedMaximum) {
1✔
1246
      return;
1✔
1247
    }
1248

1249
    for (int i = 0; i < QUEUE_TRANSFER_THRESHOLD; i++) {
1✔
1250
      if (mainProtectedWeightedSize <= mainProtectedMaximum) {
1✔
1251
        break;
1✔
1252
      }
1253

1254
      Node<K, V> demoted = accessOrderProtectedDeque().pollFirst();
1✔
1255
      if (demoted == null) {
1✔
1256
        break;
1✔
1257
      }
1258
      demoted.makeMainProbation();
1✔
1259
      accessOrderProbationDeque().offerLast(demoted);
1✔
1260
      mainProtectedWeightedSize -= demoted.getPolicyWeight();
1✔
1261
    }
1262
    setMainProtectedWeightedSize(mainProtectedWeightedSize);
1✔
1263
  }
1✔
1264

1265
  /**
1266
   * Performs the post-processing work required after a read.
1267
   *
1268
   * @param node the entry in the page replacement policy
1269
   * @param now the current time, in nanoseconds
1270
   * @param recordHit if the hit count should be incremented
1271
   * @return the refreshed value if immediately loaded, else null
1272
   */
1273
  @Nullable V afterRead(Node<K, V> node, long now, boolean recordHit) {
1274
    if (recordHit) {
1✔
1275
      statsCounter().recordHits(1);
1✔
1276
    }
1277

1278
    boolean delayable = skipReadBuffer() || (readBuffer.offer(node) != Buffer.FULL);
1✔
1279
    if (shouldDrainBuffers(delayable)) {
1✔
1280
      scheduleDrainBuffers();
1✔
1281
    }
1282
    return refreshIfNeeded(node, now);
1✔
1283
  }
1284

1285
  /** Returns if the cache should bypass the read buffer. */
1286
  boolean skipReadBuffer() {
1287
    return fastpath() && frequencySketch().isNotInitialized();
1✔
1288
  }
1289

1290
  /**
1291
   * Asynchronously refreshes the entry if eligible.
1292
   *
1293
   * @param node the entry in the cache to refresh
1294
   * @param now the current time, in nanoseconds
1295
   * @return the refreshed value if immediately loaded, else null
1296
   */
1297
  @SuppressWarnings("FutureReturnValueIgnored")
1298
  @Nullable V refreshIfNeeded(Node<K, V> node, long now) {
1299
    if (!refreshAfterWrite()) {
1✔
1300
      return null;
1✔
1301
    }
1302

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

1351
      if (refreshFuture[0] == null) {
1✔
1352
        return null;
1✔
1353
      }
1354

1355
      var refreshed = refreshFuture[0].handle((newValue, error) -> {
1✔
1356
        long loadTime = statsTicker().read() - startTime[0];
1✔
1357
        if (error != null) {
1✔
1358
          if (!(error instanceof CancellationException) && !(error instanceof TimeoutException)) {
1✔
1359
            logger.log(Level.WARNING, "Exception thrown during refresh", error);
1✔
1360
          }
1361
          refreshes.remove(keyReference, refreshFuture[0]);
1✔
1362
          statsCounter().recordLoadFailure(loadTime);
1✔
1363
          return null;
1✔
1364
        }
1365

1366
        @SuppressWarnings("unchecked")
1367
        V value = (isAsync && (newValue != null)) ? (V) refreshFuture[0] : newValue;
1✔
1368

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

1394
        if (cause[0] != null) {
1✔
1395
          notifyRemoval(key, value, cause[0]);
1✔
1396
        }
1397
        if (newValue == null) {
1✔
1398
          statsCounter().recordLoadFailure(loadTime);
1✔
1399
        } else {
1400
          statsCounter().recordLoadSuccess(loadTime);
1✔
1401
        }
1402
        return result;
1✔
1403
      });
1404
      return Async.getIfReady(refreshed);
1✔
1405
    }
1406

1407
    return null;
1✔
1408
  }
1409

1410
  /**
1411
   * Returns the expiration time for the entry after being created.
1412
   *
1413
   * @param key the key of the entry that was created
1414
   * @param value the value of the entry that was created
1415
   * @param expiry the calculator for the expiration time
1416
   * @param now the current time, in nanoseconds
1417
   * @return the expiration time
1418
   */
1419
  long expireAfterCreate(K key, V value, @Nullable Expiry<? super K, ? super V> expiry, long now) {
1420
    if (expiresVariable()) {
1✔
1421
      requireNonNull(expiry);
1✔
1422
      long duration = Math.max(0L, expiry.expireAfterCreate(key, value, now));
1✔
1423
      return isAsync ? (now + duration) : (now + Math.min(duration, MAXIMUM_EXPIRY));
1✔
1424
    }
1425
    return 0L;
1✔
1426
  }
1427

1428
  /**
1429
   * Returns the expiration time for the entry after being updated.
1430
   *
1431
   * @param node the entry in the page replacement policy
1432
   * @param key the key of the entry that was updated
1433
   * @param value the value of the entry that was updated
1434
   * @param expiry the calculator for the expiration time
1435
   * @param now the current time, in nanoseconds
1436
   * @return the expiration time
1437
   */
1438
  long expireAfterUpdate(Node<K, V> node, K key, V value,
1439
      @Nullable Expiry<? super K, ? super V> expiry, long now) {
1440
    if (expiresVariable()) {
1✔
1441
      requireNonNull(expiry);
1✔
1442
      long currentDuration = Math.max(1, node.getVariableTime() - now);
1✔
1443
      long duration = Math.max(0L, expiry.expireAfterUpdate(key, value, now, currentDuration));
1✔
1444
      return isAsync ? (now + duration) : (now + Math.min(duration, MAXIMUM_EXPIRY));
1✔
1445
    }
1446
    return 0L;
1✔
1447
  }
1448

1449
  /**
1450
   * Returns the access time for the entry after a read.
1451
   *
1452
   * @param node the entry in the page replacement policy
1453
   * @param key the key of the entry that was read
1454
   * @param value the value of the entry that was read
1455
   * @param expiry the calculator for the expiration time
1456
   * @param now the current time, in nanoseconds
1457
   * @return the expiration time
1458
   */
1459
  long expireAfterRead(Node<K, V> node, K key, V value, Expiry<K, V> expiry, long now) {
1460
    if (expiresVariable()) {
1✔
1461
      long currentDuration = Math.max(0L, node.getVariableTime() - now);
1✔
1462
      long duration = Math.max(0L, expiry.expireAfterRead(key, value, now, currentDuration));
1✔
1463
      return isAsync ? (now + duration) : (now + Math.min(duration, MAXIMUM_EXPIRY));
1✔
1464
    }
1465
    return 0L;
1✔
1466
  }
1467

1468
  /**
1469
   * Attempts to update the access time for the entry after a read.
1470
   *
1471
   * @param node the entry in the page replacement policy
1472
   * @param key the key of the entry that was read
1473
   * @param value the value of the entry that was read
1474
   * @param expiry the calculator for the expiration time
1475
   * @param now the current time, in nanoseconds
1476
   */
1477
  void tryExpireAfterRead(Node<K, V> node, K key, V value, Expiry<K, V> expiry, long now) {
1478
    if (!expiresVariable()) {
1✔
1479
      return;
1✔
1480
    }
1481

1482
    long variableTime = node.getVariableTime();
1✔
1483
    long currentDuration = Math.max(1, variableTime - now);
1✔
1484
    if (isAsync && (currentDuration > MAXIMUM_EXPIRY)) {
1✔
1485
      // expireAfterCreate has not yet set the duration after completion
1486
      return;
1✔
1487
    }
1488

1489
    long duration = Math.max(0L, expiry.expireAfterRead(key, value, now, currentDuration));
1✔
1490
    if (duration != currentDuration) {
1✔
1491
      long expirationTime = isAsync ? (now + duration) : (now + Math.min(duration, MAXIMUM_EXPIRY));
1✔
1492
      node.casVariableTime(variableTime, expirationTime);
1✔
1493
    }
1494
  }
1✔
1495

1496
  void setVariableTime(Node<K, V> node, long expirationTime) {
1497
    if (expiresVariable()) {
1✔
1498
      node.setVariableTime(expirationTime);
1✔
1499
    }
1500
  }
1✔
1501

1502
  void setWriteTime(Node<K, V> node, long now) {
1503
    if (expiresAfterWrite() || refreshAfterWrite()) {
1✔
1504
      node.setWriteTime(now & ~1L);
1✔
1505
    }
1506
  }
1✔
1507

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

1514
  /** Returns if the entry's write time would exceed the minimum expiration reorder threshold. */
1515
  boolean exceedsWriteTimeTolerance(Node<K, V> node, long varTime, long now) {
1516
    long variableTime = node.getVariableTime();
1✔
1517
    long tolerance = EXPIRE_WRITE_TOLERANCE;
1✔
1518
    long writeTime = node.getWriteTime();
1✔
1519
    return
1✔
1520
        (expiresAfterWrite()
1✔
1521
            && ((expiresAfterWriteNanos() <= tolerance) || (Math.abs(now - writeTime) > tolerance)))
1✔
1522
        || (refreshAfterWrite()
1✔
1523
            && ((refreshAfterWriteNanos() <= tolerance) || (Math.abs(now - writeTime) > tolerance)))
1✔
1524
        || (expiresVariable() && (Math.abs(varTime - variableTime) > tolerance));
1✔
1525
  }
1526

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

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

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

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

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

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

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

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

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

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

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

1715
    try {
1716
      drainReadBuffer();
1✔
1717

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1902
        var keyRef = node.getKeyReferenceOrNull();
1✔
1903
        if (keyRef != null) {
1✔
1904
          frequencySketch().increment(keyRef);
1✔
1905
        }
1906

1907
        setMissesInSample(missesInSample() + 1);
1✔
1908
      }
1909

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

1937
  /** Removes a node from the page replacement policy. */
1938
  final class RemovalTask implements Runnable {
1939
    final Node<K, V> node;
1940

1941
    RemovalTask(Node<K, V> node) {
1✔
1942
      this.node = node;
1✔
1943
    }
1✔
1944

1945
    @Override
1946
    @GuardedBy("evictionLock")
1947
    public void run() {
1948
      // add may not have been processed yet
1949
      if (node.inWindow() && (evicts() || expiresAfterAccess())) {
1✔
1950
        accessOrderWindowDeque().remove(node);
1✔
1951
      } else if (evicts()) {
1✔
1952
        if (node.inMainProbation()) {
1✔
1953
          accessOrderProbationDeque().remove(node);
1✔
1954
        } else {
1955
          accessOrderProtectedDeque().remove(node);
1✔
1956
        }
1957
      }
1958
      if (expiresAfterWrite()) {
1✔
1959
        writeOrderDeque().remove(node);
1✔
1960
      } else if (expiresVariable()) {
1✔
1961
        timerWheel().deschedule(node);
1✔
1962
      }
1963
      makeDead(node);
1✔
1964
    }
1✔
1965
  }
1966

1967
  /** Updates the weighted size. */
1968
  final class UpdateTask implements Runnable {
1969
    final int weightDifference;
1970
    final Node<K, V> node;
1971

1972
    public UpdateTask(Node<K, V> node, int weightDifference) {
1✔
1973
      this.weightDifference = weightDifference;
1✔
1974
      this.node = node;
1✔
1975
    }
1✔
1976

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

2012
        setWeightedSize(weightedSize() + weightDifference);
1✔
2013
        if (weightedSize() > MAXIMUM_CAPACITY) {
1✔
2014
          evictEntries();
1✔
2015
        }
2016
      } else if (expiresAfterAccess()) {
1✔
2017
        onAccess(node);
1✔
2018
      }
2019
    }
1✔
2020
  }
2021

2022
  /* --------------- Concurrent Map Support --------------- */
2023

2024
  @Override
2025
  public boolean isEmpty() {
2026
    return data.isEmpty();
1✔
2027
  }
2028

2029
  @Override
2030
  public int size() {
2031
    return data.size();
1✔
2032
  }
2033

2034
  @Override
2035
  public long estimatedSize() {
2036
    return data.mappingCount();
1✔
2037
  }
2038

2039
  @Override
2040
  public void clear() {
2041
    Deque<Node<K, V>> entries;
2042
    evictionLock.lock();
1✔
2043
    try {
2044
      // Discard all pending reads
2045
      readBuffer.drainTo(e -> {});
1✔
2046

2047
      // Apply all pending writes
2048
      @Var Runnable task;
2049
      while ((task = writeBuffer.poll()) != null) {
1✔
2050
        task.run();
1✔
2051
      }
2052

2053
      // Cancel the scheduled cleanup
2054
      Pacer pacer = pacer();
1✔
2055
      if (pacer != null) {
1✔
2056
        pacer.cancel();
1✔
2057
      }
2058

2059
      // Discard all entries, falling back to one-by-one to avoid excessive lock hold times
2060
      long now = expirationTicker().read();
1✔
2061
      int threshold = (WRITE_BUFFER_MAX / 2);
1✔
2062
      entries = new ArrayDeque<>(data.values());
1✔
2063
      while (!entries.isEmpty() && (writeBuffer.size() < threshold)) {
1✔
2064
        removeNode(entries.pollFirst(), now);
1✔
2065
      }
2066
    } finally {
2067
      evictionLock.unlock();
1✔
2068
    }
2069

2070
    // Remove any stragglers if released early to more aggressively flush incoming writes
2071
    @Var boolean cleanUp = false;
1✔
2072
    for (var node : entries) {
1✔
2073
      @Nullable K key = node.getKey();
1✔
2074
      if (key == null) {
1✔
2075
        cleanUp = true;
1✔
2076
      } else {
2077
        remove(key);
1✔
2078
      }
2079
    }
1✔
2080
    if (collectKeys() && cleanUp) {
1✔
2081
      cleanUp();
1✔
2082
    }
2083
  }
1✔
2084

2085
  @GuardedBy("evictionLock")
2086
  @SuppressWarnings({"GuardedByChecker", "SynchronizationOnLocalVariableOrMethodParameter"})
2087
  void removeNode(Node<K, V> node, long now) {
2088
    K key = node.getKey();
1✔
2089
    var cause = new RemovalCause[1];
1✔
2090
    var keyReference = node.getKeyReference();
1✔
2091
    @SuppressWarnings({"unchecked", "Varifier"})
2092
    @Nullable V[] value = (V[]) new Object[1];
1✔
2093

2094
    data.computeIfPresent(keyReference, (k, n) -> {
1✔
2095
      if (n != node) {
1✔
2096
        return n;
1✔
2097
      }
2098
      synchronized (n) {
1✔
2099
        value[0] = n.getValue();
1✔
2100

2101
        if ((key == null) || (value[0] == null)) {
1✔
2102
          cause[0] = RemovalCause.COLLECTED;
1✔
2103
        } else if (hasExpired(n, now)) {
1✔
2104
          cause[0] = RemovalCause.EXPIRED;
1✔
2105
        } else {
2106
          cause[0] = RemovalCause.EXPLICIT;
1✔
2107
        }
2108

2109
        if (cause[0].wasEvicted()) {
1✔
2110
          notifyEviction(key, value[0], cause[0]);
1✔
2111
        }
2112

2113
        discardRefresh(node.getKeyReference());
1✔
2114
        node.retire();
1✔
2115
        return null;
1✔
2116
      }
2117
    });
2118

2119
    if (node.inWindow() && (evicts() || expiresAfterAccess())) {
1✔
2120
      accessOrderWindowDeque().remove(node);
1✔
2121
    } else if (evicts()) {
1✔
2122
      if (node.inMainProbation()) {
1✔
2123
        accessOrderProbationDeque().remove(node);
1✔
2124
      } else {
2125
        accessOrderProtectedDeque().remove(node);
1✔
2126
      }
2127
    }
2128
    if (expiresAfterWrite()) {
1✔
2129
      writeOrderDeque().remove(node);
1✔
2130
    } else if (expiresVariable()) {
1✔
2131
      timerWheel().deschedule(node);
1✔
2132
    }
2133

2134
    synchronized (node) {
1✔
2135
      logIfAlive(node);
1✔
2136
      makeDead(node);
1✔
2137
    }
1✔
2138

2139
    if (cause[0] != null) {
1✔
2140
      notifyRemoval(key, value[0], cause[0]);
1✔
2141
    }
2142
  }
1✔
2143

2144
  @Override
2145
  public boolean containsKey(Object key) {
2146
    Node<K, V> node = data.get(nodeFactory.newLookupKey(key));
1✔
2147
    return (node != null) && (node.getValue() != null)
1✔
2148
        && !hasExpired(node, expirationTicker().read());
1✔
2149
  }
2150

2151
  @Override
2152
  public boolean containsValue(Object value) {
2153
    requireNonNull(value);
1✔
2154

2155
    long now = expirationTicker().read();
1✔
2156
    for (Node<K, V> node : data.values()) {
1✔
2157
      if (node.containsValue(value) && !hasExpired(node, now) && (node.getKey() != null)) {
1✔
2158
        return true;
1✔
2159
      }
2160
    }
1✔
2161
    return false;
1✔
2162
  }
2163

2164
  @Override
2165
  public @Nullable V get(Object key) {
2166
    return getIfPresent(key, /* recordStats= */ false);
1✔
2167
  }
2168

2169
  @Override
2170
  public @Nullable V getIfPresent(Object key, boolean recordStats) {
2171
    Node<K, V> node = data.get(nodeFactory.newLookupKey(key));
1✔
2172
    if (node == null) {
1✔
2173
      if (recordStats) {
1✔
2174
        statsCounter().recordMisses(1);
1✔
2175
      }
2176
      if (drainStatusOpaque() == REQUIRED) {
1✔
2177
        scheduleDrainBuffers();
1✔
2178
      }
2179
      return null;
1✔
2180
    }
2181

2182
    V value = node.getValue();
1✔
2183
    long now = expirationTicker().read();
1✔
2184
    if (hasExpired(node, now) || (collectValues() && (value == null))) {
1✔
2185
      if (recordStats) {
1✔
2186
        statsCounter().recordMisses(1);
1✔
2187
      }
2188
      scheduleDrainBuffers();
1✔
2189
      return null;
1✔
2190
    }
2191

2192
    if ((value != null) && !isComputingAsync(value)) {
1✔
2193
      @SuppressWarnings("unchecked")
2194
      var castedKey = (K) key;
1✔
2195
      setAccessTime(node, now);
1✔
2196
      tryExpireAfterRead(node, castedKey, value, expiry(), now);
1✔
2197
    }
2198
    V refreshed = afterRead(node, now, recordStats);
1✔
2199
    return (refreshed == null) ? value : refreshed;
1✔
2200
  }
2201

2202
  @Override
2203
  public @Nullable V getIfPresentQuietly(Object key) {
2204
    V value;
2205
    Node<K, V> node = data.get(nodeFactory.newLookupKey(key));
1✔
2206
    if ((node == null) || ((value = node.getValue()) == null)
1✔
2207
        || hasExpired(node, expirationTicker().read())) {
1✔
2208
      return null;
1✔
2209
    }
2210
    return value;
1✔
2211
  }
2212

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

2233
  @Override
2234
  public Map<K, V> getAllPresent(Iterable<? extends K> keys) {
2235
    var result = new LinkedHashMap<K, @Nullable V>(calculateHashMapCapacity(keys));
1✔
2236
    for (K key : keys) {
1✔
2237
      result.put(key, null);
1✔
2238
    }
1✔
2239

2240
    int uniqueKeys = result.size();
1✔
2241
    long now = expirationTicker().read();
1✔
2242
    for (var iter = result.entrySet().iterator(); iter.hasNext();) {
1✔
2243
      V value;
2244
      var entry = iter.next();
1✔
2245
      Node<K, V> node = data.get(nodeFactory.newLookupKey(entry.getKey()));
1✔
2246
      if ((node == null) || ((value = node.getValue()) == null) || hasExpired(node, now)) {
1✔
2247
        iter.remove();
1✔
2248
      } else {
2249
        setAccessTime(node, now);
1✔
2250
        tryExpireAfterRead(node, entry.getKey(), value, expiry(), now);
1✔
2251
        V refreshed = afterRead(node, now, /* recordHit= */ false);
1✔
2252
        entry.setValue((refreshed == null) ? value : refreshed);
1✔
2253
      }
2254
    }
1✔
2255
    statsCounter().recordHits(result.size());
1✔
2256
    statsCounter().recordMisses(uniqueKeys - result.size());
1✔
2257

2258
    @SuppressWarnings("NullableProblems")
2259
    Map<K, V> unmodifiable = Collections.unmodifiableMap(result);
1✔
2260
    return unmodifiable;
1✔
2261
  }
2262

2263
  @Override
2264
  public void putAll(Map<? extends K, ? extends V> map) {
2265
    map.forEach(this::put);
1✔
2266
  }
1✔
2267

2268
  @Override
2269
  public @Nullable V put(K key, V value) {
2270
    return put(key, value, expiry(), /* onlyIfAbsent= */ false);
1✔
2271
  }
2272

2273
  @Override
2274
  public @Nullable V putIfAbsent(K key, V value) {
2275
    return put(key, value, expiry(), /* onlyIfAbsent= */ true);
1✔
2276
  }
2277

2278
  /**
2279
   * Adds a node to the policy and the data store. If an existing node is found, then its value is
2280
   * updated if allowed.
2281
   *
2282
   * @param key key with which the specified value is to be associated
2283
   * @param value value to be associated with the specified key
2284
   * @param expiry the calculator for the write expiration time
2285
   * @param onlyIfAbsent a write is performed only if the key is not already associated with a value
2286
   * @return the prior value in or null if no mapping was found
2287
   */
2288
  @Nullable V put(K key, V value, Expiry<K, V> expiry, boolean onlyIfAbsent) {
2289
    requireNonNull(key);
1✔
2290
    requireNonNull(value);
1✔
2291

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

2336
      // A read may race with the entry's removal, so that after the entry is acquired it may no
2337
      // longer be usable. A retry will reread from the map and either find an absent mapping, a
2338
      // new entry, or a stale entry.
2339
      if (!prior.isAlive()) {
1✔
2340
        // A reread of the stale entry may occur if the state transition occurred but the map
2341
        // removal was delayed by a context switch, so that this thread spin waits until resolved.
2342
        if ((attempts & MAX_PUT_SPIN_WAIT_ATTEMPTS) != 0) {
1✔
2343
          Thread.onSpinWait();
1✔
2344
          continue;
1✔
2345
        }
2346

2347
        // If the spin wait attempts are exhausted then fallback to a map computation in order to
2348
        // deschedule this thread until the entry's removal completes. If the key was modified
2349
        // while in the map so that its equals or hashCode changed then the contents may be
2350
        // corrupted, where the cache holds an evicted (dead) entry that could not be removed.
2351
        // That is a violation of the Map contract, so we check that the mapping is in the "alive"
2352
        // state while in the computation.
2353
        data.computeIfPresent(lookupKey, (k, n) -> {
1✔
2354
          requireIsAlive(key, n);
1✔
2355
          return n;
1✔
2356
        });
2357
        continue;
1✔
2358
      }
2359

2360
      V oldValue;
2361
      long varTime;
2362
      int oldWeight;
2363
      @Var boolean expired = false;
1✔
2364
      @Var boolean mayUpdate = true;
1✔
2365
      @Var boolean exceedsTolerance = false;
1✔
2366
      synchronized (prior) {
1✔
2367
        if (!prior.isAlive()) {
1✔
2368
          continue;
1✔
2369
        }
2370
        oldValue = prior.getValue();
1✔
2371
        oldWeight = prior.getWeight();
1✔
2372
        if (oldValue == null) {
1✔
2373
          varTime = expireAfterCreate(key, value, expiry, now);
1✔
2374
          notifyEviction(key, null, RemovalCause.COLLECTED);
1✔
2375
        } else if (hasExpired(prior, now)) {
1✔
2376
          expired = true;
1✔
2377
          varTime = expireAfterCreate(key, value, expiry, now);
1✔
2378
          notifyEviction(key, oldValue, RemovalCause.EXPIRED);
1✔
2379
        } else if (onlyIfAbsent) {
1✔
2380
          mayUpdate = false;
1✔
2381
          varTime = expireAfterRead(prior, key, value, expiry, now);
1✔
2382
        } else {
2383
          varTime = expireAfterUpdate(prior, key, value, expiry, now);
1✔
2384
        }
2385

2386
        long expirationTime = isComputingAsync(value) ? (now + ASYNC_EXPIRY) : now;
1✔
2387
        if (mayUpdate) {
1✔
2388
          exceedsTolerance = exceedsWriteTimeTolerance(prior, varTime, now);
1✔
2389
          if (expired || exceedsTolerance) {
1✔
2390
            setWriteTime(prior, isComputingAsync(value) ? (now + ASYNC_EXPIRY) : now);
1✔
2391
          }
2392

2393
          prior.setValue(value, valueReferenceQueue());
1✔
2394
          prior.setWeight(newWeight);
1✔
2395

2396
          discardRefresh(prior.getKeyReference());
1✔
2397
        }
2398

2399
        setVariableTime(prior, varTime);
1✔
2400
        setAccessTime(prior, expirationTime);
1✔
2401
      }
1✔
2402

2403
      if (expired) {
1✔
2404
        notifyRemoval(key, oldValue, RemovalCause.EXPIRED);
1✔
2405
      } else if (oldValue == null) {
1✔
2406
        notifyRemoval(key, /* value= */ null, RemovalCause.COLLECTED);
1✔
2407
      } else if (mayUpdate) {
1✔
2408
        notifyOnReplace(key, oldValue, value);
1✔
2409
      }
2410

2411
      int weightedDifference = mayUpdate ? (newWeight - oldWeight) : 0;
1✔
2412
      if ((oldValue == null) || (weightedDifference != 0) || expired) {
1✔
2413
        afterWrite(new UpdateTask(prior, weightedDifference));
1✔
2414
      } else if (!onlyIfAbsent && exceedsTolerance) {
1✔
2415
        afterWrite(new UpdateTask(prior, weightedDifference));
1✔
2416
      } else {
2417
        afterRead(prior, now, /* recordHit= */ false);
1✔
2418
      }
2419

2420
      return expired ? null : oldValue;
1✔
2421
    }
2422
  }
2423

2424
  @Override
2425
  @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
2426
  public @Nullable V remove(Object key) {
2427
    @SuppressWarnings({"rawtypes", "unchecked"})
2428
    Node<K, V>[] node = new Node[1];
1✔
2429
    @SuppressWarnings({"unchecked", "Varifier"})
2430
    @Nullable K[] oldKey = (K[]) new Object[1];
1✔
2431
    @SuppressWarnings({"unchecked", "Varifier"})
2432
    @Nullable V[] oldValue = (V[]) new Object[1];
1✔
2433
    @Nullable RemovalCause[] cause = new RemovalCause[1];
1✔
2434
    Object lookupKey = nodeFactory.newLookupKey(key);
1✔
2435

2436
    data.computeIfPresent(lookupKey, (k, n) -> {
1✔
2437
      synchronized (n) {
1✔
2438
        requireIsAlive(key, n);
1✔
2439
        oldKey[0] = n.getKey();
1✔
2440
        oldValue[0] = n.getValue();
1✔
2441
        RemovalCause actualCause;
2442
        if ((oldKey[0] == null) || (oldValue[0] == null)) {
1✔
2443
          actualCause = RemovalCause.COLLECTED;
1✔
2444
        } else if (hasExpired(n, expirationTicker().read())) {
1✔
2445
          actualCause = RemovalCause.EXPIRED;
1✔
2446
        } else {
2447
          actualCause= RemovalCause.EXPLICIT;
1✔
2448
        }
2449
        if (actualCause.wasEvicted()) {
1✔
2450
          notifyEviction(oldKey[0], oldValue[0], actualCause);
1✔
2451
        }
2452
        cause[0] = actualCause;
1✔
2453
        discardRefresh(k);
1✔
2454
        node[0] = n;
1✔
2455
        n.retire();
1✔
2456
        return null;
1✔
2457
      }
2458
    });
2459

2460
    if (cause[0] != null) {
1✔
2461
      afterWrite(new RemovalTask(node[0]));
1✔
2462
      notifyRemoval(oldKey[0], oldValue[0], cause[0]);
1✔
2463
    }
2464
    return (cause[0] == RemovalCause.EXPLICIT) ? oldValue[0] : null;
1✔
2465
  }
2466

2467
  @Override
2468
  @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
2469
  public boolean remove(Object key, @Nullable Object value) {
2470
    requireNonNull(key);
1✔
2471
    if (value == null) {
1✔
2472
      return false;
1✔
2473
    }
2474

2475
    @SuppressWarnings({"rawtypes", "unchecked"})
2476
    @Nullable Node<K, V>[] removed = new Node[1];
1✔
2477
    @SuppressWarnings({"unchecked", "Varifier"})
2478
    @Nullable K[] oldKey = (K[]) new Object[1];
1✔
2479
    @SuppressWarnings({"unchecked", "Varifier"})
2480
    @Nullable V[] oldValue = (V[]) new Object[1];
1✔
2481
    RemovalCause[] cause = new RemovalCause[1];
1✔
2482
    Object lookupKey = nodeFactory.newLookupKey(key);
1✔
2483

2484
    data.computeIfPresent(lookupKey, (kR, node) -> {
1✔
2485
      synchronized (node) {
1✔
2486
        requireIsAlive(key, node);
1✔
2487
        oldKey[0] = node.getKey();
1✔
2488
        oldValue[0] = node.getValue();
1✔
2489
        if ((oldKey[0] == null) || (oldValue[0] == null)) {
1✔
2490
          cause[0] = RemovalCause.COLLECTED;
1✔
2491
        } else if (hasExpired(node, expirationTicker().read())) {
1✔
2492
          cause[0] = RemovalCause.EXPIRED;
1✔
2493
        } else if (node.containsValue(value)) {
1✔
2494
          cause[0] = RemovalCause.EXPLICIT;
1✔
2495
        } else {
2496
          return node;
1✔
2497
        }
2498
        if (cause[0].wasEvicted()) {
1✔
2499
          notifyEviction(oldKey[0], oldValue[0], cause[0]);
1✔
2500
        }
2501
        discardRefresh(kR);
1✔
2502
        removed[0] = node;
1✔
2503
        node.retire();
1✔
2504
        return null;
1✔
2505
      }
2506
    });
2507

2508
    if (removed[0] == null) {
1✔
2509
      return false;
1✔
2510
    }
2511
    afterWrite(new RemovalTask(removed[0]));
1✔
2512
    notifyRemoval(oldKey[0], oldValue[0], cause[0]);
1✔
2513

2514
    return (cause[0] == RemovalCause.EXPLICIT);
1✔
2515
  }
2516

2517
  @Override
2518
  @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
2519
  public @Nullable V replace(K key, V value) {
2520
    requireNonNull(key);
1✔
2521
    requireNonNull(value);
1✔
2522

2523
    var now = new long[1];
1✔
2524
    var oldWeight = new int[1];
1✔
2525
    var exceedsTolerance = new boolean[1];
1✔
2526
    @SuppressWarnings({"unchecked", "Varifier"})
2527
    @Nullable K[] nodeKey = (K[]) new Object[1];
1✔
2528
    @SuppressWarnings({"unchecked", "Varifier"})
2529
    @Nullable V[] oldValue = (V[]) new Object[1];
1✔
2530
    int weight = weigher.weigh(key, value);
1✔
2531
    Node<K, V> node = data.computeIfPresent(nodeFactory.newLookupKey(key), (k, n) -> {
1✔
2532
      synchronized (n) {
1✔
2533
        requireIsAlive(key, n);
1✔
2534
        nodeKey[0] = n.getKey();
1✔
2535
        oldValue[0] = n.getValue();
1✔
2536
        oldWeight[0] = n.getWeight();
1✔
2537
        if ((nodeKey[0] == null) || (oldValue[0] == null)
1✔
2538
            || hasExpired(n, now[0] = expirationTicker().read())) {
1✔
2539
          oldValue[0] = null;
1✔
2540
          return n;
1✔
2541
        }
2542

2543
        long varTime = expireAfterUpdate(n, key, value, expiry(), now[0]);
1✔
2544
        n.setValue(value, valueReferenceQueue());
1✔
2545
        n.setWeight(weight);
1✔
2546

2547
        long expirationTime = isComputingAsync(value) ? (now[0] + ASYNC_EXPIRY) : now[0];
1✔
2548
        exceedsTolerance[0] = exceedsWriteTimeTolerance(n, varTime, expirationTime);
1✔
2549
        if (exceedsTolerance[0]) {
1✔
2550
          setWriteTime(n, expirationTime);
1✔
2551
        }
2552
        setAccessTime(n, expirationTime);
1✔
2553
        setVariableTime(n, varTime);
1✔
2554

2555
        discardRefresh(k);
1✔
2556
        return n;
1✔
2557
      }
2558
    });
2559

2560
    if ((node == null) || (nodeKey[0] == null) || (oldValue[0] == null)) {
1✔
2561
      return null;
1✔
2562
    }
2563

2564
    int weightedDifference = (weight - oldWeight[0]);
1✔
2565
    if (exceedsTolerance[0] || (weightedDifference != 0)) {
1✔
2566
      afterWrite(new UpdateTask(node, weightedDifference));
1✔
2567
    } else {
2568
      afterRead(node, now[0], /* recordHit= */ false);
1✔
2569
    }
2570

2571
    notifyOnReplace(nodeKey[0], oldValue[0], value);
1✔
2572
    return oldValue[0];
1✔
2573
  }
2574

2575
  @Override
2576
  public boolean replace(K key, V oldValue, V newValue) {
2577
    return replace(key, oldValue, newValue, /* shouldDiscardRefresh= */ true);
1✔
2578
  }
2579

2580
  @Override
2581
  @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
2582
  public boolean replace(K key, V oldValue, V newValue, boolean shouldDiscardRefresh) {
2583
    requireNonNull(key);
1✔
2584
    requireNonNull(oldValue);
1✔
2585
    requireNonNull(newValue);
1✔
2586

2587
    var now = new long[1];
1✔
2588
    var oldWeight = new int[1];
1✔
2589
    var exceedsTolerance = new boolean[1];
1✔
2590
    @SuppressWarnings({"unchecked", "Varifier"})
2591
    @Nullable K[] nodeKey = (K[]) new Object[1];
1✔
2592
    @SuppressWarnings({"unchecked", "Varifier"})
2593
    @Nullable V[] prevValue = (V[]) new Object[1];
1✔
2594

2595
    int weight = weigher.weigh(key, newValue);
1✔
2596
    Node<K, V> node = data.computeIfPresent(nodeFactory.newLookupKey(key), (k, n) -> {
1✔
2597
      synchronized (n) {
1✔
2598
        requireIsAlive(key, n);
1✔
2599
        nodeKey[0] = n.getKey();
1✔
2600
        prevValue[0] = n.getValue();
1✔
2601
        oldWeight[0] = n.getWeight();
1✔
2602
        if ((nodeKey[0] == null) || (prevValue[0] == null) || !n.containsValue(oldValue)
1✔
2603
            || hasExpired(n, now[0] = expirationTicker().read())) {
1✔
2604
          prevValue[0] = null;
1✔
2605
          return n;
1✔
2606
        }
2607

2608
        long varTime = expireAfterUpdate(n, key, newValue, expiry(), now[0]);
1✔
2609
        n.setValue(newValue, valueReferenceQueue());
1✔
2610
        n.setWeight(weight);
1✔
2611

2612
        long expirationTime = isComputingAsync(newValue) ? (now[0] + ASYNC_EXPIRY) : now[0];
1✔
2613
        exceedsTolerance[0] = exceedsWriteTimeTolerance(n, varTime, expirationTime);
1✔
2614
        if (exceedsTolerance[0]) {
1✔
2615
          setWriteTime(n, expirationTime);
1✔
2616
        }
2617
        setAccessTime(n, expirationTime);
1✔
2618
        setVariableTime(n, varTime);
1✔
2619

2620
        if (shouldDiscardRefresh) {
1✔
2621
          discardRefresh(k);
1✔
2622
        }
2623
      }
1✔
2624
      return n;
1✔
2625
    });
2626

2627
    if ((node == null) || (nodeKey[0] == null) || (prevValue[0] == null)) {
1✔
2628
      return false;
1✔
2629
    }
2630

2631
    int weightedDifference = (weight - oldWeight[0]);
1✔
2632
    if (exceedsTolerance[0] || (weightedDifference != 0)) {
1✔
2633
      afterWrite(new UpdateTask(node, weightedDifference));
1✔
2634
    } else {
2635
      afterRead(node, now[0], /* recordHit= */ false);
1✔
2636
    }
2637

2638
    notifyOnReplace(nodeKey[0], prevValue[0], newValue);
1✔
2639
    return true;
1✔
2640
  }
2641

2642
  @Override
2643
  public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
2644
    requireNonNull(function);
1✔
2645

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

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

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

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

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

2718
      synchronized (n) {
1✔
2719
        requireIsAlive(key, n);
1✔
2720
        nodeKey[0] = n.getKey();
1✔
2721
        weight[0] = n.getWeight();
1✔
2722
        oldValue[0] = n.getValue();
1✔
2723
        RemovalCause actualCause;
2724
        if ((nodeKey[0] == null) || (oldValue[0] == null)) {
1✔
2725
          actualCause = RemovalCause.COLLECTED;
1✔
2726
        } else if (hasExpired(n, now[0])) {
1✔
2727
          actualCause = RemovalCause.EXPIRED;
1✔
2728
        } else {
2729
          return n;
1✔
2730
        }
2731

2732
        cause[0] = actualCause;
1✔
2733
        notifyEviction(nodeKey[0], oldValue[0], actualCause);
1✔
2734
        newValue[0] = mappingFunction.apply(key);
1✔
2735
        if (newValue[0] == null) {
1✔
2736
          discardRefresh(k);
1✔
2737
          removed[0] = n;
1✔
2738
          n.retire();
1✔
2739
          return null;
1✔
2740
        }
2741
        now[0] = expirationTicker().read();
1✔
2742
        weight[1] = weigher.weigh(key, newValue[0]);
1✔
2743
        long varTime = expireAfterCreate(key, newValue[0], expiry(), now[0]);
1✔
2744

2745
        n.setValue(newValue[0], valueReferenceQueue());
1✔
2746
        n.setWeight(weight[1]);
1✔
2747

2748
        long expirationTime = isComputingAsync(newValue[0]) ? (now[0] + ASYNC_EXPIRY) : now[0];
1✔
2749
        setAccessTime(n, expirationTime);
1✔
2750
        setWriteTime(n, expirationTime);
1✔
2751
        setVariableTime(n, varTime);
1✔
2752

2753
        discardRefresh(k);
1✔
2754
        return n;
1✔
2755
      }
2756
    });
2757

2758
    if (cause[0] != null) {
1✔
2759
      statsCounter().recordEviction(weight[0], cause[0]);
1✔
2760
      notifyRemoval(nodeKey[0], oldValue[0], cause[0]);
1✔
2761
    }
2762
    if (node == null) {
1✔
2763
      if (removed[0] != null) {
1✔
2764
        afterWrite(new RemovalTask(removed[0]));
1✔
2765
      }
2766
      return null;
1✔
2767
    }
2768
    if ((oldValue[0] != null) && (newValue[0] == null)) {
1✔
2769
      if (!isComputingAsync(oldValue[0])) {
1✔
2770
        tryExpireAfterRead(node, key, oldValue[0], expiry(), now[0]);
1✔
2771
        setAccessTime(node, now[0]);
1✔
2772
      }
2773

2774
      afterRead(node, now[0], /* recordHit= */ recordStats);
1✔
2775
      return oldValue[0];
1✔
2776
    }
2777
    if ((oldValue[0] == null) && (cause[0] == null)) {
1✔
2778
      afterWrite(new AddTask(node, weight[1]));
1✔
2779
    } else {
2780
      int weightedDifference = (weight[1] - weight[0]);
1✔
2781
      afterWrite(new UpdateTask(node, weightedDifference));
1✔
2782
    }
2783

2784
    return newValue[0];
1✔
2785
  }
2786

2787
  @Override
2788
  public @Nullable V computeIfPresent(K key,
2789
      BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
2790
    requireNonNull(key);
1✔
2791
    requireNonNull(remappingFunction);
1✔
2792

2793
    // An optimistic fast path to avoid unnecessary locking
2794
    Object lookupKey = nodeFactory.newLookupKey(key);
1✔
2795
    @Nullable Node<K, V> node = data.get(lookupKey);
1✔
2796
    long now;
2797
    if (node == null) {
1✔
2798
      return null;
1✔
2799
    } else if ((node.getValue() == null) || hasExpired(node, (now = expirationTicker().read()))) {
1✔
2800
      scheduleDrainBuffers();
1✔
2801
      return null;
1✔
2802
    }
2803

2804
    BiFunction<? super K, ? super V, ? extends V> statsAwareRemappingFunction =
1✔
2805
        statsAware(remappingFunction, /* recordLoad= */ true, /* recordLoadFailure= */ true);
1✔
2806
    return remap(key, lookupKey, statsAwareRemappingFunction,
1✔
2807
        expiry(), new long[] { now }, /* computeIfAbsent= */ false);
1✔
2808
  }
2809

2810
  @Override
2811
  public @Nullable V compute(K key,
2812
      BiFunction<? super K, ? super V, ? extends @Nullable V> remappingFunction,
2813
      @Nullable Expiry<? super K, ? super V> expiry, boolean recordLoad,
2814
      boolean recordLoadFailure) {
2815
    requireNonNull(key);
1✔
2816
    requireNonNull(remappingFunction);
1✔
2817

2818
    long[] now = { expirationTicker().read() };
1✔
2819
    Object keyRef = nodeFactory.newReferenceKey(key, keyReferenceQueue());
1✔
2820
    BiFunction<? super K, ? super V, ? extends V> statsAwareRemappingFunction =
1✔
2821
        statsAware(remappingFunction, recordLoad, recordLoadFailure);
1✔
2822
    return remap(key, keyRef, statsAwareRemappingFunction,
1✔
2823
        expiry, now, /* computeIfAbsent= */ true);
2824
  }
2825

2826
  @Override
2827
  public @Nullable V merge(K key, V value,
2828
      BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
2829
    requireNonNull(key);
1✔
2830
    requireNonNull(value);
1✔
2831
    requireNonNull(remappingFunction);
1✔
2832

2833
    long[] now = { expirationTicker().read() };
1✔
2834
    Object keyRef = nodeFactory.newReferenceKey(key, keyReferenceQueue());
1✔
2835
    BiFunction<? super K, ? super @Nullable V, ? extends @Nullable V> mergeFunction =
1✔
2836
        (k, oldValue) -> (oldValue == null)
1✔
2837
          ? value
1✔
2838
          : statsAware(remappingFunction).apply(oldValue, value);
1✔
2839
    return remap(key, keyRef, mergeFunction, expiry(), now, /* computeIfAbsent= */ true);
1✔
2840
  }
2841

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

2871
    var weight = new int[2]; // old, new
1✔
2872
    var cause = new RemovalCause[1];
1✔
2873
    var exceedsTolerance = new boolean[1];
1✔
2874

2875
    Node<K, V> node = data.compute(keyRef, (kr, n) -> {
1✔
2876
      if (n == null) {
1✔
2877
        if (!computeIfAbsent) {
1✔
2878
          return null;
1✔
2879
        }
2880
        newValue[0] = remappingFunction.apply(key, null);
1✔
2881
        if (newValue[0] == null) {
1✔
2882
          return null;
1✔
2883
        }
2884
        now[0] = expirationTicker().read();
1✔
2885
        weight[1] = weigher.weigh(key, newValue[0]);
1✔
2886
        long varTime = expireAfterCreate(key, newValue[0], expiry, now[0]);
1✔
2887
        var created = nodeFactory.newNode(keyRef, newValue[0],
1✔
2888
            valueReferenceQueue(), weight[1], now[0]);
1✔
2889

2890
        long expirationTime = isComputingAsync(newValue[0]) ? (now[0] + ASYNC_EXPIRY) : now[0];
1✔
2891
        setAccessTime(created, expirationTime);
1✔
2892
        setWriteTime(created, expirationTime);
1✔
2893
        setVariableTime(created, varTime);
1✔
2894
        discardRefresh(kr);
1✔
2895
        return created;
1✔
2896
      }
2897

2898
      synchronized (n) {
1✔
2899
        requireIsAlive(key, n);
1✔
2900
        nodeKey[0] = n.getKey();
1✔
2901
        oldValue[0] = n.getValue();
1✔
2902
        if ((nodeKey[0] == null) || (oldValue[0] == null)) {
1✔
2903
          cause[0] = RemovalCause.COLLECTED;
1✔
2904
        } else if (hasExpired(n, expirationTicker().read())) {
1✔
2905
          cause[0] = RemovalCause.EXPIRED;
1✔
2906
        }
2907
        if (cause[0] != null) {
1✔
2908
          notifyEviction(nodeKey[0], oldValue[0], cause[0]);
1✔
2909
          if (!computeIfAbsent) {
1✔
2910
            removed[0] = n;
1✔
2911
            n.retire();
1✔
2912
            return null;
1✔
2913
          }
2914
        }
2915

2916
        newValue[0] = remappingFunction.apply(nodeKey[0],
1✔
2917
            (cause[0] == null) ? oldValue[0] : null);
1✔
2918
        if (newValue[0] == null) {
1✔
2919
          if (cause[0] == null) {
1✔
2920
            cause[0] = RemovalCause.EXPLICIT;
1✔
2921
            discardRefresh(kr);
1✔
2922
          }
2923
          removed[0] = n;
1✔
2924
          n.retire();
1✔
2925
          return null;
1✔
2926
        }
2927

2928
        long varTime;
2929
        weight[0] = n.getWeight();
1✔
2930
        weight[1] = weigher.weigh(key, newValue[0]);
1✔
2931
        now[0] = expirationTicker().read();
1✔
2932
        if (cause[0] == null) {
1✔
2933
          if (newValue[0] != oldValue[0]) {
1✔
2934
            cause[0] = RemovalCause.REPLACED;
1✔
2935
          }
2936
          varTime = expireAfterUpdate(n, key, newValue[0], expiry, now[0]);
1✔
2937
        } else {
2938
          varTime = expireAfterCreate(key, newValue[0], expiry, now[0]);
1✔
2939
        }
2940

2941
        n.setValue(newValue[0], valueReferenceQueue());
1✔
2942
        n.setWeight(weight[1]);
1✔
2943

2944
        long expirationTime = isComputingAsync(newValue[0]) ? (now[0] + ASYNC_EXPIRY) : now[0];
1✔
2945
        exceedsTolerance[0] = exceedsWriteTimeTolerance(n, varTime, expirationTime);
1✔
2946
        if (((cause[0] != null) && cause[0].wasEvicted()) || exceedsTolerance[0]) {
1✔
2947
          setWriteTime(n, expirationTime);
1✔
2948
        }
2949
        setAccessTime(n, expirationTime);
1✔
2950
        setVariableTime(n, varTime);
1✔
2951

2952
        discardRefresh(kr);
1✔
2953
        return n;
1✔
2954
      }
2955
    });
2956

2957
    if (cause[0] != null) {
1✔
2958
      if (cause[0] == RemovalCause.REPLACED) {
1✔
2959
        requireNonNull(newValue[0]);
1✔
2960
        notifyOnReplace(key, oldValue[0], newValue[0]);
1✔
2961
      } else {
2962
        if (cause[0].wasEvicted()) {
1✔
2963
          statsCounter().recordEviction(weight[0], cause[0]);
1✔
2964
        }
2965
        notifyRemoval(nodeKey[0], oldValue[0], cause[0]);
1✔
2966
      }
2967
    }
2968

2969
    if (removed[0] != null) {
1✔
2970
      afterWrite(new RemovalTask(removed[0]));
1✔
2971
    } else if (node == null) {
1✔
2972
      // absent and not computable
2973
    } else if ((oldValue[0] == null) && (cause[0] == null)) {
1✔
2974
      afterWrite(new AddTask(node, weight[1]));
1✔
2975
    } else {
2976
      int weightedDifference = weight[1] - weight[0];
1✔
2977
      if (exceedsTolerance[0] || (weightedDifference != 0)) {
1✔
2978
        afterWrite(new UpdateTask(node, weightedDifference));
1✔
2979
      } else {
2980
        afterRead(node, now[0], /* recordHit= */ false);
1✔
2981
        if ((cause[0] != null) && cause[0].wasEvicted()) {
1✔
2982
          scheduleDrainBuffers();
1✔
2983
        }
2984
      }
2985
    }
2986

2987
    return newValue[0];
1✔
2988
  }
2989

2990
  @Override
2991
  public void forEach(BiConsumer<? super K, ? super V> action) {
2992
    requireNonNull(action);
1✔
2993

2994
    for (var iterator = new EntryIterator<>(this); iterator.hasNext();) {
1✔
2995
      action.accept(iterator.key, iterator.value);
1✔
2996
      iterator.advance();
1✔
2997
    }
2998
  }
1✔
2999

3000
  @Override
3001
  public Set<K> keySet() {
3002
    Set<K> ks = keySet;
1✔
3003
    return (ks == null) ? (keySet = new KeySetView<>(this)) : ks;
1✔
3004
  }
3005

3006
  @Override
3007
  public Collection<V> values() {
3008
    Collection<V> vs = values;
1✔
3009
    return (vs == null) ? (values = new ValuesView<>(this)) : vs;
1✔
3010
  }
3011

3012
  @Override
3013
  public Set<Entry<K, V>> entrySet() {
3014
    Set<Entry<K, V>> es = entrySet;
1✔
3015
    return (es == null) ? (entrySet = new EntrySetView<>(this)) : es;
1✔
3016
  }
3017

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

3048
    var map = (Map<?, ?>) o;
1✔
3049
    if (size() != map.size()) {
1✔
3050
      return false;
1✔
3051
    }
3052

3053
    long now = expirationTicker().read();
1✔
3054
    for (var node : data.values()) {
1✔
3055
      K key = node.getKey();
1✔
3056
      V value = node.getValue();
1✔
3057
      if ((key == null) || (value == null)
1✔
3058
          || !node.isAlive() || hasExpired(node, now)) {
1✔
3059
        scheduleDrainBuffers();
1✔
3060
        return false;
1✔
3061
      } else {
3062
        var val = map.get(key);
1✔
3063
        if ((val == null) || ((val != value) && !val.equals(value))) {
1✔
3064
          return false;
1✔
3065
        }
3066
      }
3067
    }
1✔
3068
    return true;
1✔
3069
  }
3070

3071
  @Override
3072
  public int hashCode() {
3073
    @Var int hash = 0;
1✔
3074
    long now = expirationTicker().read();
1✔
3075
    for (var node : data.values()) {
1✔
3076
      K key = node.getKey();
1✔
3077
      V value = node.getValue();
1✔
3078
      if ((key == null) || (value == null)
1✔
3079
          || !node.isAlive() || hasExpired(node, now)) {
1✔
3080
        scheduleDrainBuffers();
1✔
3081
      } else {
3082
        hash += key.hashCode() ^ value.hashCode();
1✔
3083
      }
3084
    }
1✔
3085
    return hash;
1✔
3086
  }
3087

3088
  @Override
3089
  public String toString() {
3090
    var result = new StringBuilder().append('{');
1✔
3091
    long now = expirationTicker().read();
1✔
3092
    for (var node : data.values()) {
1✔
3093
      K key = node.getKey();
1✔
3094
      V value = node.getValue();
1✔
3095
      if ((key == null) || (value == null)
1✔
3096
          || !node.isAlive() || hasExpired(node, now)) {
1✔
3097
        scheduleDrainBuffers();
1✔
3098
      } else {
3099
        if (result.length() != 1) {
1✔
3100
          result.append(',').append(' ');
1✔
3101
        }
3102
        result.append((key == this) ? "(this Map)" : key);
1✔
3103
        result.append('=');
1✔
3104
        result.append((value == this) ? "(this Map)" : value);
1✔
3105
      }
3106
    }
1✔
3107
    return result.append('}').toString();
1✔
3108
  }
3109

3110
  /**
3111
   * Returns the computed result from the ordered traversal of the cache entries.
3112
   *
3113
   * @param hottest the coldest or hottest iteration order
3114
   * @param transformer a function that unwraps the value
3115
   * @param mappingFunction the mapping function to compute a value
3116
   * @return the computed value
3117
   */
3118
  @SuppressWarnings("GuardedByChecker")
3119
  <T> T evictionOrder(boolean hottest, Function<@Nullable V, @Nullable V> transformer,
3120
      Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
3121
    Comparator<Node<K, V>> comparator = Comparator.comparingInt(node -> {
1✔
3122
      var keyRef = node.getKeyReferenceOrNull();
1✔
3123
      return (keyRef == null) ? 0 : frequencySketch().frequency(keyRef);
1✔
3124
    });
3125
    Iterable<Node<K, V>> iterable;
3126
    if (hottest) {
1✔
3127
      iterable = () -> {
1✔
3128
        var secondary = PeekingIterator.comparing(
1✔
3129
            accessOrderProbationDeque().descendingIterator(),
1✔
3130
            accessOrderWindowDeque().descendingIterator(), comparator);
1✔
3131
        return PeekingIterator.concat(
1✔
3132
            accessOrderProtectedDeque().descendingIterator(), secondary);
1✔
3133
      };
3134
    } else {
3135
      iterable = () -> {
1✔
3136
        var primary = PeekingIterator.comparing(
1✔
3137
            accessOrderWindowDeque().iterator(), accessOrderProbationDeque().iterator(),
1✔
3138
            comparator.reversed());
1✔
3139
        return PeekingIterator.concat(primary, accessOrderProtectedDeque().iterator());
1✔
3140
      };
3141
    }
3142
    return snapshot(iterable, transformer, mappingFunction);
1✔
3143
  }
3144

3145
  /**
3146
   * Returns the computed result from the ordered traversal of the cache entries.
3147
   *
3148
   * @param oldest the youngest or oldest iteration order
3149
   * @param transformer a function that unwraps the value
3150
   * @param mappingFunction the mapping function to compute a value
3151
   * @return the computed value
3152
   */
3153
  @SuppressWarnings("GuardedByChecker")
3154
  <T> T expireAfterAccessOrder(boolean oldest, Function<@Nullable V, @Nullable V> transformer,
3155
      Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
3156
    Iterable<Node<K, V>> iterable;
3157
    if (evicts()) {
1✔
3158
      iterable = () -> {
1✔
3159
        @Var Comparator<Node<K, V>> comparator = Comparator.comparingLong(Node::getAccessTime);
1✔
3160
        PeekingIterator<Node<K, V>> first;
3161
        PeekingIterator<Node<K, V>> second;
3162
        PeekingIterator<Node<K, V>> third;
3163
        if (oldest) {
1✔
3164
          first = accessOrderWindowDeque().iterator();
1✔
3165
          second = accessOrderProbationDeque().iterator();
1✔
3166
          third = accessOrderProtectedDeque().iterator();
1✔
3167
        } else {
3168
          comparator = comparator.reversed();
1✔
3169
          first = accessOrderWindowDeque().descendingIterator();
1✔
3170
          second = accessOrderProbationDeque().descendingIterator();
1✔
3171
          third = accessOrderProtectedDeque().descendingIterator();
1✔
3172
        }
3173
        return PeekingIterator.comparing(
1✔
3174
            PeekingIterator.comparing(first, second, comparator), third, comparator);
1✔
3175
      };
3176
    } else {
3177
      iterable = oldest
1✔
3178
          ? accessOrderWindowDeque()
1✔
3179
          : accessOrderWindowDeque()::descendingIterator;
1✔
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 iterable the supplier of the entries in the cache
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
  <T> T snapshot(Iterable<Node<K, V>> iterable, Function<@Nullable V, @Nullable V> transformer,
3193
      Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
3194
    requireNonNull(mappingFunction);
1✔
3195
    requireNonNull(transformer);
1✔
3196
    requireNonNull(iterable);
1✔
3197

3198
    evictionLock.lock();
1✔
3199
    try {
3200
      maintenance(/* ignored */ null);
1✔
3201

3202
      // Obtain the iterator as late as possible for modification count checking
3203
      try (var stream = StreamSupport.stream(Spliterators.spliteratorUnknownSize(
1✔
3204
           iterable.iterator(), DISTINCT | ORDERED | NONNULL | IMMUTABLE), /* parallel= */ false)) {
1✔
3205
        return mappingFunction.apply(stream
1✔
3206
            .map(node -> nodeToCacheEntry(node, transformer))
1✔
3207
            .filter(Objects::nonNull));
1✔
3208
      }
3209
    } finally {
3210
      evictionLock.unlock();
1✔
3211
      rescheduleCleanUpIfIncomplete();
1✔
3212
    }
3213
  }
3214

3215
  /** Returns an entry for the given node if it can be used externally, else null. */
3216
  @Nullable CacheEntry<K, V> nodeToCacheEntry(
3217
      Node<K, V> node, Function<@Nullable V, @Nullable V> transformer) {
3218
    V value = transformer.apply(node.getValue());
1✔
3219
    K key = node.getKey();
1✔
3220
    long now;
3221
    if ((key == null) || (value == null) || !node.isAlive()
1✔
3222
        || hasExpired(node, (now = expirationTicker().read()))) {
1✔
3223
      return null;
1✔
3224
    }
3225

3226
    @Var long expiresAfter = Long.MAX_VALUE;
1✔
3227
    if (expiresAfterAccess()) {
1✔
3228
      expiresAfter = Math.min(expiresAfter, now - node.getAccessTime() + expiresAfterAccessNanos());
1✔
3229
    }
3230
    if (expiresAfterWrite()) {
1✔
3231
      expiresAfter = Math.min(expiresAfter,
1✔
3232
          (now & ~1L) - (node.getWriteTime() & ~1L) + expiresAfterWriteNanos());
1✔
3233
    }
3234
    if (expiresVariable()) {
1✔
3235
      expiresAfter = node.getVariableTime() - now;
1✔
3236
    }
3237

3238
    long refreshableAt = refreshAfterWrite()
1✔
3239
        ? node.getWriteTime() + refreshAfterWriteNanos()
1✔
3240
        : now + Long.MAX_VALUE;
1✔
3241
    int weight = node.getPolicyWeight();
1✔
3242
    return SnapshotEntry.forEntry(key, value, now, weight, now + expiresAfter, refreshableAt);
1✔
3243
  }
3244

3245
  /** A function that produces an unmodifiable map up to the limit in stream order. */
3246
  static final class SizeLimiter<K, V> implements Function<Stream<CacheEntry<K, V>>, Map<K, V>> {
3247
    private final int expectedSize;
3248
    private final long limit;
3249

3250
    SizeLimiter(int expectedSize, long limit) {
1✔
3251
      requireArgument(limit >= 0);
1✔
3252
      this.expectedSize = expectedSize;
1✔
3253
      this.limit = limit;
1✔
3254
    }
1✔
3255

3256
    @Override
3257
    public Map<K, V> apply(Stream<CacheEntry<K, V>> stream) {
3258
      var map = new LinkedHashMap<K, V>(calculateHashMapCapacity(expectedSize));
1✔
3259
      stream.limit(limit).forEach(entry -> map.put(entry.getKey(), entry.getValue()));
1✔
3260
      return Collections.unmodifiableMap(map);
1✔
3261
    }
3262
  }
3263

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

3268
    private long weightedSize;
3269

3270
    WeightLimiter(long weightLimit) {
1✔
3271
      requireArgument(weightLimit >= 0);
1✔
3272
      this.weightLimit = weightLimit;
1✔
3273
    }
1✔
3274

3275
    @Override
3276
    public Map<K, V> apply(Stream<CacheEntry<K, V>> stream) {
3277
      var map = new LinkedHashMap<K, V>();
1✔
3278
      stream.takeWhile(entry -> {
1✔
3279
        weightedSize = Math.addExact(weightedSize, entry.weight());
1✔
3280
        return (weightedSize <= weightLimit);
1✔
3281
      }).forEach(entry -> map.put(entry.getKey(), entry.getValue()));
1✔
3282
      return Collections.unmodifiableMap(map);
1✔
3283
    }
3284
  }
3285

3286
  /** An adapter to safely externalize the keys. */
3287
  static final class KeySetView<K, V> extends AbstractSet<K> {
3288
    final BoundedLocalCache<K, V> cache;
3289

3290
    KeySetView(BoundedLocalCache<K, V> cache) {
1✔
3291
      this.cache = requireNonNull(cache);
1✔
3292
    }
1✔
3293

3294
    @Override
3295
    public int size() {
3296
      return cache.size();
1✔
3297
    }
3298

3299
    @Override
3300
    public void clear() {
3301
      cache.clear();
1✔
3302
    }
1✔
3303

3304
    @Override
3305
    @SuppressWarnings("SuspiciousMethodCalls")
3306
    public boolean contains(Object o) {
3307
      return cache.containsKey(o);
1✔
3308
    }
3309

3310
    @Override
3311
    public boolean removeAll(Collection<?> collection) {
3312
      requireNonNull(collection);
1✔
3313
      @Var boolean modified = false;
1✔
3314
      if ((collection instanceof Set<?>) && (collection.size() > size())) {
1✔
3315
        for (K key : this) {
1✔
3316
          if (collection.contains(key)) {
1✔
3317
            modified |= remove(key);
1✔
3318
          }
3319
        }
1✔
3320
      } else {
3321
        for (var item : collection) {
1✔
3322
          modified |= (item != null) && remove(item);
1✔
3323
        }
1✔
3324
      }
3325
      return modified;
1✔
3326
    }
3327

3328
    @Override
3329
    public boolean remove(Object o) {
3330
      return (cache.remove(o) != null);
1✔
3331
    }
3332

3333
    @Override
3334
    public boolean removeIf(Predicate<? super K> filter) {
3335
      requireNonNull(filter);
1✔
3336
      @Var boolean modified = false;
1✔
3337
      for (K key : this) {
1✔
3338
        if (filter.test(key) && remove(key)) {
1✔
3339
          modified = true;
1✔
3340
        }
3341
      }
1✔
3342
      return modified;
1✔
3343
    }
3344

3345
    @Override
3346
    public boolean retainAll(Collection<?> collection) {
3347
      requireNonNull(collection);
1✔
3348
      @Var boolean modified = false;
1✔
3349
      for (K key : this) {
1✔
3350
        if (!collection.contains(key) && remove(key)) {
1✔
3351
          modified = true;
1✔
3352
        }
3353
      }
1✔
3354
      return modified;
1✔
3355
    }
3356

3357
    @Override
3358
    public Iterator<K> iterator() {
3359
      return new KeyIterator<>(cache);
1✔
3360
    }
3361

3362
    @Override
3363
    public Spliterator<K> spliterator() {
3364
      return new KeySpliterator<>(cache);
1✔
3365
    }
3366
  }
3367

3368
  /** An adapter to safely externalize the key iterator. */
3369
  static final class KeyIterator<K, V> implements Iterator<K> {
3370
    final EntryIterator<K, V> iterator;
3371

3372
    KeyIterator(BoundedLocalCache<K, V> cache) {
1✔
3373
      this.iterator = new EntryIterator<>(cache);
1✔
3374
    }
1✔
3375

3376
    @Override
3377
    public boolean hasNext() {
3378
      return iterator.hasNext();
1✔
3379
    }
3380

3381
    @Override
3382
    public K next() {
3383
      return iterator.nextKey();
1✔
3384
    }
3385

3386
    @Override
3387
    public void remove() {
3388
      iterator.remove();
1✔
3389
    }
1✔
3390
  }
3391

3392
  /** An adapter to safely externalize the key spliterator. */
3393
  static final class KeySpliterator<K, V> implements Spliterator<K> {
3394
    final Spliterator<Node<K, V>> spliterator;
3395
    final BoundedLocalCache<K, V> cache;
3396

3397
    KeySpliterator(BoundedLocalCache<K, V> cache) {
3398
      this(cache, cache.data.values().spliterator());
1✔
3399
    }
1✔
3400

3401
    KeySpliterator(BoundedLocalCache<K, V> cache, Spliterator<Node<K, V>> spliterator) {
1✔
3402
      this.spliterator = requireNonNull(spliterator);
1✔
3403
      this.cache = requireNonNull(cache);
1✔
3404
    }
1✔
3405

3406
    @Override
3407
    public void forEachRemaining(Consumer<? super K> action) {
3408
      requireNonNull(action);
1✔
3409
      Consumer<Node<K, V>> consumer = node -> {
1✔
3410
        K key = node.getKey();
1✔
3411
        V value = node.getValue();
1✔
3412
        long now = cache.expirationTicker().read();
1✔
3413
        if ((key != null) && (value != null) && node.isAlive() && !cache.hasExpired(node, now)) {
1✔
3414
          action.accept(key);
1✔
3415
        }
3416
      };
1✔
3417
      spliterator.forEachRemaining(consumer);
1✔
3418
    }
1✔
3419

3420
    @Override
3421
    public boolean tryAdvance(Consumer<? super K> action) {
3422
      requireNonNull(action);
1✔
3423
      boolean[] advanced = { false };
1✔
3424
      Consumer<Node<K, V>> consumer = node -> {
1✔
3425
        K key = node.getKey();
1✔
3426
        V value = node.getValue();
1✔
3427
        long now = cache.expirationTicker().read();
1✔
3428
        if ((key != null) && (value != null) && node.isAlive() && !cache.hasExpired(node, now)) {
1✔
3429
          action.accept(key);
1✔
3430
          advanced[0] = true;
1✔
3431
        }
3432
      };
1✔
3433
      while (spliterator.tryAdvance(consumer)) {
1✔
3434
        if (advanced[0]) {
1✔
3435
          return true;
1✔
3436
        }
3437
      }
3438
      return false;
1✔
3439
    }
3440

3441
    @Override
3442
    public @Nullable Spliterator<K> trySplit() {
3443
      Spliterator<Node<K, V>> split = spliterator.trySplit();
1✔
3444
      return (split == null) ? null : new KeySpliterator<>(cache, split);
1✔
3445
    }
3446

3447
    @Override
3448
    public long estimateSize() {
3449
      return spliterator.estimateSize();
1✔
3450
    }
3451

3452
    @Override
3453
    public int characteristics() {
3454
      return DISTINCT | CONCURRENT | NONNULL;
1✔
3455
    }
3456
  }
3457

3458
  /** An adapter to safely externalize the values. */
3459
  static final class ValuesView<K, V> extends AbstractCollection<V> {
3460
    final BoundedLocalCache<K, V> cache;
3461

3462
    ValuesView(BoundedLocalCache<K, V> cache) {
1✔
3463
      this.cache = requireNonNull(cache);
1✔
3464
    }
1✔
3465

3466
    @Override
3467
    public int size() {
3468
      return cache.size();
1✔
3469
    }
3470

3471
    @Override
3472
    public void clear() {
3473
      cache.clear();
1✔
3474
    }
1✔
3475

3476
    @Override
3477
    @SuppressWarnings("SuspiciousMethodCalls")
3478
    public boolean contains(Object o) {
3479
      return cache.containsValue(o);
1✔
3480
    }
3481

3482
    @Override
3483
    public boolean removeAll(Collection<?> collection) {
3484
      requireNonNull(collection);
1✔
3485
      @Var boolean modified = false;
1✔
3486
      for (var iterator = new EntryIterator<>(cache); iterator.hasNext();) {
1✔
3487
        var key = requireNonNull(iterator.key);
1✔
3488
        var value = requireNonNull(iterator.value);
1✔
3489
        if (collection.contains(value) && cache.remove(key, value)) {
1✔
3490
          modified = true;
1✔
3491
        }
3492
        iterator.advance();
1✔
3493
      }
1✔
3494
      return modified;
1✔
3495
    }
3496

3497
    @Override
3498
    public boolean remove(@Nullable Object o) {
3499
      if (o == null) {
1✔
3500
        return false;
1✔
3501
      }
3502
      for (var iterator = new EntryIterator<>(cache); iterator.hasNext();) {
1✔
3503
        var key = requireNonNull(iterator.key);
1✔
3504
        var value = requireNonNull(iterator.value);
1✔
3505
        if (o.equals(value) && cache.remove(key, value)) {
1✔
3506
          return true;
1✔
3507
        }
3508
        iterator.advance();
1✔
3509
      }
1✔
3510
      return false;
1✔
3511
    }
3512

3513
    @Override
3514
    public boolean removeIf(Predicate<? super V> filter) {
3515
      requireNonNull(filter);
1✔
3516
      @Var boolean modified = false;
1✔
3517
      for (var iterator = new EntryIterator<>(cache); iterator.hasNext();) {
1✔
3518
        var value = requireNonNull(iterator.value);
1✔
3519
        if (filter.test(value)) {
1✔
3520
          var key = requireNonNull(iterator.key);
1✔
3521
          modified |= cache.remove(key, value);
1✔
3522
        }
3523
        iterator.advance();
1✔
3524
      }
1✔
3525
      return modified;
1✔
3526
    }
3527

3528
    @Override
3529
    public boolean retainAll(Collection<?> collection) {
3530
      requireNonNull(collection);
1✔
3531
      @Var boolean modified = false;
1✔
3532
      for (var iterator = new EntryIterator<>(cache); iterator.hasNext();) {
1✔
3533
        var key = requireNonNull(iterator.key);
1✔
3534
        var value = requireNonNull(iterator.value);
1✔
3535
        if (!collection.contains(value) && cache.remove(key, value)) {
1✔
3536
          modified = true;
1✔
3537
        }
3538
        iterator.advance();
1✔
3539
      }
1✔
3540
      return modified;
1✔
3541
    }
3542

3543
    @Override
3544
    public Iterator<V> iterator() {
3545
      return new ValueIterator<>(cache);
1✔
3546
    }
3547

3548
    @Override
3549
    public Spliterator<V> spliterator() {
3550
      return new ValueSpliterator<>(cache);
1✔
3551
    }
3552
  }
3553

3554
  /** An adapter to safely externalize the value iterator. */
3555
  static final class ValueIterator<K, V> implements Iterator<V> {
3556
    final EntryIterator<K, V> iterator;
3557

3558
    ValueIterator(BoundedLocalCache<K, V> cache) {
1✔
3559
      this.iterator = new EntryIterator<>(cache);
1✔
3560
    }
1✔
3561

3562
    @Override
3563
    public boolean hasNext() {
3564
      return iterator.hasNext();
1✔
3565
    }
3566

3567
    @Override
3568
    public V next() {
3569
      return iterator.nextValue();
1✔
3570
    }
3571

3572
    @Override
3573
    public void remove() {
3574
      iterator.remove();
1✔
3575
    }
1✔
3576
  }
3577

3578
  /** An adapter to safely externalize the value spliterator. */
3579
  static final class ValueSpliterator<K, V> implements Spliterator<V> {
3580
    final Spliterator<Node<K, V>> spliterator;
3581
    final BoundedLocalCache<K, V> cache;
3582

3583
    ValueSpliterator(BoundedLocalCache<K, V> cache) {
3584
      this(cache, cache.data.values().spliterator());
1✔
3585
    }
1✔
3586

3587
    ValueSpliterator(BoundedLocalCache<K, V> cache, Spliterator<Node<K, V>> spliterator) {
1✔
3588
      this.spliterator = requireNonNull(spliterator);
1✔
3589
      this.cache = requireNonNull(cache);
1✔
3590
    }
1✔
3591

3592
    @Override
3593
    public void forEachRemaining(Consumer<? super V> action) {
3594
      requireNonNull(action);
1✔
3595
      Consumer<Node<K, V>> consumer = node -> {
1✔
3596
        K key = node.getKey();
1✔
3597
        V value = node.getValue();
1✔
3598
        long now = cache.expirationTicker().read();
1✔
3599
        if ((key != null) && (value != null) && node.isAlive() && !cache.hasExpired(node, now)) {
1✔
3600
          action.accept(value);
1✔
3601
        }
3602
      };
1✔
3603
      spliterator.forEachRemaining(consumer);
1✔
3604
    }
1✔
3605

3606
    @Override
3607
    public boolean tryAdvance(Consumer<? super V> action) {
3608
      requireNonNull(action);
1✔
3609
      boolean[] advanced = { false };
1✔
3610
      long now = cache.expirationTicker().read();
1✔
3611
      Consumer<Node<K, V>> consumer = node -> {
1✔
3612
        K key = node.getKey();
1✔
3613
        V value = node.getValue();
1✔
3614
        if ((key != null) && (value != null) && !cache.hasExpired(node, now) && node.isAlive()) {
1✔
3615
          action.accept(value);
1✔
3616
          advanced[0] = true;
1✔
3617
        }
3618
      };
1✔
3619
      while (spliterator.tryAdvance(consumer)) {
1✔
3620
        if (advanced[0]) {
1✔
3621
          return true;
1✔
3622
        }
3623
      }
3624
      return false;
1✔
3625
    }
3626

3627
    @Override
3628
    public @Nullable Spliterator<V> trySplit() {
3629
      Spliterator<Node<K, V>> split = spliterator.trySplit();
1✔
3630
      return (split == null) ? null : new ValueSpliterator<>(cache, split);
1✔
3631
    }
3632

3633
    @Override
3634
    public long estimateSize() {
3635
      return spliterator.estimateSize();
1✔
3636
    }
3637

3638
    @Override
3639
    public int characteristics() {
3640
      return CONCURRENT | NONNULL;
1✔
3641
    }
3642
  }
3643

3644
  /** An adapter to safely externalize the entries. */
3645
  static final class EntrySetView<K, V> extends AbstractSet<Entry<K, V>> {
3646
    final BoundedLocalCache<K, V> cache;
3647

3648
    EntrySetView(BoundedLocalCache<K, V> cache) {
1✔
3649
      this.cache = requireNonNull(cache);
1✔
3650
    }
1✔
3651

3652
    @Override
3653
    public int size() {
3654
      return cache.size();
1✔
3655
    }
3656

3657
    @Override
3658
    public void clear() {
3659
      cache.clear();
1✔
3660
    }
1✔
3661

3662
    @Override
3663
    public boolean contains(Object o) {
3664
      if (!(o instanceof Entry<?, ?>)) {
1✔
3665
        return false;
1✔
3666
      }
3667
      var entry = (Entry<?, ?>) o;
1✔
3668
      var key = entry.getKey();
1✔
3669
      var value = entry.getValue();
1✔
3670
      if ((key == null) || (value == null)) {
1✔
3671
        return false;
1✔
3672
      }
3673
      Node<K, V> node = cache.data.get(cache.nodeFactory.newLookupKey(key));
1✔
3674
      return (node != null) && node.containsValue(value);
1✔
3675
    }
3676

3677
    @Override
3678
    public boolean removeAll(Collection<?> collection) {
3679
      requireNonNull(collection);
1✔
3680
      @Var boolean modified = false;
1✔
3681
      if ((collection instanceof Set<?>) && (collection.size() > size())) {
1✔
3682
        for (var entry : this) {
1✔
3683
          if (collection.contains(entry)) {
1✔
3684
            modified |= remove(entry);
1✔
3685
          }
3686
        }
1✔
3687
      } else {
3688
        for (var item : collection) {
1✔
3689
          modified |= (item != null) && remove(item);
1✔
3690
        }
1✔
3691
      }
3692
      return modified;
1✔
3693
    }
3694

3695
    @Override
3696
    @SuppressWarnings("SuspiciousMethodCalls")
3697
    public boolean remove(Object o) {
3698
      if (!(o instanceof Entry<?, ?>)) {
1✔
3699
        return false;
1✔
3700
      }
3701
      var entry = (Entry<?, ?>) o;
1✔
3702
      var key = entry.getKey();
1✔
3703
      return (key != null) && cache.remove(key, entry.getValue());
1✔
3704
    }
3705

3706
    @Override
3707
    public boolean removeIf(Predicate<? super Entry<K, V>> filter) {
3708
      requireNonNull(filter);
1✔
3709
      @Var boolean modified = false;
1✔
3710
      for (Entry<K, V> entry : this) {
1✔
3711
        if (filter.test(entry)) {
1✔
3712
          modified |= cache.remove(entry.getKey(), entry.getValue());
1✔
3713
        }
3714
      }
1✔
3715
      return modified;
1✔
3716
    }
3717

3718
    @Override
3719
    public boolean retainAll(Collection<?> collection) {
3720
      requireNonNull(collection);
1✔
3721
      @Var boolean modified = false;
1✔
3722
      for (var entry : this) {
1✔
3723
        if (!collection.contains(entry) && remove(entry)) {
1✔
3724
          modified = true;
1✔
3725
        }
3726
      }
1✔
3727
      return modified;
1✔
3728
    }
3729

3730
    @Override
3731
    public Iterator<Entry<K, V>> iterator() {
3732
      return new EntryIterator<>(cache);
1✔
3733
    }
3734

3735
    @Override
3736
    public Spliterator<Entry<K, V>> spliterator() {
3737
      return new EntrySpliterator<>(cache);
1✔
3738
    }
3739
  }
3740

3741
  /** An adapter to safely externalize the entry iterator. */
3742
  static final class EntryIterator<K, V> implements Iterator<Entry<K, V>> {
3743
    final BoundedLocalCache<K, V> cache;
3744
    final Iterator<Node<K, V>> iterator;
3745

3746
    @Nullable K key;
3747
    @Nullable V value;
3748
    @Nullable K removalKey;
3749
    @Nullable Node<K, V> next;
3750

3751
    EntryIterator(BoundedLocalCache<K, V> cache) {
1✔
3752
      this.iterator = cache.data.values().iterator();
1✔
3753
      this.cache = cache;
1✔
3754
    }
1✔
3755

3756
    @Override
3757
    public boolean hasNext() {
3758
      if (next != null) {
1✔
3759
        return true;
1✔
3760
      }
3761

3762
      long now = cache.expirationTicker().read();
1✔
3763
      while (iterator.hasNext()) {
1✔
3764
        next = iterator.next();
1✔
3765
        value = next.getValue();
1✔
3766
        key = next.getKey();
1✔
3767

3768
        boolean evictable = (key == null) || (value == null) || cache.hasExpired(next, now);
1✔
3769
        if (evictable || !next.isAlive()) {
1✔
3770
          if (evictable) {
1✔
3771
            cache.scheduleDrainBuffers();
1✔
3772
          }
3773
          advance();
1✔
3774
          continue;
1✔
3775
        }
3776
        return true;
1✔
3777
      }
3778
      return false;
1✔
3779
    }
3780

3781
    /** Invalidates the current position so that the iterator may compute the next position. */
3782
    void advance() {
3783
      value = null;
1✔
3784
      next = null;
1✔
3785
      key = null;
1✔
3786
    }
1✔
3787

3788
    K nextKey() {
3789
      if (!hasNext()) {
1✔
3790
        throw new NoSuchElementException();
1✔
3791
      }
3792
      removalKey = key;
1✔
3793
      advance();
1✔
3794
      return requireNonNull(removalKey);
1✔
3795
    }
3796

3797
    V nextValue() {
3798
      if (!hasNext()) {
1✔
3799
        throw new NoSuchElementException();
1✔
3800
      }
3801
      removalKey = key;
1✔
3802
      V val = value;
1✔
3803
      advance();
1✔
3804
      return requireNonNull(val);
1✔
3805
    }
3806

3807
    @Override
3808
    public Entry<K, V> next() {
3809
      if (!hasNext()) {
1✔
3810
        throw new NoSuchElementException();
1✔
3811
      }
3812
      var entry = new WriteThroughEntry<K, @NonNull V>(
1✔
3813
          cache, requireNonNull(key), requireNonNull(value));
1✔
3814
      removalKey = key;
1✔
3815
      advance();
1✔
3816
      return entry;
1✔
3817
    }
3818

3819
    @Override
3820
    public void remove() {
3821
      if (removalKey == null) {
1✔
3822
        throw new IllegalStateException();
1✔
3823
      }
3824
      cache.remove(removalKey);
1✔
3825
      removalKey = null;
1✔
3826
    }
1✔
3827
  }
3828

3829
  /** An adapter to safely externalize the entry spliterator. */
3830
  static final class EntrySpliterator<K, V> implements Spliterator<Entry<K, V>> {
3831
    final Spliterator<Node<K, V>> spliterator;
3832
    final BoundedLocalCache<K, V> cache;
3833

3834
    EntrySpliterator(BoundedLocalCache<K, V> cache) {
3835
      this(cache, cache.data.values().spliterator());
1✔
3836
    }
1✔
3837

3838
    EntrySpliterator(BoundedLocalCache<K, V> cache, Spliterator<Node<K, V>> spliterator) {
1✔
3839
      this.spliterator = requireNonNull(spliterator);
1✔
3840
      this.cache = requireNonNull(cache);
1✔
3841
    }
1✔
3842

3843
    @Override
3844
    public void forEachRemaining(Consumer<? super Entry<K, V>> action) {
3845
      requireNonNull(action);
1✔
3846
      Consumer<Node<K, V>> consumer = node -> {
1✔
3847
        K key = node.getKey();
1✔
3848
        V value = node.getValue();
1✔
3849
        long now = cache.expirationTicker().read();
1✔
3850
        if ((key != null) && (value != null) && node.isAlive() && !cache.hasExpired(node, now)) {
1✔
3851
          action.accept(new WriteThroughEntry<>(cache, key, value));
1✔
3852
        }
3853
      };
1✔
3854
      spliterator.forEachRemaining(consumer);
1✔
3855
    }
1✔
3856

3857
    @Override
3858
    public boolean tryAdvance(Consumer<? super Entry<K, V>> action) {
3859
      requireNonNull(action);
1✔
3860
      boolean[] advanced = { false };
1✔
3861
      Consumer<Node<K, V>> consumer = node -> {
1✔
3862
        K key = node.getKey();
1✔
3863
        V value = node.getValue();
1✔
3864
        long now = cache.expirationTicker().read();
1✔
3865
        if ((key != null) && (value != null) && node.isAlive() && !cache.hasExpired(node, now)) {
1✔
3866
          action.accept(new WriteThroughEntry<>(cache, key, value));
1✔
3867
          advanced[0] = true;
1✔
3868
        }
3869
      };
1✔
3870
      while (spliterator.tryAdvance(consumer)) {
1✔
3871
        if (advanced[0]) {
1✔
3872
          return true;
1✔
3873
        }
3874
      }
3875
      return false;
1✔
3876
    }
3877

3878
    @Override
3879
    public @Nullable Spliterator<Entry<K, V>> trySplit() {
3880
      Spliterator<Node<K, V>> split = spliterator.trySplit();
1✔
3881
      return (split == null) ? null : new EntrySpliterator<>(cache, split);
1✔
3882
    }
3883

3884
    @Override
3885
    public long estimateSize() {
3886
      return spliterator.estimateSize();
1✔
3887
    }
3888

3889
    @Override
3890
    public int characteristics() {
3891
      return DISTINCT | CONCURRENT | NONNULL;
1✔
3892
    }
3893
  }
3894

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

3899
    final WeakReference<BoundedLocalCache<?, ?>> reference;
3900

3901
    PerformCleanupTask(BoundedLocalCache<?, ?> cache) {
1✔
3902
      reference = new WeakReference<>(cache);
1✔
3903
    }
1✔
3904

3905
    @Override
3906
    public boolean exec() {
3907
      try {
3908
        run();
1✔
3909
      } catch (Throwable t) {
1✔
3910
        logger.log(Level.ERROR, "Exception thrown when performing the maintenance task", t);
1✔
3911
      }
1✔
3912

3913
      // Indicates that the task has not completed to allow subsequent submissions to execute
3914
      return false;
1✔
3915
    }
3916

3917
    @Override
3918
    public void run() {
3919
      BoundedLocalCache<?, ?> cache = reference.get();
1✔
3920
      if (cache != null) {
1✔
3921
        cache.performCleanUp(/* ignored */ null);
1✔
3922
      }
3923
    }
1✔
3924

3925
    /**
3926
     * This method cannot be ignored due to being final, so a hostile user supplied Executor could
3927
     * forcibly complete the task and halt future executions. There are easier ways to intentionally
3928
     * harm a system, so this is assumed to not happen in practice.
3929
     */
3930
    // public final void quietlyComplete() {}
3931

3932
    @Override public void complete(@Nullable Void value) {}
1✔
3933
    @Override public void setRawResult(@Nullable Void value) {}
1✔
3934
    @Override public @Nullable Void getRawResult() { return null; }
1✔
3935
    @Override public void completeExceptionally(@Nullable Throwable t) {}
1✔
3936
    @Override public boolean cancel(boolean mayInterruptIfRunning) { return false; }
1✔
3937
  }
3938

3939
  /** Creates a serialization proxy based on the common configuration shared by all cache types. */
3940
  static <K, V> SerializationProxy<K, V> makeSerializationProxy(BoundedLocalCache<?, ?> cache) {
3941
    var proxy = new SerializationProxy<K, V>();
1✔
3942
    proxy.weakKeys = cache.collectKeys();
1✔
3943
    proxy.weakValues = cache.nodeFactory.weakValues();
1✔
3944
    proxy.softValues = cache.nodeFactory.softValues();
1✔
3945
    proxy.isRecordingStats = cache.isRecordingStats();
1✔
3946
    proxy.evictionListener = cache.evictionListener;
1✔
3947
    proxy.removalListener = cache.removalListener();
1✔
3948
    proxy.ticker = cache.expirationTicker();
1✔
3949
    if (cache.expiresAfterAccess()) {
1✔
3950
      proxy.expiresAfterAccessNanos = cache.expiresAfterAccessNanos();
1✔
3951
    }
3952
    if (cache.expiresAfterWrite()) {
1✔
3953
      proxy.expiresAfterWriteNanos = cache.expiresAfterWriteNanos();
1✔
3954
    }
3955
    if (cache.expiresVariable()) {
1✔
3956
      proxy.expiry = cache.expiry();
1✔
3957
    }
3958
    if (cache.refreshAfterWrite()) {
1✔
3959
      proxy.refreshAfterWriteNanos = cache.refreshAfterWriteNanos();
1✔
3960
    }
3961
    if (cache.evicts()) {
1✔
3962
      if (cache.isWeighted) {
1✔
3963
        proxy.weigher = cache.weigher;
1✔
3964
        proxy.maximumWeight = cache.maximum();
1✔
3965
      } else {
3966
        proxy.maximumSize = cache.maximum();
1✔
3967
      }
3968
    }
3969
    proxy.cacheLoader = cache.cacheLoader;
1✔
3970
    proxy.async = cache.isAsync;
1✔
3971
    return proxy;
1✔
3972
  }
3973

3974
  /* --------------- Manual Cache --------------- */
3975

3976
  static class BoundedLocalManualCache<K, V> implements LocalManualCache<K, V>, Serializable {
3977
    private static final long serialVersionUID = 1;
3978

3979
    final BoundedLocalCache<K, V> cache;
3980

3981
    @Nullable Policy<K, V> policy;
3982

3983
    BoundedLocalManualCache(Caffeine<K, V> builder) {
3984
      this(builder, null);
1✔
3985
    }
1✔
3986

3987
    BoundedLocalManualCache(Caffeine<K, V> builder, @Nullable CacheLoader<? super K, V> loader) {
1✔
3988
      cache = LocalCacheFactory.newBoundedLocalCache(builder, loader, /* isAsync= */ false);
1✔
3989
    }
1✔
3990

3991
    @Override
3992
    public final BoundedLocalCache<K, V> cache() {
3993
      return cache;
1✔
3994
    }
3995

3996
    @Override
3997
    public final Policy<K, V> policy() {
3998
      if (policy == null) {
1✔
3999
        Function<@Nullable V, @Nullable V> identity = v -> v;
1✔
4000
        policy = new BoundedPolicy<>(cache, identity, cache.isWeighted);
1✔
4001
      }
4002
      return policy;
1✔
4003
    }
4004

4005
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
4006
      throw new InvalidObjectException("Proxy required");
1✔
4007
    }
4008

4009
    private Object writeReplace() {
4010
      return makeSerializationProxy(cache);
1✔
4011
    }
4012
  }
4013

4014
  @SuppressWarnings({"NullableOptional",
4015
    "OptionalAssignedToNull", "OptionalUsedAsFieldOrParameterType"})
4016
  static final class BoundedPolicy<K, V> implements Policy<K, V> {
4017
    final Function<@Nullable V, @Nullable V> transformer;
4018
    final BoundedLocalCache<K, V> cache;
4019
    final boolean isWeighted;
4020

4021
    @Nullable Optional<Eviction<K, V>> eviction;
4022
    @Nullable Optional<FixedRefresh<K, V>> refreshes;
4023
    @Nullable Optional<FixedExpiration<K, V>> afterWrite;
4024
    @Nullable Optional<FixedExpiration<K, V>> afterAccess;
4025
    @Nullable Optional<VarExpiration<K, V>> variable;
4026

4027
    BoundedPolicy(BoundedLocalCache<K, V> cache,
4028
        Function<@Nullable V, @Nullable V> transformer, boolean isWeighted) {
1✔
4029
      this.transformer = transformer;
1✔
4030
      this.isWeighted = isWeighted;
1✔
4031
      this.cache = cache;
1✔
4032
    }
1✔
4033

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

4106
    final class BoundedEviction implements Eviction<K, V> {
1✔
4107
      @Override public boolean isWeighted() {
4108
        return isWeighted;
1✔
4109
      }
4110
      @Override public OptionalInt weightOf(K key) {
4111
        requireNonNull(key);
1✔
4112
        if (!isWeighted) {
1✔
4113
          return OptionalInt.empty();
1✔
4114
        }
4115
        Node<K, V> node = cache.data.get(cache.nodeFactory.newLookupKey(key));
1✔
4116
        if ((node == null) || cache.hasExpired(node, cache.expirationTicker().read())) {
1✔
4117
          return OptionalInt.empty();
1✔
4118
        }
4119
        synchronized (node) {
1✔
4120
          return node.isAlive() ? OptionalInt.of(node.getWeight()) : OptionalInt.empty();
1✔
4121
        }
4122
      }
4123
      @Override public OptionalLong weightedSize() {
4124
        return isWeighted
1✔
4125
            ? OptionalLong.of(Math.max(0, cache.weightedSizeAcquire()))
1✔
4126
            : OptionalLong.empty();
1✔
4127
      }
4128
      @Override public long getMaximum() {
4129
        return cache.maximumAcquire();
1✔
4130
      }
4131
      @Override public void setMaximum(long maximum) {
4132
        cache.evictionLock.lock();
1✔
4133
        try {
4134
          cache.setMaximumSize(maximum);
1✔
4135
          cache.maintenance(/* ignored */ null);
1✔
4136
        } finally {
4137
          cache.evictionLock.unlock();
1✔
4138
          cache.rescheduleCleanUpIfIncomplete();
1✔
4139
        }
4140
      }
1✔
4141
      @Override public Map<K, V> coldest(int limit) {
4142
        int expectedSize = Math.min(limit, cache.size());
1✔
4143
        var limiter = new SizeLimiter<K, V>(expectedSize, limit);
1✔
4144
        return cache.evictionOrder(/* hottest= */ false, transformer, limiter);
1✔
4145
      }
4146
      @Override public Map<K, V> coldestWeighted(long weightLimit) {
4147
        var limiter = isWeighted()
1✔
4148
            ? new WeightLimiter<K, V>(weightLimit)
1✔
4149
            : new SizeLimiter<K, V>((int) Math.min(weightLimit, cache.size()), weightLimit);
1✔
4150
        return cache.evictionOrder(/* hottest= */ false, transformer, limiter);
1✔
4151
      }
4152
      @Override
4153
      public <T> T coldest(Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4154
        requireNonNull(mappingFunction);
1✔
4155
        return cache.evictionOrder(/* hottest= */ false, transformer, mappingFunction);
1✔
4156
      }
4157
      @Override public Map<K, V> hottest(int limit) {
4158
        int expectedSize = Math.min(limit, cache.size());
1✔
4159
        var limiter = new SizeLimiter<K, V>(expectedSize, limit);
1✔
4160
        return cache.evictionOrder(/* hottest= */ true, transformer, limiter);
1✔
4161
      }
4162
      @Override public Map<K, V> hottestWeighted(long weightLimit) {
4163
        var limiter = isWeighted()
1✔
4164
            ? new WeightLimiter<K, V>(weightLimit)
1✔
4165
            : new SizeLimiter<K, V>((int) Math.min(weightLimit, cache.size()), weightLimit);
1✔
4166
        return cache.evictionOrder(/* hottest= */ true, transformer, limiter);
1✔
4167
      }
4168
      @Override
4169
      public <T> T hottest(Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4170
        requireNonNull(mappingFunction);
1✔
4171
        return cache.evictionOrder(/* hottest= */ true, transformer, mappingFunction);
1✔
4172
      }
4173
    }
4174

4175
    @SuppressWarnings("PreferJavaTimeOverload")
4176
    final class BoundedExpireAfterAccess implements FixedExpiration<K, V> {
1✔
4177
      @Override public OptionalLong ageOf(K key, TimeUnit unit) {
4178
        requireNonNull(key);
1✔
4179
        requireNonNull(unit);
1✔
4180
        Object lookupKey = cache.nodeFactory.newLookupKey(key);
1✔
4181
        Node<K, V> node = cache.data.get(lookupKey);
1✔
4182
        if (node == null) {
1✔
4183
          return OptionalLong.empty();
1✔
4184
        }
4185
        long now = cache.expirationTicker().read();
1✔
4186
        return cache.hasExpired(node, now)
1✔
4187
            ? OptionalLong.empty()
1✔
4188
            : OptionalLong.of(unit.convert(now - node.getAccessTime(), TimeUnit.NANOSECONDS));
1✔
4189
      }
4190
      @Override public long getExpiresAfter(TimeUnit unit) {
4191
        return unit.convert(cache.expiresAfterAccessNanos(), TimeUnit.NANOSECONDS);
1✔
4192
      }
4193
      @Override public void setExpiresAfter(long duration, TimeUnit unit) {
4194
        requireArgument(duration >= 0);
1✔
4195
        cache.setExpiresAfterAccessNanos(unit.toNanos(duration));
1✔
4196
        cache.scheduleAfterWrite();
1✔
4197
      }
1✔
4198
      @Override public Map<K, V> oldest(int limit) {
4199
        return oldest(new SizeLimiter<>(Math.min(limit, cache.size()), limit));
1✔
4200
      }
4201
      @Override public <T> T oldest(Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4202
        return cache.expireAfterAccessOrder(/* oldest= */ true, transformer, mappingFunction);
1✔
4203
      }
4204
      @Override public Map<K, V> youngest(int limit) {
4205
        return youngest(new SizeLimiter<>(Math.min(limit, cache.size()), limit));
1✔
4206
      }
4207
      @Override public <T> T youngest(Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4208
        return cache.expireAfterAccessOrder(/* oldest= */ false, transformer, mappingFunction);
1✔
4209
      }
4210
    }
4211

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

4252
    @SuppressWarnings("PreferJavaTimeOverload")
4253
    final class BoundedVarExpiration implements VarExpiration<K, V> {
1✔
4254
      @Override public OptionalLong getExpiresAfter(K key, TimeUnit unit) {
4255
        requireNonNull(key);
1✔
4256
        requireNonNull(unit);
1✔
4257
        Object lookupKey = cache.nodeFactory.newLookupKey(key);
1✔
4258
        Node<K, V> node = cache.data.get(lookupKey);
1✔
4259
        if (node == null) {
1✔
4260
          return OptionalLong.empty();
1✔
4261
        }
4262
        long now = cache.expirationTicker().read();
1✔
4263
        return cache.hasExpired(node, now)
1✔
4264
            ? OptionalLong.empty()
1✔
4265
            : OptionalLong.of(unit.convert(node.getVariableTime() - now, TimeUnit.NANOSECONDS));
1✔
4266
      }
4267
      @Override public void setExpiresAfter(K key, long duration, TimeUnit unit) {
4268
        requireNonNull(key);
1✔
4269
        requireNonNull(unit);
1✔
4270
        requireArgument(duration >= 0);
1✔
4271
        Object lookupKey = cache.nodeFactory.newLookupKey(key);
1✔
4272
        Node<K, V> node = cache.data.get(lookupKey);
1✔
4273
        if (node != null) {
1✔
4274
          long now;
4275
          long durationNanos = TimeUnit.NANOSECONDS.convert(duration, unit);
1✔
4276
          synchronized (node) {
1✔
4277
            now = cache.expirationTicker().read();
1✔
4278
            if (cache.isComputingAsync(node.getValue()) || cache.hasExpired(node, now)) {
1✔
4279
              return;
1✔
4280
            }
4281
            node.setVariableTime(now + Math.min(durationNanos, MAXIMUM_EXPIRY));
1✔
4282
          }
1✔
4283
          cache.afterRead(node, now, /* recordHit= */ false);
1✔
4284
        }
4285
      }
1✔
4286
      @Override public @Nullable V put(K key, V value, long duration, TimeUnit unit) {
4287
        requireNonNull(unit);
1✔
4288
        requireNonNull(value);
1✔
4289
        requireArgument(duration >= 0);
1✔
4290
        return cache.isAsync
1✔
4291
            ? putAsync(key, value, duration, unit)
1✔
4292
            : putSync(key, value, duration, unit, /* onlyIfAbsent= */ false);
1✔
4293
      }
4294
      @Override public @Nullable V putIfAbsent(K key, V value, long duration, TimeUnit unit) {
4295
        requireNonNull(unit);
1✔
4296
        requireNonNull(value);
1✔
4297
        requireArgument(duration >= 0);
1✔
4298
        return cache.isAsync
1✔
4299
            ? putIfAbsentAsync(key, value, duration, unit)
1✔
4300
            : putSync(key, value, duration, unit, /* onlyIfAbsent= */ true);
1✔
4301
      }
4302
      @Nullable V putSync(K key, V value, long duration, TimeUnit unit, boolean onlyIfAbsent) {
4303
        var expiry = new FixedExpireAfterWrite<K, V>(duration, unit);
1✔
4304
        return cache.put(key, value, expiry, onlyIfAbsent);
1✔
4305
      }
4306
      @SuppressWarnings("unchecked")
4307
      @Nullable V putIfAbsentAsync(K key, V value, long duration, TimeUnit unit) {
4308
        // Keep in sync with LocalAsyncCache.AsMapView#putIfAbsent(key, value)
4309
        var expiry = (Expiry<K, V>) new AsyncExpiry<>(new FixedExpireAfterWrite<>(duration, unit));
1✔
4310
        var asyncValue = (V) CompletableFuture.completedFuture(value);
1✔
4311

4312
        for (;;) {
4313
          var priorFuture = (CompletableFuture<V>) cache.getIfPresent(
1✔
4314
              key, /* recordStats= */ false);
4315
          if (priorFuture != null) {
1✔
4316
            if (!priorFuture.isDone()) {
1✔
4317
              Async.getWhenSuccessful(priorFuture);
1✔
4318
              continue;
1✔
4319
            }
4320

4321
            V prior = Async.getWhenSuccessful(priorFuture);
1✔
4322
            if (prior != null) {
1✔
4323
              return prior;
1✔
4324
            }
4325
          }
4326

4327
          boolean[] added = { false };
1✔
4328
          var computed = (CompletableFuture<V>) cache.compute(key, (K k, @Nullable V oldValue) -> {
1✔
4329
            var oldValueFuture = (CompletableFuture<V>) oldValue;
1✔
4330
            added[0] = (oldValueFuture == null)
1✔
4331
                || (oldValueFuture.isDone() && (Async.getIfReady(oldValueFuture) == null));
1✔
4332
            return added[0] ? asyncValue : oldValue;
1✔
4333
          }, expiry, /* recordLoad= */ false, /* recordLoadFailure= */ false);
4334

4335
          if (added[0]) {
1✔
4336
            return null;
1✔
4337
          } else {
4338
            V prior = Async.getWhenSuccessful(computed);
1✔
4339
            if (prior != null) {
1✔
4340
              return prior;
1✔
4341
            }
4342
          }
4343
        }
1✔
4344
      }
4345
      @SuppressWarnings("unchecked")
4346
      @Nullable V putAsync(K key, V value, long duration, TimeUnit unit) {
4347
        var expiry = (Expiry<K, V>) new AsyncExpiry<>(new FixedExpireAfterWrite<>(duration, unit));
1✔
4348
        var asyncValue = (V) CompletableFuture.completedFuture(value);
1✔
4349

4350
        var oldValueFuture = (CompletableFuture<V>) cache.put(
1✔
4351
            key, asyncValue, expiry, /* onlyIfAbsent= */ false);
4352
        return Async.getWhenSuccessful(oldValueFuture);
1✔
4353
      }
4354
      @Override public @Nullable V compute(K key,
4355
          BiFunction<? super K, ? super V, ? extends V> remappingFunction,
4356
          Duration duration) {
4357
        requireNonNull(key);
1✔
4358
        requireNonNull(duration);
1✔
4359
        requireNonNull(remappingFunction);
1✔
4360
        requireArgument(!duration.isNegative(), "duration cannot be negative: %s", duration);
1✔
4361
        var expiry = new FixedExpireAfterWrite<K, V>(
1✔
4362
            toNanosSaturated(duration), TimeUnit.NANOSECONDS);
1✔
4363

4364
        return cache.isAsync
1✔
4365
            ? computeAsync(key, remappingFunction, expiry)
1✔
4366
            : cache.compute(key, remappingFunction, expiry,
1✔
4367
                /* recordLoad= */ true, /* recordLoadFailure= */ true);
4368
      }
4369
      @Nullable V computeAsync(K key,
4370
          BiFunction<? super K, ? super V, ? extends V> remappingFunction,
4371
          Expiry<? super K, ? super V> expiry) {
4372
        // Keep in sync with LocalAsyncCache.AsMapView#compute(key, remappingFunction)
4373
        @SuppressWarnings("unchecked")
4374
        var delegate = (LocalCache<K, CompletableFuture<V>>) cache;
1✔
4375

4376
        @SuppressWarnings({"rawtypes", "unchecked", "Varifier"})
4377
        @Nullable V[] newValue = (@Nullable V[]) new Object[1];
1✔
4378
        for (;;) {
4379
          Async.getWhenSuccessful(delegate.getIfPresentQuietly(key));
1✔
4380

4381
          CompletableFuture<V> valueFuture = delegate.compute(
1✔
4382
              key, (K k, @Nullable CompletableFuture<V> oldValueFuture) -> {
4383
                if ((oldValueFuture != null) && !oldValueFuture.isDone()) {
1✔
4384
                  return oldValueFuture;
1✔
4385
                }
4386

4387
                V oldValue = Async.getIfReady(oldValueFuture);
1✔
4388
                BiFunction<? super K, ? super V, ? extends @Nullable V> function =
1✔
4389
                    delegate.statsAware(remappingFunction,
1✔
4390
                        /* recordLoad= */ true, /* recordLoadFailure= */ true);
4391
                newValue[0] = function.apply(key, oldValue);
1✔
4392
                return (newValue[0] == null) ? null
1✔
4393
                    : CompletableFuture.completedFuture(newValue[0]);
1✔
4394
              }, new AsyncExpiry<>(expiry), /* recordLoad= */ false,
4395
              /* recordLoadFailure= */ false);
4396

4397
          if (newValue[0] != null) {
1✔
4398
            return newValue[0];
1✔
4399
          } else if (valueFuture == null) {
1✔
4400
            return null;
1✔
4401
          }
4402
        }
1✔
4403
      }
4404
      @Override public Map<K, V> oldest(int limit) {
4405
        return oldest(new SizeLimiter<>(Math.min(limit, cache.size()), limit));
1✔
4406
      }
4407
      @Override public <T> T oldest(Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4408
        return cache.snapshot(cache.timerWheel(), transformer, mappingFunction);
1✔
4409
      }
4410
      @Override public Map<K, V> youngest(int limit) {
4411
        return youngest(new SizeLimiter<>(Math.min(limit, cache.size()), limit));
1✔
4412
      }
4413
      @Override public <T> T youngest(Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4414
        return cache.snapshot(cache.timerWheel()::descendingIterator, transformer, mappingFunction);
1✔
4415
      }
4416
    }
4417

4418
    static final class FixedExpireAfterWrite<K, V> implements Expiry<K, V> {
4419
      final long duration;
4420
      final TimeUnit unit;
4421

4422
      FixedExpireAfterWrite(long duration, TimeUnit unit) {
1✔
4423
        this.duration = duration;
1✔
4424
        this.unit = unit;
1✔
4425
      }
1✔
4426
      @Override public long expireAfterCreate(K key, V value, long currentTime) {
4427
        return unit.toNanos(duration);
1✔
4428
      }
4429
      @Override public long expireAfterUpdate(
4430
          K key, V value, long currentTime, long currentDuration) {
4431
        return unit.toNanos(duration);
1✔
4432
      }
4433
      @CanIgnoreReturnValue
4434
      @Override public long expireAfterRead(
4435
          K key, V value, long currentTime, long currentDuration) {
4436
        return currentDuration;
1✔
4437
      }
4438
    }
4439

4440
    @SuppressWarnings("PreferJavaTimeOverload")
4441
    final class BoundedRefreshAfterWrite implements FixedRefresh<K, V> {
1✔
4442
      @Override public OptionalLong ageOf(K key, TimeUnit unit) {
4443
        requireNonNull(key);
1✔
4444
        requireNonNull(unit);
1✔
4445
        Object lookupKey = cache.nodeFactory.newLookupKey(key);
1✔
4446
        Node<K, V> node = cache.data.get(lookupKey);
1✔
4447
        if (node == null) {
1✔
4448
          return OptionalLong.empty();
1✔
4449
        }
4450
        long now = cache.expirationTicker().read();
1✔
4451
        return cache.hasExpired(node, now)
1✔
4452
            ? OptionalLong.empty()
1✔
4453
            : OptionalLong.of(unit.convert(now - node.getWriteTime(), TimeUnit.NANOSECONDS));
1✔
4454
      }
4455
      @Override public long getRefreshesAfter(TimeUnit unit) {
4456
        return unit.convert(cache.refreshAfterWriteNanos(), TimeUnit.NANOSECONDS);
1✔
4457
      }
4458
      @Override public void setRefreshesAfter(long duration, TimeUnit unit) {
4459
        requireArgument(duration >= 0);
1✔
4460
        cache.setRefreshAfterWriteNanos(unit.toNanos(duration));
1✔
4461
        cache.scheduleAfterWrite();
1✔
4462
      }
1✔
4463
    }
4464
  }
4465

4466
  /* --------------- Loading Cache --------------- */
4467

4468
  static final class BoundedLocalLoadingCache<K, V>
4469
      extends BoundedLocalManualCache<K, V> implements LocalLoadingCache<K, V> {
4470
    private static final long serialVersionUID = 1;
4471

4472
    final Function<K, @Nullable V> mappingFunction;
4473
    final @Nullable Function<Set<? extends K>, Map<K, V>> bulkMappingFunction;
4474

4475
    BoundedLocalLoadingCache(Caffeine<K, V> builder, CacheLoader<? super K, V> loader) {
4476
      super(builder, loader);
1✔
4477
      requireNonNull(loader);
1✔
4478
      mappingFunction = newMappingFunction(loader);
1✔
4479
      bulkMappingFunction = newBulkMappingFunction(loader);
1✔
4480
    }
1✔
4481

4482
    @Override
4483
    @SuppressWarnings({"DataFlowIssue", "NullAway"})
4484
    public AsyncCacheLoader<? super K, V> cacheLoader() {
4485
      return cache.cacheLoader;
1✔
4486
    }
4487

4488
    @Override
4489
    public Function<K, @Nullable V> mappingFunction() {
4490
      return mappingFunction;
1✔
4491
    }
4492

4493
    @Override
4494
    public @Nullable Function<Set<? extends K>, Map<K, V>> bulkMappingFunction() {
4495
      return bulkMappingFunction;
1✔
4496
    }
4497

4498
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
4499
      throw new InvalidObjectException("Proxy required");
1✔
4500
    }
4501

4502
    private Object writeReplace() {
4503
      return makeSerializationProxy(cache);
1✔
4504
    }
4505
  }
4506

4507
  /* --------------- Async Cache --------------- */
4508

4509
  static final class BoundedLocalAsyncCache<K, V> implements LocalAsyncCache<K, V>, Serializable {
4510
    private static final long serialVersionUID = 1;
4511

4512
    final BoundedLocalCache<K, CompletableFuture<V>> cache;
4513
    final boolean isWeighted;
4514

4515
    @Nullable ConcurrentMap<K, CompletableFuture<V>> mapView;
4516
    @Nullable CacheView<K, V> cacheView;
4517
    @Nullable Policy<K, V> policy;
4518

4519
    @SuppressWarnings("unchecked")
4520
    BoundedLocalAsyncCache(Caffeine<K, V> builder) {
1✔
4521
      cache = (BoundedLocalCache<K, CompletableFuture<V>>) LocalCacheFactory
1✔
4522
          .newBoundedLocalCache(builder, /* cacheLoader= */ null, /* isAsync= */ true);
1✔
4523
      isWeighted = builder.isWeighted();
1✔
4524
    }
1✔
4525

4526
    @Override
4527
    public BoundedLocalCache<K, CompletableFuture<V>> cache() {
4528
      return cache;
1✔
4529
    }
4530

4531
    @Override
4532
    public ConcurrentMap<K, CompletableFuture<V>> asMap() {
4533
      return (mapView == null) ? (mapView = new AsyncAsMapView<>(this)) : mapView;
1✔
4534
    }
4535

4536
    @Override
4537
    public Cache<K, V> synchronous() {
4538
      return (cacheView == null) ? (cacheView = new CacheView<>(this)) : cacheView;
1✔
4539
    }
4540

4541
    @Override
4542
    public Policy<K, V> policy() {
4543
      if (policy == null) {
1✔
4544
        @SuppressWarnings("unchecked")
4545
        var castCache = (BoundedLocalCache<K, V>) cache;
1✔
4546
        Function<CompletableFuture<V>, @Nullable V> transformer = Async::getIfReady;
1✔
4547
        @SuppressWarnings("unchecked")
4548
        var castTransformer = (Function<@Nullable V, @Nullable V>) transformer;
1✔
4549
        policy = new BoundedPolicy<>(castCache, castTransformer, isWeighted);
1✔
4550
      }
4551
      return policy;
1✔
4552
    }
4553

4554
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
4555
      throw new InvalidObjectException("Proxy required");
1✔
4556
    }
4557

4558
    private Object writeReplace() {
4559
      return makeSerializationProxy(cache);
1✔
4560
    }
4561
  }
4562

4563
  /* --------------- Async Loading Cache --------------- */
4564

4565
  static final class BoundedLocalAsyncLoadingCache<K, V>
4566
      extends LocalAsyncLoadingCache<K, V> implements Serializable {
4567
    private static final long serialVersionUID = 1;
4568

4569
    final BoundedLocalCache<K, CompletableFuture<V>> cache;
4570
    final boolean isWeighted;
4571

4572
    @Nullable ConcurrentMap<K, CompletableFuture<V>> mapView;
4573
    @Nullable Policy<K, V> policy;
4574

4575
    @SuppressWarnings("unchecked")
4576
    BoundedLocalAsyncLoadingCache(Caffeine<K, V> builder, AsyncCacheLoader<? super K, V> loader) {
4577
      super(loader);
1✔
4578
      isWeighted = builder.isWeighted();
1✔
4579
      cache = (BoundedLocalCache<K, CompletableFuture<V>>) LocalCacheFactory
1✔
4580
          .newBoundedLocalCache(builder, loader, /* isAsync= */ true);
1✔
4581
    }
1✔
4582

4583
    @Override
4584
    public BoundedLocalCache<K, CompletableFuture<V>> cache() {
4585
      return cache;
1✔
4586
    }
4587

4588
    @Override
4589
    public ConcurrentMap<K, CompletableFuture<V>> asMap() {
4590
      return (mapView == null) ? (mapView = new AsyncAsMapView<>(this)) : mapView;
1✔
4591
    }
4592

4593
    @Override
4594
    public Policy<K, V> policy() {
4595
      if (policy == null) {
1✔
4596
        @SuppressWarnings("unchecked")
4597
        var castCache = (BoundedLocalCache<K, V>) cache;
1✔
4598
        Function<CompletableFuture<V>, @Nullable V> transformer = Async::getIfReady;
1✔
4599
        @SuppressWarnings("unchecked")
4600
        var castTransformer = (Function<@Nullable V, @Nullable V>) transformer;
1✔
4601
        policy = new BoundedPolicy<>(castCache, castTransformer, isWeighted);
1✔
4602
      }
4603
      return policy;
1✔
4604
    }
4605

4606
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
4607
      throw new InvalidObjectException("Proxy required");
1✔
4608
    }
4609

4610
    private Object writeReplace() {
4611
      return makeSerializationProxy(cache);
1✔
4612
    }
4613
  }
4614
}
4615

4616
/** The namespace for field padding through inheritance. */
4617
@SuppressWarnings({"IdentifierName", "MultiVariableDeclaration"})
4618
final class BLCHeader {
4619

4620
  private BLCHeader() {}
4621

4622
  @SuppressWarnings("unused")
4623
  static class PadDrainStatus {
1✔
4624
    byte p000, p001, p002, p003, p004, p005, p006, p007;
4625
    byte p008, p009, p010, p011, p012, p013, p014, p015;
4626
    byte p016, p017, p018, p019, p020, p021, p022, p023;
4627
    byte p024, p025, p026, p027, p028, p029, p030, p031;
4628
    byte p032, p033, p034, p035, p036, p037, p038, p039;
4629
    byte p040, p041, p042, p043, p044, p045, p046, p047;
4630
    byte p048, p049, p050, p051, p052, p053, p054, p055;
4631
    byte p056, p057, p058, p059, p060, p061, p062, p063;
4632
    byte p064, p065, p066, p067, p068, p069, p070, p071;
4633
    byte p072, p073, p074, p075, p076, p077, p078, p079;
4634
    byte p080, p081, p082, p083, p084, p085, p086, p087;
4635
    byte p088, p089, p090, p091, p092, p093, p094, p095;
4636
    byte p096, p097, p098, p099, p100, p101, p102, p103;
4637
    byte p104, p105, p106, p107, p108, p109, p110, p111;
4638
    byte p112, p113, p114, p115, p116, p117, p118, p119;
4639
  }
4640

4641
  /** Enforces a memory layout to avoid false sharing by padding the drain status. */
4642
  abstract static class DrainStatusRef extends PadDrainStatus {
1✔
4643
    static final VarHandle DRAIN_STATUS = findVarHandle(
1✔
4644
        DrainStatusRef.class, "drainStatus", int.class);
4645

4646
    /** A drain is not taking place. */
4647
    static final int IDLE = 0;
4648
    /** A drain is required due to a pending write modification. */
4649
    static final int REQUIRED = 1;
4650
    /** A drain is in progress and will transition to idle. */
4651
    static final int PROCESSING_TO_IDLE = 2;
4652
    /** A drain is in progress and will transition to required. */
4653
    static final int PROCESSING_TO_REQUIRED = 3;
4654

4655
    /** The draining status of the buffers. */
4656
    volatile int drainStatus = IDLE;
1✔
4657

4658
    /**
4659
     * Returns whether maintenance work is needed.
4660
     *
4661
     * @param delayable if draining the read buffer can be delayed
4662
     */
4663
    @SuppressWarnings("StatementSwitchToExpressionSwitch")
4664
    boolean shouldDrainBuffers(boolean delayable) {
4665
      switch (drainStatusOpaque()) {
1✔
4666
        case IDLE:
4667
          return !delayable;
1✔
4668
        case REQUIRED:
4669
          return true;
1✔
4670
        case PROCESSING_TO_IDLE:
4671
        case PROCESSING_TO_REQUIRED:
4672
          return false;
1✔
4673
        default:
4674
          throw new IllegalStateException("Invalid drain status: " + drainStatus);
1✔
4675
      }
4676
    }
4677

4678
    int drainStatusOpaque() {
4679
      return (int) DRAIN_STATUS.getOpaque(this);
1✔
4680
    }
4681

4682
    int drainStatusAcquire() {
4683
      return (int) DRAIN_STATUS.getAcquire(this);
1✔
4684
    }
4685

4686
    void setDrainStatusOpaque(int drainStatus) {
4687
      DRAIN_STATUS.setOpaque(this, drainStatus);
1✔
4688
    }
1✔
4689

4690
    void setDrainStatusRelease(int drainStatus) {
4691
      DRAIN_STATUS.setRelease(this, drainStatus);
1✔
4692
    }
1✔
4693

4694
    boolean casDrainStatus(int expect, int update) {
4695
      return DRAIN_STATUS.compareAndSet(this, expect, update);
1✔
4696
    }
4697

4698
    static VarHandle findVarHandle(Class<?> recv, String name, Class<?> type) {
4699
      try {
4700
        return MethodHandles.lookup().findVarHandle(recv, name, type);
1✔
4701
      } catch (ReflectiveOperationException e) {
1✔
4702
        throw new ExceptionInInitializerError(e);
1✔
4703
      }
4704
    }
4705
  }
4706
}
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