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

ben-manes / caffeine / #5173

29 Dec 2025 05:27AM UTC coverage: 0.0% (-100.0%) from 100.0%
#5173

push

github

ben-manes
speed up development ci build

0 of 3838 branches covered (0.0%)

0 of 7869 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.NonNull;
63
import org.jspecify.annotations.Nullable;
64

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

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

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

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

104
  private volatile boolean closed;
105

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

674
    return replaced[0];
×
675
  }
676

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1098
  /** Checks that the cache is not closed. */
1099
  protected final void requireNotClosed() {
1100
    if (isClosed()) {
×
1101
      throw new IllegalStateException();
×
1102
    }
1103
  }
×
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({"DataFlowIssue", "NullAway"})
1113
  protected final <T> T copyOf(@Nullable T object) {
1114
    if (object == null) {
×
1115
      return null;
×
1116
    }
1117
    T copy = copier.copy(object, cacheManager.getClassLoader());
×
1118
    return requireNonNull(copy);
×
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({"DataFlowIssue", "NullAway"})
1128
  protected final V copyValue(@Nullable Expirable<V> expirable) {
1129
    if (expirable == null) {
×
1130
      return null;
×
1131
    }
1132
    V copy = copier.copy(expirable.get(), cacheManager.getClassLoader());
×
1133
    return requireNonNull(copy);
×
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();
×
1145
    return map.entrySet().stream().collect(toMap(
×
1146
        entry -> copier.copy(entry.getKey(), classLoader),
×
1147
        entry -> copier.copy(entry.getValue().get(), classLoader)));
×
1148
  }
1149

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

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

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

1222
    Map.@Nullable Entry<K, Expirable<V>> current;
1223
    Map.@Nullable Entry<K, Expirable<V>> cursor;
1224

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

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

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

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

1263
  protected enum NullCompletionListener implements CompletionListener {
×
1264
    INSTANCE;
×
1265

1266
    @Override
1267
    public void onCompletion() {}
×
1268

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