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

ben-manes / caffeine / #5557

29 Jun 2026 01:54AM UTC coverage: 99.976% (-0.02%) from 100.0%
#5557

push

github

ben-manes
Align async asMap().computeIfAbsent stats with get

AsyncAsMapView.computeIfAbsent hand-rolled its hit/miss stats and
diverged from AsyncCache.get and the sync asMap().computeIfAbsent:
it deferred a conditional hit and recorded nothing on a throwing or
null-returning mapping function. Mirror get's mechanism
(recordStats=true + handleCompletion(recordMiss=false)) so hit/miss
flow through the same statsAware/afterRead path.

It can't delegate to get directly: get's requireNonNull on the
mapping result and the returned future would break the
Map.computeIfAbsent null-return contract, so this inlines get's body
without those checks.

Stats now match get: present->hit, absent+value->miss+success,
absent+throw->miss+failure, absent+null->miss. The
computeIfAbsent_error/_nullValue/_present_failed assertions pinned
the prior incidental behavior and are corrected to the sync sibling.

4045 of 4054 branches covered (99.78%)

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

2 existing lines in 2 files now uncovered.

8243 of 8245 relevant lines covered (99.98%)

1.0 hits per line

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

99.96
/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 time window between touches for expiration updates. */
241
  static final long EXPIRE_TOLERANCE = TimeUnit.SECONDS.toNanos(1);
1✔
242
  /** The maximum duration before an entry expires. */
243
  static final long MAXIMUM_EXPIRY = (Long.MAX_VALUE >> 1); // 150 years
244
  /** The duration to wait on the eviction lock before warning of a possible misuse. */
245
  static final long WARN_AFTER_LOCK_WAIT_NANOS = TimeUnit.SECONDS.toNanos(30);
1✔
246
  /** The number of retries before computing to validate the entry's integrity; pow2 modulus. */
247
  static final int MAX_PUT_SPIN_WAIT_ATTEMPTS = 1024 - 1;
248
  /** The handle for the in-flight refresh operations. */
249
  static final VarHandle REFRESHES = fieldVarHandle(MethodHandles.lookup(),
1✔
250
      "refreshes", VarHandle.class, BoundedLocalCache.class, ConcurrentMap.class);
251

252
  final @Nullable RemovalListener<K, V> evictionListener;
253
  final @Nullable AsyncCacheLoader<K, V> cacheLoader;
254

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

265
  final boolean isWeighted;
266
  final boolean isAsync;
267

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

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

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

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

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

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

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

330
  /* --------------- Shared --------------- */
331

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

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

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

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

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

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

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

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

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

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

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

403
  /* --------------- Stats Support --------------- */
404

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

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

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

420
  /* --------------- Removal Listener Support --------------- */
421

422
  protected @Nullable RemovalListener<K, V> removalListener() {
423
    return null;
1✔
424
  }
425

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

447
  /* --------------- Eviction Listener Support --------------- */
448

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

460
  /* --------------- Reference Support --------------- */
461

462
  @Override
463
  public boolean collectKeys() {
464
    return false;
1✔
465
  }
466

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

472
  @SuppressWarnings({"DataFlowIssue", "NullAway"})
473
  protected ReferenceQueue<K> keyReferenceQueue() {
474
    return null;
1✔
475
  }
476

477
  @SuppressWarnings({"DataFlowIssue", "NullAway"})
478
  protected ReferenceQueue<V> valueReferenceQueue() {
479
    return null;
1✔
480
  }
481

482
  /* --------------- Expiration Support --------------- */
483

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

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

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

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

504
  protected void setExpiresAfterAccessNanos(long expireAfterAccessNanos) {
505
    throw new UnsupportedOperationException();
1✔
506
  }
507

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

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

518
  protected void setExpiresAfterWriteNanos(long expireAfterWriteNanos) {
519
    throw new UnsupportedOperationException();
1✔
520
  }
521

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

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

532
  protected void setRefreshAfterWriteNanos(long refreshAfterWriteNanos) {
533
    throw new UnsupportedOperationException();
1✔
534
  }
535

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

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

547
  protected TimerWheel<K, V> timerWheel() {
548
    throw new UnsupportedOperationException();
1✔
549
  }
550

551
  /* --------------- Eviction Support --------------- */
552

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

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

563
  protected FrequencySketch frequencySketch() {
564
    throw new UnsupportedOperationException();
1✔
565
  }
566

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

572
  /** Returns the maximum weighted size. */
573
  protected long maximum() {
574
    throw new UnsupportedOperationException();
1✔
575
  }
576

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

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

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

592
  @GuardedBy("evictionLock")
593
  protected void setMaximum(long maximum) {
594
    throw new UnsupportedOperationException();
1✔
595
  }
596

597
  @GuardedBy("evictionLock")
598
  protected void setWindowMaximum(long maximum) {
599
    throw new UnsupportedOperationException();
1✔
600
  }
601

602
  @GuardedBy("evictionLock")
603
  protected void setMainProtectedMaximum(long maximum) {
604
    throw new UnsupportedOperationException();
1✔
605
  }
606

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

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

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

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

627
  @GuardedBy("evictionLock")
628
  protected void setWeightedSize(long weightedSize) {
629
    throw new UnsupportedOperationException();
1✔
630
  }
631

632
  @GuardedBy("evictionLock")
633
  protected void setWindowWeightedSize(long weightedSize) {
634
    throw new UnsupportedOperationException();
1✔
635
  }
636

637
  @GuardedBy("evictionLock")
638
  protected void setMainProtectedWeightedSize(long weightedSize) {
639
    throw new UnsupportedOperationException();
1✔
640
  }
641

642
  protected long hitsInSample() {
643
    throw new UnsupportedOperationException();
1✔
644
  }
645

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

650
  protected double stepSize() {
651
    throw new UnsupportedOperationException();
1✔
652
  }
653

654
  protected double previousSampleHitRate() {
655
    throw new UnsupportedOperationException();
1✔
656
  }
657

658
  protected long adjustment() {
659
    throw new UnsupportedOperationException();
1✔
660
  }
661

662
  @GuardedBy("evictionLock")
663
  protected void setHitsInSample(long hitCount) {
664
    throw new UnsupportedOperationException();
1✔
665
  }
666

667
  @GuardedBy("evictionLock")
668
  protected void setMissesInSample(long missCount) {
669
    throw new UnsupportedOperationException();
1✔
670
  }
671

672
  @GuardedBy("evictionLock")
673
  protected void setStepSize(double stepSize) {
674
    throw new UnsupportedOperationException();
1✔
675
  }
676

677
  @GuardedBy("evictionLock")
678
  protected void setPreviousSampleHitRate(double hitRate) {
679
    throw new UnsupportedOperationException();
1✔
680
  }
681

682
  @GuardedBy("evictionLock")
683
  protected void setAdjustment(long amount) {
684
    throw new UnsupportedOperationException();
1✔
685
  }
686

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

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

704
    setMaximum(max);
1✔
705
    setWindowMaximum(window);
1✔
706
    setMainProtectedMaximum(mainProtected);
1✔
707

708
    setHitsInSample(0);
1✔
709
    setMissesInSample(0);
1✔
710
    setStepSize((max <= SMALL_CACHE_THRESHOLD) ? stepSize : -stepSize);
1✔
711

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

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

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

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

753
        setWindowWeightedSize(windowWeightedSize() - node.getPolicyWeight());
1✔
754
      }
755
      node = next;
1✔
756
    }
1✔
757

758
    return first;
1✔
759
  }
760

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

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

802
        // The pending operations will adjust the size to reflect the correct weight
803
        break;
804
      }
805

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

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

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

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

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

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

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

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

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

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

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

940
    expireAfterAccessEntries(now, accessOrderWindowDeque());
1✔
941
    if (evicts()) {
1✔
942
      expireAfterAccessEntries(now, accessOrderProbationDeque());
1✔
943
      expireAfterAccessEntries(now, accessOrderProtectedDeque());
1✔
944
    }
945
  }
1✔
946

947
  /** Expires entries in an access-order queue. */
948
  @GuardedBy("evictionLock")
949
  void expireAfterAccessEntries(long now, AccessOrderDeque<Node<K, V>> accessOrderDeque) {
950
    var head = accessOrderDeque.peekFirst();
1✔
951
    if (head == null) {
1✔
952
      return;
1✔
953
    }
954
    var duration = expiresAfterAccessNanos();
1✔
955
    var last = requireNonNull(accessOrderDeque.peekLast());
1✔
956
    for (var node = head; node != null;) {
1✔
957
      var next = (node == last) ? null : node.getNextInAccessOrder();
1✔
958
      if ((now - node.getAccessTime()) < duration) {
1✔
959
        var stalePosition = (last.getAccessTime() < node.getAccessTime());
1✔
960
        if (stalePosition || isComputingAsync(node.getValue())) {
1✔
961
          accessOrderDeque.moveToBack(node);
1✔
962
          node = next;
1✔
963
          continue;
1✔
964
        }
965
        return;
1✔
966
      }
967
      evictEntry(node, RemovalCause.EXPIRED, now);
1✔
968
      node = next;
1✔
969
    }
1✔
970
  }
1✔
971

972
  /** Expires entries on the write-order queue. */
973
  @GuardedBy("evictionLock")
974
  void expireAfterWriteEntries(long now) {
975
    if (!expiresAfterWrite()) {
1✔
976
      return;
1✔
977
    }
978

979
    var head = writeOrderDeque().peekFirst();
1✔
980
    if (head == null) {
1✔
981
      return;
1✔
982
    }
983
    var duration = expiresAfterWriteNanos();
1✔
984
    var last = requireNonNull(writeOrderDeque().peekLast());
1✔
985
    for (var node = head; node != null;) {
1✔
986
      var next = (node == last) ? null : node.getNextInWriteOrder();
1✔
987
      if ((now - node.getWriteTime()) < duration) {
1✔
988
        var stalePosition = (last.getWriteTime() < node.getWriteTime());
1✔
989
        if (stalePosition || isComputingAsync(node.getValue())) {
1✔
990
          writeOrderDeque().moveToBack(node);
1✔
991
          node = next;
1✔
992
          continue;
1✔
993
        }
994
        return;
1✔
995
      }
996
      evictEntry(node, RemovalCause.EXPIRED, now);
1✔
997
      node = next;
1✔
998
    }
1✔
999
  }
1✔
1000

1001
  /** Expires entries in the timer wheel. */
1002
  @GuardedBy("evictionLock")
1003
  void expireVariableEntries(long now) {
1004
    if (expiresVariable()) {
1✔
1005
      timerWheel().advance(this, now);
1✔
1006
    }
1007
  }
1✔
1008

1009
  /** Returns the duration until the next item expires, or {@link Long#MAX_VALUE} if none. */
1010
  @GuardedBy("evictionLock")
1011
  long getExpirationDelay(long now) {
1012
    @Var long delay = Long.MAX_VALUE;
1✔
1013
    if (expiresAfterAccess()) {
1✔
1014
      @Var Node<K, V> node = accessOrderWindowDeque().peekFirst();
1✔
1015
      if (node != null) {
1✔
1016
        long age = Math.max(0, now - node.getAccessTime());
1✔
1017
        delay = Math.min(delay, expiresAfterAccessNanos() - age);
1✔
1018
      }
1019
      if (evicts()) {
1✔
1020
        node = accessOrderProbationDeque().peekFirst();
1✔
1021
        if (node != null) {
1✔
1022
          long age = Math.max(0, now - node.getAccessTime());
1✔
1023
          delay = Math.min(delay, expiresAfterAccessNanos() - age);
1✔
1024
        }
1025
        node = accessOrderProtectedDeque().peekFirst();
1✔
1026
        if (node != null) {
1✔
1027
          long age = Math.max(0, now - node.getAccessTime());
1✔
1028
          delay = Math.min(delay, expiresAfterAccessNanos() - age);
1✔
1029
        }
1030
      }
1031
    }
1032
    if (expiresAfterWrite()) {
1✔
1033
      Node<K, V> node = writeOrderDeque().peekFirst();
1✔
1034
      if (node != null) {
1✔
1035
        long age = Math.max(0, now - node.getWriteTime());
1✔
1036
        delay = Math.min(delay, expiresAfterWriteNanos() - age);
1✔
1037
      }
1038
    }
1039
    if (expiresVariable()) {
1✔
1040
      delay = Math.min(delay, timerWheel().getExpirationDelay());
1✔
1041
    }
1042
    return delay;
1✔
1043
  }
1044

1045
  /** Returns if the entry has expired. */
1046
  @SuppressWarnings("ShortCircuitBoolean")
1047
  boolean hasExpired(Node<K, V> node, long now, V value) {
1048
    if (isComputingAsync(value)) {
1✔
1049
      return false;
1✔
1050
    }
1051
    return (expiresAfterAccess() && (now - node.getAccessTime() >= expiresAfterAccessNanos()))
1✔
1052
        | (expiresAfterWrite() && (now - node.getWriteTime() >= expiresAfterWriteNanos()))
1✔
1053
        | (expiresVariable() && (now - node.getVariableTime() >= 0));
1✔
1054
  }
1055

1056
  /**
1057
   * Attempts to evict the entry based on the given removal cause. A removal may be ignored if the
1058
   * entry was updated and is no longer eligible for eviction.
1059
   *
1060
   * @param node the entry to evict
1061
   * @param cause the reason to evict
1062
   * @param now the current time, used only if expiring
1063
   * @return if the entry was evicted
1064
   */
1065
  @GuardedBy("evictionLock")
1066
  @SuppressWarnings({"GuardedByChecker", "SynchronizationOnLocalVariableOrMethodParameter"})
1067
  boolean evictEntry(Node<K, V> node, RemovalCause cause, long now) {
1068
    K key = node.getKey();
1✔
1069
    var ctx = new EvictContext<V>();
1✔
1070
    var keyReference = node.getKeyReference();
1✔
1071

1072
    data.computeIfPresent(keyReference, (k, n) -> {
1✔
1073
      if (n != node) {
1!
UNCOV
1074
        return n;
×
1075
      }
1076
      synchronized (node) {
1✔
1077
        ctx.value = node.getValue();
1✔
1078

1079
        if ((key == null) || (ctx.value == null)) {
1✔
1080
          ctx.cause = RemovalCause.COLLECTED;
1✔
1081
        } else if (cause == RemovalCause.COLLECTED) {
1✔
1082
          ctx.resurrect = true;
1✔
1083
          return node;
1✔
1084
        } else {
1085
          ctx.cause = cause;
1✔
1086
        }
1087

1088
        if (ctx.cause == RemovalCause.EXPIRED) {
1✔
1089
          @Var boolean expired = false;
1✔
1090
          if (expiresAfterAccess()) {
1✔
1091
            expired |= ((now - node.getAccessTime()) >= expiresAfterAccessNanos());
1✔
1092
          }
1093
          if (expiresAfterWrite()) {
1✔
1094
            expired |= ((now - node.getWriteTime()) >= expiresAfterWriteNanos());
1✔
1095
          }
1096
          if (expiresVariable()) {
1✔
1097
            expired |= ((now - node.getVariableTime()) >= 0);
1✔
1098
          }
1099
          if (expired) {
1✔
1100
            if (isComputingAsync(ctx.value)) {
1✔
1101
              long sentinel = (now + ASYNC_EXPIRY);
1✔
1102
              setVariableTime(node, sentinel);
1✔
1103
              setAccessTime(node, sentinel);
1✔
1104
              setWriteTime(node, sentinel);
1✔
1105
              ctx.resurrect = true;
1✔
1106
              return node;
1✔
1107
            }
1108
          } else {
1109
            ctx.resurrect = true;
1✔
1110
            return node;
1✔
1111
          }
1112
        } else if (ctx.cause == RemovalCause.SIZE) {
1✔
1113
          int weight = node.getWeight();
1✔
1114
          if (weight == 0) {
1✔
1115
            ctx.resurrect = true;
1✔
1116
            return node;
1✔
1117
          }
1118
        }
1119

1120
        notifyEviction(key, ctx.value, ctx.cause);
1✔
1121
        discardRefresh(keyReference);
1✔
1122
        ctx.removed = true;
1✔
1123
        node.retire();
1✔
1124
        return null;
1✔
1125
      }
1126
    });
1127

1128
    // The entry is no longer eligible for eviction
1129
    if (ctx.resurrect) {
1✔
1130
      return false;
1✔
1131
    }
1132

1133
    // If the eviction fails due to a concurrent removal of the victim, that removal may cancel out
1134
    // the addition that triggered this eviction. The victim is eagerly unlinked and the size
1135
    // decremented before the removal task so that if an eviction is still required then a new
1136
    // victim will be chosen for removal.
1137
    if (node.inWindow() && (evicts() || expiresAfterAccess())) {
1✔
1138
      accessOrderWindowDeque().remove(node);
1✔
1139
    } else if (evicts()) {
1✔
1140
      if (node.inMainProbation()) {
1✔
1141
        accessOrderProbationDeque().remove(node);
1✔
1142
      } else {
1143
        accessOrderProtectedDeque().remove(node);
1✔
1144
      }
1145
    }
1146
    if (expiresAfterWrite()) {
1✔
1147
      writeOrderDeque().remove(node);
1✔
1148
    } else if (expiresVariable()) {
1✔
1149
      timerWheel().deschedule(node);
1✔
1150
    }
1151

1152
    synchronized (node) {
1✔
1153
      logIfAlive(node);
1✔
1154
      makeDead(node);
1✔
1155
    }
1✔
1156

1157
    if (ctx.removed) {
1✔
1158
      var removeCause = requireNonNull(ctx.cause);
1✔
1159
      statsCounter().recordEviction(node.getWeight(), removeCause);
1✔
1160
      notifyRemoval(key, ctx.value, removeCause);
1✔
1161
    }
1162

1163
    return true;
1✔
1164
  }
1165

1166
  /** Adapts the eviction policy to towards the optimal recency / frequency configuration. */
1167
  @GuardedBy("evictionLock")
1168
  @SuppressWarnings("UnnecessaryReturnStatement")
1169
  void climb() {
1170
    if (!evicts()) {
1✔
1171
      return;
1✔
1172
    }
1173
    determineAdjustment();
1✔
1174
    demoteFromMainProtected();
1✔
1175
    long amount = adjustment();
1✔
1176
    if (amount == 0) {
1✔
1177
      return;
1✔
1178
    } else if (amount > 0) {
1✔
1179
      increaseWindow();
1✔
1180
    } else {
1181
      decreaseWindow();
1✔
1182
    }
1183
  }
1✔
1184

1185
  /** Calculates the amount to adapt the window by and sets {@link #adjustment()} accordingly. */
1186
  @GuardedBy("evictionLock")
1187
  @SuppressWarnings("MathClampDouble")
1188
  void determineAdjustment() {
1189
    if (frequencySketch().isNotInitialized()) {
1✔
1190
      setPreviousSampleHitRate(0.0);
1✔
1191
      setMissesInSample(0);
1✔
1192
      setHitsInSample(0);
1✔
1193
      return;
1✔
1194
    }
1195

1196
    long requestCount = hitsInSample() + missesInSample();
1✔
1197
    @Var double stepDecayRate = HILL_CLIMBER_STEP_DECAY_RATE;
1✔
1198
    @Var long effectiveSampleSize = frequencySketch().sampleSize;
1✔
1199
    if (maximum() <= SMALL_CACHE_THRESHOLD) {
1✔
1200
      // Grows the sample period as the step size decays to avoid converging near the initial ratio
1201
      double initialStep = HILL_CLIMBER_STEP_PERCENT * maximum();
1✔
1202
      double magnitude = Math.max(initialStep / SMALL_CACHE_SAMPLE_RATIO_CAP, Math.abs(stepSize()));
1✔
1203
      double ratio = (magnitude == 0.0)
1✔
1204
          ? 1.0
1✔
1205
          : Math.max(1.0, Math.min(SMALL_CACHE_SAMPLE_RATIO_CAP, initialStep / magnitude));
1✔
1206
      effectiveSampleSize = (long) (effectiveSampleSize * ratio);
1✔
1207
      stepDecayRate = SMALL_CACHE_STEP_DECAY_RATE;
1✔
1208
    }
1209
    if (requestCount < effectiveSampleSize) {
1✔
1210
      return;
1✔
1211
    }
1212

1213
    double hitRate = (double) hitsInSample() / requestCount;
1✔
1214
    double hitRateChange = hitRate - previousSampleHitRate();
1✔
1215
    double amount = (hitRateChange >= 0) ? stepSize() : -stepSize();
1✔
1216
    double nextStepSize = (Math.abs(hitRateChange) >= HILL_CLIMBER_RESTART_THRESHOLD)
1✔
1217
        ? Math.copySign(
1✔
1218
            Math.max(HILL_CLIMBER_STEP_PERCENT * maximum(), HILL_CLIMBER_MIN_INITIAL_STEP), amount)
1✔
1219
        : (stepDecayRate * amount);
1✔
1220
    setPreviousSampleHitRate(hitRate);
1✔
1221
    setAdjustment((long) amount);
1✔
1222
    setStepSize(nextStepSize);
1✔
1223
    setMissesInSample(0);
1✔
1224
    setHitsInSample(0);
1✔
1225
  }
1✔
1226

1227
  /**
1228
   * Increases the size of the admission window by shrinking the portion allocated to the main
1229
   * space. As the main space is partitioned into probation and protected regions (80% / 20%), for
1230
   * simplicity only the protected is reduced. If the regions exceed their maximums, this may cause
1231
   * protected items to be demoted to the probation region and probation items to be demoted to the
1232
   * admission window.
1233
   */
1234
  @GuardedBy("evictionLock")
1235
  void increaseWindow() {
1236
    if (mainProtectedMaximum() == 0) {
1✔
1237
      return;
1✔
1238
    }
1239

1240
    @SuppressWarnings("MathClampLong")
1241
    @Var long quota = Math.min(adjustment(), mainProtectedMaximum());
1✔
1242
    setMainProtectedMaximum(mainProtectedMaximum() - quota);
1✔
1243
    setWindowMaximum(windowMaximum() + quota);
1✔
1244
    demoteFromMainProtected();
1✔
1245

1246
    for (int i = 0; i < QUEUE_TRANSFER_THRESHOLD; i++) {
1✔
1247
      @Var Node<K, V> candidate = accessOrderProbationDeque().peekFirst();
1✔
1248
      @Var boolean probation = true;
1✔
1249
      if ((candidate == null) || (quota < candidate.getPolicyWeight())) {
1✔
1250
        candidate = accessOrderProtectedDeque().peekFirst();
1✔
1251
        probation = false;
1✔
1252
      }
1253
      if (candidate == null) {
1✔
1254
        break;
1✔
1255
      }
1256

1257
      int weight = candidate.getPolicyWeight();
1✔
1258
      if (quota < weight) {
1✔
1259
        break;
1✔
1260
      }
1261

1262
      quota -= weight;
1✔
1263
      if (probation) {
1✔
1264
        accessOrderProbationDeque().remove(candidate);
1✔
1265
      } else {
1266
        setMainProtectedWeightedSize(mainProtectedWeightedSize() - weight);
1✔
1267
        accessOrderProtectedDeque().remove(candidate);
1✔
1268
      }
1269
      setWindowWeightedSize(windowWeightedSize() + weight);
1✔
1270
      accessOrderWindowDeque().offerLast(candidate);
1✔
1271
      candidate.makeWindow();
1✔
1272
    }
1273

1274
    setMainProtectedMaximum(mainProtectedMaximum() + quota);
1✔
1275
    setWindowMaximum(windowMaximum() - quota);
1✔
1276
    setAdjustment(quota);
1✔
1277
  }
