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

ben-manes / caffeine / #5590

05 Jul 2026 06:38AM UTC coverage: 99.652% (-0.3%) from 99.904%
#5590

push

github

ben-manes
Resolve substitutions in URI-loaded jcache configs

TypesafeConfigurator's file/jar/classpath branches returned a config
stack built on defaultReferenceUnresolved without calling resolve(),
so any HOCON substitution (${ref}, or ${caffeine.jcache.default}
inheritance) threw ConfigException.NotResolved on the first getter.
The default ConfigFactory.load() branch already resolves; append
.resolve() to the three URI branches to match.

4062 of 4088 branches covered (99.36%)

6 of 6 new or added lines in 1 file covered. (100.0%)

21 existing lines in 3 files now uncovered.

8308 of 8337 relevant lines covered (99.65%)

1.0 hits per line

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

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

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

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

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

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

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

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

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

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

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

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

267
  final boolean isWeighted;
268
  final boolean isAsync;
269

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

761
    return first;
1✔
762
  }
763

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1181
    return true;
1✔
1182
  }
1183

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1519
    return null;
1✔
1520
  }
1521

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2933
    return ctx.newValue;
1✔
2934
  }
2935

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

3523
    private long weightedSize;
3524

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

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

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

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

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

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

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

3565
    @Override
3566
    public boolean removeAll(Collection<?> collection) {
3567
      requireNonNull(collection);
1✔
3568
      @Var boolean modified = false;
1✔
3569
      if (cache.collectKeys() || ((collection instanceof Set<?>) && (collection.size() > size()))) {
1✔
3570
        for (K key : this) {
1✔
3571
          if (collection.contains(key)) {
1✔
3572
            modified |= remove(key);
1✔
3573
          }
3574
        }
1✔
3575
      } else {
3576
        for (var item : collection) {
1✔
3577
          modified |= (item != null) && remove(item);
1✔
3578
        }
1✔
3579
      }
3580
      return modified;
1✔
3581
    }
3582

3583
    @Override
3584
    public boolean remove(Object o) {
3585
      return (cache.remove(o) != null);
1✔
3586
    }
3587

3588
    @Override
3589
    public boolean removeIf(Predicate<? super K> filter) {
3590
      requireNonNull(filter);
1✔
3591
      @Var boolean modified = false;
1✔
3592
      for (K key : this) {
1✔
3593
        if (filter.test(key) && remove(key)) {
1✔
3594
          modified = true;
1✔
3595
        }
3596
      }
1✔
3597
      return modified;
1✔
3598
    }
3599

3600
    @Override
3601
    public boolean retainAll(Collection<?> collection) {
3602
      requireNonNull(collection);
1✔
3603
      @Var boolean modified = false;
1✔
3604
      for (K key : this) {
1✔
3605
        if (!collection.contains(key) && remove(key)) {
1✔
3606
          modified = true;
1✔
3607
        }
3608
      }
1✔
3609
      return modified;
1✔
3610
    }
3611

3612
    @Override
3613
    public Iterator<K> iterator() {
3614
      return new KeyIterator<>(cache);
1✔
3615
    }
3616

3617
    @Override
3618
    public Spliterator<K> spliterator() {
3619
      return new KeySpliterator<>(cache);
1✔
3620
    }
3621
  }
3622

3623
  /** An adapter to safely externalize the key iterator. */
3624
  static final class KeyIterator<K, V> implements Iterator<K> {
3625
    final EntryIterator<K, V> iterator;
3626

3627
    KeyIterator(BoundedLocalCache<K, V> cache) {
1✔
3628
      this.iterator = new EntryIterator<>(cache);
1✔
3629
    }
1✔
3630

3631
    @Override
3632
    public boolean hasNext() {
3633
      return iterator.hasNext();
1✔
3634
    }
3635

3636
    @Override
3637
    public K next() {
3638
      return iterator.nextKey();
1✔
3639
    }
3640

3641
    @Override
3642
    public void remove() {
3643
      iterator.remove();
1✔
3644
    }
1✔
3645
  }
3646

3647
  /** An adapter to safely externalize the key spliterator. */
3648
  static final class KeySpliterator<K, V> implements Spliterator<K> {
3649
    final Spliterator<Node<K, V>> spliterator;
3650
    final BoundedLocalCache<K, V> cache;
3651

3652
    KeySpliterator(BoundedLocalCache<K, V> cache) {
3653
      this(cache, cache.data.values().spliterator());
1✔
3654
    }
1✔
3655

3656
    KeySpliterator(BoundedLocalCache<K, V> cache, Spliterator<Node<K, V>> spliterator) {
1✔
3657
      this.spliterator = requireNonNull(spliterator);
1✔
3658
      this.cache = requireNonNull(cache);
1✔
3659
    }
1✔
3660

3661
    @Override
3662
    public void forEachRemaining(Consumer<? super K> action) {
3663
      requireNonNull(action);
1✔
3664
      Consumer<Node<K, V>> consumer = node -> {
1✔
3665
        K key = node.getKey();
1✔
3666
        V value = node.getValue();
1✔
3667
        long now = cache.expirationTicker().read();
1✔
3668
        if ((key == null) || (value == null) || cache.hasExpired(node, now, value)) {
1✔
3669
          cache.scheduleDrainBuffers();
1✔
3670
        } else if (node.isAlive()) {
1✔
3671
          action.accept(key);
1✔
3672
        }
3673
      };
1✔
3674
      spliterator.forEachRemaining(consumer);
1✔
3675
    }
1✔
3676

3677
    @Override
3678
    public boolean tryAdvance(Consumer<? super K> action) {
3679
      requireNonNull(action);
1✔
3680
      boolean[] advanced = { false };
1✔
3681
      Consumer<Node<K, V>> consumer = node -> {
1✔
3682
        K key = node.getKey();
1✔
3683
        V value = node.getValue();
1✔
3684
        long now = cache.expirationTicker().read();
1✔
3685
        if ((key == null) || (value == null) || cache.hasExpired(node, now, value)) {
1✔
3686
          cache.scheduleDrainBuffers();
1✔
3687
        } else if (node.isAlive()) {
1✔
3688
          action.accept(key);
1✔
3689
          advanced[0] = true;
1✔
3690
        }
3691
      };
1✔
3692
      while (spliterator.tryAdvance(consumer)) {
1✔
3693
        if (advanced[0]) {
1✔
3694
          return true;
1✔
3695
        }
3696
      }
3697
      return false;
1✔
3698
    }
3699

3700
    @Override
3701
    public @Nullable Spliterator<K> trySplit() {
3702
      Spliterator<Node<K, V>> split = spliterator.trySplit();
1✔
3703
      return (split == null) ? null : new KeySpliterator<>(cache, split);
1✔
3704
    }
3705

3706
    @Override
3707
    public long estimateSize() {
3708
      return spliterator.estimateSize();
1✔
3709
    }
3710

3711
    @Override
3712
    public int characteristics() {
3713
      return DISTINCT | CONCURRENT | NONNULL;
1✔
3714
    }
3715
  }
3716

3717
  /** An adapter to safely externalize the values. */
