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

ben-manes / caffeine / #4774

08 Apr 2025 03:54AM UTC coverage: 99.057% (+0.01%) from 99.044%
#4774

push

github

ben-manes
resolve build properties lazily during configuration time

7671 of 7744 relevant lines covered (99.06%)

0.99 hits per line

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

99.1
/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.Nullable;
63

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

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

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

94
  private final CaffeineConfiguration<K, V> configuration;
95
  private final CacheManager cacheManager;
96
  private final CacheWriter<K, V> writer;
97
  private final JCacheMXBean cacheMxBean;
98
  private final ExpiryPolicy expiry;
99
  private final Copier copier;
100
  private final String name;
101

102
  private volatile boolean closed;
103

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

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

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

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

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

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

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

199
    boolean statsEnabled = statistics.isEnabled();
1✔
200
    long now = statsEnabled ? ticker.read() : 0L;
1✔
201

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

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

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

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

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

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

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

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

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

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

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

309
    var result = putNoCopyOrAwait(key, value, /* publishToWriter= */ true);
1✔
310
    dispatcher.awaitSynchronous();
1✔
311

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

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

326
    var result = putNoCopyOrAwait(key, value, /* publishToWriter= */ true);
1✔
327
    dispatcher.awaitSynchronous();
1✔
328

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

550
      dispatcher.publishRemoved(this, key, expirable.get());
1✔
551
      removed[0] = expirable.get();
1✔
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

567
    boolean[] removed = { false };
1✔
568
    cache.asMap().computeIfPresent(key, (k, expirable) -> {
1✔
569
      long millis = expirable.isEternal()
1✔
570
          ? 0L
1✔
571
          : nanosToMillis((start == 0L) ? ticker.read() : start);
1✔
572
      if (expirable.hasExpired(millis)) {
1✔
573
        dispatcher.publishExpired(this, key, expirable.get());
1✔
574
        statistics.recordEvictions(1L);
1✔
575
        return null;
1✔
576
      }
577
      if (oldValue.equals(expirable.get())) {
1✔
578
        publishToCacheWriter(writer::delete, () -> key);
1✔
579
        dispatcher.publishRemoved(this, key, expirable.get());
1✔
580
        removed[0] = true;
1✔
581
        return null;
1✔
582
      }
583
      setAccessExpireTime(key, expirable, millis);
1✔
584
      return expirable;
1✔
585
    });
586
    dispatcher.awaitSynchronous();
1✔
587
    if (statsEnabled) {
1✔
588
      if (removed[0]) {
1✔
589
        statistics.recordRemovals(1L);
1✔
590
        statistics.recordHits(1L);
1✔
591
      } else {
592
        statistics.recordMisses(1L);
1✔
593
      }
594
      statistics.recordRemoveTime(ticker.read() - start);
1✔
595
    }
596
    return removed[0];
1✔
597
  }
598

599
  @Override
600
  public V getAndRemove(K key) {
601
    requireNotClosed();
1✔
602
    requireNonNull(key);
1✔
603
    boolean statsEnabled = statistics.isEnabled();
1✔
604
    long start = statsEnabled ? ticker.read() : 0L;
1✔
605

606
    publishToCacheWriter(writer::delete, () -> key);
1✔
607
    V value = removeNoCopyOrAwait(key);
1✔
608
    dispatcher.awaitSynchronous();
1✔
609
    V copy = copyOf(value);
1✔
610

611
    if (statsEnabled) {
1✔
612
      if (value == null) {
1✔
613
        statistics.recordMisses(1L);
1✔
614
      } else {
615
        statistics.recordHits(1L);
1✔
616
        statistics.recordRemovals(1L);
1✔
617
      }
618
      long duration = ticker.read() - start;
1✔
619
      statistics.recordRemoveTime(duration);
1✔
620
      statistics.recordGetTime(duration);
1✔
621
    }
622
    return copy;
1✔
623
  }
624

625
  @Override
626
  @CanIgnoreReturnValue
627
  public boolean replace(K key, V oldValue, V newValue) {
628
    requireNotClosed();
1✔
629
    requireNonNull(oldValue);
1✔
630
    requireNonNull(newValue);
1✔
631

632
    boolean statsEnabled = statistics.isEnabled();
1✔
633
    long start = statsEnabled ? ticker.read() : 0L;
1✔
634

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

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

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

675
    return replaced[0];
1✔
676
  }
677

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

685
    V oldValue = replaceNoCopyOrAwait(key, value);
1✔
686
    dispatcher.awaitSynchronous();
1✔
687
    if (oldValue == null) {
1✔
688
      statistics.recordMisses(1L);
1✔
689
      return false;
1✔
690
    }
691

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

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

706
    V oldValue = replaceNoCopyOrAwait(key, value);
1✔
707
    dispatcher.awaitSynchronous();
1✔
708
    V copy = copyOf(oldValue);
1✔
709

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

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

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

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

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

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

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

796
  @Override
797
  public void removeAll() {
798
    removeAll(cache.asMap().keySet());
1✔
799
  }
1✔
800

801
  @Override
802
  public void clear() {
803
    requireNotClosed();
1✔
804
    cache.invalidateAll();
1✔
805
  }
1✔
806

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

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

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

856
    @SuppressWarnings("unchecked")
857
    var castedResult = (T) result[0];
1✔
858
    return castedResult;
1✔
859
  }
860

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

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

931
  @Override
932
  public String getName() {
933
    return name;
1✔
934
  }
935

936
  @Override
937
  public CacheManager getCacheManager() {
938
    return cacheManager;
1✔
939
  }
940

941
  @Override
942
  public boolean isClosed() {
943
    return closed;
1✔
944
  }
945

946
  @Override
947
  public void close() {
948
    if (isClosed()) {
1✔
949
      return;
1✔
950
    }
951
    synchronized (configuration) {
1✔
952
      if (!isClosed()) {
1✔
953
        enableManagement(false);
1✔
954
        enableStatistics(false);
1✔
955
        cacheManager.destroyCache(name);
1✔
956
        closed = true;
1✔
957

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

974
  @SuppressWarnings({"FutureReturnValueIgnored", "PMD.CloseResource"})
975
  private @Nullable Throwable shutdownExecutor() {
976
    if (executor instanceof ExecutorService) {
1✔
977
      var es = (ExecutorService) executor;
1✔
978
      es.shutdown();
1✔
979
    }
980

981
    @Var Throwable thrown = null;
1✔
982
    try {
983
      CompletableFuture
1✔
984
          .allOf(inFlight.toArray(CompletableFuture[]::new))
1✔
985
          .get(10, TimeUnit.SECONDS);
1✔
986
    } catch (InterruptedException | ExecutionException | TimeoutException e) {
1✔
987
      thrown = e;
1✔
988
    }
1✔
989
    inFlight.clear();
1✔
990
    return thrown;
1✔
991
  }
992

993
  /**
994
   * Attempts to close the resource. If an error occurs and an outermost exception is set, then adds
995
   * the error to the suppression list.
996
   *
997
   * @param o the resource to close if Closeable
998
   * @param outer the outermost error, or null if unset
999
   * @return the outermost error, or null if unset and successful
1000
   */
1001
  private static @Nullable Throwable tryClose(@Nullable Object o, @Nullable Throwable outer) {
1002
    if (o instanceof AutoCloseable) {
1✔
1003
      try {
1004
        ((AutoCloseable) o).close();
1✔
1005
      } catch (Throwable t) {
1✔
1006
        if (outer == null) {
1✔
1007
          return t;
1✔
1008
        }
1009
        outer.addSuppressed(t);
1✔
1010
        return outer;
1✔
1011
      }
1✔
1012
    }
1013
    return null;
1✔
1014
  }
1015

1016
  @Override
1017
  public <T> T unwrap(Class<T> clazz) {
1018
    if (clazz.isInstance(cache)) {
1✔
1019
      return clazz.cast(cache);
1✔
1020
    } else if (clazz.isInstance(this)) {
1✔
1021
      return clazz.cast(this);
1✔
1022
    }
1023
    throw new IllegalArgumentException("Unwrapping to " + clazz
1✔
1024
        + " is not supported by this implementation");
1025
  }
1026

1027
  @Override
1028
  public void registerCacheEntryListener(
1029
      CacheEntryListenerConfiguration<K, V> cacheEntryListenerConfiguration) {
1030
    requireNotClosed();
1✔
1031
    synchronized (configuration) {
1✔
1032
      configuration.addCacheEntryListenerConfiguration(cacheEntryListenerConfiguration);
1✔
1033
      dispatcher.register(cacheEntryListenerConfiguration);
1✔
1034
    }
1✔
1035
  }
1✔
1036

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

1047
  @Override
1048
  public Iterator<Cache.Entry<K, V>> iterator() {
1049
    requireNotClosed();
1✔
1050
    return new EntryIterator();
1✔
1051
  }
1052

1053
  /** Enables or disables the configuration management JMX bean. */
1054
  void enableManagement(boolean enabled) {
1055
    requireNotClosed();
1✔
1056

1057
    synchronized (configuration) {
1✔
1058
      if (enabled) {
1✔
1059
        JmxRegistration.registerMxBean(this, cacheMxBean, MBeanType.CONFIGURATION);
1✔
1060
      } else {
1061
        JmxRegistration.unregisterMxBean(this, MBeanType.CONFIGURATION);
1✔
1062
      }
1063
      configuration.setManagementEnabled(enabled);
1✔
1064
    }
1✔
1065
  }
1✔
1066

1067
  /** Enables or disables the statistics JMX bean. */
1068
  void enableStatistics(boolean enabled) {
1069
    requireNotClosed();
1✔
1070

1071
    synchronized (configuration) {
1✔
1072
      if (enabled) {
1✔
1073
        JmxRegistration.registerMxBean(this, statistics, MBeanType.STATISTICS);
1✔
1074
      } else {
1075
        JmxRegistration.unregisterMxBean(this, MBeanType.STATISTICS);
1✔
1076
      }
1077
      statistics.enable(enabled);
1✔
1078
      configuration.setStatisticsEnabled(enabled);
1✔
1079
    }
1✔
1080
  }
1✔
1081

1082
  /** Performs the action with the cache writer if write-through is enabled. */
1083
  private <T> void publishToCacheWriter(Consumer<T> action, Supplier<T> data) {
1084
    if (!configuration.isWriteThrough()) {
1✔
1085
      return;
1✔
1086
    }
1087
    try {
1088
      action.accept(data.get());
1✔
1089
    } catch (CacheWriterException e) {
1✔
1090
      throw e;
1✔
1091
    } catch (RuntimeException e) {
1✔
1092
      throw new CacheWriterException("Exception in CacheWriter", e);
1✔
1093
    }
1✔
1094
  }
1✔
1095

1096
  /** Checks that the cache is not closed. */
1097
  protected final void requireNotClosed() {
1098
    if (isClosed()) {
1✔
1099
      throw new IllegalStateException();
1✔
1100
    }
1101
  }
1✔
1102

1103
  /**
1104
   * Returns a copy of the value if value-based caching is enabled.
1105
   *
1106
   * @param object the object to be copied
1107
   * @param <T> the type of object being copied
1108
   * @return a copy of the object if storing by value or the same instance if by reference
1109
   */
1110
  @SuppressWarnings("NullAway")
1111
  protected final <T> T copyOf(@Nullable T object) {
1112
    if (object == null) {
1✔
1113
      return null;
1✔
1114
    }
1115
    T copy = copier.copy(object, cacheManager.getClassLoader());
1✔
1116
    return requireNonNull(copy);
1✔
1117
  }
1118

1119
  /**
1120
   * Returns a copy of the value if value-based caching is enabled.
1121
   *
1122
   * @param expirable the expirable value to be copied
1123
   * @return a copy of the value if storing by value or the same instance if by reference
1124
   */
1125
  @SuppressWarnings("NullAway")
1126
  protected final V copyValue(@Nullable Expirable<V> expirable) {
1127
    if (expirable == null) {
1✔
1128
      return null;
1✔
1129
    }
1130
    V copy = copier.copy(expirable.get(), cacheManager.getClassLoader());
1✔
1131
    return requireNonNull(copy);
1✔
1132
  }
1133

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

1148
  /** Returns the current time in milliseconds. */
1149
  protected final long currentTimeMillis() {
1150
    return nanosToMillis(ticker.read());
1✔
1151
  }
1152

1153
  /** Returns the nanosecond time in milliseconds. */
1154
  protected static long nanosToMillis(long nanos) {
1155
    return TimeUnit.NANOSECONDS.toMillis(nanos);
1✔
1156
  }
1157

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

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

1214
  /** An iterator to safely expose the cache entries. */
1215
  final class EntryIterator implements Iterator<Cache.Entry<K, V>> {
1✔
1216
    // NullAway does not yet understand the @NonNull annotation in the return type of asMap.
1217
    @SuppressWarnings("NullAway")
1✔
1218
    final Iterator<Map.Entry<K, Expirable<V>>> delegate = cache.asMap().entrySet().iterator();
1✔
1219
    Map.@Nullable Entry<K, Expirable<V>> current;
1220
    Map.@Nullable Entry<K, Expirable<V>> cursor;
1221

1222
    @Override
1223
    public boolean hasNext() {
1224
      while ((cursor == null) && delegate.hasNext()) {
1✔
1225
        Map.Entry<K, Expirable<V>> entry = delegate.next();
1✔
1226
        long millis = entry.getValue().isEternal() ? 0L : currentTimeMillis();
1✔
1227
        if (!entry.getValue().hasExpired(millis)) {
1✔
1228
          setAccessExpireTime(entry.getKey(), entry.getValue(), millis);
1✔
1229
          cursor = entry;
1✔
1230
        }
1231
      }
1✔
1232
      return (cursor != null);
1✔
1233
    }
1234

1235
    @Override
1236
    public Cache.Entry<K, V> next() {
1237
      if (!hasNext()) {
1✔
1238
        throw new NoSuchElementException();
1✔
1239
      }
1240
      current = cursor;
1✔
1241
      cursor = null;
1✔
1242
      @SuppressWarnings("NullAway")
1243
      var entry = new EntryProxy<>(copyOf(current.getKey()), copyValue(current.getValue()));
1✔
1244
      return entry;
1✔
1245
    }
1246

1247
    @Override
1248
    public void remove() {
1249
      if (current == null) {
1✔
1250
        throw new IllegalStateException();
1✔
1251
      }
1252
      CacheProxy.this.remove(current.getKey(), current.getValue().get());
1✔
1253
      current = null;
1✔
1254
    }
1✔
1255
  }
1256

1257
  protected static final class PutResult<V> {
1✔
1258
    @Nullable V oldValue;
1259
    boolean written;
1260
  }
1261

1262
  protected enum NullCompletionListener implements CompletionListener {
1✔
1263
    INSTANCE;
1✔
1264

1265
    @Override
1266
    public void onCompletion() {}
1✔
1267

1268
    @Override
1269
    public void onException(Exception e) {}
1✔
1270
  }
1271
}
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