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

ben-manes / caffeine / #5835

20 Oct 2026 07:25AM UTC coverage: 99.968% (-0.01%) from 99.979%
#5835

push

github

ben-manes
join an in-flight refresh of an expired entry

A refresh of an expired or collected entry replaced any registration,
so while another refresh's loader was still running, each caller
replaced the last and loaded again: 8 refreshes made 8 loads and kept
only the last value. Such a refresh now joins the placeholder and
takes the linearization point of the refresh it joined, so a reload
whose entry expired during it is discarded at commit as it is when the
entry expires after the join. This also rejects a loader refreshing its
own key without a separate owner check.

4870 of 4882 branches covered (99.75%)

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

2 existing lines in 2 files now uncovered.

9346 of 9349 relevant lines covered (99.97%)

1.0 hits per line

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

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

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

39
import java.io.InvalidObjectException;
40
import java.io.ObjectInputStream;
41
import java.io.Serializable;
42
import java.lang.System.Logger;
43
import java.lang.System.Logger.Level;
44
import java.lang.invoke.MethodHandles;
45
import java.lang.invoke.VarHandle;
46
import java.lang.ref.ReferenceQueue;
47
import java.lang.ref.WeakReference;
48
import java.time.Duration;
49
import java.util.AbstractCollection;
50
import java.util.AbstractSet;
51
import java.util.ArrayDeque;
52
import java.util.Collection;
53
import java.util.Collections;
54
import java.util.Comparator;
55
import java.util.Deque;
56
import java.util.HashMap;
57
import java.util.IdentityHashMap;
58
import java.util.Iterator;
59
import java.util.LinkedHashMap;
60
import java.util.Map;
61
import java.util.NoSuchElementException;
62
import java.util.Objects;
63
import java.util.Optional;
64
import java.util.OptionalInt;
65
import java.util.OptionalLong;
66
import java.util.Set;
67
import java.util.Spliterator;
68
import java.util.Spliterators;
69
import java.util.concurrent.CancellationException;
70
import java.util.concurrent.CompletableFuture;
71
import java.util.concurrent.ConcurrentHashMap;
72
import java.util.concurrent.ConcurrentMap;
73
import java.util.concurrent.Executor;
74
import java.util.concurrent.ForkJoinPool;
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
161
   * rate curve. For modestly sized caches this samples the hit rate and adjusts the window size
162
   * in the direction that is improving, decaying the step until the climber converges and
163
   * restarting when a large change indicates that the workload altered. Tiny caches allow the
164
   * sample period to grow as its step decays to avoid getting stuck at a poor configuration.
165
   * Larger caches instead compare the regions' hit densities within a single sample, which is
166
   * immune to the hit rate swings of a phasey workload, and take a proportional step towards the
167
   * balance point. As that signal only observes the hits that the current split earns, a region
168
   * that earns almost nothing cannot measure what a different split would, so those samples are
169
   * never trusted to hold position: the climber probes out of the blind state by hit rate,
170
   * accepts the new position if the density signal validates it, and otherwise undoes the probe
171
   * with exponentially longer pauses between attempts.
172
   *
173
   * The historic usage is retained in a compact popularity sketch, which uses hashing to
174
   * probabilistically estimate an item's frequency. This exposes a flaw where an adversary could
175
   * use hash flooding [4] to artificially raise the frequency of the main space's victim and cause
176
   * all candidates to be rejected. In the worst case, by exploiting hash collisions, an attacker
177
   * could cause the cache to never hit and hold only worthless items, resulting in a
178
   * denial-of-service attack against the underlying resource. This is mitigated by introducing
179
   * jitter, allowing candidates that are at least moderately popular to have a small, random chance
180
   * of being admitted. This causes the victim to be evicted, but in a way that marginally impacts
181
   * the hit rate.
182
   *
183
   * Expiration:
184
   * -----------
185
   * Expiration is implemented in O(1) time complexity. The time-to-idle policy uses an access-order
186
   * queue, the time-to-live policy uses a write-order queue, and variable expiration uses a
187
   * hierarchical timer wheel [5]. The queuing policies allow for peeking at the oldest entry to
188
   * determine if it has expired. If it has not, then the younger entries must not have expired
189
   * either. If a maximum size is set, then expiration will share the queues, minimizing the
190
   * per-entry footprint. The timer wheel based policy uses hashing and cascading in a manner that
191
   * amortizes the penalty of sorting to achieve a similar algorithmic cost.
192
   *
193
   * The expiration updates are applied in a best effort fashion. The reordering of variable or
194
   * access-order expiration may be discarded by the read buffer if it is full or contended.
195
   * Similarly, recording the touch for expiration to extend its lifetime may be ignored for an
196
   * entry if the last update was within a short time window. This is done to avoid overwhelming the
197
   * write buffer and to avoid false sharing on reads due to modifying the access time. The
198
   * write-order scan compensates by moving a stale-positioned head to the back of the queue when
199
   * its timestamp is fresher than the tail, so subsequent passes can resume from the next-oldest
200
   * entry. The access-order scan does not, because those queues are also the eviction order, so a
201
   * lost reorder defers reaping the entries behind its head until that head is read again or
202
   * expires.
203
   *
204
   * [1] BP-Wrapper: A Framework Making Any Replacement Algorithms (Almost) Lock Contention Free
205
   * https://web.njit.edu/~dingxn/papers/BP-Wrapper.pdf
206
   * [2] TinyLFU: A Highly Efficient Cache Admission Policy
207
   * https://dl.acm.org/citation.cfm?id=3149371
208
   * [3] Adaptive Software Cache Management
209
   * https://dl.acm.org/citation.cfm?id=3274816
210
   * [4] Denial of Service via Algorithmic Complexity Attack
211
   * https://www.usenix.org/legacy/events/sec03/tech/full_papers/crosby/crosby.pdf
212
   * [5] Hashed and Hierarchical Timing Wheels
213
   * http://www.cs.columbia.edu/~nahum/w6998/papers/ton97-timing-wheels.pdf
214
   */
215

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

218
  /** The number of CPUs */
219
  static final int NCPU = Runtime.getRuntime().availableProcessors();
1 ✔
220
  /** The initial capacity of the write buffer. */
221
  static final int WRITE_BUFFER_MIN = 4;
222
  /** The maximum capacity of the write buffer. */
223
  static final int WRITE_BUFFER_MAX = 128 * ceilingPowerOfTwo(NCPU);
1 ✔
224
  /** The maximum weighted capacity of the map. */
225
  static final long MAXIMUM_CAPACITY = Long.MAX_VALUE - Integer.MAX_VALUE;
226
  /** The initial percent of the maximum weighted capacity dedicated to the main space. */
227
  static final double PERCENT_MAIN = 0.99d;
228
  /** The percent of the maximum weighted capacity dedicated to the main's protected space. */
229
  static final double PERCENT_MAIN_PROTECTED = 0.80d;
230
  /** The minimum popularity for allowing randomized admission. */
231
  static final int ADMIT_HASHDOS_THRESHOLD = 6;
232
  /** The maximum number of entries that can be transferred between queues. */
233
  static final int QUEUE_TRANSFER_THRESHOLD = 1_000;
234
  /** The number of expirations per policy after which a maintenance cycle defers the rest. */
235
  static final int EXPIRATION_THRESHOLD = 1_000;
236
  /** The maximum number of collected references that can be drained per maintenance cycle. */
237
  static final int REFERENCE_THRESHOLD = 1_000;
238
  /** The maximum time window between touches for expiration updates. */
239
  static final long EXPIRE_TOLERANCE = TimeUnit.SECONDS.toNanos(1);
1 ✔
240
  /** The maximum duration before an entry expires. */
241
  static final long MAXIMUM_EXPIRY = (Long.MAX_VALUE >> 1); // 150 years
242
  /** The duration to wait on the eviction lock before warning of a possible misuse. */
243
  static final long WARN_AFTER_LOCK_WAIT_NANOS = TimeUnit.SECONDS.toNanos(30);
1 ✔
244
  /** The number of retries before computing to validate the entry's integrity; pow2 modulus. */
245
  static final int MAX_PUT_SPIN_WAIT_ATTEMPTS = 1024 - 1;
246
  /** The handle for the in-flight refresh operations. */
247
  static final VarHandle REFRESHES = fieldVarHandle(MethodHandles.lookup(),
1 ✔
248
      "refreshes", VarHandle.class, BoundedLocalCache.class, ConcurrentMap.class);
249

250
  final @Nullable RemovalListener<K, V> evictionListener;
251
  final @Nullable AsyncCacheLoader<K, V> cacheLoader;
252

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

262
  final boolean isWeighted;
263
  final boolean isAsync;
264

265
  volatile Buffer<Node<K, V>> readBuffer;
266

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

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

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

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

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

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

322
  /* --------------- Shared --------------- */
323

324
  @Override
325
  public boolean isAsync() {
326
    return isAsync;
1 ✔
327
  }
328

329
  /** Returns if the node's value is currently being computed asynchronously. */
330
  final boolean isComputingAsync(@Nullable V value) {
331
    return isAsync && !Async.isReady((CompletableFuture<?>) value);
1 ✔
332
  }
333

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

339
  @GuardedBy("evictionLock")
340
  protected AccessOrderDeque<Node<K, V>> accessOrderProbationDeque() {
341
    throw new UnsupportedOperationException();
1 ✔
342
  }
343

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

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

354
  @Override
355
  public final Executor executor() {
356
    return executor;
1 ✔
357
  }
358

359
  @Override
360
  public ConcurrentMap<Object, CompletableFuture<?>> refreshes() {
361
    @Var var pending = refreshes;
1 ✔
362
    if (pending == null) {
1 ✔
363
      pending = new ConcurrentHashMap<>();
1 ✔
364
      if (!REFRESHES.compareAndSet(this, null, pending)) {
1 ✔
365
        pending = requireNonNull(refreshes);
1 ✔
366
      }
367
    }
368
    return pending;
1 ✔
369
  }
370

371
  /** Invalidate the in-flight refresh. */
372
  @SuppressWarnings("RedundantCollectionOperation")
373
  void discardRefresh(Object keyReference) {
374
    var pending = refreshes;
1 ✔
375
    if ((pending != null) && pending.containsKey(keyReference)) {
1 ✔
376
      pending.remove(keyReference);
1 ✔
377
    }
378
  }
1 ✔
379

380
  @Override
381
  public Object referenceKey(K key) {
382
    return nodeFactory.newLookupKey(key);
1 ✔
383
  }
384

385
  @Override
386
  public boolean isPendingEviction(K key) {
387
    Node<K, V> node = data.get(nodeFactory.newLookupKey(key));
1 ✔
388
    if (node == null) {
1 ✔
389
      return false;
1 ✔
390
    }
391
    boolean expired = hasExpired(node, expirationTicker().read());
1 ✔
392
    V value = node.getValue();
1 ✔
393
    return (value == null) || (expired && !isComputingAsync(value));
1 ✔
394
  }
395

396
  /* --------------- Stats Support --------------- */
397

398
  @Override
399
  public boolean isRecordingStats() {
400
    return false;
1 ✔
401
  }
402

403
  @Override
404
  public StatsCounter statsCounter() {
405
    return StatsCounter.disabledStatsCounter();
1 ✔
406
  }
407

408
  @Override
409
  public Ticker statsTicker() {
410
    return Ticker.disabledTicker();
1 ✔
411
  }
412

413
  /* --------------- Removal Listener Support --------------- */
414

415
  @Override
416
  public @Nullable RemovalListener<K, V> removalListener() {
417
    return null;
1 ✔
418
  }
419

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

444
  /* --------------- Eviction Listener Support --------------- */
445

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

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

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

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

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

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

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

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

489
  /** Returns if the cache expires entries by any of its policies. */
490
  final boolean expires() {
491
    return expiresAfterAccess() || expiresAfterWrite() || expiresVariable();
1 ✔
492
  }
493

494
  /** Returns if a read may extend when the entry expires. */
495
  final boolean expiresAfterRead() {
496
    return expiresAfterAccess() || expiresVariable();
1 ✔
497
  }
498

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

504
  /** Returns if the cache expires entries after an access time threshold. */
505
  protected boolean expiresAfterAccess() {
506
    return false;
1 ✔
507
  }
508

509
  /** Returns how long after the last access to an entry the map will retain that entry. */
510
  protected long expiresAfterAccessNanos() {
511
    throw new UnsupportedOperationException();
1 ✔
512
  }
513

514
  protected void setExpiresAfterAccessNanos(long expireAfterAccessNanos) {
515
    throw new UnsupportedOperationException();
1 ✔
516
  }
517

518
  /** Returns if the cache expires entries after a write time threshold. */
519
  protected boolean expiresAfterWrite() {
520
    return false;
1 ✔
521
  }
522

523
  /** Returns how long after the last write to an entry the map will retain that entry. */
524
  protected long expiresAfterWriteNanos() {
525
    throw new UnsupportedOperationException();
1 ✔
526
  }
527

528
  protected void setExpiresAfterWriteNanos(long expireAfterWriteNanos) {
529
    throw new UnsupportedOperationException();
1 ✔
530
  }
531

532
  /** Returns if the cache refreshes entries after a write time threshold. */
533
  protected boolean refreshAfterWrite() {
534
    return false;
1 ✔
535
  }
536

537
  /** Returns how long after the last write an entry becomes a candidate for refresh. */
538
  protected long refreshAfterWriteNanos() {
539
    throw new UnsupportedOperationException();
1 ✔
540
  }
541

542
  protected void setRefreshAfterWriteNanos(long refreshAfterWriteNanos) {
543
    throw new UnsupportedOperationException();
1 ✔
544
  }
545

546
  @Override
547
  @SuppressWarnings({"DataFlowIssue", "NullAway"})
548
  public Expiry<K, V> expiry() {
549
    return null;
1 ✔
550
  }
551

552
  /** Returns the {@link Ticker} used by this cache for expiration. */
553
  public Ticker expirationTicker() {
554
    return Ticker.disabledTicker();
1 ✔
555
  }
556

557
  protected TimerWheel<K, V> timerWheel() {
558
    throw new UnsupportedOperationException();
1 ✔
559
  }
560

561
  /* --------------- Eviction Support --------------- */
562

563
  /** Returns if the cache evicts entries due to a maximum size or weight threshold. */
564
  protected boolean evicts() {
565
    return false;
1 ✔
566
  }
567

568
  /** Returns if entries may be assigned different weights. */
569
  protected boolean isWeighted() {
570
    return isWeighted && (weigher != Weigher.singletonWeigher());
1 ✔
571
  }
572

573
  protected FrequencySketch frequencySketch() {
574
    throw new UnsupportedOperationException();
1 ✔
575
  }
576

577
  @GuardedBy("evictionLock")
578
  protected WindowClimber climber() {
579
    throw new UnsupportedOperationException();
1 ✔
580
  }
581

582
  /** Returns if an access to an entry can skip notifying the eviction policy. */
583
  protected boolean fastpath() {
584
    return false;
1 ✔
585
  }
586

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

592
  /** Returns the maximum weighted size. */
593
  protected long maximumAcquire() {
594
    throw new UnsupportedOperationException();
1 ✔
595
  }
596

597
  /** Returns the maximum weighted size of the window space. */
598
  protected long windowMaximum() {
599
    throw new UnsupportedOperationException();
1 ✔
600
  }
601

602
  /** Returns the maximum weighted size of the main's protected space. */
603
  protected long mainProtectedMaximum() {
604
    throw new UnsupportedOperationException();
1 ✔
605
  }
606

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

612
  @GuardedBy("evictionLock")
613
  protected void setWindowMaximum(long maximum) {
614
    throw new UnsupportedOperationException();
1 ✔
615
  }
616

617
  @GuardedBy("evictionLock")
618
  protected void setMainProtectedMaximum(long maximum) {
619
    throw new UnsupportedOperationException();
1 ✔
620
  }
621

622
  /** Returns the combined weight of the values in the cache (may be negative). */
623
  protected long weightedSize() {
624
    throw new UnsupportedOperationException();
1 ✔
625
  }
626

627
  /** Returns the combined weight of the values in the cache (may be negative). */
628
  protected long weightedSizeAcquire() {
629
    throw new UnsupportedOperationException();
1 ✔
630
  }
631

632
  /** Returns the uncorrected combined weight of the values in the window space. */
633
  protected long windowWeightedSize() {
634
    throw new UnsupportedOperationException();
1 ✔
635
  }
636

637
  /** Returns the uncorrected combined weight of the values in the main's protected space. */
638
  protected long mainProtectedWeightedSize() {
639
    throw new UnsupportedOperationException();
1 ✔
640
  }
641

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

647
  @GuardedBy("evictionLock")
648
  protected void setWindowWeightedSize(long weightedSize) {
649
    throw new UnsupportedOperationException();
1 ✔
650
  }
651

652
  @GuardedBy("evictionLock")
653
  protected void setMainProtectedWeightedSize(long weightedSize) {
654
    throw new UnsupportedOperationException();
1 ✔
655
  }
656

657
  /**
658
   * Sets the maximum weighted size of the cache. The caller may need to perform a maintenance cycle
659
   * to eagerly evicts entries until the cache shrinks to the appropriate size.
660
   */
661
  @GuardedBy("evictionLock")
662
  @SuppressWarnings({"ConstantValue", "Varifier"})
663
  void setMaximumSize(long maximum) {
664
    long max = Math.min(maximum, MAXIMUM_CAPACITY);
1 ✔
665
    if (max == maximum()) {
1 ✔
666
      return;
1 ✔
667
    }
668

669
    long window = max - (long) (PERCENT_MAIN * max);
1 ✔
670
    long mainProtected = (long) (PERCENT_MAIN_PROTECTED * (max - window));
1 ✔
671

672
    setMaximum(max);
1 ✔
673
    setWindowMaximum(window);
1 ✔
674
    setMainProtectedMaximum(mainProtected);
1 ✔
675

676
    if (climber() != null) {
1 ✔
677
      // null during the super constructor's initial sizing; the generated constructor replays this
678
      climber().resized(maximum());
1 ✔
679
    }
680

681
    if ((frequencySketch() != null) && !isWeighted() && (weightedSize() >= (max >>> 1))) {
1 ✔
682
      // Lazily initialize when close to the maximum size
683
      frequencySketch().ensureCapacity(max);
1 ✔
684
      recordReads();
1 ✔
685
    }
686
  }
1 ✔
687

688
  /** Evicts entries if the cache exceeds the maximum. */
689
  @GuardedBy("evictionLock")
690
  void evictEntries(long now) {
691
    if (!evicts()) {
1 ✔
692
      return;
1 ✔
693
    }
694
    var candidate = evictFromWindow();
1 ✔
695
    evictFromMain(candidate, now);
1 ✔
696
  }
1 ✔
697

698
  /**
699
   * Evicts entries from the window space into the main space while the window size exceeds a
700
   * maximum, transferring at most a bounded number of entries per maintenance cycle.
701
   *
702
   * @return the first candidate promoted into the probation space
703
   */
704
  @GuardedBy("evictionLock")
705
  @Nullable Node<K, V> evictFromWindow() {
706
    @Var Node<K, V> first = null;
1 ✔
707
    @Var int remaining = QUEUE_TRANSFER_THRESHOLD;
1 ✔
708
    @Var Node<K, V> node = accessOrderWindowDeque().peekFirst();
1 ✔
709
    while (windowWeightedSize() > windowMaximum()) {
1 ✔
710
      // The pending operations will adjust the size to reflect the correct weight
711
      if (node == null) {
1 ✔
712
        break;
1 ✔
713
      }
714
      if (remaining == 0) {
1 ✔
715
        // A resize can leave the window arbitrarily oversized, so the transfer is bounded per
716
        // maintenance cycle and re-armed to drain the backlog across cycles
717
        setDrainStatusOpaque(PROCESSING_TO_REQUIRED);
1 ✔
718
        break;
1 ✔
719
      }
720

721
      Node<K, V> next = node.getNextInAccessOrder();
1 ✔
722
      long weight = node.getPolicyWeight();
1 ✔
723
      if (weight != 0) {
1 ✔
724
        transfer(node, weight, WINDOW, PROBATION);
1 ✔
725
        if (first == null) {
1 ✔
726
          first = node;
1 ✔
727
        }
728
        remaining--;
1 ✔
729
      }
730
      node = next;
1 ✔
731
    }
1 ✔
732

733
    return first;
1 ✔
734
  }
735

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

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

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

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

791
      // Evict immediately if only one of the entries is present
792
      if (victim == null) {
1 ✔
793
        requireNonNull(candidate);
1 ✔
794
        candidate = evictAndAdvance(candidate, now, RemovalCause.SIZE);
1 ✔
795
        continue;
1 ✔
796
      } else if (candidate == null) {
1 ✔
797
        victim = evictAndAdvance(victim, now, RemovalCause.SIZE);
1 ✔
798
        continue;
1 ✔
799
      }
800

801
      // Evict immediately if both selected the same entry
802
      if (candidate == victim) {
1 ✔
803
        victim = evictAndAdvance(victim, now, RemovalCause.SIZE);
1 ✔
804
        candidate = null;
1 ✔
805
        continue;
1 ✔
806
      }
807

808
      // Evict immediately if an entry was collected
809
      var victimKeyRef = victim.getKeyReferenceOrNull();
1 ✔
810
      var candidateKeyRef = candidate.getKeyReferenceOrNull();
1 ✔
811
      if (victimKeyRef == null) {
1 ✔
812
        victim = evictAndAdvance(victim, now, RemovalCause.COLLECTED);
1 ✔
813
        continue;
1 ✔
814
      } else if (candidateKeyRef == null) {
1 ✔
815
        candidate = evictAndAdvance(candidate, now, RemovalCause.COLLECTED);
1 ✔
816
        continue;
1 ✔
817
      }
818

819
      // Evict immediately if an entry was removed
820
      if (!victim.isAlive()) {
1 ✔
821
        victim = evictAndAdvance(victim, now, RemovalCause.SIZE);
1 ✔
822
        continue;
1 ✔
823
      } else if (!candidate.isAlive()) {
1 ✔
824
        candidate = evictAndAdvance(candidate, now, RemovalCause.SIZE);
1 ✔
825
        continue;
1 ✔
826
      }
827

828
      // Evict immediately if the candidate's weight exceeds the maximum
829
      if (candidate.getPolicyWeight() > maximum()) {
1 ✔
830
        candidate = evictAndAdvance(candidate, now, RemovalCause.SIZE);
1 ✔
831
        continue;
1 ✔
832
      }
833

834
      // Evict the entry with the lowest frequency
835
      if (admit(candidateKeyRef, victimKeyRef)) {
1 ✔
836
        victim = evictAndAdvance(victim, now, RemovalCause.SIZE);
1 ✔
837
        candidate = candidate.getNextInAccessOrder();
1 ✔
838
      } else {
839
        candidate = evictAndAdvance(candidate, now, RemovalCause.SIZE);
1 ✔
840
      }
841
    }
1 ✔
842
  }
1 ✔
843

844
  /** Evicts the entry and returns its successor, or {@code null} if it was the last. */
845
  @GuardedBy("evictionLock")
846
  @Nullable Node<K, V> evictAndAdvance(Node<K, V> node, long now, RemovalCause cause) {
847
    Node<K, V> next = node.getNextInAccessOrder();
1 ✔
848
    evictEntry(node, cause, now);
1 ✔
849
    return next;
1 ✔
850
  }
851

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

878
  /** Expires entries that have expired by access, write, or variable. */
879
  @GuardedBy("evictionLock")
880
  void expireEntries(long now) {
881
    expireAfterAccessEntries(now);
1 ✔
882
    expireAfterWriteEntries(now);
1 ✔
883
    expireVariableEntries(now);
1 ✔
884

885
    Pacer pacer = pacer();
1 ✔
886
    if (pacer != null) {
1 ✔
887
      long delay = getExpirationDelay(now);
1 ✔
888
      if (delay == Long.MAX_VALUE) {
1 ✔
889
        pacer.cancel();
1 ✔
890
      } else {
891
        pacer.schedule(executor, drainBuffersTask, now, delay);
1 ✔
892
      }
893
    }
894
  }
1 ✔
895

896
  /** Expires entries in the access-order queue. */
897
  @GuardedBy("evictionLock")
898
  void expireAfterAccessEntries(long now) {
899
    if (!expiresAfterAccess()) {
1 ✔
900
      return;
1 ✔
901
    }
902

903
    @Var int remaining = EXPIRATION_THRESHOLD;
1 ✔
904
    remaining = expireAfterAccessEntries(now, WINDOW, accessOrderWindowDeque(), remaining);
1 ✔
905
    if (evicts()) {
1 ✔
906
      remaining = expireAfterAccessEntries(
1 ✔
907
          now, PROBATION, accessOrderProbationDeque(), remaining);
1 ✔
908
      remaining = expireAfterAccessEntries(
1 ✔
909
          now, PROTECTED, accessOrderProtectedDeque(), remaining);
1 ✔
910
    }
911
    if (remaining == 0) {
1 ✔
912
      setDrainStatusOpaque(PROCESSING_TO_REQUIRED);
1 ✔
913
    }
914
  }
1 ✔
915

916
  /**
917
   * Expires entries in an access-order queue, up to the {@code remaining} budget, and returns the
918
   * unused budget. When exhausted the caller re-arms maintenance to process the backlog.
919
   */
920
  @GuardedBy("evictionLock")
921
  int expireAfterAccessEntries(long now, int queueType,
922
      AccessOrderDeque<Node<K, V>> accessOrderDeque, @Var int remaining) {
923
    var head = accessOrderDeque.peekFirst();
1 ✔
924
    if (head == null) {
1 ✔
925
      return remaining;
1 ✔
926
    }
927
    long duration = expiresAfterAccessNanos();
1 ✔
928
    var last = requireNonNull(accessOrderDeque.peekLast());
1 ✔
929
    for (var node = head; (node != null) && (remaining > 0);) {
1 ✔
930
      var next = (node == last) ? null : node.getNextInAccessOrder();
1 ✔
931
      if ((now - node.getAccessTime()) < duration) {
1 ✔
932
        if (isComputingAsync(node.getValue())) {
1 ✔
933
          accessOrderDeque.moveToBack(node);
1 ✔
934
          node = next;
1 ✔
935
          continue;
1 ✔
936
        }
937
        return remaining;
1 ✔
938
      }
939
      evictEntry(node, RemovalCause.EXPIRED, now);
1 ✔
940
      remaining--;
1 ✔
941

942
      // Reentrant maintenance can unlink or move any entry, so the scan resumes from the head and
943
      // stops if the captured tail that ends the walk left the deque
944
      node = (node == last) ? null : accessOrderDeque.peekFirst();
1 ✔
945
      boolean bounded = (last.getQueueType() == queueType) && accessOrderDeque.contains(last);
1 ✔
946
      if ((node != null) && !bounded) {
1 ✔
947
        return 0;
1 ✔
948
      }
949
    }
1 ✔
950
    return remaining;
1 ✔
951
  }
952

953
  /** Expires entries on the write-order queue. */
954
  @GuardedBy("evictionLock")
955
  void expireAfterWriteEntries(long now) {
956
    if (!expiresAfterWrite()) {
1 ✔
957
      return;
1 ✔
958
    }
959

960
    var head = writeOrderDeque().peekFirst();
1 ✔
961
    if (head == null) {
1 ✔
962
      return;
1 ✔
963
    }
964
    long duration = expiresAfterWriteNanos();
1 ✔
965
    @Var int remaining = EXPIRATION_THRESHOLD;
1 ✔
966
    var last = requireNonNull(writeOrderDeque().peekLast());
1 ✔
967
    for (var node = head; (node != null) && (remaining > 0);) {
1 ✔
968
      var next = (node == last) ? null : node.getNextInWriteOrder();
1 ✔
969
      if ((now - node.getWriteTime()) < duration) {
1 ✔
970
        boolean stalePosition = ((last.getWriteTime() - node.getWriteTime()) < 0);
1 ✔
971
        if (stalePosition || isComputingAsync(node.getValue())) {
1 ✔
972
          writeOrderDeque().moveToBack(node);
1 ✔
973
          node = next;
1 ✔
974
          continue;
1 ✔
975
        }
976
        return;
1 ✔
977
      }
978
      evictEntry(node, RemovalCause.EXPIRED, now);
1 ✔
979
      remaining--;
1 ✔
980

981
      // Reentrant maintenance can unlink or move any entry, so the scan resumes from the head and
982
      // stops if the captured tail that ends the walk left the deque
983
      node = (node == last) ? null : writeOrderDeque().peekFirst();
1 ✔
984
      if ((node != null) && !writeOrderDeque().contains(last)) {
1 ✔
985
        remaining = 0;
1 ✔
986
        break;
1 ✔
987
      }
988
    }
1 ✔
989
    if (remaining == 0) {
1 ✔
990
      setDrainStatusOpaque(PROCESSING_TO_REQUIRED);
1 ✔
991
    }
992
  }
1 ✔
993

994
  /** Expires entries in the timer wheel. */
995
  @GuardedBy("evictionLock")
996
  void expireVariableEntries(long now) {
997
    if (expiresVariable() && (timerWheel().advance(this, now, EXPIRATION_THRESHOLD) == 0)) {
1 ✔
998
      setDrainStatusOpaque(PROCESSING_TO_REQUIRED);
1 ✔
999
    }
1000
  }
1 ✔
1001

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

1038
  /** Returns if the entry has expired. The caller must exempt an in-flight asynchronous load. */
1039
  @SuppressWarnings("ShortCircuitBoolean")
1040
  boolean hasExpired(Node<K, V> node, long now) {
1041
    if (!expires()) {
1 ✔
1042
      return false;
1 ✔
1043
    }
1044
    boolean expired =
1 ✔
1045
        (expiresAfterAccess() && (now - node.getAccessTime() >= expiresAfterAccessNanos()))
1 ✔
1046
        | (expiresAfterWrite() && (now - node.getWriteTime() >= expiresAfterWriteNanos()))
1 ✔
1047
        | (expiresVariable() && (now - node.getVariableTime() >= 0));
1 ✔
1048
    VarHandle.loadLoadFence();
1 ✔
1049
    return expired;
1 ✔
1050
  }
1051

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

1068
    data.computeIfPresent(keyReference, (k, n) -> {
1 ✔
1069
      if (n != node) {
1 ✔
1070
        return n;
1 ✔
1071
      }
1072
      synchronized (node) {
1 ✔
1073
        ctx.value = node.getValue();
1 ✔
1074
        boolean expired = hasExpired(node, now);
1 ✔
1075
        boolean computing = isComputingAsync(ctx.value);
1 ✔
1076
        if ((key == null) || (ctx.value == null)) {
1 ✔
1077
          ctx.cause = RemovalCause.COLLECTED;
1 ✔
1078
        } else if (cause == RemovalCause.COLLECTED) {
1 ✔
1079
          ctx.resurrect = true;
1 ✔
1080
          return node;
1 ✔
1081
        } else if (expired && !computing) {
1 ✔
1082
          ctx.cause = RemovalCause.EXPIRED;
1 ✔
1083
        } else {
1084
          ctx.cause = cause;
1 ✔
1085
        }
1086

1087
        if (ctx.cause == RemovalCause.EXPIRED) {
1 ✔
1088
          if (!expired) {
1 ✔
1089
            ctx.resurrect = true;
1 ✔
1090
            return node;
1 ✔
1091
          } else if (computing) {
1 ✔
1092
            long sentinel = (now + ASYNC_EXPIRY);
1 ✔
1093
            setVariableTime(node, sentinel);
1 ✔
1094
            setAccessTime(node, sentinel);
1 ✔
1095
            setWriteTime(node, sentinel);
1 ✔
1096
            ctx.resurrect = true;
1 ✔
1097
            return node;
1 ✔
1098
          }
1099
        } else if (ctx.cause == RemovalCause.SIZE) {
1 ✔
1100
          int weight = node.getWeight();
1 ✔
1101
          if (weight == 0) {
1 ✔
1102
            ctx.resurrect = true;
1 ✔
1103
            return node;
1 ✔
1104
          }
1105
        }
1106

1107
        notifyEviction(key, ctx.value, ctx.cause);
1 ✔
1108
        discardRefresh(keyReference);
1 ✔
1109
        ctx.removed = true;
1 ✔
1110
        node.retire();
1 ✔
1111
        return null;
1 ✔
1112
      }
1113
    });
1114

1115
    // The entry is no longer eligible for eviction
1116
    if (ctx.resurrect) {
1 ✔
1117
      return false;
1 ✔
1118
    }
1119

1120
    // If the eviction fails due to a concurrent removal of the victim, that removal may cancel out
1121
    // the addition that triggered this eviction. The victim is eagerly unlinked and the size
1122
    // decremented before the removal task so that if an eviction is still required then a new
1123
    // victim will be chosen for removal.
1124
    unlink(node);
1 ✔
1125

1126
    synchronized (node) {
1 ✔
1127
      logIfAlive(node);
1 ✔
1128
      makeDead(node);
1 ✔
1129
    }
1 ✔
1130

1131
    if (ctx.removed) {
1 ✔
1132
      var removeCause = requireNonNull(ctx.cause);
1 ✔
1133
      statsCounter().recordEviction(node.getWeight(), removeCause);
1 ✔
1134
      notifyRemoval(key, ctx.value, removeCause);
1 ✔
1135
    }
1136

1137
    return true;
1 ✔
1138
  }
1139

1140
  /** Adapts the eviction policy to towards the optimal recency / frequency configuration. */
1141
  @GuardedBy("evictionLock")
1142
  @SuppressWarnings("UnnecessaryReturnStatement")
1143
  void climb() {
1144
    if (!evicts()) {
1 ✔
1145
      return;
1 ✔
1146
    }
1147

1148
    if (frequencySketch().isNotInitialized()) {
1 ✔
1149
      climber().resetSample();
1 ✔
1150
    } else if (weightedSize() >= (maximum() >>> 1)) {
1 ✔
1151
      climber().determineAdjustment(maximum(), windowMaximum(),
1 ✔
1152
          mainProtectedMaximum(), frequencySketch().sampleSize);
1 ✔
1153
    } else {
1154
      climber().discardSample(maximum(), frequencySketch().sampleSize);
1 ✔
1155
    }
1156

1157
    demoteFromMainProtected();
1 ✔
1158
    long amount = climber().adjustment();
1 ✔
1159
    if (amount == 0) {
1 ✔
1160
      return;
1 ✔
1161
    } else if (amount > 0) {
1 ✔
1162
      increaseWindow();
1 ✔
1163
    } else {
1164
      decreaseWindow();
1 ✔
1165
    }
1166
  }
1 ✔
1167

1168
  /**
1169
   * Increases the size of the admission window by shrinking the portion allocated to the main
1170
   * space. As the main space is partitioned into probation and protected regions (80% / 20%), for
1171
   * simplicity only the protected is reduced. If the regions exceed their maximums, this may cause
1172
   * protected items to be demoted to the probation region and probation items to be demoted to the
1173
   * admission window.
1174
   */
1175
  @GuardedBy("evictionLock")
1176
  void increaseWindow() {
1177
    if (mainProtectedMaximum() == 0) {
1 ✔
1178
      return;
1 ✔
1179
    }
1180

1181
    @SuppressWarnings("MathClampLong")
1182
    @Var long quota = Math.min(climber().adjustment(), mainProtectedMaximum());
1 ✔
1183
    setMainProtectedMaximum(mainProtectedMaximum() - quota);
1 ✔
1184
    setWindowMaximum(windowMaximum() + quota);
1 ✔
1185
    demoteFromMainProtected();
1 ✔
1186

1187
    for (int i = 0; i < QUEUE_TRANSFER_THRESHOLD; i++) {
1 ✔
1188
      @Var Node<K, V> candidate = accessOrderProbationDeque().peekFirst();
1 ✔
1189
      @Var boolean probation = true;
1 ✔
1190
      if ((candidate == null) || (quota < candidate.getPolicyWeight())) {
1 ✔
1191
        candidate = accessOrderProtectedDeque().peekFirst();
1 ✔
1192
        probation = false;
1 ✔
1193
      }
1194
      if (candidate == null) {
1 ✔
1195
        break;
1 ✔
1196
      }
1197

1198
      long weight = candidate.getPolicyWeight();
1 ✔
1199
      if (quota < weight) {
1 ✔
1200
        break;
1 ✔
1201
      }
1202

1203
      quota -= weight;
1 ✔
1204
      transfer(candidate, weight, probation ? PROBATION : PROTECTED, WINDOW);
1 ✔
1205
    }
1206

1207
    setMainProtectedMaximum(mainProtectedMaximum() + quota);
1 ✔
1208
    setWindowMaximum(windowMaximum() - quota);
1 ✔
1209
    climber().carryOver(quota);
1 ✔
1210
  }
1 ✔
1211

1212
  /** Decreases the size of the admission window and increases the main's protected region. */
1213
  @GuardedBy("evictionLock")
1214
  void decreaseWindow() {
1215
    if (windowMaximum() <= 1) {
1 ✔
1216
      return;
1 ✔
1217
    }
1218

1219
    @SuppressWarnings("MathClampLong")
1220
    @Var long quota = Math.min(-climber().adjustment(), Math.max(0, windowMaximum() - 1));
1 ✔
1221
    setMainProtectedMaximum(mainProtectedMaximum() + quota);
1 ✔
1222
    setWindowMaximum(windowMaximum() - quota);
1 ✔
1223

1224
    @Var int remaining = QUEUE_TRANSFER_THRESHOLD;
1 ✔
1225
    @Var Node<K, V> candidate = accessOrderWindowDeque().peekFirst();
1 ✔
1226
    while ((candidate != null) && (remaining > 0)) {
1 ✔
1227
      Node<K, V> next = candidate.getNextInAccessOrder();
1 ✔
1228
      long weight = candidate.getPolicyWeight();
1 ✔
1229
      if (weight != 0) {
1 ✔
1230
        if (quota < weight) {
1 ✔
1231
          break;
1 ✔
1232
        }
1233
        quota -= weight;
1 ✔
1234
        transfer(candidate, weight, WINDOW, PROBATION);
1 ✔
1235
        remaining--;
1 ✔
1236
      }
1237
      candidate = next;
1 ✔
1238
    }
1 ✔
1239

1240
    setMainProtectedMaximum(mainProtectedMaximum() - quota);
1 ✔
1241
    setWindowMaximum(windowMaximum() + quota);
1 ✔
1242
    climber().carryOver(-quota);
1 ✔
1243
  }
1 ✔
1244

1245
  /** Transfers the nodes from the protected to the probation region if it exceeds the maximum. */
1246
  @GuardedBy("evictionLock")
1247
  void demoteFromMainProtected() {
1248
    long mainProtectedMaximum = mainProtectedMaximum();
1 ✔
1249
    @Var int remaining = QUEUE_TRANSFER_THRESHOLD;
1 ✔
1250
    while (mainProtectedWeightedSize() > mainProtectedMaximum) {
1 ✔
1251
      Node<K, V> demoted = accessOrderProtectedDeque().peekFirst();
1 ✔
1252
      if (demoted == null) {
1 ✔
1253
        break;
1 ✔
1254
      }
1255
      if (remaining == 0) {
1 ✔
1256
        setDrainStatusOpaque(PROCESSING_TO_REQUIRED);
1 ✔
1257
        break;
1 ✔
1258
      }
1259
      transfer(demoted, demoted.getPolicyWeight(), PROTECTED, PROBATION);
1 ✔
1260
      remaining--;
1 ✔
1261
    }
1 ✔
1262
  }
1 ✔
1263

1264
  /**
1265
   * Moves the node into another region of the eviction policy, adjusting the weighted size of each
1266
   * region that tracks its own.
1267
   */
1268
  @GuardedBy("evictionLock")
1269
  void transfer(Node<K, V> node, long weight, int from, int to) {
1270
    if (from == WINDOW) {
1 ✔
1271
      setWindowWeightedSize(windowWeightedSize() - weight);
1 ✔
1272
      accessOrderWindowDeque().remove(node);
1 ✔
1273
    } else if (from == PROBATION) {
1 ✔
1274
      accessOrderProbationDeque().remove(node);
1 ✔
1275
    } else {
1276
      setMainProtectedWeightedSize(mainProtectedWeightedSize() - weight);
1 ✔
1277
      accessOrderProtectedDeque().remove(node);
1 ✔
1278
    }
1279

1280
    if (to == WINDOW) {
1 ✔
1281
      setWindowWeightedSize(windowWeightedSize() + weight);
1 ✔
1282
      accessOrderWindowDeque().offerLast(node);
1 ✔
1283
      node.makeWindow();
1 ✔
1284
    } else if (to == PROBATION) {
1 ✔
1285
      accessOrderProbationDeque().offerLast(node);
1 ✔
1286
      node.makeMainProbation();
1 ✔
1287
    } else {
1288
      setMainProtectedWeightedSize(mainProtectedWeightedSize() + weight);
1 ✔
1289
      accessOrderProtectedDeque().offerLast(node);
1 ✔
1290
      node.makeMainProtected();
1 ✔
1291
    }
1292
  }
1 ✔
1293

1294
  /**
1295
   * Performs the post-processing work required after a read.
1296
   *
1297
   * @param node the entry in the page replacement policy
1298
   * @param now the current time, in nanoseconds
1299
   * @param recordHit if the hit count should be incremented
1300
   * @return the refreshed value if immediately loaded, else null
1301
   */
1302
  @Nullable V afterRead(Node<K, V> node, long now, boolean recordHit) {
1303
    if (recordHit) {
1 ✔
1304
      statsCounter().recordHits(1);
1 ✔
1305
    }
1306

1307
    boolean delayable = (readBuffer.offer(node) != Buffer.FULL);
1 ✔
1308
    if (shouldDrainBuffers(delayable)) {
1 ✔
1309
      scheduleDrainBuffers();
1 ✔
1310
    }
1311
    return refreshIfNeeded(node, now);
1 ✔
1312
  }
1313

1314
  /** Enables the read buffer (disabled if fastpath until the frequency sketch is initialized). */
1315
  @GuardedBy("evictionLock")
1316
  void recordReads() {
1317
    if (readBuffer == Buffer.<Node<K, V>>disabled()) {
1 ✔
1318
      readBuffer = new BoundedBuffer<>();
1 ✔
1319
    }
1320
  }
1 ✔
1321

1322
  /**
1323
   * Asynchronously refreshes the entry if eligible.
1324
   *
1325
   * @param node the entry in the cache to refresh
1326
   * @param now the current time, in nanoseconds
1327
   * @return the refreshed value if immediately loaded, else null
1328
   */
1329
  @SuppressWarnings("FutureReturnValueIgnored")
1330
  @Nullable V refreshIfNeeded(Node<K, V> node, long now) {
1331
    if (!refreshAfterWrite()) {
1 ✔
1332
      return null;
1 ✔
1333
    }
1334

1335
    K key;
1336
    V oldValue;
1337
    long writeTime = node.getWriteTime();
1 ✔
1338
    long refreshWriteTime = markRefreshing(writeTime);
1 ✔
1339
    ConcurrentMap<Object, CompletableFuture<?>> refreshes;
1340
    if (((now - writeTime) > refreshAfterWriteNanos())
1 ✔
1341
        && ((key = node.getKey()) != null) && ((oldValue = node.getValue()) != null)
1 ✔
1342
        && !isComputingAsync(oldValue) && !isRefreshing(writeTime)
1 ✔
1343
        && !(refreshes = refreshes()).containsKey(node.getKeyReference())
1 ✔
1344
        && node.isAlive() && node.casWriteTime(writeTime, refreshWriteTime)) {
1 ✔
1345
      long startTime;
1346
      var keyReference = referenceKey(key);
1 ✔
1347
      CompletableFuture<? extends @Nullable V> refreshFuture;
1348
      @SuppressWarnings({"unchecked", "Varifier"})
1349
      @Nullable V[] reloading = (@Nullable V[]) new Object[1];
1 ✔
1350
      try {
1351
        // The registration is published before the loader is called, so the loader runs outside
1352
        // the refresh map's bin
1353
        var pending = new LocalLoadingCache.PendingRefresh();
1 ✔
1354
        var registered = refreshes.computeIfAbsent(keyReference, k -> {
1 ✔
1355
          if (!node.isAlive() || (node.getWriteTime() != refreshWriteTime)) {
1 ✔
1356
            return nullRef();
1 ✔
1357
          }
1358
          // no-op if the future's completion state was modified (e.g. obtrude methods)
1359
          @SuppressWarnings("unchecked")
1360
          V value = isAsync ? Async.getIfReady((CompletableFuture<V>) oldValue) : oldValue;
1 ✔
1361
          reloading[0] = value;
1 ✔
1362
          return (value == null) ? nullRef() : pending;
1 ✔
1363
        });
1364
        if (registered != pending) {
1 ✔
1365
          return null;
1 ✔
1366
        }
1367
        try {
1368
          startTime = statsTicker().read();
1 ✔
1369
          requireNonNull(cacheLoader);
1 ✔
1370
          var refresh = cacheLoader.asyncReload(key, requireNonNull(reloading[0]), executor);
1 ✔
1371
          refreshFuture = requireNonNull(refresh, "Null future");
1 ✔
1372
        } catch (Throwable e) {
1 ✔
1373
          pending.withdraw(refreshes, keyReference, e);
1 ✔
1374
          if (e instanceof InterruptedException) {
1 ✔
1375
            Thread.currentThread().interrupt();
1 ✔
1376
          }
1377
          logger.log(Level.WARNING, "Exception thrown when submitting refresh task", e);
1 ✔
1378
          return null;
1 ✔
1379
        }
1 ✔
1380
        pending.publish(refreshes, keyReference, refreshFuture);
1 ✔
1381
      } finally {
1382
        node.casWriteTime(refreshWriteTime, writeTime);
1 ✔
1383
      }
1384

1385
      var refreshed = refreshFuture.handle((newValue, error) -> {
1 ✔
1386
        long loadTime = statsTicker().read() - startTime;
1 ✔
1387
        if (error != null) {
1 ✔
1388
          if (!(error instanceof CancellationException) && !(error instanceof TimeoutException)) {
1 ✔
1389
            logger.log(Level.WARNING, "Exception thrown during refresh", error);
1 ✔
1390
          }
1391
          refreshes.remove(keyReference, refreshFuture);
1 ✔
1392
          statsCounter().recordLoadFailure(loadTime);
1 ✔
1393
          return null;
1 ✔
1394
        }
1395

1396
        @SuppressWarnings("unchecked")
1397
        V value = (isAsync && (newValue != null)) ? (V) refreshFuture : newValue;
1 ✔
1398
        @Nullable RemovalCause[] cause = new RemovalCause[1];
1 ✔
1399
        var hints = new RemapHints();
1 ✔
1400
        hints.quietly = true;
1 ✔
1401
        V result;
1402
        try {
1403
          result = compute(key, (K k, @Nullable V currentValue) -> {
1 ✔
1404
            // Keep the refresh registered until the write clears it to avoid readers from
1405
            // prematurely scheduling another reload
1406
            boolean owned = (refreshes.get(keyReference) == refreshFuture);
1 ✔
1407
            if (currentValue == null) {
1 ✔
1408
              // If the entry is absent then drop this refresh, unless a successor owns the
1409
              // registration, and maybe notify the listener
1410
              if (value != null) {
1 ✔
1411
                cause[0] = RemovalCause.EXPLICIT;
1 ✔
1412
              }
1413
              hints.preserveRefresh = !owned;
1 ✔
1414
              return null;
1 ✔
1415
            }
1416
            if ((currentValue == oldValue) && node.isAlive()
1 ✔
1417
                && (writeTimeOf(node) == writeTime) && owned) {
1 ✔
1418
              // If the entry was not modified while in-flight (no ABA) then replace, refreshing the
1419
              // metadata even when the reloaded value is the same instance
1420
              return value;
1 ✔
1421
            }
1422
            // Otherwise the refresh is discarded. The rejection installs nothing, so the entry's
1423
            // timestamps are preserved rather than restarted at the completion time. A
1424
            // same-instance reload is not a value change, so it is not notified. When a successor
1425
            // refresh owns the registration, leave it intact so this by-key discard cannot steal
1426
            // its token.
1427
            boolean sameInstance = (currentValue == value)
1 ✔
1428
                || (isAsync && (newValue == Async.getIfReady((CompletableFuture<?>) currentValue)));
1 ✔
1429
            if ((value != null) && !sameInstance) {
1 ✔
1430
              cause[0] = RemovalCause.REPLACED;
1 ✔
1431
            }
1432
            hints.preserveTimestamps = true;
1 ✔
1433
            hints.preserveRefresh = !owned;
1 ✔
1434
            return currentValue;
1 ✔
1435
          }, expiry(), /* recordLoad= */ false, hints);
1 ✔
1436
        } catch (Throwable t) {
1 ✔
1437
          logger.log(Level.WARNING, "Exception thrown during refresh", t);
1 ✔
1438
          refreshes.remove(keyReference, refreshFuture);
1 ✔
1439
          statsCounter().recordLoadFailure(loadTime);
1 ✔
1440
          return null;
1 ✔
1441
        }
1 ✔
1442

1443
        if (cause[0] != null) {
1 ✔
1444
          notifyRemoval(key, value, cause[0]);
1 ✔
1445
        }
1446
        if (newValue == null) {
1 ✔
1447
          statsCounter().recordLoadFailure(loadTime);
1 ✔
1448
        } else {
1449
          statsCounter().recordLoadSuccess(loadTime);
1 ✔
1450
        }
1451
        return result;
1 ✔
1452
      });
1453
      return Async.getIfReady(refreshed);
1 ✔
1454
    }
1455

1456
    return null;
1 ✔
1457
  }
1458

1459
  /**
1460
   * Returns the expiration time for the entry after being created.
1461
   *
1462
   * @param key the key of the entry that was created
1463
   * @param value the value of the entry that was created
1464
   * @param expiry the calculator for the expiration time
1465
   * @param now the current time, in nanoseconds
1466
   * @return the expiration time
1467
   */
1468
  long expireAfterCreate(K key, V value, @Nullable Expiry<? super K, ? super V> expiry, long now) {
1469
    if (expiresVariable()) {
1 ✔
1470
      requireNonNull(expiry);
1 ✔
1471
      long duration = Math.max(0L, expiry.expireAfterCreate(key, value, now));
1 ✔
1472
      return expiresAt(now, duration);
1 ✔
1473
    }
1474
    return 0L;
1 ✔
1475
  }
1476

1477
  /**
1478
   * Returns the expiration time for the entry after being updated.
1479
   *
1480
   * @param node the entry in the page replacement policy
1481
   * @param key the key of the entry that was updated
1482
   * @param value the value of the entry that was updated
1483
   * @param expiry the calculator for the expiration time
1484
   * @param now the current time, in nanoseconds
1485
   * @return the expiration time
1486
   */
1487
  long expireAfterUpdate(Node<K, V> node, K key, V value,
1488
      @Nullable Expiry<? super K, ? super V> expiry, long now) {
1489
    if (expiresVariable()) {
1 ✔
1490
      requireNonNull(expiry);
1 ✔
1491
      long currentDuration = Math.max(0L, node.getVariableTime() - now);
1 ✔
1492
      long duration = Math.max(0L, expiry.expireAfterUpdate(key, value, now, currentDuration));
1 ✔
1493
      return expiresAt(now, duration);
1 ✔
1494
    }
1495
    return 0L;
1 ✔
1496
  }
1497

1498
  /**
1499
   * Returns the access time for the entry after a read.
1500
   *
1501
   * @param node the entry in the page replacement policy
1502
   * @param key the key of the entry that was read
1503
   * @param value the value of the entry that was read
1504
   * @param expiry the calculator for the expiration time
1505
   * @param now the current time, in nanoseconds
1506
   * @return the expiration time
1507
   */
1508
  long expireAfterRead(Node<K, V> node, K key, V value, Expiry<K, V> expiry, long now) {
1509
    if (expiresVariable()) {
1 ✔
1510
      long variableTime = node.getVariableTime();
1 ✔
1511
      long currentDuration = Math.max(0L, variableTime - now);
1 ✔
1512
      if (isAsync && (currentDuration > MAXIMUM_EXPIRY)) {
1 ✔
1513
        // expireAfterCreate has not yet set the duration after completion
1514
        return variableTime;
1 ✔
1515
      }
1516
      long duration = Math.max(0L, expiry.expireAfterRead(key, value, now, currentDuration));
1 ✔
1517
      return expiresAt(now, duration);
1 ✔
1518
    }
1519
    return 0L;
1 ✔
1520
  }
1521

1522
  /**
1523
   * Attempts to update the access time for the entry after a read.
1524
   *
1525
   * @param node the entry in the page replacement policy
1526
   * @param key the key of the entry that was read
1527
   * @param value the value of the entry that was read
1528
   * @param expiry the calculator for the expiration time
1529
   * @param now the current time, in nanoseconds
1530
   */
1531
  void tryExpireAfterRead(Node<K, V> node, K key, V value, Expiry<K, V> expiry, long now) {
1532
    if (!expiresVariable()) {
1 ✔
1533
      return;
1 ✔
1534
    }
1535

1536
    long variableTime = node.getVariableTime();
1 ✔
1537
    long currentDuration = Math.max(0L, variableTime - now);
1 ✔
1538
    if (isAsync && (currentDuration > MAXIMUM_EXPIRY)) {
1 ✔
1539
      // expireAfterCreate has not yet set the duration after completion
1540
      return;
1 ✔
1541
    }
1542

1543
    long tolerance = EXPIRE_TOLERANCE;
1 ✔
1544
    long duration = Math.max(0L, expiry.expireAfterRead(key, value, now, currentDuration));
1 ✔
1545
    long expirationTime = expiresAt(now, duration);
1 ✔
1546
    long extension = expirationTime - variableTime;
1 ✔
1547
    if ((extension != 0)
1 ✔
1548
        && ((duration <= tolerance) || (extension < 0) || (extension > tolerance))) {
1549
      VarHandle.loadLoadFence();
1 ✔
1550
      if (node.getValue() == value) {
1 ✔
1551
        node.casVariableTime(variableTime, expirationTime);
1 ✔
1552
      }
1553
    }
1554
  }
1 ✔
1555

1556
  void setAccessTime(Node<K, V> node, long now) {
1557
    if (!expiresAfterAccess()) {
1 ✔
1558
      return;
1 ✔
1559
    }
1560
    long tolerance = EXPIRE_TOLERANCE;
1 ✔
1561
    long accessTime = node.getAccessTime();
1 ✔
1562
    if ((expiresAfterAccessNanos() <= tolerance) || (Math.abs(now - accessTime) > tolerance)) {
1 ✔
1563
      node.setAccessTime(now);
1 ✔
1564
    }
1565
  }
1 ✔
1566

1567
  void setWriteTime(Node<K, V> node, long now) {
1568
    if (expiresAfterWrite() || refreshAfterWrite()) {
1 ✔
1569
      // The write time reserves its low bit to mark that a refresh is in flight, so a recorded time
1570
      // is always even and an age must be measured against an evenly truncated clock
1571
      node.setWriteTime(toWriteTime(now));
1 ✔
1572
    }
1573
  }
1 ✔
1574

1575
  void setVariableTime(Node<K, V> node, long expirationTime) {
1576
    if (expiresVariable()) {
1 ✔
1577
      node.setVariableTime(expirationTime);
1 ✔
1578
    }
1579
  }
1 ✔
1580

1581
  /** Returns if the entry's expiration is still deferred to its value's completion. */
1582
  boolean isExpirationDeferred(Node<K, V> node, long now) {
1583
    return expiresVariable() && ((node.getVariableTime() - now) > MAXIMUM_EXPIRY);
1 ✔
1584
  }
1585

1586
  /** Returns if the entry's write time would exceed the minimum expiration reorder threshold. */
1587
  boolean exceedsWriteTimeTolerance(Node<K, V> node, long varTime, long now) {
1588
    long variableTime = node.getVariableTime();
1 ✔
1589
    long writeTime = node.getWriteTime();
1 ✔
1590
    long tolerance = EXPIRE_TOLERANCE;
1 ✔
1591
    return
1 ✔
1592
        (expiresAfterWrite()
1 ✔
1593
            && ((expiresAfterWriteNanos() <= tolerance) || (Math.abs(now - writeTime) > tolerance)))
1 ✔
1594
        || (refreshAfterWrite()
1 ✔
1595
            && ((refreshAfterWriteNanos() <= tolerance) || (Math.abs(now - writeTime) > tolerance)))
1 ✔
1596
        || (expiresVariable() && (Math.abs(varTime - variableTime) > tolerance));
1 ✔
1597
  }
1598

1599
  /** Returns the expiration time for a duration. */
1600
  long expiresAt(long now, long duration) {
1601
    return isAsync ? (now + duration) : (now + Math.min(duration, MAXIMUM_EXPIRY));
1 ✔
1602
  }
1603

1604
  /** Returns the time to record on the entry. */
1605
  long expirationTimeFor(@Nullable V value, long now) {
1606
    return isComputingAsync(value) ? (now + ASYNC_EXPIRY) : now;
1 ✔
1607
  }
1608

1609
  /** Returns the time truncated to the write time's granularity. */
1610
  static long toWriteTime(long time) {
1611
    return (time & ~1L);
1 ✔
1612
  }
1613

1614
  /** Returns the entry's write time, with the refresh marker removed. */
1615
  static long writeTimeOf(Node<?, ?> node) {
1616
    return toWriteTime(node.getWriteTime());
1 ✔
1617
  }
1618

1619
  /** Returns if a refresh is in flight for the entry that recorded this write time. */
1620
  static boolean isRefreshing(long writeTime) {
1621
    return ((writeTime & 1L) != 0L);
1 ✔
1622
  }
1623

1624
  /** Returns the write time marked as having a refresh in flight. */
1625
  static long markRefreshing(long writeTime) {
1626
    return (writeTime | 1L);
1 ✔
1627
  }
1628

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

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

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

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

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

1748
  /** Returns whether a thread waits for the eviction lock and this thread does not hold it. */
1749
  boolean isEvictionLockAwaited() {
1750
    return !evictionLock.isHeldByCurrentThread() && evictionLock.hasQueuedThreads();
1 ✔
1751
  }
1752

1753
  @Override
1754
  public void cleanUp() {
1755
    try {
1756
      performCleanUp(/* ignored */ null);
1 ✔
1757
    } catch (RuntimeException e) {
1 ✔
1758
      logger.log(Level.ERROR, "Exception thrown when performing the maintenance task", e);
1 ✔
1759
    }
1 ✔
1760
  }
1 ✔
1761

1762
  /**
1763
   * Performs the maintenance work, blocking until the lock is acquired.
1764
   *
1765
   * @param task an additional pending task to run, or {@code null} if not present
1766
   */
1767
  void performCleanUp(@Nullable Runnable task) {
1768
    evictionLock.lock();
1 ✔
1769
    try {
1770
      maintenance(task);
1 ✔
1771
    } finally {
1772
      evictionLock.unlock();
1 ✔
1773
    }
1774
    rescheduleCleanUpIfIncomplete();
1 ✔
1775
  }
1 ✔
1776

1777
  /**
1778
   * If there remains pending operations that were not handled by the prior clean up then try to
1779
   * schedule an asynchronous maintenance task. This may occur due to a concurrent write after the
1780
   * maintenance work had started or if the amortized threshold of work per clean up was reached.
1781
   */
1782
  @SuppressWarnings("resource")
1783
  void rescheduleCleanUpIfIncomplete() {
1784
    if (drainStatusOpaque() != REQUIRED) {
1 ✔
1785
      return;
1 ✔
1786
    }
1787

1788
    // An immediate scheduling cannot be performed on a custom executor because it may use a
1789
    // caller-runs policy. This could cause the caller's penalty to exceed the amortized threshold,
1790
    // e.g. repeated concurrent writes could result in a retry loop.
1791
    if (executor == ForkJoinPool.commonPool()) {
1 ✔
1792
      scheduleDrainBuffers();
1 ✔
1793
      return;
1 ✔
1794
    }
1795

1796
    // If a scheduler was configured then the maintenance can be deferred onto the custom executor
1797
    // and run in the near future. Otherwise, it will be handled due to other cache activity.
1798
    var pacer = pacer();
1 ✔
1799
    if ((pacer != null) && !pacer.isScheduled() && evictionLock.tryLock()) {
1 ✔
1800
      try {
1801
        if ((drainStatusOpaque() == REQUIRED) && !pacer.isScheduled()) {
1 ✔
1802
          pacer.schedule(executor, drainBuffersTask, expirationTicker().read(), Pacer.TOLERANCE);
1 ✔
1803
        }
1804
      } finally {
1805
        evictionLock.unlock();
1 ✔
1806
      }
1807
    }
1808
  }
1 ✔
1809

1810
  /**
1811
   * Performs the pending maintenance work and sets the state flags during processing to avoid
1812
   * excess scheduling attempts. The read buffer, write buffer, and reference queues are drained,
1813
   * followed by expiration, and size-based eviction.
1814
   *
1815
   * @param task an additional pending task to run, or {@code null} if not present
1816
   */
1817
  @GuardedBy("evictionLock")
1818
  void maintenance(@Nullable Runnable task) {
1819
    setDrainStatusRelease(PROCESSING_TO_IDLE);
1 ✔
1820

1821
    try {
1822
      try {
1823
        drainReadBuffer();
1 ✔
1824
        drainWriteBuffer();
1 ✔
1825
      } finally {
1826
        if (task != null) {
1 ✔
1827
          task.run();
1 ✔
1828
        }
1829
      }
1830

1831
      drainKeyReferences();
1 ✔
1832
      drainValueReferences();
1 ✔
1833

1834
      long now = expirationTicker().read();
1 ✔
1835
      expireEntries(now);
1 ✔
1836
      evictEntries(now);
1 ✔
1837

1838
      climb();
1 ✔
1839
    } finally {
1840
      if ((drainStatusOpaque() != PROCESSING_TO_IDLE)
1 ✔
1841
          || !casDrainStatus(PROCESSING_TO_IDLE, IDLE)) {
1 ✔
1842
        setDrainStatusOpaque(REQUIRED);
1 ✔
1843
      }
1844
    }
1845
  }
1 ✔
1846

1847
  /** Drains the weak key references queue. */
1848
  @GuardedBy("evictionLock")
1849
  void drainKeyReferences() {
1850
    if (!collectKeys()) {
1 ✔
1851
      return;
1 ✔
1852
    }
1853
    for (int i = 0; i <= REFERENCE_THRESHOLD; i++) {
1 ✔
1854
      var keyRef = keyReferenceQueue().poll();
1 ✔
1855
      if (keyRef == null) {
1 ✔
1856
        return;
1 ✔
1857
      }
1858
      Node<K, V> node = data.get(keyRef);
1 ✔
1859
      if (node != null) {
1 ✔
1860
        evictEntry(node, RemovalCause.COLLECTED, 0L);
1 ✔
1861
      }
1862
    }
1863
    setDrainStatusOpaque(PROCESSING_TO_REQUIRED);
1 ✔
1864
  }
1 ✔
1865

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

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

1893
  /**
1894
   * Updates the node's location in the page replacement policy and credits the access.
1895
   * <p>
1896
   * A quiet access reorders but does not record the usage with the admission filter or the adaptive
1897
   * climber's sample, as an asynchronous cache's load completion finalizes the entry's weight
1898
   * rather than conveying a usage; counting it once per load skews the admission frequencies and
1899
   * misattributes window hits.
1900
   */
1901
  @GuardedBy("evictionLock")
1902
  void onAccess(Node<K, V> node, Access access) {
1903
    if (evicts()) {
1 ✔
1904
      var keyRef = node.getKeyReferenceOrNull();
1 ✔
1905
      if ((keyRef == null) || !node.isAlive()) {
1 ✔
1906
        return;
1 ✔
1907
      }
1908
      if (access.recordsUsage()) {
1 ✔
1909
        frequencySketch().increment(keyRef);
1 ✔
1910
      }
1911
      boolean inWindow = node.inWindow();
1 ✔
1912
      boolean inProbation = !inWindow && node.inMainProbation();
1 ✔
1913
      if (inWindow) {
1 ✔
1914
        reorder(accessOrderWindowDeque(), node);
1 ✔
1915
      } else if (inProbation) {
1 ✔
1916
        reorderProbation(node);
1 ✔
1917
      } else {
1918
        reorder(accessOrderProtectedDeque(), node);
1 ✔
1919
      }
1920
      if (access.recordsHit() && (node.getPolicyWeight() != 0)) {
1 ✔
1921
        // a zero-weight entry (an in-flight async load, a pinned entry) earns hits with no
1922
        // capacity, so it must not steer the sizing; the sketch still observes the key
1923
        climber().recordHit(inWindow, inProbation);
1 ✔
1924
      }
1925
    } else if (expiresAfterAccess()) {
1 ✔
1926
      reorder(accessOrderWindowDeque(), node);
1 ✔
1927
    }
1928
    if (expiresVariable()) {
1 ✔
1929
      timerWheel().reschedule(node);
1 ✔
1930
    }
1931
  }
1 ✔
1932

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

1944
    // If the protected space exceeds its maximum, the LRU items are demoted to the probation space.
1945
    // This is deferred to the adaption phase at the end of the maintenance cycle.
1946
    transfer(node, node.getPolicyWeight(), PROBATION, PROTECTED);
1 ✔
1947
  }
1 ✔
1948

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

1959
  /** Drains the write buffer. */
1960
  @GuardedBy("evictionLock")
1961
  void drainWriteBuffer() {
1962
    for (int i = 0; i <= WRITE_BUFFER_MAX; i++) {
1 ✔
1963
      Runnable task = writeBuffer.relaxedPoll();
1 ✔
1964
      if (task == null) {
1 ✔
1965
        return;
1 ✔
1966
      }
1967
      task.run();
1 ✔
1968
    }
1969
    setDrainStatusOpaque(PROCESSING_TO_REQUIRED);
1 ✔
1970
  }
1 ✔
1971

1972
  /** Removes the node from the policy's queues. */
1973
  @GuardedBy("evictionLock")
1974
  void unlink(Node<K, V> node) {
1975
    if (node.inWindow() && (evicts() || expiresAfterAccess())) {
1 ✔
1976
      accessOrderWindowDeque().remove(node);
1 ✔
1977
    } else if (evicts()) {
1 ✔
1978
      if (node.inMainProbation()) {
1 ✔
1979
        accessOrderProbationDeque().remove(node);
1 ✔
1980
      } else {
1981
        accessOrderProtectedDeque().remove(node);
1 ✔
1982
      }
1983
    }
1984
    if (expiresAfterWrite()) {
1 ✔
1985
      writeOrderDeque().remove(node);
1 ✔
1986
    } else if (expiresVariable()) {
1 ✔
1987
      timerWheel().deschedule(node);
1 ✔
1988
    }
1989
  }
1 ✔
1990

1991
  /**
1992
   * Atomically transitions the node to the <code>dead</code> state and decrements the
1993
   * <code>weightedSize</code>.
1994
   *
1995
   * @param node the entry in the page replacement policy
1996
   */
1997
  @GuardedBy("evictionLock")
1998
  @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
1999
  void makeDead(Node<K, V> node) {
2000
    synchronized (node) {
1 ✔
2001
      if (node.isDead()) {
1 ✔
2002
        return;
1 ✔
2003
      }
2004
      if (evicts()) {
1 ✔
2005
        // The node's policy weight may be out of sync due to a pending update waiting to be
2006
        // processed. At this point the node's weight is finalized, so the weight can be safely
2007
        // taken from the node's perspective and the sizes will be adjusted correctly.
2008
        if (node.inWindow()) {
1 ✔
2009
          setWindowWeightedSize(windowWeightedSize() - node.getWeight());
1 ✔
2010
        } else if (node.inMainProtected()) {
1 ✔
2011
          setMainProtectedWeightedSize(mainProtectedWeightedSize() - node.getWeight());
1 ✔
2012
        }
2013
        setWeightedSize(weightedSize() - node.getWeight());
1 ✔
2014
      }
2015
      node.die();
1 ✔
2016
    }
1 ✔
2017
  }
1 ✔
2018

2019
  /** Adds the node to the page replacement policy. */
2020
  final class AddTask implements Runnable {
2021
    final Node<K, V> node;
2022
    final int weight;
2023

2024
    AddTask(Node<K, V> node, int weight) {
1 ✔
2025
      this.weight = weight;
1 ✔
2026
      this.node = node;
1 ✔
2027
    }
1 ✔
2028

2029
    @Override
2030
    @GuardedBy("evictionLock")
2031
    public void run() {
2032
      if (evicts()) {
1 ✔
2033
        setWeightedSize(weightedSize() + weight);
1 ✔
2034
        setWindowWeightedSize(windowWeightedSize() + weight);
1 ✔
2035
        node.setPolicyWeight(node.getPolicyWeight() + weight);
1 ✔
2036

2037
        long maximum = maximum();
1 ✔
2038
        if (weightedSize() >= (maximum >>> 1)) {
1 ✔
2039
          if (weightedSize() > MAXIMUM_CAPACITY) {
1 ✔
2040
            evictEntries(expirationTicker().read());
1 ✔
2041
          } else {
2042
            // Lazily initialize when close to the maximum
2043
            long capacity = isWeighted() ? data.mappingCount() : maximum;
1 ✔
2044
            frequencySketch().ensureCapacity(capacity);
1 ✔
2045
            recordReads();
1 ✔
2046
          }
2047
        }
2048

2049
        climber().recordMiss();
1 ✔
2050
      }
2051

2052
      // ignore out-of-order write operations
2053
      boolean isAlive;
2054
      synchronized (node) {
1 ✔
2055
        isAlive = node.isAlive();
1 ✔
2056
      }
1 ✔
2057
      if (isAlive) {
1 ✔
2058
        if (expiresAfterWrite()) {
1 ✔
2059
          writeOrderDeque().offerLast(node);
1 ✔
2060
        }
2061
        if (expiresVariable()) {
1 ✔
2062
          timerWheel().schedule(node);
1 ✔
2063
        }
2064
        if (evicts()) {
1 ✔
2065
          var keyRef = node.getKeyReferenceOrNull();
1 ✔
2066
          if ((keyRef != null) && node.isAlive()) {
1 ✔
2067
            frequencySketch().increment(keyRef);
1 ✔
2068
          }
2069
          if (weight > windowMaximum()) {
1 ✔
2070
            accessOrderWindowDeque().offerFirst(node);
1 ✔
2071
          } else {
2072
            accessOrderWindowDeque().offerLast(node);
1 ✔
2073
          }
2074
          if (weight > maximum()) {
1 ✔
2075
            evictEntry(node, RemovalCause.SIZE, expirationTicker().read());
1 ✔
2076
          }
2077
        } else if (expiresAfterAccess()) {
1 ✔
2078
          accessOrderWindowDeque().offerLast(node);
1 ✔
2079
        }
2080
      }
2081
    }
1 ✔
2082
  }
2083

2084
  /** Removes a node from the page replacement policy. */
2085
  final class RemovalTask implements Runnable {
2086
    final Node<K, V> node;
2087

2088
    RemovalTask(Node<K, V> node) {
1 ✔
2089
      this.node = node;
1 ✔
2090
    }
1 ✔
2091

2092
    @Override
2093
    @GuardedBy("evictionLock")
2094
    public void run() {
2095
      // add may not have been processed yet
2096
      unlink(node);
1 ✔
2097
      makeDead(node);
1 ✔
2098
    }
1 ✔
2099
  }
2100

2101
  /** Updates the weighted size. */
2102
  final class UpdateTask implements Runnable {
2103
    final int weightDifference;
2104
    final Node<K, V> node;
2105
    final Access access;
2106

2107
    public UpdateTask(Node<K, V> node, int weightDifference) {
2108
      this(node, weightDifference, Access.HIT);
1 ✔
2109
    }
1 ✔
2110

2111
    public UpdateTask(Node<K, V> node, int weightDifference, Access access) {
1 ✔
2112
      this.weightDifference = weightDifference;
1 ✔
2113
      this.access = access;
1 ✔
2114
      this.node = node;
1 ✔
2115
    }
1 ✔
2116

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

2155
        setWeightedSize(weightedSize() + weightDifference);
1 ✔
2156
        if (weightedSize() > MAXIMUM_CAPACITY) {
1 ✔
2157
          evictEntries(expirationTicker().read());
1 ✔
2158
        }
2159
      } else if (expiresAfterAccess()) {
1 ✔
2160
        onAccess(node, access);
1 ✔
2161
      }
2162
    }
1 ✔
2163
  }
2164

2165
  /** What an access records beyond the reorder. */
2166
  enum Access {
1 ✔
2167
    HIT(/* usage= */ true, /* hit= */ true, /* miss= */ false),
1 ✔
2168
    RELOAD(/* usage= */ true, /* hit= */ false, /* miss= */ true),
1 ✔
2169
    QUIET(/* usage= */ false, /* hit= */ false, /* miss= */ false);
1 ✔
2170

2171
    private final boolean hit;
2172
    private final boolean miss;
2173
    private final boolean usage;
2174

2175
    Access(boolean usage, boolean hit, boolean miss) {
1 ✔
2176
      this.usage = usage;
1 ✔
2177
      this.hit = hit;
1 ✔
2178
      this.miss = miss;
1 ✔
2179
    }
1 ✔
2180

2181
    /** Returns whether the admission filter observes the key. */
2182
    boolean recordsUsage() {
2183
      return usage;
1 ✔
2184
    }
2185

2186
    /** Returns whether the climber's sample earns a hit. */
2187
    boolean recordsHit() {
2188
      return hit;
1 ✔
2189
    }
2190

2191
    /** Returns whether the climber's sample takes the miss an insertion would have recorded. */
2192
    boolean recordsMiss() {
2193
      return miss;
1 ✔
2194
    }
2195
  }
2196

2197
  /* --------------- Concurrent Map Support --------------- */
2198

2199
  @Override
2200
  public boolean isEmpty() {
2201
    return data.isEmpty();
1 ✔
2202
  }
2203

2204
  @Override
2205
  public int size() {
2206
    return data.size();
1 ✔
2207
  }
2208

2209
  @Override
2210
  public long estimatedSize() {
2211
    return data.mappingCount();
1 ✔
2212
  }
2213

2214
  @Override
2215
  public void clear() {
2216
    // Discard all pending refreshes
2217
    var pending = refreshes;
1 ✔
2218
    if (pending != null) {
1 ✔
2219
      pending.clear();
1 ✔
2220
    }
2221

2222
    Deque<Node<K, V>> entries;
2223
    evictionLock.lock();
1 ✔
2224
    try {
2225
      // Discard all pending reads
2226
      readBuffer.drainTo(e -> {});
1 ✔
2227

2228
      // Apply the pending writes
2229
      for (var task = writeBuffer.relaxedPoll(); task != null; task = writeBuffer.relaxedPoll()) {
1 ✔
2230
        task.run();
1 ✔
2231
      }
2232

2233
      // Cancel the scheduled cleanup
2234
      Pacer pacer = pacer();
1 ✔
2235
      if (pacer != null) {
1 ✔
2236
        pacer.cancel();
1 ✔
2237
      }
2238

2239
      // Discard all entries, falling back to one-by-one to avoid excessive lock hold times
2240
      long now = expirationTicker().read();
1 ✔
2241
      int threshold = (WRITE_BUFFER_MAX / 2);
1 ✔
2242
      entries = new ArrayDeque<>(data.values());
1 ✔
2243
      while (writeBuffer.size() < threshold) {
1 ✔
2244
        var node = entries.pollFirst();
1 ✔
2245
        if (node == null) {
1 ✔
2246
          break;
1 ✔
2247
        }
2248
        removeNode(node, now);
1 ✔
2249
      }
1 ✔
2250

2251
      // A submitted task that ran while this thread waited for the lock deferred to it
2252
      if (drainStatusOpaque() >= PROCESSING_TO_IDLE) {
1 ✔
2253
        setDrainStatusRelease(REQUIRED);
1 ✔
2254
      }
2255
    } finally {
2256
      evictionLock.unlock();
1 ✔
2257
    }
2258

2259
    // Remove any stragglers if released early to more aggressively flush incoming writes
2260
    @Var boolean cleanUp = false;
1 ✔
2261
    for (var node : entries) {
1 ✔
2262
      var key = node.getKey();
1 ✔
2263
      if (key == null) {
1 ✔
2264
        cleanUp = true;
1 ✔
2265
      } else {
2266
        remove(key);
1 ✔
2267
      }
2268
    }
1 ✔
2269
    if (collectKeys() && cleanUp) {
1 ✔
2270
      cleanUp();
1 ✔
2271
    } else {
2272
      rescheduleCleanUpIfIncomplete();
1 ✔
2273
    }
2274
  }
1 ✔
2275

2276
  @GuardedBy("evictionLock")
2277
  @SuppressWarnings({"GuardedByChecker", "SynchronizationOnLocalVariableOrMethodParameter"})
2278
  void removeNode(Node<K, V> node, long now) {
2279
    K key = node.getKey();
1 ✔
2280
    var ctx = new EvictContext<V>();
1 ✔
2281
    var keyReference = node.getKeyReference();
1 ✔
2282

2283
    data.computeIfPresent(keyReference, (k, n) -> {
1 ✔
2284
      if (n != node) {
1 ✔
2285
        return n;
1 ✔
2286
      }
2287
      synchronized (node) {
1 ✔
2288
        ctx.value = node.getValue();
1 ✔
2289
        ctx.oldWeight = node.getWeight();
1 ✔
2290

2291
        if ((key == null) || (ctx.value == null)) {
1 ✔
2292
          ctx.cause = RemovalCause.COLLECTED;
1 ✔
2293
        } else if (hasExpired(node, now) && !isComputingAsync(ctx.value)) {
1 ✔
2294
          ctx.cause = RemovalCause.EXPIRED;
1 ✔
2295
        } else {
2296
          ctx.cause = RemovalCause.EXPLICIT;
1 ✔
2297
        }
2298

2299
        if (ctx.cause.wasEvicted()) {
1 ✔
2300
          notifyEviction(key, ctx.value, ctx.cause);
1 ✔
2301
        }
2302

2303
        discardRefresh(k);
1 ✔
2304
        node.retire();
1 ✔
2305
        return null;
1 ✔
2306
      }
2307
    });
2308

2309
    unlink(node);
1 ✔
2310

2311
    synchronized (node) {
1 ✔
2312
      logIfAlive(node);
1 ✔
2313
      makeDead(node);
1 ✔
2314
    }
1 ✔
2315

2316
    if (ctx.cause != null) {
1 ✔
2317
      if (ctx.cause.wasEvicted()) {
1 ✔
2318
        statsCounter().recordEviction(ctx.oldWeight, ctx.cause);
1 ✔
2319
      }
2320
      notifyRemoval(key, ctx.value, ctx.cause);
1 ✔
2321
    }
2322
  }
1 ✔
2323

2324
  @Override
2325
  public boolean containsKey(@Nullable Object key) {
2326
    requireNonNull(key);
1 ✔
2327

2328
    Node<K, V> node = data.get(nodeFactory.newLookupKey(key));
1 ✔
2329
    if (node == null) {
1 ✔
2330
      return false;
1 ✔
2331
    }
2332
    boolean expired = hasExpired(node, expirationTicker().read());
1 ✔
2333
    V value = node.getValue();
1 ✔
2334
    if ((value == null) || (expired && !isComputingAsync(value))) {
1 ✔
2335
      scheduleDrainBuffers();
1 ✔
2336
      return false;
1 ✔
2337
    }
2338
    return true;
1 ✔
2339
  }
2340

2341
  @Override
2342
  public boolean containsValue(@Nullable Object value) {
2343
    requireNonNull(value);
1 ✔
2344

2345
    long now = expirationTicker().read();
1 ✔
2346
    for (Node<K, V> node : data.values()) {
1 ✔
2347
      boolean expired = hasExpired(node, now);
1 ✔
2348
      V nodeValue = node.getValue();
1 ✔
2349
      if ((node.getKey() == null) || (nodeValue == null)
1 ✔
2350
          || (expired && !isComputingAsync(nodeValue))) {
1 ✔
2351
        scheduleDrainBuffers();
1 ✔
2352
      } else if (node.isAlive() && node.containsValue(value)) {
1 ✔
2353
        return true;
1 ✔
2354
      }
2355
    }
1 ✔
2356
    return false;
1 ✔
2357
  }
2358

2359
  @Override
2360
  public @Nullable V get(@Nullable Object key) {
2361
    requireNonNull(key);
1 ✔
2362
    return getIfPresent(key, /* recordStats= */ false);
1 ✔
2363
  }
2364

2365
  @Override
2366
  public @Nullable V getIfPresent(Object key, boolean recordStats) {
2367
    Node<K, V> node = data.get(nodeFactory.newLookupKey(key));
1 ✔
2368
    if (node == null) {
1 ✔
2369
      if (recordStats) {
1 ✔
2370
        statsCounter().recordMisses(1);
1 ✔
2371
      }
2372
      if (drainStatusOpaque() == REQUIRED) {
1 ✔
2373
        scheduleDrainBuffers();
1 ✔
2374
      }
2375
      return null;
1 ✔
2376
    }
2377

2378
    long now = expirationTicker().read();
1 ✔
2379
    boolean expired = hasExpired(node, now);
1 ✔
2380
    V value = node.getValue();
1 ✔
2381
    if ((value == null) || (expired && !isComputingAsync(value))) {
1 ✔
2382
      if (recordStats) {
1 ✔
2383
        statsCounter().recordMisses(1);
1 ✔
2384
      }
2385
      scheduleDrainBuffers();
1 ✔
2386
      return null;
1 ✔
2387
    }
2388

2389
    if (expiresAfterRead() && !isComputingAsync(value)) {
1 ✔
2390
      @SuppressWarnings("unchecked")
2391
      var castedKey = (K) key;
1 ✔
2392
      setAccessTime(node, now);
1 ✔
2393
      tryExpireAfterRead(node, castedKey, value, expiry(), now);
1 ✔
2394
    }
2395
    V refreshed = afterRead(node, now, recordStats);
1 ✔
2396
    return (refreshed == null) ? value : refreshed;
1 ✔
2397
  }
2398

2399
  @Override
2400
  public @Nullable V getIfPresentQuietly(Object key) {
2401
    Node<K, V> node = data.get(nodeFactory.newLookupKey(key));
1 ✔
2402
    if (node == null) {
1 ✔
2403
      return null;
1 ✔
2404
    }
2405
    boolean expired = hasExpired(node, expirationTicker().read());
1 ✔
2406
    V value = node.getValue();
1 ✔
2407
    if ((value == null) || (expired && !isComputingAsync(value))) {
1 ✔
2408
      return null;
1 ✔
2409
    }
2410
    return value;
1 ✔
2411
  }
2412

2413
  /**
2414
   * Returns the key associated with the mapping in this cache, or {@code null} if there is none.
2415
   *
2416
   * @param key the key whose canonical instance is to be returned
2417
   * @return the key used by the mapping, or {@code null} if this cache does not contain a mapping
2418
   *         for the key
2419
   * @throws NullPointerException if the specified key is null
2420
   */
2421
  public @Nullable K getKey(K key) {
2422
    Node<K, V> node = data.get(nodeFactory.newLookupKey(key));
1 ✔
2423
    if (node == null) {
1 ✔
2424
      if (drainStatusOpaque() == REQUIRED) {
1 ✔
2425
        scheduleDrainBuffers();
1 ✔
2426
      }
2427
      return null;
1 ✔
2428
    }
2429
    afterRead(node, /* now= */ 0L, /* recordHit= */ false);
1 ✔
2430
    return node.getKey();
1 ✔
2431
  }
2432

2433
  @Override
2434
  public int getAllPresent(Map<K, @Nullable V> result, boolean retainAbsent) {
2435
    @Var int absent = 0;
1 ✔
2436
    @Var boolean drain = false;
1 ✔
2437
    int uniqueKeys = result.size();
1 ✔
2438
    long now = expirationTicker().read();
1 ✔
2439
    for (var iter = result.entrySet().iterator(); iter.hasNext();) {
1 ✔
2440
      var entry = iter.next();
1 ✔
2441
      Node<K, V> node = data.get(nodeFactory.newLookupKey(entry.getKey()));
1 ✔
2442
      if (node != null) {
1 ✔
2443
        boolean expired = hasExpired(node, now);
1 ✔
2444
        V value = node.getValue();
1 ✔
2445
        if ((value != null) && !expired) {
1 ✔
2446
          setAccessTime(node, now);
1 ✔
2447
          tryExpireAfterRead(node, entry.getKey(), value, expiry(), now);
1 ✔
2448
          V refreshed = afterRead(node, now, /* recordHit= */ false);
1 ✔
2449
          entry.setValue((refreshed == null) ? value : refreshed);
1 ✔
2450
          continue;
1 ✔
2451
        }
2452
        drain = true;
1 ✔
2453
      }
2454
      absent++;
1 ✔
2455
      if (!retainAbsent) {
1 ✔
2456
        iter.remove();
1 ✔
2457
      }
2458
    }
1 ✔
2459
    if (drain) {
1 ✔
2460
      scheduleDrainBuffers();
1 ✔
2461
    }
2462
    statsCounter().recordHits(uniqueKeys - absent);
1 ✔
2463
    statsCounter().recordMisses(absent);
1 ✔
2464
    return absent;
1 ✔
2465
  }
2466

2467
  @Override
2468
  public void putAll(Map<? extends K, ? extends V> map) {
2469
    map.forEach(this::put);
1 ✔
2470
  }
1 ✔
2471

2472
  @Override
2473
  public @Nullable V put(K key, V value) {
2474
    return put(key, value, expiry(), /* onlyIfAbsent= */ false);
1 ✔
2475
  }
2476

2477
  @Override
2478
  public @Nullable V putIfAbsent(K key, V value) {
2479
    return put(key, value, expiry(), /* onlyIfAbsent= */ true);
1 ✔
2480
  }
2481

2482
  /**
2483
   * Adds a node to the policy and the data store. If an existing node is found, then its value is
2484
   * updated if allowed.
2485
   *
2486
   * @param key key with which the specified value is to be associated
2487
   * @param value value to be associated with the specified key
2488
   * @param expiry the calculator for the write expiration time
2489
   * @param onlyIfAbsent a write is performed only if the key is not already associated with a value
2490
   * @return the prior value in or null if no mapping was found
2491
   */
2492
  @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
2493
  @Nullable V put(K key, V value, Expiry<K, V> expiry, boolean onlyIfAbsent) {
2494
    requireNonNull(key);
1 ✔
2495
    requireNonNull(value);
1 ✔
2496

2497
    @Var int newWeight = -1;
1 ✔
2498
    @Var Object keyRef = null;
1 ✔
2499
    @Var Node<K, V> node = null;
1 ✔
2500
    Object lookupKey = nodeFactory.newLookupKey(key);
1 ✔
2501
    for (int attempts = 1; ; attempts++) {
1 ✔
2502
      @Var Node<K, V> prior = data.get(lookupKey);
1 ✔
2503
      if (prior == null) {
1 ✔
2504
        if (node == null) {
1 ✔
2505
          if (newWeight < 0) {
1 ✔
2506
            newWeight = weigher.weigh(key, value);
1 ✔
2507
          }
2508
          long now = expirationTicker().read();
1 ✔
2509
          keyRef = nodeFactory.newReferenceKey(key, keyReferenceQueue());
1 ✔
2510
          node = nodeFactory.newNode(keyRef, value, valueReferenceQueue(), newWeight, now);
1 ✔
2511
          long expirationTime = expirationTimeFor(value, now);
1 ✔
2512
          setVariableTime(node, expireAfterCreate(key, value, expiry, now));
1 ✔
2513
          setAccessTime(node, expirationTime);
1 ✔
2514
          setWriteTime(node, expirationTime);
1 ✔
2515
        }
2516
        var newNode = node;
1 ✔
2517
        var newKeyRef = requireNonNull(keyRef);
1 ✔
2518
        prior = (cacheLoader == null)
1 ✔
2519
            ? data.putIfAbsent(newKeyRef, newNode)
1 ✔
2520
            : data.computeIfAbsent(newKeyRef, k -> {
1 ✔
2521
                discardRefresh(k);
1 ✔
2522
                return newNode;
1 ✔
2523
              });
2524
        if ((prior == null) || (prior == node)) {
1 ✔
2525
          afterWrite(new AddTask(node, newWeight));
1 ✔
2526
          return null;
1 ✔
2527
        }
2528
      }
2529
      if (onlyIfAbsent) {
1 ✔
2530
        // An optimistic fast path to avoid unnecessary locking
2531
        long now = expirationTicker().read();
1 ✔
2532
        boolean expired = hasExpired(prior, now);
1 ✔
2533
        V currentValue = prior.getValue();
1 ✔
2534
        if ((currentValue != null) && (!expired || isComputingAsync(currentValue))) {
1 ✔
2535
          if (!isComputingAsync(currentValue)) {
1 ✔
2536
            tryExpireAfterRead(prior, key, currentValue, expiry, now);
1 ✔
2537
            setAccessTime(prior, now);
1 ✔
2538
          }
2539
          afterRead(prior, now, /* recordHit= */ false);
1 ✔
2540
          return currentValue;
1 ✔
2541
        }
2542
      }
2543

2544
      // A read may race with the entry's removal, so that after the entry is acquired it may no
2545
      // longer be usable. A retry will reread from the map and either find an absent mapping, a
2546
      // new entry, or a stale entry.
2547
      if (!prior.isAlive()) {
1 ✔
2548
        // A reread of the stale entry may occur if the state transition occurred but the map
2549
        // removal was delayed by a context switch, so that this thread spin waits until resolved.
2550
        if ((attempts & MAX_PUT_SPIN_WAIT_ATTEMPTS) != 0) {
1 ✔
2551
          Thread.onSpinWait();
1 ✔
2552
          continue;
1 ✔
2553
        }
2554

2555
        // If the spin wait attempts are exhausted then fallback to a map computation in order to
2556
        // deschedule this thread until the entry's removal completes. If the key was modified
2557
        // while in the map so that its equals or hashCode changed then the contents may be
2558
        // corrupted, where the cache holds an evicted (dead) entry that could not be removed.
2559
        // That is a violation of the Map contract, so we check that the mapping is in the "alive"
2560
        // state while in the computation.
2561
        data.computeIfPresent(lookupKey, (k, n) -> {
1 ✔
2562
          requireIsAlive(key, n);
1 ✔
2563
          return n;
1 ✔
2564
        });
2565
        continue;
1 ✔
2566
      }
2567

2568
      long now;
2569
      V oldValue;
2570
      long varTime;
2571
      int oldWeight;
2572
      @Var boolean expired = false;
1 ✔
2573
      @Var boolean mayUpdate = true;
1 ✔
2574
      @Var boolean exceedsTolerance = false;
1 ✔
2575
      if (newWeight < 0) {
1 ✔
2576
        newWeight = weigher.weigh(key, value);
1 ✔
2577
      }
2578
      synchronized (prior) {
1 ✔
2579
        if (!prior.isAlive()) {
1 ✔
2580
          continue;
1 ✔
2581
        }
2582
        oldValue = prior.getValue();
1 ✔
2583
        oldWeight = prior.getWeight();
1 ✔
2584
        now = expirationTicker().read();
1 ✔
2585
        if (oldValue == null) {
1 ✔
2586
          varTime = expireAfterCreate(key, value, expiry, now);
1 ✔
2587
          notifyEviction(key, null, RemovalCause.COLLECTED);
1 ✔
2588
        } else if (hasExpired(prior, now) && !isComputingAsync(oldValue)) {
1 ✔
2589
          expired = true;
1 ✔
2590
          varTime = expireAfterCreate(key, value, expiry, now);
1 ✔
2591
          notifyEviction(key, oldValue, RemovalCause.EXPIRED);
1 ✔
2592
        } else if (onlyIfAbsent) {
1 ✔
2593
          mayUpdate = false;
1 ✔
2594
          varTime = expireAfterRead(prior, key, oldValue, expiry, now);
1 ✔
2595
        } else {
2596
          varTime = expireAfterUpdate(prior, key, value, expiry, now);
1 ✔
2597
        }
2598

2599
        long expirationTime = expirationTimeFor(mayUpdate ? value : oldValue, now);
1 ✔
2600
        if (mayUpdate) {
1 ✔
2601
          exceedsTolerance = exceedsWriteTimeTolerance(prior, varTime, expirationTime);
1 ✔
2602
          prior.setValue(value, valueReferenceQueue());
1 ✔
2603
          prior.setWeight(newWeight);
1 ✔
2604
          if (expired || (oldValue == null) || exceedsTolerance) {
1 ✔
2605
            setWriteTime(prior, expirationTime);
1 ✔
2606
          }
2607

2608
          discardRefresh(lookupKey);
1 ✔
2609
        }
2610

2611
        setVariableTime(prior, varTime);
1 ✔
2612
        setAccessTime(prior, expirationTime);
1 ✔
2613
      }
1 ✔
2614

2615
      if (expired) {
1 ✔
2616
        statsCounter().recordEviction(oldWeight, RemovalCause.EXPIRED);
1 ✔
2617
        notifyRemoval(key, oldValue, RemovalCause.EXPIRED);
1 ✔
2618
      } else if (oldValue == null) {
1 ✔
2619
        statsCounter().recordEviction(oldWeight, RemovalCause.COLLECTED);
1 ✔
2620
        notifyRemoval(key, /* value= */ null, RemovalCause.COLLECTED);
1 ✔
2621
      } else if (mayUpdate) {
1 ✔
2622
        notifyOnReplace(key, oldValue, value);
1 ✔
2623
      }
2624

2625
      int weightedDifference = mayUpdate ? (newWeight - oldWeight) : 0;
1 ✔
2626
      if ((oldValue == null) || (weightedDifference != 0) || expired) {
1 ✔
2627
        var access = (expired || (oldValue == null)) ? Access.RELOAD : Access.HIT;
1 ✔
2628
        afterWrite(new UpdateTask(prior, weightedDifference, access));
1 ✔
2629
      } else if (!onlyIfAbsent && exceedsTolerance) {
1 ✔
2630
        afterWrite(new UpdateTask(prior, weightedDifference));
1 ✔
2631
      } else {
2632
        afterRead(prior, now, /* recordHit= */ false);
1 ✔
2633
      }
2634

2635
      return expired ? null : oldValue;
1 ✔
2636
    }
2637
  }
2638

2639
  @Override
2640
  @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
2641
  public @Nullable V remove(@Nullable Object key) {
2642
    requireNonNull(key);
1 ✔
2643

2644
    var ctx = new RemoveContext<K, V>();
1 ✔
2645
    Object lookupKey = nodeFactory.newLookupKey(key);
1 ✔
2646
    data.compute(lookupKey, (k, n) -> {
1 ✔
2647
      if (n == null) {
1 ✔
2648
        discardRefresh(k);
1 ✔
2649
        return null;
1 ✔
2650
      }
2651
      synchronized (n) {
1 ✔
2652
        requireIsAlive(key, n);
1 ✔
2653
        ctx.oldKey = n.getKey();
1 ✔
2654
        ctx.oldValue = n.getValue();
1 ✔
2655
        ctx.oldWeight = n.getWeight();
1 ✔
2656
        RemovalCause actualCause;
2657
        if ((ctx.oldKey == null) || (ctx.oldValue == null)) {
1 ✔
2658
          actualCause = RemovalCause.COLLECTED;
1 ✔
2659
        } else if (hasExpired(n, expirationTicker().read()) && !isComputingAsync(ctx.oldValue)) {
1 ✔
2660
          actualCause = RemovalCause.EXPIRED;
1 ✔
2661
        } else {
2662
          actualCause = RemovalCause.EXPLICIT;
1 ✔
2663
        }
2664
        if (actualCause.wasEvicted()) {
1 ✔
2665
          notifyEviction(ctx.oldKey, ctx.oldValue, actualCause);
1 ✔
2666
        }
2667
        ctx.cause = actualCause;
1 ✔
2668
        discardRefresh(k);
1 ✔
2669
        ctx.node = n;
1 ✔
2670
        n.retire();
1 ✔
2671
        return null;
1 ✔
2672
      }
2673
    });
2674

2675
    if (ctx.cause != null) {
1 ✔
2676
      afterWrite(new RemovalTask(requireNonNull(ctx.node)));
1 ✔
2677
      if (ctx.cause.wasEvicted()) {
1 ✔
2678
        statsCounter().recordEviction(ctx.oldWeight, ctx.cause);
1 ✔
2679
      }
2680
      notifyRemoval(ctx.oldKey, ctx.oldValue, ctx.cause);
1 ✔
2681
    }
2682
    return (ctx.cause == RemovalCause.EXPLICIT) ? ctx.oldValue : null;
1 ✔
2683
  }
2684

2685
  @Override
2686
  @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
2687
  public boolean remove(@Nullable Object key, @Nullable Object value) {
2688
    requireNonNull(key);
1 ✔
2689
    if (value == null) {
1 ✔
2690
      return false;
1 ✔
2691
    }
2692

2693
    var ctx = new RemoveContext<K, V>();
1 ✔
2694
    Object lookupKey = nodeFactory.newLookupKey(key);
1 ✔
2695
    data.computeIfPresent(lookupKey, (kR, node) -> {
1 ✔
2696
      synchronized (node) {
1 ✔
2697
        requireIsAlive(key, node);
1 ✔
2698
        ctx.oldKey = node.getKey();
1 ✔
2699
        ctx.oldValue = node.getValue();
1 ✔
2700
        ctx.oldWeight = node.getWeight();
1 ✔
2701
        if ((ctx.oldKey == null) || (ctx.oldValue == null)) {
1 ✔
2702
          ctx.cause = RemovalCause.COLLECTED;
1 ✔
2703
        } else if (hasExpired(node, expirationTicker().read()) && !isComputingAsync(ctx.oldValue)) {
1 ✔
2704
          ctx.cause = RemovalCause.EXPIRED;
1 ✔
2705
        } else if (node.containsValue(value)) {
1 ✔
2706
          ctx.cause = RemovalCause.EXPLICIT;
1 ✔
2707
        } else {
2708
          return node;
1 ✔
2709
        }
2710
        if (ctx.cause.wasEvicted()) {
1 ✔
2711
          notifyEviction(ctx.oldKey, ctx.oldValue, ctx.cause);
1 ✔
2712
        }
2713
        discardRefresh(kR);
1 ✔
2714
        ctx.node = node;
1 ✔
2715
        node.retire();
1 ✔
2716
        return null;
1 ✔
2717
      }
2718
    });
2719

2720
    if (ctx.node == null) {
1 ✔
2721
      return false;
1 ✔
2722
    }
2723
    var removeCause = requireNonNull(ctx.cause);
1 ✔
2724
    afterWrite(new RemovalTask(ctx.node));
1 ✔
2725
    if (removeCause.wasEvicted()) {
1 ✔
2726
      statsCounter().recordEviction(ctx.oldWeight, removeCause);
1 ✔
2727
    }
2728
    notifyRemoval(ctx.oldKey, ctx.oldValue, removeCause);
1 ✔
2729

2730
    return (removeCause == RemovalCause.EXPLICIT);
1 ✔
2731
  }
2732

2733
  @Override
2734
  @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
2735
  public @Nullable V replace(K key, V value) {
2736
    requireNonNull(key);
1 ✔
2737
    requireNonNull(value);
1 ✔
2738
    var ctx = new ReplaceContext<K, V>();
1 ✔
2739
    int weight = weigher.weigh(key, value);
1 ✔
2740
    Node<K, V> node = data.computeIfPresent(nodeFactory.newLookupKey(key), (k, n) -> {
1 ✔
2741
      synchronized (n) {
1 ✔
2742
        requireIsAlive(key, n);
1 ✔
2743
        ctx.nodeKey = n.getKey();
1 ✔
2744
        ctx.oldValue = n.getValue();
1 ✔
2745
        ctx.oldWeight = n.getWeight();
1 ✔
2746
        if ((ctx.nodeKey == null) || (ctx.oldValue == null)
1 ✔
2747
            || (hasExpired(n, ctx.now = expirationTicker().read())
1 ✔
2748
                && !isComputingAsync(ctx.oldValue))) {
1 ✔
2749
          ctx.oldValue = null;
1 ✔
2750
          ctx.garbage = true;
1 ✔
2751
          return n;
1 ✔
2752
        }
2753

2754
        long varTime = expireAfterUpdate(n, key, value, expiry(), ctx.now);
1 ✔
2755
        n.setValue(value, valueReferenceQueue());
1 ✔
2756
        n.setWeight(weight);
1 ✔
2757

2758
        long expirationTime = expirationTimeFor(value, ctx.now);
1 ✔
2759
        ctx.exceedsTolerance = exceedsWriteTimeTolerance(n, varTime, expirationTime);
1 ✔
2760
        if (ctx.exceedsTolerance) {
1 ✔
2761
          setWriteTime(n, expirationTime);
1 ✔
2762
        }
2763
        setAccessTime(n, expirationTime);
1 ✔
2764
        setVariableTime(n, varTime);
1 ✔
2765
        discardRefresh(k);
1 ✔
2766
        return n;
1 ✔
2767
      }
2768
    });
2769

2770
    if ((node == null) || (ctx.nodeKey == null) || (ctx.oldValue == null)) {
1 ✔
2771
      if (ctx.garbage) {
1 ✔
2772
        scheduleDrainBuffers();
1 ✔
2773
      }
2774
      return null;
1 ✔
2775
    }
2776

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

2784
    notifyOnReplace(ctx.nodeKey, ctx.oldValue, value);
1 ✔
2785
    return ctx.oldValue;
1 ✔
2786
  }
2787

2788
  @Override
2789
  @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
2790
  public boolean replace(K key, V oldValue, V newValue,
2791
      boolean shouldDiscardRefresh, boolean quietly) {
2792
    requireNonNull(key);
1 ✔
2793
    requireNonNull(oldValue);
1 ✔
2794
    requireNonNull(newValue);
1 ✔
2795
    var ctx = new ReplaceContext<K, V>();
1 ✔
2796
    int weight = weigher.weigh(key, newValue);
1 ✔
2797
    Node<K, V> node = data.computeIfPresent(nodeFactory.newLookupKey(key), (k, n) -> {
1 ✔
2798
      synchronized (n) {
1 ✔
2799
        requireIsAlive(key, n);
1 ✔
2800
        ctx.nodeKey = n.getKey();
1 ✔
2801
        ctx.oldValue = n.getValue();
1 ✔
2802
        ctx.oldWeight = n.getWeight();
1 ✔
2803
        if ((ctx.nodeKey == null) || (ctx.oldValue == null)
1 ✔
2804
            || (hasExpired(n, ctx.now = expirationTicker().read())
1 ✔
2805
                && !isComputingAsync(ctx.oldValue))) {
1 ✔
2806
          ctx.oldValue = null;
1 ✔
2807
          ctx.garbage = true;
1 ✔
2808
          return n;
1 ✔
2809
        } else if (!n.containsValue(oldValue)) {
1 ✔
2810
          ctx.oldValue = null;
1 ✔
2811
          return n;
1 ✔
2812
        }
2813

2814
        // A completion finalizes only what the insertion deferred. Its caller reads the future's
2815
        // readiness before the write, so a value that becomes available in between is evaluated by
2816
        // the insertion, and evaluating it again would charge that creation as an update.
2817
        long varTime = (quietly && !isExpirationDeferred(n, ctx.now))
1 ✔
2818
            ? n.getVariableTime()
1 ✔
2819
            : expireAfterUpdate(n, key, newValue, expiry(), ctx.now);
1 ✔
2820
        n.setValue(newValue, valueReferenceQueue());
1 ✔
2821
        n.setWeight(weight);
1 ✔
2822

2823
        long expirationTime = expirationTimeFor(newValue, ctx.now);
1 ✔
2824
        ctx.exceedsTolerance = exceedsWriteTimeTolerance(n, varTime, expirationTime);
1 ✔
2825
        if (ctx.exceedsTolerance) {
1 ✔
2826
          setWriteTime(n, expirationTime);
1 ✔
2827
        }
2828
        setAccessTime(n, expirationTime);
1 ✔
2829
        setVariableTime(n, varTime);
1 ✔
2830

2831
        if (shouldDiscardRefresh) {
1 ✔
2832
          discardRefresh(k);
1 ✔
2833
        }
2834
      }
1 ✔
2835
      return n;
1 ✔
2836
    });
2837

2838
    if ((node == null) || (ctx.nodeKey == null) || (ctx.oldValue == null)) {
1 ✔
2839
      if (ctx.garbage) {
1 ✔
2840
        scheduleDrainBuffers();
1 ✔
2841
      }
2842
      return false;
1 ✔
2843
    }
2844

2845
    int weightedDifference = (weight - ctx.oldWeight);
1 ✔
2846
    if (ctx.exceedsTolerance || (weightedDifference != 0)) {
1 ✔
2847
      afterWrite(new UpdateTask(node, weightedDifference, quietly ? Access.QUIET : Access.HIT));
1 ✔
2848
    } else if (!quietly) {
1 ✔
2849
      afterRead(node, ctx.now, /* recordHit= */ false);
1 ✔
2850
    }
2851

2852
    notifyOnReplace(ctx.nodeKey, ctx.oldValue, newValue);
1 ✔
2853
    return true;
1 ✔
2854
  }
2855

2856
  @Override
2857
  public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
2858
    requireNonNull(function);
1 ✔
2859

2860
    BiFunction<K, @Nullable V, V> remappingFunction = (key, oldValue) ->
1 ✔
2861
        requireNonNull(function.apply(key, requireNonNull(oldValue)));
1 ✔
2862
    for (K key : keySet()) {
1 ✔
2863
      Object lookupKey = nodeFactory.newLookupKey(key);
1 ✔
2864
      remap(key, lookupKey, remappingFunction, expiry(),
1 ✔
2865
          new ComputeContext<>(), /* computeIfAbsent= */ false);
2866
    }
1 ✔
2867
  }
1 ✔
2868

2869
  @Override
2870
  public @Nullable V computeIfAbsent(K key,
2871
      @Var Function<? super K, ? extends @Nullable V> mappingFunction,
2872
      boolean recordStats, boolean recordLoad) {
2873
    requireNonNull(key);
1 ✔
2874
    requireNonNull(mappingFunction);
1 ✔
2875

2876
    // An optimistic fast path to avoid unnecessary locking
2877
    Node<K, V> node = data.get(nodeFactory.newLookupKey(key));
1 ✔
2878
    if (node != null) {
1 ✔
2879
      long now = expirationTicker().read();
1 ✔
2880
      boolean expired = hasExpired(node, now);
1 ✔
2881
      V value = node.getValue();
1 ✔
2882
      if ((value != null) && (!expired || isComputingAsync(value))) {
1 ✔
2883
        if (expiresAfterRead() && !isComputingAsync(value)) {
1 ✔
2884
          tryExpireAfterRead(node, key, value, expiry(), now);
1 ✔
2885
          setAccessTime(node, now);
1 ✔
2886
        }
2887
        var refreshed = afterRead(node, now, /* recordHit= */ recordStats);
1 ✔
2888
        return (refreshed == null) ? value : refreshed;
1 ✔
2889
      }
2890
    }
2891
    if (recordStats) {
1 ✔
2892
      mappingFunction = statsAware(mappingFunction, recordLoad);
1 ✔
2893
    }
2894
    Object keyRef = nodeFactory.newReferenceKey(key, keyReferenceQueue());
1 ✔
2895
    return doComputeIfAbsent(key, keyRef, mappingFunction, new ComputeContext<>(), recordStats);
1 ✔
2896
  }
2897

2898
  /** Returns the current value from a computeIfAbsent invocation. */
2899
  @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
2900
  @Nullable V doComputeIfAbsent(K key, Object keyRef,
2901
      Function<? super K, ? extends @Nullable V> mappingFunction,
2902
      ComputeContext<K, V> ctx, boolean recordStats) {
2903
    Node<K, V> node = data.compute(keyRef, (k, n) -> {
1 ✔
2904
      if (n == null) {
1 ✔
2905
        ctx.newValue = mappingFunction.apply(key);
1 ✔
2906
        if (ctx.newValue == null) {
1 ✔
2907
          discardRefresh(k);
1 ✔
2908
          return null;
1 ✔
2909
        }
2910
        ctx.now = expirationTicker().read();
1 ✔
2911
        ctx.newWeight = weigher.weigh(key, ctx.newValue);
1 ✔
2912
        var created = nodeFactory.newNode(k, ctx.newValue,
1 ✔
2913
            valueReferenceQueue(), ctx.newWeight, ctx.now);
1 ✔
2914
        long expirationTime = expirationTimeFor(ctx.newValue, ctx.now);
1 ✔
2915
        setVariableTime(created, expireAfterCreate(key, ctx.newValue, expiry(), ctx.now));
1 ✔
2916
        setAccessTime(created, expirationTime);
1 ✔
2917
        setWriteTime(created, expirationTime);
1 ✔
2918
        discardRefresh(k);
1 ✔
2919
        return created;
1 ✔
2920
      }
2921

2922
      synchronized (n) {
1 ✔
2923
        requireIsAlive(key, n);
1 ✔
2924
        ctx.nodeKey = n.getKey();
1 ✔
2925
        ctx.oldValue = n.getValue();
1 ✔
2926
        ctx.oldWeight = n.getWeight();
1 ✔
2927
        RemovalCause actualCause;
2928
        if ((ctx.nodeKey == null) || (ctx.oldValue == null)) {
1 ✔
2929
          actualCause = RemovalCause.COLLECTED;
1 ✔
2930
        } else if (hasExpired(n, ctx.now = expirationTicker().read())
1 ✔
2931
            && !isComputingAsync(ctx.oldValue)) {
1 ✔
2932
          actualCause = RemovalCause.EXPIRED;
1 ✔
2933
        } else {
2934
          return n;
1 ✔
2935
        }
2936

2937
        ctx.cause = actualCause;
1 ✔
2938
        notifyEviction(ctx.nodeKey, ctx.oldValue, actualCause);
1 ✔
2939

2940
        try {
2941
          ctx.newValue = mappingFunction.apply(key);
1 ✔
2942
          if (ctx.newValue == null) {
1 ✔
2943
            discardRefresh(k);
1 ✔
2944
            ctx.removed = n;
1 ✔
2945
            n.retire();
1 ✔
2946
            return null;
1 ✔
2947
          }
2948
          ctx.now = expirationTicker().read();
1 ✔
2949
          ctx.newWeight = weigher.weigh(key, ctx.newValue);
1 ✔
2950
          long varTime = expireAfterCreate(key, ctx.newValue, expiry(), ctx.now);
1 ✔
2951

2952
          n.setValue(ctx.newValue, valueReferenceQueue());
1 ✔
2953
          n.setWeight(ctx.newWeight);
1 ✔
2954

2955
          long expirationTime = expirationTimeFor(ctx.newValue, ctx.now);
1 ✔
2956
          setAccessTime(n, expirationTime);
1 ✔
2957
          setWriteTime(n, expirationTime);
1 ✔
2958
          setVariableTime(n, varTime);
1 ✔
2959
          discardRefresh(k);
1 ✔
2960
          return n;
1 ✔
2961
        } catch (Throwable e) {
1 ✔
2962
          ctx.newValue = null;
1 ✔
2963
          discardRefresh(k);
1 ✔
2964
          ctx.exception = e;
1 ✔
2965
          ctx.removed = n;
1 ✔
2966
          n.retire();
1 ✔
2967
          return null;
1 ✔
2968
        }
2969
      }
2970
    });
2971

2972
    if (ctx.cause != null) {
1 ✔
2973
      statsCounter().recordEviction(ctx.oldWeight, ctx.cause);
1 ✔
2974
      notifyRemoval(ctx.nodeKey, ctx.oldValue, ctx.cause);
1 ✔
2975
    }
2976
    if (node == null) {
1 ✔
2977
      if (ctx.removed != null) {
1 ✔
2978
        afterWrite(new RemovalTask(ctx.removed));
1 ✔
2979
      }
2980
      if (ctx.exception != null) {
1 ✔
2981
        throw toUnchecked(ctx.exception);
1 ✔
2982
      }
2983
      return null;
1 ✔
2984
    }
2985
    if ((ctx.oldValue != null) && (ctx.newValue == null)) {
1 ✔
2986
      if (!isComputingAsync(ctx.oldValue)) {
1 ✔
2987
        tryExpireAfterRead(node, key, ctx.oldValue, expiry(), ctx.now);
1 ✔
2988
        setAccessTime(node, ctx.now);
1 ✔
2989
      }
2990

2991
      var refreshed = afterRead(node, ctx.now, /* recordHit= */ recordStats);
1 ✔
2992
      return (refreshed == null) ? ctx.oldValue : refreshed;
1 ✔
2993
    }
2994
    if ((ctx.oldValue == null) && (ctx.cause == null)) {
1 ✔
2995
      afterWrite(new AddTask(node, ctx.newWeight));
1 ✔
2996
    } else {
2997
      int weightedDifference = (ctx.newWeight - ctx.oldWeight);
1 ✔
2998
      afterWrite(new UpdateTask(node, weightedDifference, Access.RELOAD));
1 ✔
2999
    }
3000

3001
    return ctx.newValue;
1 ✔
3002
  }
3003

3004
  @Override
3005
  public @Nullable V computeIfPresent(K key,
3006
      BiFunction<? super K, ? super V, ? extends @Nullable V> remappingFunction) {
3007
    requireNonNull(key);
1 ✔
3008
    requireNonNull(remappingFunction);
1 ✔
3009

3010
    // An optimistic fast path to avoid unnecessary locking
3011
    Object lookupKey = nodeFactory.newLookupKey(key);
1 ✔
3012
    var node = data.get(lookupKey);
1 ✔
3013
    if (node == null) {
1 ✔
3014
      return null;
1 ✔
3015
    }
3016
    boolean expired = hasExpired(node, expirationTicker().read());
1 ✔
3017
    V value = node.getValue();
1 ✔
3018
    if ((value == null) || expired) {
1 ✔
3019
      scheduleDrainBuffers();
1 ✔
3020
      return null;
1 ✔
3021
    }
3022

3023
    @SuppressWarnings("NullAway")
3024
    BiFunction<? super K, ? super @Nullable V, ? extends @Nullable V> statsAwareRemappingFunction =
1 ✔
3025
        statsAware(remappingFunction, /* recordLoad= */ true);
1 ✔
3026
    return remap(key, lookupKey, statsAwareRemappingFunction,
1 ✔
3027
        expiry(), new ComputeContext<>(), /* computeIfAbsent= */ false);
1 ✔
3028
  }
3029

3030
  @Override
3031
  public @Nullable V compute(K key,
3032
      BiFunction<? super K, ? super @Nullable V, ? extends @Nullable V> remappingFunction,
3033
      @Nullable Expiry<? super K, ? super V> expiry, boolean recordLoad,
3034
      @Nullable RemapHints hints) {
3035
    requireNonNull(key);
1 ✔
3036
    requireNonNull(remappingFunction);
1 ✔
3037

3038
    Object keyRef = nodeFactory.newReferenceKey(key, keyReferenceQueue());
1 ✔
3039
    var ctx = new ComputeContext<K, V>();
1 ✔
3040
    ctx.hints = hints;
1 ✔
3041
    return remap(key, keyRef, statsAware(remappingFunction, recordLoad),
1 ✔
3042
        expiry, ctx, /* computeIfAbsent= */ true);
3043
  }
3044

3045
  @Override
3046
  public @Nullable V merge(K key, V value,
3047
      BiFunction<? super V, ? super V, ? extends @Nullable V> remappingFunction) {
3048
    requireNonNull(key);
1 ✔
3049
    requireNonNull(value);
1 ✔
3050
    requireNonNull(remappingFunction);
1 ✔
3051

3052
    Object keyRef = nodeFactory.newReferenceKey(key, keyReferenceQueue());
1 ✔
3053
    BiFunction<? super V, ? super V, ? extends @Nullable V> f = statsAware(remappingFunction);
1 ✔
3054
    return remap(key, keyRef,
1 ✔
3055
        (k, oldValue) -> (oldValue == null) ? value : f.apply(oldValue, value), expiry(),
1 ✔
3056
        new ComputeContext<>(), /* computeIfAbsent= */ true);
3057
  }
3058

3059
  /**
3060
   * Attempts to compute a mapping for the specified key and its current mapped value (or
3061
   * {@code null} if there is no current mapping).
3062
   * <p>
3063
   * An entry that has expired or been reference collected is evicted and the computation continues
3064
   * as if the entry had not been present. This method does not pre-screen and does not wrap the
3065
   * remappingFunction to be statistics aware.
3066
   *
3067
   * @param key key with which the specified value is to be associated
3068
   * @param keyRef the key to associate with or a lookup only key if not {@code computeIfAbsent}
3069
   * @param remappingFunction the function to compute a value
3070
   * @param expiry the calculator for the expiration time
3071
   * @param ctx the mutable context for passing state to and from the compute lambda
3072
   * @param computeIfAbsent if an absent entry can be computed
3073
   * @return the new value associated with the specified key, or null if none
3074
   */
3075
  @SuppressWarnings({"StatementWithEmptyBody", "SynchronizationOnLocalVariableOrMethodParameter"})
3076
  @Nullable V remap(K key, Object keyRef,
3077
      BiFunction<? super K, ? super @Nullable V, ? extends @Nullable V> remappingFunction,
3078
      @Nullable Expiry<? super K, ? super V> expiry,
3079
      ComputeContext<K, V> ctx, boolean computeIfAbsent) {
3080
    Node<K, V> node = data.compute(keyRef, (kr, n) -> {
1 ✔
3081
      if (n == null) {
1 ✔
3082
        if (!computeIfAbsent) {
1 ✔
3083
          return null;
1 ✔
3084
        }
3085
        ctx.newValue = remappingFunction.apply(key, null);
1 ✔
3086
        if (ctx.newValue == null) {
1 ✔
3087
          // A stale refresh whose entry is absent leaves a successor's registration intact
3088
          if ((ctx.hints == null) || !ctx.hints.preserveRefresh) {
1 ✔
3089
            discardRefresh(kr);
1 ✔
3090
          }
3091
          return null;
1 ✔
3092
        }
3093
        try {
3094
          ctx.now = expirationTicker().read();
1 ✔
3095
          ctx.newWeight = weigher.weigh(key, ctx.newValue);
1 ✔
3096
          long varTime = expireAfterCreate(key, ctx.newValue, expiry, ctx.now);
1 ✔
3097
          var created = nodeFactory.newNode(keyRef, ctx.newValue,
1 ✔
3098
              valueReferenceQueue(), ctx.newWeight, ctx.now);
1 ✔
3099

3100
          long expirationTime = expirationTimeFor(ctx.newValue, ctx.now);
1 ✔
3101
          setAccessTime(created, expirationTime);
1 ✔
3102
          setWriteTime(created, expirationTime);
1 ✔
3103
          setVariableTime(created, varTime);
1 ✔
3104
          return created;
1 ✔
3105
        } finally {
3106
          discardRefresh(kr);
1 ✔
3107
        }
3108
      }
3109

3110
      synchronized (n) {
1 ✔
3111
        requireIsAlive(key, n);
1 ✔
3112
        ctx.nodeKey = n.getKey();
1 ✔
3113
        ctx.oldValue = n.getValue();
1 ✔
3114
        ctx.oldWeight = n.getWeight();
1 ✔
3115
        if ((ctx.nodeKey == null) || (ctx.oldValue == null)) {
1 ✔
3116
          ctx.cause = RemovalCause.COLLECTED;
1 ✔
3117
        } else if (hasExpired(n, expirationTicker().read()) && !isComputingAsync(ctx.oldValue)) {
1 ✔
3118
          ctx.cause = RemovalCause.EXPIRED;
1 ✔
3119
        }
3120
        if (ctx.cause != null) {
1 ✔
3121
          notifyEviction(ctx.nodeKey, ctx.oldValue, ctx.cause);
1 ✔
3122
          if (!computeIfAbsent) {
1 ✔
3123
            discardRefresh(kr);
1 ✔
3124
            ctx.removed = n;
1 ✔
3125
            n.retire();
1 ✔
3126
            return null;
1 ✔
3127
          }
3128
        }
3129

3130
        boolean wasEvicted = (ctx.cause != null);
1 ✔
3131
        try {
3132
          ctx.newValue = remappingFunction.apply(key,
1 ✔
3133
              (ctx.cause == null) ? ctx.oldValue : null);
1 ✔
3134

3135
          if (ctx.newValue == null) {
1 ✔
3136
            if (ctx.cause == null) {
1 ✔
3137
              ctx.cause = RemovalCause.EXPLICIT;
1 ✔
3138
            }
3139
            // A stale refresh whose entry was evicted leaves a successor's registration intact
3140
            if ((ctx.hints == null) || !ctx.hints.preserveRefresh) {
1 ✔
3141
              discardRefresh(kr);
1 ✔
3142
            }
3143
            ctx.removed = n;
1 ✔
3144
            n.retire();
1 ✔
3145
            return null;
1 ✔
3146
          }
3147

3148
          // If the caller flagged a same-instance return as a no-op (e.g., a refresh was rejected
3149
          // and should not touch the entry), skip the metadata updates below.
3150
          if ((ctx.hints != null) && ctx.hints.preserveTimestamps
1 ✔
3151
              && (ctx.newValue == ctx.oldValue) && (ctx.cause == null)) {
3152
            // Skip for query-style callers whose no-op path must leave any in-flight refresh intact
3153
            if (!ctx.hints.preserveRefresh) {
1 ✔
3154
              discardRefresh(kr);
1 ✔
3155
            }
3156
            ctx.unmodified = true;
1 ✔
3157
            return n;
1 ✔
3158
          }
3159

3160
          long varTime;
3161
          ctx.newWeight = weigher.weigh(key, ctx.newValue);
1 ✔
3162
          ctx.now = expirationTicker().read();
1 ✔
3163
          if (ctx.cause == null) {
1 ✔
3164
            if (ctx.newValue != ctx.oldValue) {
1 ✔
3165
              ctx.cause = RemovalCause.REPLACED;
1 ✔
3166
            }
3167
            varTime = expireAfterUpdate(n, key, ctx.newValue, expiry, ctx.now);
1 ✔
3168
          } else {
3169
            varTime = expireAfterCreate(key, ctx.newValue, expiry, ctx.now);
1 ✔
3170
          }
3171

3172
          if (ctx.newValue != ctx.oldValue) {
1 ✔
3173
            n.setValue(ctx.newValue, valueReferenceQueue());
1 ✔
3174
          }
3175
          n.setWeight(ctx.newWeight);
1 ✔
3176

3177
          long expirationTime = expirationTimeFor(ctx.newValue, ctx.now);
1 ✔
3178
          ctx.exceedsTolerance = exceedsWriteTimeTolerance(n, varTime, expirationTime);
1 ✔
3179
          if (((ctx.cause != null) && ctx.cause.wasEvicted()) || ctx.exceedsTolerance) {
1 ✔
3180
            setWriteTime(n, expirationTime);
1 ✔
3181
          }
3182
          setAccessTime(n, expirationTime);
1 ✔
3183
          setVariableTime(n, varTime);
1 ✔
3184
          discardRefresh(kr);
1 ✔
3185
          return n;
1 ✔
3186
        } catch (Throwable e) {
1 ✔
3187
          if ((ctx.hints == null) || !ctx.hints.preserveRefresh) {
1 ✔
3188
            discardRefresh(kr);
1 ✔
3189
          }
3190
          if (!wasEvicted) {
1 ✔
3191
            throw e;
1 ✔
3192
          }
3193
          ctx.newValue = null;
1 ✔
3194
          ctx.exception = e;
1 ✔
3195
          ctx.removed = n;
1 ✔
3196
          n.retire();
1 ✔
3197
          return null;
1 ✔
3198
        }
3199
      }
3200
    });
3201

3202
    if (ctx.cause != null) {
1 ✔
3203
      if (ctx.cause == RemovalCause.REPLACED) {
1 ✔
3204
        requireNonNull(ctx.newValue);
1 ✔
3205
        notifyOnReplace(key, ctx.oldValue, ctx.newValue);
1 ✔
3206
      } else {
3207
        if (ctx.cause.wasEvicted()) {
1 ✔
3208
          statsCounter().recordEviction(ctx.oldWeight, ctx.cause);
1 ✔
3209
        }
3210
        notifyRemoval(ctx.nodeKey, ctx.oldValue, ctx.cause);
1 ✔
3211
      }
3212
    }
3213

3214
    if (ctx.removed != null) {
1 ✔
3215
      afterWrite(new RemovalTask(ctx.removed));
1 ✔
3216
    } else if (node == null) {
1 ✔
3217
      // absent and not computable
3218
    } else if (ctx.unmodified) {
1 ✔
3219
      // caller flagged a same-instance return as a no-op
3220
    } else if ((ctx.oldValue == null) && (ctx.cause == null)) {
1 ✔
3221
      afterWrite(new AddTask(node, ctx.newWeight));
1 ✔
3222
    } else {
3223
      int weightedDifference = ctx.newWeight - ctx.oldWeight;
1 ✔
3224
      boolean quietly = (ctx.hints != null) && ctx.hints.quietly;
1 ✔
3225
      boolean reload = (ctx.cause != null) && ctx.cause.wasEvicted();
1 ✔
3226
      if (ctx.exceedsTolerance || (weightedDifference != 0) || (evicts() && !quietly && reload)) {
1 ✔
3227
        var access = quietly ? Access.QUIET : (reload ? Access.RELOAD : Access.HIT);
1 ✔
3228
        afterWrite(new UpdateTask(node, weightedDifference, access));
1 ✔
3229
      } else {
1 ✔
3230
        if (!quietly) {
1 ✔
3231
          afterRead(node, ctx.now, /* recordHit= */ false);
1 ✔
3232
        }
3233
        if (reload) {
1 ✔
3234
          scheduleDrainBuffers();
1 ✔
3235
        }
3236
      }
3237
    }
3238

3239
    if (ctx.exception != null) {
1 ✔
3240
      throw toUnchecked(ctx.exception);
1 ✔
3241
    }
3242
    return ctx.newValue;
1 ✔
3243
  }
3244

3245
  @Override
3246
  public void forEach(BiConsumer<? super K, ? super V> action) {
3247
    requireNonNull(action);
1 ✔
3248

3249
    for (var iterator = new EntryIterator<>(this); iterator.hasNext();) {
1 ✔
3250
      action.accept(requireNonNull(iterator.key), requireNonNull(iterator.value));
1 ✔
3251
      iterator.advance();
1 ✔
3252
    }
3253
  }
1 ✔
3254

3255
  @Override
3256
  public Set<K> keySet() {
3257
    Set<K> ks = keySet;
1 ✔
3258
    return (ks == null) ? (keySet = new KeySetView<>(this)) : ks;
1 ✔
3259
  }
3260

3261
  @Override
3262
  public Collection<V> values() {
3263
    Collection<V> vs = values;
1 ✔
3264
    return (vs == null) ? (values = new ValuesView<>(this)) : vs;
1 ✔
3265
  }
3266

3267
  @Override
3268
  public Set<Entry<K, V>> entrySet() {
3269
    Set<Entry<K, V>> es = entrySet;
1 ✔
3270
    return (es == null) ? (entrySet = new EntrySetView<>(this)) : es;
1 ✔
3271
  }
3272

3273
  /**
3274
   * Object equality requires reflexive, symmetric, transitive, and consistency properties. Of
3275
   * these, symmetry and consistency require further clarification for how they are upheld.
3276
   * <p>
3277
   * The <i>consistency</i> property between invocations requires that the results are the same if
3278
   * there are no modifications to the information used. Therefore, usages should expect that this
3279
   * operation may return misleading results if either the maps or the data held by them is modified
3280
   * during the execution of this method. This characteristic allows for comparing the map sizes and
3281
   * assuming stable mappings, as done by {@link java.util.AbstractMap}-based maps.
3282
   * <p>
3283
   * The <i>symmetric</i> property requires that the result is the same for all implementations of
3284
   * {@link Map#equals(Object)}. That contract is defined in terms of the stable mappings provided
3285
   * by {@link #entrySet()}, meaning that the {@link #size()} optimization forces that the count is
3286
   * consistent with the mappings when used for an equality check.
3287
   * <p>
3288
   * The cache's {@link #size()} method may include entries that have expired or have been reference
3289
   * collected, but have not yet been removed from the backing map. An iteration over the map may
3290
   * trigger the removal of these dead entries when skipped over during traversal. To ensure
3291
   * consistency and symmetry, usages should call {@link #cleanUp()} before this method while no
3292
   * other concurrent operations are being performed on this cache. This is not done implicitly by
3293
   * {@link #size()} as many usages assume it to be instantaneous and lock-free. As a postcondition
3294
   * the iteration count is verified against the prescreened {@link #size()} so that a concurrent
3295
   * maintenance pass that drops dead entries during traversal is detected and reported as not
3296
   * equal, rather than silently returning {@code true} on the surviving subset.
3297
   */
3298
  @Override
3299
  public boolean equals(@Nullable Object o) {
3300
    if (o == this) {
1 ✔
3301
      return true;
1 ✔
3302
    } else if (!(o instanceof Map)) {
1 ✔
3303
      return false;
1 ✔
3304
    }
3305

3306
    var map = (Map<?, ?>) o;
1 ✔
3307
    int expectedSize = size();
1 ✔
3308
    if (map.size() != expectedSize) {
1 ✔
3309
      return false;
1 ✔
3310
    }
3311

3312
    try {
3313
      @Var int count = 0;
1 ✔
3314
      long now = expirationTicker().read();
1 ✔
3315
      for (var node : data.values()) {
1 ✔
3316
        boolean expired = hasExpired(node, now);
1 ✔
3317
        K key = node.getKey();
1 ✔
3318
        V value = node.getValue();
1 ✔
3319
        if ((key == null) || (value == null) || !node.isAlive()
1 ✔
3320
            || (expired && !isComputingAsync(value))) {
1 ✔
3321
          scheduleDrainBuffers();
1 ✔
3322
          return false;
1 ✔
3323
        } else {
3324
          var val = map.get(key);
1 ✔
3325
          if ((val == null) || ((val != value) && !val.equals(value))) {
1 ✔
3326
            return false;
1 ✔
3327
          }
3328
        }
3329
        count++;
1 ✔
3330
      }
1 ✔
3331
      return (count == expectedSize);
1 ✔
3332
    } catch (ClassCastException | NullPointerException ignored) {
1 ✔
3333
      return false;
1 ✔
3334
    }
3335
  }
3336

3337
  @Override
3338
  public int hashCode() {
3339
    @Var int hash = 0;
1 ✔
3340
    @Var boolean drain = false;
1 ✔
3341
    long now = expirationTicker().read();
1 ✔
3342
    for (var node : data.values()) {
1 ✔
3343
      boolean expired = hasExpired(node, now);
1 ✔
3344
      K key = node.getKey();
1 ✔
3345
      V value = node.getValue();
1 ✔
3346
      if ((key == null) || (value == null) || !node.isAlive()
1 ✔
3347
          || (expired && !isComputingAsync(value))) {
1 ✔
3348
        drain = true;
1 ✔
3349
      } else {
3350
        hash += key.hashCode() ^ value.hashCode();
1 ✔
3351
      }
3352
    }
1 ✔
3353
    if (drain) {
1 ✔
3354
      scheduleDrainBuffers();
1 ✔
3355
    }
3356
    return hash;
1 ✔
3357
  }
3358

3359
  @Override
3360
  public String toString() {
3361
    @Var boolean drain = false;
1 ✔
3362
    long now = expirationTicker().read();
1 ✔
3363
    var result = new StringBuilder().append('{');
1 ✔
3364
    for (var node : data.values()) {
1 ✔
3365
      boolean expired = hasExpired(node, now);
1 ✔
3366
      K key = node.getKey();
1 ✔
3367
      V value = node.getValue();
1 ✔
3368
      if ((key == null) || (value == null) || !node.isAlive()
1 ✔
3369
          || (expired && !isComputingAsync(value))) {
1 ✔
3370
        drain = true;
1 ✔
3371
      } else {
3372
        if (result.length() != 1) {
1 ✔
3373
          result.append(',').append(' ');
1 ✔
3374
        }
3375
        result.append((key == this) ? "(this Map)" : key);
1 ✔
3376
        result.append('=');
1 ✔
3377
        result.append((value == this) ? "(this Map)" : value);
1 ✔
3378
      }
3379
    }
1 ✔
3380
    if (drain) {
1 ✔
3381
      scheduleDrainBuffers();
1 ✔
3382
    }
3383
    return result.append('}').toString();
1 ✔
3384
  }
3385

3386
  /**
3387
   * Returns the computed result from the ordered traversal of the cache entries.
3388
   *
3389
   * @param hottest the coldest or hottest iteration order
3390
   * @param transformer a function that unwraps the value
3391
   * @param mappingFunction the mapping function to compute a value
3392
   * @return the computed value
3393
   */
3394
  @SuppressWarnings("GuardedByChecker")
3395
  <T extends @Nullable Object> T evictionOrder(boolean hottest,
3396
      Function<@Nullable V, @Nullable V> transformer,
3397
      Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
3398
    Comparator<Node<K, V>> comparator = Comparator.comparingInt(node -> {
1 ✔
3399
      var keyRef = node.getKeyReferenceOrNull();
1 ✔
3400
      return ((keyRef == null) || !node.isAlive()) ? 0 : frequencySketch().frequency(keyRef);
1 ✔
3401
    });
3402
    Iterable<Node<K, V>> iterable;
3403
    if (hottest) {
1 ✔
3404
      iterable = () -> {
1 ✔
3405
        var secondary = PeekingIterator.comparing(
1 ✔
3406
            accessOrderProbationDeque().descendingIterator(),
1 ✔
3407
            accessOrderWindowDeque().descendingIterator(), comparator);
1 ✔
3408
        return PeekingIterator.concat(
1 ✔
3409
            accessOrderProtectedDeque().descendingIterator(), secondary);
1 ✔
3410
      };
3411
    } else {
3412
      iterable = () -> {
1 ✔
3413
        var primary = PeekingIterator.comparing(
1 ✔
3414
            accessOrderWindowDeque().iterator(), accessOrderProbationDeque().iterator(),
1 ✔
3415
            comparator.reversed());
1 ✔
3416
        return PeekingIterator.concat(primary, accessOrderProtectedDeque().iterator());
1 ✔
3417
      };
3418
    }
3419
    return snapshot(iterable, transformer, mappingFunction);
1 ✔
3420
  }
3421

3422
  /**
3423
   * Returns the computed result from the ordered traversal of the cache entries.
3424
   *
3425
   * @param oldest the youngest or oldest iteration order
3426
   * @param transformer a function that unwraps the value
3427
   * @param mappingFunction the mapping function to compute a value
3428
   * @return the computed value
3429
   */
3430
  @SuppressWarnings("GuardedByChecker")
3431
  <T extends @Nullable Object> T expireAfterAccessOrder(boolean oldest,
3432
      Function<@Nullable V, @Nullable V> transformer,
3433
      Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
3434
    Iterable<Node<K, V>> iterable;
3435
    if (evicts()) {
1 ✔
3436
      iterable = () -> {
1 ✔
3437
        @Var Comparator<Node<K, V>> comparator = (n1, n2) ->
1 ✔
3438
            Long.signum(n1.getAccessTime() - n2.getAccessTime());
1 ✔
3439
        PeekingIterator<Node<K, V>> first;
3440
        PeekingIterator<Node<K, V>> second;
3441
        PeekingIterator<Node<K, V>> third;
3442
        if (oldest) {
1 ✔
3443
          comparator = comparator.reversed();
1 ✔
3444
          first = accessOrderWindowDeque().iterator();
1 ✔
3445
          second = accessOrderProbationDeque().iterator();
1 ✔
3446
          third = accessOrderProtectedDeque().iterator();
1 ✔
3447
        } else {
3448
          first = accessOrderWindowDeque().descendingIterator();
1 ✔
3449
          second = accessOrderProbationDeque().descendingIterator();
1 ✔
3450
          third = accessOrderProtectedDeque().descendingIterator();
1 ✔
3451
        }
3452
        return PeekingIterator.comparing(
1 ✔
3453
            PeekingIterator.comparing(first, second, comparator), third, comparator);
1 ✔
3454
      };
3455
    } else {
3456
      iterable = oldest
1 ✔
3457
          ? accessOrderWindowDeque()
1 ✔
3458
          : accessOrderWindowDeque()::descendingIterator;
1 ✔
3459
    }
3460
    return snapshot(iterable, transformer, mappingFunction);
1 ✔
3461
  }
3462

3463
  /**
3464
   * Returns the computed result from the ordered traversal of the cache entries.
3465
   *
3466
   * @param iterable the supplier of the entries in the cache
3467
   * @param transformer a function that unwraps the value
3468
   * @param mappingFunction the mapping function to compute a value
3469
   * @return the computed value
3470
   */
3471
  @SuppressWarnings("MathClampLong")
3472
  <T extends @Nullable Object> T snapshot(Iterable<Node<K, V>> iterable,
3473
      Function<@Nullable V, @Nullable V> transformer,
3474
      Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
3475
    requireNonNull(mappingFunction);
1 ✔
3476
    requireNonNull(transformer);
1 ✔
3477
    requireNonNull(iterable);
1 ✔
3478

3479
    evictionLock.lock();
1 ✔
3480
    try {
3481
      maintenance(/* ignored */ null);
1 ✔
3482

3483
      // Obtain the iterator as late as possible for modification count checking
3484
      try (var stream = StreamSupport.stream(Spliterators.spliteratorUnknownSize(
1 ✔
3485
           iterable.iterator(), DISTINCT | ORDERED | NONNULL | IMMUTABLE), /* parallel= */ false)) {
1 ✔
3486
        boolean[] open = { true };
1 ✔
3487
        return mappingFunction.apply(stream.onClose(() -> open[0] = false)
1 ✔
3488
            .peek(ignored -> requireState(open[0], "stream has already been closed"))
1 ✔
3489
            .map(node -> nodeToCacheEntry(node, transformer,
1 ✔
3490
                (int) Math.max(0L, Math.min(node.getPolicyWeight(), Integer.MAX_VALUE))))
1 ✔
3491
            .filter(Objects::nonNull));
1 ✔
3492
      }
3493
    } finally {
3494
      evictionLock.unlock();
1 ✔
3495
      rescheduleCleanUpIfIncomplete();
1 ✔
3496
    }
3497
  }
3498

3499
  /**
3500
   * Returns an entry for the given node if it can be used externally, else null. The weight is
3501
   * the caller's best-effort reading and the timestamps are read after the value, so either may
3502
   * belong to a concurrent update.
3503
   */
3504
  @Nullable CacheEntry<K, V> nodeToCacheEntry(
3505
      Node<K, V> node, Function<@Nullable V, @Nullable V> transformer, int weight) {
3506
    long now = expirationTicker().read();
1 ✔
3507
    boolean expired = hasExpired(node, now);
1 ✔
3508
    V rawValue = node.getValue();
1 ✔
3509
    if (rawValue == null) {
1 ✔
3510
      return null;
1 ✔
3511
    }
3512
    V value = transformer.apply(rawValue);
1 ✔
3513
    K key = node.getKey();
1 ✔
3514
    if ((key == null) || (value == null) || !node.isAlive() || expired) {
1 ✔
3515
      return null;
1 ✔
3516
    }
3517

3518
    @Var long expiresAfter = Long.MAX_VALUE;
1 ✔
3519
    if (expiresAfterAccess()) {
1 ✔
3520
      expiresAfter = Math.min(expiresAfter,
1 ✔
3521
          expiresAfterAccessNanos() - Math.max(0L, now - node.getAccessTime()));
1 ✔
3522
    }
3523
    if (expiresAfterWrite()) {
1 ✔
3524
      expiresAfter = Math.min(expiresAfter,
1 ✔
3525
          expiresAfterWriteNanos() - Math.max(0L, toWriteTime(now) - writeTimeOf(node)));
1 ✔
3526
    }
3527
    if (expiresVariable()) {
1 ✔
3528
      expiresAfter = node.getVariableTime() - now;
1 ✔
3529
    }
3530

3531
    long refreshableAfter = refreshAfterWrite()
1 ✔
3532
        ? refreshAfterWriteNanos() - Math.max(0L, now - writeTimeOf(node))
1 ✔
3533
        : Long.MAX_VALUE;
1 ✔
3534
    return SnapshotEntry.forEntry(key, value, now,
1 ✔
3535
        isWeighted ? weight : 1, now + expiresAfter, now + refreshableAfter);
1 ✔
3536
  }
3537

3538
  /** Creates a serialization proxy based on the common configuration shared by all cache types. */
3539
  static <K, V> SerializationProxy<K, V> makeSerializationProxy(BoundedLocalCache<?, ?> cache) {
3540
    var proxy = new SerializationProxy<K, V>();
1 ✔
3541
    proxy.weakKeys = cache.collectKeys();
1 ✔
3542
    proxy.weakValues = cache.nodeFactory.weakValues();
1 ✔
3543
    proxy.softValues = cache.nodeFactory.softValues();
1 ✔
3544
    proxy.isRecordingStats = cache.isRecordingStats();
1 ✔
3545
    proxy.evictionListener = cache.evictionListener;
1 ✔
3546
    proxy.removalListener = cache.removalListener();
1 ✔
3547
    proxy.ticker = cache.expirationTicker();
1 ✔
3548
    if (cache.expiresAfterAccess()) {
1 ✔
3549
      proxy.expiresAfterAccessNanos = cache.expiresAfterAccessNanos();
1 ✔
3550
    }
3551
    if (cache.expiresAfterWrite()) {
1 ✔
3552
      proxy.expiresAfterWriteNanos = cache.expiresAfterWriteNanos();
1 ✔
3553
    }
3554
    if (cache.expiresVariable()) {
1 ✔
3555
      proxy.expiry = cache.expiry();
1 ✔
3556
    }
3557
    if (cache.refreshAfterWrite()) {
1 ✔
3558
      proxy.refreshAfterWriteNanos = cache.refreshAfterWriteNanos();
1 ✔
3559
    }
3560
    if (cache.evicts()) {
1 ✔
3561
      if (cache.isWeighted) {
1 ✔
3562
        proxy.weigher = cache.weigher;
1 ✔
3563
        proxy.maximumWeight = cache.maximumAcquire();
1 ✔
3564
      } else {
3565
        proxy.maximumSize = cache.maximumAcquire();
1 ✔
3566
      }
3567
    }
3568
    proxy.cacheLoader = cache.cacheLoader;
1 ✔
3569
    proxy.async = cache.isAsync;
1 ✔
3570
    return proxy;
1 ✔
3571
  }
3572

3573
  /** Mutable context for passing state between a lambda and the caller. */
3574
  static final class EvictContext<V> {
1 ✔
3575
    @Nullable RemovalCause cause;
3576
    @Nullable V value;
3577
    boolean resurrect;
3578
    boolean removed;
3579
    int oldWeight;
3580
  }
3581

3582
  /** Mutable context for passing state between a lambda and the caller. */
3583
  static final class RemoveContext<K, V> {
1 ✔
3584
    @Nullable K oldKey;
3585
    @Nullable V oldValue;
3586
    @Nullable Node<K, V> node;
3587
    @Nullable RemovalCause cause;
3588
    int oldWeight;
3589
  }
3590

3591
  /** Mutable context for passing state between a lambda and the caller. */
3592
  static final class ReplaceContext<K, V> {
1 ✔
3593
    @Nullable K nodeKey;
3594
    @Nullable V oldValue;
3595

3596
    long now;
3597
    int oldWeight;
3598
    boolean garbage;
3599
    boolean exceedsTolerance;
3600
  }
3601

3602
  /** Mutable context for passing state between a lambda and the caller. */
3603
  static final class ComputeContext<K, V> {
1 ✔
3604
    @Nullable K nodeKey;
3605
    @Nullable V oldValue;
3606
    @Nullable V newValue;
3607
    @Nullable RemapHints hints;
3608
    @Nullable Node<K, V> removed;
3609
    @Nullable RemovalCause cause;
3610
    @Nullable Throwable exception;
3611

3612
    long now;
3613
    int oldWeight;
3614
    int newWeight;
3615
    boolean unmodified;
3616
    boolean exceedsTolerance;
3617
  }
3618

3619
  /** A function that produces an unmodifiable map up to the limit in stream order. */
3620
  static final class SizeLimiter<K, V> implements Function<Stream<CacheEntry<K, V>>, Map<K, V>> {
3621
    private final int expectedSize;
3622
    private final long limit;
3623

3624
    SizeLimiter(int expectedSize, long limit) {
1 ✔
3625
      requireArgument(limit >= 0, "limit cannot be negative: %s", limit);
1 ✔
3626
      this.expectedSize = expectedSize;
1 ✔
3627
      this.limit = limit;
1 ✔
3628
    }
1 ✔
3629

3630
    @Override
3631
    public Map<K, V> apply(Stream<CacheEntry<K, V>> stream) {
3632
      var map = new LinkedHashMap<K, V>(calculateHashMapCapacity(expectedSize));
1 ✔
3633
      stream.limit(limit).forEach(entry -> map.put(entry.getKey(), entry.getValue()));
1 ✔
3634
      return Collections.unmodifiableMap(map);
1 ✔
3635
    }
3636
  }
3637

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

3642
    long weightedSize;
3643

3644
    WeightLimiter(long weightLimit) {
1 ✔
3645
      requireArgument(weightLimit >= 0, "weight limit cannot be negative: %s", weightLimit);
1 ✔
3646
      this.weightLimit = weightLimit;
1 ✔
3647
    }
1 ✔
3648

3649
    @Override
3650
    public Map<K, V> apply(Stream<CacheEntry<K, V>> stream) {
3651
      var map = new LinkedHashMap<K, V>();
1 ✔
3652
      stream.takeWhile(entry -> {
1 ✔
3653
        weightedSize += entry.weight();
1 ✔
3654
        if (weightedSize < 0) {
1 ✔
3655
          weightedSize = Long.MAX_VALUE;
1 ✔
3656
        }
3657
        return (weightedSize <= weightLimit);
1 ✔
3658
      }).forEach(entry -> map.put(entry.getKey(), entry.getValue()));
1 ✔
3659
      return Collections.unmodifiableMap(map);
1 ✔
3660
    }
3661
  }
3662

3663
  /** An adapter to safely externalize the keys. */
3664
  static final class KeySetView<K, V> extends AbstractSet<K> {
3665
    final BoundedLocalCache<K, V> cache;
3666

3667
    KeySetView(BoundedLocalCache<K, V> cache) {
1 ✔
3668
      this.cache = requireNonNull(cache);
1 ✔
3669
    }
1 ✔
3670

3671
    @Override
3672
    public int size() {
3673
      return cache.size();
1 ✔
3674
    }
3675

3676
    @Override
3677
    public void clear() {
3678
      cache.clear();
1 ✔
3679
    }
1 ✔
3680

3681
    @Override
3682
    @SuppressWarnings("SuspiciousMethodCalls")
3683
    public boolean contains(@Nullable Object o) {
3684
      return cache.containsKey(o);
1 ✔
3685
    }
3686

3687
    @Override
3688
    public boolean containsAll(Collection<?> collection) {
3689
      requireNonNull(collection);
1 ✔
3690
      if (collection != this) {
1 ✔
3691
        for (Object o : collection) {
1 ✔
3692
          if ((o == null) || !contains(o)) {
1 ✔
3693
            return false;
1 ✔
3694
          }
3695
        }
1 ✔
3696
      }
3697
      return true;
1 ✔
3698
    }
3699

3700
    @Override
3701
    public boolean removeAll(Collection<?> collection) {
3702
      requireNonNull(collection);
1 ✔
3703
      @Var boolean modified = false;
1 ✔
3704
      if (cache.collectKeys() || ((collection instanceof Set<?>) && (collection.size() > size()))) {
1 ✔
3705
        for (K key : this) {
1 ✔
3706
          if (collection.contains(key)) {
1 ✔
3707
            modified |= remove(key);
1 ✔
3708
          }
3709
        }
1 ✔
3710
      } else {
3711
        for (var item : collection) {
1 ✔
3712
          modified |= (item != null) && remove(item);
1 ✔
3713
        }
1 ✔
3714
      }
3715
      return modified;
1 ✔
3716
    }
3717

3718
    @Override
3719
    public boolean remove(@Nullable Object o) {
3720
      return (cache.remove(o) != null);
1 ✔
3721
    }
3722

3723
    @Override
3724
    public boolean removeIf(Predicate<? super K> filter) {
3725
      requireNonNull(filter);
1 ✔
3726
      @Var boolean modified = false;
1 ✔
3727
      for (K key : this) {
1 ✔
3728
        if (filter.test(key) && remove(key)) {
1 ✔
3729
          modified = true;
1 ✔
3730
        }
3731
      }
1 ✔
3732
      return modified;
1 ✔
3733
    }
3734

3735
    @Override
3736
    public boolean retainAll(Collection<?> collection) {
3737
      requireNonNull(collection);
1 ✔
3738
      @Var boolean modified = false;
1 ✔
3739
      for (K key : this) {
1 ✔
3740
        if (!collection.contains(key) && remove(key)) {
1 ✔
3741
          modified = true;
1 ✔
3742
        }
3743
      }
1 ✔
3744
      return modified;
1 ✔
3745
    }
3746

3747
    @Override
3748
    public Iterator<K> iterator() {
3749
      return new KeyIterator<>(cache);
1 ✔
3750
    }
3751

3752
    @Override
3753
    public Spliterator<K> spliterator() {
3754
      return new KeySpliterator<>(cache);
1 ✔
3755
    }
3756
  }
3757

3758
  /** An adapter to safely externalize the key iterator. */
3759
  static final class KeyIterator<K, V> implements Iterator<K> {
3760
    final EntryIterator<K, V> iterator;
3761

3762
    KeyIterator(BoundedLocalCache<K, V> cache) {
1 ✔
3763
      this.iterator = new EntryIterator<>(cache);
1 ✔
3764
    }
1 ✔
3765

3766
    @Override
3767
    public boolean hasNext() {
3768
      return iterator.hasNext();
1 ✔
3769
    }
3770

3771
    @Override
3772
    public K next() {
3773
      return iterator.nextKey();
1 ✔
3774
    }
3775

3776
    @Override
3777
    public void remove() {
3778
      iterator.remove();
1 ✔
3779
    }
1 ✔
3780
  }
3781

3782
  /** An adapter to safely externalize the key spliterator. */
3783
  static final class KeySpliterator<K, V> implements Spliterator<K> {
3784
    final Spliterator<Node<K, V>> spliterator;
3785
    final BoundedLocalCache<K, V> cache;
3786

3787
    KeySpliterator(BoundedLocalCache<K, V> cache) {
3788
      this(cache, cache.data.values().spliterator());
1 ✔
3789
    }
1 ✔
3790

3791
    KeySpliterator(BoundedLocalCache<K, V> cache, Spliterator<Node<K, V>> spliterator) {
1 ✔
3792
      this.spliterator = requireNonNull(spliterator);
1 ✔
3793
      this.cache = requireNonNull(cache);
1 ✔
3794
    }
1 ✔
3795

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

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

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

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

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

3854
  /** An adapter to safely externalize the values. */
3855
  static final class ValuesView<K, V> extends AbstractCollection<V> {
3856
    final BoundedLocalCache<K, V> cache;
3857

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

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

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

3872
    @Override
3873
    @SuppressWarnings("SuspiciousMethodCalls")
3874
    public boolean contains(@Nullable Object o) {
3875
      return cache.containsValue(o);
1 ✔
3876
    }
3877

3878
    @Override
3879
    public boolean containsAll(Collection<?> collection) {
3880
      requireNonNull(collection);
1 ✔
3881
      if (collection != this) {
1 ✔
3882
        for (Object o : collection) {
1 ✔
3883
          if ((o == null) || !contains(o)) {
1 ✔
3884
            return false;
1 ✔
3885
          }
3886
        }
1 ✔
3887
      }
3888
      return true;
1 ✔
3889
    }
3890

3891
    @Override
3892
    public boolean removeAll(Collection<?> collection) {
3893
      requireNonNull(collection);
1 ✔
3894
      @Var boolean modified = false;
1 ✔
3895
      for (var iterator = new EntryIterator<>(cache); iterator.hasNext();) {
1 ✔
3896
        var key = requireNonNull(iterator.key);
1 ✔
3897
        var value = requireNonNull(iterator.value);
1 ✔
3898
        if (collection.contains(value) && cache.remove(key, value)) {
1 ✔
3899
          modified = true;
1 ✔
3900
        }
3901
        iterator.advance();
1 ✔
3902
      }
1 ✔
3903
      return modified;
1 ✔
3904
    }
3905

3906
    @Override
3907
    public boolean remove(@Nullable Object o) {
3908
      if (o == null) {
1 ✔
3909
        return false;
1 ✔
3910
      }
3911
      for (var iterator = new EntryIterator<>(cache); iterator.hasNext();) {
1 ✔
3912
        var key = requireNonNull(iterator.key);
1 ✔
3913
        var node = requireNonNull(iterator.next);
1 ✔
3914
        var value = requireNonNull(iterator.value);
1 ✔
3915
        if (node.containsValue(o) && cache.remove(key, value)) {
1 ✔
3916
          return true;
1 ✔
3917
        }
3918
        iterator.advance();
1 ✔
3919
      }
1 ✔
3920
      return false;
1 ✔
3921
    }
3922

3923
    @Override
3924
    public boolean removeIf(Predicate<? super V> filter) {
3925
      requireNonNull(filter);
1 ✔
3926
      @Var boolean modified = false;
1 ✔
3927
      for (var iterator = new EntryIterator<>(cache); iterator.hasNext();) {
1 ✔
3928
        var value = requireNonNull(iterator.value);
1 ✔
3929
        if (filter.test(value)) {
1 ✔
3930
          var key = requireNonNull(iterator.key);
1 ✔
3931
          modified |= cache.remove(key, value);
1 ✔
3932
        }
3933
        iterator.advance();
1 ✔
3934
      }
1 ✔
3935
      return modified;
1 ✔
3936
    }
3937

3938
    @Override
3939
    public boolean retainAll(Collection<?> collection) {
3940
      requireNonNull(collection);
1 ✔
3941
      @Var boolean modified = false;
1 ✔
3942
      for (var iterator = new EntryIterator<>(cache); iterator.hasNext();) {
1 ✔
3943
        var key = requireNonNull(iterator.key);
1 ✔
3944
        var value = requireNonNull(iterator.value);
1 ✔
3945
        if (!collection.contains(value) && cache.remove(key, value)) {
1 ✔
3946
          modified = true;
1 ✔
3947
        }
3948
        iterator.advance();
1 ✔
3949
      }
1 ✔
3950
      return modified;
1 ✔
3951
    }
3952

3953
    @Override
3954
    public Iterator<V> iterator() {
3955
      return new ValueIterator<>(cache);
1 ✔
3956
    }
3957

3958
    @Override
3959
    public Spliterator<V> spliterator() {
3960
      return new ValueSpliterator<>(cache);
1 ✔
3961
    }
3962
  }
3963

3964
  /** An adapter to safely externalize the value iterator. */
3965
  static final class ValueIterator<K, V> implements Iterator<V> {
3966
    final EntryIterator<K, V> iterator;
3967

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

3972
    @Override
3973
    public boolean hasNext() {
3974
      return iterator.hasNext();
1 ✔
3975
    }
3976

3977
    @Override
3978
    public V next() {
3979
      return iterator.nextValue();
1 ✔
3980
    }
3981

3982
    @Override
3983
    public void remove() {
3984
      iterator.remove();
1 ✔
3985
    }
1 ✔
3986
  }
3987

3988
  /** An adapter to safely externalize the value spliterator. */
3989
  static final class ValueSpliterator<K, V> implements Spliterator<V> {
3990
    final Spliterator<Node<K, V>> spliterator;
3991
    final BoundedLocalCache<K, V> cache;
3992

3993
    ValueSpliterator(BoundedLocalCache<K, V> cache) {
3994
      this(cache, cache.data.values().spliterator());
1 ✔
3995
    }
1 ✔
3996

3997
    ValueSpliterator(BoundedLocalCache<K, V> cache, Spliterator<Node<K, V>> spliterator) {
1 ✔
3998
      this.spliterator = requireNonNull(spliterator);
1 ✔
3999
      this.cache = requireNonNull(cache);
1 ✔
4000
    }
1 ✔
4001

4002
    @Override
4003
    public void forEachRemaining(Consumer<? super V> action) {
4004
      requireNonNull(action);
1 ✔
4005
      Consumer<Node<K, V>> consumer = node -> {
1 ✔
4006
        long now = cache.expirationTicker().read();
1 ✔
4007
        boolean expired = cache.hasExpired(node, now);
1 ✔
4008
        K key = node.getKey();
1 ✔
4009
        V value = node.getValue();
1 ✔
4010
        if ((key == null) || (value == null) || (expired && !cache.isComputingAsync(value))) {
1 ✔
4011
          cache.scheduleDrainBuffers();
1 ✔
4012
        } else if (node.isAlive()) {
1 ✔
4013
          action.accept(value);
1 ✔
4014
        }
4015
      };
1 ✔
4016
      spliterator.forEachRemaining(consumer);
1 ✔
4017
    }
1 ✔
4018

4019
    @Override
4020
    public boolean tryAdvance(Consumer<? super V> action) {
4021
      requireNonNull(action);
1 ✔
4022
      boolean[] advanced = { false };
1 ✔
4023
      Consumer<Node<K, V>> consumer = node -> {
1 ✔
4024
        long now = cache.expirationTicker().read();
1 ✔
4025
        boolean expired = cache.hasExpired(node, now);
1 ✔
4026
        K key = node.getKey();
1 ✔
4027
        V value = node.getValue();
1 ✔
4028
        if ((key == null) || (value == null) || (expired && !cache.isComputingAsync(value))) {
1 ✔
4029
          cache.scheduleDrainBuffers();
1 ✔
4030
        } else if (node.isAlive()) {
1 ✔
4031
          action.accept(value);
1 ✔
4032
          advanced[0] = true;
1 ✔
4033
        }
4034
      };
1 ✔
4035
      while (spliterator.tryAdvance(consumer)) {
1 ✔
4036
        if (advanced[0]) {
1 ✔
4037
          return true;
1 ✔
4038
        }
4039
      }
4040
      return false;
1 ✔
4041
    }
4042

4043
    @Override
4044
    public @Nullable Spliterator<V> trySplit() {
4045
      Spliterator<Node<K, V>> split = spliterator.trySplit();
1 ✔
4046
      return (split == null) ? null : new ValueSpliterator<>(cache, split);
1 ✔
4047
    }
4048

4049
    @Override
4050
    public long estimateSize() {
4051
      return spliterator.estimateSize();
1 ✔
4052
    }
4053

4054
    @Override
4055
    public int characteristics() {
4056
      return CONCURRENT | NONNULL;
1 ✔
4057
    }
4058
  }
4059

4060
  /** An adapter to safely externalize the entries. */
4061
  static final class EntrySetView<K, V> extends AbstractSet<Entry<K, V>> {
4062
    final BoundedLocalCache<K, V> cache;
4063

4064
    EntrySetView(BoundedLocalCache<K, V> cache) {
1 ✔
4065
      this.cache = requireNonNull(cache);
1 ✔
4066
    }
1 ✔
4067

4068
    @Override
4069
    public int size() {
4070
      return cache.size();
1 ✔
4071
    }
4072

4073
    @Override
4074
    public void clear() {
4075
      cache.clear();
1 ✔
4076
    }
1 ✔
4077

4078
    @Override
4079
    public boolean contains(@Nullable Object o) {
4080
      if (!(o instanceof Entry<?, ?>)) {
1 ✔
4081
        return false;
1 ✔
4082
      }
4083
      var entry = (Entry<?, ?>) o;
1 ✔
4084
      var key = entry.getKey();
1 ✔
4085
      var value = entry.getValue();
1 ✔
4086
      if ((key == null) || (value == null)) {
1 ✔
4087
        return false;
1 ✔
4088
      }
4089
      Node<K, V> node = cache.data.get(cache.nodeFactory.newLookupKey(key));
1 ✔
4090
      if (node == null) {
1 ✔
4091
        return false;
1 ✔
4092
      }
4093
      boolean expired = cache.hasExpired(node, cache.expirationTicker().read());
1 ✔
4094
      V nodeValue = node.getValue();
1 ✔
4095
      return (nodeValue != null) && node.containsValue(value)
1 ✔
4096
          && (!expired || cache.isComputingAsync(nodeValue));
1 ✔
4097
    }
4098

4099
    @Override
4100
    public boolean containsAll(Collection<?> collection) {
4101
      requireNonNull(collection);
1 ✔
4102
      if (collection != this) {
1 ✔
4103
        for (Object o : collection) {
1 ✔
4104
          if ((o == null) || !contains(o)) {
1 ✔
4105
            return false;
1 ✔
4106
          }
4107
        }
1 ✔
4108
      }
4109
      return true;
1 ✔
4110
    }
4111

4112
    @Override
4113
    public boolean removeAll(Collection<?> collection) {
4114
      requireNonNull(collection);
1 ✔
4115
      @Var boolean modified = false;
1 ✔
4116
      if (cache.collectKeys() || cache.collectValues()
1 ✔
4117
          || ((collection instanceof Set<?>) && (collection.size() > size()))) {
1 ✔
4118
        for (var entry : this) {
1 ✔
4119
          if (collection.contains(entry)) {
1 ✔
4120
            modified |= remove(entry);
1 ✔
4121
          }
4122
        }
1 ✔
4123
      } else {
4124
        for (var item : collection) {
1 ✔
4125
          modified |= remove(item);
1 ✔
4126
        }
1 ✔
4127
      }
4128
      return modified;
1 ✔
4129
    }
4130

4131
    @Override
4132
    @SuppressWarnings("SuspiciousMethodCalls")
4133
    public boolean remove(@Nullable Object o) {
4134
      if (!(o instanceof Entry<?, ?>)) {
1 ✔
4135
        return false;
1 ✔
4136
      }
4137
      var entry = (Entry<?, ?>) o;
1 ✔
4138
      var key = entry.getKey();
1 ✔
4139
      return (key != null) && cache.remove(key, entry.getValue());
1 ✔
4140
    }
4141

4142
    @Override
4143
    public boolean removeIf(Predicate<? super Entry<K, V>> filter) {
4144
      requireNonNull(filter);
1 ✔
4145
      @Var boolean modified = false;
1 ✔
4146
      for (var iterator = new EntryIterator<>(cache); iterator.hasNext();) {
1 ✔
4147
        var key = requireNonNull(iterator.key);
1 ✔
4148
        var value = requireNonNull(iterator.value);
1 ✔
4149
        if (filter.test(Map.entry(key, value))) {
1 ✔
4150
          modified |= cache.remove(key, value);
1 ✔
4151
        }
4152
        iterator.advance();
1 ✔
4153
      }
1 ✔
4154
      return modified;
1 ✔
4155
    }
4156

4157
    @Override
4158
    public boolean retainAll(Collection<?> collection) {
4159
      requireNonNull(collection);
1 ✔
4160
      @Var boolean modified = false;
1 ✔
4161
      for (var entry : this) {
1 ✔
4162
        if (!collection.contains(entry) && remove(entry)) {
1 ✔
4163
          modified = true;
1 ✔
4164
        }
4165
      }
1 ✔
4166
      return modified;
1 ✔
4167
    }
4168

4169
    @Override
4170
    public Iterator<Entry<K, V>> iterator() {
4171
      return new EntryIterator<>(cache);
1 ✔
4172
    }
4173

4174
    @Override
4175
    public Spliterator<Entry<K, V>> spliterator() {
4176
      return new EntrySpliterator<>(cache);
1 ✔
4177
    }
4178
  }
4179

4180
  /** An adapter to safely externalize the entry iterator. */
4181
  static final class EntryIterator<K, V> implements Iterator<Entry<K, V>> {
4182
    final BoundedLocalCache<K, V> cache;
4183
    final Iterator<Node<K, V>> iterator;
4184

4185
    @Nullable K key;
4186
    @Nullable V value;
4187
    @Nullable K removalKey;
4188
    @Nullable Node<K, V> next;
4189

4190
    EntryIterator(BoundedLocalCache<K, V> cache) {
1 ✔
4191
      this.iterator = cache.data.values().iterator();
1 ✔
4192
      this.cache = cache;
1 ✔
4193
    }
1 ✔
4194

4195
    @Override
4196
    public boolean hasNext() {
4197
      if (next != null) {
1 ✔
4198
        return true;
1 ✔
4199
      }
4200

4201
      long now = cache.expirationTicker().read();
1 ✔
4202
      while (iterator.hasNext()) {
1 ✔
4203
        next = iterator.next();
1 ✔
4204
        boolean expired = cache.hasExpired(next, now);
1 ✔
4205
        value = next.getValue();
1 ✔
4206
        key = next.getKey();
1 ✔
4207

4208
        boolean evictable = (key == null) || (value == null)
1 ✔
4209
            || (expired && !cache.isComputingAsync(value));
1 ✔
4210
        if (evictable || !next.isAlive()) {
1 ✔
4211
          if (evictable) {
1 ✔
4212
            cache.scheduleDrainBuffers();
1 ✔
4213
          }
4214
          advance();
1 ✔
4215
          continue;
1 ✔
4216
        }
4217
        return true;
1 ✔
4218
      }
4219
      return false;
1 ✔
4220
    }
4221

4222
    /** Invalidates the current position so that the iterator may compute the next position. */
4223
    void advance() {
4224
      value = null;
1 ✔
4225
      next = null;
1 ✔
4226
      key = null;
1 ✔
4227
    }
1 ✔
4228

4229
    K nextKey() {
4230
      if (!hasNext()) {
1 ✔
4231
        throw new NoSuchElementException();
1 ✔
4232
      }
4233
      removalKey = key;
1 ✔
4234
      advance();
1 ✔
4235
      return requireNonNull(removalKey);
1 ✔
4236
    }
4237

4238
    V nextValue() {
4239
      if (!hasNext()) {
1 ✔
4240
        throw new NoSuchElementException();
1 ✔
4241
      }
4242
      removalKey = key;
1 ✔
4243
      V val = value;
1 ✔
4244
      advance();
1 ✔
4245
      return requireNonNull(val);
1 ✔
4246
    }
4247

4248
    @Override
4249
    public Entry<K, V> next() {
4250
      if (!hasNext()) {
1 ✔
4251
        throw new NoSuchElementException();
1 ✔
4252
      }
4253
      var entry = new WriteThroughEntry<K, @NonNull V>(
1 ✔
4254
          cache, requireNonNull(key), requireNonNull(value));
1 ✔
4255
      removalKey = key;
1 ✔
4256
      advance();
1 ✔
4257
      return entry;
1 ✔
4258
    }
4259

4260
    @Override
4261
    public void remove() {
4262
      if (removalKey == null) {
1 ✔
4263
        throw new IllegalStateException();
1 ✔
4264
      }
4265
      cache.remove(removalKey);
1 ✔
4266
      removalKey = null;
1 ✔
4267
    }
1 ✔
4268
  }
4269

4270
  /** An adapter to safely externalize the entry spliterator. */
4271
  static final class EntrySpliterator<K, V> implements Spliterator<Entry<K, V>> {
4272
    final Spliterator<Node<K, V>> spliterator;
4273
    final BoundedLocalCache<K, V> cache;
4274

4275
    EntrySpliterator(BoundedLocalCache<K, V> cache) {
4276
      this(cache, cache.data.values().spliterator());
1 ✔
4277
    }
1 ✔
4278

4279
    EntrySpliterator(BoundedLocalCache<K, V> cache, Spliterator<Node<K, V>> spliterator) {
1 ✔
4280
      this.spliterator = requireNonNull(spliterator);
1 ✔
4281
      this.cache = requireNonNull(cache);
1 ✔
4282
    }
1 ✔
4283

4284
    @Override
4285
    public void forEachRemaining(Consumer<? super Entry<K, V>> action) {
4286
      requireNonNull(action);
1 ✔
4287
      Consumer<Node<K, V>> consumer = node -> {
1 ✔
4288
        long now = cache.expirationTicker().read();
1 ✔
4289
        boolean expired = cache.hasExpired(node, now);
1 ✔
4290
        K key = node.getKey();
1 ✔
4291
        V value = node.getValue();
1 ✔
4292
        if ((key == null) || (value == null) || (expired && !cache.isComputingAsync(value))) {
1 ✔
4293
          cache.scheduleDrainBuffers();
1 ✔
4294
        } else if (node.isAlive()) {
1 ✔
4295
          action.accept(new WriteThroughEntry<>(cache, key, value));
1 ✔
4296
        }
4297
      };
1 ✔
4298
      spliterator.forEachRemaining(consumer);
1 ✔
4299
    }
1 ✔
4300

4301
    @Override
4302
    public boolean tryAdvance(Consumer<? super Entry<K, V>> action) {
4303
      requireNonNull(action);
1 ✔
4304
      boolean[] advanced = { false };
1 ✔
4305
      Consumer<Node<K, V>> consumer = node -> {
1 ✔
4306
        long now = cache.expirationTicker().read();
1 ✔
4307
        boolean expired = cache.hasExpired(node, now);
1 ✔
4308
        K key = node.getKey();
1 ✔
4309
        V value = node.getValue();
1 ✔
4310
        if ((key == null) || (value == null) || (expired && !cache.isComputingAsync(value))) {
1 ✔
4311
          cache.scheduleDrainBuffers();
1 ✔
4312
        } else if (node.isAlive()) {
1 ✔
4313
          action.accept(new WriteThroughEntry<>(cache, key, value));
1 ✔
4314
          advanced[0] = true;
1 ✔
4315
        }
4316
      };
1 ✔
4317
      while (spliterator.tryAdvance(consumer)) {
1 ✔
4318
        if (advanced[0]) {
1 ✔
4319
          return true;
1 ✔
4320
        }
4321
      }
4322
      return false;
1 ✔
4323
    }
4324

4325
    @Override
4326
    public @Nullable Spliterator<Entry<K, V>> trySplit() {
4327
      Spliterator<Node<K, V>> split = spliterator.trySplit();
1 ✔
4328
      return (split == null) ? null : new EntrySpliterator<>(cache, split);
1 ✔
4329
    }
4330

4331
    @Override
4332
    public long estimateSize() {
4333
      return spliterator.estimateSize();
1 ✔
4334
    }
4335

4336
    @Override
4337
    public int characteristics() {
4338
      return DISTINCT | CONCURRENT | NONNULL;
1 ✔
4339
    }
4340
  }
4341

4342
  /** A reusable task that performs the maintenance work. */
4343
  static final class PerformCleanupTask
4344
      extends WeakReference<BoundedLocalCache<?, ?>> implements Runnable {
4345

4346
    PerformCleanupTask(BoundedLocalCache<?, ?> cache) {
4347
      super(cache);
1 ✔
4348
    }
1 ✔
4349

4350
    @Override
4351
    public void run() {
4352
      BoundedLocalCache<?, ?> cache = get();
1 ✔
4353
      // A thread waiting for the lock performs the maintenance, so a copy need not queue behind it
4354
      if ((cache != null) && !cache.isEvictionLockAwaited()) {
1 ✔
4355
        try {
4356
          cache.performCleanUp(/* ignored */ null);
1 ✔
4357
        } catch (Throwable t) {
1 ✔
4358
          logger.log(Level.ERROR, "Exception thrown when performing the maintenance task", t);
1 ✔
4359
        }
1 ✔
4360
      }
4361
    }
1 ✔
4362
    @Override public void clear() {}
1 ✔
4363
    @Override public boolean enqueue() {
4364
      return false;
1 ✔
4365
    }
4366
  }
4367

4368
  /* --------------- Manual Cache --------------- */
4369

4370
  static class BoundedLocalManualCache<K, V> implements LocalManualCache<K, V>, Serializable {
4371
    private static final long serialVersionUID = 1;
4372

4373
    final BoundedLocalCache<K, V> cache;
4374

4375
    @Nullable Policy<K, V> policy;
4376

4377
    BoundedLocalManualCache(Caffeine<K, V> builder) {
4378
      this(builder, null);
1 ✔
4379
    }
1 ✔
4380

4381
    BoundedLocalManualCache(Caffeine<K, V> builder, @Nullable CacheLoader<? super K, V> loader) {
1 ✔
4382
      cache = LocalCacheFactory.newBoundedLocalCache(builder, loader, /* isAsync= */ false);
1 ✔
4383
    }
1 ✔
4384

4385
    @Override
4386
    public final BoundedLocalCache<K, V> cache() {
4387
      return cache;
1 ✔
4388
    }
4389

4390
    @Override
4391
    public final Policy<K, V> policy() {
4392
      @Var var p = policy;
1 ✔
4393
      if (p == null) {
1 ✔
4394
        Function<@Nullable V, @Nullable V> identity = v -> v;
1 ✔
4395
        p = new BoundedPolicy<>(cache, identity);
1 ✔
4396
        policy = p;
1 ✔
4397
      }
4398
      return p;
1 ✔
4399
    }
4400

4401
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
4402
      throw new InvalidObjectException("Proxy required");
1 ✔
4403
    }
4404

4405
    private Object writeReplace() {
4406
      return makeSerializationProxy(cache);
1 ✔
4407
    }
4408
  }
4409

4410
  @SuppressWarnings({"NullableOptional",
4411
    "OptionalAssignedToNull", "OptionalUsedAsFieldOrParameterType"})
4412
  static final class BoundedPolicy<K, V> implements Policy<K, V> {
4413
    final Function<@Nullable V, @Nullable V> transformer;
4414
    final BoundedLocalCache<K, V> cache;
4415

4416
    @Nullable Optional<Eviction<K, V>> eviction;
4417
    @Nullable Optional<FixedRefresh<K, V>> refreshes;
4418
    @Nullable Optional<FixedExpiration<K, V>> afterWrite;
4419
    @Nullable Optional<FixedExpiration<K, V>> afterAccess;
4420
    @Nullable Optional<VarExpiration<K, V>> variable;
4421

4422
    BoundedPolicy(BoundedLocalCache<K, V> cache, Function<@Nullable V, @Nullable V> transformer) {
1 ✔
4423
      this.transformer = transformer;
1 ✔
4424
      this.cache = cache;
1 ✔
4425
    }
1 ✔
4426

4427
    @Override public boolean isRecordingStats() {
4428
      return cache.isRecordingStats();
1 ✔
4429
    }
4430
    @Override public @Nullable V getIfPresentQuietly(K key) {
4431
      return transformer.apply(cache.getIfPresentQuietly(key));
1 ✔
4432
    }
4433
    @SuppressWarnings("GuardedByChecker")
4434
    @Override public @Nullable CacheEntry<K, V> getEntryIfPresentQuietly(K key) {
4435
      Node<K, V> node = cache.data.get(cache.nodeFactory.newLookupKey(key));
1 ✔
4436
      return (node == null) ? null : cache.nodeToCacheEntry(node, transformer, node.getWeight());
1 ✔
4437
    }
4438
    @SuppressWarnings("Java9CollectionFactory")
4439
    @Override public Map<K, CompletableFuture<V>> refreshes() {
4440
      var refreshes = cache.refreshes;
1 ✔
4441
      if ((refreshes == null) || refreshes.isEmpty()) {
1 ✔
4442
        @SuppressWarnings({"ImmutableMapOf", "RedundantUnmodifiable"})
4443
        Map<K, CompletableFuture<V>> emptyMap = Collections.unmodifiableMap(Collections.emptyMap());
1 ✔
4444
        return emptyMap;
1 ✔
4445
      } else if (cache.collectKeys()) {
1 ✔
4446
        var inFlight = new IdentityHashMap<K, CompletableFuture<V>>(refreshes.size());
1 ✔
4447
        for (var entry : refreshes.entrySet()) {
1 ✔
4448
          @SuppressWarnings("unchecked")
4449
          var key = ((InternalReference<K>) entry.getKey()).get();
1 ✔
4450
          @SuppressWarnings("unchecked")
4451
          var future = (CompletableFuture<V>) entry.getValue();
1 ✔
4452
          if ((key != null) && !(future instanceof LocalLoadingCache.PendingRefresh)) {
1 ✔
4453
            inFlight.put(key, future);
1 ✔
4454
          }
4455
        }
1 ✔
4456
        return Collections.unmodifiableMap(inFlight);
1 ✔
4457
      }
4458
      @SuppressWarnings("unchecked")
4459
      var castedRefreshes = (Map<K, CompletableFuture<V>>) (Object) refreshes;
1 ✔
4460
      var inFlight = new HashMap<>(castedRefreshes);
1 ✔
4461
      inFlight.values().removeIf(LocalLoadingCache.PendingRefresh.class::isInstance);
1 ✔
4462
      return Collections.unmodifiableMap(inFlight);
1 ✔
4463
    }
4464
    @Override public Optional<Eviction<K, V>> eviction() {
4465
      if (!cache.evicts()) {
1 ✔
4466
        return Optional.empty();
1 ✔
4467
      }
4468
      var e = eviction;
1 ✔
4469
      return (e == null) ? (eviction = Optional.of(new BoundedEviction())) : e;
1 ✔
4470
    }
4471
    @Override public Optional<FixedExpiration<K, V>> expireAfterAccess() {
4472
      if (!cache.expiresAfterAccess()) {
1 ✔
4473
        return Optional.empty();
1 ✔
4474
      }
4475
      var e = afterAccess;
1 ✔
4476
      return (e == null)
1 ✔
4477
          ? (afterAccess = Optional.of(new BoundedExpireAfterAccess()))
1 ✔
4478
          : e;
1 ✔
4479
    }
4480
    @Override public Optional<FixedExpiration<K, V>> expireAfterWrite() {
4481
      if (!cache.expiresAfterWrite()) {
1 ✔
4482
        return Optional.empty();
1 ✔
4483
      }
4484
      var e = afterWrite;
1 ✔
4485
      return (e == null)
1 ✔
4486
          ? (afterWrite = Optional.of(new BoundedExpireAfterWrite()))
1 ✔
4487
          : e;
1 ✔
4488
    }
4489
    @Override public Optional<VarExpiration<K, V>> expireVariably() {
4490
      if (!cache.expiresVariable()) {
1 ✔
4491
        return Optional.empty();
1 ✔
4492
      }
4493
      var e = variable;
1 ✔
4494
      return (e == null)
1 ✔
4495
          ? (variable = Optional.of(new BoundedVarExpiration()))
1 ✔
4496
          : e;
1 ✔
4497
    }
4498
    @Override public Optional<FixedRefresh<K, V>> refreshAfterWrite() {
4499
      if (!cache.refreshAfterWrite()) {
1 ✔
4500
        return Optional.empty();
1 ✔
4501
      }
4502
      var r = refreshes;
1 ✔
4503
      return (r == null)
1 ✔
4504
          ? (refreshes = Optional.of(new BoundedRefreshAfterWrite()))
1 ✔
4505
          : r;
1 ✔
4506
    }
4507

4508
    final class BoundedEviction implements Eviction<K, V> {
1 ✔
4509
      @Override public boolean isWeighted() {
4510
        return cache.isWeighted;
1 ✔
4511
      }
4512
      @Override public OptionalInt weightOf(K key) {
4513
        requireNonNull(key);
1 ✔
4514
        if (!cache.isWeighted) {
1 ✔
4515
          return OptionalInt.empty();
1 ✔
4516
        }
4517
        Node<K, V> node = cache.data.get(cache.nodeFactory.newLookupKey(key));
1 ✔
4518
        if (node == null) {
1 ✔
4519
          return OptionalInt.empty();
1 ✔
4520
        }
4521
        boolean expired = cache.hasExpired(node, cache.expirationTicker().read());
1 ✔
4522
        if (expired) {
1 ✔
4523
          return OptionalInt.empty();
1 ✔
4524
        }
4525
        synchronized (node) {
1 ✔
4526
          V value = node.getValue();
1 ✔
4527
          return ((value != null) && node.isAlive() && !cache.isComputingAsync(value))
1 ✔
4528
              ? OptionalInt.of(node.getWeight())
1 ✔
4529
              : OptionalInt.empty();
1 ✔
4530
        }
4531
      }
4532
      @Override public OptionalLong weightedSize() {
4533
        return cache.isWeighted
1 ✔
4534
            ? OptionalLong.of(Math.max(0, cache.weightedSizeAcquire()))
1 ✔
4535
            : OptionalLong.empty();
1 ✔
4536
      }
4537
      @Override public long getMaximum() {
4538
        return cache.maximumAcquire();
1 ✔
4539
      }
4540
      @Override public void setMaximum(long maximum) {
4541
        requireArgument(maximum >= 0, "maximum must not be negative");
1 ✔
4542
        cache.evictionLock.lock();
1 ✔
4543
        try {
4544
          cache.setMaximumSize(maximum);
1 ✔
4545
          cache.maintenance(/* ignored */ null);
1 ✔
4546
        } finally {
4547
          cache.evictionLock.unlock();
1 ✔
4548
          cache.rescheduleCleanUpIfIncomplete();
1 ✔
4549
        }
4550
      }
1 ✔
4551
      @Override public Map<K, V> coldest(int limit) {
4552
        int expectedSize = Math.min(limit, cache.size());
1 ✔
4553
        var limiter = new SizeLimiter<K, V>(expectedSize, limit);
1 ✔
4554
        return cache.evictionOrder(/* hottest= */ false, transformer, limiter);
1 ✔
4555
      }
4556
      @Override public Map<K, V> coldestWeighted(long weightLimit) {
4557
        var limiter = isWeighted()
1 ✔
4558
            ? new WeightLimiter<K, V>(weightLimit)
1 ✔
4559
            : new SizeLimiter<K, V>((int) Math.min(weightLimit, cache.size()), weightLimit);
1 ✔
4560
        return cache.evictionOrder(/* hottest= */ false, transformer, limiter);
1 ✔
4561
      }
4562
      @Override
4563
      public <T extends @Nullable Object> T coldest(
4564
          Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4565
        requireNonNull(mappingFunction);
1 ✔
4566
        return cache.evictionOrder(/* hottest= */ false, transformer, mappingFunction);
1 ✔
4567
      }
4568
      @Override public Map<K, V> hottest(int limit) {
4569
        int expectedSize = Math.min(limit, cache.size());
1 ✔
4570
        var limiter = new SizeLimiter<K, V>(expectedSize, limit);
1 ✔
4571
        return cache.evictionOrder(/* hottest= */ true, transformer, limiter);
1 ✔
4572
      }
4573
      @Override public Map<K, V> hottestWeighted(long weightLimit) {
4574
        var limiter = isWeighted()
1 ✔
4575
            ? new WeightLimiter<K, V>(weightLimit)
1 ✔
4576
            : new SizeLimiter<K, V>((int) Math.min(weightLimit, cache.size()), weightLimit);
1 ✔
4577
        return cache.evictionOrder(/* hottest= */ true, transformer, limiter);
1 ✔
4578
      }
4579
      @Override
4580
      public <T extends @Nullable Object> T hottest(
4581
          Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4582
        requireNonNull(mappingFunction);
1 ✔
4583
        return cache.evictionOrder(/* hottest= */ true, transformer, mappingFunction);
1 ✔
4584
      }
4585
    }
4586

4587
    @SuppressWarnings("PreferJavaTimeOverload")
4588
    final class BoundedExpireAfterAccess implements FixedExpiration<K, V> {
1 ✔
4589
      @Override public OptionalLong ageOf(K key, TimeUnit unit) {
4590
        requireNonNull(key);
1 ✔
4591
        requireNonNull(unit);
1 ✔
4592
        Object lookupKey = cache.nodeFactory.newLookupKey(key);
1 ✔
4593
        Node<K, V> node = cache.data.get(lookupKey);
1 ✔
4594
        if (node == null) {
1 ✔
4595
          return OptionalLong.empty();
1 ✔
4596
        }
4597
        long now = cache.expirationTicker().read();
1 ✔
4598
        boolean expired = cache.hasExpired(node, now);
1 ✔
4599
        V value = node.getValue();
1 ✔
4600
        if ((value == null) || expired || cache.isComputingAsync(value)) {
1 ✔
4601
          return OptionalLong.empty();
1 ✔
4602
        }
4603
        long age = now - node.getAccessTime();
1 ✔
4604
        return (age < 0)
1 ✔
4605
            ? OptionalLong.empty()
1 ✔
4606
            : OptionalLong.of(unit.convert(age, TimeUnit.NANOSECONDS));
1 ✔
4607
      }
4608
      @Override public long getExpiresAfter(TimeUnit unit) {
4609
        return unit.convert(cache.expiresAfterAccessNanos(), TimeUnit.NANOSECONDS);
1 ✔
4610
      }
4611
      @Override public void setExpiresAfter(long duration, TimeUnit unit) {
4612
        requireArgument(duration >= 0, "duration cannot be negative: %s %s", duration, unit);
1 ✔
4613
        cache.setExpiresAfterAccessNanos(unit.toNanos(duration));
1 ✔
4614
        cache.scheduleAfterWrite();
1 ✔
4615
      }
1 ✔
4616
      @Override public Map<K, V> oldest(int limit) {
4617
        return oldest(new SizeLimiter<>(Math.min(limit, cache.size()), limit));
1 ✔
4618
      }
4619
      @Override public <T extends @Nullable Object> T oldest(
4620
          Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4621
        return cache.expireAfterAccessOrder(/* oldest= */ true, transformer, mappingFunction);
1 ✔
4622
      }
4623
      @Override public Map<K, V> youngest(int limit) {
4624
        return youngest(new SizeLimiter<>(Math.min(limit, cache.size()), limit));
1 ✔
4625
      }
4626
      @Override public <T extends @Nullable Object> T youngest(
4627
          Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4628
        return cache.expireAfterAccessOrder(/* oldest= */ false, transformer, mappingFunction);
1 ✔
4629
      }
4630
    }
4631

4632
    @SuppressWarnings("PreferJavaTimeOverload")
4633
    final class BoundedExpireAfterWrite implements FixedExpiration<K, V> {
1 ✔
4634
      @Override public OptionalLong ageOf(K key, TimeUnit unit) {
4635
        requireNonNull(key);
1 ✔
4636
        requireNonNull(unit);
1 ✔
4637
        Object lookupKey = cache.nodeFactory.newLookupKey(key);
1 ✔
4638
        Node<K, V> node = cache.data.get(lookupKey);
1 ✔
4639
        if (node == null) {
1 ✔
4640
          return OptionalLong.empty();
1 ✔
4641
        }
4642
        long now = cache.expirationTicker().read();
1 ✔
4643
        boolean expired = cache.hasExpired(node, now);
1 ✔
4644
        V value = node.getValue();
1 ✔
4645
        if ((value == null) || expired || cache.isComputingAsync(value)) {
1 ✔
4646
          return OptionalLong.empty();
1 ✔
4647
        }
4648
        long age = toWriteTime(now) - writeTimeOf(node);
1 ✔
4649
        return (age < 0)
1 ✔
4650
            ? OptionalLong.empty()
1 ✔
4651
            : OptionalLong.of(unit.convert(age, TimeUnit.NANOSECONDS));
1 ✔
4652
      }
4653
      @Override public long getExpiresAfter(TimeUnit unit) {
4654
        return unit.convert(cache.expiresAfterWriteNanos(), TimeUnit.NANOSECONDS);
1 ✔
4655
      }
4656
      @Override public void setExpiresAfter(long duration, TimeUnit unit) {
4657
        requireArgument(duration >= 0, "duration cannot be negative: %s %s", duration, unit);
1 ✔
4658
        cache.setExpiresAfterWriteNanos(unit.toNanos(duration));
1 ✔
4659
        cache.scheduleAfterWrite();
1 ✔
4660
      }
1 ✔
4661
      @Override public Map<K, V> oldest(int limit) {
4662
        return oldest(new SizeLimiter<>(Math.min(limit, cache.size()), limit));
1 ✔
4663
      }
4664
      @SuppressWarnings("GuardedByChecker")
4665
      @Override public <T extends @Nullable Object> T oldest(
4666
          Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4667
        return cache.snapshot(cache.writeOrderDeque(), transformer, mappingFunction);
1 ✔
4668
      }
4669
      @Override public Map<K, V> youngest(int limit) {
4670
        return youngest(new SizeLimiter<>(Math.min(limit, cache.size()), limit));
1 ✔
4671
      }
4672
      @SuppressWarnings("GuardedByChecker")
4673
      @Override public <T extends @Nullable Object> T youngest(
4674
          Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4675
        return cache.snapshot(cache.writeOrderDeque()::descendingIterator,
1 ✔
4676
            transformer, mappingFunction);
4677
      }
4678
    }
4679

4680
    @SuppressWarnings("PreferJavaTimeOverload")
4681
    final class BoundedVarExpiration implements VarExpiration<K, V> {
1 ✔
4682
      @Override public OptionalLong getExpiresAfter(K key, TimeUnit unit) {
4683
        requireNonNull(key);
1 ✔
4684
        requireNonNull(unit);
1 ✔
4685
        Object lookupKey = cache.nodeFactory.newLookupKey(key);
1 ✔
4686
        Node<K, V> node = cache.data.get(lookupKey);
1 ✔
4687
        if (node == null) {
1 ✔
4688
          return OptionalLong.empty();
1 ✔
4689
        }
4690
        long now = cache.expirationTicker().read();
1 ✔
4691
        boolean expired = cache.hasExpired(node, now);
1 ✔
4692
        V value = node.getValue();
1 ✔
4693
        if ((value == null) || expired || cache.isComputingAsync(value)) {
1 ✔
4694
          return OptionalLong.empty();
1 ✔
4695
        }
4696
        long duration = node.getVariableTime() - now;
1 ✔
4697
        return (cache.isAsync && (duration > MAXIMUM_EXPIRY))
1 ✔
4698
            ? OptionalLong.empty()
1 ✔
4699
            : OptionalLong.of(unit.convert(duration, TimeUnit.NANOSECONDS));
1 ✔
4700
      }
4701
      @Override public void setExpiresAfter(K key, long duration, TimeUnit unit) {
4702
        requireNonNull(key);
1 ✔
4703
        requireNonNull(unit);
1 ✔
4704
        requireArgument(duration >= 0, "duration cannot be negative: %s %s", duration, unit);
1 ✔
4705
        Object lookupKey = cache.nodeFactory.newLookupKey(key);
1 ✔
4706
        Node<K, V> node = cache.data.get(lookupKey);
1 ✔
4707
        if (node != null) {
1 ✔
4708
          long extension;
4709
          long durationNanos = TimeUnit.NANOSECONDS.convert(duration, unit);
1 ✔
4710
          synchronized (node) {
1 ✔
4711
            long now = cache.expirationTicker().read();
1 ✔
4712
            V value = node.getValue();
1 ✔
4713
            if ((value == null) || cache.isComputingAsync(value)
1 ✔
4714
                || cache.hasExpired(node, now)) {
1 ✔
4715
              return;
1 ✔
4716
            }
4717
            long expirationTime = now + Math.min(durationNanos, MAXIMUM_EXPIRY);
1 ✔
4718
            extension = expirationTime - node.getVariableTime();
1 ✔
4719
            node.setVariableTime(expirationTime);
1 ✔
4720
          }
1 ✔
4721
          if ((extension < 0) || (extension > EXPIRE_TOLERANCE)) {
1 ✔
4722
            cache.afterWrite(cache.new UpdateTask(node, 0, Access.QUIET));
1 ✔
4723
          }
4724
        }
4725
      }
1 ✔
4726
      @Override public @Nullable V put(K key, V value, long duration, TimeUnit unit) {
4727
        requireNonNull(unit);
1 ✔
4728
        requireNonNull(value);
1 ✔
4729
        requireArgument(duration >= 0, "duration cannot be negative: %s %s", duration, unit);
1 ✔
4730
        return cache.isAsync
1 ✔
4731
            ? putAsync(key, value, duration, unit)
1 ✔
4732
            : putSync(key, value, duration, unit, /* onlyIfAbsent= */ false);
1 ✔
4733
      }
4734
      @Override public @Nullable V putIfAbsent(K key, V value, long duration, TimeUnit unit) {
4735
        requireNonNull(unit);
1 ✔
4736
        requireNonNull(value);
1 ✔
4737
        requireArgument(duration >= 0, "duration cannot be negative: %s %s", duration, unit);
1 ✔
4738
        return cache.isAsync
1 ✔
4739
            ? putIfAbsentAsync(key, value, duration, unit)
1 ✔
4740
            : putSync(key, value, duration, unit, /* onlyIfAbsent= */ true);
1 ✔
4741
      }
4742
      @Nullable V putSync(K key, V value, long duration, TimeUnit unit, boolean onlyIfAbsent) {
4743
        var expiry = new FixedExpireAfterWrite<K, V>(duration, unit);
1 ✔
4744
        return cache.put(key, value, expiry, onlyIfAbsent);
1 ✔
4745
      }
4746
      @SuppressWarnings("unchecked")
4747
      @Nullable V putIfAbsentAsync(K key, V value, long duration, TimeUnit unit) {
4748
        // Mirrors LocalAsyncCache.AsMapView#putIfAbsent(key, value), but the first probe reads a
4749
        // present entry through the fixed duration, as putSync does, not the user's Expiry
4750
        var expiry = (Expiry<K, V>) new AsyncExpiry<>(new FixedExpireAfterWrite<>(duration, unit));
1 ✔
4751
        var asyncValue = (V) CompletableFuture.completedFuture(value);
1 ✔
4752

4753
        var probe = (CompletableFuture<V>) cache.put(
1 ✔
4754
            key, asyncValue, expiry, /* onlyIfAbsent= */ true);
4755
        V present = Async.getIfReady(probe);
1 ✔
4756
        if ((probe == null) || (present != null)) {
1 ✔
4757
          return present;
1 ✔
4758
        }
4759

4760
        for (;;) {
4761
          var priorFuture = (CompletableFuture<V>) cache.getIfPresentQuietly(key);
1 ✔
4762
          if (priorFuture != null) {
1 ✔
4763
            if (!priorFuture.isDone()) {
1 ✔
4764
              Async.getWhenSuccessful(priorFuture);
1 ✔
4765
              continue;
1 ✔
4766
            }
4767

4768
            V prior = Async.getWhenSuccessful(priorFuture);
1 ✔
4769
            if (prior != null) {
1 ✔
4770
              return prior;
1 ✔
4771
            }
4772
          }
4773

4774
          boolean[] added = { false };
1 ✔
4775
          var hints = new LocalCache.RemapHints();
1 ✔
4776
          var computed = (CompletableFuture<V>) cache.compute(key, (K k, @Nullable V oldValue) -> {
1 ✔
4777
            var oldValueFuture = (CompletableFuture<V>) oldValue;
1 ✔
4778
            added[0] = (oldValueFuture == null)
1 ✔
4779
                || (oldValueFuture.isDone() && (Async.getIfReady(oldValueFuture) == null));
1 !
4780
            if (added[0]) {
1 ✔
4781
              return asyncValue;
1 ✔
4782
            }
4783
            hints.preserveEntry();
1 ✔
4784
            return oldValue;
1 ✔
4785
          }, expiry, /* recordLoad= */ false, hints);
4786

4787
          if (added[0]) {
1 ✔
4788
            return null;
1 ✔
4789
          } else {
4790
            V prior = Async.getWhenSuccessful(computed);
1 ✔
4791
            if (prior != null) {
1 !
UNCOV
4792
              return prior;
×
4793
            }
4794
          }
4795
        }
1 ✔
4796
      }
4797
      @SuppressWarnings("unchecked")
4798
      @Nullable V putAsync(K key, V value, long duration, TimeUnit unit) {
4799
        var expiry = (Expiry<K, V>) new AsyncExpiry<>(new FixedExpireAfterWrite<>(duration, unit));
1 ✔
4800
        var asyncValue = (V) CompletableFuture.completedFuture(value);
1 ✔
4801

4802
        var oldValueFuture = (CompletableFuture<V>) cache.put(
1 ✔
4803
            key, asyncValue, expiry, /* onlyIfAbsent= */ false);
4804
        return Async.getWhenSuccessful(oldValueFuture);
1 ✔
4805
      }
4806
      @Override public @Nullable V compute(K key,
4807
          BiFunction<? super K, ? super @Nullable V, ? extends @Nullable V> remappingFunction,
4808
          Duration duration) {
4809
        requireNonNull(key);
1 ✔
4810
        requireNonNull(duration);
1 ✔
4811
        requireNonNull(remappingFunction);
1 ✔
4812
        requireArgument(!duration.isNegative(), "duration cannot be negative: %s", duration);
1 ✔
4813
        var expiry = new FixedExpireAfterWrite<K, V>(
1 ✔
4814
            toNanosSaturated(duration), TimeUnit.NANOSECONDS);
1 ✔
4815

4816
        return cache.isAsync
1 ✔
4817
            ? computeAsync(key, remappingFunction, expiry)
1 ✔
4818
            : cache.compute(key, remappingFunction, expiry, /* recordLoad= */ true);
1 ✔
4819
      }
4820
      @Nullable V computeAsync(K key,
4821
          BiFunction<? super K, ? super @Nullable V, ? extends @Nullable V> remappingFunction,
4822
          Expiry<? super K, ? super V> expiry) {
4823
        // Keep in sync with LocalAsyncCache.AsMapView#compute(key, remappingFunction)
4824
        @SuppressWarnings("unchecked")
4825
        var delegate = (LocalCache<K, CompletableFuture<V>>) cache;
1 ✔
4826

4827
        @SuppressWarnings({"unchecked", "Varifier"})
4828
        @Nullable V[] newValue = (@Nullable V[]) new Object[1];
1 ✔
4829
        for (;;) {
4830
          Async.getWhenSuccessful(delegate.getIfPresentQuietly(key));
1 ✔
4831

4832
          var hints = new LocalCache.RemapHints();
1 ✔
4833
          CompletableFuture<V> valueFuture = delegate.compute(
1 ✔
4834
              key, (K k, @Nullable CompletableFuture<V> oldValueFuture) -> {
4835
                if ((oldValueFuture != null) && !oldValueFuture.isDone()) {
1 ✔
4836
                  hints.preserveEntry();
1 ✔
4837
                  return oldValueFuture;
1 ✔
4838
                }
4839

4840
                V oldValue = Async.getIfReady(oldValueFuture);
1 ✔
4841
                BiFunction<? super K, ? super @Nullable V, ? extends @Nullable V> function =
1 ✔
4842
                    delegate.statsAware(remappingFunction, /* recordLoad= */ true);
1 ✔
4843
                newValue[0] = function.apply(key, oldValue);
1 ✔
4844
                return (newValue[0] == null) ? null
1 ✔
4845
                    : CompletableFuture.completedFuture(newValue[0]);
1 ✔
4846
              }, new AsyncExpiry<>(expiry), /* recordLoad= */ false, hints);
4847

4848
          if (newValue[0] != null) {
1 ✔
4849
            return newValue[0];
1 ✔
4850
          } else if (valueFuture == null) {
1 ✔
4851
            return null;
1 ✔
4852
          }
4853
        }
1 ✔
4854
      }
4855
      @Override public Map<K, V> oldest(int limit) {
4856
        return oldest(new SizeLimiter<>(Math.min(limit, cache.size()), limit));
1 ✔
4857
      }
4858
      @Override public <T extends @Nullable Object> T oldest(
4859
          Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4860
        return cache.snapshot(cache.timerWheel(), transformer, mappingFunction);
1 ✔
4861
      }
4862
      @Override public Map<K, V> youngest(int limit) {
4863
        return youngest(new SizeLimiter<>(Math.min(limit, cache.size()), limit));
1 ✔
4864
      }
4865
      @Override public <T extends @Nullable Object> T youngest(
4866
          Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
4867
        return cache.snapshot(cache.timerWheel()::descendingIterator, transformer, mappingFunction);
1 ✔
4868
      }
4869
    }
4870

4871
    static final class FixedExpireAfterWrite<K, V> implements Expiry<K, V> {
4872
      final long duration;
4873
      final TimeUnit unit;
4874

4875
      FixedExpireAfterWrite(long duration, TimeUnit unit) {
1 ✔
4876
        this.duration = duration;
1 ✔
4877
        this.unit = unit;
1 ✔
4878
      }
1 ✔
4879
      @Override public long expireAfterCreate(K key, V value, long currentTime) {
4880
        return unit.toNanos(duration);
1 ✔
4881
      }
4882
      @Override public long expireAfterUpdate(
4883
          K key, V value, long currentTime, long currentDuration) {
4884
        return unit.toNanos(duration);
1 ✔
4885
      }
4886
      @CanIgnoreReturnValue
4887
      @Override public long expireAfterRead(
4888
          K key, V value, long currentTime, long currentDuration) {
4889
        return currentDuration;
1 ✔
4890
      }
4891
    }
4892

4893
    @SuppressWarnings("PreferJavaTimeOverload")
4894
    final class BoundedRefreshAfterWrite implements FixedRefresh<K, V> {
1 ✔
4895
      @Override public OptionalLong ageOf(K key, TimeUnit unit) {
4896
        requireNonNull(key);
1 ✔
4897
        requireNonNull(unit);
1 ✔
4898
        Object lookupKey = cache.nodeFactory.newLookupKey(key);
1 ✔
4899
        Node<K, V> node = cache.data.get(lookupKey);
1 ✔
4900
        if (node == null) {
1 ✔
4901
          return OptionalLong.empty();
1 ✔
4902
        }
4903
        long now = cache.expirationTicker().read();
1 ✔
4904
        boolean expired = cache.hasExpired(node, now);
1 ✔
4905
        V value = node.getValue();
1 ✔
4906
        if ((value == null) || expired || cache.isComputingAsync(value)) {
1 ✔
4907
          return OptionalLong.empty();
1 ✔
4908
        }
4909
        long age = toWriteTime(now) - writeTimeOf(node);
1 ✔
4910
        return (age < 0)
1 ✔
4911
            ? OptionalLong.empty()
1 ✔
4912
            : OptionalLong.of(unit.convert(age, TimeUnit.NANOSECONDS));
1 ✔
4913
      }
4914
      @Override public long getRefreshesAfter(TimeUnit unit) {
4915
        return unit.convert(cache.refreshAfterWriteNanos(), TimeUnit.NANOSECONDS);
1 ✔
4916
      }
4917
      @Override public void setRefreshesAfter(long duration, TimeUnit unit) {
4918
        requireNonNull(unit);
1 ✔
4919
        requireArgument(duration > 0, "duration must be positive: %s %s", duration, unit);
1 ✔
4920
        cache.setRefreshAfterWriteNanos(unit.toNanos(duration));
1 ✔
4921
        cache.scheduleAfterWrite();
1 ✔
4922
      }
1 ✔
4923
    }
4924
  }
4925

4926
  /* --------------- Loading Cache --------------- */
4927

4928
  static final class BoundedLocalLoadingCache<K, V>
4929
      extends BoundedLocalManualCache<K, V> implements LocalLoadingCache<K, V> {
4930
    private static final long serialVersionUID = 1;
4931

4932
    final Function<K, @Nullable V> mappingFunction;
4933
    final @Nullable Function<Set<? extends K>, Map<K, V>> bulkMappingFunction;
4934

4935
    BoundedLocalLoadingCache(Caffeine<K, V> builder, CacheLoader<? super K, V> loader) {
4936
      super(builder, loader);
1 ✔
4937
      requireNonNull(loader);
1 ✔
4938
      mappingFunction = newMappingFunction(loader);
1 ✔
4939
      bulkMappingFunction = newBulkMappingFunction(loader);
1 ✔
4940
    }
1 ✔
4941

4942
    @Override
4943
    @SuppressWarnings({"DataFlowIssue", "NullAway"})
4944
    public AsyncCacheLoader<? super K, V> cacheLoader() {
4945
      return cache.cacheLoader;
1 ✔
4946
    }
4947

4948
    @Override
4949
    public Function<K, @Nullable V> mappingFunction() {
4950
      return mappingFunction;
1 ✔
4951
    }
4952

4953
    @Override
4954
    public @Nullable Function<Set<? extends K>, Map<K, V>> bulkMappingFunction() {
4955
      return bulkMappingFunction;
1 ✔
4956
    }
4957

4958
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
4959
      throw new InvalidObjectException("Proxy required");
1 ✔
4960
    }
4961

4962
    private Object writeReplace() {
4963
      return makeSerializationProxy(cache);
1 ✔
4964
    }
4965
  }
4966

4967
  /* --------------- Async Cache --------------- */
4968

4969
  static final class BoundedLocalAsyncCache<K, V> implements LocalAsyncCache<K, V>, Serializable {
4970
    private static final long serialVersionUID = 1;
4971

4972
    final BoundedLocalCache<K, CompletableFuture<V>> cache;
4973

4974
    @Nullable ConcurrentMap<K, CompletableFuture<V>> mapView;
4975
    @Nullable CacheView<K, V> cacheView;
4976
    @Nullable Policy<K, V> policy;
4977

4978
    @SuppressWarnings("unchecked")
4979
    BoundedLocalAsyncCache(Caffeine<K, V> builder) {
1 ✔
4980
      cache = (BoundedLocalCache<K, CompletableFuture<V>>) LocalCacheFactory
1 ✔
4981
          .newBoundedLocalCache(builder, /* cacheLoader= */ null, /* isAsync= */ true);
1 ✔
4982
    }
1 ✔
4983

4984
    @Override
4985
    public BoundedLocalCache<K, CompletableFuture<V>> cache() {
4986
      return cache;
1 ✔
4987
    }
4988

4989
    @Override
4990
    public ConcurrentMap<K, CompletableFuture<V>> asMap() {
4991
      var mv = mapView;
1 ✔
4992
      return (mv == null) ? (mapView = new AsyncAsMapView<>(this)) : mv;
1 ✔
4993
    }
4994

4995
    @Override
4996
    public Cache<K, V> synchronous() {
4997
      var cv = cacheView;
1 ✔
4998
      return (cv == null) ? (cacheView = new CacheView<>(this)) : cv;
1 ✔
4999
    }
5000

5001
    @Override
5002
    public Policy<K, V> policy() {
5003
      @Var var p = policy;
1 ✔
5004
      if (p == null) {
1 ✔
5005
        @SuppressWarnings("unchecked")
5006
        var castCache = (BoundedLocalCache<K, V>) cache;
1 ✔
5007
        Function<CompletableFuture<V>, @Nullable V> transformer = Async::getIfReady;
1 ✔
5008
        @SuppressWarnings("unchecked")
5009
        var castTransformer = (Function<@Nullable V, @Nullable V>) transformer;
1 ✔
5010
        p = new BoundedPolicy<>(castCache, castTransformer);
1 ✔
5011
        policy = p;
1 ✔
5012
      }
5013
      return p;
1 ✔
5014
    }
5015

5016
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
5017
      throw new InvalidObjectException("Proxy required");
1 ✔
5018
    }
5019

5020
    private Object writeReplace() {
5021
      return makeSerializationProxy(cache);
1 ✔
5022
    }
5023
  }
5024

5025
  /* --------------- Async Loading Cache --------------- */
5026

5027
  static final class BoundedLocalAsyncLoadingCache<K, V>
5028
      extends LocalAsyncLoadingCache<K, V> implements Serializable {
5029
    private static final long serialVersionUID = 1;
5030

5031
    final BoundedLocalCache<K, CompletableFuture<V>> cache;
5032

5033
    @Nullable ConcurrentMap<K, CompletableFuture<V>> mapView;
5034
    @Nullable Policy<K, V> policy;
5035

5036
    @SuppressWarnings("unchecked")
5037
    BoundedLocalAsyncLoadingCache(Caffeine<K, V> builder, AsyncCacheLoader<? super K, V> loader) {
5038
      super(loader);
1 ✔
5039
      cache = (BoundedLocalCache<K, CompletableFuture<V>>) LocalCacheFactory
1 ✔
5040
          .newBoundedLocalCache(builder, loader, /* isAsync= */ true);
1 ✔
5041
    }
1 ✔
5042

5043
    @Override
5044
    public BoundedLocalCache<K, CompletableFuture<V>> cache() {
5045
      return cache;
1 ✔
5046
    }
5047

5048
    @Override
5049
    public ConcurrentMap<K, CompletableFuture<V>> asMap() {
5050
      var mv = mapView;
1 ✔
5051
      return (mv == null) ? (mapView = new AsyncAsMapView<>(this)) : mv;
1 ✔
5052
    }
5053

5054
    @Override
5055
    public Policy<K, V> policy() {
5056
      @Var var p = policy;
1 ✔
5057
      if (p == null) {
1 ✔
5058
        @SuppressWarnings("unchecked")
5059
        var castCache = (BoundedLocalCache<K, V>) cache;
1 ✔
5060
        Function<CompletableFuture<V>, @Nullable V> transformer = Async::getIfReady;
1 ✔
5061
        @SuppressWarnings("unchecked")
5062
        var castTransformer = (Function<@Nullable V, @Nullable V>) transformer;
1 ✔
5063
        p = new BoundedPolicy<>(castCache, castTransformer);
1 ✔
5064
        policy = p;
1 ✔
5065
      }
5066
      return p;
1 ✔
5067
    }
5068

5069
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
5070
      throw new InvalidObjectException("Proxy required");
1 ✔
5071
    }
5072

5073
    private Object writeReplace() {
5074
      return makeSerializationProxy(cache);
1 ✔
5075
    }
5076
  }
5077
}
5078

5079
/** The namespace for field padding through inheritance. */
5080
@SuppressWarnings({"IdentifierName", "MultiVariableDeclaration"})
5081
final class BLCHeader {
5082

5083
  private BLCHeader() {}
5084

5085
  @SuppressWarnings("unused")
5086
  static class PadDrainStatus {
1 ✔
5087
    byte p000, p001, p002, p003, p004, p005, p006, p007;
5088
    byte p008, p009, p010, p011, p012, p013, p014, p015;
5089
    byte p016, p017, p018, p019, p020, p021, p022, p023;
5090
    byte p024, p025, p026, p027, p028, p029, p030, p031;
5091
    byte p032, p033, p034, p035, p036, p037, p038, p039;
5092
    byte p040, p041, p042, p043, p044, p045, p046, p047;
5093
    byte p048, p049, p050, p051, p052, p053, p054, p055;
5094
    byte p056, p057, p058, p059, p060, p061, p062, p063;
5095
    byte p064, p065, p066, p067, p068, p069, p070, p071;
5096
    byte p072, p073, p074, p075, p076, p077, p078, p079;
5097
    byte p080, p081, p082, p083, p084, p085, p086, p087;
5098
    byte p088, p089, p090, p091, p092, p093, p094, p095;
5099
    byte p096, p097, p098, p099, p100, p101, p102, p103;
5100
    byte p104, p105, p106, p107, p108, p109, p110, p111;
5101
    byte p112, p113, p114, p115, p116, p117, p118, p119;
5102
  }
5103

5104
  /** Enforces a memory layout to avoid false sharing by padding the drain status. */
5105
  abstract static class DrainStatusRef extends PadDrainStatus {
1 ✔
5106
    static final VarHandle DRAIN_STATUS = fieldVarHandle(MethodHandles.lookup(),
1 ✔
5107
        "drainStatus", VarHandle.class, DrainStatusRef.class, int.class);
5108

5109
    /** A drain is not taking place. */
5110
    static final int IDLE = 0;
5111
    /** A drain is required due to a pending write modification. */
5112
    static final int REQUIRED = 1;
5113
    /** A drain is in progress and will transition to idle. */
5114
    static final int PROCESSING_TO_IDLE = 2;
5115
    /** A drain is in progress and will transition to required. */
5116
    static final int PROCESSING_TO_REQUIRED = 3;
5117

5118
    /** The draining status of the buffers. */
5119
    volatile int drainStatus = IDLE;
1 ✔
5120

5121
    /**
5122
     * Returns whether maintenance work is needed.
5123
     *
5124
     * @param delayable if draining the read buffer can be delayed
5125
     */
5126
    @SuppressWarnings("StatementSwitchToExpressionSwitch")
5127
    boolean shouldDrainBuffers(boolean delayable) {
5128
      switch (drainStatusOpaque()) {
1 ✔
5129
        case IDLE:
5130
          return !delayable;
1 ✔
5131
        case REQUIRED:
5132
          return true;
1 ✔
5133
        case PROCESSING_TO_IDLE:
5134
        case PROCESSING_TO_REQUIRED:
5135
          return false;
1 ✔
5136
        default:
5137
          throw new IllegalStateException("Invalid drain status: " + drainStatus);
1 ✔
5138
      }
5139
    }
5140

5141
    int drainStatusOpaque() {
5142
      return (int) DRAIN_STATUS.getOpaque(this);
1 ✔
5143
    }
5144

5145
    int drainStatusAcquire() {
5146
      return (int) DRAIN_STATUS.getAcquire(this);
1 ✔
5147
    }
5148

5149
    void setDrainStatusOpaque(int drainStatus) {
5150
      DRAIN_STATUS.setOpaque(this, drainStatus);
1 ✔
5151
    }
1 ✔
5152

5153
    void setDrainStatusRelease(int drainStatus) {
5154
      DRAIN_STATUS.setRelease(this, drainStatus);
1 ✔
5155
    }
1 ✔
5156

5157
    boolean casDrainStatus(int expect, int update) {
5158
      return DRAIN_STATUS.compareAndSet(this, expect, update);
1 ✔
5159
    }
5160
  }
5161
}
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