3718
  static final class ValuesView<K, V> extends AbstractCollection<V> {
3719
    final BoundedLocalCache<K, V> cache;
3720

3721
    ValuesView(BoundedLocalCache<K, V> cache) {
1✔
3722
      this.cache = requireNonNull(cache);
1✔
3723
    }
1✔
3724

3725
    @Override
3726
    public int size() {
3727
      return cache.size();
1✔
3728
    }
3729

3730
    @Override
3731
    public void clear() {
3732
      cache.clear();
1✔
3733
    }
1✔
3734

3735
    @Override
3736
    @SuppressWarnings("SuspiciousMethodCalls")
3737
    public boolean contains(Object o) {
3738
      return cache.containsValue(o);
1✔
3739
    }
3740

3741
    @Override
3742
    public boolean removeAll(Collection<?> collection) {
3743
      requireNonNull(collection);
1✔
3744
      @Var boolean modified = false;
1✔
3745
      for (var iterator = new EntryIterator<>(cache); iterator.hasNext();) {
1✔
3746
        var key = requireNonNull(iterator.key);
1✔
3747
        var value = requireNonNull(iterator.value);
1✔
3748
        if (collection.contains(value) && cache.remove(key, value)) {
1✔
3749
          modified = true;
1✔
3750
        }
3751
        iterator.advance();
1✔
3752
      }
1✔
3753
      return modified;
1✔
3754
    }
3755

3756
    @Override
3757
    public boolean remove(@Nullable Object o) {
3758
      if (o == null) {
1✔
3759
        return false;
1✔
3760
      }
3761
      for (var iterator = new EntryIterator<>(cache); iterator.hasNext();) {
1✔
3762
        var key = requireNonNull(iterator.key);
1✔
3763
        var node = requireNonNull(iterator.next);
1✔
3764
        var value = requireNonNull(iterator.value);
1✔
3765
        if (node.containsValue(o) && cache.remove(key, value)) {
1✔
3766
          return true;
1✔
3767
        }
3768
        iterator.advance();
1✔
3769
      }
1✔
3770
      return false;
1✔
3771
    }
3772

3773
    @Override
3774
    public boolean removeIf(Predicate<? super V> filter) {
3775
      requireNonNull(filter);
1✔
3776
      @Var boolean modified = false;
1✔
3777
      for (var iterator = new EntryIterator<>(cache); iterator.hasNext();) {
1✔
3778
        var value = requireNonNull(iterator.value);
1✔
3779
        if (filter.test(value)) {
1✔
3780
          var key = requireNonNull(iterator.key);
1✔
3781
          modified |= cache.remove(key, value);
1✔
3782
        }
3783
        iterator.advance();
1✔
3784
      }
1✔
3785
      return modified;
1✔
3786
    }
3787

3788
    @Override
3789
    public boolean retainAll(Collection<?> collection) {
3790
      requireNonNull(collection);
1✔
3791
      @Var boolean modified = false;
1✔
3792
      for (var iterator = new EntryIterator<>(cache); iterator.hasNext();) {
1✔
3793
        var key = requireNonNull(iterator.key);
1✔
3794
        var value = requireNonNull(iterator.value);
1✔
3795
        if (!collection.contains(value) && cache.remove(key, value)) {
1✔
3796
          modified = true;
1✔
3797
        }
3798
        iterator.advance();
1✔
3799
      }
1✔
3800
      return modified;
1✔
3801
    }
3802

3803
    @Override
3804
    public Iterator<V> iterator() {
3805
      return new ValueIterator<>(cache);
1✔
3806
    }
3807

3808
    @Override
3809
    public Spliterator<V> spliterator() {
3810
      return new ValueSpliterator<>(cache);
1✔
3811
    }
3812
  }
3813

3814
  /** An adapter to safely externalize the value iterator. */
3815
  static final class ValueIterator<K, V> implements Iterator<V> {
3816
    final EntryIterator<K, V> iterator;
3817

3818
    ValueIterator(BoundedLocalCache<K, V> cache) {
1✔
3819
      this.iterator = new EntryIterator<>(cache);
1✔
3820
    }
1✔
3821

3822
    @Override
3823
    public boolean hasNext() {
3824
      return iterator.hasNext();
1✔
3825
    }
3826

3827
    @Override
3828
    public V next() {
3829
      return iterator.nextValue();
1✔
3830
    }
3831

3832
    @Override
3833
    public void remove() {
3834
      iterator.remove();
1✔
3835
    }
1✔
3836
  }
3837

3838
  /** An adapter to safely externalize the value spliterator. */
3839
  static final class ValueSpliterator<K, V> implements Spliterator<V> {
3840
    final Spliterator<Node<K, V>> spliterator;
3841
    final BoundedLocalCache<K, V> cache;
3842

3843
    ValueSpliterator(BoundedLocalCache<K, V> cache) {
3844
      this(cache, cache.data.values().spliterator());
1✔
3845
    }
1✔
3846

3847
    ValueSpliterator(BoundedLocalCache<K, V> cache, Spliterator<Node<K, V>> spliterator) {
1✔
3848
      this.spliterator = requireNonNull(spliterator);
1✔
3849
      this.cache = requireNonNull(cache);
1✔
3850
    }
1✔
3851

3852
    @Override
3853
    public void forEachRemaining(Consumer<? super V> action) {
3854
      requireNonNull(action);
1✔
3855
      Consumer<Node<K, V>> consumer = node -> {
1✔
3856
        K key = node.getKey();
1✔
3857
        V value = node.getValue();
1✔
3858
        long now = cache.expirationTicker().read();
1✔
3859
        if ((key == null) || (value == null) || cache.hasExpired(node, now, value)) {
1✔
3860
          cache.scheduleDrainBuffers();
1✔
3861
        } else if (node.isAlive()) {
1✔
3862
          action.accept(value);
1✔
3863
        }
3864
      };
1✔
3865
      spliterator.forEachRemaining(consumer);
1✔
3866
    }
1✔
3867

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

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

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

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

3908
  /** An adapter to safely externalize the entries. */
3909
  static final class EntrySetView<K, V> extends AbstractSet<Entry<K, V>> {
3910
    final BoundedLocalCache<K, V> cache;
3911

3912
    EntrySetView(BoundedLocalCache<K, V> cache) {
1✔
3913
      this.cache = requireNonNull(cache);
1✔
3914
    }
1✔
3915

3916
    @Override
3917
    public int size() {
3918
      return cache.size();
1✔
3919
    }
3920

3921
    @Override
3922
    public void clear() {
3923
      cache.clear();
1✔
3924
    }
1✔
3925

3926
    @Override
3927
    public boolean contains(Object o) {
3928
      if (!(o instanceof Entry<?, ?>)) {
1✔
3929
        return false;
1✔
3930
      }
3931
      var entry = (Entry<?, ?>) o;
1✔
3932
      var key = entry.getKey();
1✔
3933
      var value = entry.getValue();
1✔
3934
      if ((key == null) || (value == null)) {
1✔
3935
        return false;
1✔
3936
      }
3937
      Node<K, V> node = cache.data.get(cache.nodeFactory.newLookupKey(key));
1✔
3938
      if (node == null) {
1✔
3939
        return false;
1✔
3940
      }
3941
      V nodeValue = node.getValue();
1✔
3942
      return (nodeValue != null) && node.containsValue(value)
1✔
3943
          && !cache.hasExpired(node, cache.expirationTicker().read(), nodeValue);
1!
3944
    }
3945

3946
    @Override
3947
    public boolean removeAll(Collection<?> collection) {
3948
      requireNonNull(collection);
1✔
3949
      @Var boolean modified = false;
1✔
3950
      if (cache.collectKeys() || ((collection instanceof Set<?>) && (collection.size() > size()))) {
1✔
3951
        for (var entry : this) {
1✔
3952
          if (collection.contains(entry)) {
1✔
3953
            modified |= remove(entry);
1✔
3954
          }
3955
        }
1✔
3956
      } else {
3957
        for (var item : collection) {
1✔
3958
          modified |= (item != null) && remove(item);
1✔
3959
        }
1✔
3960
      }
3961
      return modified;
1✔
3962
    }
3963

3964
    @Override
3965
    @SuppressWarnings("SuspiciousMethodCalls")
3966
    public boolean remove(Object o) {
3967
      if (!(o instanceof Entry<?, ?>)) {
1✔
3968
        return false;
1✔
3969
      }
3970
      var entry = (Entry<?, ?>) o;
1✔
3971
      var key = entry.getKey();
1✔
3972
      return (key != null) && cache.remove(key, entry.getValue());
1✔
3973
    }
3974

3975
    @Override
3976
    public boolean removeIf(Predicate<? super Entry<K, V>> filter) {
3977
      requireNonNull(filter);
1✔
3978
      @Var boolean modified = false;
1✔
3979
      for (var iterator = new EntryIterator<>(cache); iterator.hasNext();) {
1✔
3980
        var key = requireNonNull(iterator.key);
1✔
3981
        var value = requireNonNull(iterator.value);
1✔
3982
        if (filter.test(Map.entry(key, value))) {
1✔
3983
          modified |= cache.remove(key, value);
1✔
3984
        }
3985
        iterator.advance();
1✔
3986
      }
1✔
3987
      return modified;
1✔
3988
    }
3989

3990
    @Override
3991
    public boolean retainAll(Collection<?> collection) {
3992
      requireNonNull(collection);
1✔
3993
      @Var boolean modified = false;
1✔
3994
      for (var entry : this) {
1✔
3995
        if (!collection.contains(entry) && remove(entry)) {
1✔
3996
          modified = true;
1✔
3997
        }
3998
      }
1✔
3999
      return modified;
1✔
4000
    }
4001

4002
    @Override
4003
    public Iterator<Entry<K, V>> iterator() {
4004
      return new EntryIterator<>(cache);
1✔
4005
    }
4006

4007
    @Override
4008
    public Spliterator<Entry<K, V>> spliterator() {
4009
      return new EntrySpliterator<>(cache);
1✔
4010
    }
4011
  }
4012

4013
  /** An adapter to safely externalize the entry iterator. */
4014
  static final class EntryIterator<K, V> implements Iterator<Entry<K, V>> {
4015
    final BoundedLocalCache<K, V> cache;
4016
    final Iterator<Node<K, V>> iterator;
4017

4018
    @Nullable K key;
4019
    @Nullable V value;
4020
    @Nullable K removalKey;
4021
    @Nullable Node<K, V> next;
4022

4023
    EntryIterator(BoundedLocalCache<K, V> cache) {
1✔
4024
      this.iterator = cache.data.values().iterator();
1✔
4025
      this.cache = cache;
1✔
4026
    }
1✔
4027

4028
    @Override
4029
    public boolean hasNext() {
4030
      if (next != null) {
1✔
4031
        return true;
1✔
4032
      }
4033

4034
      long now = cache.expirationTicker().read();
1✔
4035
      while (iterator.hasNext()) {
1✔
4036
        next = iterator.next();
1✔
4037
        value = next.getValue();
1✔
4038
        key = next.getKey();
1✔
4039

4040
        boolean evictable = (key == null) || (value == null) || cache.hasExpired(next, now, value);
1✔
4041
        if (evictable || !next.isAlive()) {
1✔
4042
          if (evictable) {
1✔
4043
            cache.scheduleDrainBuffers();
1✔
4044
          }
4045
          advance();
1✔
4046
          continue;
1✔
4047
        }
4048
        return true;
1✔
4049
      }
4050
      return false;
1✔
4051
    }
4052

4053
    /** Invalidates the current position so that the iterator may compute the next position. */
4054
    void advance() {
4055
      value = null;
1✔
4056
      next = null;
1✔
4057
      key = null;
1✔
4058
    }
1✔
4059

4060
    K nextKey() {
4061
      if (!hasNext()) {
1✔
4062
        throw new NoSuchElementException();
1✔
4063
      }
4064
      removalKey = key;
1✔
4065
      advance();
1✔
4066
      return requireNonNull(removalKey);
1✔
4067
    }
4068

4069
    V nextValue() {
4070
      if (!hasNext()) {
1✔
4071
        throw new NoSuchElementException();
1✔
4072
      }
4073
      removalKey = key;
1✔
4074
      V val = value;
1✔
4075
      advance();
1✔
4076
      return requireNonNull(val);
1✔
4077
    }
4078

4079
    @Override
4080
    public Entry<K, V> next() {
4081
      if (!hasNext()) {
1✔
4082
        throw new NoSuchElementException();
1✔
4083
      }
4084
      var entry = new WriteThroughEntry<K, @NonNull V>(
1✔
4085
          cache, requireNonNull(key), requireNonNull(value));
1✔
4086
      removalKey = key;
1✔
4087
      advance();
1✔
4088
      return entry;
1✔
4089
    }
4090

4091
    @Override
4092
    public void remove() {
4093
      if (removalKey == null) {
1✔
4094
        throw new IllegalStateException();
1✔
4095
      }
4096
      cache.remove(removalKey);
1✔
4097
      removalKey = null;
1✔
4098
    }
1✔
4099
  }
4100

4101
  /** An adapter to safely externalize the entry spliterator. */
4102
  static final class EntrySpliterator<K, V> implements Spliterator<Entry<K, V>> {
4103
    final Spliterator<Node<K, V>> spliterator;
4104
    final BoundedLocalCache<K, V> cache;
4105

4106
    EntrySpliterator(BoundedLocalCache<K, V> cache) {
4107
      this(cache, cache.data.values().spliterator());
1✔
4108
    }
1✔
4109

4110
    EntrySpliterator(BoundedLocalCache<K, V> cache, Spliterator<Node<K, V>> spliterator) {
1✔
4111
      this.spliterator = requireNonNull(spliterator);
1✔
4112
      this.cache = requireNonNull(cache);
1✔
4113
    }
1✔
4114

4115
    @Override
4116
    public void forEachRemaining(Consumer<? super Entry<K, V>> action) {
4117
      requireNonNull(action);
1✔
4118
      Consumer<Node<K, V>> consumer = node -> {
1✔
4119
        K key = node.getKey();
1✔
4120
        V value = node.getValue();
1✔
4121
        long now = cache.expirationTicker().read();
1✔
4122
        if ((key == null) || (value == null) || cache.hasExpired(node, now, value)) {
1✔
4123
          cache.scheduleDrainBuffers();
1✔
4124
        } else if (node.isAlive()) {
1✔
4125
          action.accept(new WriteThroughEntry<>(cache, key, value));
1✔
4126
        }
4127
      };
1✔
4128
      spliterator.forEachRemaining(consumer);
1✔
4129
    }
1✔
4130

4131
    @Override
4132
    public boolean tryAdvance(Consumer<? super Entry<K, V>> action) {
4133
      requireNonNull(action);
1✔
4134
      boolean[] advanced = { false };
1✔
4135
      Consumer<Node<K, V>> consumer = node -> {
1✔
4136
        K key = node.getKey();
1✔
4137
        V value = node.getValue();
1✔
4138
        long now = cache.expirationTicker().read();
1✔
4139
        if ((key == null) || (value == null) || cache.hasExpired(node, now, value)) {
1✔
4140
          cache.scheduleDrainBuffers();
1✔
4141
        } else if (node.isAlive()) {
1✔
4142
          action.accept(new WriteThroughEntry<>(cache, key, value));
1✔
4143
          advanced[0] = true;
1✔
4144
        }
4145
      };
1✔
4146
      while (spliterator.tryAdvance(consumer)) {
1✔
4147
        if (advanced[0]) {
1✔
4148
          return true;
1✔
4149
        }
4150
      }
4151
      return false;
1✔
4152
    }
4153

4154
    @Override
4155
    public @Nullable Spliterator<Entry<K, V>> trySplit() {
4156
      Spliterator<Node<K, V>> split = spliterator.trySplit();
1✔
4157
      return (split == null) ? null : new EntrySpliterator<>(cache, split);
1✔
4158
    }
4159

4160
    @Override
4161
    public long estimateSize() {
4162
      return spliterator.estimateSize();
1✔
4163
    }
4164

4165
    @Override
4166
    public int characteristics() {
4167
      return DISTINCT | CONCURRENT | NONNULL;
1✔
4168
    }
4169
  }
4170

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

4175
    final WeakReference<BoundedLocalCache<?, ?>> reference;
4176

4177
    PerformCleanupTask(BoundedLocalCache<?, ?> cache) {
1✔
4178
      reference = new WeakReference<>(cache);
1✔
4179
    }
1✔
4180

4181
    @Override
4182
    protected boolean exec() {
4183
      try {
4184
        run();
1✔
4185
      } catch (Throwable t) {
1✔
4186
        logger.log(Level.ERROR, "Exception thrown when performing the maintenance task", t);
1✔
4187
      }
1✔
4188

4189
      // Indicates that the task has not completed to allow subsequent submissions to execute
4190
      return false;
1✔
4191
    }
4192

4193
    @Override
4194
    public void run() {
4195
      BoundedLocalCache<?, ?> cache = reference.get();
1✔
4196
      if (cache != null) {
1✔
4197
        cache.performCleanUp(/* ignored */ null);
1✔
4198
      }
4199
    }
1✔
4200

4201
    /**
4202
     * This method cannot be ignored due to being final, so a hostile user supplied Executor could
4203
     * forcibly complete the task and halt future executions. There are easier ways to intentionally
4204
     * harm a system, so this is assumed to not happen in practice.
4205
     */
4206
    // public final void quietlyComplete() {}
4207

4208
    @Override public void complete(@Nullable Void value) {}
1✔
4209
    @Override public void setRawResult(@Nullable Void value) {}
1✔
4210
    @Override public @Nullable Void getRawResult() { return null; }
1✔
4211
    @Override public void completeExceptionally(@Nullable Throwable t) {}
1✔
4212
    @Override public boolean cancel(boolean mayInterruptIfRunning) { return false; }
1✔
4213
  }
