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

ben-manes / caffeine / #5449

10 May 2026 06:04PM UTC coverage: 99.903% (+0.001%) from 99.902%
#5449

push

github

ben-manes
Skip cacheProvider.close on a GC'd ClassLoader

CacheManagerImpl held its ClassLoader via WeakReference. If the loader
was GC'd while the manager remained reachable, close() forwarded null
to cacheProvider.close(uri, ...), which fell back to the default
ClassLoader and could close an unrelated peer manager registered under
the default loader for the same URI. Read the WeakReference once and
skip the forward when the loader is gone — the provider's WeakHashMap
entry has already been expunged, so there's nothing to remove there.

issue found by detail.dev

3990 of 3998 branches covered (99.8%)

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

8 existing lines in 2 files now uncovered.

8201 of 8209 relevant lines covered (99.9%)

1.0 hits per line

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

99.43
/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.CacheManager;
49
import javax.cache.configuration.CacheEntryListenerConfiguration;
50
import javax.cache.configuration.Configuration;
51
import javax.cache.expiry.Duration;
52
import javax.cache.expiry.ExpiryPolicy;
53
import javax.cache.integration.CacheLoader;
54
import javax.cache.integration.CacheLoaderException;
55
import javax.cache.integration.CacheWriter;
56
import javax.cache.integration.CacheWriterException;
57
import javax.cache.integration.CompletionListener;
58
import javax.cache.processor.EntryProcessor;
59
import javax.cache.processor.EntryProcessorException;
60
import javax.cache.processor.EntryProcessorResult;
61

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

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

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

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

97
  private final CacheManager cacheManager;
98
  private final CacheWriter<K, V> writer;
99
  private final JCacheMXBean cacheMxBean;
100
  private final ExpiryPolicy expiry;
101
  private final Copier copier;
102
  private final String name;
103

104
  private volatile boolean closed;
105

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

124
    copier = configuration.isStoreByValue()
1✔
125
        ? configuration.getCopierFactory().create()
1✔
126
        : Copier.identity();
1✔
127
    cacheMxBean = new JCacheMXBean(this);
1✔
128
    inFlight = ConcurrentHashMap.newKeySet();
1✔
129
  }
1✔
130

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

153
  @Override
154
  public @Nullable V get(K key) {
155
    requireNotClosed();
1✔
156
    Expirable<V> expirable = cache.getIfPresent(key);
1✔
157
    if (expirable == null) {
1✔
158
      statistics.recordMisses(1L);
1✔
159
      return null;
1✔
160
    }
161

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

188
    setAccessExpireTime(key, expirable, millis);
1✔
189
    V value = copyValue(expirable);
1✔
190
    if (statsEnabled) {
1✔
191
      statistics.recordHits(1L);
1✔
192
      statistics.recordGetTime(ticker.read() - start);
1✔
193
    }
194
    return value;
1✔
195
  }
196

197
  @Override
198
  public Map<K, V> getAll(Set<? extends K> keys) {
199
    requireNotClosed();
1✔
200

201
    boolean statsEnabled = statistics.isEnabled();
1✔
202
    long now = statsEnabled ? ticker.read() : 0L;
1✔
203
    try {
204
      Map<K, Expirable<V>> result =
1✔
205
          getAndFilterExpiredEntries(keys, /* updateAccessTime= */ true);
1✔
206
      if (statsEnabled) {
1✔
207
        statistics.recordGetTime(ticker.read() - now);
1✔
208
      }
209
      return copyMap(result);
1✔
210
    } finally {
211
      dispatcher.awaitSynchronous();
1✔
212
    }
213
  }
214

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

245
    statistics.recordHits(result.size());
1✔
246
    statistics.recordMisses(keys.size() - result.size());
1✔
247
    statistics.recordEvictions(expired[0]);
1✔
248
    return result;
1✔
249
  }
250

251
  @Override
252
  @SuppressWarnings({"CollectionUndefinedEquality", "FutureReturnValueIgnored"})
253
  public void loadAll(Set<? extends K> keys, boolean replaceExistingValues,
254
      @Nullable CompletionListener completionListener) {
255
    requireNotClosed();
1✔
256
    keys.forEach(Objects::requireNonNull);
1✔
257
    CompletionListener listener = (completionListener == null)
1✔
258
        ? NullCompletionListener.INSTANCE
1✔
259
        : completionListener;
1✔
260

261
    if (cacheLoader.isEmpty()) {
1✔
262
      listener.onCompletion();
1✔
263
      return;
1✔
264
    }
265

266
    var future = new CompletableFuture<@Nullable Void>();
1✔
267
    synchronized (configuration) {
1✔
268
      requireNotClosed();
1✔
269
      inFlight.add(future);
1✔
270
    }
1✔
271
    try {
272
      CompletableFuture.runAsync(() -> {
1✔
273
        try {
274
          if (replaceExistingValues) {
1✔
275
            loadAllAndReplaceExisting(keys);
1✔
276
          } else {
277
            loadAllAndKeepExisting(keys);
1✔
278
          }
279
          listener.onCompletion();
1✔
280
        } catch (CacheLoaderException e) {
1✔
281
          listener.onException(e);
1✔
282
        } catch (RuntimeException e) {
1✔
283
          listener.onException(new CacheLoaderException(e));
1✔
284
        } finally {
285
          dispatcher.ignoreSynchronous();
1✔
286
        }
287
      }, executor).whenComplete((r, e) -> {
1✔
288
        inFlight.remove(future);
1✔
289
        future.complete(null);
1✔
290
      });
1✔
291
    } catch (RuntimeException e) {
1✔
292
      inFlight.remove(future);
1✔
293
      future.complete(null);
1✔
294
      listener.onException(new CacheLoaderException(e));
1✔
UNCOV
295
    } catch (Throwable t) {
×
UNCOV
296
      inFlight.remove(future);
×
UNCOV
297
      future.complete(null);
×
UNCOV
298
      throw t;
×
299
    }
1✔
300
  }
1✔
301

302
  /** Performs the bulk load where the existing entries are replaced. */
303
  private void loadAllAndReplaceExisting(Set<? extends K> keys) {
304
    Map<K, V> loaded = cacheLoader.orElseThrow().loadAll(keys);
1✔
305
    for (var entry : loaded.entrySet()) {
1✔
306
      putNoCopyOrAwait(entry.getKey(), entry.getValue(), /* publishToWriter= */ false);
1✔
307
    }
1✔
308
  }
1✔
309

310
  /** Performs the bulk load where the existing entries are retained. */
311
  @SuppressWarnings("ConstantValue")
312
  private void loadAllAndKeepExisting(Set<? extends K> keys) {
313
    List<K> keysToLoad = keys.stream()
1✔
314
        .filter(key -> !cache.asMap().containsKey(key))
1✔
315
        .collect(toUnmodifiableList());
1✔
316
    Map<K, V> result = cacheLoader.orElseThrow().loadAll(keysToLoad);
1✔
317
    for (var entry : result.entrySet()) {
1✔
318
      if ((entry.getKey() != null) && (entry.getValue() != null)) {
1✔
319
        putIfAbsentNoAwait(entry.getKey(), entry.getValue(), /* publishToWriter= */ false);
1✔
320
      }
321
    }
1✔
322
  }
1✔
323

324
  @Override
325
  public void put(K key, V value) {
326
    requireNotClosed();
1✔
327
    boolean statsEnabled = statistics.isEnabled();
1✔
328
    long start = statsEnabled ? ticker.read() : 0L;
1✔
329

330
    var result = putNoCopyOrAwait(key, value, /* publishToWriter= */ true);
1✔
331
    dispatcher.awaitSynchronous();
1✔
332

333
    if (statsEnabled) {
1✔
334
      if (result.written) {
1✔
335
        statistics.recordPuts(1);
1✔
336
      }
337
      statistics.recordPutTime(ticker.read() - start);
1✔
338
    }
339
  }
1✔
340

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

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

350
    if (statsEnabled) {
1✔
351
      if (result.oldValue == null) {
1✔
352
        statistics.recordMisses(1L);
1✔
353
      } else {
354
        statistics.recordHits(1L);
1✔
355
      }
356
      if (result.written) {
1✔
357
        statistics.recordPuts(1);
1✔
358
      }
359
      long duration = ticker.read() - start;
1✔
360
      statistics.recordGetTime(duration);
1✔
361
      statistics.recordPutTime(duration);
1✔
362
    }
363
    return copyOf(result.oldValue);
1✔
364
  }
365

366
  /**
367
   * Associates the specified value with the specified key in the cache.
368
   *
369
   * @param key key with which the specified value is to be associated
370
   * @param value value to be associated with the specified key
371
   * @param publishToWriter if the writer should be notified
372
   * @return the old value
373
   */
374
  @CanIgnoreReturnValue
375
  protected PutResult<V> putNoCopyOrAwait(K key, V value, boolean publishToWriter) {
376
    requireNonNull(key);
1✔
377
    requireNonNull(value);
1✔
378

379
    var result = new PutResult<V>();
1✔
380
    cache.asMap().compute(copyOf(key), (K k, @Var Expirable<V> expirable) -> {
1✔
381
      V newValue = copyOf(value);
1✔
382
      if (publishToWriter) {
1✔
383
        publishToCacheWriter(writer::write, () -> new EntryProxy<>(key, value));
1✔
384
      }
385
      if ((expirable != null) && !expirable.isEternal()
1✔
386
          && expirable.hasExpired(currentTimeMillis())) {
1✔
387
        dispatcher.publishExpired(this, key, expirable.get());
1✔
388
        statistics.recordEvictions(1L);
1✔
389
        expirable = null;
1✔
390
      }
391
      @Var long expireTimeMillis = getWriteExpireTimeMillis((expirable == null));
1✔
392
      if ((expirable != null) && (expireTimeMillis == Long.MIN_VALUE)) {
1✔
393
        expireTimeMillis = expirable.getExpireTimeMillis();
1✔
394
      }
395
      if (expireTimeMillis == 0) {
1✔
396
        // The TCK asserts that expired entry is not counted in the puts stats, despite the javadoc
397
        // saying otherwise. See CacheMBStatisticsBeanTest.testExpiryOnCreation()
398
        result.written = false;
1✔
399

400
        // The TCK asserts that a create is not published, so skipping on update for consistency.
401
        // See CacheExpiryTest.expire_whenCreated_CreatedExpiryPolicy()
402
        result.oldValue = (expirable == null) ? null : expirable.get();
1✔
403

404
        dispatcher.publishExpired(this, key, newValue);
1✔
405
        return null;
1✔
406
      } else if (expirable == null) {
1✔
407
        dispatcher.publishCreated(this, key, newValue);
1✔
408
      } else {
409
        result.oldValue = expirable.get();
1✔
410
        dispatcher.publishUpdated(this, key, expirable.get(), newValue);
1✔
411
      }
412
      result.written = true;
1✔
413
      return new Expirable<>(newValue, expireTimeMillis);
1✔
414
    });
415
    return result;
1✔
416
  }
417

418
  @Override
419
  public void putAll(Map<? extends K, ? extends V> map) {
420
    requireNotClosed();
1✔
421
    for (var entry : map.entrySet()) {
1✔
422
      requireNonNull(entry.getKey());
1✔
423
      requireNonNull(entry.getValue());
1✔
424
    }
1✔
425

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

446
    @Var int puts = 0;
1✔
447
    for (var entry : map.entrySet()) {
1✔
448
      if (!failedKeys.contains(entry.getKey())) {
1✔
449
        var result = putNoCopyOrAwait(entry.getKey(),
1✔
450
            entry.getValue(), /* publishToWriter= */ false);
1✔
451
        if (result.written) {
1✔
452
          puts++;
1✔
453
        }
454
      }
455
    }
1✔
456
    dispatcher.awaitSynchronous();
1✔
457

458
    if (statsEnabled) {
1✔
459
      statistics.recordPuts(puts);
1✔
460
      statistics.recordPutTime(ticker.read() - start);
1✔
461
    }
462
    if (error != null) {
1✔
463
      throw error;
1✔
464
    }
465
  }
1✔
466

467
  @Override
468
  public boolean putIfAbsent(K key, V value) {
469
    requireNotClosed();
1✔
470
    requireNonNull(value);
1✔
471
    boolean statsEnabled = statistics.isEnabled();
1✔
472
    long start = statsEnabled ? ticker.read() : 0L;
1✔
473

474
    boolean added = putIfAbsentNoAwait(key, value, /* publishToWriter= */ true);
1✔
475
    dispatcher.awaitSynchronous();
1✔
476

477
    if (statsEnabled) {
1✔
478
      if (added) {
1✔
479
        statistics.recordPuts(1L);
1✔
480
        statistics.recordMisses(1L);
1✔
481
      } else {
482
        statistics.recordHits(1L);
1✔
483
      }
484
      statistics.recordPutTime(ticker.read() - start);
1✔
485
    }
486
    return added;
1✔
487
  }
488

489
  /**
490
   * Associates the specified value with the specified key in the cache if there is no existing
491
   * mapping.
492
   *
493
   * @param key key with which the specified value is to be associated
494
   * @param value value to be associated with the specified key
495
   * @param publishToWriter if the writer should be notified
496
   * @return if the mapping was successful
497
   */
498
  @CanIgnoreReturnValue
499
  private boolean putIfAbsentNoAwait(K key, V value, boolean publishToWriter) {
500
    boolean[] absent = { false };
1✔
501
    cache.asMap().compute(copyOf(key), (K k, @Var Expirable<V> expirable) -> {
1✔
502
      if ((expirable != null) && !expirable.isEternal()
1✔
503
          && expirable.hasExpired(currentTimeMillis())) {
1✔
504
        dispatcher.publishExpired(this, key, expirable.get());
1✔
505
        statistics.recordEvictions(1L);
1✔
506
        expirable = null;
1✔
507
      }
508
      if (expirable != null) {
1✔
509
        return expirable;
1✔
510
      }
511
      if (publishToWriter) {
1✔
512
        publishToCacheWriter(writer::write, () -> new EntryProxy<>(key, value));
1✔
513
      }
514

515
      absent[0] = true;
1✔
516
      V copy = copyOf(value);
1✔
517
      long expireTimeMillis = getWriteExpireTimeMillis(/* created= */ true);
1✔
518
      if (expireTimeMillis == 0) {
1✔
519
        // The TCK asserts that a create is not published in
520
        // CacheExpiryTest.expire_whenCreated_CreatedExpiryPolicy()
521
        dispatcher.publishExpired(this, key, copy);
1✔
522
        return null;
1✔
523
      } else {
524
        dispatcher.publishCreated(this, key, copy);
1✔
525
        return new Expirable<>(copy, expireTimeMillis);
1✔
526
      }
527
    });
528
    return absent[0];
1✔
529
  }
530

531
  @Override
532
  public boolean remove(K key) {
533
    requireNotClosed();
1✔
534
    requireNonNull(key);
1✔
535
    boolean statsEnabled = statistics.isEnabled();
1✔
536
    long start = statsEnabled ? ticker.read() : 0L;
1✔
537

538
    publishToCacheWriter(writer::delete, () -> key);
1✔
539
    V value = removeNoCopyOrAwait(key);
1✔
540
    dispatcher.awaitSynchronous();
1✔
541

542
    if (statsEnabled) {
1✔
543
      statistics.recordRemoveTime(ticker.read() - start);
1✔
544
    }
545
    if (value != null) {
1✔
546
      statistics.recordRemovals(1L);
1✔
547
      return true;
1✔
548
    }
549
    return false;
1✔
550
  }
551

552
  /**
553
   * Removes the mapping from the cache without store-by-value copying nor waiting for synchronous
554
   * listeners to complete.
555
   *
556
   * @param key key whose mapping is to be removed from the cache
557
   * @return the old value
558
   */
559
  private @Nullable V removeNoCopyOrAwait(K key) {
560
    @SuppressWarnings("unchecked")
561
    var removed = (V[]) new Object[1];
1✔
562
    cache.asMap().computeIfPresent(key, (k, expirable) -> {
1✔
563
      if (!expirable.isEternal() && expirable.hasExpired(currentTimeMillis())) {
1✔
564
        dispatcher.publishExpired(this, key, expirable.get());
1✔
565
        statistics.recordEvictions(1L);
1✔
566
      } else {
567
        dispatcher.publishRemoved(this, key, expirable.get());
1✔
568
        removed[0] = expirable.get();
1✔
569
      }
570
      return null;
1✔
571
    });
572
    return removed[0];
1✔
573
  }
574

575
  @Override
576
  @CanIgnoreReturnValue
577
  public boolean remove(K key, V oldValue) {
578
    requireNotClosed();
1✔
579
    requireNonNull(key);
1✔
580
    requireNonNull(oldValue);
1✔
581

582
    boolean statsEnabled = statistics.isEnabled();
1✔
583
    long start = statsEnabled ? ticker.read() : 0L;
1✔
584
    boolean[] found = { false };
1✔
585

586
    boolean[] removed = { false };
1✔
587
    cache.asMap().computeIfPresent(key, (k, expirable) -> {
1✔
588
      long millis = expirable.isEternal()
1✔
589
          ? 0L
1✔
590
          : nanosToMillis((start == 0L) ? ticker.read() : start);
1✔
591
      if (expirable.hasExpired(millis)) {
1✔
592
        dispatcher.publishExpired(this, key, expirable.get());
1✔
593
        statistics.recordEvictions(1L);
1✔
594
        return null;
1✔
595
      }
596

597
      found[0] = true;
1✔
598
      if (oldValue.equals(expirable.get())) {
1✔
599
        publishToCacheWriter(writer::delete, () -> key);
1✔
600
        dispatcher.publishRemoved(this, key, expirable.get());
1✔
601
        removed[0] = true;
1✔
602
        return null;
1✔
603
      }
604
      setAccessExpireTime(key, expirable, millis);
1✔
605
      return expirable;
1✔
606
    });
607
    dispatcher.awaitSynchronous();
1✔
608
    if (statsEnabled) {
1✔
609
      if (removed[0]) {
1✔
610
        statistics.recordRemovals(1L);
1✔
611
        statistics.recordHits(1L);
1✔
612
      } else if (found[0]) {
1✔
613
        statistics.recordHits(1L);
1✔
614
      } else {
615
        statistics.recordMisses(1L);
1✔
616
      }
617
      statistics.recordRemoveTime(ticker.read() - start);
1✔
618
    }
619
    return removed[0];
1✔
620
  }
