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

ben-manes / caffeine / #5651

18 Jul 2026 03:26AM UTC coverage: 66.303% (-33.7%) from 100.0%
#5651

push

github

ben-manes
dependency updates

2809 of 4164 branches covered (67.46%)

5584 of 8422 relevant lines covered (66.3%)

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

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

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

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

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

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

106
  private volatile boolean closed;
107

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

348
    var result = putNoCopyOrAwait(key, value, /* publishToWriter= */ true);
×
349
    dispatcher.awaitSynchronous();
×
350

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

367
  /**
368
   * Associates the specified value with the specified key in the cache.
369
   *
370
   * @param key key with which the specified value is to be associated
371
   * @param value value to be associated with the specified key
372
   * @param publishToWriter if the writer should be notified
373
   * @return the oldValue and if the new value was stored
374
   */
375
  @CanIgnoreReturnValue
376
  protected PutResult<V> putNoCopyOrAwait(K key, V value, boolean publishToWriter) {
377
    requireNonNull(key);
×
378
    requireNonNull(value);
×
379

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

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

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

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

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

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

471
    var result = putIfAbsentNoAwait(key, value, /* publishToWriter= */ true);
×
472
    dispatcher.awaitSynchronous();
×
473

474
    if (statsEnabled) {
×
475
      if (result.oldValue != null) {
×
476
        statistics.recordHits(1L);
×
477
      } else {
478
        statistics.recordMisses(1L);
×
479
      }
480
      if (result.written) {
×
481
        statistics.recordPuts(1L);
×
482
        statistics.recordPutTime(ticker.read() - start);
×
483
      }
484
    }
485
    return result.written;
×
486
  }
487

488
  /**
489
   * Associates the specified value with the specified key in the cache if there is no existing
490
   * mapping.
491
   *
492
   * @param key key with which the specified value is to be associated
493
   * @param value value to be associated with the specified key
494
   * @param publishToWriter if the writer should be notified
495
   * @return the oldValue and if the new value was stored
496
   */
497
  @CanIgnoreReturnValue
498
  private PutResult<V> putIfAbsentNoAwait(K key, V value, boolean publishToWriter) {
499
    var result = new PutResult<V>();
×
500
    cache.asMap().compute(copyOf(key), (K k, Expirable<V> expirable) -> {
×
501
      if ((expirable != null)
×
502
          && (expirable.isEternal() || !expirable.hasExpired(currentTimeMillis()))) {
×
503
        result.oldValue = expirable.get();
×
504
        return expirable;
×
505
      }
506

507
      V copy = copyOf(value);
×
508
      if (publishToWriter) {
×
509
        publishToCacheWriter(writer::write, () -> new EntryProxy<>(key, value));
×
510
      }
511
      if (expirable != null) {
×
512
        dispatcher.publishExpired(this, key, expirable.get());
×
513
        statistics.recordEvictions(1L);
×
514
      }
515

516
      long expireTimeMillis = getWriteExpireTimeMillis(/* created= */ true);
×
517
      if (expireTimeMillis == 0) {
×
518
        // A zero creation expiry means the entry is already expired and is not added
519
        dispatcher.publishExpired(this, key, copy);
×
520
        return null;
×
521
      } else {
522
        result.written = true;
×
523
        dispatcher.publishCreated(this, key, copy);
×
524
        return new Expirable<>(copy, expireTimeMillis);
×
525
      }
526
    });
527
    return result;
×
528
  }
529

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

537
    V value = removeNoCopyOrAwait(key, /* publishToWriter= */ true);
×
538
    dispatcher.awaitSynchronous();
×
539

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

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

579
  @Override
580
  @CanIgnoreReturnValue
581
  public boolean remove(K key, V oldValue) {
582
    requireNotClosed();
×
583
    requireNonNull(key);
×
584
    requireNonNull(oldValue);
×
585

586
    boolean statsEnabled = statistics.isEnabled();
×
587
    long start = statsEnabled ? ticker.read() : 0L;
×
588
    boolean[] found = { false };
×
589

590
    boolean[] removed = { false };
×
591
    cache.asMap().computeIfPresent(key, (k, expirable) -> {
×
592
      long millis = expirable.isEternal()
×
593
          ? 0L
×
594
          : nanosToMillis((start == 0L) ? ticker.read() : start);
×
595
      if (expirable.hasExpired(millis)) {
×
596
        dispatcher.publishExpired(this, key, expirable.get());
×
597
        statistics.recordEvictions(1L);
×
598
        return null;
×
599
      }
600

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

626
  @Override
627
  public @Nullable V getAndRemove(K key) {
628
    requireNotClosed();
×
629
    requireNonNull(key);
×
630
    boolean statsEnabled = statistics.isEnabled();
×
631
    long start = statsEnabled ? ticker.read() : 0L;
×
632

633
    V value = removeNoCopyOrAwait(key, /* publishToWriter= */ true);
×
634
    dispatcher.awaitSynchronous();
×
635

636
    V copy = (value == null) ? null : copyOf(value);
×
637
    if (statsEnabled) {
×
638
      long duration = ticker.read() - start;
×
639
      if (copy == null) {
×
640
        statistics.recordMisses(1L);
×
641
      } else {
642
        statistics.recordHits(1L);
×
643
        statistics.recordRemovals(1L);
×
644
        statistics.recordRemoveTime(duration);
×
645
      }
646
      statistics.recordGetTime(duration);
×
647
    }
648
    return copy;
×
649
  }
650

651
  @Override
652
  public boolean replace(K key, V oldValue, V newValue) {
653
    requireNotClosed();
×
654
    requireNonNull(oldValue);
×
655
    requireNonNull(newValue);
×
656

657
    boolean statsEnabled = statistics.isEnabled();
×
658
    long start = statsEnabled ? ticker.read() : 0L;
×
659

660
    boolean[] found = { false };
×
661
    boolean[] replaced = { false };
×
662
    cache.asMap().computeIfPresent(key, (k, expirable) -> {
×
663
      long millis = expirable.isEternal()
×
664
          ? 0L
×
665
          : nanosToMillis((start == 0L) ? ticker.read() : start);
×
666
      if (expirable.hasExpired(millis)) {
×
667
        dispatcher.publishExpired(this, key, expirable.get());
×
668
        statistics.recordEvictions(1L);
×
669
        return null;
×
670
      }
671

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

692
    if (statsEnabled) {
×
693
      statistics.recordMisses(found[0] ? 0L : 1L);
×
694
      statistics.recordHits(found[0] ? 1L : 0L);
×
695
      long duration = ticker.read() - start;
×
696
      if (replaced[0]) {
×
697
        statistics.recordPuts(1L);
×
698
        statistics.recordPutTime(duration);
×
699
      }
700
      statistics.recordGetTime(duration);
×
701
    }
702

703
    return replaced[0];
×
704
  }
705

706
  @Override
707
  public boolean replace(K key, V value) {
708
    requireNotClosed();
×
709
    boolean statsEnabled = statistics.isEnabled();
×
710
    long start = statsEnabled ? ticker.read() : 0L;
×
711

712
    @Nullable V oldValue = replaceNoCopyOrAwait(key, value);
×
713
    dispatcher.awaitSynchronous();
×
714
    if (oldValue == null) {
×
715
      statistics.recordMisses(1L);
×
716
      if (statsEnabled) {
×
717
        statistics.recordGetTime(ticker.read() - start);
×
718
      }
719
      return false;
×
720
    }
721

722
    if (statsEnabled) {
×
723
      statistics.recordHits(1L);
×
724
      statistics.recordPuts(1L);
×
725
      long duration = ticker.read() - start;
×
726
      statistics.recordGetTime(duration);
×
727
      statistics.recordPutTime(duration);
×
728
    }
729
    return true;
×
730
  }
731

732
  @Override
733
  public @Nullable V getAndReplace(K key, V value) {
734
    requireNotClosed();
×
735
    boolean statsEnabled = statistics.isEnabled();
×
736
    long start = statsEnabled ? ticker.read() : 0L;
×
737

738
    V oldValue = replaceNoCopyOrAwait(key, value);
×
739
    dispatcher.awaitSynchronous();
×
740

741
    V copy = (oldValue == null) ? null : copyOf(oldValue);
×
742
    if (statsEnabled) {
×
743
      long duration = ticker.read() - start;
×
744
      if (copy == null) {
×
745
        statistics.recordMisses(1L);
×
746
      } else {
747
        statistics.recordHits(1L);
×
748
        statistics.recordPuts(1L);
×
749
        statistics.recordPutTime(duration);
×
750
      }
751
      statistics.recordGetTime(duration);
×
752
    }
753
    return copy;
×
754
  }
755

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

776
      publishToCacheWriter(writer::write, () -> new EntryProxy<>(key, value));
×
777
      @Var long expireTimeMillis = getWriteExpireTimeMillis(/* created= */ false);
×
778
      if (expireTimeMillis == Long.MIN_VALUE) {
×
779
        expireTimeMillis = expirable.getExpireTimeMillis();
×
780
      }
781
      dispatcher.publishUpdated(this, key, expirable.get(), copy);
×
782
      replaced[0] = expirable.get();
×
783
      return new Expirable<>(copy, expireTimeMillis);
×
784
    });
785
    return replaced[0];
×
786
  }
787

788
  @Override
789
  public void removeAll(Set<? extends K> keys) {
790
    requireNotClosed();
×
791
    var keysToRemove = new LinkedHashSet<>(keys);
×
792
    keysToRemove.forEach(Objects::requireNonNull);
×
793

794
    @Var CacheWriterException error = null;
×
795
    @Var Set<? extends K> failedKeys = Set.of();
×
796
    boolean statsEnabled = statistics.isEnabled();
×
797
    long start = statsEnabled ? ticker.read() : 0L;
×
798
    if (configuration.isWriteThrough() && !keysToRemove.isEmpty()) {
×
799
      var keysToWrite = new LinkedHashSet<>(keysToRemove);
×
800
      try {
801
        writer.deleteAll(keysToWrite);
×
802
      } catch (CacheWriterException e) {
×
803
        error = e;
×
804
        failedKeys = keysToWrite;
×
805
      } catch (RuntimeException e) {
×
806
        error = new CacheWriterException("Exception in CacheWriter", e);
×
807
        failedKeys = keysToWrite;
×
808
      }
×
809
    }
810

811
    @Var int removed = 0;
×
812
    for (var key : keysToRemove) {
×
813
      if (!failedKeys.contains(key)
×
814
          && (removeNoCopyOrAwait(key, /* publishToWriter= */ false) != null)) {
×
815
        removed++;
×
816
      }
817
    }
×
818
    dispatcher.awaitSynchronous();
×
819

820
    if (statsEnabled && (removed > 0)) {
×
821
      statistics.recordRemovals(removed);
×
822
      statistics.recordRemoveTime(ticker.read() - start);
×
823
    }
824
    if (error != null) {
×
825
      throw error;
×
826
    }
827
  }
×
828

829
  @Override
830
  public void removeAll() {
831
    removeAll(cache.asMap().keySet());
×
832
  }
×
833

834
  @Override
835
  public void clear() {
836
    requireNotClosed();
×
837
    cache.invalidateAll();
×
838
  }
×
839

840
  @Override
841
  public <C extends Configuration<K, V>> C getConfiguration(Class<C> clazz) {
842
    if (clazz.isInstance(configuration)) {
×
843
      synchronized (configuration) {
×
844
        return clazz.cast(configuration.immutableCopy());
×
845
      }
846
    }
847
    throw new IllegalArgumentException("The configuration class " + clazz
×
848
        + " is not supported by this implementation");
849
  }
850

851
  @Override
852
  public <T extends @Nullable Object> T invoke(K key,
853
      EntryProcessor<K, V, T> entryProcessor, Object... arguments) {
854
    requireNonNull(entryProcessor);
×
855
    requireNonNull(arguments);
×
856
    requireNotClosed();
×
857

858
    var result = new Object[1];
×
859
    var failure = new Throwable[1];
×
860
    BiFunction<K, Expirable<V>, Expirable<V>> remappingFunction = (k, expirable) -> {
×
861
      // Publish a lazily-expired prior's expiration before the processor observes it as absent,
862
      // so listeners see a linearizable sequence and the expiration is committed exactly once
863
      boolean expired;
864
      Expirable<V> prior;
865
      if ((expirable != null) && !expirable.isEternal()
×
866
          && expirable.hasExpired(currentTimeMillis())) {
×
867
        dispatcher.publishExpired(this, key, expirable.get());
×
868
        statistics.recordEvictions(1L);
×
869
        expired = true;
×
870
        prior = null;
×
871
      } else {
872
        prior = expirable;
×
873
        expired = false;
×
874
      }
875

876
      V value;
877
      if (prior == null) {
×
878
        statistics.recordMisses(1L);
×
879
        value = null;
×
880
      } else {
881
        value = copyOf(prior.get());
×
882
        statistics.recordHits(1L);
×
883
      }
884
      var entry = new EntryProcessorEntry<>(key, value,
×
885
          configuration.isReadThrough() ? cacheLoader : Optional.empty());
×
886
      try {
887
        result[0] = entryProcessor.process(entry, arguments);
×
888
        return postProcess(prior, entry);
×
889
      } catch (Throwable e) {
×
890
        if (!expired) {
×
891
          throw processorFailure(e);
×
892
        }
893
        failure[0] = e;
×
894
        return null;
×
895
      }
896
    };
897
    try {
898
      cache.asMap().compute(copyOf(key), remappingFunction);
×
899
    } catch (Throwable t) {
×
900
      dispatcher.ignoreSynchronous();
×
901
      throw t;
×
902
    }
×
903
    dispatcher.awaitSynchronous();
×
904
    if (failure[0] != null) {
×
905
      throw processorFailure(failure[0]);
×
906
    }
907

908
    @SuppressWarnings("unchecked")
909
    var castedResult = (T) result[0];
×
910
    return castedResult;
×
911
  }
912

913
  /** Returns the exception to rethrow on an entry processor failure. */
914
  private static RuntimeException processorFailure(Throwable e) {
915
    if (e instanceof Error) {
×
916
      throw (Error) e;
×
917
    } else if (e instanceof EntryProcessorException) {
×
918
      return (EntryProcessorException) e;
×
919
    }
920
    return new EntryProcessorException(e);
×
921
  }
922

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

979
  @Override
980
  public <T> Map<K, EntryProcessorResult<T>> invokeAll(Set<? extends K> keys,
981
      EntryProcessor<K, V, T> entryProcessor, Object... arguments) {
982
    requireNotClosed();
×
983
    requireNonNull(keys);
×
984
    requireNonNull(arguments);
×
985
    requireNonNull(entryProcessor);
×
986
    keys.forEach(Objects::requireNonNull);
×
987

988
    var results = new HashMap<K, EntryProcessorResult<T>>(keys.size(), 1.0f);
×
989
    for (K key : keys) {
×
990
      try {
991
        T result = invoke(key, entryProcessor, arguments);
×
992
        if (result != null) {
×
993
          results.put(key, () -> result);
×
994
        }
995
      } catch (EntryProcessorException e) {
×
996
        results.put(key, () -> { throw e; });
×
997
      } catch (RuntimeException e) {
×
998
        results.put(key, () -> { throw new EntryProcessorException(e); });
×
999
      }
×
1000
    }
×
1001
    return results;
×
1002
  }
1003

1004
  @Override
1005
  public String getName() {
1006
    return name;
×
1007
  }
1008

1009
  @Override
1010
  public CacheManager getCacheManager() {
1011
    return cacheManager;
×
1012
  }
1013

1014
  @Override
1015
  public boolean isClosed() {
1016
    return closed;
×
1017
  }
1018

1019
  @Override
1020
  public void close() {
1021
    if (isClosed()) {
×
1022
      return;
×
1023
    }
1024
    synchronized (configuration) {
×
1025
      if (!isClosed()) {
×
1026
        closed = true;
×
1027
        try {
1028
          cacheManager.destroyCache(name, this);
×
1029
        } catch (IllegalStateException ignored) { /* manager already closed */ }
×
1030

1031
        @Var var thrown = shutdownExecutor();
×
1032
        thrown = tryClose(expiry, thrown);
×
1033
        thrown = tryClose(writer, thrown);
×
1034
        thrown = tryClose(cacheLoader.orElse(null), thrown);
×
1035
        for (Registration<K, V> registration : dispatcher.registrations()) {
×
1036
          thrown = tryClose(registration.getCacheEntryListener(), thrown);
×
1037
        }
×
1038
        thrown = tryClose((AutoCloseable) () ->
×
1039
            JmxRegistration.unregisterMxBean(this, MBeanType.STATISTICS), thrown);
×
1040
        thrown = tryClose((AutoCloseable) () ->
×
1041
            JmxRegistration.unregisterMxBean(this, MBeanType.CONFIGURATION), thrown);
×
1042
        if (thrown != null) {
×
1043
          logger.log(Level.WARNING, "Failure when closing cache resources", thrown);
×
1044
        }
1045
      }
1046
    }
×
1047
    cache.invalidateAll();
×
1048
  }
×
1049

1050
  @SuppressWarnings("FutureReturnValueIgnored")
1051
  private @Nullable Throwable shutdownExecutor() {
1052
    if (executor instanceof ExecutorService) {
×
1053
      @SuppressWarnings("PMD.CloseResource")
1054
      var es = (ExecutorService) executor;
×
1055
      es.shutdown();
×
1056
    }
1057

1058
    @Var Throwable thrown = null;
×
1059
    try {
1060
      CompletableFuture
×
1061
          .allOf(inFlight.toArray(CompletableFuture[]::new))
×
1062
          .get(10, TimeUnit.SECONDS);
×
1063
    } catch (ExecutionException | TimeoutException e) {
×
1064
      thrown = e;
×
1065
    } catch (InterruptedException e) {
×
1066
      Thread.currentThread().interrupt();
×
1067
      thrown = e;
×
1068
    }
×
1069
    inFlight.clear();
×
1070

1071
    if (!(executor instanceof ExecutorService)) {
×
1072
      thrown = tryClose(executor, thrown);
×
1073
    }
1074
    return thrown;
×
1075
  }
1076

1077
  /**
1078
   * Attempts to close the resource. If an error occurs and an outermost exception is set, then adds
1079
   * the error to the suppression list.
1080
   *
1081
   * @param o the resource to close if Closeable
1082
   * @param outer the outermost error, or null if unset
1083
   * @return the outermost error, or null if unset and successful
1084
   */
1085
  private static @Nullable Throwable tryClose(@Nullable Object o, @Nullable Throwable outer) {
1086
    if (o instanceof AutoCloseable) {
×
1087
      try {
1088
        ((AutoCloseable) o).close();
×
1089
      } catch (Throwable t) {
×
1090
        if (outer == null) {
×
1091
          return t;
×
1092
        } else if (outer != t) {
×
1093
          outer.addSuppressed(t);
×
1094
        }
1095
      }
×
1096
    }
1097
    return outer;
×
1098
  }
1099

1100
  @Override
1101
  public <T> T unwrap(Class<T> clazz) {
1102
    if (clazz.isInstance(cache)) {
×
1103
      return clazz.cast(cache);
×
1104
    } else if (clazz.isInstance(this)) {
×
1105
      return clazz.cast(this);
×
1106
    }
1107
    throw new IllegalArgumentException("Unwrapping to " + clazz
×
1108
        + " is not supported by this implementation");
1109
  }
1110

1111
  @Override
1112
  public void registerCacheEntryListener(
1113
      CacheEntryListenerConfiguration<K, V> cacheEntryListenerConfiguration) {
1114
    synchronized (configuration) {
×
1115
      requireNotClosed();
×
1116
      configuration.addCacheEntryListenerConfiguration(cacheEntryListenerConfiguration);
×
1117
      dispatcher.register(cacheEntryListenerConfiguration);
×
1118
    }
×
1119
  }
×
1120

1121
  @Override
1122
  public void deregisterCacheEntryListener(
1123
      CacheEntryListenerConfiguration<K, V> cacheEntryListenerConfiguration) {
1124
    synchronized (configuration) {
×
1125
      requireNotClosed();
×
1126
      configuration.removeCacheEntryListenerConfiguration(cacheEntryListenerConfiguration);
×
1127
      dispatcher.deregister(cacheEntryListenerConfiguration);
×
1128
    }
×
1129
  }
×
1130

1131
  @Override
1132
  public Iterator<Cache.Entry<K, V>> iterator() {
1133
    requireNotClosed();
×
1134
    return new EntryIterator();
×
1135
  }
1136

1137
  /** Enables or disables the configuration management JMX bean. */
1138
  void enableManagement(boolean enabled) {
1139
    synchronized (configuration) {
×
1140
      requireNotClosed();
×
1141
      if (enabled) {
×
1142
        JmxRegistration.registerMxBean(this, cacheMxBean, MBeanType.CONFIGURATION);
×
1143
      } else {
1144
        JmxRegistration.unregisterMxBean(this, MBeanType.CONFIGURATION);
×
1145
      }
1146
      configuration.setManagementEnabled(enabled);
×
1147
    }
×
1148
  }
×
1149

1150
  /** Enables or disables the statistics JMX bean. */
1151
  void enableStatistics(boolean enabled) {
1152
    synchronized (configuration) {
×
1153
      requireNotClosed();
×
1154
      if (enabled) {
×
1155
        JmxRegistration.registerMxBean(this, statistics, MBeanType.STATISTICS);
×
1156
      } else {
1157
        JmxRegistration.unregisterMxBean(this, MBeanType.STATISTICS);
×
1158
      }
1159
      statistics.enable(enabled);
×
1160
      configuration.setStatisticsEnabled(enabled);
×
1161
    }
×
1162
  }
×
1163

1164
  /** Performs the action with the cache writer if write-through is enabled. */
1165
  private <T> void publishToCacheWriter(Consumer<T> action, Supplier<T> data) {
1166
    if (!configuration.isWriteThrough()) {
×
1167
      return;
×
1168
    }
1169
    try {
1170
      action.accept(data.get());
×
1171
    } catch (CacheWriterException e) {
×
1172
      throw e;
×
1173
    } catch (RuntimeException e) {
×
1174
      throw new CacheWriterException("Exception in CacheWriter", e);
×
1175
    }
×
1176
  }
×
1177

1178
  /** Checks that the cache is not closed. */
1179
  protected final void requireNotClosed() {
1180
    if (isClosed()) {
×
1181
      throw new IllegalStateException();
×
1182
    }
1183
  }
×
1184

1185
  /**
1186
   * Returns a copy of the value if value-based caching is enabled.
1187
   *
1188
   * @param object the object to be copied
1189
   * @param <T> the type of object being copied
1190
   * @return a copy of the object if storing by value or the same instance if by reference
1191
   */
1192
  protected final <T> T copyOf(T object) {
1193
    try {
1194
      return requireNonNull(
×
1195
          copier.copy(requireNonNull(object), requireNonNull(cacheManager.getClassLoader())));
×
1196
    } catch (NullPointerException | IllegalStateException | ClassCastException | CacheException e) {
×
1197
      throw e;
×
1198
    } catch (RuntimeException e) {
×
1199
      throw new CacheException(e);
×
1200
    }
1201
  }
1202

1203
  /**
1204
   * Returns a deep copy of the map if value-based caching is enabled.
1205
   *
1206
   * @param map the mapping of keys to expirable values
1207
   * @return a deep or shallow copy of the mappings depending on the store by value setting
1208
   */
1209
  @SuppressWarnings("CollectorMutability")
1210
  protected final Map<K, V> copyMap(Map<K, Expirable<V>> map) {
1211
    return map.entrySet().stream().collect(toMap(
×
1212
        entry -> copyOf(entry.getKey()),
×
1213
        entry -> copyOf(entry.getValue().get())));
×
1214
  }
1215

1216
  /** Returns the current time in milliseconds. */
1217
  protected final long currentTimeMillis() {
1218
    return nanosToMillis(ticker.read());
×
1219
  }
1220

1221
  /** Returns the nanosecond time in milliseconds. */
1222
  protected static long nanosToMillis(long nanos) {
1223
    return TimeUnit.NANOSECONDS.toMillis(nanos);
×
1224
  }
1225

1226
  /**
1227
   * Sets the access expiration time.
1228
   *
1229
   * @param key the entry's key
1230
   * @param expirable the entry that was operated on
1231
   * @param currentTimeMillis the current time, or 0 if not read yet
1232
   */
1233
  protected final void setAccessExpireTime(K key,
1234
      Expirable<?> expirable, @Var long currentTimeMillis) {
1235
    try {
1236
      Duration duration = expiry.getExpiryForAccess();
×
1237
      if (duration == null) {
×
1238
        return;
×
1239
      } else if (duration.isZero()) {
×
1240
        expirable.setExpireTimeMillis(0L);
×
1241
        cache.policy().expireVariably().ifPresent(policy ->
×
1242
            policy.setExpiresAfter(key, 0L, TimeUnit.NANOSECONDS));
×
1243
      } else if (duration.isEternal()) {
×
1244
        expirable.setExpireTimeMillis(Long.MAX_VALUE);
×
1245
        cache.policy().expireVariably().ifPresent(policy ->
×
1246
            policy.setExpiresAfter(key, Long.MAX_VALUE, TimeUnit.NANOSECONDS));
×
1247
      } else {
1248
        if (currentTimeMillis == 0L) {
×
1249
          currentTimeMillis = currentTimeMillis();
×
1250
        }
1251
        @Var long expireTimeMillis = duration.getAdjustedTime(currentTimeMillis);
×
1252
        expireTimeMillis = ((expireTimeMillis == 0L) || (expireTimeMillis == Long.MAX_VALUE))
×
1253
            ? (expireTimeMillis - 1)
×
1254
            : expireTimeMillis;
×
1255
        expirable.setExpireTimeMillis(expireTimeMillis);
×
1256
        cache.policy().expireVariably().ifPresent(policy ->
×
1257
            policy.setExpiresAfter(key, duration.getDurationAmount(), duration.getTimeUnit()));
×
1258
      }
1259
    } catch (RuntimeException e) {
×
1260
      logger.log(Level.WARNING, "Failed to set the entry's expiration time", e);
×
1261
    }
×
1262
  }
×
1263

1264
  /**
1265
   * Returns the time when the entry will expire.
1266
   *
1267
   * @param created if the write operation is an insert or an update
1268
   * @return the time when the entry will expire, zero if it should expire immediately,
1269
   *         Long.MIN_VALUE if it should not be changed, or Long.MAX_VALUE if eternal
1270
   */
1271
  protected final long getWriteExpireTimeMillis(boolean created) {
1272
    try {
1273
      Duration duration = created ? expiry.getExpiryForCreation() : expiry.getExpiryForUpdate();
×
1274
      if (duration == null) {
×
1275
        return Long.MIN_VALUE;
×
1276
      } else if (duration.isZero()) {
×
1277
        return 0L;
×
1278
      } else if (duration.isEternal()) {
×
1279
        return Long.MAX_VALUE;
×
1280
      }
1281
      long expireTimeMillis = duration.getAdjustedTime(currentTimeMillis());
×
1282
      return ((expireTimeMillis == 0L) || (expireTimeMillis == Long.MAX_VALUE))
×
1283
          ? (expireTimeMillis - 1)
×
1284
          : expireTimeMillis;
×
1285
    } catch (RuntimeException e) {
×
1286
      logger.log(Level.WARNING, "Failed to get the policy's expiration time", e);
×
1287
      return created ? Long.MAX_VALUE : Long.MIN_VALUE;
×
1288
    }
1289
  }
1290

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

1297
    Map.@Nullable Entry<K, Expirable<V>> current;
1298
    Map.@Nullable Entry<K, Expirable<V>> cursor;
1299

1300
    @Override
1301
    public boolean hasNext() {
1302
      while ((cursor == null) && delegate.hasNext()) {
×
1303
        Map.Entry<K, Expirable<V>> entry = delegate.next();
×
1304
        long millis = entry.getValue().isEternal() ? 0L : currentTimeMillis();
×
1305
        if (!entry.getValue().hasExpired(millis)) {
×
1306
          setAccessExpireTime(entry.getKey(), entry.getValue(), millis);
×
1307
          cursor = entry;
×
1308
        }
1309
      }
×
1310
      return (cursor != null);
×
1311
    }
1312

1313
    @Override
1314
    public Cache.Entry<K, V> next() {
1315
      if (!hasNext()) {
×
1316
        throw new NoSuchElementException();
×
1317
      }
1318
      statistics.recordHits(1L);
×
1319
      current = requireNonNull(cursor);
×
1320
      cursor = null;
×
1321
      return new EntryProxy<>(copyOf(current.getKey()), copyOf(current.getValue().get()));
×
1322
    }
1323

1324
    @Override
1325
    public void remove() {
1326
      if (current == null) {
×
1327
        throw new IllegalStateException();
×
1328
      }
1329
      boolean[] removed = { false };
×
1330
      boolean statsEnabled = statistics.isEnabled();
×
1331
      long start = statsEnabled ? ticker.read() : 0L;
×
1332

1333
      K key = current.getKey();
×
1334
      V oldValue = current.getValue().get();
×
1335
      cache.asMap().computeIfPresent(key, (k, expirable) -> {
×
1336
        if (oldValue.equals(expirable.get())) {
×
1337
          publishToCacheWriter(writer::delete, () -> key);
×
1338
          dispatcher.publishRemoved(CacheProxy.this, key, expirable.get());
×
1339
          removed[0] = true;
×
1340
          return null;
×
1341
        }
1342
        return expirable;
×
1343
      });
1344
      dispatcher.awaitSynchronous();
×
1345
      if (removed[0]) {
×
1346
        statistics.recordRemovals(1L);
×
1347
        if (statsEnabled) {
×
1348
          statistics.recordRemoveTime(ticker.read() - start);
×
1349
        }
1350
      }
1351
      current = null;
×
1352
    }
×
1353
  }
1354

1355
  protected static final class PutResult<V> {
×
1356
    @Nullable V oldValue;
1357
    boolean written;
1358
  }
1359

1360
  protected enum NullCompletionListener implements CompletionListener {
×
1361
    INSTANCE;
×
1362

1363
    @Override
1364
    public void onCompletion() {}
×
1365

1366
    @Override
1367
    public void onException(Exception e) {}
×
1368
  }
1369
}
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