• 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

87.32
/caffeine/src/main/java/com/github/benmanes/caffeine/cache/LocalAsyncLoadingCache.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.LinkedHashMap;
26
import java.util.Map;
27
import java.util.Set;
28
import java.util.concurrent.CancellationException;
29
import java.util.concurrent.CompletableFuture;
30
import java.util.concurrent.CompletionException;
31
import java.util.concurrent.Executor;
32
import java.util.concurrent.TimeoutException;
33
import java.util.function.BiFunction;
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 AsyncLoadingCache} interface to
41
 * minimize the effort required to implement a {@link LocalCache}.
42
 *
43
 * @author ben.manes@gmail.com (Ben Manes)
44
 */
45
abstract class LocalAsyncLoadingCache<K, V>
46
    implements LocalAsyncCache<K, V>, AsyncLoadingCache<K, V> {
47
  static final Logger logger = System.getLogger(LocalAsyncLoadingCache.class.getName());
1✔
48

49
  final @Nullable BiFunction<? super Set<? extends K>, ? super Executor,
50
      ? extends CompletableFuture<? extends Map<? extends K, ? extends V>>> bulkMappingFunction;
51
  final BiFunction<? super K, ? super Executor,
52
      ? extends CompletableFuture<? extends V>> mappingFunction;
53
  final AsyncCacheLoader<K, V> cacheLoader;
54

55
  @Nullable LoadingCacheView<K, V> cacheView;
56

57
  @SuppressWarnings("unchecked")
58
  LocalAsyncLoadingCache(AsyncCacheLoader<? super K, V> cacheLoader) {
1✔
59
    this.bulkMappingFunction = newBulkMappingFunction(cacheLoader);
1✔
60
    this.cacheLoader = (AsyncCacheLoader<K, V>) cacheLoader;
1✔
61
    this.mappingFunction = newMappingFunction(cacheLoader);
1✔
62
  }
1✔
63

64
  /** Returns a mapping function that adapts to {@link AsyncCacheLoader#asyncLoad}. */
65
  BiFunction<
66
      ? super K,
67
      ? super Executor,
68
      ? extends CompletableFuture<? extends V>> newMappingFunction(
69
          AsyncCacheLoader<? super K, V> cacheLoader) {
70
    return (key, executor) -> {
1✔
71
      try {
72
        return cacheLoader.asyncLoad(key, executor);
1✔
73
      } catch (RuntimeException e) {
1✔
74
        throw e;
1✔
75
      } catch (InterruptedException e) {
1✔
76
        Thread.currentThread().interrupt();
1✔
77
        throw new CompletionException(e);
1✔
78
      } catch (Exception e) {
1✔
79
        throw new CompletionException(e);
1✔
80
      }
81
    };
82
  }
83

84
  /**
85
   * Returns a mapping function that adapts to {@link AsyncCacheLoader#asyncLoadAll}, if
86
   * implemented.
87
   */
88
  @Nullable BiFunction<Set<? extends K>, Executor, CompletableFuture<Map<K, V>>>
89
      newBulkMappingFunction(AsyncCacheLoader<? super K, V> cacheLoader) {
90
    if (!canBulkLoad(cacheLoader)) {
1✔
91
      return null;
1✔
92
    }
93
    return (keysToLoad, executor) -> {
1✔
94
      try {
95
        @SuppressWarnings("unchecked")
96
        var loaded = (CompletableFuture<Map<K, V>>) cacheLoader.asyncLoadAll(keysToLoad, executor);
1✔
97
        return loaded;
1✔
UNCOV
98
      } catch (RuntimeException e) {
×
UNCOV
99
        throw e;
×
100
      } catch (InterruptedException e) {
1✔
101
        Thread.currentThread().interrupt();
1✔
102
        throw new CompletionException(e);
1✔
UNCOV
103
      } catch (Exception e) {
×
UNCOV
104
        throw new CompletionException(e);
×
105
      }
106
    };
107
  }
108

109
  /** Returns whether the supplied cache loader has bulk load functionality. */
110
  boolean canBulkLoad(AsyncCacheLoader<?, ?> loader) {
111
    @Var Class<?> defaultLoaderClass = AsyncCacheLoader.class;
1✔
112
    if (loader instanceof CacheLoader<?, ?>) {
1✔
113
      defaultLoaderClass = CacheLoader.class;
1✔
114
      if (hasMethodOverride(defaultLoaderClass, loader, "loadAll", Set.class)) {
1✔
115
        return true;
1✔
116
      }
117
    }
118
    return hasMethodOverride(defaultLoaderClass,
1✔
119
        loader, "asyncLoadAll", Set.class, Executor.class);
120
  }
121

122
  @Override
123
  public CompletableFuture<V> get(K key) {
124
    return get(key, mappingFunction);
1✔
125
  }
126

127
  @Override
128
  public CompletableFuture<Map<K, V>> getAll(Iterable<? extends K> keys) {
129
    if (bulkMappingFunction != null) {
1✔
130
      return getAll(keys, bulkMappingFunction);
1✔
131
    }
132

133
    int initialCapacity = calculateHashMapCapacity(keys);
1✔
134
    var result = new LinkedHashMap<K, @Nullable CompletableFuture<@Nullable V>>(initialCapacity);
1✔
135
    for (K key : keys) {
1✔
136
      result.put(requireNonNull(key), null);
1✔
137
    }
1✔
138
    for (var entry : result.entrySet()) {
1✔
139
      entry.setValue(requireNonNull(get(entry.getKey())));
1✔
140
    }
1✔
141

142
    @SuppressWarnings("NullAway")
143
    Map<K, CompletableFuture<@Nullable V>> results = result;
1✔
144
    return composeResult(results);
1✔
145
  }
146

147
  @Override
148
  public LoadingCache<K, V> synchronous() {
149
    return (cacheView == null) ? (cacheView = new LoadingCacheView<>(this)) : cacheView;
1✔
150
  }
151

152
  /* --------------- Synchronous views --------------- */
153

154
  static final class LoadingCacheView<K, V>
155
      extends AbstractCacheView<K, V> implements LoadingCache<K, V> {
156
    private static final long serialVersionUID = 1L;
157

158
    @SuppressWarnings("serial")
159
    final LocalAsyncLoadingCache<K, V> asyncCache;
160

161
    LoadingCacheView(LocalAsyncLoadingCache<K, V> asyncCache) {
1✔
162
      this.asyncCache = requireNonNull(asyncCache);
1✔
163
    }
1✔
164

165
    @Override
166
    LocalAsyncLoadingCache<K, V> asyncCache() {
167
      return asyncCache;
1✔
168
    }
169

170
    @Override
171
    public V get(K key) {
172
      return resolve(asyncCache.get(key));
1✔
173
    }
174

175
    @Override
176
    public Map<K, V> getAll(Iterable<? extends K> keys) {
177
      return resolve(asyncCache.getAll(keys));
1✔
178
    }
179

180
    @Override
181
    public CompletableFuture<V> refresh(K key) {
182
      requireNonNull(key);
1✔
183

184
      Object keyReference = asyncCache.cache().referenceKey(key);
1✔
185
      for (;;) {
186
        @Var var future = tryOptimisticRefresh(key, keyReference);
1✔
187
        if (future == null) {
1✔
188
          future = tryComputeRefresh(key, keyReference);
1✔
189
        }
190
        if (future != null) {
1!
191
          return future;
1✔
192
        }
UNCOV
193
      }
×
194
    }
195

196
    @Override
197
    public CompletableFuture<Map<K, V>> refreshAll(Iterable<? extends K> keys) {
198
      var result = new LinkedHashMap<K, CompletableFuture<@Nullable V>>(
1✔
199
          calculateHashMapCapacity(keys));
1✔
200
      for (K key : keys) {
1✔
201
        result.computeIfAbsent(key, this::refresh);
1✔
202
      }
1✔
203
      return composeResult(result);
1✔
204
    }
205

206
    /** Attempts to avoid a reload if the entry is absent, or a load or reload is in-flight. */
207
    @SuppressWarnings("FutureReturnValueIgnored")
208
    private @Nullable CompletableFuture<V> tryOptimisticRefresh(K key, Object keyReference) {
209
      // If a refresh is in-flight, then return it directly. If completed and not yet removed, then
210
      // remove to trigger a new reload.
211
      @SuppressWarnings("unchecked")
212
      var lastRefresh = (CompletableFuture<V>) asyncCache.cache().refreshes().get(keyReference);
1✔
213
      if (lastRefresh != null) {
1✔
214
        if (Async.isReady(lastRefresh) || asyncCache.cache().isPendingEviction(key)) {
1!
215
          asyncCache.cache().refreshes().remove(keyReference, lastRefresh);
1✔
216
        } else {
217
          return lastRefresh;
1✔
218
        }
219
      }
220

221
      // If the entry is absent then perform a new load, else if in-flight then return it
222
      var oldValueFuture = asyncCache.cache().getIfPresentQuietly(key);
1✔
223
      if ((oldValueFuture == null)
1✔
224
          || (oldValueFuture.isDone() && oldValueFuture.isCompletedExceptionally())) {
1!
225
        if (oldValueFuture != null) {
1!
UNCOV
226
          asyncCache.cache().remove(key, oldValueFuture);
×
227
        }
228
        var future = asyncCache.get(key, asyncCache.mappingFunction, /* recordStats= */ false);
1✔
229
        @SuppressWarnings("unchecked")
230
        var prior = (CompletableFuture<V>) asyncCache.cache()
1✔
231
            .refreshes().putIfAbsent(keyReference, future);
1✔
232
        var result = (prior == null) ? future : prior;
1!
233
        result.whenComplete((r, e) -> asyncCache.cache().refreshes().remove(keyReference, result));
1✔
234
        return result;
1✔
235
      } else if (!oldValueFuture.isDone()) {
1!
236
        // no-op if load is pending
UNCOV
237
        return oldValueFuture;
×
238
      }
239

240
      // Fallback to the slow path, possibly retrying
241
      return null;
1✔
242
    }
243

244
    /** Begins a refresh if the entry has materialized and no reload is in-flight. */
245
    @SuppressWarnings("FutureReturnValueIgnored")
246
    private @Nullable CompletableFuture<V> tryComputeRefresh(K key, Object keyReference) {
247
      var startTime = new long[1];
1✔
248
      var refreshed = new boolean[1];
1✔
249
      @SuppressWarnings({"rawtypes", "unchecked"})
250
      @Nullable CompletableFuture<V>[] oldValueFuture = new CompletableFuture[1];
1✔
251
      var future = asyncCache.cache().refreshes().computeIfAbsent(keyReference, k -> {
1✔
252
        oldValueFuture[0] = asyncCache.cache().getIfPresentQuietly(key);
1✔
253
        V oldValue = Async.getIfReady(oldValueFuture[0]);
1✔
254
        if (oldValue == null) {
1!
UNCOV
255
          return null;
×
256
        }
257

258
        refreshed[0] = true;
1✔
259
        startTime[0] = asyncCache.cache().statsTicker().read();
1✔
260
        try {
261
          var reloadFuture = asyncCache.cacheLoader.asyncReload(
1✔
262
              key, oldValue, asyncCache.cache().executor());
1✔
263
          return requireNonNull(reloadFuture, "Null future");
1✔
264
        } catch (RuntimeException e) {
1✔
265
          throw e;
1✔
UNCOV
266
        } catch (InterruptedException e) {
×
UNCOV
267
          Thread.currentThread().interrupt();
×
UNCOV
268
          throw new CompletionException(e);
×
UNCOV
269
        } catch (Exception e) {
×
UNCOV
270
          throw new CompletionException(e);
×
271
        }
272
      });
273

274
      if (future == null) {
1!
275
        // Retry the optimistic path
UNCOV
276
        return null;
×
277
      }
278

279
      @SuppressWarnings("unchecked")
280
      var castedFuture = (CompletableFuture<V>) future;
1✔
281
      if (refreshed[0]) {
1!
282
        castedFuture.whenComplete((newValue, error) -> {
1✔
283
          long loadTime = asyncCache.cache().statsTicker().read() - startTime[0];
1✔
284
          if (error != null) {
1✔
285
            if (!(error instanceof CancellationException) && !(error instanceof TimeoutException)) {
1!
286
              logger.log(Level.WARNING, "Exception thrown during refresh", error);
1✔
287
            }
288
            asyncCache.cache().refreshes().remove(keyReference, castedFuture);
1✔
289
            asyncCache.cache().statsCounter().recordLoadFailure(loadTime);
1✔
290
            return;
1✔
291
          }
292

293
          try {
294
            var discard = new boolean[1];
1✔
295
            var hints = new LocalCache.RemapHints();
1✔
296
            var value = asyncCache.cache().compute(key, (ignored, currentValue) -> {
1✔
297
              // Keep the refresh registered until the write clears it to avoid refreshAfterWrite
298
              // readers from prematurely scheduling another reload
299
              boolean successful =
1✔
300
                  (asyncCache.cache().refreshes().get(keyReference) == castedFuture);
1✔
301
              if (successful && (currentValue == oldValueFuture[0])) {
1!
302
                return (newValue == null) ? null : castedFuture;
1!
303
              }
304
              // Otherwise, a write invalidated the refresh so discard it
305
              hints.preserveTimestamps = true;
1✔
306
              discard[0] = (newValue != Async.getIfReady((CompletableFuture<?>) currentValue));
1✔
307
              return currentValue;
1✔
308
            }, asyncCache.cache().expiry(), /* recordLoad= */ false,
1✔
309
                /* recordLoadFailure= */ true, hints);
310

311
            if (discard[0] && (newValue != null)) {
1!
312
              var cause = (value == null) ? RemovalCause.EXPLICIT : RemovalCause.REPLACED;
1✔
313
              asyncCache.cache().notifyRemoval(key, castedFuture, cause);
1✔
314
            }
315
            if (newValue == null) {
1✔
316
              asyncCache.cache().statsCounter().recordLoadFailure(loadTime);
1✔
317
            } else {
318
              asyncCache.cache().statsCounter().recordLoadSuccess(loadTime);
1✔
319
            }
UNCOV
320
          } catch (Throwable t) {
×
UNCOV
321
            logger.log(Level.WARNING, "Exception thrown during refresh", t);
×
UNCOV
322
            asyncCache.cache().statsCounter().recordLoadFailure(loadTime);
×
UNCOV
323
            asyncCache.cache().remove(key, castedFuture);
×
324
          }
1✔
325
        });
1✔
326
      }
327
      return castedFuture;
1✔
328
    }
329
  }
330
}
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