621

622
  @Override
623
  public @Nullable V getAndRemove(K key) {
624
    requireNotClosed();
1✔
625
    requireNonNull(key);
1✔
626
    boolean statsEnabled = statistics.isEnabled();
1✔
627
    long start = statsEnabled ? ticker.read() : 0L;
1✔
628

629
    publishToCacheWriter(writer::delete, () -> key);
1✔
630
    V value = removeNoCopyOrAwait(key);
1✔
631
    dispatcher.awaitSynchronous();
1✔
632
    V copy = copyOf(value);
1✔
633

634
    if (statsEnabled) {
1✔
635
      if (value == null) {
1✔
636
        statistics.recordMisses(1L);
1✔
637
      } else {
638
        statistics.recordHits(1L);
1✔
639
        statistics.recordRemovals(1L);
1✔
640
      }
641
      long duration = ticker.read() - start;
1✔
642
      statistics.recordRemoveTime(duration);
1✔
643
      statistics.recordGetTime(duration);
1✔
644
    }
645
    return copy;
1✔
646
  }
647

648
  @Override
649
  public boolean replace(K key, V oldValue, V newValue) {
650
    requireNotClosed();
1✔
651
    requireNonNull(oldValue);
1✔
652
    requireNonNull(newValue);
1✔
653

654
    boolean statsEnabled = statistics.isEnabled();
1✔
655
    long start = statsEnabled ? ticker.read() : 0L;
1✔
656

657
    boolean[] found = { false };
1✔
658
    boolean[] replaced = { false };
1✔
659
    cache.asMap().computeIfPresent(key, (k, expirable) -> {
1✔
660
      long millis = expirable.isEternal()
1✔
661
          ? 0L
1✔
662
          : nanosToMillis((start == 0L) ? ticker.read() : start);
1✔
663
      if (expirable.hasExpired(millis)) {
1✔
664
        dispatcher.publishExpired(this, key, expirable.get());
1✔
665
        statistics.recordEvictions(1L);
1✔
666
        return null;
1✔
667
      }
668

669
      found[0] = true;
1✔
670
      Expirable<V> result;
671
      if (oldValue.equals(expirable.get())) {
1✔
672
        V copy = copyOf(newValue);
1✔
673
        publishToCacheWriter(writer::write, () -> new EntryProxy<>(key, newValue));
1✔
674
        dispatcher.publishUpdated(this, key, expirable.get(), copy);
1✔
675
        @Var long expireTimeMillis = getWriteExpireTimeMillis(/* created= */ false);
1✔
676
        if (expireTimeMillis == Long.MIN_VALUE) {
1✔
677
          expireTimeMillis = expirable.getExpireTimeMillis();
1✔
678
        }
679
        result = new Expirable<>(copy, expireTimeMillis);
1✔
680
        replaced[0] = true;
1✔
681
      } else {
1✔
682
        result = expirable;
1✔
683
        setAccessExpireTime(key, expirable, millis);
1✔
684
      }
685
      return result;
1✔
686
    });
687
    dispatcher.awaitSynchronous();
1✔
688

689
    if (statsEnabled) {
1✔
690
      statistics.recordPuts(replaced[0] ? 1L : 0L);
1✔
691
      statistics.recordMisses(found[0] ? 0L : 1L);
1✔
692
      statistics.recordHits(found[0] ? 1L : 0L);
1✔
693
      long duration = ticker.read() - start;
1✔
694
      statistics.recordGetTime(duration);
1✔
695
      statistics.recordPutTime(duration);
1✔
696
    }
697

698
    return replaced[0];
1✔
699
  }
700

701
  @Override
702
  public boolean replace(K key, V value) {
703
    requireNotClosed();
1✔
704
    boolean statsEnabled = statistics.isEnabled();
1✔
705
    long start = statsEnabled ? ticker.read() : 0L;
1✔
706

707
    @Nullable V oldValue = replaceNoCopyOrAwait(key, value);
1✔
708
    dispatcher.awaitSynchronous();
1✔
709
    if (oldValue == null) {
1✔
710
      statistics.recordMisses(1L);
1✔
711
      return false;
1✔
712
    }
713

714
    if (statsEnabled) {
1✔
715
      statistics.recordHits(1L);
1✔
716
      statistics.recordPuts(1L);
1✔
717
      statistics.recordPutTime(ticker.read() - start);
1✔
718
    }
719
    return true;
1✔
720
  }
721

722
  @Override
723
  public @Nullable V getAndReplace(K key, V value) {
724
    requireNotClosed();
1✔
725
    boolean statsEnabled = statistics.isEnabled();
1✔
726
    long start = statsEnabled ? ticker.read() : 0L;
1✔
727

728
    V oldValue = replaceNoCopyOrAwait(key, value);
1✔
729
    dispatcher.awaitSynchronous();
1✔
730
    V copy = copyOf(oldValue);
1✔
731

732
    if (statsEnabled) {
1✔
733
      if (oldValue == null) {
1✔
734
        statistics.recordMisses(1L);
1✔
735
      } else {
736
        statistics.recordHits(1L);
1✔
737
        statistics.recordPuts(1L);
1✔
738
      }
739
      long duration = ticker.read() - start;
1✔
740
      statistics.recordGetTime(duration);
1✔
741
      statistics.recordPutTime(duration);
1✔
742
    }
743
    return copy;
1✔
744
  }
745

746
  /**
747
   * Replaces the entry for the specified key only if it is currently mapped to some value. The
748
   * entry is not store-by-value copied nor does the method wait for synchronous listeners to
749
   * complete.
750
   *
751
   * @param key key with which the specified value is associated
752
   * @param value value to be associated with the specified key
753
   * @return the old value
754
   */
