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

ben-manes / caffeine / #5669

21 Jul 2026 02:11AM UTC coverage: 99.965% (-0.01%) from 99.976%
#5669

push

github

ben-manes
Record a read-through get miss on a loader throw

LoadingCacheProxy.getOrLoad recorded the miss after cache.get runs the
read-through load, so a throwing loader propagated before the miss was
counted -- getCacheMisses stayed 0. The loading getAll, the RI
(increaseCacheMisses before cacheLoader.load), and core all count the
miss regardless of load outcome (the find already failed). Record it
before the load. Not TCK-observable (its stats tests use no loader).

Pin untested jcache close-gate and config paths

Add coverage for public-API paths the TCK never exercises: clear() and
register/deregisterCacheEntryListener on a closed cache throw ISE
(CG-1, CG-2); createCache of a name defined in application.conf throws
CacheException (CG-6); and createCache with a plain non-Complete
Configuration (types + store-by-value only) succeeds via the basic
resolveConfigurationFor branch (CG-7). Test-only; the code was already
correct.

4194 of 4203 branches covered (99.79%)

9 of 9 new or added lines in 2 files covered. (100.0%)

2 existing lines in 2 files now uncovered.

8472 of 8475 relevant lines covered (99.96%)

1.0 hits per line

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

99.87
/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.expiry.Duration;
53
import javax.cache.expiry.ExpiryPolicy;
54
import javax.cache.integration.CacheLoader;
55
import javax.cache.integration.CacheLoaderException;
56
import javax.cache.integration.CacheWriter;
57
import javax.cache.integration.CacheWriterException;
58
import javax.cache.integration.CompletionListener;
59
import javax.cache.processor.EntryProcessor;
60
import javax.cache.processor.EntryProcessorException;
61
import javax.cache.processor.EntryProcessorResult;
62

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

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

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

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

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

106
  private volatile boolean closed;
107

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

130
  @Override
131
  public boolean containsKey(K key) {
132
    requireNotClosed();
1✔
133
    Expirable<V> expirable = cache.getIfPresent(key);
1✔
134
    if (expirable == null) {
1✔
135
      return false;
1✔
136
    }
137
    if (!expirable.isEternal() && expirable.hasExpired(currentTimeMillis())) {
1✔
138
      cache.asMap().computeIfPresent(key, (k, e) -> {
1✔
139
        if (e == expirable) {
1✔
140
          dispatcher.publishExpired(this, key, expirable.get());
1✔
141
          statistics.recordEvictions(1L);
1✔
142
          return null;
1✔
143
        }
144
        return e;
1✔
145
      });
146
      dispatcher.awaitSynchronous();
1✔
147
      return false;
1✔
148
    }
149
    return true;
1✔
150
  }
151

152
  @Override
153
  public @Nullable V get(K key) {
154
    requireNotClosed();
1✔
155
    boolean statsEnabled = statistics.isEnabled();
1✔
156
    long start = statsEnabled ? ticker.read() : 0L;
1✔
157

158
    Expirable<V> expirable = cache.getIfPresent(key);
1✔
159
    if (expirable == null) {
1✔
160
      statistics.recordMisses(1L);
1✔
161
      if (statsEnabled) {
1✔
162
        statistics.recordGetTime(ticker.read() - start);
1✔
163
      }
164
      return null;
1✔
165
    }
166

167
    long millis;
168
    if (expirable.isEternal()) {
1✔
169
      millis = 0L;
1✔
170
    } else {
171
      long now = ticker.read();
1✔
172
      millis = nanosToMillis(now);
1✔
173
      if (expirable.hasExpired(millis)) {
1✔
174
        cache.asMap().computeIfPresent(key, (k, e) -> {
1✔
175
          if (e == expirable) {
1✔
176
            dispatcher.publishExpired(this, key, expirable.get());
1✔
177
            statistics.recordEvictions(1L);
1✔
178
            return null;
1✔
179
          }
180
          return e;
1✔
181
        });
182
        dispatcher.awaitSynchronous();
1✔
183
        statistics.recordMisses(1L);
1✔
184
        if (statsEnabled) {
1✔
185
          statistics.recordGetTime(ticker.read() - start);
1✔
186
        }
187
        return null;
1✔
188
      }
189
    }
190

191
    var duration = getAccessExpireTime();
1✔
192
    setVariableExpiration(key, duration);
1✔
193
    setAccessExpireTime(expirable, duration, millis);
1✔
194
    V value = copyOf(expirable.get());
1✔
195
    if (statsEnabled) {
1✔
196
      statistics.recordHits(1L);
1✔
197
      statistics.recordGetTime(ticker.read() - start);
1✔
198
    }
199
    return value;
1✔
200
  }
201

202
  @Override
203
  public Map<K, V> getAll(Set<? extends K> keys) {
204
    requireNotClosed();
1✔
205

206
    boolean statsEnabled = statistics.isEnabled();
1✔
207
    long now = statsEnabled ? ticker.read() : 0L;
1✔
208
    try {
209
      Map<K, Expirable<V>> result = getAndFilterExpiredEntries(keys);
1✔
210
      if (statsEnabled) {
1✔
211
        statistics.recordGetTime(ticker.read() - now);
1✔
212
      }
213
      return copyMap(result);
1✔
214
    } finally {
215
      dispatcher.awaitSynchronous();
1✔
216
    }
217
  }
218

219
  /**
220
   * Returns all of the mappings present, expiring as required, and updates their access expiry
221
   * time.
222
   */
223
  protected Map<K, Expirable<V>> getAndFilterExpiredEntries(Set<? extends K> keys) {
224
    int[] expired = { 0 };
1✔
225
    long[] millis = { 0L };
1✔
226
    var result = new HashMap<K, @NonNull Expirable<V>>(cache.getAllPresent(keys));
1✔
227
    result.entrySet().removeIf(entry -> {
1✔
228
      if (!entry.getValue().isEternal() && (millis[0] == 0L)) {
1✔
229
        millis[0] = currentTimeMillis();
1✔
230
      }
231
      if (entry.getValue().hasExpired(millis[0])) {
1✔
232
        cache.asMap().computeIfPresent(entry.getKey(), (k, expirable) -> {
1✔
233
          if (expirable == entry.getValue()) {
1✔
234
            dispatcher.publishExpired(this, entry.getKey(), entry.getValue().get());
1✔
235
            expired[0]++;
1✔
236
            return null;
1✔
237
          }
238
          return expirable;
1✔
239
        });
240
        return true;
1✔
241
      }
242
      var duration = getAccessExpireTime();
1✔
243
      setVariableExpiration(entry.getKey(), duration);
1✔
244
      setAccessExpireTime(entry.getValue(), duration, millis[0]);
1✔
245
      return false;
1✔
246
    });
247

248
    statistics.recordHits(result.size());
1✔
249
    statistics.recordMisses(keys.size() - result.size());
1✔
250
    statistics.recordEvictions(expired[0]);
1✔
251
    return result;
1✔
252
  }
253

254
  @Override
255
  @SuppressWarnings({"CollectionUndefinedEquality", "FutureReturnValueIgnored"})
256
  public void loadAll(Set<? extends K> keys, boolean replaceExistingValues,
257
      @Nullable CompletionListener completionListener) {
258
    requireNotClosed();
1✔
259
    keys.forEach(Objects::requireNonNull);
1✔
260
    CompletionListener listener = (completionListener == null)
1✔
261
        ? NullCompletionListener.INSTANCE
1✔
262
        : completionListener;
1✔
263
    if (cacheLoader.isEmpty()) {
1✔
264
      listener.onCompletion();
1✔
265
      return;
1✔
266
    }
267

268
    var future = new CompletableFuture<@Nullable Void>();
1✔
269
    synchronized (configuration) {
1✔
270
      requireNotClosed();
1✔
271
      inFlight.add(future);
1✔
272
    }
1✔
273
    try {
274
      CompletableFuture.runAsync(() -> {
1✔
275
        @Var boolean success = false;
1✔
276
        try {
277
          if (replaceExistingValues) {
1✔
278
            loadAllAndReplaceExisting(keys);
1✔
279
          } else {
280
            loadAllAndKeepExisting(keys);
1✔
281
          }
282
          success = true;
1✔
283
        } catch (CacheLoaderException e) {
1✔
284
          dispatcher.ignoreSynchronous();
1✔
285
          listener.onException(e);
1✔
286
        } catch (RuntimeException e) {
1✔
287
          dispatcher.ignoreSynchronous();
1✔
288
          listener.onException(new CacheLoaderException(e));
1✔
289
        }
1✔
290
        if (success) {
1✔
291
          dispatcher.chainSynchronous().whenComplete((failure, error) -> {
1✔
292
            if (error != null) {
1!
UNCOV
293
              listener.onException(new CacheLoaderException(error));
×
294
            } else if (failure != null) {
1✔
295
              listener.onException(failure);
1✔
296
            } else {
297
              listener.onCompletion();
1✔
298
            }
299
          });
1✔
300
        }
301
      }, executor).whenComplete((r, e) -> {
1✔
302
        inFlight.remove(future);
1✔
303
        future.complete(null);
1✔
304
      });
1✔
305
    } catch (RuntimeException e) {
1✔
306
      inFlight.remove(future);
1✔
307
      future.complete(null);
1✔
308
      listener.onException(new CacheLoaderException(e));
1✔
309
    }
1✔
310
  }
1✔
311

312
  /** Performs the bulk load where the existing entries are replaced. */
313
  protected void loadAllAndReplaceExisting(Set<? extends K> keys) {
314
    Map<K, V> loaded = cacheLoader.orElseThrow().loadAll(keys);
1✔
315
    for (var entry : loaded.entrySet()) {
1✔
316
      if ((entry.getKey() != null) && (entry.getValue() != null)) {
1✔
317
        putNoCopyOrAwait(entry.getKey(), entry.getValue(), /* publishToWriter= */ false);
1✔
318
      }
319
    }
1✔
320
  }
1✔
321

322
  /** Performs the bulk load where the existing entries are retained. */
323
  @SuppressWarnings("ConstantValue")
324
  protected void loadAllAndKeepExisting(Set<? extends K> keys) {
325
    List<K> keysToLoad = keys.stream()
1✔
326
        .filter(key -> {
1✔
327
          var expirable = cache.policy().getIfPresentQuietly(key);
1✔
328
          return (expirable == null)
1✔
329
              || (!expirable.isEternal() && expirable.hasExpired(currentTimeMillis()));
1✔
330
        }).collect(toUnmodifiableList());
1✔
331
    Map<K, V> result = cacheLoader.orElseThrow().loadAll(keysToLoad);
1✔
332
    for (var entry : result.entrySet()) {
1✔
333
      if ((entry.getKey() != null) && (entry.getValue() != null)) {
1✔
334
        putIfAbsentNoAwait(entry.getKey(), entry.getValue(), /* publishToWriter= */ false);
1✔
335
      }
336
    }
1✔
337
  }
1✔
338

339
  @Override
340
  public void put(K key, V value) {
341
    requireNotClosed();
1✔
342
    boolean statsEnabled = statistics.isEnabled();
1✔
343
    long start = statsEnabled ? ticker.read() : 0L;
1✔
344

345
    var result = putNoCopyOrAwait(key, value, /* publishToWriter= */ true);
1✔
346
    dispatcher.awaitSynchronous();
1✔
347

348
    if (statsEnabled && result.written) {
1✔
349
      statistics.recordPuts(1);
1✔
350
      statistics.recordPutTime(ticker.read() - start);
1✔
351
    }
352
  }
1✔
353

354
  @Override
355
  public @Nullable V getAndPut(K key, V value) {
356
    requireNotClosed();
1✔
357
    boolean statsEnabled = statistics.isEnabled();
1✔
358
    long start = statsEnabled ? ticker.read() : 0L;
1✔
359

360
    var result = putNoCopyOrAwait(key, value, /* publishToWriter= */ true);
1✔
361
    dispatcher.awaitSynchronous();
1✔
362

363
    if (statsEnabled) {
1✔
364
      if (result.oldValue == null) {
1✔
365
        statistics.recordMisses(1L);
1✔
366
      } else {
367
        statistics.recordHits(1L);
1✔
368
      }
369
      long duration = ticker.read() - start;
1✔
370
      if (result.written) {
1✔
371
        statistics.recordPuts(1);
1✔
372
        statistics.recordPutTime(duration);
1✔
373
      }
374
      statistics.recordGetTime(duration);
1✔
375
    }
376
    return (result.oldValue == null) ? null : copyOf(result.oldValue);
1✔
377
  }
378

379
  /**
380
   * Associates the specified value with the specified key in the cache.
381
   *
382
   * @param key key with which the specified value is to be associated
383
   * @param value value to be associated with the specified key
384
   * @param publishToWriter if the writer should be notified
385
   * @return the oldValue and if the new value was stored
386
   */
387
  @CanIgnoreReturnValue
388
  protected PutResult<V> putNoCopyOrAwait(K key, V value, boolean publishToWriter) {
389
    requireNonNull(key);
1✔
390
    requireNonNull(value);
1✔
391

392
    V newValue = copyOf(value);
1✔
393
    var result = new PutResult<V>();
1✔
394
    cache.asMap().compute(copyOf(key), (K k, @Var Expirable<V> expirable) -> {
1✔
395
      if (publishToWriter) {
1✔
396
        publishToCacheWriter(writer::write, () -> new EntryProxy<>(key, value));
1✔
397
      }
398
      if ((expirable != null) && !expirable.isEternal()
1✔
399
          && expirable.hasExpired(currentTimeMillis())) {
1✔
400
        dispatcher.publishExpired(this, key, expirable.get());
1✔
401
        statistics.recordEvictions(1L);
1✔
402
        expirable = null;
1✔
403
      }
404
      @Var long expireTimeMillis = getWriteExpireTimeMillis((expirable == null));
1✔
405
      if ((expirable != null) && (expireTimeMillis == Long.MIN_VALUE)) {
1✔
406
        expireTimeMillis = expirable.getExpireTimeMillis();
1✔
407
      }
408
      if ((expireTimeMillis == 0) && (expirable == null)) {
1✔
409
        result.written = false;
1✔
410
        dispatcher.publishExpired(this, key, newValue);
1✔
411
        return null;
1✔
412
      } else if (expirable == null) {
1✔
413
        dispatcher.publishCreated(this, key, newValue);
1✔
414
      } else {
415
        result.oldValue = expirable.get();
1✔
416
        dispatcher.publishUpdated(this, key, expirable.get(), newValue);
1✔
417
      }
418
      result.written = true;
1✔
419
      return new Expirable<>(newValue, expireTimeMillis);
1✔
420
    });
421
    return result;
1✔
422
  }
423

424
  @Override
425
  public void putAll(Map<? extends K, ? extends V> map) {
426
    requireNotClosed();
1✔
427
    for (var entry : map.entrySet()) {
1✔
428
      requireNonNull(entry.getKey());
1✔
429
      requireNonNull(entry.getValue());
1✔
430
    }
1✔
431

432
    @Var CacheWriterException error = null;
1✔
433
    @Var Set<? extends K> failedKeys = Set.of();
1✔
434
    boolean statsEnabled = statistics.isEnabled();
1✔
435
    long start = statsEnabled ? ticker.read() : 0L;
1✔
436
    if (configuration.isWriteThrough() && !map.isEmpty()) {
1✔
437
      var entries = new ArrayList<Cache.Entry<? extends K, ? extends V>>(map.size());
1✔
438
      for (var entry : map.entrySet()) {
1✔
439
        entries.add(new EntryProxy<>(entry.getKey(), entry.getValue()));
1✔
440
      }
1✔
441
      try {
442
        writer.writeAll(entries);
1✔
443
      } catch (CacheWriterException e) {
1✔
444
        failedKeys = entries.stream().map(Cache.Entry::getKey).collect(toSet());
1✔
445
        error = e;
1✔
446
      } catch (RuntimeException e) {
1✔
447
        failedKeys = entries.stream().map(Cache.Entry::getKey).collect(toSet());
1✔
448
        error = new CacheWriterException("Exception in CacheWriter", e);
1✔
449
      }
1✔
450
    }
451

452
    @Var int puts = 0;
1✔
453
    try {
454
      for (var entry : map.entrySet()) {
1✔
455
        if (!failedKeys.contains(entry.getKey())) {
1✔
456
          var result = putNoCopyOrAwait(entry.getKey(),
1✔
457
              entry.getValue(), /* publishToWriter= */ false);
1✔
458
          if (result.written) {
1✔
459
            puts++;
1✔
460
          }
461
        }
462
      }
1✔
463
    } finally {
464
      dispatcher.awaitSynchronous();
1✔
465
    }
466

467
    if (statsEnabled && (puts > 0)) {
1✔
468
      statistics.recordPuts(puts);
1✔
469
      statistics.recordPutTime(ticker.read() - start);
1✔
470
    }
471
    if (error != null) {
1✔
472
      throw error;
1✔
473
    }
474
  }
1✔
475

476
  @Override
477
  public boolean putIfAbsent(K key, V value) {
478
    requireNotClosed();
1✔
479
    requireNonNull(value);
1✔
480
    boolean statsEnabled = statistics.isEnabled();
1✔
481
    long start = statsEnabled ? ticker.read() : 0L;
1✔
482

483
    var result = putIfAbsentNoAwait(key, value, /* publishToWriter= */ true);
1✔
484
    dispatcher.awaitSynchronous();
1✔
485

486
    if (statsEnabled) {
1✔
487
      if (result.oldValue != null) {
1✔
488
        statistics.recordHits(1L);
1✔
489
      } else {
490
        statistics.recordMisses(1L);
1✔
491
      }
492
      if (result.written) {
1✔
493
        statistics.recordPuts(1L);
1✔
494
        statistics.recordPutTime(ticker.read() - start);
1✔
495
      }
496
    }
497
    return result.written;
1✔
498
  }
499

500
  /**
501
   * Associates the specified value with the specified key in the cache if there is no existing
502
   * mapping.
503
   *
504
   * @param key key with which the specified value is to be associated
505
   * @param value value to be associated with the specified key
506
   * @param publishToWriter if the writer should be notified
507
   * @return the oldValue and if the new value was stored
508
   */
509
  @CanIgnoreReturnValue
510
  private PutResult<V> putIfAbsentNoAwait(K key, V value, boolean publishToWriter) {
511
    var result = new PutResult<V>();
1✔
512
    cache.asMap().compute(copyOf(key), (K k, Expirable<V> expirable) -> {
1✔
513
      if ((expirable != null)
1✔
514
          && (expirable.isEternal() || !expirable.hasExpired(currentTimeMillis()))) {
1✔
515
        result.oldValue = expirable.get();
1✔
516
        return expirable;
1✔
517
      }
518

519
      V copy = copyOf(value);
1✔
520
      if (publishToWriter) {
1✔
521
        publishToCacheWriter(writer::write, () -> new EntryProxy<>(key, value));
1✔
522
      }
523
      if (expirable != null) {
1✔
524
        dispatcher.publishExpired(this, key, expirable.get());
1✔
525
        statistics.recordEvictions(1L);
1✔
526
      }
527

528
      long expireTimeMillis = getWriteExpireTimeMillis(/* created= */ true);
1✔
529
      if (expireTimeMillis == 0) {
1✔
530
        // A zero creation expiry means the entry is already expired and is not added
531
        dispatcher.publishExpired(this, key, copy);
1✔
532
        return null;
1✔
533
      } else {
534
        result.written = true;
1✔
535
        dispatcher.publishCreated(this, key, copy);
1✔
536
        return new Expirable<>(copy, expireTimeMillis);
1✔
537
      }
538
    });
539
    return result;
1✔
540
  }
541

542
  @Override
543
  public boolean remove(K key) {
544
    requireNotClosed();
1✔
545
    requireNonNull(key);
1✔
546
    boolean statsEnabled = statistics.isEnabled();
1✔
547
    long start = statsEnabled ? ticker.read() : 0L;
1✔
548

549
    V value = removeNoCopyOrAwait(key, /* publishToWriter= */ true);
1✔
550
    dispatcher.awaitSynchronous();
1✔
551

552
    if (value != null) {
1✔
553
      statistics.recordRemovals(1L);
1✔
554
      if (statsEnabled) {
1✔
555
        statistics.recordRemoveTime(ticker.read() - start);
1✔
556
      }
557
      return true;
1✔
558
    }
559
    return false;
1✔
560
  }
561

562
  /**
563
   * Removes the mapping from the cache without store-by-value copying nor waiting for synchronous
564
   * listeners to complete.
565
   *
566
   * @param key key whose mapping is to be removed from the cache
567
   * @param publishToWriter if the writer should be notified
568
   * @return the old value
569
   */
570
  private @Nullable V removeNoCopyOrAwait(K key, boolean publishToWriter) {
571
    @SuppressWarnings("unchecked")
572
    var removed = (V[]) new Object[1];
1✔
573
    cache.asMap().compute(key, (K k, Expirable<V> expirable) -> {
1✔
574
      if (publishToWriter) {
1✔
575
        publishToCacheWriter(writer::delete, () -> key);
1✔
576
      }
577
      if (expirable != null) {
1✔
578
        if (!expirable.isEternal() && expirable.hasExpired(currentTimeMillis())) {
1✔
579
          dispatcher.publishExpired(this, key, expirable.get());
1✔
580
          statistics.recordEvictions(1L);
1✔
581
        } else {
582
          dispatcher.publishRemoved(this, key, expirable.get());
1✔
583
          removed[0] = expirable.get();
1✔
584
        }
585
      }
586
      return null;
1✔
587
    });
588
    return removed[0];
1✔
589
  }
590

591
  @Override
592
  @CanIgnoreReturnValue
593
  public boolean remove(K key, V oldValue) {
594
    requireNotClosed();
1✔
595
    requireNonNull(key);
1✔
596
    requireNonNull(oldValue);
1✔
597

598
    boolean statsEnabled = statistics.isEnabled();
1✔
599
    long start = statsEnabled ? ticker.read() : 0L;
1✔
600
    boolean[] found = { false };
1✔
601

602
    boolean[] removed = { false };
1✔
603
    cache.asMap().computeIfPresent(key, (k, expirable) -> {
1✔
604
      long millis = expirable.isEternal()
1✔
605
          ? 0L
1✔
606
          : nanosToMillis((start == 0L) ? ticker.read() : start);
1✔
607
      if (expirable.hasExpired(millis)) {
1✔
608
        dispatcher.publishExpired(this, key, expirable.get());
1✔
609
        statistics.recordEvictions(1L);
1✔
610
        return null;
1✔
611
      }
612

613
      found[0] = true;
1✔
614
      if (oldValue.equals(expirable.get())) {
1✔
615
        publishToCacheWriter(writer::delete, () -> key);
1✔
616
        dispatcher.publishRemoved(this, key, expirable.get());
1✔
617
        removed[0] = true;
1✔
618
        return null;
1✔
619
      }
620
      setAccessExpireTime(expirable, getAccessExpireTime(), millis);
1✔
621
      return expirable;
1✔
622
    });
623
    dispatcher.awaitSynchronous();
1✔
624
    if (statsEnabled) {
1✔
625
      if (removed[0]) {
1✔
626
        statistics.recordRemovals(1L);
1✔
627
        statistics.recordHits(1L);
1✔
628
        statistics.recordRemoveTime(ticker.read() - start);
1✔
629
      } else if (found[0]) {
1✔
630
        statistics.recordHits(1L);
1✔
631
      } else {
632
        statistics.recordMisses(1L);
1✔
633
      }
634
    }
635
    return removed[0];
1✔
636
  }
637

638
  @Override
639
  public @Nullable V getAndRemove(K key) {
640
    requireNotClosed();
1✔
641
    requireNonNull(key);
1✔
642
    boolean statsEnabled = statistics.isEnabled();
1✔
643
    long start = statsEnabled ? ticker.read() : 0L;
1✔
644

645
    V value = removeNoCopyOrAwait(key, /* publishToWriter= */ true);
1✔
646
    dispatcher.awaitSynchronous();
1✔
647

648
    V copy = (value == null) ? null : copyOf(value);
1✔
649
    if (statsEnabled) {
1✔
650
      long duration = ticker.read() - start;
1✔
651
      if (copy == null) {
1✔
652
        statistics.recordMisses(1L);
1✔
653
      } else {
654
        statistics.recordHits(1L);
1✔
655
        statistics.recordRemovals(1L);
1✔
656
        statistics.recordRemoveTime(duration);
1✔
657
      }
658
      statistics.recordGetTime(duration);
1✔
659
    }
660
    return copy;
1✔
661
  }
662

663
  @Override
664
  public boolean replace(K key, V oldValue, V newValue) {
665
    requireNotClosed();
1✔
666
    requireNonNull(oldValue);
1✔
667
    requireNonNull(newValue);
1✔
668

669
    boolean statsEnabled = statistics.isEnabled();
1✔
670
    long start = statsEnabled ? ticker.read() : 0L;
1✔
671

672
    boolean[] found = { false };
1✔
673
    boolean[] replaced = { false };
1✔
674
    cache.asMap().computeIfPresent(key, (k, expirable) -> {
1✔
675
      long millis = expirable.isEternal()
1✔
676
          ? 0L
1✔
677
          : nanosToMillis((start == 0L) ? ticker.read() : start);
1✔
678
      if (expirable.hasExpired(millis)) {
1✔
679
        dispatcher.publishExpired(this, key, expirable.get());
1✔
680
        statistics.recordEvictions(1L);
1✔
681
        return null;
1✔
682
      }
683

684
      found[0] = true;
1✔
685
      Expirable<V> result;
686
      if (oldValue.equals(expirable.get())) {
1✔
687
        V copy = copyOf(newValue);
1✔
688
        publishToCacheWriter(writer::write, () -> new EntryProxy<>(key, newValue));
1✔
689
        dispatcher.publishUpdated(this, key, expirable.get(), copy);
1✔
690
        @Var long expireTimeMillis = getWriteExpireTimeMillis(/* created= */ false);
1✔
691
        if (expireTimeMillis == Long.MIN_VALUE) {
1✔
692
          expireTimeMillis = expirable.getExpireTimeMillis();
1✔
693
        }
694
        result = new Expirable<>(copy, expireTimeMillis);
1✔
695
        replaced[0] = true;
1✔
696
      } else {
1✔
697
        result = expirable;
1✔
698
        setAccessExpireTime(expirable, getAccessExpireTime(), millis);
1✔
699
      }
700
      return result;
1✔
701
    });
702
    dispatcher.awaitSynchronous();
1✔
703

704
    if (statsEnabled) {
1✔
705
      statistics.recordMisses(found[0] ? 0L : 1L);
1✔
706
      statistics.recordHits(found[0] ? 1L : 0L);
1✔
707
      long duration = ticker.read() - start;
1✔
708
      if (replaced[0]) {
1✔
709
        statistics.recordPuts(1L);
1✔
710
        statistics.recordPutTime(duration);
1✔
711
      }
712
      statistics.recordGetTime(duration);
1✔
713
    }
714

715
    return replaced[0];
1✔
716
  }
717

718
  @Override
719
  public boolean replace(K key, V value) {
720
    requireNotClosed();
1✔
721
    boolean statsEnabled = statistics.isEnabled();
1✔
722
    long start = statsEnabled ? ticker.read() : 0L;
1✔
723

724
    @Nullable V oldValue = replaceNoCopyOrAwait(key, value);
1✔
725
    dispatcher.awaitSynchronous();
1✔
726
    if (oldValue == null) {
1✔
727
      statistics.recordMisses(1L);
1✔
728
      if (statsEnabled) {
1✔
729
        statistics.recordGetTime(ticker.read() - start);
1✔
730
      }
731
      return false;
1✔
732
    }
733

734
    if (statsEnabled) {
1✔
735
      statistics.recordHits(1L);
1✔
736
      statistics.recordPuts(1L);
1✔
737
      long duration = ticker.read() - start;
1✔
738
      statistics.recordGetTime(duration);
1✔
739
      statistics.recordPutTime(duration);
1✔
740
    }
741
    return true;
1✔
742
  }
743

744
  @Override
745
  public @Nullable V getAndReplace(K key, V value) {
746
    requireNotClosed();
1✔
747
    boolean statsEnabled = statistics.isEnabled();
1✔
748
    long start = statsEnabled ? ticker.read() : 0L;
1✔
749

750
    V oldValue = replaceNoCopyOrAwait(key, value);
1✔
751
    dispatcher.awaitSynchronous();
1✔
752

753
    V copy = (oldValue == null) ? null : copyOf(oldValue);
1✔
754
    if (statsEnabled) {
1✔
755
      long duration = ticker.read() - start;
1✔
756
      if (copy == null) {
1✔
757
        statistics.recordMisses(1L);
1✔
758
      } else {
759
        statistics.recordHits(1L);
1✔
760
        statistics.recordPuts(1L);
1✔
761
        statistics.recordPutTime(duration);
1✔
762
      }
763
      statistics.recordGetTime(duration);
1✔
764
    }
765
    return copy;
1✔
766
  }
767

768
  /**
769
   * Replaces the entry for the specified key only if it is currently mapped to some value. The
770
   * entry is not store-by-value copied nor does the method wait for synchronous listeners to
771
   * complete.
772
   *
773
   * @param key key with which the specified value is associated
774
   * @param value value to be associated with the specified key
775
   * @return the old value
776
   */
777
  private @Nullable V replaceNoCopyOrAwait(K key, V value) {
778
    V copy = copyOf(value);
1✔
779
    @SuppressWarnings("unchecked")
780
    var replaced = (V[]) new Object[1];
1✔
781
    cache.asMap().computeIfPresent(key, (k, expirable) -> {
1✔
782
      if (!expirable.isEternal() && expirable.hasExpired(currentTimeMillis())) {
1✔
783
        dispatcher.publishExpired(this, key, expirable.get());
1✔
784
        statistics.recordEvictions(1L);
1✔
785
        return null;
1✔
786
      }
787

788
      publishToCacheWriter(writer::write, () -> new EntryProxy<>(key, value));
1✔
789
      @Var long expireTimeMillis = getWriteExpireTimeMillis(/* created= */ false);
1✔
790
      if (expireTimeMillis == Long.MIN_VALUE) {
1✔
791
        expireTimeMillis = expirable.getExpireTimeMillis();
1✔
792
      }
793
      dispatcher.publishUpdated(this, key, expirable.get(), copy);
1✔
794
      replaced[0] = expirable.get();
1✔
795
      return new Expirable<>(copy, expireTimeMillis);
1✔
796
    });
797
    return replaced[0];
1✔
798
  }
799

800
  @Override
801
  public void removeAll(Set<? extends K> keys) {
802
    requireNotClosed();
1✔
803
    var keysToRemove = new LinkedHashSet<>(keys);
1✔
804
    keysToRemove.forEach(Objects::requireNonNull);
1✔
805

806
    @Var CacheWriterException error = null;
1✔
807
    @Var Set<? extends K> failedKeys = Set.of();
1✔
808
    boolean statsEnabled = statistics.isEnabled();
1✔
809
    long start = statsEnabled ? ticker.read() : 0L;
1✔
810
    if (configuration.isWriteThrough() && !keysToRemove.isEmpty()) {
1✔
811
      var keysToWrite = new LinkedHashSet<>(keysToRemove);
1✔
812
      try {
813
        writer.deleteAll(keysToWrite);
1✔
814
      } catch (CacheWriterException e) {
1✔
815
        error = e;
1✔
816
        failedKeys = keysToWrite;
1✔
817
      } catch (RuntimeException e) {
1✔
818
        error = new CacheWriterException("Exception in CacheWriter", e);
1✔
819
        failedKeys = keysToWrite;
1✔
820
      }
1✔
821
    }
822

823
    @Var int removed = 0;
1✔
824
    try {
825
      for (var key : keysToRemove) {
1✔
826
        if (!failedKeys.contains(key)
1✔
827
            && (removeNoCopyOrAwait(key, /* publishToWriter= */ false) != null)) {
1✔
828
          removed++;
1✔
829
        }
830
      }
1✔
831
    } finally {
832
      dispatcher.awaitSynchronous();
1✔
833
    }
834

835
    if (statsEnabled && (removed > 0)) {
1✔
836
      statistics.recordRemovals(removed);
1✔
837
      statistics.recordRemoveTime(ticker.read() - start);
1✔
838
    }
839
    if (error != null) {
1✔
840
      throw error;
1✔
841
    }
842
  }
1✔
843

844
  @Override
845
  public void removeAll() {
846
    removeAll(cache.asMap().keySet());
1✔
847
  }
1✔
848

849
  @Override
850
  public void clear() {
851
    requireNotClosed();
1✔
852
    cache.invalidateAll();
1✔
853
  }
1✔
854

855
  @Override
856
  public <C extends Configuration<K, V>> C getConfiguration(Class<C> clazz) {
857
    if (clazz.isInstance(configuration)) {
1✔
858
      synchronized (configuration) {
1✔
859
        return clazz.cast(configuration.immutableCopy());
1✔
860
      }
861
    }
862
    throw new IllegalArgumentException("The configuration class " + clazz
1✔
863
        + " is not supported by this implementation");
864
  }
865

866
  @Override
867
  public <T extends @Nullable Object> T invoke(K key,
868
      EntryProcessor<K, V, T> entryProcessor, Object... arguments) {
869
    requireNonNull(entryProcessor);
1✔
870
    requireNonNull(arguments);
1✔
871
    requireNotClosed();
1✔
872

873
    var result = new Object[1];
1✔
874
    var failure = new Throwable[1];
1✔
875
    BiFunction<K, Expirable<V>, Expirable<V>> remappingFunction = (k, expirable) -> {
1✔
876
      // Publish a lazily-expired prior's expiration before the processor observes it as absent,
877
      // so listeners see a linearizable sequence and the expiration is committed exactly once
878
      boolean expired;
879
      Expirable<V> prior;
880
      if ((expirable != null) && !expirable.isEternal()
1✔
881
          && expirable.hasExpired(currentTimeMillis())) {
1✔
882
        dispatcher.publishExpired(this, key, expirable.get());
1✔
883
        statistics.recordEvictions(1L);
1✔
884
        expired = true;
1✔
885
        prior = null;
1✔
886
      } else {
887
        prior = expirable;
1✔
888
        expired = false;
1✔
889
      }
890

891
      V value;
892
      if (prior == null) {
1✔
893
        statistics.recordMisses(1L);
1✔
894
        value = null;
1✔
895
      } else {
896
        value = copyOf(prior.get());
1✔
897
        statistics.recordHits(1L);
1✔
898
      }
899
      var entry = new EntryProcessorEntry<>(key, value,
1✔
900
          configuration.isReadThrough() ? cacheLoader : Optional.empty());
1✔
901
      try {
902
        result[0] = entryProcessor.process(entry, arguments);
1✔
903
        return postProcess(prior, entry);
1✔
904
      } catch (Throwable e) {
1✔
905
        if (!expired) {
1✔
906
          throw processorFailure(e);
1✔
907
        }
908
        failure[0] = e;
1✔
909
        return null;
1✔
910
      }
911
    };
912
    try {
913
      cache.asMap().compute(copyOf(key), remappingFunction);
1✔
914
    } catch (Throwable t) {
1✔
915
      dispatcher.ignoreSynchronous();
1✔
916
      throw t;
1✔
917
    }
1✔
918
    dispatcher.awaitSynchronous();
1✔
919
    if (failure[0] != null) {
1✔
920
      throw processorFailure(failure[0]);
1✔
921
    }
922

923
    @SuppressWarnings("unchecked")
924
    var castedResult = (T) result[0];
1✔
925
    return castedResult;
1✔
926
  }
927

928
  /** Returns the exception to rethrow on an entry processor failure. */
929
  private static RuntimeException processorFailure(Throwable e) {
930
    if (e instanceof Error) {
1✔
931
      throw (Error) e;
1✔
932
    } else if (e instanceof EntryProcessorException) {
1✔
933
      return (EntryProcessorException) e;
1✔
934
    }
935
    return new EntryProcessorException(e);
1✔
936
  }
937

938
  /**
939
   * Returns the updated expirable value after performing the post-processing actions. A null
940
   * {@code expirable} means the entry was absent and READ/UPDATED (which require a live prior)
941
   * never observe one.
942
   */
943
  @Nullable Expirable<V> postProcess(
944
      @Nullable Expirable<V> expirable, EntryProcessorEntry<K, V> entry) {
945
    switch (entry.getAction()) {
1✔
946
      case NONE:
947
        return expirable;
1✔
948
      case READ: {
949
        setAccessExpireTime(requireNonNull(expirable), getAccessExpireTime(), 0L);
1✔
950
        return expirable;
1✔
951
      }
952
      case CREATED:
953
      case LOADED: {
954
        V value = requireNonNull(entry.getValue());
1✔
955
        V copy = copyOf(value);
1✔
956
        if (entry.getAction() == Action.CREATED) {
1✔
957
          publishToCacheWriter(writer::write, () -> new EntryProxy<>(entry.getKey(), value));
1✔
958
        }
959
        long expireTimeMillis = getWriteExpireTimeMillis(/* created= */ true);
1✔
960
        if (expireTimeMillis == 0) {
1✔
961
          // A zero creation expiry means the entry is already expired and is not added, so the
962
          // create is neither counted nor published
963
          dispatcher.publishExpired(this, entry.getKey(), copy);
1✔
964
          return null;
1✔
965
        }
966
        if (entry.getAction() == Action.CREATED) {
1✔
967
          statistics.recordPuts(1L);
1✔
968
        }
969
        dispatcher.publishCreated(this, entry.getKey(), copy);
1✔
970
        return new Expirable<>(copy, expireTimeMillis);
1✔
971
      }
972
      case UPDATED: {
973
        requireNonNull(expirable, "Expected a previous value but was null");
1✔
974
        V value = requireNonNull(entry.getValue(), "Expected a new value but was null");
1✔
975
        V copy = copyOf(value);
1✔
976
        publishToCacheWriter(writer::write, () -> new EntryProxy<>(entry.getKey(), value));
1✔
977
        statistics.recordPuts(1L);
1✔
978
        dispatcher.publishUpdated(this, entry.getKey(), expirable.get(), copy);
1✔
979
        @Var long expireTimeMillis = getWriteExpireTimeMillis(/* created= */ false);
1✔
980
        if (expireTimeMillis == Long.MIN_VALUE) {
1✔
981
          expireTimeMillis = expirable.getExpireTimeMillis();
1✔
982
        }
983
        return new Expirable<>(copy, expireTimeMillis);
1✔
984
      }
985
      case DELETED:
986
        publishToCacheWriter(writer::delete, entry::getKey);
1✔
987
        if (expirable != null) {
1✔
988
          statistics.recordRemovals(1L);
1✔
989
          dispatcher.publishRemoved(this, entry.getKey(), expirable.get());
1✔
990
        }
991
        return null;
1✔
992
    }
993
    throw new IllegalStateException("Unknown state: " + entry.getAction());
1✔
994
  }
995

996
  @Override
997
  public <T> Map<K, EntryProcessorResult<T>> invokeAll(Set<? extends K> keys,
998
      EntryProcessor<K, V, T> entryProcessor, Object... arguments) {
999
    requireNotClosed();
1✔
1000
    requireNonNull(keys);
1✔
1001
    requireNonNull(arguments);
1✔
1002
    requireNonNull(entryProcessor);
1✔
1003
    keys.forEach(Objects::requireNonNull);
1✔
1004

1005
    var results = new HashMap<K, EntryProcessorResult<T>>(keys.size(), 1.0f);
1✔
1006
    for (K key : keys) {
1✔
1007
      try {
1008
        T result = invoke(key, entryProcessor, arguments);
1✔
1009
        if (result != null) {
1✔
1010
          results.put(key, () -> result);
1✔
1011
        }
1012
      } catch (EntryProcessorException e) {
1✔
1013
        results.put(key, () -> { throw e; });
1✔
1014
      } catch (RuntimeException e) {
1✔
1015
        results.put(key, () -> { throw new EntryProcessorException(e); });
1✔
1016
      }
1✔
1017
    }
1✔
1018
    return results;
1✔
1019
  }
1020

1021
  @Override
1022
  public String getName() {
1023
    return name;
1✔
1024
  }
1025

1026
  @Override
1027
  public CacheManager getCacheManager() {
1028
    return cacheManager;
1✔
1029
  }
1030

1031
  @Override
1032
  public boolean isClosed() {
1033
    return closed;
1✔
1034
  }
1035

1036
  @Override
1037
  public void close() {
1038
    if (isClosed()) {
1✔
1039
      return;
1✔
1040
    }
1041
    synchronized (configuration) {
1✔
1042
      if (!isClosed()) {
1✔
1043
        closed = true;
1✔
1044
        try {
1045
          cacheManager.destroyCache(name, this);
1✔
1046
        } catch (IllegalStateException ignored) { /* manager already closed */ }
1✔
1047

1048
        @Var var thrown = shutdownExecutor();
1✔
1049
        thrown = tryClose(expiry, thrown);
1✔
1050
        thrown = tryClose(writer, thrown);
1✔
1051
        thrown = tryClose(cacheLoader.orElse(null), thrown);
1✔
1052
        for (Registration<K, V> registration : dispatcher.registrations()) {
1✔
1053
          thrown = tryClose(registration.getCacheEntryListener(), thrown);
1✔
1054
        }
1✔
1055
        thrown = tryClose((AutoCloseable) () ->
1✔
1056
            JmxRegistration.unregisterMxBean(this, MBeanType.STATISTICS), thrown);
1✔
1057
        thrown = tryClose((AutoCloseable) () ->
1✔
1058
            JmxRegistration.unregisterMxBean(this, MBeanType.CONFIGURATION), thrown);
1✔
1059
        if (thrown != null) {
1✔
1060
          logger.log(Level.WARNING, "Failure when closing cache resources", thrown);
1✔
1061
        }
1062
      }
1063
    }
1✔
1064
    cache.invalidateAll();
1✔
1065
  }
1✔
1066

1067
  @SuppressWarnings("FutureReturnValueIgnored")
1068
  private @Nullable Throwable shutdownExecutor() {
1069
    if (executor instanceof ExecutorService) {
1✔
1070
      @SuppressWarnings("PMD.CloseResource")
1071
      var es = (ExecutorService) executor;
1✔
1072
      es.shutdown();
1✔
1073
    }
1074

1075
    @Var Throwable thrown = null;
1✔
1076
    try {
1077
      CompletableFuture
1✔
1078
          .allOf(inFlight.toArray(CompletableFuture[]::new))
1✔
1079
          .get(10, TimeUnit.SECONDS);
1✔
1080
    } catch (ExecutionException | TimeoutException e) {
1✔
1081
      thrown = e;
1✔
1082
    } catch (InterruptedException e) {
1✔
1083
      Thread.currentThread().interrupt();
1✔
1084
      thrown = e;
1✔
1085
    }
1✔
1086
    inFlight.clear();
1✔
1087

1088
    if (!(executor instanceof ExecutorService)) {
1✔
1089
      thrown = tryClose(executor, thrown);
1✔
1090
    }
1091
    return thrown;
1✔
1092
  }
1093

1094
  /**
1095
   * Attempts to close the resource. If an error occurs and an outermost exception is set, then adds
1096
   * the error to the suppression list.
1097
   *
1098
   * @param o the resource to close if Closeable
1099
   * @param outer the outermost error, or null if unset
1100
   * @return the outermost error, or null if unset and successful
1101
   */
1102
  private static @Nullable Throwable tryClose(@Nullable Object o, @Nullable Throwable outer) {
1103
    if (o instanceof AutoCloseable) {
1✔
1104
      try {
1105
        ((AutoCloseable) o).close();
1✔
1106
      } catch (Throwable t) {
1✔
1107
        if (outer == null) {
1✔
1108
          return t;
1✔
1109
        } else if (outer != t) {
1✔
1110
          outer.addSuppressed(t);
1✔
1111
        }
1112
      }
1✔
1113
    }
1114
    return outer;
1✔
1115
  }
1116

1117
  @Override
1118
  public <T> T unwrap(Class<T> clazz) {
1119
    if (clazz.isInstance(cache)) {
1✔
1120
      return clazz.cast(cache);
1✔
1121
    } else if (clazz.isInstance(this)) {
1✔
1122
      return clazz.cast(this);
1✔
1123
    }
1124
    throw new IllegalArgumentException("Unwrapping to " + clazz
1✔
1125
        + " is not supported by this implementation");
1126
  }
1127

1128
  @Override
1129
  public void registerCacheEntryListener(
1130
      CacheEntryListenerConfiguration<K, V> cacheEntryListenerConfiguration) {
1131
    synchronized (configuration) {
1✔
1132
      requireNotClosed();
1✔
1133
      configuration.addCacheEntryListenerConfiguration(cacheEntryListenerConfiguration);
1✔
1134
      dispatcher.register(cacheEntryListenerConfiguration);
1✔
1135
    }
1✔
1136
  }
1✔
1137

1138
  @Override
1139
  public void deregisterCacheEntryListener(
1140
      CacheEntryListenerConfiguration<K, V> cacheEntryListenerConfiguration) {
1141
    synchronized (configuration) {
1✔
1142
      requireNotClosed();
1✔
1143
      configuration.removeCacheEntryListenerConfiguration(cacheEntryListenerConfiguration);
1✔
1144
      dispatcher.deregister(cacheEntryListenerConfiguration);
1✔
1145
    }
1✔
1146
  }
1✔
1147

1148
  @Override
1149
  public Iterator<Cache.Entry<K, V>> iterator() {
1150
    requireNotClosed();
1✔
1151
    return new EntryIterator();
1✔
1152
  }
1153

1154
  /** Enables or disables the configuration management JMX bean. */
1155
  void enableManagement(boolean enabled) {
1156
    synchronized (configuration) {
1✔
1157
      requireNotClosed();
1✔
1158
      if (enabled) {
1✔
1159
        JmxRegistration.registerMxBean(this, cacheMxBean, MBeanType.CONFIGURATION);
1✔
1160
      } else {
1161
        JmxRegistration.unregisterMxBean(this, MBeanType.CONFIGURATION);
1✔
1162
      }
1163
      configuration.setManagementEnabled(enabled);
1✔
1164
    }
1✔
1165
  }
1✔
1166

1167
  /** Enables or disables the statistics JMX bean. */
1168
  void enableStatistics(boolean enabled) {
1169
    synchronized (configuration) {
1✔
1170
      requireNotClosed();
1✔
1171
      if (enabled) {
1✔
1172
        JmxRegistration.registerMxBean(this, statistics, MBeanType.STATISTICS);
1✔
1173
      } else {
1174
        JmxRegistration.unregisterMxBean(this, MBeanType.STATISTICS);
1✔
1175
      }
1176
      statistics.enable(enabled);
1✔
1177
      configuration.setStatisticsEnabled(enabled);
1✔
1178
    }
1✔
1179
  }
1✔
1180

1181
  /** Performs the action with the cache writer if write-through is enabled. */
1182
  private <T> void publishToCacheWriter(Consumer<T> action, Supplier<T> data) {
1183
    if (!configuration.isWriteThrough()) {
1✔
1184
      return;
1✔
1185
    }
1186
    try {
1187
      action.accept(data.get());
1✔
1188
    } catch (CacheWriterException e) {
1✔
1189
      throw e;
1✔
1190
    } catch (RuntimeException e) {
1✔
1191
      throw new CacheWriterException("Exception in CacheWriter", e);
1✔
1192
    }
1✔
1193
  }
1✔
1194

1195
  /** Checks that the cache is not closed. */
1196
  protected final void requireNotClosed() {
1197
    if (isClosed()) {
1✔
1198
      throw new IllegalStateException();
1✔
1199
    }
1200
  }
1✔
1201

1202
  /**
1203
   * Returns a copy of the value if value-based caching is enabled.
1204
   *
1205
   * @param object the object to be copied
1206
   * @param <T> the type of object being copied
1207
   * @return a copy of the object if storing by value or the same instance if by reference
1208
   */
1209
  protected final <T> T copyOf(T object) {
1210
    try {
1211
      return requireNonNull(
1✔
1212
          copier.copy(requireNonNull(object), requireNonNull(cacheManager.getClassLoader())));
1✔
1213
    } catch (NullPointerException | IllegalStateException | ClassCastException | CacheException e) {
1✔
1214
      throw e;
1✔
1215
    } catch (RuntimeException e) {
1✔
1216
      throw new CacheException(e);
1✔
1217
    }
1218
  }
1219

1220
  /**
1221
   * Returns a deep copy of the map if value-based caching is enabled.
1222
   *
1223
   * @param map the mapping of keys to expirable values
1224
   * @return a deep or shallow copy of the mappings depending on the store by value setting
1225
   */
1226
  @SuppressWarnings("CollectorMutability")
1227
  protected final Map<K, V> copyMap(Map<K, Expirable<V>> map) {
1228
    return map.entrySet().stream().collect(toMap(
1✔
1229
        entry -> copyOf(entry.getKey()),
1✔
1230
        entry -> copyOf(entry.getValue().get())));
1✔
1231
  }
1232

1233
  /** Returns the current time in milliseconds. */
1234
  protected final long currentTimeMillis() {
1235
    return nanosToMillis(ticker.read());
1✔
1236
  }
1237

1238
  /** Returns the nanosecond time in milliseconds. */
1239
  protected static long nanosToMillis(long nanos) {
1240
    return TimeUnit.NANOSECONDS.toMillis(nanos);
1✔
1241
  }
1242

1243
  /** Returns the duration to expire an accessed entry after, or {@code null} if unchanged. */
1244
  protected final @Nullable Duration getAccessExpireTime() {
1245
    try {
1246
      return expiry.getExpiryForAccess();
1✔
1247
    } catch (RuntimeException e) {
1✔
1248
      logger.log(Level.WARNING, "Failed to get the policy's expiration time", e);
1✔
1249
      return null;
1✔
1250
    }
1251
  }
1252

1253
  /**
1254
   * Sets the JCache access expiration time.
1255
   *
1256
   * @param expirable the entry that was operated on
1257
   * @param duration the access duration, or null if unchanged
1258
   * @param currentTimeMillis the current time, or zero if not read yet
1259
   */
1260
  protected final void setAccessExpireTime(Expirable<?> expirable,
1261
      @Nullable Duration duration, @Var long currentTimeMillis) {
1262
    if (duration == null) {
1✔
1263
      return;
1✔
1264
    } else if (duration.isZero()) {
1✔
1265
      expirable.setExpireTimeMillis(0L);
1✔
1266
    } else if (duration.isEternal()) {
1✔
1267
      expirable.setExpireTimeMillis(Long.MAX_VALUE);
1✔
1268
    } else {
1269
      if (currentTimeMillis == 0L) {
1✔
1270
        currentTimeMillis = currentTimeMillis();
1✔
1271
      }
1272
      @Var long expireTimeMillis = duration.getAdjustedTime(currentTimeMillis);
1✔
1273
      expireTimeMillis = ((expireTimeMillis == 0L) || (expireTimeMillis == Long.MAX_VALUE))
1✔
1274
          ? (expireTimeMillis - 1)
1✔
1275
          : expireTimeMillis;
1✔
1276
      expirable.setExpireTimeMillis(expireTimeMillis);
1✔
1277
    }
1278
  }
1✔
1279

1280
  /**
1281
   * Sets the native access expiration time.
1282
   *
1283
   * @param key the entry's key
1284
   * @param duration the access duration, or null if unchanged
1285
   */
1286
  protected final void setVariableExpiration(K key, @Nullable Duration duration) {
1287
    if (duration == null) {
1✔
1288
      return;
1✔
1289
    }
1290
    cache.policy().expireVariably().ifPresent(policy -> {
1✔
1291
      if (duration.isZero()) {
1✔
1292
        policy.setExpiresAfter(key, 0L, TimeUnit.NANOSECONDS);
1✔
1293
      } else if (duration.isEternal()) {
1✔
1294
        policy.setExpiresAfter(key, Long.MAX_VALUE, TimeUnit.NANOSECONDS);
1✔
1295
      } else {
1296
        policy.setExpiresAfter(key, duration.getDurationAmount(), duration.getTimeUnit());
1✔
1297
      }
1298
    });
1✔
1299
  }
1✔
1300

1301
  /**
1302
   * Returns the time when the entry will expire.
1303
   *
1304
   * @param created if the write operation is an insert or an update
1305
   * @return the time when the entry will expire, zero if it should expire immediately,
1306
   *         Long.MIN_VALUE if it should not be changed, or Long.MAX_VALUE if eternal
1307
   */
1308
  protected final long getWriteExpireTimeMillis(boolean created) {
1309
    try {
1310
      Duration duration = created ? expiry.getExpiryForCreation() : expiry.getExpiryForUpdate();
1✔
1311
      if (duration == null) {
1✔
1312
        return created ? Long.MAX_VALUE : Long.MIN_VALUE;
1✔
1313
      } else if (duration.isZero()) {
1✔
1314
        return 0L;
1✔
1315
      } else if (duration.isEternal()) {
1✔
1316
        return Long.MAX_VALUE;
1✔
1317
      }
1318
      long expireTimeMillis = duration.getAdjustedTime(currentTimeMillis());
1✔
1319
      return ((expireTimeMillis == 0L) || (expireTimeMillis == Long.MAX_VALUE))
1✔
1320
          ? (expireTimeMillis - 1)
1✔
1321
          : expireTimeMillis;
1✔
1322
    } catch (RuntimeException e) {
1✔
1323
      logger.log(Level.WARNING, "Failed to get the policy's expiration time", e);
1✔
1324
      return created ? Long.MAX_VALUE : Long.MIN_VALUE;
1✔
1325
    }
1326
  }
1327

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

1334
    Map.@Nullable Entry<K, Expirable<V>> current;
1335
    Map.@Nullable Entry<K, Expirable<V>> cursor;
1336

1337
    @Override
1338
    public boolean hasNext() {
1339
      while ((cursor == null) && delegate.hasNext()) {
1✔
1340
        Map.Entry<K, Expirable<V>> entry = delegate.next();
1✔
1341
        long millis = entry.getValue().isEternal() ? 0L : currentTimeMillis();
1✔
1342
        if (!entry.getValue().hasExpired(millis)) {
1✔
1343
          var duration = getAccessExpireTime();
1✔
1344
          setVariableExpiration(entry.getKey(), duration);
1✔
1345
          setAccessExpireTime(entry.getValue(), duration, millis);
1✔
1346
          cursor = entry;
1✔
1347
        }
1348
      }
1✔
1349
      return (cursor != null);
1✔
1350
    }
1351

1352
    @Override
1353
    public Cache.Entry<K, V> next() {
1354
      if (!hasNext()) {
1✔
1355
        throw new NoSuchElementException();
1✔
1356
      }
1357
      statistics.recordHits(1L);
1✔
1358
      current = requireNonNull(cursor);
1✔
1359
      cursor = null;
1✔
1360
      return new EntryProxy<>(copyOf(current.getKey()), copyOf(current.getValue().get()));
1✔
1361
    }
1362

1363
    @Override
1364
    @SuppressWarnings("CheckReturnValue")
1365
    public void remove() {
1366
      if (current == null) {
1✔
1367
        throw new IllegalStateException();
1✔
1368
      }
1369
      CacheProxy.this.remove(current.getKey());
1✔
1370
      current = null;
1✔
1371
    }
1✔
1372
  }
1373

1374
  protected static final class PutResult<V> {
1✔
1375
    @Nullable V oldValue;
1376
    boolean written;
1377
  }
1378

1379
  protected enum NullCompletionListener implements CompletionListener {
1✔
1380
    INSTANCE;
1✔
1381

1382
    @Override
1383
    public void onCompletion() {}
1✔
1384

1385
    @Override
1386
    public void onException(Exception e) {}
1✔
1387
  }
1388
}
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