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

ben-manes / caffeine / #5707

02 Aug 2026 12:45AM UTC coverage: 0.106% (-99.9%) from 100.0%
#5707

push

github

ben-manes
fix wikibench trace reader after overly strict audit fixes

0 of 4235 branches covered (0.0%)

9 of 8528 relevant lines covered (0.11%)

0.0 hits per line

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

0.0
/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());
×
209

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

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

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

267
  final boolean isWeighted;
268
  final boolean isAsync;
269

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

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

297
    if (evicts()) {
×
298
      setMaximumSize(builder.getMaximum());
×
299
    }
300
  }
×
301

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

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

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

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

334
  /* --------------- Shared --------------- */
335

336
  @Override
337
  public boolean isAsync() {
338
    return isAsync;
×
339
  }
340

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

346
  @GuardedBy("evictionLock")
347
  protected AccessOrderDeque<Node<K, V>> accessOrderWindowDeque() {
348
    throw new UnsupportedOperationException();
×
349
  }
350

351
  @GuardedBy("evictionLock")
352
  protected AccessOrderDeque<Node<K, V>> accessOrderProbationDeque() {
353
    throw new UnsupportedOperationException();
×
354
  }
355

356
  @GuardedBy("evictionLock")
357
  protected AccessOrderDeque<Node<K, V>> accessOrderProtectedDeque() {
358
    throw new UnsupportedOperationException();
×
359
  }
360

361
  @GuardedBy("evictionLock")
362
  protected WriteOrderDeque<Node<K, V>> writeOrderDeque() {
363
    throw new UnsupportedOperationException();
×
364
  }
365

366
  @Override
367
  public final Executor executor() {
368
    return executor;
×
369
  }
370

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

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

392
  @Override
393
  public Object referenceKey(K key) {
394
    return nodeFactory.newLookupKey(key);
×
395
  }
396

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

407
  /* --------------- Stats Support --------------- */
408

409
  @Override
410
  public boolean isRecordingStats() {
411
    return false;
×
412
  }
413

414
  @Override
415
  public StatsCounter statsCounter() {
416
    return StatsCounter.disabledStatsCounter();
×
417
  }
418

419
  @Override
420
  public Ticker statsTicker() {
421
    return Ticker.disabledTicker();
×
422
  }
423

424
  /* --------------- Removal Listener Support --------------- */
425

426
  @Override
427
  public @Nullable RemovalListener<K, V> removalListener() {
428
    return null;
×
429
  }
430

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

452
  /* --------------- Eviction Listener Support --------------- */
453

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

465
  /* --------------- Reference Support --------------- */
466

467
  @Override
468
  public boolean collectKeys() {
469
    return false;
×
470
  }
471

472
  /** Returns if the values are weak or soft reference garbage collected. */
473
  protected boolean collectValues() {
474
    return false;
×
475
  }
476

477
  @SuppressWarnings({"DataFlowIssue", "NullAway"})
478
  protected ReferenceQueue<K> keyReferenceQueue() {
479
    return null;
×
480
  }
481

482
  @SuppressWarnings({"DataFlowIssue", "NullAway"})
483
  protected ReferenceQueue<V> valueReferenceQueue() {
484
    return null;
×
485
  }
486

487
  /* --------------- Expiration Support --------------- */
488

489
  /** Returns the {@link Pacer} used to schedule the maintenance task. */
490
  protected @Nullable Pacer pacer() {
491
    return null;
×
492
  }
493

494
  /** Returns if the cache expires entries after a variable time threshold. */
495
  protected boolean expiresVariable() {
496
    return false;
×
497
  }
498

499
  /** Returns if the cache expires entries after an access time threshold. */
500
  protected boolean expiresAfterAccess() {
501
    return false;
×
502
  }
503

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

509
  protected void setExpiresAfterAccessNanos(long expireAfterAccessNanos) {
510
    throw new UnsupportedOperationException();
×
511
  }
512

513
  /** Returns if the cache expires entries after a write time threshold. */
514
  protected boolean expiresAfterWrite() {
515
    return false;
×
516
  }
517

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

523
  protected void setExpiresAfterWriteNanos(long expireAfterWriteNanos) {
524
    throw new UnsupportedOperationException();
×
525
  }
526

527
  /** Returns if the cache refreshes entries after a write time threshold. */
528
  protected boolean refreshAfterWrite() {
529
    return false;
×
530
  }
531

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

537
  protected void setRefreshAfterWriteNanos(long refreshAfterWriteNanos) {
538
    throw new UnsupportedOperationException();
×
539
  }
540

541
  @Override
542
  @SuppressWarnings({"DataFlowIssue", "NullAway"})
543
  public Expiry<K, V> expiry() {
544
    return null;
×
545
  }
546

547
  /** Returns the {@link Ticker} used by this cache for expiration. */
548
  public Ticker expirationTicker() {
549
    return Ticker.disabledTicker();
×
550
  }
551

552
  protected TimerWheel<K, V> timerWheel() {
553
    throw new UnsupportedOperationException();
×
554
  }
555

556
  /* --------------- Eviction Support --------------- */
557

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

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

568
  protected FrequencySketch frequencySketch() {
569
    throw new UnsupportedOperationException();
×
570
  }
571

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

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

582
  /** Returns the maximum weighted size. */
583
  protected long maximumAcquire() {
584
    throw new UnsupportedOperationException();
×
585
  }
586

587
  /** Returns the maximum weighted size of the window space. */
588
  protected long windowMaximum() {
589
    throw new UnsupportedOperationException();
×
590
  }
591

592
  /** Returns the maximum weighted size of the main's protected space. */
593
  protected long mainProtectedMaximum() {
594
    throw new UnsupportedOperationException();
×
595
  }
596

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

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

607
  @GuardedBy("evictionLock")
608
  protected void setMainProtectedMaximum(long maximum) {
609
    throw new UnsupportedOperationException();
×
610
  }
611

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

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

622
  /** Returns the uncorrected combined weight of the values in the window space. */
623
  protected long windowWeightedSize() {
624
    throw new UnsupportedOperationException();
×
625
  }
626

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

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

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

642
  @GuardedBy("evictionLock")
643
  protected void setMainProtectedWeightedSize(long weightedSize) {
644
    throw new UnsupportedOperationException();
×
645
  }
646

647
  protected long hitsInSample() {
648
    throw new UnsupportedOperationException();
×
649
  }
650

651
  protected long missesInSample() {
652
    throw new UnsupportedOperationException();
×
653
  }
654

655
  protected double stepSize() {
656
    throw new UnsupportedOperationException();
×
657
  }
658

659
  protected double previousSampleHitRate() {
660
    throw new UnsupportedOperationException();
×
661
  }
662

663
  protected long adjustment() {
664
    throw new UnsupportedOperationException();
×
665
  }
666

667
  @GuardedBy("evictionLock")
668
  protected void setHitsInSample(long hitCount) {
669
    throw new UnsupportedOperationException();
×
670
  }
671

672
  @GuardedBy("evictionLock")
673
  protected void setMissesInSample(long missCount) {
674
    throw new UnsupportedOperationException();
×
675
  }
676

677
  @GuardedBy("evictionLock")
678
  protected void setStepSize(double stepSize) {
679
    throw new UnsupportedOperationException();
×
680
  }
681

682
  @GuardedBy("evictionLock")
683
  protected void setPreviousSampleHitRate(double hitRate) {
684
    throw new UnsupportedOperationException();
×
685
  }
686

687
  @GuardedBy("evictionLock")
688
  protected void setAdjustment(long amount) {
689
    throw new UnsupportedOperationException();
×
690
  }
691

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

704
    long max = Math.min(maximum, MAXIMUM_CAPACITY);
×
705
    long window = max - (long) (PERCENT_MAIN * max);
×
706
    long mainProtected = (long) (PERCENT_MAIN_PROTECTED * (max - window));
×
707
    double stepSize = Math.max(HILL_CLIMBER_STEP_PERCENT * max, HILL_CLIMBER_MIN_INITIAL_STEP);
×
708

709
    setMaximum(max);
×
710
    setAdjustment(0);
×
711
    setHitsInSample(0);
×
712
    setMissesInSample(0);
×
713
    setWindowMaximum(window);
×
714
    setPreviousSampleHitRate(0.0);
×
715
    setMainProtectedMaximum(mainProtected);
×
716
    setStepSize((max <= SMALL_CACHE_THRESHOLD) ? stepSize : -stepSize);
×
717

718
    if ((frequencySketch() != null) && !isWeighted() && (weightedSize() >= (max >>> 1))) {
×
719
      // Lazily initialize when close to the maximum size
720
      frequencySketch().ensureCapacity(max);
×
721
    }
722
  }
×
723

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

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

750
      Node<K, V> next = node.getNextInAccessOrder();
×
751
      if (node.getPolicyWeight() != 0) {
×
752
        node.makeMainProbation();
×
753
        accessOrderWindowDeque().remove(node);
×
754
        accessOrderProbationDeque().offerLast(node);
×
755
        if (first == null) {
×
756
          first = node;
×
757
        }
758

759
        setWindowWeightedSize(windowWeightedSize() - node.getPolicyWeight());
×
760
      }
761
      node = next;
×
762
    }
×
763

764
    return first;
×
765
  }
766

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

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

808
        // The pending operations will adjust the size to reflect the correct weight
809
        break;
810
      }
811

812
      // Skip over entries with zero weight
813
      if ((victim != null) && (victim.getPolicyWeight() == 0)) {
×
814
        victim = victim.getNextInAccessOrder();
×
815
        continue;
×
816
      } else if ((candidate != null) && (candidate.getPolicyWeight() == 0)) {
×
817
        candidate = candidate.getNextInAccessOrder();
×
818
        continue;
×
819
      }
820

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

836
      // Evict immediately if both selected the same entry
837
      if (candidate == victim) {
×
838
        victim = victim.getNextInAccessOrder();
×
839
        evictEntry(candidate, RemovalCause.SIZE, 0L);
×
840
        candidate = null;
×
841
        continue;
×
842
      }
843

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

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

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

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

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

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

928
    Pacer pacer = pacer();
×
929
    if (pacer != null) {
×
930
      long delay = getExpirationDelay(now);
×
931
      if (delay == Long.MAX_VALUE) {
×
932
        pacer.cancel();
×
933
      } else {
934
        pacer.schedule(executor, drainBuffersTask, now, delay);
×
935
      }
936
    }
937
  }
×
938

939
  /** Expires entries in the access-order queue. */
940
  @GuardedBy("evictionLock")
941
  void expireAfterAccessEntries(long now) {
942
    if (!expiresAfterAccess()) {
×
943
      return;
×
944
    }
945

946
    @Var int remaining = EXPIRATION_THRESHOLD;
×
947
    remaining = expireAfterAccessEntries(now, accessOrderWindowDeque(), remaining);
×
948
    if (evicts()) {
×
949
      remaining = expireAfterAccessEntries(now, accessOrderProbationDeque(), remaining);
×
950
      remaining = expireAfterAccessEntries(now, accessOrderProtectedDeque(), remaining);
×
951
    }
952
    if (remaining == 0) {
×
953
      setDrainStatusOpaque(PROCESSING_TO_REQUIRED);
×
954
    }
955
  }
×
956

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

988
  /** Expires entries on the write-order queue. */
989
  @GuardedBy("evictionLock")
990
  void expireAfterWriteEntries(long now) {
991
    if (!expiresAfterWrite()) {
×
992
      return;
×
993
    }
994

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

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

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

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

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

1093
    data.computeIfPresent(keyReference, (k, n) -> {
×
1094
      if (n != node) {
×
1095
        return n;
×
1096
      }
1097
      synchronized (node) {
×
1098
        ctx.value = node.getValue();
×
1099

1100
        if ((key == null) || (ctx.value == null)) {
×
1101
          ctx.cause = RemovalCause.COLLECTED;
×
1102
        } else if (cause == RemovalCause.COLLECTED) {
×
1103
          ctx.resurrect = true;
×
1104
          return node;
×
1105
        } else {
1106
          ctx.cause = cause;
×
1107
        }
1108

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

1141
        notifyEviction(key, ctx.value, ctx.cause);
×
1142
        discardRefresh(keyReference);
×
1143
        ctx.removed = true;
×
1144
        node.retire();
×
1145
        return null;
×
1146
      }
1147
    });
1148

1149
    // The entry is no longer eligible for eviction
1150
    if (ctx.resurrect) {
×
1151
      return false;
×
1152
    }
1153

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

1173
    synchronized (node) {
×
1174
      logIfAlive(node);
×
1175
      makeDead(node);
×
1176
    }
×
1177

1178
    if (ctx.removed) {
×
1179
      var removeCause = requireNonNull(ctx.cause);
×
1180
      statsCounter().recordEviction(node.getWeight(), removeCause);
×
1181
      notifyRemoval(key, ctx.value, removeCause);
×
1182
    }
1183

1184
    return true;
×
1185
  }
1186

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

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

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

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

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

1261
    @SuppressWarnings("MathClampLong")
1262
    @Var long quota = Math.min(adjustment(), mainProtectedMaximum());
×
1263
    setMainProtectedMaximum(mainProtectedMaximum() - quota);
×
1264
    setWindowMaximum(windowMaximum() + quota);
×
1265
    demoteFromMainProtected();
×
1266

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

1278
      int weight = candidate.getPolicyWeight();
×
1279
      if (quota < weight) {
×
1280
        break;
×
1281
      }
1282

1283
      quota -= weight;
×
1284
      if (probation) {
×
1285
        accessOrderProbationDeque().remove(candidate);
×
1286
      } else {
1287
        setMainProtectedWeightedSize(mainProtectedWeightedSize() - weight);
×
1288
        accessOrderProtectedDeque().remove(candidate);
×
1289
      }
1290
      setWindowWeightedSize(windowWeightedSize() + weight);
×
1291
      accessOrderWindowDeque().offerLast(candidate);
×
1292
      candidate.makeWindow();
×
1293
    }
1294

1295
    setMainProtectedMaximum(mainProtectedMaximum() + quota);
×
1296
    setWindowMaximum(windowMaximum() - quota);
×
1297
    setAdjustment(quota);
×
1298
  }
×
1299

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

1307
    @SuppressWarnings("MathClampLong")
1308
    @Var long quota = Math.min(-adjustment(), Math.max(0, windowMaximum() - 1));
×
1309
    setMainProtectedMaximum(mainProtectedMaximum() + quota);
×
1310
    setWindowMaximum(windowMaximum() - quota);
×
1311

1312
    for (int i = 0; i < QUEUE_TRANSFER_THRESHOLD; i++) {
×
1313
      Node<K, V> candidate = accessOrderWindowDeque().peekFirst();
×
1314
      if (candidate == null) {
×
1315
        break;
×
1316
      }
1317

1318
      int weight = candidate.getPolicyWeight();
×
1319
      if (quota < weight) {
×
1320
        break;
×
1321
      }
1322

1323
      quota -= weight;
×
1324
      setWindowWeightedSize(windowWeightedSize() - weight);
×
1325
      accessOrderWindowDeque().remove(candidate);
×
1326
      accessOrderProbationDeque().offerLast(candidate);
×
1327
      candidate.makeMainProbation();
×
1328
    }
1329

1330
    setMainProtectedMaximum(mainProtectedMaximum() - quota);
×
1331
    setWindowMaximum(windowMaximum() + quota);
×
1332
    setAdjustment(-quota);
×
1333
  }
×
1334

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

1344
    for (int i = 0; i < QUEUE_TRANSFER_THRESHOLD; i++) {
×
1345
      if (mainProtectedWeightedSize <= mainProtectedMaximum) {
×
1346
        break;
×
1347
      }
1348

1349
      Node<K, V> demoted = accessOrderProtectedDeque().pollFirst();
×
1350
      if (demoted == null) {
×
1351
        break;
×
1352
      }
1353
      demoted.makeMainProbation();
×
1354
      accessOrderProbationDeque().offerLast(demoted);
×
1355
      mainProtectedWeightedSize -= demoted.getPolicyWeight();
×
1356
    }
1357
    setMainProtectedWeightedSize(mainProtectedWeightedSize);
×
1358
  }
×
1359

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

1373
    boolean delayable = skipReadBuffer() || (readBuffer.offer(node) != Buffer.FULL);
×
1374
    if (shouldDrainBuffers(delayable)) {
×
1375
      scheduleDrainBuffers();
×
1376
    }
1377
    return refreshIfNeeded(node, now);
×
1378
  }
1379

1380
  /** Returns if the cache should bypass the read buffer. */
1381
  boolean skipReadBuffer() {
1382
    return fastpath() && frequencySketch().isNotInitialized();
×
1383
  }
1384

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

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

1449
      if (refreshFuture[0] == null) {
×
1450
        return null;
×
1451
      }
1452

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

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

1511
        if (cause[0] != null) {
×
1512
          notifyRemoval(key, value, cause[0]);
×
1513
        }
1514
        if (newValue == null) {
×
1515
          statsCounter().recordLoadFailure(loadTime);
×
1516
        } else {
1517
          statsCounter().recordLoadSuccess(loadTime);
×
1518
        }
1519
        return result;
×
1520
      });
1521
      return Async.getIfReady(refreshed);
×
1522
    }
1523

1524
    return null;
×
1525
  }
1526

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

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

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

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

1599
    long variableTime = node.getVariableTime();
×
1600
    long currentDuration = Math.max(1, variableTime - now);
×
1601
    if (isAsync && (currentDuration > MAXIMUM_EXPIRY)) {
×
1602
      // expireAfterCreate has not yet set the duration after completion
1603
      return;
×
1604
    }
1605

1606
    long tolerance = EXPIRE_TOLERANCE;
×
1607
    long duration = Math.max(0L, expiry.expireAfterRead(key, value, now, currentDuration));
×
1608
    long expirationTime = isAsync ? (now + duration) : (now + Math.min(duration, MAXIMUM_EXPIRY));
×
1609
    if (((duration <= tolerance) || (Math.abs(expirationTime - variableTime) > tolerance))
×
1610
        && (node.getValue() == value)) {
×
1611
      node.casVariableTime(variableTime, expirationTime);
×
1612
    }
1613
  }
×
1614

1615
  void setVariableTime(Node<K, V> node, long expirationTime) {
1616
    if (expiresVariable()) {
×
1617
      node.setVariableTime(expirationTime);
×
1618
    }
1619
  }
×
1620

1621
  void setWriteTime(Node<K, V> node, long now) {
1622
    if (expiresAfterWrite() || refreshAfterWrite()) {
×
1623
      node.setWriteTime(now & ~1L);
×
1624
    }
1625
  }
×
1626

1627
  void setAccessTime(Node<K, V> node, long now) {
1628
    if (!expiresAfterAccess()) {
×
1629
      return;
×
1630
    }
1631
    long tolerance = EXPIRE_TOLERANCE;
×
1632
    long accessTime = node.getAccessTime();
×
1633
    if ((expiresAfterAccessNanos() <= tolerance) || (Math.abs(now - accessTime) > tolerance)) {
×
1634
      node.setAccessTime(now);
×
1635
    }
1636
  }
×
1637

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

1651
  /**
1652
   * Performs the post-processing work required after a write.
1653
   *
1654
   * @param task the pending operation to be applied
1655
   */
1656
  void afterWrite(Runnable task) {
1657
    if (writeBuffer.offer(task)) {
×
1658
      scheduleAfterWrite();
×
1659
      return;
×
1660
    }
1661

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

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

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

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

1767
  @Override
1768
  public void cleanUp() {
1769
    try {
1770
      performCleanUp(/* ignored */ null);
×
1771
    } catch (RuntimeException e) {
×
1772
      logger.log(Level.ERROR, "Exception thrown when performing the maintenance task", e);
×
1773
    }
×
1774
  }
×
1775

1776
  /**
1777
   * Performs the maintenance work, blocking until the lock is acquired.
1778
   *
1779
   * @param task an additional pending task to run, or {@code null} if not present
1780
   */
1781
  void performCleanUp(@Nullable Runnable task) {
1782
    evictionLock.lock();
×
1783
    try {
1784
      maintenance(task);
×
1785
    } finally {
1786
      evictionLock.unlock();
×
1787
    }
1788
    rescheduleCleanUpIfIncomplete();
×
1789
  }
×
1790

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

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

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

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

1835
    try {
1836
      try {
1837
        drainReadBuffer();
×
1838
        drainWriteBuffer();
×
1839
      } finally {
1840
        if (task != null) {
×
1841
          task.run();
×
1842
        }
1843
      }
1844

1845
      drainKeyReferences();
×
1846
      drainValueReferences();
×
1847

1848
      expireEntries();
×
1849
      evictEntries();
×
1850

1851
      climb();
×
1852
    } finally {
1853
      if ((drainStatusOpaque() != PROCESSING_TO_IDLE)
×
1854
          || !casDrainStatus(PROCESSING_TO_IDLE, IDLE)) {
×
1855
        setDrainStatusOpaque(REQUIRED);
×
1856
      }
1857
    }
1858
  }
×
1859

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

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

1892
  /** Drains the read buffer. */
1893
  @GuardedBy("evictionLock")
1894
  void drainReadBuffer() {
1895
    if (!skipReadBuffer()) {
×
1896
      readBuffer.drainTo(accessPolicy);
×
1897
    }
1898
  }
×
1899

1900
  /**
1901
   * Updates the node's location in the page replacement policy.
1902
   * <p>
1903
   * A quiet access reorders but does not record the usage with the admission filter or the adaptive
1904
   * climber's sample, as an asynchronous cache's load completion finalizes the entry's weight
1905
   * rather than conveying a usage; counting it once per load skews the admission frequencies and
1906
   * inflates the climber's hit sample.
1907
   */
1908
  @GuardedBy("evictionLock")
1909
  void onAccess(Node<K, V> node, boolean quietly) {
1910
    if (evicts()) {
×
1911
      var keyRef = node.getKeyReferenceOrNull();
×
1912
      if ((keyRef == null) || !node.isAlive()) {
×
1913
        return;
×
1914
      }
1915
      if (!quietly) {
×
1916
        frequencySketch().increment(keyRef);
×
1917
      }
1918
      if (node.inWindow()) {
×
1919
        reorder(accessOrderWindowDeque(), node);
×
1920
      } else if (node.inMainProbation()) {
×
1921
        reorderProbation(node);
×
1922
      } else {
1923
        reorder(accessOrderProtectedDeque(), node);
×
1924
      }
1925
      if (!quietly) {
×
1926
        setHitsInSample(hitsInSample() + 1);
×
1927
      }
1928
    } else if (expiresAfterAccess()) {
×
1929
      reorder(accessOrderWindowDeque(), node);
×
1930
    }
1931
    if (expiresVariable()) {
×
1932
      timerWheel().reschedule(node);
×
1933
    }
1934
  }
×
1935

1936
  /** Promote the node from probation to protected on an access. */
1937
  @GuardedBy("evictionLock")
1938
  void reorderProbation(Node<K, V> node) {
1939
    if (!accessOrderProbationDeque().contains(node)) {
×
1940
      // Ignore stale accesses for an entry that is no longer present
1941
      return;
×
1942
    } else if (node.getPolicyWeight() > mainProtectedMaximum()) {
×
1943
      reorder(accessOrderProbationDeque(), node);
×
1944
      return;
×
1945
    }
1946

1947
    // If the protected space exceeds its maximum, the LRU items are demoted to the probation space.
1948
    // This is deferred to the adaption phase at the end of the maintenance cycle.
1949
    setMainProtectedWeightedSize(mainProtectedWeightedSize() + node.getPolicyWeight());
×
1950
    accessOrderProbationDeque().remove(node);
×
1951
    accessOrderProtectedDeque().offerLast(node);
×
1952
    node.makeMainProtected();
×
1953
  }
×
1954

1955
  /** Updates the node's location in the policy's deque. */
1956
  static <K, V> void reorder(LinkedDeque<Node<K, V>> deque, Node<K, V> node) {
1957
    // An entry may be scheduled for reordering despite having been removed. This can occur when the
1958
    // entry was concurrently read while a writer was removing it. If the entry is no longer linked
1959
    // then it does not need to be processed.
1960
    if (deque.contains(node)) {
×
1961
      deque.moveToBack(node);
×
1962
    }
1963
  }
×
1964

1965
  /** Drains the write buffer. */
1966
  @GuardedBy("evictionLock")
1967
  void drainWriteBuffer() {
1968
    for (int i = 0; i <= WRITE_BUFFER_MAX; i++) {
×
1969
      Runnable task = writeBuffer.poll();
×
1970
      if (task == null) {
×
1971
        return;
×
1972
      }
1973
      task.run();
×
1974
    }
1975
    setDrainStatusOpaque(PROCESSING_TO_REQUIRED);
×
1976
  }
×
1977

1978
  /**
1979
   * Atomically transitions the node to the <code>dead</code> state and decrements the
1980
   * <code>weightedSize</code>.
1981
   *
1982
   * @param node the entry in the page replacement policy
1983
   */
1984
  @GuardedBy("evictionLock")
1985
  @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
1986
  void makeDead(Node<K, V> node) {
1987
    synchronized (node) {
×
1988
      if (node.isDead()) {
×
1989
        return;
×
1990
      }
1991
      if (evicts()) {
×
1992
        // The node's policy weight may be out of sync due to a pending update waiting to be
1993
        // processed. At this point the node's weight is finalized, so the weight can be safely
1994
        // taken from the node's perspective and the sizes will be adjusted correctly.
1995
        if (node.inWindow()) {
×
1996
          setWindowWeightedSize(windowWeightedSize() - node.getWeight());
×
1997
        } else if (node.inMainProtected()) {
×
1998
          setMainProtectedWeightedSize(mainProtectedWeightedSize() - node.getWeight());
×
1999
        }
2000
        setWeightedSize(weightedSize() - node.getWeight());
×
2001
      }
2002
      node.die();
×
2003
    }
×
2004
  }
×
2005

2006
  /** Adds the node to the page replacement policy. */
2007
  final class AddTask implements Runnable {
2008
    final Node<K, V> node;
2009
    final int weight;
2010

2011
    AddTask(Node<K, V> node, int weight) {
×
2012
      this.weight = weight;
×
2013
      this.node = node;
×
2014
    }
×
2015

2016
    @Override
2017
    @GuardedBy("evictionLock")
2018
    public void run() {
2019
      if (evicts()) {
×
2020
        setWeightedSize(weightedSize() + weight);
×
2021
        setWindowWeightedSize(windowWeightedSize() + weight);
×
2022
        node.setPolicyWeight(node.getPolicyWeight() + weight);
×
2023

2024
        long maximum = maximum();
×
2025
        if (weightedSize() >= (maximum >>> 1)) {
×
2026
          if (weightedSize() > MAXIMUM_CAPACITY) {
×
2027
            evictEntries();
×
2028
          } else {
2029
            // Lazily initialize when close to the maximum
2030
            long capacity = isWeighted() ? data.mappingCount() : maximum;
×
2031
            frequencySketch().ensureCapacity(capacity);
×
2032
          }
2033
        }
2034

2035
        setMissesInSample(missesInSample() + 1);
×
2036
      }
2037

2038
      // ignore out-of-order write operations
2039
      boolean isAlive;
2040
      synchronized (node) {
×
2041
        isAlive = node.isAlive();
×
2042
      }
×
2043
      if (isAlive) {
×
2044
        if (expiresAfterWrite()) {
×
2045
          writeOrderDeque().offerLast(node);
×
2046
        }
2047
        if (expiresVariable()) {
×
2048
          timerWheel().schedule(node);
×
2049
        }
2050
        if (evicts()) {
×
2051
          var keyRef = node.getKeyReferenceOrNull();
×
2052
          if ((keyRef != null) && node.isAlive()) {
×
2053
            frequencySketch().increment(keyRef);
×
2054
          }
2055
          if (weight > maximum()) {
×
2056
            evictEntry(node, RemovalCause.SIZE, expirationTicker().read());
×
2057
          } else if (weight > windowMaximum()) {
×
2058
            accessOrderWindowDeque().offerFirst(node);
×
2059
          } else {
2060
            accessOrderWindowDeque().offerLast(node);
×
2061
          }
2062
        } else if (expiresAfterAccess()) {
×
2063
          accessOrderWindowDeque().offerLast(node);
×
2064
        }
2065
      }
2066
    }
×
2067
  }
2068

2069
  /** Removes a node from the page replacement policy. */
2070
  final class RemovalTask implements Runnable {
2071
    final Node<K, V> node;
2072

2073
    RemovalTask(Node<K, V> node) {
×
2074
      this.node = node;
×
2075
    }
×
2076

2077
    @Override
2078
    @GuardedBy("evictionLock")
2079
    public void run() {
2080
      // add may not have been processed yet
2081
      if (node.inWindow() && (evicts() || expiresAfterAccess())) {
×
2082
        accessOrderWindowDeque().remove(node);
×
2083
      } else if (evicts()) {
×
2084
        if (node.inMainProbation()) {
×
2085
          accessOrderProbationDeque().remove(node);
×
2086
        } else {
2087
          accessOrderProtectedDeque().remove(node);
×
2088
        }
2089
      }
2090
      if (expiresAfterWrite()) {
×
2091
        writeOrderDeque().remove(node);
×
2092
      } else if (expiresVariable()) {
×
2093
        timerWheel().deschedule(node);
×
2094
      }
2095
      makeDead(node);
×
2096
    }
×
2097
  }
2098

2099
  /** Updates the weighted size. */
2100
  final class UpdateTask implements Runnable {
2101
    final int weightDifference;
2102
    final boolean quietly;
2103
    final Node<K, V> node;
2104

2105
    public UpdateTask(Node<K, V> node, int weightDifference) {
2106
      this(node, weightDifference, /* quietly= */ false);
×
2107
    }
×
2108

2109
    public UpdateTask(Node<K, V> node, int weightDifference, boolean quietly) {
×
2110
      this.weightDifference = weightDifference;
×
2111
      this.quietly = quietly;
×
2112
      this.node = node;
×
2113
    }
×
2114

2115
    @Override
2116
    @GuardedBy("evictionLock")
2117
    public void run() {
2118
      if (expiresAfterWrite()) {
×
2119
        reorder(writeOrderDeque(), node);
×
2120
      } else if (expiresVariable()) {
×
2121
        timerWheel().reschedule(node);
×
2122
      }
2123
      if (evicts()) {
×
2124
        int oldWeightedSize = node.getPolicyWeight();
×
2125
        node.setPolicyWeight(oldWeightedSize + weightDifference);
×
2126
        if (node.inWindow()) {
×
2127
          setWindowWeightedSize(windowWeightedSize() + weightDifference);
×
2128
          if (node.getPolicyWeight() > maximum()) {
×
2129
            evictEntry(node, RemovalCause.SIZE, expirationTicker().read());
×
2130
          } else if (node.getPolicyWeight() <= windowMaximum()) {
×
2131
            onAccess(node, quietly);
×
2132
          } else if (accessOrderWindowDeque().contains(node)) {
×
2133
            accessOrderWindowDeque().moveToFront(node);
×
2134
          }
2135
        } else if (node.inMainProbation()) {
×
2136
            if (node.getPolicyWeight() <= maximum()) {
×
2137
              onAccess(node, quietly);
×
2138
            } else {
2139
              evictEntry(node, RemovalCause.SIZE, expirationTicker().read());
×
2140
            }
2141
        } else {
2142
          setMainProtectedWeightedSize(mainProtectedWeightedSize() + weightDifference);
×
2143
          if (node.getPolicyWeight() <= maximum()) {
×
2144
            onAccess(node, quietly);
×
2145
          } else {
2146
            evictEntry(node, RemovalCause.SIZE, expirationTicker().read());
×
2147
          }
2148
        }
2149

2150
        setWeightedSize(weightedSize() + weightDifference);
×
2151
        if (weightedSize() > MAXIMUM_CAPACITY) {
×
2152
          evictEntries();
×
2153
        }
2154
      } else if (expiresAfterAccess()) {
×
2155
        onAccess(node, quietly);
×
2156
      }
2157
    }
×
2158
  }
2159

2160
  /* --------------- Concurrent Map Support --------------- */
2161

2162
  @Override
2163
  public boolean isEmpty() {
2164
    return data.isEmpty();
×
2165
  }
2166

2167
  @Override
2168
  public int size() {
2169
    return data.size();
×
2170
  }
2171

2172
  @Override
2173
  public long estimatedSize() {
2174
    return data.mappingCount();
×
2175
  }
2176

2177
  @Override
2178
  public void clear() {
2179
    // Discard all pending refreshes
2180
    var pending = refreshes;
×
2181
    if (pending != null) {
×
2182
      pending.clear();
×
2183
    }
2184

2185
    Deque<Node<K, V>> entries;
2186
    evictionLock.lock();
×
2187
    try {
2188
      // Discard all pending reads
2189
      readBuffer.drainTo(e -> {});
×
2190

2191
      // Apply all pending writes
2192
      @Var Runnable task;
2193
      while ((task = writeBuffer.poll()) != null) {
×
2194
        task.run();
×
2195
      }
2196

2197
      // Cancel the scheduled cleanup
2198
      Pacer pacer = pacer();
×
2199
      if (pacer != null) {
×
2200
        pacer.cancel();
×
2201
      }
2202

2203
      // Discard all entries, falling back to one-by-one to avoid excessive lock hold times
2204
      long now = expirationTicker().read();
×
2205
      int threshold = (WRITE_BUFFER_MAX / 2);
×
2206
      entries = new ArrayDeque<>(data.values());
×
2207
      while (!entries.isEmpty() && (writeBuffer.size() < threshold)) {
×
2208
        removeNode(entries.pollFirst(), now);
×
2209
      }
2210
    } finally {
2211
      evictionLock.unlock();
×
2212
    }
2213

2214
    // Remove any stragglers if released early to more aggressively flush incoming writes
2215
    @Var boolean cleanUp = false;
×
2216
    for (var node : entries) {
×
2217
      @Nullable K key = node.getKey();
×
2218
      if (key == null) {
×
2219
        cleanUp = true;
×
2220
      } else {
2221
        remove(key);
×
2222
      }
2223
    }
×
2224
    if (collectKeys() && cleanUp) {
×
2225
      cleanUp();
×
2226
    } else {
2227
      rescheduleCleanUpIfIncomplete();
×
2228
    }
2229
  }
×
2230

2231
  @GuardedBy("evictionLock")
2232
  @SuppressWarnings({"GuardedByChecker", "SynchronizationOnLocalVariableOrMethodParameter"})
2233
  void removeNode(Node<K, V> node, long now) {
2234
    K key = node.getKey();
×
2235
    var ctx = new EvictContext<V>();
×
2236
    var keyReference = node.getKeyReference();
×
2237

2238
    data.computeIfPresent(keyReference, (k, n) -> {
×
2239
      if (n != node) {
×
2240
        return n;
×
2241
      }
2242
      synchronized (node) {
×
2243
        ctx.value = node.getValue();
×
2244
        ctx.oldWeight = node.getWeight();
×
2245

2246
        if ((key == null) || (ctx.value == null)) {
×
2247
          ctx.cause = RemovalCause.COLLECTED;
×
2248
        } else if (hasExpired(node, now, ctx.value)) {
×
2249
          ctx.cause = RemovalCause.EXPIRED;
×
2250
        } else {
2251
          ctx.cause = RemovalCause.EXPLICIT;
×
2252
        }
2253

2254
        if (ctx.cause.wasEvicted()) {
×
2255
          notifyEviction(key, ctx.value, ctx.cause);
×
2256
        }
2257

2258
        discardRefresh(node.getKeyReference());
×
2259
        node.retire();
×
2260
        return null;
×
2261
      }
2262
    });
2263

2264
    if (node.inWindow() && (evicts() || expiresAfterAccess())) {
×
2265
      accessOrderWindowDeque().remove(node);
×
2266
    } else if (evicts()) {
×
2267
      if (node.inMainProbation()) {
×
2268
        accessOrderProbationDeque().remove(node);
×
2269
      } else {
2270
        accessOrderProtectedDeque().remove(node);
×
2271
      }
2272
    }
2273
    if (expiresAfterWrite()) {
×
2274
      writeOrderDeque().remove(node);
×
2275
    } else if (expiresVariable()) {
×
2276
      timerWheel().deschedule(node);
×
2277
    }
2278

2279
    synchronized (node) {
×
2280
      logIfAlive(node);
×
2281
      makeDead(node);
×
2282
    }
×
2283

2284
    if (ctx.cause != null) {
×
2285
      if (ctx.cause.wasEvicted()) {
×
2286
        statsCounter().recordEviction(ctx.oldWeight, ctx.cause);
×
2287
      }
2288
      notifyRemoval(key, ctx.value, ctx.cause);
×
2289
    }
2290
  }
×
2291

2292
  @Override
2293
  public boolean containsKey(Object key) {
2294
    Node<K, V> node = data.get(nodeFactory.newLookupKey(key));
×
2295
    if (node == null) {
×
2296
      return false;
×
2297
    }
2298
    V value = node.getValue();
×
2299
    if ((value == null) || hasExpired(node, expirationTicker().read(), value)) {
×
2300
      scheduleDrainBuffers();
×
2301
      return false;
×
2302
    }
2303
    return true;
×
2304
  }
2305

2306
  @Override
2307
  public boolean containsValue(Object value) {
2308
    requireNonNull(value);
×
2309

2310
    long now = expirationTicker().read();
×
2311
    for (Node<K, V> node : data.values()) {
×
2312
      V nodeValue = node.getValue();
×
2313
      if ((node.getKey() == null) || (nodeValue == null) || hasExpired(node, now, nodeValue)) {
×
2314
        scheduleDrainBuffers();
×
2315
      } else if (node.isAlive() && node.containsValue(value)) {
×
2316
        return true;
×
2317
      }
2318
    }
×
2319
    return false;
×
2320
  }
2321

2322
  @Override
2323
  public @Nullable V get(Object key) {
2324
    return getIfPresent(key, /* recordStats= */ false);
×
2325
  }
2326

2327
  @Override
2328
  public @Nullable V getIfPresent(Object key, boolean recordStats) {
2329
    Node<K, V> node = data.get(nodeFactory.newLookupKey(key));
×
2330
    if (node == null) {
×
2331
      if (recordStats) {
×
2332
        statsCounter().recordMisses(1);
×
2333
      }
2334
      if (drainStatusOpaque() == REQUIRED) {
×
2335
        scheduleDrainBuffers();
×
2336
      }
2337
      return null;
×
2338
    }
2339

2340
    V value = node.getValue();
×
2341
    long now = expirationTicker().read();
×
2342
    if ((value == null) || hasExpired(node, now, value)) {
×
2343
      if (recordStats) {
×
2344
        statsCounter().recordMisses(1);
×
2345
      }
2346
      scheduleDrainBuffers();
×
2347
      return null;
×
2348
    }
2349

2350
    if (!isComputingAsync(value)) {
×
2351
      @SuppressWarnings("unchecked")
2352
      var castedKey = (K) key;
×
2353
      setAccessTime(node, now);
×
2354
      tryExpireAfterRead(node, castedKey, value, expiry(), now);
×
2355
    }
2356
    V refreshed = afterRead(node, now, recordStats);
×
2357
    return (refreshed == null) ? value : refreshed;
×
2358
  }
2359

2360
  @Override
2361
  public @Nullable V getIfPresentQuietly(Object key) {
2362
    V value;
2363
    Node<K, V> node = data.get(nodeFactory.newLookupKey(key));
×
2364
    if ((node == null) || ((value = node.getValue()) == null)
×
2365
        || hasExpired(node, expirationTicker().read(), value)) {
×
2366
      return null;
×
2367
    }
2368
    return value;
×
2369
  }
2370

2371
  /**
2372
   * Returns the key associated with the mapping in this cache, or {@code null} if there is none.
2373
   *
2374
   * @param key the key whose canonical instance is to be returned
2375
   * @return the key used by the mapping, or {@code null} if this cache does not contain a mapping
2376
   *         for the key
2377
   * @throws NullPointerException if the specified key is null
2378
   */
2379
  public @Nullable K getKey(K key) {
2380
    Node<K, V> node = data.get(nodeFactory.newLookupKey(key));
×
2381
    if (node == null) {
×
2382
      if (drainStatusOpaque() == REQUIRED) {
×
2383
        scheduleDrainBuffers();
×
2384
      }
2385
      return null;
×
2386
    }
2387
    afterRead(node, /* now= */ 0L, /* recordHit= */ false);
×
2388
    return node.getKey();
×
2389
  }
2390

2391
  @Override
2392
  public Map<K, V> getAllPresent(Iterable<? extends K> keys) {
2393
    var result = new LinkedHashMap<K, @Nullable V>(calculateHashMapCapacity(keys));
×
2394
    for (K key : keys) {
×
2395
      result.put(key, null);
×
2396
    }
×
2397

2398
    @Var boolean drain = false;
×
2399
    int uniqueKeys = result.size();
×
2400
    long now = expirationTicker().read();
×
2401
    for (var iter = result.entrySet().iterator(); iter.hasNext();) {
×
2402
      V value;
2403
      var entry = iter.next();
×
2404
      Node<K, V> node = data.get(nodeFactory.newLookupKey(entry.getKey()));
×
2405
      if (node == null) {
×
2406
        iter.remove();
×
2407
      } else if (((value = node.getValue()) == null) || hasExpired(node, now, value)) {
×
2408
        iter.remove();
×
2409
        drain = true;
×
2410
      } else {
2411
        setAccessTime(node, now);
×
2412
        tryExpireAfterRead(node, entry.getKey(), value, expiry(), now);
×
2413
        V refreshed = afterRead(node, now, /* recordHit= */ false);
×
2414
        entry.setValue((refreshed == null) ? value : refreshed);
×
2415
      }
2416
    }
×
2417
    if (drain) {
×
2418
      scheduleDrainBuffers();
×
2419
    }
2420
    statsCounter().recordHits(result.size());
×
2421
    statsCounter().recordMisses(uniqueKeys - result.size());
×
2422

2423
    @SuppressWarnings("NullableProblems")
2424
    Map<K, V> unmodifiable = Collections.unmodifiableMap(result);
×
2425
    return unmodifiable;
×
2426
  }
2427

2428
  @Override
2429
  public void putAll(Map<? extends K, ? extends V> map) {
2430
    map.forEach(this::put);
×
2431
  }
×
2432

2433
  @Override
2434
  public @Nullable V put(K key, V value) {
2435
    return put(key, value, expiry(), /* onlyIfAbsent= */ false);
×
2436
  }
2437

2438
  @Override
2439
  public @Nullable V putIfAbsent(K key, V value) {
2440
    return put(key, value, expiry(), /* onlyIfAbsent= */ true);
×
2441
  }
2442

2443
  /**
2444
   * Adds a node to the policy and the data store. If an existing node is found, then its value is
2445
   * updated if allowed.
2446
   *
2447
   * @param key key with which the specified value is to be associated
2448
   * @param value value to be associated with the specified key
2449
   * @param expiry the calculator for the write expiration time
2450
   * @param onlyIfAbsent a write is performed only if the key is not already associated with a value
2451
   * @return the prior value in or null if no mapping was found
2452
   */
2453
  @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
2454
  @Nullable V put(K key, V value, Expiry<K, V> expiry, boolean onlyIfAbsent) {
2455
    requireNonNull(key);
×
2456
    requireNonNull(value);
×
2457

2458
    @Var int newWeight = -1;
×
2459
    @Var Object keyRef = null;
×
2460
    @Var Node<K, V> node = null;
×
2461
    Object lookupKey = nodeFactory.newLookupKey(key);
×
2462
    for (int attempts = 1; ; attempts++) {
×
2463
      @Var Node<K, V> prior = data.get(lookupKey);
×
2464
      if (prior == null) {
×
2465
        if (node == null) {
×
2466
          if (newWeight < 0) {
×
2467
            newWeight = weigher.weigh(key, value);
×
2468
          }
2469
          long now = expirationTicker().read();
×
2470
          keyRef = nodeFactory.newReferenceKey(key, keyReferenceQueue());
×
2471
          node = nodeFactory.newNode(keyRef, value, valueReferenceQueue(), newWeight, now);
×
2472
          long expirationTime = isComputingAsync(value) ? (now + ASYNC_EXPIRY) : now;
×
2473
          setVariableTime(node, expireAfterCreate(key, value, expiry, now));
×
2474
          setAccessTime(node, expirationTime);
×
2475
          setWriteTime(node, expirationTime);
×
2476
        }
2477
        var newNode = node;
×
2478
        prior = (cacheLoader == null)
×
2479
            ? data.putIfAbsent(keyRef, newNode)
×
2480
            : data.computeIfAbsent(keyRef, k -> {
×
2481
                discardRefresh(k);
×
2482
                return newNode;
×
2483
              });
2484
        if ((prior == null) || (prior == node)) {
×
2485
          afterWrite(new AddTask(node, newWeight));
×
2486
          return null;
×
2487
        } else if (onlyIfAbsent) {
×
2488
          // An optimistic fast path to avoid unnecessary locking
2489
          V currentValue = prior.getValue();
×
2490
          long now = expirationTicker().read();
×
2491
          if ((currentValue != null) && !hasExpired(prior, now, currentValue)) {
×
2492
            if (!isComputingAsync(currentValue)) {
×
2493
              tryExpireAfterRead(prior, key, currentValue, expiry, now);
×
2494
              setAccessTime(prior, now);
×
2495
            }
2496
            afterRead(prior, now, /* recordHit= */ false);
×
2497
            return currentValue;
×
2498
          }
2499
        }
2500
      } else if (onlyIfAbsent) {
×
2501
        // An optimistic fast path to avoid unnecessary locking
2502
        V currentValue = prior.getValue();
×
2503
        long now = expirationTicker().read();
×
2504
        if ((currentValue != null) && !hasExpired(prior, now, currentValue)) {
×
2505
          if (!isComputingAsync(currentValue)) {
×
2506
            tryExpireAfterRead(prior, key, currentValue, expiry, now);
×
2507
            setAccessTime(prior, now);
×
2508
          }
2509
          afterRead(prior, now, /* recordHit= */ false);
×
2510
          return currentValue;
×
2511
        }
2512
      }
2513

2514
      // A read may race with the entry's removal, so that after the entry is acquired it may no
2515
      // longer be usable. A retry will reread from the map and either find an absent mapping, a
2516
      // new entry, or a stale entry.
2517
      if (!prior.isAlive()) {
×
2518
        // A reread of the stale entry may occur if the state transition occurred but the map
2519
        // removal was delayed by a context switch, so that this thread spin waits until resolved.
2520
        if ((attempts & MAX_PUT_SPIN_WAIT_ATTEMPTS) != 0) {
×
2521
          Thread.onSpinWait();
×
2522
          continue;
×
2523
        }
2524

2525
        // If the spin wait attempts are exhausted then fallback to a map computation in order to
2526
        // deschedule this thread until the entry's removal completes. If the key was modified
2527
        // while in the map so that its equals or hashCode changed then the contents may be
2528
        // corrupted, where the cache holds an evicted (dead) entry that could not be removed.
2529
        // That is a violation of the Map contract, so we check that the mapping is in the "alive"
2530
        // state while in the computation.
2531
        data.computeIfPresent(lookupKey, (k, n) -> {
×
2532
          requireIsAlive(key, n);
×
2533
          return n;
×
2534
        });
2535
        continue;
×
2536
      }
2537

2538
      long now;
2539
      V oldValue;
2540
      long varTime;
2541
      int oldWeight;
2542
      @Var boolean expired = false;
×
2543
      @Var boolean mayUpdate = true;
×
2544
      @Var boolean exceedsTolerance = false;
×
2545
      if (newWeight < 0) {
×
2546
        newWeight = weigher.weigh(key, value);
×
2547
      }
2548
      synchronized (prior) {
×
2549
        if (!prior.isAlive()) {
×
2550
          continue;
×
2551
        }
2552
        oldValue = prior.getValue();
×
2553
        oldWeight = prior.getWeight();
×
2554
        now = expirationTicker().read();
×
2555
        if (oldValue == null) {
×
2556
          varTime = expireAfterCreate(key, value, expiry, now);
×
2557
          notifyEviction(key, null, RemovalCause.COLLECTED);
×
2558
        } else if (hasExpired(prior, now, oldValue)) {
×
2559
          expired = true;
×
2560
          varTime = expireAfterCreate(key, value, expiry, now);
×
2561
          notifyEviction(key, oldValue, RemovalCause.EXPIRED);
×
2562
        } else if (onlyIfAbsent) {
×
2563
          mayUpdate = false;
×
2564
          varTime = expireAfterRead(prior, key, oldValue, expiry, now);
×
2565
        } else {
2566
          varTime = expireAfterUpdate(prior, key, value, expiry, now);
×
2567
        }
2568

2569
        long expirationTime = isComputingAsync(mayUpdate ? value : oldValue)
×
2570
            ? (now + ASYNC_EXPIRY)
×
2571
            : now;
×
2572
        if (mayUpdate) {
×
2573
          exceedsTolerance = exceedsWriteTimeTolerance(prior, varTime, expirationTime);
×
2574
          if (expired || exceedsTolerance) {
×
2575
            setWriteTime(prior, expirationTime);
×
2576
          }
2577

2578
          prior.setValue(value, valueReferenceQueue());
×
2579
          prior.setWeight(newWeight);
×
2580

2581
          discardRefresh(prior.getKeyReference());
×
2582
        }
2583

2584
        setVariableTime(prior, varTime);
×
2585
        setAccessTime(prior, expirationTime);
×
2586
      }
×
2587

2588
      if (expired) {
×
2589
        statsCounter().recordEviction(oldWeight, RemovalCause.EXPIRED);
×
2590
        notifyRemoval(key, oldValue, RemovalCause.EXPIRED);
×
2591
      } else if (oldValue == null) {
×
2592
        statsCounter().recordEviction(oldWeight, RemovalCause.COLLECTED);
×
2593
        notifyRemoval(key, /* value= */ null, RemovalCause.COLLECTED);
×
2594
      } else if (mayUpdate) {
×
2595
        notifyOnReplace(key, oldValue, value);
×
2596
      }
2597

2598
      int weightedDifference = mayUpdate ? (newWeight - oldWeight) : 0;
×
2599
      if ((oldValue == null) || (weightedDifference != 0) || expired) {
×
2600
        afterWrite(new UpdateTask(prior, weightedDifference));
×
2601
      } else if (!onlyIfAbsent && exceedsTolerance) {
×
2602
        afterWrite(new UpdateTask(prior, weightedDifference));
×
2603
      } else {
2604
        afterRead(prior, now, /* recordHit= */ false);
×
2605
      }
2606

2607
      return expired ? null : oldValue;
×
2608
    }
2609
  }
2610

2611
  @Override
2612
  @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
2613
  public @Nullable V remove(Object key) {
2614
    var ctx = new RemoveContext<K, V>();
×
2615
    Object lookupKey = nodeFactory.newLookupKey(key);
×
2616
    data.compute(lookupKey, (k, n) -> {
×
2617
      if (n == null) {
×
2618
        discardRefresh(k);
×
2619
        return null;
×
2620
      }
2621
      synchronized (n) {
×
2622
        requireIsAlive(key, n);
×
2623
        ctx.oldKey = n.getKey();
×
2624
        ctx.oldValue = n.getValue();
×
2625
        ctx.oldWeight = n.getWeight();
×
2626
        RemovalCause actualCause;
2627
        if ((ctx.oldKey == null) || (ctx.oldValue == null)) {
×
2628
          actualCause = RemovalCause.COLLECTED;
×
2629
        } else if (hasExpired(n, expirationTicker().read(), ctx.oldValue)) {
×
2630
          actualCause = RemovalCause.EXPIRED;
×
2631
        } else {
2632
          actualCause = RemovalCause.EXPLICIT;
×
2633
        }
2634
        if (actualCause.wasEvicted()) {
×
2635
          notifyEviction(ctx.oldKey, ctx.oldValue, actualCause);
×
2636
        }
2637
        ctx.cause = actualCause;
×
2638
        discardRefresh(k);
×
2639
        ctx.node = n;
×
2640
        n.retire();
×
2641
        return null;
×
2642
      }
2643
    });
2644

2645
    if (ctx.cause != null) {
×
2646
      afterWrite(new RemovalTask(requireNonNull(ctx.node)));
×
2647
      if (ctx.cause.wasEvicted()) {
×
2648
        statsCounter().recordEviction(ctx.oldWeight, ctx.cause);
×
2649
      }
2650
      notifyRemoval(ctx.oldKey, ctx.oldValue, ctx.cause);
×
2651
    }
2652
    return (ctx.cause == RemovalCause.EXPLICIT) ? ctx.oldValue : null;
×
2653
  }
2654

2655
  @Override
2656
  @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
2657
  public boolean remove(Object key, @Nullable Object value) {
2658
    requireNonNull(key);
×
2659
    if (value == null) {
×
2660
      return false;
×
2661
    }
2662

2663
    var ctx = new RemoveContext<K, V>();
×
2664
    Object lookupKey = nodeFactory.newLookupKey(key);
×
2665
    data.computeIfPresent(lookupKey, (kR, node) -> {
×
2666
      synchronized (node) {
×
2667
        requireIsAlive(key, node);
×
2668
        ctx.oldKey = node.getKey();
×
2669
        ctx.oldValue = node.getValue();
×
2670
        ctx.oldWeight = node.getWeight();
×
2671
        if ((ctx.oldKey == null) || (ctx.oldValue == null)) {
×
2672
          ctx.cause = RemovalCause.COLLECTED;
×
2673
        } else if (hasExpired(node, expirationTicker().read(), ctx.oldValue)) {
×
2674
          ctx.cause = RemovalCause.EXPIRED;
×
2675
        } else if (node.containsValue(value)) {
×
2676
          ctx.cause = RemovalCause.EXPLICIT;
×
2677
        } else {
2678
          return node;
×
2679
        }
2680
        if (ctx.cause.wasEvicted()) {
×
2681
          notifyEviction(ctx.oldKey, ctx.oldValue, ctx.cause);
×
2682
        }
2683
        discardRefresh(kR);
×
2684
        ctx.node = node;
×
2685
        node.retire();
×
2686
        return null;
×
2687
      }
2688
    });
2689

2690
    if (ctx.node == null) {
×
2691
      return false;
×
2692
    }
2693
    var removeCause = requireNonNull(ctx.cause);
×
2694
    afterWrite(new RemovalTask(ctx.node));
×
2695
    if (removeCause.wasEvicted()) {
×
2696
      statsCounter().recordEviction(ctx.oldWeight, removeCause);
×
2697
    }
2698
    notifyRemoval(ctx.oldKey, ctx.oldValue, removeCause);
×
2699

2700
    return (removeCause == RemovalCause.EXPLICIT);
×
2701
  }
2702

2703
  @Override
2704
  @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
2705
  public @Nullable V replace(K key, V value) {
2706
    requireNonNull(key);
×
2707
    requireNonNull(value);
×
2708
    var ctx = new ReplaceContext<K, V>();
×
2709
    int weight = weigher.weigh(key, value);
×
2710
    Node<K, V> node = data.computeIfPresent(nodeFactory.newLookupKey(key), (k, n) -> {
×
2711
      synchronized (n) {
×
2712
        requireIsAlive(key, n);
×
2713
        ctx.nodeKey = n.getKey();
×
2714
        ctx.oldValue = n.getValue();
×
2715
        ctx.oldWeight = n.getWeight();
×
2716
        if ((ctx.nodeKey == null) || (ctx.oldValue == null)
×
2717
            || hasExpired(n, ctx.now = expirationTicker().read(), ctx.oldValue)) {
×
2718
          ctx.oldValue = null;
×
2719
          return n;
×
2720
        }
2721

2722
        long varTime = expireAfterUpdate(n, key, value, expiry(), ctx.now);
×
2723
        n.setValue(value, valueReferenceQueue());
×
2724
        n.setWeight(weight);
×
2725

2726
        long expirationTime = isComputingAsync(value) ? (ctx.now + ASYNC_EXPIRY) : ctx.now;
×
2727
        ctx.exceedsTolerance = exceedsWriteTimeTolerance(n, varTime, expirationTime);
×
2728
        if (ctx.exceedsTolerance) {
×
2729
          setWriteTime(n, expirationTime);
×
2730
        }
2731
        setAccessTime(n, expirationTime);
×
2732
        setVariableTime(n, varTime);
×
2733
        discardRefresh(k);
×
2734
        return n;
×
2735
      }
2736
    });
2737

2738
    if ((node == null) || (ctx.nodeKey == null) || (ctx.oldValue == null)) {
×
2739
      if (node != null) {
×
2740
        scheduleDrainBuffers();
×
2741
      }
2742
      return null;
×
2743
    }
2744

2745
    int weightedDifference = (weight - ctx.oldWeight);
×
2746
    if (ctx.exceedsTolerance || (weightedDifference != 0)) {
×
2747
      afterWrite(new UpdateTask(node, weightedDifference));
×
2748
    } else {
2749
      afterRead(node, ctx.now, /* recordHit= */ false);
×
2750
    }
2751

2752
    notifyOnReplace(ctx.nodeKey, ctx.oldValue, value);
×
2753
    return ctx.oldValue;
×
2754
  }
2755

2756
  @Override
2757
  @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
2758
  public boolean replace(K key, V oldValue, V newValue,
2759
      boolean shouldDiscardRefresh, boolean quietly) {
2760
    requireNonNull(key);
×
2761
    requireNonNull(oldValue);
×
2762
    requireNonNull(newValue);
×
2763
    var ctx = new ReplaceContext<K, V>();
×
2764
    int weight = weigher.weigh(key, newValue);
×
2765
    Node<K, V> node = data.computeIfPresent(nodeFactory.newLookupKey(key), (k, n) -> {
×
2766
      synchronized (n) {
×
2767
        requireIsAlive(key, n);
×
2768
        ctx.nodeKey = n.getKey();
×
2769
        ctx.oldValue = n.getValue();
×
2770
        ctx.oldWeight = n.getWeight();
×
2771
        if ((ctx.nodeKey == null) || (ctx.oldValue == null) || !n.containsValue(oldValue)
×
2772
            || hasExpired(n, ctx.now = expirationTicker().read(), ctx.oldValue)) {
×
2773
          ctx.oldValue = null;
×
2774
          return n;
×
2775
        }
2776

2777
        long varTime = expireAfterUpdate(n, key, newValue, expiry(), ctx.now);
×
2778
        n.setValue(newValue, valueReferenceQueue());
×
2779
        n.setWeight(weight);
×
2780

2781
        long expirationTime = isComputingAsync(newValue) ? (ctx.now + ASYNC_EXPIRY) : ctx.now;
×
2782
        ctx.exceedsTolerance = exceedsWriteTimeTolerance(n, varTime, expirationTime);
×
2783
        if (ctx.exceedsTolerance) {
×
2784
          setWriteTime(n, expirationTime);
×
2785
        }
2786
        setAccessTime(n, expirationTime);
×
2787
        setVariableTime(n, varTime);
×
2788

2789
        if (shouldDiscardRefresh) {
×
2790
          discardRefresh(k);
×
2791
        }
2792
      }
×
2793
      return n;
×
2794
    });
2795

2796
    if ((node == null) || (ctx.nodeKey == null) || (ctx.oldValue == null)) {
×
2797
      if (node != null) {
×
2798
        scheduleDrainBuffers();
×
2799
      }
2800
      return false;
×
2801
    }
2802

2803
    int weightedDifference = (weight - ctx.oldWeight);
×
2804
    if (ctx.exceedsTolerance || (weightedDifference != 0)) {
×
2805
      afterWrite(new UpdateTask(node, weightedDifference, quietly));
×
2806
    } else if (!quietly) {
×
2807
      afterRead(node, ctx.now, /* recordHit= */ false);
×
2808
    }
2809

2810
    notifyOnReplace(ctx.nodeKey, ctx.oldValue, newValue);
×
2811
    return true;
×
2812
  }
2813

2814
  @Override
2815
  public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
2816
    requireNonNull(function);
×
2817

2818
    BiFunction<K, V, V> remappingFunction = (key, oldValue) ->
×
2819
        requireNonNull(function.apply(key, oldValue));
×
2820
    for (K key : keySet()) {
×
2821
      Object lookupKey = nodeFactory.newLookupKey(key);
×
2822
      remap(key, lookupKey, remappingFunction, expiry(),
×
2823
          new ComputeContext<>(expirationTicker().read()), /* computeIfAbsent= */ false);
×
2824
    }
×
2825
  }
×
2826

2827
  @Override
2828
  public @Nullable V computeIfAbsent(K key,
2829
      @Var Function<? super K, ? extends @Nullable V> mappingFunction,
2830
      boolean recordStats, boolean recordLoad) {
2831
    requireNonNull(key);
×
2832
    requireNonNull(mappingFunction);
×
2833

2834
    // An optimistic fast path to avoid unnecessary locking
2835
    Node<K, V> node = data.get(nodeFactory.newLookupKey(key));
×
2836
    long now = expirationTicker().read();
×
2837
    if (node != null) {
×
2838
      V value = node.getValue();
×
2839
      if ((value != null) && !hasExpired(node, now, value)) {
×
2840
        if (!isComputingAsync(value)) {
×
2841
          tryExpireAfterRead(node, key, value, expiry(), now);
×
2842
          setAccessTime(node, now);
×
2843
        }
2844
        @Nullable V refreshed = afterRead(node, now, /* recordHit= */ recordStats);
×
2845
        return (refreshed == null) ? value : refreshed;
×
2846
      }
2847
    }
2848
    if (recordStats) {
×
2849
      mappingFunction = statsAware(mappingFunction, recordLoad);
×
2850
    }
2851
    Object keyRef = nodeFactory.newReferenceKey(key, keyReferenceQueue());
×
2852
    return doComputeIfAbsent(key, keyRef, mappingFunction,
×
2853
        new ComputeContext<>(now), recordStats);
2854
  }
2855

2856
  /** Returns the current value from a computeIfAbsent invocation. */
2857
  @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
2858
  @Nullable V doComputeIfAbsent(K key, Object keyRef,
2859
      Function<? super K, ? extends @Nullable V> mappingFunction,
2860
      ComputeContext<K, V> ctx, boolean recordStats) {
2861
    Node<K, V> node = data.compute(keyRef, (k, n) -> {
×
2862
      if (n == null) {
×
2863
        ctx.newValue = mappingFunction.apply(key);
×
2864
        if (ctx.newValue == null) {
×
2865
          discardRefresh(k);
×
2866
          return null;
×
2867
        }
2868
        ctx.now = expirationTicker().read();
×
2869
        ctx.newWeight = weigher.weigh(key, ctx.newValue);
×
2870
        var created = nodeFactory.newNode(k, ctx.newValue,
×
2871
            valueReferenceQueue(), ctx.newWeight, ctx.now);
×
2872
        long expirationTime = isComputingAsync(ctx.newValue)
×
2873
            ? ctx.now + ASYNC_EXPIRY
×
2874
            : ctx.now;
×
2875
        setVariableTime(created, expireAfterCreate(key, ctx.newValue, expiry(), ctx.now));
×
2876
        setAccessTime(created, expirationTime);
×
2877
        setWriteTime(created, expirationTime);
×
2878
        discardRefresh(k);
×
2879
        return created;
×
2880
      }
2881

2882
      synchronized (n) {
×
2883
        requireIsAlive(key, n);
×
2884
        ctx.nodeKey = n.getKey();
×
2885
        ctx.oldValue = n.getValue();
×
2886
        ctx.oldWeight = n.getWeight();
×
2887
        RemovalCause actualCause;
2888
        if ((ctx.nodeKey == null) || (ctx.oldValue == null)) {
×
2889
          actualCause = RemovalCause.COLLECTED;
×
2890
        } else if (hasExpired(n, ctx.now = expirationTicker().read(), ctx.oldValue)) {
×
2891
          actualCause = RemovalCause.EXPIRED;
×
2892
        } else {
2893
          return n;
×
2894
        }
2895

2896
        ctx.cause = actualCause;
×
2897
        notifyEviction(ctx.nodeKey, ctx.oldValue, actualCause);
×
2898

2899
        try {
2900
          ctx.newValue = mappingFunction.apply(key);
×
2901
          if (ctx.newValue == null) {
×
2902
            discardRefresh(k);
×
2903
            ctx.removed = n;
×
2904
            n.retire();
×
2905
            return null;
×
2906
          }
2907
          ctx.now = expirationTicker().read();
×
2908
          ctx.newWeight = weigher.weigh(key, ctx.newValue);
×
2909
          long varTime = expireAfterCreate(key, ctx.newValue, expiry(), ctx.now);
×
2910

2911
          n.setValue(ctx.newValue, valueReferenceQueue());
×
2912
          n.setWeight(ctx.newWeight);
×
2913

2914
          long expirationTime = isComputingAsync(ctx.newValue)
×
2915
              ? (ctx.now + ASYNC_EXPIRY) : ctx.now;
×
2916
          setAccessTime(n, expirationTime);
×
2917
          setWriteTime(n, expirationTime);
×
2918
          setVariableTime(n, varTime);
×
2919
          discardRefresh(k);
×
2920
          return n;
×
2921
        } catch (Throwable e) {
×
2922
          ctx.newValue = null;
×
2923
          discardRefresh(k);
×
2924
          ctx.exception = e;
×
2925
          ctx.removed = n;
×
2926
          n.retire();
×
2927
          return null;
×
2928
        }
2929
      }
2930
    });
2931

2932
    if (ctx.cause != null) {
×
2933
      statsCounter().recordEviction(ctx.oldWeight, ctx.cause);
×
2934
      notifyRemoval(ctx.nodeKey, ctx.oldValue, ctx.cause);
×
2935
    }
2936
    if (node == null) {
×
2937
      if (ctx.removed != null) {
×
2938
        afterWrite(new RemovalTask(ctx.removed));
×
2939
      }
2940
      if (ctx.exception != null) {
×
2941
        throw toUncheckedException(ctx.exception);
×
2942
      }
2943
      return null;
×
2944
    }
2945
    if ((ctx.oldValue != null) && (ctx.newValue == null)) {
×
2946
      if (!isComputingAsync(ctx.oldValue)) {
×
2947
        tryExpireAfterRead(node, key, ctx.oldValue, expiry(), ctx.now);
×
2948
        setAccessTime(node, ctx.now);
×
2949
      }
2950

2951
      @Nullable V refreshed = afterRead(node, ctx.now, /* recordHit= */ recordStats);
×
2952
      return (refreshed == null) ? ctx.oldValue : refreshed;
×
2953
    }
2954
    if ((ctx.oldValue == null) && (ctx.cause == null)) {
×
2955
      afterWrite(new AddTask(node, ctx.newWeight));
×
2956
    } else {
2957
      int weightedDifference = (ctx.newWeight - ctx.oldWeight);
×
2958
      afterWrite(new UpdateTask(node, weightedDifference));
×
2959
    }
2960

2961
    return ctx.newValue;
×
2962
  }
2963

2964
  @Override
2965
  public @Nullable V computeIfPresent(K key,
2966
      BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
2967
    requireNonNull(key);
×
2968
    requireNonNull(remappingFunction);
×
2969

2970
    // An optimistic fast path to avoid unnecessary locking
2971
    Object lookupKey = nodeFactory.newLookupKey(key);
×
2972
    @Nullable Node<K, V> node = data.get(lookupKey);
×
2973
    long now;
2974
    if (node == null) {
×
2975
      return null;
×
2976
    }
2977
    V value = node.getValue();
×
2978
    if ((value == null) || hasExpired(node, now = expirationTicker().read(), value)) {
×
2979
      scheduleDrainBuffers();
×
2980
      return null;
×
2981
    }
2982

2983
    BiFunction<? super K, ? super V, ? extends V> statsAwareRemappingFunction =
×
2984
        statsAware(remappingFunction, /* recordLoad= */ true, /* recordLoadFailure= */ true);
×
2985
    return remap(key, lookupKey, statsAwareRemappingFunction,
×
2986
        expiry(), new ComputeContext<>(now), /* computeIfAbsent= */ false);
×
2987
  }
2988

2989
  @Override
2990
  public @Nullable V compute(K key,
2991
      BiFunction<? super K, ? super V, ? extends @Nullable V> remappingFunction,
2992
      @Nullable Expiry<? super K, ? super V> expiry, boolean recordLoad, boolean recordLoadFailure,
2993
      @Nullable RemapHints hints) {
2994
    requireNonNull(key);
×
2995
    requireNonNull(remappingFunction);
×
2996

2997
    Object keyRef = nodeFactory.newReferenceKey(key, keyReferenceQueue());
×
2998
    BiFunction<? super K, ? super V, ? extends V> statsAwareRemappingFunction =
×
2999
        statsAware(remappingFunction, recordLoad, recordLoadFailure);
×
3000
    var ctx = new ComputeContext<K, V>(expirationTicker().read());
×
3001
    ctx.hints = hints;
×
3002
    return remap(key, keyRef, statsAwareRemappingFunction,
×
3003
        expiry, ctx, /* computeIfAbsent= */ true);
3004
  }
3005

3006
  @Override
3007
  public @Nullable V merge(K key, V value,
3008
      BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
3009
    requireNonNull(key);
×
3010
    requireNonNull(value);
×
3011
    requireNonNull(remappingFunction);
×
3012

3013
    Object keyRef = nodeFactory.newReferenceKey(key, keyReferenceQueue());
×
3014
    BiFunction<? super V, ? super V, ? extends V> f = statsAware(remappingFunction);
×
3015
    BiFunction<? super K, ? super @Nullable V, ? extends @Nullable V> mergeFunction =
×
3016
        (k, oldValue) -> (oldValue == null) ? value : f.apply(oldValue, value);
×
3017
    return remap(key, keyRef, mergeFunction, expiry(),
×
3018
        new ComputeContext<>(expirationTicker().read()), /* computeIfAbsent= */ true);
×
3019
  }
3020

3021
  /**
3022
   * Attempts to compute a mapping for the specified key and its current mapped value (or
3023
   * {@code null} if there is no current mapping).
3024
   * <p>
3025
   * An entry that has expired or been reference collected is evicted and the computation continues
3026
   * as if the entry had not been present. This method does not pre-screen and does not wrap the
3027
   * remappingFunction to be statistics aware.
3028
   *
3029
   * @param key key with which the specified value is to be associated
3030
   * @param keyRef the key to associate with or a lookup only key if not {@code computeIfAbsent}
3031
   * @param remappingFunction the function to compute a value
3032
   * @param expiry the calculator for the expiration time
3033
   * @param ctx the mutable context for passing state to and from the {@link ConcurrentHashMap}
3034
   *        compute lambda, with {@link ComputeContext#now} set to the current ticker time
3035
   * @param computeIfAbsent if an absent entry can be computed
3036
   * @return the new value associated with the specified key, or null if none
3037
   */
3038
  @SuppressWarnings({"StatementWithEmptyBody", "SynchronizationOnLocalVariableOrMethodParameter"})
3039
  @Nullable V remap(K key, Object keyRef,
3040
      BiFunction<? super K, ? super V, ? extends @Nullable V> remappingFunction,
3041
      @Nullable Expiry<? super K, ? super V> expiry,
3042
      ComputeContext<K, V> ctx, boolean computeIfAbsent) {
3043
    Node<K, V> node = data.compute(keyRef, (kr, n) -> {
×
3044
      if (n == null) {
×
3045
        if (!computeIfAbsent) {
×
3046
          return null;
×
3047
        }
3048
        ctx.newValue = remappingFunction.apply(key, null);
×
3049
        if (ctx.newValue == null) {
×
3050
          // A stale refresh whose entry is absent leaves a successor's registration intact
3051
          if ((ctx.hints == null) || !ctx.hints.preserveRefresh) {
×
3052
            discardRefresh(kr);
×
3053
          }
3054
          return null;
×
3055
        }
3056
        try {
3057
          ctx.now = expirationTicker().read();
×
3058
          ctx.newWeight = weigher.weigh(key, ctx.newValue);
×
3059
          long varTime = expireAfterCreate(key, ctx.newValue, expiry, ctx.now);
×
3060
          var created = nodeFactory.newNode(keyRef, ctx.newValue,
×
3061
              valueReferenceQueue(), ctx.newWeight, ctx.now);
×
3062

3063
          long expirationTime = isComputingAsync(ctx.newValue)
×
3064
              ? ctx.now + ASYNC_EXPIRY
×
3065
              : ctx.now;
×
3066
          setAccessTime(created, expirationTime);
×
3067
          setWriteTime(created, expirationTime);
×
3068
          setVariableTime(created, varTime);
×
3069
          return created;
×
3070
        } finally {
3071
          discardRefresh(kr);
×
3072
        }
3073
      }
3074

3075
      synchronized (n) {
×
3076
        requireIsAlive(key, n);
×
3077
        ctx.nodeKey = n.getKey();
×
3078
        ctx.oldValue = n.getValue();
×
3079
        ctx.oldWeight = n.getWeight();
×
3080
        if ((ctx.nodeKey == null) || (ctx.oldValue == null)) {
×
3081
          ctx.cause = RemovalCause.COLLECTED;
×
3082
        } else if (hasExpired(n, expirationTicker().read(), ctx.oldValue)) {
×
3083
          ctx.cause = RemovalCause.EXPIRED;
×
3084
        }
3085
        if (ctx.cause != null) {
×
3086
          notifyEviction(ctx.nodeKey, ctx.oldValue, ctx.cause);
×
3087
          if (!computeIfAbsent) {
×
3088
            discardRefresh(kr);
×
3089
            ctx.removed = n;
×
3090
            n.retire();
×
3091
            return null;
×
3092
          }
3093
        }
3094

3095
        boolean wasEvicted = (ctx.cause != null);
×
3096
        try {
3097
          ctx.newValue = remappingFunction.apply(key,
×
3098
              (ctx.cause == null) ? ctx.oldValue : null);
×
3099

3100
          if (ctx.newValue == null) {
×
3101
            if (ctx.cause == null) {
×
3102
              ctx.cause = RemovalCause.EXPLICIT;
×
3103
            }
3104
            // A stale refresh whose entry was evicted leaves a successor's registration intact
3105
            if ((ctx.hints == null) || !ctx.hints.preserveRefresh) {
×
3106
              discardRefresh(kr);
×
3107
            }
3108
            ctx.removed = n;
×
3109
            n.retire();
×
3110
            return null;
×
3111
          }
3112

3113
          // If the caller flagged a same-instance return as a no-op (e.g., a refresh was rejected
3114
          // and should not touch the entry), skip the metadata updates below.
3115
          if ((ctx.hints != null) && ctx.hints.preserveTimestamps
×
3116
              && (ctx.newValue == ctx.oldValue) && (ctx.cause == null)) {
3117
            // Skip for query-style callers whose no-op path must leave any in-flight refresh intact
3118
            if (!ctx.hints.preserveRefresh) {
×
3119
              discardRefresh(kr);
×
3120
            }
3121
            return n;
×
3122
          }
3123

3124
          long varTime;
3125
          ctx.newWeight = weigher.weigh(key, ctx.newValue);
×
3126
          ctx.now = expirationTicker().read();
×
3127
          if (ctx.cause == null) {
×
3128
            if (ctx.newValue != ctx.oldValue) {
×
3129
              ctx.cause = RemovalCause.REPLACED;
×
3130
            }
3131
            varTime = expireAfterUpdate(n, key, ctx.newValue, expiry, ctx.now);
×
3132
          } else {
3133
            varTime = expireAfterCreate(key, ctx.newValue, expiry, ctx.now);
×
3134
          }
3135

3136
          if (ctx.newValue != ctx.oldValue) {
×
3137
            n.setValue(ctx.newValue, valueReferenceQueue());
×
3138
          }
3139
          n.setWeight(ctx.newWeight);
×
3140

3141
          long expirationTime = isComputingAsync(ctx.newValue)
×
3142
              ? ctx.now + ASYNC_EXPIRY
×
3143
              : ctx.now;
×
3144
          ctx.exceedsTolerance = exceedsWriteTimeTolerance(n, varTime, expirationTime);
×
3145
          if (((ctx.cause != null) && ctx.cause.wasEvicted()) || ctx.exceedsTolerance) {
×
3146
            setWriteTime(n, expirationTime);
×
3147
          }
3148
          setAccessTime(n, expirationTime);
×
3149
          setVariableTime(n, varTime);
×
3150
          discardRefresh(kr);
×
3151
          return n;
×
3152
        } catch (Throwable e) {
×
3153
          discardRefresh(kr);
×
3154
          if (!wasEvicted) {
×
3155
            throw e;
×
3156
          }
3157
          ctx.newValue = null;
×
3158
          ctx.exception = e;
×
3159
          ctx.removed = n;
×
3160
          n.retire();
×
3161
          return null;
×
3162
        }
3163
      }
3164
    });
3165

3166
    if (ctx.cause != null) {
×
3167
      if (ctx.cause == RemovalCause.REPLACED) {
×
3168
        requireNonNull(ctx.newValue);
×
3169
        notifyOnReplace(key, ctx.oldValue, ctx.newValue);
×
3170
      } else {
3171
        if (ctx.cause.wasEvicted()) {
×
3172
          statsCounter().recordEviction(ctx.oldWeight, ctx.cause);
×
3173
        }
3174
        notifyRemoval(ctx.nodeKey, ctx.oldValue, ctx.cause);
×
3175
      }
3176
    }
3177

3178
    if (ctx.removed != null) {
×
3179
      afterWrite(new RemovalTask(ctx.removed));
×
3180
    } else if (node == null) {
×
3181
      // absent and not computable
3182
    } else if ((ctx.hints != null) && ctx.hints.preserveTimestamps) {
×
3183
      // The remapping was a signaled no-op; the node was not modified
3184
    } else if ((ctx.oldValue == null) && (ctx.cause == null)) {
×
3185
      afterWrite(new AddTask(node, ctx.newWeight));
×
3186
    } else {
3187
      int weightedDifference = ctx.newWeight - ctx.oldWeight;
×
3188
      if (ctx.exceedsTolerance || (weightedDifference != 0)) {
×
3189
        afterWrite(new UpdateTask(node, weightedDifference));
×
3190
      } else {
3191
        afterRead(node, ctx.now, /* recordHit= */ false);
×
3192
        if ((ctx.cause != null) && ctx.cause.wasEvicted()) {
×
3193
          scheduleDrainBuffers();
×
3194
        }
3195
      }
3196
    }
3197

3198
    if (ctx.exception != null) {
×
3199
      throw toUncheckedException(ctx.exception);
×
3200
    }
3201
    return ctx.newValue;
×
3202
  }
3203

3204
  @Override
3205
  public void forEach(BiConsumer<? super K, ? super V> action) {
3206
    requireNonNull(action);
×
3207

3208
    for (var iterator = new EntryIterator<>(this); iterator.hasNext();) {
×
3209
      action.accept(iterator.key, iterator.value);
×
3210
      iterator.advance();
×
3211
    }
3212
  }
×
3213

3214
  @Override
3215
  public Set<K> keySet() {
3216
    Set<K> ks = keySet;
×
3217
    return (ks == null) ? (keySet = new KeySetView<>(this)) : ks;
×
3218
  }
3219

3220
  @Override
3221
  public Collection<V> values() {
3222
    Collection<V> vs = values;
×
3223
    return (vs == null) ? (values = new ValuesView<>(this)) : vs;
×
3224
  }
3225

3226
  @Override
3227
  public Set<Entry<K, V>> entrySet() {
3228
    Set<Entry<K, V>> es = entrySet;
×
3229
    return (es == null) ? (entrySet = new EntrySetView<>(this)) : es;
×
3230
  }
3231

3232
  /**
3233
   * Object equality requires reflexive, symmetric, transitive, and consistency properties. Of
3234
   * these, symmetry and consistency require further clarification for how they are upheld.
3235
   * <p>
3236
   * The <i>consistency</i> property between invocations requires that the results are the same if
3237
   * there are no modifications to the information used. Therefore, usages should expect that this
3238
   * operation may return misleading results if either the maps or the data held by them is modified
3239
   * during the execution of this method. This characteristic allows for comparing the map sizes and
3240
   * assuming stable mappings, as done by {@link java.util.AbstractMap}-based maps.
3241
   * <p>
3242
   * The <i>symmetric</i> property requires that the result is the same for all implementations of
3243
   * {@link Map#equals(Object)}. That contract is defined in terms of the stable mappings provided
3244
   * by {@link #entrySet()}, meaning that the {@link #size()} optimization forces that the count is
3245
   * consistent with the mappings when used for an equality check.
3246
   * <p>
3247
   * The cache's {@link #size()} method may include entries that have expired or have been reference
3248
   * collected, but have not yet been removed from the backing map. An iteration over the map may
3249
   * trigger the removal of these dead entries when skipped over during traversal. To ensure
3250
   * consistency and symmetry, usages should call {@link #cleanUp()} before this method while no
3251
   * other concurrent operations are being performed on this cache. This is not done implicitly by
3252
   * {@link #size()} as many usages assume it to be instantaneous and lock-free. As a postcondition
3253
   * the iteration count is verified against the prescreened {@link #size()} so that a concurrent
3254
   * maintenance pass that drops dead entries during traversal is detected and reported as not
3255
   * equal, rather than silently returning {@code true} on the surviving subset.
3256
   */
3257
  @Override
3258
  public boolean equals(@Nullable Object o) {
3259
    if (o == this) {
×
3260
      return true;
×
3261
    } else if (!(o instanceof Map)) {
×
3262
      return false;
×
3263
    }
3264

3265
    var map = (Map<?, ?>) o;
×
3266
    int expectedSize = size();
×
3267
    if (map.size() != expectedSize) {
×
3268
      return false;
×
3269
    }
3270

3271
    try {
3272
      @Var int count = 0;
×
3273
      long now = expirationTicker().read();
×
3274
      for (var node : data.values()) {
×
3275
        K key = node.getKey();
×
3276
        V value = node.getValue();
×
3277
        if ((key == null) || (value == null)
×
3278
            || !node.isAlive() || hasExpired(node, now, value)) {
×
3279
          scheduleDrainBuffers();
×
3280
          return false;
×
3281
        } else {
3282
          var val = map.get(key);
×
3283
          if ((val == null) || ((val != value) && !val.equals(value))) {
×
3284
            return false;
×
3285
          }
3286
        }
3287
        count++;
×
3288
      }
×
3289
      return (count == expectedSize);
×
3290
    } catch (ClassCastException | NullPointerException ignored) {
×
3291
      return false;
×
3292
    }
3293
  }
3294

3295
  @Override
3296
  public int hashCode() {
3297
    @Var int hash = 0;
×
3298
    @Var boolean drain = false;
×
3299
    long now = expirationTicker().read();
×
3300
    for (var node : data.values()) {
×
3301
      K key = node.getKey();
×
3302
      V value = node.getValue();
×
3303
      if ((key == null) || (value == null)
×
3304
          || !node.isAlive() || hasExpired(node, now, value)) {
×
3305
        drain = true;
×
3306
      } else {
3307
        hash += key.hashCode() ^ value.hashCode();
×
3308
      }
3309
    }
×
3310
    if (drain) {
×
3311
      scheduleDrainBuffers();
×
3312
    }
3313
    return hash;
×
3314
  }
3315

3316
  @Override
3317
  public String toString() {
3318
    @Var boolean drain = false;
×
3319
    long now = expirationTicker().read();
×
3320
    var result = new StringBuilder().append('{');
×
3321
    for (var node : data.values()) {
×
3322
      K key = node.getKey();
×
3323
      V value = node.getValue();
×
3324
      if ((key == null) || (value == null)
×
3325
          || !node.isAlive() || hasExpired(node, now, value)) {
×
3326
        drain = true;
×
3327
      } else {
3328
        if (result.length() != 1) {
×
3329
          result.append(',').append(' ');
×
3330
        }
3331
        result.append((key == this) ? "(this Map)" : key);
×
3332
        result.append('=');
×
3333
        result.append((value == this) ? "(this Map)" : value);
×
3334
      }
3335
    }
×
3336
    if (drain) {
×
3337
      scheduleDrainBuffers();
×
3338
    }
3339
    return result.append('}').toString();
×
3340
  }
3341

3342
  /**
3343
   * Returns the computed result from the ordered traversal of the cache entries.
3344
   *
3345
   * @param hottest the coldest or hottest iteration order
3346
   * @param transformer a function that unwraps the value
3347
   * @param mappingFunction the mapping function to compute a value
3348
   * @return the computed value
3349
   */
3350
  @SuppressWarnings("GuardedByChecker")
3351
  <T> T evictionOrder(boolean hottest, Function<@Nullable V, @Nullable V> transformer,
3352
      Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
3353
    Comparator<Node<K, V>> comparator = Comparator.comparingInt(node -> {
×
3354
      var keyRef = node.getKeyReferenceOrNull();
×
3355
      return ((keyRef == null) || !node.isAlive()) ? 0 : frequencySketch().frequency(keyRef);
×
3356
    });
3357
    Iterable<Node<K, V>> iterable;
3358
    if (hottest) {
×
3359
      iterable = () -> {
×
3360
        var secondary = PeekingIterator.comparing(
×
3361
            accessOrderProbationDeque().descendingIterator(),
×
3362
            accessOrderWindowDeque().descendingIterator(), comparator);
×
3363
        return PeekingIterator.concat(
×
3364
            accessOrderProtectedDeque().descendingIterator(), secondary);
×
3365
      };
3366
    } else {
3367
      iterable = () -> {
×
3368
        var primary = PeekingIterator.comparing(
×
3369
            accessOrderWindowDeque().iterator(), accessOrderProbationDeque().iterator(),
×
3370
            comparator.reversed());
×
3371
        return PeekingIterator.concat(primary, accessOrderProtectedDeque().iterator());
×
3372
      };
3373
    }
3374
    return snapshot(iterable, transformer, mappingFunction);
×
3375
  }
3376

3377
  /**
3378
   * Returns the computed result from the ordered traversal of the cache entries.
3379
   *
3380
   * @param oldest the youngest or oldest iteration order
3381
   * @param transformer a function that unwraps the value
3382
   * @param mappingFunction the mapping function to compute a value
3383
   * @return the computed value
3384
   */
3385
  @SuppressWarnings("GuardedByChecker")
3386
  <T> T expireAfterAccessOrder(boolean oldest, Function<@Nullable V, @Nullable V> transformer,
3387
      Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
3388
    Iterable<Node<K, V>> iterable;
3389
    if (evicts()) {
×
3390
      iterable = () -> {
×
3391
        @Var Comparator<Node<K, V>> comparator = Comparator.comparingLong(Node::getAccessTime);
×
3392
        PeekingIterator<Node<K, V>> first;
3393
        PeekingIterator<Node<K, V>> second;
3394
        PeekingIterator<Node<K, V>> third;
3395
        if (oldest) {
×
3396
          comparator = comparator.reversed();
×
3397
          first = accessOrderWindowDeque().iterator();
×
3398
          second = accessOrderProbationDeque().iterator();
×
3399
          third = accessOrderProtectedDeque().iterator();
×
3400
        } else {
3401
          first = accessOrderWindowDeque().descendingIterator();
×
3402
          second = accessOrderProbationDeque().descendingIterator();
×
3403
          third = accessOrderProtectedDeque().descendingIterator();
×
3404
        }
3405
        return PeekingIterator.comparing(
×
3406
            PeekingIterator.comparing(first, second, comparator), third, comparator);
×
3407
      };
3408
    } else {
3409
      iterable = oldest
×
3410
          ? accessOrderWindowDeque()
×
3411
          : accessOrderWindowDeque()::descendingIterator;
×
3412
    }
3413
    return snapshot(iterable, transformer, mappingFunction);
×
3414
  }
3415

3416
  /**
3417
   * Returns the computed result from the ordered traversal of the cache entries.
3418
   *
3419
   * @param iterable the supplier of the entries in the cache
3420
   * @param transformer a function that unwraps the value
3421
   * @param mappingFunction the mapping function to compute a value
3422
   * @return the computed value
3423
   */
3424
  <T> T snapshot(Iterable<Node<K, V>> iterable, Function<@Nullable V, @Nullable V> transformer,
3425
      Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
3426
    requireNonNull(mappingFunction);
×
3427
    requireNonNull(transformer);
×
3428
    requireNonNull(iterable);
×
3429

3430
    evictionLock.lock();
×
3431
    try {
3432
      maintenance(/* ignored */ null);
×
3433

3434
      // Obtain the iterator as late as possible for modification count checking
3435
      try (var stream = StreamSupport.stream(Spliterators.spliteratorUnknownSize(
×
3436
           iterable.iterator(), DISTINCT | ORDERED | NONNULL | IMMUTABLE), /* parallel= */ false)) {
×
3437
        return mappingFunction.apply(stream
×
3438
            .map(node -> nodeToCacheEntry(node, transformer, node.getPolicyWeight()))
×
3439
            .filter(Objects::nonNull));
×
3440
      }
3441
    } finally {
3442
      evictionLock.unlock();
×
3443
      rescheduleCleanUpIfIncomplete();
×
3444
    }
3445
  }
3446

3447
  /**
3448
   * Returns an entry for the given node if it can be used externally, else null. The weight is
3449
   * caller-supplied: snapshot callers hold evictionLock and read policyWeight (in sync with the
3450
   * drain thread); unlocked readers read weight, which may be stale for a concurrent in-place
3451
   * update (acceptable for a point-in-time CacheEntry).
3452
   */
3453
  @Nullable CacheEntry<K, V> nodeToCacheEntry(
3454
      Node<K, V> node, Function<@Nullable V, @Nullable V> transformer, int weight) {
3455
    V rawValue = node.getValue();
×
3456
    if (rawValue == null) {
×
3457
      return null;
×
3458
    }
3459
    V value = transformer.apply(rawValue);
×
3460
    K key = node.getKey();
×
3461
    long now;
3462
    if ((key == null) || (value == null) || !node.isAlive()
×
3463
        || hasExpired(node, (now = expirationTicker().read()), rawValue)) {
×
3464
      return null;
×
3465
    }
3466

3467
    @Var long expiresAfter = Long.MAX_VALUE;
×
3468
    if (expiresAfterAccess()) {
×
3469
      expiresAfter = Math.min(expiresAfter,
×
3470
          expiresAfterAccessNanos() - (now - node.getAccessTime()));
×
3471
    }
3472
    if (expiresAfterWrite()) {
×
3473
      expiresAfter = Math.min(expiresAfter,
×
3474
          expiresAfterWriteNanos() - ((now & ~1L) - (node.getWriteTime() & ~1L)));
×
3475
    }
3476
    if (expiresVariable()) {
×
3477
      expiresAfter = node.getVariableTime() - now;
×
3478
    }
3479

3480
    long refreshableAt = refreshAfterWrite()
×
3481
        ? (node.getWriteTime() & ~1L) + refreshAfterWriteNanos()
×
3482
        : now + Long.MAX_VALUE;
×
3483
    return SnapshotEntry.forEntry(key, value, now, weight, now + expiresAfter, refreshableAt);
×
3484
  }
3485

3486
  /** Mutable context for passing state between a lambda and the caller. */
3487
  static final class EvictContext<V> {
×
3488
    @Nullable RemovalCause cause;
3489
    @Nullable V value;
3490
    boolean resurrect;
3491
    boolean removed;
3492
    int oldWeight;
3493
  }
3494

3495
  /** Mutable context for passing state between a lambda and the caller. */
3496
  static final class RemoveContext<K, V> {
×
3497
    @Nullable K oldKey;
3498
    @Nullable V oldValue;
3499
    @Nullable Node<K, V> node;
3500
    @Nullable RemovalCause cause;
3501
    int oldWeight;
3502
  }
3503

3504
  /** Mutable context for passing state between a lambda and the caller. */
3505
  static final class ReplaceContext<K, V> {
×
3506
    @Nullable K nodeKey;
3507
    @Nullable V oldValue;
3508

3509
    long now;
3510
    int oldWeight;
3511
    boolean exceedsTolerance;
3512
  }
3513

3514
  /** Mutable context for passing state between a lambda and the caller. */
3515
  static final class ComputeContext<K, V> {
3516
    @Nullable K nodeKey;
3517
    @Nullable V oldValue;
3518
    @Nullable V newValue;
3519
    @Nullable Node<K, V> removed;
3520
    @Nullable RemovalCause cause;
3521
    @Nullable Throwable exception;
3522
    @Nullable RemapHints hints;
3523

3524
    long now;
3525
    int oldWeight;
3526
    int newWeight;
3527
    boolean exceedsTolerance;
3528

3529
    ComputeContext(long now) {
×
3530
      this.now = now;
×
3531
    }
×
3532
  }
3533

3534
  /** A function that produces an unmodifiable map up to the limit in stream order. */
3535
  static final class SizeLimiter<K, V> implements Function<Stream<CacheEntry<K, V>>, Map<K, V>> {
3536
    private final int expectedSize;
3537
    private final long limit;
3538

3539
    SizeLimiter(int expectedSize, long limit) {
×
3540
      requireArgument(limit >= 0, "limit cannot be negative: %s", limit);
×
3541
      this.expectedSize = expectedSize;
×
3542
      this.limit = limit;
×
3543
    }
×
3544

3545
    @Override
3546
    public Map<K, V> apply(Stream<CacheEntry<K, V>> stream) {
3547
      var map = new LinkedHashMap<K, V>(calculateHashMapCapacity(expectedSize));
×
3548
      stream.limit(limit).forEach(entry -> map.put(entry.getKey(), entry.getValue()));
×
3549
      return Collections.unmodifiableMap(map);
×
3550
    }
3551
  }
3552

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

3557
    long weightedSize;
3558

3559
    WeightLimiter(long weightLimit) {
×
3560
      requireArgument(weightLimit >= 0, "weight limit cannot be negative: %s", weightLimit);
×
3561
      this.weightLimit = weightLimit;
×
3562
    }
×
3563

3564
    @Override
3565
    public Map<K, V> apply(Stream<CacheEntry<K, V>> stream) {
3566
      var map = new LinkedHashMap<K, V>();
×
3567
      stream.takeWhile(entry -> {
×
3568
        weightedSize += entry.weight();
×
3569
        if (weightedSize < 0) {
×
3570
          weightedSize = Long.MAX_VALUE;
×
3571
        }
3572
        return (weightedSize <= weightLimit);
×
3573
      }).forEach(entry -> map.put(entry.getKey(), entry.getValue()));
×
3574
      return Collections.unmodifiableMap(map);
×
3575
    }
3576
  }
3577

3578
  /** An adapter to safely externalize the keys. */
3579
  static final class KeySetView<K, V> extends AbstractSet<K> {
3580
    final BoundedLocalCache<K, V> cache;
3581

3582
    KeySetView(BoundedLocalCache<K, V> cache) {
×
3583
      this.cache = requireNonNull(cache);
×
3584
    }
×
3585

3586
    @Override
3587
    public int size() {
3588
      return cache.size();
×
3589
    }
3590

3591
    @Override
3592
    public void clear() {
3593
      cache.clear();
×
3594
    }
×
3595

3596
    @Override
3597
    @SuppressWarnings("SuspiciousMethodCalls")
3598
    public boolean contains(Object o) {
3599
      return cache.containsKey(o);
×
3600
    }
3601

3602
    @Override
3603
    public boolean containsAll(Collection<?> collection) {
3604
      requireNonNull(collection);
×
3605
      if (collection != this) {
×
3606
        for (Object o : collection) {
×
3607
          if ((o == null) || !contains(o)) {
×
3608
            return false;
×
3609
          }
3610
        }
×
3611
      }
3612
      return true;
×
3613
    }
3614

3615
    @Override
3616
    public boolean removeAll(Collection<?> collection) {
3617
      requireNonNull(collection);
×
3618
      @Var boolean modified = false;
×
3619
      if (cache.collectKeys() || ((collection instanceof Set<?>) && (collection.size() > size()))) {
×
3620
        for (K key : this) {
×
3621
          if (collection.contains(key)) {
×
3622
            modified |= remove(key);
×
3623
          }
3624
        }
×
3625
      } else {
3626
        for (var item : collection) {
×
3627
          modified |= (item != null) && remove(item);
×
3628
        }
×
3629
      }
3630
      return modified;
×
3631
    }
3632

3633
    @Override
3634
    public boolean remove(Object o) {
3635
      return (cache.remove(o) != null);
×
3636
    }
3637

3638
    @Override
3639
    public boolean removeIf(Predicate<? super K> filter) {
3640
      requireNonNull(filter);
×
3641
      @Var boolean modified = false;
×
3642
      for (K key : this) {
×
3643
        if (filter.test(key) && remove(key)) {
×
3644
          modified = true;
×
3645
        }
3646
      }
×
3647
      return modified;
×
3648
    }
3649

3650
    @Override
3651
    public boolean retainAll(Collection<?> collection) {
3652
      requireNonNull(collection);
×
3653
      @Var boolean modified = false;
×
3654
      for (K key : this) {
×
3655
        if (!collection.contains(key) && remove(key)) {
×
3656
          modified = true;
×
3657
        }
3658
      }
×
3659
      return modified;
×
3660
    }
3661

3662
    @Override
3663
    public Iterator<K> iterator() {
3664
      return new KeyIterator<>(cache);
×
3665
    }
3666

3667
    @Override
3668
    public Spliterator<K> spliterator() {
3669
      return new KeySpliterator<>(cache);
×
3670
    }
3671
  }
3672

3673
  /** An adapter to safely externalize the key iterator. */
3674
  static final class KeyIterator<K, V> implements Iterator<K> {
3675
    final EntryIterator<K, V> iterator;
3676

3677
    KeyIterator(BoundedLocalCache<K, V> cache) {
×
3678
      this.iterator = new EntryIterator<>(cache);
×
3679
    }
×
3680

3681
    @Override
3682
    public boolean hasNext() {
3683
      return iterator.hasNext();
×
3684
    }
3685

3686
    @Override
3687
    public K next() {
3688
      return iterator.nextKey();
×
3689
    }
3690

3691
    @Override
3692
    public void remove() {
3693
      iterator.remove();
×
3694
    }
×
3695
  }
3696

3697
  /** An adapter to safely externalize the key spliterator. */
3698
  static final class KeySpliterator<K, V> implements Spliterator<K> {
3699
    final Spliterator<Node<K, V>> spliterator;
3700
    final BoundedLocalCache<K, V> cache;
3701

3702
    KeySpliterator(BoundedLocalCache<K, V> cache) {
3703
      this(cache, cache.data.values().spliterator());
×
3704
    }
×
3705

3706
    KeySpliterator(BoundedLocalCache<K, V> cache, Spliterator<Node<K, V>> spliterator) {
×
3707
      this.spliterator = requireNonNull(spliterator);
×
3708
      this.cache = requireNonNull(cache);
×
3709
    }
×
3710

3711
    @Override
3712
    public void forEachRemaining(Consumer<? super K> action) {
3713
      requireNonNull(action);
×
3714
      Consumer<Node<K, V>> consumer = node -> {
×
3715
        K key = node.getKey();
×
3716
        V value = node.getValue();
×
3717
        long now = cache.expirationTicker().read();
×
3718
        if ((key == null) || (value == null) || cache.hasExpired(node, now, value)) {
×
3719
          cache.scheduleDrainBuffers();
×
3720
        } else if (node.isAlive()) {
×
3721
          action.accept(key);
×
3722
        }
3723
      };
×
3724
      spliterator.forEachRemaining(consumer);
×
3725
    }
×
3726

3727
    @Override
3728
    public boolean tryAdvance(Consumer<? super K> action) {
3729
      requireNonNull(action);
×
3730
      boolean[] advanced = { false };
×
3731
      Consumer<Node<K, V>> consumer = node -> {
×
3732
        K key = node.getKey();
×
3733
        V value = node.getValue();
×
3734
        long now = cache.expirationTicker().read();
×
3735
        if ((key == null) || (value == null) || cache.hasExpired(node, now, value)) {
×
3736
          cache.scheduleDrainBuffers();
×
3737
        } else if (node.isAlive()) {
×
3738
          action.accept(key);
×
3739
          advanced[0] = true;
×
3740
        }
3741
      };
×
3742
      while (spliterator.tryAdvance(consumer)) {
×
3743
        if (advanced[0]) {
×
3744
          return true;
×
3745
        }
3746
      }
3747
      return false;
×
3748
    }
3749

3750
    @Override
3751
    public @Nullable Spliterator<K> trySplit() {
3752
      Spliterator<Node<K, V>> split = spliterator.trySplit();
×
3753
      return (split == null) ? null : new KeySpliterator<>(cache, split);
×
3754
    }
3755

3756
    @Override
3757
    public long estimateSize() {
3758
      return spliterator.estimateSize();
×
3759
    }
3760

3761
    @Override
3762
    public int characteristics() {
3763
      return DISTINCT | CONCURRENT | NONNULL;
×
3764
    }
3765
  }
3766

3767
  /** An adapter to safely externalize the values. */
3768
  static final class ValuesView<K, V> extends AbstractCollection<V> {
3769
    final BoundedLocalCache<K, V> cache;
3770

3771
    ValuesView(BoundedLocalCache<K, V> cache) {
×
3772
      this.cache = requireNonNull(cache);
×
3773
    }
×
3774

3775
    @Override
3776
    public int size() {
3777
      return cache.size();
×
3778
    }
3779

3780
    @Override
3781
    public void clear() {
3782
      cache.clear();
×
3783
    }
×
3784

3785
    @Override
3786
    @SuppressWarnings("SuspiciousMethodCalls")
3787
    public boolean contains(Object o) {
3788
      return cache.containsValue(o);
×
3789
    }
3790

3791
    @Override
3792
    public boolean containsAll(Collection<?> collection) {
3793
      requireNonNull(collection);
×
3794
      if (collection != this) {
×
3795
        for (Object o : collection) {
×
3796
          if ((o == null) || !contains(o)) {
×
3797
            return false;
×
3798
          }
3799
        }
×
3800
      }
3801
      return true;
×
3802
    }
3803

3804
    @Override
3805
    public boolean removeAll(Collection<?> collection) {
3806
      requireNonNull(collection);
×
3807
      @Var boolean modified = false;
×
3808
      for (var iterator = new EntryIterator<>(cache); iterator.hasNext();) {
×
3809
        var key = requireNonNull(iterator.key);
×
3810
        var value = requireNonNull(iterator.value);
×
3811
        if (collection.contains(value) && cache.remove(key, value)) {
×
3812
          modified = true;
×
3813
        }
3814
        iterator.advance();
×
3815
      }
×
3816
      return modified;
×
3817
    }
3818

3819
    @Override
3820
    public boolean remove(@Nullable Object o) {
3821
      if (o == null) {
×
3822
        return false;
×
3823
      }
3824
      for (var iterator = new EntryIterator<>(cache); iterator.hasNext();) {
×
3825
        var key = requireNonNull(iterator.key);
×
3826
        var node = requireNonNull(iterator.next);
×
3827
        var value = requireNonNull(iterator.value);
×
3828
        if (node.containsValue(o) && cache.remove(key, value)) {
×
3829
          return true;
×
3830
        }
3831
        iterator.advance();
×
3832
      }
×
3833
      return false;
×
3834
    }
3835

3836
    @Override
3837
    public boolean removeIf(Predicate<? super V> filter) {
3838
      requireNonNull(filter);
×
3839
      @Var boolean modified = false;
×
3840
      for (var iterator = new EntryIterator<>(cache); iterator.hasNext();) {
×
3841
        var value = requireNonNull(iterator.value);
×
3842
        if (filter.test(value)) {
×
3843
          var key = requireNonNull(iterator.key);
×
3844
          modified |= cache.remove(key, value);
×
3845
        }
3846
        iterator.advance();
×
3847
      }
×
3848
      return modified;
×
3849
    }
3850

3851
    @Override
3852
    public boolean retainAll(Collection<?> collection) {
3853
      requireNonNull(collection);
×
3854
      @Var boolean modified = false;
×
3855
      for (var iterator = new EntryIterator<>(cache); iterator.hasNext();) {
×
3856
        var key = requireNonNull(iterator.key);
×
3857
        var value = requireNonNull(iterator.value);
×
3858
        if (!collection.contains(value) && cache.remove(key, value)) {
×
3859
          modified = true;
×
3860
        }
3861
        iterator.advance();
×
3862
      }
×
3863
      return modified;
×
3864
    }
3865

3866
    @Override
3867
    public Iterator<V> iterator() {
3868
      return new ValueIterator<>(cache);
×
3869
    }
3870

3871
    @Override
3872
    public Spliterator<V> spliterator() {
3873
      return new ValueSpliterator<>(cache);
×
3874
    }
3875
  }
3876

3877
  /** An adapter to safely externalize the value iterator. */
3878
  static final class ValueIterator<K, V> implements Iterator<V> {
3879
    final EntryIterator<K, V> iterator;
3880

3881
    ValueIterator(BoundedLocalCache<K, V> cache) {
×
3882
      this.iterator = new EntryIterator<>(cache);
×
3883
    }
×
3884

3885
    @Override
3886
    public boolean hasNext() {
3887
      return iterator.hasNext();
×
3888
    }
3889

3890
    @Override
3891
    public V next() {
3892
      return iterator.nextValue();
×
3893
    }
3894

3895
    @Override
3896
    public void remove() {
3897
      iterator.remove();
×
3898
    }
×
3899
  }
3900

3901
  /** An adapter to safely externalize the value spliterator. */
3902
  static final class ValueSpliterator<K, V> implements Spliterator<V> {
3903
    final Spliterator<Node<K, V>> spliterator;
3904
    final BoundedLocalCache<K, V> cache;
3905

3906
    ValueSpliterator(BoundedLocalCache<K, V> cache) {
3907
      this(cache, cache.data.values().spliterator());
×
3908
    }
×
3909

3910
    ValueSpliterator(BoundedLocalCache<K, V> cache, Spliterator<Node<K, V>> spliterator) {
×
3911
      this.spliterator = requireNonNull(spliterator);
×
3912
      this.cache = requireNonNull(cache);
×
3913
    }
×
3914

3915
    @Override
3916
    public void forEachRemaining(Consumer<? super V> action) {
3917
      requireNonNull(action);
×
3918
      Consumer<Node<K, V>> consumer = node -> {
×
3919
        K key = node.getKey();
×
3920
        V value = node.getValue();
×
3921
        long now = cache.expirationTicker().read();
×
3922
        if ((key == null) || (value == null) || cache.hasExpired(node, now, value)) {
×
3923
          cache.scheduleDrainBuffers();
×
3924
        } else if (node.isAlive()) {
×
3925
          action.accept(value);
×
3926
        }
3927
      };
×
3928
      spliterator.forEachRemaining(consumer);
×
3929
    }
×
3930

3931
    @Override
3932
    public boolean tryAdvance(Consumer<? super V> action) {
3933
      requireNonNull(action);
×
3934
      boolean[] advanced = { false };
×
3935
      Consumer<Node<K, V>> consumer = node -> {
×
3936
        K key = node.getKey();
×
3937
        V value = node.getValue();
×
3938
        long now = cache.expirationTicker().read();
×
3939
        if ((key == null) || (value == null) || cache.hasExpired(node, now, value)) {
×
3940
          cache.scheduleDrainBuffers();
×
3941
        } else if (node.isAlive()) {
×
3942
          action.accept(value);
×
3943
          advanced[0] = true;
×
3944
        }
3945
      };
×
3946
      while (spliterator.tryAdvance(consumer)) {
×
3947
        if (advanced[0]) {
×
3948
          return true;
×
3949
        }
3950
      }
3951
      return false;
×
3952
    }
3953

3954
    @Override
3955
    public @Nullable Spliterator<V> trySplit() {
3956
      Spliterator<Node<K, V>> split = spliterator.trySplit();
×
3957
      return (split == null) ? null : new ValueSpliterator<>(cache, split);
×
3958
    }
3959

3960
    @Override
3961
    public long estimateSize() {
3962
      return spliterator.estimateSize();
×
3963
    }
3964

3965
    @Override
3966
    public int characteristics() {
3967
      return CONCURRENT | NONNULL;
×
3968
    }
3969
  }
3970

3971
  /** An adapter to safely externalize the entries. */
3972
  static final class EntrySetView<K, V> extends AbstractSet<Entry<K, V>> {
3973
    final BoundedLocalCache<K, V> cache;
3974

3975
    EntrySetView(BoundedLocalCache<K, V> cache) {
×
3976
      this.cache = requireNonNull(cache);
×
3977
    }
×
3978

3979
    @Override
3980
    public int size() {
3981
      return cache.size();
×
3982
    }
3983

3984
    @Override
3985
    public void clear() {
3986
      cache.clear();
×
3987
    }
×
3988

3989
    @Override
3990
    public boolean contains(Object o) {
3991
      if (!(o instanceof Entry<?, ?>)) {
×
3992
        return false;
×
3993
      }
3994
      var entry = (Entry<?, ?>) o;
×
3995
      var key = entry.getKey();
×
3996
      var value = entry.getValue();
×
3997
      if ((key == null) || (value == null)) {
×
3998
        return false;
×
3999
      }
4000
      Node<K, V> node = cache.data.get(cache.nodeFactory.newLookupKey(key));
×
4001
      if (node == null) {
×
4002
        return false;
×
4003
      }
4004
      V nodeValue = node.getValue();
×
4005
      return (nodeValue != null) && node.containsValue(value)
×
4006
          && !cache.hasExpired(node, cache.expirationTicker().read(), nodeValue);
×
4007
    }
4008

4009
    @Override
4010
    public boolean removeAll(Collection<?> collection) {
4011
      requireNonNull(collection);
×
4012
      @Var boolean modified = false;
×
4013
      if (cache.collectKeys() || ((collection instanceof Set<?>) && (collection.size() > size()))) {
×
4014
        for (var entry : this) {
×
4015
          if (collection.contains(entry)) {
×
4016
            modified |= remove(entry);
×
4017
          }
4018
        }
×
4019
      } else {
4020
        for (var item : collection) {
×
4021
          modified |= (item != null) && remove(item);
×
4022
        }
×
4023
      }
4024
      return modified;
×
4025
    }
4026

4027
    @Override
4028
    @SuppressWarnings("SuspiciousMethodCalls")
4029
    public boolean remove(Object o) {
4030
      if (!(o instanceof Entry<?, ?>)) {
×
4031
        return false;
×
4032
      }
4033
      var entry = (Entry<?, ?>) o;
×
4034
      var key = entry.getKey();
×
4035
      return (key != null) && cache.remove(key, entry.getValue());
×
4036
    }
4037

4038
    @Override
4039
    public boolean removeIf(Predicate<? super Entry<K, V>> filter) {
4040
      requireNonNull(filter);
×
4041
      @Var boolean modified = false;
×
4042
      for (var iterator = new EntryIterator<>(cache); iterator.hasNext();) {
×
4043
        var key = requireNonNull(iterator.key);
×
4044
        var value = requireNonNull(iterator.value);
×
4045
        if (filter.test(Map.entry(key, value))) {
×
4046
          modified |= cache.remove(key, value);
×
4047
        }
4048
        iterator.advance();
×
4049
      }
×
4050
      return modified;
×
4051
    }
4052

4053
    @Override
4054
    public boolean retainAll(Collection<?> collection) {
4055
      requireNonNull(collection);
×
4056
      @Var boolean modified = false;
×
4057
      for (var entry : this) {
×
4058
        if (!collection.contains(entry) && remove(entry)) {
×
4059
          modified = true;
×
4060
        }
4061
      }
×
4062
      return modified;
×
4063
    }
4064

4065
    @Override
4066
    public Iterator<Entry<K, V>> iterator() {
4067
      return new EntryIterator<>(cache);
×
4068
    }
4069

4070
    @Override
4071
    public Spliterator<Entry<K, V>> spliterator() {
4072
      return new EntrySpliterator<>(cache);
×
4073
    }
4074
  }
4075

4076
  /** An adapter to safely externalize the entry iterator. */
4077
  static final class EntryIterator<K, V> implements Iterator<Entry<K, V>> {
4078
    final BoundedLocalCache<K, V> cache;
4079
    final Iterator<Node<K, V>> iterator;
4080

4081
    @Nullable K key;
4082
    @Nullable V value;
4083
    @Nullable K removalKey;
4084
    @Nullable Node<K, V> next;
4085

4086
    EntryIterator(BoundedLocalCache<K, V> cache) {
×
4087
      this.iterator = cache.data.values().iterator();
×
4088
      this.cache = cache;
×
4089
    }
×
4090

4091
    @Override
4092
    public boolean hasNext() {
4093
      if (next != null) {
×
4094
        return true;
×
4095
      }
4096

4097
      long now = cache.expirationTicker().read();
×
4098
      while (iterator.hasNext()) {
×
4099
        next = iterator.next();
×
4100
        value = next.getValue();
×
4101
        key = next.getKey();
×
4102

4103
        boolean evictable = (key == null) || (value == null) || cache.hasExpired(next, now, value);
×
4104
        if (evictable || !next.isAlive()) {
×
4105
          if (evictable) {
×
4106
            cache.scheduleDrainBuffers();
×
4107
          }
4108
          advance();
×
4109
          continue;
×
4110
        }
4111
        return true;
×
4112
      }
4113
      return false;
×
4114
    }
4115

4116
    /** Invalidates the current position so that the iterator may compute the next position. */
4117
    void advance() {
4118
      value = null;
×
4119
      next = null;
×
4120
      key = null;
×
4121
    }
×
4122

4123
    K nextKey() {
4124
      if (!hasNext()) {
×
4125
        throw new NoSuchElementException();
×
4126
      }
4127
      removalKey = key;
×
4128
      advance();
×
4129
      return requireNonNull(removalKey);
×
4130
    }
4131

4132
    V nextValue() {
4133
      if (!hasNext()) {
×
4134
        throw new NoSuchElementException();
×
4135
      }
4136
      removalKey = key;
×
4137
      V val = value;
×
4138
      advance();
×
4139
      return requireNonNull(val);
×
4140
    }
4141

4142
    @Override
4143
    public Entry<K, V> next() {
4144
      if (!hasNext()) {
×
4145
        throw new NoSuchElementException();
×
4146
      }
4147
      var entry = new WriteThroughEntry<K, @NonNull V>(
×
4148
          cache, requireNonNull(key), requireNonNull(value));
×
4149
      removalKey = key;
×
4150
      advance();
×
4151
      return entry;
×
4152
    }
4153

4154
    @Override
4155
    public void remove() {
4156
      if (removalKey == null) {
×
4157
        throw new IllegalStateException();
×
4158
      }
4159
      cache.remove(removalKey);
×
4160
      removalKey = null;
×
4161
    }
×
4162
  }
4163

4164
  /** An adapter to safely externalize the entry spliterator. */
4165
  static final class EntrySpliterator<K, V> implements Spliterator<Entry<K, V>> {
4166
    final Spliterator<Node<K, V>> spliterator;
4167
    final BoundedLocalCache<K, V> cache;
4168

4169
    EntrySpliterator(BoundedLocalCache<K, V> cache) {
4170
      this(cache, cache.data.values().spliterator());
×
4171
    }
×
4172

4173
    EntrySpliterator(BoundedLocalCache<K, V> cache, Spliterator<Node<K, V>> spliterator) {
×
4174
      this.spliterator = requireNonNull(spliterator);
×
4175
      this.cache = requireNonNull(cache);
×
4176
    }
×
4177

4178
    @Override
4179
    public void forEachRemaining(Consumer<? super Entry<K, V>> action) {
4180
      requireNonNull(action);
×
4181
      Consumer<Node<K, V>> consumer = node -> {
×
4182
        K key = node.getKey();
×
4183
        V value = node.getValue();
×
4184
        long now = cache.expirationTicker().read();
×
4185
        if ((key == null) || (value == null) || cache.hasExpired(node, now, value)) {
×
4186
          cache.scheduleDrainBuffers();
×
4187
        } else if (node.isAlive()) {
×
4188
          action.accept(new WriteThroughEntry<>(cache, key, value));
×
4189
        }
4190
      };
×
4191
      spliterator.forEachRemaining(consumer);
×
4192
    }
×
4193

4194
    @Override
4195
    public boolean tryAdvance(Consumer<? super Entry<K, V>> action) {
4196
      requireNonNull(action);
×
4197
      boolean[] advanced = { false };
×
4198
      Consumer<Node<K, V>> consumer = node -> {
×
4199
        K key = node.getKey();
×
4200
        V value = node.getValue();
×
4201
        long now = cache.expirationTicker().read();
×
4202
        if ((key == null) || (value == null) || cache.hasExpired(node, now, value)) {
×
4203
          cache.scheduleDrainBuffers();
×
4204
        } else if (node.isAlive()) {
×
4205
          action.accept(new WriteThroughEntry<>(cache, key, value));
×
4206
          advanced[0] = true;
×
4207
        }
4208
      };
×
4209
      while (spliterator.tryAdvance(consumer)) {
×
4210
        if (advanced[0]) {
×
4211
          return true;
×
4212
        }
4213
      }
4214
      return false;
×
4215
    }
4216

4217
    @Override
4218
    public @Nullable Spliterator<Entry<K, V>> trySplit() {
4219
      Spliterator<Node<K, V>> split = spliterator.trySplit();
×
4220
      return (split == null) ? null : new EntrySpliterator<>(cache, split);
×
4221
    }
4222

4223
    @Override
4224
    public long estimateSize() {
4225
      return spliterator.estimateSize();
×
4226
    }
4227

4228
    @Override
4229
    public int characteristics() {
4230
      return DISTINCT | CONCURRENT | NONNULL;
×
4231
    }
4232
  }
4233

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

4238
    final WeakReference<BoundedLocalCache<?, ?>> reference;
4239

4240
    PerformCleanupTask(BoundedLocalCache<?, ?> cache) {
×
4241
      reference = new WeakReference<>(cache);
×
4242
    }
×
4243

4244
    @Override
4245
    protected boolean exec() {
4246
      try {
4247
        run();
×
4248
      } catch (Throwable t) {
×
4249
        logger.log(Level.ERROR, "Exception thrown when performing the maintenance task", t);
×
4250
      }
×
4251

4252
      // Indicates that the task has not completed to allow subsequent submissions to execute
4253
      return false;
×
4254
    }
4255

4256
    @Override
4257
    public void run() {
4258
      BoundedLocalCache<?, ?> cache = reference.get();
×
4259
      if (cache != null) {
×
4260
        cache.performCleanUp(/* ignored */ null);
×
4261
      }
4262
    }
×
4263

4264
    /**
4265
     * This method cannot be ignored due to being final, so a hostile user supplied Executor could
4266
     * forcibly complete the task and halt future executions. There are easier ways to intentionally
4267
     * harm a system, so this is assumed to not happen in practice.
4268
     */
4269
    // public final void quietlyComplete() {}
4270

4271
    @Override public void complete(@Nullable Void value) {}
×
4272
    @Override public void setRawResult(@Nullable Void value) {}
×
4273
    @Override public @Nullable Void getRawResult() { return null; }
×
4274
    @Override public void completeExceptionally(@Nullable Throwable t) {}
×
4275
    @Override public boolean cancel(boolean mayInterruptIfRunning) { return false; }
×
4276
  }
4277

4278
  /** Creates a serialization proxy based on the common configuration shared by all cache types. */
4279
  static <K, V> SerializationProxy<K, V> makeSerializationProxy(BoundedLocalCache<?, ?> cache) {
4280
    var proxy = new SerializationProxy<K, V>();
×
4281
    proxy.weakKeys = cache.collectKeys();
×
4282
    proxy.weakValues = cache.nodeFactory.weakValues();
×
4283
    proxy.softValues = cache.nodeFactory.softValues();
×
4284
    proxy.isRecordingStats = cache.isRecordingStats();
×
4285
    proxy.evictionListener = cache.evictionListener;
×
4286
    proxy.removalListener = cache.removalListener();
×
4287
    proxy.ticker = cache.expirationTicker();
×
4288
    if (cache.expiresAfterAccess()) {
×
4289
      proxy.expiresAfterAccessNanos = cache.expiresAfterAccessNanos();
×
4290
    }
4291
    if (cache.expiresAfterWrite()) {
×
4292
      proxy.expiresAfterWriteNanos = cache.expiresAfterWriteNanos();
×
4293
    }
4294
    if (cache.expiresVariable()) {
×
4295
      proxy.expiry = cache.expiry();
×
4296
    }
4297
    if (cache.refreshAfterWrite()) {
×
4298
      proxy.refreshAfterWriteNanos = cache.refreshAfterWriteNanos();
×
4299
    }
4300
    if (cache.evicts()) {
×
4301
      if (cache.isWeighted) {
×
4302
        proxy.weigher = cache.weigher;
×
4303
        proxy.maximumWeight = cache.maximum();
×
4304
      } else {
4305
        proxy.maximumSize = cache.maximum();
×
4306
      }
4307
    }
4308
    proxy.cacheLoader = cache.cacheLoader;
×
4309
    proxy.async = cache.isAsync;
×
4310
    return proxy;
×
4311
  }
4312

4313
  /* --------------- Manual Cache --------------- */
4314

4315
  static class BoundedLocalManualCache<K, V> implements LocalManualCache<K, V>, Serializable {
4316
    private static final long serialVersionUID = 1;
4317

4318
    final BoundedLocalCache<K, V> cache;
4319

4320
    @Nullable Policy<K, V> policy;
4321

4322
    BoundedLocalManualCache(Caffeine<K, V> builder) {
4323
      this(builder, null);
×
4324
    }
×
4325

4326
    BoundedLocalManualCache(Caffeine<K, V> builder, @Nullable CacheLoader<? super K, V> loader) {
×
4327
      cache = LocalCacheFactory.newBoundedLocalCache(builder, loader, /* isAsync= */ false);
×
4328
    }
×
4329

4330
    @Override
4331
    public final BoundedLocalCache<K, V> cache() {
4332
      return cache;
×
4333
    }
4334

4335
    @Override
4336
    public final Policy<K, V> policy() {
4337
      if (policy == null) {
×
4338
        Function<@Nullable V, @Nullable V> identity = v -> v;
×
4339
        policy = new BoundedPolicy<>(cache, identity, cache.isWeighted);
×
4340
      }
4341
      return policy;
×
4342
    }
4343

4344
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
4345
      throw new InvalidObjectException("Proxy required");
×
4346
    }
4347

4348
    private Object writeReplace() {
4349
      return makeSerializationProxy(cache);
×
4350
    }
4351
  }
4352

4353
  @SuppressWarnings({"NullableOptional",
4354
    "OptionalAssignedToNull", "OptionalUsedAsFieldOrParameterType"})
4355
  static final class BoundedPolicy<K, V> implements Policy<K, V> {
4356
    final Function<@Nullable V, @Nullable V> transformer;
4357
    final BoundedLocalCache<K, V> cache;
4358
    final boolean isWeighted;
4359

4360
    @Nullable Optional<Eviction<K, V>> eviction;
4361
    @Nullable Optional<FixedRefresh<K, V>> refreshes;
4362
    @Nullable Optional<FixedExpiration<K, V>> afterWrite;
4363
    @Nullable Optional<FixedExpiration<K, V>> afterAccess;
4364
    @Nullable Optional<VarExpiration<K, V>> variable;
4365

4366
    BoundedPolicy(BoundedLocalCache<K, V> cache,
4367
        Function<@Nullable V, @Nullable V> transformer, boolean isWeighted) {
×
4368
      this.transformer = transformer;
×
4369
      this.isWeighted = isWeighted;
×
4370
      this.cache = cache;
×
4371
    }
×
4372

4373
    @Override public boolean isRecordingStats() {
4374
      return cache.isRecordingStats();
×
4375
    }
4376
    @Override public @Nullable V getIfPresentQuietly(K key) {
4377
      return transformer.apply(cache.getIfPresentQuietly(key));
×
4378
    }
4379
    @SuppressWarnings("GuardedByChecker")
4380
    @Override public @Nullable CacheEntry<K, V> getEntryIfPresentQuietly(K key) {
4381
      Node<K, V> node = cache.data.get(cache.nodeFactory.newLookupKey(key));
×
4382
      return (node == null) ? null : cache.nodeToCacheEntry(node, transformer, node.getWeight());
×
4383
    }
4384
    @SuppressWarnings("Java9CollectionFactory")
4385
    @Override public Map<K, CompletableFuture<V>> refreshes() {
4386
      var refreshes = cache.refreshes;
×
4387
      if ((refreshes == null) || refreshes.isEmpty()) {
×
4388
        @SuppressWarnings({"ImmutableMapOf", "RedundantUnmodifiable"})
4389
        Map<K, CompletableFuture<V>> emptyMap = Collections.unmodifiableMap(Collections.emptyMap());
×
4390
        return emptyMap;
×
4391
      } else if (cache.collectKeys()) {
×
4392
        var inFlight = new IdentityHashMap<K, CompletableFuture<V>>(refreshes.size());
×
4393
        for (var entry : refreshes.entrySet()) {
×
4394
          @SuppressWarnings("unchecked")
4395
          @Nullable K key = ((InternalReference<K>) entry.getKey()).get();
×
4396
          @SuppressWarnings("unchecked")
4397
          var future = (CompletableFuture<V>) entry.getValue();
×
4398
          if (key != null) {
×
4399
            inFlight.put(key, future);
×
4400
          }
4401
        }
×
4402
        return Collections.unmodifiableMap(inFlight);
×
4403
      }
4404
      @SuppressWarnings("unchecked")
4405
      var castedRefreshes = (Map<K, CompletableFuture<V>>) (Object) refreshes;
×
4406
      return Collections.unmodifiableMap(new HashMap<>(castedRefreshes));
×
4407
    }
4408
    @Override public Optional<Eviction<K, V>> eviction() {
4409
      return cache.evicts()
×
4410
          ? (eviction == null) ? (eviction = Optional.of(new BoundedEviction())) : eviction
×
4411
          : Optional.empty();
×
4412
    }
4413
    @Override public Optional<FixedExpiration<K, V>> expireAfterAccess() {
4414
      if (!cache.expiresAfterAccess()) {
×
4415
        return Optional.empty();
×
4416
      }
4417
      return (afterAccess == null)
×
4418
          ? (afterAccess = Optional.of(new BoundedExpireAfterAccess()))
×
4419
          : afterAccess;
×
4420
    }
4421
    @Override public Optional<FixedExpiration<K, V>> expireAfterWrite() {
4422
      if (!cache.expiresAfterWrite()) {
×
4423
        return Optional.empty();
×
4424
      }
4425
      return (afterWrite == null)
×
4426
          ? (afterWrite = Optional.of(new BoundedExpireAfterWrite()))
×
4427
          : afterWrite;
×
4428
    }
4429
    @Override public Optional<VarExpiration<K, V>> expireVariably() {
4430
      if (!cache.expiresVariable()) {
×
4431
        return Optional.empty();
×
4432
      }
4433
      return (variable == null)
×
4434
          ? (variable = Optional.of(new BoundedVarExpiration()))
×
4435
          : variable;
×
4436
    }
4437
    @Override public Optional<FixedRefresh<K, V>> refreshAfterWrite() {
4438
      if (!cache.refreshAfterWrite()) {
×
4439
        return Optional.empty();
×
4440
      }
4441
      return (refreshes == null)
×
4442
          ? (refreshes = Optional.of(new BoundedRefreshAfterWrite()))
×
4443
          : refreshes;
×
4444
    }
4445

4446
    final class BoundedEviction implements Eviction<K, V> {
×
4447
      @Override public boolean isWeighted() {
4448
        return isWeighted;
×
4449
      }
4450
      @Override public OptionalInt weightOf(K key) {
4451
        requireNonNull(key);
×
4452
        if (!isWeighted) {
×
4453
          return OptionalInt.empty();
×
4454
        }
4455
        Node<K, V> node = cache.data.get(cache.nodeFactory.newLookupKey(key));
×
4456
        if (node == null) {
×
4457
          return OptionalInt.empty();
×
4458
        }
4459
        V value = node.getValue();
×
4460
        if ((value == null) || cache.hasExpired(node, cache.expirationTicker().read(), value)) {
×
4461
          return OptionalInt.empty();
×
4462
        }
4463
        synchronized (node) {
×
4464
          return node.isAlive() ? OptionalInt.of(node.getWeight()) : OptionalInt.empty();
×
4465
        }
4466
      }
4467
      @Override public OptionalLong weightedSize() {
4468
        return isWeighted
×
4469
            ? OptionalLong.of(Math.max(0, cache.weightedSizeAcquire()))
×
4470
            : OptionalLong.empty();
×
4471
      }
4472
      @Override public long getMaximum() {
4473
        return cache.maximumAcquire();
×
4474
      }
4475
      @Override public void setMaximum(long maximum) {
4476
        cache.evictionLock.lock();
×
4477
        try {
4478
          cache.setMaximumSize(maximum);
×
4479
          cache.maintenance(/* ignored */ null);
×
4480
        } finally {
4481
          cache.evictionLock.unlock();
×
4482
          cache.rescheduleCleanUpIfIncomplete();
×
4483
        }
4484
      }
×
4485
      @Override public Map<K, V> coldest(int limit) {
4486
        int expectedSize = Math.min(limit, cache.size());
×
4487
        var limiter = new SizeLimiter<K, V>(expectedSize, limit);
×
4488
        return cache.evictionOrder(/* hottest= */ false, transformer, limiter);
×
4489
      }
4490
      @Override public Map<K, V> coldestWeighted(long weightLimit) {
4491
        var limiter = isWeighted()
×
4492
            ? new WeightLimiter<K, V>(weightLimit)
×
4493
            : new SizeLimiter<K, V>((int) Math.min(weightLimit, cache.size()), weightLimit);
×
4494
        return cache.evictionOrder(/* hottest= */ false, transformer, limiter);
×
4495
      }
4496
      @Override
4497
      public <T> T coldest(Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4498
        requireNonNull(mappingFunction);
×
4499
        return cache.evictionOrder(/* hottest= */ false, transformer, mappingFunction);
×
4500
      }
4501
      @Override public Map<K, V> hottest(int limit) {
4502
        int expectedSize = Math.min(limit, cache.size());
×
4503
        var limiter = new SizeLimiter<K, V>(expectedSize, limit);
×
4504
        return cache.evictionOrder(/* hottest= */ true, transformer, limiter);
×
4505
      }
4506
      @Override public Map<K, V> hottestWeighted(long weightLimit) {
4507
        var limiter = isWeighted()
×
4508
            ? new WeightLimiter<K, V>(weightLimit)
×
4509
            : new SizeLimiter<K, V>((int) Math.min(weightLimit, cache.size()), weightLimit);
×
4510
        return cache.evictionOrder(/* hottest= */ true, transformer, limiter);
×
4511
      }
4512
      @Override
4513
      public <T> T hottest(Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4514
        requireNonNull(mappingFunction);
×
4515
        return cache.evictionOrder(/* hottest= */ true, transformer, mappingFunction);
×
4516
      }
4517
    }
4518

4519
    @SuppressWarnings("PreferJavaTimeOverload")
4520
    final class BoundedExpireAfterAccess implements FixedExpiration<K, V> {
×
4521
      @Override public OptionalLong ageOf(K key, TimeUnit unit) {
4522
        requireNonNull(key);
×
4523
        requireNonNull(unit);
×
4524
        Object lookupKey = cache.nodeFactory.newLookupKey(key);
×
4525
        Node<K, V> node = cache.data.get(lookupKey);
×
4526
        if (node == null) {
×
4527
          return OptionalLong.empty();
×
4528
        }
4529
        V value = node.getValue();
×
4530
        if (value == null) {
×
4531
          return OptionalLong.empty();
×
4532
        }
4533
        long now = cache.expirationTicker().read();
×
4534
        return cache.hasExpired(node, now, value)
×
4535
            ? OptionalLong.empty()
×
4536
            : OptionalLong.of(unit.convert(now - node.getAccessTime(), TimeUnit.NANOSECONDS));
×
4537
      }
4538
      @Override public long getExpiresAfter(TimeUnit unit) {
4539
        return unit.convert(cache.expiresAfterAccessNanos(), TimeUnit.NANOSECONDS);
×
4540
      }
4541
      @Override public void setExpiresAfter(long duration, TimeUnit unit) {
4542
        requireArgument(duration >= 0, "duration cannot be negative: %s %s", duration, unit);
×
4543
        cache.setExpiresAfterAccessNanos(unit.toNanos(duration));
×
4544
        cache.scheduleAfterWrite();
×
4545
      }
×
4546
      @Override public Map<K, V> oldest(int limit) {
4547
        return oldest(new SizeLimiter<>(Math.min(limit, cache.size()), limit));
×
4548
      }
4549
      @Override public <T> T oldest(Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4550
        return cache.expireAfterAccessOrder(/* oldest= */ true, transformer, mappingFunction);
×
4551
      }
4552
      @Override public Map<K, V> youngest(int limit) {
4553
        return youngest(new SizeLimiter<>(Math.min(limit, cache.size()), limit));
×
4554
      }
4555
      @Override public <T> T youngest(Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4556
        return cache.expireAfterAccessOrder(/* oldest= */ false, transformer, mappingFunction);
×
4557
      }
4558
    }
4559

4560
    @SuppressWarnings("PreferJavaTimeOverload")
4561
    final class BoundedExpireAfterWrite implements FixedExpiration<K, V> {
×
4562
      @Override public OptionalLong ageOf(K key, TimeUnit unit) {
4563
        requireNonNull(key);
×
4564
        requireNonNull(unit);
×
4565
        Object lookupKey = cache.nodeFactory.newLookupKey(key);
×
4566
        Node<K, V> node = cache.data.get(lookupKey);
×
4567
        if (node == null) {
×
4568
          return OptionalLong.empty();
×
4569
        }
4570
        V value = node.getValue();
×
4571
        if (value == null) {
×
4572
          return OptionalLong.empty();
×
4573
        }
4574
        long now = cache.expirationTicker().read();
×
4575
        return cache.hasExpired(node, now, value)
×
4576
            ? OptionalLong.empty()
×
4577
            : OptionalLong.of(unit.convert(
×
4578
                (now & ~1L) - (node.getWriteTime() & ~1L), TimeUnit.NANOSECONDS));
×
4579
      }
4580
      @Override public long getExpiresAfter(TimeUnit unit) {
4581
        return unit.convert(cache.expiresAfterWriteNanos(), TimeUnit.NANOSECONDS);
×
4582
      }
4583
      @Override public void setExpiresAfter(long duration, TimeUnit unit) {
4584
        requireArgument(duration >= 0, "duration cannot be negative: %s %s", duration, unit);
×
4585
        cache.setExpiresAfterWriteNanos(unit.toNanos(duration));
×
4586
        cache.scheduleAfterWrite();
×
4587
      }
×
4588
      @Override public Map<K, V> oldest(int limit) {
4589
        return oldest(new SizeLimiter<>(Math.min(limit, cache.size()), limit));
×
4590
      }
4591
      @SuppressWarnings("GuardedByChecker")
4592
      @Override public <T> T oldest(Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4593
        return cache.snapshot(cache.writeOrderDeque(), transformer, mappingFunction);
×
4594
      }
4595
      @Override public Map<K, V> youngest(int limit) {
4596
        return youngest(new SizeLimiter<>(Math.min(limit, cache.size()), limit));
×
4597
      }
4598
      @SuppressWarnings("GuardedByChecker")
4599
      @Override public <T> T youngest(Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4600
        return cache.snapshot(cache.writeOrderDeque()::descendingIterator,
×
4601
            transformer, mappingFunction);
4602
      }
4603
    }
4604

4605
    @SuppressWarnings("PreferJavaTimeOverload")
4606
    final class BoundedVarExpiration implements VarExpiration<K, V> {
×
4607
      @Override public OptionalLong getExpiresAfter(K key, TimeUnit unit) {
4608
        requireNonNull(key);
×
4609
        requireNonNull(unit);
×
4610
        Object lookupKey = cache.nodeFactory.newLookupKey(key);
×
4611
        Node<K, V> node = cache.data.get(lookupKey);
×
4612
        if (node == null) {
×
4613
          return OptionalLong.empty();
×
4614
        }
4615
        V value = node.getValue();
×
4616
        if (value == null) {
×
4617
          return OptionalLong.empty();
×
4618
        }
4619
        long now = cache.expirationTicker().read();
×
4620
        return cache.hasExpired(node, now, value)
×
4621
            ? OptionalLong.empty()
×
4622
            : OptionalLong.of(unit.convert(node.getVariableTime() - now, TimeUnit.NANOSECONDS));
×
4623
      }
4624
      @Override public void setExpiresAfter(K key, long duration, TimeUnit unit) {
4625
        requireNonNull(key);
×
4626
        requireNonNull(unit);
×
4627
        requireArgument(duration >= 0, "duration cannot be negative: %s %s", duration, unit);
×
4628
        Object lookupKey = cache.nodeFactory.newLookupKey(key);
×
4629
        Node<K, V> node = cache.data.get(lookupKey);
×
4630
        if (node != null) {
×
4631
          long durationNanos = TimeUnit.NANOSECONDS.convert(duration, unit);
×
4632
          synchronized (node) {
×
4633
            long now = cache.expirationTicker().read();
×
4634
            V value = node.getValue();
×
4635
            if ((value == null) || cache.isComputingAsync(value)
×
4636
                || cache.hasExpired(node, now, value)) {
×
4637
              return;
×
4638
            }
4639
            node.setVariableTime(now + Math.min(durationNanos, MAXIMUM_EXPIRY));
×
4640
          }
×
4641
          cache.afterWrite(cache.new UpdateTask(node, 0, /* quietly= */ true));
×
4642
        }
4643
      }
×
4644
      @Override public @Nullable V put(K key, V value, long duration, TimeUnit unit) {
4645
        requireNonNull(unit);
×
4646
        requireNonNull(value);
×
4647
        requireArgument(duration >= 0, "duration cannot be negative: %s %s", duration, unit);
×
4648
        return cache.isAsync
×
4649
            ? putAsync(key, value, duration, unit)
×
4650
            : putSync(key, value, duration, unit, /* onlyIfAbsent= */ false);
×
4651
      }
4652
      @Override public @Nullable V putIfAbsent(K key, V value, long duration, TimeUnit unit) {
4653
        requireNonNull(unit);
×
4654
        requireNonNull(value);
×
4655
        requireArgument(duration >= 0, "duration cannot be negative: %s %s", duration, unit);
×
4656
        return cache.isAsync
×
4657
            ? putIfAbsentAsync(key, value, duration, unit)
×
4658
            : putSync(key, value, duration, unit, /* onlyIfAbsent= */ true);
×
4659
      }
4660
      @Nullable V putSync(K key, V value, long duration, TimeUnit unit, boolean onlyIfAbsent) {
4661
        var expiry = new FixedExpireAfterWrite<K, V>(duration, unit);
×
4662
        return cache.put(key, value, expiry, onlyIfAbsent);
×
4663
      }
4664
      @SuppressWarnings("unchecked")
4665
      @Nullable V putIfAbsentAsync(K key, V value, long duration, TimeUnit unit) {
4666
        // Mirrors LocalAsyncCache.AsMapView#putIfAbsent(key, value), but always probes quietly:
4667
        // the fixed duration must not be recomputed by the user's Expiry on a present entry
4668
        var expiry = (Expiry<K, V>) new AsyncExpiry<>(new FixedExpireAfterWrite<>(duration, unit));
×
4669
        var asyncValue = (V) CompletableFuture.completedFuture(value);
×
4670

4671
        for (;;) {
4672
          var priorFuture = (CompletableFuture<V>) cache.getIfPresentQuietly(key);
×
4673
          if (priorFuture != null) {
×
4674
            if (!priorFuture.isDone()) {
×
4675
              Async.getWhenSuccessful(priorFuture);
×
4676
              continue;
×
4677
            }
4678

4679
            V prior = Async.getWhenSuccessful(priorFuture);
×
4680
            if (prior != null) {
×
4681
              return prior;
×
4682
            }
4683
          }
4684

4685
          boolean[] added = { false };
×
4686
          var hints = new LocalCache.RemapHints();
×
4687
          var computed = (CompletableFuture<V>) cache.compute(key, (K k, @Nullable V oldValue) -> {
×
4688
            var oldValueFuture = (CompletableFuture<V>) oldValue;
×
4689
            added[0] = (oldValueFuture == null)
×
4690
                || (oldValueFuture.isDone() && (Async.getIfReady(oldValueFuture) == null));
×
4691
            if (added[0]) {
×
4692
              return asyncValue;
×
4693
            }
4694
            hints.preserveTimestamps = true;
×
4695
            hints.preserveRefresh = true;
×
4696
            return oldValue;
×
4697
          }, expiry, /* recordLoad= */ false, /* recordLoadFailure= */ false, hints);
4698

4699
          if (added[0]) {
×
4700
            return null;
×
4701
          } else {
4702
            V prior = Async.getWhenSuccessful(computed);
×
4703
            if (prior != null) {
×
4704
              return prior;
×
4705
            }
4706
          }
4707
        }
×
4708
      }
4709
      @SuppressWarnings("unchecked")
4710
      @Nullable V putAsync(K key, V value, long duration, TimeUnit unit) {
4711
        var expiry = (Expiry<K, V>) new AsyncExpiry<>(new FixedExpireAfterWrite<>(duration, unit));
×
4712
        var asyncValue = (V) CompletableFuture.completedFuture(value);
×
4713

4714
        var oldValueFuture = (CompletableFuture<V>) cache.put(
×
4715
            key, asyncValue, expiry, /* onlyIfAbsent= */ false);
4716
        return Async.getWhenSuccessful(oldValueFuture);
×
4717
      }
4718
      @Override public @Nullable V compute(K key,
4719
          BiFunction<? super K, ? super V, ? extends V> remappingFunction,
4720
          Duration duration) {
4721
        requireNonNull(key);
×
4722
        requireNonNull(duration);
×
4723
        requireNonNull(remappingFunction);
×
4724
        requireArgument(!duration.isNegative(), "duration cannot be negative: %s", duration);
×
4725
        var expiry = new FixedExpireAfterWrite<K, V>(
×
4726
            toNanosSaturated(duration), TimeUnit.NANOSECONDS);
×
4727

4728
        return cache.isAsync
×
4729
            ? computeAsync(key, remappingFunction, expiry)
×
4730
            : cache.compute(key, remappingFunction, expiry,
×
4731
                /* recordLoad= */ true, /* recordLoadFailure= */ true);
4732
      }
4733
      @Nullable V computeAsync(K key,
4734
          BiFunction<? super K, ? super V, ? extends V> remappingFunction,
4735
          Expiry<? super K, ? super V> expiry) {
4736
        // Keep in sync with LocalAsyncCache.AsMapView#compute(key, remappingFunction)
4737
        @SuppressWarnings("unchecked")
4738
        var delegate = (LocalCache<K, CompletableFuture<V>>) cache;
×
4739

4740
        @SuppressWarnings({"rawtypes", "unchecked", "Varifier"})
4741
        @Nullable V[] newValue = (@Nullable V[]) new Object[1];
×
4742
        for (;;) {
4743
          Async.getWhenSuccessful(delegate.getIfPresentQuietly(key));
×
4744

4745
          var hints = new LocalCache.RemapHints();
×
4746
          CompletableFuture<V> valueFuture = delegate.compute(
×
4747
              key, (K k, @Nullable CompletableFuture<V> oldValueFuture) -> {
4748
                if ((oldValueFuture != null) && !oldValueFuture.isDone()) {
×
4749
                  hints.preserveTimestamps = true;
×
4750
                  hints.preserveRefresh = true;
×
4751
                  return oldValueFuture;
×
4752
                }
4753

4754
                V oldValue = Async.getIfReady(oldValueFuture);
×
4755
                BiFunction<? super K, ? super V, ? extends @Nullable V> function =
×
4756
                    delegate.statsAware(remappingFunction,
×
4757
                        /* recordLoad= */ true, /* recordLoadFailure= */ true);
4758
                newValue[0] = function.apply(key, oldValue);
×
4759
                return (newValue[0] == null) ? null
×
4760
                    : CompletableFuture.completedFuture(newValue[0]);
×
4761
              }, new AsyncExpiry<>(expiry), /* recordLoad= */ false,
4762
              /* recordLoadFailure= */ false, hints);
4763

4764
          if (newValue[0] != null) {
×
4765
            return newValue[0];
×
4766
          } else if (valueFuture == null) {
×
4767
            return null;
×
4768
          }
4769
        }
×
4770
      }
4771
      @Override public Map<K, V> oldest(int limit) {
4772
        return oldest(new SizeLimiter<>(Math.min(limit, cache.size()), limit));
×
4773
      }
4774
      @Override public <T> T oldest(Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4775
        return cache.snapshot(cache.timerWheel(), transformer, mappingFunction);
×
4776
      }
4777
      @Override public Map<K, V> youngest(int limit) {
4778
        return youngest(new SizeLimiter<>(Math.min(limit, cache.size()), limit));
×
4779
      }
4780
      @Override public <T> T youngest(Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4781
        return cache.snapshot(cache.timerWheel()::descendingIterator, transformer, mappingFunction);
×
4782
      }
4783
    }
4784

4785
    static final class FixedExpireAfterWrite<K, V> implements Expiry<K, V> {
4786
      final long duration;
4787
      final TimeUnit unit;
4788

4789
      FixedExpireAfterWrite(long duration, TimeUnit unit) {
×
4790
        this.duration = duration;
×
4791
        this.unit = unit;
×
4792
      }
×
4793
      @Override public long expireAfterCreate(K key, V value, long currentTime) {
4794
        return unit.toNanos(duration);
×
4795
      }
4796
      @Override public long expireAfterUpdate(
4797
          K key, V value, long currentTime, long currentDuration) {
4798
        return unit.toNanos(duration);
×
4799
      }
4800
      @CanIgnoreReturnValue
4801
      @Override public long expireAfterRead(
4802
          K key, V value, long currentTime, long currentDuration) {
4803
        return currentDuration;
×
4804
      }
4805
    }
4806

4807
    @SuppressWarnings("PreferJavaTimeOverload")
4808
    final class BoundedRefreshAfterWrite implements FixedRefresh<K, V> {
×
4809
      @Override public OptionalLong ageOf(K key, TimeUnit unit) {
4810
        requireNonNull(key);
×
4811
        requireNonNull(unit);
×
4812
        Object lookupKey = cache.nodeFactory.newLookupKey(key);
×
4813
        Node<K, V> node = cache.data.get(lookupKey);
×
4814
        if (node == null) {
×
4815
          return OptionalLong.empty();
×
4816
        }
4817
        V value = node.getValue();
×
4818
        if (value == null) {
×
4819
          return OptionalLong.empty();
×
4820
        }
4821
        long now = cache.expirationTicker().read();
×
4822
        return cache.hasExpired(node, now, value)
×
4823
            ? OptionalLong.empty()
×
4824
            : OptionalLong.of(unit.convert(
×
4825
                (now & ~1L) - (node.getWriteTime() & ~1L), TimeUnit.NANOSECONDS));
×
4826
      }
4827
      @Override public long getRefreshesAfter(TimeUnit unit) {
4828
        return unit.convert(cache.refreshAfterWriteNanos(), TimeUnit.NANOSECONDS);
×
4829
      }
4830
      @Override public void setRefreshesAfter(long duration, TimeUnit unit) {
4831
        requireNonNull(unit);
×
4832
        requireArgument(duration > 0, "duration must be positive: %s %s", duration, unit);
×
4833
        cache.setRefreshAfterWriteNanos(unit.toNanos(duration));
×
4834
        cache.scheduleAfterWrite();
×
4835
      }
×
4836
    }
4837
  }
4838

4839
  /* --------------- Loading Cache --------------- */
4840

4841
  static final class BoundedLocalLoadingCache<K, V>
4842
      extends BoundedLocalManualCache<K, V> implements LocalLoadingCache<K, V> {
4843
    private static final long serialVersionUID = 1;
4844

4845
    final Function<K, @Nullable V> mappingFunction;
4846
    final @Nullable Function<Set<? extends K>, Map<K, V>> bulkMappingFunction;
4847

4848
    BoundedLocalLoadingCache(Caffeine<K, V> builder, CacheLoader<? super K, V> loader) {
4849
      super(builder, loader);
×
4850
      requireNonNull(loader);
×
4851
      mappingFunction = newMappingFunction(loader);
×
4852
      bulkMappingFunction = newBulkMappingFunction(loader);
×
4853
    }
×
4854

4855
    @Override
4856
    @SuppressWarnings({"DataFlowIssue", "NullAway"})
4857
    public AsyncCacheLoader<? super K, V> cacheLoader() {
4858
      return cache.cacheLoader;
×
4859
    }
4860

4861
    @Override
4862
    public Function<K, @Nullable V> mappingFunction() {
4863
      return mappingFunction;
×
4864
    }
4865

4866
    @Override
4867
    public @Nullable Function<Set<? extends K>, Map<K, V>> bulkMappingFunction() {
4868
      return bulkMappingFunction;
×
4869
    }
4870

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

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

4880
  /* --------------- Async Cache --------------- */
4881

4882
  static final class BoundedLocalAsyncCache<K, V> implements LocalAsyncCache<K, V>, Serializable {
4883
    private static final long serialVersionUID = 1;
4884

4885
    final BoundedLocalCache<K, CompletableFuture<V>> cache;
4886
    final boolean isWeighted;
4887

4888
    @Nullable ConcurrentMap<K, CompletableFuture<V>> mapView;
4889
    @Nullable CacheView<K, V> cacheView;
4890
    @Nullable Policy<K, V> policy;
4891

4892
    @SuppressWarnings("unchecked")
4893
    BoundedLocalAsyncCache(Caffeine<K, V> builder) {
×
4894
      cache = (BoundedLocalCache<K, CompletableFuture<V>>) LocalCacheFactory
×
4895
          .newBoundedLocalCache(builder, /* cacheLoader= */ null, /* isAsync= */ true);
×
4896
      isWeighted = builder.isWeighted();
×
4897
    }
×
4898

4899
    @Override
4900
    public BoundedLocalCache<K, CompletableFuture<V>> cache() {
4901
      return cache;
×
4902
    }
4903

4904
    @Override
4905
    public ConcurrentMap<K, CompletableFuture<V>> asMap() {
4906
      return (mapView == null) ? (mapView = new AsyncAsMapView<>(this)) : mapView;
×
4907
    }
4908

4909
    @Override
4910
    public Cache<K, V> synchronous() {
4911
      return (cacheView == null) ? (cacheView = new CacheView<>(this)) : cacheView;
×
4912
    }
4913

4914
    @Override
4915
    public Policy<K, V> policy() {
4916
      if (policy == null) {
×
4917
        @SuppressWarnings("unchecked")
4918
        var castCache = (BoundedLocalCache<K, V>) cache;
×
4919
        Function<CompletableFuture<V>, @Nullable V> transformer = Async::getIfReady;
×
4920
        @SuppressWarnings("unchecked")
4921
        var castTransformer = (Function<@Nullable V, @Nullable V>) transformer;
×
4922
        policy = new BoundedPolicy<>(castCache, castTransformer, isWeighted);
×
4923
      }
4924
      return policy;
×
4925
    }
4926

4927
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
4928
      throw new InvalidObjectException("Proxy required");
×
4929
    }
4930

4931
    private Object writeReplace() {
4932
      return makeSerializationProxy(cache);
×
4933
    }
4934
  }
4935

4936
  /* --------------- Async Loading Cache --------------- */
4937

4938
  static final class BoundedLocalAsyncLoadingCache<K, V>
4939
      extends LocalAsyncLoadingCache<K, V> implements Serializable {
4940
    private static final long serialVersionUID = 1;
4941

4942
    final BoundedLocalCache<K, CompletableFuture<V>> cache;
4943
    final boolean isWeighted;
4944

4945
    @Nullable ConcurrentMap<K, CompletableFuture<V>> mapView;
4946
    @Nullable Policy<K, V> policy;
4947

4948
    @SuppressWarnings("unchecked")
4949
    BoundedLocalAsyncLoadingCache(Caffeine<K, V> builder, AsyncCacheLoader<? super K, V> loader) {
4950
      super(loader);
×
4951
      isWeighted = builder.isWeighted();
×
4952
      cache = (BoundedLocalCache<K, CompletableFuture<V>>) LocalCacheFactory
×
4953
          .newBoundedLocalCache(builder, loader, /* isAsync= */ true);
×
4954
    }
×
4955

4956
    @Override
4957
    public BoundedLocalCache<K, CompletableFuture<V>> cache() {
4958
      return cache;
×
4959
    }
4960

4961
    @Override
4962
    public ConcurrentMap<K, CompletableFuture<V>> asMap() {
4963
      return (mapView == null) ? (mapView = new AsyncAsMapView<>(this)) : mapView;
×
4964
    }
4965

4966
    @Override
4967
    public Policy<K, V> policy() {
4968
      if (policy == null) {
×
4969
        @SuppressWarnings("unchecked")
4970
        var castCache = (BoundedLocalCache<K, V>) cache;
×
4971
        Function<CompletableFuture<V>, @Nullable V> transformer = Async::getIfReady;
×
4972
        @SuppressWarnings("unchecked")
4973
        var castTransformer = (Function<@Nullable V, @Nullable V>) transformer;
×
4974
        policy = new BoundedPolicy<>(castCache, castTransformer, isWeighted);
×
4975
      }
4976
      return policy;
×
4977
    }
4978

4979
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
4980
      throw new InvalidObjectException("Proxy required");
×
4981
    }
4982

4983
    private Object writeReplace() {
4984
      return makeSerializationProxy(cache);
×
4985
    }
4986
  }
4987
}
4988

4989
/** The namespace for field padding through inheritance. */
4990
@SuppressWarnings({"IdentifierName", "MultiVariableDeclaration"})
4991
final class BLCHeader {
4992

4993
  private BLCHeader() {}
4994

4995
  @SuppressWarnings("unused")
4996
  static class PadDrainStatus {
×
4997
    byte p000, p001, p002, p003, p004, p005, p006, p007;
4998
    byte p008, p009, p010, p011, p012, p013, p014, p015;
4999
    byte p016, p017, p018, p019, p020, p021, p022, p023;
5000
    byte p024, p025, p026, p027, p028, p029, p030, p031;
5001
    byte p032, p033, p034, p035, p036, p037, p038, p039;
5002
    byte p040, p041, p042, p043, p044, p045, p046, p047;
5003
    byte p048, p049, p050, p051, p052, p053, p054, p055;
5004
    byte p056, p057, p058, p059, p060, p061, p062, p063;
5005
    byte p064, p065, p066, p067, p068, p069, p070, p071;
5006
    byte p072, p073, p074, p075, p076, p077, p078, p079;
5007
    byte p080, p081, p082, p083, p084, p085, p086, p087;
5008
    byte p088, p089, p090, p091, p092, p093, p094, p095;
5009
    byte p096, p097, p098, p099, p100, p101, p102, p103;
5010
    byte p104, p105, p106, p107, p108, p109, p110, p111;
5011
    byte p112, p113, p114, p115, p116, p117, p118, p119;
5012
  }
5013

5014
  /** Enforces a memory layout to avoid false sharing by padding the drain status. */
5015
  abstract static class DrainStatusRef extends PadDrainStatus {
×
5016
    static final VarHandle DRAIN_STATUS = fieldVarHandle(MethodHandles.lookup(),
×
5017
        "drainStatus", VarHandle.class, DrainStatusRef.class, int.class);
5018

5019
    /** A drain is not taking place. */
5020
    static final int IDLE = 0;
5021
    /** A drain is required due to a pending write modification. */
5022
    static final int REQUIRED = 1;
5023
    /** A drain is in progress and will transition to idle. */
5024
    static final int PROCESSING_TO_IDLE = 2;
5025
    /** A drain is in progress and will transition to required. */
5026
    static final int PROCESSING_TO_REQUIRED = 3;
5027

5028
    /** The draining status of the buffers. */
5029
    volatile int drainStatus = IDLE;
×
5030

5031
    /**
5032
     * Returns whether maintenance work is needed.
5033
     *
5034
     * @param delayable if draining the read buffer can be delayed
5035
     */
5036
    @SuppressWarnings("StatementSwitchToExpressionSwitch")
5037
    boolean shouldDrainBuffers(boolean delayable) {
5038
      switch (drainStatusOpaque()) {
×
5039
        case IDLE:
5040
          return !delayable;
×
5041
        case REQUIRED:
5042
          return true;
×
5043
        case PROCESSING_TO_IDLE:
5044
        case PROCESSING_TO_REQUIRED:
5045
          return false;
×
5046
        default:
5047
          throw new IllegalStateException("Invalid drain status: " + drainStatus);
×
5048
      }
5049
    }
5050

5051
    int drainStatusOpaque() {
5052
      return (int) DRAIN_STATUS.getOpaque(this);
×
5053
    }
5054

5055
    int drainStatusAcquire() {
5056
      return (int) DRAIN_STATUS.getAcquire(this);
×
5057
    }
5058

5059
    void setDrainStatusOpaque(int drainStatus) {
5060
      DRAIN_STATUS.setOpaque(this, drainStatus);
×
5061
    }
×
5062

5063
    void setDrainStatusRelease(int drainStatus) {
5064
      DRAIN_STATUS.setRelease(this, drainStatus);
×
5065
    }
×
5066

5067
    boolean casDrainStatus(int expect, int update) {
5068
      return DRAIN_STATUS.compareAndSet(this, expect, update);
×
5069
    }
5070
  }
5071
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc