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

ben-manes / caffeine / #5156

03 Dec 2025 03:21AM UTC coverage: 0.0% (-100.0%) from 100.0%
#5156

push

github

ben-manes
add loading type to parameterized test dimensions to reduce task size

0 of 3834 branches covered (0.0%)

0 of 7848 relevant lines covered (0.0%)

0.0 hits per line

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

0.0
/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
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
84
public class CacheProxy<K, V> implements Cache<K, V> {
85
  private static final Logger logger = System.getLogger(CacheProxy.class.getName());
×
86

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

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

103
  private volatile boolean closed;
104

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

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

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

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

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

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

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

200
    boolean statsEnabled = statistics.isEnabled();
×
201
    long now = statsEnabled ? ticker.read() : 0L;
×
202

203
    Map<K, Expirable<V>> result = getAndFilterExpiredEntries(keys, /* updateAccessTime= */ true);
×
204

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

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

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

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

257
    if (cacheLoader.isEmpty()) {
×
258
      listener.onCompletion();
×
259
      return;
×
260
    }
261

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

279
    inFlight.add(future);
×
280
    future.whenComplete((r, e) -> inFlight.remove(future));
×
281
  }
×
282

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

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

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

310
    var result = putNoCopyOrAwait(key, value, /* publishToWriter= */ true);
×
311
    dispatcher.awaitSynchronous();
×
312

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

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

327
    var result = putNoCopyOrAwait(key, value, /* publishToWriter= */ true);
×
328
    dispatcher.awaitSynchronous();
×
329

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

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

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

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

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

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

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

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

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

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

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

457
    if (statsEnabled) {
×
458
      if (added) {
×
459
        statistics.recordPuts(1L);
×
460
        statistics.recordMisses(1L);
×
461
      } else {
462
        statistics.recordHits(1L);
×
463
      }
464
      statistics.recordPutTime(ticker.read() - start);
×
465
    }
466
    return added;
×
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 };
×
481
    cache.asMap().compute(copyOf(key), (K k, @Var Expirable<V> expirable) -> {
×
482
      if ((expirable != null) && !expirable.isEternal()
×
483
          && expirable.hasExpired(currentTimeMillis())) {
×
484
        dispatcher.publishExpired(this, key, expirable.get());
×
485
        statistics.recordEvictions(1L);
×
486
        expirable = null;
×
487
      }
488
      if (expirable != null) {
×
489
        return expirable;
×
490
      }
491
      if (publishToWriter) {
×
492
        publishToCacheWriter(writer::write, () -> new EntryProxy<>(key, value));
×
493
      }
494

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

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

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

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

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

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

562
    boolean statsEnabled = statistics.isEnabled();
×
563
    long start = statsEnabled ? ticker.read() : 0L;
×
564

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

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

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

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

623
  @Override
624
  public boolean replace(K key, V oldValue, V newValue) {
625
    requireNotClosed();
×
626
    requireNonNull(oldValue);
×
627
    requireNonNull(newValue);
×
628

629
    boolean statsEnabled = statistics.isEnabled();
×
630
    long start = statsEnabled ? ticker.read() : 0L;
×
631

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

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

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

672
    return replaced[0];
×
673
  }
674

675
  @Override
676
  public boolean replace(K key, V value) {
677
    requireNotClosed();
×
678
    boolean statsEnabled = statistics.isEnabled();
×
679
    long start = statsEnabled ? ticker.read() : 0L;
×
680

681
    V oldValue = replaceNoCopyOrAwait(key, value);
×
682
    dispatcher.awaitSynchronous();
×
683
    if (oldValue == null) {
×
684
      statistics.recordMisses(1L);
×
685
      return false;
×
686
    }
687

688
    if (statsEnabled) {
×
689
      statistics.recordHits(1L);
×
690
      statistics.recordPuts(1L);
×
691
      statistics.recordPutTime(ticker.read() - start);
×
692
    }
693
    return true;
×
694
  }
695

696
  @Override
697
  public V getAndReplace(K key, V value) {
698
    requireNotClosed();
×
699
    boolean statsEnabled = statistics.isEnabled();
×
700
    long start = statsEnabled ? ticker.read() : 0L;
×
701

702
    V oldValue = replaceNoCopyOrAwait(key, value);
×
703
    dispatcher.awaitSynchronous();
×
704
    V copy = copyOf(oldValue);
×
705

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

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

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

753
  @Override
754
  public void removeAll(Set<? extends K> keys) {
755
    requireNotClosed();
×
756
    keys.forEach(Objects::requireNonNull);
×
757

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

775
    @Var int removed = 0;
×
776
    for (var key : keys) {
×
777
      if (!failedKeys.contains(key) && (removeNoCopyOrAwait(key) != null)) {
×
778
        removed++;
×
779
      }
780
    }
×
781
    dispatcher.awaitSynchronous();
×
782

783
    if (statsEnabled) {
×
784
      statistics.recordRemovals(removed);
×
785
      statistics.recordRemoveTime(ticker.read() - start);
×
786
    }
787
    if (error != null) {
×
788
      throw error;
×
789
    }
790
  }
×
791

792
  @Override
793
  public void removeAll() {
794
    removeAll(cache.asMap().keySet());
×
795
  }
×
796

797
  @Override
798
  public void clear() {
799
    requireNotClosed();
×
800
    cache.invalidateAll();
×
801
  }
×
802

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

814
  @Override
815
  public <T> @Nullable T invoke(K key,
816
      EntryProcessor<K, V, T> entryProcessor, Object... arguments) {
817
    requireNonNull(entryProcessor);
×
818
    requireNonNull(arguments);
×
819
    requireNotClosed();
×
820

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

852
    @SuppressWarnings("unchecked")
853
    var castedResult = (T) result[0];
×
854
    return castedResult;
×
855
  }
856

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

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

930
  @Override
931
  public String getName() {
932
    return name;
×
933
  }
934

935
  @Override
936
  public CacheManager getCacheManager() {
937
    return cacheManager;
×
938
  }
939

940
  @Override
941
  public boolean isClosed() {
942
    return closed;
×
943
  }
944

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

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

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

981
    @Var Throwable thrown = null;
×
982
    try {
983
      CompletableFuture
×
984
          .allOf(inFlight.toArray(CompletableFuture[]::new))
×
985
          .get(10, TimeUnit.SECONDS);
×
986
    } catch (InterruptedException | ExecutionException | TimeoutException e) {
×
987
      thrown = e;
×
988
    }
×
989
    inFlight.clear();
×
990
    return thrown;
×
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) {
×
1003
      try {
1004
        ((AutoCloseable) o).close();
×
1005
      } catch (Throwable t) {
×
1006
        if (outer == null) {
×
1007
          return t;
×
1008
        }
1009
        outer.addSuppressed(t);
×
1010
        return outer;
×
1011
      }
×
1012
    }
1013
    return null;
×
1014
  }
1015

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

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

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

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

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

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

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

1071
    synchronized (configuration) {
×
1072
      if (enabled) {
×
1073
        JmxRegistration.registerMxBean(this, statistics, MBeanType.STATISTICS);
×
1074
      } else {
1075
        JmxRegistration.unregisterMxBean(this, MBeanType.STATISTICS);
×
1076
      }
1077
      statistics.enable(enabled);
×
1078
      configuration.setStatisticsEnabled(enabled);
×
1079
    }
×
1080
  }
×
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()) {
×
1085
      return;
×
1086
    }
1087
    try {
1088
      action.accept(data.get());
×
1089
    } catch (CacheWriterException e) {
×
1090
      throw e;
×
1091
    } catch (RuntimeException e) {
×
1092
      throw new CacheWriterException("Exception in CacheWriter", e);
×
1093
    }
×
1094
  }
×
1095

1096
  /** Checks that the cache is not closed. */
1097
  protected final void requireNotClosed() {
1098
    if (isClosed()) {
×
1099
      throw new IllegalStateException();
×
1100
    }
1101
  }
×
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) {
×
1113
      return null;
×
1114
    }
1115
    T copy = copier.copy(object, cacheManager.getClassLoader());
×
1116
    return requireNonNull(copy);
×
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) {
×
1128
      return null;
×
1129
    }
1130
    V copy = copier.copy(expirable.get(), cacheManager.getClassLoader());
×
1131
    return requireNonNull(copy);
×
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();
×
1143
    return map.entrySet().stream().collect(toMap(
×
1144
        entry -> copier.copy(entry.getKey(), classLoader),
×
1145
        entry -> copier.copy(entry.getValue().get(), classLoader)));
×
1146
  }
1147

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

1153
  /** Returns the nanosecond time in milliseconds. */
1154
  protected static long nanosToMillis(long nanos) {
1155
    return TimeUnit.NANOSECONDS.toMillis(nanos);
×
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();
×
1169
      if (duration == null) {
×
1170
        return;
×
1171
      } else if (duration.isZero()) {
×
1172
        expirable.setExpireTimeMillis(0L);
×
1173
      } else if (duration.isEternal()) {
×
1174
        expirable.setExpireTimeMillis(Long.MAX_VALUE);
×
1175
      } else {
1176
        if (currentTimeMillis == 0L) {
×
1177
          currentTimeMillis = currentTimeMillis();
×
1178
        }
1179
        long expireTimeMillis = duration.getAdjustedTime(currentTimeMillis);
×
1180
        expirable.setExpireTimeMillis(expireTimeMillis);
×
1181
      }
1182
      cache.policy().expireVariably().ifPresent(policy -> {
×
1183
        policy.setExpiresAfter(key, duration.getDurationAmount(), duration.getTimeUnit());
×
1184
      });
×
1185
    } catch (RuntimeException e) {
×
1186
      logger.log(Level.WARNING, "Failed to set the entry's expiration time", e);
×
1187
    }
×
1188
  }
×
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();
×
1200
      if (duration == null) {
×
1201
        return Long.MIN_VALUE;
×
1202
      } else if (duration.isZero()) {
×
1203
        return 0L;
×
1204
      } else if (duration.isEternal()) {
×
1205
        return Long.MAX_VALUE;
×
1206
      }
1207
      return duration.getAdjustedTime(currentTimeMillis());
×
1208
    } catch (RuntimeException e) {
×
1209
      logger.log(Level.WARNING, "Failed to get the policy's expiration time", e);
×
1210
      return Long.MIN_VALUE;
×
1211
    }
1212
  }
1213

1214
  /** An iterator to safely expose the cache entries. */
1215
  final class EntryIterator implements Iterator<Cache.Entry<K, V>> {
×
1216
    // NullAway does not yet understand the @NonNull annotation in the return type of asMap.
1217
    @SuppressWarnings("NullAway")
×
1218
    Iterator<Map.Entry<K, Expirable<V>>> delegate = cache.asMap().entrySet().iterator();
×
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()) {
×
1225
        Map.Entry<K, Expirable<V>> entry = delegate.next();
×
1226
        long millis = entry.getValue().isEternal() ? 0L : currentTimeMillis();
×
1227
        if (!entry.getValue().hasExpired(millis)) {
×
1228
          setAccessExpireTime(entry.getKey(), entry.getValue(), millis);
×
1229
          cursor = entry;
×
1230
        }
1231
      }
×
1232
      return (cursor != null);
×
1233
    }
1234

1235
    @Override
1236
    public Cache.Entry<K, V> next() {
1237
      if (!hasNext()) {
×
1238
        throw new NoSuchElementException();
×
1239
      }
1240
      current = requireNonNull(cursor);
×
1241
      cursor = null;
×
1242
      return new EntryProxy<>(copyOf(current.getKey()), copyValue(current.getValue()));
×
1243
    }
1244

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

1255
  protected static final class PutResult<V> {
×
1256
    @Nullable V oldValue;
1257
    boolean written;
1258
  }
1259

1260
  protected enum NullCompletionListener implements CompletionListener {
×
1261
    INSTANCE;
×
1262

1263
    @Override
1264
    public void onCompletion() {}
×
1265

1266
    @Override
1267
    public void onException(Exception e) {}
×
1268
  }
1269
}
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