4214

4215
  /** Creates a serialization proxy based on the common configuration shared by all cache types. */
4216
  static <K, V> SerializationProxy<K, V> makeSerializationProxy(BoundedLocalCache<?, ?> cache) {
4217
    var proxy = new SerializationProxy<K, V>();
1✔
4218
    proxy.weakKeys = cache.collectKeys();
1✔
4219
    proxy.weakValues = cache.nodeFactory.weakValues();
1✔
4220
    proxy.softValues = cache.nodeFactory.softValues();
1✔
4221
    proxy.isRecordingStats = cache.isRecordingStats();
1✔
4222
    proxy.evictionListener = cache.evictionListener;
1✔
4223
    proxy.removalListener = cache.removalListener();
1✔
4224
    proxy.ticker = cache.expirationTicker();
1✔
4225
    if (cache.expiresAfterAccess()) {
1✔
4226
      proxy.expiresAfterAccessNanos = cache.expiresAfterAccessNanos();
1✔
4227
    }
4228
    if (cache.expiresAfterWrite()) {
1✔
4229
      proxy.expiresAfterWriteNanos = cache.expiresAfterWriteNanos();
1✔
4230
    }
4231
    if (cache.expiresVariable()) {
1✔
4232
      proxy.expiry = cache.expiry();
1✔
4233
    }
4234
    if (cache.refreshAfterWrite()) {
1✔
4235
      proxy.refreshAfterWriteNanos = cache.refreshAfterWriteNanos();
1✔
4236
    }
4237
    if (cache.evicts()) {
1✔
4238
      if (cache.isWeighted) {
1✔
4239
        proxy.weigher = cache.weigher;
1✔
4240
        proxy.maximumWeight = cache.maximum();
1✔
4241
      } else {
4242
        proxy.maximumSize = cache.maximum();
1✔
4243
      }
4244
    }
4245
    proxy.cacheLoader = cache.cacheLoader;
1✔
4246
    proxy.async = cache.isAsync;
1✔
4247
    return proxy;
1✔
4248
  }
4249

4250
  /* --------------- Manual Cache --------------- */
4251

4252
  static class BoundedLocalManualCache<K, V> implements LocalManualCache<K, V>, Serializable {
4253
    private static final long serialVersionUID = 1;
4254

4255
    final BoundedLocalCache<K, V> cache;
4256

4257
    @Nullable Policy<K, V> policy;
4258

4259
    BoundedLocalManualCache(Caffeine<K, V> builder) {
4260
      this(builder, null);
1✔
4261
    }
1✔
4262

4263
    BoundedLocalManualCache(Caffeine<K, V> builder, @Nullable CacheLoader<? super K, V> loader) {
1✔
4264
      cache = LocalCacheFactory.newBoundedLocalCache(builder, loader, /* isAsync= */ false);
1✔
4265
    }
1✔
4266

4267
    @Override
4268
    public final BoundedLocalCache<K, V> cache() {
4269
      return cache;
1✔
4270
    }
4271

4272
    @Override
4273
    public final Policy<K, V> policy() {
4274
      if (policy == null) {
1✔
4275
        Function<@Nullable V, @Nullable V> identity = v -> v;
1✔
4276
        policy = new BoundedPolicy<>(cache, identity, cache.isWeighted);
1✔
4277
      }
4278
      return policy;
1✔
4279
    }
4280

4281
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
4282
      throw new InvalidObjectException("Proxy required");
1✔
4283
    }
4284

4285
    private Object writeReplace() {
4286
      return makeSerializationProxy(cache);
1✔
4287
    }
4288
  }
4289

4290
  @SuppressWarnings({"NullableOptional",
4291
    "OptionalAssignedToNull", "OptionalUsedAsFieldOrParameterType"})
