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

ben-manes / caffeine / #5707

02 Aug 2026 12:45AM UTC coverage: 0.106% (-99.9%) from 100.0%
#5707

push

github

ben-manes
fix wikibench trace reader after overly strict audit fixes

0 of 4235 branches covered (0.0%)

9 of 8528 relevant lines covered (0.11%)

0.0 hits per line

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

0.0
/jcache/src/main/java/com/github/benmanes/caffeine/jcache/CacheProxy.java
1
/*
2
 * Copyright 2015 Ben Manes. All Rights Reserved.
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *     http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16
package com.github.benmanes.caffeine.jcache;
17

18
import static java.util.Objects.requireNonNull;
19
import static java.util.Objects.requireNonNullElse;
20
import static java.util.stream.Collectors.toMap;
21
import static java.util.stream.Collectors.toSet;
22
import static java.util.stream.Collectors.toUnmodifiableList;
23

24
import java.lang.System.Logger;
25
import java.lang.System.Logger.Level;
26
import java.util.ArrayList;
27
import java.util.HashMap;
28
import java.util.Iterator;
29
import java.util.LinkedHashSet;
30
import java.util.List;
31
import java.util.Map;
32
import java.util.NoSuchElementException;
33
import java.util.Objects;
34
import java.util.Optional;
35
import java.util.Set;
36
import java.util.concurrent.CompletableFuture;
37
import java.util.concurrent.ConcurrentHashMap;
38
import java.util.concurrent.ExecutionException;
39
import java.util.concurrent.Executor;
40
import java.util.concurrent.ExecutorService;
41
import java.util.concurrent.TimeUnit;
42
import java.util.concurrent.TimeoutException;
43
import java.util.function.BiFunction;
44
import java.util.function.Consumer;
45
import java.util.function.Supplier;
46

47
import javax.cache.Cache;
48
import javax.cache.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());
×
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) {
×
115
    this.writer = requireNonNullElse(configuration.getCacheWriter(), DisabledCacheWriter.get());
×
116
    this.configuration = requireNonNull(configuration);
×
117
    this.cacheManager = requireNonNull(cacheManager);
×
118
    this.cacheLoader = requireNonNull(cacheLoader);
×
119
    this.inFlight = ConcurrentHashMap.newKeySet();
×
120
    this.dispatcher = requireNonNull(dispatcher);
×
121
    this.statistics = requireNonNull(statistics);
×
122
    this.cacheMxBean = new JCacheMXBean(this);
×
123
    this.executor = requireNonNull(executor);
×
124
    this.expiry = requireNonNull(expiry);
×
125
    this.ticker = requireNonNull(ticker);
×
126
    this.cache = requireNonNull(cache);
×
127
    this.copier = requireNonNull(copier);
×
128
    this.name = requireNonNull(name);
×
129
  }
×
130

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

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

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

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

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

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

207
    boolean statsEnabled = statistics.isEnabled();
×
208
    long now = statsEnabled ? ticker.read() : 0L;
×
209
    try {
210
      Map<K, Expirable<V>> result = getAndFilterExpiredEntries(keys);
×
211
      if (statsEnabled) {
×
212
        statistics.recordGetTime(ticker.read() - now);
×
213
      }
214
      return copyMap(result);
×
215
    } finally {
216
      dispatcher.awaitSynchronous();
×
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 };
×
226
    long[] millis = { 0L };
×
227
    var result = new HashMap<K, @NonNull Expirable<V>>(cache.getAllPresent(keys));
×
228
    result.entrySet().removeIf(entry -> {
×
229
      if (!entry.getValue().isEternal() && (millis[0] == 0L)) {
×
230
        millis[0] = currentTimeMillis();
×
231
      }
232
      if (entry.getValue().hasExpired(millis[0])) {
×
233
        cache.asMap().computeIfPresent(entry.getKey(), (k, expirable) -> {
×
234
          if (expirable == entry.getValue()) {
×
235
            dispatcher.publishExpired(this, entry.getKey(), entry.getValue().get());
×
236
            expired[0]++;
×
237
            return null;
×
238
          }
239
          return expirable;
×
240
        });
241
        return true;
×
242
      }
243
      var duration = getAccessExpireTime();
×
244
      setVariableExpiration(entry.getKey(), duration);
×
245
      setAccessExpireTime(entry.getValue(), duration, millis[0]);
×
246
      return false;
×
247
    });
248

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

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

269
    var future = new CompletableFuture<@Nullable Void>();
×
270
    synchronized (configuration) {
×
271
      requireNotClosed();
×
272
      inFlight.add(future);
×
273
    }
×
274
    try {
275
      CompletableFuture.<CompletableFuture<@Nullable Void>>supplyAsync(() -> {
×
276
        @Var boolean success = false;
×
277
        try {
278
          if (replaceExistingValues) {
×
279
            loadAllAndReplaceExisting(keys);
×
280
          } else {
281
            loadAllAndKeepExisting(keys);
×
282
          }
283
          success = true;
×
284
        } catch (CacheLoaderException e) {
×
285
          listener.onException(e);
×
286
        } catch (RuntimeException e) {
×
287
          listener.onException(new CacheLoaderException(e));
×
288
        } finally {
289
          if (!success) {
×
290
            dispatcher.ignoreSynchronous();
×
291
          }
292
        }
293
        if (!success) {
×
294
          return CompletableFuture.completedFuture(null);
×
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) -> {
×
298
          if (error != null) {
×
299
            listener.onException(new CacheLoaderException(error));
×
300
          } else if (failure != null) {
×
301
            listener.onException(failure);
×
302
          } else {
303
            listener.onCompletion();
×
304
          }
305
          return null;
×
306
        });
307
      }, executor).thenCompose(chain -> chain).whenComplete((r, e) -> {
×
308
        inFlight.remove(future);
×
309
        future.complete(null);
×
310
      });
×
311
    } catch (RuntimeException e) {
×
312
      inFlight.remove(future);
×
313
      future.complete(null);
×
314
      listener.onException(new CacheLoaderException(e));
×
315
    }
×
316
  }
×
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);
×
321
    for (var entry : loaded.entrySet()) {
×
322
      if ((entry.getKey() != null) && (entry.getValue() != null)) {
×
323
        putNoCopyOrAwait(entry.getKey(), entry.getValue(), /* publishToWriter= */ false);
×
324
      }
325
    }
×
326
  }
×
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()
×
332
        .filter(key -> {
×
333
          var expirable = cache.policy().getIfPresentQuietly(key);
×
334
          return (expirable == null)
×
335
              || (!expirable.isEternal() && expirable.hasExpired(currentTimeMillis()));
×
336
        }).collect(toUnmodifiableList());
×
337
    Map<K, V> result = cacheLoader.orElseThrow().loadAll(keysToLoad);
×
338
    for (var entry : result.entrySet()) {
×
339
      if ((entry.getKey() != null) && (entry.getValue() != null)) {
×
340
        putIfAbsentNoAwait(entry.getKey(), entry.getValue(), /* publishToWriter= */ false);
×
341
      }
342
    }
×
343
  }
×
344

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

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

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

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

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

369
    if (statsEnabled) {
×
370
      if (result.oldValue == null) {
×
371
        statistics.recordMisses(1L);
×
372
      } else {
373
        statistics.recordHits(1L);
×
374
      }
375
      long duration = ticker.read() - start;
×
376
      if (result.written) {
×
377
        statistics.recordPuts(1);
×
378
        statistics.recordPutTime(duration);
×
379
      }
380
      statistics.recordGetTime(duration);
×
381
    }
382
    return (result.oldValue == null) ? null : copyOf(result.oldValue);
×
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);
×
396
    requireNonNull(value);
×
397

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

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

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

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

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

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

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

495
    if (statsEnabled) {
×
496
      if (result.oldValue != null) {
×
497
        statistics.recordHits(1L);
×
498
      } else {
499
        statistics.recordMisses(1L);
×
500
      }
501
      if (result.written) {
×
502
        statistics.recordPuts(1L);
×
503
        statistics.recordPutTime(ticker.read() - start);
×
504
      }
505
    }
506
    return result.written;
×
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>();
×
521
    cache.asMap().compute(copyOf(key), (K k, Expirable<V> expirable) -> {
×
522
      if ((expirable != null)
×
523
          && (expirable.isEternal() || !expirable.hasExpired(currentTimeMillis()))) {
×
524
        result.oldValue = expirable.get();
×
525
        return expirable;
×
526
      }
527

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

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

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

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

561
    if (value != null) {
×
562
      statistics.recordRemovals(1L);
×
563
      if (statsEnabled) {
×
564
        statistics.recordRemoveTime(ticker.read() - start);
×
565
      }
566
      return true;
×
567
    }
568
    return false;
×
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];
×
582
    cache.asMap().compute(key, (K k, Expirable<V> expirable) -> {
×
583
      if (publishToWriter) {
×
584
        publishToCacheWriter(writer::delete, () -> key);
×
585
      }
586
      if (expirable != null) {
×
587
        if (!expirable.isEternal() && expirable.hasExpired(currentTimeMillis())) {
×
588
          dispatcher.publishExpired(this, key, expirable.get());
×
589
          statistics.recordEvictions(1L);
×
590
        } else {
591
          dispatcher.publishRemoved(this, key, expirable.get());
×
592
          removed[0] = expirable.get();
×
593
        }
594
      }
595
      return null;
×
596
    });
597
    return removed[0];
×
598
  }
599

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

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

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

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

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

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

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

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

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

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

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

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

724
    return replaced[0];
×
725
  }
726

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

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

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

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

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

762
    V copy = (oldValue == null) ? null : copyOf(oldValue);
×
763
    if (statsEnabled) {
×
764
      long duration = ticker.read() - start;
×
765
      if (copy == null) {
×
766
        statistics.recordMisses(1L);
×
767
      } else {
768
        statistics.recordHits(1L);
×
769
        statistics.recordPuts(1L);
×
770
        statistics.recordPutTime(duration);
×
771
      }
772
      statistics.recordGetTime(duration);
×
773
    }
774
    return copy;
×
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);
×
788
    @SuppressWarnings("unchecked")
789
    var replaced = (V[]) new Object[1];
×
790
    cache.asMap().computeIfPresent(key, (k, expirable) -> {
×
791
      if (!expirable.isEternal() && expirable.hasExpired(currentTimeMillis())) {
×
792
        dispatcher.publishExpired(this, key, expirable.get());
×
793
        statistics.recordEvictions(1L);
×
794
        return null;
×
795
      }
796

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

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

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

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

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

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

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

867
  @Override
868
  public <C extends Configuration<K, V>> C getConfiguration(Class<C> clazz) {
869
    if (clazz.isInstance(configuration)) {
×
870
      synchronized (configuration) {
×
871
        return clazz.cast(configuration.immutableCopy());
×
872
      }
873
    }
874
    throw new IllegalArgumentException("The configuration class " + clazz
×
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);
×
882
    requireNonNull(arguments);
×
883
    requireNotClosed();
×
884

885
    var result = new Object[1];
×
886
    var failure = new Throwable[1];
×
887
    BiFunction<K, Expirable<V>, Expirable<V>> remappingFunction = (k, expirable) -> {
×
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()
×
893
          && expirable.hasExpired(currentTimeMillis())) {
×
894
        dispatcher.publishExpired(this, key, expirable.get());
×
895
        statistics.recordEvictions(1L);
×
896
        expired = true;
×
897
        prior = null;
×
898
      } else {
899
        prior = expirable;
×
900
        expired = false;
×
901
      }
902

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

942
    @SuppressWarnings("unchecked")
943
    var castedResult = (T) result[0];
×
944
    return castedResult;
×
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();
×
954
      return null;
×
955
    } catch (CacheEntryListenerException e) {
×
956
      return e;
×
957
    }
958
  }
959

960
  /** Waits for the synchronous listeners and suppresses any failures onto the existing error. */
961
  @SuppressWarnings("CheckReturnValue")
962
  protected final void awaitAndSuppressFailure(Throwable error) {
963
    suppress(error, awaitSynchronousFailure());
×
964
  }
×
965

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

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

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

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

1059
  @Override
1060
  public String getName() {
1061
    return name;
×
1062
  }
1063

1064
  @Override
1065
  public CacheManager getCacheManager() {
1066
    return cacheManager;
×
1067
  }
1068

1069
  @Override
1070
  public boolean isClosed() {
1071
    return closed;
×
1072
  }
1073

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

1085
        closed = true;
×
1086
        try {
1087
          cacheManager.destroyCache(name, this);
×
1088
        } catch (IllegalStateException ignored) { /* manager already closed */ }
×
1089

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

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

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

1125
    if (!(executor instanceof ExecutorService)) {
×
1126
      thrown = tryClose(executor, thrown);
×
1127
    }
1128
    return thrown;
×
1129
  }
1130

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

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

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

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

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

1195
  @Override
1196
  public Iterator<Cache.Entry<K, V>> iterator() {
1197
    requireNotClosed();
×
1198
    return new EntryIterator();
×
1199
  }
1200

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

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

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

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

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

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

1280
  /** Returns the current time in milliseconds. */
1281
  protected final long currentTimeMillis() {
1282
    return nanosToMillis(ticker.read());
×
1283
  }
1284

1285
  /** Returns the nanosecond time in milliseconds. */
1286
  protected static long nanosToMillis(long nanos) {
1287
    return TimeUnit.NANOSECONDS.toMillis(nanos);
×
1288
  }
1289

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

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

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

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

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

1381
    Map.@Nullable Entry<K, Expirable<V>> current;
1382
    Map.@Nullable Entry<K, Expirable<V>> cursor;
1383

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

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

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

1421
  protected static final class PutResult<V> {
×
1422
    @Nullable V oldValue;
1423
    boolean written;
1424
  }
1425

1426
  protected enum NullCompletionListener implements CompletionListener {
×
1427
    INSTANCE;
×
1428

1429
    @Override
1430
    public void onCompletion() {}
×
1431

1432
    @Override
1433
    public void onException(Exception e) {}
×
1434
  }
1435
}
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