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

ben-manes / caffeine / #5452

11 May 2026 04:59AM UTC coverage: 99.806% (-0.1%) from 99.903%
#5452

push

github

ben-manes
Fix two TCK regressions from the spec-compliance sweep

1. LoadingCacheProxy.loadAll non-replacing path now routes through
   loadAllAndKeepExisting (the #L fix), which calls the user CacheLoader
   directly and bypasses JCacheLoaderAdapter.loadAll's wrap. TCK
   CacheLoaderTest.shouldPropagateExceptionUsingLoadAll asserts the
   listener sees a CacheLoaderException — 1.1.1 relaxed the spec rule
   but the TCK still enforces. Wrap RuntimeException in the inner catch
   to match CacheProxy.loadAll's convention.

2. EntryIterator.remove() routed through CacheProxy.remove(K, V) which
   records a hit on success. After #K added a hit on each next(),
   iterate+remove double-counted (200 hits for 100 entries). TCK
   CacheMBStatisticsBeanTest.testIterateAndRemove asserts CacheHits ==
   100 after iterating and removing 100 entries. Inline a conditional
   remove that records only the removal (not the hit), matching the
   stats table p.126's split: iterator() Hits=Yes on next, Removals=Yes
   on remove().

4011 of 4026 branches covered (99.63%)

19 of 24 new or added lines in 2 files covered. (79.17%)

11 existing lines in 2 files now uncovered.

8243 of 8259 relevant lines covered (99.81%)

1.0 hits per line

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

98.64
/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());
1✔
87

88
  protected final com.github.benmanes.caffeine.cache.Cache<K, @Nullable Expirable<V>> cache;
89
  protected final CaffeineConfiguration<K, V> configuration;
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 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) {
1✔
112
    this.writer = requireNonNullElse(configuration.getCacheWriter(), DisabledCacheWriter.get());
1✔
113
    this.configuration = requireNonNull(configuration);
1✔
114
    this.cacheManager = requireNonNull(cacheManager);
1✔
115
    this.cacheLoader = requireNonNull(cacheLoader);
1✔
116
    this.dispatcher = requireNonNull(dispatcher);
1✔
117
    this.statistics = requireNonNull(statistics);
1✔
118
    this.executor = requireNonNull(executor);
1✔
119
    this.expiry = requireNonNull(expiry);
1✔
120
    this.ticker = requireNonNull(ticker);
1✔
121
    this.cache = requireNonNull(cache);
1✔
122
    this.name = requireNonNull(name);
1✔
123

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

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

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

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

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

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

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

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

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

249
    statistics.recordHits(result.size());
1✔
250
    statistics.recordMisses(keys.size() - result.size());
1✔
251
    statistics.recordEvictions(expired[0]);
1✔
252
    return result;
1✔
253
  }
254

255
  @Override
256
  @SuppressWarnings({"CollectionUndefinedEquality", "FutureReturnValueIgnored"})
257
  public void loadAll(Set<? extends K> keys, boolean replaceExistingValues,
258
      @Nullable CompletionListener completionListener) {
259
    requireNotClosed();
1✔
260
    keys.forEach(Objects::requireNonNull);
1✔
261
    CompletionListener listener = (completionListener == null)
1✔
262
        ? NullCompletionListener.INSTANCE
1✔
263
        : completionListener;
1✔
264

265
    if (cacheLoader.isEmpty()) {
1✔
266
      listener.onCompletion();
1✔
267
      return;
1✔
268
    }
269

270
    var future = new CompletableFuture<@Nullable Void>();
1✔
271
    synchronized (configuration) {
1✔
272
      requireNotClosed();
1✔
273
      inFlight.add(future);
1✔
274
    }
1✔
275
    try {
276
      CompletableFuture.runAsync(() -> {
1✔
277
        @Var boolean success = false;
1✔
278
        try {
279
          if (replaceExistingValues) {
1✔
280
            loadAllAndReplaceExisting(keys);
1✔
281
          } else {
282
            loadAllAndKeepExisting(keys);
1✔
283
          }
284
          success = true;
1✔
UNCOV
285
        } catch (CacheLoaderException e) {
×
UNCOV
286
          listener.onException(e);
×
287
        } catch (RuntimeException e) {
1✔
288
          listener.onException(new CacheLoaderException(e));
1✔
289
        } finally {
290
          dispatcher.ignoreSynchronous();
1✔
291
        }
292
        // Per JSR-107 1.1.1 p.64: success → onCompletion, failure → onException.
293
        // Both are terminal callbacks for one operation. Call onCompletion outside
294
        // the catch so a throw from it does not also fire onException.
295
        if (success) {
1✔
296
          listener.onCompletion();
1✔
297
        }
298
      }, executor).whenComplete((r, e) -> {
1✔
299
        inFlight.remove(future);
1✔
300
        future.complete(null);
1✔
301
      });
1✔
302
    } catch (RuntimeException e) {
1✔
303
      inFlight.remove(future);
1✔
304
      future.complete(null);
1✔
305
      listener.onException(new CacheLoaderException(e));
1✔
UNCOV
306
    } catch (Throwable t) {
×
UNCOV
307
      inFlight.remove(future);
×
UNCOV
308
      future.complete(null);
×
UNCOV
309
      throw t;
×
310
    }
1✔
311
  }
1✔
312

313
  /** Performs the bulk load where the existing entries are replaced. */
314
  private void loadAllAndReplaceExisting(Set<? extends K> keys) {
315
    Map<K, V> loaded = cacheLoader.orElseThrow().loadAll(keys);
1✔
316
    for (var entry : loaded.entrySet()) {
1✔
317
      putNoCopyOrAwait(entry.getKey(), entry.getValue(), /* publishToWriter= */ false);
1✔
318
    }
1✔
319
  }