4292
  static final class BoundedPolicy<K, V> implements Policy<K, V> {
4293
    final Function<@Nullable V, @Nullable V> transformer;
4294
    final BoundedLocalCache<K, V> cache;
4295
    final boolean isWeighted;
4296

4297
    @Nullable Optional<Eviction<K, V>> eviction;
4298
    @Nullable Optional<FixedRefresh<K, V>> refreshes;
4299
    @Nullable Optional<FixedExpiration<K, V>> afterWrite;
4300
    @Nullable Optional<FixedExpiration<K, V>> afterAccess;
4301
    @Nullable Optional<VarExpiration<K, V>> variable;
4302

4303
    BoundedPolicy(BoundedLocalCache<K, V> cache,
4304
        Function<@Nullable V, @Nullable V> transformer, boolean isWeighted) {
1✔
4305
      this.transformer = transformer;
1✔
4306
      this.isWeighted = isWeighted;
1✔
4307
      this.cache = cache;
1✔
4308
    }
1✔
4309

4310
    @Override public boolean isRecordingStats() {
4311
      return cache.isRecordingStats();
1✔
4312
    }
4313
    @Override public @Nullable V getIfPresentQuietly(K key) {
4314
      return transformer.apply(cache.getIfPresentQuietly(key));
1✔
4315
    }
4316
    @SuppressWarnings("GuardedByChecker")
4317
    @Override public @Nullable CacheEntry<K, V> getEntryIfPresentQuietly(K key) {
4318
      Node<K, V> node = cache.data.get(cache.nodeFactory.newLookupKey(key));
1✔
4319
      return (node == null) ? null : cache.nodeToCacheEntry(node, transformer, node.getWeight());
1✔
4320
    }
4321
    @SuppressWarnings("Java9CollectionFactory")
4322
    @Override public Map<K, CompletableFuture<V>> refreshes() {
4323
      var refreshes = cache.refreshes;
1✔
4324
      if ((refreshes == null) || refreshes.isEmpty()) {
1✔
4325
        @SuppressWarnings({"ImmutableMapOf", "RedundantUnmodifiable"})
4326
        Map<K, CompletableFuture<V>> emptyMap = Collections.unmodifiableMap(Collections.emptyMap());
1✔
4327
        return emptyMap;
1✔
4328
      } else if (cache.collectKeys()) {
1✔
4329
        var inFlight = new IdentityHashMap<K, CompletableFuture<V>>(refreshes.size());
1✔
4330
        for (var entry : refreshes.entrySet()) {
1✔
4331
          @SuppressWarnings("unchecked")
4332
          @Nullable K key = ((InternalReference<K>) entry.getKey()).get();
1✔
4333
          @SuppressWarnings("unchecked")
4334
          var future = (CompletableFuture<V>) entry.getValue();
1✔
4335
          if (key != null) {
1✔
4336
            inFlight.put(key, future);
1✔
4337
          }
4338
        }
1✔
4339
        return Collections.unmodifiableMap(inFlight);
1✔
4340
      }
4341
      @SuppressWarnings("unchecked")
4342
      var castedRefreshes = (Map<K, CompletableFuture<V>>) (Object) refreshes;
1✔
4343
      return Collections.unmodifiableMap(new HashMap<>(castedRefreshes));
1✔
4344
    }
4345
    @Override public Optional<Eviction<K, V>> eviction() {
4346
      return cache.evicts()
1✔
4347
          ? (eviction == null) ? (eviction = Optional.of(new BoundedEviction())) : eviction
1✔
4348
          : Optional.empty();
1✔
4349
    }
4350
    @Override public Optional<FixedExpiration<K, V>> expireAfterAccess() {
4351
      if (!cache.expiresAfterAccess()) {
1✔
4352
        return Optional.empty();
1✔
4353
      }
4354
      return (afterAccess == null)
1✔
4355
          ? (afterAccess = Optional.of(new BoundedExpireAfterAccess()))
1✔
4356
          : afterAccess;
1✔
4357
    }
4358
    @Override public Optional<FixedExpiration<K, V>> expireAfterWrite() {
4359
      if (!cache.expiresAfterWrite()) {
1✔
4360
        return Optional.empty();
1✔
4361
      }
4362
      return (afterWrite == null)
1✔
4363
          ? (afterWrite = Optional.of(new BoundedExpireAfterWrite()))
1✔
4364
          : afterWrite;
1✔
4365
    }
4366
    @Override public Optional<VarExpiration<K, V>> expireVariably() {
4367
      if (!cache.expiresVariable()) {
1✔
4368
        return Optional.empty();
1✔
4369
      }
4370
      return (variable == null)
1✔
4371
          ? (variable = Optional.of(new BoundedVarExpiration()))
1✔
4372
          : variable;
1✔
4373
    }
4374
    @Override public Optional<FixedRefresh<K, V>> refreshAfterWrite() {
4375
      if (!cache.refreshAfterWrite()) {
1✔
4376
        return Optional.empty();
1✔
4377
      }
4378
      return (refreshes == null)
1✔
4379
          ? (refreshes = Optional.of(new BoundedRefreshAfterWrite()))
1✔
4380
          : refreshes;
1✔
4381
    }
4382

4383
    final class BoundedEviction implements Eviction<K, V> {
1✔
4384
      @Override public boolean isWeighted() {
4385
        return isWeighted;
1✔
4386
      }
4387
      @Override public OptionalInt weightOf(K key) {
4388
        requireNonNull(key);
1✔
4389
        if (!isWeighted) {
1✔
4390
          return OptionalInt.empty();
1✔
4391
        }
4392
        Node<K, V> node = cache.data.get(cache.nodeFactory.newLookupKey(key));
1✔
4393
        if (node == null) {
1✔
4394
          return OptionalInt.empty();
1✔
4395
        }
4396
        V value = node.getValue();
1✔
4397
        if ((value == null) || cache.hasExpired(node, cache.expirationTicker().read(), value)) {
1✔
4398
          return OptionalInt.empty();
1✔
4399
        }
4400
        synchronized (node) {
1✔
4401
          return node.isAlive() ? OptionalInt.of(node.getWeight()) : OptionalInt.empty();
1✔
4402
        }
4403
      }
4404
      @Override public OptionalLong weightedSize() {
4405
        return isWeighted
1✔
4406
            ? OptionalLong.of(Math.max(0, cache.weightedSizeAcquire()))
1✔
4407
            : OptionalLong.empty();
1✔
4408
      }
4409
      @Override public long getMaximum() {
4410
        return cache.maximumAcquire();
1✔
4411
      }
4412
      @Override public void setMaximum(long maximum) {
4413
        cache.evictionLock.lock();
1✔
4414
        try {
4415
          cache.setMaximumSize(maximum);
1✔
4416
          cache.maintenance(/* ignored */ null);
1✔
4417
        } finally {
4418
          cache.evictionLock.unlock();
1✔
4419
          cache.rescheduleCleanUpIfIncomplete();
1✔
4420
        }
4421
      }
1✔
4422
      @Override public Map<K, V> coldest(int limit) {
4423
        int expectedSize = Math.min(limit, cache.size());
1✔
4424
        var limiter = new SizeLimiter<K, V>(expectedSize, limit);
1✔
4425
        return cache.evictionOrder(/* hottest= */ false, transformer, limiter);
1✔
4426
      }
4427
      @Override public Map<K, V> coldestWeighted(long weightLimit) {
4428
        var limiter = isWeighted()
1✔
4429
            ? new WeightLimiter<K, V>(weightLimit)
1✔
4430
            : new SizeLimiter<K, V>((int) Math.min(weightLimit, cache.size()), weightLimit);
1✔
4431
        return cache.evictionOrder(/* hottest= */ false, transformer, limiter);
1✔
4432
      }
4433
      @Override
4434
      public <T> T coldest(Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4435
        requireNonNull(mappingFunction);
1✔
4436
        return cache.evictionOrder(/* hottest= */ false, transformer, mappingFunction);
1✔
4437
      }
4438
      @Override public Map<K, V> hottest(int limit) {
4439
        int expectedSize = Math.min(limit, cache.size());
1✔
4440
        var limiter = new SizeLimiter<K, V>(expectedSize, limit);
1✔
4441
        return cache.evictionOrder(/* hottest= */ true, transformer, limiter);
1✔
4442
      }
4443
      @Override public Map<K, V> hottestWeighted(long weightLimit) {
4444
        var limiter = isWeighted()
1✔
4445
            ? new WeightLimiter<K, V>(weightLimit)
1✔
4446
            : new SizeLimiter<K, V>((int) Math.min(weightLimit, cache.size()), weightLimit);
1✔
4447
        return cache.evictionOrder(/* hottest= */ true, transformer, limiter);
1✔
4448
      }
4449
      @Override
4450
      public <T> T hottest(Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4451
        requireNonNull(mappingFunction);
1✔
4452
        return cache.evictionOrder(/* hottest= */ true, transformer, mappingFunction);
1✔
4453
      }
4454
    }
4455

4456
    @SuppressWarnings("PreferJavaTimeOverload")
4457
    final class BoundedExpireAfterAccess implements FixedExpiration<K, V> {
1✔
4458
      @Override public OptionalLong ageOf(K key, TimeUnit unit) {
4459
        requireNonNull(key);
1✔
4460
        requireNonNull(unit);
1✔
4461
        Object lookupKey = cache.nodeFactory.newLookupKey(key);
1✔
4462
        Node<K, V> node = cache.data.get(lookupKey);
1✔
4463
        if (node == null) {
1✔
4464
          return OptionalLong.empty();
1✔
4465
        }
4466
        V value = node.getValue();
1✔
4467
        if (value == null) {
1✔
4468
          return OptionalLong.empty();
1✔
4469
        }
4470
        long now = cache.expirationTicker().read();
1✔
4471
        return cache.hasExpired(node, now, value)
1✔
4472
            ? OptionalLong.empty()
1✔
4473
            : OptionalLong.of(unit.convert(now - node.getAccessTime(), TimeUnit.NANOSECONDS));
1✔
4474
      }
4475
      @Override public long getExpiresAfter(TimeUnit unit) {
4476
        return unit.convert(cache.expiresAfterAccessNanos(), TimeUnit.NANOSECONDS);
1✔
4477
      }
4478
      @Override public void setExpiresAfter(long duration, TimeUnit unit) {
4479
        requireArgument(duration >= 0);
1✔
4480
        cache.setExpiresAfterAccessNanos(unit.toNanos(duration));
1✔
4481
        cache.scheduleAfterWrite();
1✔
4482
      }
1✔
4483
      @Override public Map<K, V> oldest(int limit) {
4484
        return oldest(new SizeLimiter<>(Math.min(limit, cache.size()), limit));
1✔
4485
      }
4486
      @Override public <T> T oldest(Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4487
        return cache.expireAfterAccessOrder(/* oldest= */ true, transformer, mappingFunction);
1✔
4488
      }
4489
      @Override public Map<K, V> youngest(int limit) {
4490
        return youngest(new SizeLimiter<>(Math.min(limit, cache.size()), limit));
1✔
4491
      }
4492
      @Override public <T> T youngest(Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4493
        return cache.expireAfterAccessOrder(/* oldest= */ false, transformer, mappingFunction);
1✔
4494
      }
4495
    }
4496

4497
    @SuppressWarnings("PreferJavaTimeOverload")
4498
    final class BoundedExpireAfterWrite implements FixedExpiration<K, V> {
1✔
4499
      @Override public OptionalLong ageOf(K key, TimeUnit unit) {
4500
        requireNonNull(key);
1✔
4501
        requireNonNull(unit);
1✔
4502
        Object lookupKey = cache.nodeFactory.newLookupKey(key);
1✔
4503
        Node<K, V> node = cache.data.get(lookupKey);
1✔
4504
        if (node == null) {
1✔
4505
          return OptionalLong.empty();
1✔
4506
        }
4507
        V value = node.getValue();
1✔
4508
        if (value == null) {
1✔
4509
          return OptionalLong.empty();
1✔
4510
        }
4511
        long now = cache.expirationTicker().read();
1✔
4512
        return cache.hasExpired(node, now, value)
1✔
4513
            ? OptionalLong.empty()
1✔
4514
            : OptionalLong.of(unit.convert(
1✔
4515
                (now & ~1L) - (node.getWriteTime() & ~1L), TimeUnit.NANOSECONDS));
1✔
4516
      }
4517
      @Override public long getExpiresAfter(TimeUnit unit) {
4518
        return unit.convert(cache.expiresAfterWriteNanos(), TimeUnit.NANOSECONDS);
1✔
4519
      }
4520
      @Override public void setExpiresAfter(long duration, TimeUnit unit) {
4521
        requireArgument(duration >= 0);
1!
4522
        cache.setExpiresAfterWriteNanos(unit.toNanos(duration));
1✔
4523
        cache.scheduleAfterWrite();
1✔
4524
      }
1✔
4525
      @Override public Map<K, V> oldest(int limit) {
4526
        return oldest(new SizeLimiter<>(Math.min(limit, cache.size()), limit));
1✔
4527
      }
4528
      @SuppressWarnings("GuardedByChecker")
4529
      @Override public <T> T oldest(Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4530
        return cache.snapshot(cache.writeOrderDeque(), transformer, mappingFunction);
1✔
4531
      }
4532
      @Override public Map<K, V> youngest(int limit) {
4533
        return youngest(new SizeLimiter<>(Math.min(limit, cache.size()), limit));
1✔
4534
      }
4535
      @SuppressWarnings("GuardedByChecker")
4536
      @Override public <T> T youngest(Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4537
        return cache.snapshot(cache.writeOrderDeque()::descendingIterator,
1✔
4538
            transformer, mappingFunction);
4539
      }
4540
    }
4541

4542
    @SuppressWarnings("PreferJavaTimeOverload")
4543
    final class BoundedVarExpiration implements VarExpiration<K, V> {
1✔
4544
      @Override public OptionalLong getExpiresAfter(K key, TimeUnit unit) {
4545
        requireNonNull(key);
1✔
4546
        requireNonNull(unit);
1✔
4547
        Object lookupKey = cache.nodeFactory.newLookupKey(key);
1✔
4548
        Node<K, V> node = cache.data.get(lookupKey);
1✔
4549
        if (node == null) {
1✔
4550
          return OptionalLong.empty();
1✔
4551
        }
4552
        V value = node.getValue();
1✔
4553
        if (value == null) {
1✔
4554
          return OptionalLong.empty();
1✔
4555
        }
4556
        long now = cache.expirationTicker().read();
1✔
4557
        return cache.hasExpired(node, now, value)
1✔
4558
            ? OptionalLong.empty()
1✔
4559
            : OptionalLong.of(unit.convert(node.getVariableTime() - now, TimeUnit.NANOSECONDS));
1✔
4560
      }
4561
      @Override public void setExpiresAfter(K key, long duration, TimeUnit unit) {
4562
        requireNonNull(key);
1✔
4563
        requireNonNull(unit);
1✔
4564
        requireArgument(duration >= 0);
1✔
4565
        Object lookupKey = cache.nodeFactory.newLookupKey(key);
1✔
4566
        Node<K, V> node = cache.data.get(lookupKey);
1✔
4567
        if (node != null) {
1✔
4568
          long now;
4569
          long durationNanos = TimeUnit.NANOSECONDS.convert(duration, unit);
1✔
4570
          synchronized (node) {
1✔
4571
            now = cache.expirationTicker().read();
1✔
4572
            V value = node.getValue();
1✔
4573
            if ((value == null) || cache.isComputingAsync(value)
1✔
4574
                || cache.hasExpired(node, now, value)) {
1✔
4575
              return;
1✔
4576
            }
4577
            node.setVariableTime(now + Math.min(durationNanos, MAXIMUM_EXPIRY));
1✔
4578
          }
1✔
4579
          cache.afterRead(node, now, /* recordHit= */ false);
1✔
4580
        }
4581
      }
1✔
4582
      @Override public @Nullable V put(K key, V value, long duration, TimeUnit unit) {
4583
        requireNonNull(unit);
1✔
4584
        requireNonNull(value);
1✔
4585
        requireArgument(duration >= 0);
1✔
4586
        return cache.isAsync
1✔
4587
            ? putAsync(key, value, duration, unit)
1✔
4588
            : putSync(key, value, duration, unit, /* onlyIfAbsent= */ false);
1✔
4589
      }
4590
      @Override public @Nullable V putIfAbsent(K key, V value, long duration, TimeUnit unit) {
4591
        requireNonNull(unit);
1✔
4592
        requireNonNull(value);
1✔
4593
        requireArgument(duration >= 0);
1✔
4594
        return cache.isAsync
1✔
4595
            ? putIfAbsentAsync(key, value, duration, unit)
1✔
4596
            : putSync(key, value, duration, unit, /* onlyIfAbsent= */ true);
1✔
4597
      }
4598
      @Nullable V putSync(K key, V value, long duration, TimeUnit unit, boolean onlyIfAbsent) {
4599
        var expiry = new FixedExpireAfterWrite<K, V>(duration, unit);
1✔
4600
        return cache.put(key, value, expiry, onlyIfAbsent);
1✔
4601
      }
4602
      @SuppressWarnings("unchecked")
4603
      @Nullable V putIfAbsentAsync(K key, V value, long duration, TimeUnit unit) {
4604
        // Keep in sync with LocalAsyncCache.AsMapView#putIfAbsent(key, value)
4605
        var expiry = (Expiry<K, V>) new AsyncExpiry<>(new FixedExpireAfterWrite<>(duration, unit));
1✔
4606
        var asyncValue = (V) CompletableFuture.completedFuture(value);
1✔
4607

4608
        @Var CompletableFuture<V> priorFuture = null;
1✔
4609
        for (;;) {
4610
          priorFuture = (CompletableFuture<V>) ((priorFuture == null)
1✔
4611
              ? cache.getIfPresent(key, /* recordStats= */ false)
1✔
4612
              : cache.getIfPresentQuietly(key));
1✔
4613
          if (priorFuture != null) {
1✔
4614
            if (!priorFuture.isDone()) {
1✔
4615
              Async.getWhenSuccessful(priorFuture);
1✔
4616
              continue;
1✔
4617
            }
4618

4619
            V prior = Async.getWhenSuccessful(priorFuture);
1✔
4620
            if (prior != null) {
1✔
4621
              return prior;
1✔
4622
            }
4623
          }
4624

4625
          boolean[] added = { false };
1✔
4626
          var hints = new LocalCache.RemapHints();
1✔
4627
          var computed = (CompletableFuture<V>) cache.compute(key, (K k, @Nullable V oldValue) -> {
1✔
4628
            var oldValueFuture = (CompletableFuture<V>) oldValue;
1✔
4629
            added[0] = (oldValueFuture == null)
1✔
4630
                || (oldValueFuture.isDone() && (Async.getIfReady(oldValueFuture) == null));
1✔
4631
            if (added[0]) {
1✔
4632
              return asyncValue;
1✔
4633
            }
4634
            hints.preserveTimestamps = true;
1✔
4635
            hints.preserveRefresh = true;
1✔
4636
            return oldValue;
1✔
4637
          }, expiry, /* recordLoad= */ false, /* recordLoadFailure= */ false, hints);
4638

4639
          if (added[0]) {
1✔
4640
            return null;
1✔
4641
          } else {
4642
            V prior = Async.getWhenSuccessful(computed);
1✔
4643
            if (prior != null) {
1✔
4644
              return prior;
1✔
4645
            }
4646
          }
4647
        }
1✔
4648
      }
4649
      @SuppressWarnings("unchecked")
4650
      @Nullable V putAsync(K key, V value, long duration, TimeUnit unit) {
4651
        var expiry = (Expiry<K, V>) new AsyncExpiry<>(new FixedExpireAfterWrite<>(duration, unit));
1✔
4652
        var asyncValue = (V) CompletableFuture.completedFuture(value);
1✔
4653

4654
        var oldValueFuture = (CompletableFuture<V>) cache.put(
1✔
4655
            key, asyncValue, expiry, /* onlyIfAbsent= */ false);
4656
        return Async.getWhenSuccessful(oldValueFuture);
1✔
4657
      }
4658
      @Override public @Nullable V compute(K key,
4659
          BiFunction<? super K, ? super V, ? extends V> remappingFunction,
4660
          Duration duration) {
4661
        requireNonNull(key);
1✔
4662
        requireNonNull(duration);
1✔
4663
        requireNonNull(remappingFunction);
1✔
4664
        requireArgument(!duration.isNegative(), "duration cannot be negative: %s", duration);
1✔
4665
        var expiry = new FixedExpireAfterWrite<K, V>(
1✔
4666
            toNanosSaturated(duration), TimeUnit.NANOSECONDS);
1✔
4667

4668
        return cache.isAsync
1✔
4669
            ? computeAsync(key, remappingFunction, expiry)
1✔
4670
            : cache.compute(key, remappingFunction, expiry,
1✔
4671
                /* recordLoad= */ true, /* recordLoadFailure= */ true);
4672
      }
4673
      @Nullable V computeAsync(K key,
4674
          BiFunction<? super K, ? super V, ? extends V> remappingFunction,
4675
          Expiry<? super K, ? super V> expiry) {
4676
        // Keep in sync with LocalAsyncCache.AsMapView#compute(key, remappingFunction)
4677
        @SuppressWarnings("unchecked")
4678
        var delegate = (LocalCache<K, CompletableFuture<V>>) cache;
1✔
4679

4680
        @SuppressWarnings({"rawtypes", "unchecked", "Varifier"})
4681
        @Nullable V[] newValue = (@Nullable V[]) new Object[1];
1✔
4682
        for (;;) {
4683
          Async.getWhenSuccessful(delegate.getIfPresentQuietly(key));
1✔
4684

4685
          var hints = new LocalCache.RemapHints();
1✔
4686
          CompletableFuture<V> valueFuture = delegate.compute(
1✔
4687
              key, (K k, @Nullable CompletableFuture<V> oldValueFuture) -> {
4688
                if ((oldValueFuture != null) && !oldValueFuture.isDone()) {
1✔
4689
                  hints.preserveTimestamps = true;
1✔
4690
                  hints.preserveRefresh = true;
1✔
4691
                  return oldValueFuture;
1✔
4692
                }
4693

4694
                V oldValue = Async.getIfReady(oldValueFuture);
1✔
4695
                BiFunction<? super K, ? super V, ? extends @Nullable V> function =
1✔
4696
                    delegate.statsAware(remappingFunction,
1✔
4697
                        /* recordLoad= */ true, /* recordLoadFailure= */ true);
4698
                newValue[0] = function.apply(key, oldValue);
1✔
4699
                return (newValue[0] == null) ? null
1✔
4700
                    : CompletableFuture.completedFuture(newValue[0]);
1✔
4701
              }, new AsyncExpiry<>(expiry), /* recordLoad= */ false,
4702
              /* recordLoadFailure= */ false, hints);
4703

4704
          if (newValue[0] != null) {
1✔
4705
            return newValue[0];
1✔
4706
          } else if (valueFuture == null) {
1✔
4707
            return null;
1✔
4708
          }
4709
        }
1✔
4710
      }
4711
      @Override public Map<K, V> oldest(int limit) {
4712
        return oldest(new SizeLimiter<>(Math.min(limit, cache.size()), limit));
1✔
4713
      }
4714
      @Override public <T> T oldest(Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4715
        return cache.snapshot(cache.timerWheel(), transformer, mappingFunction);
1✔
4716
      }
4717
      @Override public Map<K, V> youngest(int limit) {
4718
        return youngest(new SizeLimiter<>(Math.min(limit, cache.size()), limit));
1✔
4719
      }
4720
      @Override public <T> T youngest(Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4721
        return cache.snapshot(cache.timerWheel()::descendingIterator, transformer, mappingFunction);
1✔
4722
      }
4723
    }
4724

4725
    static final class FixedExpireAfterWrite<K, V> implements Expiry<K, V> {
4726
      final long duration;
4727
      final TimeUnit unit;
4728

4729
      FixedExpireAfterWrite(long duration, TimeUnit unit) {
1✔
4730
        this.duration = duration;
1✔
4731
        this.unit = unit;
1✔
4732
      }
1✔
4733
      @Override public long expireAfterCreate(K key, V value, long currentTime) {
4734
        return unit.toNanos(duration);
1✔
4735
      }
4736
      @Override public long expireAfterUpdate(
4737
          K key, V value, long currentTime, long currentDuration) {
4738
        return unit.toNanos(duration);
1✔
4739
      }
4740
      @CanIgnoreReturnValue
4741
      @Override public long expireAfterRead(
4742
          K key, V value, long currentTime, long currentDuration) {
4743
        return currentDuration;
1✔
4744
      }
4745
    }
4746

4747
    @SuppressWarnings("PreferJavaTimeOverload")
4748
    final class BoundedRefreshAfterWrite implements FixedRefresh<K, V> {
1✔
4749
      @Override public OptionalLong ageOf(K key, TimeUnit unit) {
4750
        requireNonNull(key);
1✔
4751
        requireNonNull(unit);
1✔
4752
        Object lookupKey = cache.nodeFactory.newLookupKey(key);
1✔
4753
        Node<K, V> node = cache.data.get(lookupKey);
1✔
4754
        if (node == null) {
1✔
4755
          return OptionalLong.empty();
1✔
4756
        }
4757
        V value = node.getValue();
1✔
4758
        if (value == null) {
1✔
4759
          return OptionalLong.empty();
1✔
4760
        }
4761
        long now = cache.expirationTicker().read();
1✔
4762
        return cache.hasExpired(node, now, value)
1✔
4763
            ? OptionalLong.empty()
1✔
4764
            : OptionalLong.of(unit.convert(
1✔
4765
                (now & ~1L) - (node.getWriteTime() & ~1L), TimeUnit.NANOSECONDS));
1✔
4766
      }
4767
      @Override public long getRefreshesAfter(TimeUnit unit) {
4768
        return unit.convert(cache.refreshAfterWriteNanos(), TimeUnit.NANOSECONDS);
1✔
4769
      }
4770
      @Override public void setRefreshesAfter(long duration, TimeUnit unit) {
4771
        requireNonNull(unit);
1✔
4772
        requireArgument(duration > 0, "duration must be positive: %s %s", duration, unit);
1✔
4773
        cache.setRefreshAfterWriteNanos(unit.toNanos(duration));
1✔
4774
        cache.scheduleAfterWrite();
1✔
4775
      }
1✔
4776
    }
4777
  }
4778

4779
  /* --------------- Loading Cache --------------- */
4780

4781
  static final class BoundedLocalLoadingCache<K, V>
4782
      extends BoundedLocalManualCache<K, V> implements LocalLoadingCache<K, V> {
4783
    private static final long serialVersionUID = 1;
4784

4785
    final Function<K, @Nullable V> mappingFunction;
4786
    final @Nullable Function<Set<? extends K>, Map<K, V>> bulkMappingFunction;
4787

4788
    BoundedLocalLoadingCache(Caffeine<K, V> builder, CacheLoader<? super K, V> loader) {
4789
      super(builder, loader);
1✔
4790
      requireNonNull(loader);
1✔
4791
      mappingFunction = newMappingFunction(loader);
1✔
4792
      bulkMappingFunction = newBulkMappingFunction(loader);
1✔
4793
    }
1✔
4794

4795
    @Override
4796
    @SuppressWarnings({"DataFlowIssue", "NullAway"})
4797
    public AsyncCacheLoader<? super K, V> cacheLoader() {
4798
      return cache.cacheLoader;
1✔
4799
    }
4800

4801
    @Override
4802
    public Function<K, @Nullable V> mappingFunction() {
4803
      return mappingFunction;
1✔
4804
    }
4805

4806
    @Override
4807
    public @Nullable Function<Set<? extends K>, Map<K, V>> bulkMappingFunction() {
4808
      return bulkMappingFunction;
1✔
4809
    }
4810

4811
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
4812
      throw new InvalidObjectException("Proxy required");
1✔
4813
    }
