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

ben-manes / caffeine / #5638

14 Jul 2026 08:25PM UTC coverage: 99.964% (-0.02%) from 99.988%
#5638

push

github

ben-manes
Publish an EntryProcessor's expired prior before the processor runs

Mirror BoundedLocalCache's evict-before-callback ordering in the jcache
invoke path. A lazily-expired prior is reconciled inline before the
processor runs: it publishes EXPIRED and counts the eviction up front,
then the processor sees the entry as absent. On any Throwable from the
processor or a write-through CacheWriter, the expired prior's removal is
committed, its synchronous EXPIRED listener is awaited, and the failure
is rethrown after the compute returns (catch-commit-rethrow) via
processorFailure -- an Error as-is, otherwise an EntryProcessorException
per Cache.invoke's contract. So a failed invoke still fires exactly one
EXPIRED and one eviction; the expiration is a clock fact, not contingent
on the operation succeeding.

postProcess is now expiry-free: a null prior means the entry was absent,
so READ/UPDATED (which require a live prior) never observe one and their
requireNonNull is a proven invariant. This drops the hoisted null that
made READ/UPDATED look NPE-prone (superseding #1992) and the
currentTimeMillis == 0 re-read sentinel.

Tests: postProcess reduced to 2-arg replay plus an eternal invoke guard
(isEternal shields hasExpired from a negative-clock overflow); and
invoke-level guards for an expired prior under create, remove, a thrown
RuntimeException and Error, an awaited synchronous listener on the async
executor, and EXPIRED-before-CREATED ordering. Verified with
:jcache:test and :jcache:tckTest.

4137 of 4148 branches covered (99.73%)

24 of 26 new or added lines in 1 file covered. (92.31%)

8411 of 8414 relevant lines covered (99.96%)

1.0 hits per line

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

99.73
/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.CacheException;
49
import javax.cache.CacheManager;
50
import javax.cache.configuration.CacheEntryListenerConfiguration;
51
import javax.cache.configuration.Configuration;
52
import javax.cache.expiry.Duration;
53
import javax.cache.expiry.ExpiryPolicy;
54
import javax.cache.integration.CacheLoader;
55
import javax.cache.integration.CacheLoaderException;
56
import javax.cache.integration.CacheWriter;
57
import javax.cache.integration.CacheWriterException;
58
import javax.cache.integration.CompletionListener;
59
import javax.cache.processor.EntryProcessor;
60
import javax.cache.processor.EntryProcessorException;
61
import javax.cache.processor.EntryProcessorResult;
62

63
import org.jspecify.annotations.NonNull;
64
import org.jspecify.annotations.Nullable;
65

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

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

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

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

105
  private volatile boolean closed;
106

107
  @SuppressWarnings({"PMD.ExcessiveParameterList", "this-escape", "TooManyParameters"})
108
  public CacheProxy(String name, Executor executor, CacheManagerImpl cacheManager,
109
      CaffeineConfiguration<K, V> configuration,
110
      com.github.benmanes.caffeine.cache.Cache<K, @Nullable Expirable<V>> cache,
111
      EventDispatcher<K, V> dispatcher, Optional<CacheLoader<K, V>> cacheLoader,
112
      ExpiryPolicy expiry, Ticker ticker, JCacheStatisticsMXBean statistics, Copier copier) {
1✔
113
    this.writer = requireNonNullElse(configuration.getCacheWriter(), DisabledCacheWriter.get());
1✔
114
    this.configuration = requireNonNull(configuration);
1✔
115
    this.cacheManager = requireNonNull(cacheManager);
1✔
116
    this.cacheLoader = requireNonNull(cacheLoader);
1✔
117
    this.inFlight = ConcurrentHashMap.newKeySet();
1✔
118
    this.dispatcher = requireNonNull(dispatcher);
1✔
119
    this.statistics = requireNonNull(statistics);
1✔
120
    this.cacheMxBean = new JCacheMXBean(this);
1✔
121
    this.executor = requireNonNull(executor);
1✔
122
    this.expiry = requireNonNull(expiry);
1✔
123
    this.ticker = requireNonNull(ticker);
1✔
124
    this.cache = requireNonNull(cache);
1✔
125
    this.copier = requireNonNull(copier);
1✔
126
    this.name = requireNonNull(name);
1✔
127
  }
1✔
128

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

151
  @Override
152
  public @Nullable V get(K key) {
153
    requireNotClosed();
1✔
154
    boolean statsEnabled = statistics.isEnabled();
1✔
155
    long start = statsEnabled ? ticker.read() : 0L;
1✔
156

157
    Expirable<V> expirable = cache.getIfPresent(key);
1✔
158
    if (expirable == null) {
1✔
159
      statistics.recordMisses(1L);
1✔
160
      if (statsEnabled) {
1✔
161
        statistics.recordGetTime(ticker.read() - start);
1✔
162
      }
163
      return null;
1✔
164
    }
165

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

190
    setAccessExpireTime(key, expirable, millis);
1✔
191
    V value = copyOf(expirable.get());
1✔
192
    if (statsEnabled) {
1✔
193
      statistics.recordHits(1L);
1✔
194
      statistics.recordGetTime(ticker.read() - start);
1✔
195
    }
196
    return value;
1✔
197
  }
198

199
  @Override
200
  public Map<K, V> getAll(Set<? extends K> keys) {
201
    requireNotClosed();
1✔
202

203
    boolean statsEnabled = statistics.isEnabled();
1✔
204
    long now = statsEnabled ? ticker.read() : 0L;
1✔
205
    try {
206
      Map<K, Expirable<V>> result = getAndFilterExpiredEntries(keys);
1✔
207
      if (statsEnabled) {
1✔
208
        statistics.recordGetTime(ticker.read() - now);
1✔
209
      }
210
      return copyMap(result);
1✔
211
    } finally {
212
      dispatcher.awaitSynchronous();
1✔
213
    }
214
  }
215

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

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

249
  @Override
250
  @SuppressWarnings({"CollectionUndefinedEquality", "FutureReturnValueIgnored"})
251
  public void loadAll(Set<? extends K> keys, boolean replaceExistingValues,
252
      @Nullable CompletionListener completionListener) {
253
    requireNotClosed();
1✔
254
    keys.forEach(Objects::requireNonNull);
1✔
255
    CompletionListener listener = (completionListener == null)
1✔
256
        ? NullCompletionListener.INSTANCE
1✔
257
        : completionListener;
1✔
258
    if (cacheLoader.isEmpty()) {
1✔
259
      listener.onCompletion();
1✔
260
      return;
1✔
261
    }
262

263
    var future = new CompletableFuture<@Nullable Void>();
1✔
264
    synchronized (configuration) {
1✔
265
      requireNotClosed();
1✔
266
      inFlight.add(future);
1✔
267
    }
1✔
268
    try {
269
      CompletableFuture.runAsync(() -> {
1✔
270
        @Var boolean success = false;
1✔
271
        try {
272
          if (replaceExistingValues) {
1✔
273
            loadAllAndReplaceExisting(keys);
1✔
274
          } else {
275
            loadAllAndKeepExisting(keys);
1✔
276
          }
277
          success = true;
1✔
278
        } catch (CacheLoaderException e) {
1✔
279
          listener.onException(e);
1✔
280
        } catch (RuntimeException e) {
1✔
281
          listener.onException(new CacheLoaderException(e));
1✔
282
        } finally {
283
          dispatcher.ignoreSynchronous();
1✔
284
        }
285
        if (success) {
1✔
286
          listener.onCompletion();
1✔
287
        }
288
      }, executor).whenComplete((r, e) -> {
1✔
289
        inFlight.remove(future);
1✔
290
        future.complete(null);
1✔
291
      });
1✔
292
    } catch (RuntimeException e) {
1✔
293
      inFlight.remove(future);
1✔
294
      future.complete(null);
1✔
295
      listener.onException(new CacheLoaderException(e));
1✔
296
    }
1✔
297
  }
1✔
298

299
  /** Performs the bulk load where the existing entries are replaced. */
300
  protected void loadAllAndReplaceExisting(Set<? extends K> keys) {
301
    Map<K, V> loaded = cacheLoader.orElseThrow().loadAll(keys);
1✔
302
    for (var entry : loaded.entrySet()) {
1✔
303
      if ((entry.getKey() != null) && (entry.getValue() != null)) {
1✔
304
        putNoCopyOrAwait(entry.getKey(), entry.getValue(), /* publishToWriter= */ false);
1✔
305
      }
306
    }
1✔
307
  }
1✔
308

309
  /** Performs the bulk load where the existing entries are retained. */
310
  @SuppressWarnings("ConstantValue")
311
  protected void loadAllAndKeepExisting(Set<? extends K> keys) {
312
    List<K> keysToLoad = keys.stream()
1✔
313
        .filter(key -> {
1✔
314
          var expirable = cache.policy().getIfPresentQuietly(key);
1✔
315
          return (expirable == null)
1✔
316
              || (!expirable.isEternal() && expirable.hasExpired(currentTimeMillis()));
1✔
317
        }).collect(toUnmodifiableList());
1✔
318
    Map<K, V> result = cacheLoader.orElseThrow().loadAll(keysToLoad);
1✔
319
    for (var entry : result.entrySet()) {
1✔
320
      if ((entry.getKey() != null) && (entry.getValue() != null)) {
1✔
321
        putIfAbsentNoAwait(entry.getKey(), entry.getValue(), /* publishToWriter= */ false);
1✔
322
      }
323
    }
1✔
324
  }
1✔
325

326
  @Override
327
  public void put(K key, V value) {
328
    requireNotClosed();
1✔
329
    boolean statsEnabled = statistics.isEnabled();
1✔
330
    long start = statsEnabled ? ticker.read() : 0L;
1✔
331

332
    var result = putNoCopyOrAwait(key, value, /* publishToWriter= */ true);
1✔
333
    dispatcher.awaitSynchronous();
1✔
334

335
    if (statsEnabled) {
1✔
336
      if (result.written) {
1✔
337
        statistics.recordPuts(1);
1✔
338
      }
339
      statistics.recordPutTime(ticker.read() - start);
1✔
340
    }
341
  }
1✔
342

343
  @Override
344
  public @Nullable V getAndPut(K key, V value) {
345
    requireNotClosed();
1✔
346
    boolean statsEnabled = statistics.isEnabled();
1✔
347
    long start = statsEnabled ? ticker.read() : 0L;
1✔
348

349
    var result = putNoCopyOrAwait(key, value, /* publishToWriter= */ true);
1✔
350
    dispatcher.awaitSynchronous();
1✔
351

352
    if (statsEnabled) {
1✔
353
      if (result.oldValue == null) {
1✔
354
        statistics.recordMisses(1L);
1✔
355
      } else {
356
        statistics.recordHits(1L);
1✔
357
      }
358
      if (result.written) {
1✔
359
        statistics.recordPuts(1);
1✔
360
      }
361
      long duration = ticker.read() - start;
1✔
362
      statistics.recordGetTime(duration);
1✔
363
      statistics.recordPutTime(duration);
1✔
364
    }
365
    return (result.oldValue == null) ? null : copyOf(result.oldValue);
1✔
366
  }
367

368
  /**
369
   * Associates the specified value with the specified key in the cache.
370
   *
371
   * @param key key with which the specified value is to be associated
372
   * @param value value to be associated with the specified key
373
   * @param publishToWriter if the writer should be notified
374
   * @return the old value
375
   */
376
  @CanIgnoreReturnValue
377
  protected PutResult<V> putNoCopyOrAwait(K key, V value, boolean publishToWriter) {
378
    requireNonNull(key);
1✔
379
    requireNonNull(value);
1✔
380

381
    V newValue = copyOf(value);
1✔
382
    var result = new PutResult<V>();
1✔
383
    cache.asMap().compute(copyOf(key), (K k, @Var Expirable<V> expirable) -> {
1✔
384
      if (publishToWriter) {
1✔
385
        publishToCacheWriter(writer::write, () -> new EntryProxy<>(key, value));
1✔
386
      }
387
      if ((expirable != null) && !expirable.isEternal()
1✔
388
          && expirable.hasExpired(currentTimeMillis())) {
1✔
389
        dispatcher.publishExpired(this, key, expirable.get());
1✔
390
        statistics.recordEvictions(1L);
1✔
391
        expirable = null;
1✔
392
      }
393
      @Var long expireTimeMillis = getWriteExpireTimeMillis((expirable == null));
1✔
394
      if ((expirable != null) && (expireTimeMillis == Long.MIN_VALUE)) {
1✔
395
        expireTimeMillis = expirable.getExpireTimeMillis();
1✔
396
      }
397
      if ((expireTimeMillis == 0) && (expirable == null)) {
1✔
398
        result.written = false;
1✔
399
        dispatcher.publishExpired(this, key, newValue);
1✔
400
        return null;
1✔
401
      } else if (expirable == null) {
1✔
402
        dispatcher.publishCreated(this, key, newValue);
1✔
403
      } else {
404
        result.oldValue = expirable.get();
1✔
405
        dispatcher.publishUpdated(this, key, expirable.get(), newValue);
1✔
406
      }
407
      result.written = true;
1✔
408
      return new Expirable<>(newValue, expireTimeMillis);
1✔
409
    });
410
    return result;
1✔
411
  }
412

413
  @Override
414
  public void putAll(Map<? extends K, ? extends V> map) {
415
    requireNotClosed();
1✔
416
    for (var entry : map.entrySet()) {
1✔
417
      requireNonNull(entry.getKey());
1✔
418
      requireNonNull(entry.getValue());
1✔
419
    }
1✔
420

421
    @Var CacheWriterException error = null;
1✔
422
    @Var Set<? extends K> failedKeys = Set.of();
1✔
423
    boolean statsEnabled = statistics.isEnabled();
1✔
424
    long start = statsEnabled ? ticker.read() : 0L;
1✔
425
    if (configuration.isWriteThrough() && !map.isEmpty()) {
1✔
426
      var entries = new ArrayList<Cache.Entry<? extends K, ? extends V>>(map.size());
1✔
427
      for (var entry : map.entrySet()) {
1✔
428
        entries.add(new EntryProxy<>(entry.getKey(), entry.getValue()));
1✔
429
      }
1✔
430
      try {
431
        writer.writeAll(entries);
1✔
432
      } catch (CacheWriterException e) {
1✔
433
        failedKeys = entries.stream().map(Cache.Entry::getKey).collect(toSet());
1✔
434
        error = e;
1✔
435
      } catch (RuntimeException e) {
1✔
436
        failedKeys = entries.stream().map(Cache.Entry::getKey).collect(toSet());
1✔
437
        error = new CacheWriterException("Exception in CacheWriter", e);
1✔
438
      }
1✔
439
    }
440

441
    @Var int puts = 0;
1✔
442
    try {
443
      for (var entry : map.entrySet()) {
1✔
444
        if (!failedKeys.contains(entry.getKey())) {
1✔
445
          var result = putNoCopyOrAwait(entry.getKey(),
1✔
446
              entry.getValue(), /* publishToWriter= */ false);
1✔
447
          if (result.written) {
1✔
448
            puts++;
1✔
449
          }
450
        }
451
      }
1✔
452
    } finally {
453
      dispatcher.awaitSynchronous();
1✔
454
    }
455

456
    if (statsEnabled) {
1✔
457
      statistics.recordPuts(puts);
1✔
458
      statistics.recordPutTime(ticker.read() - start);
1✔
459
    }
460
    if (error != null) {
1✔
461
      throw error;
1✔
462
    }
463
  }
1✔
464

465
  @Override
466
  public boolean putIfAbsent(K key, V value) {
467
    requireNotClosed();
1✔
468
    requireNonNull(value);
1✔
469
    boolean statsEnabled = statistics.isEnabled();
1✔
470
    long start = statsEnabled ? ticker.read() : 0L;
1✔
471

472
    boolean added = putIfAbsentNoAwait(key, value, /* publishToWriter= */ true);
1✔
473
    dispatcher.awaitSynchronous();
1✔
474

475
    if (statsEnabled) {
1✔
476
      if (added) {
1✔
477
        statistics.recordPuts(1L);
1✔
478
        statistics.recordMisses(1L);
1✔
479
      } else {
480
        statistics.recordHits(1L);
1✔
481
      }
482
      statistics.recordPutTime(ticker.read() - start);
1✔
483
    }
484
    return added;
1✔
485
  }
486

487
  /**
488
   * Associates the specified value with the specified key in the cache if there is no existing
489
   * mapping.
490
   *
491
   * @param key key with which the specified value is to be associated
492
   * @param value value to be associated with the specified key
493
   * @param publishToWriter if the writer should be notified
494
   * @return if the mapping was successful
495
   */
496
  @CanIgnoreReturnValue
497
  private boolean putIfAbsentNoAwait(K key, V value, boolean publishToWriter) {
498
    boolean[] absent = { false };
1✔
499
    cache.asMap().compute(copyOf(key), (K k, Expirable<V> expirable) -> {
1✔
500
      if ((expirable != null)
1✔
501
          && (expirable.isEternal() || !expirable.hasExpired(currentTimeMillis()))) {
1✔
502
        return expirable;
1✔
503
      }
504

505
      V copy = copyOf(value);
1✔
506
      if (publishToWriter) {
1✔
507
        publishToCacheWriter(writer::write, () -> new EntryProxy<>(key, value));
1✔
508
      }
509
      if (expirable != null) {
1✔
510
        dispatcher.publishExpired(this, key, expirable.get());
1✔
511
        statistics.recordEvictions(1L);
1✔
512
      }
513

514
      long expireTimeMillis = getWriteExpireTimeMillis(/* created= */ true);
1✔
515
      if (expireTimeMillis == 0) {
1✔
516
        // A zero creation expiry means the entry is already expired and is not added
517
        dispatcher.publishExpired(this, key, copy);
1✔
518
        return null;
1✔
519
      } else {
520
        absent[0] = true;
1✔
521
        dispatcher.publishCreated(this, key, copy);
1✔
522
        return new Expirable<>(copy, expireTimeMillis);
1✔
523
      }
524
    });
525
    return absent[0];
1✔
526
  }
527

528
  @Override
529
  public boolean remove(K key) {
530
    requireNotClosed();
1✔
531
    requireNonNull(key);
1✔
532
    boolean statsEnabled = statistics.isEnabled();
1✔
533
    long start = statsEnabled ? ticker.read() : 0L;
1✔
534

535
    publishToCacheWriter(writer::delete, () -> key);
1✔
536
    V value = removeNoCopyOrAwait(key);
1✔
537
    dispatcher.awaitSynchronous();
1✔
538

539
    if (statsEnabled) {
1✔
540
      statistics.recordRemoveTime(ticker.read() - start);
1✔
541
    }
542
    if (value != null) {
1✔
543
      statistics.recordRemovals(1L);
1✔
544
      return true;
1✔
545
    }
546
    return false;
1✔
547
  }
548

549
  /**
550
   * Removes the mapping from the cache without store-by-value copying nor waiting for synchronous
551
   * listeners to complete.
552
   *
553
   * @param key key whose mapping is to be removed from the cache
554
   * @return the old value
555
   */
556
  private @Nullable V removeNoCopyOrAwait(K key) {
557
    @SuppressWarnings("unchecked")
558
    var removed = (V[]) new Object[1];
1✔
559
    cache.asMap().computeIfPresent(key, (k, expirable) -> {
1✔
560
      if (!expirable.isEternal() && expirable.hasExpired(currentTimeMillis())) {
1✔
561
        dispatcher.publishExpired(this, key, expirable.get());
1✔
562
        statistics.recordEvictions(1L);
1✔
563
      } else {
564
        dispatcher.publishRemoved(this, key, expirable.get());
1✔
565
        removed[0] = expirable.get();
1✔
566
      }
567
      return null;
1✔
568
    });
569
    return removed[0];
1✔
570
  }
571

572
  @Override
573
  @CanIgnoreReturnValue
574
  public boolean remove(K key, V oldValue) {
575
    requireNotClosed();
1✔
576
    requireNonNull(key);
1✔
577
    requireNonNull(oldValue);
1✔
578

579
    boolean statsEnabled = statistics.isEnabled();
1✔
580
    long start = statsEnabled ? ticker.read() : 0L;
1✔
581
    boolean[] found = { false };
1✔
582

583
    boolean[] removed = { false };
1✔
584
    cache.asMap().computeIfPresent(key, (k, expirable) -> {
1✔
585
      long millis = expirable.isEternal()
1✔
586
          ? 0L
1✔
587
          : nanosToMillis((start == 0L) ? ticker.read() : start);
1✔
588
      if (expirable.hasExpired(millis)) {
1✔
589
        dispatcher.publishExpired(this, key, expirable.get());
1✔
590
        statistics.recordEvictions(1L);
1✔
591
        return null;
1✔
592
      }
593

594
      found[0] = true;
1✔
595
      if (oldValue.equals(expirable.get())) {
1✔
596
        publishToCacheWriter(writer::delete, () -> key);
1✔
597
        dispatcher.publishRemoved(this, key, expirable.get());
1✔
598
        removed[0] = true;
1✔
599
        return null;
1✔
600
      }
601
      setAccessExpireTime(key, expirable, millis);
1✔
602
      return expirable;
1✔
603
    });
604
    dispatcher.awaitSynchronous();
1✔
605
    if (statsEnabled) {
1✔
606
      if (removed[0]) {
1✔
607
        statistics.recordRemovals(1L);
1✔
608
        statistics.recordHits(1L);
1✔
609
      } else if (found[0]) {
1✔
610
        statistics.recordHits(1L);
1✔
611
      } else {
612
        statistics.recordMisses(1L);
1✔
613
      }
614
      statistics.recordRemoveTime(ticker.read() - start);
1✔
615
    }
616
    return removed[0];
1✔
617
  }
618

619
  @Override
620
  public @Nullable V getAndRemove(K key) {
621
    requireNotClosed();
1✔
622
    requireNonNull(key);
1✔
623
    boolean statsEnabled = statistics.isEnabled();
1✔
624
    long start = statsEnabled ? ticker.read() : 0L;
1✔
625

626
    publishToCacheWriter(writer::delete, () -> key);
1✔
627
    V value = removeNoCopyOrAwait(key);
1✔
628
    dispatcher.awaitSynchronous();
1✔
629

630
    V copy = (value == null) ? null : copyOf(value);
1✔
631
    if (statsEnabled) {
1✔
632
      if (copy == null) {
1✔
633
        statistics.recordMisses(1L);
1✔
634
      } else {
635
        statistics.recordHits(1L);
1✔
636
        statistics.recordRemovals(1L);
1✔
637
      }
638
      long duration = ticker.read() - start;
1✔
639
      statistics.recordRemoveTime(duration);
1✔
640
      statistics.recordGetTime(duration);
1✔
641
    }
642
    return copy;
1✔
643
  }
644

645
  @Override
646
  public boolean replace(K key, V oldValue, V newValue) {
647
    requireNotClosed();
1✔
648
    requireNonNull(oldValue);
1✔
649
    requireNonNull(newValue);
1✔
650

651
    boolean statsEnabled = statistics.isEnabled();
1✔
652
    long start = statsEnabled ? ticker.read() : 0L;
1✔
653

654
    boolean[] found = { false };
1✔
655
    boolean[] replaced = { false };
1✔
656
    cache.asMap().computeIfPresent(key, (k, expirable) -> {
1✔
657
      long millis = expirable.isEternal()
1✔
658
          ? 0L
1✔
659
          : nanosToMillis((start == 0L) ? ticker.read() : start);
1✔
660
      if (expirable.hasExpired(millis)) {
1✔
661
        dispatcher.publishExpired(this, key, expirable.get());
1✔
662
        statistics.recordEvictions(1L);
1✔
663
        return null;
1✔
664
      }
665

666
      found[0] = true;
1✔
667
      Expirable<V> result;
668
      if (oldValue.equals(expirable.get())) {
1✔
669
        V copy = copyOf(newValue);
1✔
670
        publishToCacheWriter(writer::write, () -> new EntryProxy<>(key, newValue));
1✔
671
        dispatcher.publishUpdated(this, key, expirable.get(), copy);
1✔
672
        @Var long expireTimeMillis = getWriteExpireTimeMillis(/* created= */ false);
1✔
673
        if (expireTimeMillis == Long.MIN_VALUE) {
1✔
674
          expireTimeMillis = expirable.getExpireTimeMillis();
1✔
675
        }
676
        result = new Expirable<>(copy, expireTimeMillis);
1✔
677
        replaced[0] = true;
1✔
678
      } else {
1✔
679
        result = expirable;
1✔
680
        setAccessExpireTime(key, expirable, millis);
1✔
681
      }
682
      return result;
1✔
683
    });
684
    dispatcher.awaitSynchronous();
1✔
685

686
    if (statsEnabled) {
1✔
687
      statistics.recordPuts(replaced[0] ? 1L : 0L);
1✔
688
      statistics.recordMisses(found[0] ? 0L : 1L);
1✔
689
      statistics.recordHits(found[0] ? 1L : 0L);
1✔
690
      long duration = ticker.read() - start;
1✔
691
      statistics.recordGetTime(duration);
1✔
692
      statistics.recordPutTime(duration);
1✔
693
    }
694

695
    return replaced[0];
1✔
696
  }
697

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

704
    @Nullable V oldValue = replaceNoCopyOrAwait(key, value);
1✔
705
    dispatcher.awaitSynchronous();
1✔
706
    if (oldValue == null) {
1✔
707
      statistics.recordMisses(1L);
1✔
708
      if (statsEnabled) {
1✔
709
        statistics.recordGetTime(ticker.read() - start);
1✔
710
      }
711
      return false;
1✔
712
    }
713

714
    if (statsEnabled) {
1✔
715
      statistics.recordHits(1L);
1✔
716
      statistics.recordPuts(1L);
1✔
717
      long duration = ticker.read() - start;
1✔
718
      statistics.recordGetTime(duration);
1✔
719
      statistics.recordPutTime(duration);
1✔
720
    }
721
    return true;
1✔
722
  }
723

724
  @Override
725
  public @Nullable V getAndReplace(K key, V value) {
726
    requireNotClosed();
1✔
727
    boolean statsEnabled = statistics.isEnabled();
1✔
728
    long start = statsEnabled ? ticker.read() : 0L;
1✔
729

730
    V oldValue = replaceNoCopyOrAwait(key, value);
1✔
731
    dispatcher.awaitSynchronous();
1✔
732

733
    V copy = (oldValue == null) ? null : copyOf(oldValue);
1✔
734
    if (statsEnabled) {
1✔
735
      if (copy == null) {
1✔
736
        statistics.recordMisses(1L);
1✔
737
      } else {
738
        statistics.recordHits(1L);
1✔
739
        statistics.recordPuts(1L);
1✔
740
      }
741
      long duration = ticker.read() - start;
1✔
742
      statistics.recordGetTime(duration);
1✔
743
      statistics.recordPutTime(duration);
1✔
744
    }
745
    return copy;
1✔
746
  }
747

748
  /**
749
   * Replaces the entry for the specified key only if it is currently mapped to some value. The
750
   * entry is not store-by-value copied nor does the method wait for synchronous listeners to
751
   * complete.
752
   *
753
   * @param key key with which the specified value is associated
754
   * @param value value to be associated with the specified key
755
   * @return the old value
756
   */
757
  private @Nullable V replaceNoCopyOrAwait(K key, V value) {
758
    V copy = copyOf(value);
1✔
759
    @SuppressWarnings("unchecked")
760
    var replaced = (V[]) new Object[1];
1✔
761
    cache.asMap().computeIfPresent(key, (k, expirable) -> {
1✔
762
      if (!expirable.isEternal() && expirable.hasExpired(currentTimeMillis())) {
1✔
763
        dispatcher.publishExpired(this, key, expirable.get());
1✔
764
        statistics.recordEvictions(1L);
1✔
765
        return null;
1✔
766
      }
767

768
      publishToCacheWriter(writer::write, () -> new EntryProxy<>(key, value));
1✔
769
      @Var long expireTimeMillis = getWriteExpireTimeMillis(/* created= */ false);
1✔
770
      if (expireTimeMillis == Long.MIN_VALUE) {
1✔
771
        expireTimeMillis = expirable.getExpireTimeMillis();
1✔
772
      }
773
      dispatcher.publishUpdated(this, key, expirable.get(), copy);
1✔
774
      replaced[0] = expirable.get();
1✔
775
      return new Expirable<>(copy, expireTimeMillis);
1✔
776
    });
777
    return replaced[0];
1✔
778
  }
779

780
  @Override
