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

ben-manes / caffeine / #5630

14 Jul 2026 01:10AM UTC coverage: 99.905% (-0.08%) from 99.988%
#5630

push

github

ben-manes
Match Guava's null-rejection in the adapter's asMap().replace

The facade's asMap().replace(K, V, V) short-circuited on the old-value
guard first, so replace(null, null, v) and replace(k, null, null)
returned false where Guava throws. Guava's LocalCache.replace checks
the key and new value for null before considering the old value; do the
same so the null corners reject rather than silently no-op. The
existing null-old-value case (replace(k, null, v) -> false) is
unchanged. Pinned by a CaffeinatedGuavaTest parity case run against
both the adapter and a real Guava cache.

4136 of 4150 branches covered (99.66%)

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

7 existing lines in 2 files now uncovered.

8406 of 8414 relevant lines covered (99.9%)

1.0 hits per line

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

94.44
/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LocalLoadingCache.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.cache;
17

18
import static com.github.benmanes.caffeine.cache.Caffeine.calculateHashMapCapacity;
19
import static com.github.benmanes.caffeine.cache.Caffeine.hasMethodOverride;
20
import static com.github.benmanes.caffeine.cache.LocalAsyncCache.composeResult;
21
import static java.util.Objects.requireNonNull;
22

23
import java.lang.System.Logger;
24
import java.lang.System.Logger.Level;
25
import java.util.Collections;
26
import java.util.LinkedHashMap;
27
import java.util.Map;
28
import java.util.Set;
29
import java.util.concurrent.CancellationException;
30
import java.util.concurrent.CompletableFuture;
31
import java.util.concurrent.CompletionException;
32
import java.util.concurrent.TimeoutException;
33
import java.util.function.Function;
34

35
import org.jspecify.annotations.Nullable;
36

37
import com.google.errorprone.annotations.Var;
38

39
/**
40
 * This class provides a skeletal implementation of the {@link LoadingCache} interface to minimize
41
 * the effort required to implement a {@link LocalCache}.
42
 *
43
 * @author ben.manes@gmail.com (Ben Manes)
44
 */
45
interface LocalLoadingCache<K, V> extends LocalManualCache<K, V>, LoadingCache<K, V> {
46
  Logger logger = System.getLogger(LocalLoadingCache.class.getName());
1✔
47

48
  /** Returns the {@link AsyncCacheLoader} used by this cache. */
49
  AsyncCacheLoader<? super K, V> cacheLoader();
50

51
  /** Returns the {@link CacheLoader#load} as a mapping function. */
52
  Function<K, @Nullable V> mappingFunction();
53

54
  /** Returns the {@link CacheLoader#loadAll} as a mapping function, if implemented. */
55
  @Nullable Function<Set<? extends K>, Map<K, V>> bulkMappingFunction();
56

57
  @Override
58
  @SuppressWarnings("NullAway")
59
  default V get(K key) {
60
    return cache().computeIfAbsent(key, mappingFunction());
1✔
61
  }
62

63
  @Override
64
  default Map<K, V> getAll(Iterable<? extends K> keys) {
65
    Function<Set<? extends K>, Map<K, V>> mappingFunction = bulkMappingFunction();
1✔
66
    return (mappingFunction == null)
1✔
67
        ? loadSequentially(keys)
1✔
68
        : getAll(keys, mappingFunction);
1✔
69
  }
70

71
  /** Sequentially loads each missing entry. */
72
  default Map<K, V> loadSequentially(Iterable<? extends K> keys) {
73
    var result = new LinkedHashMap<K, @Nullable V>(calculateHashMapCapacity(keys));
1✔
74
    for (K key : keys) {
1✔
75
      result.put(requireNonNull(key), null);
1✔
76
    }
1✔
77

78
    @Var int count = 0;
1✔
79
    int size = result.size();
1✔
80
    try {
81
      for (var iter = result.entrySet().iterator(); iter.hasNext();) {
1✔
82
        Map.Entry<K, @Nullable V> entry = iter.next();
1✔
83
        count++;
1✔
84

85
        V value = get(entry.getKey());
1✔
86
        if (value == null) {
1✔
87
          iter.remove();
1✔
88
        } else {
89
          entry.setValue(value);
1✔
90
        }
91
      }
1✔
92
    } catch (Throwable t) {
1✔
93
      cache().statsCounter().recordMisses(size - count);
1✔
94
      throw t;
1✔
95
    }
1✔
96
    @SuppressWarnings("NullableProblems")
97
    Map<K, V> unmodifiable = Collections.unmodifiableMap(result);
1✔
98
    return unmodifiable;
1✔
99
  }
100

101
  @Override
102
  @SuppressWarnings("FutureReturnValueIgnored")
103
  default CompletableFuture<V> refresh(K key) {
104
    requireNonNull(key);
1✔
105

106
    var startTime = new long[1];
1✔
107
    @SuppressWarnings({"unchecked", "Varifier"})
108
    @Nullable V[] oldValue = (V[]) new Object[1];
1✔
109
    @SuppressWarnings({"rawtypes", "unchecked"})
110
    @Nullable CompletableFuture<? extends V>[] reloading = new CompletableFuture[1];
1✔
111
    Object keyReference = cache().referenceKey(key);
1✔
112

113
    var future = cache().refreshes().compute(keyReference, (k, existing) -> {
1✔
114
      if ((existing != null) && !Async.isReady(existing) && !cache().isPendingEviction(key)) {
1✔
115
        return existing;
1✔
116
      }
117

118
      try {
119
        startTime[0] = cache().statsTicker().read();
1✔
120
        oldValue[0] = cache().getIfPresentQuietly(key);
1✔
121
        var refreshFuture = (oldValue[0] == null)
1✔
122
            ? cacheLoader().asyncLoad(key, cache().executor())
1✔
123
            : cacheLoader().asyncReload(key, oldValue[0], cache().executor());
1✔
124
        reloading[0] = requireNonNull(refreshFuture, "Null future");
1✔
125
        return refreshFuture;
1✔
126
      } catch (RuntimeException e) {
1✔
127
        throw e;
1✔
128
      } catch (InterruptedException e) {
1✔
129
        Thread.currentThread().interrupt();
1✔
130
        throw new CompletionException(e);
1✔
131
      } catch (Exception e) {
1✔
132
        throw new CompletionException(e);
1✔
133
      }
134
    });
135

136
    if (reloading[0] != null) {
1✔
137
      reloading[0].whenComplete((newValue, error) -> {
1✔
138
        long loadTime = cache().statsTicker().read() - startTime[0];
1✔
139
        if (error != null) {
1✔
140
          if (!(error instanceof CancellationException) && !(error instanceof TimeoutException)) {
1✔
141
            logger.log(Level.WARNING, "Exception thrown during refresh", error);
1✔
142
          }
143
          cache().refreshes().remove(keyReference, reloading[0]);
1✔
144
          cache().statsCounter().recordLoadFailure(loadTime);
1✔
145
          return;
1✔
146
        }
147

148
        try {
149
          var discard = new boolean[1];
1✔
150
          var hints = new LocalCache.RemapHints();
1✔
151
          @Nullable V value = cache().compute(key, (K k, @Nullable V currentValue) -> {
1✔
152
            // Keep the refresh registered until the write clears it to avoid refreshAfterWrite
153
            // readers from prematurely scheduling another reload
154
            boolean owned = (cache().refreshes().get(keyReference) == reloading[0]);
1✔
155
            if (owned && (currentValue == oldValue[0])) {
1✔
156
              return (currentValue == null) && (newValue == null) ? null : newValue;
1✔
157
            }
158
            discard[0] = (currentValue != newValue);
1✔
159
            hints.preserveTimestamps = true;
1✔
160
            return currentValue;
1✔
161
          }, cache().expiry(), /* recordLoad= */ false,
1✔
162
              /* recordLoadFailure= */ true, hints);
163

164
          if (discard[0] && (newValue != null)) {
1✔
165
            var cause = (value == null) ? RemovalCause.EXPLICIT : RemovalCause.REPLACED;
1✔
166
            cache().notifyRemoval(key, newValue, cause);
1✔
167
          }
168
          if (newValue == null) {
1✔
169
            cache().statsCounter().recordLoadFailure(loadTime);
1✔
170
          } else {
171
            cache().statsCounter().recordLoadSuccess(loadTime);
1✔
172
          }
173
        } catch (Throwable t) {
1✔
174
          logger.log(Level.WARNING, "Exception thrown during refresh", t);
1✔
175
          cache().statsCounter().recordLoadFailure(loadTime);
1✔
176
        }
1✔
177
      });
1✔
178
    }
179

180
    @SuppressWarnings("unchecked")
181
    var castedFuture = (CompletableFuture<V>) future;
1✔
182
    return castedFuture;
1✔
183
  }
184

185
  @Override
186
  default CompletableFuture<Map<K, V>> refreshAll(Iterable<? extends K> keys) {
187
    var result = new LinkedHashMap<K, CompletableFuture<@Nullable V>>(
1✔
188
        calculateHashMapCapacity(keys));
1✔
189
    for (K key : keys) {
1✔
190
      result.computeIfAbsent(key, this::refresh);
1✔
191
    }
1✔
192
    return composeResult(result);
1✔
193
  }
194

195
  /** Returns a mapping function that adapts to {@link CacheLoader#load}. */
196
  static <K, V> Function<K, @Nullable V> newMappingFunction(CacheLoader<? super K, V> cacheLoader) {
197
    return key -> {
1✔
198
      try {
199
        return cacheLoader.load(key);
1✔
200
      } catch (RuntimeException e) {
1✔
201
        throw e;
1✔
UNCOV
202
      } catch (InterruptedException e) {
×
UNCOV
203
        Thread.currentThread().interrupt();
×
UNCOV
204
        throw new CompletionException(e);
×
205
      } catch (Exception e) {
1✔
206
        throw new CompletionException(e);
1✔
207
      }
208
    };
209
  }
210

211
  /** Returns a mapping function that adapts to {@link CacheLoader#loadAll}, if implemented. */
212
  static <K, V> @Nullable Function<Set<? extends K>, Map<K, V>> newBulkMappingFunction(
213
      CacheLoader<? super K, V> cacheLoader) {
214
    if (!hasLoadAll(cacheLoader)) {
1✔
215
      return null;
1✔
216
    }
217
    return keysToLoad -> {
1✔
218
      try {
219
        @SuppressWarnings("unchecked")
220
        var loaded = (Map<K, V>) cacheLoader.loadAll(keysToLoad);
1✔
221
        return loaded;
1✔
222
      } catch (RuntimeException e) {
1✔
223
        throw e;
1✔
UNCOV
224
      } catch (InterruptedException e) {
×
UNCOV
225
        Thread.currentThread().interrupt();
×
UNCOV
226
        throw new CompletionException(e);
×
227
      } catch (Exception e) {
1✔
228
        throw new CompletionException(e);
1✔
229
      }
230
    };
231
  }
232

233
  /** Returns whether the supplied cache loader has bulk load functionality. */
234
  static boolean hasLoadAll(CacheLoader<?, ?> cacheLoader) {
235
    return hasMethodOverride(CacheLoader.class, cacheLoader, "loadAll", Set.class);
1✔
236
  }
237
}
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