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

ben-manes / caffeine / #5319

23 Feb 2026 03:40AM UTC coverage: 99.937% (-0.03%) from 99.962%
#5319

push

github

ben-manes
fix minor expiration edge cases

1. The calculated remaining duration was incorrect in the
Policy.CacheEntry snapshot by giving a higher value. The mapping will
expire at the correct time.
2. Map.entrySet().contains(entry) now returns false if the entry is
present but has expired and is pending eviction.
3. JCache's Duration.ETERNAL returns a null TimeUnit. Previously this
caused the cache to log a failure when updating Caffeine's metadata,
though the eternal would be honored as set correctly on the adapter's
wrapped value.
4. JCache's expiry time now ensures that the sentinel flags, 0 and
Long.MAX_VALUE, are avoided in wrap around calculations due to
System.nanoTime() being randomized. In these cases the duration is
adjusted to occur up to 1ms sooner.

3838 of 3850 branches covered (99.69%)

19 of 21 new or added lines in 2 files covered. (90.48%)

7883 of 7888 relevant lines covered (99.94%)

1.0 hits per line

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

99.27
/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✔
995
    } catch (InterruptedException e) {
×
996
      Thread.currentThread().interrupt();
×
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
        cache.policy().expireVariably().ifPresent(policy ->
1✔
1183
            policy.setExpiresAfter(key, 0L, TimeUnit.NANOSECONDS));
1✔
1184
      } else if (duration.isEternal()) {
1✔
1185
        expirable.setExpireTimeMillis(Long.MAX_VALUE);
1✔
1186
        cache.policy().expireVariably().ifPresent(policy ->
1✔
1187
            policy.setExpiresAfter(key, Long.MAX_VALUE, TimeUnit.NANOSECONDS));
1✔
1188
      } else {
1189
        if (currentTimeMillis == 0L) {
1✔
1190
          currentTimeMillis = currentTimeMillis();
1✔
1191
        }
1192
        @Var long expireTimeMillis = duration.getAdjustedTime(currentTimeMillis);
1✔
1193
        expireTimeMillis = ((expireTimeMillis == 0L) || (expireTimeMillis == Long.MAX_VALUE))
1!
NEW
1194
            ? (expireTimeMillis - 1)
×
1195
            : expireTimeMillis;
1✔
1196
        expirable.setExpireTimeMillis(expireTimeMillis);
1✔
1197
        cache.policy().expireVariably().ifPresent(policy ->
1✔
1198
            policy.setExpiresAfter(key, duration.getDurationAmount(), duration.getTimeUnit()));
1✔
1199
      }
1200
    } catch (RuntimeException e) {
1✔
1201
      logger.log(Level.WARNING, "Failed to set the entry's expiration time", e);
1✔
1202
    }
1✔
1203
  }
1✔
1204

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

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

1238
    Map.@Nullable Entry<K, Expirable<V>> current;
1239
    Map.@Nullable Entry<K, Expirable<V>> cursor;
1240

1241
    @Override
1242
    public boolean hasNext() {
1243
      while ((cursor == null) && delegate.hasNext()) {
1✔
1244
        Map.Entry<K, Expirable<V>> entry = delegate.next();
1✔
1245
        long millis = entry.getValue().isEternal() ? 0L : currentTimeMillis();
1✔
1246
        if (!entry.getValue().hasExpired(millis)) {
1✔
1247
          setAccessExpireTime(entry.getKey(), entry.getValue(), millis);
1✔
1248
          cursor = entry;
1✔
1249
        }
1250
      }
1✔
1251
      return (cursor != null);
1✔
1252
    }
1253

1254
    @Override
1255
    public Cache.Entry<K, V> next() {
1256
      if (!hasNext()) {
1✔
1257
        throw new NoSuchElementException();
1✔
1258
      }
1259
      current = requireNonNull(cursor);
1✔
1260
      cursor = null;
1✔
1261
      return new EntryProxy<>(copyOf(current.getKey()), copyValue(current.getValue()));
1✔
1262
    }
1263

1264
    @Override
1265
    public void remove() {
1266
      if (current == null) {
1✔
1267
        throw new IllegalStateException();
1✔
1268
      }
1269
      CacheProxy.this.remove(current.getKey(), current.getValue().get());
1✔
1270
      current = null;
1✔
1271
    }
1✔
1272
  }
1273

1274
  protected static final class PutResult<V> {
1✔
1275
    @Nullable V oldValue;
1276
    boolean written;
1277
  }
1278

1279
  protected enum NullCompletionListener implements CompletionListener {
1✔
1280
    INSTANCE;
1✔
1281

1282
    @Override
1283
    public void onCompletion() {}
1✔
1284

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

© 2026 Coveralls, Inc