781
  public void removeAll(Set<? extends K> keys) {
782
    requireNotClosed();
1✔
783
    var keysToRemove = new LinkedHashSet<>(keys);
1✔
784
    keysToRemove.forEach(Objects::requireNonNull);
1✔
785

786
    @Var CacheWriterException error = null;
1✔
787
    @Var Set<? extends K> failedKeys = Set.of();
1✔
788
    boolean statsEnabled = statistics.isEnabled();
1✔
789
    long start = statsEnabled ? ticker.read() : 0L;
1✔
790
    if (configuration.isWriteThrough() && !keysToRemove.isEmpty()) {
1✔
791
      var keysToWrite = new LinkedHashSet<>(keysToRemove);
1✔
792
      try {
793
        writer.deleteAll(keysToWrite);
1✔
794
      } catch (CacheWriterException e) {
1✔
795
        error = e;
1✔
796
        failedKeys = keysToWrite;
1✔
797
      } catch (RuntimeException e) {
1✔
798
        error = new CacheWriterException("Exception in CacheWriter", e);
1✔
799
        failedKeys = keysToWrite;
1✔
800
      }
1✔
801
    }
802

803
    @Var int removed = 0;
1✔
804
    for (var key : keysToRemove) {
1✔
805
      if (!failedKeys.contains(key) && (removeNoCopyOrAwait(key) != null)) {
1✔
806
        removed++;
1✔
807
      }
808
    }
1✔
809
    dispatcher.awaitSynchronous();
1✔
810

811
    if (statsEnabled) {
1✔
812
      statistics.recordRemovals(removed);
1✔
813
      statistics.recordRemoveTime(ticker.read() - start);
1✔
814
    }
815
    if (error != null) {
1✔
816
      throw error;
1✔
817
    }
818
  }
1✔
819

820
  @Override
821
  public void removeAll() {
822
    removeAll(cache.asMap().keySet());
1✔
823
  }
1✔
824

825
  @Override
826
  public void clear() {
827
    requireNotClosed();
1✔
828
    cache.invalidateAll();
1✔
829
  }
1✔
830

831
  @Override
832
  public <C extends Configuration<K, V>> C getConfiguration(Class<C> clazz) {
833
    if (clazz.isInstance(configuration)) {
1✔
834
      synchronized (configuration) {
1✔
835
        return clazz.cast(configuration.immutableCopy());
1✔
836
      }
837
    }
838
    throw new IllegalArgumentException("The configuration class " + clazz
1✔
839
        + " is not supported by this implementation");
840
  }
841

842
  @Override
843
  public <T extends @Nullable Object> T invoke(K key,
844
      EntryProcessor<K, V, T> entryProcessor, Object... arguments) {
845
    requireNonNull(entryProcessor);
1✔
846
    requireNonNull(arguments);
1✔
847
    requireNotClosed();
1✔
848

849
    var result = new Object[1];
1✔
850
    var failure = new Throwable[1];
1✔
851
    BiFunction<K, Expirable<V>, Expirable<V>> remappingFunction = (k, expirable) -> {
1✔
852
      // Publish a lazily-expired prior's expiration before the processor observes it as absent,
853
      // so listeners see a linearizable sequence and the expiration is committed exactly once
854
      boolean expired;
855
      Expirable<V> prior;
856
      if ((expirable != null) && !expirable.isEternal()
1✔
857
          && expirable.hasExpired(currentTimeMillis())) {
1✔
858
        dispatcher.publishExpired(this, key, expirable.get());
1✔
859
        statistics.recordEvictions(1L);
1✔
860
        expired = true;
1✔
861
        prior = null;
1✔
862
      } else {
863
        prior = expirable;
1✔
864
        expired = false;
1✔
865
      }
866

867
      V value;
868
      if (prior == null) {
1✔
869
        statistics.recordMisses(1L);
1✔
870
        value = null;
1✔
871
      } else {
872
        value = copyOf(prior.get());
1✔
873
        statistics.recordHits(1L);
1✔
874
      }
875
      var entry = new EntryProcessorEntry<>(key, value,
1✔
876
          configuration.isReadThrough() ? cacheLoader : Optional.empty());
1✔
877
      try {
878
        result[0] = entryProcessor.process(entry, arguments);
1✔
879
        return postProcess(prior, entry);
1✔
880
      } catch (Throwable e) {
1✔
881
        if (!expired) {
1✔
NEW
882
          throw processorFailure(e);
×
883
        }
884
        failure[0] = e;
1✔
885
        return null;
1✔
886
      }
887
    };
888
    try {
889
      cache.asMap().compute(copyOf(key), remappingFunction);
1✔
890
    } catch (Throwable t) {
1✔
891
      dispatcher.ignoreSynchronous();
1✔
892
      throw t;
1✔
893
    }
1✔
894
    dispatcher.awaitSynchronous();
1✔
895
    if (failure[0] != null) {
1✔
NEW
896
      throw processorFailure(failure[0]);
×
897
    }
898

899
    @SuppressWarnings("unchecked")
900
    var castedResult = (T) result[0];
1✔
901
    return castedResult;
1✔
902
  }
903

904
  /** Rethrows on an entry processor failure. */
905
  private static RuntimeException processorFailure(Throwable e) {
906
    if (e instanceof Error) {
1✔
907
      throw (Error) e;
1✔
908
    } else if (e instanceof EntryProcessorException) {
1✔
909
      throw (EntryProcessorException) e;
1✔
910
    }
911
    throw new EntryProcessorException(e);
1✔
912
  }
913

914
  /**
915
   * Returns the updated expirable value after performing the post-processing actions. A null
916
   * {@code expirable} means the entry was absent and READ/UPDATED (which require a live prior)
917
   * never observe one.
918
   */
919
  @SuppressWarnings("fallthrough")
920
  @Nullable Expirable<V> postProcess(
921
      @Nullable Expirable<V> expirable, EntryProcessorEntry<K, V> entry) {
922
    switch (entry.getAction()) {
1✔
923
      case NONE:
924
        return expirable;
1✔
925
      case READ: {
926
        setAccessExpireTime(entry.getKey(), requireNonNull(expirable), 0L);
1✔
927
        return expirable;
1✔
928
      }
929
      case CREATED:
930
        publishToCacheWriter(writer::write, () -> entry);
1✔
931
        // fallthrough
932
      case LOADED: {
933
        V value = copyOf(requireNonNull(entry.getValue()));
1✔
934
        long expireTimeMillis = getWriteExpireTimeMillis(/* created= */ true);
1✔
935
        if (expireTimeMillis == 0) {
1✔
936
          // A zero creation expiry means the entry is already expired and is not added, so the
937
          // create is neither counted nor published
938
          dispatcher.publishExpired(this, entry.getKey(), value);
1✔
939
          return null;
1✔
940
        }
941
        statistics.recordPuts(1L);
1✔
942
        dispatcher.publishCreated(this, entry.getKey(), value);
1✔
943
        return new Expirable<>(value, expireTimeMillis);
1✔
944
      }
945
      case UPDATED: {
946
        publishToCacheWriter(writer::write, () -> entry);
1✔
947
        statistics.recordPuts(1L);
1✔
948
        requireNonNull(expirable, "Expected a previous value but was null");
1✔
949
        V value = copyOf(requireNonNull(entry.getValue(), "Expected a new value but was null"));
1✔
950
        dispatcher.publishUpdated(this, entry.getKey(), expirable.get(), value);
1✔
951
        @Var long expireTimeMillis = getWriteExpireTimeMillis(/* created= */ false);
1✔
952
        if (expireTimeMillis == Long.MIN_VALUE) {
1✔
953
          expireTimeMillis = expirable.getExpireTimeMillis();
1✔
954
        }
955
        return new Expirable<>(value, expireTimeMillis);
1✔
956
      }
957
      case DELETED:
958
        publishToCacheWriter(writer::delete, entry::getKey);
1✔
959
        if (expirable != null) {
1✔
960
          statistics.recordRemovals(1L);
1✔
961
          dispatcher.publishRemoved(this, entry.getKey(), expirable.get());
1✔
962
        }
963
        return null;
1✔
964
    }
965
    throw new IllegalStateException("Unknown state: " + entry.getAction());
1✔
966
  }
967

968
  @Override
969
  public <T> Map<K, EntryProcessorResult<T>> invokeAll(Set<? extends K> keys,
970
      EntryProcessor<K, V, T> entryProcessor, Object... arguments) {
971
    requireNotClosed();
1✔
972
    requireNonNull(keys);
1✔
973
    requireNonNull(arguments);
1✔
974
    requireNonNull(entryProcessor);
1✔
975
    keys.forEach(Objects::requireNonNull);
1✔
976

977
    var results = new HashMap<K, EntryProcessorResult<T>>(keys.size(), 1.0f);
1✔
978
    for (K key : keys) {
1✔
979
      try {
980
        T result = invoke(key, entryProcessor, arguments);
1✔
981
        if (result != null) {
1✔
982
          results.put(key, () -> result);
1✔
983
        }
984
      } catch (EntryProcessorException e) {
1✔
985
        results.put(key, () -> { throw e; });
1✔
986
      } catch (RuntimeException e) {
1✔
987
        results.put(key, () -> { throw new EntryProcessorException(e); });
1✔
988
      }
1✔
989
    }
1✔
990
    return results;
1✔
991
  }
992

993
  @Override
994
  public String getName() {
995
    return name;
1✔
996
  }
997

998
  @Override
999
  public CacheManager getCacheManager() {
1000
    return cacheManager;
1✔
1001
  }
1002

1003
  @Override
1004
  public boolean isClosed() {
1005
    return closed;
1✔
1006
  }
1007

1008
  @Override
1009
  public void close() {
1010
    if (isClosed()) {
1✔
1011
      return;
1✔
1012
    }
1013
    synchronized (configuration) {
1✔
1014
      if (!isClosed()) {
1✔
1015
        closed = true;
1✔
1016
        try {
1017
          cacheManager.destroyCache(name, this);
1✔
1018
        } catch (IllegalStateException ignored) { /* manager already closed */ }
1✔
1019

1020
        @Var var thrown = shutdownExecutor();
1✔
1021
        thrown = tryClose(expiry, thrown);
1✔
1022
        thrown = tryClose(writer, thrown);
1✔
1023
        thrown = tryClose(cacheLoader.orElse(null), thrown);
1✔
1024
        for (Registration<K, V> registration : dispatcher.registrations()) {
1✔
1025
          thrown = tryClose(registration.getCacheEntryListener(), thrown);
1✔
1026
        }
1✔
1027
        thrown = tryClose((AutoCloseable) () ->
1✔
1028
            JmxRegistration.unregisterMxBean(this, MBeanType.STATISTICS), thrown);
1✔
1029
        thrown = tryClose((AutoCloseable) () ->
1✔
1030
            JmxRegistration.unregisterMxBean(this, MBeanType.CONFIGURATION), thrown);
1✔
1031
        if (thrown != null) {
1✔
1032
          logger.log(Level.WARNING, "Failure when closing cache resources", thrown);
1✔
1033
        }
1034
      }
1035
    }
1✔
1036
    cache.invalidateAll();
1✔
1037
  }
1✔
1038

1039
  @SuppressWarnings("FutureReturnValueIgnored")
1040
  private @Nullable Throwable shutdownExecutor() {
1041
    if (executor instanceof ExecutorService) {
1✔
1042
      @SuppressWarnings("PMD.CloseResource")
1043
      var es = (ExecutorService) executor;
1✔
1044
      es.shutdown();
1✔
1045
    }
1046

1047
    @Var Throwable thrown = null;
1✔
1048
    try {
1049
      CompletableFuture
1✔
1050
          .allOf(inFlight.toArray(CompletableFuture[]::new))
1✔
1051
          .get(10, TimeUnit.SECONDS);
1✔
1052
    } catch (ExecutionException | TimeoutException e) {
1✔
1053
      thrown = e;
1✔
1054
    } catch (InterruptedException e) {
1✔
1055
      Thread.currentThread().interrupt();
1✔
1056
      thrown = e;
1✔
1057
    }
1✔
1058
    inFlight.clear();
1✔
1059

1060
    if (!(executor instanceof ExecutorService)) {
1✔
1061
      thrown = tryClose(executor, thrown);
1✔
1062
    }
1063
    return thrown;
1✔
1064
  }
1065

1066
  /**
1067
   * Attempts to close the resource. If an error occurs and an outermost exception is set, then adds
1068
   * the error to the suppression list.
1069
   *
1070
   * @param o the resource to close if Closeable
1071
   * @param outer the outermost error, or null if unset
1072
   * @return the outermost error, or null if unset and successful
1073
   */
1074
  private static @Nullable Throwable tryClose(@Nullable Object o, @Nullable Throwable outer) {
1075
    if (o instanceof AutoCloseable) {
1✔
1076
      try {
1077
        ((AutoCloseable) o).close();
1✔
1078
      } catch (Throwable t) {
1✔
1079
        if (outer == null) {
1✔
1080
          return t;
1✔
1081
        } else if (outer != t) {
1✔
1082
          outer.addSuppressed(t);
1✔
1083
        }
1084
      }
1✔
1085
    }
1086
    return outer;
1✔
1087
  }
1088

1089
  @Override
1090
  public <T> T unwrap(Class<T> clazz) {
1091
    if (clazz.isInstance(cache)) {
1✔
1092
      return clazz.cast(cache);
1✔
1093
    } else if (clazz.isInstance(this)) {
1✔
1094
      return clazz.cast(this);
1✔
1095
    }
1096
    throw new IllegalArgumentException("Unwrapping to " + clazz
1✔
1097
        + " is not supported by this implementation");
1098
  }
1099

1100
  @Override
1101
  public void registerCacheEntryListener(
1102
      CacheEntryListenerConfiguration<K, V> cacheEntryListenerConfiguration) {
1103
    synchronized (configuration) {
1✔
1104
      requireNotClosed();
1✔
1105
      configuration.addCacheEntryListenerConfiguration(cacheEntryListenerConfiguration);
1✔
1106
      dispatcher.register(cacheEntryListenerConfiguration);
1✔
1107
    }
1✔
1108
  }
1✔
1109

1110
  @Override
1111
  public void deregisterCacheEntryListener(
1112
      CacheEntryListenerConfiguration<K, V> cacheEntryListenerConfiguration) {
1113
    synchronized (configuration) {
1✔
1114
      requireNotClosed();
1✔
1115
      configuration.removeCacheEntryListenerConfiguration(cacheEntryListenerConfiguration);
1✔
1116
      dispatcher.deregister(cacheEntryListenerConfiguration);
1✔
1117
    }
1✔
1118
  }
1✔
1119

1120
  @Override
1121
  public Iterator<Cache.Entry<K, V>> iterator() {
1122
    requireNotClosed();
1✔
1123
    return new EntryIterator();
1✔
1124
  }
1125

1126
  /** Enables or disables the configuration management JMX bean. */
1127
  void enableManagement(boolean enabled) {
1128
    synchronized (configuration) {
1✔
1129
      requireNotClosed();
1✔
1130
      if (enabled) {
1✔
1131
        JmxRegistration.registerMxBean(this, cacheMxBean, MBeanType.CONFIGURATION);
1✔
1132
      } else {
1133
        JmxRegistration.unregisterMxBean(this, MBeanType.CONFIGURATION);
1✔
1134
      }
1135
      configuration.setManagementEnabled(enabled);
1✔
1136
    }
1✔
1137
  }
1✔
1138

1139
  /** Enables or disables the statistics JMX bean. */
1140
  void enableStatistics(boolean enabled) {
1141
    synchronized (configuration) {
1✔
1142
      requireNotClosed();
1✔
1143
      if (enabled) {
1✔
1144
        JmxRegistration.registerMxBean(this, statistics, MBeanType.STATISTICS);
1✔
1145
      } else {
1146
        JmxRegistration.unregisterMxBean(this, MBeanType.STATISTICS);
1✔
1147
      }
1148
      statistics.enable(enabled);
1✔
1149
      configuration.setStatisticsEnabled(enabled);
1✔
1150
    }
1✔
1151
  }
1✔
1152

1153
  /** Performs the action with the cache writer if write-through is enabled. */
1154
  private <T> void publishToCacheWriter(Consumer<T> action, Supplier<T> data) {
1155
    if (!configuration.isWriteThrough()) {
1✔
1156
      return;
1✔
1157
    }
1158
    try {
1159
      action.accept(data.get());
1✔
1160
    } catch (CacheWriterException e) {
1✔
1161
      throw e;
1✔
1162
    } catch (RuntimeException e) {
1✔
1163
      throw new CacheWriterException("Exception in CacheWriter", e);
1✔
1164
    }
1✔
1165
  }
1✔
1166

1167
  /** Checks that the cache is not closed. */
1168
  protected final void requireNotClosed() {
1169
    if (isClosed()) {
1✔
1170
      throw new IllegalStateException();
1✔
1171
    }
1172
  }
1✔
1173

1174
  /**
1175
   * Returns a copy of the value if value-based caching is enabled.
1176
   *
1177
   * @param object the object to be copied
1178
   * @param <T> the type of object being copied
1179
   * @return a copy of the object if storing by value or the same instance if by reference
1180
   */
1181
  protected final <T> T copyOf(T object) {
1182
    try {
1183
      return requireNonNull(
1✔
1184
          copier.copy(requireNonNull(object), requireNonNull(cacheManager.getClassLoader())));
1✔
1185
    } catch (NullPointerException | IllegalStateException | ClassCastException | CacheException e) {
1✔
1186
      throw e;
1✔
1187
    } catch (RuntimeException e) {
1✔
1188
      throw new CacheException(e);
1✔
1189
    }
1190
  }
1191

1192
  /**
1193
   * Returns a deep copy of the map if value-based caching is enabled.
1194
   *
1195
   * @param map the mapping of keys to expirable values
1196
   * @return a deep or shallow copy of the mappings depending on the store by value setting
1197
   */
1198
  @SuppressWarnings("CollectorMutability")
1199
  protected final Map<K, V> copyMap(Map<K, Expirable<V>> map) {
1200
    return map.entrySet().stream().collect(toMap(
1✔
1201
        entry -> copyOf(entry.getKey()),
1✔
1202
        entry -> copyOf(entry.getValue().get())));
1✔
1203
  }
1204

1205
  /** Returns the current time in milliseconds. */
1206
  protected final long currentTimeMillis() {
1207
    return nanosToMillis(ticker.read());
1✔
1208
  }
1209

1210
  /** Returns the nanosecond time in milliseconds. */
1211
  protected static long nanosToMillis(long nanos) {
1212
    return TimeUnit.NANOSECONDS.toMillis(nanos);
1✔
1213
  }
1214

1215
  /**
1216
   * Sets the access expiration time.
1217
   *
1218
   * @param key the entry's key
1219
   * @param expirable the entry that was operated on
1220
   * @param currentTimeMillis the current time, or 0 if not read yet
1221
   */
1222
  protected final void setAccessExpireTime(K key,
1223
      Expirable<?> expirable, @Var long currentTimeMillis) {
1224
    try {
1225
      Duration duration = expiry.getExpiryForAccess();
1✔
1226
      if (duration == null) {
1✔
1227
        return;
1✔
1228
      } else if (duration.isZero()) {
1✔
1229
        expirable.setExpireTimeMillis(0L);
1✔
1230
        cache.policy().expireVariably().ifPresent(policy ->
1✔
1231
            policy.setExpiresAfter(key, 0L, TimeUnit.NANOSECONDS));
1✔
1232
      } else if (duration.isEternal()) {
1✔
1233
        expirable.setExpireTimeMillis(Long.MAX_VALUE);
1✔
1234
        cache.policy().expireVariably().ifPresent(policy ->
1✔
1235
            policy.setExpiresAfter(key, Long.MAX_VALUE, TimeUnit.NANOSECONDS));
1✔
1236
      } else {
1237
        if (currentTimeMillis == 0L) {
1✔
1238
          currentTimeMillis = currentTimeMillis();
1✔
1239
        }
1240
        @Var long expireTimeMillis = duration.getAdjustedTime(currentTimeMillis);
1✔
1241
        expireTimeMillis = ((expireTimeMillis == 0L) || (expireTimeMillis == Long.MAX_VALUE))
1✔
1242
            ? (expireTimeMillis - 1)
1✔
1243
            : expireTimeMillis;
1✔
1244
        expirable.setExpireTimeMillis(expireTimeMillis);
1✔
1245
        cache.policy().expireVariably().ifPresent(policy ->
1✔
1246
            policy.setExpiresAfter(key, duration.getDurationAmount(), duration.getTimeUnit()));
1✔
1247
      }
1248
    } catch (RuntimeException e) {
1✔
1249
      logger.log(Level.WARNING, "Failed to set the entry's expiration time", e);
1✔
1250
    }
1✔
1251
  }
1✔
1252

1253
  /**
1254
   * Returns the time when the entry will expire.
1255
   *
1256
   * @param created if the write operation is an insert or an update
1257
   * @return the time when the entry will expire, zero if it should expire immediately,
1258
   *         Long.MIN_VALUE if it should not be changed, or Long.MAX_VALUE if eternal
1259
   */
1260
  protected final long getWriteExpireTimeMillis(boolean created) {
1261
    try {
1262
      Duration duration = created ? expiry.getExpiryForCreation() : expiry.getExpiryForUpdate();
1✔
1263
      if (duration == null) {
1✔
1264
        return Long.MIN_VALUE;
1✔
1265
      } else if (duration.isZero()) {
1✔
1266
        return 0L;
1✔
1267
      } else if (duration.isEternal()) {
1✔
1268
        return Long.MAX_VALUE;
1✔
1269
      }
1270
      long expireTimeMillis = duration.getAdjustedTime(currentTimeMillis());
1✔
1271
      return ((expireTimeMillis == 0L) || (expireTimeMillis == Long.MAX_VALUE))
1✔
1272
          ? (expireTimeMillis - 1)
1✔
1273
          : expireTimeMillis;
1✔
1274
    } catch (RuntimeException e) {
1✔
1275
      logger.log(Level.WARNING, "Failed to get the policy's expiration time", e);
1✔
1276
      return created ? Long.MAX_VALUE : Long.MIN_VALUE;
1✔
1277
    }
1278
  }
1279

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

1286
    Map.@Nullable Entry<K, Expirable<V>> current;
1287
    Map.@Nullable Entry<K, Expirable<V>> cursor;
1288

1289
    @Override
1290
    public boolean hasNext() {
1291
      while ((cursor == null) && delegate.hasNext()) {
1✔
1292
        Map.Entry<K, Expirable<V>> entry = delegate.next();
1✔
1293
        long millis = entry.getValue().isEternal() ? 0L : currentTimeMillis();
1✔
1294
        if (!entry.getValue().hasExpired(millis)) {
1✔
1295
          setAccessExpireTime(entry.getKey(), entry.getValue(), millis);
1✔
1296
          cursor = entry;
1✔
1297
        }
1298
      }
1✔
1299
      return (cursor != null);
1✔
1300
    }
1301

1302
    @Override
1303
    public Cache.Entry<K, V> next() {
1304
      if (!hasNext()) {
1✔
1305
        throw new NoSuchElementException();
1✔
1306
      }
1307
      statistics.recordHits(1L);
1✔
1308
      current = requireNonNull(cursor);
1✔
1309
      cursor = null;
1✔
1310
      return new EntryProxy<>(copyOf(current.getKey()), copyOf(current.getValue().get()));
1✔
1311
    }
1312

1313
    @Override
1314
    public void remove() {
1315
      if (current == null) {
1✔
1316
        throw new IllegalStateException();
1✔
1317
      }
1318
      boolean[] removed = { false };
1✔
1319
      boolean statsEnabled = statistics.isEnabled();
1✔
1320
      long start = statsEnabled ? ticker.read() : 0L;
1✔
1321

1322
      K key = current.getKey();
1✔
1323
      V oldValue = current.getValue().get();
1✔
1324
      cache.asMap().computeIfPresent(key, (k, expirable) -> {
1✔
1325
        if (oldValue.equals(expirable.get())) {
1✔
1326
          publishToCacheWriter(writer::delete, () -> key);
1✔
1327
          dispatcher.publishRemoved(CacheProxy.this, key, expirable.get());
1✔
1328
          removed[0] = true;
1✔
1329
          return null;
1✔
1330
        }
1331
        return expirable;
1✔
1332
      });
1333
      dispatcher.awaitSynchronous();
1✔
1334
      if (removed[0]) {
1✔
1335
        statistics.recordRemovals(1L);
1✔
1336
        if (statsEnabled) {
1✔
1337
          statistics.recordRemoveTime(ticker.read() - start);
1✔
1338
        }
1339
      }
1340
      current = null;
1✔
1341
    }
1✔
1342
  }
1343

1344
  protected static final class PutResult<V> {
1✔
1345
    @Nullable V oldValue;
1346
    boolean written;
1347
  }
1348

1349
  protected enum NullCompletionListener implements CompletionListener {
1✔
1350
    INSTANCE;
1✔
1351

1352
    @Override
1353
    public void onCompletion() {}
1✔
1354

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

© 2026 Coveralls, Inc