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

ben-manes / caffeine / #5612

11 Jul 2026 03:47PM UTC coverage: 71.292% (-28.7%) from 100.0%
#5612

push

github

ben-manes
Preserve timestamps on a raced same-instance refresh

The automatic refreshIfNeeded path checked for an equal-value reload
before the ABA/ownership check and returned without preserving the
timestamps, so a reload completing after a concurrent put(k,
sameInstance) did a full update — shifting the entry's expiration
forward by the reload's in-flight duration and stomping the write.
Mirror the explicit-refresh fix: check ownership first. An owned
reload installs the value (refreshing metadata even for a same
instance), while a raced reload preserves the write's timestamps and
only notifies REPLACED when the value differs.

2957 of 4134 branches covered (71.53%)

3 of 3 new or added lines in 1 file covered. (100.0%)

2408 existing lines in 53 files now uncovered.

5980 of 8388 relevant lines covered (71.29%)

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

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

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

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

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

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

104
  private volatile boolean closed;
105

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

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

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

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

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

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

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

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

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

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

UNCOV
245
    statistics.recordHits(result.size());
×
UNCOV
246
    statistics.recordMisses(keys.size() - result.size());
×
UNCOV
247
    statistics.recordEvictions(expired[0]);
×
UNCOV
248
    return result;
×
249
  }
250

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

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

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

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

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

UNCOV
334
    var result = putNoCopyOrAwait(key, value, /* publishToWriter= */ true);
×
UNCOV
335
    dispatcher.awaitSynchronous();
×
336

UNCOV
337
    if (statsEnabled) {
×
UNCOV
338
      if (result.written) {
×
UNCOV
339
        statistics.recordPuts(1);
×
340
      }
UNCOV
341
      statistics.recordPutTime(ticker.read() - start);
×
342
    }
UNCOV
343
  }
×
344

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

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

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

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

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

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

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

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

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

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

UNCOV
471
    boolean added = putIfAbsentNoAwait(key, value, /* publishToWriter= */ true);
×
UNCOV
472
    dispatcher.awaitSynchronous();
×
473

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

486
  /**
487
   * Associates the specified value with the specified key in the cache if there is no existing
488
   * mapping.
489
   *
490
   * @param key key with which the specified value is to be associated
491
   * @param value value to be associated with the specified key
492
   * @param publishToWriter if the writer should be notified
493
   * @return if the mapping was successful
494
   */
495
  @CanIgnoreReturnValue
496
  private boolean putIfAbsentNoAwait(K key, V value, boolean publishToWriter) {
UNCOV
497
    boolean[] absent = { false };
×
UNCOV
498
    cache.asMap().compute(copyOf(key), (K k, @Var Expirable<V> expirable) -> {
×
UNCOV
499
      if ((expirable != null) && !expirable.isEternal()
×
UNCOV
500
          && expirable.hasExpired(currentTimeMillis())) {
×
UNCOV
501
        dispatcher.publishExpired(this, key, expirable.get());
×
UNCOV
502
        statistics.recordEvictions(1L);
×
UNCOV
503
        expirable = null;
×
504
      }
UNCOV
505
      if (expirable != null) {
×
UNCOV
506
        return expirable;
×
507
      }
UNCOV
508
      if (publishToWriter) {
×
UNCOV
509
        publishToCacheWriter(writer::write, () -> new EntryProxy<>(key, value));
×
510
      }
511

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

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

UNCOV
534
    publishToCacheWriter(writer::delete, () -> key);
×
UNCOV
535
    V value = removeNoCopyOrAwait(key);
×
UNCOV
536
    dispatcher.awaitSynchronous();
×
537

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

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

571
  @Override
572
  @CanIgnoreReturnValue
573
  public boolean remove(K key, V oldValue) {
UNCOV
574
    requireNotClosed();
×
UNCOV
575
    requireNonNull(key);
×
UNCOV
576
    requireNonNull(oldValue);
×
577

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

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

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

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

UNCOV
625
    publishToCacheWriter(writer::delete, () -> key);
×
UNCOV
626
    V value = removeNoCopyOrAwait(key);
×
UNCOV
627
    dispatcher.awaitSynchronous();
×
UNCOV
628
    V copy = copyOf(value);
×
629

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

644
  @Override
645
  public boolean replace(K key, V oldValue, V newValue) {
UNCOV
646
    requireNotClosed();
×
UNCOV
647
    requireNonNull(oldValue);
×
UNCOV
648
    requireNonNull(newValue);
×
649

UNCOV
650
    boolean statsEnabled = statistics.isEnabled();
×
UNCOV
651
    long start = statsEnabled ? ticker.read() : 0L;
×
652

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

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

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

UNCOV
694
    return replaced[0];
×
695
  }
696

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

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

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

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

UNCOV
729
    V oldValue = replaceNoCopyOrAwait(key, value);
×
UNCOV
730
    dispatcher.awaitSynchronous();
×
UNCOV
731
    V copy = copyOf(oldValue);
×
732

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

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

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

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

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

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

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

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

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

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

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

UNCOV
849
    Object[] result = new Object[1];
×
UNCOV
850
    BiFunction<K, Expirable<V>, Expirable<V>> remappingFunction = (k, expirable) -> {
×
851
      V value;
UNCOV
852
      @Var long millis = 0L;
×
UNCOV
853
      if ((expirable == null)
×
UNCOV
854
          || (!expirable.isEternal() && expirable.hasExpired(millis = currentTimeMillis()))) {
×
UNCOV
855
        statistics.recordMisses(1L);
×
UNCOV
856
        value = null;
×
857
      } else {
UNCOV
858
        value = copyValue(expirable);
×
UNCOV
859
        statistics.recordHits(1L);
×
860
      }
UNCOV
861
      var entry = new EntryProcessorEntry<>(key, value,
×
UNCOV
862
          configuration.isReadThrough() ? cacheLoader : Optional.empty());
×
863
      try {
UNCOV
864
        result[0] = entryProcessor.process(entry, arguments);
×
UNCOV
865
        return postProcess(expirable, entry, millis);
×
UNCOV
866
      } catch (EntryProcessorException e) {
×
UNCOV
867
        throw e;
×
UNCOV
868
      } catch (RuntimeException e) {
×
UNCOV
869
        throw new EntryProcessorException(e);
×
870
      }
871
    };
872
    try {
UNCOV
873
      cache.asMap().compute(copyOf(key), remappingFunction);
×
UNCOV
874
      dispatcher.awaitSynchronous();
×
UNCOV
875
    } catch (Throwable t) {
×
UNCOV
876
      dispatcher.ignoreSynchronous();
×
UNCOV
877
      throw t;
×
UNCOV
878
    }
×
879

880
    @SuppressWarnings("unchecked")
UNCOV
881
    var castedResult = (T) result[0];
×
UNCOV
882
    return castedResult;
×
883
  }
884

885
  /** Returns the updated expirable value after performing the post-processing actions. */
886
  @SuppressWarnings("fallthrough")
887
  @Nullable Expirable<V> postProcess(@Var @Nullable Expirable<V> expirable,
888
      EntryProcessorEntry<K, V> entry, @Var long currentTimeMillis) {
UNCOV
889
    if ((expirable != null) && !expirable.isEternal()) {
×
UNCOV
890
      if (currentTimeMillis == 0) {
×
UNCOV
891
        currentTimeMillis = currentTimeMillis();
×
892
      }
UNCOV
893
      if (expirable.hasExpired(currentTimeMillis)) {
×
UNCOV
894
        dispatcher.publishExpired(this, entry.getKey(), expirable.get());
×
UNCOV
895
        statistics.recordEvictions(1L);
×
UNCOV
896
        expirable = null;
×
897
      }
898
    }
UNCOV
899
    switch (entry.getAction()) {
×
900
      case NONE:
UNCOV
901
        return expirable;
×
902
      case READ: {
UNCOV
903
        setAccessExpireTime(entry.getKey(), requireNonNull(expirable), 0L);
×
UNCOV
904
        return expirable;
×
905
      }
906
      case CREATED:
UNCOV
907
        this.publishToCacheWriter(writer::write, () -> entry);
×
908
        // fallthrough
909
      case LOADED: {
UNCOV
910
        V value = copyOf(requireNonNull(entry.getValue()));
×
UNCOV
911
        long expireTimeMillis = getWriteExpireTimeMillis(/* created= */ true);
×
UNCOV
912
        if (expireTimeMillis == 0) {
×
913
          // A zero creation expiry means the entry is already expired and is not added, so the
914
          // create is neither counted nor published
UNCOV
915
          dispatcher.publishExpired(this, entry.getKey(), value);
×
UNCOV
916
          return null;
×
917
        }
UNCOV
918
        statistics.recordPuts(1L);
×
UNCOV
919
        dispatcher.publishCreated(this, entry.getKey(), value);
×
UNCOV
920
        return new Expirable<>(value, expireTimeMillis);
×
921
      }
922
      case UPDATED: {
UNCOV
923
        publishToCacheWriter(writer::write, () -> entry);
×
UNCOV
924
        statistics.recordPuts(1L);
×
UNCOV
925
        requireNonNull(expirable, "Expected a previous value but was null");
×
UNCOV
926
        V value = copyOf(requireNonNull(entry.getValue(), "Expected a new value but was null"));
×
UNCOV
927
        dispatcher.publishUpdated(this, entry.getKey(), expirable.get(), value);
×
UNCOV
928
        @Var long expireTimeMillis = getWriteExpireTimeMillis(/* created= */ false);
×
UNCOV
929
        if (expireTimeMillis == Long.MIN_VALUE) {
×
UNCOV
930
          expireTimeMillis = expirable.getExpireTimeMillis();
×
931
        }
UNCOV
932
        return new Expirable<>(value, expireTimeMillis);
×
933
      }
934
      case DELETED:
UNCOV
935
        publishToCacheWriter(writer::delete, entry::getKey);
×
UNCOV
936
        if (expirable != null) {
×
UNCOV
937
          statistics.recordRemovals(1L);
×
UNCOV
938
          dispatcher.publishRemoved(this, entry.getKey(), expirable.get());
×
939
        }
UNCOV
940
        return null;
×
941
    }
UNCOV
942
    throw new IllegalStateException("Unknown state: " + entry.getAction());
×
943
  }
944

945
  @Override
946
  public <T> Map<K, EntryProcessorResult<T>> invokeAll(Set<? extends K> keys,
947
      EntryProcessor<K, V, T> entryProcessor, Object... arguments) {
UNCOV
948
    requireNotClosed();
×
UNCOV
949
    requireNonNull(keys);
×
UNCOV
950
    requireNonNull(arguments);
×
UNCOV
951
    requireNonNull(entryProcessor);
×
UNCOV
952
    keys.forEach(Objects::requireNonNull);
×
953

UNCOV
954
    var results = new HashMap<K, EntryProcessorResult<T>>(keys.size(), 1.0f);
×
UNCOV
955
    for (K key : keys) {
×
956
      try {
UNCOV
957
        T result = invoke(key, entryProcessor, arguments);
×
UNCOV
958
        if (result != null) {
×
UNCOV
959
          results.put(key, () -> result);
×
960
        }
UNCOV
961
      } catch (EntryProcessorException e) {
×
UNCOV
962
        results.put(key, () -> { throw e; });
×
UNCOV
963
      }
×
UNCOV
964
    }
×
UNCOV
965
    return results;
×
966
  }
967

968
  @Override
969
  public String getName() {
UNCOV
970
    return name;
×
971
  }
972

973
  @Override
974
  public CacheManager getCacheManager() {
UNCOV
975
    return cacheManager;
×
976
  }
977

978
  @Override
979
  public boolean isClosed() {
UNCOV
980
    return closed;
×
981
  }
982

983
  @Override
984
  public void close() {
UNCOV
985
    if (isClosed()) {
×
UNCOV
986
      return;
×
987
    }
UNCOV
988
    synchronized (configuration) {
×
UNCOV
989
      if (!isClosed()) {
×
UNCOV
990
        closed = true;
×
991
        try {
UNCOV
992
          cacheManager.destroyCache(name, this);
×
UNCOV
993
        } catch (IllegalStateException ignored) { /* manager already closed */ }
×
994

UNCOV
995
        @Var var thrown = shutdownExecutor();
×
UNCOV
996
        thrown = tryClose(expiry, thrown);
×
UNCOV
997
        thrown = tryClose(writer, thrown);
×
UNCOV
998
        thrown = tryClose(cacheLoader.orElse(null), thrown);
×
UNCOV
999
        for (Registration<K, V> registration : dispatcher.registrations()) {
×
UNCOV
1000
          thrown = tryClose(registration.getCacheEntryListener(), thrown);
×
UNCOV
1001
        }
×
UNCOV
1002
        thrown = tryClose((AutoCloseable) () ->
×
UNCOV
1003
            JmxRegistration.unregisterMxBean(this, MBeanType.STATISTICS), thrown);
×
UNCOV
1004
        thrown = tryClose((AutoCloseable) () ->
×
UNCOV
1005
            JmxRegistration.unregisterMxBean(this, MBeanType.CONFIGURATION), thrown);
×
UNCOV
1006
        if (thrown != null) {
×
UNCOV
1007
          logger.log(Level.WARNING, "Failure when closing cache resources", thrown);
×
1008
        }
1009
      }
UNCOV
1010
    }
×
UNCOV
1011
    cache.invalidateAll();
×
UNCOV
1012
  }
×
1013

1014
  @SuppressWarnings("FutureReturnValueIgnored")
1015
  private @Nullable Throwable shutdownExecutor() {
UNCOV
1016
    if (executor instanceof ExecutorService) {
×
1017
      @SuppressWarnings("PMD.CloseResource")
UNCOV
1018
      var es = (ExecutorService) executor;
×
UNCOV
1019
      es.shutdown();
×
1020
    }
1021

UNCOV
1022
    @Var Throwable thrown = null;
×
1023
    try {
UNCOV
1024
      CompletableFuture
×
UNCOV
1025
          .allOf(inFlight.toArray(CompletableFuture[]::new))
×
UNCOV
1026
          .get(10, TimeUnit.SECONDS);
×
UNCOV
1027
    } catch (ExecutionException | TimeoutException e) {
×
UNCOV
1028
      thrown = e;
×
UNCOV
1029
    } catch (InterruptedException e) {
×
UNCOV
1030
      Thread.currentThread().interrupt();
×
UNCOV
1031
      thrown = e;
×
UNCOV
1032
    }
×
UNCOV
1033
    inFlight.clear();
×
1034

UNCOV
1035
    if (!(executor instanceof ExecutorService)) {
×
UNCOV
1036
      thrown = tryClose(executor, thrown);
×
1037
    }
UNCOV
1038
    return thrown;
×
1039
  }
1040

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

1064
  @Override
1065
  public <T> T unwrap(Class<T> clazz) {
UNCOV
1066
    if (clazz.isInstance(cache)) {
×
UNCOV
1067
      return clazz.cast(cache);
×
UNCOV
1068
    } else if (clazz.isInstance(this)) {
×
UNCOV
1069
      return clazz.cast(this);
×
1070
    }
UNCOV
1071
    throw new IllegalArgumentException("Unwrapping to " + clazz
×
1072
        + " is not supported by this implementation");
1073
  }
1074

1075
  @Override
1076
  public void registerCacheEntryListener(
1077
      CacheEntryListenerConfiguration<K, V> cacheEntryListenerConfiguration) {
UNCOV
1078
    synchronized (configuration) {
×
UNCOV
1079
      requireNotClosed();
×
UNCOV
1080
      configuration.addCacheEntryListenerConfiguration(cacheEntryListenerConfiguration);
×
UNCOV
1081
      dispatcher.register(cacheEntryListenerConfiguration);
×
UNCOV
1082
    }
×
UNCOV
1083
  }
×
1084

1085
  @Override
1086
  public void deregisterCacheEntryListener(
1087
      CacheEntryListenerConfiguration<K, V> cacheEntryListenerConfiguration) {
UNCOV
1088
    synchronized (configuration) {
×
UNCOV
1089
      requireNotClosed();
×
UNCOV
1090
      configuration.removeCacheEntryListenerConfiguration(cacheEntryListenerConfiguration);
×
UNCOV
1091
      dispatcher.deregister(cacheEntryListenerConfiguration);
×
UNCOV
1092
    }
×
UNCOV
1093
  }
×
1094

1095
  @Override
1096
  public Iterator<Cache.Entry<K, V>> iterator() {
UNCOV
1097
    requireNotClosed();
×
UNCOV
1098
    return new EntryIterator();
×
1099
  }
1100

1101
  /** Enables or disables the configuration management JMX bean. */
1102
  void enableManagement(boolean enabled) {
UNCOV
1103
    synchronized (configuration) {
×
UNCOV
1104
      requireNotClosed();
×
UNCOV
1105
      if (enabled) {
×
UNCOV
1106
        JmxRegistration.registerMxBean(this, cacheMxBean, MBeanType.CONFIGURATION);
×
1107
      } else {
UNCOV
1108
        JmxRegistration.unregisterMxBean(this, MBeanType.CONFIGURATION);
×
1109
      }
UNCOV
1110
      configuration.setManagementEnabled(enabled);
×
UNCOV
1111
    }
×
UNCOV
1112
  }
×
1113

1114
  /** Enables or disables the statistics JMX bean. */
1115
  void enableStatistics(boolean enabled) {
UNCOV
1116
    synchronized (configuration) {
×
UNCOV
1117
      requireNotClosed();
×
UNCOV
1118
      if (enabled) {
×
UNCOV
1119
        JmxRegistration.registerMxBean(this, statistics, MBeanType.STATISTICS);
×
1120
      } else {
UNCOV
1121
        JmxRegistration.unregisterMxBean(this, MBeanType.STATISTICS);
×
1122
      }
UNCOV
1123
      statistics.enable(enabled);
×
UNCOV
1124
      configuration.setStatisticsEnabled(enabled);
×
UNCOV
1125
    }
×
UNCOV
1126
  }
×
1127

1128
  /** Performs the action with the cache writer if write-through is enabled. */
1129
  private <T> void publishToCacheWriter(Consumer<T> action, Supplier<T> data) {
UNCOV
1130
    if (!configuration.isWriteThrough()) {
×
UNCOV
1131
      return;
×
1132
    }
1133
    try {
UNCOV
1134
      action.accept(data.get());
×
UNCOV
1135
    } catch (CacheWriterException e) {
×
UNCOV
1136
      throw e;
×
UNCOV
1137
    } catch (RuntimeException e) {
×
UNCOV
1138
      throw new CacheWriterException("Exception in CacheWriter", e);
×
UNCOV
1139
    }
×
UNCOV
1140
  }
×
1141

1142
  /** Checks that the cache is not closed. */
1143
  protected final void requireNotClosed() {
UNCOV
1144
    if (isClosed()) {
×
UNCOV
1145
      throw new IllegalStateException();
×
1146
    }
UNCOV
1147
  }
×
1148

1149
  /**
1150
   * Returns a copy of the value if value-based caching is enabled.
1151
   *
1152
   * @param object the object to be copied
1153
   * @param <T> the type of object being copied
1154
   * @return a copy of the object if storing by value or the same instance if by reference
1155
   */
1156
  @SuppressWarnings({"DataFlowIssue", "NullAway"})
1157
  protected final <T> T copyOf(@Nullable T object) {
UNCOV
1158
    if (object == null) {
×
UNCOV
1159
      return null;
×
1160
    }
UNCOV
1161
    T copy = copier.copy(object, requireNonNull(cacheManager.getClassLoader()));
×
UNCOV
1162
    return requireNonNull(copy);
×
1163
  }
1164

1165
  /**
1166
   * Returns a copy of the value if value-based caching is enabled.
1167
   *
1168
   * @param expirable the expirable value to be copied
1169
   * @return a copy of the value if storing by value or the same instance if by reference
1170
   */
1171
  @SuppressWarnings({"DataFlowIssue", "NullAway"})
1172
  protected final V copyValue(@Nullable Expirable<V> expirable) {
UNCOV
1173
    if (expirable == null) {
×
UNCOV
1174
      return null;
×
1175
    }
UNCOV
1176
    V copy = copier.copy(expirable.get(), requireNonNull(cacheManager.getClassLoader()));
×
UNCOV
1177
    return requireNonNull(copy);
×
1178
  }
1179

1180
  /**
1181
   * Returns a deep copy of the map if value-based caching is enabled.
1182
   *
1183
   * @param map the mapping of keys to expirable values
1184
   * @return a deep or shallow copy of the mappings depending on the store by value setting
1185
   */
1186
  @SuppressWarnings("CollectorMutability")
1187
  protected final Map<K, V> copyMap(Map<K, Expirable<V>> map) {
UNCOV
1188
    var classLoader = requireNonNull(cacheManager.getClassLoader());
×
UNCOV
1189
    return map.entrySet().stream().collect(toMap(
×
UNCOV
1190
        entry -> copier.copy(entry.getKey(), classLoader),
×
UNCOV
1191
        entry -> copier.copy(entry.getValue().get(), classLoader)));
×
1192
  }
1193

1194
  /** Returns the current time in milliseconds. */
1195
  protected final long currentTimeMillis() {
UNCOV
1196
    return nanosToMillis(ticker.read());
×
1197
  }
1198

1199
  /** Returns the nanosecond time in milliseconds. */
1200
  protected static long nanosToMillis(long nanos) {
UNCOV
1201
    return TimeUnit.NANOSECONDS.toMillis(nanos);
×
1202
  }
1203

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

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

1269
  /** An iterator to safely expose the cache entries. */
UNCOV
1270
  final class EntryIterator implements Iterator<Cache.Entry<K, V>> {
×
1271
    // NullAway does not yet understand the @NonNull annotation in the return type of asMap.
UNCOV
1272
    @SuppressWarnings("NullAway")
×
UNCOV
1273
    final Iterator<Map.Entry<K, Expirable<V>>> delegate = cache.asMap().entrySet().iterator();
×
1274

1275
    Map.@Nullable Entry<K, Expirable<V>> current;
1276
    Map.@Nullable Entry<K, Expirable<V>> cursor;
1277

1278
    @Override
1279
    public boolean hasNext() {
UNCOV
1280
      while ((cursor == null) && delegate.hasNext()) {
×
UNCOV
1281
        Map.Entry<K, Expirable<V>> entry = delegate.next();
×
UNCOV
1282
        long millis = entry.getValue().isEternal() ? 0L : currentTimeMillis();
×
UNCOV
1283
        if (!entry.getValue().hasExpired(millis)) {
×
UNCOV
1284
          setAccessExpireTime(entry.getKey(), entry.getValue(), millis);
×
UNCOV
1285
          cursor = entry;
×
1286
        }
UNCOV
1287
      }
×
UNCOV
1288
      return (cursor != null);
×
1289
    }
1290

1291
    @Override
1292
    public Cache.Entry<K, V> next() {
UNCOV
1293
      if (!hasNext()) {
×
UNCOV
1294
        throw new NoSuchElementException();
×
1295
      }
UNCOV
1296
      statistics.recordHits(1L);
×
UNCOV
1297
      current = requireNonNull(cursor);
×
UNCOV
1298
      cursor = null;
×
UNCOV
1299
      return new EntryProxy<>(copyOf(current.getKey()), copyValue(current.getValue()));
×
1300
    }
1301

1302
    @Override
1303
    public void remove() {
UNCOV
1304
      if (current == null) {
×
UNCOV
1305
        throw new IllegalStateException();
×
1306
      }
UNCOV
1307
      boolean[] removed = { false };
×
UNCOV
1308
      boolean statsEnabled = statistics.isEnabled();
×
UNCOV
1309
      long start = statsEnabled ? ticker.read() : 0L;
×
1310

UNCOV
1311
      K key = current.getKey();
×
UNCOV
1312
      V oldValue = current.getValue().get();
×
UNCOV
1313
      cache.asMap().computeIfPresent(key, (k, expirable) -> {
×
UNCOV
1314
        if (oldValue.equals(expirable.get())) {
×
UNCOV
1315
          publishToCacheWriter(writer::delete, () -> key);
×
UNCOV
1316
          dispatcher.publishRemoved(CacheProxy.this, key, expirable.get());
×
UNCOV
1317
          removed[0] = true;
×
UNCOV
1318
          return null;
×
1319
        }
UNCOV
1320
        return expirable;
×
1321
      });
UNCOV
1322
      dispatcher.awaitSynchronous();
×
UNCOV
1323
      if (removed[0]) {
×
UNCOV
1324
        statistics.recordRemovals(1L);
×
UNCOV
1325
        if (statsEnabled) {
×
UNCOV
1326
          statistics.recordRemoveTime(ticker.read() - start);
×
1327
        }
1328
      }
UNCOV
1329
      current = null;
×
UNCOV
1330
    }
×
1331
  }
1332

UNCOV
1333
  protected static final class PutResult<V> {
×
1334
    @Nullable V oldValue;
1335
    boolean written;
1336
  }
1337

UNCOV
1338
  protected enum NullCompletionListener implements CompletionListener {
×
UNCOV
1339
    INSTANCE;
×
1340

1341
    @Override
UNCOV
1342
    public void onCompletion() {}
×
1343

1344
    @Override
UNCOV
1345
    public void onException(Exception e) {}
×
1346
  }
1347
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc