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

ben-manes / caffeine / #4835

04 May 2025 09:57PM UTC coverage: 99.29% (+0.01%) from 99.277%
#4835

push

github

ben-manes
switch to a maintained csv library

7692 of 7747 relevant lines covered (99.29%)

0.99 hits per line

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

99.25
/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
      } else {
548
        dispatcher.publishRemoved(this, key, expirable.get());
1✔
549
        removed[0] = expirable.get();
1✔
550
      }
551
      return null;
1✔
552
    });
553
    return removed[0];
1✔
554
  }
555

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

563
    boolean statsEnabled = statistics.isEnabled();
1✔
564
    long start = statsEnabled ? ticker.read() : 0L;
1✔
565

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1237
    @Override
1238
    public Cache.Entry<K, V> next() {
1239
      if (!hasNext()) {
1✔
1240
        throw new NoSuchElementException();
1✔
1241
      }
1242
      current = requireNonNull(cursor);
1✔
1243
      cursor = null;
1✔
1244
      return new EntryProxy<>(copyOf(current.getKey()), copyValue(current.getValue()));
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