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

ben-manes / caffeine / #5830

19 Oct 2026 03:02AM UTC coverage: 99.947% (-0.03%) from 99.978%
#5830

push

github

ben-manes
rerun expiration promptly after a spent budget

A cycle that spends the timer wheel's budget rewinds the wheel's clock,
and expireEntries then armed the pacer at the wheel's delay, measured
from that clock. With a Scheduler and an executor other than the common
pool, each remaining bucket waited the distance to its original time:
1,000 entries due in each of five buckets drained one bucket per ~12s.
A cycle that already knows it is incomplete now asks the pacer for the
next run at once, which drained them in 6s.

Discovered by Anthropic's OSS Scanner, as ANT-2026-RJ1G50WC.

4871 of 4882 branches covered (99.77%)

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

5 existing lines in 2 files now uncovered.

9343 of 9348 relevant lines covered (99.95%)

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.castNonNull;
26
import static com.github.benmanes.caffeine.cache.LocalCache.nullRef;
27
import static com.github.benmanes.caffeine.cache.LocalLoadingCache.newBulkMappingFunction;
28
import static com.github.benmanes.caffeine.cache.LocalLoadingCache.newMappingFunction;
29
import static com.github.benmanes.caffeine.cache.Node.PROBATION;
30
import static com.github.benmanes.caffeine.cache.Node.PROTECTED;
31
import static com.github.benmanes.caffeine.cache.Node.WINDOW;
32
import static java.lang.invoke.ConstantBootstraps.fieldVarHandle;
33
import static java.util.Locale.US;
34
import static java.util.Objects.requireNonNull;
35
import static java.util.Spliterator.DISTINCT;
36
import static java.util.Spliterator.IMMUTABLE;
37
import static java.util.Spliterator.NONNULL;
38
import static java.util.Spliterator.ORDERED;
39

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

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

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

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

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

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

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

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

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

263
  final boolean isWeighted;
264
  final boolean isAsync;
265

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

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

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

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

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

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

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

323
  /* --------------- Shared --------------- */
324

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

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

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

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

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

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

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

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

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

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

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

397
  /* --------------- Stats Support --------------- */
398

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

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

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

414
  /* --------------- Removal Listener Support --------------- */
415

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

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

445
  /* --------------- Eviction Listener Support --------------- */
446

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

461
  /* --------------- Reference Support --------------- */
462

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

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

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

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

483
  /* --------------- Expiration Support --------------- */
484

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

562
  /* --------------- Eviction Support --------------- */
563

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

671
    long window = max - (long) (PERCENT_MAIN * max);
1 ✔
672
    long mainProtected = (long) (PERCENT_MAIN_PROTECTED * (max - window));
1 ✔
673

674
    setMaximum(max);
1 ✔
675
    setWindowMaximum(window);
1 ✔
676
    setMainProtectedMaximum(mainProtected);
1 ✔
677

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

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

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

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

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

735
    return first;
1 ✔
736
  }
737

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

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

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

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

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

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

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

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

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

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

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

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

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

887
    Pacer pacer = pacer();
1 ✔
888
    if (pacer != null) {
1 ✔
889
      // A cycle that reached a limit runs again as soon as the pacer allows
890
      long delay = (drainStatusOpaque() == PROCESSING_TO_REQUIRED) ? 0L : getExpirationDelay(now);
1 ✔
891
      if (delay == Long.MAX_VALUE) {
1 ✔
892
        pacer.cancel();
1 ✔
893
      } else {
894
        pacer.schedule(executor, drainBuffersTask, now, delay);
1 ✔
895
      }
896
    }
897
  }
1 ✔
898

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1129
    synchronized (node) {
1 ✔
1130
      logIfAlive(node);
1 ✔
1131
      makeDead(node);
1 ✔
1132
    }
1 ✔
1133

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

1140
    return true;
1 ✔
1141
  }
1142

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

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

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

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

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

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

1201
      long weight = candidate.getPolicyWeight();
1 ✔
1202
      if (quota < weight) {
1 ✔
1203
        break;
1 ✔
1204
      }
1205

1206
      quota -= weight;
1 ✔
1207
      transfer(candidate, weight, probation ? PROBATION : PROTECTED, WINDOW);
1 ✔
1208
    }
1209

1210
    setMainProtectedMaximum(mainProtectedMaximum() + quota);
1 ✔
1211
    setWindowMaximum(windowMaximum() - quota);
1 ✔
1212
    climber().carryOver(quota);
1 ✔
1213
  }
1 ✔
1214

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

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

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

1243
    setMainProtectedMaximum(mainProtectedMaximum() - quota);
1 ✔
1244
    setWindowMaximum(windowMaximum() + quota);
1 ✔
1245
    climber().carryOver(-quota);
1 ✔
1246
  }
1 ✔
1247

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

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

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

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

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

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

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

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

1389
      if (refreshFuture[0] == null) {
1 ✔
1390
        return null;
1 ✔
1391
      }
1392

1393
      var refreshed = refreshFuture[0].handle((newValue, error) -> {
1 ✔
1394
        long loadTime = statsTicker().read() - startTime[0];
1 ✔
1395
        if (error != null) {
1 ✔
1396
          if (!(error instanceof CancellationException) && !(error instanceof TimeoutException)) {
1 ✔
1397
            logger.log(Level.WARNING, "Exception thrown during refresh", error);
1 ✔
1398
          }
1399
          refreshes.remove(keyReference, refreshFuture[0]);
1 ✔
1400
          statsCounter().recordLoadFailure(loadTime);
1 ✔
1401
          return null;
1 ✔
1402
        }
1403

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

1451
        if (cause[0] != null) {
1 ✔
1452
          notifyRemoval(key, value, cause[0]);
1 ✔
1453
        }
1454
        if (newValue == null) {
1 ✔
1455
          statsCounter().recordLoadFailure(loadTime);
1 ✔
1456
        } else {
1457
          statsCounter().recordLoadSuccess(loadTime);
1 ✔
1458
        }
1459
        return result;
1 ✔
1460
      });
1461
      return Async.getIfReady(refreshed);
1 ✔
1462
    }
1463

1464
    return null;
1 ✔
1465
  }
1466

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

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

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

1530
  /**
1531
   * Attempts to update the access time for the entry after a read.
1532
   *
1533
   * @param node the entry in the page replacement policy
1534
   * @param key the key of the entry that was read
1535
   * @param value the value of the entry that was read
1536
   * @param expiry the calculator for the expiration time
1537
   * @param now the current time, in nanoseconds
1538
   */
1539
  void tryExpireAfterRead(Node<K, V> node, K key, V value, Expiry<K, V> expiry, long now) {
1540
    if (!expiresVariable()) {
1 ✔
1541
      return;
1 ✔
1542
    }
1543

1544
    long variableTime = node.getVariableTime();
1 ✔
1545
    long currentDuration = Math.max(0L, variableTime - now);
1 ✔
1546
    if (isAsync && (currentDuration > MAXIMUM_EXPIRY)) {
1 ✔
1547
      // expireAfterCreate has not yet set the duration after completion
1548
      return;
1 ✔
1549
    }
1550

1551
    long tolerance = EXPIRE_TOLERANCE;
1 ✔
1552
    long duration = Math.max(0L, expiry.expireAfterRead(key, value, now, currentDuration));
1 ✔
1553
    long expirationTime = expiresAt(now, duration);
1 ✔
1554
    long extension = expirationTime - variableTime;
1 ✔
1555
    if ((extension != 0)
1 ✔
1556
        && ((duration <= tolerance) || (extension < 0) || (extension > tolerance))) {
1557
      VarHandle.loadLoadFence();
1 ✔
1558
      if (node.getValue() == value) {
1 ✔
1559
        node.casVariableTime(variableTime, expirationTime);
1 ✔
1560
      }
1561
    }
1562
  }
1 ✔
1563

1564
  void setAccessTime(Node<K, V> node, long now) {
1565
    if (!expiresAfterAccess()) {
1 ✔
1566
      return;
1 ✔
1567
    }
1568
    long tolerance = EXPIRE_TOLERANCE;
1 ✔
1569
    long accessTime = node.getAccessTime();
1 ✔
1570
    if ((expiresAfterAccessNanos() <= tolerance) || (Math.abs(now - accessTime) > tolerance)) {
1 ✔
1571
      node.setAccessTime(now);
1 ✔
1572
    }
1573
  }
1 ✔
1574

1575
  void setWriteTime(Node<K, V> node, long now) {
1576
    if (expiresAfterWrite() || refreshAfterWrite()) {
1 ✔
1577
      // The write time reserves its low bit to mark that a refresh is in flight, so a recorded time
1578
      // is always even and an age must be measured against an evenly truncated clock
1579
      node.setWriteTime(toWriteTime(now));
1 ✔
1580
    }
1581
  }
1 ✔
1582

1583
  void setVariableTime(Node<K, V> node, long expirationTime) {
1584
    if (expiresVariable()) {
1 ✔
1585
      node.setVariableTime(expirationTime);
1 ✔
1586
    }
1587
  }
1 ✔
1588

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

1594
  /** Returns if the entry's write time would exceed the minimum expiration reorder threshold. */
1595
  boolean exceedsWriteTimeTolerance(Node<K, V> node, long varTime, long now) {
1596
    long variableTime = node.getVariableTime();
1 ✔
1597
    long writeTime = node.getWriteTime();
1 ✔
1598
    long tolerance = EXPIRE_TOLERANCE;
1 ✔
1599
    return
1 ✔
1600
        (expiresAfterWrite()
1 ✔
1601
            && ((expiresAfterWriteNanos() <= tolerance) || (Math.abs(now - writeTime) > tolerance)))
1 ✔
1602
        || (refreshAfterWrite()
1 ✔
1603
            && ((refreshAfterWriteNanos() <= tolerance) || (Math.abs(now - writeTime) > tolerance)))
1 ✔
1604
        || (expiresVariable() && (Math.abs(varTime - variableTime) > tolerance));
1 ✔
1605
  }
1606

1607
  /** Returns the expiration time for a duration. */
1608
  long expiresAt(long now, long duration) {
1609
    return isAsync ? (now + duration) : (now + Math.min(duration, MAXIMUM_EXPIRY));
1 ✔
1610
  }
1611

1612
  /** Returns the time to record on the entry. */
1613
  long expirationTimeFor(@Nullable V value, long now) {
1614
    return isComputingAsync(value) ? (now + ASYNC_EXPIRY) : now;
1 ✔
1615
  }
1616

1617
  /** Returns the time truncated to the write time's granularity. */
1618
  static long toWriteTime(long time) {
1619
    return (time & ~1L);
1 ✔
1620
  }
1621

1622
  /** Returns the entry's write time, with the refresh marker removed. */
1623
  static long writeTimeOf(Node<?, ?> node) {
1624
    return toWriteTime(node.getWriteTime());
1 ✔
1625
  }
1626

1627
  /** Returns if a refresh is in flight for the entry that recorded this write time. */
1628
  static boolean isRefreshing(long writeTime) {
1629
    return ((writeTime & 1L) != 0L);
1 ✔
1630
  }
1631

1632
  /** Returns the write time marked as having a refresh in flight. */
1633
  static long markRefreshing(long writeTime) {
1634
    return (writeTime | 1L);
1 ✔
1635
  }
1636

1637
  /**
1638
   * Performs the post-processing work required after a write.
1639
   *
1640
   * @param task the pending operation to be applied
1641
   */
1642
  void afterWrite(Runnable task) {
1643
    if (writeBuffer.offer(task)) {
1 ✔
1644
      scheduleAfterWrite();
1 ✔
1645
      return;
1 ✔
1646
    }
1647

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

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

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

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

1756
  /** Returns whether a thread waits for the eviction lock and this thread does not hold it. */
1757
  boolean isEvictionLockAwaited() {
1758
    return !evictionLock.isHeldByCurrentThread() && evictionLock.hasQueuedThreads();
1 ✔
1759
  }
1760

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

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

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

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

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

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

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

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

1842
      long now = expirationTicker().read();
1 ✔
1843
      expireEntries(now);
1 ✔
1844
      evictEntries(now);
1 ✔
1845

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

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

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

1895
  /** Drains the read buffer. */
1896
  @GuardedBy("evictionLock")
1897
  void drainReadBuffer() {
1898
    readBuffer.drainTo(accessPolicy);
1 ✔
1899
  }
1 ✔
1900

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

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

1952
    // If the protected space exceeds its maximum, the LRU items are demoted to the probation space.
1953
    // This is deferred to the adaption phase at the end of the maintenance cycle.
1954
    transfer(node, node.getPolicyWeight(), PROBATION, PROTECTED);
1 ✔
1955
  }
1 ✔
1956

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

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

1980
  /** Removes the node from the policy's queues. */
1981
  @GuardedBy("evictionLock")
1982
  void unlink(Node<K, V> node) {
1983
    if (node.inWindow() && (evicts() || expiresAfterAccess())) {
1 ✔
1984
      accessOrderWindowDeque().remove(node);
1 ✔
1985
    } else if (evicts()) {
1 ✔
1986
      if (node.inMainProbation()) {
1 ✔
1987
        accessOrderProbationDeque().remove(node);
1 ✔
1988
      } else {
1989
        accessOrderProtectedDeque().remove(node);
1 ✔
1990
      }
1991
    }
1992
    if (expiresAfterWrite()) {
1 ✔
1993
      writeOrderDeque().remove(node);
1 ✔
1994
    } else if (expiresVariable()) {
1 ✔
1995
      timerWheel().deschedule(node);
1 ✔
1996
    }
1997
  }
1 ✔
1998

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

2027
  /** Adds the node to the page replacement policy. */
2028
  final class AddTask implements Runnable {
2029
    final Node<K, V> node;
2030
    final int weight;
2031

2032
    AddTask(Node<K, V> node, int weight) {
1 ✔
2033
      this.weight = weight;
1 ✔
2034
      this.node = node;
1 ✔
2035
    }
1 ✔
2036

2037
    @Override
2038
    @GuardedBy("evictionLock")
2039
    public void run() {
2040
      if (evicts()) {
1 ✔
2041
        setWeightedSize(weightedSize() + weight);
1 ✔
2042
        setWindowWeightedSize(windowWeightedSize() + weight);
1 ✔
2043
        node.setPolicyWeight(node.getPolicyWeight() + weight);
1 ✔
2044

2045
        long maximum = maximum();
1 ✔
2046
        if (weightedSize() >= (maximum >>> 1)) {
1 ✔
2047
          if (weightedSize() > MAXIMUM_CAPACITY) {
1 ✔
2048
            evictEntries(expirationTicker().read());
1 ✔
2049
          } else {
2050
            // Lazily initialize when close to the maximum
2051
            long capacity = isWeighted() ? data.mappingCount() : maximum;
1 ✔
2052
            frequencySketch().ensureCapacity(capacity);
1 ✔
2053
            recordReads();
1 ✔
2054
          }
2055
        }
2056

2057
        climber().recordMiss();
1 ✔
2058
      }
2059

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

2092
  /** Removes a node from the page replacement policy. */
2093
  final class RemovalTask implements Runnable {
2094
    final Node<K, V> node;
2095

2096
    RemovalTask(Node<K, V> node) {
1 ✔
2097
      this.node = node;
1 ✔
2098
    }
1 ✔
2099

2100
    @Override
2101
    @GuardedBy("evictionLock")
2102
    public void run() {
2103
      // add may not have been processed yet
2104
      unlink(node);
1 ✔
2105
      makeDead(node);
1 ✔
2106
    }
1 ✔
2107
  }
2108

2109
  /** Updates the weighted size. */
2110
  final class UpdateTask implements Runnable {
2111
    final int weightDifference;
2112
    final Node<K, V> node;
2113
    final Access access;
2114

2115
    public UpdateTask(Node<K, V> node, int weightDifference) {
2116
      this(node, weightDifference, Access.HIT);
1 ✔
2117
    }
1 ✔
2118

2119
    public UpdateTask(Node<K, V> node, int weightDifference, Access access) {
1 ✔
2120
      this.weightDifference = weightDifference;
1 ✔
2121
      this.access = access;
1 ✔
2122
      this.node = node;
1 ✔
2123
    }
1 ✔
2124

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

2163
        setWeightedSize(weightedSize() + weightDifference);
1 ✔
2164
        if (weightedSize() > MAXIMUM_CAPACITY) {
1 ✔
2165
          evictEntries(expirationTicker().read());
1 ✔
2166
        }
2167
      } else if (expiresAfterAccess()) {
1 ✔
2168
        onAccess(node, access);
1 ✔
2169
      }
2170
    }
1 ✔
2171
  }
2172

2173
  /** What an access records beyond the reorder. */
2174
  enum Access {
1 ✔
2175
    HIT(/* usage= */ true, /* hit= */ true, /* miss= */ false),
1 ✔
2176
    RELOAD(/* usage= */ true, /* hit= */ false, /* miss= */ true),
1 ✔
2177
    QUIET(/* usage= */ false, /* hit= */ false, /* miss= */ false);
1 ✔
2178

2179
    private final boolean hit;
2180
    private final boolean miss;
2181
    private final boolean usage;
2182

2183
    Access(boolean usage, boolean hit, boolean miss) {
1 ✔
2184
      this.usage = usage;
1 ✔
2185
      this.hit = hit;
1 ✔
2186
      this.miss = miss;
1 ✔
2187
    }
1 ✔
2188

2189
    /** Returns whether the admission filter observes the key. */
2190
    boolean recordsUsage() {
2191
      return usage;
1 ✔
2192
    }
2193

2194
    /** Returns whether the climber's sample earns a hit. */
2195
    boolean recordsHit() {
2196
      return hit;
1 ✔
2197
    }
2198

2199
    /** Returns whether the climber's sample takes the miss an insertion would have recorded. */
2200
    boolean recordsMiss() {
2201
      return miss;
1 ✔
2202
    }
2203
  }
2204

2205
  /* --------------- Concurrent Map Support --------------- */
2206

2207
  @Override
2208
  public boolean isEmpty() {
2209
    return data.isEmpty();
1 ✔
2210
  }
2211

2212
  @Override
2213
  public int size() {
2214
    return data.size();
1 ✔
2215
  }
2216

2217
  @Override
2218
  public long estimatedSize() {
2219
    return data.mappingCount();
1 ✔
2220
  }
2221

2222
  @Override
2223
  public void clear() {
2224
    // Discard all pending refreshes
2225
    var pending = refreshes;
1 ✔
2226
    if (pending != null) {
1 ✔
2227
      pending.clear();
1 ✔
2228
    }
2229

2230
    Deque<Node<K, V>> entries;
2231
    evictionLock.lock();
1 ✔
2232
    try {
2233
      // Discard all pending reads
2234
      readBuffer.drainTo(e -> {});
1 ✔
2235

2236
      // Apply the pending writes
2237
      for (var task = writeBuffer.relaxedPoll(); task != null; task = writeBuffer.relaxedPoll()) {
1 ✔
2238
        task.run();
1 ✔
2239
      }
2240

2241
      // Cancel the scheduled cleanup
2242
      Pacer pacer = pacer();
1 ✔
2243
      if (pacer != null) {
1 ✔
2244
        pacer.cancel();
1 ✔
2245
      }
2246

2247
      // Discard all entries, falling back to one-by-one to avoid excessive lock hold times
2248
      long now = expirationTicker().read();
1 ✔
2249
      int threshold = (WRITE_BUFFER_MAX / 2);
1 ✔
2250
      entries = new ArrayDeque<>(data.values());
1 ✔
2251
      while (writeBuffer.size() < threshold) {
1 ✔
2252
        var node = entries.pollFirst();
1 ✔
2253
        if (node == null) {
1 ✔
2254
          break;
1 ✔
2255
        }
2256
        removeNode(node, now);
1 ✔
2257
      }
1 ✔
2258

2259
      // A submitted task that ran while this thread waited for the lock deferred to it
2260
      if (drainStatusOpaque() >= PROCESSING_TO_IDLE) {
1 ✔
2261
        setDrainStatusRelease(REQUIRED);
1 ✔
2262
      }
2263
    } finally {
2264
      evictionLock.unlock();
1 ✔
2265
    }
2266

2267
    // Remove any stragglers if released early to more aggressively flush incoming writes
2268
    @Var boolean cleanUp = false;
1 ✔
2269
    for (var node : entries) {
1 ✔
2270
      var key = node.getKey();
1 ✔
2271
      if (key == null) {
1 ✔
2272
        cleanUp = true;
1 ✔
2273
      } else {
2274
        remove(key);
1 ✔
2275
      }
2276
    }
1 ✔
2277
    if (collectKeys() && cleanUp) {
1 ✔
2278
      cleanUp();
1 ✔
2279
    } else {
2280
      rescheduleCleanUpIfIncomplete();
1 ✔
2281
    }
2282
  }
1 ✔
2283

2284
  @GuardedBy("evictionLock")
2285
  @SuppressWarnings({"GuardedByChecker", "SynchronizationOnLocalVariableOrMethodParameter"})
2286
  void removeNode(Node<K, V> node, long now) {
2287
    K key = node.getKey();
1 ✔
2288
    var ctx = new EvictContext<V>();
1 ✔
2289
    var keyReference = node.getKeyReference();
1 ✔
2290

2291
    data.computeIfPresent(keyReference, (k, n) -> {
1 ✔
2292
      if (n != node) {
1 ✔
2293
        return n;
1 ✔
2294
      }
2295
      synchronized (node) {
1 ✔
2296
        ctx.value = node.getValue();
1 ✔
2297
        ctx.oldWeight = node.getWeight();
1 ✔
2298

2299
        if ((key == null) || (ctx.value == null)) {
1 ✔
2300
          ctx.cause = RemovalCause.COLLECTED;
1 ✔
2301
        } else if (hasExpired(node, now) && !isComputingAsync(ctx.value)) {
1 ✔
2302
          ctx.cause = RemovalCause.EXPIRED;
1 ✔
2303
        } else {
2304
          ctx.cause = RemovalCause.EXPLICIT;
1 ✔
2305
        }
2306

2307
        if (ctx.cause.wasEvicted()) {
1 ✔
2308
          notifyEviction(key, ctx.value, ctx.cause);
1 ✔
2309
        }
2310

2311
        discardRefresh(k);
1 ✔
2312
        node.retire();
1 ✔
2313
        return null;
1 ✔
2314
      }
2315
    });
2316

2317
    unlink(node);
1 ✔
2318

2319
    synchronized (node) {
1 ✔
2320
      logIfAlive(node);
1 ✔
2321
      makeDead(node);
1 ✔
2322
    }
1 ✔
2323

2324
    if (ctx.cause != null) {
1 ✔
2325
      if (ctx.cause.wasEvicted()) {
1 ✔
2326
        statsCounter().recordEviction(ctx.oldWeight, ctx.cause);
1 ✔
2327
      }
2328
      notifyRemoval(key, ctx.value, ctx.cause);
1 ✔
2329
    }
2330
  }
1 ✔
2331

2332
  @Override
2333
  public boolean containsKey(@Nullable Object key) {
2334
    requireNonNull(key);
1 ✔
2335

2336
    Node<K, V> node = data.get(nodeFactory.newLookupKey(key));
1 ✔
2337
    if (node == null) {
1 ✔
2338
      return false;
1 ✔
2339
    }
2340
    boolean expired = hasExpired(node, expirationTicker().read());
1 ✔
2341
    V value = node.getValue();
1 ✔
2342
    if ((value == null) || (expired && !isComputingAsync(value))) {
1 ✔
2343
      scheduleDrainBuffers();
1 ✔
2344
      return false;
1 ✔
2345
    }
2346
    return true;
1 ✔
2347
  }