755
  private @Nullable V replaceNoCopyOrAwait(K key, V value) {
756
    requireNonNull(value);
1✔
757
    V copy = copyOf(value);
1✔
758
    @SuppressWarnings("unchecked")
759
    var replaced = (V[]) new Object[1];
1✔
760
    cache.asMap().computeIfPresent(key, (k, expirable) -> {
1✔
761
      if (!expirable.isEternal() && expirable.hasExpired(currentTimeMillis())) {
1✔
762
        dispatcher.publishExpired(this, key, expirable.get());
1✔
763
        statistics.recordEvictions(1L);
1✔
764
        return null;
1✔
765
      }
766

767
      publishToCacheWriter(writer::write, () -> new EntryProxy<>(key, value));
1✔
768
      @Var long expireTimeMillis = getWriteExpireTimeMillis(/* created= */ false);
1✔
769
      if (expireTimeMillis == Long.MIN_VALUE) {
1✔
770
        expireTimeMillis = expirable.getExpireTimeMillis();
1✔
771
      }
772
      dispatcher.publishUpdated(this, key, expirable.get(), copy);
1✔
773
      replaced[0] = expirable.get();
1✔
774
      return new Expirable<>(copy, expireTimeMillis);
1✔
775
    });
776
    return replaced[0];
1✔
777
  }
778

779
  @Override
780
  public void removeAll(Set<? extends K> keys) {
781
    requireNotClosed();
1✔
782
    keys.forEach(Objects::requireNonNull);
1✔
783

784
    @Var CacheWriterException error = null;
1✔
785
    @Var Set<? extends K> failedKeys = Set.of();
1✔
786
    boolean statsEnabled = statistics.isEnabled();
1✔
787
    long start = statsEnabled ? ticker.read() : 0L;
1✔
788
    if (configuration.isWriteThrough() && !keys.isEmpty()) {
1✔
789
      var keysToWrite = new LinkedHashSet<>(keys);
1✔
790
      try {
791
        writer.deleteAll(keysToWrite);
1✔
792
      } catch (CacheWriterException e) {
1✔
793
        error = e;
1✔
794
        failedKeys = keysToWrite;
1✔
795
      } catch (RuntimeException e) {
1✔
796
        error = new CacheWriterException("Exception in CacheWriter", e);
1✔
797
        failedKeys = keysToWrite;
1✔
798
      }
1✔
799
    }
800

801
    @Var int removed = 0;
1✔
802
    for (var key : keys) {
1✔
803
      if (!failedKeys.contains(key) && (removeNoCopyOrAwait(key) != null)) {
1✔
804
        removed++;
1✔
805
      }
806
    }
1✔
807
    dispatcher.awaitSynchronous();
1✔
808

809
    if (statsEnabled) {
1✔
810
      statistics.recordRemovals(removed);
1✔
811
      statistics.recordRemoveTime(ticker.read() - start);
1✔
812
    }
813
    if (error != null) {
1✔
814
      throw error;
1✔
815
    }
816
  }
1✔
817

818
  @Override
819
  public void removeAll() {
820
    removeAll(cache.asMap().keySet());
1✔
821
  }
1✔
822

823
  @Override
824
  public void clear() {
825
    requireNotClosed();
1✔
826
    cache.invalidateAll();
1✔
827
  }
1✔
828

829
  @Override
830
  public <C extends Configuration<K, V>> C getConfiguration(Class<C> clazz) {
831
    if (clazz.isInstance(configuration)) {
1✔
832
      synchronized (configuration) {
1✔
833
        return clazz.cast(configuration.immutableCopy());
1✔
834
      }
835
    }
836
    throw new IllegalArgumentException("The configuration class " + clazz
1✔
837
        + " is not supported by this implementation");
838
  }
839

840
  @Override
841
  public <T> @Nullable T invoke(K key,
842
      EntryProcessor<K, V, T> entryProcessor, Object... arguments) {
843
    requireNonNull(entryProcessor);
1✔
844
    requireNonNull(arguments);
1✔
845
    requireNotClosed();
1✔
846

847
    Object[] result = new Object[1];
1✔
848
    BiFunction<K, Expirable<V>, Expirable<V>> remappingFunction = (k, expirable) -> {
1✔
849
      V value;
850
      @Var long millis = 0L;
1✔
851
      if ((expirable == null)
1✔
852
          || (!expirable.isEternal() && expirable.hasExpired(millis = currentTimeMillis()))) {
1✔
853
        statistics.recordMisses(1L);
1✔
854
        value = null;
1✔
855
      } else {
856
        value = copyValue(expirable);
1✔
857
        statistics.recordHits(1L);
1✔
858
      }
859
      var entry = new EntryProcessorEntry<>(key, value,
1✔
860
          configuration.isReadThrough() ? cacheLoader : Optional.empty());
1✔
861
      try {
862
        result[0] = entryProcessor.process(entry, arguments);
1✔
863
        return postProcess(expirable, entry, millis);
1✔
864
      } catch (EntryProcessorException e) {
1✔
865
        throw e;
1✔
866
      } catch (RuntimeException e) {
1✔
867
        throw new EntryProcessorException(e);
1✔
868
      }
869
    };
870
    try {
871
      cache.asMap().compute(copyOf(key), remappingFunction);
1✔
872
      dispatcher.awaitSynchronous();
1✔
873
    } catch (Throwable t) {
1✔
874
      dispatcher.ignoreSynchronous();
1✔
875
      throw t;
1✔
876
    }
1✔
877

878
    @SuppressWarnings("unchecked")
879
    var castedResult = (T) result[0];
1✔
880
    return castedResult;
1✔
881
  }
882

883
  /** Returns the updated expirable value after performing the post-processing actions. */
884
  @SuppressWarnings("fallthrough")
885
  @Nullable Expirable<V> postProcess(@Nullable Expirable<V> expirable,
886
      EntryProcessorEntry<K, V> entry, @Var long currentTimeMillis) {
887
    switch (entry.getAction()) {
1✔
888
      case NONE:
889
        if (expirable == null) {
1✔
890
          return null;
1✔
891
        } else if (expirable.isEternal()) {
1✔
892
          return expirable;
1✔
893
        }
894
        if (currentTimeMillis == 0) {
1✔
895
          currentTimeMillis = currentTimeMillis();
1✔
896
        }
897
        if (expirable.hasExpired(currentTimeMillis)) {
1✔
898
          dispatcher.publishExpired(this, entry.getKey(), expirable.get());
1✔
899
          statistics.recordEvictions(1);
1✔
900
          return null;
1✔
901
        }
902
        return expirable;
1✔
903
      case READ: {
904
        setAccessExpireTime(entry.getKey(), requireNonNull(expirable), 0L);
1✔
905
        return expirable;
1✔
906
      }
907
      case CREATED:
908
        this.publishToCacheWriter(writer::write, () -> entry);
1✔
909
        // fallthrough
910
      case LOADED: {
911
        statistics.recordPuts(1L);
1✔
912
        V value = copyOf(requireNonNull(entry.getValue()));
1✔
913
        dispatcher.publishCreated(this, entry.getKey(), value);
1✔
914
        return new Expirable<>(value, getWriteExpireTimeMillis(/* created= */ true));
1✔
915
      }
916
      case UPDATED: {
917
        statistics.recordPuts(1L);
1✔
918
        publishToCacheWriter(writer::write, () -> entry);
1✔
919
        requireNonNull(expirable, "Expected a previous value but was null");
1✔
920
        V value = copyOf(requireNonNull(entry.getValue(), "Expected a new value but was null"));
1✔
921
        dispatcher.publishUpdated(this, entry.getKey(), expirable.get(), value);
1✔
922
        @Var long expireTimeMillis = getWriteExpireTimeMillis(/* created= */ false);
1✔
923
        if (expireTimeMillis == Long.MIN_VALUE) {
1✔
924
          expireTimeMillis = expirable.getExpireTimeMillis();
1✔
925
        }
926
        return new Expirable<>(value, expireTimeMillis);
1✔
927
      }
928
      case DELETED:
929
        statistics.recordRemovals(1L);
1✔
930
        publishToCacheWriter(writer::delete, entry::getKey);
1✔
931
        if (expirable != null) {
1✔
932
          dispatcher.publishRemoved(this, entry.getKey(), expirable.get());
1✔
933
        }
934
        return null;
1✔
935
    }
936
    throw new IllegalStateException("Unknown state: " + entry.getAction());
1✔
937
  }
938

939
  @Override
940
  public <T> Map<K, EntryProcessorResult<T>> invokeAll(Set<? extends K> keys,
941
      EntryProcessor<K, V, T> entryProcessor, Object... arguments) {
942
    requireNotClosed();
1✔
943
    requireNonNull(keys);
1✔
944
    requireNonNull(arguments);
1✔
945
    requireNonNull(entryProcessor);
1✔
946
    keys.forEach(Objects::requireNonNull);
1✔
947

948
    var results = new HashMap<K, EntryProcessorResult<T>>(keys.size(), 1.0f);
1✔
949
    for (K key : keys) {
1✔
950
      try {
951
        T result = invoke(key, entryProcessor, arguments);
1✔
952
        if (result != null) {
1✔
953
          results.put(key, () -> result);
1✔
954
        }
955
      } catch (EntryProcessorException e) {
1✔
956
        results.put(key, () -> { throw e; });
1✔
957
      }
1✔
958
    }
1✔
959
    return results;
1✔
960
  }
961

962
  @Override
963
  public String getName() {
964
    return name;
1✔
965
  }
966

967
  @Override
968
  public CacheManager getCacheManager() {
969
    return cacheManager;
1✔
970
  }
971

972
  @Override
973
  public boolean isClosed() {
974
    return closed;
1✔
975
  }
976

977
  @Override
978
  public void close() {
979
    if (isClosed()) {
1✔
980
      return;
1✔
981
    }
982
    synchronized (configuration) {
1✔
983
      if (!isClosed()) {
1✔
984
        enableManagement(false);
1✔
985
        enableStatistics(false);
1✔
986
        closed = true;
1✔
987
        try {
988
          cacheManager.destroyCache(name);
1✔
989
        } catch (IllegalStateException ignored) { /* manager already closed */ }
1✔
990

991
        @Var var thrown = shutdownExecutor();
1✔
992
        thrown = tryClose(expiry, thrown);
1✔
993
        thrown = tryClose(writer, thrown);
1✔
994
        thrown = tryClose(executor, thrown);
1✔
995
        thrown = tryClose(cacheLoader.orElse(null), thrown);
1✔
996
        for (Registration<K, V> registration : dispatcher.registrations()) {
1✔
997
          thrown = tryClose(registration.getCacheEntryListener(), thrown);
1✔
998
        }
1✔
999
        if (thrown != null) {
1✔
1000
          logger.log(Level.WARNING, "Failure when closing cache resources", thrown);
1✔
1001
        }
1002
      }
1003
    }
1✔
1004
    cache.invalidateAll();
1✔
1005
  }
1✔
1006

1007
  @SuppressWarnings("FutureReturnValueIgnored")
1008
  private @Nullable Throwable shutdownExecutor() {
1009
    if (executor instanceof ExecutorService) {
1✔
1010
      @SuppressWarnings("PMD.CloseResource")
1011
      var es = (ExecutorService) executor;
1✔
1012
      es.shutdown();
1✔
1013
    }
1014

1015
    @Var Throwable thrown = null;
1✔
1016
    try {
1017
      CompletableFuture
1✔
1018
          .allOf(inFlight.toArray(CompletableFuture[]::new))
1✔
1019
          .get(10, TimeUnit.SECONDS);
1✔
1020
    } catch (ExecutionException | TimeoutException e) {
1✔
1021
      thrown = e;
1✔
1022
    } catch (InterruptedException e) {
1✔
1023
      Thread.currentThread().interrupt();
1✔
1024
      thrown = e;
1✔
1025
    }
1✔
1026
    inFlight.clear();
1✔
1027
    return thrown;
1✔
1028
  }
1029

1030
  /**
1031
   * Attempts to close the resource. If an error occurs and an outermost exception is set, then adds
1032
   * the error to the suppression list.
1033
   *
1034
   * @param o the resource to close if Closeable
1035
   * @param outer the outermost error, or null if unset
1036
   * @return the outermost error, or null if unset and successful
1037
   */
1038
  private static @Nullable Throwable tryClose(@Nullable Object o, @Nullable Throwable outer) {
1039
    if (o instanceof AutoCloseable) {
1✔
1040
      try {
1041
        ((AutoCloseable) o).close();
1✔
1042
      } catch (Throwable t) {
1✔
1043
        if (outer == null) {
1✔
1044
          return t;
1✔
1045
        }
1046
        outer.addSuppressed(t);
1✔
1047
      }
1✔
1048
    }
1049
    return outer;
1✔
1050
  }
1051

1052
  @Override
1053
  public <T> T unwrap(Class<T> clazz) {
1054
    if (clazz.isInstance(cache)) {
1✔
1055
      return clazz.cast(cache);
1✔
1056
    } else if (clazz.isInstance(this)) {
1✔
1057
      return clazz.cast(this);
1✔
1058
    }
1059
    throw new IllegalArgumentException("Unwrapping to " + clazz
1✔
1060
        + " is not supported by this implementation");
1061
  }
1062

1063
  @Override
1064
  public void registerCacheEntryListener(
1065
      CacheEntryListenerConfiguration<K, V> cacheEntryListenerConfiguration) {
1066
    requireNotClosed();
1✔
1067
    synchronized (configuration) {
1✔
1068
      configuration.addCacheEntryListenerConfiguration(cacheEntryListenerConfiguration);
1✔
1069
      dispatcher.register(cacheEntryListenerConfiguration);
1✔
1070
    }
1✔
1071
  }
1✔
1072

1073
  @Override
1074
  public void deregisterCacheEntryListener(
1075
      CacheEntryListenerConfiguration<K, V> cacheEntryListenerConfiguration) {
1076
    requireNotClosed();
1✔
1077
    synchronized (configuration) {
1✔
1078
      configuration.removeCacheEntryListenerConfiguration(cacheEntryListenerConfiguration);
1✔
1079
      dispatcher.deregister(cacheEntryListenerConfiguration);
1✔
1080
    }
1✔
1081
  }
1✔
1082

1083
  @Override
1084
  public Iterator<Cache.Entry<K, V>> iterator() {
1085
    requireNotClosed();
1✔
1086
    return new EntryIterator();
1✔
1087
  }
1088

1089
  /** Enables or disables the configuration management JMX bean. */
1090
  void enableManagement(boolean enabled) {
1091
    requireNotClosed();
1✔
1092

1093
    synchronized (configuration) {
1✔
1094
      if (enabled) {
1✔
1095
        JmxRegistration.registerMxBean(this, cacheMxBean, MBeanType.CONFIGURATION);
1✔
1096
      } else {
1097
        JmxRegistration.unregisterMxBean(this, MBeanType.CONFIGURATION);
1✔
1098
      }
1099
      configuration.setManagementEnabled(enabled);
1✔
1100
    }
1✔
1101
  }
1✔
1102

1103
  /** Enables or disables the statistics JMX bean. */
1104
  void enableStatistics(boolean enabled) {
1105
    requireNotClosed();
1✔
1106

1107
    synchronized (configuration) {
1✔
1108
      if (enabled) {
1✔
1109
        JmxRegistration.registerMxBean(this, statistics, MBeanType.STATISTICS);
1✔
1110
      } else {
1111
        JmxRegistration.unregisterMxBean(this, MBeanType.STATISTICS);
1✔
1112
      }
1113
      statistics.enable(enabled);
1✔
1114
      configuration.setStatisticsEnabled(enabled);
1✔
1115
    }
1✔
1116
  }
1✔
1117

1118
  /** Performs the action with the cache writer if write-through is enabled. */
1119
  private <T> void publishToCacheWriter(Consumer<T> action, Supplier<T> data) {
1120
    if (!configuration.isWriteThrough()) {
1✔
1121
      return;
1✔
1122
    }
1123
    try {
1124
      action.accept(data.get());
1✔
1125
    } catch (CacheWriterException e) {
1✔
1126
      throw e;
1✔
1127
    } catch (RuntimeException e) {
1✔
1128
      throw new CacheWriterException("Exception in CacheWriter", e);
1✔
1129
    }
1✔
1130
  }
1✔
1131

1132
  /** Checks that the cache is not closed. */
1133
  protected final void requireNotClosed() {
1134
    if (isClosed()) {
1✔
1135
      throw new IllegalStateException();
1✔
1136
    }
1137
  }
1✔
1138

1139
  /**
1140
   * Returns a copy of the value if value-based caching is enabled.
1141
   *
1142
   * @param object the object to be copied
1143
   * @param <T> the type of object being copied
1144
   * @return a copy of the object if storing by value or the same instance if by reference
1145
   */
1146
  @SuppressWarnings({"DataFlowIssue", "NullAway"})
1147
  protected final <T> T copyOf(@Nullable T object) {
1148
    if (object == null) {
1✔
1149
      return null;
1✔
1150
    }
1151
    T copy = copier.copy(object, cacheManager.getClassLoader());
1✔
1152
    return requireNonNull(copy);
1✔
1153
  }
1154

1155
  /**
1156
   * Returns a copy of the value if value-based caching is enabled.
1157
   *
1158
   * @param expirable the expirable value to be copied
1159
   * @return a copy of the value if storing by value or the same instance if by reference
1160
   */
1161
  @SuppressWarnings({"DataFlowIssue", "NullAway"})
1162
  protected final V copyValue(@Nullable Expirable<V> expirable) {
1163
    if (expirable == null) {
1✔
1164
      return null;
1✔
1165
    }
1166
    V copy = copier.copy(expirable.get(), cacheManager.getClassLoader());
1✔
1167
    return requireNonNull(copy);
1✔
1168
  }
1169

1170
  /**
1171
   * Returns a deep copy of the map if value-based caching is enabled.
1172
   *
1173
   * @param map the mapping of keys to expirable values
1174
   * @return a deep or shallow copy of the mappings depending on the store by value setting
1175
   */
1176
  @SuppressWarnings("CollectorMutability")
1177
  protected final Map<K, V> copyMap(Map<K, Expirable<V>> map) {
1178
    ClassLoader classLoader = cacheManager.getClassLoader();
1✔
1179
    return map.entrySet().stream().collect(toMap(
1✔
1180
        entry -> copier.copy(entry.getKey(), classLoader),
1✔
1181
        entry -> copier.copy(entry.getValue().get(), classLoader)));
1✔
1182
  }
1183

1184
  /** Returns the current time in milliseconds. */
1185
  protected final long currentTimeMillis() {
1186
    return nanosToMillis(ticker.read());
1✔
1187
  }
1188

1189
  /** Returns the nanosecond time in milliseconds. */
1190
  protected static long nanosToMillis(long nanos) {
1191
    return TimeUnit.NANOSECONDS.toMillis(nanos);
1✔
1192
  }
1193

1194
  /**
1195
   * Sets the access expiration time.
1196
   *
1197
   * @param key the entry's key
1198
   * @param expirable the entry that was operated on
1199
   * @param currentTimeMillis the current time, or 0 if not read yet
1200
   */
1201
  protected final void setAccessExpireTime(K key,
1202
      Expirable<?> expirable, @Var long currentTimeMillis) {
1203
    try {
1204
      Duration duration = expiry.getExpiryForAccess();
1✔
1205
      if (duration == null) {
1✔
1206
        return;
1✔
1207
      } else if (duration.isZero()) {
1✔
1208
        expirable.setExpireTimeMillis(0L);
1✔
1209
        cache.policy().expireVariably().ifPresent(policy ->
1✔
1210
            policy.setExpiresAfter(key, 0L, TimeUnit.NANOSECONDS));
1✔
1211
      } else if (duration.isEternal()) {
1✔
1212
        expirable.setExpireTimeMillis(Long.MAX_VALUE);
1✔
1213
        cache.policy().expireVariably().ifPresent(policy ->
1✔
1214
            policy.setExpiresAfter(key, Long.MAX_VALUE, TimeUnit.NANOSECONDS));
1✔
1215
      } else {
1216
        if (currentTimeMillis == 0L) {
1✔
1217
          currentTimeMillis = currentTimeMillis();
1✔
1218
        }
1219
        @Var long expireTimeMillis = duration.getAdjustedTime(currentTimeMillis);
1✔
1220
        expireTimeMillis = ((expireTimeMillis == 0L) || (expireTimeMillis == Long.MAX_VALUE))
1✔
1221
            ? (expireTimeMillis - 1)
1✔
1222
            : expireTimeMillis;
1✔
1223
        expirable.setExpireTimeMillis(expireTimeMillis);
1✔
1224
        cache.policy().expireVariably().ifPresent(policy ->
1✔
1225
            policy.setExpiresAfter(key, duration.getDurationAmount(), duration.getTimeUnit()));
1✔
1226
      }
1227
    } catch (RuntimeException e) {
1✔
1228
      logger.log(Level.WARNING, "Failed to set the entry's expiration time", e);
1✔
1229
    }
1✔
1230
  }
1✔
1231

1232
  /**
1233
   * Returns the time when the entry will expire.
1234
   *
1235
   * @param created if the write operation is an insert or an update
1236
   * @return the time when the entry will expire, zero if it should expire immediately,
1237
   *         Long.MIN_VALUE if it should not be changed, or Long.MAX_VALUE if eternal
1238
   */
1239
  protected final long getWriteExpireTimeMillis(boolean created) {
1240
    try {
1241
      Duration duration = created ? expiry.getExpiryForCreation() : expiry.getExpiryForUpdate();
1✔
1242
      if (duration == null) {
1✔
1243
        return Long.MIN_VALUE;
1✔
1244
      } else if (duration.isZero()) {
1✔
1245
        return 0L;
1✔
1246
      } else if (duration.isEternal()) {
1✔
1247
        return Long.MAX_VALUE;
1✔
1248
      }
1249
      long expireTimeMillis = duration.getAdjustedTime(currentTimeMillis());
1✔
1250
      return ((expireTimeMillis == 0L) || (expireTimeMillis == Long.MAX_VALUE))
1✔
1251
          ? (expireTimeMillis - 1)
1✔
1252
          : expireTimeMillis;
1✔
1253
    } catch (RuntimeException e) {
1✔
1254
      logger.log(Level.WARNING, "Failed to get the policy's expiration time", e);
1✔
1255
      return Long.MIN_VALUE;
1✔
1256
    }
1257
  }
1258

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

1265
    Map.@Nullable Entry<K, Expirable<V>> current;
1266
    Map.@Nullable Entry<K, Expirable<V>> cursor;
1267

1268
    @Override
1269
    public boolean hasNext() {
1270
      while ((cursor == null) && delegate.hasNext()) {
1✔
1271
        Map.Entry<K, Expirable<V>> entry = delegate.next();
1✔
1272
        long millis = entry.getValue().isEternal() ? 0L : currentTimeMillis();
1✔
1273
        if (!entry.getValue().hasExpired(millis)) {
1✔
1274
          setAccessExpireTime(entry.getKey(), entry.getValue(), millis);
1✔
1275
          cursor = entry;
1✔
1276
        }
1277
      }
1✔
1278
      return (cursor != null);
1✔
1279
    }
1280

1281
    @Override
1282
    public Cache.Entry<K, V> next() {
1283
      if (!hasNext()) {
1✔
1284
        throw new NoSuchElementException();
1✔
1285
      }
1286
      current = requireNonNull(cursor);
1✔
1287
      cursor = null;
1✔
1288
      return new EntryProxy<>(copyOf(current.getKey()), copyValue(current.getValue()));
1✔
1289
    }
1290

1291
    @Override
1292
    public void remove() {
1293
      if (current == null) {
1✔
1294
        throw new IllegalStateException();
1✔
1295
      }
1296
      CacheProxy.this.remove(current.getKey(), current.getValue().get());
1✔
1297
      current = null;
1✔
1298
    }
1✔
1299
  }
1300

1301
  protected static final class PutResult<V> {
1✔
1302
    @Nullable V oldValue;
1303
    boolean written;
1304
  }
1305

1306
  protected enum NullCompletionListener implements CompletionListener {
1✔
1307
    INSTANCE;
1✔
1308

1309
    @Override
1310
    public void onCompletion() {}
1✔
1311

1312
    @Override
1313
    public void onException(Exception e) {}
1✔
1314
  }
1315
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc