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

ben-manes / caffeine / #5312

22 Feb 2026 05:24AM UTC coverage: 99.962% (-0.04%) from 100.0%
#5312

push

github

ben-manes
add qlty static analyzer

3831 of 3840 branches covered (99.77%)

2 of 4 new or added lines in 1 file covered. (50.0%)

1 existing line in 1 file now uncovered.

7874 of 7877 relevant lines covered (99.96%)

1.0 hits per line

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

99.55
/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 Optional<CacheLoader<K, V>> cacheLoader;
90
  protected final Set<CompletableFuture<?>> inFlight;
91
  protected final JCacheStatisticsMXBean statistics;
92
  protected final EventDispatcher<K, V> dispatcher;
93
  protected final Executor executor;
94
  protected final Ticker ticker;
95

96
  private final CaffeineConfiguration<K, V> configuration;
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

204
    Map<K, Expirable<V>> result = getAndFilterExpiredEntries(keys, /* updateAccessTime= */ true);
1✔
205

206
    if (statsEnabled) {
1✔
207
      statistics.recordGetTime(ticker.read() - now);
1✔
208
    }
209
    return copyMap(result);
1✔
210
  }
211

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

242
    statistics.recordHits(result.size());
1✔
243
    statistics.recordMisses(keys.size() - result.size());
1✔
244
    statistics.recordEvictions(expired[0]);
1✔
245
    return result;
1✔
246
  }
247

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

258
    if (cacheLoader.isEmpty()) {
1✔
259
      listener.onCompletion();
1✔
260
      return;
1✔
261
    }
262

263
    var future = CompletableFuture.runAsync(() -> {
1✔
264
      try {
265
        if (replaceExistingValues) {
1✔
266
          loadAllAndReplaceExisting(keys);
1✔
267
        } else {
268
          loadAllAndKeepExisting(keys);
1✔
269
        }
270
        listener.onCompletion();
1✔
271
      } catch (CacheLoaderException e) {
1✔
272
        listener.onException(e);
1✔
273
      } catch (RuntimeException e) {
1✔
274
        listener.onException(new CacheLoaderException(e));
1✔
275
      } finally {
276
        dispatcher.ignoreSynchronous();
1✔
277
      }
278
    }, executor);
1✔
279

280
    inFlight.add(future);
1✔
281
    future.whenComplete((r, e) -> inFlight.remove(future));
1✔
282
  }
1✔
283

284
  /** Performs the bulk load where the existing entries are replaced. */
285
  private void loadAllAndReplaceExisting(Set<? extends K> keys) {
286
    Map<K, V> loaded = cacheLoader.orElseThrow().loadAll(keys);
1✔
287
    for (var entry : loaded.entrySet()) {
1✔
288
      putNoCopyOrAwait(entry.getKey(), entry.getValue(), /* publishToWriter= */ false);
1✔
289
    }
1✔
290
  }
1✔
291

292
  /** Performs the bulk load where the existing entries are retained. */
293
  @SuppressWarnings("ConstantValue")
294
  private void loadAllAndKeepExisting(Set<? extends K> keys) {
295
    List<K> keysToLoad = keys.stream()
1✔
296
        .filter(key -> !cache.asMap().containsKey(key))
1✔
297
        .collect(toUnmodifiableList());
1✔
298
    Map<K, V> result = cacheLoader.orElseThrow().loadAll(keysToLoad);
1✔
299
    for (var entry : result.entrySet()) {
1✔
300
      if ((entry.getKey() != null) && (entry.getValue() != null)) {
1✔
301
        putIfAbsentNoAwait(entry.getKey(), entry.getValue(), /* publishToWriter= */ false);
1✔
302
      }
303
    }
1✔
304
  }
1✔
305

306
  @Override
307
  public void put(K key, V value) {
308
    requireNotClosed();
1✔
309
    boolean statsEnabled = statistics.isEnabled();
1✔
310
    long start = statsEnabled ? ticker.read() : 0L;
1✔
311

312
    var result = putNoCopyOrAwait(key, value, /* publishToWriter= */ true);
1✔
313
    dispatcher.awaitSynchronous();
1✔
314

315
    if (statsEnabled) {
1✔
316
      if (result.written) {
1✔
317
        statistics.recordPuts(1);
1✔
318
      }
319
      statistics.recordPutTime(ticker.read() - start);
1✔
320
    }
321
  }
1✔
322

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

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

332
    if (statsEnabled) {
1✔
333
      if (result.oldValue == null) {
1✔
334
        statistics.recordMisses(1L);
1✔
335
      } else {
336
        statistics.recordHits(1L);
1✔
337
      }
338
      if (result.written) {
1✔
339
        statistics.recordPuts(1);
1✔
340
      }
341
      long duration = ticker.read() - start;
1✔
342
      statistics.recordGetTime(duration);
1✔
343
      statistics.recordPutTime(duration);
1✔
344
    }
345
    return copyOf(result.oldValue);
1✔
346
  }
347

348
  /**
349
   * Associates the specified value with the specified key in the cache.
350
   *
351
   * @param key key with which the specified value is to be associated
352
   * @param value value to be associated with the specified key
353
   * @param publishToWriter if the writer should be notified
354
   * @return the old value
355
   */
356
  @CanIgnoreReturnValue
357
  protected PutResult<V> putNoCopyOrAwait(K key, V value, boolean publishToWriter) {
358
    requireNonNull(key);
1✔
359
    requireNonNull(value);
1✔
360

361
    var result = new PutResult<V>();
1✔
362
    cache.asMap().compute(copyOf(key), (K k, @Var Expirable<V> expirable) -> {
1✔
363
      V newValue = copyOf(value);
1✔
364
      if (publishToWriter) {
1✔
365
        publishToCacheWriter(writer::write, () -> new EntryProxy<>(key, value));
1✔
366
      }
367
      if ((expirable != null) && !expirable.isEternal()
1✔
368
          && expirable.hasExpired(currentTimeMillis())) {
1✔
369
        dispatcher.publishExpired(this, key, expirable.get());
1✔
370
        statistics.recordEvictions(1L);
1✔
371
        expirable = null;
1✔
372
      }
373
      @Var long expireTimeMillis = getWriteExpireTimeMillis((expirable == null));
1✔
374
      if ((expirable != null) && (expireTimeMillis == Long.MIN_VALUE)) {
1✔
375
        expireTimeMillis = expirable.getExpireTimeMillis();
1✔
376
      }
377
      if (expireTimeMillis == 0) {
1✔
378
        // The TCK asserts that expired entry is not counted in the puts stats, despite the javadoc
379
        // saying otherwise. See CacheMBStatisticsBeanTest.testExpiryOnCreation()
380
        result.written = false;
1✔
381

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

386
        dispatcher.publishExpired(this, key, value);
1✔
387
        return null;
1✔
388
      } else if (expirable == null) {
1✔
389
        dispatcher.publishCreated(this, key, newValue);
1✔
390
      } else {
391
        result.oldValue = expirable.get();
1✔
392
        dispatcher.publishUpdated(this, key, expirable.get(), newValue);
1✔
393
      }
394
      result.written = true;
1✔
395
      return new Expirable<>(newValue, expireTimeMillis);
1✔
396
    });
397
    return result;
1✔
398
  }
399

400
  @Override
401
  public void putAll(Map<? extends K, ? extends V> map) {
402
    requireNotClosed();
1✔
403
    for (var entry : map.entrySet()) {
1✔
404
      requireNonNull(entry.getKey());
1✔
405
      requireNonNull(entry.getValue());
1✔
406
    }
1✔
407

408
    @Var CacheWriterException error = null;
1✔
409
    @Var Set<? extends K> failedKeys = Set.of();
1✔
410
    boolean statsEnabled = statistics.isEnabled();
1✔
411
    long start = statsEnabled ? ticker.read() : 0L;
1✔
412
    if (configuration.isWriteThrough() && !map.isEmpty()) {
1✔
413
      var entries = new ArrayList<Cache.Entry<? extends K, ? extends V>>(map.size());
1✔
414
      for (var entry : map.entrySet()) {
1✔
415
        entries.add(new EntryProxy<>(entry.getKey(), entry.getValue()));
1✔
416
      }
1✔
417
      try {
418
        writer.writeAll(entries);
1✔
419
      } catch (CacheWriterException e) {
1✔
420
        failedKeys = entries.stream().map(Cache.Entry::getKey).collect(toSet());
1✔
421
        error = e;
1✔
422
      } catch (RuntimeException e) {
1✔
423
        failedKeys = entries.stream().map(Cache.Entry::getKey).collect(toSet());
1✔
424
        error = new CacheWriterException("Exception in CacheWriter", e);
1✔
425
      }
1✔
426
    }
427

428
    @Var int puts = 0;
1✔
429
    for (var entry : map.entrySet()) {
1✔
430
      if (!failedKeys.contains(entry.getKey())) {
1✔
431
        var result = putNoCopyOrAwait(entry.getKey(),
1✔
432
            entry.getValue(), /* publishToWriter= */ false);
1✔
433
        if (result.written) {
1✔
434
          puts++;
1✔
435
        }
436
      }
437
    }
1✔
438
    dispatcher.awaitSynchronous();
1✔
439

440
    if (statsEnabled) {
1✔
441
      statistics.recordPuts(puts);
1✔
442
      statistics.recordPutTime(ticker.read() - start);
1✔
443
    }
444
    if (error != null) {
1✔
445
      throw error;
1✔
446
    }
447
  }
1✔
448

449
  @Override
450
  public boolean putIfAbsent(K key, V value) {
451
    requireNotClosed();
1✔
452
    requireNonNull(value);
1✔
453
    boolean statsEnabled = statistics.isEnabled();
1✔
454
    long start = statsEnabled ? ticker.read() : 0L;
1✔
455

456
    boolean added = putIfAbsentNoAwait(key, value, /* publishToWriter= */ true);
1✔
457
    dispatcher.awaitSynchronous();
1✔
458

459
    if (statsEnabled) {
1✔
460
      if (added) {
1✔
461
        statistics.recordPuts(1L);
1✔
462
        statistics.recordMisses(1L);
1✔
463
      } else {
464
        statistics.recordHits(1L);
1✔
465
      }
466
      statistics.recordPutTime(ticker.read() - start);
1✔
467
    }
468
    return added;
1✔
469
  }
470

471
  /**
472
   * Associates the specified value with the specified key in the cache if there is no existing
473
   * mapping.
474
   *
475
   * @param key key with which the specified value is to be associated
476
   * @param value value to be associated with the specified key
477
   * @param publishToWriter if the writer should be notified
478
   * @return if the mapping was successful
479
   */
480
  @CanIgnoreReturnValue
481
  private boolean putIfAbsentNoAwait(K key, V value, boolean publishToWriter) {
482
    boolean[] absent = { false };
1✔
483
    cache.asMap().compute(copyOf(key), (K k, @Var Expirable<V> expirable) -> {
1✔
484
      if ((expirable != null) && !expirable.isEternal()
1✔
485
          && expirable.hasExpired(currentTimeMillis())) {
1✔
486
        dispatcher.publishExpired(this, key, expirable.get());
1✔
487
        statistics.recordEvictions(1L);
1✔
488
        expirable = null;
1✔
489
      }
490
      if (expirable != null) {
1✔
491
        return expirable;
1✔
492
      }
493
      if (publishToWriter) {
1✔
494
        publishToCacheWriter(writer::write, () -> new EntryProxy<>(key, value));
1✔
495
      }
496

497
      absent[0] = true;
1✔
498
      V copy = copyOf(value);
1✔
499
      long expireTimeMillis = getWriteExpireTimeMillis(/* created= */ true);
1✔
500
      if (expireTimeMillis == 0) {
1✔
501
        // The TCK asserts that a create is not published in
502
        // CacheExpiryTest.expire_whenCreated_CreatedExpiryPolicy()
503
        dispatcher.publishExpired(this, key, copy);
1✔
504
        return null;
1✔
505
      } else {
506
        dispatcher.publishCreated(this, key, copy);
1✔
507
        return new Expirable<>(copy, expireTimeMillis);
1✔
508
      }
509
    });
510
    return absent[0];
1✔
511
  }
512

513
  @Override
514
  public boolean remove(K key) {
515
    requireNotClosed();
1✔
516
    requireNonNull(key);
1✔
517
    boolean statsEnabled = statistics.isEnabled();
1✔
518
    long start = statsEnabled ? ticker.read() : 0L;
1✔
519

520
    publishToCacheWriter(writer::delete, () -> key);
1✔
521
    V value = removeNoCopyOrAwait(key);
1✔
522
    dispatcher.awaitSynchronous();
1✔
523

524
    if (statsEnabled) {
1✔
525
      statistics.recordRemoveTime(ticker.read() - start);
1✔
526
    }
527
    if (value != null) {
1✔
528
      statistics.recordRemovals(1L);
1✔
529
      return true;
1✔
530
    }
531
    return false;
1✔
532
  }
533

534
  /**
535
   * Removes the mapping from the cache without store-by-value copying nor waiting for synchronous
536
   * listeners to complete.
537
   *
538
   * @param key key whose mapping is to be removed from the cache
539
   * @return the old value
540
   */
541
  private @Nullable V removeNoCopyOrAwait(K key) {
542
    @SuppressWarnings("unchecked")
543
    var removed = (V[]) new Object[1];
1✔
544
    cache.asMap().computeIfPresent(key, (k, expirable) -> {
1✔
545
      if (!expirable.isEternal() && expirable.hasExpired(currentTimeMillis())) {
1✔
546
        dispatcher.publishExpired(this, key, expirable.get());
1✔
547
        statistics.recordEvictions(1L);
1✔
548
      } else {
549
        dispatcher.publishRemoved(this, key, expirable.get());
1✔
550
        removed[0] = expirable.get();
1✔
551
      }
552
      return null;
1✔
553
    });
554
    return removed[0];
1✔
555
  }
556

557
  @Override
558
  @CanIgnoreReturnValue
559
  public boolean remove(K key, V oldValue) {
560
    requireNotClosed();
1✔
561
    requireNonNull(key);
1✔
562
    requireNonNull(oldValue);
1✔
563

564
    boolean statsEnabled = statistics.isEnabled();
1✔
565
    long start = statsEnabled ? ticker.read() : 0L;
1✔
566
    boolean[] found = { false };
1✔
567

568
    boolean[] removed = { false };
1✔
569
    cache.asMap().computeIfPresent(key, (k, expirable) -> {
1✔
570
      long millis = expirable.isEternal()
1✔
571
          ? 0L
1✔
572
          : nanosToMillis((start == 0L) ? ticker.read() : start);
1✔
573
      if (expirable.hasExpired(millis)) {
1✔
574
        dispatcher.publishExpired(this, key, expirable.get());
1✔
575
        statistics.recordEvictions(1L);
1✔
576
        return null;
1✔
577
      }
578

579
      found[0] = true;
1✔
580
      if (oldValue.equals(expirable.get())) {
1✔
581
        publishToCacheWriter(writer::delete, () -> key);
1✔
582
        dispatcher.publishRemoved(this, key, expirable.get());
1✔
583
        removed[0] = true;
1✔
584
        return null;
1✔
585
      }
586
      setAccessExpireTime(key, expirable, millis);
1✔
587
      return expirable;
1✔
588
    });
589
    dispatcher.awaitSynchronous();
1✔
590
    if (statsEnabled) {
1✔
591
      if (removed[0]) {
1✔
592
        statistics.recordRemovals(1L);
1✔
593
        statistics.recordHits(1L);
1✔
594
      } else if (found[0]) {
1✔
595
        statistics.recordHits(1L);
1✔
596
      } else {
597
        statistics.recordMisses(1L);
1✔
598
      }
599
      statistics.recordRemoveTime(ticker.read() - start);
1✔
600
    }
601
    return removed[0];
1✔
602
  }
603

604
  @Override
605
  public @Nullable V getAndRemove(K key) {
606
    requireNotClosed();
1✔
607
    requireNonNull(key);
1✔
608
    boolean statsEnabled = statistics.isEnabled();
1✔
609
    long start = statsEnabled ? ticker.read() : 0L;
1✔
610

611
    publishToCacheWriter(writer::delete, () -> key);
1✔
612
    V value = removeNoCopyOrAwait(key);
1✔
613
    dispatcher.awaitSynchronous();
1✔
614
    V copy = copyOf(value);
1✔
615

616
    if (statsEnabled) {
1✔
617
      if (value == null) {
1✔
618
        statistics.recordMisses(1L);
1✔
619
      } else {
620
        statistics.recordHits(1L);
1✔
621
        statistics.recordRemovals(1L);
1✔
622
      }
623
      long duration = ticker.read() - start;
1✔
624
      statistics.recordRemoveTime(duration);
1✔
625
      statistics.recordGetTime(duration);
1✔
626
    }
627
    return copy;
1✔
628
  }
629

630
  @Override
631
  public boolean replace(K key, V oldValue, V newValue) {
632
    requireNotClosed();
1✔
633
    requireNonNull(oldValue);
1✔
634
    requireNonNull(newValue);
1✔
635

636
    boolean statsEnabled = statistics.isEnabled();
1✔
637
    long start = statsEnabled ? ticker.read() : 0L;
1✔
638

639
    boolean[] found = { false };
1✔
640
    boolean[] replaced = { false };
1✔
641
    cache.asMap().computeIfPresent(key, (k, expirable) -> {
1✔
642
      long millis = expirable.isEternal()
1✔
643
          ? 0L
1✔
644
          : nanosToMillis((start == 0L) ? ticker.read() : start);
1✔
645
      if (expirable.hasExpired(millis)) {
1✔
646
        dispatcher.publishExpired(this, key, expirable.get());
1✔
647
        statistics.recordEvictions(1L);
1✔
648
        return null;
1✔
649
      }
650

651
      found[0] = true;
1✔
652
      Expirable<V> result;
653
      if (oldValue.equals(expirable.get())) {
1✔
654
        publishToCacheWriter(writer::write, () -> new EntryProxy<>(key, newValue));
1✔
655
        dispatcher.publishUpdated(this, key, expirable.get(), copyOf(newValue));
1✔
656
        @Var long expireTimeMillis = getWriteExpireTimeMillis(/* created= */ false);
1✔
657
        if (expireTimeMillis == Long.MIN_VALUE) {
1✔
658
          expireTimeMillis = expirable.getExpireTimeMillis();
1✔
659
        }
660
        result = new Expirable<>(newValue, expireTimeMillis);
1✔
661
        replaced[0] = true;
1✔
662
      } else {
1✔
663
        result = expirable;
1✔
664
        setAccessExpireTime(key, expirable, millis);
1✔
665
      }
666
      return result;
1✔
667
    });
668
    dispatcher.awaitSynchronous();
1✔
669

670
    if (statsEnabled) {
1✔
671
      statistics.recordPuts(replaced[0] ? 1L : 0L);
1✔
672
      statistics.recordMisses(found[0] ? 0L : 1L);
1✔
673
      statistics.recordHits(found[0] ? 1L : 0L);
1✔
674
      long duration = ticker.read() - start;
1✔
675
      statistics.recordGetTime(duration);
1✔
676
      statistics.recordPutTime(duration);
1✔
677
    }
678

679
    return replaced[0];
1✔
680
  }
681

682
  @Override
683
  public boolean replace(K key, V value) {
684
    requireNotClosed();
1✔
685
    boolean statsEnabled = statistics.isEnabled();
1✔
686
    long start = statsEnabled ? ticker.read() : 0L;
1✔
687

688
    @Nullable V oldValue = replaceNoCopyOrAwait(key, value);
1✔
689
    dispatcher.awaitSynchronous();
1✔
690
    if (oldValue == null) {
1✔
691
      statistics.recordMisses(1L);
1✔
692
      return false;
1✔
693
    }
694

695
    if (statsEnabled) {
1✔
696
      statistics.recordHits(1L);
1✔
697
      statistics.recordPuts(1L);
1✔
698
      statistics.recordPutTime(ticker.read() - start);
1✔
699
    }
700
    return true;
1✔
701
  }
702

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

709
    V oldValue = replaceNoCopyOrAwait(key, value);
1✔
710
    dispatcher.awaitSynchronous();
1✔
711
    V copy = copyOf(oldValue);
1✔
712

713
    if (statsEnabled) {
1✔
714
      if (oldValue == null) {
1✔
715
        statistics.recordMisses(1L);
1✔
716
      } else {
717
        statistics.recordHits(1L);
1✔
718
        statistics.recordPuts(1L);
1✔
719
      }
720
      long duration = ticker.read() - start;
1✔
721
      statistics.recordGetTime(duration);
1✔
722
      statistics.recordPutTime(duration);
1✔
723
    }
724
    return copy;
1✔
725
  }
726

727
  /**
728
   * Replaces the entry for the specified key only if it is currently mapped to some value. The
729
   * entry is not store-by-value copied nor does the method wait for synchronous listeners to
730
   * complete.
731
   *
732
   * @param key key with which the specified value is associated
733
   * @param value value to be associated with the specified key
734
   * @return the old value
735
   */
736
  private @Nullable V replaceNoCopyOrAwait(K key, V value) {
737
    requireNonNull(value);
1✔
738
    V copy = copyOf(value);
1✔
739
    @SuppressWarnings("unchecked")
740
    var replaced = (V[]) new Object[1];
1✔
741
    cache.asMap().computeIfPresent(key, (k, expirable) -> {
1✔
742
      if (!expirable.isEternal() && expirable.hasExpired(currentTimeMillis())) {
1✔
743
        dispatcher.publishExpired(this, key, expirable.get());
1✔
744
        statistics.recordEvictions(1L);
1✔
745
        return null;
1✔
746
      }
747

748
      publishToCacheWriter(writer::write, () -> new EntryProxy<>(key, value));
1✔
749
      @Var long expireTimeMillis = getWriteExpireTimeMillis(/* created= */ false);
1✔
750
      if (expireTimeMillis == Long.MIN_VALUE) {
1✔
751
        expireTimeMillis = expirable.getExpireTimeMillis();
1✔
752
      }
753
      dispatcher.publishUpdated(this, key, expirable.get(), copy);
1✔
754
      replaced[0] = expirable.get();
1✔
755
      return new Expirable<>(copy, expireTimeMillis);
1✔
756
    });
757
    return replaced[0];
1✔
758
  }
759

760
  @Override
761
  public void removeAll(Set<? extends K> keys) {
762
    requireNotClosed();
1✔
763
    keys.forEach(Objects::requireNonNull);
1✔
764

765
    @Var CacheWriterException error = null;
1✔
766
    @Var Set<? extends K> failedKeys = Set.of();
1✔
767
    boolean statsEnabled = statistics.isEnabled();
1✔
768
    long start = statsEnabled ? ticker.read() : 0L;
1✔
769
    if (configuration.isWriteThrough() && !keys.isEmpty()) {
1✔
770
      var keysToWrite = new LinkedHashSet<>(keys);
1✔
771
      try {
772
        writer.deleteAll(keysToWrite);
1✔
773
      } catch (CacheWriterException e) {
1✔
774
        error = e;
1✔
775
        failedKeys = keysToWrite;
1✔
776
      } catch (RuntimeException e) {
1✔
777
        error = new CacheWriterException("Exception in CacheWriter", e);
1✔
778
        failedKeys = keysToWrite;
1✔
779
      }
1✔
780
    }
781

782
    @Var int removed = 0;
1✔
783
    for (var key : keys) {
1✔
784
      if (!failedKeys.contains(key) && (removeNoCopyOrAwait(key) != null)) {
1✔
785
        removed++;
1✔
786
      }
787
    }
1✔
788
    dispatcher.awaitSynchronous();
1✔
789

790
    if (statsEnabled) {
1✔
791
      statistics.recordRemovals(removed);
1✔
792
      statistics.recordRemoveTime(ticker.read() - start);
1✔
793
    }
794
    if (error != null) {
1✔
795
      throw error;
1✔
796
    }
797
  }
1✔
798

799
  @Override
800
  public void removeAll() {
801
    removeAll(cache.asMap().keySet());
1✔
802
  }
1✔
803

804
  @Override
805
  public void clear() {
806
    requireNotClosed();
1✔
807
    cache.invalidateAll();
1✔
808
  }
1✔
809

810
  @Override
811
  public <C extends Configuration<K, V>> C getConfiguration(Class<C> clazz) {
812
    if (clazz.isInstance(configuration)) {
1✔
813
      synchronized (configuration) {
1✔
814
        return clazz.cast(configuration.immutableCopy());
1✔
815
      }
816
    }
817
    throw new IllegalArgumentException("The configuration class " + clazz
1✔
818
        + " is not supported by this implementation");
819
  }
820

821
  @Override
822
  public <T> @Nullable T invoke(K key,
823
      EntryProcessor<K, V, T> entryProcessor, Object... arguments) {
824
    requireNonNull(entryProcessor);
1✔
825
    requireNonNull(arguments);
1✔
826
    requireNotClosed();
1✔
827

828
    Object[] result = new Object[1];
1✔
829
    BiFunction<K, Expirable<V>, Expirable<V>> remappingFunction = (k, expirable) -> {
1✔
830
      V value;
831
      @Var long millis = 0L;
1✔
832
      if ((expirable == null)
1✔
833
          || (!expirable.isEternal() && expirable.hasExpired(millis = currentTimeMillis()))) {
1✔
834
        statistics.recordMisses(1L);
1✔
835
        value = null;
1✔
836
      } else {
837
        value = copyValue(expirable);
1✔
838
        statistics.recordHits(1L);
1✔
839
      }
840
      var entry = new EntryProcessorEntry<>(key, value,
1✔
841
          configuration.isReadThrough() ? cacheLoader : Optional.empty());
1✔
842
      try {
843
        result[0] = entryProcessor.process(entry, arguments);
1✔
844
        return postProcess(expirable, entry, millis);
1✔
845
      } catch (EntryProcessorException e) {
1✔
846
        throw e;
1✔
847
      } catch (RuntimeException e) {
1✔
848
        throw new EntryProcessorException(e);
1✔
849
      }
850
    };
851
    try {
852
      cache.asMap().compute(copyOf(key), remappingFunction);
1✔
853
      dispatcher.awaitSynchronous();
1✔
854
    } catch (Throwable t) {
1✔
855
      dispatcher.ignoreSynchronous();
1✔
856
      throw t;
1✔
857
    }
1✔
858

859
    @SuppressWarnings("unchecked")
860
    var castedResult = (T) result[0];
1✔
861
    return castedResult;
1✔
862
  }
863

864
  /** Returns the updated expirable value after performing the post-processing actions. */
865
  @SuppressWarnings("fallthrough")
866
  @Nullable Expirable<V> postProcess(@Nullable Expirable<V> expirable,
867
      EntryProcessorEntry<K, V> entry, @Var long currentTimeMillis) {
868
    switch (entry.getAction()) {
1✔
869
      case NONE:
870
        if (expirable == null) {
1✔
871
          return null;
1✔
872
        } else if (expirable.isEternal()) {
1✔
873
          return expirable;
1✔
874
        }
875
        if (currentTimeMillis == 0) {
1✔
876
          currentTimeMillis = currentTimeMillis();
1✔
877
        }
878
        if (expirable.hasExpired(currentTimeMillis)) {
1✔
879
          dispatcher.publishExpired(this, entry.getKey(), expirable.get());
1✔
880
          statistics.recordEvictions(1);
1✔
881
          return null;
1✔
882
        }
883
        return expirable;
1✔
884
      case READ: {
885
        setAccessExpireTime(entry.getKey(), requireNonNull(expirable), 0L);
1✔
886
        return expirable;
1✔
887
      }
888
      case CREATED:
889
        this.publishToCacheWriter(writer::write, () -> entry);
1✔
890
        // fallthrough
891
      case LOADED: {
892
        statistics.recordPuts(1L);
1✔
893
        var value = requireNonNull(entry.getValue());
1✔
894
        dispatcher.publishCreated(this, entry.getKey(), value);
1✔
895
        return new Expirable<>(value, getWriteExpireTimeMillis(/* created= */ true));
1✔
896
      }
897
      case UPDATED: {
898
        statistics.recordPuts(1L);
1✔
899
        publishToCacheWriter(writer::write, () -> entry);
1✔
900
        requireNonNull(expirable, "Expected a previous value but was null");
1✔
901
        var value = requireNonNull(entry.getValue(), "Expected a new value but was null");
1✔
902
        dispatcher.publishUpdated(this, entry.getKey(), expirable.get(), value);
1✔
903
        @Var long expireTimeMillis = getWriteExpireTimeMillis(/* created= */ false);
1✔
904
        if (expireTimeMillis == Long.MIN_VALUE) {
1✔
905
          expireTimeMillis = expirable.getExpireTimeMillis();
1✔
906
        }
907
        return new Expirable<>(value, expireTimeMillis);
1✔
908
      }
909
      case DELETED:
910
        statistics.recordRemovals(1L);
1✔
911
        publishToCacheWriter(writer::delete, entry::getKey);
1✔
912
        if (expirable != null) {
1✔
913
          dispatcher.publishRemoved(this, entry.getKey(), expirable.get());
1✔
914
        }
915
        return null;
1✔
916
    }
917
    throw new IllegalStateException("Unknown state: " + entry.getAction());
1✔
918
  }
919

920
  @Override
921
  public <T> Map<K, EntryProcessorResult<T>> invokeAll(Set<? extends K> keys,
922
      EntryProcessor<K, V, T> entryProcessor, Object... arguments) {
923
    var results = new HashMap<K, EntryProcessorResult<T>>(keys.size(), 1.0f);
1✔
924
    for (K key : keys) {
1✔
925
      try {
926
        T result = invoke(key, entryProcessor, arguments);
1✔
927
        if (result != null) {
1✔
928
          results.put(key, () -> result);
1✔
929
        }
930
      } catch (EntryProcessorException e) {
1✔
931
        results.put(key, () -> { throw e; });
1✔
932
      }
1✔
933
    }
1✔
934
    return results;
1✔
935
  }
936

937
  @Override
938
  public String getName() {
939
    return name;
1✔
940
  }
941

942
  @Override
943
  public CacheManager getCacheManager() {
944
    return cacheManager;
1✔
945
  }
946

947
  @Override
948
  public boolean isClosed() {
949
    return closed;
1✔
950
  }
951

952
  @Override
953
  public void close() {
954
    if (isClosed()) {
1✔
955
      return;
1✔
956
    }
957
    synchronized (configuration) {
1✔
958
      if (!isClosed()) {
1✔
959
        enableManagement(false);
1✔
960
        enableStatistics(false);
1✔
961
        cacheManager.destroyCache(name);
1✔
962
        closed = true;
1✔
963

964
        @Var var thrown = shutdownExecutor();
1✔
965
        thrown = tryClose(expiry, thrown);
1✔
966
        thrown = tryClose(writer, thrown);
1✔
967
        thrown = tryClose(executor, thrown);
1✔
968
        thrown = tryClose(cacheLoader.orElse(null), thrown);
1✔
969
        for (Registration<K, V> registration : dispatcher.registrations()) {
1✔
970
          thrown = tryClose(registration.getCacheEntryListener(), thrown);
1✔
971
        }
1✔
972
        if (thrown != null) {
1✔
973
          logger.log(Level.WARNING, "Failure when closing cache resources", thrown);
1✔
974
        }
975
      }
976
    }
1✔
977
    cache.invalidateAll();
1✔
978
  }
1✔
979

980
  @SuppressWarnings("FutureReturnValueIgnored")
981
  private @Nullable Throwable shutdownExecutor() {
982
    if (executor instanceof ExecutorService) {
1✔
983
      @SuppressWarnings("PMD.CloseResource")
984
      var es = (ExecutorService) executor;
1✔
985
      es.shutdown();
1✔
986
    }
987

988
    @Var Throwable thrown = null;
1✔
989
    try {
990
      CompletableFuture
1✔
991
          .allOf(inFlight.toArray(CompletableFuture[]::new))
1✔
992
          .get(10, TimeUnit.SECONDS);
1✔
993
    } catch (ExecutionException | TimeoutException e) {
1✔
994
      thrown = e;
1✔
NEW
995
    } catch (InterruptedException e) {
×
NEW
996
      Thread.currentThread().interrupt();
×
UNCOV
997
      thrown = e;
×
998
    }
1✔
999
    inFlight.clear();
1✔
1000
    return thrown;
1✔
1001
  }
1002

1003
  /**
1004
   * Attempts to close the resource. If an error occurs and an outermost exception is set, then adds
1005
   * the error to the suppression list.
1006
   *
1007
   * @param o the resource to close if Closeable
1008
   * @param outer the outermost error, or null if unset
1009
   * @return the outermost error, or null if unset and successful
1010
   */
1011
  private static @Nullable Throwable tryClose(@Nullable Object o, @Nullable Throwable outer) {
1012
    if (o instanceof AutoCloseable) {
1✔
1013
      try {
1014
        ((AutoCloseable) o).close();
1✔
1015
      } catch (Throwable t) {
1✔
1016
        if (outer == null) {
1✔
1017
          return t;
1✔
1018
        }
1019
        outer.addSuppressed(t);
1✔
1020
      }
1✔
1021
    }
1022
    return outer;
1✔
1023
  }
1024

1025
  @Override
1026
  public <T> T unwrap(Class<T> clazz) {
1027
    if (clazz.isInstance(cache)) {
1✔
1028
      return clazz.cast(cache);
1✔
1029
    } else if (clazz.isInstance(this)) {
1✔
1030
      return clazz.cast(this);
1✔
1031
    }
1032
    throw new IllegalArgumentException("Unwrapping to " + clazz
1✔
1033
        + " is not supported by this implementation");
1034
  }
1035

1036
  @Override
1037
  public void registerCacheEntryListener(
1038
      CacheEntryListenerConfiguration<K, V> cacheEntryListenerConfiguration) {
1039
    requireNotClosed();
1✔
1040
    synchronized (configuration) {
1✔
1041
      configuration.addCacheEntryListenerConfiguration(cacheEntryListenerConfiguration);
1✔
1042
      dispatcher.register(cacheEntryListenerConfiguration);
1✔
1043
    }
1✔
1044
  }
1✔
1045

1046
  @Override
1047
  public void deregisterCacheEntryListener(
1048
      CacheEntryListenerConfiguration<K, V> cacheEntryListenerConfiguration) {
1049
    requireNotClosed();
1✔
1050
    synchronized (configuration) {
1✔
1051
      configuration.removeCacheEntryListenerConfiguration(cacheEntryListenerConfiguration);
1✔
1052
      dispatcher.deregister(cacheEntryListenerConfiguration);
1✔
1053
    }
1✔
1054
  }
1✔
1055

1056
  @Override
1057
  public Iterator<Cache.Entry<K, V>> iterator() {
1058
    requireNotClosed();
1✔
1059
    return new EntryIterator();
1✔
1060
  }
1061

1062
  /** Enables or disables the configuration management JMX bean. */
1063
  void enableManagement(boolean enabled) {
1064
    requireNotClosed();
1✔
1065

1066
    synchronized (configuration) {
1✔
1067
      if (enabled) {
1✔
1068
        JmxRegistration.registerMxBean(this, cacheMxBean, MBeanType.CONFIGURATION);
1✔
1069
      } else {
1070
        JmxRegistration.unregisterMxBean(this, MBeanType.CONFIGURATION);
1✔
1071
      }
1072
      configuration.setManagementEnabled(enabled);
1✔
1073
    }
1✔
1074
  }
1✔
1075

1076
  /** Enables or disables the statistics JMX bean. */
1077
  void enableStatistics(boolean enabled) {
1078
    requireNotClosed();
1✔
1079

1080
    synchronized (configuration) {
1✔
1081
      if (enabled) {
1✔
1082
        JmxRegistration.registerMxBean(this, statistics, MBeanType.STATISTICS);
1✔
1083
      } else {
1084
        JmxRegistration.unregisterMxBean(this, MBeanType.STATISTICS);
1✔
1085
      }
1086
      statistics.enable(enabled);
1✔
1087
      configuration.setStatisticsEnabled(enabled);
1✔
1088
    }
1✔
1089
  }
1✔
1090

1091
  /** Performs the action with the cache writer if write-through is enabled. */
1092
  private <T> void publishToCacheWriter(Consumer<T> action, Supplier<T> data) {
1093
    if (!configuration.isWriteThrough()) {
1✔
1094
      return;
1✔
1095
    }
1096
    try {
1097
      action.accept(data.get());
1✔
1098
    } catch (CacheWriterException e) {
1✔
1099
      throw e;
1✔
1100
    } catch (RuntimeException e) {
1✔
1101
      throw new CacheWriterException("Exception in CacheWriter", e);
1✔
1102
    }
1✔
1103
  }
1✔
1104

1105
  /** Checks that the cache is not closed. */
1106
  protected final void requireNotClosed() {
1107
    if (isClosed()) {
1✔
1108
      throw new IllegalStateException();
1✔
1109
    }
1110
  }
1✔
1111

1112
  /**
1113
   * Returns a copy of the value if value-based caching is enabled.
1114
   *
1115
   * @param object the object to be copied
1116
   * @param <T> the type of object being copied
1117
   * @return a copy of the object if storing by value or the same instance if by reference
1118
   */
1119
  @SuppressWarnings({"DataFlowIssue", "NullAway"})
1120
  protected final <T> T copyOf(@Nullable T object) {
1121
    if (object == null) {
1✔
1122
      return null;
1✔
1123
    }
1124
    T copy = copier.copy(object, cacheManager.getClassLoader());
1✔
1125
    return requireNonNull(copy);
1✔
1126
  }
1127

1128
  /**
1129
   * Returns a copy of the value if value-based caching is enabled.
1130
   *
1131
   * @param expirable the expirable value to be copied
1132
   * @return a copy of the value if storing by value or the same instance if by reference
1133
   */
1134
  @SuppressWarnings({"DataFlowIssue", "NullAway"})
1135
  protected final V copyValue(@Nullable Expirable<V> expirable) {
1136
    if (expirable == null) {
1✔
1137
      return null;
1✔
1138
    }
1139
    V copy = copier.copy(expirable.get(), cacheManager.getClassLoader());
1✔
1140
    return requireNonNull(copy);
1✔
1141
  }
1142

1143
  /**
1144
   * Returns a deep copy of the map if value-based caching is enabled.
1145
   *
1146
   * @param map the mapping of keys to expirable values
1147
   * @return a deep or shallow copy of the mappings depending on the store by value setting
1148
   */
1149
  @SuppressWarnings("CollectorMutability")
1150
  protected final Map<K, V> copyMap(Map<K, Expirable<V>> map) {
1151
    ClassLoader classLoader = cacheManager.getClassLoader();
1✔
1152
    return map.entrySet().stream().collect(toMap(
1✔
1153
        entry -> copier.copy(entry.getKey(), classLoader),
1✔
1154
        entry -> copier.copy(entry.getValue().get(), classLoader)));
1✔
1155
  }
1156

1157
  /** Returns the current time in milliseconds. */
1158
  protected final long currentTimeMillis() {
1159
    return nanosToMillis(ticker.read());
1✔
1160
  }
1161

1162
  /** Returns the nanosecond time in milliseconds. */
1163
  protected static long nanosToMillis(long nanos) {
1164
    return TimeUnit.NANOSECONDS.toMillis(nanos);
1✔
1165
  }
1166

1167
  /**
1168
   * Sets the access expiration time.
1169
   *
1170
   * @param key the entry's key
1171
   * @param expirable the entry that was operated on
1172
   * @param currentTimeMillis the current time, or 0 if not read yet
1173
   */
1174
  protected final void setAccessExpireTime(K key,
1175
      Expirable<?> expirable, @Var long currentTimeMillis) {
1176
    try {
1177
      Duration duration = expiry.getExpiryForAccess();
1✔
1178
      if (duration == null) {
1✔
1179
        return;
1✔
1180
      } else if (duration.isZero()) {
1✔
1181
        expirable.setExpireTimeMillis(0L);
1✔
1182
      } else if (duration.isEternal()) {
1✔
1183
        expirable.setExpireTimeMillis(Long.MAX_VALUE);
1✔
1184
      } else {
1185
        if (currentTimeMillis == 0L) {
1✔
1186
          currentTimeMillis = currentTimeMillis();
1✔
1187
        }
1188
        long expireTimeMillis = duration.getAdjustedTime(currentTimeMillis);
1✔
1189
        expirable.setExpireTimeMillis(expireTimeMillis);
1✔
1190
      }
1191
      cache.policy().expireVariably().ifPresent(policy -> {
1✔
1192
        policy.setExpiresAfter(key, duration.getDurationAmount(), duration.getTimeUnit());
1✔
1193
      });
1✔
1194
    } catch (RuntimeException e) {
1✔
1195
      logger.log(Level.WARNING, "Failed to set the entry's expiration time", e);
1✔
1196
    }
1✔
1197
  }
1✔
1198

1199
  /**
1200
   * Returns the time when the entry will expire.
1201
   *
1202
   * @param created if the write operation is an insert or an update
1203
   * @return the time when the entry will expire, zero if it should expire immediately,
1204
   *         Long.MIN_VALUE if it should not be changed, or Long.MAX_VALUE if eternal
1205
   */
1206
  protected final long getWriteExpireTimeMillis(boolean created) {
1207
    try {
1208
      Duration duration = created ? expiry.getExpiryForCreation() : expiry.getExpiryForUpdate();
1✔
1209
      if (duration == null) {
1✔
1210
        return Long.MIN_VALUE;
1✔
1211
      } else if (duration.isZero()) {
1✔
1212
        return 0L;
1✔
1213
      } else if (duration.isEternal()) {
1✔
1214
        return Long.MAX_VALUE;
1✔
1215
      }
1216
      return duration.getAdjustedTime(currentTimeMillis());
1✔
1217
    } catch (RuntimeException e) {
1✔
1218
      logger.log(Level.WARNING, "Failed to get the policy's expiration time", e);
1✔
1219
      return Long.MIN_VALUE;
1✔
1220
    }
1221
  }
1222

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

1229
    Map.@Nullable Entry<K, Expirable<V>> current;
1230
    Map.@Nullable Entry<K, Expirable<V>> cursor;
1231

1232
    @Override
1233
    public boolean hasNext() {
1234
      while ((cursor == null) && delegate.hasNext()) {
1✔
1235
        Map.Entry<K, Expirable<V>> entry = delegate.next();
1✔
1236
        long millis = entry.getValue().isEternal() ? 0L : currentTimeMillis();
1✔
1237
        if (!entry.getValue().hasExpired(millis)) {
1✔
1238
          setAccessExpireTime(entry.getKey(), entry.getValue(), millis);
1✔
1239
          cursor = entry;
1✔
1240
        }
1241
      }
1✔
1242
      return (cursor != null);
1✔
1243
    }
1244

1245
    @Override
1246
    public Cache.Entry<K, V> next() {
1247
      if (!hasNext()) {
1✔
1248
        throw new NoSuchElementException();
1✔
1249
      }
1250
      current = requireNonNull(cursor);
1✔
1251
      cursor = null;
1✔
1252
      return new EntryProxy<>(copyOf(current.getKey()), copyValue(current.getValue()));
1✔
1253
    }
1254

1255
    @Override
1256
    public void remove() {
1257
      if (current == null) {
1✔
1258
        throw new IllegalStateException();
1✔
1259
      }
1260
      CacheProxy.this.remove(current.getKey(), current.getValue().get());
1✔
1261
      current = null;
1✔
1262
    }
1✔
1263
  }
1264

1265
  protected static final class PutResult<V> {
1✔
1266
    @Nullable V oldValue;
1267
    boolean written;
1268
  }
1269

1270
  protected enum NullCompletionListener implements CompletionListener {
1✔
1271
    INSTANCE;
1✔
1272

1273
    @Override
1274
    public void onCompletion() {}
1✔
1275

1276
    @Override
1277
    public void onException(Exception e) {}
1✔
1278
  }
1279
}
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