1✔
1278

1279
  /** Decreases the size of the admission window and increases the main's protected region. */
1280
  @GuardedBy("evictionLock")
1281
  void decreaseWindow() {
1282
    if (windowMaximum() <= 1) {
1✔
1283
      return;
1✔
1284
    }
1285

1286
    @SuppressWarnings("MathClampLong")
1287
    @Var long quota = Math.min(-adjustment(), Math.max(0, windowMaximum() - 1));
1✔
1288
    setMainProtectedMaximum(mainProtectedMaximum() + quota);
1✔
1289
    setWindowMaximum(windowMaximum() - quota);
1✔
1290

1291
    for (int i = 0; i < QUEUE_TRANSFER_THRESHOLD; i++) {
1✔
1292
      Node<K, V> candidate = accessOrderWindowDeque().peekFirst();
1✔
1293
      if (candidate == null) {
1✔
1294
        break;
1✔
1295
      }
1296

1297
      int weight = candidate.getPolicyWeight();
1✔
1298
      if (quota < weight) {
1✔
1299
        break;
1✔
1300
      }
1301

1302
      quota -= weight;
1✔
1303
      setWindowWeightedSize(windowWeightedSize() - weight);
1✔
1304
      accessOrderWindowDeque().remove(candidate);
1✔
1305
      accessOrderProbationDeque().offerLast(candidate);
1✔
1306
      candidate.makeMainProbation();
1✔
1307
    }
1308

1309
    setMainProtectedMaximum(mainProtectedMaximum() - quota);
1✔
1310
    setWindowMaximum(windowMaximum() + quota);
1✔
1311
    setAdjustment(-quota);
1✔
1312
  }
1✔
1313

1314
  /** Transfers the nodes from the protected to the probation region if it exceeds the maximum. */
1315
  @GuardedBy("evictionLock")
1316
  void demoteFromMainProtected() {
1317
    long mainProtectedMaximum = mainProtectedMaximum();
1✔
1318
    @Var long mainProtectedWeightedSize = mainProtectedWeightedSize();
1✔
1319
    if (mainProtectedWeightedSize <= mainProtectedMaximum) {
1✔
1320
      return;
1✔
1321
    }
1322

1323
    for (int i = 0; i < QUEUE_TRANSFER_THRESHOLD; i++) {
1✔
1324
      if (mainProtectedWeightedSize <= mainProtectedMaximum) {
1✔
1325
        break;
1✔
1326
      }
1327

1328
      Node<K, V> demoted = accessOrderProtectedDeque().pollFirst();
1✔
1329
      if (demoted == null) {
1✔
1330
        break;
1✔
1331
      }
1332
      demoted.makeMainProbation();
1✔
1333
      accessOrderProbationDeque().offerLast(demoted);
1✔
1334
      mainProtectedWeightedSize -= demoted.getPolicyWeight();
1✔
1335
    }
1336
    setMainProtectedWeightedSize(mainProtectedWeightedSize);
1✔
1337
  }
1✔
1338

1339
  /**
1340
   * Performs the post-processing work required after a read.
1341
   *
1342
   * @param node the entry in the page replacement policy
1343
   * @param now the current time, in nanoseconds
1344
   * @param recordHit if the hit count should be incremented
1345
   * @return the refreshed value if immediately loaded, else null
1346
   */
1347
  @Nullable V afterRead(Node<K, V> node, long now, boolean recordHit) {
1348
    if (recordHit) {
1✔
1349
      statsCounter().recordHits(1);
1✔
1350
    }
1351

1352
    boolean delayable = skipReadBuffer() || (readBuffer.offer(node) != Buffer.FULL);
1✔
1353
    if (shouldDrainBuffers(delayable)) {
1✔
1354
      scheduleDrainBuffers();
1✔
1355
    }
1356
    return refreshIfNeeded(node, now);
1✔
1357
  }
1358

1359
  /** Returns if the cache should bypass the read buffer. */
1360
  boolean skipReadBuffer() {
1361
    return fastpath() && frequencySketch().isNotInitialized();
1✔
1362
  }
1363

1364
  /**
1365
   * Asynchronously refreshes the entry if eligible.
1366
   *
1367
   * @param node the entry in the cache to refresh
1368
   * @param now the current time, in nanoseconds
1369
   * @return the refreshed value if immediately loaded, else null
1370
   */
1371
  @SuppressWarnings("FutureReturnValueIgnored")
1372
  @Nullable V refreshIfNeeded(Node<K, V> node, long now) {
1373
    if (!refreshAfterWrite()) {
1✔
1374
      return null;
1✔
1375
    }
1376

1377
    K key;
1378
    V oldValue;
1379
    Object keyReference;
1380
    long writeTime = node.getWriteTime();
1✔
1381
    long refreshWriteTime = writeTime | 1L;
1✔
1382
    ConcurrentMap<Object, CompletableFuture<?>> refreshes;
1383
    if (((now - writeTime) > refreshAfterWriteNanos())
1✔
1384
        && ((key = node.getKey()) != null) && ((oldValue = node.getValue()) != null)
1✔
1385
        && !isComputingAsync(oldValue) && ((writeTime & 1L) == 0L)
1✔
1386
        && !(refreshes = refreshes()).containsKey(keyReference = node.getKeyReference())
1✔
1387
        && node.isAlive() && node.casWriteTime(writeTime, refreshWriteTime)) {
1✔
1388
      long[] startTime = new long[1];
1✔
1389
      @SuppressWarnings({"rawtypes", "unchecked"})
1390
      @Nullable CompletableFuture<? extends @Nullable V>[] refreshFuture = new CompletableFuture[1];
1✔
1391
      try {
1392
        refreshes.computeIfAbsent(keyReference, k -> {
1✔
1393
          if (!node.isAlive() || (node.getWriteTime() != refreshWriteTime)) {
1✔
1394
            return null;
1✔
1395
          }
1396
          try {
1397
            startTime[0] = statsTicker().read();
1✔
1398
            if (isAsync) {
1✔
1399
              @SuppressWarnings("unchecked")
1400
              var future = (CompletableFuture<V>) oldValue;
1✔
1401
              if (Async.isReady(future)) {
1✔
1402
                requireNonNull(cacheLoader);
1✔
1403
                var refresh = cacheLoader.asyncReload(key, future.join(), executor);
1✔
1404
                refreshFuture[0] = requireNonNull(refresh, "Null future");
1✔
1405
              } else {
1✔
1406
                // no-op if the future's completion state was modified (e.g. obtrude methods)
1407
                return null;
1✔
1408
              }
1409
            } else {
1✔
1410
              requireNonNull(cacheLoader);
1✔
1411
              var refresh = cacheLoader.asyncReload(key, oldValue, executor);
1✔
1412
              refreshFuture[0] = requireNonNull(refresh, "Null future");
1✔
1413
            }
1414
            return refreshFuture[0];
1✔
1415
          } catch (InterruptedException e) {
1✔
1416
            Thread.currentThread().interrupt();
1✔
1417
            logger.log(Level.WARNING, "Exception thrown when submitting refresh task", e);
1✔
1418
            return null;
1✔
1419
          } catch (Throwable e) {
1✔
1420
            logger.log(Level.WARNING, "Exception thrown when submitting refresh task", e);
1✔
1421
            return null;
1✔
1422
          }
1423
        });
1424
      } finally {
1425
        node.casWriteTime(refreshWriteTime, writeTime);
1✔
1426
      }
1427

1428
      if (refreshFuture[0] == null) {
1✔
1429
        return null;
1✔
1430
      }
1431

1432
      var refreshed = refreshFuture[0].handle((newValue, error) -> {
1✔
1433
        long loadTime = statsTicker().read() - startTime[0];
1✔
1434
        if (error != null) {
1✔
1435
          if (!(error instanceof CancellationException) && !(error instanceof TimeoutException)) {
1✔
1436
            logger.log(Level.WARNING, "Exception thrown during refresh", error);
1✔
1437
          }
1438
          refreshes.remove(keyReference, refreshFuture[0]);
1✔
1439
          statsCounter().recordLoadFailure(loadTime);
1✔
1440
          return null;
1✔
1441
        }
1442

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

1488
        if (cause[0] != null) {
1✔
1489
          notifyRemoval(key, value, cause[0]);
1✔
1490
        }
1491
        if (newValue == null) {
1✔
1492
          statsCounter().recordLoadFailure(loadTime);
1✔
1493
        } else {
1494
          statsCounter().recordLoadSuccess(loadTime);
1✔
1495
        }
1496
        return result;
1✔
1497
      });
1498
      return Async.getIfReady(refreshed);
1✔
1499
    }
1500

1501
    return null;
1✔
1502
  }
1503

1504
  /**
1505
   * Returns the expiration time for the entry after being created.
1506
   *
1507
   * @param key the key of the entry that was created
1508
   * @param value the value of the entry that was created
1509
   * @param expiry the calculator for the expiration time
1510
   * @param now the current time, in nanoseconds
1511
   * @return the expiration time
1512
   */
1513
  long expireAfterCreate(K key, V value, @Nullable Expiry<? super K, ? super V> expiry, long now) {
1514
    if (expiresVariable()) {
1✔
1515
      requireNonNull(expiry);
1✔
1516
      long duration = Math.max(0L, expiry.expireAfterCreate(key, value, now));
1✔
1517
      return isAsync ? (now + duration) : (now + Math.min(duration, MAXIMUM_EXPIRY));
1✔
1518
    }
1519
    return 0L;
1✔
1520
  }
1521

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

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

1562
  /**
1563
   * Attempts to update the access time for the entry after a read.
1564
   *
1565
   * @param node the entry in the page replacement policy
1566
   * @param key the key of the entry that was read
1567
   * @param value the value of the entry that was read
1568
   * @param expiry the calculator for the expiration time
1569
   * @param now the current time, in nanoseconds
1570
   */
1571
  void tryExpireAfterRead(Node<K, V> node, K key, V value, Expiry<K, V> expiry, long now) {
1572
    if (!expiresVariable()) {
1✔
1573
      return;
1✔
1574
    }
1575

1576
    long variableTime = node.getVariableTime();
1✔
1577
    long currentDuration = Math.max(1, variableTime - now);
1✔
1578
    if (isAsync && (currentDuration > MAXIMUM_EXPIRY)) {
1✔
1579
      // expireAfterCreate has not yet set the duration after completion
1580
      return;
1✔
1581
    }
1582

1583
    long tolerance = EXPIRE_TOLERANCE;
1✔
1584
    long duration = Math.max(0L, expiry.expireAfterRead(key, value, now, currentDuration));
1✔
1585
    long expirationTime = isAsync ? (now + duration) : (now + Math.min(duration, MAXIMUM_EXPIRY));
1✔
1586
    if ((duration <= tolerance) || (Math.abs(expirationTime - variableTime) > tolerance)) {
1✔
1587
      node.casVariableTime(variableTime, expirationTime);
1✔
1588
    }
1589
  }
1✔
1590

1591
  void setVariableTime(Node<K, V> node, long expirationTime) {
1592
    if (expiresVariable()) {
1✔
1593
      node.setVariableTime(expirationTime);
1✔
1594
    }
1595
  }
1✔
1596

1597
  void setWriteTime(Node<K, V> node, long now) {
1598
    if (expiresAfterWrite() || refreshAfterWrite()) {
1✔
1599
      node.setWriteTime(now & ~1L);
1✔
1600
    }
1601
  }
1✔
1602

1603
  void setAccessTime(Node<K, V> node, long now) {
1604
    if (!expiresAfterAccess()) {
1✔
1605
      return;
1✔
1606
    }
1607
    long tolerance = EXPIRE_TOLERANCE;
1✔
1608
    long accessTime = node.getAccessTime();
1✔
1609
    if ((expiresAfterAccessNanos() <= tolerance) || (Math.abs(now - accessTime) > tolerance)) {
1✔
1610
      node.setAccessTime(now);
1✔
1611
    }
1612
  }
1✔
1613

1614
  /** Returns if the entry's write time would exceed the minimum expiration reorder threshold. */
1615
  boolean exceedsWriteTimeTolerance(Node<K, V> node, long varTime, long now) {
1616
    long variableTime = node.getVariableTime();
1✔
1617
    long writeTime = node.getWriteTime();
1✔
1618
    long tolerance = EXPIRE_TOLERANCE;
1✔
1619
    return
1✔
1620
        (expiresAfterWrite()
1✔
1621
            && ((expiresAfterWriteNanos() <= tolerance) || (Math.abs(now - writeTime) > tolerance)))
1✔
1622
        || (refreshAfterWrite()
1✔
1623
            && ((refreshAfterWriteNanos() <= tolerance) || (Math.abs(now - writeTime) > tolerance)))
1✔
1624
        || (expiresVariable() && (Math.abs(varTime - variableTime) > tolerance));
1✔
1625
  }
1626

1627
  /**
1628
   * Performs the post-processing work required after a write.
1629
   *
1630
   * @param task the pending operation to be applied
1631
   */
1632
  void afterWrite(Runnable task) {
1633
    if (writeBuffer.offer(task)) {
1✔
1634
      scheduleAfterWrite();
1✔
1635
      return;
1✔
1636
    }
1637

1638
    // In scenarios where the writing threads cannot make progress then they attempt to provide
1639
    // assistance by performing the eviction work directly. This can resolve cases where the
1640
    // maintenance task is scheduled but not running. That might occur due to all of the executor's
1641
    // threads being busy (perhaps writing into this cache), the write rate greatly exceeds the
1642
    // consuming rate, priority inversion, or if the executor silently discarded the maintenance
1643
    // task. Unfortunately this cannot resolve when the eviction is blocked waiting on a long-
1644
    // running computation due to an eviction listener, the victim is being computed on by a writer,
1645
    // or the victim residing in the same hash bin as a computing entry. In those cases a warning is
1646
    // logged to encourage the application to decouple these computations from the map operations.
1647
    lock();
1✔
1648
    try {
1649
      maintenance(task);
1✔
1650
    } catch (RuntimeException e) {
1✔
1651
      logger.log(Level.ERROR, "Exception thrown when performing the maintenance task", e);
1✔
1652
    } finally {
1653
      evictionLock.unlock();
1✔
1654
    }
1655
    rescheduleCleanUpIfIncomplete();
1✔
1656
  }
1✔
1657

1658
  /** Acquires the eviction lock. */
1659
  void lock() {
1660
    @Var long remainingNanos = WARN_AFTER_LOCK_WAIT_NANOS;
1✔
1661
    long end = System.nanoTime() + remainingNanos;
1✔
1662
    @Var boolean interrupted = false;
1✔
1663
    try {
1664
      for (;;) {
1665
        try {
1666
          if (evictionLock.tryLock(remainingNanos, TimeUnit.NANOSECONDS)) {
1✔
1667
            return;
1✔
1668
          }
1669
          logger.log(Level.WARNING, "The cache is experiencing excessive wait times for acquiring "
1✔
1670
              + "the eviction lock. This may indicate that a long-running computation has halted "
1671
              + "eviction when trying to remove the victim entry. Consider using AsyncCache to "
1672
              + "decouple the computation from the map operation.", new TimeoutException());
1673
          evictionLock.lock();
1✔
1674
          return;
1✔
1675
        } catch (InterruptedException e) {
1✔
1676
          remainingNanos = end - System.nanoTime();
1✔
1677
          interrupted = true;
1✔
1678
        }
1✔
1679
      }
1680
    } finally {
1681
      if (interrupted) {
1✔
1682
        Thread.currentThread().interrupt();
1✔
1683
      }
1684
    }
1685
  }
1686

1687
  /**
1688
   * Conditionally schedules the asynchronous maintenance task after a write operation. If the
1689
   * task status was IDLE or REQUIRED then the maintenance task is scheduled immediately. If it
1690
   * is already processing then it is set to transition to REQUIRED upon completion so that a new
1691
   * execution is triggered by the next operation.
1692
   */
1693
  void scheduleAfterWrite() {
1694
    @Var int drainStatus = drainStatusOpaque();
1✔
1695
    for (;;) {
1696
      switch (drainStatus) {
1✔
1697
        case IDLE:
1698
          casDrainStatus(IDLE, REQUIRED);
1✔
1699
          scheduleDrainBuffers();
1✔
1700
          return;
1✔
1701
        case REQUIRED:
1702
          scheduleDrainBuffers();
1✔
1703
          return;
1✔
1704
        case PROCESSING_TO_IDLE:
1705
          if (casDrainStatus(PROCESSING_TO_IDLE, PROCESSING_TO_REQUIRED)) {
1✔
1706
            return;
1✔
1707
          }
1708
          drainStatus = drainStatusAcquire();
1✔
1709
          continue;
1✔
1710
        case PROCESSING_TO_REQUIRED:
1711
          return;
1✔
1712
        default:
1713
          throw new IllegalStateException("Invalid drain status: " + drainStatus);
1✔
1714
      }
1715
    }
1716
  }
1717

1718
  /**
1719
   * Attempts to schedule an asynchronous task to apply the pending operations to the page
1720
   * replacement policy. If the executor rejects the task then it is run directly.
1721
   */
1722
  void scheduleDrainBuffers() {
1723
    if (drainStatusOpaque() >= PROCESSING_TO_IDLE) {
1✔
1724
      return;
1✔
1725
    }
1726
    if (evictionLock.tryLock()) {
1✔
1727
      try {
1728
        int drainStatus = drainStatusOpaque();
1✔
1729
        if (drainStatus >= PROCESSING_TO_IDLE) {
1✔
1730
          return;
1✔
1731
        }
1732
        setDrainStatusRelease(PROCESSING_TO_IDLE);
1✔
1733
        executor.execute(drainBuffersTask);
1✔
1734
      } catch (Throwable t) {
1✔
1735
        logger.log(Level.WARNING, "Exception thrown when submitting maintenance task", t);
1✔
1736
        maintenance(/* ignored */ null);
1✔
1737
      } finally {
1738
        evictionLock.unlock();
1✔
1739
      }
1740
    }
1741
  }
1✔
1742

1743
  @Override
1744
  public void cleanUp() {
1745
    try {
1746
      performCleanUp(/* ignored */ null);
1✔
1747
    } catch (RuntimeException e) {
1✔
1748
      logger.log(Level.ERROR, "Exception thrown when performing the maintenance task", e);
1✔
1749
    }
1✔
1750
  }
1✔
1751

1752
  /**
1753
   * Performs the maintenance work, blocking until the lock is acquired.
1754
   *
1755
   * @param task an additional pending task to run, or {@code null} if not present
1756
   */
1757
  void performCleanUp(@Nullable Runnable task) {
1758
    evictionLock.lock();
1✔
1759
    try {
1760
      maintenance(task);
1✔
1761
    } finally {
1762
      evictionLock.unlock();
1✔
1763
    }
1764
    rescheduleCleanUpIfIncomplete();
1✔
1765
  }
1✔
1766

1767
  /**
1768
   * If there remains pending operations that were not handled by the prior clean up then try to
1769
   * schedule an asynchronous maintenance task. This may occur due to a concurrent write after the
1770
   * maintenance work had started or if the amortized threshold of work per clean up was reached.
1771
   */
1772
  @SuppressWarnings("resource")
1773
  void rescheduleCleanUpIfIncomplete() {
1774
    if (drainStatusOpaque() != REQUIRED) {
1✔
1775
      return;
1✔
1776
    }
1777

1778
    // An immediate scheduling cannot be performed on a custom executor because it may use a
1779
    // caller-runs policy. This could cause the caller's penalty to exceed the amortized threshold,
1780
    // e.g. repeated concurrent writes could result in a retry loop.
1781
    if (executor == ForkJoinPool.commonPool()) {
1✔
1782
      scheduleDrainBuffers();
1✔
1783
      return;
1✔
1784
    }
1785

1786
    // If a scheduler was configured then the maintenance can be deferred onto the custom executor
1787
    // and run in the near future. Otherwise, it will be handled due to other cache activity.
1788
    var pacer = pacer();
1✔
1789
    if ((pacer != null) && !pacer.isScheduled() && evictionLock.tryLock()) {
1✔
1790
      try {
1791
        if ((drainStatusOpaque() == REQUIRED) && !pacer.isScheduled()) {
1✔
1792
          pacer.schedule(executor, drainBuffersTask, expirationTicker().read(), Pacer.TOLERANCE);
1✔
1793
        }
1794
      } finally {
1795
        evictionLock.unlock();
1✔
1796
      }
1797
    }
1798
  }
1✔
1799

1800
  /**
1801
   * Performs the pending maintenance work and sets the state flags during processing to avoid
1802
   * excess scheduling attempts. The read buffer, write buffer, and reference queues are drained,
1803
   * followed by expiration, and size-based eviction.
1804
   *
1805
   * @param task an additional pending task to run, or {@code null} if not present
1806
   */
1807
  @GuardedBy("evictionLock")
1808
  void maintenance(@Nullable Runnable task) {
1809
    setDrainStatusRelease(PROCESSING_TO_IDLE);
1✔
1810

1811
    try {
1812
      try {
1813
        drainReadBuffer();
1✔
1814
        drainWriteBuffer();
1✔
1815
      } finally {
1816
        if (task != null) {
1✔
1817
          task.run();
1✔
1818
        }
1819
      }
1820

1821
      drainKeyReferences();
1✔
1822
      drainValueReferences();
1✔
1823

1824
      expireEntries();
1✔
1825
      evictEntries();
1✔
1826

1827
      climb();
1✔
1828
    } finally {
1829
      if ((drainStatusOpaque() != PROCESSING_TO_IDLE)
1✔
1830
          || !casDrainStatus(PROCESSING_TO_IDLE, IDLE)) {
1✔
1831
        setDrainStatusOpaque(REQUIRED);
1✔
1832
      }
1833
    }
1834
  }
1✔
1835

1836
  /** Drains the weak key references queue. */
1837
  @GuardedBy("evictionLock")
1838
  void drainKeyReferences() {
1839
    if (!collectKeys()) {
1✔
1840
      return;
1✔
1841
    }
1842
    @Var Reference<? extends K> keyRef;
1843
    while ((keyRef = keyReferenceQueue().poll()) != null) {
1✔
1844
      Node<K, V> node = data.get(keyRef);
1✔
1845
      if (node != null) {
1✔
1846
        evictEntry(node, RemovalCause.COLLECTED, 0L);
1✔
1847
      }
1848
    }
1✔
1849
  }
1✔
1850

1851
  /** Drains the weak / soft value references queue. */
1852
  @GuardedBy("evictionLock")
1853
  void drainValueReferences() {
1854
    if (!collectValues()) {
1✔
1855
      return;
1✔
1856
    }
1857
    @Var Reference<? extends V> valueRef;
1858
    while ((valueRef = valueReferenceQueue().poll()) != null) {
1✔
1859
      @SuppressWarnings("unchecked")
1860
      var ref = (InternalReference<V>) valueRef;
1✔
1861
      Node<K, V> node = data.get(ref.getKeyReference());
1✔
1862
      if ((node != null) && (valueRef == node.getValueReference())) {
1✔
1863
        evictEntry(node, RemovalCause.COLLECTED, 0L);
1✔
1864
      }
1865
    }
1✔
1866
  }
1✔
1867

1868
  /** Drains the read buffer. */
1869
  @GuardedBy("evictionLock")
1870
  void drainReadBuffer() {
1871
    if (!skipReadBuffer()) {
1✔
1872
      readBuffer.drainTo(accessPolicy);
1✔
1873
    }
1874
  }
1✔
1875

1876
  /** Updates the node's location in the page replacement policy. */
1877
  @GuardedBy("evictionLock")
1878
  void onAccess(Node<K, V> node) {
1879
    if (evicts()) {
1✔
1880
      var keyRef = node.getKeyReferenceOrNull();
1✔
1881
      if ((keyRef == null) || !node.isAlive()) {
1✔
1882
        return;
1✔
1883
      }
1884
      frequencySketch().increment(keyRef);
1✔
1885
      if (node.inWindow()) {
1✔
1886
        reorder(accessOrderWindowDeque(), node);
1✔
1887
      } else if (node.inMainProbation()) {
1✔
1888
        reorderProbation(node);
1✔
1889
      } else {
1890
        reorder(accessOrderProtectedDeque(), node);
1✔
1891
      }
1892
      setHitsInSample(hitsInSample() + 1);
1✔
1893
    } else if (expiresAfterAccess()) {
1✔
1894
      reorder(accessOrderWindowDeque(), node);
1✔
1895
    }
1896
    if (expiresVariable()) {
1✔
1897
      timerWheel().reschedule(node);
1✔
1898
    }
1899
  }
1✔
1900

1901
  /** Promote the node from probation to protected on an access. */
1902
  @GuardedBy("evictionLock")
1903
  void reorderProbation(Node<K, V> node) {
1904
    if (!accessOrderProbationDeque().contains(node)) {
1✔
1905
      // Ignore stale accesses for an entry that is no longer present
1906
      return;
1✔
1907
    } else if (node.getPolicyWeight() > mainProtectedMaximum()) {
1✔
1908
      reorder(accessOrderProbationDeque(), node);
1✔
1909
      return;
1✔
1910
    }
1911

1912
    // If the protected space exceeds its maximum, the LRU items are demoted to the probation space.
1913
    // This is deferred to the adaption phase at the end of the maintenance cycle.
1914
    setMainProtectedWeightedSize(mainProtectedWeightedSize() + node.getPolicyWeight());
1✔
1915
    accessOrderProbationDeque().remove(node);
1✔
1916
    accessOrderProtectedDeque().offerLast(node);
1✔
1917
    node.makeMainProtected();
1✔
1918
  }
1✔
1919

1920
  /** Updates the node's location in the policy's deque. */
1921
  static <K, V> void reorder(LinkedDeque<Node<K, V>> deque, Node<K, V> node) {
1922
    // An entry may be scheduled for reordering despite having been removed. This can occur when the
1923
    // entry was concurrently read while a writer was removing it. If the entry is no longer linked
1924
    // then it does not need to be processed.
1925
    if (deque.contains(node)) {
1✔
1926
      deque.moveToBack(node);
1✔
1927
    }
1928
  }
1✔
1929

1930
  /** Drains the write buffer. */
1931
  @GuardedBy("evictionLock")
1932
  void drainWriteBuffer() {
1933
    for (int i = 0; i <= WRITE_BUFFER_MAX; i++) {
1✔
1934
      Runnable task = writeBuffer.poll();
1✔
1935
      if (task == null) {
1✔
1936
        return;
1✔
1937
      }
1938
      task.run();
1✔
1939
    }
1940
    setDrainStatusOpaque(PROCESSING_TO_REQUIRED);
1✔
1941
  }
1✔
1942

1943
  /**
1944
   * Atomically transitions the node to the <code>dead</code> state and decrements the
1945
   * <code>weightedSize</code>.
1946
   *
1947
   * @param node the entry in the page replacement policy
1948
   */
1949
  @GuardedBy("evictionLock")
1950
  @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
1951
  void makeDead(Node<K, V> node) {
1952
    synchronized (node) {
1✔
1953
      if (node.isDead()) {
1✔
1954
        return;
1✔
1955
      }
1956
      if (evicts()) {
1✔
1957
        // The node's policy weight may be out of sync due to a pending update waiting to be
1958
        // processed. At this point the node's weight is finalized, so the weight can be safely
1959
        // taken from the node's perspective and the sizes will be adjusted correctly.
1960
        if (node.inWindow()) {
1✔
1961
          setWindowWeightedSize(windowWeightedSize() - node.getWeight());
1✔
1962
        } else if (node.inMainProtected()) {
1✔
1963
          setMainProtectedWeightedSize(mainProtectedWeightedSize() - node.getWeight());
1✔
1964
        }
1965
        setWeightedSize(weightedSize() - node.getWeight());
1✔
1966
      }
1967
      node.die();
1✔
1968
    }
1✔
1969
  }
1✔
1970

1971
  /** Adds the node to the page replacement policy. */
1972
  final class AddTask implements Runnable {
1973
    final Node<K, V> node;
1974
    final int weight;
1975

1976
    AddTask(Node<K, V> node, int weight) {
1✔
1977
      this.weight = weight;
1✔
1978
      this.node = node;
1✔
1979
    }
1✔
1980

1981
    @Override
1982
    @GuardedBy("evictionLock")
1983
    public void run() {
1984
      if (evicts()) {
1✔
1985
        setWeightedSize(weightedSize() + weight);
1✔
1986
        setWindowWeightedSize(windowWeightedSize() + weight);
1✔
1987
        node.setPolicyWeight(node.getPolicyWeight() + weight);
1✔
1988

1989
        long maximum = maximum();
1✔
1990
        if (weightedSize() >= (maximum >>> 1)) {
1✔
1991
          if (weightedSize() > MAXIMUM_CAPACITY) {
1✔
1992
            evictEntries();
1✔
1993
          } else {
1994
            // Lazily initialize when close to the maximum
1995
            long capacity = isWeighted() ? data.mappingCount() : maximum;
1✔
1996
            frequencySketch().ensureCapacity(capacity);
1✔
1997
          }
1998
        }
1999

2000
        setMissesInSample(missesInSample() + 1);
1✔
2001
      }
2002

2003
      // ignore out-of-order write operations
2004
      boolean isAlive;
2005
      synchronized (node) {
1✔
2006
        isAlive = node.isAlive();
1✔
2007
      }
1✔
2008
      if (isAlive) {
1✔
2009
        if (expiresAfterWrite()) {
1✔
2010
          writeOrderDeque().offerLast(node);
1✔
2011
        }
2012
        if (expiresVariable()) {
1✔
2013
          timerWheel().schedule(node);
1✔
2014
        }
2015
        if (evicts()) {
1✔
2016
          var keyRef = node.getKeyReferenceOrNull();
1✔
2017
          if ((keyRef != null) && node.isAlive()) {
1✔
2018
            frequencySketch().increment(keyRef);
1✔
2019
          }
2020
          if (weight > maximum()) {
1✔
2021
            evictEntry(node, RemovalCause.SIZE, expirationTicker().read());
1✔
2022
          } else if (weight > windowMaximum()) {
1✔
2023
            accessOrderWindowDeque().offerFirst(node);
1✔
2024
          } else {
2025
            accessOrderWindowDeque().offerLast(node);
1✔
2026
          }
2027
        } else if (expiresAfterAccess()) {
1✔
2028
          accessOrderWindowDeque().offerLast(node);
1✔
2029
        }
2030
      }
2031
    }
1✔
2032
  }
2033

2034
  /** Removes a node from the page replacement policy. */
2035
  final class RemovalTask implements Runnable {
2036
    final Node<K, V> node;
2037

2038
    RemovalTask(Node<K, V> node) {
1✔
2039
      this.node = node;
1✔
2040
    }
1✔
2041

2042
    @Override
2043
    @GuardedBy("evictionLock")
2044
    public void run() {
2045
      // add may not have been processed yet
2046
      if (node.inWindow() && (evicts() || expiresAfterAccess())) {
1✔
2047
        accessOrderWindowDeque().remove(node);
1✔
2048
      } else if (evicts()) {
1✔
2049
        if (node.inMainProbation()) {
1✔
2050
          accessOrderProbationDeque().remove(node);
1✔
2051
        } else {
2052
          accessOrderProtectedDeque().remove(node);
1✔
2053
        }
2054
      }
2055
      if (expiresAfterWrite()) {
1✔
2056
        writeOrderDeque().remove(node);
1✔
2057
      } else if (expiresVariable()) {
1✔
2058
        timerWheel().deschedule(node);
1✔
2059
      }
2060
      makeDead(node);
1✔
2061
    }
1✔
2062
  }
2063

2064
  /** Updates the weighted size. */
2065
  final class UpdateTask implements Runnable {
2066
    final int weightDifference;
2067
    final Node<K, V> node;
2068

2069
    public UpdateTask(Node<K, V> node, int weightDifference) {
1✔
2070
      this.weightDifference = weightDifference;
1✔
2071
      this.node = node;
1✔
2072
    }
1✔
2073

2074
    @Override
2075
    @GuardedBy("evictionLock")
2076
    public void run() {
2077
      if (expiresAfterWrite()) {
1✔
2078
        reorder(writeOrderDeque(), node);
1✔
2079
      } else if (expiresVariable()) {
1✔
2080
        timerWheel().reschedule(node);
1✔
2081
      }
2082
      if (evicts()) {
1✔
2083
        int oldWeightedSize = node.getPolicyWeight();
1✔
2084
        node.setPolicyWeight(oldWeightedSize + weightDifference);
1✔
2085
        if (node.inWindow()) {
1✔
2086
          setWindowWeightedSize(windowWeightedSize() + weightDifference);
1✔
2087
          if (node.getPolicyWeight() > maximum()) {
1✔
2088
            evictEntry(node, RemovalCause.SIZE, expirationTicker().read());
1✔
2089
          } else if (node.getPolicyWeight() <= windowMaximum()) {
1✔
2090
            onAccess(node);
1✔
2091
          } else if (accessOrderWindowDeque().contains(node)) {
1✔
2092
            accessOrderWindowDeque().moveToFront(node);
1✔
2093
          }
2094
        } else if (node.inMainProbation()) {
1✔
2095
            if (node.getPolicyWeight() <= maximum()) {
1✔
2096
              onAccess(node);
1✔
2097
            } else {
2098
              evictEntry(node, RemovalCause.SIZE, expirationTicker().read());
1✔
2099
            }
2100
        } else {
2101
          setMainProtectedWeightedSize(mainProtectedWeightedSize() + weightDifference);
1✔
2102
          if (node.getPolicyWeight() <= maximum()) {
1✔
2103
            onAccess(node);
1✔
2104
          } else {
2105
            evictEntry(node, RemovalCause.SIZE, expirationTicker().read());
1✔
2106
          }
2107
        }
2108

2109
        setWeightedSize(weightedSize() + weightDifference);
1✔
2110
        if (weightedSize() > MAXIMUM_CAPACITY) {
1✔
2111
          evictEntries();
1✔
2112
        }
2113
      } else if (expiresAfterAccess()) {
1✔
2114
        onAccess(node);
1✔
2115
      }
2116
    }
1✔
2117
  }
2118

2119
  /* --------------- Concurrent Map Support --------------- */
2120

2121
  @Override
2122
  public boolean isEmpty() {
2123
    return data.isEmpty();
1✔
2124
  }
2125

2126
  @Override
2127
  public int size() {
2128
    return data.size();
1✔
2129
  }
2130

2131
  @Override
2132
  public long estimatedSize() {
2133
    return data.mappingCount();
1✔
2134
  }
2135

2136
  @Override
2137
  public void clear() {
2138
    Deque<Node<K, V>> entries;
2139
    evictionLock.lock();
1✔
2140
    try {
2141
      // Discard all pending reads
2142
      readBuffer.drainTo(e -> {});
1✔
2143

2144
      // Apply all pending writes
2145
      @Var Runnable task;
2146
      while ((task = writeBuffer.poll()) != null) {
1✔
2147
        task.run();
1✔
2148
      }
2149

2150
      // Cancel the scheduled cleanup
2151
      Pacer pacer = pacer();
1✔
2152
      if (pacer != null) {
1✔
2153
        pacer.cancel();
1✔
2154
      }
2155

2156
      // Discard all entries, falling back to one-by-one to avoid excessive lock hold times
2157
      long now = expirationTicker().read();
1✔
2158
      int threshold = (WRITE_BUFFER_MAX / 2);
1✔
2159
      entries = new ArrayDeque<>(data.values());
1✔
2160
      while (!entries.isEmpty() && (writeBuffer.size() < threshold)) {
1✔
2161
        removeNode(entries.pollFirst(), now);
1✔
2162
      }
2163
    } finally {
2164
      evictionLock.unlock();
1✔
2165
    }
2166

2167
    // Remove any stragglers if released early to more aggressively flush incoming writes
2168
    @Var boolean cleanUp = false;
1✔
2169
    for (var node : entries) {
1✔
2170
      @Nullable K key = node.getKey();
1✔
2171
      if (key == null) {
1✔
2172
        cleanUp = true;
1✔
2173
      } else {
2174
        remove(key);
1✔
2175
      }
2176
    }
1✔
2177
    if (collectKeys() && cleanUp) {
1✔
2178
      cleanUp();
1✔
2179
    }
2180
  }
1✔
2181

2182
  @GuardedBy("evictionLock")
2183
  @SuppressWarnings({"GuardedByChecker", "SynchronizationOnLocalVariableOrMethodParameter"})
2184
  void removeNode(Node<K, V> node, long now) {
2185
    K key = node.getKey();
1✔
2186
    var ctx = new EvictContext<V>();
1✔
2187
    var keyReference = node.getKeyReference();
1✔
2188

2189
    data.computeIfPresent(keyReference, (k, n) -> {
1✔
2190
      if (n != node) {
1✔
2191
        return n;
1✔
2192
      }
2193
      synchronized (node) {
1✔
2194
        ctx.value = node.getValue();
1✔
2195
        ctx.oldWeight = node.getWeight();
1✔
2196

2197
        if ((key == null) || (ctx.value == null)) {
1✔
2198
          ctx.cause = RemovalCause.COLLECTED;
1✔
2199
        } else if (hasExpired(node, now, ctx.value)) {
1✔
2200
          ctx.cause = RemovalCause.EXPIRED;
1✔
2201
        } else {
2202
          ctx.cause = RemovalCause.EXPLICIT;
1✔
2203
        }
2204

2205
        if (ctx.cause.wasEvicted()) {
1✔
2206
          notifyEviction(key, ctx.value, ctx.cause);
1✔
2207
        }
2208

2209
        discardRefresh(node.getKeyReference());
1✔
2210
        node.retire();
1✔
2211
        return null;
1✔
2212
      }
2213
    });
2214

2215
    if (node.inWindow() && (evicts() || expiresAfterAccess())) {
1✔
2216
      accessOrderWindowDeque().remove(node);
1✔
2217
    } else if (evicts()) {
1✔
2218
      if (node.inMainProbation()) {
1✔
2219
        accessOrderProbationDeque().remove(node);
1✔
2220
      } else {
2221
        accessOrderProtectedDeque().remove(node);
1✔
2222
      }
2223
    }
2224
    if (expiresAfterWrite()) {
1✔
2225
      writeOrderDeque().remove(node);
1✔
2226
    } else if (expiresVariable()) {
1✔
2227
      timerWheel().deschedule(node);
1✔
2228
    }
2229

2230
    synchronized (node) {
1✔
2231
      logIfAlive(node);
1✔
2232
      makeDead(node);
1✔
2233
    }
1✔
2234

2235
    if (ctx.cause != null) {
1✔
2236
      if (ctx.cause.wasEvicted()) {
1✔
2237
        statsCounter().recordEviction(ctx.oldWeight, ctx.cause);
1✔
2238
      }
2239
      notifyRemoval(key, ctx.value, ctx.cause);
1✔
2240
    }
2241
  }
1✔
2242

2243
  @Override
2244
  public boolean containsKey(Object key) {
2245
    Node<K, V> node = data.get(nodeFactory.newLookupKey(key));
1✔
2246
    if (node == null) {
1✔
2247
      return false;
1✔
2248
    }
2249
    V value = node.getValue();
1✔
2250
    return (value != null) && !hasExpired(node, expirationTicker().read(), value);
1✔
2251
  }
2252

2253
  @Override
2254
  public boolean containsValue(Object value) {
2255
    requireNonNull(value);
1✔
2256

2257
    long now = expirationTicker().read();
1✔
2258
    for (Node<K, V> node : data.values()) {
1✔
2259
      V nodeValue = node.getValue();
1✔
2260
      if (node.isAlive() && (node.getKey() != null) && (nodeValue != null)
1✔
2261
          && node.containsValue(value) && !hasExpired(node, now, nodeValue)) {
1✔
2262
        return true;
1✔
2263
      }
2264
    }
1✔
2265
    return false;
1✔
2266
  }
2267

2268
  @Override
2269
  public @Nullable V get(Object key) {
2270
    return getIfPresent(key, /* recordStats= */ false);
1✔
2271
  }
2272

2273
  @Override
2274
  public @Nullable V getIfPresent(Object key, boolean recordStats) {
2275
    Node<K, V> node = data.get(nodeFactory.newLookupKey(key));
1✔
2276
    if (node == null) {
1✔
2277
      if (recordStats) {
1✔
2278
        statsCounter().recordMisses(1);
1✔
2279
      }
2280
      if (drainStatusOpaque() == REQUIRED) {
1✔
2281
        scheduleDrainBuffers();
1✔
2282
      }
2283
      return null;
1✔
2284
    }
2285

2286
    V value = node.getValue();
1✔
2287
    long now = expirationTicker().read();
1✔
2288
    if ((value == null) || hasExpired(node, now, value)) {
1✔
2289
      if (recordStats) {
1✔
2290
        statsCounter().recordMisses(1);
1✔
2291
      }
2292
      scheduleDrainBuffers();
1✔
2293
      return null;
1✔
2294
    }
2295

2296
    if (!isComputingAsync(value)) {
1✔
2297
      @SuppressWarnings("unchecked")
2298
      var castedKey = (K) key;
1✔
2299
      setAccessTime(node, now);
1✔
2300
      tryExpireAfterRead(node, castedKey, value, expiry(), now);
1✔
2301
    }
2302
    V refreshed = afterRead(node, now, recordStats);
1✔
2303
    return (refreshed == null) ? value : refreshed;
1✔
2304
  }
2305

2306
  @Override
2307
  public @Nullable V getIfPresentQuietly(Object key) {
2308
    V value;
2309
    Node<K, V> node = data.get(nodeFactory.newLookupKey(key));
1✔
2310
    if ((node == null) || ((value = node.getValue()) == null)
1✔
2311
        || hasExpired(node, expirationTicker().read(), value)) {
1✔
2312
      return null;
1✔
2313
    }
2314
    return value;
1✔
2315
  }
2316

2317
  /**
2318
   * Returns the key associated with the mapping in this cache, or {@code null} if there is none.
2319
   *
2320
   * @param key the key whose canonical instance is to be returned
2321
   * @return the key used by the mapping, or {@code null} if this cache does not contain a mapping
2322
   *         for the key
2323
   * @throws NullPointerException if the specified key is null
2324
   */
2325
  public @Nullable K getKey(K key) {
2326
    Node<K, V> node = data.get(nodeFactory.newLookupKey(key));
1✔
2327
    if (node == null) {
1✔
2328
      if (drainStatusOpaque() == REQUIRED) {
1✔
2329
        scheduleDrainBuffers();
1✔
2330
      }
2331
      return null;
1✔
2332
    }
2333
    afterRead(node, /* now= */ 0L, /* recordHit= */ false);
1✔
2334
    return node.getKey();
1✔
2335
  }
2336

2337
  @Override
2338
  public Map<K, V> getAllPresent(Iterable<? extends K> keys) {
2339
    var result = new LinkedHashMap<K, @Nullable V>(calculateHashMapCapacity(keys));
1✔
2340
    for (K key : keys) {
1✔
2341
      result.put(key, null);
1✔
2342
    }
1✔
2343

2344
    int uniqueKeys = result.size();
1✔
2345
    long now = expirationTicker().read();
1✔
2346
    for (var iter = result.entrySet().iterator(); iter.hasNext();) {
1✔
2347
      V value;
2348
      var entry = iter.next();
1✔
2349
      Node<K, V> node = data.get(nodeFactory.newLookupKey(entry.getKey()));
1✔
2350
      if ((node == null) || ((value = node.getValue()) == null)
1✔
2351
          || hasExpired(node, now, value)) {
1✔
2352
        iter.remove();
1✔
2353
      } else {
2354
        setAccessTime(node, now);
1✔
2355
        tryExpireAfterRead(node, entry.getKey(), value, expiry(), now);
1✔
2356
        V refreshed = afterRead(node, now, /* recordHit= */ false);
1✔
2357
        entry.setValue((refreshed == null) ? value : refreshed);
1✔
2358
      }
2359
    }
1✔
2360
    statsCounter().recordHits(result.size());
1✔
2361
    statsCounter().recordMisses(uniqueKeys - result.size());
1✔
2362

2363
    @SuppressWarnings("NullableProblems")
2364
    Map<K, V> unmodifiable = Collections.unmodifiableMap(result);
1✔
2365
    return unmodifiable;
1✔
2366
  }
2367

2368
  @Override
2369
  public void putAll(Map<? extends K, ? extends V> map) {
2370
    map.forEach(this::put);
1✔
2371
  }
1✔
2372

2373
  @Override
2374
  public @Nullable V put(K key, V value) {
2375
    return put(key, value, expiry(), /* onlyIfAbsent= */ false);
1✔
2376
  }
2377

2378
  @Override
2379
  public @Nullable V putIfAbsent(K key, V value) {
2380
    return put(key, value, expiry(), /* onlyIfAbsent= */ true);
1✔
2381
  }
2382

2383
  /**
2384
   * Adds a node to the policy and the data store. If an existing node is found, then its value is
2385
   * updated if allowed.
2386
   *
2387
   * @param key key with which the specified value is to be associated
2388
   * @param value value to be associated with the specified key
2389
   * @param expiry the calculator for the write expiration time
2390
   * @param onlyIfAbsent a write is performed only if the key is not already associated with a value
2391
   * @return the prior value in or null if no mapping was found
2392
   */
2393
  @Nullable V put(K key, V value, Expiry<K, V> expiry, boolean onlyIfAbsent) {
2394
    requireNonNull(key);
1✔
2395
    requireNonNull(value);
1✔
2396

2397
    @Var int newWeight = -1;
1✔
2398
    @Var Object keyRef = null;
1✔
2399
    @Var Node<K, V> node = null;
1✔
2400
    Object lookupKey = nodeFactory.newLookupKey(key);
1✔
2401
    for (int attempts = 1; ; attempts++) {
1✔
2402
      @Var Node<K, V> prior = data.get(lookupKey);
1✔
2403
      if (prior == null) {
1✔
2404
        if (node == null) {
1✔
2405
          if (newWeight < 0) {
1✔
2406
            newWeight = weigher.weigh(key, value);
1✔
2407
          }
2408
          long now = expirationTicker().read();
1✔
2409
          keyRef = nodeFactory.newReferenceKey(key, keyReferenceQueue());
1✔
2410
          node = nodeFactory.newNode(keyRef, value, valueReferenceQueue(), newWeight, now);
1✔
2411
          long expirationTime = isComputingAsync(value) ? (now + ASYNC_EXPIRY) : now;
1✔
2412
          setVariableTime(node, expireAfterCreate(key, value, expiry, now));
1✔
2413
          setAccessTime(node, expirationTime);
1✔
2414
          setWriteTime(node, expirationTime);
1✔
2415
        }
2416
        var newNode = node;
1✔
2417
        prior = (cacheLoader == null)
1✔
2418
            ? data.putIfAbsent(keyRef, newNode)
1✔
2419
            : data.computeIfAbsent(keyRef, k -> {
1✔
2420
                discardRefresh(k);
1✔
2421
                return newNode;
1✔
2422
              });
2423
        if ((prior == null) || (prior == node)) {
1✔
2424
          afterWrite(new AddTask(node, newWeight));
1✔
2425
          return null;
1✔
2426
        } else if (onlyIfAbsent) {
1✔
2427
          // An optimistic fast path to avoid unnecessary locking
2428
          V currentValue = prior.getValue();
1✔
2429
          long now = expirationTicker().read();
1✔
2430
          if ((currentValue != null) && !hasExpired(prior, now, currentValue)) {
1✔
2431
            if (!isComputingAsync(currentValue)) {
1✔
2432
              tryExpireAfterRead(prior, key, currentValue, expiry, now);
1✔
2433
              setAccessTime(prior, now);
1✔
2434
            }
2435
            afterRead(prior, now, /* recordHit= */ false);
1✔
2436
            return currentValue;
1✔
2437
          }
2438
        }
2439
      } else if (onlyIfAbsent) {
1✔
2440
        // An optimistic fast path to avoid unnecessary locking
2441
        V currentValue = prior.getValue();
1✔
2442
        long now = expirationTicker().read();
1✔
2443
        if ((currentValue != null) && !hasExpired(prior, now, currentValue)) {
1✔
2444
          if (!isComputingAsync(currentValue)) {
1✔
2445
            tryExpireAfterRead(prior, key, currentValue, expiry, now);
1✔
2446
            setAccessTime(prior, now);
1✔
2447
          }
2448
          afterRead(prior, now, /* recordHit= */ false);
1✔
2449
          return currentValue;
1✔
2450
        }
2451
      }
2452

2453
      // A read may race with the entry's removal, so that after the entry is acquired it may no
2454
      // longer be usable. A retry will reread from the map and either find an absent mapping, a
2455
      // new entry, or a stale entry.
2456
      if (!prior.isAlive()) {
1✔
2457
        // A reread of the stale entry may occur if the state transition occurred but the map
2458
        // removal was delayed by a context switch, so that this thread spin waits until resolved.
2459
        if ((attempts & MAX_PUT_SPIN_WAIT_ATTEMPTS) != 0) {
1✔
2460
          Thread.onSpinWait();
1✔
2461
          continue;
1✔
2462
        }
2463

2464
        // If the spin wait attempts are exhausted then fallback to a map computation in order to
2465
        // deschedule this thread until the entry's removal completes. If the key was modified
2466
        // while in the map so that its equals or hashCode changed then the contents may be
2467
        // corrupted, where the cache holds an evicted (dead) entry that could not be removed.
2468
        // That is a violation of the Map contract, so we check that the mapping is in the "alive"
2469
        // state while in the computation.
2470
        data.computeIfPresent(lookupKey, (k, n) -> {
1✔
2471
          requireIsAlive(key, n);
1✔
2472
          return n;
1✔
2473
        });
2474
        continue;
1✔
2475
      }
2476

2477
      long now;
2478
      V oldValue;
2479
      long varTime;
2480
      int oldWeight;
2481
      @Var boolean expired = false;
1✔
2482
      @Var boolean mayUpdate = true;
1✔
2483
      @Var boolean exceedsTolerance = false;
1✔
2484
      if (newWeight < 0) {
1✔
2485
        newWeight = weigher.weigh(key, value);
1✔
2486
      }
2487
      synchronized (prior) {
1✔
2488
        if (!prior.isAlive()) {
1✔
2489
          continue;
1✔
2490
        }
2491
        oldValue = prior.getValue();
1✔
2492
        oldWeight = prior.getWeight();
1✔
2493
        now = expirationTicker().read();
1✔
2494
        if (oldValue == null) {
1✔
2495
          varTime = expireAfterCreate(key, value, expiry, now);
1✔
2496
          notifyEviction(key, null, RemovalCause.COLLECTED);
1✔
2497
        } else if (hasExpired(prior, now, oldValue)) {
1✔
2498
          expired = true;
1✔
2499
          varTime = expireAfterCreate(key, value, expiry, now);
1✔
2500
          notifyEviction(key, oldValue, RemovalCause.EXPIRED);
1✔
2501
        } else if (onlyIfAbsent) {
1✔
2502
          mayUpdate = false;
1✔
2503
          varTime = expireAfterRead(prior, key, oldValue, expiry, now);
1✔
2504
        } else {
2505
          varTime = expireAfterUpdate(prior, key, value, expiry, now);
1✔
2506
        }
2507

2508
        long expirationTime = isComputingAsync(mayUpdate ? value : oldValue)
1✔
2509
            ? (now + ASYNC_EXPIRY)
1✔
2510
            : now;
1✔
2511
        if (mayUpdate) {
1✔
2512
          exceedsTolerance = exceedsWriteTimeTolerance(prior, varTime, expirationTime);
1✔
2513
          if (expired || exceedsTolerance) {
1✔
2514
            setWriteTime(prior, expirationTime);
1✔
2515
          }
2516

2517
          prior.setValue(value, valueReferenceQueue());
1✔
2518
          prior.setWeight(newWeight);
1✔
2519

2520
          discardRefresh(prior.getKeyReference());
1✔
2521
        }
2522

2523
        setVariableTime(prior, varTime);
1✔
2524
        setAccessTime(prior, expirationTime);
1✔
2525
      }
1✔
2526

2527
      if (expired) {
1✔
2528
        statsCounter().recordEviction(oldWeight, RemovalCause.EXPIRED);
1✔
2529
        notifyRemoval(key, oldValue, RemovalCause.EXPIRED);
1✔
2530
      } else if (oldValue == null) {
1✔
2531
        statsCounter().recordEviction(oldWeight, RemovalCause.COLLECTED);
1✔
2532
        notifyRemoval(key, /* value= */ null, RemovalCause.COLLECTED);
1✔
2533
      } else if (mayUpdate) {
1✔
2534
        notifyOnReplace(key, oldValue, value);
1✔
2535
      }
2536

2537
      int weightedDifference = mayUpdate ? (newWeight - oldWeight) : 0;
1✔
2538
      if ((oldValue == null) || (weightedDifference != 0) || expired) {
1✔
2539
        afterWrite(new UpdateTask(prior, weightedDifference));
1✔
2540
      } else if (!onlyIfAbsent && exceedsTolerance) {
1✔
2541
        afterWrite(new UpdateTask(prior, weightedDifference));
1✔
2542
      } else {
2543
        afterRead(prior, now, /* recordHit= */ false);
1✔
2544
      }
2545

2546
      return expired ? null : oldValue;
1✔
2547
    }
2548
  }
2549

2550
  @Override
2551
  @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
2552
  public @Nullable V remove(Object key) {
2553
    var ctx = new RemoveContext<K, V>();
1✔
2554
    Object lookupKey = nodeFactory.newLookupKey(key);
1✔
2555
    data.computeIfPresent(lookupKey, (k, n) -> {
1✔
2556
      synchronized (n) {
1✔
2557
        requireIsAlive(key, n);
1✔
2558
        ctx.oldKey = n.getKey();
1✔
2559
        ctx.oldValue = n.getValue();
1✔
2560
        ctx.oldWeight = n.getWeight();
1✔
2561
        RemovalCause actualCause;
2562
        if ((ctx.oldKey == null) || (ctx.oldValue == null)) {
1✔
2563
          actualCause = RemovalCause.COLLECTED;
1✔
2564
        } else if (hasExpired(n, expirationTicker().read(), ctx.oldValue)) {
1✔
2565
          actualCause = RemovalCause.EXPIRED;
1✔
2566
        } else {
2567
          actualCause = RemovalCause.EXPLICIT;
1✔
2568
        }
2569
        if (actualCause.wasEvicted()) {
1✔
2570
          notifyEviction(ctx.oldKey, ctx.oldValue, actualCause);
1✔
2571
        }
2572
        ctx.cause = actualCause;
1✔
2573
        discardRefresh(k);
1✔
2574
        ctx.node = n;
1✔
2575
        n.retire();
1✔
2576
        return null;
1✔
2577
      }
2578
    });
2579

2580
    if (ctx.cause != null) {
1✔
2581
      afterWrite(new RemovalTask(requireNonNull(ctx.node)));
1✔
2582
      if (ctx.cause.wasEvicted()) {
1✔
2583
        statsCounter().recordEviction(ctx.oldWeight, ctx.cause);
1✔
2584
      }
2585
      notifyRemoval(ctx.oldKey, ctx.oldValue, ctx.cause);
1✔
2586
    }
2587
    return (ctx.cause == RemovalCause.EXPLICIT) ? ctx.oldValue : null;
1✔
2588
  }
2589

2590
  @Override
2591
  @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
2592
  public boolean remove(Object key, @Nullable Object value) {
2593
    requireNonNull(key);
1✔
2594
    if (value == null) {
1✔
2595
      return false;
1✔
2596
    }
2597

2598
    var ctx = new RemoveContext<K, V>();
1✔
2599
    Object lookupKey = nodeFactory.newLookupKey(key);
1✔
2600
    data.computeIfPresent(lookupKey, (kR, node) -> {
1✔
2601
      synchronized (node) {
1✔
2602
        requireIsAlive(key, node);
1✔
2603
        ctx.oldKey = node.getKey();
1✔
2604
        ctx.oldValue = node.getValue();
1✔
2605
        ctx.oldWeight = node.getWeight();
1✔
2606
        if ((ctx.oldKey == null) || (ctx.oldValue == null)) {
1✔
2607
          ctx.cause = RemovalCause.COLLECTED;
1✔
2608
        } else if (hasExpired(node, expirationTicker().read(), ctx.oldValue)) {
1✔
2609
          ctx.cause = RemovalCause.EXPIRED;
1✔
2610
        } else if (node.containsValue(value)) {
1✔
2611
          ctx.cause = RemovalCause.EXPLICIT;
1✔
2612
        } else {
2613
          return node;
1✔
2614
        }
2615
        if (ctx.cause.wasEvicted()) {
1✔
2616
          notifyEviction(ctx.oldKey, ctx.oldValue, ctx.cause);
1✔
2617
        }
2618
        discardRefresh(kR);
1✔
2619
        ctx.node = node;
1✔
2620
        node.retire();
1✔
2621
        return null;
1✔
2622
      }
2623
    });
2624

2625
    if (ctx.node == null) {
1✔
2626
      return false;
1✔
2627
    }
2628
    var removeCause = requireNonNull(ctx.cause);
1✔
2629
    afterWrite(new RemovalTask(ctx.node));
1✔
2630
    if (removeCause.wasEvicted()) {
1✔
2631
      statsCounter().recordEviction(ctx.oldWeight, removeCause);
1✔
2632
    }
2633
    notifyRemoval(ctx.oldKey, ctx.oldValue, removeCause);
1✔
2634

2635
    return (removeCause == RemovalCause.EXPLICIT);
1✔
2636
  }
2637

2638
  @Override
2639
  @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
2640
  public @Nullable V replace(K key, V value) {
2641
    requireNonNull(key);
1✔
2642
    requireNonNull(value);
1✔
2643
    var ctx = new ReplaceContext<K, V>();
1✔
2644
    int weight = weigher.weigh(key, value);
1✔
2645
    Node<K, V> node = data.computeIfPresent(nodeFactory.newLookupKey(key), (k, n) -> {
1✔
2646
      synchronized (n) {
1✔
2647
        requireIsAlive(key, n);
1✔
2648
        ctx.nodeKey = n.getKey();
1✔
2649
        ctx.oldValue = n.getValue();
1✔
2650
        ctx.oldWeight = n.getWeight();
1✔
2651
        if ((ctx.nodeKey == null) || (ctx.oldValue == null)
1✔
2652
            || hasExpired(n, ctx.now = expirationTicker().read(), ctx.oldValue)) {
1✔
2653
          ctx.oldValue = null;
1✔
2654
          return n;
1✔
2655
        }
2656

2657
        long varTime = expireAfterUpdate(n, key, value, expiry(), ctx.now);
1✔
2658
        n.setValue(value, valueReferenceQueue());
1✔
2659
        n.setWeight(weight);
1✔
2660

2661
        long expirationTime = isComputingAsync(value) ? (ctx.now + ASYNC_EXPIRY) : ctx.now;
1✔
2662
        ctx.exceedsTolerance = exceedsWriteTimeTolerance(n, varTime, expirationTime);
1✔
2663
        if (ctx.exceedsTolerance) {
1✔
2664
          setWriteTime(n, expirationTime);
1✔
2665
        }
2666
        setAccessTime(n, expirationTime);
1✔
2667
        setVariableTime(n, varTime);
1✔
2668
        discardRefresh(k);
1✔
2669
        return n;
1✔
2670
      }
2671
    });
2672

2673
    if ((node == null) || (ctx.nodeKey == null) || (ctx.oldValue == null)) {
1✔
2674
      if (node != null) {
1✔
2675
        scheduleDrainBuffers();
1✔
2676
      }
2677
      return null;
1✔
2678
    }
2679

2680
    int weightedDifference = (weight - ctx.oldWeight);
1✔
2681
    if (ctx.exceedsTolerance || (weightedDifference != 0)) {
1✔
2682
      afterWrite(new UpdateTask(node, weightedDifference));
1✔
2683
    } else {
2684
      afterRead(node, ctx.now, /* recordHit= */ false);
1✔
2685
    }
2686

2687
    notifyOnReplace(ctx.nodeKey, ctx.oldValue, value);
1✔
2688
    return ctx.oldValue;
1✔
2689
  }
2690

2691
  @Override
2692
  public boolean replace(K key, V oldValue, V newValue) {
2693
    return replace(key, oldValue, newValue, /* shouldDiscardRefresh= */ true);
1✔
2694
  }
2695

2696
  @Override
2697
  @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
2698
  public boolean replace(K key, V oldValue, V newValue, boolean shouldDiscardRefresh) {
2699
    requireNonNull(key);
1✔
2700
    requireNonNull(oldValue);
1✔
2701
    requireNonNull(newValue);
1✔
2702
    var ctx = new ReplaceContext<K, V>();
1✔
2703
    int weight = weigher.weigh(key, newValue);
1✔
2704
    Node<K, V> node = data.computeIfPresent(nodeFactory.newLookupKey(key), (k, n) -> {
1✔
2705
      synchronized (n) {
1✔
2706
        requireIsAlive(key, n);
1✔
2707
        ctx.nodeKey = n.getKey();
1✔
2708
        ctx.oldValue = n.getValue();
1✔
2709
        ctx.oldWeight = n.getWeight();
1✔
2710
        if ((ctx.nodeKey == null) || (ctx.oldValue == null) || !n.containsValue(oldValue)
1✔
2711
            || hasExpired(n, ctx.now = expirationTicker().read(), ctx.oldValue)) {
1✔
2712
          ctx.oldValue = null;
1✔
2713
          return n;
1✔
2714
        }
2715

2716
        long varTime = expireAfterUpdate(n, key, newValue, expiry(), ctx.now);
1✔
2717
        n.setValue(newValue, valueReferenceQueue());
1✔
2718
        n.setWeight(weight);
1✔
2719

2720
        long expirationTime = isComputingAsync(newValue) ? (ctx.now + ASYNC_EXPIRY) : ctx.now;
1✔
2721
        ctx.exceedsTolerance = exceedsWriteTimeTolerance(n, varTime, expirationTime);
1✔
2722
        if (ctx.exceedsTolerance) {
1✔
2723
          setWriteTime(n, expirationTime);
1✔
2724
        }
2725
        setAccessTime(n, expirationTime);
1✔
2726
        setVariableTime(n, varTime);
1✔
2727

2728
        if (shouldDiscardRefresh) {
1✔
2729
          discardRefresh(k);
1✔
2730
        }
2731
      }
1✔
2732
      return n;
1✔
2733
    });
2734

2735
    if ((node == null) || (ctx.nodeKey == null) || (ctx.oldValue == null)) {
1✔
2736
      if (node != null) {
1✔
2737
        scheduleDrainBuffers();
1✔
2738
      }
2739
      return false;
1✔
2740
    }
2741

2742
    int weightedDifference = (weight - ctx.oldWeight);
1✔
2743
    if (ctx.exceedsTolerance || (weightedDifference != 0)) {
1✔
2744
      afterWrite(new UpdateTask(node, weightedDifference));
1✔
2745
    } else {
2746
      afterRead(node, ctx.now, /* recordHit= */ false);
1✔
2747
    }
2748

2749
    notifyOnReplace(ctx.nodeKey, ctx.oldValue, newValue);
1✔
2750
    return true;
1✔
2751
  }
2752

2753
  @Override
2754
  public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
2755
    requireNonNull(function);
1✔
2756

2757
    BiFunction<K, V, V> remappingFunction = (key, oldValue) ->
1✔
2758
        requireNonNull(function.apply(key, oldValue));
1✔
2759
    for (K key : keySet()) {
1✔
2760
      Object lookupKey = nodeFactory.newLookupKey(key);
1✔
2761
      remap(key, lookupKey, remappingFunction, expiry(),
1✔
2762
          new ComputeContext<>(expirationTicker().read()), /* computeIfAbsent= */ false);
1✔
2763
    }
1✔
2764
  }
1✔
2765

2766
  @Override
2767
  public @Nullable V computeIfAbsent(K key,
2768
      @Var Function<? super K, ? extends @Nullable V> mappingFunction,
2769
      boolean recordStats, boolean recordLoad) {
2770
    requireNonNull(key);
1✔
2771
    requireNonNull(mappingFunction);
1✔
2772

2773
    // An optimistic fast path to avoid unnecessary locking
2774
    Node<K, V> node = data.get(nodeFactory.newLookupKey(key));
1✔
2775
    long now = expirationTicker().read();
1✔
2776
    if (node != null) {
1✔
2777
      V value = node.getValue();
1✔
2778
      if ((value != null) && !hasExpired(node, now, value)) {
1✔
2779
        if (!isComputingAsync(value)) {
1✔
2780
          tryExpireAfterRead(node, key, value, expiry(), now);
1✔
2781
          setAccessTime(node, now);
1✔
2782
        }
2783
        @Nullable V refreshed = afterRead(node, now, /* recordHit= */ recordStats);
1✔
2784
        return (refreshed == null) ? value : refreshed;
1✔
2785
      }
2786
    }
2787
    if (recordStats) {
1✔
2788
      mappingFunction = statsAware(mappingFunction, recordLoad);
1✔
2789
    }
2790
    Object keyRef = nodeFactory.newReferenceKey(key, keyReferenceQueue());
1✔
2791
    return doComputeIfAbsent(key, keyRef, mappingFunction,
1✔
2792
        new ComputeContext<>(now), recordStats);
2793
  }
2794

2795
  /** Returns the current value from a computeIfAbsent invocation. */
2796
  @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
2797
  @Nullable V doComputeIfAbsent(K key, Object keyRef,
2798
      Function<? super K, ? extends @Nullable V> mappingFunction,
2799
      ComputeContext<K, V> ctx, boolean recordStats) {
2800
    Node<K, V> node = data.compute(keyRef, (k, n) -> {
1✔
2801
      if (n == null) {
1✔
2802
        ctx.newValue = mappingFunction.apply(key);
1✔
2803
        if (ctx.newValue == null) {
1✔
2804
          discardRefresh(k);
1✔
2805
          return null;
1✔
2806
        }
2807
        ctx.now = expirationTicker().read();
1✔
2808
        ctx.newWeight = weigher.weigh(key, ctx.newValue);
1✔
2809
        var created = nodeFactory.newNode(k, ctx.newValue,
1✔
2810
            valueReferenceQueue(), ctx.newWeight, ctx.now);
1✔
2811
        long expirationTime = isComputingAsync(ctx.newValue)
1✔
2812
            ? ctx.now + ASYNC_EXPIRY
1✔
2813
            : ctx.now;
1✔
2814
        setVariableTime(created, expireAfterCreate(key, ctx.newValue, expiry(), ctx.now));
1✔
2815
        setAccessTime(created, expirationTime);
1✔
2816
        setWriteTime(created, expirationTime);
1✔
2817
        discardRefresh(k);
1✔
2818
        return created;
1✔
2819
      }
2820

2821
      synchronized (n) {
1✔
2822
        requireIsAlive(key, n);
1✔
2823
        ctx.nodeKey = n.getKey();
1✔
2824
        ctx.oldValue = n.getValue();
1✔
2825
        ctx.oldWeight = n.getWeight();
1✔
2826
        RemovalCause actualCause;
2827
        if ((ctx.nodeKey == null) || (ctx.oldValue == null)) {
1✔
2828
          actualCause = RemovalCause.COLLECTED;
1✔
2829
        } else if (hasExpired(n, ctx.now = expirationTicker().read(), ctx.oldValue)) {
1✔
2830
          actualCause = RemovalCause.EXPIRED;
1✔
2831
        } else {
2832
          return n;
1✔
2833
        }
2834

2835
        ctx.cause = actualCause;
1✔
2836
        notifyEviction(ctx.nodeKey, ctx.oldValue, actualCause);
1✔
2837

2838
        try {
2839
          ctx.newValue = mappingFunction.apply(key);
1✔
2840
          if (ctx.newValue == null) {
1✔
2841
            discardRefresh(k);
1✔
2842
            ctx.removed = n;
1✔
2843
            n.retire();
1✔
2844
            return null;
1✔
2845
          }
2846
          ctx.now = expirationTicker().read();
1✔
2847
          ctx.newWeight = weigher.weigh(key, ctx.newValue);
1✔
2848
          long varTime = expireAfterCreate(key, ctx.newValue, expiry(), ctx.now);
1✔
2849

2850
          n.setValue(ctx.newValue, valueReferenceQueue());
1✔
2851
          n.setWeight(ctx.newWeight);
1✔
2852

2853
          long expirationTime = isComputingAsync(ctx.newValue)
1✔
2854
              ? (ctx.now + ASYNC_EXPIRY) : ctx.now;
1✔
2855
          setAccessTime(n, expirationTime);
1✔
2856
          setWriteTime(n, expirationTime);
1✔
2857
          setVariableTime(n, varTime);
1✔
2858
          discardRefresh(k);
1✔
2859
          return n;
1✔
2860
        } catch (Throwable e) {
1✔
2861
          ctx.newValue = null;
1✔
2862
          discardRefresh(k);
1✔
2863
          ctx.exception = e;
1✔
2864
          ctx.removed = n;
1✔
2865
          n.retire();
1✔
2866
          return null;
1✔
2867
        }
2868
      }
2869
    });
2870

2871
    if (ctx.cause != null) {
1✔
2872
      statsCounter().recordEviction(ctx.oldWeight, ctx.cause);
1✔
2873
      notifyRemoval(ctx.nodeKey, ctx.oldValue, ctx.cause);
1✔
2874
    }
2875
    if (node == null) {
1✔
2876
      if (ctx.removed != null) {
1✔
2877
        afterWrite(new RemovalTask(ctx.removed));
1✔
2878
      }
2879
      if (ctx.exception != null) {
1✔
2880
        throw toUncheckedException(ctx.exception);
1✔
2881
      }
2882
      return null;
1✔
2883
    }
2884
    if ((ctx.oldValue != null) && (ctx.newValue == null)) {
1✔
2885
      if (!isComputingAsync(ctx.oldValue)) {
1✔
2886
        tryExpireAfterRead(node, key, ctx.oldValue, expiry(), ctx.now);
1✔
2887
        setAccessTime(node, ctx.now);
1✔
2888
      }
2889

2890
      @Nullable V refreshed = afterRead(node, ctx.now, /* recordHit= */ recordStats);
1✔
2891
      return (refreshed == null) ? ctx.oldValue : refreshed;
1✔
2892
    }
2893
    if ((ctx.oldValue == null) && (ctx.cause == null)) {
1✔
2894
      afterWrite(new AddTask(node, ctx.newWeight));
1✔
2895
    } else {
2896
      int weightedDifference = (ctx.newWeight - ctx.oldWeight);
1✔
2897
      afterWrite(new UpdateTask(node, weightedDifference));
1✔
2898
    }
2899

2900
    return ctx.newValue;
1✔
2901
  }
2902

2903
  @Override
2904
  public @Nullable V computeIfPresent(K key,
2905
      BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
2906
    requireNonNull(key);
1✔
2907
    requireNonNull(remappingFunction);
1✔
2908

2909
    // An optimistic fast path to avoid unnecessary locking
2910
    Object lookupKey = nodeFactory.newLookupKey(key);
1✔
2911
    @Nullable Node<K, V> node = data.get(lookupKey);
1✔
2912
    long now;
2913
    if (node == null) {
1✔
2914
      return null;
1✔
2915
    }
2916
    V value = node.getValue();
1✔
2917
    if ((value == null) || hasExpired(node, now = expirationTicker().read(), value)) {
1✔
2918
      scheduleDrainBuffers();
1✔
2919
      return null;
1✔
2920
    }
2921

2922
    BiFunction<? super K, ? super V, ? extends V> statsAwareRemappingFunction =
1✔
2923
        statsAware(remappingFunction, /* recordLoad= */ true, /* recordLoadFailure= */ true);
1✔
2924
    return remap(key, lookupKey, statsAwareRemappingFunction,
1✔
2925
        expiry(), new ComputeContext<>(now), /* computeIfAbsent= */ false);
1✔
2926
  }
2927

2928
  @Override
2929
  public @Nullable V compute(K key,
2930
      BiFunction<? super K, ? super V, ? extends @Nullable V> remappingFunction,
2931
      @Nullable Expiry<? super K, ? super V> expiry, boolean recordLoad, boolean recordLoadFailure,
2932
      @Nullable RemapHints hints) {
2933
    requireNonNull(key);
1✔
2934
    requireNonNull(remappingFunction);
1✔
2935

2936
    Object keyRef = nodeFactory.newReferenceKey(key, keyReferenceQueue());
1✔
2937
    BiFunction<? super K, ? super V, ? extends V> statsAwareRemappingFunction =
1✔
2938
        statsAware(remappingFunction, recordLoad, recordLoadFailure);
1✔
2939
    var ctx = new ComputeContext<K, V>(expirationTicker().read());
1✔
2940
    ctx.hints = hints;
1✔
2941
    return remap(key, keyRef, statsAwareRemappingFunction,
1✔
2942
        expiry, ctx, /* computeIfAbsent= */ true);
2943
  }
2944

2945
  @Override
2946
  public @Nullable V merge(K key, V value,
2947
      BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
2948
    requireNonNull(key);
1✔
2949
    requireNonNull(value);
1✔
2950
    requireNonNull(remappingFunction);
1✔
2951

2952
    Object keyRef = nodeFactory.newReferenceKey(key, keyReferenceQueue());
1✔
2953
    BiFunction<? super V, ? super V, ? extends V> f = statsAware(remappingFunction);
1✔
2954
    BiFunction<? super K, ? super @Nullable V, ? extends @Nullable V> mergeFunction =
1✔
2955
        (k, oldValue) -> (oldValue == null) ? value : f.apply(oldValue, value);
1✔
2956
    return remap(key, keyRef, mergeFunction, expiry(),
1✔
2957
        new ComputeContext<>(expirationTicker().read()), /* computeIfAbsent= */ true);
1✔
2958
  }
2959

2960
  /**
2961
   * Attempts to compute a mapping for the specified key and its current mapped value (or
2962
   * {@code null} if there is no current mapping).
2963
   * <p>
2964
   * An entry that has expired or been reference collected is evicted and the computation continues
2965
   * as if the entry had not been present. This method does not pre-screen and does not wrap the
2966
   * remappingFunction to be statistics aware.
2967
   *
2968
   * @param key key with which the specified value is to be associated
2969
   * @param keyRef the key to associate with or a lookup only key if not {@code computeIfAbsent}
2970
   * @param remappingFunction the function to compute a value
2971
   * @param expiry the calculator for the expiration time
2972
   * @param ctx the mutable context for passing state to and from the {@link ConcurrentHashMap}
2973
   *        compute lambda, with {@link ComputeContext#now} set to the current ticker time
2974
   * @param computeIfAbsent if an absent entry can be computed
2975
   * @return the new value associated with the specified key, or null if none
2976
   */
2977
  @SuppressWarnings({"StatementWithEmptyBody", "SynchronizationOnLocalVariableOrMethodParameter"})
2978
  @Nullable V remap(K key, Object keyRef,
2979
      BiFunction<? super K, ? super V, ? extends @Nullable V> remappingFunction,
2980
      @Nullable Expiry<? super K, ? super V> expiry,
2981
      ComputeContext<K, V> ctx, boolean computeIfAbsent) {
2982
    Node<K, V> node = data.compute(keyRef, (kr, n) -> {
1✔
2983
      if (n == null) {
1✔
2984
        if (!computeIfAbsent) {
1✔
2985
          return null;
1✔
2986
        }
2987
        ctx.newValue = remappingFunction.apply(key, null);
1✔
2988
        if (ctx.newValue == null) {
1✔
2989
          discardRefresh(kr);
1✔
2990
          return null;
1✔
2991
        }
2992
        try {
2993
          ctx.now = expirationTicker().read();
1✔
2994
          ctx.newWeight = weigher.weigh(key, ctx.newValue);
1✔
2995
          long varTime = expireAfterCreate(key, ctx.newValue, expiry, ctx.now);
1✔
2996
          var created = nodeFactory.newNode(keyRef, ctx.newValue,
1✔
2997
              valueReferenceQueue(), ctx.newWeight, ctx.now);
1✔
2998

2999
          long expirationTime = isComputingAsync(ctx.newValue)
1✔
3000
              ? ctx.now + ASYNC_EXPIRY
1✔
3001
              : ctx.now;
1✔
3002
          setAccessTime(created, expirationTime);
1✔
3003
          setWriteTime(created, expirationTime);
1✔
3004
          setVariableTime(created, varTime);
1✔
3005
          return created;
1✔
3006
        } finally {
3007
          discardRefresh(kr);
1✔
3008
        }
3009
      }
3010

3011
      synchronized (n) {
1✔
3012
        requireIsAlive(key, n);
1✔
3013
        ctx.nodeKey = n.getKey();
1✔
3014
        ctx.oldValue = n.getValue();
1✔
3015
        ctx.oldWeight = n.getWeight();
1✔
3016
        if ((ctx.nodeKey == null) || (ctx.oldValue == null)) {
1✔
3017
          ctx.cause = RemovalCause.COLLECTED;
1✔
3018
        } else if (hasExpired(n, expirationTicker().read(), ctx.oldValue)) {
1✔
3019
          ctx.cause = RemovalCause.EXPIRED;
1✔
3020
        }
3021
        if (ctx.cause != null) {
1✔
3022
          notifyEviction(ctx.nodeKey, ctx.oldValue, ctx.cause);
1✔
3023
          if (!computeIfAbsent) {
1✔
3024
            discardRefresh(kr);
1✔
3025
            ctx.removed = n;
1✔
3026
            n.retire();
1✔
3027
            return null;
1✔
3028
          }
3029
        }
3030

3031
        boolean wasEvicted = (ctx.cause != null);
1✔
3032
        try {
3033
          ctx.newValue = remappingFunction.apply(ctx.nodeKey,
1✔
3034
              (ctx.cause == null) ? ctx.oldValue : null);
1✔
3035

3036
          if (ctx.newValue == null) {
1✔
3037
            if (ctx.cause == null) {
1✔
3038
              ctx.cause = RemovalCause.EXPLICIT;
1✔
3039
            }
3040
            discardRefresh(kr);
1✔
3041
            ctx.removed = n;
1✔
3042
            n.retire();
1✔
3043
            return null;
1✔
3044
          }
3045

3046
          // If the caller flagged a same-instance return as a no-op (e.g., a refresh was rejected
3047
          // and should not touch the entry), skip the metadata updates below.
3048
          if ((ctx.hints != null) && ctx.hints.preserveTimestamps
1✔
3049
              && (ctx.newValue == ctx.oldValue) && (ctx.cause == null)) {
3050
            // Skip for query-style callers whose no-op path must leave any in-flight refresh intact
3051
            if (!ctx.hints.preserveRefresh) {
1✔
3052
              discardRefresh(kr);
1✔
3053
            }
3054
            return n;
1✔
3055
          }
3056

3057
          long varTime;
3058
          ctx.newWeight = weigher.weigh(key, ctx.newValue);
1✔
3059
          ctx.now = expirationTicker().read();
1✔
3060
          if (ctx.cause == null) {
1✔
3061
            if (ctx.newValue != ctx.oldValue) {
1✔
3062
              ctx.cause = RemovalCause.REPLACED;
1✔
3063
            }
3064
            varTime = expireAfterUpdate(n, key, ctx.newValue, expiry, ctx.now);
1✔
3065
          } else {
3066
            varTime = expireAfterCreate(key, ctx.newValue, expiry, ctx.now);
1✔
3067
          }
3068

3069
          if (ctx.newValue != ctx.oldValue) {
1✔
3070
            n.setValue(ctx.newValue, valueReferenceQueue());
1✔
3071
          }
3072
          n.setWeight(ctx.newWeight);
1✔
3073

3074
          long expirationTime = isComputingAsync(ctx.newValue)
1✔
3075
              ? ctx.now + ASYNC_EXPIRY
1✔
3076
              : ctx.now;
1✔
3077
          ctx.exceedsTolerance = exceedsWriteTimeTolerance(n, varTime, expirationTime);
1✔
3078
          if (((ctx.cause != null) && ctx.cause.wasEvicted()) || ctx.exceedsTolerance) {
1✔
3079
            setWriteTime(n, expirationTime);
1✔
3080
          }
3081
          setAccessTime(n, expirationTime);
1✔
3082
          setVariableTime(n, varTime);
1✔
3083
          discardRefresh(kr);
1✔
3084
          return n;
1✔
3085
        } catch (Throwable e) {
1✔
3086
          discardRefresh(kr);
1✔
3087
          if (!wasEvicted) {
1✔
3088
            throw e;
1✔
3089
          }
3090
          ctx.newValue = null;
1✔
3091
          ctx.exception = e;
1✔
3092
          ctx.removed = n;
1✔
3093
          n.retire();
1✔
3094
          return null;
1✔
3095
        }
3096
      }
3097
    });
3098

3099
    if (ctx.cause != null) {
1✔
3100
      if (ctx.cause == RemovalCause.REPLACED) {
1✔
3101
        requireNonNull(ctx.newValue);
1✔
3102
        notifyOnReplace(key, ctx.oldValue, ctx.newValue);
1✔
3103
      } else {
3104
        if (ctx.cause.wasEvicted()) {
1✔
3105
          statsCounter().recordEviction(ctx.oldWeight, ctx.cause);
1✔
3106
        }
3107
        notifyRemoval(ctx.nodeKey, ctx.oldValue, ctx.cause);
1✔
3108
      }
3109
    }
3110

3111
    if (ctx.removed != null) {
1✔
3112
      afterWrite(new RemovalTask(ctx.removed));
1✔
3113
    } else if (node == null) {
1✔
3114
      // absent and not computable
3115
    } else if ((ctx.hints != null) && ctx.hints.preserveTimestamps) {
1✔
3116
      // The remapping was a signaled no-op; the node was not modified
3117
    } else if ((ctx.oldValue == null) && (ctx.cause == null)) {
1✔
3118
      afterWrite(new AddTask(node, ctx.newWeight));
1✔
3119
    } else {
3120
      int weightedDifference = ctx.newWeight - ctx.oldWeight;
1✔
3121
      if (ctx.exceedsTolerance || (weightedDifference != 0)) {
1✔
3122
        afterWrite(new UpdateTask(node, weightedDifference));
1✔
3123
      } else {
3124
        afterRead(node, ctx.now, /* recordHit= */ false);
1✔
3125
        if ((ctx.cause != null) && ctx.cause.wasEvicted()) {
1✔
3126
          scheduleDrainBuffers();
1✔
3127
        }
3128
      }
3129
    }
3130

3131
    if (ctx.exception != null) {
1✔
3132
      throw toUncheckedException(ctx.exception);
1✔
3133
    }
3134
    return ctx.newValue;
1✔
3135
  }
3136

3137
  @Override
3138
  public void forEach(BiConsumer<? super K, ? super V> action) {
3139
    requireNonNull(action);
1✔
3140

3141
    for (var iterator = new EntryIterator<>(this); iterator.hasNext();) {
1✔
3142
      action.accept(iterator.key, iterator.value);
1✔
3143
      iterator.advance();
1✔
3144
    }
3145
  }
1✔
3146

3147
  @Override
3148
  public Set<K> keySet() {
3149
    Set<K> ks = keySet;
1✔
3150
    return (ks == null) ? (keySet = new KeySetView<>(this)) : ks;
1✔
3151
  }
3152

3153
  @Override
3154
  public Collection<V> values() {
3155
    Collection<V> vs = values;
1✔
3156
    return (vs == null) ? (values = new ValuesView<>(this)) : vs;
1✔
3157
  }
3158

3159
  @Override
3160
  public Set<Entry<K, V>> entrySet() {
3161
    Set<Entry<K, V>> es = entrySet;
1✔
3162
    return (es == null) ? (entrySet = new EntrySetView<>(this)) : es;
1✔
3163
  }
3164

3165
  /**
3166
   * Object equality requires reflexive, symmetric, transitive, and consistency properties. Of
3167
   * these, symmetry and consistency require further clarification for how they are upheld.
3168
   * <p>
3169
   * The <i>consistency</i> property between invocations requires that the results are the same if
3170
   * there are no modifications to the information used. Therefore, usages should expect that this
3171
   * operation may return misleading results if either the maps or the data held by them is modified
3172
   * during the execution of this method. This characteristic allows for comparing the map sizes and
3173
   * assuming stable mappings, as done by {@link java.util.AbstractMap}-based maps.
3174
   * <p>
3175
   * The <i>symmetric</i> property requires that the result is the same for all implementations of
3176
   * {@link Map#equals(Object)}. That contract is defined in terms of the stable mappings provided
3177
   * by {@link #entrySet()}, meaning that the {@link #size()} optimization forces that the count is
3178
   * consistent with the mappings when used for an equality check.
3179
   * <p>
3180
   * The cache's {@link #size()} method may include entries that have expired or have been reference
3181
   * collected, but have not yet been removed from the backing map. An iteration over the map may
3182
   * trigger the removal of these dead entries when skipped over during traversal. To ensure
3183
   * consistency and symmetry, usages should call {@link #cleanUp()} before this method while no
3184
   * other concurrent operations are being performed on this cache. This is not done implicitly by
3185
   * {@link #size()} as many usages assume it to be instantaneous and lock-free. As a postcondition
3186
   * the iteration count is verified against the prescreened {@link #size()} so that a concurrent
3187
   * maintenance pass that drops dead entries during traversal is detected and reported as not
3188
   * equal, rather than silently returning {@code true} on the surviving subset.
3189
   */
3190
  @Override
3191
  public boolean equals(@Nullable Object o) {
3192
    if (o == this) {
1✔
3193
      return true;
1✔
3194
    } else if (!(o instanceof Map)) {
1✔
3195
      return false;
1✔
3196
    }
3197

3198
    var map = (Map<?, ?>) o;
1✔
3199
    int expectedSize = size();
1✔
3200
    if (map.size() != expectedSize) {
1✔
3201
      return false;
1✔
3202
    }
3203

3204
    long now = expirationTicker().read();
1✔
3205
    @Var int count = 0;
1✔
3206
    for (var node : data.values()) {
1✔
3207
      K key = node.getKey();
1✔
3208
      V value = node.getValue();
1✔
3209
      if ((key == null) || (value == null)
1✔
3210
          || !node.isAlive() || hasExpired(node, now, value)) {
1✔
3211
        scheduleDrainBuffers();
1✔
3212
        return false;
1✔
3213
      } else {
3214
        var val = map.get(key);
1✔
3215
        if ((val == null) || ((val != value) && !val.equals(value))) {
1✔
3216
          return false;
1✔
3217
        }
3218
      }
3219
      count++;
1✔
3220
    }
1✔
3221
    return (count == expectedSize);
1✔
3222
  }
3223

3224
  @Override
3225
  public int hashCode() {
3226
    @Var int hash = 0;
1✔
3227
    @Var boolean drain = false;
1✔
3228
    long now = expirationTicker().read();
1✔
3229
    for (var node : data.values()) {
1✔
3230
      K key = node.getKey();
1✔
3231
      V value = node.getValue();
1✔
3232
      if ((key == null) || (value == null)
1✔
3233
          || !node.isAlive() || hasExpired(node, now, value)) {
1✔
3234
        drain = true;
1✔
3235
      } else {
3236
        hash += key.hashCode() ^ value.hashCode();
1✔
3237
      }
3238
    }
1✔
3239
    if (drain) {
1✔
3240
      scheduleDrainBuffers();
1✔
3241
    }
3242
    return hash;
1✔
3243
  }
3244

3245
  @Override
3246
  public String toString() {
3247
    @Var boolean drain = false;
1✔
3248
    long now = expirationTicker().read();
1✔
3249
    var result = new StringBuilder().append('{');
1✔
3250
    for (var node : data.values()) {
1✔
3251
      K key = node.getKey();
1✔
3252
      V value = node.getValue();
1✔
3253
      if ((key == null) || (value == null)
1✔
3254
          || !node.isAlive() || hasExpired(node, now, value)) {
1✔
3255
        drain = true;
1✔
3256
      } else {
3257
        if (result.length() != 1) {
1✔
3258
          result.append(',').append(' ');
1✔
3259
        }
3260
        result.append((key == this) ? "(this Map)" : key);
1✔
3261
        result.append('=');
1✔
3262
        result.append((value == this) ? "(this Map)" : value);
1✔
3263
      }
3264
    }
1✔
3265
    if (drain) {
1✔
3266
      scheduleDrainBuffers();
1✔
3267
    }
3268
    return result.append('}').toString();
1✔
3269
  }
3270

3271
  /**
3272
   * Returns the computed result from the ordered traversal of the cache entries.
3273
   *
3274
   * @param hottest the coldest or hottest iteration order
3275
   * @param transformer a function that unwraps the value
3276
   * @param mappingFunction the mapping function to compute a value
3277
   * @return the computed value
3278
   */
3279
  @SuppressWarnings("GuardedByChecker")
3280
  <T> T evictionOrder(boolean hottest, Function<@Nullable V, @Nullable V> transformer,
3281
      Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
3282
    Comparator<Node<K, V>> comparator = Comparator.comparingInt(node -> {
1✔
3283
      var keyRef = node.getKeyReferenceOrNull();
1✔
3284
      return ((keyRef == null) || !node.isAlive()) ? 0 : frequencySketch().frequency(keyRef);
1✔
3285
    });
3286
    Iterable<Node<K, V>> iterable;
3287
    if (hottest) {
1✔
3288
      iterable = () -> {
1✔
3289
        var secondary = PeekingIterator.comparing(
1✔
3290
            accessOrderProbationDeque().descendingIterator(),
1✔
3291
            accessOrderWindowDeque().descendingIterator(), comparator);
1✔
3292
        return PeekingIterator.concat(
1✔
3293
            accessOrderProtectedDeque().descendingIterator(), secondary);
1✔
3294
      };
3295
    } else {
3296
      iterable = () -> {
1✔
3297
        var primary = PeekingIterator.comparing(
1✔
3298
            accessOrderWindowDeque().iterator(), accessOrderProbationDeque().iterator(),
1✔
3299
            comparator.reversed());
1✔
3300
        return PeekingIterator.concat(primary, accessOrderProtectedDeque().iterator());
1✔
3301
      };
3302
    }
3303
    return snapshot(iterable, transformer, mappingFunction);
1✔
3304
  }
3305

3306
  /**
3307
   * Returns the computed result from the ordered traversal of the cache entries.
3308
   *
3309
   * @param oldest the youngest or oldest iteration order
3310
   * @param transformer a function that unwraps the value
3311
   * @param mappingFunction the mapping function to compute a value
3312
   * @return the computed value
3313
   */
3314
  @SuppressWarnings("GuardedByChecker")
3315
  <T> T expireAfterAccessOrder(boolean oldest, Function<@Nullable V, @Nullable V> transformer,
3316
      Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
3317
    Iterable<Node<K, V>> iterable;
3318
    if (evicts()) {
1✔
3319
      iterable = () -> {
1✔
3320
        @Var Comparator<Node<K, V>> comparator = Comparator.comparingLong(Node::getAccessTime);
1✔
3321
        PeekingIterator<Node<K, V>> first;
3322
        PeekingIterator<Node<K, V>> second;
3323
        PeekingIterator<Node<K, V>> third;
3324
        if (oldest) {
1✔
3325
          comparator = comparator.reversed();
1✔
3326
          first = accessOrderWindowDeque().iterator();
1✔
3327
          second = accessOrderProbationDeque().iterator();
1✔
3328
          third = accessOrderProtectedDeque().iterator();
1✔
3329
        } else {
3330
          first = accessOrderWindowDeque().descendingIterator();
1✔
3331
          second = accessOrderProbationDeque().descendingIterator();
1✔
3332
          third = accessOrderProtectedDeque().descendingIterator();
1✔
3333
        }
3334
        return PeekingIterator.comparing(
1✔
3335
            PeekingIterator.comparing(first, second, comparator), third, comparator);
1✔
3336
      };
3337
    } else {
3338
      iterable = oldest
1✔
3339
          ? accessOrderWindowDeque()
1✔
3340
          : accessOrderWindowDeque()::descendingIterator;
1✔
3341
    }
3342
    return snapshot(iterable, transformer, mappingFunction);
1✔
3343
  }
3344

3345
  /**
3346
   * Returns the computed result from the ordered traversal of the cache entries.
3347
   *
3348
   * @param iterable the supplier of the entries in the cache
3349
   * @param transformer a function that unwraps the value
3350
   * @param mappingFunction the mapping function to compute a value
3351
   * @return the computed value
3352
   */
3353
  <T> T snapshot(Iterable<Node<K, V>> iterable, Function<@Nullable V, @Nullable V> transformer,
3354
      Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
3355
    requireNonNull(mappingFunction);
1✔
3356
    requireNonNull(transformer);
1✔
3357
    requireNonNull(iterable);
1✔
3358

3359
    evictionLock.lock();
1✔
3360
    try {
3361
      maintenance(/* ignored */ null);
1✔
3362

3363
      // Obtain the iterator as late as possible for modification count checking
3364
      try (var stream = StreamSupport.stream(Spliterators.spliteratorUnknownSize(
1✔
3365
           iterable.iterator(), DISTINCT | ORDERED | NONNULL | IMMUTABLE), /* parallel= */ false)) {
1✔
3366
        return mappingFunction.apply(stream
1✔
3367
            .map(node -> nodeToCacheEntry(node, transformer, node.getPolicyWeight()))
1✔
3368
            .filter(Objects::nonNull));
1✔
3369
      }
3370
    } finally {
3371
      evictionLock.unlock();
1✔
3372
      rescheduleCleanUpIfIncomplete();
1✔
3373
    }
3374
  }
3375

3376
  /**
3377
   * Returns an entry for the given node if it can be used externally, else null. The weight is
3378
   * caller-supplied: snapshot callers hold evictionLock and read policyWeight (in sync with the
3379
   * drain thread); unlocked readers read weight, which may be stale for a concurrent in-place
3380
   * update (acceptable for a point-in-time CacheEntry).
3381
   */
3382
  @Nullable CacheEntry<K, V> nodeToCacheEntry(
3383
      Node<K, V> node, Function<@Nullable V, @Nullable V> transformer, int weight) {
3384
    V rawValue = node.getValue();
1✔
3385
    if (rawValue == null) {
1✔
3386
      return null;
1✔
3387
    }
3388
    V value = transformer.apply(rawValue);
1✔
3389
    K key = node.getKey();
1✔
3390
    long now;
3391
    if ((key == null) || (value == null) || !node.isAlive()
1✔
3392
        || hasExpired(node, (now = expirationTicker().read()), rawValue)) {
1✔
3393
      return null;
1✔
3394
    }
3395

3396
    @Var long expiresAfter = Long.MAX_VALUE;
1✔
3397
    if (expiresAfterAccess()) {
1✔
3398
      expiresAfter = Math.min(expiresAfter,
1✔
3399
          expiresAfterAccessNanos() - (now - node.getAccessTime()));
1✔
3400
    }
3401
    if (expiresAfterWrite()) {
1✔
3402
      expiresAfter = Math.min(expiresAfter,
1✔
3403
          expiresAfterWriteNanos() - ((now & ~1L) - (node.getWriteTime() & ~1L)));
1✔
3404
    }
3405
    if (expiresVariable()) {
1✔
3406
      expiresAfter = node.getVariableTime() - now;
1✔
3407
    }
3408

3409
    long refreshableAt = refreshAfterWrite()
1✔
3410
        ? (node.getWriteTime() & ~1L) + refreshAfterWriteNanos()
1✔
3411
        : now + Long.MAX_VALUE;
1✔
3412
    return SnapshotEntry.forEntry(key, value, now, weight, now + expiresAfter, refreshableAt);
1✔
3413
  }
3414

3415
  /** Mutable context for passing state between a lambda and the caller. */
3416
  static final class EvictContext<V> {
1✔
3417
    @Nullable RemovalCause cause;
3418
    @Nullable V value;
3419
    boolean resurrect;
3420
    boolean removed;
3421
    int oldWeight;
3422
  }
3423

3424
  /** Mutable context for passing state between a lambda and the caller. */
3425
  static final class RemoveContext<K, V> {
1✔
3426
    @Nullable K oldKey;
3427
    @Nullable V oldValue;
3428
    @Nullable Node<K, V> node;
3429
    @Nullable RemovalCause cause;
3430
    int oldWeight;
3431
  }
3432

3433
  /** Mutable context for passing state between a lambda and the caller. */
3434
  static final class ReplaceContext<K, V> {
1✔
3435
    @Nullable K nodeKey;
3436
    @Nullable V oldValue;
3437

3438
    long now;
3439
    int oldWeight;
3440
    boolean exceedsTolerance;
3441
  }
3442

3443
  /** Mutable context for passing state between a lambda and the caller. */
3444
  static final class ComputeContext<K, V> {
3445
    @Nullable K nodeKey;
3446
    @Nullable V oldValue;
3447
    @Nullable V newValue;
3448
    @Nullable Node<K, V> removed;
3449
    @Nullable RemovalCause cause;
3450
    @Nullable Throwable exception;
3451
    @Nullable RemapHints hints;
3452

3453
    long now;
3454
    int oldWeight;
3455
    int newWeight;
3456
    boolean exceedsTolerance;
3457

3458
    ComputeContext(long now) {
1✔
3459
      this.now = now;
1✔
3460
    }
1✔
3461
  }
3462

3463
  /** A function that produces an unmodifiable map up to the limit in stream order. */
3464
  static final class SizeLimiter<K, V> implements Function<Stream<CacheEntry<K, V>>, Map<K, V>> {
3465
    private final int expectedSize;
3466
    private final long limit;
3467

3468
    SizeLimiter(int expectedSize, long limit) {
1✔
3469
      requireArgument(limit >= 0);
1✔
3470
      this.expectedSize = expectedSize;
1✔
3471
      this.limit = limit;
1✔
3472
    }
1✔
3473

3474
    @Override
3475
    public Map<K, V> apply(Stream<CacheEntry<K, V>> stream) {
3476
      var map = new LinkedHashMap<K, V>(calculateHashMapCapacity(expectedSize));
1✔
3477
      stream.limit(limit).forEach(entry -> map.put(entry.getKey(), entry.getValue()));
1✔
3478
      return Collections.unmodifiableMap(map);
1✔
3479
    }
3480
  }
3481

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

3486
    private long weightedSize;
3487

3488
    WeightLimiter(long weightLimit) {
1✔
3489
      requireArgument(weightLimit >= 0);
1✔
3490
      this.weightLimit = weightLimit;
1✔
3491
    }
1✔
3492

3493
    @Override
3494
    public Map<K, V> apply(Stream<CacheEntry<K, V>> stream) {
3495
      var map = new LinkedHashMap<K, V>();
1✔
3496
      stream.takeWhile(entry -> {
1✔
3497
        weightedSize = Math.addExact(weightedSize, entry.weight());
1✔
3498
        return (weightedSize <= weightLimit);
1✔
3499
      }).forEach(entry -> map.put(entry.getKey(), entry.getValue()));
1✔
3500
      return Collections.unmodifiableMap(map);
1✔
3501
    }
3502
  }
3503

3504
  /** An adapter to safely externalize the keys. */
3505
  static final class KeySetView<K, V> extends AbstractSet<K> {
3506
    final BoundedLocalCache<K, V> cache;
3507

3508
    KeySetView(BoundedLocalCache<K, V> cache) {
1✔
3509
      this.cache = requireNonNull(cache);
1✔
3510
    }
1✔
3511

3512
    @Override
3513
    public int size() {
3514
      return cache.size();
1✔
3515
    }
3516

3517
    @Override
3518
    public void clear() {
3519
      cache.clear();
1✔
3520
    }
1✔
3521

3522
    @Override
3523
    @SuppressWarnings("SuspiciousMethodCalls")
3524
    public boolean contains(Object o) {
3525
      return cache.containsKey(o);
1✔
3526
    }
3527

3528
    @Override
3529
    public boolean removeAll(Collection<?> collection) {
3530
      requireNonNull(collection);
1✔
3531
      @Var boolean modified = false;
1✔
3532
      if (cache.collectKeys() || ((collection instanceof Set<?>) && (collection.size() > size()))) {
1✔
3533
        for (K key : this) {
1✔
3534
          if (collection.contains(key)) {
1✔
3535
            modified |= remove(key);
1✔
3536
          }
3537
        }
1✔
3538
      } else {
3539
        for (var item : collection) {
1✔
3540
          modified |= (item != null) && remove(item);
1✔
3541
        }
1✔
3542
      }
3543
      return modified;
1✔
3544
    }
3545

3546
    @Override
3547
    public boolean remove(Object o) {
3548
      return (cache.remove(o) != null);
1✔
3549
    }
3550

3551
    @Override
3552
    public boolean removeIf(Predicate<? super K> filter) {
3553
      requireNonNull(filter);
1✔
3554
      @Var boolean modified = false;
1✔
3555
      for (K key : this) {
1✔
3556
        if (filter.test(key) && remove(key)) {
1✔
3557
          modified = true;
1✔
3558
        }
3559
      }
1✔
3560
      return modified;
1✔
3561
    }
3562

3563
    @Override
3564
    public boolean retainAll(Collection<?> collection) {
3565
      requireNonNull(collection);
1✔
3566
      @Var boolean modified = false;
1✔
3567
      for (K key : this) {
1✔
3568
        if (!collection.contains(key) && remove(key)) {
1✔
3569
          modified = true;
1✔
3570
        }
3571
      }
1✔
3572
      return modified;
1✔
3573
    }
3574

3575
    @Override
3576
    public Iterator<K> iterator() {
3577
      return new KeyIterator<>(cache);
1✔
3578
    }
3579

3580
    @Override
3581
    public Spliterator<K> spliterator() {
3582
      return new KeySpliterator<>(cache);
1✔
3583
    }
3584
  }
3585

3586
  /** An adapter to safely externalize the key iterator. */
3587
  static final class KeyIterator<K, V> implements Iterator<K> {
3588
    final EntryIterator<K, V> iterator;
3589

3590
    KeyIterator(BoundedLocalCache<K, V> cache) {
1✔
3591
      this.iterator = new EntryIterator<>(cache);
1✔
3592
    }
1✔
3593

3594
    @Override
3595
    public boolean hasNext() {
3596
      return iterator.hasNext();
1✔
3597
    }
3598

3599
    @Override
3600
    public K next() {
3601
      return iterator.nextKey();
1✔
3602
    }
3603

3604
    @Override
3605
    public void remove() {
3606
      iterator.remove();
1✔
3607
    }
1✔
3608
  }
3609

3610
  /** An adapter to safely externalize the key spliterator. */
3611
  static final class KeySpliterator<K, V> implements Spliterator<K> {
3612
    final Spliterator<Node<K, V>> spliterator;
3613
    final BoundedLocalCache<K, V> cache;
3614

3615
    KeySpliterator(BoundedLocalCache<K, V> cache) {
3616
      this(cache, cache.data.values().spliterator());
1✔
3617
    }
1✔
3618

3619
    KeySpliterator(BoundedLocalCache<K, V> cache, Spliterator<Node<K, V>> spliterator) {
1✔
3620
      this.spliterator = requireNonNull(spliterator);
1✔
3621
      this.cache = requireNonNull(cache);
1✔
3622
    }
1✔
3623

3624
    @Override
3625
    public void forEachRemaining(Consumer<? super K> action) {
3626
      requireNonNull(action);
1✔
3627
      Consumer<Node<K, V>> consumer = node -> {
1✔
3628
        K key = node.getKey();
1✔
3629
        V value = node.getValue();
1✔
3630
        long now = cache.expirationTicker().read();
1✔
3631
        if ((key != null) && (value != null) && node.isAlive()
1✔
3632
            && !cache.hasExpired(node, now, value)) {
1✔
3633
          action.accept(key);
1✔
3634
        }
3635
      };
1✔
3636
      spliterator.forEachRemaining(consumer);
1✔
3637
    }
1✔
3638

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

3661
    @Override
3662
    public @Nullable Spliterator<K> trySplit() {
3663
      Spliterator<Node<K, V>> split = spliterator.trySplit();
1✔
3664
      return (split == null) ? null : new KeySpliterator<>(cache, split);
1✔
3665
    }
3666

3667
    @Override
3668
    public long estimateSize() {
3669
      return spliterator.estimateSize();
1✔
3670
    }
3671

3672
    @Override
3673
    public int characteristics() {
3674
      return DISTINCT | CONCURRENT | NONNULL;
1✔
3675
    }
3676
  }
3677

3678
  /** An adapter to safely externalize the values. */
3679
  static final class ValuesView<K, V> extends AbstractCollection<V> {
3680
    final BoundedLocalCache<K, V> cache;
3681

3682
    ValuesView(BoundedLocalCache<K, V> cache) {
1✔
3683
      this.cache = requireNonNull(cache);
1✔
3684
    }
1✔
3685

3686
    @Override
3687
    public int size() {
3688
      return cache.size();
1✔
3689
    }
3690

3691
    @Override
3692
    public void clear() {
3693
      cache.clear();
1✔
3694
    }
1✔
3695

3696
    @Override
3697
    @SuppressWarnings("SuspiciousMethodCalls")
3698
    public boolean contains(Object o) {
3699
      return cache.containsValue(o);
1✔
3700
    }
3701

3702
    @Override
3703
    public boolean removeAll(Collection<?> collection) {
3704
      requireNonNull(collection);
1✔
3705
      @Var boolean modified = false;
1✔
3706
      for (var iterator = new EntryIterator<>(cache); iterator.hasNext();) {
1✔
3707
        var key = requireNonNull(iterator.key);
1✔
3708
        var value = requireNonNull(iterator.value);
1✔
3709
        if (collection.contains(value) && cache.remove(key, value)) {
1✔
3710
          modified = true;
1✔
3711
        }
3712
        iterator.advance();
1✔
3713
      }
1✔
3714
      return modified;
1✔
3715
    }
3716

3717
    @Override
3718
    public boolean remove(@Nullable Object o) {
3719
      if (o == null) {
1✔
3720
        return false;
1✔
3721
      }
3722
      for (var iterator = new EntryIterator<>(cache); iterator.hasNext();) {
1✔
3723
        var key = requireNonNull(iterator.key);
1✔
3724
        var node = requireNonNull(iterator.next);
1✔
3725
        var value = requireNonNull(iterator.value);
1✔
3726
        if (node.containsValue(o) && cache.remove(key, value)) {
1✔
3727
          return true;
1✔
3728
        }
3729
        iterator.advance();
1✔
3730
      }
1✔
3731
      return false;
1✔
3732
    }
3733

3734
    @Override
3735
    public boolean removeIf(Predicate<? super V> filter) {
3736
      requireNonNull(filter);
1✔
3737
      @Var boolean modified = false;
1✔
3738
      for (var iterator = new EntryIterator<>(cache); iterator.hasNext();) {
1✔
3739
        var value = requireNonNull(iterator.value);
1✔
3740
        if (filter.test(value)) {
1✔
3741
          var key = requireNonNull(iterator.key);
1✔
3742
          modified |= cache.remove(key, value);
1✔
3743
        }
3744
        iterator.advance();
1✔
3745
      }
1✔
3746
      return modified;
1✔
3747
    }
3748

3749
    @Override
3750
    public boolean retainAll(Collection<?> collection) {
3751
      requireNonNull(collection);
1✔
3752
      @Var boolean modified = false;
1✔
3753
      for (var iterator = new EntryIterator<>(cache); iterator.hasNext();) {
1✔
3754
        var key = requireNonNull(iterator.key);
1✔
3755
        var value = requireNonNull(iterator.value);
1✔
3756
        if (!collection.contains(value) && cache.remove(key, value)) {
1✔
3757
          modified = true;
1✔
3758
        }
3759
        iterator.advance();
1✔
3760
      }
1✔
3761
      return modified;
1✔
3762
    }
3763

3764
    @Override
3765
    public Iterator<V> iterator() {
3766
      return new ValueIterator<>(cache);
1✔
3767
    }
3768

3769
    @Override
3770
    public Spliterator<V> spliterator() {
3771
      return new ValueSpliterator<>(cache);
1✔
3772
    }
3773
  }
3774

3775
  /** An adapter to safely externalize the value iterator. */
3776
  static final class ValueIterator<K, V> implements Iterator<V> {
3777
    final EntryIterator<K, V> iterator;
3778

3779
    ValueIterator(BoundedLocalCache<K, V> cache) {
1✔
3780
      this.iterator = new EntryIterator<>(cache);
1✔
3781
    }
1✔
3782

3783
    @Override
3784
    public boolean hasNext() {
3785
      return iterator.hasNext();
1✔
3786
    }
3787

3788
    @Override
3789
    public V next() {
3790
      return iterator.nextValue();
1✔
3791
    }
3792

3793
    @Override
3794
    public void remove() {
3795
      iterator.remove();
1✔
3796
    }
1✔
3797
  }
3798

3799
  /** An adapter to safely externalize the value spliterator. */
3800
  static final class ValueSpliterator<K, V> implements Spliterator<V> {
3801
    final Spliterator<Node<K, V>> spliterator;
3802
    final BoundedLocalCache<K, V> cache;
3803

3804
    ValueSpliterator(BoundedLocalCache<K, V> cache) {
3805
      this(cache, cache.data.values().spliterator());
1✔
3806
    }
1✔
3807

3808
    ValueSpliterator(BoundedLocalCache<K, V> cache, Spliterator<Node<K, V>> spliterator) {
1✔
3809
      this.spliterator = requireNonNull(spliterator);
1✔
3810
      this.cache = requireNonNull(cache);
1✔
3811
    }
1✔
3812

3813
    @Override
3814
    public void forEachRemaining(Consumer<? super V> action) {
3815
      requireNonNull(action);
1✔
3816
      Consumer<Node<K, V>> consumer = node -> {
1✔
3817
        K key = node.getKey();
1✔
3818
        V value = node.getValue();
1✔
3819
        long now = cache.expirationTicker().read();
1✔
3820
        if ((key != null) && (value != null) && node.isAlive()
1✔
3821
            && !cache.hasExpired(node, now, value)) {
1✔
3822
          action.accept(value);
1✔
3823
        }
3824
      };
1✔
3825
      spliterator.forEachRemaining(consumer);
1✔
3826
    }
1✔
3827

3828
    @Override
3829
    public boolean tryAdvance(Consumer<? super V> action) {
3830
      requireNonNull(action);
1✔
3831
      boolean[] advanced = { false };
1✔
3832
      Consumer<Node<K, V>> consumer = node -> {
1✔
3833
        K key = node.getKey();
1✔
3834
        V value = node.getValue();
1✔
3835
        long now = cache.expirationTicker().read();
1✔
3836
        if ((key != null) && (value != null) && node.isAlive()
1✔
3837
            && !cache.hasExpired(node, now, value)) {
1✔
3838
          action.accept(value);
1✔
3839
          advanced[0] = true;
1✔
3840
        }
3841
      };
1✔
3842
      while (spliterator.tryAdvance(consumer)) {
1✔
3843
        if (advanced[0]) {
1✔
3844
          return true;
1✔
3845
        }
3846
      }
3847
      return false;
1✔
3848
    }
3849

3850
    @Override
3851
    public @Nullable Spliterator<V> trySplit() {
3852
      Spliterator<Node<K, V>> split = spliterator.trySplit();
1✔
3853
      return (split == null) ? null : new ValueSpliterator<>(cache, split);
1✔
3854
    }
3855

3856
    @Override
3857
    public long estimateSize() {
3858
      return spliterator.estimateSize();
1✔
3859
    }
3860

3861
    @Override
3862
    public int characteristics() {
3863
      return CONCURRENT | NONNULL;
1✔
3864
    }
3865
  }
3866

3867
  /** An adapter to safely externalize the entries. */
3868
  static final class EntrySetView<K, V> extends AbstractSet<Entry<K, V>> {
3869
    final BoundedLocalCache<K, V> cache;
3870

3871
    EntrySetView(BoundedLocalCache<K, V> cache) {
1✔
3872
      this.cache = requireNonNull(cache);
1✔
3873
    }
1✔
3874

3875
    @Override
3876
    public int size() {
3877
      return cache.size();
1✔
3878
    }
3879

3880
    @Override
3881
    public void clear() {
3882
      cache.clear();
1✔
3883
    }
1✔
3884

3885
    @Override
3886
    public boolean contains(Object o) {
3887
      if (!(o instanceof Entry<?, ?>)) {
1✔
3888
        return false;
1✔
3889
      }
3890
      var entry = (Entry<?, ?>) o;
1✔
3891
      var key = entry.getKey();
1✔
3892
      var value = entry.getValue();
1✔
3893
      if ((key == null) || (value == null)) {
1✔
3894
        return false;
1✔
3895
      }
3896
      Node<K, V> node = cache.data.get(cache.nodeFactory.newLookupKey(key));
1✔
3897
      if (node == null) {
1✔
3898
        return false;
1✔
3899
      }
3900
      V nodeValue = node.getValue();
1✔
3901
      return (nodeValue != null) && node.containsValue(value)
1✔
3902
          && !cache.hasExpired(node, cache.expirationTicker().read(), nodeValue);
1✔
3903
    }
3904

3905
    @Override
3906
    public boolean removeAll(Collection<?> collection) {
3907
      requireNonNull(collection);
1✔
3908
      @Var boolean modified = false;
1✔
3909
      if (cache.collectKeys() || ((collection instanceof Set<?>) && (collection.size() > size()))) {
1✔
3910
        for (var entry : this) {
1✔
3911
          if (collection.contains(entry)) {
1✔
3912
            modified |= remove(entry);
1✔
3913
          }
3914
        }
1✔
3915
      } else {
3916
        for (var item : collection) {
1✔
3917
          modified |= (item != null) && remove(item);
1✔
3918
        }
1✔
3919
      }
3920
      return modified;
1✔
3921
    }
3922

3923
    @Override
3924
    @SuppressWarnings("SuspiciousMethodCalls")
3925
    public boolean remove(Object o) {
3926
      if (!(o instanceof Entry<?, ?>)) {
1✔
3927
        return false;
1✔
3928
      }
3929
      var entry = (Entry<?, ?>) o;
1✔
3930
      var key = entry.getKey();
1✔
3931
      return (key != null) && cache.remove(key, entry.getValue());
1✔
3932
    }
3933

3934
    @Override
3935
    public boolean removeIf(Predicate<? super Entry<K, V>> filter) {
3936
      requireNonNull(filter);
1✔
3937
      @Var boolean modified = false;
1✔
3938
      for (Entry<K, V> entry : this) {
1✔
3939
        if (filter.test(entry)) {
1✔
3940
          modified |= cache.remove(entry.getKey(), entry.getValue());
1✔
3941
        }
3942
      }
1✔
3943
      return modified;
1✔
3944
    }
3945

3946
    @Override
3947
    public boolean retainAll(Collection<?> collection) {
3948
      requireNonNull(collection);
1✔
3949
      @Var boolean modified = false;
1✔
3950
      for (var entry : this) {
1✔
3951
        if (!collection.contains(entry) && remove(entry)) {
1✔
3952
          modified = true;
1✔
3953
        }
3954
      }
1✔
3955
      return modified;
1✔
3956
    }
3957

3958
    @Override
3959
    public Iterator<Entry<K, V>> iterator() {
3960
      return new EntryIterator<>(cache);
1✔
3961
    }
3962

3963
    @Override
3964
    public Spliterator<Entry<K, V>> spliterator() {
3965
      return new EntrySpliterator<>(cache);
1✔
3966
    }
3967
  }
3968

3969
  /** An adapter to safely externalize the entry iterator. */
3970
  static final class EntryIterator<K, V> implements Iterator<Entry<K, V>> {
3971
    final BoundedLocalCache<K, V> cache;
3972
    final Iterator<Node<K, V>> iterator;
3973

3974
    @Nullable K key;
3975
    @Nullable V value;
3976
    @Nullable K removalKey;
3977
    @Nullable Node<K, V> next;
3978

3979
    EntryIterator(BoundedLocalCache<K, V> cache) {
1✔
3980
      this.iterator = cache.data.values().iterator();
1✔
3981
      this.cache = cache;
1✔
3982
    }
1✔
3983

3984
    @Override
3985
    public boolean hasNext() {
3986
      if (next != null) {
1✔
3987
        return true;
1✔
3988
      }
3989

3990
      long now = cache.expirationTicker().read();
1✔
3991
      while (iterator.hasNext()) {
1✔
3992
        next = iterator.next();
1✔
3993
        value = next.getValue();
1✔
3994
        key = next.getKey();
1✔
3995

3996
        boolean evictable = (key == null) || (value == null) || cache.hasExpired(next, now, value);
1✔
3997
        if (evictable || !next.isAlive()) {
1✔
3998
          if (evictable) {
1✔
3999
            cache.scheduleDrainBuffers();
1✔
4000
          }
4001
          advance();
1✔
4002
          continue;
1✔
4003
        }
4004
        return true;
1✔
4005
      }
4006
      return false;
1✔
4007
    }
4008

4009
    /** Invalidates the current position so that the iterator may compute the next position. */
4010
    void advance() {
4011
      value = null;
1✔
4012
      next = null;
1✔
4013
      key = null;
1✔
4014
    }
1✔
4015

4016
    K nextKey() {
4017
      if (!hasNext()) {
1✔
4018
        throw new NoSuchElementException();
1✔
4019
      }
4020
      removalKey = key;
1✔
4021
      advance();
1✔
4022
      return requireNonNull(removalKey);
1✔
4023
    }
4024

4025
    V nextValue() {
4026
      if (!hasNext()) {
1✔
4027
        throw new NoSuchElementException();
1✔
4028
      }
4029
      removalKey = key;
1✔
4030
      V val = value;
1✔
4031
      advance();
1✔
4032
      return requireNonNull(val);
1✔
4033
    }
4034

4035
    @Override
4036
    public Entry<K, V> next() {
4037
      if (!hasNext()) {
1✔
4038
        throw new NoSuchElementException();
1✔
4039
      }
4040
      var entry = new WriteThroughEntry<K, @NonNull V>(
1✔
4041
          cache, requireNonNull(key), requireNonNull(value));
1✔
4042
      removalKey = key;
1✔
4043
      advance();
1✔
4044
      return entry;
1✔
4045
    }
4046

4047
    @Override
4048
    public void remove() {
4049
      if (removalKey == null) {
1✔
4050
        throw new IllegalStateException();
1✔
4051
      }
4052
      cache.remove(removalKey);
1✔
4053
      removalKey = null;
1✔
4054
    }
1✔
4055
  }
4056

4057
  /** An adapter to safely externalize the entry spliterator. */
4058
  static final class EntrySpliterator<K, V> implements Spliterator<Entry<K, V>> {
4059
    final Spliterator<Node<K, V>> spliterator;
4060
    final BoundedLocalCache<K, V> cache;
4061

4062
    EntrySpliterator(BoundedLocalCache<K, V> cache) {
4063
      this(cache, cache.data.values().spliterator());
1✔
4064
    }
1✔
4065

4066
    EntrySpliterator(BoundedLocalCache<K, V> cache, Spliterator<Node<K, V>> spliterator) {
1✔
4067
      this.spliterator = requireNonNull(spliterator);
1✔
4068
      this.cache = requireNonNull(cache);
1✔
4069
    }
1✔
4070

4071
    @Override
4072
    public void forEachRemaining(Consumer<? super Entry<K, V>> action) {
4073
      requireNonNull(action);
1✔
4074
      Consumer<Node<K, V>> consumer = node -> {
1✔
4075
        K key = node.getKey();
1✔
4076
        V value = node.getValue();
1✔
4077
        long now = cache.expirationTicker().read();
1✔
4078
        if ((key != null) && (value != null) && node.isAlive()
1✔
4079
            && !cache.hasExpired(node, now, value)) {
1✔
4080
          action.accept(new WriteThroughEntry<>(cache, key, value));
1✔
4081
        }
4082
      };
1✔
4083
      spliterator.forEachRemaining(consumer);
1✔
4084
    }
1✔
4085

4086
    @Override
4087
    public boolean tryAdvance(Consumer<? super Entry<K, V>> action) {
4088
      requireNonNull(action);
1✔
4089
      boolean[] advanced = { false };
1✔
4090
      Consumer<Node<K, V>> consumer = node -> {
1✔
4091
        K key = node.getKey();
1✔
4092
        V value = node.getValue();
1✔
4093
        long now = cache.expirationTicker().read();
1✔
4094
        if ((key != null) && (value != null) && node.isAlive()
1✔
4095
            && !cache.hasExpired(node, now, value)) {
1✔
4096
          action.accept(new WriteThroughEntry<>(cache, key, value));
1✔
4097
          advanced[0] = true;
1✔
4098
        }
4099
      };
1✔
4100
      while (spliterator.tryAdvance(consumer)) {
1✔
4101
        if (advanced[0]) {
1✔
4102
          return true;
1✔
4103
        }
4104
      }
4105
      return false;
1✔
4106
    }
4107

4108
    @Override
4109
    public @Nullable Spliterator<Entry<K, V>> trySplit() {
4110
      Spliterator<Node<K, V>> split = spliterator.trySplit();
1✔
4111
      return (split == null) ? null : new EntrySpliterator<>(cache, split);
1✔
4112
    }
4113

4114
    @Override
4115
    public long estimateSize() {
4116
      return spliterator.estimateSize();
1✔
4117
    }
4118

4119
    @Override
4120
    public int characteristics() {
4121
      return DISTINCT | CONCURRENT | NONNULL;
1✔
4122
    }
4123
  }
4124

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

4129
    final WeakReference<BoundedLocalCache<?, ?>> reference;
4130

4131
    PerformCleanupTask(BoundedLocalCache<?, ?> cache) {
1✔
4132
      reference = new WeakReference<>(cache);
1✔
4133
    }
1✔
4134

4135
    @Override
4136
    protected boolean exec() {
4137
      try {
4138
        run();
1✔
4139
      } catch (Throwable t) {
1✔
4140
        logger.log(Level.ERROR, "Exception thrown when performing the maintenance task", t);
1✔
4141
      }
1✔
4142

4143
      // Indicates that the task has not completed to allow subsequent submissions to execute
4144
      return false;
1✔
4145
    }
4146

4147
    @Override
4148
    public void run() {
4149
      BoundedLocalCache<?, ?> cache = reference.get();
1✔
4150
      if (cache != null) {
1✔
4151
        cache.performCleanUp(/* ignored */ null);
1✔
4152
      }
4153
    }
1✔
4154

4155
    /**
4156
     * This method cannot be ignored due to being final, so a hostile user supplied Executor could
4157
     * forcibly complete the task and halt future executions. There are easier ways to intentionally
4158
     * harm a system, so this is assumed to not happen in practice.
4159
     */
4160
    // public final void quietlyComplete() {}
4161

4162
    @Override public void complete(@Nullable Void value) {}
1✔
4163
    @Override public void setRawResult(@Nullable Void value) {}
1✔
4164
    @Override public @Nullable Void getRawResult() { return null; }
1✔
4165
    @Override public void completeExceptionally(@Nullable Throwable t) {}
1✔
4166
    @Override public boolean cancel(boolean mayInterruptIfRunning) { return false; }
1✔
4167
  }
4168

4169
  /** Creates a serialization proxy based on the common configuration shared by all cache types. */
4170
  static <K, V> SerializationProxy<K, V> makeSerializationProxy(BoundedLocalCache<?, ?> cache) {
4171
    var proxy = new SerializationProxy<K, V>();
1✔
4172
    proxy.weakKeys = cache.collectKeys();
1✔
4173
    proxy.weakValues = cache.nodeFactory.weakValues();
1✔
4174
    proxy.softValues = cache.nodeFactory.softValues();
1✔
4175
    proxy.isRecordingStats = cache.isRecordingStats();
1✔
4176
    proxy.evictionListener = cache.evictionListener;
1✔
4177
    proxy.removalListener = cache.removalListener();
1✔
4178
    proxy.ticker = cache.expirationTicker();
1✔
4179
    if (cache.expiresAfterAccess()) {
1✔
4180
      proxy.expiresAfterAccessNanos = cache.expiresAfterAccessNanos();
1✔
4181
    }
4182
    if (cache.expiresAfterWrite()) {
1✔
4183
      proxy.expiresAfterWriteNanos = cache.expiresAfterWriteNanos();
1✔
4184
    }
4185
    if (cache.expiresVariable()) {
1✔
4186
      proxy.expiry = cache.expiry();
1✔
4187
    }
4188
    if (cache.refreshAfterWrite()) {
1✔
4189
      proxy.refreshAfterWriteNanos = cache.refreshAfterWriteNanos();
1✔
4190
    }
4191
    if (cache.evicts()) {
1✔
4192
      if (cache.isWeighted) {
1✔
4193
        proxy.weigher = cache.weigher;
1✔
4194
        proxy.maximumWeight = cache.maximum();
1✔
4195
      } else {
4196
        proxy.maximumSize = cache.maximum();
1✔
4197
      }
4198
    }
4199
    proxy.cacheLoader = cache.cacheLoader;
1✔
4200
    proxy.async = cache.isAsync;
1✔
4201
    return proxy;
1✔
4202
  }
4203

4204
  /* --------------- Manual Cache --------------- */
4205

4206
  static class BoundedLocalManualCache<K, V> implements LocalManualCache<K, V>, Serializable {
4207
    private static final long serialVersionUID = 1;
4208

4209
    final BoundedLocalCache<K, V> cache;
4210

4211
    @Nullable Policy<K, V> policy;
4212

4213
    BoundedLocalManualCache(Caffeine<K, V> builder) {
4214
      this(builder, null);
1✔
4215
    }
1✔
4216

4217
    BoundedLocalManualCache(Caffeine<K, V> builder, @Nullable CacheLoader<? super K, V> loader) {
1✔
4218
      cache = LocalCacheFactory.newBoundedLocalCache(builder, loader, /* isAsync= */ false);
1✔
4219
    }
1✔
4220

4221
    @Override
4222
    public final BoundedLocalCache<K, V> cache() {
4223
      return cache;
1✔
4224
    }
4225

4226
    @Override
4227
    public final Policy<K, V> policy() {
4228
      if (policy == null) {
1✔
4229
        Function<@Nullable V, @Nullable V> identity = v -> v;
1✔
4230
        policy = new BoundedPolicy<>(cache, identity, cache.isWeighted);
1✔
4231
      }
4232
      return policy;
1✔
4233
    }
4234

4235
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
4236
      throw new InvalidObjectException("Proxy required");
1✔
4237
    }
4238

4239
    private Object writeReplace() {
4240
      return makeSerializationProxy(cache);
1✔
4241
    }
4242
  }
4243

4244
  @SuppressWarnings({"NullableOptional",
4245
    "OptionalAssignedToNull", "OptionalUsedAsFieldOrParameterType"})
4246
  static final class BoundedPolicy<K, V> implements Policy<K, V> {
4247
    final Function<@Nullable V, @Nullable V> transformer;
4248
    final BoundedLocalCache<K, V> cache;
4249
    final boolean isWeighted;
4250

4251
    @Nullable Optional<Eviction<K, V>> eviction;
4252
    @Nullable Optional<FixedRefresh<K, V>> refreshes;
4253
    @Nullable Optional<FixedExpiration<K, V>> afterWrite;
4254
    @Nullable Optional<FixedExpiration<K, V>> afterAccess;
4255
    @Nullable Optional<VarExpiration<K, V>> variable;
4256

4257
    BoundedPolicy(BoundedLocalCache<K, V> cache,
4258
        Function<@Nullable V, @Nullable V> transformer, boolean isWeighted) {
1✔
4259
      this.transformer = transformer;
1✔
4260
      this.isWeighted = isWeighted;
1✔
4261
      this.cache = cache;
1✔
4262
    }
1✔
4263

4264
    @Override public boolean isRecordingStats() {
4265
      return cache.isRecordingStats();
1✔
4266
    }
4267
    @Override public @Nullable V getIfPresentQuietly(K key) {
4268
      return transformer.apply(cache.getIfPresentQuietly(key));
1✔
4269
    }
4270
    @SuppressWarnings("GuardedByChecker")
4271
    @Override public @Nullable CacheEntry<K, V> getEntryIfPresentQuietly(K key) {
4272
      Node<K, V> node = cache.data.get(cache.nodeFactory.newLookupKey(key));
1✔
4273
      return (node == null) ? null : cache.nodeToCacheEntry(node, transformer, node.getWeight());
1✔
4274
    }
4275
    @SuppressWarnings("Java9CollectionFactory")
4276
    @Override public Map<K, CompletableFuture<V>> refreshes() {
4277
      var refreshes = cache.refreshes;
1✔
4278
      if ((refreshes == null) || refreshes.isEmpty()) {
1✔
4279
        @SuppressWarnings({"ImmutableMapOf", "RedundantUnmodifiable"})
4280
        Map<K, CompletableFuture<V>> emptyMap = Collections.unmodifiableMap(Collections.emptyMap());
1✔
4281
        return emptyMap;
1✔
4282
      } else if (cache.collectKeys()) {
1✔
4283
        var inFlight = new IdentityHashMap<K, CompletableFuture<V>>(refreshes.size());
1✔
4284
        for (var entry : refreshes.entrySet()) {
1✔
4285
          @SuppressWarnings("unchecked")
4286
          @Nullable K key = ((InternalReference<K>) entry.getKey()).get();
1✔
4287
          @SuppressWarnings("unchecked")
4288
          var future = (CompletableFuture<V>) entry.getValue();
1✔
4289
          if (key != null) {
1✔
4290
            inFlight.put(key, future);
1✔
4291
          }
4292
        }
1✔
4293
        return Collections.unmodifiableMap(inFlight);
1✔
4294
      }
4295
      @SuppressWarnings("unchecked")
4296
      var castedRefreshes = (Map<K, CompletableFuture<V>>) (Object) refreshes;
1✔
4297
      return Collections.unmodifiableMap(new HashMap<>(castedRefreshes));
1✔
4298
    }
4299
    @Override public Optional<Eviction<K, V>> eviction() {
4300
      return cache.evicts()
1✔
4301
          ? (eviction == null) ? (eviction = Optional.of(new BoundedEviction())) : eviction
1✔
4302
          : Optional.empty();
1✔
4303
    }
4304
    @Override public Optional<FixedExpiration<K, V>> expireAfterAccess() {
4305
      if (!cache.expiresAfterAccess()) {
1✔
4306
        return Optional.empty();
1✔
4307
      }
4308
      return (afterAccess == null)
1✔
4309
          ? (afterAccess = Optional.of(new BoundedExpireAfterAccess()))
1✔
4310
          : afterAccess;
1✔
4311
    }
4312
    @Override public Optional<FixedExpiration<K, V>> expireAfterWrite() {
4313
      if (!cache.expiresAfterWrite()) {
1✔
4314
        return Optional.empty();
1✔
4315
      }
4316
      return (afterWrite == null)
1✔
4317
          ? (afterWrite = Optional.of(new BoundedExpireAfterWrite()))
1✔
4318
          : afterWrite;
1✔
4319
    }
4320
    @Override public Optional<VarExpiration<K, V>> expireVariably() {
4321
      if (!cache.expiresVariable()) {
1✔
4322
        return Optional.empty();
1✔
4323
      }
4324
      return (variable == null)
1✔
4325
          ? (variable = Optional.of(new BoundedVarExpiration()))
1✔
4326
          : variable;
1✔
4327
    }
4328
    @Override public Optional<FixedRefresh<K, V>> refreshAfterWrite() {
4329
      if (!cache.refreshAfterWrite()) {
1✔
4330
        return Optional.empty();
1✔
4331
      }
4332
      return (refreshes == null)
1✔
4333
          ? (refreshes = Optional.of(new BoundedRefreshAfterWrite()))
1✔
4334
          : refreshes;
1✔
4335
    }
4336

4337
    final class BoundedEviction implements Eviction<K, V> {
1✔
4338
      @Override public boolean isWeighted() {
4339
        return isWeighted;
1✔
4340
      }
4341
      @Override public OptionalInt weightOf(K key) {
4342
        requireNonNull(key);
1✔
4343
        if (!isWeighted) {
1✔
4344
          return OptionalInt.empty();
1✔
4345
        }
4346
        Node<K, V> node = cache.data.get(cache.nodeFactory.newLookupKey(key));
1✔
4347
        if (node == null) {
1✔
4348
          return OptionalInt.empty();
1✔
4349
        }
4350
        V value = node.getValue();
1✔
4351
        if ((value == null) || cache.hasExpired(node, cache.expirationTicker().read(), value)) {
1✔
4352
          return OptionalInt.empty();
1✔
4353
        }
4354
        synchronized (node) {
1✔
4355
          return node.isAlive() ? OptionalInt.of(node.getWeight()) : OptionalInt.empty();
1✔
4356
        }
4357
      }
4358
      @Override public OptionalLong weightedSize() {
4359
        return isWeighted
1✔
4360
            ? OptionalLong.of(Math.max(0, cache.weightedSizeAcquire()))
1✔
4361
            : OptionalLong.empty();
1✔
4362
      }
4363
      @Override public long getMaximum() {
4364
        return cache.maximumAcquire();
1✔
4365
      }
4366
      @Override public void setMaximum(long maximum) {
4367
        cache.evictionLock.lock();
1✔
4368
        try {
4369
          cache.setMaximumSize(maximum);
1✔
4370
          cache.maintenance(/* ignored */ null);
1✔
4371
        } finally {
4372
          cache.evictionLock.unlock();
1✔
4373
          cache.rescheduleCleanUpIfIncomplete();
1✔
4374
        }
4375
      }
1✔
4376
      @Override public Map<K, V> coldest(int limit) {
4377
        int expectedSize = Math.min(limit, cache.size());
1✔
4378
        var limiter = new SizeLimiter<K, V>(expectedSize, limit);
1✔
4379
        return cache.evictionOrder(/* hottest= */ false, transformer, limiter);
1✔
4380
      }
4381
      @Override public Map<K, V> coldestWeighted(long weightLimit) {
4382
        var limiter = isWeighted()
1✔
4383
            ? new WeightLimiter<K, V>(weightLimit)
1✔
4384
            : new SizeLimiter<K, V>((int) Math.min(weightLimit, cache.size()), weightLimit);
1✔
4385
        return cache.evictionOrder(/* hottest= */ false, transformer, limiter);
1✔
4386
      }
4387
      @Override
4388
      public <T> T coldest(Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4389
        requireNonNull(mappingFunction);
1✔
4390
        return cache.evictionOrder(/* hottest= */ false, transformer, mappingFunction);
1✔
4391
      }
4392
      @Override public Map<K, V> hottest(int limit) {
4393
        int expectedSize = Math.min(limit, cache.size());
1✔
4394
        var limiter = new SizeLimiter<K, V>(expectedSize, limit);
1✔
4395
        return cache.evictionOrder(/* hottest= */ true, transformer, limiter);
1✔
4396
      }
4397
      @Override public Map<K, V> hottestWeighted(long weightLimit) {
4398
        var limiter = isWeighted()
1✔
4399
            ? new WeightLimiter<K, V>(weightLimit)
1✔
4400
            : new SizeLimiter<K, V>((int) Math.min(weightLimit, cache.size()), weightLimit);
1✔
4401
        return cache.evictionOrder(/* hottest= */ true, transformer, limiter);
1✔
4402
      }
4403
      @Override
4404
      public <T> T hottest(Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4405
        requireNonNull(mappingFunction);
1✔
4406
        return cache.evictionOrder(/* hottest= */ true, transformer, mappingFunction);
1✔
4407
      }
4408
    }
4409

4410
    @SuppressWarnings("PreferJavaTimeOverload")
4411
    final class BoundedExpireAfterAccess implements FixedExpiration<K, V> {
1✔
4412
      @Override public OptionalLong ageOf(K key, TimeUnit unit) {
4413
        requireNonNull(key);
1✔
4414
        requireNonNull(unit);
1✔
4415
        Object lookupKey = cache.nodeFactory.newLookupKey(key);
1✔
4416
        Node<K, V> node = cache.data.get(lookupKey);
1✔
4417
        if (node == null) {
1✔
4418
          return OptionalLong.empty();
1✔
4419
        }
4420
        V value = node.getValue();
1✔
4421
        if (value == null) {
1✔
4422
          return OptionalLong.empty();
1✔
4423
        }
4424
        long now = cache.expirationTicker().read();
1✔
4425
        return cache.hasExpired(node, now, value)
1✔
4426
            ? OptionalLong.empty()
1✔
4427
            : OptionalLong.of(unit.convert(now - node.getAccessTime(), TimeUnit.NANOSECONDS));
1✔
4428
      }
4429
      @Override public long getExpiresAfter(TimeUnit unit) {
4430
        return unit.convert(cache.expiresAfterAccessNanos(), TimeUnit.NANOSECONDS);
1✔
4431
      }
4432
      @Override public void setExpiresAfter(long duration, TimeUnit unit) {
4433
        requireArgument(duration >= 0);
1✔
4434
        cache.setExpiresAfterAccessNanos(unit.toNanos(duration));
1✔
4435
        cache.scheduleAfterWrite();
1✔
4436
      }
1✔
4437
      @Override public Map<K, V> oldest(int limit) {
4438
        return oldest(new SizeLimiter<>(Math.min(limit, cache.size()), limit));
1✔
4439
      }
4440
      @Override public <T> T oldest(Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4441
        return cache.expireAfterAccessOrder(/* oldest= */ true, transformer, mappingFunction);
1✔
4442
      }
4443
      @Override public Map<K, V> youngest(int limit) {
4444
        return youngest(new SizeLimiter<>(Math.min(limit, cache.size()), limit));
1✔
4445
      }
4446
      @Override public <T> T youngest(Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4447
        return cache.expireAfterAccessOrder(/* oldest= */ false, transformer, mappingFunction);
1✔
4448
      }
4449
    }
4450

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

4496
    @SuppressWarnings("PreferJavaTimeOverload")
4497
    final class BoundedVarExpiration implements VarExpiration<K, V> {
1✔
4498
      @Override public OptionalLong getExpiresAfter(K key, TimeUnit unit) {
4499
        requireNonNull(key);
1✔
4500
        requireNonNull(unit);
1✔
4501
        Object lookupKey = cache.nodeFactory.newLookupKey(key);
1✔
4502
        Node<K, V> node = cache.data.get(lookupKey);
1✔
4503
        if (node == null) {
1✔
4504
          return OptionalLong.empty();
1✔
4505
        }
4506
        V value = node.getValue();
1✔
4507
        if (value == null) {
1✔
4508
          return OptionalLong.empty();
1✔
4509
        }
4510
        long now = cache.expirationTicker().read();
1✔
4511
        return cache.hasExpired(node, now, value)
1✔
4512
            ? OptionalLong.empty()
1✔
4513
            : OptionalLong.of(unit.convert(node.getVariableTime() - now, TimeUnit.NANOSECONDS));
1✔
4514
      }
4515
      @Override public void setExpiresAfter(K key, long duration, TimeUnit unit) {
4516
        requireNonNull(key);
1✔
4517
        requireNonNull(unit);
1✔
4518
        requireArgument(duration >= 0);
1✔
4519
        Object lookupKey = cache.nodeFactory.newLookupKey(key);
1✔
4520
        Node<K, V> node = cache.data.get(lookupKey);
1✔
4521
        if (node != null) {
1✔
4522
          long now;
4523
          long durationNanos = TimeUnit.NANOSECONDS.convert(duration, unit);
1✔
4524
          synchronized (node) {
1✔
4525
            now = cache.expirationTicker().read();
1✔
4526
            V value = node.getValue();
1✔
4527
            if ((value == null) || cache.isComputingAsync(value)
1✔
4528
                || cache.hasExpired(node, now, value)) {
1✔
4529
              return;
1✔
4530
            }
4531
            node.setVariableTime(now + Math.min(durationNanos, MAXIMUM_EXPIRY));
1✔
4532
          }
1✔
4533
          cache.afterRead(node, now, /* recordHit= */ false);
1✔
4534
        }
4535
      }
1✔
4536
      @Override public @Nullable V put(K key, V value, long duration, TimeUnit unit) {
4537
        requireNonNull(unit);
1✔
4538
        requireNonNull(value);
1✔
4539
        requireArgument(duration >= 0);
1✔
4540
        return cache.isAsync
1✔
4541
            ? putAsync(key, value, duration, unit)
1✔
4542
            : putSync(key, value, duration, unit, /* onlyIfAbsent= */ false);
1✔
4543
      }
4544
      @Override public @Nullable V putIfAbsent(K key, V value, long duration, TimeUnit unit) {
4545
        requireNonNull(unit);
1✔
4546
        requireNonNull(value);
1✔
4547
        requireArgument(duration >= 0);
1✔
4548
        return cache.isAsync
1✔
4549
            ? putIfAbsentAsync(key, value, duration, unit)
1✔
4550
            : putSync(key, value, duration, unit, /* onlyIfAbsent= */ true);
1✔
4551
      }
4552
      @Nullable V putSync(K key, V value, long duration, TimeUnit unit, boolean onlyIfAbsent) {
4553
        var expiry = new FixedExpireAfterWrite<K, V>(duration, unit);
1✔
4554
        return cache.put(key, value, expiry, onlyIfAbsent);
1✔
4555
      }
4556
      @SuppressWarnings("unchecked")
4557
      @Nullable V putIfAbsentAsync(K key, V value, long duration, TimeUnit unit) {
4558
        // Keep in sync with LocalAsyncCache.AsMapView#putIfAbsent(key, value)
4559
        var expiry = (Expiry<K, V>) new AsyncExpiry<>(new FixedExpireAfterWrite<>(duration, unit));
1✔
4560
        var asyncValue = (V) CompletableFuture.completedFuture(value);
1✔
4561

4562
        for (;;) {
4563
          var priorFuture = (CompletableFuture<V>) cache.getIfPresent(
1✔
4564
              key, /* recordStats= */ false);
4565
          if (priorFuture != null) {
1✔
4566
            if (!priorFuture.isDone()) {
1✔
4567
              Async.getWhenSuccessful(priorFuture);
1✔
4568
              continue;
1✔
4569
            }
4570

4571
            V prior = Async.getWhenSuccessful(priorFuture);
1✔
4572
            if (prior != null) {
1✔
4573
              return prior;
1✔
4574
            }
4575
          }
4576

4577
          boolean[] added = { false };
1✔
4578
          var hints = new LocalCache.RemapHints();
1✔
4579
          var computed = (CompletableFuture<V>) cache.compute(key, (K k, @Nullable V oldValue) -> {
1✔
4580
            var oldValueFuture = (CompletableFuture<V>) oldValue;
1✔
4581
            added[0] = (oldValueFuture == null)
1✔
4582
                || (oldValueFuture.isDone() && (Async.getIfReady(oldValueFuture) == null));
1✔
4583
            if (added[0]) {
1✔
4584
              return asyncValue;
1✔
4585
            }
4586
            hints.preserveTimestamps = true;
1✔
4587
            hints.preserveRefresh = true;
1✔
4588
            return oldValue;
1✔
4589
          }, expiry, /* recordLoad= */ false, /* recordLoadFailure= */ false, hints);
4590

4591
          if (added[0]) {
1✔
4592
            return null;
1✔
4593
          } else {
4594
            V prior = Async.getWhenSuccessful(computed);
1✔
4595
            if (prior != null) {
1✔
4596
              return prior;
1✔
4597
            }
4598
          }
4599
        }
1✔
4600
      }
4601
      @SuppressWarnings("unchecked")
4602
      @Nullable V putAsync(K key, V value, long duration, TimeUnit unit) {
4603
        var expiry = (Expiry<K, V>) new AsyncExpiry<>(new FixedExpireAfterWrite<>(duration, unit));
1✔
4604
        var asyncValue = (V) CompletableFuture.completedFuture(value);
1✔
4605

4606
        var oldValueFuture = (CompletableFuture<V>) cache.put(
1✔
4607
            key, asyncValue, expiry, /* onlyIfAbsent= */ false);
4608
        return Async.getWhenSuccessful(oldValueFuture);
1✔
4609
      }
4610
      @Override public @Nullable V compute(K key,
4611
          BiFunction<? super K, ? super V, ? extends V> remappingFunction,
4612
          Duration duration) {
4613
        requireNonNull(key);
1✔
4614
        requireNonNull(duration);
1✔
4615
        requireNonNull(remappingFunction);
1✔
4616
        requireArgument(!duration.isNegative(), "duration cannot be negative: %s", duration);
1✔
4617
        var expiry = new FixedExpireAfterWrite<K, V>(
1✔
4618
            toNanosSaturated(duration), TimeUnit.NANOSECONDS);
1✔
4619

4620
        return cache.isAsync
1✔
4621
            ? computeAsync(key, remappingFunction, expiry)
1✔
4622
            : cache.compute(key, remappingFunction, expiry,
1✔
4623
                /* recordLoad= */ true, /* recordLoadFailure= */ true);
4624
      }
4625
      @Nullable V computeAsync(K key,
4626
          BiFunction<? super K, ? super V, ? extends V> remappingFunction,
4627
          Expiry<? super K, ? super V> expiry) {
4628
        // Keep in sync with LocalAsyncCache.AsMapView#compute(key, remappingFunction)
4629
        @SuppressWarnings("unchecked")
4630
        var delegate = (LocalCache<K, CompletableFuture<V>>) cache;
1✔
4631

4632
        @SuppressWarnings({"rawtypes", "unchecked", "Varifier"})
4633
        @Nullable V[] newValue = (@Nullable V[]) new Object[1];
1✔
4634
        for (;;) {
4635
          Async.getWhenSuccessful(delegate.getIfPresentQuietly(key));
1✔
4636

4637
          var hints = new LocalCache.RemapHints();
1✔
4638
          CompletableFuture<V> valueFuture = delegate.compute(
1✔
4639
              key, (K k, @Nullable CompletableFuture<V> oldValueFuture) -> {
4640
                if ((oldValueFuture != null) && !oldValueFuture.isDone()) {
1✔
4641
                  hints.preserveTimestamps = true;
1✔
4642
                  hints.preserveRefresh = true;
1✔
4643
                  return oldValueFuture;
1✔
4644
                }
4645

4646
                V oldValue = Async.getIfReady(oldValueFuture);
1✔
4647
                BiFunction<? super K, ? super V, ? extends @Nullable V> function =
1✔
4648
                    delegate.statsAware(remappingFunction,
1✔
4649
                        /* recordLoad= */ true, /* recordLoadFailure= */ true);
4650
                newValue[0] = function.apply(key, oldValue);
1✔
4651
                return (newValue[0] == null) ? null
1✔
4652
                    : CompletableFuture.completedFuture(newValue[0]);
1✔
4653
              }, new AsyncExpiry<>(expiry), /* recordLoad= */ false,
4654
              /* recordLoadFailure= */ false, hints);
4655

4656
          if (newValue[0] != null) {
1✔
4657
            return newValue[0];
1✔
4658
          } else if (valueFuture == null) {
1✔
4659
            return null;
1✔
4660
          }
4661
        }
1✔
4662
      }
4663
      @Override public Map<K, V> oldest(int limit) {
4664
        return oldest(new SizeLimiter<>(Math.min(limit, cache.size()), limit));
1✔
4665
      }
4666
      @Override public <T> T oldest(Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4667
        return cache.snapshot(cache.timerWheel(), transformer, mappingFunction);
1✔
4668
      }
4669
      @Override public Map<K, V> youngest(int limit) {
4670
        return youngest(new SizeLimiter<>(Math.min(limit, cache.size()), limit));
1✔
4671
      }
4672
      @Override public <T> T youngest(Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4673
        return cache.snapshot(cache.timerWheel()::descendingIterator, transformer, mappingFunction);
1✔
4674
      }
4675
    }
4676

4677
    static final class FixedExpireAfterWrite<K, V> implements Expiry<K, V> {
4678
      final long duration;
4679
      final TimeUnit unit;
4680

4681
      FixedExpireAfterWrite(long duration, TimeUnit unit) {
1✔
4682
        this.duration = duration;
1✔
4683
        this.unit = unit;
1✔
4684
      }
1✔
4685
      @Override public long expireAfterCreate(K key, V value, long currentTime) {
4686
        return unit.toNanos(duration);
1✔
4687
      }
4688
      @Override public long expireAfterUpdate(
4689
          K key, V value, long currentTime, long currentDuration) {
4690
        return unit.toNanos(duration);
1✔
4691
      }
4692
      @CanIgnoreReturnValue
4693
      @Override public long expireAfterRead(
4694
          K key, V value, long currentTime, long currentDuration) {
4695
        return currentDuration;
1✔
4696
      }
4697
    }
4698

4699
    @SuppressWarnings("PreferJavaTimeOverload")
4700
    final class BoundedRefreshAfterWrite implements FixedRefresh<K, V> {
1✔
4701
      @Override public OptionalLong ageOf(K key, TimeUnit unit) {
4702
        requireNonNull(key);
1✔
4703
        requireNonNull(unit);
1✔
4704
        Object lookupKey = cache.nodeFactory.newLookupKey(key);
1✔
4705
        Node<K, V> node = cache.data.get(lookupKey);
1✔
4706
        if (node == null) {
1✔
4707
          return OptionalLong.empty();
1✔
4708
        }
4709
        V value = node.getValue();
1✔
4710
        if (value == null) {
1✔
4711
          return OptionalLong.empty();
1✔
4712
        }
4713
        long now = cache.expirationTicker().read();
1✔
4714
        return cache.hasExpired(node, now, value)
1✔
4715
            ? OptionalLong.empty()
1✔
4716
            : OptionalLong.of(unit.convert(
1✔
4717
                (now & ~1L) - (node.getWriteTime() & ~1L), TimeUnit.NANOSECONDS));
1✔
4718
      }
4719
      @Override public long getRefreshesAfter(TimeUnit unit) {
4720
        return unit.convert(cache.refreshAfterWriteNanos(), TimeUnit.NANOSECONDS);
1✔
4721
      }
4722
      @Override public void setRefreshesAfter(long duration, TimeUnit unit) {
4723
        requireNonNull(unit);
1✔
4724
        requireArgument(duration > 0, "duration must be positive: %s %s", duration, unit);
1✔
4725
        cache.setRefreshAfterWriteNanos(unit.toNanos(duration));
1✔
4726
        cache.scheduleAfterWrite();
1✔
4727
      }
1✔
4728
    }
4729
  }
4730

4731
  /* --------------- Loading Cache --------------- */
4732

4733
  static final class BoundedLocalLoadingCache<K, V>
4734
      extends BoundedLocalManualCache<K, V> implements LocalLoadingCache<K, V> {
4735
    private static final long serialVersionUID = 1;
4736

4737
    final Function<K, @Nullable V> mappingFunction;
4738
    final @Nullable Function<Set<? extends K>, Map<K, V>> bulkMappingFunction;
4739

4740
    BoundedLocalLoadingCache(Caffeine<K, V> builder, CacheLoader<? super K, V> loader) {
4741
      super(builder, loader);
1✔
4742
      requireNonNull(loader);
1✔
4743
      mappingFunction = newMappingFunction(loader);
1✔
4744
      bulkMappingFunction = newBulkMappingFunction(loader);
1✔
4745
    }
1✔
4746

4747
    @Override
4748
    @SuppressWarnings({"DataFlowIssue", "NullAway"})
4749
    public AsyncCacheLoader<? super K, V> cacheLoader() {
4750
      return cache.cacheLoader;
1✔
4751
    }
4752

4753
    @Override
4754
    public Function<K, @Nullable V> mappingFunction() {
4755
      return mappingFunction;
1✔
4756
    }
4757

4758
    @Override
4759
    public @Nullable Function<Set<? extends K>, Map<K, V>> bulkMappingFunction() {
4760
      return bulkMappingFunction;
1✔
4761
    }
4762

4763
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
4764
      throw new InvalidObjectException("Proxy required");
1✔
4765
    }
4766

4767
    private Object writeReplace() {
4768
      return makeSerializationProxy(cache);
1✔
4769
    }
4770
  }
4771

4772
  /* --------------- Async Cache --------------- */
4773

4774
  static final class BoundedLocalAsyncCache<K, V> implements LocalAsyncCache<K, V>, Serializable {
4775
    private static final long serialVersionUID = 1;
4776

4777
    final BoundedLocalCache<K, CompletableFuture<V>> cache;
4778
    final boolean isWeighted;
4779

4780
    @Nullable ConcurrentMap<K, CompletableFuture<V>> mapView;
4781
    @Nullable CacheView<K, V> cacheView;
4782
    @Nullable Policy<K, V> policy;
4783

4784
    @SuppressWarnings("unchecked")
4785
    BoundedLocalAsyncCache(Caffeine<K, V> builder) {
1✔
4786
      cache = (BoundedLocalCache<K, CompletableFuture<V>>) LocalCacheFactory
1✔
4787
          .newBoundedLocalCache(builder, /* cacheLoader= */ null, /* isAsync= */ true);
1✔
4788
      isWeighted = builder.isWeighted();
1✔
4789
    }
1✔
4790

4791
    @Override
4792
    public BoundedLocalCache<K, CompletableFuture<V>> cache() {
4793
      return cache;
1✔
4794
    }
4795

4796
    @Override
4797
    public ConcurrentMap<K, CompletableFuture<V>> asMap() {
4798
      return (mapView == null) ? (mapView = new AsyncAsMapView<>(this)) : mapView;
1✔
4799
    }
4800

4801
    @Override
4802
    public Cache<K, V> synchronous() {
4803
      return (cacheView == null) ? (cacheView = new CacheView<>(this)) : cacheView;
1✔
4804
    }
4805

4806
    @Override
4807
    public Policy<K, V> policy() {
4808
      if (policy == null) {
1✔
4809
        @SuppressWarnings("unchecked")
4810
        var castCache = (BoundedLocalCache<K, V>) cache;
1✔
4811
        Function<CompletableFuture<V>, @Nullable V> transformer = Async::getIfReady;
1✔
4812
        @SuppressWarnings("unchecked")
4813
        var castTransformer = (Function<@Nullable V, @Nullable V>) transformer;
1✔
4814
        policy = new BoundedPolicy<>(castCache, castTransformer, isWeighted);
1✔
4815
      }
4816
      return policy;
1✔
4817
    }
4818

4819
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
4820
      throw new InvalidObjectException("Proxy required");
1✔
4821
    }
4822

4823
    private Object writeReplace() {
4824
      return makeSerializationProxy(cache);
1✔
4825
    }
4826
  }
4827

4828
  /* --------------- Async Loading Cache --------------- */
4829

4830
  static final class BoundedLocalAsyncLoadingCache<K, V>
4831
      extends LocalAsyncLoadingCache<K, V> implements Serializable {
4832
    private static final long serialVersionUID = 1;
4833

4834
    final BoundedLocalCache<K, CompletableFuture<V>> cache;
4835
    final boolean isWeighted;
4836

4837
    @Nullable ConcurrentMap<K, CompletableFuture<V>> mapView;
4838
    @Nullable Policy<K, V> policy;
4839

4840
    @SuppressWarnings("unchecked")
4841
    BoundedLocalAsyncLoadingCache(Caffeine<K, V> builder, AsyncCacheLoader<? super K, V> loader) {
4842
      super(loader);
1✔
4843
      isWeighted = builder.isWeighted();
1✔
4844
      cache = (BoundedLocalCache<K, CompletableFuture<V>>) LocalCacheFactory
1✔
4845
          .newBoundedLocalCache(builder, loader, /* isAsync= */ true);
1✔
4846
    }
1✔
4847

4848
    @Override
4849
    public BoundedLocalCache<K, CompletableFuture<V>> cache() {
4850
      return cache;
1✔
4851
    }
4852

4853
    @Override
4854
    public ConcurrentMap<K, CompletableFuture<V>> asMap() {
4855
      return (mapView == null) ? (mapView = new AsyncAsMapView<>(this)) : mapView;
1✔
4856
    }
4857

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

4871
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
4872
      throw new InvalidObjectException("Proxy required");
1✔
4873
    }
4874

4875
    private Object writeReplace() {
4876
      return makeSerializationProxy(cache);
1✔
4877
    }
4878
  }
4879
}
4880

4881
/** The namespace for field padding through inheritance. */
4882
@SuppressWarnings({"IdentifierName", "MultiVariableDeclaration"})
4883
final class BLCHeader {
4884

4885
  private BLCHeader() {}
4886

4887
  @SuppressWarnings("unused")
4888
  static class PadDrainStatus {
1✔
4889
    byte p000, p001, p002, p003, p004, p005, p006, p007;
4890
    byte p008, p009, p010, p011, p012, p013, p014, p015;
4891
    byte p016, p017, p018, p019, p020, p021, p022, p023;
4892
    byte p024, p025, p026, p027, p028, p029, p030, p031;
4893
    byte p032, p033, p034, p035, p036, p037, p038, p039;
4894
    byte p040, p041, p042, p043, p044, p045, p046, p047;
4895
    byte p048, p049, p050, p051, p052, p053, p054, p055;
4896
    byte p056, p057, p058, p059, p060, p061, p062, p063;
4897
    byte p064, p065, p066, p067, p068, p069, p070, p071;
4898
    byte p072, p073, p074, p075, p076, p077, p078, p079;
4899
    byte p080, p081, p082, p083, p084, p085, p086, p087;
4900
    byte p088, p089, p090, p091, p092, p093, p094, p095;
4901
    byte p096, p097, p098, p099, p100, p101, p102, p103;
4902
    byte p104, p105, p106, p107, p108, p109, p110, p111;
4903
    byte p112, p113, p114, p115, p116, p117, p118, p119;
4904
  }
4905

4906
  /** Enforces a memory layout to avoid false sharing by padding the drain status. */
4907
  abstract static class DrainStatusRef extends PadDrainStatus {
1✔
4908
    static final VarHandle DRAIN_STATUS = fieldVarHandle(MethodHandles.lookup(),
1✔
4909
        "drainStatus", VarHandle.class, DrainStatusRef.class, int.class);
4910

4911
    /** A drain is not taking place. */
4912
    static final int IDLE = 0;
4913
    /** A drain is required due to a pending write modification. */
4914
    static final int REQUIRED = 1;
4915
    /** A drain is in progress and will transition to idle. */
4916
    static final int PROCESSING_TO_IDLE = 2;
4917
    /** A drain is in progress and will transition to required. */
4918
    static final int PROCESSING_TO_REQUIRED = 3;
4919

4920
    /** The draining status of the buffers. */
4921
    volatile int drainStatus = IDLE;
1✔
4922

4923
    /**
4924
     * Returns whether maintenance work is needed.
4925
     *
4926
     * @param delayable if draining the read buffer can be delayed
4927
     */
4928
    @SuppressWarnings("StatementSwitchToExpressionSwitch")
4929
    boolean shouldDrainBuffers(boolean delayable) {
4930
      switch (drainStatusOpaque()) {
1✔
4931
        case IDLE:
4932
          return !delayable;
1✔
4933
        case REQUIRED:
4934
          return true;
1✔
4935
        case PROCESSING_TO_IDLE:
4936
        case PROCESSING_TO_REQUIRED:
4937
          return false;
1✔
4938
        default:
4939
          throw new IllegalStateException("Invalid drain status: " + drainStatus);
1✔
4940
      }
4941
    }
4942

4943
    int drainStatusOpaque() {
4944
      return (int) DRAIN_STATUS.getOpaque(this);
1✔
4945
    }
4946

4947
    int drainStatusAcquire() {
4948
      return (int) DRAIN_STATUS.getAcquire(this);
1✔
4949
    }
4950

4951
    void setDrainStatusOpaque(int drainStatus) {
4952
      DRAIN_STATUS.setOpaque(this, drainStatus);
1✔
4953
    }
1✔
4954

4955
    void setDrainStatusRelease(int drainStatus) {
4956
      DRAIN_STATUS.setRelease(this, drainStatus);
1✔
4957
    }
1✔
4958

4959
    boolean casDrainStatus(int expect, int update) {
4960
      return DRAIN_STATUS.compareAndSet(this, expect, update);
1✔
4961
    }
4962
  }
4963
}
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