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

ben-manes / caffeine / #5689

23 Jul 2026 05:57AM UTC coverage: 99.953% (-0.04%) from 99.988%
#5689

push

github

ben-manes
Align the 2Q in-queue default to the paper's recommendation

Johnson and Shasha recommend Kin ~= 25% (Kout ~= 50%); the default was
0.20 / 0.50. Match the paper. 2Q is insensitive to Kin in this range,
so the hit rate is unchanged on the bundled traces.

4192 of 4203 branches covered (99.74%)

8471 of 8475 relevant lines covered (99.95%)

1.0 hits per line

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

99.87
/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.Action;
77
import com.github.benmanes.caffeine.jcache.processor.EntryProcessorEntry;
78
import com.google.errorprone.annotations.CanIgnoreReturnValue;
79
import com.google.errorprone.annotations.Var;
80

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

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

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

106
  private volatile boolean closed;
107

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

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

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

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

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

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

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

206
    boolean statsEnabled = statistics.isEnabled();
1✔
207
    long now = statsEnabled ? ticker.read() : 0L;
1✔
208
    try {
209
      Map<K, Expirable<V>> result = getAndFilterExpiredEntries(keys);
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 updates their access expiry
221
   * time.
222
   */
223
  protected Map<K, Expirable<V>> getAndFilterExpiredEntries(Set<? extends K> keys) {
224
    int[] expired = { 0 };
1✔
225
    long[] millis = { 0L };
1✔
226
    var result = new HashMap<K, @NonNull Expirable<V>>(cache.getAllPresent(keys));
1✔
227
    result.entrySet().removeIf(entry -> {
1✔
228
      if (!entry.getValue().isEternal() && (millis[0] == 0L)) {
1✔
229
        millis[0] = currentTimeMillis();
1✔
230
      }
231
      if (entry.getValue().hasExpired(millis[0])) {
1✔
232
        cache.asMap().computeIfPresent(entry.getKey(), (k, expirable) -> {
1✔
233
          if (expirable == entry.getValue()) {
1✔
234
            dispatcher.publishExpired(this, entry.getKey(), entry.getValue().get());
1✔
235
            expired[0]++;
1✔
236
            return null;
1✔
237
          }
238
          return expirable;
1✔
239
        });
240
        return true;
1✔
241
      }
242
      var duration = getAccessExpireTime();
1✔
243
      setVariableExpiration(entry.getKey(), duration);
1✔
244
      setAccessExpireTime(entry.getValue(), duration, millis[0]);
1✔
245
      return false;
1✔
246
    });
247

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

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

268
    var future = new CompletableFuture<@Nullable Void>();
1✔
269
    synchronized (configuration) {
1✔
270
      requireNotClosed();
1✔
271
      inFlight.add(future);
1✔
272
    }
1✔
273
    try {
274
      CompletableFuture.runAsync(() -> {
1✔
275
        @Var boolean success = false;
1✔
276
        try {
277
          if (replaceExistingValues) {
1✔
278
            loadAllAndReplaceExisting(keys);
1✔
279
          } else {
280
            loadAllAndKeepExisting(keys);
1✔
281
          }
282
          success = true;
1✔
283
        } catch (CacheLoaderException e) {
1✔
284
          dispatcher.ignoreSynchronous();
1✔
285
          listener.onException(e);
1✔
286
        } catch (RuntimeException e) {
1✔
287
          dispatcher.ignoreSynchronous();
1✔
288
          listener.onException(new CacheLoaderException(e));
1✔
289
        }
1✔
290
        if (success) {
1✔
291
          dispatcher.chainSynchronous().whenComplete((failure, error) -> {
1✔
292
            if (error != null) {
1!
293
              listener.onException(new CacheLoaderException(error));
×
294
            } else if (failure != null) {
1✔
295
              listener.onException(failure);
1✔
296
            } else {
297
              listener.onCompletion();
1✔
298
            }
299
          });
1✔
300
        }
301
      }, executor).whenComplete((r, e) -> {
1✔
302
        inFlight.remove(future);
1✔
303
        future.complete(null);
1✔
304
      });
1✔
305
    } catch (RuntimeException e) {
1✔
306
      inFlight.remove(future);
1✔
307
      future.complete(null);
1✔
308
      listener.onException(new CacheLoaderException(e));
1✔
309
    }
1✔
310
  }
1✔
311

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

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

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

345
    var result = putNoCopyOrAwait(key, value, /* publishToWriter= */ true);
1✔
346
    dispatcher.awaitSynchronous();
1✔
347

348
    if (statsEnabled && result.written) {
1✔
349
      statistics.recordPuts(1);
1✔
350
      statistics.recordPutTime(ticker.read() - start);
1✔
351
    }
352
  }
1✔
353

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

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

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

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

392
    V newValue = copyOf(value);
1✔
393
    var result = new PutResult<V>();
1✔
394
    cache.asMap().compute(copyOf(key), (K k, @Var Expirable<V> expirable) -> {
1✔
395
      if (publishToWriter) {
1✔
396
        publishToCacheWriter(writer::write, () -> new EntryProxy<>(key, value));
1✔
397
      }
398
      if ((expirable != null) && !expirable.isEternal()
1✔
399
          && expirable.hasExpired(currentTimeMillis())) {
1✔
400
        dispatcher.publishExpired(this, key, expirable.get());
1✔
401
        statistics.recordEvictions(1L);
1✔
402
        expirable = null;
1✔
403
      }
404
      @Var long expireTimeMillis = getWriteExpireTimeMillis((expirable == null));
1✔
405
      if ((expirable != null) && (expireTimeMillis == Long.MIN_VALUE)) {
1✔
406
        expireTimeMillis = expirable.getExpireTimeMillis();
1✔
407
      }
408
      if ((expireTimeMillis == 0) && (expirable == null)) {
1✔
409
        result.written = false;
1✔
410
        dispatcher.publishExpired(this, key, newValue);
1✔
411
        return null;
1✔
412
      } else if (expirable == null) {
1✔
413
        dispatcher.publishCreated(this, key, newValue);
1✔
414
      } else {
415
        result.oldValue = expirable.get();
1✔
416
        dispatcher.publishUpdated(this, key, expirable.get(), newValue);
1✔
417
      }
418
      result.written = true;
1✔
419
      return new Expirable<>(newValue, expireTimeMillis);
1✔
420
    });
421
    return result;
1✔
422
  }
423

424
  @Override
425
  public void putAll(Map<? extends K, ? extends V> map) {
426
    requireNotClosed();
1✔
427
    for (var entry : map.entrySet()) {
1✔
428
      requireNonNull(entry.getKey());
1✔
429
      requireNonNull(entry.getValue());
1✔
430
    }
1✔
431

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

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

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

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

483
    var result = putIfAbsentNoAwait(key, value, /* publishToWriter= */ true);
1✔
484
    dispatcher.awaitSynchronous();
1✔
485

486
    if (statsEnabled) {
1✔
487
      if (result.oldValue != null) {
1✔
488
        statistics.recordHits(1L);
1✔
489
      } else {
490
        statistics.recordMisses(1L);
1✔
491
      }
492
      if (result.written) {
1✔
493
        statistics.recordPuts(1L);
1✔
494
        statistics.recordPutTime(ticker.read() - start);
1✔
495
      }
496
    }
497
    return result.written;
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 the oldValue and if the new value was stored
508
   */
509
  @CanIgnoreReturnValue
510
  private PutResult<V> putIfAbsentNoAwait(K key, V value, boolean publishToWriter) {
511
    var result = new PutResult<V>();
1✔
512
    cache.asMap().compute(copyOf(key), (K k, Expirable<V> expirable) -> {
1✔
513
      if ((expirable != null)
1✔
514
          && (expirable.isEternal() || !expirable.hasExpired(currentTimeMillis()))) {
1✔
515
        result.oldValue = expirable.get();
1✔
516
        return expirable;
1✔
517
      }
518

519
      V copy = copyOf(value);
1✔
520
      if (publishToWriter) {
1✔
521
        publishToCacheWriter(writer::write, () -> new EntryProxy<>(key, value));
1✔
522
      }
523
      if (expirable != null) {
1✔
524
        dispatcher.publishExpired(this, key, expirable.get());
1✔
525
        statistics.recordEvictions(1L);
1✔
526
      }
527

528
      long expireTimeMillis = getWriteExpireTimeMillis(/* created= */ true);
1✔
529
      if (expireTimeMillis == 0) {
1✔
530
        // A zero creation expiry means the entry is already expired and is not added
531
        dispatcher.publishExpired(this, key, copy);
1✔
532
        return null;
1✔
533
      } else {
534
        result.written = true;
1✔
535
        dispatcher.publishCreated(this, key, copy);
1✔
536
        return new Expirable<>(copy, expireTimeMillis);
1✔
537
      }
538
    });
539
    return result;
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
    V value = removeNoCopyOrAwait(key, /* publishToWriter= */ true);
1✔
550
    dispatcher.awaitSynchronous();
1✔
551

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

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

591
  @Override
592
  @CanIgnoreReturnValue
593
  public boolean remove(K key, V oldValue) {
594
    requireNotClosed();
1✔
595
    requireNonNull(key);
1✔
596
    requireNonNull(oldValue);
1✔
597

598
    boolean statsEnabled = statistics.isEnabled();
1✔
599
    long start = statsEnabled ? ticker.read() : 0L;
1✔
600
    boolean[] found = { false };
1✔
601

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

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

638
  @Override
639
  public @Nullable V getAndRemove(K key) {
640
    requireNotClosed();
1✔
641
    requireNonNull(key);
1✔
642
    boolean statsEnabled = statistics.isEnabled();
1✔
643
    long start = statsEnabled ? ticker.read() : 0L;
1✔
644

645
    V value = removeNoCopyOrAwait(key, /* publishToWriter= */ true);
1✔
646
    dispatcher.awaitSynchronous();
1✔
647

648
    V copy = (value == null) ? null : copyOf(value);
1✔
649
    if (statsEnabled) {
1✔
650
      long duration = ticker.read() - start;
1✔
651
      if (copy == null) {
1✔
652
        statistics.recordMisses(1L);
1✔
653
      } else {
654
        statistics.recordHits(1L);
1✔
655
        statistics.recordRemovals(1L);
1✔
656
        statistics.recordRemoveTime(duration);
1✔
657
      }
658
      statistics.recordGetTime(duration);
1✔
659
    }
660
    return copy;
1✔
661
  }
662

663
  @Override
664
  public boolean replace(K key, V oldValue, V newValue) {
665
    requireNotClosed();
1✔
666
    requireNonNull(oldValue);
1✔
667
    requireNonNull(newValue);
1✔
668

669
    boolean statsEnabled = statistics.isEnabled();
1✔
670
    long start = statsEnabled ? ticker.read() : 0L;
1✔
671

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

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

704
    if (statsEnabled) {
1✔
705
      statistics.recordMisses(found[0] ? 0L : 1L);
1✔
706
      statistics.recordHits(found[0] ? 1L : 0L);
1✔
707
      long duration = ticker.read() - start;
1✔
708
      if (replaced[0]) {
1✔
709
        statistics.recordPuts(1L);
1✔
710
        statistics.recordPutTime(duration);
1✔
711
      }
712
      statistics.recordGetTime(duration);
1✔
713
    }
714

715
    return replaced[0];
1✔
716
  }
717

718
  @Override
719
  public boolean replace(K key, V value) {
720
    requireNotClosed();
1✔
721
    boolean statsEnabled = statistics.isEnabled();
1✔
722
    long start = statsEnabled ? ticker.read() : 0L;
1✔
723

724
    @Nullable V oldValue = replaceNoCopyOrAwait(key, value);
1✔
725
    dispatcher.awaitSynchronous();
1✔
726
    if (oldValue == null) {
1✔
727
      statistics.recordMisses(1L);
1✔
728
      if (statsEnabled) {
1✔
729
        statistics.recordGetTime(ticker.read() - start);
1✔
730
      }
731
      return false;
1✔
732
    }
733

734
    if (statsEnabled) {
1✔
735
      statistics.recordHits(1L);
1✔
736
      statistics.recordPuts(1L);
1✔
737
      long duration = ticker.read() - start;
1✔
738
      statistics.recordGetTime(duration);
1✔
739
      statistics.recordPutTime(duration);
1✔
740
    }
741
    return true;
1✔
742
  }
743

744
  @Override
745
  public @Nullable V getAndReplace(K key, V value) {
746
    requireNotClosed();
1✔
747
    boolean statsEnabled = statistics.isEnabled();
1✔
748
    long start = statsEnabled ? ticker.read() : 0L;
1✔
749

750
    V oldValue = replaceNoCopyOrAwait(key, value);
1✔
751
    dispatcher.awaitSynchronous();
1✔
752

753
    V copy = (oldValue == null) ? null : copyOf(oldValue);
1✔
754
    if (statsEnabled) {
1✔
755
      long duration = ticker.read() - start;
1✔
756
      if (copy == null) {
1✔
757
        statistics.recordMisses(1L);
1✔
758
      } else {
759
        statistics.recordHits(1L);
1✔
760
        statistics.recordPuts(1L);
1✔
761
        statistics.recordPutTime(duration);
1✔
762
      }
763
      statistics.recordGetTime(duration);
1✔
764
    }
765
    return copy;
1✔
766
  }
767

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

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

800
  @Override
801
  public void removeAll(Set<? extends K> keys) {
802
    requireNotClosed();
1✔
803
    var keysToRemove = new LinkedHashSet<>(keys);
1✔
804
    keysToRemove.forEach(Objects::requireNonNull);
1✔
805

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

823
    @Var int removed = 0;
1✔
824
    try {
825
      for (var key : keysToRemove) {
1✔
826
        if (!failedKeys.contains(key)
1✔
827
            && (removeNoCopyOrAwait(key, /* publishToWriter= */ false) != null)) {
1✔
828
          removed++;
1✔
829
        }
830
      }
1✔
831
    } finally {
832
      dispatcher.awaitSynchronous();
1✔
833
    }
834

835
    if (statsEnabled && (removed > 0)) {
1✔
836
      statistics.recordRemovals(removed);
1✔
837
      statistics.recordRemoveTime(ticker.read() - start);
1✔
838
    }
839
    if (error != null) {
1✔
840
      throw error;
1✔
841
    }
842
  }
1✔
843

844
  @Override
845
  public void removeAll() {
846
    removeAll(cache.asMap().keySet());
1✔
847
  }
1✔
848

849
  @Override
850
  public void clear() {
851
    requireNotClosed();
1✔
852
    cache.invalidateAll();
1✔
853
  }
1✔
854

855
  @Override
856
  public <C extends Configuration<K, V>> C getConfiguration(Class<C> clazz) {
857
    if (clazz.isInstance(configuration)) {
1✔
858
      synchronized (configuration) {
1✔
859
        return clazz.cast(configuration.immutableCopy());
1✔
860
      }
861
    }
862
    throw new IllegalArgumentException("The configuration class " + clazz
1✔
863
        + " is not supported by this implementation");
864
  }
865

866
  @Override
867
  public <T extends @Nullable Object> T invoke(K key,
868
      EntryProcessor<K, V, T> entryProcessor, Object... arguments) {
869
    requireNonNull(entryProcessor);
1✔
870
    requireNonNull(arguments);
1✔
871
    requireNotClosed();
1✔
872

873
    var result = new Object[1];
1✔
874
    var failure = new Throwable[1];
1✔
875
    BiFunction<K, Expirable<V>, Expirable<V>> remappingFunction = (k, expirable) -> {
1✔
876
      // Publish a lazily-expired prior's expiration before the processor observes it as absent,
877
      // so listeners see a linearizable sequence and the expiration is committed exactly once
878
      boolean expired;
879
      Expirable<V> prior;
880
      if ((expirable != null) && !expirable.isEternal()
1✔
881
          && expirable.hasExpired(currentTimeMillis())) {
1✔
882
        dispatcher.publishExpired(this, key, expirable.get());
1✔
883
        statistics.recordEvictions(1L);
1✔
884
        expired = true;
1✔
885
        prior = null;
1✔
886
      } else {
887
        prior = expirable;
1✔
888
        expired = false;
1✔
889
      }
890

891
      V value;
892
      if (prior == null) {
1✔
893
        statistics.recordMisses(1L);
1✔
894
        value = null;
1✔
895
      } else {
896
        value = copyOf(prior.get());
1✔
897
        statistics.recordHits(1L);
1✔
898
      }
899
      var entry = new EntryProcessorEntry<>(key, value,
1✔
900
          configuration.isReadThrough() ? cacheLoader : Optional.empty());
1✔
901
      try {
902
        result[0] = entryProcessor.process(entry, arguments);
1✔
903
        return postProcess(prior, entry);
1✔
904
      } catch (Throwable e) {
1✔
905
        if (!expired) {
1✔
906
          throw processorFailure(e);
1✔
907
        }
908
        failure[0] = e;
1✔
909
        return null;
1✔
910
      }
911
    };
912
    try {
913
      cache.asMap().compute(copyOf(key), remappingFunction);
1✔
914
    } catch (Throwable t) {
1✔
915
      dispatcher.ignoreSynchronous();
1✔
916
      throw t;
1✔
917
    }
1✔
918
    dispatcher.awaitSynchronous();
1✔
919
    if (failure[0] != null) {
1✔
920
      throw processorFailure(failure[0]);
1✔
921
    }
922

923
    @SuppressWarnings("unchecked")
924
    var castedResult = (T) result[0];
1✔
925
    return castedResult;
1✔
926
  }
927

928
  /** Returns the exception to rethrow on an entry processor failure. */
929
  private static RuntimeException processorFailure(Throwable e) {
930
    if (e instanceof Error) {
1✔
931
      throw (Error) e;
1✔
932
    } else if (e instanceof EntryProcessorException) {
1✔
933
      return (EntryProcessorException) e;
1✔
934
    }
935
    return new EntryProcessorException(e);
1✔
936
  }
937

938
  /**
939
   * Returns the updated expirable value after performing the post-processing actions. A null
940
   * {@code expirable} means the entry was absent and READ/UPDATED (which require a live prior)
941
   * never observe one.
942
   */
943
  @Nullable Expirable<V> postProcess(
944
      @Nullable Expirable<V> expirable, EntryProcessorEntry<K, V> entry) {
945
    switch (entry.getAction()) {
1✔
946
      case NONE:
947
        return expirable;
1✔
948
      case READ: {
949
        setAccessExpireTime(requireNonNull(expirable), getAccessExpireTime(), 0L);
1✔
950
        return expirable;
1✔
951
      }
952
      case CREATED:
953
      case LOADED: {
954
        V value = requireNonNull(entry.getValue());
1✔
955
        V copy = copyOf(value);
1✔
956
        if (entry.getAction() == Action.CREATED) {
1✔
957
          publishToCacheWriter(writer::write, () -> new EntryProxy<>(entry.getKey(), value));
1✔
958
        }
959
        long expireTimeMillis = getWriteExpireTimeMillis(/* created= */ true);
1✔
960
        if (expireTimeMillis == 0) {
1✔
961
          // A zero creation expiry means the entry is already expired and is not added, so the
962
          // create is neither counted nor published
963
          dispatcher.publishExpired(this, entry.getKey(), copy);
1✔
964
          return null;
1✔
965
        }
966
        if (entry.getAction() == Action.CREATED) {
1✔
967
          statistics.recordPuts(1L);
1✔
968
        }
969
        dispatcher.publishCreated(this, entry.getKey(), copy);
1✔
970
        return new Expirable<>(copy, expireTimeMillis);
1✔
971
      }
972
      case UPDATED: {
973
        requireNonNull(expirable, "Expected a previous value but was null");
1✔
974
        V value = requireNonNull(entry.getValue(), "Expected a new value but was null");
1✔
975
        V copy = copyOf(value);
1✔
976
        publishToCacheWriter(writer::write, () -> new EntryProxy<>(entry.getKey(), value));
1✔
977
        statistics.recordPuts(1L);
1✔
978
        dispatcher.publishUpdated(this, entry.getKey(), expirable.get(), copy);
1✔
979
        @Var long expireTimeMillis = getWriteExpireTimeMillis(/* created= */ false);
1✔
980
        if (expireTimeMillis == Long.MIN_VALUE) {
1✔
981
          expireTimeMillis = expirable.getExpireTimeMillis();
1✔
982
        }
983
        return new Expirable<>(copy, expireTimeMillis);
1✔
984
      }
985
      case DELETED:
986
        publishToCacheWriter(writer::delete, entry::getKey);
1✔
987
        if (expirable != null) {
1✔
988
          statistics.recordRemovals(1L);
1✔
989
          dispatcher.publishRemoved(this, entry.getKey(), expirable.get());
1✔
990
        }
991
        return null;
1✔
992
    }
993
    throw new IllegalStateException("Unknown state: " + entry.getAction());
1✔
994
  }
995

996
  @Override
997
  public <T> Map<K, EntryProcessorResult<T>> invokeAll(Set<? extends K> keys,
998
      EntryProcessor<K, V, T> entryProcessor, Object... arguments) {
999
    requireNotClosed();
1✔
1000
    requireNonNull(keys);
1✔
1001
    requireNonNull(arguments);
1✔
1002
    requireNonNull(entryProcessor);
1✔
1003
    keys.forEach(Objects::requireNonNull);
1✔
1004

1005
    var results = new HashMap<K, EntryProcessorResult<T>>(keys.size(), 1.0f);
1✔
1006
    for (K key : keys) {
1✔
1007
      try {
1008
        T result = invoke(key, entryProcessor, arguments);
1✔
1009
        if (result != null) {
1✔
1010
          results.put(key, () -> result);
1✔
1011
        }
1012
      } catch (EntryProcessorException e) {
1✔
1013
        results.put(key, () -> { throw e; });
1✔
1014
      } catch (RuntimeException e) {
1✔
1015
        results.put(key, () -> { throw new EntryProcessorException(e); });
1✔
1016
      }
1✔
1017
    }
1✔
1018
    return results;
1✔
1019
  }
1020

1021
  @Override
1022
  public String getName() {
1023
    return name;
1✔
1024
  }
1025

1026
  @Override
1027
  public CacheManager getCacheManager() {
1028
    return cacheManager;
1✔
1029
  }
1030

1031
  @Override
1032
  public boolean isClosed() {
1033
    return closed;
1✔
1034
  }
1035

1036
  @Override
1037
  public void close() {
1038
    if (isClosed()) {
1✔
1039
      return;
1✔
1040
    }
1041
    synchronized (configuration) {
1✔
1042
      if (!isClosed()) {
1✔
1043
        closed = true;
1✔
1044
        try {
1045
          cacheManager.destroyCache(name, this);
1✔
1046
        } catch (IllegalStateException ignored) { /* manager already closed */ }
1✔
1047

1048
        @Var var thrown = shutdownExecutor();
1✔
1049
        thrown = tryClose(expiry, thrown);
1✔
1050
        thrown = tryClose(writer, thrown);
1✔
1051
        thrown = tryClose(cacheLoader.orElse(null), thrown);
1✔
1052
        for (Registration<K, V> registration : dispatcher.registrations()) {
1✔
1053
          thrown = tryClose(registration.getCacheEntryListener(), thrown);
1✔
1054
        }
1✔
1055
        thrown = tryClose((AutoCloseable) () ->
1✔
1056
            JmxRegistration.unregisterMxBean(this, MBeanType.STATISTICS), thrown);
1✔
1057
        thrown = tryClose((AutoCloseable) () ->
1✔
1058
            JmxRegistration.unregisterMxBean(this, MBeanType.CONFIGURATION), thrown);
1✔
1059
        if (thrown != null) {
1✔
1060
          logger.log(Level.WARNING, "Failure when closing cache resources", thrown);
1✔
1061
        }
1062
      }
1063
    }
1✔
1064
    cache.invalidateAll();
1✔
1065
  }
1✔
1066

1067
  @SuppressWarnings("FutureReturnValueIgnored")
1068
  private @Nullable Throwable shutdownExecutor() {
1069
    if (executor instanceof ExecutorService) {
1✔
1070
      @SuppressWarnings("PMD.CloseResource")
1071
      var es = (ExecutorService) executor;
1✔
1072
      es.shutdown();
1✔
1073
    }
1074

1075
    @Var Throwable thrown = null;
1✔
1076
    try {
1077
      CompletableFuture
1✔
1078
          .allOf(inFlight.toArray(CompletableFuture[]::new))
1✔
1079
          .get(10, TimeUnit.SECONDS);
1✔
1080
    } catch (ExecutionException | TimeoutException e) {
1✔
1081
      thrown = e;
1✔
1082
    } catch (InterruptedException e) {
1✔
1083
      Thread.currentThread().interrupt();
1✔
1084
      thrown = e;
1✔
1085
    }
1✔
1086
    inFlight.clear();
1✔
1087

1088
    if (!(executor instanceof ExecutorService)) {
1✔
1089
      thrown = tryClose(executor, thrown);
1✔
1090
    }
1091
    return thrown;
1✔
1092
  }
1093

1094
  /**
1095
   * Attempts to close the resource. If an error occurs and an outermost exception is set, then adds
1096
   * the error to the suppression list.
1097
   *
1098
   * @param o the resource to close if Closeable
1099
   * @param outer the outermost error, or null if unset
1100
   * @return the outermost error, or null if unset and successful
1101
   */
1102
  private static @Nullable Throwable tryClose(@Nullable Object o, @Nullable Throwable outer) {
1103
    if (o instanceof AutoCloseable) {
1✔
1104
      try {
1105
        ((AutoCloseable) o).close();
1✔
1106
      } catch (Throwable t) {
1✔
1107
        if (outer == null) {
1✔
1108
          return t;
1✔
1109
        } else if (outer != t) {
1✔
1110
          outer.addSuppressed(t);
1✔
1111
        }
1112
      }
1✔
1113
    }
1114
    return outer;
1✔
1115
  }
1116

1117
  @Override
1118
  public <T> T unwrap(Class<T> clazz) {
1119
    if (clazz.isInstance(cache)) {
1✔
1120
      return clazz.cast(cache);
1✔
1121
    } else if (clazz.isInstance(this)) {
1✔
1122
      return clazz.cast(this);
1✔
1123
    }
1124
    throw new IllegalArgumentException("Unwrapping to " + clazz
1✔
1125
        + " is not supported by this implementation");
1126
  }
1127

1128
  @Override
1129
  public void registerCacheEntryListener(
1130
      CacheEntryListenerConfiguration<K, V> cacheEntryListenerConfiguration) {
1131
    synchronized (configuration) {
1✔
1132
      requireNotClosed();
1✔
1133
      configuration.addCacheEntryListenerConfiguration(cacheEntryListenerConfiguration);
1✔
1134
      dispatcher.register(cacheEntryListenerConfiguration);
1✔
1135
    }
1✔
1136
  }
1✔
1137

1138
  @Override
1139
  public void deregisterCacheEntryListener(
1140
      CacheEntryListenerConfiguration<K, V> cacheEntryListenerConfiguration) {
1141
    synchronized (configuration) {
1✔
1142
      requireNotClosed();
1✔
1143
      configuration.removeCacheEntryListenerConfiguration(cacheEntryListenerConfiguration);
1✔
1144
      dispatcher.deregister(cacheEntryListenerConfiguration);
1✔
1145
    }
1✔
1146
  }
1✔
1147

1148
  @Override
1149
  public Iterator<Cache.Entry<K, V>> iterator() {
1150
    requireNotClosed();
1✔
1151
    return new EntryIterator();
1✔
1152
  }
1153

1154
  /** Enables or disables the configuration management JMX bean. */
1155
  void enableManagement(boolean enabled) {
1156
    synchronized (configuration) {
1✔
1157
      requireNotClosed();
1✔
1158
      if (enabled) {
1✔
1159
        JmxRegistration.registerMxBean(this, cacheMxBean, MBeanType.CONFIGURATION);
1✔
1160
      } else {
1161
        JmxRegistration.unregisterMxBean(this, MBeanType.CONFIGURATION);
1✔
1162
      }
1163
      configuration.setManagementEnabled(enabled);
1✔
1164
    }
1✔
1165
  }
1✔
1166

1167
  /** Enables or disables the statistics JMX bean. */
1168
  void enableStatistics(boolean enabled) {
1169
    synchronized (configuration) {
1✔
1170
      requireNotClosed();
1✔
1171
      if (enabled) {
1✔
1172
        JmxRegistration.registerMxBean(this, statistics, MBeanType.STATISTICS);
1✔
1173
      } else {
1174
        JmxRegistration.unregisterMxBean(this, MBeanType.STATISTICS);
1✔
1175
      }
1176
      statistics.enable(enabled);
1✔
1177
      configuration.setStatisticsEnabled(enabled);
1✔
1178
    }
1✔
1179
  }
1✔
1180

1181
  /** Performs the action with the cache writer if write-through is enabled. */
1182
  private <T> void publishToCacheWriter(Consumer<T> action, Supplier<T> data) {
1183
    if (!configuration.isWriteThrough()) {
1✔
1184
      return;
1✔
1185
    }
1186
    try {
1187
      action.accept(data.get());
1✔
1188
    } catch (CacheWriterException e) {
1✔
1189
      throw e;
1✔
1190
    } catch (RuntimeException e) {
1✔
1191
      throw new CacheWriterException("Exception in CacheWriter", e);
1✔
1192
    }
1✔
1193
  }
1✔
1194

1195
  /** Checks that the cache is not closed. */
1196
  protected final void requireNotClosed() {
1197
    if (isClosed()) {
1✔
1198
      throw new IllegalStateException();
1✔
1199
    }
1200
  }
1✔
1201

1202
  /**
1203
   * Returns a copy of the value if value-based caching is enabled.
1204
   *
1205
   * @param object the object to be copied
1206
   * @param <T> the type of object being copied
1207
   * @return a copy of the object if storing by value or the same instance if by reference
1208
   */
1209
  protected final <T> T copyOf(T object) {
1210
    try {
1211
      return requireNonNull(
1✔
1212
          copier.copy(requireNonNull(object), requireNonNull(cacheManager.getClassLoader())));
1✔
1213
    } catch (NullPointerException | IllegalStateException | ClassCastException | CacheException e) {
1✔
1214
      throw e;
1✔
1215
    } catch (RuntimeException e) {
1✔
1216
      throw new CacheException(e);
1✔
1217
    }
1218
  }
1219

1220
  /**
1221
   * Returns a deep copy of the map if value-based caching is enabled.
1222
   *
1223
   * @param map the mapping of keys to expirable values
1224
   * @return a deep or shallow copy of the mappings depending on the store by value setting
1225
   */
1226
  @SuppressWarnings("CollectorMutability")
1227
  protected final Map<K, V> copyMap(Map<K, Expirable<V>> map) {
1228
    return map.entrySet().stream().collect(toMap(
1✔
1229
        entry -> copyOf(entry.getKey()),
1✔
1230
        entry -> copyOf(entry.getValue().get())));
1✔
1231
  }
1232

1233
  /** Returns the current time in milliseconds. */
1234
  protected final long currentTimeMillis() {
1235
    return nanosToMillis(ticker.read());
1✔
1236
  }
1237

1238
  /** Returns the nanosecond time in milliseconds. */
1239
  protected static long nanosToMillis(long nanos) {
1240
    return TimeUnit.NANOSECONDS.toMillis(nanos);
1✔
1241
  }
1242

1243
  /** Returns the duration to expire an accessed entry after, or {@code null} if unchanged. */
1244
  protected final @Nullable Duration getAccessExpireTime() {
1245
    try {
1246
      return expiry.getExpiryForAccess();
1✔
1247
    } catch (RuntimeException e) {
1✔
1248
      logger.log(Level.WARNING, "Failed to get the policy's expiration time", e);
1✔
1249
      return null;
1✔
1250
    }
1251
  }
1252

1253
  /**
1254
   * Sets the JCache access expiration time.
1255
   *
1256
   * @param expirable the entry that was operated on
1257
   * @param duration the access duration, or null if unchanged
1258
   * @param currentTimeMillis the current time, or zero if not read yet
1259
   */
1260
  protected final void setAccessExpireTime(Expirable<?> expirable,
1261
      @Nullable Duration duration, @Var long currentTimeMillis) {
1262
    if (duration == null) {
1✔
1263
      return;
1✔
1264
    } else if (duration.isZero()) {
1✔
1265
      expirable.setExpireTimeMillis(0L);
1✔
1266
    } else if (duration.isEternal()) {
1✔
1267
      expirable.setExpireTimeMillis(Long.MAX_VALUE);
1✔
1268
    } else {
1269
      if (currentTimeMillis == 0L) {
1✔
1270
        currentTimeMillis = currentTimeMillis();
1✔
1271
      }
1272
      @Var long expireTimeMillis = duration.getAdjustedTime(currentTimeMillis);
1✔
1273
      expireTimeMillis = ((expireTimeMillis == 0L) || (expireTimeMillis == Long.MAX_VALUE))
1✔
1274
          ? (expireTimeMillis - 1)
1✔
1275
          : expireTimeMillis;
1✔
1276
      expirable.setExpireTimeMillis(expireTimeMillis);
1✔
1277
    }
1278
  }
1✔
1279

1280
  /**
1281
   * Sets the native access expiration time.
1282
   *
1283
   * @param key the entry's key
1284
   * @param duration the access duration, or null if unchanged
1285
   */
1286
  protected final void setVariableExpiration(K key, @Nullable Duration duration) {
1287
    if (duration == null) {
1✔
1288
      return;
1✔
1289
    }
1290
    cache.policy().expireVariably().ifPresent(policy -> {
1✔
1291
      if (duration.isZero()) {
1✔
1292
        policy.setExpiresAfter(key, 0L, TimeUnit.NANOSECONDS);
1✔
1293
      } else if (duration.isEternal()) {
1✔
1294
        policy.setExpiresAfter(key, Long.MAX_VALUE, TimeUnit.NANOSECONDS);
1✔
1295
      } else {
1296
        policy.setExpiresAfter(key, duration.getDurationAmount(), duration.getTimeUnit());
1✔
1297
      }
1298
    });
1✔
1299
  }
1✔
1300

1301
  /**
1302
   * Returns the time when the entry will expire.
1303
   *
1304
   * @param created if the write operation is an insert or an update
1305
   * @return the time when the entry will expire, zero if it should expire immediately,
1306
   *         Long.MIN_VALUE if it should not be changed, or Long.MAX_VALUE if eternal
1307
   */
1308
  protected final long getWriteExpireTimeMillis(boolean created) {
1309
    try {
1310
      Duration duration = created ? expiry.getExpiryForCreation() : expiry.getExpiryForUpdate();
1✔
1311
      if (duration == null) {
1✔
1312
        return created ? Long.MAX_VALUE : Long.MIN_VALUE;
1✔
1313
      } else if (duration.isZero()) {
1✔
1314
        return 0L;
1✔
1315
      } else if (duration.isEternal()) {
1✔
1316
        return Long.MAX_VALUE;
1✔
1317
      }
1318
      long expireTimeMillis = duration.getAdjustedTime(currentTimeMillis());
1✔
1319
      return ((expireTimeMillis == 0L) || (expireTimeMillis == Long.MAX_VALUE))
1✔
1320
          ? (expireTimeMillis - 1)
1✔
1321
          : expireTimeMillis;
1✔
1322
    } catch (RuntimeException e) {
1✔
1323
      logger.log(Level.WARNING, "Failed to get the policy's expiration time", e);
1✔
1324
      return created ? Long.MAX_VALUE : Long.MIN_VALUE;
1✔
1325
    }
1326
  }
1327

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

1334
    Map.@Nullable Entry<K, Expirable<V>> current;
1335
    Map.@Nullable Entry<K, Expirable<V>> cursor;
1336

1337
    @Override
1338
    public boolean hasNext() {
1339
      while ((cursor == null) && delegate.hasNext()) {
1✔
1340
        Map.Entry<K, Expirable<V>> entry = delegate.next();
1✔
1341
        long millis = entry.getValue().isEternal() ? 0L : currentTimeMillis();
1✔
1342
        if (!entry.getValue().hasExpired(millis)) {
1✔
1343
          var duration = getAccessExpireTime();
1✔
1344
          setVariableExpiration(entry.getKey(), duration);
1✔
1345
          setAccessExpireTime(entry.getValue(), duration, millis);
1✔
1346
          cursor = entry;
1✔
1347
        }
1348
      }
1✔
1349
      return (cursor != null);
1✔
1350
    }
1351

1352
    @Override
1353
    public Cache.Entry<K, V> next() {
1354
      if (!hasNext()) {
1✔
1355
        throw new NoSuchElementException();
1✔
1356
      }
1357
      statistics.recordHits(1L);
1✔
1358
      current = requireNonNull(cursor);
1✔
1359
      cursor = null;
1✔
1360
      return new EntryProxy<>(copyOf(current.getKey()), copyOf(current.getValue().get()));
1✔
1361
    }
1362

1363
    @Override
1364
    @SuppressWarnings("CheckReturnValue")
1365
    public void remove() {
1366
      if (current == null) {
1✔
1367
        throw new IllegalStateException();
1✔
1368
      }
1369
      CacheProxy.this.remove(current.getKey());
1✔
1370
      current = null;
1✔
1371
    }
1✔
1372
  }
1373

1374
  protected static final class PutResult<V> {
1✔
1375
    @Nullable V oldValue;
1376
    boolean written;
1377
  }
1378

1379
  protected enum NullCompletionListener implements CompletionListener {
1✔
1380
    INSTANCE;
1✔
1381

1382
    @Override
1383
    public void onCompletion() {}
1✔
1384

1385
    @Override
1386
    public void onException(Exception e) {}
1✔
1387
  }
1388
}
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