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

ben-manes / caffeine / #5452

11 May 2026 04:59AM UTC coverage: 99.806% (-0.1%) from 99.903%
#5452

push

github

ben-manes
Fix two TCK regressions from the spec-compliance sweep

1. LoadingCacheProxy.loadAll non-replacing path now routes through
   loadAllAndKeepExisting (the #L fix), which calls the user CacheLoader
   directly and bypasses JCacheLoaderAdapter.loadAll's wrap. TCK
   CacheLoaderTest.shouldPropagateExceptionUsingLoadAll asserts the
   listener sees a CacheLoaderException — 1.1.1 relaxed the spec rule
   but the TCK still enforces. Wrap RuntimeException in the inner catch
   to match CacheProxy.loadAll's convention.

2. EntryIterator.remove() routed through CacheProxy.remove(K, V) which
   records a hit on success. After #K added a hit on each next(),
   iterate+remove double-counted (200 hits for 100 entries). TCK
   CacheMBStatisticsBeanTest.testIterateAndRemove asserts CacheHits ==
   100 after iterating and removing 100 entries. Inline a conditional
   remove that records only the removal (not the hit), matching the
   stats table p.126's split: iterator() Hits=Yes on next, Removals=Yes
   on remove().

4011 of 4026 branches covered (99.63%)

19 of 24 new or added lines in 2 files covered. (79.17%)

11 existing lines in 2 files now uncovered.

8243 of 8259 relevant lines covered (99.81%)

1.0 hits per line

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

93.81
/jcache/src/main/java/com/github/benmanes/caffeine/jcache/LoadingCacheProxy.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.stream.Collectors.toUnmodifiableList;
19

20
import java.util.List;
21
import java.util.Map;
22
import java.util.Objects;
23
import java.util.Optional;
24
import java.util.Set;
25
import java.util.concurrent.CompletableFuture;
26
import java.util.concurrent.Executor;
27

28
import javax.cache.Cache;
29
import javax.cache.CacheException;
30
import javax.cache.CacheManager;
31
import javax.cache.expiry.ExpiryPolicy;
32
import javax.cache.integration.CacheLoader;
33
import javax.cache.integration.CacheLoaderException;
34
import javax.cache.integration.CompletionListener;
35

36
import org.jspecify.annotations.Nullable;
37

38
import com.github.benmanes.caffeine.cache.LoadingCache;
39
import com.github.benmanes.caffeine.cache.Ticker;
40
import com.github.benmanes.caffeine.jcache.configuration.CaffeineConfiguration;
41
import com.github.benmanes.caffeine.jcache.event.EventDispatcher;
42
import com.github.benmanes.caffeine.jcache.management.JCacheStatisticsMXBean;
43
import com.google.errorprone.annotations.Var;
44

45
/**
46
 * An implementation of JSR-107 {@link Cache} backed by a Caffeine loading cache.
47
 *
48
 * @author ben.manes@gmail.com (Ben Manes)
49
 */
50
@SuppressWarnings("OvershadowingSubclassFields")
51
public final class LoadingCacheProxy<K, V> extends CacheProxy<K, V> {
52
  private final LoadingCache<K, @Nullable Expirable<V>> cache;
53

54
  @SuppressWarnings({"PMD.ExcessiveParameterList", "TooManyParameters"})
55
  public LoadingCacheProxy(String name, Executor executor, CacheManager cacheManager,
56
      CaffeineConfiguration<K, V> configuration, LoadingCache<K, @Nullable Expirable<V>> cache,
57
      EventDispatcher<K, V> dispatcher, CacheLoader<K, V> cacheLoader, ExpiryPolicy expiry,
58
      Ticker ticker, JCacheStatisticsMXBean statistics) {
59
    super(name, executor, cacheManager, configuration, cache, dispatcher,
1✔
60
        Optional.of(cacheLoader), expiry, ticker, statistics);
1✔
61
    this.cache = cache;
1✔
62
  }
1✔
63

64
  @Override
65
  public @Nullable V get(K key) {
66
    requireNotClosed();
1✔
67
    try {
68
      return getOrLoad(key);
1✔
69
    } catch (NullPointerException | IllegalStateException | ClassCastException | CacheException e) {
1✔
70
      throw e;
1✔
71
    } catch (RuntimeException e) {
1✔
72
      throw new CacheException(e);
1✔
73
    } finally {
74
      dispatcher.awaitSynchronous();
1✔
75
    }
76
  }
77

78
  /** Retrieves the value from the cache, loading it if necessary. */
79
  private @Nullable V getOrLoad(K key) {
80
    boolean statsEnabled = statistics.isEnabled();
1✔
81
    long start = statsEnabled ? ticker.read() : 0L;
1✔
82

83
    @Var long millis = 0L;
1✔
84
    @Var Expirable<V> expirable = cache.getIfPresent(key);
1✔
85
    if ((expirable != null) && !expirable.isEternal()) {
1✔
86
      millis = nanosToMillis((start == 0L) ? ticker.read() : start);
1✔
87
      if (expirable.hasExpired(millis)) {
1✔
88
        var expired = expirable;
1✔
89
        cache.asMap().computeIfPresent(key, (k, e) -> {
1✔
90
          if (e == expired) {
1✔
91
            dispatcher.publishExpired(this, key, expired.get());
1✔
92
            statistics.recordEvictions(1);
1✔
93
            return null;
1✔
94
          }
95
          return e;
1✔
96
        });
97
        expirable = null;
1✔
98
      }
99
    }
100

101
    if (expirable == null) {
1✔
102
      expirable = cache.get(key);
1✔
103
      statistics.recordMisses(1L);
1✔
104
    } else {
105
      setAccessExpireTime(key, expirable, millis);
1✔
106
      statistics.recordHits(1L);
1✔
107
    }
108

109
    @Var V value = null;
1✔
110
    if (expirable != null) {
1✔
111
      value = copyValue(expirable);
1✔
112
    }
113
    if (statsEnabled) {
1✔
114
      statistics.recordGetTime(ticker.read() - start);
1✔
115
    }
116
    return value;
1✔
117
  }
118

119
  @Override
120
  public Map<K, V> getAll(Set<? extends K> keys) {
121
    return getAll(keys, /* updateAccessTime= */ true);
1✔
122
  }
123

124
  /** Returns the entries, loading if necessary, and optionally updates their access expiry time. */
125
  private Map<K, V> getAll(Set<? extends K> keys, boolean updateAccessTime) {
126
    requireNotClosed();
1✔
127
    boolean statsEnabled = statistics.isEnabled();
1✔
128
    long start = statsEnabled ? ticker.read() : 0L;
1✔
129
    try {
130
      Map<K, Expirable<V>> entries = getAndFilterExpiredEntries(keys, updateAccessTime);
1✔
131

132
      if (entries.size() != keys.size()) {
1✔
133
        List<K> keysToLoad = keys.stream()
1✔
134
            .filter(key -> !entries.containsKey(key))
1✔
135
            .collect(toUnmodifiableList());
1✔
136
        entries.putAll(cache.getAll(keysToLoad));
1✔
137
      }
138

139
      Map<K, V> result = copyMap(entries);
1✔
140
      if (statsEnabled) {
1✔
141
        statistics.recordGetTime(ticker.read() - start);
1✔
142
      }
143
      return result;
1✔
144
    } catch (NullPointerException | IllegalStateException | ClassCastException | CacheException e) {
1✔
145
      throw e;
1✔
146
    } catch (RuntimeException e) {
1✔
147
      throw new CacheException(e);
1✔
148
    } finally {
149
      dispatcher.awaitSynchronous();
1✔
150
    }
151
  }
152

153
  @Override
154
  @SuppressWarnings({"CheckReturnValue", "CollectionUndefinedEquality",
155
      "FutureReturnValueIgnored", "ResultOfMethodCallIgnored"})
156
  public void loadAll(Set<? extends K> keys, boolean replaceExistingValues,
157
      @Nullable CompletionListener completionListener) {
158
    requireNotClosed();
1✔
159
    keys.forEach(Objects::requireNonNull);
1✔
160
    CompletionListener listener = (completionListener == null)
1✔
161
        ? NullCompletionListener.INSTANCE
1✔
162
        : completionListener;
1✔
163

164
    var future = new CompletableFuture<@Nullable Void>();
1✔
165
    synchronized (configuration) {
1✔
166
      requireNotClosed();
1✔
167
      inFlight.add(future);
1✔
168
    }
1✔
169
    try {
170
      CompletableFuture.runAsync(() -> {
1✔
171
        @Var boolean success = false;
1✔
172
        try {
173
          if (replaceExistingValues) {
1✔
174
            Map<K, V> loaded = cacheLoader.orElseThrow().loadAll(keys);
1✔
175
            for (var entry : loaded.entrySet()) {
1✔
176
              putNoCopyOrAwait(entry.getKey(), entry.getValue(), /* publishToWriter= */ false);
1✔
177
            }
1✔
178
          } else {
1✔
179
            // Don't route through getAll(...) — its getAndFilterExpiredEntries
180
            // records hits/misses, which JSR-107 1.1.1 §12.4 (p.126) prohibits
181
            // for loadAll. Use the same put-if-absent path as CacheProxy.
182
            loadAllAndKeepExisting(keys);
1✔
183
          }
184
          success = true;
1✔
NEW
185
        } catch (CacheLoaderException e) {
×
UNCOV
186
          listener.onException(e);
×
187
        } catch (RuntimeException e) {
1✔
188
          listener.onException(new CacheLoaderException(e));
1✔
189
        } finally {
190
          dispatcher.ignoreSynchronous();
1✔
191
        }
192
        // Call onCompletion outside the catch so a throw from it doesn't also
193
        // fire onException; per JSR-107 1.1.1 p.64 they're terminal callbacks.
194
        if (success) {
1✔
195
          listener.onCompletion();
1✔
196
        }
197
      }, executor).whenComplete((r, e) -> {
1✔
198
        inFlight.remove(future);
1✔
199
        future.complete(null);
1✔
200
      });
1✔
201
    } catch (RuntimeException e) {
1✔
202
      inFlight.remove(future);
1✔
203
      future.complete(null);
1✔
204
      listener.onException(e);
1✔
UNCOV
205
    } catch (Throwable t) {
×
UNCOV
206
      inFlight.remove(future);
×
UNCOV
207
      future.complete(null);
×
UNCOV
208
      throw t;
×
209
    }
1✔
210
  }
1✔
211
}
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