4814

4815
    private Object writeReplace() {
4816
      return makeSerializationProxy(cache);
1✔
4817
    }
4818
  }
4819

4820
  /* --------------- Async Cache --------------- */
4821

4822
  static final class BoundedLocalAsyncCache<K, V> implements LocalAsyncCache<K, V>, Serializable {
4823
    private static final long serialVersionUID = 1;
4824

4825
    final BoundedLocalCache<K, CompletableFuture<V>> cache;
4826
    final boolean isWeighted;
4827

4828
    @Nullable ConcurrentMap<K, CompletableFuture<V>> mapView;
4829
    @Nullable CacheView<K, V> cacheView;
4830
    @Nullable Policy<K, V> policy;
4831

4832
    @SuppressWarnings("unchecked")
4833
    BoundedLocalAsyncCache(Caffeine<K, V> builder) {
1✔
4834
      cache = (BoundedLocalCache<K, CompletableFuture<V>>) LocalCacheFactory
1✔
4835
          .newBoundedLocalCache(builder, /* cacheLoader= */ null, /* isAsync= */ true);
1✔
4836
      isWeighted = builder.isWeighted();
1✔
4837
    }
1✔
4838

4839
    @Override
4840
    public BoundedLocalCache<K, CompletableFuture<V>> cache() {
4841
      return cache;
1✔
4842
    }
4843

4844
    @Override
4845
    public ConcurrentMap<K, CompletableFuture<V>> asMap() {
4846
      return (mapView == null) ? (mapView = new AsyncAsMapView<>(this)) : mapView;
1✔
4847
    }
4848

4849
    @Override
4850
    public Cache<K, V> synchronous() {
4851
      return (cacheView == null) ? (cacheView = new CacheView<>(this)) : cacheView;
1✔
4852
    }
4853

4854
    @Override
4855
    public Policy<K, V> policy() {
4856
      if (policy == null) {
1✔
4857
        @SuppressWarnings("unchecked")
4858
        var castCache = (BoundedLocalCache<K, V>) cache;
1✔
4859
        Function<CompletableFuture<V>, @Nullable V> transformer = Async::getIfReady;
1✔
4860
        @SuppressWarnings("unchecked")
4861
        var castTransformer = (Function<@Nullable V, @Nullable V>) transformer;
1✔
4862
        policy = new BoundedPolicy<>(castCache, castTransformer, isWeighted);
1✔
4863
      }
4864
      return policy;
1✔
4865
    }
4866

4867
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
4868
      throw new InvalidObjectException("Proxy required");
1✔
4869
    }
4870

4871
    private Object writeReplace() {
4872
      return makeSerializationProxy(cache);
1✔
4873
    }
4874
  }
4875

4876
  /* --------------- Async Loading Cache --------------- */
4877

4878
  static final class BoundedLocalAsyncLoadingCache<K, V>
4879
      extends LocalAsyncLoadingCache<K, V> implements Serializable {
4880
    private static final long serialVersionUID = 1;
4881

4882
    final BoundedLocalCache<K, CompletableFuture<V>> cache;
4883
    final boolean isWeighted;
4884

4885
    @Nullable ConcurrentMap<K, CompletableFuture<V>> mapView;
4886
    @Nullable Policy<K, V> policy;
4887

4888
    @SuppressWarnings("unchecked")
4889
    BoundedLocalAsyncLoadingCache(Caffeine<K, V> builder, AsyncCacheLoader<? super K, V> loader) {
4890
      super(loader);
1✔
4891
      isWeighted = builder.isWeighted();
1✔
4892
      cache = (BoundedLocalCache<K, CompletableFuture<V>>) LocalCacheFactory
1✔
4893
          .newBoundedLocalCache(builder, loader, /* isAsync= */ true);
1✔
4894
    }
1✔
4895

4896
    @Override
4897
    public BoundedLocalCache<K, CompletableFuture<V>> cache() {
4898
      return cache;
1✔
4899
    }
4900

4901
    @Override
4902
    public ConcurrentMap<K, CompletableFuture<V>> asMap() {
4903
      return (mapView == null) ? (mapView = new AsyncAsMapView<>(this)) : mapView;
1✔
4904
    }
4905

4906
    @Override
4907
    public Policy<K, V> policy() {
4908
      if (policy == null) {
1✔
4909
        @SuppressWarnings("unchecked")
4910
        var castCache = (BoundedLocalCache<K, V>) cache;
1✔
4911
        Function<CompletableFuture<V>, @Nullable V> transformer = Async::getIfReady;
1✔
4912
        @SuppressWarnings("unchecked")
4913
        var castTransformer = (Function<@Nullable V, @Nullable V>) transformer;
1✔
4914
        policy = new BoundedPolicy<>(castCache, castTransformer, isWeighted);
1✔
4915
      }
4916
      return policy;
1✔
4917
    }
4918

4919
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
4920
      throw new InvalidObjectException("Proxy required");
1✔
4921
    }
4922

4923
    private Object writeReplace() {
4924
      return makeSerializationProxy(cache);
1✔
4925
    }
4926
  }
4927
}
4928

4929
/** The namespace for field padding through inheritance. */
4930
@SuppressWarnings({"IdentifierName", "MultiVariableDeclaration"})
4931
final class BLCHeader {
4932

4933
  private BLCHeader() {}
4934

4935
  @SuppressWarnings("unused")
4936
  static class PadDrainStatus {
1✔
4937
    byte p000, p001, p002, p003, p004, p005, p006, p007;
4938
    byte p008, p009, p010, p011, p012, p013, p014, p015;
4939
    byte p016, p017, p018, p019, p020, p021, p022, p023;
4940
    byte p024, p025, p026, p027, p028, p029, p030, p031;
4941
    byte p032, p033, p034, p035, p036, p037, p038, p039;
4942
    byte p040, p041, p042, p043, p044, p045, p046, p047;
4943
    byte p048, p049, p050, p051, p052, p053, p054, p055;
4944
    byte p056, p057, p058, p059, p060, p061, p062, p063;
4945
    byte p064, p065, p066, p067, p068, p069, p070, p071;
4946
    byte p072, p073, p074, p075, p076, p077, p078, p079;
4947
    byte p080, p081, p082, p083, p084, p085, p086, p087;
4948
    byte p088, p089, p090, p091, p092, p093, p094, p095;
4949
    byte p096, p097, p098, p099, p100, p101, p102, p103;
4950
    byte p104, p105, p106, p107, p108, p109, p110, p111;
4951
    byte p112, p113, p114, p115, p116, p117, p118, p119;
4952
  }
4953

4954
  /** Enforces a memory layout to avoid false sharing by padding the drain status. */
4955
  abstract static class DrainStatusRef extends PadDrainStatus {
1✔
4956
    static final VarHandle DRAIN_STATUS = fieldVarHandle(MethodHandles.lookup(),
1✔
4957
        "drainStatus", VarHandle.class, DrainStatusRef.class, int.class);
4958

4959
    /** A drain is not taking place. */
4960
    static final int IDLE = 0;
4961
    /** A drain is required due to a pending write modification. */
4962
    static final int REQUIRED = 1;
4963
    /** A drain is in progress and will transition to idle. */
4964
    static final int PROCESSING_TO_IDLE = 2;
4965
    /** A drain is in progress and will transition to required. */
4966
    static final int PROCESSING_TO_REQUIRED = 3;
4967

4968
    /** The draining status of the buffers. */
4969
    volatile int drainStatus = IDLE;
1✔
4970

4971
    /**
4972
     * Returns whether maintenance work is needed.
4973
     *
4974
     * @param delayable if draining the read buffer can be delayed
4975
     */
4976
    @SuppressWarnings("StatementSwitchToExpressionSwitch")
4977
    boolean shouldDrainBuffers(boolean delayable) {
4978
      switch (drainStatusOpaque()) {
1✔
4979
        case IDLE:
4980
          return !delayable;
1✔
4981
        case REQUIRED:
4982
          return true;
1✔
4983
        case PROCESSING_TO_IDLE:
4984
        case PROCESSING_TO_REQUIRED:
4985
          return false;
1✔
4986
        default:
4987
          throw new IllegalStateException("Invalid drain status: " + drainStatus);
1✔
4988
      }
4989
    }
4990

4991
    int drainStatusOpaque() {
4992
      return (int) DRAIN_STATUS.getOpaque(this);
1✔
4993
    }
4994

4995
    int drainStatusAcquire() {
4996
      return (int) DRAIN_STATUS.getAcquire(this);
1✔
4997
    }
4998

4999
    void setDrainStatusOpaque(int drainStatus) {
5000
      DRAIN_STATUS.setOpaque(this, drainStatus);
1✔
5001
    }
1✔
5002

5003
    void setDrainStatusRelease(int drainStatus) {
5004
      DRAIN_STATUS.setRelease(this, drainStatus);
1✔
5005
    }
1✔
5006

5007
    boolean casDrainStatus(int expect, int update) {
5008
      return DRAIN_STATUS.compareAndSet(this, expect, update);
1✔
5009
    }
5010
  }
5011
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc