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

ben-manes / caffeine / #5527

21 Jun 2026 10:56AM UTC coverage: 99.976% (-0.01%) from 99.988%
#5527

push

github

ben-manes
avoid over-aggressive refresh discard (fixes #1970)

The low bit of an entry's write time is the refresh soft-lock marker.
A reader probing for a refresh sets it transiently (and starts no
load), but the completion's `getWriteTime() == writeTime` ABA check
mistook that marker for a concurrent write and discarded the freshly
completed reload.

Compare the base write time (masking the marker bit) in the completion,
and keep the refresh token registered across the value swap so the
compute machinery's discardRefresh clears it only after setWriteTime,
closing the remove-then-refresh stampede window. Re-validate the
soft-lock inside the entry's computeIfAbsent so a delayed reader aborts
instead of submitting a duplicate, stale reload. Apply the same
hold-token change to the explicit LocalLoadingCache.refresh and
LocalAsyncLoadingCache.tryComputeRefresh completions.

Cover the discard and the stampede with deterministic Fray and Lincheck
(runConcurrentTest) regressions and @CacheSpec token-cleared tests,
alongside the reporter's stress test.

4045 of 4054 branches covered (99.78%)

23 of 24 new or added lines in 4 files covered. (95.83%)

8238 of 8240 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✔
1074
        return n;
1✔
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.getWriteTime() != refreshWriteTime) {
1!
NEW
1394
            return null;
×
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
      drainReadBuffer();
1✔
1813

1814
      drainWriteBuffer();
1✔
1815
      if (task != null) {
1✔
1816
        task.run();
1✔
1817
      }
1818

1819
      drainKeyReferences();
1✔
1820
      drainValueReferences();
1✔
1821

1822
      expireEntries();
1✔
1823
      evictEntries();
1✔
1824

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1998
        setMissesInSample(missesInSample() + 1);
1✔
1999
      }
2000

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

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

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

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

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

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

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

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

2117
  /* --------------- Concurrent Map Support --------------- */
2118

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2228
    synchronized (node) {
1✔
2229
      logIfAlive(node);
1✔
2230
      makeDead(node);
1✔
2231
    }
1✔
2232

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2442
      // A read may race with the entry's removal, so that after the entry is acquired it may no
2443
      // longer be usable. A retry will reread from the map and either find an absent mapping, a
2444
      // new entry, or a stale entry.
2445
      if (!prior.isAlive()) {
1✔
2446
        // A reread of the stale entry may occur if the state transition occurred but the map
2447
        // removal was delayed by a context switch, so that this thread spin waits until resolved.
2448
        if ((attempts & MAX_PUT_SPIN_WAIT_ATTEMPTS) != 0) {
1✔
2449
          Thread.onSpinWait();
1✔
2450
          continue;
1✔
2451
        }
2452

2453
        // If the spin wait attempts are exhausted then fallback to a map computation in order to
2454
        // deschedule this thread until the entry's removal completes. If the key was modified
2455
        // while in the map so that its equals or hashCode changed then the contents may be
2456
        // corrupted, where the cache holds an evicted (dead) entry that could not be removed.
2457
        // That is a violation of the Map contract, so we check that the mapping is in the "alive"
2458
        // state while in the computation.
2459
        data.computeIfPresent(lookupKey, (k, n) -> {
1✔
2460
          requireIsAlive(key, n);
1✔
2461
          return n;
1✔
2462
        });
2463
        continue;
1✔
2464
      }
2465

2466
      V oldValue;
2467
      long varTime;
2468
      int oldWeight;
2469
      @Var boolean expired = false;
1✔
2470
      @Var boolean mayUpdate = true;
1✔
2471
      @Var boolean exceedsTolerance = false;
1✔
2472
      if (newWeight < 0) {
1✔
2473
        newWeight = weigher.weigh(key, value);
1✔
2474
      }
2475
      synchronized (prior) {
1✔
2476
        if (!prior.isAlive()) {
1✔
2477
          continue;
1✔
2478
        }
2479
        oldValue = prior.getValue();
1✔
2480
        oldWeight = prior.getWeight();
1✔
2481
        if (oldValue == null) {
1✔
2482
          varTime = expireAfterCreate(key, value, expiry, now);
1✔
2483
          notifyEviction(key, null, RemovalCause.COLLECTED);
1✔
2484
        } else if (hasExpired(prior, now, oldValue)) {
1✔
2485
          expired = true;
1✔
2486
          varTime = expireAfterCreate(key, value, expiry, now);
1✔
2487
          notifyEviction(key, oldValue, RemovalCause.EXPIRED);
1✔
2488
        } else if (onlyIfAbsent) {
1✔
2489
          mayUpdate = false;
1✔
2490
          varTime = expireAfterRead(prior, key, oldValue, expiry, now);
1✔
2491
        } else {
2492
          varTime = expireAfterUpdate(prior, key, value, expiry, now);
1✔
2493
        }
2494

2495
        long expirationTime = isComputingAsync(mayUpdate ? value : oldValue)
1✔
2496
            ? (now + ASYNC_EXPIRY)
1✔
2497
            : now;
1✔
2498
        if (mayUpdate) {
1✔
2499
          exceedsTolerance = exceedsWriteTimeTolerance(prior, varTime, expirationTime);
1✔
2500
          if (expired || exceedsTolerance) {
1✔
2501
            setWriteTime(prior, expirationTime);
1✔
2502
          }
2503

2504
          prior.setValue(value, valueReferenceQueue());
1✔
2505
          prior.setWeight(newWeight);
1✔
2506

2507
          discardRefresh(prior.getKeyReference());
1✔
2508
        }
2509

2510
        setVariableTime(prior, varTime);
1✔
2511
        setAccessTime(prior, expirationTime);
1✔
2512
      }
1✔
2513

2514
      if (expired) {
1✔
2515
        statsCounter().recordEviction(oldWeight, RemovalCause.EXPIRED);
1✔
2516
        notifyRemoval(key, oldValue, RemovalCause.EXPIRED);
1✔
2517
      } else if (oldValue == null) {
1✔
2518
        statsCounter().recordEviction(oldWeight, RemovalCause.COLLECTED);
1✔
2519
        notifyRemoval(key, /* value= */ null, RemovalCause.COLLECTED);
1✔
2520
      } else if (mayUpdate) {
1✔
2521
        notifyOnReplace(key, oldValue, value);
1✔
2522
      }
2523

2524
      int weightedDifference = mayUpdate ? (newWeight - oldWeight) : 0;
1✔
2525
      if ((oldValue == null) || (weightedDifference != 0) || expired) {
1✔
2526
        afterWrite(new UpdateTask(prior, weightedDifference));
1✔
2527
      } else if (!onlyIfAbsent && exceedsTolerance) {
1✔
2528
        afterWrite(new UpdateTask(prior, weightedDifference));
1✔
2529
      } else {
2530
        afterRead(prior, now, /* recordHit= */ false);
1✔
2531
      }
2532

2533
      return expired ? null : oldValue;
1✔
2534
    }
2535
  }
2536

2537
  @Override
2538
  @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
2539
  public @Nullable V remove(Object key) {
2540
    var ctx = new RemoveContext<K, V>();
1✔
2541
    Object lookupKey = nodeFactory.newLookupKey(key);
1✔
2542
    data.computeIfPresent(lookupKey, (k, n) -> {
1✔
2543
      synchronized (n) {
1✔
2544
        requireIsAlive(key, n);
1✔
2545
        ctx.oldKey = n.getKey();
1✔
2546
        ctx.oldValue = n.getValue();
1✔
2547
        ctx.oldWeight = n.getWeight();
1✔
2548
        RemovalCause actualCause;
2549
        if ((ctx.oldKey == null) || (ctx.oldValue == null)) {
1✔
2550
          actualCause = RemovalCause.COLLECTED;
1✔
2551
        } else if (hasExpired(n, expirationTicker().read(), ctx.oldValue)) {
1✔
2552
          actualCause = RemovalCause.EXPIRED;
1✔
2553
        } else {
2554
          actualCause = RemovalCause.EXPLICIT;
1✔
2555
        }
2556
        if (actualCause.wasEvicted()) {
1✔
2557
          notifyEviction(ctx.oldKey, ctx.oldValue, actualCause);
1✔
2558
        }
2559
        ctx.cause = actualCause;
1✔
2560
        discardRefresh(k);
1✔
2561
        ctx.node = n;
1✔
2562
        n.retire();
1✔
2563
        return null;
1✔
2564
      }
2565
    });
2566

2567
    if (ctx.cause != null) {
1✔
2568
      afterWrite(new RemovalTask(requireNonNull(ctx.node)));
1✔
2569
      if (ctx.cause.wasEvicted()) {
1✔
2570
        statsCounter().recordEviction(ctx.oldWeight, ctx.cause);
1✔
2571
      }
2572
      notifyRemoval(ctx.oldKey, ctx.oldValue, ctx.cause);
1✔
2573
    }
2574
    return (ctx.cause == RemovalCause.EXPLICIT) ? ctx.oldValue : null;
1✔
2575
  }
2576

2577
  @Override
2578
  @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
2579
  public boolean remove(Object key, @Nullable Object value) {
2580
    requireNonNull(key);
1✔
2581
    if (value == null) {
1✔
2582
      return false;
1✔
2583
    }
2584

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

2612
    if (ctx.node == null) {
1✔
2613
      return false;
1✔
2614
    }
2615
    var removeCause = requireNonNull(ctx.cause);
1✔
2616
    afterWrite(new RemovalTask(ctx.node));
1✔
2617
    if (removeCause.wasEvicted()) {
1✔
2618
      statsCounter().recordEviction(ctx.oldWeight, removeCause);
1✔
2619
    }
2620
    notifyRemoval(ctx.oldKey, ctx.oldValue, removeCause);
1✔
2621

2622
    return (removeCause == RemovalCause.EXPLICIT);
1✔
2623
  }
2624

2625
  @Override
2626
  @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
