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

ben-manes / caffeine / #5731

17 Aug 2026 06:26AM UTC coverage: 99.866% (-0.1%) from 99.989%
#5731

push

github

ben-manes
fix performance issues and test coverage gaps found by the audit sweep

- Budget the reference queue drains per maintenance cycle
- Leave an unpublished write buffer slot for its own producer
- Probe an async hit's readiness only where a policy reads it

4540 of 4558 branches covered (99.61%)

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

11 existing lines in 4 files now uncovered.

8957 of 8969 relevant lines covered (99.87%)

1.0 hits per line

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

99.88
/jcache/src/main/java/com/github/benmanes/caffeine/jcache/CacheProxy.java
1
/*
2
 * Copyright 2015 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.jcache;
17

18
import static java.util.Objects.requireNonNull;
19
import static java.util.Objects.requireNonNullElse;
20
import static java.util.stream.Collectors.toMap;
21
import static java.util.stream.Collectors.toSet;
22
import static java.util.stream.Collectors.toUnmodifiableList;
23

24
import java.lang.System.Logger;
25
import java.lang.System.Logger.Level;
26
import java.util.ArrayList;
27
import java.util.HashMap;
28
import java.util.Iterator;
29
import java.util.LinkedHashSet;
30
import java.util.List;
31
import java.util.Map;
32
import java.util.NoSuchElementException;
33
import java.util.Objects;
34
import java.util.Optional;
35
import java.util.Set;
36
import java.util.concurrent.CompletableFuture;
37
import java.util.concurrent.ConcurrentHashMap;
38
import java.util.concurrent.ExecutionException;
39
import java.util.concurrent.Executor;
40
import java.util.concurrent.ExecutorService;
41
import java.util.concurrent.TimeUnit;
42
import java.util.concurrent.TimeoutException;
43
import java.util.function.BiFunction;
44
import java.util.function.Consumer;
45
import java.util.function.Supplier;
46

47
import javax.cache.Cache;
48
import javax.cache.CacheException;
49
import javax.cache.CacheManager;
50
import javax.cache.configuration.CacheEntryListenerConfiguration;
51
import javax.cache.configuration.Configuration;
52
import javax.cache.event.CacheEntryListenerException;
53
import javax.cache.expiry.Duration;
54
import javax.cache.expiry.ExpiryPolicy;
55
import javax.cache.integration.CacheLoader;
56
import javax.cache.integration.CacheLoaderException;
57
import javax.cache.integration.CacheWriter;
58
import javax.cache.integration.CacheWriterException;
59
import javax.cache.integration.CompletionListener;
60
import javax.cache.processor.EntryProcessor;
61
import javax.cache.processor.EntryProcessorException;
62
import javax.cache.processor.EntryProcessorResult;
63

64
import org.jspecify.annotations.NonNull;
65
import org.jspecify.annotations.Nullable;
66

67
import com.github.benmanes.caffeine.cache.Ticker;
68
import com.github.benmanes.caffeine.jcache.configuration.CaffeineConfiguration;
69
import com.github.benmanes.caffeine.jcache.copy.Copier;
70
import com.github.benmanes.caffeine.jcache.event.EventDispatcher;
71
import com.github.benmanes.caffeine.jcache.event.Registration;
72
import com.github.benmanes.caffeine.jcache.integration.DisabledCacheWriter;
73
import com.github.benmanes.caffeine.jcache.management.JCacheMXBean;
74
import com.github.benmanes.caffeine.jcache.management.JCacheStatisticsMXBean;
75
import com.github.benmanes.caffeine.jcache.management.JmxRegistration;
76
import com.github.benmanes.caffeine.jcache.management.JmxRegistration.MBeanType;
77
import com.github.benmanes.caffeine.jcache.processor.Action;
78
import com.github.benmanes.caffeine.jcache.processor.EntryProcessorEntry;
79
import com.google.errorprone.annotations.CanIgnoreReturnValue;
80
import com.google.errorprone.annotations.Var;
81

82
/**
83
 * An implementation of JSR-107 {@link Cache} backed by a Caffeine cache.
84
 *
85
 * @author ben.manes@gmail.com (Ben Manes)
86
 */
87
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
88
public class CacheProxy<K, V> implements Cache<K, V> {
89
  private static final Logger logger = System.getLogger(CacheProxy.class.getName());
1✔
90

91
  protected final com.github.benmanes.caffeine.cache.Cache<K, @Nullable Expirable<V>> cache;
92
  protected final Optional<CacheLoader<K, V>> cacheLoader;
93
  protected final Set<CompletableFuture<?>> inFlight;
94
  protected final JCacheStatisticsMXBean statistics;
95
  protected final EventDispatcher<K, V> dispatcher;
96
  protected final Executor executor;
97
  protected final Ticker ticker;
98

99
  private final CaffeineConfiguration<K, V> configuration;
100
  private final CacheManagerImpl cacheManager;
101
  private final CacheWriter<K, V> writer;
102
  private final JCacheMXBean cacheMxBean;
103
  private final ExpiryPolicy expiry;
104
  private final Copier copier;
105
  private final String name;
106

107
  private volatile boolean closed;
108

109
  @SuppressWarnings({"PMD.ExcessiveParameterList", "this-escape", "TooManyParameters"})
110
  public CacheProxy(String name, Executor executor, CacheManagerImpl cacheManager,
111
      CaffeineConfiguration<K, V> configuration,
112
      com.github.benmanes.caffeine.cache.Cache<K, @Nullable Expirable<V>> cache,
113
      EventDispatcher<K, V> dispatcher, Optional<CacheLoader<K, V>> cacheLoader,
114
      ExpiryPolicy expiry, Ticker ticker, JCacheStatisticsMXBean statistics, Copier copier) {
1✔
115
    this.writer = requireNonNullElse(configuration.getCacheWriter(), DisabledCacheWriter.get());
1✔
116
    this.configuration = requireNonNull(configuration);
1✔
117
    this.cacheManager = requireNonNull(cacheManager);
1✔
118
    this.cacheLoader = requireNonNull(cacheLoader);
1✔
119
    this.inFlight = ConcurrentHashMap.newKeySet();
1✔
120
    this.dispatcher = requireNonNull(dispatcher);
1✔
121
    this.statistics = requireNonNull(statistics);
1✔
122
    this.cacheMxBean = new JCacheMXBean(this);
1✔
123
    this.executor = requireNonNull(executor);
1✔
124
    this.expiry = requireNonNull(expiry);
1✔
125
    this.ticker = requireNonNull(ticker);
1✔
126
    this.cache = requireNonNull(cache);
1✔
127
    this.copier = requireNonNull(copier);
1✔
128
    this.name = requireNonNull(name);
1✔
129
  }
1✔
130

131
  @Override
132
  public boolean containsKey(K key) {
133
    requireOperable();
1✔
134

135
    Expirable<V> expirable = cache.getIfPresent(key);
1✔
136
    if (expirable == null) {
1✔
137
      return false;
1✔
138
    }
139
    if (!expirable.isEternal() && expirable.hasExpired(currentTimeMillis())) {
1✔
140
      dispatcher.beginComputation();
1✔
141
      try {
142
        cache.asMap().computeIfPresent(key, (k, e) -> {
1✔
143
          if (e == expirable) {
1✔
144
            dispatcher.publishExpired(this, key, expirable.get());
1✔
145
            statistics.recordEvictions(1L);
1✔
146
            return null;
1✔
147
          }
148
          return e;
1✔
149
        });
150
      } finally {
151
        dispatcher.endComputation();
1✔
152
      }
153
      dispatcher.awaitSynchronous();
1✔
154
      return false;
1✔
155
    }
156
    return true;
1✔
157
  }
158

159
  @Override
160
  public @Nullable V get(K key) {
161
    requireOperable();
1✔
162

163
    boolean statsEnabled = statistics.isEnabled();
1✔
164
    long start = statsEnabled ? ticker.read() : 0L;
1✔
165
    Expirable<V> expirable = cache.getIfPresent(key);
1✔
166
    if (expirable == null) {
1✔
167
      statistics.recordMisses(1L);
1✔
168
      if (statsEnabled) {
1✔
169
        statistics.recordGetTime(ticker.read() - start);
1✔
170
      }
171
      return null;
1✔
172
    }
173

174
    long millis;
175
    if (expirable.isEternal()) {
1✔
176
      millis = 0L;
1✔
177
    } else {
178
      long now = ticker.read();
1✔
179
      millis = nanosToMillis(now);
1✔
180
      if (expirable.hasExpired(millis)) {
1✔
181
        dispatcher.beginComputation();
1✔
182
        try {
183
          cache.asMap().computeIfPresent(key, (k, e) -> {
1✔
184
            if (e == expirable) {
1✔
185
              dispatcher.publishExpired(this, key, expirable.get());
1✔
186
              statistics.recordEvictions(1L);
1✔
187
              return null;
1✔
188
            }
189
            return e;
1✔
190
          });
191
        } finally {
192
          dispatcher.endComputation();
1✔
193
        }
194
        var listenerFailure = awaitSynchronousFailure();
1✔
195
        statistics.recordMisses(1L);
1✔
196
        if (statsEnabled) {
1✔
197
          statistics.recordGetTime(ticker.read() - start);
1✔
198
        }
199
        rethrowListenerFailure(listenerFailure);
1✔
200
        return null;
1✔
201
      }
202
    }
203

204
    var duration = getAccessExpireTime();
1✔
205
    setVariableExpiration(key, duration);
1✔
206
    setAccessExpireTime(expirable, duration, millis);
1✔
207
    V value = copyOf(expirable.get());
1✔
208
    if (statsEnabled) {
1✔
209
      statistics.recordHits(1L);
1✔
210
      statistics.recordGetTime(ticker.read() - start);
1✔
211
    }
212
    return value;
1✔
213
  }
214

215
  @Override
216
  public Map<K, V> getAll(Set<? extends K> keys) {
217
    requireOperable();
1✔
218

219
    boolean statsEnabled = statistics.isEnabled();
1✔
220
    long now = statsEnabled ? ticker.read() : 0L;
1✔
221
    Map<K, V> result;
222
    try {
223
      Map<K, Expirable<V>> entries = getAndFilterExpiredEntries(keys);
1✔
224
      if (statsEnabled) {
1✔
225
        statistics.recordGetTime(ticker.read() - now);
1✔
226
      }
227
      result = copyMap(entries);
1✔
228
    } catch (Throwable t) {
1✔
229
      awaitAndSuppressFailure(t);
1✔
230
      throw t;
1✔
231
    }
1✔
232
    rethrowListenerFailure(awaitSynchronousFailure());
1✔
233
    return result;
1✔
234
  }
235

236
  /**
237
   * Returns all of the mappings present, expiring as required, and updates their access expiry
238
   * time.
239
   */
240
  protected Map<K, Expirable<V>> getAndFilterExpiredEntries(Set<? extends K> keys) {
241
    int[] expired = { 0 };
1✔
242
    long[] millis = { 0L };
1✔
243
    var result = new HashMap<K, @NonNull Expirable<V>>(cache.getAllPresent(keys));
1✔
244
    result.entrySet().removeIf(entry -> {
1✔
245
      if (!entry.getValue().isEternal() && (millis[0] == 0L)) {
1✔
246
        millis[0] = currentTimeMillis();
1✔
247
      }
248
      if (entry.getValue().hasExpired(millis[0])) {
1✔
249
        dispatcher.beginComputation();
1✔
250
        try {
251
          cache.asMap().computeIfPresent(entry.getKey(), (k, expirable) -> {
1✔
252
            if (expirable == entry.getValue()) {
1✔
253
              dispatcher.publishExpired(this, entry.getKey(), entry.getValue().get());
1✔
254
              expired[0]++;
1✔
255
              return null;
1✔
256
            }
257
            return expirable;
1✔
258
          });
259
        } finally {
260
          dispatcher.endComputation();
1✔
261
        }
262
        return true;
1✔
263
      }
264
      var duration = getAccessExpireTime();
1✔
265
      setVariableExpiration(entry.getKey(), duration);
1✔
266
      setAccessExpireTime(entry.getValue(), duration, millis[0]);
1✔
267
      return false;
1✔
268
    });
269

270
    statistics.recordHits(result.size());
1✔
271
    statistics.recordMisses(keys.size() - result.size());
1✔
272
    statistics.recordEvictions(expired[0]);
1✔
273
    return result;
1✔
274
  }
275

276
  @Override
277
  @SuppressWarnings({"CollectionUndefinedEquality", "FutureReturnValueIgnored"})
278
  public void loadAll(Set<? extends K> keys, boolean replaceExistingValues,
279
      @Nullable CompletionListener completionListener) {
280
    requireOperable();
1✔
281
    keys.forEach(Objects::requireNonNull);
1✔
282
    CompletionListener listener = (completionListener == null)
1✔
283
        ? NullCompletionListener.INSTANCE
1✔
284
        : completionListener;
1✔
285
    if (cacheLoader.isEmpty()) {
1✔
286
      listener.onCompletion();
1✔
287
      return;
1✔
288
    }
289

290
    var future = new CompletableFuture<@Nullable Void>();
1✔
291
    synchronized (configuration) {
1✔
292
      requireNotClosed();
1✔
293
      inFlight.add(future);
1✔
294
    }
1✔
295
    try {
296
      CompletableFuture.<CompletableFuture<@Nullable Void>>supplyAsync(() -> {
1✔
297
        @Var boolean success = false;
1✔
298
        try {
299
          if (replaceExistingValues) {
1✔
300
            loadAllAndReplaceExisting(keys);
1✔
301
          } else {
302
            loadAllAndKeepExisting(keys);
1✔
303
          }
304
          success = true;
1✔
305
        } catch (CacheLoaderException e) {
1✔
306
          listener.onException(e);
1✔
307
        } catch (RuntimeException e) {
1✔
308
          listener.onException(new CacheLoaderException(e));
1✔
309
        } finally {
310
          if (!success) {
1✔
311
            dispatcher.ignoreSynchronous();
1✔
312
          }
313
        }
314
        if (!success) {
1✔
315
          return CompletableFuture.completedFuture(null);
1✔
316
        }
317
        // the tracked future spans the notification so that close() awaits the listener's callback
318
        return dispatcher.chainSynchronous().<@Nullable Void>handle((failure, error) -> {
1✔
319
          if (error != null) {
1✔
320
            listener.onException(new CacheLoaderException(error));
1✔
321
          } else if (failure != null) {
1✔
322
            listener.onException(failure);
1✔
323
          } else {
324
            listener.onCompletion();
1✔
325
          }
326
          return null;
1✔
327
        });
328
      }, executor).thenCompose(chain -> chain).whenComplete((r, e) -> {
1✔
329
        inFlight.remove(future);
1✔
330
        future.complete(null);
1✔
331
      });
1✔
332
    } catch (RuntimeException e) {
1✔
333
      inFlight.remove(future);
1✔
334
      future.complete(null);
1✔
335
      listener.onException(new CacheLoaderException(e));
1✔
336
    }
1✔
337
  }
1✔
338

339
  /** Performs the bulk load where the existing entries are replaced. */
340
  protected void loadAllAndReplaceExisting(Set<? extends K> keys) {
341
    Map<K, V> loaded = cacheLoader.orElseThrow().loadAll(keys);
1✔
342
    for (var entry : loaded.entrySet()) {
1✔
343
      if ((entry.getKey() != null) && (entry.getValue() != null)) {
1✔
344
        putNoCopyOrAwait(entry.getKey(), entry.getValue(), /* publishToWriter= */ false);
1✔
345
      }
346
    }
1✔
347
  }
1✔
348

349
  /** Performs the bulk load where the existing entries are retained. */
350
  @SuppressWarnings("ConstantValue")
351
  protected void loadAllAndKeepExisting(Set<? extends K> keys) {
352
    List<K> keysToLoad = keys.stream()
1✔
353
        .filter(key -> {
1✔
354
          var expirable = cache.policy().getIfPresentQuietly(key);
1✔
355
          return (expirable == null)
1✔
356
              || (!expirable.isEternal() && expirable.hasExpired(currentTimeMillis()));
1✔
357
        }).collect(toUnmodifiableList());
1✔
358
    Map<K, V> result = cacheLoader.orElseThrow().loadAll(keysToLoad);
1✔
359
    for (var entry : result.entrySet()) {
1✔
360
      if ((entry.getKey() != null) && (entry.getValue() != null)) {
1✔
361
        putIfAbsentNoAwait(entry.getKey(), entry.getValue(), /* publishToWriter= */ false);
1✔
362
      }
363
    }
1✔
364
  }
1✔
365

366
  @Override
367
  public void put(K key, V value) {
368
    requireOperable();
1✔
369

370
    boolean statsEnabled = statistics.isEnabled();
1✔
371
    long start = statsEnabled ? ticker.read() : 0L;
1✔
372
    var result = putNoCopyOrAwait(key, value, /* publishToWriter= */ true);
1✔
373
    var listenerFailure = awaitSynchronousFailure();
1✔
374
    if (statsEnabled && result.written) {
1✔
375
      statistics.recordPuts(1);
1✔
376
      statistics.recordPutTime(ticker.read() - start);
1✔
377
    }
378
    rethrowListenerFailure(listenerFailure);
1✔
379
  }
1✔
380

381
  @Override
382
  public @Nullable V getAndPut(K key, V value) {
383
    requireOperable();
1✔
384

385
    boolean statsEnabled = statistics.isEnabled();
1✔
386
    long start = statsEnabled ? ticker.read() : 0L;
1✔
387
    var result = putNoCopyOrAwait(key, value, /* publishToWriter= */ true);
1✔
388
    var listenerFailure = awaitSynchronousFailure();
1✔
389
    if (statsEnabled) {
1✔
390
      if (result.oldValue == null) {
1✔
391
        statistics.recordMisses(1L);
1✔
392
      } else {
393
        statistics.recordHits(1L);
1✔
394
      }
395
      long duration = ticker.read() - start;
1✔
396
      if (result.written) {
1✔
397
        statistics.recordPuts(1);
1✔
398
        statistics.recordPutTime(duration);
1✔
399
      }
400
      statistics.recordGetTime(duration);
1✔
401
    }
402
    rethrowListenerFailure(listenerFailure);
1✔
403
    return (result.oldValue == null) ? null : copyOf(result.oldValue);
1✔
404
  }
405

406
  /**
407
   * Associates the specified value with the specified key in the cache.
408
   *
409
   * @param key key with which the specified value is to be associated
410
   * @param value value to be associated with the specified key
411
   * @param publishToWriter if the writer should be notified
412
   * @return the oldValue and if the new value was stored
413
   */
414
  @CanIgnoreReturnValue
415
  protected PutResult<V> putNoCopyOrAwait(K key, V value, boolean publishToWriter) {
416
    requireNonNull(key);
1✔
417
    requireNonNull(value);
1✔
418
    var entry = new CopiedEntry<>(key, copyOf(key), value, copyOf(value));
1✔
419
    return putCopiedOrAwait(entry, publishToWriter);
1✔
420
  }
421

422
  /**
423
   * Associates the copied value with the copied key in the cache.
424
   *
425
   * @param entry the caller's entry alongside the copies to store
426
   * @param publishToWriter if the writer should be notified
427
   * @return the oldValue and if the new value was stored
428
   */
429
  @CanIgnoreReturnValue
430
  private PutResult<V> putCopiedOrAwait(CopiedEntry<K, V> entry, boolean publishToWriter) {
431
    K key = entry.key;
1✔
432
    V newValue = entry.copiedValue;
1✔
433
    var result = new PutResult<V>();
1✔
434
    BiFunction<K, @Nullable Expirable<V>, @Nullable Expirable<V>> remappingFunction =
1✔
435
        (K k, @Var Expirable<V> expirable) -> {
436
      if (publishToWriter) {
1✔
437
        publishToCacheWriter(writer::write, () -> new EntryProxy<>(key, entry.value));
1✔
438
      }
439
      if ((expirable != null) && !expirable.isEternal()
1✔
440
          && expirable.hasExpired(currentTimeMillis())) {
1✔
441
        dispatcher.publishExpired(this, key, expirable.get());
1✔
442
        statistics.recordEvictions(1L);
1✔
443
        expirable = null;
1✔
444
      }
445
      @Var long expireTimeMillis = getWriteExpireTimeMillis((expirable == null));
1✔
446
      if ((expirable != null) && (expireTimeMillis == Long.MIN_VALUE)) {
1✔
447
        expireTimeMillis = expirable.getExpireTimeMillis();
1✔
448
      }
449
      if ((expireTimeMillis == 0) && (expirable == null)) {
1✔
450
        result.written = false;
1✔
451
        dispatcher.publishExpired(this, key, newValue);
1✔
452
        return null;
1✔
453
      } else if (expirable == null) {
1✔
454
        dispatcher.publishCreated(this, key, newValue);
1✔
455
      } else {
456
        result.oldValue = expirable.get();
1✔
457
        dispatcher.publishUpdated(this, key, expirable.get(), newValue);
1✔
458
      }
459
      result.written = true;
1✔
460
      return new Expirable<>(newValue, expireTimeMillis);
1✔
461
    };
462
    dispatcher.beginComputation();
1✔
463
    try {
464
      cache.asMap().compute(entry.copiedKey, remappingFunction);
1✔
465
    } finally {
466
      dispatcher.endComputation();
1✔
467
    }
468
    return result;
1✔
469
  }
470

471
  @Override
472
  public void putAll(Map<? extends K, ? extends V> map) {
473
    requireOperable();
1✔
474

475
    var copies = new ArrayList<CopiedEntry<K, V>>(map.size());
1✔
476
    for (var entry : map.entrySet()) {
1✔
477
      K key = requireNonNull(entry.getKey());
1✔
478
      V value = requireNonNull(entry.getValue());
1✔
479
      copies.add(new CopiedEntry<>(key, copyOf(key), value, copyOf(value)));
1✔
480
    }
1✔
481

482
    @Var CacheWriterException error = null;
1✔
483
    @Var Set<? extends K> failedKeys = Set.of();
1✔
484
    boolean statsEnabled = statistics.isEnabled();
1✔
485
    long start = statsEnabled ? ticker.read() : 0L;
1✔
486
    if (configuration.isWriteThrough() && !copies.isEmpty()) {
1✔
487
      var entries = new ArrayList<Cache.Entry<? extends K, ? extends V>>(copies.size());
1✔
488
      for (var copy : copies) {
1✔
489
        entries.add(new EntryProxy<>(copy.key, copy.value));
1✔
490
      }
1✔
491
      try {
492
        writer.writeAll(entries);
1✔
493
      } catch (CacheWriterException e) {
1✔
494
        failedKeys = entries.stream().map(Cache.Entry::getKey).collect(toSet());
1✔
495
        error = e;
1✔
496
      } catch (RuntimeException e) {
1✔
497
        failedKeys = entries.stream().map(Cache.Entry::getKey).collect(toSet());
1✔
498
        error = new CacheWriterException("Exception in CacheWriter", e);
1✔
499
      }
1✔
500
    }
501

502
    @Var int puts = 0;
1✔
503
    try {
504
      for (var copy : copies) {
1✔
505
        if (!failedKeys.contains(copy.key)) {
1✔
506
          var result = putCopiedOrAwait(copy, /* publishToWriter= */ false);
1✔
507
          if (result.written) {
1✔
508
            puts++;
1✔
509
          }
510
        }
511
      }
1✔
512
    } catch (Throwable t) {
1✔
513
      if (error != null) {
1✔
514
        t.addSuppressed(error);
1✔
515
      }
516
      awaitAndSuppressFailure(t);
1✔
517
      throw t;
1✔
518
    }
1✔
519
    var listenerFailure = awaitSynchronousFailure();
1✔
520

521
    if (statsEnabled && (puts > 0)) {
1✔
522
      statistics.recordPuts(puts);
1✔
523
      statistics.recordPutTime(ticker.read() - start);
1✔
524
    }
525
    var failure = suppress(error, listenerFailure);
1✔
526
    if (failure != null) {
1✔
527
      throw failure;
1✔
528
    }
529
  }
1✔
530

531
  @Override
532
  public boolean putIfAbsent(K key, V value) {
533
    requireOperable();
1✔
534
    requireNonNull(value);
1✔
535

536
    boolean statsEnabled = statistics.isEnabled();
1✔
537
    long start = statsEnabled ? ticker.read() : 0L;
1✔
538
    var result = putIfAbsentNoAwait(key, value, /* publishToWriter= */ true);
1✔
539
    var listenerFailure = awaitSynchronousFailure();
1✔
540
    if (statsEnabled) {
1✔
541
      if (result.oldValue != null) {
1✔
542
        statistics.recordHits(1L);
1✔
543
      } else {
544
        statistics.recordMisses(1L);
1✔
545
      }
546
      if (result.written) {
1✔
547
        statistics.recordPuts(1L);
1✔
548
        statistics.recordPutTime(ticker.read() - start);
1✔
549
      }
550
    }
551
    rethrowListenerFailure(listenerFailure);
1✔
552
    return result.written;
1✔
553
  }
554

555
  /**
556
   * Associates the specified value with the specified key in the cache if there is no existing
557
   * mapping.
558
   *
559
   * @param key key with which the specified value is to be associated
560
   * @param value value to be associated with the specified key
561
   * @param publishToWriter if the writer should be notified
562
   * @return the oldValue and if the new value was stored
563
   */
564
  @CanIgnoreReturnValue
565
  private PutResult<V> putIfAbsentNoAwait(K key, V value, boolean publishToWriter) {
566
    var result = new PutResult<V>();
1✔
567
    BiFunction<K, @Nullable Expirable<V>, @Nullable Expirable<V>> remappingFunction =
1✔
568
        (K k, Expirable<V> expirable) -> {
569
      if ((expirable != null)
1✔
570
          && (expirable.isEternal() || !expirable.hasExpired(currentTimeMillis()))) {
1✔
571
        result.oldValue = expirable.get();
1✔
572
        return expirable;
1✔
573
      }
574

575
      V copy = copyOf(value);
1✔
576
      if (publishToWriter) {
1✔
577
        publishToCacheWriter(writer::write, () -> new EntryProxy<>(key, value));
1✔
578
      }
579
      if (expirable != null) {
1✔
580
        dispatcher.publishExpired(this, key, expirable.get());
1✔
581
        statistics.recordEvictions(1L);
1✔
582
      }
583

584
      long expireTimeMillis = getWriteExpireTimeMillis(/* created= */ true);
1✔
585
      if (expireTimeMillis == 0) {
1✔
586
        // A zero creation expiry means the entry is already expired and is not added
587
        dispatcher.publishExpired(this, key, copy);
1✔
588
        return null;
1✔
589
      } else {
590
        result.written = true;
1✔
591
        dispatcher.publishCreated(this, key, copy);
1✔
592
        return new Expirable<>(copy, expireTimeMillis);
1✔
593
      }
594
    };
595
    dispatcher.beginComputation();
1✔
596
    try {
597
      cache.asMap().compute(copyOf(key), remappingFunction);
1✔
598
    } finally {
599
      dispatcher.endComputation();
1✔
600
    }
601
    return result;
1✔
602
  }
603

604
  @Override
605
  public boolean remove(K key) {
606
    requireOperable();
1✔
607
    requireNonNull(key);
1✔
608

609
    boolean statsEnabled = statistics.isEnabled();
1✔
610
    long start = statsEnabled ? ticker.read() : 0L;
1✔
611
    V value = removeNoCopyOrAwait(key, /* publishToWriter= */ true);
1✔
612
    var listenerFailure = awaitSynchronousFailure();
1✔
613
    if (value != null) {
1✔
614
      statistics.recordRemovals(1L);
1✔
615
      if (statsEnabled) {
1✔
616
        statistics.recordRemoveTime(ticker.read() - start);
1✔
617
      }
618
    }
619
    rethrowListenerFailure(listenerFailure);
1✔
620
    return (value != null);
1✔
621
  }
622

623
  /**
624
   * Removes the mapping from the cache without store-by-value copying nor waiting for synchronous
625
   * listeners to complete.
626
   *
627
   * @param key key whose mapping is to be removed from the cache
628
   * @param publishToWriter if the writer should be notified
629
   * @return the old value
630
   */
631
  private @Nullable V removeNoCopyOrAwait(K key, boolean publishToWriter) {
632
    @SuppressWarnings("unchecked")
633
    var removed = (V[]) new Object[1];
1✔
634
    BiFunction<K, @Nullable Expirable<V>, @Nullable Expirable<V>> remappingFunction =
1✔
635
        (K k, Expirable<V> expirable) -> {
636
      if (publishToWriter) {
1✔
637
        publishToCacheWriter(writer::delete, () -> key);
1✔
638
      }
639
      if (expirable != null) {
1✔
640
        if (!expirable.isEternal() && expirable.hasExpired(currentTimeMillis())) {
1✔
641
          dispatcher.publishExpired(this, key, expirable.get());
1✔
642
          statistics.recordEvictions(1L);
1✔
643
        } else {
644
          dispatcher.publishRemoved(this, key, expirable.get());
1✔
645
          removed[0] = expirable.get();
1✔
646
        }
647
      }
648
      return null;
1✔
649
    };
650
    dispatcher.beginComputation();
1✔
651
    try {
652
      cache.asMap().compute(key, remappingFunction);
1✔
653
    } finally {
654
      dispatcher.endComputation();
1✔
655
    }
656
    return removed[0];
1✔
657
  }
658

659
  @Override
660
  @CanIgnoreReturnValue
661
  public boolean remove(K key, V oldValue) {
662
    requireOperable();
1✔
663
    requireNonNull(key);
1✔
664
    requireNonNull(oldValue);
1✔
665

666
    boolean statsEnabled = statistics.isEnabled();
1✔
667
    long start = statsEnabled ? ticker.read() : 0L;
1✔
668
    boolean[] removed = { false };
1✔
669
    boolean[] found = { false };
1✔
670
    dispatcher.beginComputation();
1✔
671
    try {
672
      cache.asMap().computeIfPresent(key, (k, expirable) -> {
1✔
673
        long millis = expirable.isEternal()
1✔
674
            ? 0L
1✔
675
            : nanosToMillis((start == 0L) ? ticker.read() : start);
1✔
676
        if (expirable.hasExpired(millis)) {
1✔
677
          dispatcher.publishExpired(this, key, expirable.get());
1✔
678
          statistics.recordEvictions(1L);
1✔
679
          return null;
1✔
680
        }
681

682
        found[0] = true;
1✔
683
        if (oldValue.equals(expirable.get())) {
1✔
684
          publishToCacheWriter(writer::delete, () -> key);
1✔
685
          dispatcher.publishRemoved(this, key, expirable.get());
1✔
686
          removed[0] = true;
1✔
687
          return null;
1✔
688
        }
689
        setAccessExpireTime(expirable, getAccessExpireTime(), millis);
1✔
690
        return expirable;
1✔
691
      });
692
    } finally {
693
      dispatcher.endComputation();
1✔
694
    }
695
    var listenerFailure = awaitSynchronousFailure();
1✔
696
    if (statsEnabled) {
1✔
697
      if (removed[0]) {
1✔
698
        statistics.recordRemovals(1L);
1✔
699
        statistics.recordHits(1L);
1✔
700
        statistics.recordRemoveTime(ticker.read() - start);
1✔
701
      } else if (found[0]) {
1✔
702
        statistics.recordHits(1L);
1✔
703
      } else {
704
        statistics.recordMisses(1L);
1✔
705
      }
706
    }
707
    rethrowListenerFailure(listenerFailure);
1✔
708
    return removed[0];
1✔
709
  }
710

711
  @Override
712
  public @Nullable V getAndRemove(K key) {
713
    requireOperable();
1✔
714
    requireNonNull(key);
1✔
715

716
    boolean statsEnabled = statistics.isEnabled();
1✔
717
    long start = statsEnabled ? ticker.read() : 0L;
1✔
718
    V value = removeNoCopyOrAwait(key, /* publishToWriter= */ true);
1✔
719
    var listenerFailure = awaitSynchronousFailure();
1✔
720
    if (statsEnabled) {
1✔
721
      long duration = ticker.read() - start;
1✔
722
      if (value == null) {
1✔
723
        statistics.recordMisses(1L);
1✔
724
      } else {
725
        statistics.recordHits(1L);
1✔
726
        statistics.recordRemovals(1L);
1✔
727
        statistics.recordRemoveTime(duration);
1✔
728
      }
729
      statistics.recordGetTime(duration);
1✔
730
    }
731
    rethrowListenerFailure(listenerFailure);
1✔
732
    return (value == null) ? null : copyOf(value);
1✔
733
  }
734

735
  @Override
736
  public boolean replace(K key, V oldValue, V newValue) {
737
    requireOperable();
1✔
738
    requireNonNull(oldValue);
1✔
739
    requireNonNull(newValue);
1✔
740

741
    boolean statsEnabled = statistics.isEnabled();
1✔
742
    long start = statsEnabled ? ticker.read() : 0L;
1✔
743
    boolean[] replaced = { false };
1✔
744
    boolean[] found = { false };
1✔
745
    dispatcher.beginComputation();
1✔
746
    try {
747
      cache.asMap().computeIfPresent(key, (k, expirable) -> {
1✔
748
        long millis = expirable.isEternal()
1✔
749
            ? 0L
1✔
750
            : nanosToMillis((start == 0L) ? ticker.read() : start);
1✔
751
        if (expirable.hasExpired(millis)) {
1✔
752
          dispatcher.publishExpired(this, key, expirable.get());
1✔
753
          statistics.recordEvictions(1L);
1✔
754
          return null;
1✔
755
        }
756

757
        found[0] = true;
1✔
758
        Expirable<V> result;
759
        if (oldValue.equals(expirable.get())) {
1✔
760
          V copy = copyOf(newValue);
1✔
761
          publishToCacheWriter(writer::write, () -> new EntryProxy<>(key, newValue));
1✔
762
          dispatcher.publishUpdated(this, key, expirable.get(), copy);
1✔
763
          @Var long expireTimeMillis = getWriteExpireTimeMillis(/* created= */ false);
1✔
764
          if (expireTimeMillis == Long.MIN_VALUE) {
1✔
765
            expireTimeMillis = expirable.getExpireTimeMillis();
1✔
766
          }
767
          result = new Expirable<>(copy, expireTimeMillis);
1✔
768
          replaced[0] = true;
1✔
769
        } else {
1✔
770
          result = expirable;
1✔
771
          setAccessExpireTime(expirable, getAccessExpireTime(), millis);
1✔
772
        }
773
        return result;
1✔
774
      });
775
    } finally {
776
      dispatcher.endComputation();
1✔
777
    }
778
    var listenerFailure = awaitSynchronousFailure();
1✔
779

780
    if (statsEnabled) {
1✔
781
      statistics.recordMisses(found[0] ? 0L : 1L);
1✔
782
      statistics.recordHits(found[0] ? 1L : 0L);
1✔
783
      long duration = ticker.read() - start;
1✔
784
      if (replaced[0]) {
1✔
785
        statistics.recordPuts(1L);
1✔
786
        statistics.recordPutTime(duration);
1✔
787
      }
788
      statistics.recordGetTime(duration);
1✔
789
    }
790

791
    rethrowListenerFailure(listenerFailure);
1✔
792
    return replaced[0];
1✔
793
  }
794

795
  @Override
796
  public boolean replace(K key, V value) {
797
    requireOperable();
1✔
798

799
    boolean statsEnabled = statistics.isEnabled();
1✔
800
    long start = statsEnabled ? ticker.read() : 0L;
1✔
801
    @Nullable V oldValue = replaceNoCopyOrAwait(key, value);
1✔
802
    var listenerFailure = awaitSynchronousFailure();
1✔
803
    if (oldValue == null) {
1✔
804
      statistics.recordMisses(1L);
1✔
805
      if (statsEnabled) {
1✔
806
        statistics.recordGetTime(ticker.read() - start);
1✔
807
      }
808
    } else if (statsEnabled) {
1✔
809
      statistics.recordHits(1L);
1✔
810
      statistics.recordPuts(1L);
1✔
811
      long duration = ticker.read() - start;
1✔
812
      statistics.recordGetTime(duration);
1✔
813
      statistics.recordPutTime(duration);
1✔
814
    }
815
    rethrowListenerFailure(listenerFailure);
1✔
816
    return (oldValue != null);
1✔
817
  }
818

819
  @Override
820
  public @Nullable V getAndReplace(K key, V value) {
821
    requireOperable();
1✔
822

823
    boolean statsEnabled = statistics.isEnabled();
1✔
824
    long start = statsEnabled ? ticker.read() : 0L;
1✔
825
    V oldValue = replaceNoCopyOrAwait(key, value);
1✔
826
    var listenerFailure = awaitSynchronousFailure();
1✔
827
    if (statsEnabled) {
1✔
828
      long duration = ticker.read() - start;
1✔
829
      if (oldValue == null) {
1✔
830
        statistics.recordMisses(1L);
1✔
831
      } else {
832
        statistics.recordHits(1L);
1✔
833
        statistics.recordPuts(1L);
1✔
834
        statistics.recordPutTime(duration);
1✔
835
      }
836
      statistics.recordGetTime(duration);
1✔
837
    }
838
    rethrowListenerFailure(listenerFailure);
1✔
839
    return (oldValue == null) ? null : copyOf(oldValue);
1✔
840
  }
841

842
  /**
843
   * Replaces the entry for the specified key only if it is currently mapped to some value. The
844
   * entry is not store-by-value copied nor does the method wait for synchronous listeners to
845
   * complete.
846
   *
847
   * @param key key with which the specified value is associated
848
   * @param value value to be associated with the specified key
849
   * @return the old value
850
   */
851
  private @Nullable V replaceNoCopyOrAwait(K key, V value) {
852
    V copy = copyOf(value);
1✔
853
    @SuppressWarnings("unchecked")
854
    var replaced = (V[]) new Object[1];
1✔
855
    dispatcher.beginComputation();
1✔
856
    try {
857
      cache.asMap().computeIfPresent(key, (k, expirable) -> {
1✔
858
        if (!expirable.isEternal() && expirable.hasExpired(currentTimeMillis())) {
1✔
859
          dispatcher.publishExpired(this, key, expirable.get());
1✔
860
          statistics.recordEvictions(1L);
1✔
861
          return null;
1✔
862
        }
863

864
        publishToCacheWriter(writer::write, () -> new EntryProxy<>(key, value));
1✔
865
        @Var long expireTimeMillis = getWriteExpireTimeMillis(/* created= */ false);
1✔
866
        if (expireTimeMillis == Long.MIN_VALUE) {
1✔
867
          expireTimeMillis = expirable.getExpireTimeMillis();
1✔
868
        }
869
        dispatcher.publishUpdated(this, key, expirable.get(), copy);
1✔
870
        replaced[0] = expirable.get();
1✔
871
        return new Expirable<>(copy, expireTimeMillis);
1✔
872
      });
873
    } finally {
874
      dispatcher.endComputation();
1✔
875
    }
876
    return replaced[0];
1✔
877
  }
878

879
  @Override
880
  public void removeAll(Set<? extends K> keys) {
881
    requireOperable();
1✔
882
    var keysToRemove = new LinkedHashSet<>(keys);
1✔
883
    keysToRemove.forEach(Objects::requireNonNull);
1✔
884

885
    @Var CacheWriterException error = null;
1✔
886
    @Var Set<? extends K> failedKeys = Set.of();
1✔
887
    boolean statsEnabled = statistics.isEnabled();
1✔
888
    long start = statsEnabled ? ticker.read() : 0L;
1✔
889
    if (configuration.isWriteThrough() && !keysToRemove.isEmpty()) {
1✔
890
      var keysToWrite = new LinkedHashSet<>(keysToRemove);
1✔
891
      try {
892
        writer.deleteAll(keysToWrite);
1✔
893
      } catch (CacheWriterException e) {
1✔
894
        error = e;
1✔
895
        failedKeys = keysToWrite;
1✔
896
      } catch (RuntimeException e) {
1✔
897
        error = new CacheWriterException("Exception in CacheWriter", e);
1✔
898
        failedKeys = keysToWrite;
1✔
899
      }
1✔
900
    }
901

902
    @Var int removed = 0;
1✔
903
    try {
904
      for (var key : keysToRemove) {
1✔
905
        if (!failedKeys.contains(key)
1✔
906
            && (removeNoCopyOrAwait(key, /* publishToWriter= */ false) != null)) {
1✔
907
          removed++;
1✔
908
        }
909
      }
1✔
910
    } catch (Throwable t) {
1✔
911
      if (error != null) {
1!
UNCOV
912
        t.addSuppressed(error);
×
913
      }
914
      awaitAndSuppressFailure(t);
1✔
915
      throw t;
1✔
916
    }
1✔
917
    var listenerFailure = awaitSynchronousFailure();
1✔
918

919
    if (statsEnabled && (removed > 0)) {
1✔
920
      statistics.recordRemovals(removed);
1✔
921
      statistics.recordRemoveTime(ticker.read() - start);
1✔
922
    }
923
    var failure = suppress(error, listenerFailure);
1✔
924
    if (failure != null) {
1✔
925
      throw failure;
1✔
926
    }
927
  }
1✔
928

929
  @Override
930
  public void removeAll() {
931
    removeAll(cache.asMap().keySet());
1✔
932
  }
1✔
933

934
  @Override
935
  public void clear() {
936
    requireOperable();
1✔
937
    dispatcher.beginComputation();
1✔
938
    try {
939
      cache.invalidateAll();
1✔
940
    } finally {
941
      dispatcher.endComputation();
1✔
942
    }
943
  }
1✔
944

945
  @Override
946
  public <C extends Configuration<K, V>> C getConfiguration(Class<C> clazz) {
947
    if (clazz.isInstance(configuration)) {
1✔
948
      synchronized (configuration) {
1✔
949
        return clazz.cast(configuration.immutableCopy());
1✔
950
      }
951
    }
952
    throw new IllegalArgumentException("The configuration class " + clazz
1✔
953
        + " is not supported by this implementation");
954
  }
955

956
  @Override
957
  public <T extends @Nullable Object> T invoke(K key,
958
      EntryProcessor<K, V, T> entryProcessor, Object... arguments) {
959
    requireNonNull(entryProcessor);
1✔
960
    requireNonNull(arguments);
1✔
961
    requireOperable();
1✔
962

963
    var result = new Object[1];
1✔
964
    var failure = new Throwable[1];
1✔
965
    BiFunction<K, Expirable<V>, Expirable<V>> remappingFunction = (k, expirable) -> {
1✔
966
      // Publish a lazily-expired prior's expiration before the processor observes it as absent,
967
      // so listeners see a linearizable sequence and the expiration is committed exactly once
968
      boolean expired;
969
      Expirable<V> prior;
970
      if ((expirable != null) && !expirable.isEternal()
1✔
971
          && expirable.hasExpired(currentTimeMillis())) {
1✔
972
        dispatcher.publishExpired(this, key, expirable.get());
1✔
973
        statistics.recordEvictions(1L);
1✔
974
        expired = true;
1✔
975
        prior = null;
1✔
976
      } else {
977
        prior = expirable;
1✔
978
        expired = false;
1✔
979
      }
980

981
      V value;
982
      if (prior == null) {
1✔
983
        statistics.recordMisses(1L);
1✔
984
        value = null;
1✔
985
      } else {
986
        value = copyOf(prior.get());
1✔
987
        statistics.recordHits(1L);
1✔
988
      }
989
      var entry = new EntryProcessorEntry<>(key, value,
1✔
990
          configuration.isReadThrough() ? cacheLoader : Optional.empty());
1✔
991
      try {
992
        result[0] = entryProcessor.process(entry, arguments);
1✔
993
        return postProcess(prior, entry);
1✔
994
      } catch (Throwable e) {
1✔
995
        if (!expired) {
1✔
996
          throw processorFailure(e);
1✔
997
        }
998
        failure[0] = e;
1✔
999
        return null;
1✔
1000
      }
1001
    };
1002
    try {
1003
      dispatcher.beginComputation();
1✔
1004
      try {
1005
        cache.asMap().compute(copyOf(key), remappingFunction);
1✔
1006
      } finally {
1007
        dispatcher.endComputation();
1✔
1008
      }
1009
    } catch (Throwable t) {
1✔
1010
      dispatcher.ignoreSynchronous();
1✔
1011
      throw t;
1✔
1012
    }
1✔
1013
    var listenerFailure = awaitSynchronousFailure();
1✔
1014
    if (failure[0] != null) {
1✔
1015
      var error = processorFailure(failure[0]);
1✔
1016
      if (listenerFailure != null) {
1✔
1017
        error.addSuppressed(listenerFailure);
1✔
1018
      }
1019
      throw error;
1✔
1020
    }
1021
    rethrowListenerFailure(listenerFailure);
1✔
1022

1023
    @SuppressWarnings("unchecked")
1024
    var castedResult = (T) result[0];
1✔
1025
    return castedResult;
1✔
1026
  }
1027

1028
  /**
1029
   * Waits for the synchronous listeners and returns their failure instead of throwing it. An
1030
   * {@code Error} is not captured and propagates.
1031
   */
1032
  protected final @Nullable CacheEntryListenerException awaitSynchronousFailure() {
1033
    try {
1034
      dispatcher.awaitSynchronous();
1✔
1035
      return null;
1✔
1036
    } catch (CacheEntryListenerException e) {
1✔
1037
      return e;
1✔
1038
    }
1039
  }
1040

1041
  /** Waits for the synchronous listeners and suppresses any failures onto the existing error. */
1042
  @SuppressWarnings("CheckReturnValue")
1043
  protected final void awaitAndSuppressFailure(Throwable error) {
1044
    suppress(error, awaitSynchronousFailure());
1✔
1045
  }
1✔
1046

1047
  /** Rethrows a captured synchronous listener failure after the operation is accounted for. */
1048
  protected static void rethrowListenerFailure(@Nullable CacheEntryListenerException failure) {
1049
    if (failure != null) {
1✔
1050
      throw failure;
1✔
1051
    }
1052
  }
1✔
1053

1054
  /** Returns the exception to rethrow on an entry processor failure. */
1055
  private static RuntimeException processorFailure(Throwable e) {
1056
    if (e instanceof Error) {
1✔
1057
      throw (Error) e;
1✔
1058
    } else if (e instanceof EntryProcessorException) {
1✔
1059
      return (EntryProcessorException) e;
1✔
1060
    }
1061
    return new EntryProcessorException(e);
1✔
1062
  }
1063

1064
  /**
1065
   * Returns the updated expirable value after performing the post-processing actions. A null
1066
   * {@code expirable} means the entry was absent and READ/UPDATED (which require a live prior)
1067
   * never observe one.
1068
   */
1069
  @Nullable Expirable<V> postProcess(
1070
      @Nullable Expirable<V> expirable, EntryProcessorEntry<K, V> entry) {
1071
    switch (entry.getAction()) {
1✔
1072
      case NONE:
1073
        return expirable;
1✔
1074
      case READ: {
1075
        setAccessExpireTime(requireNonNull(expirable), getAccessExpireTime(), 0L);
1✔
1076
        return expirable;
1✔
1077
      }
1078
      case CREATED:
1079
      case LOADED: {
1080
        V value = requireNonNull(entry.getValue());
1✔
1081
        V copy = copyOf(value);
1✔
1082
        if (entry.getAction() == Action.CREATED) {
1✔
1083
          publishToCacheWriter(writer::write, () -> new EntryProxy<>(entry.getKey(), value));
1✔
1084
        }
1085
        long expireTimeMillis = getWriteExpireTimeMillis(/* created= */ true);
1✔
1086
        if (expireTimeMillis == 0) {
1✔
1087
          // A zero creation expiry means the entry is already expired and is not added, so the
1088
          // create is neither counted nor published
1089
          dispatcher.publishExpired(this, entry.getKey(), copy);
1✔
1090
          return null;
1✔
1091
        }
1092
        if (entry.getAction() == Action.CREATED) {
1✔
1093
          statistics.recordPuts(1L);
1✔
1094
        }
1095
        dispatcher.publishCreated(this, entry.getKey(), copy);
1✔
1096
        return new Expirable<>(copy, expireTimeMillis);
1✔
1097
      }
1098
      case UPDATED: {
1099
        requireNonNull(expirable, "Expected a previous value but was null");
1✔
1100
        V value = requireNonNull(entry.getValue(), "Expected a new value but was null");
1✔
1101
        V copy = copyOf(value);
1✔
1102
        publishToCacheWriter(writer::write, () -> new EntryProxy<>(entry.getKey(), value));
1✔
1103
        statistics.recordPuts(1L);
1✔
1104
        dispatcher.publishUpdated(this, entry.getKey(), expirable.get(), copy);
1✔
1105
        @Var long expireTimeMillis = getWriteExpireTimeMillis(/* created= */ false);
1✔
1106
        if (expireTimeMillis == Long.MIN_VALUE) {
1✔
1107
          expireTimeMillis = expirable.getExpireTimeMillis();
1✔
1108
        }
1109
        return new Expirable<>(copy, expireTimeMillis);
1✔
1110
      }
1111
      case DELETED:
1112
        publishToCacheWriter(writer::delete, entry::getKey);
1✔
1113
        if (expirable != null) {
1✔
1114
          statistics.recordRemovals(1L);
1✔
1115
          dispatcher.publishRemoved(this, entry.getKey(), expirable.get());
1✔
1116
        }
1117
        return null;
1✔
1118
    }
1119
    throw new IllegalStateException("Unknown state: " + entry.getAction());
1✔
1120
  }
1121

1122
  @Override
1123
  public <T> Map<K, EntryProcessorResult<T>> invokeAll(Set<? extends K> keys,
1124
      EntryProcessor<K, V, T> entryProcessor, Object... arguments) {
1125
    requireOperable();
1✔
1126
    requireNonNull(keys);
1✔
1127
    requireNonNull(arguments);
1✔
1128
    requireNonNull(entryProcessor);
1✔
1129
    keys.forEach(Objects::requireNonNull);
1✔
1130

1131
    var results = new HashMap<K, EntryProcessorResult<T>>(keys.size(), 1.0f);
1✔
1132
    for (K key : keys) {
1✔
1133
      try {
1134
        T result = invoke(key, entryProcessor, arguments);
1✔
1135
        if (result != null) {
1✔
1136
          results.put(key, () -> result);
1✔
1137
        }
1138
      } catch (EntryProcessorException e) {
1✔
1139
        results.put(key, () -> { throw e; });
1✔
1140
      } catch (RuntimeException e) {
1✔
1141
        results.put(key, () -> { throw new EntryProcessorException(e); });
1✔
1142
      }
1✔
1143
    }
1✔
1144
    return results;
1✔
1145
  }
1146

1147
  @Override
1148
  public String getName() {
1149
    return name;
1✔
1150
  }
1151

1152
  @Override
1153
  public CacheManager getCacheManager() {
1154
    return cacheManager;
1✔
1155
  }
1156

1157
  @Override
1158
  public boolean isClosed() {
1159
    return closed;
1✔
1160
  }
1161

1162
  @Override
1163
  public void close() {
1164
    if (isClosed()) {
1✔
1165
      return;
1✔
1166
    }
1167
    synchronized (configuration) {
1✔
1168
      if (!isClosed()) {
1✔
1169
        @Var Throwable thrown = null;
1✔
1170
        thrown = tryClose((AutoCloseable) () -> enableManagement(false), thrown);
1✔
1171
        thrown = tryClose((AutoCloseable) () -> enableStatistics(false), thrown);
1✔
1172

1173
        closed = true;
1✔
1174
        try {
1175
          cacheManager.destroyCache(name, this);
1✔
1176
        } catch (IllegalStateException ignored) { /* manager already closed */ }
1✔
1177

1178
        thrown = shutdownExecutor(thrown);
1✔
1179
        thrown = tryClose(expiry, thrown);
1✔
1180
        thrown = tryClose(writer, thrown);
1✔
1181
        thrown = tryClose(cacheLoader.orElse(null), thrown);
1✔
1182
        for (Registration<K, V> registration : dispatcher.registrations()) {
1✔
1183
          thrown = tryClose(registration.getCacheEntryListener(), thrown);
1✔
1184
        }
1✔
1185
        if (thrown != null) {
1✔
1186
          logger.log(Level.WARNING, "Failure when closing cache resources", thrown);
1✔
1187
        }
1188
      }
1189
    }
1✔
1190
    dispatcher.beginComputation();
1✔
1191
    try {
1192
      cache.invalidateAll();
1✔
1193
    } finally {
1194
      dispatcher.endComputation();
1✔
1195
    }
1196
  }
1✔
1197

1198
  @SuppressWarnings("FutureReturnValueIgnored")
1199
  private @Nullable Throwable shutdownExecutor(@Var @Nullable Throwable thrown) {
1200
    if (executor instanceof ExecutorService) {
1✔
1201
      @SuppressWarnings("PMD.CloseResource")
1202
      var es = (ExecutorService) executor;
1✔
1203
      es.shutdown();
1✔
1204
    }
1205

1206
    try {
1207
      CompletableFuture
1✔
1208
          .allOf(inFlight.toArray(CompletableFuture[]::new))
1✔
1209
          .get(10, TimeUnit.SECONDS);
1✔
1210
    } catch (ExecutionException | TimeoutException e) {
1✔
1211
      thrown = suppress(thrown, e);
1✔
1212
    } catch (InterruptedException e) {
1✔
1213
      Thread.currentThread().interrupt();
1✔
1214
      thrown = suppress(thrown, e);
1✔
1215
    }
1✔
1216
    inFlight.clear();
1✔
1217

1218
    if (!(executor instanceof ExecutorService)) {
1✔
1219
      thrown = tryClose(executor, thrown);
1✔
1220
    }
1221
    return thrown;
1✔
1222
  }
1223

1224
  /**
1225
   * Attempts to close the resource. If an error occurs and an outermost exception is set, then adds
1226
   * the error to the suppression list.
1227
   *
1228
   * @param o the resource to close if Closeable
1229
   * @param outer the outermost error, or null if unset
1230
   * @return the outermost error, or null if unset and successful
1231
   */
1232
  private static @Nullable Throwable tryClose(@Nullable Object o, @Nullable Throwable outer) {
1233
    if (o instanceof AutoCloseable) {
1✔
1234
      try {
1235
        ((AutoCloseable) o).close();
1✔
1236
      } catch (Throwable t) {
1✔
1237
        return suppress(outer, t);
1✔
1238
      }
1✔
1239
    }
1240
    return outer;
1✔
1241
  }
1242

1243
  /** Returns the outermost error, retaining the error as suppressed if one is already set. */
1244
  private static <T extends Throwable> @Nullable T suppress(@Nullable T outer, @Nullable T t) {
1245
    if (t == null) {
1✔
1246
      return outer;
1✔
1247
    }
1248
    if (outer == null) {
1✔
1249
      return t;
1✔
1250
    }
1251
    if (outer != t) {
1✔
1252
      outer.addSuppressed(t);
1✔
1253
    }
1254
    return outer;
1✔
1255
  }
1256

1257
  @Override
1258
  public <T> T unwrap(Class<T> clazz) {
1259
    if (clazz.isInstance(cache)) {
1✔
1260
      return clazz.cast(cache);
1✔
1261
    } else if (clazz.isInstance(this)) {
1✔
1262
      return clazz.cast(this);
1✔
1263
    }
1264
    throw new IllegalArgumentException("Unwrapping to " + clazz
1✔
1265
        + " is not supported by this implementation");
1266
  }
1267

1268
  @Override
1269
  public void registerCacheEntryListener(
1270
      CacheEntryListenerConfiguration<K, V> cacheEntryListenerConfiguration) {
1271
    synchronized (configuration) {
1✔
1272
      requireOperable();
1✔
1273
      configuration.addCacheEntryListenerConfiguration(cacheEntryListenerConfiguration);
1✔
1274
      try {
1275
        dispatcher.register(cacheEntryListenerConfiguration);
1✔
1276
      } catch (Throwable t) {
1✔
1277
        configuration.removeCacheEntryListenerConfiguration(cacheEntryListenerConfiguration);
1✔
1278
        throw t;
1✔
1279
      }
1✔
1280
    }
1✔
1281
  }
1✔
1282

1283
  @Override
1284
  public void deregisterCacheEntryListener(
1285
      CacheEntryListenerConfiguration<K, V> cacheEntryListenerConfiguration) {
1286
    synchronized (configuration) {
1✔
1287
      requireOperable();
1✔
1288
      configuration.removeCacheEntryListenerConfiguration(cacheEntryListenerConfiguration);
1✔
1289
      dispatcher.deregister(cacheEntryListenerConfiguration);
1✔
1290
    }
1✔
1291
  }
1✔
1292

1293
  @Override
1294
  public Iterator<Cache.Entry<K, V>> iterator() {
1295
    requireOperable();
1✔
1296
    return new EntryIterator();
1✔
1297
  }
1298

1299
  /** Enables or disables the configuration management JMX bean. */
1300
  void enableManagement(boolean enabled) {
1301
    synchronized (configuration) {
1✔
1302
      requireNotClosed();
1✔
1303
      if (enabled) {
1✔
1304
        JmxRegistration.registerMxBean(this, cacheMxBean, MBeanType.CONFIGURATION);
1✔
1305
      } else {
1306
        JmxRegistration.unregisterMxBean(this, MBeanType.CONFIGURATION);
1✔
1307
      }
1308
      configuration.setManagementEnabled(enabled);
1✔
1309
    }
1✔
1310
  }
1✔
1311

1312
  /** Enables or disables the statistics JMX bean. */
1313
  void enableStatistics(boolean enabled) {
1314
    synchronized (configuration) {
1✔
1315
      requireNotClosed();
1✔
1316
      if (enabled) {
1✔
1317
        JmxRegistration.registerMxBean(this, statistics, MBeanType.STATISTICS);
1✔
1318
      } else {
1319
        JmxRegistration.unregisterMxBean(this, MBeanType.STATISTICS);
1✔
1320
      }
1321
      statistics.enable(enabled);
1✔
1322
      configuration.setStatisticsEnabled(enabled);
1✔
1323
    }
1✔
1324
  }
1✔
1325

1326
  /** Performs the action with the cache writer if write-through is enabled. */
1327
  private <T> void publishToCacheWriter(Consumer<T> action, Supplier<T> data) {
1328
    if (!configuration.isWriteThrough()) {
1✔
1329
      return;
1✔
1330
    }
1331
    try {
1332
      action.accept(data.get());
1✔
1333
    } catch (CacheWriterException e) {
1✔
1334
      throw e;
1✔
1335
    } catch (RuntimeException e) {
1✔
1336
      throw new CacheWriterException("Exception in CacheWriter", e);
1✔
1337
    }
1✔
1338
  }
1✔
1339

1340
  /** Checks that the cache is not closed. */
1341
  protected final void requireNotClosed() {
1342
    if (isClosed()) {
1✔
1343
      throw new IllegalStateException();
1✔
1344
    }
1345
  }
1✔
1346

1347
  /** Checks that the operation may begin. */
1348
  protected final void requireOperable() {
1349
    requireNotClosed();
1✔
1350
    if (dispatcher.inCallback()) {
1✔
1351
      throw new IllegalStateException("Recursive cache operation");
1✔
1352
    }
1353
  }
1✔
1354

1355
  /**
1356
   * Returns a copy of the value if value-based caching is enabled.
1357
   *
1358
   * @param object the object to be copied
1359
   * @param <T> the type of object being copied
1360
   * @return a copy of the object if storing by value or the same instance if by reference
1361
   */
1362
  protected final <T> T copyOf(T object) {
1363
    try {
1364
      return requireNonNull(
1✔
1365
          copier.copy(requireNonNull(object), requireNonNull(cacheManager.getClassLoader())));
1✔
1366
    } catch (NullPointerException | IllegalStateException | ClassCastException | CacheException e) {
1✔
1367
      throw e;
1✔
1368
    } catch (RuntimeException e) {
1✔
1369
      throw new CacheException(e);
1✔
1370
    }
1371
  }
1372

1373
  /**
1374
   * Returns a deep copy of the map if value-based caching is enabled.
1375
   *
1376
   * @param map the mapping of keys to expirable values
1377
   * @return a deep or shallow copy of the mappings depending on the store by value setting
1378
   */
1379
  @SuppressWarnings("CollectorMutability")
1380
  protected final Map<K, V> copyMap(Map<K, Expirable<V>> map) {
1381
    return map.entrySet().stream().collect(toMap(
1✔
1382
        entry -> copyOf(entry.getKey()),
1✔
1383
        entry -> copyOf(entry.getValue().get())));
1✔
1384
  }
1385

1386
  /** Returns the current time in milliseconds. */
1387
  protected final long currentTimeMillis() {
1388
    return nanosToMillis(ticker.read());
1✔
1389
  }
1390

1391
  /** Returns the nanosecond time in milliseconds. */
1392
  protected static long nanosToMillis(long nanos) {
1393
    return TimeUnit.NANOSECONDS.toMillis(nanos);
1✔
1394
  }
1395

1396
  /** Returns the duration to expire an accessed entry after, or {@code null} if unchanged. */
1397
  protected final @Nullable Duration getAccessExpireTime() {
1398
    try {
1399
      return expiry.getExpiryForAccess();
1✔
1400
    } catch (RuntimeException e) {
1✔
1401
      logger.log(Level.WARNING, "Failed to get the policy's expiration time", e);
1✔
1402
      return null;
1✔
1403
    }
1404
  }
1405

1406
  /**
1407
   * Sets the JCache access expiration time.
1408
   *
1409
   * @param expirable the entry that was operated on
1410
   * @param duration the access duration, or null if unchanged
1411
   * @param currentTimeMillis the current time, or zero if not read yet
1412
   */
1413
  protected final void setAccessExpireTime(Expirable<?> expirable,
1414
      @Nullable Duration duration, @Var long currentTimeMillis) {
1415
    if (duration == null) {
1✔
1416
      return;
1✔
1417
    } else if (duration.isZero()) {
1✔
1418
      expirable.setExpireTimeMillis(0L);
1✔
1419
    } else if (duration.isEternal()) {
1✔
1420
      expirable.setExpireTimeMillis(Long.MAX_VALUE);
1✔
1421
    } else {
1422
      if (currentTimeMillis == 0L) {
1✔
1423
        currentTimeMillis = currentTimeMillis();
1✔
1424
      }
1425
      @Var long expireTimeMillis = duration.getAdjustedTime(currentTimeMillis);
1✔
1426
      expireTimeMillis = ((expireTimeMillis == 0L) || (expireTimeMillis == Long.MAX_VALUE))
1✔
1427
          ? (expireTimeMillis - 1)
1✔
1428
          : expireTimeMillis;
1✔
1429
      expirable.setExpireTimeMillis(expireTimeMillis);
1✔
1430
    }
1431
  }
1✔
1432

1433
  /**
1434
   * Sets the native access expiration time.
1435
   *
1436
   * @param key the entry's key
1437
   * @param duration the access duration, or null if unchanged
1438
   */
1439
  protected final void setVariableExpiration(K key, @Nullable Duration duration) {
1440
    if (duration == null) {
1✔
1441
      return;
1✔
1442
    }
1443
    cache.policy().expireVariably().ifPresent(policy -> {
1✔
1444
      if (duration.isZero()) {
1✔
1445
        policy.setExpiresAfter(key, 0L, TimeUnit.NANOSECONDS);
1✔
1446
      } else if (duration.isEternal()) {
1✔
1447
        policy.setExpiresAfter(key, Long.MAX_VALUE, TimeUnit.NANOSECONDS);
1✔
1448
      } else {
1449
        policy.setExpiresAfter(key, duration.getDurationAmount(), duration.getTimeUnit());
1✔
1450
      }
1451
    });
1✔
1452
  }
1✔
1453

1454
  /**
1455
   * Returns the time when the entry will expire.
1456
   *
1457
   * @param created if the write operation is an insert or an update
1458
   * @return the time when the entry will expire, zero if it should expire immediately,
1459
   *         Long.MIN_VALUE if it should not be changed, or Long.MAX_VALUE if eternal
1460
   */
1461
  protected final long getWriteExpireTimeMillis(boolean created) {
1462
    try {
1463
      Duration duration = created ? expiry.getExpiryForCreation() : expiry.getExpiryForUpdate();
1✔
1464
      if (duration == null) {
1✔
1465
        return created ? Long.MAX_VALUE : Long.MIN_VALUE;
1✔
1466
      } else if (duration.isZero()) {
1✔
1467
        return 0L;
1✔
1468
      } else if (duration.isEternal()) {
1✔
1469
        return Long.MAX_VALUE;
1✔
1470
      }
1471
      long expireTimeMillis = duration.getAdjustedTime(currentTimeMillis());
1✔
1472
      return ((expireTimeMillis == 0L) || (expireTimeMillis == Long.MAX_VALUE))
1✔
1473
          ? (expireTimeMillis - 1)
1✔
1474
          : expireTimeMillis;
1✔
1475
    } catch (RuntimeException e) {
1✔
1476
      logger.log(Level.WARNING, "Failed to get the policy's expiration time", e);
1✔
1477
      return created ? Long.MAX_VALUE : Long.MIN_VALUE;
1✔
1478
    }
1479
  }
1480

1481
  /** An iterator to safely expose the cache entries. */
1482
  final class EntryIterator implements Iterator<Cache.Entry<K, V>> {
1✔
1483
    // NullAway does not yet understand the @NonNull annotation in the return type of asMap.
1484
    @SuppressWarnings("NullAway")
1✔
1485
    final Iterator<Map.Entry<K, Expirable<V>>> delegate = cache.asMap().entrySet().iterator();
1✔
1486

1487
    Map.@Nullable Entry<K, Expirable<V>> current;
1488
    Map.@Nullable Entry<K, Expirable<V>> cursor;
1489

1490
    @Override
1491
    public boolean hasNext() {
1492
      while ((cursor == null) && delegate.hasNext()) {
1✔
1493
        Map.Entry<K, Expirable<V>> entry = delegate.next();
1✔
1494
        long millis = entry.getValue().isEternal() ? 0L : currentTimeMillis();
1✔
1495
        if (!entry.getValue().hasExpired(millis)) {
1✔
1496
          var duration = getAccessExpireTime();
1✔
1497
          setVariableExpiration(entry.getKey(), duration);
1✔
1498
          setAccessExpireTime(entry.getValue(), duration, millis);
1✔
1499
          cursor = entry;
1✔
1500
        }
1501
      }
1✔
1502
      return (cursor != null);
1✔
1503
    }
1504

1505
    @Override
1506
    public Cache.Entry<K, V> next() {
1507
      if (!hasNext()) {
1✔
1508
        throw new NoSuchElementException();
1✔
1509
      }
1510
      statistics.recordHits(1L);
1✔
1511
      current = requireNonNull(cursor);
1✔
1512
      cursor = null;
1✔
1513
      return new EntryProxy<>(copyOf(current.getKey()), copyOf(current.getValue().get()));
1✔
1514
    }
1515

1516
    @Override
1517
    @SuppressWarnings("CheckReturnValue")
1518
    public void remove() {
1519
      if (current == null) {
1✔
1520
        throw new IllegalStateException();
1✔
1521
      }
1522
      CacheProxy.this.remove(current.getKey());
1✔
1523
      current = null;
1✔
1524
    }
1✔
1525
  }
1526

1527
  /** An entry paired with the store-by-value copies to be stored in its place. */
1528
  private static final class CopiedEntry<K, V> {
1529
    final K key;
1530
    final V value;
1531
    final K copiedKey;
1532
    final V copiedValue;
1533

1534
    CopiedEntry(K key, K copiedKey, V value, V copiedValue) {
1✔
1535
      this.copiedValue = copiedValue;
1✔
1536
      this.copiedKey = copiedKey;
1✔
1537
      this.value = value;
1✔
1538
      this.key = key;
1✔
1539
    }
1✔
1540
  }
1541

1542
  protected static final class PutResult<V> {
1✔
1543
    @Nullable V oldValue;
1544
    boolean written;
1545
  }
1546

1547
  protected enum NullCompletionListener implements CompletionListener {
1✔
1548
    INSTANCE;
1✔
1549

1550
    @Override
1551
    public void onCompletion() {}
1✔
1552

1553
    @Override
1554
    public void onException(Exception e) {}
1✔
1555
  }
1556
}
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