1✔
320

321
  /** Performs the bulk load where the existing entries are retained. */
322
  @SuppressWarnings("ConstantValue")
323
  protected void loadAllAndKeepExisting(Set<? extends K> keys) {
324
    List<K> keysToLoad = keys.stream()
1✔
325
        .filter(key -> !cache.asMap().containsKey(key))
1✔
326
        .collect(toUnmodifiableList());
1✔
327
    Map<K, V> result = cacheLoader.orElseThrow().loadAll(keysToLoad);
1✔
328
    for (var entry : result.entrySet()) {
1✔
329
      if ((entry.getKey() != null) && (entry.getValue() != null)) {
1✔
330
        putIfAbsentNoAwait(entry.getKey(), entry.getValue(), /* publishToWriter= */ false);
1✔
331
      }
332
    }
1✔
333
  }
1✔
334

335
  @Override
336
  public void put(K key, V value) {
337
    requireNotClosed();
1✔
338
    boolean statsEnabled = statistics.isEnabled();
1✔
339
    long start = statsEnabled ? ticker.read() : 0L;
1✔
340

341
    var result = putNoCopyOrAwait(key, value, /* publishToWriter= */ true);
1✔
342
    dispatcher.awaitSynchronous();
1✔
343

344
    if (statsEnabled) {
1✔
345
      if (result.written) {
1✔
346
        statistics.recordPuts(1);
1✔
347
      }
348
      statistics.recordPutTime(ticker.read() - start);
1✔
349
    }
350
  }
1✔
351

352
  @Override
353
  public @Nullable V getAndPut(K key, V value) {
354
    requireNotClosed();
1✔
355
    boolean statsEnabled = statistics.isEnabled();
1✔
356
    long start = statsEnabled ? ticker.read() : 0L;
1✔
357

358
    var result = putNoCopyOrAwait(key, value, /* publishToWriter= */ true);
1✔
359
    dispatcher.awaitSynchronous();
1✔
360

361
    if (statsEnabled) {
1✔
362
      if (result.oldValue == null) {
1✔
363
        statistics.recordMisses(1L);
1✔
364
      } else {
365
        statistics.recordHits(1L);
1✔
366
      }
367
      if (result.written) {
1✔
368
        statistics.recordPuts(1);
1✔
369
      }
370
      long duration = ticker.read() - start;
1✔
371
      statistics.recordGetTime(duration);
1✔
372
      statistics.recordPutTime(duration);
1✔
373
    }
374
    return copyOf(result.oldValue);
1✔
375
  }
376

377
  /**
378
   * Associates the specified value with the specified key in the cache.
379
   *
380
   * @param key key with which the specified value is to be associated
381
   * @param value value to be associated with the specified key
382
   * @param publishToWriter if the writer should be notified
383
   * @return the old value
384
   */
385
  @CanIgnoreReturnValue
386
  protected PutResult<V> putNoCopyOrAwait(K key, V value, boolean publishToWriter) {
387
    requireNonNull(key);
1✔
388
    requireNonNull(value);
1✔
389

390
    var result = new PutResult<V>();
1✔
391
    cache.asMap().compute(copyOf(key), (K k, @Var Expirable<V> expirable) -> {
1✔
392
      V newValue = copyOf(value);
1✔
393
      if (publishToWriter) {
1✔
394
        publishToCacheWriter(writer::write, () -> new EntryProxy<>(key, value));
1✔
395
      }
396
      if ((expirable != null) && !expirable.isEternal()
1✔
397
          && expirable.hasExpired(currentTimeMillis())) {
1✔
398
        dispatcher.publishExpired(this, key, expirable.get());
1✔
399
        statistics.recordEvictions(1L);
1✔
400
        expirable = null;
1✔
401
      }
402
      @Var long expireTimeMillis = getWriteExpireTimeMillis((expirable == null));
1✔
403
      if ((expirable != null) && (expireTimeMillis == Long.MIN_VALUE)) {
1✔
404
        expireTimeMillis = expirable.getExpireTimeMillis();
1✔
405
      }
406
      if (expireTimeMillis == 0) {
1✔
407
        // The TCK asserts that expired entry is not counted in the puts stats, despite the javadoc
408
        // saying otherwise. See CacheMBStatisticsBeanTest.testExpiryOnCreation()
409
        result.written = false;
1✔
410

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

415
        dispatcher.publishExpired(this, key, newValue);
1✔
416
        return null;
1✔
417
      } else if (expirable == null) {
1✔
418
        dispatcher.publishCreated(this, key, newValue);
1✔
419
      } else {
420
        result.oldValue = expirable.get();
1✔
421
        dispatcher.publishUpdated(this, key, expirable.get(), newValue);
1✔
422
      }
423
      result.written = true;
1✔
424
      return new Expirable<>(newValue, expireTimeMillis);
1✔
425
    });
426
    return result;
1✔
427
  }
428

429
  @Override
430
  public void putAll(Map<? extends K, ? extends V> map) {
431
    requireNotClosed();
1✔
432
    for (var entry : map.entrySet()) {
1✔
433
      requireNonNull(entry.getKey());
1✔
434
      requireNonNull(entry.getValue());
1✔
435
    }
1✔
436

437
    @Var CacheWriterException error = null;
1✔
438
    @Var Set<? extends K> failedKeys = Set.of();
1✔
439
    boolean statsEnabled = statistics.isEnabled();
1✔
440
    long start = statsEnabled ? ticker.read() : 0L;
1✔
441
    if (configuration.isWriteThrough() && !map.isEmpty()) {
1✔
442
      var entries = new ArrayList<Cache.Entry<? extends K, ? extends V>>(map.size());
1✔
443
      for (var entry : map.entrySet()) {
1✔
444
        entries.add(new EntryProxy<>(entry.getKey(), entry.getValue()));
1✔
445
      }
1✔
446
      try {
447
        writer.writeAll(entries);
1✔
448
      } catch (CacheWriterException e) {
1✔
449
        failedKeys = entries.stream().map(Cache.Entry::getKey).collect(toSet());
1✔
450
        error = e;
1✔
451
      } catch (RuntimeException e) {
1✔
452
        failedKeys = entries.stream().map(Cache.Entry::getKey).collect(toSet());
1✔
453
        error = new CacheWriterException("Exception in CacheWriter", e);
1✔
454
      }
1✔
455
    }
456

457
    @Var int puts = 0;
1✔
458
    for (var entry : map.entrySet()) {
1✔
459
      if (!failedKeys.contains(entry.getKey())) {
1✔
460
        var result = putNoCopyOrAwait(entry.getKey(),
1✔
461
            entry.getValue(), /* publishToWriter= */ false);
1✔
462
        if (result.written) {
1✔
463
          puts++;
1✔
464
        }
465
      }
466
    }
1✔
467
    dispatcher.awaitSynchronous();
1✔
468

469
    if (statsEnabled) {
1✔
470
      statistics.recordPuts(puts);
1✔
471
      statistics.recordPutTime(ticker.read() - start);
1✔
472
    }
473
    if (error != null) {
1✔
474
      throw error;
1✔
475
    }
476
  }
1✔
477

478
  @Override
479
  public boolean putIfAbsent(K key, V value) {
480
    requireNotClosed();
1✔
481
    requireNonNull(value);
1✔
482
    boolean statsEnabled = statistics.isEnabled();
1✔
483
    long start = statsEnabled ? ticker.read() : 0L;
1✔
484

485
    boolean added = putIfAbsentNoAwait(key, value, /* publishToWriter= */ true);
1✔
486
    dispatcher.awaitSynchronous();
1✔
487

488
    if (statsEnabled) {
1✔
489
      if (added) {
1✔
490
        statistics.recordPuts(1L);
1✔
491
        statistics.recordMisses(1L);
1✔
492
      } else {
493
        statistics.recordHits(1L);
1✔
494
      }
495
      statistics.recordPutTime(ticker.read() - start);
1✔
496
    }
497
    return added;
1✔
498
  }
499

500
  /**
501
   * Associates the specified value with the specified key in the cache if there is no existing
502
   * mapping.
503
   *
504
   * @param key key with which the specified value is to be associated
505
   * @param value value to be associated with the specified key
506
   * @param publishToWriter if the writer should be notified
507
   * @return if the mapping was successful
508
   */
509
  @CanIgnoreReturnValue
510
  private boolean putIfAbsentNoAwait(K key, V value, boolean publishToWriter) {
511
    boolean[] absent = { false };
1✔
512
    cache.asMap().compute(copyOf(key), (K k, @Var Expirable<V> expirable) -> {
1✔
513
      if ((expirable != null) && !expirable.isEternal()
1✔
514
          && expirable.hasExpired(currentTimeMillis())) {
1✔
515
        dispatcher.publishExpired(this, key, expirable.get());
1✔
516
        statistics.recordEvictions(1L);
1✔
517
        expirable = null;
1✔
518
      }
519
      if (expirable != null) {
1✔
520
        return expirable;
1✔
521
      }
522
      if (publishToWriter) {
1✔
523
        publishToCacheWriter(writer::write, () -> new EntryProxy<>(key, value));
1✔
524
      }
525

526
      absent[0] = true;
1✔
527
      V copy = copyOf(value);
1✔
528
      long expireTimeMillis = getWriteExpireTimeMillis(/* created= */ true);
1✔
529
      if (expireTimeMillis == 0) {
1✔
530
        // The TCK asserts that a create is not published in
531
        // CacheExpiryTest.expire_whenCreated_CreatedExpiryPolicy()
532
        dispatcher.publishExpired(this, key, copy);
1✔
533
        return null;
1✔
534
      } else {
535
        dispatcher.publishCreated(this, key, copy);
1✔
536
        return new Expirable<>(copy, expireTimeMillis);
1✔
537
      }
538
    });
539
    return absent[0];
1✔
540
  }
541

542
  @Override
543
  public boolean remove(K key) {
544
    requireNotClosed();
1✔
545
    requireNonNull(key);
1✔
546
    boolean statsEnabled = statistics.isEnabled();
1✔
547
    long start = statsEnabled ? ticker.read() : 0L;
1✔
548

549
    publishToCacheWriter(writer::delete, () -> key);
1✔
550
    V value = removeNoCopyOrAwait(key);
1✔
551
    dispatcher.awaitSynchronous();
1✔
552

553
    if (statsEnabled) {
1✔
554
      statistics.recordRemoveTime(ticker.read() - start);
1✔
555
    }
556
    if (value != null) {
1✔
557
      statistics.recordRemovals(1L);
1✔
558
      return true;
1✔
559
    }
560
    return false;
1✔
561
  }
562

563
  /**
564
   * Removes the mapping from the cache without store-by-value copying nor waiting for synchronous
565
   * listeners to complete.
566
   *
567
   * @param key key whose mapping is to be removed from the cache
568
   * @return the old value
569
   */
570
  private @Nullable V removeNoCopyOrAwait(K key) {
571
    @SuppressWarnings("unchecked")
572
    var removed = (V[]) new Object[1];
1✔
573
    cache.asMap().computeIfPresent(key, (k, expirable) -> {
1✔
574
      if (!expirable.isEternal() && expirable.hasExpired(currentTimeMillis())) {
1✔
575
        dispatcher.publishExpired(this, key, expirable.get());
1✔
576
        statistics.recordEvictions(1L);
1✔
577
      } else {
578
        dispatcher.publishRemoved(this, key, expirable.get());
1✔
579
        removed[0] = expirable.get();
1✔
580
      }
581
      return null;
1✔
582
    });
583
    return removed[0];
1✔
584
  }
585

586
  @Override
587
  @CanIgnoreReturnValue
588
  public boolean remove(K key, V oldValue) {
589
    requireNotClosed();
1✔
590
    requireNonNull(key);
1✔
591
    requireNonNull(oldValue);
1✔
592

593
    boolean statsEnabled = statistics.isEnabled();
1✔
594
    long start = statsEnabled ? ticker.read() : 0L;
1✔
595
    boolean[] found = { false };
1✔
596

597
    boolean[] removed = { false };
1✔
598
    cache.asMap().computeIfPresent(key, (k, expirable) -> {
1✔
599
      long millis = expirable.isEternal()
1✔
600
          ? 0L
1✔
601
          : nanosToMillis((start == 0L) ? ticker.read() : start);
1✔
602
      if (expirable.hasExpired(millis)) {
1✔
603
        dispatcher.publishExpired(this, key, expirable.get());
1✔
604
        statistics.recordEvictions(1L);
1✔
605
        return null;
1✔
606
      }
607

608
      found[0] = true;
1✔
609
      if (oldValue.equals(expirable.get())) {
1✔
610
        publishToCacheWriter(writer::delete, () -> key);
1✔
611
        dispatcher.publishRemoved(this, key, expirable.get());
1✔
612
        removed[0] = true;
1✔
613
        return null;
1✔
614
      }
615
      setAccessExpireTime(key, expirable, millis);
1✔
616
      return expirable;
1✔
617
    });
618
    dispatcher.awaitSynchronous();
1✔
619
    if (statsEnabled) {
1✔
620
      if (removed[0]) {
1✔
621
        statistics.recordRemovals(1L);
1✔
622
        statistics.recordHits(1L);
1✔
623
      } else if (found[0]) {
1✔
624
        statistics.recordHits(1L);
1✔
625
      } else {
626
        statistics.recordMisses(1L);
1✔
627
      }
628
      statistics.recordRemoveTime(ticker.read() - start);
1✔
629
    }
630
    return removed[0];
1✔
631
  }
632

633
  @Override
634
  public @Nullable V getAndRemove(K key) {
635
    requireNotClosed();
1✔
636
    requireNonNull(key);
1✔
637
    boolean statsEnabled = statistics.isEnabled();
1✔
638
    long start = statsEnabled ? ticker.read() : 0L;
1✔
639

640
    publishToCacheWriter(writer::delete, () -> key);
1✔
641
    V value = removeNoCopyOrAwait(key);
1✔
642
    dispatcher.awaitSynchronous();
1✔
643
    V copy = copyOf(value);
1✔
644

645
    if (statsEnabled) {
1✔
646
      if (value == null) {
1✔
647
        statistics.recordMisses(1L);
1✔
648
      } else {
649
        statistics.recordHits(1L);
1✔
650
        statistics.recordRemovals(1L);
1✔
651
      }
652
      long duration = ticker.read() - start;
1✔
653
      statistics.recordRemoveTime(duration);
1✔
654
      statistics.recordGetTime(duration);
1✔
655
    }
656
    return copy;
1✔
657
  }
658

659
  @Override
660
  public boolean replace(K key, V oldValue, V newValue) {
661
    requireNotClosed();
1✔
662
    requireNonNull(oldValue);
1✔
663
    requireNonNull(newValue);
1✔
664

665
    boolean statsEnabled = statistics.isEnabled();
1✔
666
    long start = statsEnabled ? ticker.read() : 0L;
1✔
667

668
    boolean[] found = { false };
1✔
669
    boolean[] replaced = { false };
1✔
670
    cache.asMap().computeIfPresent(key, (k, expirable) -> {
1✔
671
      long millis = expirable.isEternal()
1✔
672
          ? 0L
1✔
673
          : nanosToMillis((start == 0L) ? ticker.read() : start);
1✔
674
      if (expirable.hasExpired(millis)) {
1✔
675
        dispatcher.publishExpired(this, key, expirable.get());
1✔
676
        statistics.recordEvictions(1L);
1✔
677
        return null;
1✔
678
      }
679

680
      found[0] = true;
1✔
681
      Expirable<V> result;
682
      if (oldValue.equals(expirable.get())) {
1✔
683
        V copy = copyOf(newValue);
1✔
684
        publishToCacheWriter(writer::write, () -> new EntryProxy<>(key, newValue));
1✔
685
        dispatcher.publishUpdated(this, key, expirable.get(), copy);
1✔
686
        @Var long expireTimeMillis = getWriteExpireTimeMillis(/* created= */ false);
1✔
687
        if (expireTimeMillis == Long.MIN_VALUE) {
1✔
688
          expireTimeMillis = expirable.getExpireTimeMillis();
1✔
689
        }
690
        result = new Expirable<>(copy, expireTimeMillis);
1✔
691
        replaced[0] = true;
1✔
692
      } else {
1✔
693
        result = expirable;
1✔
694
        setAccessExpireTime(key, expirable, millis);
1✔
695
      }
696
      return result;
1✔
697
    });
698
    dispatcher.awaitSynchronous();
1✔
699

700
    if (statsEnabled) {
1✔
701
      statistics.recordPuts(replaced[0] ? 1L : 0L);
1✔
702
      statistics.recordMisses(found[0] ? 0L : 1L);
1✔
703
      statistics.recordHits(found[0] ? 1L : 0L);
1✔
704
      long duration = ticker.read() - start;
1✔
705
      statistics.recordGetTime(duration);
1✔
706
      statistics.recordPutTime(duration);
1✔
707
    }
708

709
    return replaced[0];
1✔
710
  }
711

712
  @Override
713
  public boolean replace(K key, V value) {
714
    requireNotClosed();
1✔
715
    boolean statsEnabled = statistics.isEnabled();
1✔
716
    long start = statsEnabled ? ticker.read() : 0L;
1✔
717

718
    @Nullable V oldValue = replaceNoCopyOrAwait(key, value);
1✔
719
    dispatcher.awaitSynchronous();
1✔
720
    if (oldValue == null) {
1✔
721
      statistics.recordMisses(1L);
1✔
722
      if (statsEnabled) {
1✔
723
        statistics.recordGetTime(ticker.read() - start);
1✔
724
      }
725
      return false;
1✔
726
    }
727

728
    if (statsEnabled) {
1✔
729
      statistics.recordHits(1L);
1✔
730
      statistics.recordPuts(1L);
1✔
731
      long duration = ticker.read() - start;
1✔
732
      statistics.recordGetTime(duration);
1✔
733
      statistics.recordPutTime(duration);
1✔
734
    }
735
    return true;
1✔
736
  }
737

738
  @Override
739
  public @Nullable V getAndReplace(K key, V value) {
740
    requireNotClosed();
1✔
741
    boolean statsEnabled = statistics.isEnabled();
1✔
742
    long start = statsEnabled ? ticker.read() : 0L;
1✔
743

744
    V oldValue = replaceNoCopyOrAwait(key, value);
1✔
745
    dispatcher.awaitSynchronous();
1✔
746
    V copy = copyOf(oldValue);
1✔
747

748
    if (statsEnabled) {
1✔
749
      if (oldValue == null) {
1✔
750
        statistics.recordMisses(1L);
1✔
751
      } else {
752
        statistics.recordHits(1L);
1✔
753
        statistics.recordPuts(1L);
1✔
754
      }
755
      long duration = ticker.read() - start;
1✔
756
      statistics.recordGetTime(duration);
1✔
757
      statistics.recordPutTime(duration);
1✔
758
    }
759
    return copy;
1✔
760
  }
761

762
  /**
763
   * Replaces the entry for the specified key only if it is currently mapped to some value. The
764
   * entry is not store-by-value copied nor does the method wait for synchronous listeners to
765
   * complete.
766
   *
767
   * @param key key with which the specified value is associated
768
   * @param value value to be associated with the specified key
769
   * @return the old value
770
   */
771
  private @Nullable V replaceNoCopyOrAwait(K key, V value) {
772
    requireNonNull(value);
1✔
773
    V copy = copyOf(value);
1✔
774
    @SuppressWarnings("unchecked")
775
    var replaced = (V[]) new Object[1];
1✔
776
    cache.asMap().computeIfPresent(key, (k, expirable) -> {
1✔
777
      if (!expirable.isEternal() && expirable.hasExpired(currentTimeMillis())) {
1✔
778
        dispatcher.publishExpired(this, key, expirable.get());
1✔
779
        statistics.recordEvictions(1L);
1✔
780
        return null;
1✔
781
      }
782

783
      publishToCacheWriter(writer::write, () -> new EntryProxy<>(key, value));
1✔
784
      @Var long expireTimeMillis = getWriteExpireTimeMillis(/* created= */ false);
1✔
785
      if (expireTimeMillis == Long.MIN_VALUE) {
1✔
786
        expireTimeMillis = expirable.getExpireTimeMillis();
1✔
787
      }
788
      dispatcher.publishUpdated(this, key, expirable.get(), copy);
1✔
789
      replaced[0] = expirable.get();
1✔
790
      return new Expirable<>(copy, expireTimeMillis);
1✔
791
    });
792
    return replaced[0];
1✔
793
  }
794

795
  @Override
796
  public void removeAll(Set<? extends K> keys) {
797
    requireNotClosed();
1✔
798
    keys.forEach(Objects::requireNonNull);
1✔
799

800
    @Var CacheWriterException error = null;
1✔
801
    @Var Set<? extends K> failedKeys = Set.of();
1✔
802
    boolean statsEnabled = statistics.isEnabled();
1✔
803
    long start = statsEnabled ? ticker.read() : 0L;
1✔
804
    if (configuration.isWriteThrough() && !keys.isEmpty()) {
1✔
805
      var keysToWrite = new LinkedHashSet<>(keys);
1✔
806
      try {
807
        writer.deleteAll(keysToWrite);
1✔
808
      } catch (CacheWriterException e) {
1✔
809
        error = e;
1✔
810
        failedKeys = keysToWrite;
1✔
811
      } catch (RuntimeException e) {
1✔
812
        error = new CacheWriterException("Exception in CacheWriter", e);
1✔
813
        failedKeys = keysToWrite;
1✔
814
      }
1✔
815
    }
816

817
    @Var int removed = 0;
1✔
818
    for (var key : keys) {
1✔
819
      if (!failedKeys.contains(key) && (removeNoCopyOrAwait(key) != null)) {
1✔
820
        removed++;
1✔
821
      }
822
    }
1✔
823
    dispatcher.awaitSynchronous();
1✔
824

825
    if (statsEnabled) {
1✔
826
      statistics.recordRemovals(removed);
1✔
827
      statistics.recordRemoveTime(ticker.read() - start);
1✔
828
    }
829
    if (error != null) {
1✔
830
      throw error;
1✔
831
    }
832
  }
1✔
833

834
  @Override
835
  public void removeAll() {
836
    removeAll(cache.asMap().keySet());
1✔
837
  }
1✔
838

839
  @Override
840
  public void clear() {
841
    requireNotClosed();
1✔
842
    cache.invalidateAll();
1✔
843
  }
1✔
844

845
  @Override
846
  public <C extends Configuration<K, V>> C getConfiguration(Class<C> clazz) {
847
    if (clazz.isInstance(configuration)) {
1✔
848
      synchronized (configuration) {
1✔
849
        return clazz.cast(configuration.immutableCopy());
1✔
850
      }
851
    }
852
    throw new IllegalArgumentException("The configuration class " + clazz
1✔
853
        + " is not supported by this implementation");
854
  }
855

856
  @Override
857
  public <T> @Nullable T invoke(K key,
858
      EntryProcessor<K, V, T> entryProcessor, Object... arguments) {
859
    requireNonNull(entryProcessor);
1✔
860
    requireNonNull(arguments);
1✔
861
    requireNotClosed();
1✔
862

863
    Object[] result = new Object[1];
1✔
864
    BiFunction<K, Expirable<V>, Expirable<V>> remappingFunction = (k, expirable) -> {
1✔
865
      V value;
866
      @Var long millis = 0L;
1✔
867
      if ((expirable == null)
1✔
868
          || (!expirable.isEternal() && expirable.hasExpired(millis = currentTimeMillis()))) {
1✔
869
        statistics.recordMisses(1L);
1✔
870
        value = null;
1✔
871
      } else {
872
        value = copyValue(expirable);
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(expirable, entry, millis);
1✔
880
      } catch (EntryProcessorException e) {
1✔
881
        throw e;
1✔
882
      } catch (RuntimeException e) {
1✔
883
        throw new EntryProcessorException(e);
1✔
884
      }
885
    };
886
    try {
887
      cache.asMap().compute(copyOf(key), remappingFunction);
1✔
888
      dispatcher.awaitSynchronous();
1✔
889
    } catch (Throwable t) {
1✔
890
      dispatcher.ignoreSynchronous();
1✔
891
      throw t;
1✔
892
    }
1✔
893

894
    @SuppressWarnings("unchecked")
895
    var castedResult = (T) result[0];
1✔
896
    return castedResult;
1✔
897
  }
898

899
  /** Returns the updated expirable value after performing the post-processing actions. */
900
  @SuppressWarnings("fallthrough")
901
  @Nullable Expirable<V> postProcess(@Nullable Expirable<V> expirable,
902
      EntryProcessorEntry<K, V> entry, @Var long currentTimeMillis) {
903
    switch (entry.getAction()) {
1✔
904
      case NONE:
905
        if (expirable == null) {
1✔
906
          return null;
1✔
907
        } else if (expirable.isEternal()) {
1✔
908
          return expirable;
1✔
909
        }
910
        if (currentTimeMillis == 0) {
1✔
911
          currentTimeMillis = currentTimeMillis();
1✔
912
        }
913
        if (expirable.hasExpired(currentTimeMillis)) {
1✔
914
          dispatcher.publishExpired(this, entry.getKey(), expirable.get());
1✔
915
          statistics.recordEvictions(1);
1✔
916
          return null;
1✔
917
        }
918
        return expirable;
1✔
919
      case READ: {
920
        setAccessExpireTime(entry.getKey(), requireNonNull(expirable), 0L);
1✔
921
        return expirable;
1✔
922
      }
923
      case CREATED:
924
        this.publishToCacheWriter(writer::write, () -> entry);
1✔
925
        // fallthrough
926
      case LOADED: {
927
        statistics.recordPuts(1L);
1✔
928
        V value = copyOf(requireNonNull(entry.getValue()));
1✔
929
        dispatcher.publishCreated(this, entry.getKey(), value);
1✔
930
        return new Expirable<>(value, getWriteExpireTimeMillis(/* created= */ true));
1✔
931
      }
932
      case UPDATED: {
933
        statistics.recordPuts(1L);
1✔
934
        publishToCacheWriter(writer::write, () -> entry);
1✔
935
        requireNonNull(expirable, "Expected a previous value but was null");
1✔
936
        V value = copyOf(requireNonNull(entry.getValue(), "Expected a new value but was null"));
1✔
937
        dispatcher.publishUpdated(this, entry.getKey(), expirable.get(), value);
1✔
938
        @Var long expireTimeMillis = getWriteExpireTimeMillis(/* created= */ false);
1✔
939
        if (expireTimeMillis == Long.MIN_VALUE) {
1✔
940
          expireTimeMillis = expirable.getExpireTimeMillis();
1✔
941
        }
942
        return new Expirable<>(value, expireTimeMillis);
1✔
943
      }
944
      case DELETED:
945
        statistics.recordRemovals(1L);
1✔
946
        publishToCacheWriter(writer::delete, entry::getKey);
1✔
947
        if (expirable != null) {
1✔
948
          dispatcher.publishRemoved(this, entry.getKey(), expirable.get());
1✔
949
        }
950
        return null;
1✔
951
    }
952
    throw new IllegalStateException("Unknown state: " + entry.getAction());
1✔
953
  }
954

955
  @Override
956
  public <T> Map<K, EntryProcessorResult<T>> invokeAll(Set<? extends K> keys,
957
      EntryProcessor<K, V, T> entryProcessor, Object... arguments) {
958
    requireNotClosed();
1✔
959
    requireNonNull(keys);
1✔
960
    requireNonNull(arguments);
1✔
961
    requireNonNull(entryProcessor);
1✔
962
    keys.forEach(Objects::requireNonNull);
1✔
963

964
    var results = new HashMap<K, EntryProcessorResult<T>>(keys.size(), 1.0f);
1✔
965
    for (K key : keys) {
1✔
966
      try {
967
        T result = invoke(key, entryProcessor, arguments);
1✔
968
        if (result != null) {
1✔
969
          results.put(key, () -> result);
1✔
970
        }
971
      } catch (EntryProcessorException e) {
1✔
972
        results.put(key, () -> { throw e; });
1✔
973
      }
1✔
974
    }
1✔
975
    return results;
1✔
976
  }
977

978
  @Override
979
  public String getName() {
980
    return name;
1✔
981
  }
982

983
  @Override
984
  public CacheManager getCacheManager() {
985
    return cacheManager;
1✔
986
  }
987

988
  @Override
989
  public boolean isClosed() {
990
    return closed;
1✔
991
  }
992

993
  @Override
994
  public void close() {
995
    if (isClosed()) {
1✔
996
      return;
1✔
997
    }
998
    synchronized (configuration) {
1✔
999
      if (!isClosed()) {
1✔
1000
        enableManagement(false);
1✔
1001
        enableStatistics(false);
1✔
1002
        closed = true;
1✔
1003
        try {
1004
          cacheManager.destroyCache(name);
1✔
1005
        } catch (IllegalStateException ignored) { /* manager already closed */ }
1✔
1006

1007
        @Var var thrown = shutdownExecutor();
1✔
1008
        thrown = tryClose(expiry, thrown);
1✔
1009
        thrown = tryClose(writer, thrown);
1✔
1010
        thrown = tryClose(executor, thrown);
1✔
1011
        thrown = tryClose(cacheLoader.orElse(null), thrown);
1✔
1012
        for (Registration<K, V> registration : dispatcher.registrations()) {
1✔
1013
          thrown = tryClose(registration.getCacheEntryListener(), thrown);
1✔
1014
        }
1✔
1015
        if (thrown != null) {
1✔
1016
          logger.log(Level.WARNING, "Failure when closing cache resources", thrown);
1✔
1017
        }
1018
      }
1019
    }
1✔
1020
    cache.invalidateAll();
1✔
1021
  }
1✔
1022

1023
  @SuppressWarnings("FutureReturnValueIgnored")
1024
  private @Nullable Throwable shutdownExecutor() {
1025
    if (executor instanceof ExecutorService) {
1✔
1026
      @SuppressWarnings("PMD.CloseResource")
1027
      var es = (ExecutorService) executor;
1✔
1028
      es.shutdown();
1✔
1029
    }
1030

1031
    @Var Throwable thrown = null;
1✔
1032
    try {
1033
      CompletableFuture
1✔
1034
          .allOf(inFlight.toArray(CompletableFuture[]::new))
1✔
1035
          .get(10, TimeUnit.SECONDS);
1✔
1036
    } catch (ExecutionException | TimeoutException e) {
1✔
1037
      thrown = e;
1✔
1038
    } catch (InterruptedException e) {
1✔
1039
      Thread.currentThread().interrupt();
1✔
1040
      thrown = e;
1✔
1041
    }
1✔
1042
    inFlight.clear();
1✔
1043
    return thrown;
1✔
1044
  }
1045

1046
  /**
1047
   * Attempts to close the resource. If an error occurs and an outermost exception is set, then adds
1048
   * the error to the suppression list.
1049
   *
1050
   * @param o the resource to close if Closeable
1051
   * @param outer the outermost error, or null if unset
1052
   * @return the outermost error, or null if unset and successful
1053
   */
1054
  private static @Nullable Throwable tryClose(@Nullable Object o, @Nullable Throwable outer) {
1055
    if (o instanceof AutoCloseable) {
1✔
1056
      try {
1057
        ((AutoCloseable) o).close();
1✔
1058
      } catch (Throwable t) {
1✔
1059
        if (outer == null) {
1✔
1060
          return t;
1✔
1061
        }
1062
        outer.addSuppressed(t);
1✔
1063
      }
1✔
1064
    }
1065
    return outer;
1✔
1066
  }
1067

1068
  @Override
1069
  public <T> T unwrap(Class<T> clazz) {
1070
    if (clazz.isInstance(cache)) {
1✔
1071
      return clazz.cast(cache);
1✔
1072
    } else if (clazz.isInstance(this)) {
1✔
1073
      return clazz.cast(this);
1✔
1074
    }
1075
    throw new IllegalArgumentException("Unwrapping to " + clazz
1✔
1076
        + " is not supported by this implementation");
1077
  }
1078

1079
  @Override
1080
  public void registerCacheEntryListener(
1081
      CacheEntryListenerConfiguration<K, V> cacheEntryListenerConfiguration) {
1082
    requireNotClosed();
1✔
1083
    synchronized (configuration) {
1✔
1084
      configuration.addCacheEntryListenerConfiguration(cacheEntryListenerConfiguration);
1✔
1085
      dispatcher.register(cacheEntryListenerConfiguration);
1✔
1086
    }
1✔
1087
  }
1✔
1088

1089
  @Override
1090
  public void deregisterCacheEntryListener(
1091
      CacheEntryListenerConfiguration<K, V> cacheEntryListenerConfiguration) {
1092
    requireNotClosed();
1✔
1093
    synchronized (configuration) {
1✔
1094
      configuration.removeCacheEntryListenerConfiguration(cacheEntryListenerConfiguration);
1✔
1095
      dispatcher.deregister(cacheEntryListenerConfiguration);
1✔
1096
    }
1✔
1097
  }
1✔
1098

1099
  @Override
1100
  public Iterator<Cache.Entry<K, V>> iterator() {
1101
    requireNotClosed();
1✔
1102
    return new EntryIterator();
1✔
1103
  }
1104

1105
  /** Enables or disables the configuration management JMX bean. */
1106
  void enableManagement(boolean enabled) {
1107
    requireNotClosed();
1✔
1108

1109
    synchronized (configuration) {
1✔
1110
      if (enabled) {
1✔
1111
        JmxRegistration.registerMxBean(this, cacheMxBean, MBeanType.CONFIGURATION);
1✔
1112
      } else {
1113
        JmxRegistration.unregisterMxBean(this, MBeanType.CONFIGURATION);
1✔
1114
      }
1115
      configuration.setManagementEnabled(enabled);
1✔
1116
    }
1✔
1117
  }
1✔
1118

1119
  /** Enables or disables the statistics JMX bean. */
1120
  void enableStatistics(boolean enabled) {
1121
    requireNotClosed();
1✔
1122

1123
    synchronized (configuration) {
1✔
1124
      if (enabled) {
1✔
1125
        JmxRegistration.registerMxBean(this, statistics, MBeanType.STATISTICS);
1✔
1126
      } else {
1127
        JmxRegistration.unregisterMxBean(this, MBeanType.STATISTICS);
1✔
1128
      }
1129
      statistics.enable(enabled);
1✔
1130
      configuration.setStatisticsEnabled(enabled);
1✔
1131
    }
1✔
1132
  }
1✔
1133

1134
  /** Performs the action with the cache writer if write-through is enabled. */
1135
  private <T> void publishToCacheWriter(Consumer<T> action, Supplier<T> data) {
1136
    if (!configuration.isWriteThrough()) {
1✔
1137
      return;
1✔
1138
    }
1139
    try {
1140
      action.accept(data.get());
1✔
1141
    } catch (CacheWriterException e) {
1✔
1142
      throw e;
1✔
1143
    } catch (RuntimeException e) {
1✔
1144
      throw new CacheWriterException("Exception in CacheWriter", e);
1✔
1145
    }
1✔
1146
  }
1✔
1147

1148
  /** Checks that the cache is not closed. */
1149
  protected final void requireNotClosed() {
1150
    if (isClosed()) {
1✔
1151
      throw new IllegalStateException();
1✔
1152
    }
1153
  }
1✔
1154

1155
  /**
1156
   * Returns a copy of the value if value-based caching is enabled.
1157
   *
1158
   * @param object the object to be copied
1159
   * @param <T> the type of object being copied
1160
   * @return a copy of the object if storing by value or the same instance if by reference
1161
   */
1162
  @SuppressWarnings({"DataFlowIssue", "NullAway"})
1163
  protected final <T> T copyOf(@Nullable T object) {
1164
    if (object == null) {
1✔
1165
      return null;
1✔
1166
    }
1167
    T copy = copier.copy(object, cacheManager.getClassLoader());
1✔
1168
    return requireNonNull(copy);
1✔
1169
  }
1170

1171
  /**
1172
   * Returns a copy of the value if value-based caching is enabled.
1173
   *
1174
   * @param expirable the expirable value to be copied
1175
   * @return a copy of the value if storing by value or the same instance if by reference
1176
   */
1177
  @SuppressWarnings({"DataFlowIssue", "NullAway"})
1178
  protected final V copyValue(@Nullable Expirable<V> expirable) {
1179
    if (expirable == null) {
1✔
1180
      return null;
1✔
1181
    }
1182
    V copy = copier.copy(expirable.get(), cacheManager.getClassLoader());
1✔
1183
    return requireNonNull(copy);
1✔
1184
  }
1185

1186
  /**
1187
   * Returns a deep copy of the map if value-based caching is enabled.
1188
   *
1189
   * @param map the mapping of keys to expirable values
1190
   * @return a deep or shallow copy of the mappings depending on the store by value setting
1191
   */
1192
  @SuppressWarnings("CollectorMutability")
1193
  protected final Map<K, V> copyMap(Map<K, Expirable<V>> map) {
1194
    ClassLoader classLoader = cacheManager.getClassLoader();
1✔
1195
    return map.entrySet().stream().collect(toMap(
1✔
1196
        entry -> copier.copy(entry.getKey(), classLoader),
1✔
1197
        entry -> copier.copy(entry.getValue().get(), classLoader)));
1✔
1198
  }
1199

1200
  /** Returns the current time in milliseconds. */
1201
  protected final long currentTimeMillis() {
1202
    return nanosToMillis(ticker.read());
1✔
1203
  }
1204

1205
  /** Returns the nanosecond time in milliseconds. */
1206
  protected static long nanosToMillis(long nanos) {
1207
    return TimeUnit.NANOSECONDS.toMillis(nanos);
1✔
1208
  }
1209

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

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

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

1281
    Map.@Nullable Entry<K, Expirable<V>> current;
1282
    Map.@Nullable Entry<K, Expirable<V>> cursor;
1283

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

1297
    @Override
1298
    public Cache.Entry<K, V> next() {
1299
      if (!hasNext()) {
1✔
1300
        throw new NoSuchElementException();
1✔
1301
      }
1302
      statistics.recordHits(1L);
1✔
1303
      current = requireNonNull(cursor);
1✔
1304
      cursor = null;
1✔
1305
      return new EntryProxy<>(copyOf(current.getKey()), copyValue(current.getValue()));
1✔
1306
    }
1307

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

1317
      K key = current.getKey();
1✔
1318
      V oldValue = current.getValue().get();
1✔
1319
      cache.asMap().computeIfPresent(key, (k, expirable) -> {
1✔
1320
        if (!expirable.isEternal() && expirable.hasExpired(currentTimeMillis())) {
1!
NEW
1321
          dispatcher.publishExpired(CacheProxy.this, key, expirable.get());
×
NEW
1322
          statistics.recordEvictions(1L);
×
NEW
1323
          return null;
×
1324
        }
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
        }
NEW
1331
        return expirable;
×
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 · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc