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

ben-manes / caffeine / #4743

16 Feb 2025 03:59AM UTC coverage: 99.07% (+0.01%) from 99.057%
#4743

push

github

ben-manes
modernize the simulator to use jdk21 (unrelated to the core library)

As dependencies are moving on from jdk11, it makes sense for the
analysis tools to upgrade as well. This was already executing as 21
to support those dependencies and now simply requires it at the
language level. The user-facing library and jmh benchmarks remain jdk11
compatible.

A profile of the execution showed excessive garbage, which is now
reduced to improve runtimes.
- A direct mapped cache of boxed Long keys are used to reduce the
allocations by caching libraries. The research policies use primitive
based data structures, but user-facing libraries are not as fortunate.
- Using cache listeners to track eviction counts can require the
libraries allocate, e.g. copy the entry. This is safe enough to trust
their stats because its usually the hit rate that is accidentally
miscalculated and we verify that matches. The eviction count is easily
derrived (miss count - size) so just a sanity check.
* This particular sped up Ehcache from an 18m run to 17m, because if
any event type is registered against then it emits and does work for
the unregistered event types too. This may seem like low-hanging fruit
for them to fix, but every other cache runs in seconds (7s for
Caffeine) so it is already unusable. Their response was that Ehcache v3
is not intended to be used in scenarios where LRU eviction might occur,
only as a safety net, so any eviction is considered user error.

Some nice simplifications due to language improvements:
- Replaced AutoValue with Records
- More concise instanceof casting
- More concise switch expressions

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

1 existing line in 1 file now uncovered.

7672 of 7744 relevant lines covered (99.07%)

0.99 hits per line

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

99.4
/caffeine/src/main/java/com/github/benmanes/caffeine/cache/UnboundedLocalCache.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.Caffeine.calculateHashMapCapacity;
19
import static com.github.benmanes.caffeine.cache.LocalLoadingCache.newBulkMappingFunction; // NOPMD
20
import static com.github.benmanes.caffeine.cache.LocalLoadingCache.newMappingFunction; // NOPMD
21
import static java.util.Objects.requireNonNull;
22
import static java.util.function.Function.identity;
23

24
import java.io.InvalidObjectException;
25
import java.io.ObjectInputStream;
26
import java.io.Serializable;
27
import java.lang.System.Logger;
28
import java.lang.System.Logger.Level;
29
import java.lang.invoke.MethodHandles;
30
import java.lang.invoke.VarHandle;
31
import java.util.AbstractCollection;
32
import java.util.AbstractSet;
33
import java.util.Collection;
34
import java.util.Collections;
35
import java.util.HashMap;
36
import java.util.Iterator;
37
import java.util.LinkedHashMap;
38
import java.util.List;
39
import java.util.Map;
40
import java.util.Optional;
41
import java.util.Set;
42
import java.util.Spliterator;
43
import java.util.concurrent.CompletableFuture;
44
import java.util.concurrent.ConcurrentHashMap;
45
import java.util.concurrent.ConcurrentMap;
46
import java.util.concurrent.Executor;
47
import java.util.function.BiConsumer;
48
import java.util.function.BiFunction;
49
import java.util.function.Consumer;
50
import java.util.function.Function;
51
import java.util.function.Predicate;
52

53
import org.jspecify.annotations.Nullable;
54

55
import com.github.benmanes.caffeine.cache.stats.StatsCounter;
56
import com.google.errorprone.annotations.CanIgnoreReturnValue;
57
import com.google.errorprone.annotations.Var;
58

59
/**
60
 * An in-memory cache that has no capabilities for bounding the map. This implementation provides
61
 * a lightweight wrapper on top of {@link ConcurrentHashMap}.
62
 *
63
 * @author ben.manes@gmail.com (Ben Manes)
64
 */
65
@SuppressWarnings("serial")
66
final class UnboundedLocalCache<K, V> implements LocalCache<K, V> {
67
  static final Logger logger = System.getLogger(UnboundedLocalCache.class.getName());
1✔
68
  static final VarHandle REFRESHES;
69

70
  final @Nullable RemovalListener<K, V> removalListener;
71
  final ConcurrentHashMap<K, V> data;
72
  final StatsCounter statsCounter;
73
  final boolean isRecordingStats;
74
  final Executor executor;
75
  final boolean isAsync;
76

77
  @Nullable Set<K> keySet;
78
  @Nullable Collection<V> values;
79
  @Nullable Set<Entry<K, V>> entrySet;
80
  volatile @Nullable ConcurrentMap<Object, CompletableFuture<?>> refreshes;
81

82
  UnboundedLocalCache(Caffeine<? super K, ? super V> builder, boolean isAsync) {
1✔
83
    this.data = new ConcurrentHashMap<>(builder.getInitialCapacity());
1✔
84
    this.statsCounter = builder.getStatsCounterSupplier().get();
1✔
85
    this.removalListener = builder.getRemovalListener(isAsync);
1✔
86
    this.isRecordingStats = builder.isRecordingStats();
1✔
87
    this.executor = builder.getExecutor();
1✔
88
    this.isAsync = isAsync;
1✔
89
  }
1✔
90

91
  static {
92
    try {
93
      REFRESHES = MethodHandles.lookup()
1✔
94
          .findVarHandle(UnboundedLocalCache.class, "refreshes", ConcurrentMap.class);
1✔
95
    } catch (ReflectiveOperationException e) {
×
96
      throw new ExceptionInInitializerError(e);
×
97
    }
1✔
98
  }
1✔
99

100
  @Override
101
  public boolean isAsync() {
102
    return isAsync;
1✔
103
  }
104

105
  @Override
106
  @SuppressWarnings("NullAway")
107
  public @Nullable Expiry<K, V> expiry() {
108
    return null;
1✔
109
  }
110

111
  @Override
112
  @CanIgnoreReturnValue
113
  public Object referenceKey(K key) {
114
    return key;
1✔
115
  }
116

117
  @Override
118
  public boolean isPendingEviction(K key) {
119
    return false;
1✔
120
  }
121

122
  /* --------------- Cache --------------- */
123

124
  @Override
125
  @SuppressWarnings("SuspiciousMethodCalls")
126
  public @Nullable V getIfPresent(Object key, boolean recordStats) {
127
    V value = data.get(key);
1✔
128

129
    if (recordStats) {
1✔
130
      if (value == null) {
1✔
131
        statsCounter.recordMisses(1);
1✔
132
      } else {
133
        statsCounter.recordHits(1);
1✔
134
      }
135
    }
136
    return value;
1✔
137
  }
138

139
  @Override
140
  @SuppressWarnings("SuspiciousMethodCalls")
141
  public @Nullable V getIfPresentQuietly(Object key) {
142
    return data.get(key);
1✔
143
  }
144

145
  @Override
146
  public long estimatedSize() {
147
    return data.mappingCount();
1✔
148
  }
149

150
  @Override
151
  public Map<K, V> getAllPresent(Iterable<? extends K> keys) {
152
    var result = new LinkedHashMap<K, V>(calculateHashMapCapacity(keys));
1✔
153
    for (K key : keys) {
1✔
154
      result.put(key, null);
1✔
155
    }
1✔
156

157
    int uniqueKeys = result.size();
1✔
158
    for (var iter = result.entrySet().iterator(); iter.hasNext();) {
1✔
159
      Map.Entry<K, V> entry = iter.next();
1✔
160
      V value = data.get(entry.getKey());
1✔
161
      if (value == null) {
1✔
162
        iter.remove();
1✔
163
      } else {
164
        entry.setValue(value);
1✔
165
      }
166
    }
1✔
167
    statsCounter.recordHits(result.size());
1✔
168
    statsCounter.recordMisses(uniqueKeys - result.size());
1✔
169

170
    return Collections.unmodifiableMap(result);
1✔
171
  }
172

173
  @Override
174
  public void cleanUp() {}
1✔
175

176
  @Override
177
  public StatsCounter statsCounter() {
178
    return statsCounter;
1✔
179
  }
180

181
  private boolean hasRemovalListener() {
182
    return (removalListener != null);
1✔
183
  }
184

185
  @Override
186
  @SuppressWarnings("NullAway")
187
  public void notifyRemoval(@Nullable K key, @Nullable V value, RemovalCause cause) {
188
    if (!hasRemovalListener()) {
1✔
189
      return;
1✔
190
    }
191
    Runnable task = () -> {
1✔
192
      try {
193
        removalListener.onRemoval(key, value, cause);
1✔
194
      } catch (Throwable t) {
1✔
195
        logger.log(Level.WARNING, "Exception thrown by removal listener", t);
1✔
196
      }
1✔
197
    };
1✔
198
    try {
199
      executor.execute(task);
1✔
200
    } catch (Throwable t) {
1✔
201
      logger.log(Level.ERROR, "Exception thrown when submitting removal listener", t);
1✔
202
      task.run();
1✔
203
    }
1✔
204
  }
1✔
205

206
  @Override
207
  public boolean isRecordingStats() {
208
    return isRecordingStats;
1✔
209
  }
210

211
  @Override
212
  public Executor executor() {
213
    return executor;
1✔
214
  }
215

216
  @Override
217
  @SuppressWarnings("NullAway")
218
  public ConcurrentMap<Object, CompletableFuture<?>> refreshes() {
219
    @Var var pending = refreshes;
1✔
220
    if (pending == null) {
1✔
221
      pending = new ConcurrentHashMap<>();
1✔
222
      if (!REFRESHES.compareAndSet(this, null, pending)) {
1✔
UNCOV
223
        pending = refreshes;
×
224
      }
225
    }
226
    return pending;
1✔
227
  }
228

229
  /** Invalidate the in-flight refresh. */
230
  void discardRefresh(Object keyReference) {
231
    var pending = refreshes;
1✔
232
    if (pending != null) {
1✔
233
      pending.remove(keyReference);
1✔
234
    }
235
  }
1✔
236

237
  @Override
238
  public Ticker statsTicker() {
239
    return isRecordingStats ? Ticker.systemTicker() : Ticker.disabledTicker();
1✔
240
  }
241

242
  /* --------------- JDK8+ Map extensions --------------- */
243

244
  @Override
245
  public void forEach(BiConsumer<? super K, ? super V> action) {
246
    data.forEach(action);
1✔
247
  }
1✔
248

249
  @Override
250
  public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
251
    requireNonNull(function);
1✔
252

253
    // ensures that the removal notification is processed after the removal has completed
254
    @SuppressWarnings({"rawtypes", "unchecked", "Varifier"})
255
    @Nullable K[] notificationKey = (K[]) new Object[1];
1✔
256
    @SuppressWarnings({"rawtypes", "unchecked", "Varifier"})
257
    @Nullable V[] notificationValue = (V[]) new Object[1];
1✔
258
    data.replaceAll((key, value) -> {
1✔
259
      if (notificationKey[0] != null) {
1✔
260
        notifyRemoval(notificationKey[0], notificationValue[0], RemovalCause.REPLACED);
1✔
261
        notificationValue[0] = null;
1✔
262
        notificationKey[0] = null;
1✔
263
      }
264

265
      V newValue = requireNonNull(function.apply(key, value));
1✔
266
      if (newValue != value) {
1✔
267
        notificationKey[0] = key;
1✔
268
        notificationValue[0] = value;
1✔
269
      }
270

271
      return newValue;
1✔
272
    });
273
    if (notificationKey[0] != null) {
1✔
274
      notifyRemoval(notificationKey[0], notificationValue[0], RemovalCause.REPLACED);
1✔
275
    }
276
  }
1✔
277

278
  @Override
279
  public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction,
280
      boolean recordStats, boolean recordLoad) {
281
    requireNonNull(mappingFunction);
1✔
282

283
    // optimistic fast path due to computeIfAbsent always locking
284
    @Var V value = data.get(key);
1✔
285
    if (value != null) {
1✔
286
      if (recordStats) {
1✔
287
        statsCounter.recordHits(1);
1✔
288
      }
289
      return value;
1✔
290
    }
291

292
    boolean[] missed = new boolean[1];
1✔
293
    value = data.computeIfAbsent(key, k -> {
1✔
294
      // Do not communicate to CacheWriter on a load
295
      missed[0] = true;
1✔
296
      return recordStats
1✔
297
          ? statsAware(mappingFunction, recordLoad).apply(key)
1✔
298
          : mappingFunction.apply(key);
1✔
299
    });
300
    if (!missed[0] && recordStats) {
1✔
301
      statsCounter.recordHits(1);
1✔
302
    }
303
    return value;
1✔
304
  }