2627
  public @Nullable V replace(K key, V value) {
2628
    requireNonNull(key);
1✔
2629
    requireNonNull(value);
1✔
2630
    var ctx = new ReplaceContext<K, V>();
1✔
2631
    int weight = weigher.weigh(key, value);
1✔
2632
    Node<K, V> node = data.computeIfPresent(nodeFactory.newLookupKey(key), (k, n) -> {
1✔
2633
      synchronized (n) {
1✔
2634
        requireIsAlive(key, n);
1✔
2635
        ctx.nodeKey = n.getKey();
1✔
2636
        ctx.oldValue = n.getValue();
1✔
2637
        ctx.oldWeight = n.getWeight();
1✔
2638
        if ((ctx.nodeKey == null) || (ctx.oldValue == null)
1✔
2639
            || hasExpired(n, ctx.now = expirationTicker().read(), ctx.oldValue)) {
1✔
2640
          ctx.oldValue = null;
1✔
2641
          return n;
1✔
2642
        }
2643

2644
        long varTime = expireAfterUpdate(n, key, value, expiry(), ctx.now);
1✔
2645
        n.setValue(value, valueReferenceQueue());
1✔
2646
        n.setWeight(weight);
1✔
2647

2648
        long expirationTime = isComputingAsync(value) ? (ctx.now + ASYNC_EXPIRY) : ctx.now;
1✔
2649
        ctx.exceedsTolerance = exceedsWriteTimeTolerance(n, varTime, expirationTime);
1✔
2650
        if (ctx.exceedsTolerance) {
1✔
2651
          setWriteTime(n, expirationTime);
1✔
2652
        }
2653
        setAccessTime(n, expirationTime);
1✔
2654
        setVariableTime(n, varTime);
1✔
2655
        discardRefresh(k);
1✔
2656
        return n;
1✔
2657
      }
2658
    });
2659

2660
    if ((node == null) || (ctx.nodeKey == null) || (ctx.oldValue == null)) {
1✔
2661
      if (node != null) {
1✔
2662
        scheduleDrainBuffers();
1✔
2663
      }
2664
      return null;
1✔
2665
    }
2666

2667
    int weightedDifference = (weight - ctx.oldWeight);
1✔
2668
    if (ctx.exceedsTolerance || (weightedDifference != 0)) {
1✔
2669
      afterWrite(new UpdateTask(node, weightedDifference));
1✔
2670
    } else {
2671
      afterRead(node, ctx.now, /* recordHit= */ false);
1✔
2672
    }
2673

2674
    notifyOnReplace(ctx.nodeKey, ctx.oldValue, value);
1✔
2675
    return ctx.oldValue;
1✔
2676
  }
2677

2678
  @Override
2679
  public boolean replace(K key, V oldValue, V newValue) {
2680
    return replace(key, oldValue, newValue, /* shouldDiscardRefresh= */ true);
1✔
2681
  }
2682

2683
  @Override
2684
  @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
2685
  public boolean replace(K key, V oldValue, V newValue, boolean shouldDiscardRefresh) {
2686
    requireNonNull(key);
1✔
2687
    requireNonNull(oldValue);
1✔
2688
    requireNonNull(newValue);
1✔
2689
    var ctx = new ReplaceContext<K, V>();
1✔
2690
    int weight = weigher.weigh(key, newValue);
1✔
2691
    Node<K, V> node = data.computeIfPresent(nodeFactory.newLookupKey(key), (k, n) -> {
1✔
2692
      synchronized (n) {
1✔
2693
        requireIsAlive(key, n);
1✔
2694
        ctx.nodeKey = n.getKey();
1✔
2695
        ctx.oldValue = n.getValue();
1✔
2696
        ctx.oldWeight = n.getWeight();
1✔
2697
        if ((ctx.nodeKey == null) || (ctx.oldValue == null) || !n.containsValue(oldValue)
1✔
2698
            || hasExpired(n, ctx.now = expirationTicker().read(), ctx.oldValue)) {
1✔
2699
          ctx.oldValue = null;
1✔
2700
          return n;
1✔
2701
        }
2702

2703
        long varTime = expireAfterUpdate(n, key, newValue, expiry(), ctx.now);
1✔
2704
        n.setValue(newValue, valueReferenceQueue());
1✔
2705
        n.setWeight(weight);
1✔
2706

2707
        long expirationTime = isComputingAsync(newValue) ? (ctx.now + ASYNC_EXPIRY) : ctx.now;
1✔
2708
        ctx.exceedsTolerance = exceedsWriteTimeTolerance(n, varTime, expirationTime);
1✔
2709
        if (ctx.exceedsTolerance) {
1✔
2710
          setWriteTime(n, expirationTime);
1✔
2711
        }
2712
        setAccessTime(n, expirationTime);
1✔
2713
        setVariableTime(n, varTime);
1✔
2714

2715
        if (shouldDiscardRefresh) {
1✔
2716
          discardRefresh(k);
1✔
2717
        }
2718
      }
1✔
2719
      return n;
1✔
2720
    });
2721

2722
    if ((node == null) || (ctx.nodeKey == null) || (ctx.oldValue == null)) {
1✔
2723
      if (node != null) {
1✔
2724
        scheduleDrainBuffers();
1✔
2725
      }
2726
      return false;
1✔
2727
    }
2728

2729
    int weightedDifference = (weight - ctx.oldWeight);
1✔
2730
    if (ctx.exceedsTolerance || (weightedDifference != 0)) {
1✔
2731
      afterWrite(new UpdateTask(node, weightedDifference));
1✔
2732
    } else {
2733
      afterRead(node, ctx.now, /* recordHit= */ false);
1✔
2734
    }
2735

2736
    notifyOnReplace(ctx.nodeKey, ctx.oldValue, newValue);
1✔
2737
    return true;
1✔
2738
  }
2739

2740
  @Override
2741
  public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
2742
    requireNonNull(function);
1✔
2743

2744
    BiFunction<K, V, V> remappingFunction = (key, oldValue) ->
1✔
2745
        requireNonNull(function.apply(key, oldValue));
1✔
2746
    for (K key : keySet()) {
1✔
2747
      Object lookupKey = nodeFactory.newLookupKey(key);
1✔
2748
      remap(key, lookupKey, remappingFunction, expiry(),
1✔
2749
          new ComputeContext<>(expirationTicker().read()), /* computeIfAbsent= */ false);
1✔
2750
    }
1✔
2751
  }
1✔
2752

2753
  @Override
2754
  public @Nullable V computeIfAbsent(K key,
2755
      @Var Function<? super K, ? extends @Nullable V> mappingFunction,
2756
      boolean recordStats, boolean recordLoad) {
2757
    requireNonNull(key);
1✔
2758
    requireNonNull(mappingFunction);
1✔
2759
    long now = expirationTicker().read();
1✔
2760

2761
    // An optimistic fast path to avoid unnecessary locking
2762
    Node<K, V> node = data.get(nodeFactory.newLookupKey(key));
1✔
2763
    if (node != null) {
1✔
2764
      V value = node.getValue();
1✔
2765
      if ((value != null) && !hasExpired(node, now, value)) {
1✔
2766
        if (!isComputingAsync(value)) {
1✔
2767
          tryExpireAfterRead(node, key, value, expiry(), now);
1✔
2768
          setAccessTime(node, now);
1✔
2769
        }
2770
        @Nullable V refreshed = afterRead(node, now, /* recordHit= */ recordStats);
1✔
2771
        return (refreshed == null) ? value : refreshed;
1✔
2772
      }
2773
    }
2774
    if (recordStats) {
1✔
2775
      mappingFunction = statsAware(mappingFunction, recordLoad);
1✔
2776
    }
2777
    Object keyRef = nodeFactory.newReferenceKey(key, keyReferenceQueue());
1✔
2778
    return doComputeIfAbsent(key, keyRef, mappingFunction,
1✔
2779
        new ComputeContext<>(now), recordStats);
2780
  }
2781

2782
  /** Returns the current value from a computeIfAbsent invocation. */
2783
  @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
2784
  @Nullable V doComputeIfAbsent(K key, Object keyRef,
2785
      Function<? super K, ? extends @Nullable V> mappingFunction,
2786
      ComputeContext<K, V> ctx, boolean recordStats) {
2787
    Node<K, V> node = data.compute(keyRef, (k, n) -> {
1✔
2788
      if (n == null) {
1✔
2789
        ctx.newValue = mappingFunction.apply(key);
1✔
2790
        if (ctx.newValue == null) {
1✔
2791
          discardRefresh(k);
1✔
2792
          return null;
1✔
2793
        }
2794
        ctx.now = expirationTicker().read();
1✔
2795
        ctx.newWeight = weigher.weigh(key, ctx.newValue);
1✔
2796
        var created = nodeFactory.newNode(k, ctx.newValue,
1✔
2797
            valueReferenceQueue(), ctx.newWeight, ctx.now);
1✔
2798
        long expirationTime = isComputingAsync(ctx.newValue)
1✔
2799
            ? ctx.now + ASYNC_EXPIRY
1✔
2800
            : ctx.now;
1✔
2801
        setVariableTime(created, expireAfterCreate(key, ctx.newValue, expiry(), ctx.now));
1✔
2802
        setAccessTime(created, expirationTime);
1✔
2803
        setWriteTime(created, expirationTime);
1✔
2804
        discardRefresh(k);
1✔
2805
        return created;
1✔
2806
      }
2807

2808
      synchronized (n) {
1✔
2809
        requireIsAlive(key, n);
1✔
2810
        ctx.nodeKey = n.getKey();
1✔
2811
        ctx.oldValue = n.getValue();
1✔
2812
        ctx.oldWeight = n.getWeight();
1✔
2813
        RemovalCause actualCause;
2814
        if ((ctx.nodeKey == null) || (ctx.oldValue == null)) {
1✔
2815
          actualCause = RemovalCause.COLLECTED;
1✔
2816
        } else if (hasExpired(n, ctx.now, ctx.oldValue)) {
1✔
2817
          actualCause = RemovalCause.EXPIRED;
1✔
2818
        } else {
2819
          return n;
1✔
2820
        }
2821

2822
        ctx.cause = actualCause;
1✔
2823
        notifyEviction(ctx.nodeKey, ctx.oldValue, actualCause);
1✔
2824

2825
        try {
2826
          ctx.newValue = mappingFunction.apply(key);
1✔
2827
          if (ctx.newValue == null) {
1✔
2828
            discardRefresh(k);
1✔
2829
            ctx.removed = n;
1✔
2830
            n.retire();
1✔
2831
            return null;
1✔
2832
          }
2833
          ctx.now = expirationTicker().read();
1✔
2834
          ctx.newWeight = weigher.weigh(key, ctx.newValue);
1✔
2835
          long varTime = expireAfterCreate(key, ctx.newValue, expiry(), ctx.now);
1✔
2836

2837
          n.setValue(ctx.newValue, valueReferenceQueue());
1✔
2838
          n.setWeight(ctx.newWeight);
1✔
2839

2840
          long expirationTime = isComputingAsync(ctx.newValue)
1✔
2841
              ? (ctx.now + ASYNC_EXPIRY) : ctx.now;
1✔
2842
          setAccessTime(n, expirationTime);
1✔
2843
          setWriteTime(n, expirationTime);
1✔
2844
          setVariableTime(n, varTime);
1✔
2845
          discardRefresh(k);
1✔
2846
          return n;
1✔
2847
        } catch (Throwable e) {
1✔
2848
          ctx.newValue = null;
1✔
2849
          discardRefresh(k);
1✔
2850
          ctx.exception = e;
1✔
2851
          ctx.removed = n;
1✔
2852
          n.retire();
1✔
2853
          return null;
1✔
2854
        }
2855
      }
2856
    });
2857

2858
    if (ctx.cause != null) {
1✔
2859
      statsCounter().recordEviction(ctx.oldWeight, ctx.cause);
1✔
2860
      notifyRemoval(ctx.nodeKey, ctx.oldValue, ctx.cause);
1✔
2861
    }
2862
    if (node == null) {
1✔
2863
      if (ctx.removed != null) {
1✔
2864
        afterWrite(new RemovalTask(ctx.removed));
1✔
2865
      }
2866
      if (ctx.exception != null) {
1✔
2867
        throw toUncheckedException(ctx.exception);
1✔
2868
      }
2869
      return null;
1✔
2870
    }
2871
    if ((ctx.oldValue != null) && (ctx.newValue == null)) {
1✔
2872
      if (!isComputingAsync(ctx.oldValue)) {
1✔
2873
        tryExpireAfterRead(node, key, ctx.oldValue, expiry(), ctx.now);
1✔
2874
        setAccessTime(node, ctx.now);
1✔
2875
      }
2876

2877
      @Nullable V refreshed = afterRead(node, ctx.now, /* recordHit= */ recordStats);
1✔
2878
      return (refreshed == null) ? ctx.oldValue : refreshed;
1✔
2879
    }
2880
    if ((ctx.oldValue == null) && (ctx.cause == null)) {
1✔
2881
      afterWrite(new AddTask(node, ctx.newWeight));
1✔
2882
    } else {
2883
      int weightedDifference = (ctx.newWeight - ctx.oldWeight);
1✔
2884
      afterWrite(new UpdateTask(node, weightedDifference));
1✔
2885
    }
2886

2887
    return ctx.newValue;
1✔
2888
  }
2889

2890
  @Override
2891
  public @Nullable V computeIfPresent(K key,
2892
      BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
2893
    requireNonNull(key);
1✔
2894
    requireNonNull(remappingFunction);
1✔
2895

2896
    // An optimistic fast path to avoid unnecessary locking
2897
    Object lookupKey = nodeFactory.newLookupKey(key);
1✔
2898
    @Nullable Node<K, V> node = data.get(lookupKey);
1✔
2899
    long now;
2900
    if (node == null) {
1✔
2901
      return null;
1✔
2902
    }
2903
    V value = node.getValue();
1✔
2904
    if ((value == null) || hasExpired(node, now = expirationTicker().read(), value)) {
1✔
2905
      scheduleDrainBuffers();
1✔
2906
      return null;
1✔
2907
    }
2908

2909
    BiFunction<? super K, ? super V, ? extends V> statsAwareRemappingFunction =
1✔
2910
        statsAware(remappingFunction, /* recordLoad= */ true, /* recordLoadFailure= */ true);
1✔
2911
    return remap(key, lookupKey, statsAwareRemappingFunction,
1✔
2912
        expiry(), new ComputeContext<>(now), /* computeIfAbsent= */ false);
1✔
2913
  }
2914

2915
  @Override
2916
  public @Nullable V compute(K key,
2917
      BiFunction<? super K, ? super V, ? extends @Nullable V> remappingFunction,
2918
      @Nullable Expiry<? super K, ? super V> expiry, boolean recordLoad, boolean recordLoadFailure,
2919
      @Nullable RemapHints hints) {
2920
    requireNonNull(key);
1✔
2921
    requireNonNull(remappingFunction);
1✔
2922

2923
    Object keyRef = nodeFactory.newReferenceKey(key, keyReferenceQueue());
1✔
2924
    BiFunction<? super K, ? super V, ? extends V> statsAwareRemappingFunction =
1✔
2925
        statsAware(remappingFunction, recordLoad, recordLoadFailure);
1✔
2926
    var ctx = new ComputeContext<K, V>(expirationTicker().read());
1✔
2927
    ctx.hints = hints;
1✔
2928
    return remap(key, keyRef, statsAwareRemappingFunction,
1✔
2929
        expiry, ctx, /* computeIfAbsent= */ true);
2930
  }
2931

2932
  @Override
2933
  public @Nullable V merge(K key, V value,
2934
      BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
2935
    requireNonNull(key);
1✔
2936
    requireNonNull(value);
1✔
2937
    requireNonNull(remappingFunction);
1✔
2938

2939
    Object keyRef = nodeFactory.newReferenceKey(key, keyReferenceQueue());
1✔
2940
    BiFunction<? super V, ? super V, ? extends V> f = statsAware(remappingFunction);
1✔
2941
    BiFunction<? super K, ? super @Nullable V, ? extends @Nullable V> mergeFunction =
1✔
2942
        (k, oldValue) -> (oldValue == null) ? value : f.apply(oldValue, value);
1✔
2943
    return remap(key, keyRef, mergeFunction, expiry(),
1✔
2944
        new ComputeContext<>(expirationTicker().read()), /* computeIfAbsent= */ true);
1✔
2945
  }
2946

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

2986
          long expirationTime = isComputingAsync(ctx.newValue)
1✔
2987
              ? ctx.now + ASYNC_EXPIRY
1✔
2988
              : ctx.now;
1✔
2989
          setAccessTime(created, expirationTime);
1✔
2990
          setWriteTime(created, expirationTime);
1✔
2991
          setVariableTime(created, varTime);
1✔
2992
          return created;
1✔
2993
        } finally {
2994
          discardRefresh(kr);
1✔
2995
        }
2996
      }
2997

2998
      synchronized (n) {
1✔
2999
        requireIsAlive(key, n);
1✔
3000
        ctx.nodeKey = n.getKey();
1✔
3001
        ctx.oldValue = n.getValue();
1✔
3002
        ctx.oldWeight = n.getWeight();
1✔
3003
        if ((ctx.nodeKey == null) || (ctx.oldValue == null)) {
1✔
3004
          ctx.cause = RemovalCause.COLLECTED;
1✔
3005
        } else if (hasExpired(n, expirationTicker().read(), ctx.oldValue)) {
1✔
3006
          ctx.cause = RemovalCause.EXPIRED;
1✔
3007
        }
3008
        if (ctx.cause != null) {
1✔
3009
          notifyEviction(ctx.nodeKey, ctx.oldValue, ctx.cause);
1✔
3010
          if (!computeIfAbsent) {
1✔
3011
            discardRefresh(kr);
1✔
3012
            ctx.removed = n;
1✔
3013
            n.retire();
1✔
3014
            return null;
1✔
3015
          }
3016
        }
3017

3018
        boolean wasEvicted = (ctx.cause != null);
1✔
3019
        try {
3020
          ctx.newValue = remappingFunction.apply(ctx.nodeKey,
1✔
3021
              (ctx.cause == null) ? ctx.oldValue : null);
1✔
3022

3023
          if (ctx.newValue == null) {
1✔
3024
            if (ctx.cause == null) {
1✔
3025
              ctx.cause = RemovalCause.EXPLICIT;
1✔
3026
            }
3027
            discardRefresh(kr);
1✔
3028
            ctx.removed = n;
1✔
3029
            n.retire();
1✔
3030
            return null;
1✔
3031
          }
3032

3033
          // If the caller flagged a same-instance return as a no-op (e.g., a refresh was rejected
3034
          // and should not touch the entry), skip the metadata updates below.
3035
          if ((ctx.hints != null) && ctx.hints.preserveTimestamps
1✔
3036
              && (ctx.newValue == ctx.oldValue) && (ctx.cause == null)) {
3037
            // Skip for query-style callers whose no-op path must leave any in-flight refresh intact
3038
            if (!ctx.hints.preserveRefresh) {
1✔
3039
              discardRefresh(kr);
1✔
3040
            }
3041
            return n;
1✔
3042
          }
3043

3044
          long varTime;
3045
          ctx.newWeight = weigher.weigh(key, ctx.newValue);
1✔
3046
          ctx.now = expirationTicker().read();
1✔
3047
          if (ctx.cause == null) {
1✔
3048
            if (ctx.newValue != ctx.oldValue) {
1✔
3049
              ctx.cause = RemovalCause.REPLACED;
1✔
3050
            }
3051
            varTime = expireAfterUpdate(n, key, ctx.newValue, expiry, ctx.now);
1✔
3052
          } else {
3053
            varTime = expireAfterCreate(key, ctx.newValue, expiry, ctx.now);
1✔
3054
          }
3055

3056
          if (ctx.newValue != ctx.oldValue) {
1✔
3057
            n.setValue(ctx.newValue, valueReferenceQueue());
1✔
3058
          }
3059
          n.setWeight(ctx.newWeight);
1✔
3060

3061
          long expirationTime = isComputingAsync(ctx.newValue)
1✔
3062
              ? ctx.now + ASYNC_EXPIRY
1✔
3063
              : ctx.now;
1✔
3064
          ctx.exceedsTolerance = exceedsWriteTimeTolerance(n, varTime, expirationTime);
1✔
3065
          if (((ctx.cause != null) && ctx.cause.wasEvicted()) || ctx.exceedsTolerance) {
1✔
3066
            setWriteTime(n, expirationTime);
1✔
3067
          }
3068
          setAccessTime(n, expirationTime);
1✔
3069
          setVariableTime(n, varTime);
1✔
3070
          discardRefresh(kr);
1✔
3071
          return n;
1✔
3072
        } catch (Throwable e) {
1✔
3073
          if (!wasEvicted) {
1✔
3074
            discardRefresh(kr);
1✔
3075
            throw e;
1✔
3076
          }
3077
          ctx.newValue = null;
1✔
3078
          discardRefresh(kr);
1✔
3079
          ctx.exception = e;
1✔
3080
          ctx.removed = n;
1✔
3081
          n.retire();
1✔
3082
          return null;
1✔
3083
        }
3084
      }
3085
    });
3086

3087
    if (ctx.cause != null) {
1✔
3088
      if (ctx.cause == RemovalCause.REPLACED) {
1✔
3089
        requireNonNull(ctx.newValue);
1✔
3090
        notifyOnReplace(key, ctx.oldValue, ctx.newValue);
1✔
3091
      } else {
3092
        if (ctx.cause.wasEvicted()) {
1✔
3093
          statsCounter().recordEviction(ctx.oldWeight, ctx.cause);
1✔
3094
        }
3095
        notifyRemoval(ctx.nodeKey, ctx.oldValue, ctx.cause);
1✔
3096
      }
3097
    }
3098

3099
    if (ctx.removed != null) {
1✔
3100
      afterWrite(new RemovalTask(ctx.removed));
1✔
3101
    } else if (node == null) {
1✔
3102
      // absent and not computable
3103
    } else if ((ctx.hints != null) && ctx.hints.preserveTimestamps) {
1✔
3104
      // The remapping was a signaled no-op; the node was not modified
3105
    } else if ((ctx.oldValue == null) && (ctx.cause == null)) {
1✔
3106
      afterWrite(new AddTask(node, ctx.newWeight));
1✔
3107
    } else {
3108
      int weightedDifference = ctx.newWeight - ctx.oldWeight;
1✔
3109
      if (ctx.exceedsTolerance || (weightedDifference != 0)) {
1✔
3110
        afterWrite(new UpdateTask(node, weightedDifference));
1✔
3111
      } else {
3112
        afterRead(node, ctx.now, /* recordHit= */ false);
1✔
3113
        if ((ctx.cause != null) && ctx.cause.wasEvicted()) {
1✔
3114
          scheduleDrainBuffers();
1✔
3115
        }
3116
      }
3117
    }
3118

3119
    if (ctx.exception != null) {
1✔
3120
      throw toUncheckedException(ctx.exception);
1✔
3121
    }
3122
    return ctx.newValue;
1✔
3123
  }
3124

3125
  @Override
3126
  public void forEach(BiConsumer<? super K, ? super V> action) {
3127
    requireNonNull(action);
1✔
3128

3129
    for (var iterator = new EntryIterator<>(this); iterator.hasNext();) {
1✔
3130
      action.accept(iterator.key, iterator.value);
1✔
3131
      iterator.advance();
1✔
3132
    }
3133
  }
1✔
3134

3135
  @Override
3136
  public Set<K> keySet() {
3137
    Set<K> ks = keySet;
1✔
3138
    return (ks == null) ? (keySet = new KeySetView<>(this)) : ks;
1✔
3139
  }
3140

3141
  @Override
3142
  public Collection<V> values() {
3143
    Collection<V> vs = values;
1✔
3144
    return (vs == null) ? (values = new ValuesView<>(this)) : vs;
1✔
3145
  }
3146

3147
  @Override
3148
  public Set<Entry<K, V>> entrySet() {
3149
    Set<Entry<K, V>> es = entrySet;
1✔
3150
    return (es == null) ? (entrySet = new EntrySetView<>(this)) : es;
1✔
3151
  }
3152

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

3186
    var map = (Map<?, ?>) o;
1✔
3187
    int expectedSize = size();
1✔
3188
    if (map.size() != expectedSize) {
1✔
3189
      return false;
1✔
3190
    }
3191

3192
    long now = expirationTicker().read();
1✔
3193
    @Var int count = 0;
1✔
3194
    for (var node : data.values()) {
1✔
3195
      K key = node.getKey();
1✔
3196
      V value = node.getValue();
1✔
3197
      if ((key == null) || (value == null)
1✔
3198
          || !node.isAlive() || hasExpired(node, now, value)) {
1✔
3199
        scheduleDrainBuffers();
1✔
3200
        return false;
1✔
3201
      } else {
3202
        var val = map.get(key);
1✔
3203
        if ((val == null) || ((val != value) && !val.equals(value))) {
1✔
3204
          return false;
1✔
3205
        }
3206
      }
3207
      count++;
1✔
3208
    }
1✔
3209
    return (count == expectedSize);
1✔
3210
  }
3211

3212
  @Override
3213
  public int hashCode() {
3214
    @Var int hash = 0;
1✔
3215
    @Var boolean drain = false;
1✔
3216
    long now = expirationTicker().read();
1✔
3217
    for (var node : data.values()) {
1✔
3218
      K key = node.getKey();
1✔
3219
      V value = node.getValue();
1✔
3220
      if ((key == null) || (value == null)
1✔
3221
          || !node.isAlive() || hasExpired(node, now, value)) {
1✔
3222
        drain = true;
1✔
3223
      } else {
3224
        hash += key.hashCode() ^ value.hashCode();
1✔
3225
      }
3226
    }
1✔
3227
    if (drain) {
1✔
3228
      scheduleDrainBuffers();
1✔
3229
    }
3230
    return hash;
1✔
3231
  }
3232

3233
  @Override
3234
  public String toString() {
3235
    @Var boolean drain = false;
1✔
3236
    long now = expirationTicker().read();
1✔
3237
    var result = new StringBuilder().append('{');
1✔
3238
    for (var node : data.values()) {
1✔
3239
      K key = node.getKey();
1✔
3240
      V value = node.getValue();
1✔
3241
      if ((key == null) || (value == null)
1✔
3242
          || !node.isAlive() || hasExpired(node, now, value)) {
1✔
3243
        drain = true;
1✔
3244
      } else {
3245
        if (result.length() != 1) {
1✔
3246
          result.append(',').append(' ');
1✔
3247
        }
3248
        result.append((key == this) ? "(this Map)" : key);
1✔
3249
        result.append('=');
1✔
3250
        result.append((value == this) ? "(this Map)" : value);
1✔
3251
      }
3252
    }
1✔
3253
    if (drain) {
1✔
3254
      scheduleDrainBuffers();
1✔
3255
    }
3256
    return result.append('}').toString();
1✔
3257
  }
3258

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

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

3333
  /**
3334
   * Returns the computed result from the ordered traversal of the cache entries.
3335
   *
3336
   * @param iterable the supplier of the entries in the cache
3337
   * @param transformer a function that unwraps the value
3338
   * @param mappingFunction the mapping function to compute a value
3339
   * @return the computed value
3340
   */
3341
  <T> T snapshot(Iterable<Node<K, V>> iterable, Function<@Nullable V, @Nullable V> transformer,
3342
      Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
3343
    requireNonNull(mappingFunction);
1✔
3344
    requireNonNull(transformer);
1✔
3345
    requireNonNull(iterable);
1✔
3346

3347
    evictionLock.lock();
1✔
3348
    try {
3349
      maintenance(/* ignored */ null);
1✔
3350

3351
      // Obtain the iterator as late as possible for modification count checking
3352
      try (var stream = StreamSupport.stream(Spliterators.spliteratorUnknownSize(
1✔
3353
           iterable.iterator(), DISTINCT | ORDERED | NONNULL | IMMUTABLE), /* parallel= */ false)) {
1✔
3354
        return mappingFunction.apply(stream
1✔
3355
            .map(node -> nodeToCacheEntry(node, transformer, node.getPolicyWeight()))
1✔
3356
            .filter(Objects::nonNull));
1✔
3357
      }
3358
    } finally {
3359
      evictionLock.unlock();
1✔
3360
      rescheduleCleanUpIfIncomplete();
1✔
3361
    }
3362
  }
3363

3364
  /**
3365
   * Returns an entry for the given node if it can be used externally, else null. The weight is
3366
   * caller-supplied: snapshot callers hold evictionLock and read policyWeight (in sync with the
3367
   * drain thread); unlocked readers read weight, which may be stale for a concurrent in-place
3368
   * update (acceptable for a point-in-time CacheEntry).
3369
   */
3370
  @Nullable CacheEntry<K, V> nodeToCacheEntry(
3371
      Node<K, V> node, Function<@Nullable V, @Nullable V> transformer, int weight) {
3372
    V rawValue = node.getValue();
1✔
3373
    if (rawValue == null) {
1✔
3374
      return null;
1✔
3375
    }
3376
    V value = transformer.apply(rawValue);
1✔
3377
    K key = node.getKey();
1✔
3378
    long now;
3379
    if ((key == null) || (value == null) || !node.isAlive()
1✔
3380
        || hasExpired(node, (now = expirationTicker().read()), rawValue)) {
1✔
3381
      return null;
1✔
3382
    }
3383

3384
    @Var long expiresAfter = Long.MAX_VALUE;
1✔
3385
    if (expiresAfterAccess()) {
1✔
3386
      expiresAfter = Math.min(expiresAfter,
1✔
3387
          expiresAfterAccessNanos() - (now - node.getAccessTime()));
1✔
3388
    }
3389
    if (expiresAfterWrite()) {
1✔
3390
      expiresAfter = Math.min(expiresAfter,
1✔
3391
          expiresAfterWriteNanos() - ((now & ~1L) - (node.getWriteTime() & ~1L)));
1✔
3392
    }
3393
    if (expiresVariable()) {
1✔
3394
      expiresAfter = node.getVariableTime() - now;
1✔
3395
    }
3396

3397
    long refreshableAt = refreshAfterWrite()
1✔
3398
        ? (node.getWriteTime() & ~1L) + refreshAfterWriteNanos()
1✔
3399
        : now + Long.MAX_VALUE;
1✔
3400
    return SnapshotEntry.forEntry(key, value, now, weight, now + expiresAfter, refreshableAt);
1✔
3401
  }
3402

3403
  /** Mutable context for passing state between a lambda and the caller. */
3404
  static final class EvictContext<V> {
1✔
3405
    @Nullable RemovalCause cause;
3406
    @Nullable V value;
3407
    boolean resurrect;
3408
    boolean removed;
3409
    int oldWeight;
3410
  }
3411

3412
  /** Mutable context for passing state between a lambda and the caller. */
3413
  static final class RemoveContext<K, V> {
1✔
3414
    @Nullable K oldKey;
3415
    @Nullable V oldValue;
3416
    @Nullable Node<K, V> node;
3417
    @Nullable RemovalCause cause;
3418
    int oldWeight;
3419
  }
3420

3421
  /** Mutable context for passing state between a lambda and the caller. */
3422
  static final class ReplaceContext<K, V> {
1✔
3423
    @Nullable K nodeKey;
3424
    @Nullable V oldValue;
3425

3426
    long now;
3427
    int oldWeight;
3428
    boolean exceedsTolerance;
3429
  }
3430

3431
  /** Mutable context for passing state between a lambda and the caller. */
3432
  static final class ComputeContext<K, V> {
3433
    @Nullable K nodeKey;
3434
    @Nullable V oldValue;
3435
    @Nullable V newValue;
3436
    @Nullable Node<K, V> removed;
3437
    @Nullable RemovalCause cause;
3438
    @Nullable Throwable exception;
3439
    @Nullable RemapHints hints;
3440

3441
    long now;
3442
    int oldWeight;
3443
    int newWeight;
3444
    boolean exceedsTolerance;
3445

3446
    ComputeContext(long now) {
1✔
3447
      this.now = now;
1✔
3448
    }
1✔
3449
  }
3450

3451
  /** A function that produces an unmodifiable map up to the limit in stream order. */
3452
  static final class SizeLimiter<K, V> implements Function<Stream<CacheEntry<K, V>>, Map<K, V>> {
3453
    private final int expectedSize;
3454
    private final long limit;
3455

3456
    SizeLimiter(int expectedSize, long limit) {
1✔
3457
      requireArgument(limit >= 0);
1✔
3458
      this.expectedSize = expectedSize;
1✔
3459
      this.limit = limit;
1✔
3460
    }
1✔
3461

3462
    @Override
3463
    public Map<K, V> apply(Stream<CacheEntry<K, V>> stream) {
3464
      var map = new LinkedHashMap<K, V>(calculateHashMapCapacity(expectedSize));
1✔
3465
      stream.limit(limit).forEach(entry -> map.put(entry.getKey(), entry.getValue()));
1✔
3466
      return Collections.unmodifiableMap(map);
1✔
3467
    }
3468
  }
3469

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

3474
    private long weightedSize;
3475

3476
    WeightLimiter(long weightLimit) {
1✔
3477
      requireArgument(weightLimit >= 0);
1✔
3478
      this.weightLimit = weightLimit;
1✔
3479
    }
1✔
3480

3481
    @Override
3482
    public Map<K, V> apply(Stream<CacheEntry<K, V>> stream) {
3483
      var map = new LinkedHashMap<K, V>();
1✔
3484
      stream.takeWhile(entry -> {
1✔
3485
        weightedSize = Math.addExact(weightedSize, entry.weight());
1✔
3486
        return (weightedSize <= weightLimit);
1✔
3487
      }).forEach(entry -> map.put(entry.getKey(), entry.getValue()));
1✔
3488
      return Collections.unmodifiableMap(map);
1✔
3489
    }
3490
  }
3491

3492
  /** An adapter to safely externalize the keys. */
3493
  static final class KeySetView<K, V> extends AbstractSet<K> {
3494
    final BoundedLocalCache<K, V> cache;
3495

3496
    KeySetView(BoundedLocalCache<K, V> cache) {
1✔
3497
      this.cache = requireNonNull(cache);
1✔
3498
    }
1✔
3499

3500
    @Override
3501
    public int size() {
3502
      return cache.size();
1✔
3503
    }
3504

3505
    @Override
3506
    public void clear() {
3507
      cache.clear();
1✔
3508
    }
1✔
3509

3510
    @Override
3511
    @SuppressWarnings("SuspiciousMethodCalls")
3512
    public boolean contains(Object o) {
3513
      return cache.containsKey(o);
1✔
3514
    }
3515

3516
    @Override
3517
    public boolean removeAll(Collection<?> collection) {
3518
      requireNonNull(collection);
1✔
3519
      @Var boolean modified = false;
1✔
3520
      if (cache.collectKeys() || ((collection instanceof Set<?>) && (collection.size() > size()))) {
1✔
3521
        for (K key : this) {
1✔
3522
          if (collection.contains(key)) {
1✔
3523
            modified |= remove(key);
1✔
3524
          }
3525
        }
1✔
3526
      } else {
3527
        for (var item : collection) {
1✔
3528
          modified |= (item != null) && remove(item);
1✔
3529
        }
1✔
3530
      }
3531
      return modified;
1✔
3532
    }
3533

3534
    @Override
3535
    public boolean remove(Object o) {
3536
      return (cache.remove(o) != null);
1✔
3537
    }
3538

3539
    @Override
3540
    public boolean removeIf(Predicate<? super K> filter) {
3541
      requireNonNull(filter);
1✔
3542
      @Var boolean modified = false;
1✔
3543
      for (K key : this) {
1✔
3544
        if (filter.test(key) && remove(key)) {
1✔
3545
          modified = true;
1✔
3546
        }
3547
      }
1✔
3548
      return modified;
1✔
3549
    }
3550

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

3563
    @Override
3564
    public Iterator<K> iterator() {
3565
      return new KeyIterator<>(cache);
1✔
3566
    }
3567

3568
    @Override
3569
    public Spliterator<K> spliterator() {
3570
      return new KeySpliterator<>(cache);
1✔
3571
    }
3572
  }
3573

3574
  /** An adapter to safely externalize the key iterator. */
3575
  static final class KeyIterator<K, V> implements Iterator<K> {
3576
    final EntryIterator<K, V> iterator;
3577

3578
    KeyIterator(BoundedLocalCache<K, V> cache) {
1✔
3579
      this.iterator = new EntryIterator<>(cache);
1✔
3580
    }
1✔
3581

3582
    @Override
3583
    public boolean hasNext() {
3584
      return iterator.hasNext();
1✔
3585
    }
3586

3587
    @Override
3588
    public K next() {
3589
      return iterator.nextKey();
1✔
3590
    }
3591

3592
    @Override
3593
    public void remove() {
3594
      iterator.remove();
1✔
3595
    }
1✔
3596
  }
3597

3598
  /** An adapter to safely externalize the key spliterator. */
3599
  static final class KeySpliterator<K, V> implements Spliterator<K> {
3600
    final Spliterator<Node<K, V>> spliterator;
3601
    final BoundedLocalCache<K, V> cache;
3602

3603
    KeySpliterator(BoundedLocalCache<K, V> cache) {
3604
      this(cache, cache.data.values().spliterator());
1✔
3605
    }
1✔
3606

3607
    KeySpliterator(BoundedLocalCache<K, V> cache, Spliterator<Node<K, V>> spliterator) {
1✔
3608
      this.spliterator = requireNonNull(spliterator);
1✔
3609
      this.cache = requireNonNull(cache);
1✔
3610
    }
1✔
3611

3612
    @Override
3613
    public void forEachRemaining(Consumer<? super K> action) {
3614
      requireNonNull(action);
1✔
3615
      Consumer<Node<K, V>> consumer = node -> {
1✔
3616
        K key = node.getKey();
1✔
3617
        V value = node.getValue();
1✔
3618
        long now = cache.expirationTicker().read();
1✔
3619
        if ((key != null) && (value != null) && node.isAlive()
1✔
3620
            && !cache.hasExpired(node, now, value)) {
1✔
3621
          action.accept(key);
1✔
3622
        }
3623
      };
1✔
3624
      spliterator.forEachRemaining(consumer);
1✔
3625
    }
1✔
3626

3627
    @Override
3628
    public boolean tryAdvance(Consumer<? super K> action) {
3629
      requireNonNull(action);
1✔
3630
      boolean[] advanced = { false };
1✔
3631
      Consumer<Node<K, V>> consumer = node -> {
1✔
3632
        K key = node.getKey();
1✔
3633
        V value = node.getValue();
1✔
3634
        long now = cache.expirationTicker().read();
1✔
3635
        if ((key != null) && (value != null) && node.isAlive()
1✔
3636
            && !cache.hasExpired(node, now, value)) {
1✔
3637
          action.accept(key);
1✔
3638
          advanced[0] = true;
1✔
3639
        }
3640
      };
1✔
3641
      while (spliterator.tryAdvance(consumer)) {
1✔
3642
        if (advanced[0]) {
1✔
3643
          return true;
1✔
3644
        }
3645
      }
3646
      return false;
1✔
3647
    }
3648

3649
    @Override
3650
    public @Nullable Spliterator<K> trySplit() {
3651
      Spliterator<Node<K, V>> split = spliterator.trySplit();
1✔
3652
      return (split == null) ? null : new KeySpliterator<>(cache, split);
1✔
3653
    }
3654

3655
    @Override
3656
    public long estimateSize() {
3657
      return spliterator.estimateSize();
1✔
3658
    }
3659

3660
    @Override
3661
    public int characteristics() {
3662
      return DISTINCT | CONCURRENT | NONNULL;
1✔
3663
    }
3664
  }
3665

3666
  /** An adapter to safely externalize the values. */
3667
  static final class ValuesView<K, V> extends AbstractCollection<V> {
3668
    final BoundedLocalCache<K, V> cache;
3669

3670
    ValuesView(BoundedLocalCache<K, V> cache) {
1✔
3671
      this.cache = requireNonNull(cache);
1✔
3672
    }
1✔
3673

3674
    @Override
3675
    public int size() {
3676
      return cache.size();
1✔
3677
    }
3678

3679
    @Override
3680
    public void clear() {
3681
      cache.clear();
1✔
3682
    }
1✔
3683

3684
    @Override
3685
    @SuppressWarnings("SuspiciousMethodCalls")
3686
    public boolean contains(Object o) {
3687
      return cache.containsValue(o);
1✔
3688
    }
3689

3690
    @Override
3691
    public boolean removeAll(Collection<?> collection) {
3692
      requireNonNull(collection);
1✔
3693
      @Var boolean modified = false;
1✔
3694
      for (var iterator = new EntryIterator<>(cache); iterator.hasNext();) {
1✔
3695
        var key = requireNonNull(iterator.key);
1✔
3696
        var value = requireNonNull(iterator.value);
1✔
3697
        if (collection.contains(value) && cache.remove(key, value)) {
1✔
3698
          modified = true;
1✔
3699
        }
3700
        iterator.advance();
1✔
3701
      }
1✔
3702
      return modified;
1✔
3703
    }
3704

3705
    @Override
3706
    public boolean remove(@Nullable Object o) {
3707
      if (o == null) {
1✔
3708
        return false;
1✔
3709
      }
3710
      for (var iterator = new EntryIterator<>(cache); iterator.hasNext();) {
1✔
3711
        var key = requireNonNull(iterator.key);
1✔
3712
        var node = requireNonNull(iterator.next);
1✔
3713
        var value = requireNonNull(iterator.value);
1✔
3714
        if (node.containsValue(o) && cache.remove(key, value)) {
1✔
3715
          return true;
1✔
3716
        }
3717
        iterator.advance();
1✔
3718
      }
1✔
3719
      return false;
1✔
3720
    }
3721

3722
    @Override
3723
    public boolean removeIf(Predicate<? super V> filter) {
3724
      requireNonNull(filter);
1✔
3725
      @Var boolean modified = false;
1✔
3726
      for (var iterator = new EntryIterator<>(cache); iterator.hasNext();) {
1✔
3727
        var value = requireNonNull(iterator.value);
1✔
3728
        if (filter.test(value)) {
1✔
3729
          var key = requireNonNull(iterator.key);
1✔
3730
          modified |= cache.remove(key, value);
1✔
3731
        }
3732
        iterator.advance();
1✔
3733
      }
1✔
3734
      return modified;
1✔
3735
    }
3736

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

3752
    @Override
3753
    public Iterator<V> iterator() {
3754
      return new ValueIterator<>(cache);
1✔
3755
    }
3756

3757
    @Override
3758
    public Spliterator<V> spliterator() {
3759
      return new ValueSpliterator<>(cache);
1✔
3760
    }
3761
  }
3762

3763
  /** An adapter to safely externalize the value iterator. */
3764
  static final class ValueIterator<K, V> implements Iterator<V> {
3765
    final EntryIterator<K, V> iterator;
3766

3767
    ValueIterator(BoundedLocalCache<K, V> cache) {
1✔
3768
      this.iterator = new EntryIterator<>(cache);
1✔
3769
    }
1✔
3770

3771
    @Override
3772
    public boolean hasNext() {
3773
      return iterator.hasNext();
1✔
3774
    }
3775

3776
    @Override
3777
    public V next() {
3778
      return iterator.nextValue();
1✔
3779
    }
3780

3781
    @Override
3782
    public void remove() {
3783
      iterator.remove();
1✔
3784
    }
1✔
3785
  }
3786

3787
  /** An adapter to safely externalize the value spliterator. */
3788
  static final class ValueSpliterator<K, V> implements Spliterator<V> {
3789
    final Spliterator<Node<K, V>> spliterator;
3790
    final BoundedLocalCache<K, V> cache;
3791

3792
    ValueSpliterator(BoundedLocalCache<K, V> cache) {
3793
      this(cache, cache.data.values().spliterator());
1✔
3794
    }
1✔
3795

3796
    ValueSpliterator(BoundedLocalCache<K, V> cache, Spliterator<Node<K, V>> spliterator) {
1✔
3797
      this.spliterator = requireNonNull(spliterator);
1✔
3798
      this.cache = requireNonNull(cache);
1✔
3799
    }
1✔
3800

3801
    @Override
3802
    public void forEachRemaining(Consumer<? super V> action) {
3803
      requireNonNull(action);
1✔
3804
      Consumer<Node<K, V>> consumer = node -> {
1✔
3805
        K key = node.getKey();
1✔
3806
        V value = node.getValue();
1✔
3807
        long now = cache.expirationTicker().read();
1✔
3808
        if ((key != null) && (value != null) && node.isAlive()
1✔
3809
            && !cache.hasExpired(node, now, value)) {
1✔
3810
          action.accept(value);
1✔
3811
        }
3812
      };
1✔
3813
      spliterator.forEachRemaining(consumer);
1✔
3814
    }
1✔
3815

3816
    @Override
3817
    public boolean tryAdvance(Consumer<? super V> action) {
3818
      requireNonNull(action);
1✔
3819
      boolean[] advanced = { false };
1✔
3820
      Consumer<Node<K, V>> consumer = node -> {
1✔
3821
        K key = node.getKey();
1✔
3822
        V value = node.getValue();
1✔
3823
        long now = cache.expirationTicker().read();
1✔
3824
        if ((key != null) && (value != null) && node.isAlive()
1✔
3825
            && !cache.hasExpired(node, now, value)) {
1✔
3826
          action.accept(value);
1✔
3827
          advanced[0] = true;
1✔
3828
        }
3829
      };
1✔
3830
      while (spliterator.tryAdvance(consumer)) {
1✔
3831
        if (advanced[0]) {
1✔
3832
          return true;
1✔
3833
        }
3834
      }
3835
      return false;
1✔
3836
    }
3837

3838
    @Override
3839
    public @Nullable Spliterator<V> trySplit() {
3840
      Spliterator<Node<K, V>> split = spliterator.trySplit();
1✔
3841
      return (split == null) ? null : new ValueSpliterator<>(cache, split);
1✔
3842
    }
3843

3844
    @Override
3845
    public long estimateSize() {
3846
      return spliterator.estimateSize();
1✔
3847
    }
3848

3849
    @Override
3850
    public int characteristics() {
3851
      return CONCURRENT | NONNULL;
1✔
3852
    }
3853
  }
3854

3855
  /** An adapter to safely externalize the entries. */
3856
  static final class EntrySetView<K, V> extends AbstractSet<Entry<K, V>> {
3857
    final BoundedLocalCache<K, V> cache;
3858

3859
    EntrySetView(BoundedLocalCache<K, V> cache) {
1✔
3860
      this.cache = requireNonNull(cache);
1✔
3861
    }
1✔
3862

3863
    @Override
3864
    public int size() {
3865
      return cache.size();
1✔
3866
    }
3867

3868
    @Override
3869
    public void clear() {
3870
      cache.clear();
1✔
3871
    }
1✔
3872

3873
    @Override
3874
    public boolean contains(Object o) {
3875
      if (!(o instanceof Entry<?, ?>)) {
1✔
3876
        return false;
1✔
3877
      }
3878
      var entry = (Entry<?, ?>) o;
1✔
3879
      var key = entry.getKey();
1✔
3880
      var value = entry.getValue();
1✔
3881
      if ((key == null) || (value == null)) {
1✔
3882
        return false;
1✔
3883
      }
3884
      Node<K, V> node = cache.data.get(cache.nodeFactory.newLookupKey(key));
1✔
3885
      if (node == null) {
1✔
3886
        return false;
1✔
3887
      }
3888
      V nodeValue = node.getValue();
1✔
3889
      return (nodeValue != null) && node.containsValue(value)
1✔
3890
          && !cache.hasExpired(node, cache.expirationTicker().read(), nodeValue);
1✔
3891
    }
3892

3893
    @Override
3894
    public boolean removeAll(Collection<?> collection) {
3895
      requireNonNull(collection);
1✔
3896
      @Var boolean modified = false;
1✔
3897
      if (cache.collectKeys() || ((collection instanceof Set<?>) && (collection.size() > size()))) {
1✔
3898
        for (var entry : this) {
1✔
3899
          if (collection.contains(entry)) {
1✔
3900
            modified |= remove(entry);
1✔
3901
          }
3902
        }
1✔
3903
      } else {
3904
        for (var item : collection) {
1✔
3905
          modified |= (item != null) && remove(item);
1✔
3906
        }
1✔
3907
      }
3908
      return modified;
1✔
3909
    }
3910

3911
    @Override
3912
    @SuppressWarnings("SuspiciousMethodCalls")
3913
    public boolean remove(Object o) {
3914
      if (!(o instanceof Entry<?, ?>)) {
1✔
3915
        return false;
1✔
3916
      }
3917
      var entry = (Entry<?, ?>) o;
1✔
3918
      var key = entry.getKey();
1✔
3919
      return (key != null) && cache.remove(key, entry.getValue());
1✔
3920
    }
3921

3922
    @Override
3923
    public boolean removeIf(Predicate<? super Entry<K, V>> filter) {
3924
      requireNonNull(filter);
1✔
3925
      @Var boolean modified = false;
1✔
3926
      for (Entry<K, V> entry : this) {
1✔
3927
        if (filter.test(entry)) {
1✔
3928
          modified |= cache.remove(entry.getKey(), entry.getValue());
1✔
3929
        }
3930
      }
1✔
3931
      return modified;
1✔
3932
    }
3933

3934
    @Override
3935
    public boolean retainAll(Collection<?> collection) {
3936
      requireNonNull(collection);
1✔
3937
      @Var boolean modified = false;
1✔
3938
      for (var entry : this) {
1✔
3939
        if (!collection.contains(entry) && remove(entry)) {
1✔
3940
          modified = true;
1✔
3941
        }
3942
      }
1✔
3943
      return modified;
1✔
3944
    }
3945

3946
    @Override
3947
    public Iterator<Entry<K, V>> iterator() {
3948
      return new EntryIterator<>(cache);
1✔
3949
    }
3950

3951
    @Override
3952
    public Spliterator<Entry<K, V>> spliterator() {
3953
      return new EntrySpliterator<>(cache);
1✔
3954
    }
3955
  }
3956

3957
  /** An adapter to safely externalize the entry iterator. */
3958
  static final class EntryIterator<K, V> implements Iterator<Entry<K, V>> {
3959
    final BoundedLocalCache<K, V> cache;
3960
    final Iterator<Node<K, V>> iterator;
3961

3962
    @Nullable K key;
3963
    @Nullable V value;
3964
    @Nullable K removalKey;
3965
    @Nullable Node<K, V> next;
3966

3967
    EntryIterator(BoundedLocalCache<K, V> cache) {
1✔
3968
      this.iterator = cache.data.values().iterator();
1✔
3969
      this.cache = cache;
1✔
3970
    }
1✔
3971

3972
    @Override
3973
    public boolean hasNext() {
3974
      if (next != null) {
1✔
3975
        return true;
1✔
3976
      }
3977

3978
      long now = cache.expirationTicker().read();
1✔
3979
      while (iterator.hasNext()) {
1✔
3980
        next = iterator.next();
1✔
3981
        value = next.getValue();
1✔
3982
        key = next.getKey();
1✔
3983

3984
        boolean evictable = (key == null) || (value == null) || cache.hasExpired(next, now, value);
1✔
3985
        if (evictable || !next.isAlive()) {
1✔
3986
          if (evictable) {
1✔
3987
            cache.scheduleDrainBuffers();
1✔
3988
          }
3989
          advance();
1✔
3990
          continue;
1✔
3991
        }
3992
        return true;
1✔
3993
      }
3994
      return false;
1✔
3995
    }
3996

3997
    /** Invalidates the current position so that the iterator may compute the next position. */
3998
    void advance() {
3999
      value = null;
1✔
4000
      next = null;
1✔
4001
      key = null;
1✔
4002
    }
1✔
4003

4004
    K nextKey() {
4005
      if (!hasNext()) {
1✔
4006
        throw new NoSuchElementException();
1✔
4007
      }
4008
      removalKey = key;
1✔
4009
      advance();
1✔
4010
      return requireNonNull(removalKey);
1✔
4011
    }
4012

4013
    V nextValue() {
4014
      if (!hasNext()) {
1✔
4015
        throw new NoSuchElementException();
1✔
4016
      }
4017
      removalKey = key;
1✔
4018
      V val = value;
1✔
4019
      advance();
1✔
4020
      return requireNonNull(val);
1✔
4021
    }
4022

4023
    @Override
4024
    public Entry<K, V> next() {
4025
      if (!hasNext()) {
1✔
4026
        throw new NoSuchElementException();
1✔
4027
      }
4028
      var entry = new WriteThroughEntry<K, @NonNull V>(
1✔
4029
          cache, requireNonNull(key), requireNonNull(value));
1✔
4030
      removalKey = key;
1✔
4031
      advance();
1✔
4032
      return entry;
1✔
4033
    }
4034

4035
    @Override
4036
    public void remove() {
4037
      if (removalKey == null) {
1✔
4038
        throw new IllegalStateException();
1✔
4039
      }
4040
      cache.remove(removalKey);
1✔
4041
      removalKey = null;
1✔
4042
    }
1✔
4043
  }
4044

4045
  /** An adapter to safely externalize the entry spliterator. */
4046
  static final class EntrySpliterator<K, V> implements Spliterator<Entry<K, V>> {
4047
    final Spliterator<Node<K, V>> spliterator;
4048
    final BoundedLocalCache<K, V> cache;
4049

4050
    EntrySpliterator(BoundedLocalCache<K, V> cache) {
4051
      this(cache, cache.data.values().spliterator());
1✔
4052
    }
1✔
4053

4054
    EntrySpliterator(BoundedLocalCache<K, V> cache, Spliterator<Node<K, V>> spliterator) {
1✔
4055
      this.spliterator = requireNonNull(spliterator);
1✔
4056
      this.cache = requireNonNull(cache);
1✔
4057
    }
1✔
4058

4059
    @Override
4060
    public void forEachRemaining(Consumer<? super Entry<K, V>> action) {
4061
      requireNonNull(action);
1✔
4062
      Consumer<Node<K, V>> consumer = node -> {
1✔
4063
        K key = node.getKey();
1✔
4064
        V value = node.getValue();
1✔
4065
        long now = cache.expirationTicker().read();
1✔
4066
        if ((key != null) && (value != null) && node.isAlive()
1✔
4067
            && !cache.hasExpired(node, now, value)) {
1✔
4068
          action.accept(new WriteThroughEntry<>(cache, key, value));
1✔
4069
        }
4070
      };
1✔
4071
      spliterator.forEachRemaining(consumer);
1✔
4072
    }
1✔
4073

4074
    @Override
4075
    public boolean tryAdvance(Consumer<? super Entry<K, V>> action) {
4076
      requireNonNull(action);
1✔
4077
      boolean[] advanced = { false };
1✔
4078
      Consumer<Node<K, V>> consumer = node -> {
1✔
4079
        K key = node.getKey();
1✔
4080
        V value = node.getValue();
1✔
4081
        long now = cache.expirationTicker().read();
1✔
4082
        if ((key != null) && (value != null) && node.isAlive()
1✔
4083
            && !cache.hasExpired(node, now, value)) {
1✔
4084
          action.accept(new WriteThroughEntry<>(cache, key, value));
1✔
4085
          advanced[0] = true;
1✔
4086
        }
4087
      };
1✔
4088
      while (spliterator.tryAdvance(consumer)) {
1✔
4089
        if (advanced[0]) {
1✔
4090
          return true;
1✔
4091
        }
4092
      }
4093
      return false;
1✔
4094
    }
4095

4096
    @Override
4097
    public @Nullable Spliterator<Entry<K, V>> trySplit() {
4098
      Spliterator<Node<K, V>> split = spliterator.trySplit();
1✔
4099
      return (split == null) ? null : new EntrySpliterator<>(cache, split);
1✔
4100
    }
4101

4102
    @Override
4103
    public long estimateSize() {
4104
      return spliterator.estimateSize();
1✔
4105
    }
4106

4107
    @Override
4108
    public int characteristics() {
4109
      return DISTINCT | CONCURRENT | NONNULL;
1✔
4110
    }
4111
  }
4112

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

4117
    final WeakReference<BoundedLocalCache<?, ?>> reference;
4118

4119
    PerformCleanupTask(BoundedLocalCache<?, ?> cache) {
1✔
4120
      reference = new WeakReference<>(cache);
1✔
4121
    }
1✔
4122

4123
    @Override
4124
    protected boolean exec() {
4125
      try {
4126
        run();
1✔
4127
      } catch (Throwable t) {
1✔
4128
        logger.log(Level.ERROR, "Exception thrown when performing the maintenance task", t);
1✔
4129
      }
1✔
4130

4131
      // Indicates that the task has not completed to allow subsequent submissions to execute
4132
      return false;
1✔
4133
    }
4134

4135
    @Override
4136
    public void run() {
4137
      BoundedLocalCache<?, ?> cache = reference.get();
1✔
4138
      if (cache != null) {
1✔
4139
        cache.performCleanUp(/* ignored */ null);
1✔
4140
      }
4141
    }
1✔
4142

4143
    /**
4144
     * This method cannot be ignored due to being final, so a hostile user supplied Executor could
4145
     * forcibly complete the task and halt future executions. There are easier ways to intentionally
4146
     * harm a system, so this is assumed to not happen in practice.
4147
     */
4148
    // public final void quietlyComplete() {}
4149

4150
    @Override public void complete(@Nullable Void value) {}
1✔
4151
    @Override public void setRawResult(@Nullable Void value) {}
1✔
4152
    @Override public @Nullable Void getRawResult() { return null; }
1✔
4153
    @Override public void completeExceptionally(@Nullable Throwable t) {}
1✔
4154
    @Override public boolean cancel(boolean mayInterruptIfRunning) { return false; }
1✔
4155
  }
4156

4157
  /** Creates a serialization proxy based on the common configuration shared by all cache types. */
4158
  static <K, V> SerializationProxy<K, V> makeSerializationProxy(BoundedLocalCache<?, ?> cache) {
4159
    var proxy = new SerializationProxy<K, V>();
1✔
4160
    proxy.weakKeys = cache.collectKeys();
1✔
4161
    proxy.weakValues = cache.nodeFactory.weakValues();
1✔
4162
    proxy.softValues = cache.nodeFactory.softValues();
1✔
4163
    proxy.isRecordingStats = cache.isRecordingStats();
1✔
4164
    proxy.evictionListener = cache.evictionListener;
1✔
4165
    proxy.removalListener = cache.removalListener();
1✔
4166
    proxy.ticker = cache.expirationTicker();
1✔
4167
    if (cache.expiresAfterAccess()) {
1✔
4168
      proxy.expiresAfterAccessNanos = cache.expiresAfterAccessNanos();
1✔
4169
    }
4170
    if (cache.expiresAfterWrite()) {
1✔
4171
      proxy.expiresAfterWriteNanos = cache.expiresAfterWriteNanos();
1✔
4172
    }
4173
    if (cache.expiresVariable()) {
1✔
4174
      proxy.expiry = cache.expiry();
1✔
4175
    }
4176
    if (cache.refreshAfterWrite()) {
1✔
4177
      proxy.refreshAfterWriteNanos = cache.refreshAfterWriteNanos();
1✔
4178
    }
4179
    if (cache.evicts()) {
1✔
4180
      if (cache.isWeighted) {
1✔
4181
        proxy.weigher = cache.weigher;
1✔
4182
        proxy.maximumWeight = cache.maximum();
1✔
4183
      } else {
4184
        proxy.maximumSize = cache.maximum();
1✔
4185
      }
4186
    }
4187
    proxy.cacheLoader = cache.cacheLoader;
1✔
4188
    proxy.async = cache.isAsync;
1✔
4189
    return proxy;
1✔
4190
  }
4191

4192
  /* --------------- Manual Cache --------------- */
4193

4194
  static class BoundedLocalManualCache<K, V> implements LocalManualCache<K, V>, Serializable {
4195
    private static final long serialVersionUID = 1;
4196

4197
    final BoundedLocalCache<K, V> cache;
4198

4199
    @Nullable Policy<K, V> policy;
4200

4201
    BoundedLocalManualCache(Caffeine<K, V> builder) {
4202
      this(builder, null);
1✔
4203
    }
1✔
4204

4205
    BoundedLocalManualCache(Caffeine<K, V> builder, @Nullable CacheLoader<? super K, V> loader) {
1✔
4206
      cache = LocalCacheFactory.newBoundedLocalCache(builder, loader, /* isAsync= */ false);
1✔
4207
    }
1✔
4208

4209
    @Override
4210
    public final BoundedLocalCache<K, V> cache() {
4211
      return cache;
1✔
4212
    }
4213

4214
    @Override
4215
    public final Policy<K, V> policy() {
4216
      if (policy == null) {
1✔
4217
        Function<@Nullable V, @Nullable V> identity = v -> v;
1✔
4218
        policy = new BoundedPolicy<>(cache, identity, cache.isWeighted);
1✔
4219
      }
4220
      return policy;
1✔
4221
    }
4222

4223
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
4224
      throw new InvalidObjectException("Proxy required");
1✔
4225
    }
4226

4227
    private Object writeReplace() {
4228
      return makeSerializationProxy(cache);
1✔
4229
    }
4230
  }
4231

4232
  @SuppressWarnings({"NullableOptional",
4233
    "OptionalAssignedToNull", "OptionalUsedAsFieldOrParameterType"})
4234
  static final class BoundedPolicy<K, V> implements Policy<K, V> {
4235
    final Function<@Nullable V, @Nullable V> transformer;
4236
    final BoundedLocalCache<K, V> cache;
4237
    final boolean isWeighted;
4238

4239
    @Nullable Optional<Eviction<K, V>> eviction;
4240
    @Nullable Optional<FixedRefresh<K, V>> refreshes;
4241
    @Nullable Optional<FixedExpiration<K, V>> afterWrite;
4242
    @Nullable Optional<FixedExpiration<K, V>> afterAccess;
4243
    @Nullable Optional<VarExpiration<K, V>> variable;
4244

4245
    BoundedPolicy(BoundedLocalCache<K, V> cache,
4246
        Function<@Nullable V, @Nullable V> transformer, boolean isWeighted) {
1✔
4247
      this.transformer = transformer;
1✔
4248
      this.isWeighted = isWeighted;
1✔
4249
      this.cache = cache;
1✔
4250
    }
1✔
4251

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

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

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

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

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

4550
        for (;;) {
4551
          var priorFuture = (CompletableFuture<V>) cache.getIfPresent(
1✔
4552
              key, /* recordStats= */ false);
4553
          if (priorFuture != null) {
1✔
4554
            if (!priorFuture.isDone()) {
1✔
4555
              Async.getWhenSuccessful(priorFuture);
1✔
4556
              continue;
1✔
4557
            }
4558

4559
            V prior = Async.getWhenSuccessful(priorFuture);
1✔
4560
            if (prior != null) {
1✔
4561
              return prior;
1✔
4562
            }
4563
          }
4564

4565
          boolean[] added = { false };
1✔
4566
          var hints = new LocalCache.RemapHints();
1✔
4567
          var computed = (CompletableFuture<V>) cache.compute(key, (K k, @Nullable V oldValue) -> {
1✔
4568
            var oldValueFuture = (CompletableFuture<V>) oldValue;
1✔
4569
            added[0] = (oldValueFuture == null)
1✔
4570
                || (oldValueFuture.isDone() && (Async.getIfReady(oldValueFuture) == null));
1✔
4571
            if (added[0]) {
1✔
4572
              return asyncValue;
1✔
4573
            }
4574
            hints.preserveTimestamps = true;
1✔
4575
            hints.preserveRefresh = true;
1✔
4576
            return oldValue;
1✔
4577
          }, expiry, /* recordLoad= */ false, /* recordLoadFailure= */ false, hints);
4578

4579
          if (added[0]) {
1✔
4580
            return null;
1✔
4581
          } else {
4582
            V prior = Async.getWhenSuccessful(computed);
1✔
4583
            if (prior != null) {
1✔
4584
              return prior;
1✔
4585
            }
4586
          }
4587
        }
1✔
4588
      }
4589
      @SuppressWarnings("unchecked")
4590
      @Nullable V putAsync(K key, V value, long duration, TimeUnit unit) {
4591
        var expiry = (Expiry<K, V>) new AsyncExpiry<>(new FixedExpireAfterWrite<>(duration, unit));
1✔
4592
        var asyncValue = (V) CompletableFuture.completedFuture(value);
1✔
4593

4594
        var oldValueFuture = (CompletableFuture<V>) cache.put(
1✔
4595
            key, asyncValue, expiry, /* onlyIfAbsent= */ false);
4596
        return Async.getWhenSuccessful(oldValueFuture);
1✔
4597
      }
4598
      @Override public @Nullable V compute(K key,
4599
          BiFunction<? super K, ? super V, ? extends V> remappingFunction,
4600
          Duration duration) {
4601
        requireNonNull(key);
1✔
4602
        requireNonNull(duration);
1✔
4603
        requireNonNull(remappingFunction);
1✔
4604
        requireArgument(!duration.isNegative(), "duration cannot be negative: %s", duration);
1✔
4605
        var expiry = new FixedExpireAfterWrite<K, V>(
1✔
4606
            toNanosSaturated(duration), TimeUnit.NANOSECONDS);
1✔
4607

4608
        return cache.isAsync
1✔
4609
            ? computeAsync(key, remappingFunction, expiry)
1✔
4610
            : cache.compute(key, remappingFunction, expiry,
1✔
4611
                /* recordLoad= */ true, /* recordLoadFailure= */ true);
4612
      }
4613
      @Nullable V computeAsync(K key,
4614
          BiFunction<? super K, ? super V, ? extends V> remappingFunction,
4615
          Expiry<? super K, ? super V> expiry) {
4616
        // Keep in sync with LocalAsyncCache.AsMapView#compute(key, remappingFunction)
4617
        @SuppressWarnings("unchecked")
4618
        var delegate = (LocalCache<K, CompletableFuture<V>>) cache;
1✔
4619

4620
        @SuppressWarnings({"rawtypes", "unchecked", "Varifier"})
4621
        @Nullable V[] newValue = (@Nullable V[]) new Object[1];
1✔
4622
        for (;;) {
4623
          Async.getWhenSuccessful(delegate.getIfPresentQuietly(key));
1✔
4624

4625
          var hints = new LocalCache.RemapHints();
1✔
4626
          CompletableFuture<V> valueFuture = delegate.compute(
1✔
4627
              key, (K k, @Nullable CompletableFuture<V> oldValueFuture) -> {
4628
                if ((oldValueFuture != null) && !oldValueFuture.isDone()) {
1✔
4629
                  hints.preserveTimestamps = true;
1✔
4630
                  hints.preserveRefresh = true;
1✔
4631
                  return oldValueFuture;
1✔
4632
                }
4633

4634
                V oldValue = Async.getIfReady(oldValueFuture);
1✔
4635
                BiFunction<? super K, ? super V, ? extends @Nullable V> function =
1✔
4636
                    delegate.statsAware(remappingFunction,
1✔
4637
                        /* recordLoad= */ true, /* recordLoadFailure= */ true);
4638
                newValue[0] = function.apply(key, oldValue);
1✔
4639
                return (newValue[0] == null) ? null
1✔
4640
                    : CompletableFuture.completedFuture(newValue[0]);
1✔
4641
              }, new AsyncExpiry<>(expiry), /* recordLoad= */ false,
4642
              /* recordLoadFailure= */ false, hints);
4643

4644
          if (newValue[0] != null) {
1✔
4645
            return newValue[0];
1✔
4646
          } else if (valueFuture == null) {
1✔
4647
            return null;
1✔
4648
          }
4649
        }
1✔
4650
      }
4651
      @Override public Map<K, V> oldest(int limit) {
4652
        return oldest(new SizeLimiter<>(Math.min(limit, cache.size()), limit));
1✔
4653
      }
4654
      @Override public <T> T oldest(Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4655
        return cache.snapshot(cache.timerWheel(), transformer, mappingFunction);
1✔
4656
      }
4657
      @Override public Map<K, V> youngest(int limit) {
4658
        return youngest(new SizeLimiter<>(Math.min(limit, cache.size()), limit));
1✔
4659
      }
4660
      @Override public <T> T youngest(Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4661
        return cache.snapshot(cache.timerWheel()::descendingIterator, transformer, mappingFunction);
1✔
4662
      }
4663
    }
4664

4665
    static final class FixedExpireAfterWrite<K, V> implements Expiry<K, V> {
4666
      final long duration;
4667
      final TimeUnit unit;
4668

4669
      FixedExpireAfterWrite(long duration, TimeUnit unit) {
1✔
4670
        this.duration = duration;
1✔
4671
        this.unit = unit;
1✔
4672
      }
1✔
4673
      @Override public long expireAfterCreate(K key, V value, long currentTime) {
4674
        return unit.toNanos(duration);
1✔
4675
      }
4676
      @Override public long expireAfterUpdate(
4677
          K key, V value, long currentTime, long currentDuration) {
4678
        return unit.toNanos(duration);
1✔
4679
      }
4680
      @CanIgnoreReturnValue
4681
      @Override public long expireAfterRead(
4682
          K key, V value, long currentTime, long currentDuration) {
4683
        return currentDuration;
1✔
4684
      }
4685
    }
4686

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

4719
  /* --------------- Loading Cache --------------- */
4720

4721
  static final class BoundedLocalLoadingCache<K, V>
4722
      extends BoundedLocalManualCache<K, V> implements LocalLoadingCache<K, V> {
4723
    private static final long serialVersionUID = 1;
4724

4725
    final Function<K, @Nullable V> mappingFunction;
4726
    final @Nullable Function<Set<? extends K>, Map<K, V>> bulkMappingFunction;
4727

4728
    BoundedLocalLoadingCache(Caffeine<K, V> builder, CacheLoader<? super K, V> loader) {
4729
      super(builder, loader);
1✔
4730
      requireNonNull(loader);
1✔
4731
      mappingFunction = newMappingFunction(loader);
1✔
4732
      bulkMappingFunction = newBulkMappingFunction(loader);
1✔
4733
    }
1✔
4734

4735
    @Override
4736
    @SuppressWarnings({"DataFlowIssue", "NullAway"})
4737
    public AsyncCacheLoader<? super K, V> cacheLoader() {
4738
      return cache.cacheLoader;
1✔
4739
    }
4740

4741
    @Override
4742
    public Function<K, @Nullable V> mappingFunction() {
4743
      return mappingFunction;
1✔
4744
    }
4745

4746
    @Override
4747
    public @Nullable Function<Set<? extends K>, Map<K, V>> bulkMappingFunction() {
4748
      return bulkMappingFunction;
1✔
4749
    }
4750

4751
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
4752
      throw new InvalidObjectException("Proxy required");
1✔
4753
    }
4754

4755
    private Object writeReplace() {
4756
      return makeSerializationProxy(cache);
1✔
4757
    }
4758
  }
4759

4760
  /* --------------- Async Cache --------------- */
4761

4762
  static final class BoundedLocalAsyncCache<K, V> implements LocalAsyncCache<K, V>, Serializable {
4763
    private static final long serialVersionUID = 1;
4764

4765
    final BoundedLocalCache<K, CompletableFuture<V>> cache;
4766
    final boolean isWeighted;
4767

4768
    @Nullable ConcurrentMap<K, CompletableFuture<V>> mapView;
4769
    @Nullable CacheView<K, V> cacheView;
4770
    @Nullable Policy<K, V> policy;
4771

4772
    @SuppressWarnings("unchecked")
4773
    BoundedLocalAsyncCache(Caffeine<K, V> builder) {
1✔
4774
      cache = (BoundedLocalCache<K, CompletableFuture<V>>) LocalCacheFactory
1✔
4775
          .newBoundedLocalCache(builder, /* cacheLoader= */ null, /* isAsync= */ true);
1✔
4776
      isWeighted = builder.isWeighted();
1✔
4777
    }
1✔
4778

4779
    @Override
4780
    public BoundedLocalCache<K, CompletableFuture<V>> cache() {
4781
      return cache;
1✔
4782
    }
4783

4784
    @Override
4785
    public ConcurrentMap<K, CompletableFuture<V>> asMap() {
4786
      return (mapView == null) ? (mapView = new AsyncAsMapView<>(this)) : mapView;
1✔
4787
    }
4788

4789
    @Override
4790
    public Cache<K, V> synchronous() {
4791
      return (cacheView == null) ? (cacheView = new CacheView<>(this)) : cacheView;
1✔
4792
    }
4793

4794
    @Override
4795
    public Policy<K, V> policy() {
4796
      if (policy == null) {
1✔
4797
        @SuppressWarnings("unchecked")
4798
        var castCache = (BoundedLocalCache<K, V>) cache;
1✔
4799
        Function<CompletableFuture<V>, @Nullable V> transformer = Async::getIfReady;
1✔
4800
        @SuppressWarnings("unchecked")
4801
        var castTransformer = (Function<@Nullable V, @Nullable V>) transformer;
1✔
4802
        policy = new BoundedPolicy<>(castCache, castTransformer, isWeighted);
1✔
4803
      }
4804
      return policy;
1✔
4805
    }
4806

4807
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
4808
      throw new InvalidObjectException("Proxy required");
1✔
4809
    }
4810

4811
    private Object writeReplace() {
4812
      return makeSerializationProxy(cache);
1✔
4813
    }
4814
  }
4815

4816
  /* --------------- Async Loading Cache --------------- */
4817

4818
  static final class BoundedLocalAsyncLoadingCache<K, V>
4819
      extends LocalAsyncLoadingCache<K, V> implements Serializable {
4820
    private static final long serialVersionUID = 1;
4821

4822
    final BoundedLocalCache<K, CompletableFuture<V>> cache;
4823
    final boolean isWeighted;
4824

4825
    @Nullable ConcurrentMap<K, CompletableFuture<V>> mapView;
4826
    @Nullable Policy<K, V> policy;
4827

4828
    @SuppressWarnings("unchecked")
4829
    BoundedLocalAsyncLoadingCache(Caffeine<K, V> builder, AsyncCacheLoader<? super K, V> loader) {
4830
      super(loader);
1✔
4831
      isWeighted = builder.isWeighted();
1✔
4832
      cache = (BoundedLocalCache<K, CompletableFuture<V>>) LocalCacheFactory
1✔
4833
          .newBoundedLocalCache(builder, loader, /* isAsync= */ true);
1✔
4834
    }
1✔
4835

4836
    @Override
4837
    public BoundedLocalCache<K, CompletableFuture<V>> cache() {
4838
      return cache;
1✔
4839
    }
4840

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

4846
    @Override
4847
    public Policy<K, V> policy() {
4848
      if (policy == null) {
1✔
4849
        @SuppressWarnings("unchecked")
4850
        var castCache = (BoundedLocalCache<K, V>) cache;
1✔
4851
        Function<CompletableFuture<V>, @Nullable V> transformer = Async::getIfReady;
1✔
4852
        @SuppressWarnings("unchecked")
4853
        var castTransformer = (Function<@Nullable V, @Nullable V>) transformer;
1✔
4854
        policy = new BoundedPolicy<>(castCache, castTransformer, isWeighted);
1✔
4855
      }
4856
      return policy;
1✔
4857
    }
4858

4859
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
4860
      throw new InvalidObjectException("Proxy required");
1✔
4861
    }
4862

4863
    private Object writeReplace() {
4864
      return makeSerializationProxy(cache);
1✔
4865
    }
4866
  }
4867
}
4868

4869
/** The namespace for field padding through inheritance. */
4870
@SuppressWarnings({"IdentifierName", "MultiVariableDeclaration"})
4871
final class BLCHeader {
4872

4873
  private BLCHeader() {}
4874

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

4894
  /** Enforces a memory layout to avoid false sharing by padding the drain status. */
4895
  abstract static class DrainStatusRef extends PadDrainStatus {
1✔
4896
    static final VarHandle DRAIN_STATUS = fieldVarHandle(MethodHandles.lookup(),
1✔
4897
        "drainStatus", VarHandle.class, DrainStatusRef.class, int.class);
4898

4899
    /** A drain is not taking place. */
4900
    static final int IDLE = 0;
4901
    /** A drain is required due to a pending write modification. */
4902
    static final int REQUIRED = 1;
4903
    /** A drain is in progress and will transition to idle. */
4904
    static final int PROCESSING_TO_IDLE = 2;
4905
    /** A drain is in progress and will transition to required. */
4906
    static final int PROCESSING_TO_REQUIRED = 3;
4907

4908
    /** The draining status of the buffers. */
4909
    volatile int drainStatus = IDLE;
1✔
4910

4911
    /**
4912
     * Returns whether maintenance work is needed.
4913
     *
4914
     * @param delayable if draining the read buffer can be delayed
4915
     */
4916
    @SuppressWarnings("StatementSwitchToExpressionSwitch")
4917
    boolean shouldDrainBuffers(boolean delayable) {
4918
      switch (drainStatusOpaque()) {
1✔
4919
        case IDLE:
4920
          return !delayable;
1✔
4921
        case REQUIRED:
4922
          return true;
1✔
4923
        case PROCESSING_TO_IDLE:
4924
        case PROCESSING_TO_REQUIRED:
4925
          return false;
1✔
4926
        default:
4927
          throw new IllegalStateException("Invalid drain status: " + drainStatus);
1✔
4928
      }
4929
    }
4930

4931
    int drainStatusOpaque() {
4932
      return (int) DRAIN_STATUS.getOpaque(this);
1✔
4933
    }
4934

4935
    int drainStatusAcquire() {
4936
      return (int) DRAIN_STATUS.getAcquire(this);
1✔
4937
    }
4938

4939
    void setDrainStatusOpaque(int drainStatus) {
4940
      DRAIN_STATUS.setOpaque(this, drainStatus);
1✔
4941
    }
1✔
4942

4943
    void setDrainStatusRelease(int drainStatus) {
4944
      DRAIN_STATUS.setRelease(this, drainStatus);
1✔
4945
    }
1✔
4946

4947
    boolean casDrainStatus(int expect, int update) {
4948
      return DRAIN_STATUS.compareAndSet(this, expect, update);
1✔
4949
    }
4950
  }
4951
}
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