2348

2349
  @Override
2350
  public boolean containsValue(@Nullable Object value) {
2351
    requireNonNull(value);
1 ✔
2352

2353
    long now = expirationTicker().read();
1 ✔
2354
    for (Node<K, V> node : data.values()) {
1 ✔
2355
      boolean expired = hasExpired(node, now);
1 ✔
2356
      V nodeValue = node.getValue();
1 ✔
2357
      if ((node.getKey() == null) || (nodeValue == null)
1 ✔
2358
          || (expired && !isComputingAsync(nodeValue))) {
1 ✔
2359
        scheduleDrainBuffers();
1 ✔
2360
      } else if (node.isAlive() && node.containsValue(value)) {
1 ✔
2361
        return true;
1 ✔
2362
      }
2363
    }
1 ✔
2364
    return false;
1 ✔
2365
  }
2366

2367
  @Override
2368
  public @Nullable V get(@Nullable Object key) {
2369
    requireNonNull(key);
1 ✔
2370
    return getIfPresent(key, /* recordStats= */ false);
1 ✔
2371
  }
2372

2373
  @Override
2374
  public @Nullable V getIfPresent(Object key, boolean recordStats) {
2375
    Node<K, V> node = data.get(nodeFactory.newLookupKey(key));
1 ✔
2376
    if (node == null) {
1 ✔
2377
      if (recordStats) {
1 ✔
2378
        statsCounter().recordMisses(1);
1 ✔
2379
      }
2380
      if (drainStatusOpaque() == REQUIRED) {
1 ✔
2381
        scheduleDrainBuffers();
1 ✔
2382
      }
2383
      return null;
1 ✔
2384
    }
2385

2386
    long now = expirationTicker().read();
1 ✔
2387
    boolean expired = hasExpired(node, now);
1 ✔
2388
    V value = node.getValue();
1 ✔
2389
    if ((value == null) || (expired && !isComputingAsync(value))) {
1 ✔
2390
      if (recordStats) {
1 ✔
2391
        statsCounter().recordMisses(1);
1 ✔
2392
      }
2393
      scheduleDrainBuffers();
1 ✔
2394
      return null;
1 ✔
2395
    }
2396

2397
    if (expiresAfterRead() && !isComputingAsync(value)) {
1 ✔
2398
      @SuppressWarnings("unchecked")
2399
      var castedKey = (K) key;
1 ✔
2400
      setAccessTime(node, now);
1 ✔
2401
      tryExpireAfterRead(node, castedKey, value, expiry(), now);
1 ✔
2402
    }
2403
    V refreshed = afterRead(node, now, recordStats);
1 ✔
2404
    return (refreshed == null) ? value : refreshed;
1 ✔
2405
  }
2406

2407
  @Override
2408
  public @Nullable V getIfPresentQuietly(Object key) {
2409
    Node<K, V> node = data.get(nodeFactory.newLookupKey(key));
1 ✔
2410
    if (node == null) {
1 ✔
2411
      return null;
1 ✔
2412
    }
2413
    boolean expired = hasExpired(node, expirationTicker().read());
1 ✔
2414
    V value = node.getValue();
1 ✔
2415
    if ((value == null) || (expired && !isComputingAsync(value))) {
1 ✔
2416
      return null;
1 ✔
2417
    }
2418
    return value;
1 ✔
2419
  }
2420

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

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

2475
  @Override
2476
  public void putAll(Map<? extends K, ? extends V> map) {
2477
    map.forEach(this::put);
1 ✔
2478
  }
1 ✔
2479

2480
  @Override
2481
  public @Nullable V put(K key, V value) {
2482
    return put(key, value, expiry(), /* onlyIfAbsent= */ false);
1 ✔
2483
  }
2484

2485
  @Override
2486
  public @Nullable V putIfAbsent(K key, V value) {
2487
    return put(key, value, expiry(), /* onlyIfAbsent= */ true);
1 ✔
2488
  }
2489

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

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

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

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

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

2607
        long expirationTime = expirationTimeFor(mayUpdate ? value : oldValue, now);
1 ✔
2608
        if (mayUpdate) {
1 ✔
2609
          exceedsTolerance = exceedsWriteTimeTolerance(prior, varTime, expirationTime);
1 ✔
2610
          prior.setValue(value, valueReferenceQueue());
1 ✔
2611
          prior.setWeight(newWeight);
1 ✔
2612
          if (expired || (oldValue == null) || exceedsTolerance) {
1 ✔
2613
            setWriteTime(prior, expirationTime);
1 ✔
2614
          }
2615

2616
          discardRefresh(lookupKey);
1 ✔
2617
        }
2618

2619
        setVariableTime(prior, varTime);
1 ✔
2620
        setAccessTime(prior, expirationTime);
1 ✔
2621
      }
1 ✔
2622

2623
      if (expired) {
1 ✔
2624
        statsCounter().recordEviction(oldWeight, RemovalCause.EXPIRED);
1 ✔
2625
        notifyRemoval(key, oldValue, RemovalCause.EXPIRED);
1 ✔
2626
      } else if (oldValue == null) {
1 ✔
2627
        statsCounter().recordEviction(oldWeight, RemovalCause.COLLECTED);
1 ✔
2628
        notifyRemoval(key, /* value= */ null, RemovalCause.COLLECTED);
1 ✔
2629
      } else if (mayUpdate) {
1 ✔
2630
        notifyOnReplace(key, oldValue, value);
1 ✔
2631
      }
2632

2633
      int weightedDifference = mayUpdate ? (newWeight - oldWeight) : 0;
1 ✔
2634
      if ((oldValue == null) || (weightedDifference != 0) || expired) {
1 ✔
2635
        var access = (expired || (oldValue == null)) ? Access.RELOAD : Access.HIT;
1 ✔
2636
        afterWrite(new UpdateTask(prior, weightedDifference, access));
1 ✔
2637
      } else if (!onlyIfAbsent && exceedsTolerance) {
1 ✔
2638
        afterWrite(new UpdateTask(prior, weightedDifference));
1 ✔
2639
      } else {
2640
        afterRead(prior, now, /* recordHit= */ false);
1 ✔
2641
      }
2642

2643
      return expired ? null : oldValue;
1 ✔
2644
    }
2645
  }
2646

2647
  @Override
2648
  @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
2649
  public @Nullable V remove(@Nullable Object key) {
2650
    requireNonNull(key);
1 ✔
2651

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

2683
    if (ctx.cause != null) {
1 ✔
2684
      afterWrite(new RemovalTask(requireNonNull(ctx.node)));
1 ✔
2685
      if (ctx.cause.wasEvicted()) {
1 ✔
2686
        statsCounter().recordEviction(ctx.oldWeight, ctx.cause);
1 ✔
2687
      }
2688
      notifyRemoval(ctx.oldKey, ctx.oldValue, ctx.cause);
1 ✔
2689
    }
2690
    return (ctx.cause == RemovalCause.EXPLICIT) ? ctx.oldValue : null;
1 ✔
2691
  }
2692

2693
  @Override
2694
  @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
2695
  public boolean remove(@Nullable Object key, @Nullable Object value) {
2696
    requireNonNull(key);
1 ✔
2697
    if (value == null) {
1 ✔
2698
      return false;
1 ✔
2699
    }
2700

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

2728
    if (ctx.node == null) {
1 ✔
2729
      return false;
1 ✔
2730
    }
2731
    var removeCause = requireNonNull(ctx.cause);
1 ✔
2732
    afterWrite(new RemovalTask(ctx.node));
1 ✔
2733
    if (removeCause.wasEvicted()) {
1 ✔
2734
      statsCounter().recordEviction(ctx.oldWeight, removeCause);
1 ✔
2735
    }
2736
    notifyRemoval(ctx.oldKey, ctx.oldValue, removeCause);
1 ✔
2737

2738
    return (removeCause == RemovalCause.EXPLICIT);
1 ✔
2739
  }
2740

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

2762
        long varTime = expireAfterUpdate(n, key, value, expiry(), ctx.now);
1 ✔
2763
        n.setValue(value, valueReferenceQueue());
1 ✔
2764
        n.setWeight(weight);
1 ✔
2765

2766
        long expirationTime = expirationTimeFor(value, ctx.now);
1 ✔
2767
        ctx.exceedsTolerance = exceedsWriteTimeTolerance(n, varTime, expirationTime);
1 ✔
2768
        if (ctx.exceedsTolerance) {
1 ✔
2769
          setWriteTime(n, expirationTime);
1 ✔
2770
        }
2771
        setAccessTime(n, expirationTime);
1 ✔
2772
        setVariableTime(n, varTime);
1 ✔
2773
        discardRefresh(k);
1 ✔
2774
        return n;
1 ✔
2775
      }
2776
    });
2777

2778
    if ((node == null) || (ctx.nodeKey == null) || (ctx.oldValue == null)) {
1 ✔
2779
      if (ctx.garbage) {
1 ✔
2780
        scheduleDrainBuffers();
1 ✔
2781
      }
2782
      return null;
1 ✔
2783
    }
2784

2785
    int weightedDifference = (weight - ctx.oldWeight);
1 ✔
2786
    if (ctx.exceedsTolerance || (weightedDifference != 0)) {
1 ✔
2787
      afterWrite(new UpdateTask(node, weightedDifference));
1 ✔
2788
    } else {
2789
      afterRead(node, ctx.now, /* recordHit= */ false);
1 ✔
2790
    }
2791

2792
    notifyOnReplace(ctx.nodeKey, ctx.oldValue, value);
1 ✔
2793
    return ctx.oldValue;
1 ✔
2794
  }
2795

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

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

2831
        long expirationTime = expirationTimeFor(newValue, ctx.now);
1 ✔
2832
        ctx.exceedsTolerance = exceedsWriteTimeTolerance(n, varTime, expirationTime);
1 ✔
2833
        if (ctx.exceedsTolerance) {
1 ✔
2834
          setWriteTime(n, expirationTime);
1 ✔
2835
        }
2836
        setAccessTime(n, expirationTime);
1 ✔
2837
        setVariableTime(n, varTime);
1 ✔
2838

2839
        if (shouldDiscardRefresh) {
1 ✔
2840
          discardRefresh(k);
1 ✔
2841
        }
2842
      }
1 ✔
2843
      return n;
1 ✔
2844
    });
2845

2846
    if ((node == null) || (ctx.nodeKey == null) || (ctx.oldValue == null)) {
1 ✔
2847
      if (ctx.garbage) {
1 ✔
2848
        scheduleDrainBuffers();
1 ✔
2849
      }
2850
      return false;
1 ✔
2851
    }
2852

2853
    int weightedDifference = (weight - ctx.oldWeight);
1 ✔
2854
    if (ctx.exceedsTolerance || (weightedDifference != 0)) {
1 ✔
2855
      afterWrite(new UpdateTask(node, weightedDifference, quietly ? Access.QUIET : Access.HIT));
1 ✔
2856
    } else if (!quietly) {
1 ✔
2857
      afterRead(node, ctx.now, /* recordHit= */ false);
1 ✔
2858
    }
2859

2860
    notifyOnReplace(ctx.nodeKey, ctx.oldValue, newValue);
1 ✔
2861
    return true;
1 ✔
2862
  }
2863

2864
  @Override
2865
  public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
2866
    requireNonNull(function);
1 ✔
2867

2868
    BiFunction<K, @Nullable V, V> remappingFunction = (key, oldValue) ->
1 ✔
2869
        requireNonNull(function.apply(key, requireNonNull(oldValue)));
1 ✔
2870
    for (K key : keySet()) {
1 ✔
2871
      Object lookupKey = nodeFactory.newLookupKey(key);
1 ✔
2872
      remap(key, lookupKey, remappingFunction, expiry(),
1 ✔
2873
          new ComputeContext<>(), /* computeIfAbsent= */ false);
2874
    }
1 ✔
2875
  }
1 ✔
2876

2877
  @Override
2878
  public @Nullable V computeIfAbsent(K key,
2879
      @Var Function<? super K, ? extends @Nullable V> mappingFunction,
2880
      boolean recordStats, boolean recordLoad) {
2881
    requireNonNull(key);
1 ✔
2882
    requireNonNull(mappingFunction);
1 ✔
2883

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

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

2930
      synchronized (n) {
1 ✔
2931
        requireIsAlive(key, n);
1 ✔
2932
        ctx.nodeKey = n.getKey();
1 ✔
2933
        ctx.oldValue = n.getValue();
1 ✔
2934
        ctx.oldWeight = n.getWeight();
1 ✔
2935
        RemovalCause actualCause;
2936
        if ((ctx.nodeKey == null) || (ctx.oldValue == null)) {
1 ✔
2937
          actualCause = RemovalCause.COLLECTED;
1 ✔
2938
        } else if (hasExpired(n, ctx.now = expirationTicker().read())
1 ✔
2939
            && !isComputingAsync(ctx.oldValue)) {
1 ✔
2940
          actualCause = RemovalCause.EXPIRED;
1 ✔
2941
        } else {
2942
          return n;
1 ✔
2943
        }
2944

2945
        ctx.cause = actualCause;
1 ✔
2946
        notifyEviction(ctx.nodeKey, ctx.oldValue, actualCause);
1 ✔
2947

2948
        try {
2949
          ctx.newValue = mappingFunction.apply(key);
1 ✔
2950
          if (ctx.newValue == null) {
1 ✔
2951
            discardRefresh(k);
1 ✔
2952
            ctx.removed = n;
1 ✔
2953
            n.retire();
1 ✔
2954
            return null;
1 ✔
2955
          }
2956
          ctx.now = expirationTicker().read();
1 ✔
2957
          ctx.newWeight = weigher.weigh(key, ctx.newValue);
1 ✔
2958
          long varTime = expireAfterCreate(key, ctx.newValue, expiry(), ctx.now);
1 ✔
2959

2960
          n.setValue(ctx.newValue, valueReferenceQueue());
1 ✔
2961
          n.setWeight(ctx.newWeight);
1 ✔
2962

2963
          long expirationTime = expirationTimeFor(ctx.newValue, ctx.now);
1 ✔
2964
          setAccessTime(n, expirationTime);
1 ✔
2965
          setWriteTime(n, expirationTime);
1 ✔
2966
          setVariableTime(n, varTime);
1 ✔
2967
          discardRefresh(k);
1 ✔
2968
          return n;
1 ✔
2969
        } catch (Throwable e) {
1 ✔
2970
          ctx.newValue = null;
1 ✔
2971
          discardRefresh(k);
1 ✔
2972
          ctx.exception = e;
1 ✔
2973
          ctx.removed = n;
1 ✔
2974
          n.retire();
1 ✔
2975
          return null;
1 ✔
2976
        }
2977
      }
2978
    });
2979

2980
    if (ctx.cause != null) {
1 ✔
2981
      statsCounter().recordEviction(ctx.oldWeight, ctx.cause);
1 ✔
2982
      notifyRemoval(ctx.nodeKey, ctx.oldValue, ctx.cause);
1 ✔
2983
    }
2984
    if (node == null) {
1 ✔
2985
      if (ctx.removed != null) {
1 ✔
2986
        afterWrite(new RemovalTask(ctx.removed));
1 ✔
2987
      }
2988
      if (ctx.exception != null) {
1 ✔
2989
        throw toUnchecked(ctx.exception);
1 ✔
2990
      }
2991
      return null;
1 ✔
2992
    }
2993
    if ((ctx.oldValue != null) && (ctx.newValue == null)) {
1 ✔
2994
      if (!isComputingAsync(ctx.oldValue)) {
1 ✔
2995
        tryExpireAfterRead(node, key, ctx.oldValue, expiry(), ctx.now);
1 ✔
2996
        setAccessTime(node, ctx.now);
1 ✔
2997
      }
2998

2999
      var refreshed = afterRead(node, ctx.now, /* recordHit= */ recordStats);
1 ✔
3000
      return (refreshed == null) ? ctx.oldValue : refreshed;
1 ✔
3001
    }
3002
    if ((ctx.oldValue == null) && (ctx.cause == null)) {
1 ✔
3003
      afterWrite(new AddTask(node, ctx.newWeight));
1 ✔
3004
    } else {
3005
      int weightedDifference = (ctx.newWeight - ctx.oldWeight);
1 ✔
3006
      afterWrite(new UpdateTask(node, weightedDifference, Access.RELOAD));
1 ✔
3007
    }
3008

3009
    return ctx.newValue;
1 ✔
3010
  }
3011

3012
  @Override
3013
  public @Nullable V computeIfPresent(K key,
3014
      BiFunction<? super K, ? super V, ? extends @Nullable V> remappingFunction) {
3015
    requireNonNull(key);
1 ✔
3016
    requireNonNull(remappingFunction);
1 ✔
3017

3018
    // An optimistic fast path to avoid unnecessary locking
3019
    Object lookupKey = nodeFactory.newLookupKey(key);
1 ✔
3020
    var node = data.get(lookupKey);
1 ✔
3021
    if (node == null) {
1 ✔
3022
      return null;
1 ✔
3023
    }
3024
    boolean expired = hasExpired(node, expirationTicker().read());
1 ✔
3025
    V value = node.getValue();
1 ✔
3026
    if ((value == null) || expired) {
1 ✔
3027
      scheduleDrainBuffers();
1 ✔
3028
      return null;
1 ✔
3029
    }
3030

3031
    @SuppressWarnings("NullAway")
3032
    BiFunction<? super K, ? super @Nullable V, ? extends @Nullable V> statsAwareRemappingFunction =
1 ✔
3033
        statsAware(remappingFunction, /* recordLoad= */ true);
1 ✔
3034
    return remap(key, lookupKey, statsAwareRemappingFunction,
1 ✔
3035
        expiry(), new ComputeContext<>(), /* computeIfAbsent= */ false);
1 ✔
3036
  }
3037

3038
  @Override
3039
  public @Nullable V compute(K key,
3040
      BiFunction<? super K, ? super @Nullable V, ? extends @Nullable V> remappingFunction,
3041
      @Nullable Expiry<? super K, ? super V> expiry, boolean recordLoad,
3042
      @Nullable RemapHints hints) {
3043
    requireNonNull(key);
1 ✔
3044
    requireNonNull(remappingFunction);
1 ✔
3045

3046
    Object keyRef = nodeFactory.newReferenceKey(key, keyReferenceQueue());
1 ✔
3047
    var ctx = new ComputeContext<K, V>();
1 ✔
3048
    ctx.hints = hints;
1 ✔
3049
    return remap(key, keyRef, statsAware(remappingFunction, recordLoad),
1 ✔
3050
        expiry, ctx, /* computeIfAbsent= */ true);
3051
  }
3052

3053
  @Override
3054
  public @Nullable V merge(K key, V value,
3055
      BiFunction<? super V, ? super V, ? extends @Nullable V> remappingFunction) {
3056
    requireNonNull(key);
1 ✔
3057
    requireNonNull(value);
1 ✔
3058
    requireNonNull(remappingFunction);
1 ✔
3059

3060
    Object keyRef = nodeFactory.newReferenceKey(key, keyReferenceQueue());
1 ✔
3061
    BiFunction<? super V, ? super V, ? extends @Nullable V> f = statsAware(remappingFunction);
1 ✔
3062
    return remap(key, keyRef,
1 ✔
3063
        (k, oldValue) -> (oldValue == null) ? value : f.apply(oldValue, value), expiry(),
1 ✔
3064
        new ComputeContext<>(), /* computeIfAbsent= */ true);
3065
  }
3066

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

3108
          long expirationTime = expirationTimeFor(ctx.newValue, ctx.now);
1 ✔
3109
          setAccessTime(created, expirationTime);
1 ✔
3110
          setWriteTime(created, expirationTime);
1 ✔
3111
          setVariableTime(created, varTime);
1 ✔
3112
          return created;
1 ✔
3113
        } finally {
3114
          discardRefresh(kr);
1 ✔
3115
        }
3116
      }
3117

3118
      synchronized (n) {
1 ✔
3119
        requireIsAlive(key, n);
1 ✔
3120
        ctx.nodeKey = n.getKey();
1 ✔
3121
        ctx.oldValue = n.getValue();
1 ✔
3122
        ctx.oldWeight = n.getWeight();
1 ✔
3123
        if ((ctx.nodeKey == null) || (ctx.oldValue == null)) {
1 ✔
3124
          ctx.cause = RemovalCause.COLLECTED;
1 ✔
3125
        } else if (hasExpired(n, expirationTicker().read()) && !isComputingAsync(ctx.oldValue)) {
1 ✔
3126
          ctx.cause = RemovalCause.EXPIRED;
1 ✔
3127
        }
3128
        if (ctx.cause != null) {
1 ✔
3129
          notifyEviction(ctx.nodeKey, ctx.oldValue, ctx.cause);
1 ✔
3130
          if (!computeIfAbsent) {
1 ✔
3131
            discardRefresh(kr);
1 ✔
3132
            ctx.removed = n;
1 ✔
3133
            n.retire();
1 ✔
3134
            return null;
1 ✔
3135
          }
3136
        }
3137

3138
        boolean wasEvicted = (ctx.cause != null);
1 ✔
3139
        try {
3140
          ctx.newValue = remappingFunction.apply(key,
1 ✔
3141
              (ctx.cause == null) ? ctx.oldValue : null);
1 ✔
3142

3143
          if (ctx.newValue == null) {
1 ✔
3144
            if (ctx.cause == null) {
1 ✔
3145
              ctx.cause = RemovalCause.EXPLICIT;
1 ✔
3146
            }
3147
            // A stale refresh whose entry was evicted leaves a successor's registration intact
3148
            if ((ctx.hints == null) || !ctx.hints.preserveRefresh) {
1 ✔
3149
              discardRefresh(kr);
1 ✔
3150
            }
3151
            ctx.removed = n;
1 ✔
3152
            n.retire();
1 ✔
3153
            return null;
1 ✔
3154
          }
3155

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

3168
          long varTime;
3169
          ctx.newWeight = weigher.weigh(key, ctx.newValue);
1 ✔
3170
          ctx.now = expirationTicker().read();
1 ✔
3171
          if (ctx.cause == null) {
1 ✔
3172
            if (ctx.newValue != ctx.oldValue) {
1 ✔
3173
              ctx.cause = RemovalCause.REPLACED;
1 ✔
3174
            }
3175
            varTime = expireAfterUpdate(n, key, ctx.newValue, expiry, ctx.now);
1 ✔
3176
          } else {
3177
            varTime = expireAfterCreate(key, ctx.newValue, expiry, ctx.now);
1 ✔
3178
          }
3179

3180
          if (ctx.newValue != ctx.oldValue) {
1 ✔
3181
            n.setValue(ctx.newValue, valueReferenceQueue());
1 ✔
3182
          }
3183
          n.setWeight(ctx.newWeight);
1 ✔
3184

3185
          long expirationTime = expirationTimeFor(ctx.newValue, ctx.now);
1 ✔
3186
          ctx.exceedsTolerance = exceedsWriteTimeTolerance(n, varTime, expirationTime);
1 ✔
3187
          if (((ctx.cause != null) && ctx.cause.wasEvicted()) || ctx.exceedsTolerance) {
1 ✔
3188
            setWriteTime(n, expirationTime);
1 ✔
3189
          }
3190
          setAccessTime(n, expirationTime);
1 ✔
3191
          setVariableTime(n, varTime);
1 ✔
3192
          discardRefresh(kr);
1 ✔
3193
          return n;
1 ✔
3194
        } catch (Throwable e) {
1 ✔
3195
          if ((ctx.hints == null) || !ctx.hints.preserveRefresh) {
1 ✔
3196
            discardRefresh(kr);
1 ✔
3197
          }
3198
          if (!wasEvicted) {
1 ✔
3199
            throw e;
1 ✔
3200
          }
3201
          ctx.newValue = null;
1 ✔
3202
          ctx.exception = e;
1 ✔
3203
          ctx.removed = n;
1 ✔
3204
          n.retire();
1 ✔
3205
          return null;
1 ✔
3206
        }
3207
      }
3208
    });
3209

3210
    if (ctx.cause != null) {
1 ✔
3211
      if (ctx.cause == RemovalCause.REPLACED) {
1 ✔
3212
        requireNonNull(ctx.newValue);
1 ✔
3213
        notifyOnReplace(key, ctx.oldValue, ctx.newValue);
1 ✔
3214
      } else {
3215
        if (ctx.cause.wasEvicted()) {
1 ✔
3216
          statsCounter().recordEviction(ctx.oldWeight, ctx.cause);
1 ✔
3217
        }
3218
        notifyRemoval(ctx.nodeKey, ctx.oldValue, ctx.cause);
1 ✔
3219
      }
3220
    }
3221

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

3247
    if (ctx.exception != null) {
1 ✔
3248
      throw toUnchecked(ctx.exception);
1 ✔
3249
    }
3250
    return ctx.newValue;
1 ✔
3251
  }
3252

3253
  @Override
3254
  public void forEach(BiConsumer<? super K, ? super V> action) {
3255
    requireNonNull(action);
1 ✔
3256

3257
    for (var iterator = new EntryIterator<>(this); iterator.hasNext();) {
1 ✔
3258
      action.accept(requireNonNull(iterator.key), requireNonNull(iterator.value));
1 ✔
3259
      iterator.advance();
1 ✔
3260
    }
3261
  }
1 ✔
3262

3263
  @Override
3264
  public Set<K> keySet() {
3265
    Set<K> ks = keySet;
1 ✔
3266
    return (ks == null) ? (keySet = new KeySetView<>(this)) : ks;
1 ✔
3267
  }
3268

3269
  @Override
3270
  public Collection<V> values() {
3271
    Collection<V> vs = values;
1 ✔
3272
    return (vs == null) ? (values = new ValuesView<>(this)) : vs;
1 ✔
3273
  }
3274

3275
  @Override
3276
  public Set<Entry<K, V>> entrySet() {
3277
    Set<Entry<K, V>> es = entrySet;
1 ✔
3278
    return (es == null) ? (entrySet = new EntrySetView<>(this)) : es;
1 ✔
3279
  }
3280

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

3314
    var map = (Map<?, ?>) o;
1 ✔
3315
    int expectedSize = size();
1 ✔
3316
    if (map.size() != expectedSize) {
1 ✔
3317
      return false;
1 ✔
3318
    }
3319

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

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

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

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

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

3471
  /**
3472
   * Returns the computed result from the ordered traversal of the cache entries.
3473
   *
3474
   * @param iterable the supplier of the entries in the cache
3475
   * @param transformer a function that unwraps the value
3476
   * @param mappingFunction the mapping function to compute a value
3477
   * @return the computed value
3478
   */
3479
  @SuppressWarnings("MathClampLong")
3480
  <T extends @Nullable Object> T snapshot(Iterable<Node<K, V>> iterable,
3481
      Function<@Nullable V, @Nullable V> transformer,
3482
      Function<Stream<CacheEntry<K, V>>, T> mappingFunction) {
3483
    requireNonNull(mappingFunction);
1 ✔
3484
    requireNonNull(transformer);
1 ✔
3485
    requireNonNull(iterable);
1 ✔
3486

3487
    evictionLock.lock();
1 ✔
3488
    try {
3489
      maintenance(/* ignored */ null);
1 ✔
3490

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

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

3526
    @Var long expiresAfter = Long.MAX_VALUE;
1 ✔
3527
    if (expiresAfterAccess()) {
1 ✔
3528
      expiresAfter = Math.min(expiresAfter,
1 ✔
3529
          expiresAfterAccessNanos() - Math.max(0L, now - node.getAccessTime()));
1 ✔
3530
    }
3531
    if (expiresAfterWrite()) {
1 ✔
3532
      expiresAfter = Math.min(expiresAfter,
1 ✔
3533
          expiresAfterWriteNanos() - Math.max(0L, toWriteTime(now) - writeTimeOf(node)));
1 ✔
3534
    }
3535
    if (expiresVariable()) {
1 ✔
3536
      expiresAfter = node.getVariableTime() - now;
1 ✔
3537
    }
3538

3539
    long refreshableAfter = refreshAfterWrite()
1 ✔
3540
        ? refreshAfterWriteNanos() - Math.max(0L, now - writeTimeOf(node))
1 ✔
3541
        : Long.MAX_VALUE;
1 ✔
3542
    return SnapshotEntry.forEntry(key, value, now,
1 ✔
3543
        isWeighted ? weight : 1, now + expiresAfter, now + refreshableAfter);
1 ✔
3544
  }
3545

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

3581
  /** Mutable context for passing state between a lambda and the caller. */
3582
  static final class EvictContext<V> {
1 ✔
3583
    @Nullable RemovalCause cause;
3584
    @Nullable V value;
3585
    boolean resurrect;
3586
    boolean removed;
3587
    int oldWeight;
3588
  }
3589

3590
  /** Mutable context for passing state between a lambda and the caller. */
3591
  static final class RemoveContext<K, V> {
1 ✔
3592
    @Nullable K oldKey;
3593
    @Nullable V oldValue;
3594
    @Nullable Node<K, V> node;
3595
    @Nullable RemovalCause cause;
3596
    int oldWeight;
3597
  }
3598

3599
  /** Mutable context for passing state between a lambda and the caller. */
3600
  static final class ReplaceContext<K, V> {
1 ✔
3601
    @Nullable K nodeKey;
3602
    @Nullable V oldValue;
3603

3604
    long now;
3605
    int oldWeight;
3606
    boolean garbage;
3607
    boolean exceedsTolerance;
3608
  }
3609

3610
  /** Mutable context for passing state between a lambda and the caller. */
3611
  static final class ComputeContext<K, V> {
1 ✔
3612
    @Nullable K nodeKey;
3613
    @Nullable V oldValue;
3614
    @Nullable V newValue;
3615
    @Nullable RemapHints hints;
3616
    @Nullable Node<K, V> removed;
3617
    @Nullable RemovalCause cause;
3618
    @Nullable Throwable exception;
3619

3620
    long now;
3621
    int oldWeight;
3622
    int newWeight;
3623
    boolean unmodified;
3624
    boolean exceedsTolerance;
3625
  }
3626

3627
  /** A function that produces an unmodifiable map up to the limit in stream order. */
3628
  static final class SizeLimiter<K, V> implements Function<Stream<CacheEntry<K, V>>, Map<K, V>> {
3629
    private final int expectedSize;
3630
    private final long limit;
3631

3632
    SizeLimiter(int expectedSize, long limit) {
1 ✔
3633
      requireArgument(limit >= 0, "limit cannot be negative: %s", limit);
1 ✔
3634
      this.expectedSize = expectedSize;
1 ✔
3635
      this.limit = limit;
1 ✔
3636
    }
1 ✔
3637

3638
    @Override
3639
    public Map<K, V> apply(Stream<CacheEntry<K, V>> stream) {
3640
      var map = new LinkedHashMap<K, V>(calculateHashMapCapacity(expectedSize));
1 ✔
3641
      stream.limit(limit).forEach(entry -> map.put(entry.getKey(), entry.getValue()));
1 ✔
3642
      return Collections.unmodifiableMap(map);
1 ✔
3643
    }
3644
  }
3645

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

3650
    long weightedSize;
3651

3652
    WeightLimiter(long weightLimit) {
1 ✔
3653
      requireArgument(weightLimit >= 0, "weight limit cannot be negative: %s", weightLimit);
1 ✔
3654
      this.weightLimit = weightLimit;
1 ✔
3655
    }
1 ✔
3656

3657
    @Override
3658
    public Map<K, V> apply(Stream<CacheEntry<K, V>> stream) {
3659
      var map = new LinkedHashMap<K, V>();
1 ✔
3660
      stream.takeWhile(entry -> {
1 ✔
3661
        weightedSize += entry.weight();
1 ✔
3662
        if (weightedSize < 0) {
1 ✔
3663
          weightedSize = Long.MAX_VALUE;
1 ✔
3664
        }
3665
        return (weightedSize <= weightLimit);
1 ✔
3666
      }).forEach(entry -> map.put(entry.getKey(), entry.getValue()));
1 ✔
3667
      return Collections.unmodifiableMap(map);
1 ✔
3668
    }
3669
  }
3670

3671
  /** An adapter to safely externalize the keys. */
3672
  static final class KeySetView<K, V> extends AbstractSet<K> {
3673
    final BoundedLocalCache<K, V> cache;
3674

3675
    KeySetView(BoundedLocalCache<K, V> cache) {
1 ✔
3676
      this.cache = requireNonNull(cache);
1 ✔
3677
    }
1 ✔
3678

3679
    @Override
3680
    public int size() {
3681
      return cache.size();
1 ✔
3682
    }
3683

3684
    @Override
3685
    public void clear() {
3686
      cache.clear();
1 ✔
3687
    }
1 ✔
3688

3689
    @Override
3690
    @SuppressWarnings("SuspiciousMethodCalls")
3691
    public boolean contains(@Nullable Object o) {
3692
      return cache.containsKey(o);
1 ✔
3693
    }
3694

3695
    @Override
3696
    public boolean containsAll(Collection<?> collection) {
3697
      requireNonNull(collection);
1 ✔
3698
      if (collection != this) {
1 ✔
3699
        for (Object o : collection) {
1 ✔
3700
          if ((o == null) || !contains(o)) {
1 ✔
3701
            return false;
1 ✔
3702
          }
3703
        }
1 ✔
3704
      }
3705
      return true;
1 ✔
3706
    }
3707

3708
    @Override
3709
    public boolean removeAll(Collection<?> collection) {
3710
      requireNonNull(collection);
1 ✔
3711
      @Var boolean modified = false;
1 ✔
3712
      if (cache.collectKeys() || ((collection instanceof Set<?>) && (collection.size() > size()))) {
1 ✔
3713
        for (K key : this) {
1 ✔
3714
          if (collection.contains(key)) {
1 ✔
3715
            modified |= remove(key);
1 ✔
3716
          }
3717
        }
1 ✔
3718
      } else {
3719
        for (var item : collection) {
1 ✔
3720
          modified |= (item != null) && remove(item);
1 ✔
3721
        }
1 ✔
3722
      }
3723
      return modified;
1 ✔
3724
    }
3725

3726
    @Override
3727
    public boolean remove(@Nullable Object o) {
3728
      return (cache.remove(o) != null);
1 ✔
3729
    }
3730

3731
    @Override
3732
    public boolean removeIf(Predicate<? super K> filter) {
3733
      requireNonNull(filter);
1 ✔
3734
      @Var boolean modified = false;
1 ✔
3735
      for (K key : this) {
1 ✔
3736
        if (filter.test(key) && remove(key)) {
1 ✔
3737
          modified = true;
1 ✔
3738
        }
3739
      }
1 ✔
3740
      return modified;
1 ✔
3741
    }
3742

3743
    @Override
3744
    public boolean retainAll(Collection<?> collection) {
3745
      requireNonNull(collection);
1 ✔
3746
      @Var boolean modified = false;
1 ✔
3747
      for (K key : this) {
1 ✔
3748
        if (!collection.contains(key) && remove(key)) {
1 ✔
3749
          modified = true;
1 ✔
3750
        }
3751
      }
1 ✔
3752
      return modified;
1 ✔
3753
    }
3754

3755
    @Override
3756
    public Iterator<K> iterator() {
3757
      return new KeyIterator<>(cache);
1 ✔
3758
    }
3759

3760
    @Override
3761
    public Spliterator<K> spliterator() {
3762
      return new KeySpliterator<>(cache);
1 ✔
3763
    }
3764
  }
3765

3766
  /** An adapter to safely externalize the key iterator. */
3767
  static final class KeyIterator<K, V> implements Iterator<K> {
3768
    final EntryIterator<K, V> iterator;
3769

3770
    KeyIterator(BoundedLocalCache<K, V> cache) {
1 ✔
3771
      this.iterator = new EntryIterator<>(cache);
1 ✔
3772
    }
1 ✔
3773

3774
    @Override
3775
    public boolean hasNext() {
3776
      return iterator.hasNext();
1 ✔
3777
    }
3778

3779
    @Override
3780
    public K next() {
3781
      return iterator.nextKey();
1 ✔
3782
    }
3783

3784
    @Override
3785
    public void remove() {
3786
      iterator.remove();
1 ✔
3787
    }
1 ✔
3788
  }
3789

3790
  /** An adapter to safely externalize the key spliterator. */
3791
  static final class KeySpliterator<K, V> implements Spliterator<K> {
3792
    final Spliterator<Node<K, V>> spliterator;
3793
    final BoundedLocalCache<K, V> cache;
3794

3795
    KeySpliterator(BoundedLocalCache<K, V> cache) {
3796
      this(cache, cache.data.values().spliterator());
1 ✔
3797
    }
1 ✔
3798

3799
    KeySpliterator(BoundedLocalCache<K, V> cache, Spliterator<Node<K, V>> spliterator) {
1 ✔
3800
      this.spliterator = requireNonNull(spliterator);
1 ✔
3801
      this.cache = requireNonNull(cache);
1 ✔
3802
    }
1 ✔
3803

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

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

3845
    @Override
3846
    public @Nullable Spliterator<K> trySplit() {
3847
      Spliterator<Node<K, V>> split = spliterator.trySplit();
1 ✔
3848
      return (split == null) ? null : new KeySpliterator<>(cache, split);
1 ✔
3849
    }
3850

3851
    @Override
3852
    public long estimateSize() {
3853
      return spliterator.estimateSize();
1 ✔
3854
    }
3855

3856
    @Override
3857
    public int characteristics() {
3858
      return DISTINCT | CONCURRENT | NONNULL;
1 ✔
3859
    }
3860
  }
3861

3862
  /** An adapter to safely externalize the values. */
3863
  static final class ValuesView<K, V> extends AbstractCollection<V> {
3864
    final BoundedLocalCache<K, V> cache;
3865

3866
    ValuesView(BoundedLocalCache<K, V> cache) {
1 ✔
3867
      this.cache = requireNonNull(cache);
1 ✔
3868
    }
1 ✔
3869

3870
    @Override
3871
    public int size() {
3872
      return cache.size();
1 ✔
3873
    }
3874

3875
    @Override
3876
    public void clear() {
3877
      cache.clear();
1 ✔
3878
    }
1 ✔
3879

3880
    @Override
3881
    @SuppressWarnings("SuspiciousMethodCalls")
3882
    public boolean contains(@Nullable Object o) {
3883
      return cache.containsValue(o);
1 ✔
3884
    }
3885

3886
    @Override
3887
    public boolean containsAll(Collection<?> collection) {
3888
      requireNonNull(collection);
1 ✔
3889
      if (collection != this) {
1 ✔
3890
        for (Object o : collection) {
1 ✔
3891
          if ((o == null) || !contains(o)) {
1 ✔
3892
            return false;
1 ✔
3893
          }
3894
        }
1 ✔
3895
      }
3896
      return true;
1 ✔
3897
    }
3898

3899
    @Override
3900
    public boolean removeAll(Collection<?> collection) {
3901
      requireNonNull(collection);
1 ✔
3902
      @Var boolean modified = false;
1 ✔
3903
      for (var iterator = new EntryIterator<>(cache); iterator.hasNext();) {
1 ✔
3904
        var key = requireNonNull(iterator.key);
1 ✔
3905
        var value = requireNonNull(iterator.value);
1 ✔
3906
        if (collection.contains(value) && cache.remove(key, value)) {
1 ✔
3907
          modified = true;
1 ✔
3908
        }
3909
        iterator.advance();
1 ✔
3910
      }
1 ✔
3911
      return modified;
1 ✔
3912
    }
3913

3914
    @Override
3915
    public boolean remove(@Nullable Object o) {
3916
      if (o == null) {
1 ✔
3917
        return false;
1 ✔
3918
      }
3919
      for (var iterator = new EntryIterator<>(cache); iterator.hasNext();) {
1 ✔
3920
        var key = requireNonNull(iterator.key);
1 ✔
3921
        var node = requireNonNull(iterator.next);
1 ✔
3922
        var value = requireNonNull(iterator.value);
1 ✔
3923
        if (node.containsValue(o) && cache.remove(key, value)) {
1 ✔
3924
          return true;
1 ✔
3925
        }
3926
        iterator.advance();
1 ✔
3927
      }
1 ✔
3928
      return false;
1 ✔
3929
    }
3930

3931
    @Override
3932
    public boolean removeIf(Predicate<? super V> filter) {
3933
      requireNonNull(filter);
1 ✔
3934
      @Var boolean modified = false;
1 ✔
3935
      for (var iterator = new EntryIterator<>(cache); iterator.hasNext();) {
1 ✔
3936
        var value = requireNonNull(iterator.value);
1 ✔
3937
        if (filter.test(value)) {
1 ✔
3938
          var key = requireNonNull(iterator.key);
1 ✔
3939
          modified |= cache.remove(key, value);
1 ✔
3940
        }
3941
        iterator.advance();
1 ✔
3942
      }
1 ✔
3943
      return modified;
1 ✔
3944
    }
3945

3946
    @Override
3947
    public boolean retainAll(Collection<?> collection) {
3948
      requireNonNull(collection);
1 ✔
3949
      @Var boolean modified = false;
1 ✔
3950
      for (var iterator = new EntryIterator<>(cache); iterator.hasNext();) {
1 ✔
3951
        var key = requireNonNull(iterator.key);
1 ✔
3952
        var value = requireNonNull(iterator.value);
1 ✔
3953
        if (!collection.contains(value) && cache.remove(key, value)) {
1 ✔
3954
          modified = true;
1 ✔
3955
        }
3956
        iterator.advance();
1 ✔
3957
      }
1 ✔
3958
      return modified;
1 ✔
3959
    }
3960

3961
    @Override
3962
    public Iterator<V> iterator() {
3963
      return new ValueIterator<>(cache);
1 ✔
3964
    }
3965

3966
    @Override
3967
    public Spliterator<V> spliterator() {
3968
      return new ValueSpliterator<>(cache);
1 ✔
3969
    }
3970
  }
3971

3972
  /** An adapter to safely externalize the value iterator. */
3973
  static final class ValueIterator<K, V> implements Iterator<V> {
3974
    final EntryIterator<K, V> iterator;
3975

3976
    ValueIterator(BoundedLocalCache<K, V> cache) {
1 ✔
3977
      this.iterator = new EntryIterator<>(cache);
1 ✔
3978
    }
1 ✔
3979

3980
    @Override
3981
    public boolean hasNext() {
3982
      return iterator.hasNext();
1 ✔
3983
    }
3984

3985
    @Override
3986
    public V next() {
3987
      return iterator.nextValue();
1 ✔
3988
    }
3989

3990
    @Override
3991
    public void remove() {
3992
      iterator.remove();
1 ✔
3993
    }
1 ✔
3994
  }
3995

3996
  /** An adapter to safely externalize the value spliterator. */
3997
  static final class ValueSpliterator<K, V> implements Spliterator<V> {
3998
    final Spliterator<Node<K, V>> spliterator;
3999
    final BoundedLocalCache<K, V> cache;
4000

4001
    ValueSpliterator(BoundedLocalCache<K, V> cache) {
4002
      this(cache, cache.data.values().spliterator());
1 ✔
4003
    }
1 ✔
4004

4005
    ValueSpliterator(BoundedLocalCache<K, V> cache, Spliterator<Node<K, V>> spliterator) {
1 ✔
4006
      this.spliterator = requireNonNull(spliterator);
1 ✔
4007
      this.cache = requireNonNull(cache);
1 ✔
4008
    }
1 ✔
4009

4010
    @Override
4011
    public void forEachRemaining(Consumer<? super V> action) {
4012
      requireNonNull(action);
1 ✔
4013
      Consumer<Node<K, V>> consumer = node -> {
1 ✔
4014
        long now = cache.expirationTicker().read();
1 ✔
4015
        boolean expired = cache.hasExpired(node, now);
1 ✔
4016
        K key = node.getKey();
1 ✔
4017
        V value = node.getValue();
1 ✔
4018
        if ((key == null) || (value == null) || (expired && !cache.isComputingAsync(value))) {
1 ✔
4019
          cache.scheduleDrainBuffers();
1 ✔
4020
        } else if (node.isAlive()) {
1 ✔
4021
          action.accept(value);
1 ✔
4022
        }
4023
      };
1 ✔
4024
      spliterator.forEachRemaining(consumer);
1 ✔
4025
    }
1 ✔
4026

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

4051
    @Override
4052
    public @Nullable Spliterator<V> trySplit() {
4053
      Spliterator<Node<K, V>> split = spliterator.trySplit();
1 ✔
4054
      return (split == null) ? null : new ValueSpliterator<>(cache, split);
1 ✔
4055
    }
4056

4057
    @Override
4058
    public long estimateSize() {
4059
      return spliterator.estimateSize();
1 ✔
4060
    }
4061

4062
    @Override
4063
    public int characteristics() {
4064
      return CONCURRENT | NONNULL;
1 ✔
4065
    }
4066
  }
4067

4068
  /** An adapter to safely externalize the entries. */
4069
  static final class EntrySetView<K, V> extends AbstractSet<Entry<K, V>> {
4070
    final BoundedLocalCache<K, V> cache;
4071

4072
    EntrySetView(BoundedLocalCache<K, V> cache) {
1 ✔
4073
      this.cache = requireNonNull(cache);
1 ✔
4074
    }
1 ✔
4075

4076
    @Override
4077
    public int size() {
4078
      return cache.size();
1 ✔
4079
    }
4080

4081
    @Override
4082
    public void clear() {
4083
      cache.clear();
1 ✔
4084
    }
1 ✔
4085

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

4107
    @Override
4108
    public boolean containsAll(Collection<?> collection) {
4109
      requireNonNull(collection);
1 ✔
4110
      if (collection != this) {
1 ✔
4111
        for (Object o : collection) {
1 ✔
4112
          if ((o == null) || !contains(o)) {
1 ✔
4113
            return false;
1 ✔
4114
          }
4115
        }
1 ✔
4116
      }
4117
      return true;
1 ✔
4118
    }
4119

4120
    @Override
4121
    public boolean removeAll(Collection<?> collection) {
4122
      requireNonNull(collection);
1 ✔
4123
      @Var boolean modified = false;
1 ✔
4124
      if (cache.collectKeys() || cache.collectValues()
1 ✔
4125
          || ((collection instanceof Set<?>) && (collection.size() > size()))) {
1 ✔
4126
        for (var entry : this) {
1 ✔
4127
          if (collection.contains(entry)) {
1 ✔
4128
            modified |= remove(entry);
1 ✔
4129
          }
4130
        }
1 ✔
4131
      } else {
4132
        for (var item : collection) {
1 ✔
4133
          modified |= remove(item);
1 ✔
4134
        }
1 ✔
4135
      }
4136
      return modified;
1 ✔
4137
    }
4138

4139
    @Override
4140
    @SuppressWarnings("SuspiciousMethodCalls")
4141
    public boolean remove(@Nullable Object o) {
4142
      if (!(o instanceof Entry<?, ?>)) {
1 ✔
4143
        return false;
1 ✔
4144
      }
4145
      var entry = (Entry<?, ?>) o;
1 ✔
4146
      var key = entry.getKey();
1 ✔
4147
      return (key != null) && cache.remove(key, entry.getValue());
1 ✔
4148
    }
4149

4150
    @Override
4151
    public boolean removeIf(Predicate<? super Entry<K, V>> filter) {
4152
      requireNonNull(filter);
1 ✔
4153
      @Var boolean modified = false;
1 ✔
4154
      for (var iterator = new EntryIterator<>(cache); iterator.hasNext();) {
1 ✔
4155
        var key = requireNonNull(iterator.key);
1 ✔
4156
        var value = requireNonNull(iterator.value);
1 ✔
4157
        if (filter.test(Map.entry(key, value))) {
1 ✔
4158
          modified |= cache.remove(key, value);
1 ✔
4159
        }
4160
        iterator.advance();
1 ✔
4161
      }
1 ✔
4162
      return modified;
1 ✔
4163
    }
4164

4165
    @Override
4166
    public boolean retainAll(Collection<?> collection) {
4167
      requireNonNull(collection);
1 ✔
4168
      @Var boolean modified = false;
1 ✔
4169
      for (var entry : this) {
1 ✔
4170
        if (!collection.contains(entry) && remove(entry)) {
1 ✔
4171
          modified = true;
1 ✔
4172
        }
4173
      }
1 ✔
4174
      return modified;
1 ✔
4175
    }
4176

4177
    @Override
4178
    public Iterator<Entry<K, V>> iterator() {
4179
      return new EntryIterator<>(cache);
1 ✔
4180
    }
4181

4182
    @Override
4183
    public Spliterator<Entry<K, V>> spliterator() {
4184
      return new EntrySpliterator<>(cache);
1 ✔
4185
    }
4186
  }
4187

4188
  /** An adapter to safely externalize the entry iterator. */
4189
  static final class EntryIterator<K, V> implements Iterator<Entry<K, V>> {
4190
    final BoundedLocalCache<K, V> cache;
4191
    final Iterator<Node<K, V>> iterator;
4192

4193
    @Nullable K key;
4194
    @Nullable V value;
4195
    @Nullable K removalKey;
4196
    @Nullable Node<K, V> next;
4197

4198
    EntryIterator(BoundedLocalCache<K, V> cache) {
1 ✔
4199
      this.iterator = cache.data.values().iterator();
1 ✔
4200
      this.cache = cache;
1 ✔
4201
    }
1 ✔
4202

4203
    @Override
4204
    public boolean hasNext() {
4205
      if (next != null) {
1 ✔
4206
        return true;
1 ✔
4207
      }
4208

4209
      long now = cache.expirationTicker().read();
1 ✔
4210
      while (iterator.hasNext()) {
1 ✔
4211
        next = iterator.next();
1 ✔
4212
        boolean expired = cache.hasExpired(next, now);
1 ✔
4213
        value = next.getValue();
1 ✔
4214
        key = next.getKey();
1 ✔
4215

4216
        boolean evictable = (key == null) || (value == null)
1 ✔
4217
            || (expired && !cache.isComputingAsync(value));
1 ✔
4218
        if (evictable || !next.isAlive()) {
1 ✔
4219
          if (evictable) {
1 ✔
4220
            cache.scheduleDrainBuffers();
1 ✔
4221
          }
4222
          advance();
1 ✔
4223
          continue;
1 ✔
4224
        }
4225
        return true;
1 ✔
4226
      }
4227
      return false;
1 ✔
4228
    }
4229

4230
    /** Invalidates the current position so that the iterator may compute the next position. */
4231
    void advance() {
4232
      value = null;
1 ✔
4233
      next = null;
1 ✔
4234
      key = null;
1 ✔
4235
    }
1 ✔
4236

4237
    K nextKey() {
4238
      if (!hasNext()) {
1 ✔
4239
        throw new NoSuchElementException();
1 ✔
4240
      }
4241
      removalKey = key;
1 ✔
4242
      advance();
1 ✔
4243
      return requireNonNull(removalKey);
1 ✔
4244
    }
4245

4246
    V nextValue() {
4247
      if (!hasNext()) {
1 ✔
4248
        throw new NoSuchElementException();
1 ✔
4249
      }
4250
      removalKey = key;
1 ✔
4251
      V val = value;
1 ✔
4252
      advance();
1 ✔
4253
      return requireNonNull(val);
1 ✔
4254
    }
4255

4256
    @Override
4257
    public Entry<K, V> next() {
4258
      if (!hasNext()) {
1 ✔
4259
        throw new NoSuchElementException();
1 ✔
4260
      }
4261
      var entry = new WriteThroughEntry<K, @NonNull V>(
1 ✔
4262
          cache, requireNonNull(key), requireNonNull(value));
1 ✔
4263
      removalKey = key;
1 ✔
4264
      advance();
1 ✔
4265
      return entry;
1 ✔
4266
    }
4267

4268
    @Override
4269
    public void remove() {
4270
      if (removalKey == null) {
1 ✔
4271
        throw new IllegalStateException();
1 ✔
4272
      }
4273
      cache.remove(removalKey);
1 ✔
4274
      removalKey = null;
1 ✔
4275
    }
1 ✔
4276
  }
4277

4278
  /** An adapter to safely externalize the entry spliterator. */
4279
  static final class EntrySpliterator<K, V> implements Spliterator<Entry<K, V>> {
4280
    final Spliterator<Node<K, V>> spliterator;
4281
    final BoundedLocalCache<K, V> cache;
4282

4283
    EntrySpliterator(BoundedLocalCache<K, V> cache) {
4284
      this(cache, cache.data.values().spliterator());
1 ✔
4285
    }
1 ✔
4286

4287
    EntrySpliterator(BoundedLocalCache<K, V> cache, Spliterator<Node<K, V>> spliterator) {
1 ✔
4288
      this.spliterator = requireNonNull(spliterator);
1 ✔
4289
      this.cache = requireNonNull(cache);
1 ✔
4290
    }
1 ✔
4291

4292
    @Override
4293
    public void forEachRemaining(Consumer<? super Entry<K, V>> action) {
4294
      requireNonNull(action);
1 ✔
4295
      Consumer<Node<K, V>> consumer = node -> {
1 ✔
4296
        long now = cache.expirationTicker().read();
1 ✔
4297
        boolean expired = cache.hasExpired(node, now);
1 ✔
4298
        K key = node.getKey();
1 ✔
4299
        V value = node.getValue();
1 ✔
4300
        if ((key == null) || (value == null) || (expired && !cache.isComputingAsync(value))) {
1 ✔
4301
          cache.scheduleDrainBuffers();
1 ✔
4302
        } else if (node.isAlive()) {
1 ✔
4303
          action.accept(new WriteThroughEntry<>(cache, key, value));
1 ✔
4304
        }
4305
      };
1 ✔
4306
      spliterator.forEachRemaining(consumer);
1 ✔
4307
    }
1 ✔
4308

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

4333
    @Override
4334
    public @Nullable Spliterator<Entry<K, V>> trySplit() {
4335
      Spliterator<Node<K, V>> split = spliterator.trySplit();
1 ✔
4336
      return (split == null) ? null : new EntrySpliterator<>(cache, split);
1 ✔
4337
    }
4338

4339
    @Override
4340
    public long estimateSize() {
4341
      return spliterator.estimateSize();
1 ✔
4342
    }
4343

4344
    @Override
4345
    public int characteristics() {
4346
      return DISTINCT | CONCURRENT | NONNULL;
1 ✔
4347
    }
4348
  }
4349

4350
  /** A reusable task that performs the maintenance work. */
4351
  static final class PerformCleanupTask
4352
      extends WeakReference<BoundedLocalCache<?, ?>> implements Runnable {
4353

4354
    PerformCleanupTask(BoundedLocalCache<?, ?> cache) {
4355
      super(cache);
1 ✔
4356
    }
1 ✔
4357

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

4376
  /* --------------- Manual Cache --------------- */
4377

4378
  static class BoundedLocalManualCache<K, V> implements LocalManualCache<K, V>, Serializable {
4379
    private static final long serialVersionUID = 1;
4380

4381
    final BoundedLocalCache<K, V> cache;
4382

4383
    @Nullable Policy<K, V> policy;
4384

4385
    BoundedLocalManualCache(Caffeine<K, V> builder) {
4386
      this(builder, null);
1 ✔
4387
    }
1 ✔
4388

4389
    BoundedLocalManualCache(Caffeine<K, V> builder, @Nullable CacheLoader<? super K, V> loader) {
1 ✔
4390
      cache = LocalCacheFactory.newBoundedLocalCache(builder, loader, /* isAsync= */ false);
1 ✔
4391
    }
1 ✔
4392

4393
    @Override
4394
    public final BoundedLocalCache<K, V> cache() {
4395
      return cache;
1 ✔
4396
    }
4397

4398
    @Override
4399
    public final Policy<K, V> policy() {
4400
      @Var var p = policy;
1 ✔
4401
      if (p == null) {
1 ✔
4402
        Function<@Nullable V, @Nullable V> identity = v -> v;
1 ✔
4403
        p = new BoundedPolicy<>(cache, identity);
1 ✔
4404
        policy = p;
1 ✔
4405
      }
4406
      return p;
1 ✔
4407
    }
4408

4409
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
4410
      throw new InvalidObjectException("Proxy required");
1 ✔
4411
    }
4412

4413
    private Object writeReplace() {
4414
      return makeSerializationProxy(cache);
1 ✔
4415
    }
4416
  }
4417

4418
  @SuppressWarnings({"NullableOptional",
4419
    "OptionalAssignedToNull", "OptionalUsedAsFieldOrParameterType"})
4420
  static final class BoundedPolicy<K, V> implements Policy<K, V> {
4421
    final Function<@Nullable V, @Nullable V> transformer;
4422
    final BoundedLocalCache<K, V> cache;
4423

4424
    @Nullable Optional<Eviction<K, V>> eviction;
4425
    @Nullable Optional<FixedRefresh<K, V>> refreshes;
4426
    @Nullable Optional<FixedExpiration<K, V>> afterWrite;
4427
    @Nullable Optional<FixedExpiration<K, V>> afterAccess;
4428
    @Nullable Optional<VarExpiration<K, V>> variable;
4429

4430
    BoundedPolicy(BoundedLocalCache<K, V> cache, Function<@Nullable V, @Nullable V> transformer) {
1 ✔
4431
      this.transformer = transformer;
1 ✔
4432
      this.cache = cache;
1 ✔
4433
    }
1 ✔
4434

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

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

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

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

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

4760
        var probe = (CompletableFuture<V>) cache.put(
1 ✔
4761
            key, asyncValue, expiry, /* onlyIfAbsent= */ true);
4762
        V present = Async.getIfReady(probe);
1 ✔
4763
        if ((probe == null) || (present != null)) {
1 ✔
4764
          return present;
1 ✔
4765
        }
4766

4767
        for (;;) {
4768
          var priorFuture = (CompletableFuture<V>) cache.getIfPresentQuietly(key);
1 ✔
4769
          if (priorFuture != null) {
1 ✔
4770
            if (!priorFuture.isDone()) {
1 ✔
4771
              Async.getWhenSuccessful(priorFuture);
1 ✔
4772
              continue;
1 ✔
4773
            }
4774

4775
            V prior = Async.getWhenSuccessful(priorFuture);
1 ✔
4776
            if (prior != null) {
1 ✔
4777
              return prior;
1 ✔
4778
            }
4779
          }
4780

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

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

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

4823
        return cache.isAsync
1 ✔
4824
            ? computeAsync(key, remappingFunction, expiry)
1 ✔
4825
            : cache.compute(key, remappingFunction, expiry, /* recordLoad= */ true);
1 ✔
4826
      }
4827
      @Nullable V computeAsync(K key,
4828
          BiFunction<? super K, ? super @Nullable V, ? extends @Nullable V> remappingFunction,
4829
          Expiry<? super K, ? super V> expiry) {
4830
        // Keep in sync with LocalAsyncCache.AsMapView#compute(key, remappingFunction)
4831
        @SuppressWarnings("unchecked")
4832
        var delegate = (LocalCache<K, CompletableFuture<V>>) cache;
1 ✔
4833

4834
        @SuppressWarnings({"rawtypes", "unchecked", "Varifier"})
4835
        @Nullable V[] newValue = (@Nullable V[]) new Object[1];
1 ✔
4836
        for (;;) {
4837
          Async.getWhenSuccessful(delegate.getIfPresentQuietly(key));
1 ✔
4838

4839
          var hints = new LocalCache.RemapHints();
1 ✔
4840
          CompletableFuture<V> valueFuture = delegate.compute(
1 ✔
4841
              key, (K k, @Nullable CompletableFuture<V> oldValueFuture) -> {
4842
                if ((oldValueFuture != null) && !oldValueFuture.isDone()) {
1 ✔
4843
                  hints.preserveEntry();
1 ✔
4844
                  return oldValueFuture;
1 ✔
4845
                }
4846

4847
                V oldValue = Async.getIfReady(oldValueFuture);
1 ✔
4848
                BiFunction<? super K, ? super @Nullable V, ? extends @Nullable V> function =
1 ✔
4849
                    delegate.statsAware(remappingFunction, /* recordLoad= */ true);
1 ✔
4850
                newValue[0] = function.apply(key, oldValue);
1 ✔
4851
                return (newValue[0] == null) ? null
1 ✔
4852
                    : CompletableFuture.completedFuture(newValue[0]);
1 ✔
4853
              }, new AsyncExpiry<>(expiry), /* recordLoad= */ false, hints);
4854

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

4878
    static final class FixedExpireAfterWrite<K, V> implements Expiry<K, V> {
4879
      final long duration;
4880
      final TimeUnit unit;
4881

4882
      FixedExpireAfterWrite(long duration, TimeUnit unit) {
1 ✔
4883
        this.duration = duration;
1 ✔
4884
        this.unit = unit;
1 ✔
4885
      }
1 ✔
4886
      @Override public long expireAfterCreate(K key, V value, long currentTime) {
4887
        return unit.toNanos(duration);
1 ✔
4888
      }
4889
      @Override public long expireAfterUpdate(
4890
          K key, V value, long currentTime, long currentDuration) {
4891
        return unit.toNanos(duration);
1 ✔
4892
      }
4893
      @CanIgnoreReturnValue
4894
      @Override public long expireAfterRead(
4895
          K key, V value, long currentTime, long currentDuration) {
4896
        return currentDuration;
1 ✔
4897
      }
4898
    }
4899

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

4933
  /* --------------- Loading Cache --------------- */
4934

4935
  static final class BoundedLocalLoadingCache<K, V>
4936
      extends BoundedLocalManualCache<K, V> implements LocalLoadingCache<K, V> {
4937
    private static final long serialVersionUID = 1;
4938

4939
    final Function<K, @Nullable V> mappingFunction;
4940
    final @Nullable Function<Set<? extends K>, Map<K, V>> bulkMappingFunction;
4941

4942
    BoundedLocalLoadingCache(Caffeine<K, V> builder, CacheLoader<? super K, V> loader) {
4943
      super(builder, loader);
1 ✔
4944
      requireNonNull(loader);
1 ✔
4945
      mappingFunction = newMappingFunction(loader);
1 ✔
4946
      bulkMappingFunction = newBulkMappingFunction(loader);
1 ✔
4947
    }
1 ✔
4948

4949
    @Override
4950
    @SuppressWarnings({"DataFlowIssue", "NullAway"})
4951
    public AsyncCacheLoader<? super K, V> cacheLoader() {
4952
      return cache.cacheLoader;
1 ✔
4953
    }
4954

4955
    @Override
4956
    public Function<K, @Nullable V> mappingFunction() {
4957
      return mappingFunction;
1 ✔
4958
    }
4959

4960
    @Override
4961
    public @Nullable Function<Set<? extends K>, Map<K, V>> bulkMappingFunction() {
4962
      return bulkMappingFunction;
1 ✔
4963
    }
4964

4965
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
4966
      throw new InvalidObjectException("Proxy required");
1 ✔
4967
    }
4968

4969
    private Object writeReplace() {
4970
      return makeSerializationProxy(cache);
1 ✔
4971
    }
4972
  }
4973

4974
  /* --------------- Async Cache --------------- */
4975

4976
  static final class BoundedLocalAsyncCache<K, V> implements LocalAsyncCache<K, V>, Serializable {
4977
    private static final long serialVersionUID = 1;
4978

4979
    final BoundedLocalCache<K, CompletableFuture<V>> cache;
4980

4981
    @Nullable ConcurrentMap<K, CompletableFuture<V>> mapView;
4982
    @Nullable CacheView<K, V> cacheView;
4983
    @Nullable Policy<K, V> policy;
4984

4985
    @SuppressWarnings("unchecked")
4986
    BoundedLocalAsyncCache(Caffeine<K, V> builder) {
1 ✔
4987
      cache = (BoundedLocalCache<K, CompletableFuture<V>>) LocalCacheFactory
1 ✔
4988
          .newBoundedLocalCache(builder, /* cacheLoader= */ null, /* isAsync= */ true);
1 ✔
4989
    }
1 ✔
4990

4991
    @Override
4992
    public BoundedLocalCache<K, CompletableFuture<V>> cache() {
4993
      return cache;
1 ✔
4994
    }
4995

4996
    @Override
4997
    public ConcurrentMap<K, CompletableFuture<V>> asMap() {
4998
      var mv = mapView;
1 ✔
4999
      return (mv == null) ? (mapView = new AsyncAsMapView<>(this)) : mv;
1 ✔
5000
    }
5001

5002
    @Override
5003
    public Cache<K, V> synchronous() {
5004
      var cv = cacheView;
1 ✔
5005
      return (cv == null) ? (cacheView = new CacheView<>(this)) : cv;
1 ✔
5006
    }
5007

5008
    @Override
5009
    public Policy<K, V> policy() {
5010
      @Var var p = policy;
1 ✔
5011
      if (p == null) {
1 ✔
5012
        @SuppressWarnings("unchecked")
5013
        var castCache = (BoundedLocalCache<K, V>) cache;
1 ✔
5014
        Function<CompletableFuture<V>, @Nullable V> transformer = Async::getIfReady;
1 ✔
5015
        @SuppressWarnings("unchecked")
5016
        var castTransformer = (Function<@Nullable V, @Nullable V>) transformer;
1 ✔
5017
        p = new BoundedPolicy<>(castCache, castTransformer);
1 ✔
5018
        policy = p;
1 ✔
5019
      }
5020
      return p;
1 ✔
5021
    }
5022

5023
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
5024
      throw new InvalidObjectException("Proxy required");
1 ✔
5025
    }
5026

5027
    private Object writeReplace() {
5028
      return makeSerializationProxy(cache);
1 ✔
5029
    }
5030
  }
5031

5032
  /* --------------- Async Loading Cache --------------- */
5033

5034
  static final class BoundedLocalAsyncLoadingCache<K, V>
5035
      extends LocalAsyncLoadingCache<K, V> implements Serializable {
5036
    private static final long serialVersionUID = 1;
5037

5038
    final BoundedLocalCache<K, CompletableFuture<V>> cache;
5039

5040
    @Nullable ConcurrentMap<K, CompletableFuture<V>> mapView;
5041
    @Nullable Policy<K, V> policy;
5042

5043
    @SuppressWarnings("unchecked")
5044
    BoundedLocalAsyncLoadingCache(Caffeine<K, V> builder, AsyncCacheLoader<? super K, V> loader) {
5045
      super(loader);
1 ✔
5046
      cache = (BoundedLocalCache<K, CompletableFuture<V>>) LocalCacheFactory
1 ✔
5047
          .newBoundedLocalCache(builder, loader, /* isAsync= */ true);
1 ✔
5048
    }
1 ✔
5049

5050
    @Override
5051
    public BoundedLocalCache<K, CompletableFuture<V>> cache() {
5052
      return cache;
1 ✔
5053
    }
5054

5055
    @Override
5056
    public ConcurrentMap<K, CompletableFuture<V>> asMap() {
5057
      var mv = mapView;
1 ✔
5058
      return (mv == null) ? (mapView = new AsyncAsMapView<>(this)) : mv;
1 ✔
5059
    }
5060

5061
    @Override
5062
    public Policy<K, V> policy() {
5063
      @Var var p = policy;
1 ✔
5064
      if (p == null) {
1 ✔
5065
        @SuppressWarnings("unchecked")
5066
        var castCache = (BoundedLocalCache<K, V>) cache;
1 ✔
5067
        Function<CompletableFuture<V>, @Nullable V> transformer = Async::getIfReady;
1 ✔
5068
        @SuppressWarnings("unchecked")
5069
        var castTransformer = (Function<@Nullable V, @Nullable V>) transformer;
1 ✔
5070
        p = new BoundedPolicy<>(castCache, castTransformer);
1 ✔
5071
        policy = p;
1 ✔
5072
      }
5073
      return p;
1 ✔
5074
    }
5075

5076
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
5077
      throw new InvalidObjectException("Proxy required");
1 ✔
5078
    }
5079

5080
    private Object writeReplace() {
5081
      return makeSerializationProxy(cache);
1 ✔
5082
    }
5083
  }
5084
}
5085

5086
/** The namespace for field padding through inheritance. */
5087
@SuppressWarnings({"IdentifierName", "MultiVariableDeclaration"})
5088
final class BLCHeader {
5089

5090
  private BLCHeader() {}
5091

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

5111
  /** Enforces a memory layout to avoid false sharing by padding the drain status. */
5112
  abstract static class DrainStatusRef extends PadDrainStatus {
1 ✔
5113
    static final VarHandle DRAIN_STATUS = fieldVarHandle(MethodHandles.lookup(),
1 ✔
5114
        "drainStatus", VarHandle.class, DrainStatusRef.class, int.class);
5115

5116
    /** A drain is not taking place. */
5117
    static final int IDLE = 0;
5118
    /** A drain is required due to a pending write modification. */
5119
    static final int REQUIRED = 1;
5120
    /** A drain is in progress and will transition to idle. */
5121
    static final int PROCESSING_TO_IDLE = 2;
5122
    /** A drain is in progress and will transition to required. */
5123
    static final int PROCESSING_TO_REQUIRED = 3;
5124

5125
    /** The draining status of the buffers. */
5126
    volatile int drainStatus = IDLE;
1 ✔
5127

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

5148
    int drainStatusOpaque() {
5149
      return (int) DRAIN_STATUS.getOpaque(this);
1 ✔
5150
    }
5151

5152
    int drainStatusAcquire() {
5153
      return (int) DRAIN_STATUS.getAcquire(this);
1 ✔
5154
    }
5155

5156
    void setDrainStatusOpaque(int drainStatus) {
5157
      DRAIN_STATUS.setOpaque(this, drainStatus);
1 ✔
5158
    }
1 ✔
5159

5160
    void setDrainStatusRelease(int drainStatus) {
5161
      DRAIN_STATUS.setRelease(this, drainStatus);
1 ✔
5162
    }
1 ✔
5163

5164
    boolean casDrainStatus(int expect, int update) {
5165
      return DRAIN_STATUS.compareAndSet(this, expect, update);
1 ✔
5166
    }
5167
  }
5168
}
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