305

306
  @Override
307
  public @Nullable V computeIfPresent(K key,
308
      BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
309
    requireNonNull(remappingFunction);
1✔
310

311
    // optimistic fast path due to computeIfAbsent always locking
312
    if (!data.containsKey(key)) {
1✔
313
      return null;
1✔
314
    }
315

316
    // ensures that the removal notification is processed after the removal has completed
317
    @SuppressWarnings({"rawtypes", "unchecked"})
318
    var oldValue = (V[]) new Object[1];
1✔
319
    boolean[] replaced = new boolean[1];
1✔
320
    V nv = data.computeIfPresent(key, (K k, V value) -> {
1✔
321
      BiFunction<? super K, ? super V, ? extends V> function = statsAware(remappingFunction,
1✔
322
          /* recordLoad= */ true, /* recordLoadFailure= */ true);
323
      V newValue = function.apply(k, value);
1✔
324

325
      replaced[0] = (newValue != null);
1✔
326
      if (newValue != value) {
1✔
327
        oldValue[0] = value;
1✔
328
      }
329

330
      discardRefresh(k);
1✔
331
      return newValue;
1✔
332
    });
333
    if (replaced[0]) {
1✔
334
      notifyOnReplace(key, oldValue[0], nv);
1✔
335
    } else if (oldValue[0] != null) {
1✔
336
      notifyRemoval(key, oldValue[0], RemovalCause.EXPLICIT);
1✔
337
    }
338
    return nv;
1✔
339
  }
340

341
  @Override
342
  public V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction,
343
      @Nullable Expiry<? super K, ? super V> expiry, boolean recordLoad,
344
      boolean recordLoadFailure) {
345
    requireNonNull(remappingFunction);
1✔
346
    return remap(key, statsAware(remappingFunction, recordLoad, recordLoadFailure));
1✔
347
  }
348

349
  @Override
350
  public V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
351
    requireNonNull(remappingFunction);
1✔
352
    requireNonNull(value);
1✔
353

354
    return remap(key, (k, oldValue) ->
1✔
355
      (oldValue == null) ? value : statsAware(remappingFunction).apply(oldValue, value));
1✔
356
  }
357

358
  /**
359
   * A {@link Map#compute(Object, BiFunction)} that does not directly record any cache statistics.
360
   *
361
   * @param key key with which the specified value is to be associated
362
   * @param remappingFunction the function to compute a value
363
   * @return the new value associated with the specified key, or null if none
364
   */
365
  V remap(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
366
    // ensures that the removal notification is processed after the removal has completed
367
    @SuppressWarnings({"rawtypes", "unchecked"})
368
    var oldValue = (V[]) new Object[1];
1✔
369
    boolean[] replaced = new boolean[1];
1✔
370
    V nv = data.compute(key, (K k, V value) -> {
1✔
371
      V newValue = remappingFunction.apply(k, value);
1✔
372
      if ((value == null) && (newValue == null)) {
1✔
373
        return null;
1✔
374
      }
375

376
      replaced[0] = (newValue != null);
1✔
377
      if (newValue != value) {
1✔
378
        oldValue[0] = value;
1✔
379
      }
380

381
      discardRefresh(k);
1✔
382
      return newValue;
1✔
383
    });
384
    if (replaced[0]) {
1✔
385
      notifyOnReplace(key, oldValue[0], nv);
1✔
386
    } else if (oldValue[0] != null) {
1✔
387
      notifyRemoval(key, oldValue[0], RemovalCause.EXPLICIT);
1✔
388
    }
389
    return nv;
1✔
390
  }
391

392
  /* --------------- Concurrent Map --------------- */
393

394
  @Override
395
  public boolean isEmpty() {
396
    return data.isEmpty();
1✔
397
  }
398

399
  @Override
400
  public int size() {
401
    return data.size();
1✔
402
  }
403

404
  @Override
405
  public void clear() {
406
    if (!hasRemovalListener() && ((refreshes == null) || refreshes.isEmpty())) {
1✔
407
      data.clear();
1✔
408
      return;
1✔
409
    }
410
    for (K key : List.copyOf(data.keySet())) {
1✔
411
      remove(key);
1✔
412
    }
1✔
413
  }
1✔
414

415
  @Override
416
  public boolean containsKey(Object key) {
417
    return data.containsKey(key);
1✔
418
  }
419

420
  @Override
421
  public boolean containsValue(Object value) {
422
    return data.containsValue(value);
1✔
423
  }
424

425
  @Override
426
  public @Nullable V get(Object key) {
427
    return getIfPresent(key, /* recordStats= */ false);
1✔
428
  }
429

430
  @Override
431
  public @Nullable V put(K key, V value) {
432
    V oldValue = data.put(key, value);
1✔
433
    notifyOnReplace(key, oldValue, value);
1✔
434
    return oldValue;
1✔
435
  }
436

437
  @Override
438
  public @Nullable V putIfAbsent(K key, V value) {
439
    return data.putIfAbsent(key, value);
1✔
440
  }
441

442
  @Override
443
  public void putAll(Map<? extends K, ? extends V> map) {
444
    if (hasRemovalListener()) {
1✔
445
      map.forEach(this::put);
1✔
446
    } else {
447
      data.putAll(map);
1✔
448
    }
449
  }
1✔
450

451
  @Override
452
  public @Nullable V remove(Object key) {
453
    @SuppressWarnings("unchecked")
454
    var castKey = (K) key;
1✔
455
    @SuppressWarnings({"rawtypes", "unchecked"})
456
    var oldValue = (V[]) new Object[1];
1✔
457
    data.computeIfPresent(castKey, (k, v) -> {
1✔
458
      discardRefresh(k);
1✔
459
      oldValue[0] = v;
1✔
460
      return null;
1✔
461
    });
462

463
    if (oldValue[0] != null) {
1✔
464
      notifyRemoval(castKey, oldValue[0], RemovalCause.EXPLICIT);
1✔
465
    }
466

467
    return oldValue[0];
1✔
468
  }
469

470
  @Override
471
  public boolean remove(Object key, Object value) {
472
    if (value == null) {
1✔
473
      requireNonNull(key);
1✔
474
      return false;
1✔
475
    }
476

477
    @SuppressWarnings("unchecked")
478
    var castKey = (K) key;
1✔
479
    @SuppressWarnings({"rawtypes", "unchecked"})
480
    var oldValue = (V[]) new Object[1];
1✔
481

482
    data.computeIfPresent(castKey, (k, v) -> {
1✔
483
      if (v.equals(value)) {
1✔
484
        discardRefresh(k);
1✔
485
        oldValue[0] = v;
1✔
486
        return null;
1✔
487
      }
488
      return v;
1✔
489
    });
490

491
    if (oldValue[0] != null) {
1✔
492
      notifyRemoval(castKey, oldValue[0], RemovalCause.EXPLICIT);
1✔
493
      return true;
1✔
494
    }
495
    return false;
1✔
496
  }
497

498
  @Override
499
  public @Nullable V replace(K key, V value) {
500
    requireNonNull(value);
1✔
501

502
    @SuppressWarnings({"rawtypes", "unchecked"})
503
    var oldValue = (V[]) new Object[1];
1✔
504
    data.computeIfPresent(key, (k, v) -> {
1✔
505
      discardRefresh(k);
1✔
506
      oldValue[0] = v;
1✔
507
      return value;
1✔
508
    });
509

510
    if ((oldValue[0] != null) && (oldValue[0] != value)) {
1✔
511
      notifyRemoval(key, oldValue[0], RemovalCause.REPLACED);
1✔
512
    }
513
    return oldValue[0];
1✔
514
  }
515

516
  @Override
517
  public boolean replace(K key, V oldValue, V newValue) {
518
    return replace(key, oldValue, newValue, /* shouldDiscardRefresh= */ true);
1✔
519
  }
520

521
  @Override
522
  public boolean replace(K key, V oldValue, V newValue, boolean shouldDiscardRefresh) {
523
    requireNonNull(oldValue);
1✔
524
    requireNonNull(newValue);
1✔
525

526
    @SuppressWarnings({"rawtypes", "unchecked"})
527
    var prev = (V[]) new Object[1];
1✔
528
    data.computeIfPresent(key, (k, v) -> {
1✔
529
      if (v.equals(oldValue)) {
1✔
530
        if (shouldDiscardRefresh) {
1✔
531
          discardRefresh(k);
1✔
532
        }
533
        prev[0] = v;
1✔
534
        return newValue;
1✔
535
      }
536
      return v;
1✔
537
    });
538

539
    boolean replaced = (prev[0] != null);
1✔
540
    if (replaced && (prev[0] != newValue)) {
1✔
541
      notifyRemoval(key, prev[0], RemovalCause.REPLACED);
1✔
542
    }
543
    return replaced;
1✔
544
  }
545

546
  @Override
547
  @SuppressWarnings("EqualsWhichDoesntCheckParameterClass")
548
  public boolean equals(@Nullable Object o) {
549
    return (o == this) || data.equals(o);
1✔
550
  }
551

552
  @Override
553
  public int hashCode() {
554
    return data.hashCode();
1✔
555
  }
556

557
  @Override
558
  public String toString() {
559
    var result = new StringBuilder(50).append('{');
1✔
560
    data.forEach((key, value) -> {
1✔
561
      if (result.length() != 1) {
1✔
562
        result.append(", ");
1✔
563
      }
564
      result.append((key == this) ? "(this Map)" : key)
1✔
565
          .append('=')
1✔
566
          .append((value == this) ? "(this Map)" : value);
1✔
567
    });
1✔
568
    return result.append('}').toString();
1✔
569
  }
570

571
  @Override
572
  public Set<K> keySet() {
573
    Set<K> ks = keySet;
1✔
574
    return (ks == null) ? (keySet = new KeySetView<>(this)) : ks;
1✔
575
  }
576

577
  @Override
578
  public Collection<V> values() {
579
    Collection<V> vs = values;
1✔
580
    return (vs == null) ? (values = new ValuesView<>(this)) : vs;
1✔
581
  }
582

583
  @Override
584
  public Set<Entry<K, V>> entrySet() {
585
    Set<Entry<K, V>> es = entrySet;
1✔
586
    return (es == null) ? (entrySet = new EntrySetView<>(this)) : es;
1✔
587
  }
588

589
  /** An adapter to safely externalize the keys. */
590
  static final class KeySetView<K> extends AbstractSet<K> {
591
    final UnboundedLocalCache<K, ?> cache;
592

593
    KeySetView(UnboundedLocalCache<K, ?> cache) {
1✔
594
      this.cache = requireNonNull(cache);
1✔
595
    }
1✔
596

597
    @Override
598
    public boolean isEmpty() {
599
      return cache.isEmpty();
1✔
600
    }
601

602
    @Override
603
    public int size() {
604
      return cache.size();
1✔
605
    }
606

607
    @Override
608
    public void clear() {
609
      cache.clear();
1✔
610
    }
1✔
611

612
    @Override
613
    @SuppressWarnings("SuspiciousMethodCalls")
614
    public boolean contains(Object o) {
615
      return cache.containsKey(o);
1✔
616
    }
617

618
    @Override
619
    public boolean removeAll(Collection<?> collection) {
620
      requireNonNull(collection);
1✔
621
      @Var boolean modified = false;
1✔
622
      if ((collection instanceof Set<?>) && (collection.size() > size())) {
1✔
623
        for (K key : this) {
1✔
624
          if (collection.contains(key)) {
1✔
625
            modified |= remove(key);
1✔
626
          }
627
        }
1✔
628
      } else {
629
        for (var o : collection) {
1✔
630
          modified |= (o != null) && remove(o);
1✔
631
        }
1✔
632
      }
633
      return modified;
1✔
634
    }
635

636
    @Override
637
    public boolean remove(Object o) {
638
      return (cache.remove(o) != null);
1✔
639
    }
640

641
    @Override
642
    public boolean removeIf(Predicate<? super K> filter) {
643
      requireNonNull(filter);
1✔
644
      @Var boolean modified = false;
1✔
645
      for (K key : this) {
1✔
646
        if (filter.test(key) && remove(key)) {
1✔
647
          modified = true;
1✔
648
        }
649
      }
1✔
650
      return modified;
1✔
651
    }
652

653
    @Override
654
    public boolean retainAll(Collection<?> collection) {
655
      requireNonNull(collection);
1✔
656
      @Var boolean modified = false;
1✔
657
      for (K key : this) {
1✔
658
        if (!collection.contains(key) && remove(key)) {
1✔
659
          modified = true;
1✔
660
        }
661
      }
1✔
662
      return modified;
1✔
663
    }
664

665
    @Override
666
    public void forEach(Consumer<? super K> action) {
667
      cache.data.keySet().forEach(action);
1✔
668
    }
1✔
669

670
    @Override
671
    public Iterator<K> iterator() {
672
      return new KeyIterator<>(cache);
1✔
673
    }
674

675
    @Override
676
    public Spliterator<K> spliterator() {
677
      return cache.data.keySet().spliterator();
1✔
678
    }
679

680
    @Override
681
    public Object[] toArray() {
682
      return cache.data.keySet().toArray();
1✔
683
    }
684

685
    @Override
686
    public <T> T[] toArray(T[] array) {
687
      return cache.data.keySet().toArray(array);
1✔
688
    }
689
  }
690

691
  /** An adapter to safely externalize the key iterator. */
692
  static final class KeyIterator<K> implements Iterator<K> {
693
    final UnboundedLocalCache<K, ?> cache;
694
    final Iterator<K> iterator;
695
    @Nullable K current;
696

697
    KeyIterator(UnboundedLocalCache<K, ?> cache) {
1✔
698
      this.iterator = cache.data.keySet().iterator();
1✔
699
      this.cache = cache;
1✔
700
    }
1✔
701

702
    @Override
703
    public boolean hasNext() {
704
      return iterator.hasNext();
1✔
705
    }
706

707
    @Override
708
    public K next() {
709
      current = iterator.next();
1✔
710
      return current;
1✔
711
    }
712

713
    @Override
714
    public void remove() {
715
      if (current == null) {
1✔
716
        throw new IllegalStateException();
1✔
717
      }
718
      cache.remove(current);
1✔
719
      current = null;
1✔
720
    }
1✔
721
  }
722

723
  /** An adapter to safely externalize the values. */
724
  static final class ValuesView<K, V> extends AbstractCollection<V> {
725
    final UnboundedLocalCache<K, V> cache;
726

727
    ValuesView(UnboundedLocalCache<K, V> cache) {
1✔
728
      this.cache = requireNonNull(cache);
1✔
729
    }
1✔
730

731
    @Override
732
    public boolean isEmpty() {
733
      return cache.isEmpty();
1✔
734
    }
735

736
    @Override
737
    public int size() {
738
      return cache.size();
1✔
739
    }
740

741
    @Override
742
    public void clear() {
743
      cache.clear();
1✔
744
    }
1✔
745

746
    @Override
747
    @SuppressWarnings("SuspiciousMethodCalls")
748
    public boolean contains(Object o) {
749
      return cache.containsValue(o);
1✔
750
    }
751

752
    @Override
753
    public boolean removeAll(Collection<?> collection) {
754
      requireNonNull(collection);
1✔
755
      @Var boolean modified = false;
1✔
756
      for (var entry : cache.data.entrySet()) {
1✔
757
        if (collection.contains(entry.getValue())
1✔
758
            && cache.remove(entry.getKey(), entry.getValue())) {
1✔
759
          modified = true;
1✔
760
        }
761
      }
1✔
762
      return modified;
1✔
763
    }
764

765
    @Override
766
    public boolean remove(Object o) {
767
      if (o == null) {
1✔
768
        return false;
1✔
769
      }
770
      for (var entry : cache.data.entrySet()) {
1✔
771
        if (o.equals(entry.getValue()) && cache.remove(entry.getKey(), entry.getValue())) {
1✔
772
          return true;
1✔
773
        }
774
      }
1✔
775
      return false;
1✔
776
    }
777

778
    @Override
779
    public boolean removeIf(Predicate<? super V> filter) {
780
      requireNonNull(filter);
1✔
781
      @Var boolean removed = false;
1✔
782
      for (var entry : cache.data.entrySet()) {
1✔
783
        if (filter.test(entry.getValue())) {
1✔
784
          removed |= cache.remove(entry.getKey(), entry.getValue());
1✔
785
        }
786
      }
1✔
787
      return removed;
1✔
788
    }
789

790
    @Override
791
    public boolean retainAll(Collection<?> collection) {
792
      requireNonNull(collection);
1✔
793
      @Var boolean modified = false;
1✔
794
      for (var entry : cache.data.entrySet()) {
1✔
795
        if (!collection.contains(entry.getValue())
1✔
796
            && cache.remove(entry.getKey(), entry.getValue())) {
1✔
797
          modified = true;
1✔
798
        }
799
      }
1✔
800
      return modified;
1✔
801
    }
802

803
    @Override
804
    public void forEach(Consumer<? super V> action) {
805
      cache.data.values().forEach(action);
1✔
806
    }
1✔
807

808
    @Override
809
    public Iterator<V> iterator() {
810
      return new ValuesIterator<>(cache);
1✔
811
    }
812

813
    @Override
814
    public Spliterator<V> spliterator() {
815
      return cache.data.values().spliterator();
1✔
816
    }
817

818
    @Override
819
    public Object[] toArray() {
820
      return cache.data.values().toArray();
1✔
821
    }
822

823
    @Override
824
    public <T> T[] toArray(T[] array) {
825
      return cache.data.values().toArray(array);
1✔
826
    }
827
  }
828

829
  /** An adapter to safely externalize the value iterator. */
830
  static final class ValuesIterator<K, V> implements Iterator<V> {
831
    final UnboundedLocalCache<K, V> cache;
832
    final Iterator<Entry<K, V>> iterator;
833
    @Nullable Entry<K, V> entry;
834

835
    ValuesIterator(UnboundedLocalCache<K, V> cache) {
1✔
836
      this.iterator = cache.data.entrySet().iterator();
1✔
837
      this.cache = cache;
1✔
838
    }
1✔
839

840
    @Override
841
    public boolean hasNext() {
842
      return iterator.hasNext();
1✔
843
    }
844

845
    @Override
846
    public V next() {
847
      entry = iterator.next();
1✔
848
      return entry.getValue();
1✔
849
    }
850

851
    @Override
852
    public void remove() {
853
      if (entry == null) {
1✔
854
        throw new IllegalStateException();
1✔
855
      }
856
      cache.remove(entry.getKey());
1✔
857
      entry = null;
1✔
858
    }
1✔
859
  }
860

861
  /** An adapter to safely externalize the entries. */
862
  static final class EntrySetView<K, V> extends AbstractSet<Entry<K, V>> {
863
    final UnboundedLocalCache<K, V> cache;
864

865
    EntrySetView(UnboundedLocalCache<K, V> cache) {
1✔
866
      this.cache = requireNonNull(cache);
1✔
867
    }
1✔
868

869
    @Override
870
    public boolean isEmpty() {
871
      return cache.isEmpty();
1✔
872
    }
873

874
    @Override
875
    public int size() {
876
      return cache.size();
1✔
877
    }
878

879
    @Override
880
    public void clear() {
881
      cache.clear();
1✔
882
    }
1✔
883

884
    @Override
885
    @SuppressWarnings("SuspiciousMethodCalls")
886
    public boolean contains(Object o) {
887
      if (!(o instanceof Entry<?, ?>)) {
1✔
888
        return false;
1✔
889
      }
890
      var entry = (Entry<?, ?>) o;
1✔
891
      var key = entry.getKey();
1✔
892
      var value = entry.getValue();
1✔
893
      if ((key == null) || (value == null)) {
1✔
894
        return false;
1✔
895
      }
896
      V cachedValue = cache.get(key);
1✔
897
      return (cachedValue != null) && cachedValue.equals(value);
1✔
898
    }
899

900
    @Override
901
    public boolean removeAll(Collection<?> collection) {
902
      requireNonNull(collection);
1✔
903
      @Var boolean modified = false;
1✔
904
      if ((collection instanceof Set<?>) && (collection.size() > size())) {
1✔
905
        for (var entry : this) {
1✔
906
          if (collection.contains(entry)) {
1✔
907
            modified |= remove(entry);
1✔
908
          }
909
        }
1✔
910
      } else {
911
        for (var o : collection) {
1✔
912
          modified |= remove(o);
1✔
913
        }
1✔
914
      }
915
      return modified;
1✔
916
    }
917

918
    @Override
919
    @SuppressWarnings("SuspiciousMethodCalls")
920
    public boolean remove(Object o) {
921
      if (!(o instanceof Entry<?, ?>)) {
1✔
922
        return false;
1✔
923
      }
924
      var entry = (Entry<?, ?>) o;
1✔
925
      var key = entry.getKey();
1✔
926
      return (key != null) && cache.remove(key, entry.getValue());
1✔
927
    }
928

929
    @Override
930
    public boolean removeIf(Predicate<? super Entry<K, V>> filter) {
931
      requireNonNull(filter);
1✔
932
      @Var boolean removed = false;
1✔
933
      for (var entry : cache.data.entrySet()) {
1✔
934
        if (filter.test(entry)) {
1✔
935
          removed |= cache.remove(entry.getKey(), entry.getValue());
1✔
936
        }
937
      }
1✔
938
      return removed;
1✔
939
    }
940

941
    @Override
942
    public boolean retainAll(Collection<?> collection) {
943
      requireNonNull(collection);
1✔
944
      @Var boolean modified = false;
1✔
945
      for (var entry : this) {
1✔
946
        if (!collection.contains(entry) && remove(entry)) {
1✔
947
          modified = true;
1✔
948
        }
949
      }
1✔
950
      return modified;
1✔
951
    }
952

953
    @Override
954
    public Iterator<Entry<K, V>> iterator() {
955
      return new EntryIterator<>(cache);
1✔
956
    }
957

958
    @Override
959
    public Spliterator<Entry<K, V>> spliterator() {
960
      return new EntrySpliterator<>(cache);
1✔
961
    }
962
  }
963

964
  /** An adapter to safely externalize the entry iterator. */
965
  static final class EntryIterator<K, V> implements Iterator<Entry<K, V>> {
966
    final UnboundedLocalCache<K, V> cache;
967
    final Iterator<Entry<K, V>> iterator;
968
    @Nullable Entry<K, V> entry;
969

970
    EntryIterator(UnboundedLocalCache<K, V> cache) {
1✔
971
      this.iterator = cache.data.entrySet().iterator();
1✔
972
      this.cache = cache;
1✔
973
    }
1✔
974

975
    @Override
976
    public boolean hasNext() {
977
      return iterator.hasNext();
1✔
978
    }
979

980
    @Override
981
    public Entry<K, V> next() {
982
      entry = iterator.next();
1✔
983
      return new WriteThroughEntry<>(cache, entry.getKey(), entry.getValue());
1✔
984
    }
985

986
    @Override
987
    public void remove() {
988
      if (entry == null) {
1✔
989
        throw new IllegalStateException();
1✔
990
      }
991
      cache.remove(entry.getKey());
1✔
992
      entry = null;
1✔
993
    }
1✔
994
  }
995

996
  /** An adapter to safely externalize the entry spliterator. */
997
  static final class EntrySpliterator<K, V> implements Spliterator<Entry<K, V>> {
998
    final Spliterator<Entry<K, V>> spliterator;
999
    final UnboundedLocalCache<K, V> cache;
1000

1001
    EntrySpliterator(UnboundedLocalCache<K, V> cache) {
1002
      this(cache, cache.data.entrySet().spliterator());
1✔
1003
    }
1✔
1004

1005
    EntrySpliterator(UnboundedLocalCache<K, V> cache, Spliterator<Entry<K, V>> spliterator) {
1✔
1006
      this.spliterator = requireNonNull(spliterator);
1✔
1007
      this.cache = requireNonNull(cache);
1✔
1008
    }
1✔
1009

1010
    @Override
1011
    public void forEachRemaining(Consumer<? super Entry<K, V>> action) {
1012
      requireNonNull(action);
1✔
1013
      spliterator.forEachRemaining(entry -> {
1✔
1014
        var e = new WriteThroughEntry<>(cache, entry.getKey(), entry.getValue());
1✔
1015
        action.accept(e);
1✔
1016
      });
1✔
1017
    }
1✔
1018

1019
    @Override
1020
    public boolean tryAdvance(Consumer<? super Entry<K, V>> action) {
1021
      requireNonNull(action);
1✔
1022
      return spliterator.tryAdvance(entry -> {
1✔
1023
        var e = new WriteThroughEntry<>(cache, entry.getKey(), entry.getValue());
1✔
1024
        action.accept(e);
1✔
1025
      });
1✔
1026
    }
1027

1028
    @Override
1029
    public @Nullable EntrySpliterator<K, V> trySplit() {
1030
      Spliterator<Entry<K, V>> split = spliterator.trySplit();
1✔
1031
      return (split == null) ? null : new EntrySpliterator<>(cache, split);
1✔
1032
    }
1033

1034
    @Override
1035
    public long estimateSize() {
1036
      return spliterator.estimateSize();
1✔
1037
    }
1038

1039
    @Override
1040
    public int characteristics() {
1041
      return spliterator.characteristics();
1✔
1042
    }
1043
  }
1044

1045
  /* --------------- Manual Cache --------------- */
1046

1047
  static class UnboundedLocalManualCache<K, V> implements LocalManualCache<K, V>, Serializable {
1048
    private static final long serialVersionUID = 1;
1049

1050
    final UnboundedLocalCache<K, V> cache;
1051
    @Nullable Policy<K, V> policy;
1052

1053
    UnboundedLocalManualCache(Caffeine<K, V> builder) {
1✔
1054
      cache = new UnboundedLocalCache<>(builder, /* isAsync= */ false);
1✔
1055
    }
1✔
1056

1057
    @Override
1058
    public final UnboundedLocalCache<K, V> cache() {
1059
      return cache;
1✔
1060
    }
1061

1062
    @Override
1063
    public final Policy<K, V> policy() {
1064
      if (policy == null) {
1✔
1065
        @SuppressWarnings("NullAway")
1066
        Function<@Nullable V, @Nullable V> identity = identity();
1✔
1067
        policy = new UnboundedPolicy<>(cache, identity);
1✔
1068
      }
1069
      return policy;
1✔
1070
    }
1071

1072
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
1073
      throw new InvalidObjectException("Proxy required");
1✔
1074
    }
1075

1076
    Object writeReplace() {
1077
      var proxy = new SerializationProxy<K, V>();
1✔
1078
      proxy.isRecordingStats = cache.isRecordingStats;
1✔
1079
      proxy.removalListener = cache.removalListener;
1✔
1080
      return proxy;
1✔
1081
    }
1082
  }
1083

1084
  /** An eviction policy that supports no bounding. */
1085
  static final class UnboundedPolicy<K, V> implements Policy<K, V> {
1086
    final Function<@Nullable V, @Nullable V> transformer;
1087
    final UnboundedLocalCache<K, V> cache;
1088

1089
    UnboundedPolicy(UnboundedLocalCache<K, V> cache,
1090
        Function<@Nullable V, @Nullable V> transformer) {
1✔
1091
      this.transformer = transformer;
1✔
1092
      this.cache = cache;
1✔
1093
    }
1✔
1094
    @Override public boolean isRecordingStats() {
1095
      return cache.isRecordingStats;
1✔
1096
    }
1097
    @Override public @Nullable V getIfPresentQuietly(K key) {
1098
      return transformer.apply(cache.data.get(key));
1✔
1099
    }
1100
    @Override public @Nullable CacheEntry<K, V> getEntryIfPresentQuietly(K key) {
1101
      V value = transformer.apply(cache.data.get(key));
1✔
1102
      return (value == null) ? null : SnapshotEntry.forEntry(key, value);
1✔
1103
    }
1104
    @SuppressWarnings("Java9CollectionFactory")
1105
    @Override public Map<K, CompletableFuture<V>> refreshes() {
1106
      var refreshes = cache.refreshes;
1✔
1107
      if ((refreshes == null) || refreshes.isEmpty()) {
1✔
1108
        @SuppressWarnings("ImmutableMapOf")
1109
        Map<K, CompletableFuture<V>> emptyMap = Collections.unmodifiableMap(Collections.emptyMap());
1✔
1110
        return emptyMap;
1✔
1111
      }
1112
      @SuppressWarnings("unchecked")
1113
      var castedRefreshes = (Map<K, CompletableFuture<V>>) (Object) refreshes;
1✔
1114
      return Collections.unmodifiableMap(new HashMap<>(castedRefreshes));
1✔
1115
    }
1116
    @Override public Optional<Eviction<K, V>> eviction() {
1117
      return Optional.empty();
1✔
1118
    }
1119
    @Override public Optional<FixedExpiration<K, V>> expireAfterAccess() {
1120
      return Optional.empty();
1✔
1121
    }
1122
    @Override public Optional<FixedExpiration<K, V>> expireAfterWrite() {
1123
      return Optional.empty();
1✔
1124
    }
1125
    @Override public Optional<VarExpiration<K, V>> expireVariably() {
1126
      return Optional.empty();
1✔
1127
    }
1128
    @Override public Optional<FixedRefresh<K, V>> refreshAfterWrite() {
1129
      return Optional.empty();
1✔
1130
    }
1131
  }
1132

1133
  /* --------------- Loading Cache --------------- */
1134

1135
  static final class UnboundedLocalLoadingCache<K, V> extends UnboundedLocalManualCache<K, V>
1136
      implements LocalLoadingCache<K, V> {
1137
    private static final long serialVersionUID = 1;
1138

1139
    final Function<K, V> mappingFunction;
1140
    final CacheLoader<? super K, V> cacheLoader;
1141
    final @Nullable Function<Set<? extends K>, Map<K, V>> bulkMappingFunction;
1142

1143
    UnboundedLocalLoadingCache(Caffeine<K, V> builder, CacheLoader<? super K, V> cacheLoader) {
1144
      super(builder);
1✔
1145
      this.cacheLoader = cacheLoader;
1✔
1146
      this.mappingFunction = newMappingFunction(cacheLoader);
1✔
1147
      this.bulkMappingFunction = newBulkMappingFunction(cacheLoader);
1✔
1148
    }
1✔
1149

1150
    @Override
1151
    public AsyncCacheLoader<? super K, V> cacheLoader() {
1152
      return cacheLoader;
1✔
1153
    }
1154

1155
    @Override
1156
    public Function<K, V> mappingFunction() {
1157
      return mappingFunction;
1✔
1158
    }
1159

1160
    @Override
1161
    public @Nullable Function<Set<? extends K>, Map<K, V>>  bulkMappingFunction() {
1162
      return bulkMappingFunction;
1✔
1163
    }
1164

1165
    @Override
1166
    Object writeReplace() {
1167
      @SuppressWarnings("unchecked")
1168
      var proxy = (SerializationProxy<K, V>) super.writeReplace();
1✔
1169
      proxy.cacheLoader = cacheLoader;
1✔
1170
      return proxy;
1✔
1171
    }
1172

1173
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
1174
      throw new InvalidObjectException("Proxy required");
1✔
1175
    }
1176
  }
1177

1178
  /* --------------- Async Cache --------------- */
1179

1180
  static final class UnboundedLocalAsyncCache<K, V> implements LocalAsyncCache<K, V>, Serializable {
1181
    private static final long serialVersionUID = 1;
1182

1183
    final UnboundedLocalCache<K, CompletableFuture<V>> cache;
1184

1185
    @Nullable ConcurrentMap<K, CompletableFuture<V>> mapView;
1186
    @Nullable CacheView<K, V> cacheView;
1187
    @Nullable Policy<K, V> policy;
1188

1189
    @SuppressWarnings("unchecked")
1190
    UnboundedLocalAsyncCache(Caffeine<K, V> builder) {
1✔
1191
      cache = new UnboundedLocalCache<>(
1✔
1192
          (Caffeine<K, CompletableFuture<V>>) builder, /* isAsync= */ true);
1193
    }
1✔
1194

1195
    @Override
1196
    public UnboundedLocalCache<K, CompletableFuture<V>> cache() {
1197
      return cache;
1✔
1198
    }
1199

1200
    @Override
1201
    public ConcurrentMap<K, CompletableFuture<V>> asMap() {
1202
      return (mapView == null) ? (mapView = new AsyncAsMapView<>(this)) : mapView;
1✔
1203
    }
1204

1205
    @Override
1206
    public Cache<K, V> synchronous() {
1207
      return (cacheView == null) ? (cacheView = new CacheView<>(this)) : cacheView;
1✔
1208
    }
1209

1210
    @Override
1211
    public Policy<K, V> policy() {
1212
      @SuppressWarnings("unchecked")
1213
      var castCache = (UnboundedLocalCache<K, V>) cache;
1✔
1214
      Function<CompletableFuture<V>, @Nullable V> transformer = Async::getIfReady;
1✔
1215
      @SuppressWarnings({"NullAway", "unchecked", "Varifier"})
1216
      Function<@Nullable V, @Nullable V> castTransformer = (Function<V, V>) transformer;
1✔
1217
      return (policy == null)
1✔
1218
          ? (policy = new UnboundedPolicy<>(castCache, castTransformer))
1✔
1219
          : policy;
1✔
1220
    }
1221

1222
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
1223
      throw new InvalidObjectException("Proxy required");
1✔
1224
    }
1225

1226
    Object writeReplace() {
1227
      var proxy = new SerializationProxy<K, V>();
1✔
1228
      proxy.isRecordingStats = cache.isRecordingStats;
1✔
1229
      proxy.removalListener = cache.removalListener;
1✔
1230
      proxy.async = true;
1✔
1231
      return proxy;
1✔
1232
    }
1233
  }
1234

1235
  /* --------------- Async Loading Cache --------------- */
1236

1237
  static final class UnboundedLocalAsyncLoadingCache<K, V>
1238
      extends LocalAsyncLoadingCache<K, V> implements Serializable {
1239
    private static final long serialVersionUID = 1;
1240

1241
    final UnboundedLocalCache<K, CompletableFuture<V>> cache;
1242

1243
    @Nullable ConcurrentMap<K, CompletableFuture<V>> mapView;
1244
    @Nullable Policy<K, V> policy;
1245

1246
    @SuppressWarnings("unchecked")
1247
    UnboundedLocalAsyncLoadingCache(Caffeine<K, V> builder, AsyncCacheLoader<? super K, V> loader) {
1248
      super(loader);
1✔
1249
      cache = new UnboundedLocalCache<>(
1✔
1250
          (Caffeine<K, CompletableFuture<V>>) builder, /* isAsync= */ true);
1251
    }
1✔
1252

1253
    @Override
1254
    public LocalCache<K, CompletableFuture<V>> cache() {
1255
      return cache;
1✔
1256
    }
1257

1258
    @Override
1259
    public ConcurrentMap<K, CompletableFuture<V>> asMap() {
1260
      return (mapView == null) ? (mapView = new AsyncAsMapView<>(this)) : mapView;
1✔
1261
    }
1262

1263
    @Override
1264
    public Policy<K, V> policy() {
1265
      @SuppressWarnings("unchecked")
1266
      var castCache = (UnboundedLocalCache<K, V>) cache;
1✔
1267
      Function<CompletableFuture<V>, @Nullable V> transformer = Async::getIfReady;
1✔
1268
      @SuppressWarnings({"NullAway", "unchecked", "Varifier"})
1269
      Function<@Nullable V, @Nullable V> castTransformer = (Function<V, V>) transformer;
1✔
1270
      return (policy == null)
1✔
1271
          ? (policy = new UnboundedPolicy<>(castCache, castTransformer))
1✔
1272
          : policy;
1✔
1273
    }
1274

1275
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
1276
      throw new InvalidObjectException("Proxy required");
1✔
1277
    }
1278

1279
    Object writeReplace() {
1280
      var proxy = new SerializationProxy<K, V>();
1✔
1281
      proxy.isRecordingStats = cache.isRecordingStats();
1✔
1282
      proxy.removalListener = cache.removalListener;
1✔
1283
      proxy.cacheLoader = cacheLoader;
1✔
1284
      proxy.async = true;
1✔
1285
      return proxy;
1✔
1286
    }
1287
  }
1288
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc