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

ben-manes / caffeine / #5698

30 Jul 2026 09:29AM UTC coverage: 99.918% (-0.07%) from 99.988%
#5698

push

github

ben-manes
Fix minor defects found by a review sweep

- Prefer the operation's own failure over a throwing synchronous jcache
  listener's, retaining the latter as suppressed. awaitSynchronous()
  became throwing, so putAll, removeAll, invoke, and LoadingCacheProxy's
  get and getAll discarded the writer, loader, or entry processor
  failure that the specification requires the caller to observe.
- Chain the per-key dispatch queue with handleAsync so that a
  predecessor which completed exceptionally, as dispatch rethrows an
  Error, cannot suppress the next event's delivery. thenApplyAsync
  relayed the throwable and silently never invoked the listener, losing
  a same-key event. The predecessor's failure is still observed through
  the pending list, which holds each synchronous event's own future.
- Guard the synchronous listener exception fold against self-
  suppression, as a listener that rethrows a stored exception yields the
  same instance for two events and Throwable.addSuppressed then throws,
  masking the failure it was folding. Restore awaitSynchronous()'s fast
  path for when nothing is pending, which delegating to
  chainSynchronous() had dropped.
- Span the CompletionListener notification with loadAll's tracked future
  so that close() awaits the callback rather than returning while it is
  still queued, and clear the pending synchronous futures in a finally
  so an Error cannot strand them for the next load to join and
  misreport. The notification is composed onto the load rather than
  joined, as its dispatches run on the executor that the load itself
  occupies.
- Unregister the JMX beans before destroyCache frees the cache name, as
  otherwise a same-named replacement cache's beans are unregistered
  instead, and route them through the toggles so that the statistics and
  configuration flags are applied and a failing unregistration is
  aggregated rather than aborting the close. Share the suppression logic
  as suppress(), which tolerates a nu... (continued)

4221 of 4239 branches covered (99.58%)

83 of 88 new or added lines in 7 files covered. (94.32%)

1 existing line in 1 file now uncovered.

8523 of 8530 relevant lines covered (99.92%)

1.0 hits per line

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

99.61
/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.event.CacheEntryListenerException;
53
import javax.cache.expiry.Duration;
54
import javax.cache.expiry.ExpiryPolicy;
55
import javax.cache.integration.CacheLoader;
56
import javax.cache.integration.CacheLoaderException;
57
import javax.cache.integration.CacheWriter;
58
import javax.cache.integration.CacheWriterException;
59
import javax.cache.integration.CompletionListener;
60
import javax.cache.processor.EntryProcessor;
61
import javax.cache.processor.EntryProcessorException;
62
import javax.cache.processor.EntryProcessorResult;
63

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

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

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

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

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

107
  private volatile boolean closed;
108

109
  @SuppressWarnings({"PMD.ExcessiveParameterList", "this-escape", "TooManyParameters"})
110
  public CacheProxy(String name, Executor executor, CacheManagerImpl cacheManager,
111
      CaffeineConfiguration<K, V> configuration,
112
      com.github.benmanes.caffeine.cache.Cache<K, @Nullable Expirable<V>> cache,
113
      EventDispatcher<K, V> dispatcher, Optional<CacheLoader<K, V>> cacheLoader,
114
      ExpiryPolicy expiry, Ticker ticker, JCacheStatisticsMXBean statistics, Copier copier) {
1✔
115
    this.writer = requireNonNullElse(configuration.getCacheWriter(), DisabledCacheWriter.get());
1✔
116
    this.configuration = requireNonNull(configuration);
1✔
117
    this.cacheManager = requireNonNull(cacheManager);
1✔
118
    this.cacheLoader = requireNonNull(cacheLoader);
1✔
119
    this.inFlight = ConcurrentHashMap.newKeySet();
1✔
120
    this.dispatcher = requireNonNull(dispatcher);
1✔
121
    this.statistics = requireNonNull(statistics);
1✔
122
    this.cacheMxBean = new JCacheMXBean(this);
1✔
123
    this.executor = requireNonNull(executor);
1✔
124
    this.expiry = requireNonNull(expiry);
1✔
125
    this.ticker = requireNonNull(ticker);
1✔
126
    this.cache = requireNonNull(cache);
1✔
127
    this.copier = requireNonNull(copier);
1✔
128
    this.name = requireNonNull(name);
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(1L);
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(1L);
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
    var duration = getAccessExpireTime();
1✔
193
    setVariableExpiration(key, duration);
1✔
194
    setAccessExpireTime(expirable, duration, millis);
1✔
195
    V value = copyOf(expirable.get());
1✔
196
    if (statsEnabled) {
1✔
197
      statistics.recordHits(1L);
1✔
198
      statistics.recordGetTime(ticker.read() - start);
1✔
199
    }
200
    return value;
1✔
201
  }
202

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

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

220
  /**
221
   * Returns all of the mappings present, expiring as required, and updates their access expiry
222
   * time.
223
   */
224
  protected Map<K, Expirable<V>> getAndFilterExpiredEntries(Set<? extends K> keys) {
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
      var duration = getAccessExpireTime();
1✔
244
      setVariableExpiration(entry.getKey(), duration);
1✔
245
      setAccessExpireTime(entry.getValue(), duration, millis[0]);
1✔
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
    if (cacheLoader.isEmpty()) {
1✔
265
      listener.onCompletion();
1✔
266
      return;
1✔
267
    }
268

269
    var future = new CompletableFuture<@Nullable Void>();
1✔
270
    synchronized (configuration) {
1✔
271
      requireNotClosed();
1✔
272
      inFlight.add(future);
1✔
273
    }
1✔
274
    try {
275
      CompletableFuture.<CompletableFuture<@Nullable Void>>supplyAsync(() -> {
1✔
276
        @Var boolean success = false;
1✔
277
        try {
278
          if (replaceExistingValues) {
1✔
279
            loadAllAndReplaceExisting(keys);
1✔
280
          } else {
281
            loadAllAndKeepExisting(keys);
1✔
282
          }
283
          success = true;
1✔
284
        } catch (CacheLoaderException e) {
1✔
285
          listener.onException(e);
1✔
286
        } catch (RuntimeException e) {
1✔
287
          listener.onException(new CacheLoaderException(e));
1✔
288
        } finally {
289
          if (!success) {
1✔
290
            dispatcher.ignoreSynchronous();
1✔
291
          }
292
        }
293
        if (!success) {
1✔
294
          return CompletableFuture.completedFuture(null);
1✔
295
        }
296
        // the tracked future spans the notification so that close() awaits the listener's callback
297
        return dispatcher.chainSynchronous().<@Nullable Void>handle((failure, error) -> {
1✔
298
          if (error != null) {
1✔
299
            listener.onException(new CacheLoaderException(error));
1✔
300
          } else if (failure != null) {
1✔
301
            listener.onException(failure);
1✔
302
          } else {
303
            listener.onCompletion();
1✔
304
          }
305
          return null;
1✔
306
        });
307
      }, executor).thenCompose(chain -> chain).whenComplete((r, e) -> {
1✔
308
        inFlight.remove(future);
1✔
309
        future.complete(null);
1✔
310
      });
1✔
311
    } catch (RuntimeException e) {
1✔
312
      inFlight.remove(future);
1✔
313
      future.complete(null);
1✔
314
      listener.onException(new CacheLoaderException(e));
1✔
315
    }
1✔
316
  }
1✔
317

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

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

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

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

354
    if (statsEnabled && result.written) {
1✔
355
      statistics.recordPuts(1);
1✔
356
      statistics.recordPutTime(ticker.read() - start);
1✔
357
    }
358
  }
1✔
359

360
  @Override
361
  public @Nullable V getAndPut(K key, V value) {
362
    requireNotClosed();
1✔
363
    boolean statsEnabled = statistics.isEnabled();
1✔
364
    long start = statsEnabled ? ticker.read() : 0L;
1✔
365

366
    var result = putNoCopyOrAwait(key, value, /* publishToWriter= */ true);
1✔
367
    dispatcher.awaitSynchronous();
1✔
368

369
    if (statsEnabled) {
1✔
370
      if (result.oldValue == null) {
1✔
371
        statistics.recordMisses(1L);
1✔
372
      } else {
373
        statistics.recordHits(1L);
1✔
374
      }
375
      long duration = ticker.read() - start;
1✔
376
      if (result.written) {
1✔
377
        statistics.recordPuts(1);
1✔
378
        statistics.recordPutTime(duration);
1✔
379
      }
380
      statistics.recordGetTime(duration);
1✔
381
    }
382
    return (result.oldValue == null) ? null : copyOf(result.oldValue);
1✔
383
  }
384

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

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

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

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

458
    @Var int puts = 0;
1✔
459
    try {
460
      for (var entry : map.entrySet()) {
1✔
461
        if (!failedKeys.contains(entry.getKey())) {
1✔
462
          var result = putNoCopyOrAwait(entry.getKey(),
1✔
463
              entry.getValue(), /* publishToWriter= */ false);
1✔
464
          if (result.written) {
1✔
465
            puts++;
1✔
466
          }
467
        }
468
      }
1✔
469
    } catch (Throwable t) {
1✔
470
      awaitAndSuppressFailure(t);
1✔
471
      throw t;
1✔
472
    }
1✔
473
    var listenerFailure = awaitSynchronousFailure();
1✔
474

475
    if (statsEnabled && (puts > 0)) {
1✔
476
      statistics.recordPuts(puts);
1✔
477
      statistics.recordPutTime(ticker.read() - start);
1✔
478
    }
479
    var failure = suppress(error, listenerFailure);
1✔
480
    if (failure != null) {
1✔
481
      throw failure;
1✔
482
    }
483
  }
1✔
484

485
  @Override
486
  public boolean putIfAbsent(K key, V value) {
487
    requireNotClosed();
1✔
488
    requireNonNull(value);
1✔
489
    boolean statsEnabled = statistics.isEnabled();
1✔
490
    long start = statsEnabled ? ticker.read() : 0L;
1✔
491

492
    var result = putIfAbsentNoAwait(key, value, /* publishToWriter= */ true);
1✔
493
    dispatcher.awaitSynchronous();
1✔
494

495
    if (statsEnabled) {
1✔
496
      if (result.oldValue != null) {
1✔
497
        statistics.recordHits(1L);
1✔
498
      } else {
499
        statistics.recordMisses(1L);
1✔
500
      }
501
      if (result.written) {
1✔
502
        statistics.recordPuts(1L);
1✔
503
        statistics.recordPutTime(ticker.read() - start);
1✔
504
      }
505
    }
506
    return result.written;
1✔
507
  }
508

509
  /**
510
   * Associates the specified value with the specified key in the cache if there is no existing
511
   * mapping.
512
   *
513
   * @param key key with which the specified value is to be associated
514
   * @param value value to be associated with the specified key
515
   * @param publishToWriter if the writer should be notified
516
   * @return the oldValue and if the new value was stored
517
   */
518
  @CanIgnoreReturnValue
519
  private PutResult<V> putIfAbsentNoAwait(K key, V value, boolean publishToWriter) {
520
    var result = new PutResult<V>();
1✔
521
    cache.asMap().compute(copyOf(key), (K k, Expirable<V> expirable) -> {
1✔
522
      if ((expirable != null)
1✔
523
          && (expirable.isEternal() || !expirable.hasExpired(currentTimeMillis()))) {
1✔
524
        result.oldValue = expirable.get();
1✔
525
        return expirable;
1✔
526
      }
527

528
      V copy = copyOf(value);
1✔
529
      if (publishToWriter) {
1✔
530
        publishToCacheWriter(writer::write, () -> new EntryProxy<>(key, value));
1✔
531
      }
532
      if (expirable != null) {
1✔
533
        dispatcher.publishExpired(this, key, expirable.get());
1✔
534
        statistics.recordEvictions(1L);
1✔
535
      }
536

537
      long expireTimeMillis = getWriteExpireTimeMillis(/* created= */ true);
1✔
538
      if (expireTimeMillis == 0) {
1✔
539
        // A zero creation expiry means the entry is already expired and is not added
540
        dispatcher.publishExpired(this, key, copy);
1✔
541
        return null;
1✔
542
      } else {
543
        result.written = true;
1✔
544
        dispatcher.publishCreated(this, key, copy);
1✔
545
        return new Expirable<>(copy, expireTimeMillis);
1✔
546
      }
547
    });
548
    return result;
1✔
549
  }
550

551
  @Override
552
  public boolean remove(K key) {
553
    requireNotClosed();
1✔
554
    requireNonNull(key);
1✔
555
    boolean statsEnabled = statistics.isEnabled();
1✔
556
    long start = statsEnabled ? ticker.read() : 0L;
1✔
557

558
    V value = removeNoCopyOrAwait(key, /* publishToWriter= */ true);
1✔
559
    dispatcher.awaitSynchronous();
1✔
560

561
    if (value != null) {
1✔
562
      statistics.recordRemovals(1L);
1✔
563
      if (statsEnabled) {
1✔
564
        statistics.recordRemoveTime(ticker.read() - start);
1✔
565
      }
566
      return true;
1✔
567
    }
568
    return false;
1✔
569
  }
570

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

600
  @Override
601
  @CanIgnoreReturnValue
602
  public boolean remove(K key, V oldValue) {
603
    requireNotClosed();
1✔
604
    requireNonNull(key);
1✔
605
    requireNonNull(oldValue);
1✔
606

607
    boolean statsEnabled = statistics.isEnabled();
1✔
608
    long start = statsEnabled ? ticker.read() : 0L;
1✔
609
    boolean[] found = { false };
1✔
610

611
    boolean[] removed = { false };
1✔
612
    cache.asMap().computeIfPresent(key, (k, expirable) -> {
1✔
613
      long millis = expirable.isEternal()
1✔
614
          ? 0L
1✔
615
          : nanosToMillis((start == 0L) ? ticker.read() : start);
1✔
616
      if (expirable.hasExpired(millis)) {
1✔
617
        dispatcher.publishExpired(this, key, expirable.get());
1✔
618
        statistics.recordEvictions(1L);
1✔
619
        return null;
1✔
620
      }
621

622
      found[0] = true;
1✔
623
      if (oldValue.equals(expirable.get())) {
1✔
624
        publishToCacheWriter(writer::delete, () -> key);
1✔
625
        dispatcher.publishRemoved(this, key, expirable.get());
1✔
626
        removed[0] = true;
1✔
627
        return null;
1✔
628
      }
629
      setAccessExpireTime(expirable, getAccessExpireTime(), millis);
1✔
630
      return expirable;
1✔
631
    });
632
    dispatcher.awaitSynchronous();
1✔
633
    if (statsEnabled) {
1✔
634
      if (removed[0]) {
1✔
635
        statistics.recordRemovals(1L);
1✔
636
        statistics.recordHits(1L);
1✔
637
        statistics.recordRemoveTime(ticker.read() - start);
1✔
638
      } else if (found[0]) {
1✔
639
        statistics.recordHits(1L);
1✔
640
      } else {
641
        statistics.recordMisses(1L);
1✔
642
      }
643
    }
644
    return removed[0];
1✔
645
  }
646

647
  @Override
648
  public @Nullable V getAndRemove(K key) {
649
    requireNotClosed();
1✔
650
    requireNonNull(key);
1✔
651
    boolean statsEnabled = statistics.isEnabled();
1✔
652
    long start = statsEnabled ? ticker.read() : 0L;
1✔
653

654
    V value = removeNoCopyOrAwait(key, /* publishToWriter= */ true);
1✔
655
    dispatcher.awaitSynchronous();
1✔
656

657
    V copy = (value == null) ? null : copyOf(value);
1✔
658
    if (statsEnabled) {
1✔
659
      long duration = ticker.read() - start;
1✔
660
      if (copy == null) {
1✔
661
        statistics.recordMisses(1L);
1✔
662
      } else {
663
        statistics.recordHits(1L);
1✔
664
        statistics.recordRemovals(1L);
1✔
665
        statistics.recordRemoveTime(duration);
1✔
666
      }
667
      statistics.recordGetTime(duration);
1✔
668
    }
669
    return copy;
1✔
670
  }
671

672
  @Override
673
  public boolean replace(K key, V oldValue, V newValue) {
674
    requireNotClosed();
1✔
675
    requireNonNull(oldValue);
1✔
676
    requireNonNull(newValue);
1✔
677

678
    boolean statsEnabled = statistics.isEnabled();
1✔
679
    long start = statsEnabled ? ticker.read() : 0L;
1✔
680

681
    boolean[] found = { false };
1✔
682
    boolean[] replaced = { false };
1✔
683
    cache.asMap().computeIfPresent(key, (k, expirable) -> {
1✔
684
      long millis = expirable.isEternal()
1✔
685
          ? 0L
1✔
686
          : nanosToMillis((start == 0L) ? ticker.read() : start);
1✔
687
      if (expirable.hasExpired(millis)) {
1✔
688
        dispatcher.publishExpired(this, key, expirable.get());
1✔
689
        statistics.recordEvictions(1L);
1✔
690
        return null;
1✔
691
      }
692

693
      found[0] = true;
1✔
694
      Expirable<V> result;
695
      if (oldValue.equals(expirable.get())) {
1✔
696
        V copy = copyOf(newValue);
1✔
697
        publishToCacheWriter(writer::write, () -> new EntryProxy<>(key, newValue));
1✔
698
        dispatcher.publishUpdated(this, key, expirable.get(), copy);
1✔
699
        @Var long expireTimeMillis = getWriteExpireTimeMillis(/* created= */ false);
1✔
700
        if (expireTimeMillis == Long.MIN_VALUE) {
1✔
701
          expireTimeMillis = expirable.getExpireTimeMillis();
1✔
702
        }
703
        result = new Expirable<>(copy, expireTimeMillis);
1✔
704
        replaced[0] = true;
1✔
705
      } else {
1✔
706
        result = expirable;
1✔
707
        setAccessExpireTime(expirable, getAccessExpireTime(), millis);
1✔
708
      }
709
      return result;
1✔
710
    });
711
    dispatcher.awaitSynchronous();
1✔
712

713
    if (statsEnabled) {
1✔
714
      statistics.recordMisses(found[0] ? 0L : 1L);
1✔
715
      statistics.recordHits(found[0] ? 1L : 0L);
1✔
716
      long duration = ticker.read() - start;
1✔
717
      if (replaced[0]) {
1✔
718
        statistics.recordPuts(1L);
1✔
719
        statistics.recordPutTime(duration);
1✔
720
      }
721
      statistics.recordGetTime(duration);
1✔
722
    }
723

724
    return replaced[0];
1✔
725
  }
726

727
  @Override
728
  public boolean replace(K key, V value) {
729
    requireNotClosed();
1✔
730
    boolean statsEnabled = statistics.isEnabled();
1✔
731
    long start = statsEnabled ? ticker.read() : 0L;
1✔
732

733
    @Nullable V oldValue = replaceNoCopyOrAwait(key, value);
1✔
734
    dispatcher.awaitSynchronous();
1✔
735
    if (oldValue == null) {
1✔
736
      statistics.recordMisses(1L);
1✔
737
      if (statsEnabled) {
1✔
738
        statistics.recordGetTime(ticker.read() - start);
1✔
739
      }
740
      return false;
1✔
741
    }
742

743
    if (statsEnabled) {
1✔
744
      statistics.recordHits(1L);
1✔
745
      statistics.recordPuts(1L);
1✔
746
      long duration = ticker.read() - start;
1✔
747
      statistics.recordGetTime(duration);
1✔
748
      statistics.recordPutTime(duration);
1✔
749
    }
750
    return true;
1✔
751
  }
752

753
  @Override
754
  public @Nullable V getAndReplace(K key, V value) {
755
    requireNotClosed();
1✔
756
    boolean statsEnabled = statistics.isEnabled();
1✔
757
    long start = statsEnabled ? ticker.read() : 0L;
1✔
758

759
    V oldValue = replaceNoCopyOrAwait(key, value);
1✔
760
    dispatcher.awaitSynchronous();
1✔
761

762
    V copy = (oldValue == null) ? null : copyOf(oldValue);
1✔
763
    if (statsEnabled) {
1✔
764
      long duration = ticker.read() - start;
1✔
765
      if (copy == null) {
1✔
766
        statistics.recordMisses(1L);
1✔
767
      } else {
768
        statistics.recordHits(1L);
1✔
769
        statistics.recordPuts(1L);
1✔
770
        statistics.recordPutTime(duration);
1✔
771
      }
772
      statistics.recordGetTime(duration);
1✔
773
    }
774
    return copy;
1✔
775
  }
776

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

797
      publishToCacheWriter(writer::write, () -> new EntryProxy<>(key, value));
1✔
798
      @Var long expireTimeMillis = getWriteExpireTimeMillis(/* created= */ false);
1✔
799
      if (expireTimeMillis == Long.MIN_VALUE) {
1✔
800
        expireTimeMillis = expirable.getExpireTimeMillis();
1✔
801
      }
802
      dispatcher.publishUpdated(this, key, expirable.get(), copy);
1✔
803
      replaced[0] = expirable.get();
1✔
804
      return new Expirable<>(copy, expireTimeMillis);
1✔
805
    });
806
    return replaced[0];
1✔
807
  }
808

809
  @Override
810
  public void removeAll(Set<? extends K> keys) {
811
    requireNotClosed();
1✔
812
    var keysToRemove = new LinkedHashSet<>(keys);
1✔
813
    keysToRemove.forEach(Objects::requireNonNull);
1✔
814

815
    @Var CacheWriterException error = null;
1✔
816
    @Var Set<? extends K> failedKeys = Set.of();
1✔
817
    boolean statsEnabled = statistics.isEnabled();
1✔
818
    long start = statsEnabled ? ticker.read() : 0L;
1✔
819
    if (configuration.isWriteThrough() && !keysToRemove.isEmpty()) {
1✔
820
      var keysToWrite = new LinkedHashSet<>(keysToRemove);
1✔
821
      try {
822
        writer.deleteAll(keysToWrite);
1✔
823
      } catch (CacheWriterException e) {
1✔
824
        error = e;
1✔
825
        failedKeys = keysToWrite;
1✔
826
      } catch (RuntimeException e) {
1✔
827
        error = new CacheWriterException("Exception in CacheWriter", e);
1✔
828
        failedKeys = keysToWrite;
1✔
829
      }
1✔
830
    }
831

832
    @Var int removed = 0;
1✔
833
    try {
834
      for (var key : keysToRemove) {
1✔
835
        if (!failedKeys.contains(key)
1✔
836
            && (removeNoCopyOrAwait(key, /* publishToWriter= */ false) != null)) {
1✔
837
          removed++;
1✔
838
        }
839
      }
1✔
840
    } catch (Throwable t) {
1✔
841
      awaitAndSuppressFailure(t);
1✔
842
      throw t;
1✔
843
    }
1✔
844
    var listenerFailure = awaitSynchronousFailure();
1✔
845

846
    if (statsEnabled && (removed > 0)) {
1✔
847
      statistics.recordRemovals(removed);
1✔
848
      statistics.recordRemoveTime(ticker.read() - start);
1✔
849
    }
850
    var failure = suppress(error, listenerFailure);
1✔
851
    if (failure != null) {
1✔
852
      throw failure;
1✔
853
    }
854
  }
1✔
855

856
  @Override
857
  public void removeAll() {
858
    removeAll(cache.asMap().keySet());
1✔
859
  }
1✔
860

861
  @Override
862
  public void clear() {
863
    requireNotClosed();
1✔
864
    cache.invalidateAll();
1✔
865
  }
1✔
866

867
  @Override
868
  public <C extends Configuration<K, V>> C getConfiguration(Class<C> clazz) {
869
    if (clazz.isInstance(configuration)) {
1✔
870
      synchronized (configuration) {
1✔
871
        return clazz.cast(configuration.immutableCopy());
1✔
872
      }
873
    }
874
    throw new IllegalArgumentException("The configuration class " + clazz
1✔
875
        + " is not supported by this implementation");
876
  }
877

878
  @Override
879
  public <T extends @Nullable Object> T invoke(K key,
880
      EntryProcessor<K, V, T> entryProcessor, Object... arguments) {
881
    requireNonNull(entryProcessor);
1✔
882
    requireNonNull(arguments);
1✔
883
    requireNotClosed();
1✔
884

885
    var result = new Object[1];
1✔
886
    var failure = new Throwable[1];
1✔
887
    BiFunction<K, Expirable<V>, Expirable<V>> remappingFunction = (k, expirable) -> {
1✔
888
      // Publish a lazily-expired prior's expiration before the processor observes it as absent,
889
      // so listeners see a linearizable sequence and the expiration is committed exactly once
890
      boolean expired;
891
      Expirable<V> prior;
892
      if ((expirable != null) && !expirable.isEternal()
1✔
893
          && expirable.hasExpired(currentTimeMillis())) {
1✔
894
        dispatcher.publishExpired(this, key, expirable.get());
1✔
895
        statistics.recordEvictions(1L);
1✔
896
        expired = true;
1✔
897
        prior = null;
1✔
898
      } else {
899
        prior = expirable;
1✔
900
        expired = false;
1✔
901
      }
902

903
      V value;
904
      if (prior == null) {
1✔
905
        statistics.recordMisses(1L);
1✔
906
        value = null;
1✔
907
      } else {
908
        value = copyOf(prior.get());
1✔
909
        statistics.recordHits(1L);
1✔
910
      }
911
      var entry = new EntryProcessorEntry<>(key, value,
1✔
912
          configuration.isReadThrough() ? cacheLoader : Optional.empty());
1✔
913
      try {
914
        result[0] = entryProcessor.process(entry, arguments);
1✔
915
        return postProcess(prior, entry);
1✔
916
      } catch (Throwable e) {
1✔
917
        if (!expired) {
1✔
918
          throw processorFailure(e);
1✔
919
        }
920
        failure[0] = e;
1✔
921
        return null;
1✔
922
      }
923
    };
924
    try {
925
      cache.asMap().compute(copyOf(key), remappingFunction);
1✔
926
    } catch (Throwable t) {
1✔
927
      dispatcher.ignoreSynchronous();
1✔
928
      throw t;
1✔
929
    }
1✔
930
    var listenerFailure = awaitSynchronousFailure();
1✔
931
    if (failure[0] != null) {
1✔
932
      var error = processorFailure(failure[0]);
1✔
933
      if (listenerFailure != null) {
1!
NEW
934
        error.addSuppressed(listenerFailure);
×
935
      }
936
      throw error;
1✔
937
    }
938
    if (listenerFailure != null) {
1!
NEW
939
      throw listenerFailure;
×
940
    }
941

942
    @SuppressWarnings("unchecked")
943
    var castedResult = (T) result[0];
1✔
944
    return castedResult;
1✔
945
  }
946

947
  /**
948
   * Waits for the synchronous listeners and returns their failure instead of throwing it. An
949
   * {@code Error} is not captured and propagates.
950
   */
951
  protected final @Nullable CacheEntryListenerException awaitSynchronousFailure() {
952
    try {
953
      dispatcher.awaitSynchronous();
1✔
954
      return null;
1✔
955
    } catch (CacheEntryListenerException e) {
1✔
956
      return e;
1✔
957
    }
958
  }
959

960
  /** Waits for the synchronous listeners and suppresses any failures onto the existing error. */
961
  protected final void awaitAndSuppressFailure(Throwable error) {
962
    var listenerFailure = awaitSynchronousFailure();
1✔
963
    if ((listenerFailure != null) && (listenerFailure != error)) {
1!
NEW
964
      error.addSuppressed(listenerFailure);
×
965
    }
966
  }
1✔
967

968
  /** Returns the exception to rethrow on an entry processor failure. */
969
  private static RuntimeException processorFailure(Throwable e) {
970
    if (e instanceof Error) {
1✔
971
      throw (Error) e;
1✔
972
    } else if (e instanceof EntryProcessorException) {
1✔
973
      return (EntryProcessorException) e;
1✔
974
    }
975
    return new EntryProcessorException(e);
1✔
976
  }
977

978
  /**
979
   * Returns the updated expirable value after performing the post-processing actions. A null
980
   * {@code expirable} means the entry was absent and READ/UPDATED (which require a live prior)
981
   * never observe one.
982
   */
983
  @Nullable Expirable<V> postProcess(
984
      @Nullable Expirable<V> expirable, EntryProcessorEntry<K, V> entry) {
985
    switch (entry.getAction()) {
1✔
986
      case NONE:
987
        return expirable;
1✔
988
      case READ: {
989
        setAccessExpireTime(requireNonNull(expirable), getAccessExpireTime(), 0L);
1✔
990
        return expirable;
1✔
991
      }
992
      case CREATED:
993
      case LOADED: {
994
        V value = requireNonNull(entry.getValue());
1✔
995
        V copy = copyOf(value);
1✔
996
        if (entry.getAction() == Action.CREATED) {
1✔
997
          publishToCacheWriter(writer::write, () -> new EntryProxy<>(entry.getKey(), value));
1✔
998
        }
999
        long expireTimeMillis = getWriteExpireTimeMillis(/* created= */ true);
1✔
1000
        if (expireTimeMillis == 0) {
1✔
1001
          // A zero creation expiry means the entry is already expired and is not added, so the
1002
          // create is neither counted nor published
1003
          dispatcher.publishExpired(this, entry.getKey(), copy);
1✔
1004
          return null;
1✔
1005
        }
1006
        if (entry.getAction() == Action.CREATED) {
1✔
1007
          statistics.recordPuts(1L);
1✔
1008
        }
1009
        dispatcher.publishCreated(this, entry.getKey(), copy);
1✔
1010
        return new Expirable<>(copy, expireTimeMillis);
1✔
1011
      }
1012
      case UPDATED: {
1013
        requireNonNull(expirable, "Expected a previous value but was null");
1✔
1014
        V value = requireNonNull(entry.getValue(), "Expected a new value but was null");
1✔
1015
        V copy = copyOf(value);
1✔
1016
        publishToCacheWriter(writer::write, () -> new EntryProxy<>(entry.getKey(), value));
1✔
1017
        statistics.recordPuts(1L);
1✔
1018
        dispatcher.publishUpdated(this, entry.getKey(), expirable.get(), copy);
1✔
1019
        @Var long expireTimeMillis = getWriteExpireTimeMillis(/* created= */ false);
1✔
1020
        if (expireTimeMillis == Long.MIN_VALUE) {
1✔
1021
          expireTimeMillis = expirable.getExpireTimeMillis();
1✔
1022
        }
1023
        return new Expirable<>(copy, expireTimeMillis);
1✔
1024
      }
1025
      case DELETED:
1026
        publishToCacheWriter(writer::delete, entry::getKey);
1✔
1027
        if (expirable != null) {
1✔
1028
          statistics.recordRemovals(1L);
1✔
1029
          dispatcher.publishRemoved(this, entry.getKey(), expirable.get());
1✔
1030
        }
1031
        return null;
1✔
1032
    }
1033
    throw new IllegalStateException("Unknown state: " + entry.getAction());
1✔
1034
  }
1035

1036
  @Override
1037
  public <T> Map<K, EntryProcessorResult<T>> invokeAll(Set<? extends K> keys,
1038
      EntryProcessor<K, V, T> entryProcessor, Object... arguments) {
1039
    requireNotClosed();
1✔
1040
    requireNonNull(keys);
1✔
1041
    requireNonNull(arguments);
1✔
1042
    requireNonNull(entryProcessor);
1✔
1043
    keys.forEach(Objects::requireNonNull);
1✔
1044

1045
    var results = new HashMap<K, EntryProcessorResult<T>>(keys.size(), 1.0f);
1✔
1046
    for (K key : keys) {
1✔
1047
      try {
1048
        T result = invoke(key, entryProcessor, arguments);
1✔
1049
        if (result != null) {
1✔
1050
          results.put(key, () -> result);
1✔
1051
        }
1052
      } catch (EntryProcessorException e) {
1✔
1053
        results.put(key, () -> { throw e; });
1✔
1054
      } catch (RuntimeException e) {
1✔
1055
        results.put(key, () -> { throw new EntryProcessorException(e); });
1✔
1056
      }
1✔
1057
    }
1✔
1058
    return results;
1✔
1059
  }
1060

1061
  @Override
1062
  public String getName() {
1063
    return name;
1✔
1064
  }
1065

1066
  @Override
1067
  public CacheManager getCacheManager() {
1068
    return cacheManager;
1✔
1069
  }
1070

1071
  @Override
1072
  public boolean isClosed() {
1073
    return closed;
1✔
1074
  }
1075

1076
  @Override
1077
  public void close() {
1078
    if (isClosed()) {
1✔
1079
      return;
1✔
1080
    }
1081
    synchronized (configuration) {
1✔
1082
      if (!isClosed()) {
1✔
1083
        @Var Throwable thrown = null;
1✔
1084
        thrown = tryClose((AutoCloseable) () -> enableManagement(false), thrown);
1✔
1085
        thrown = tryClose((AutoCloseable) () -> enableStatistics(false), thrown);
1✔
1086

1087
        closed = true;
1✔
1088
        try {
1089
          cacheManager.destroyCache(name, this);
1✔
1090
        } catch (IllegalStateException ignored) { /* manager already closed */ }
1✔
1091

1092
        thrown = shutdownExecutor(thrown);
1✔
1093
        thrown = tryClose(expiry, thrown);
1✔
1094
        thrown = tryClose(writer, thrown);
1✔
1095
        thrown = tryClose(cacheLoader.orElse(null), thrown);
1✔
1096
        for (Registration<K, V> registration : dispatcher.registrations()) {
1✔
1097
          thrown = tryClose(registration.getCacheEntryListener(), thrown);
1✔
1098
        }
1✔
1099
        if (thrown != null) {
1✔
1100
          logger.log(Level.WARNING, "Failure when closing cache resources", thrown);
1✔
1101
        }
1102
      }
1103
    }
1✔
1104
    cache.invalidateAll();
1✔
1105
  }
1✔
1106

1107
  @SuppressWarnings("FutureReturnValueIgnored")
1108
  private @Nullable Throwable shutdownExecutor(@Var @Nullable Throwable thrown) {
1109
    if (executor instanceof ExecutorService) {
1✔
1110
      @SuppressWarnings("PMD.CloseResource")
1111
      var es = (ExecutorService) executor;
1✔
1112
      es.shutdown();
1✔
1113
    }
1114

1115
    try {
1116
      CompletableFuture
1✔
1117
          .allOf(inFlight.toArray(CompletableFuture[]::new))
1✔
1118
          .get(10, TimeUnit.SECONDS);
1✔
1119
    } catch (ExecutionException | TimeoutException e) {
1✔
1120
      thrown = suppress(thrown, e);
1✔
1121
    } catch (InterruptedException e) {
1✔
1122
      Thread.currentThread().interrupt();
1✔
1123
      thrown = suppress(thrown, e);
1✔
1124
    }
1✔
1125
    inFlight.clear();
1✔
1126

1127
    if (!(executor instanceof ExecutorService)) {
1✔
1128
      thrown = tryClose(executor, thrown);
1✔
1129
    }
1130
    return thrown;
1✔
1131
  }
1132

1133
  /**
1134
   * Attempts to close the resource. If an error occurs and an outermost exception is set, then adds
1135
   * the error to the suppression list.
1136
   *
1137
   * @param o the resource to close if Closeable
1138
   * @param outer the outermost error, or null if unset
1139
   * @return the outermost error, or null if unset and successful
1140
   */
1141
  private static @Nullable Throwable tryClose(@Nullable Object o, @Nullable Throwable outer) {
1142
    if (o instanceof AutoCloseable) {
1✔
1143
      try {
1144
        ((AutoCloseable) o).close();
1✔
1145
      } catch (Throwable t) {
1✔
1146
        return suppress(outer, t);
1✔
1147
      }
1✔
1148
    }
1149
    return outer;
1✔
1150
  }
1151

1152
  /** Returns the outermost error, retaining the error as suppressed if one is already set. */
1153
  private static <T extends Throwable> @Nullable T suppress(@Nullable T outer, @Nullable T t) {
1154
    if (t == null) {
1✔
1155
      return outer;
1✔
1156
    }
1157
    if (outer == null) {
1✔
1158
      return t;
1✔
1159
    }
1160
    if (outer != t) {
1✔
1161
      outer.addSuppressed(t);
1✔
1162
    }
1163
    return outer;
1✔
1164
  }
1165

1166
  @Override
1167
  public <T> T unwrap(Class<T> clazz) {
1168
    if (clazz.isInstance(cache)) {
1✔
1169
      return clazz.cast(cache);
1✔
1170
    } else if (clazz.isInstance(this)) {
1✔
1171
      return clazz.cast(this);
1✔
1172
    }
1173
    throw new IllegalArgumentException("Unwrapping to " + clazz
1✔
1174
        + " is not supported by this implementation");
1175
  }
1176

1177
  @Override
1178
  public void registerCacheEntryListener(
1179
      CacheEntryListenerConfiguration<K, V> cacheEntryListenerConfiguration) {
1180
    synchronized (configuration) {
1✔
1181
      requireNotClosed();
1✔
1182
      configuration.addCacheEntryListenerConfiguration(cacheEntryListenerConfiguration);
1✔
1183
      dispatcher.register(cacheEntryListenerConfiguration);
1✔
1184
    }
1✔
1185
  }
1✔
1186

1187
  @Override
1188
  public void deregisterCacheEntryListener(
1189
      CacheEntryListenerConfiguration<K, V> cacheEntryListenerConfiguration) {
1190
    synchronized (configuration) {
1✔
1191
      requireNotClosed();
1✔
1192
      configuration.removeCacheEntryListenerConfiguration(cacheEntryListenerConfiguration);
1✔
1193
      dispatcher.deregister(cacheEntryListenerConfiguration);
1✔
1194
    }
1✔
1195
  }
1✔
1196

1197
  @Override
1198
  public Iterator<Cache.Entry<K, V>> iterator() {
1199
    requireNotClosed();
1✔
1200
    return new EntryIterator();
1✔
1201
  }
1202

1203
  /** Enables or disables the configuration management JMX bean. */
1204
  void enableManagement(boolean enabled) {
1205
    synchronized (configuration) {
1✔
1206
      requireNotClosed();
1✔
1207
      if (enabled) {
1✔
1208
        JmxRegistration.registerMxBean(this, cacheMxBean, MBeanType.CONFIGURATION);
1✔
1209
      } else {
1210
        JmxRegistration.unregisterMxBean(this, MBeanType.CONFIGURATION);
1✔
1211
      }
1212
      configuration.setManagementEnabled(enabled);
1✔
1213
    }
1✔
1214
  }
1✔
1215

1216
  /** Enables or disables the statistics JMX bean. */
1217
  void enableStatistics(boolean enabled) {
1218
    synchronized (configuration) {
1✔
1219
      requireNotClosed();
1✔
1220
      if (enabled) {
1✔
1221
        JmxRegistration.registerMxBean(this, statistics, MBeanType.STATISTICS);
1✔
1222
      } else {
1223
        JmxRegistration.unregisterMxBean(this, MBeanType.STATISTICS);
1✔
1224
      }
1225
      statistics.enable(enabled);
1✔
1226
      configuration.setStatisticsEnabled(enabled);
1✔
1227
    }
1✔
1228
  }
1✔
1229

1230
  /** Performs the action with the cache writer if write-through is enabled. */
1231
  private <T> void publishToCacheWriter(Consumer<T> action, Supplier<T> data) {
1232
    if (!configuration.isWriteThrough()) {
1✔
1233
      return;
1✔
1234
    }
1235
    try {
1236
      action.accept(data.get());
1✔
1237
    } catch (CacheWriterException e) {
1✔
1238
      throw e;
1✔
1239
    } catch (RuntimeException e) {
1✔
1240
      throw new CacheWriterException("Exception in CacheWriter", e);
1✔
1241
    }
1✔
1242
  }
1✔
1243

1244
  /** Checks that the cache is not closed. */
1245
  protected final void requireNotClosed() {
1246
    if (isClosed()) {
1✔
1247
      throw new IllegalStateException();
1✔
1248
    }
1249
  }
1✔
1250

1251
  /**
1252
   * Returns a copy of the value if value-based caching is enabled.
1253
   *
1254
   * @param object the object to be copied
1255
   * @param <T> the type of object being copied
1256
   * @return a copy of the object if storing by value or the same instance if by reference
1257
   */
1258
  protected final <T> T copyOf(T object) {
1259
    try {
1260
      return requireNonNull(
1✔
1261
          copier.copy(requireNonNull(object), requireNonNull(cacheManager.getClassLoader())));
1✔
1262
    } catch (NullPointerException | IllegalStateException | ClassCastException | CacheException e) {
1✔
1263
      throw e;
1✔
1264
    } catch (RuntimeException e) {
1✔
1265
      throw new CacheException(e);
1✔
1266
    }
1267
  }
1268

1269
  /**
1270
   * Returns a deep copy of the map if value-based caching is enabled.
1271
   *
1272
   * @param map the mapping of keys to expirable values
1273
   * @return a deep or shallow copy of the mappings depending on the store by value setting
1274
   */
1275
  @SuppressWarnings("CollectorMutability")
1276
  protected final Map<K, V> copyMap(Map<K, Expirable<V>> map) {
1277
    return map.entrySet().stream().collect(toMap(
1✔
1278
        entry -> copyOf(entry.getKey()),
1✔
1279
        entry -> copyOf(entry.getValue().get())));
1✔
1280
  }
1281

1282
  /** Returns the current time in milliseconds. */
1283
  protected final long currentTimeMillis() {
1284
    return nanosToMillis(ticker.read());
1✔
1285
  }
1286

1287
  /** Returns the nanosecond time in milliseconds. */
1288
  protected static long nanosToMillis(long nanos) {
1289
    return TimeUnit.NANOSECONDS.toMillis(nanos);
1✔
1290
  }
1291

1292
  /** Returns the duration to expire an accessed entry after, or {@code null} if unchanged. */
1293
  protected final @Nullable Duration getAccessExpireTime() {
1294
    try {
1295
      return expiry.getExpiryForAccess();
1✔
1296
    } catch (RuntimeException e) {
1✔
1297
      logger.log(Level.WARNING, "Failed to get the policy's expiration time", e);
1✔
1298
      return null;
1✔
1299
    }
1300
  }
1301

1302
  /**
1303
   * Sets the JCache access expiration time.
1304
   *
1305
   * @param expirable the entry that was operated on
1306
   * @param duration the access duration, or null if unchanged
1307
   * @param currentTimeMillis the current time, or zero if not read yet
1308
   */
1309
  protected final void setAccessExpireTime(Expirable<?> expirable,
1310
      @Nullable Duration duration, @Var long currentTimeMillis) {
1311
    if (duration == null) {
1✔
1312
      return;
1✔
1313
    } else if (duration.isZero()) {
1✔
1314
      expirable.setExpireTimeMillis(0L);
1✔
1315
    } else if (duration.isEternal()) {
1✔
1316
      expirable.setExpireTimeMillis(Long.MAX_VALUE);
1✔
1317
    } else {
1318
      if (currentTimeMillis == 0L) {
1✔
1319
        currentTimeMillis = currentTimeMillis();
1✔
1320
      }
1321
      @Var long expireTimeMillis = duration.getAdjustedTime(currentTimeMillis);
1✔
1322
      expireTimeMillis = ((expireTimeMillis == 0L) || (expireTimeMillis == Long.MAX_VALUE))
1✔
1323
          ? (expireTimeMillis - 1)
1✔
1324
          : expireTimeMillis;
1✔
1325
      expirable.setExpireTimeMillis(expireTimeMillis);
1✔
1326
    }
1327
  }
1✔
1328

1329
  /**
1330
   * Sets the native access expiration time.
1331
   *
1332
   * @param key the entry's key
1333
   * @param duration the access duration, or null if unchanged
1334
   */
1335
  protected final void setVariableExpiration(K key, @Nullable Duration duration) {
1336
    if (duration == null) {
1✔
1337
      return;
1✔
1338
    }
1339
    cache.policy().expireVariably().ifPresent(policy -> {
1✔
1340
      if (duration.isZero()) {
1✔
1341
        policy.setExpiresAfter(key, 0L, TimeUnit.NANOSECONDS);
1✔
1342
      } else if (duration.isEternal()) {
1✔
1343
        policy.setExpiresAfter(key, Long.MAX_VALUE, TimeUnit.NANOSECONDS);
1✔
1344
      } else {
1345
        policy.setExpiresAfter(key, duration.getDurationAmount(), duration.getTimeUnit());
1✔
1346
      }
1347
    });
1✔
1348
  }
1✔
1349

1350
  /**
1351
   * Returns the time when the entry will expire.
1352
   *
1353
   * @param created if the write operation is an insert or an update
1354
   * @return the time when the entry will expire, zero if it should expire immediately,
1355
   *         Long.MIN_VALUE if it should not be changed, or Long.MAX_VALUE if eternal
1356
   */
1357
  protected final long getWriteExpireTimeMillis(boolean created) {
1358
    try {
1359
      Duration duration = created ? expiry.getExpiryForCreation() : expiry.getExpiryForUpdate();
1✔
1360
      if (duration == null) {
1✔
1361
        return created ? Long.MAX_VALUE : Long.MIN_VALUE;
1✔
1362
      } else if (duration.isZero()) {
1✔
1363
        return 0L;
1✔
1364
      } else if (duration.isEternal()) {
1✔
1365
        return Long.MAX_VALUE;
1✔
1366
      }
1367
      long expireTimeMillis = duration.getAdjustedTime(currentTimeMillis());
1✔
1368
      return ((expireTimeMillis == 0L) || (expireTimeMillis == Long.MAX_VALUE))
1✔
1369
          ? (expireTimeMillis - 1)
1✔
1370
          : expireTimeMillis;
1✔
1371
    } catch (RuntimeException e) {
1✔
1372
      logger.log(Level.WARNING, "Failed to get the policy's expiration time", e);
1✔
1373
      return created ? Long.MAX_VALUE : Long.MIN_VALUE;
1✔
1374
    }
1375
  }
1376

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

1383
    Map.@Nullable Entry<K, Expirable<V>> current;
1384
    Map.@Nullable Entry<K, Expirable<V>> cursor;
1385

1386
    @Override
1387
    public boolean hasNext() {
1388
      while ((cursor == null) && delegate.hasNext()) {
1✔
1389
        Map.Entry<K, Expirable<V>> entry = delegate.next();
1✔
1390
        long millis = entry.getValue().isEternal() ? 0L : currentTimeMillis();
1✔
1391
        if (!entry.getValue().hasExpired(millis)) {
1✔
1392
          var duration = getAccessExpireTime();
1✔
1393
          setVariableExpiration(entry.getKey(), duration);
1✔
1394
          setAccessExpireTime(entry.getValue(), duration, millis);
1✔
1395
          cursor = entry;
1✔
1396
        }
1397
      }
1✔
1398
      return (cursor != null);
1✔
1399
    }
1400

1401
    @Override
1402
    public Cache.Entry<K, V> next() {
1403
      if (!hasNext()) {
1✔
1404
        throw new NoSuchElementException();
1✔
1405
      }
1406
      statistics.recordHits(1L);
1✔
1407
      current = requireNonNull(cursor);
1✔
1408
      cursor = null;
1✔
1409
      return new EntryProxy<>(copyOf(current.getKey()), copyOf(current.getValue().get()));
1✔
1410
    }
1411

1412
    @Override
1413
    @SuppressWarnings("CheckReturnValue")
1414
    public void remove() {
1415
      if (current == null) {
1✔
1416
        throw new IllegalStateException();
1✔
1417
      }
1418
      CacheProxy.this.remove(current.getKey());
1✔
1419
      current = null;
1✔
1420
    }
1✔
1421
  }
1422

1423
  protected static final class PutResult<V> {
1✔
1424
    @Nullable V oldValue;
1425
    boolean written;
1426
  }
1427

1428
  protected enum NullCompletionListener implements CompletionListener {
1✔
1429
    INSTANCE;
1✔
1430

1431
    @Override
1432
    public void onCompletion() {}
1✔
1433

1434
    @Override
1435
    public void onException(Exception e) {}
1✔
1436
  }
1437
}
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