• 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/CacheFactory.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

20
import java.time.Duration;
21
import java.util.ArrayList;
22
import java.util.Optional;
23
import java.util.concurrent.Executor;
24
import java.util.concurrent.TimeUnit;
25

26
import javax.cache.CacheManager;
27
import javax.cache.configuration.CacheEntryListenerConfiguration;
28
import javax.cache.configuration.CompleteConfiguration;
29
import javax.cache.configuration.Configuration;
30
import javax.cache.configuration.Factory;
31
import javax.cache.expiry.EternalExpiryPolicy;
32
import javax.cache.expiry.ExpiryPolicy;
33
import javax.cache.integration.CacheLoader;
34

35
import org.jspecify.annotations.Nullable;
36

37
import com.github.benmanes.caffeine.cache.Caffeine;
38
import com.github.benmanes.caffeine.cache.Expiry;
39
import com.github.benmanes.caffeine.cache.Scheduler;
40
import com.github.benmanes.caffeine.cache.Ticker;
41
import com.github.benmanes.caffeine.cache.Weigher;
42
import com.github.benmanes.caffeine.jcache.configuration.CaffeineConfiguration;
43
import com.github.benmanes.caffeine.jcache.configuration.TypesafeConfigurator;
44
import com.github.benmanes.caffeine.jcache.event.EventDispatcher;
45
import com.github.benmanes.caffeine.jcache.event.JCacheEvictionListener;
46
import com.github.benmanes.caffeine.jcache.integration.JCacheLoaderAdapter;
47
import com.github.benmanes.caffeine.jcache.management.JCacheStatisticsMXBean;
48
import com.google.errorprone.annotations.Var;
49
import com.typesafe.config.Config;
50

51
/**
52
 * A factory for creating a cache from the configuration.
53
 *
54
 * @author ben.manes@gmail.com (Ben Manes)
55
 */
56
final class CacheFactory {
57

58
  private CacheFactory() {}
59

60
  /**
61
   * Returns if the cache definition is found in the external settings file.
62
   *
63
   * @param cacheManager the owner
64
   * @param cacheName the name of the cache
65
   * @return {@code true} if a definition exists
66
   */
67
  public static boolean isDefinedExternally(CacheManager cacheManager, String cacheName) {
UNCOV
68
    return TypesafeConfigurator.cacheNames(rootConfig(cacheManager)).contains(cacheName);
×
69
  }
70

71
  /**
72
   * Returns a newly created cache instance if a definition is found in the external settings file.
73
   *
74
   * @param cacheManager the owner
75
   * @param cacheName the name of the cache
76
   * @return a new cache instance or null if the named cache is not defined in the settings file
77
   */
78
  @SuppressWarnings("resource")
79
  public static <K, V> @Nullable CacheProxy<K, V> tryToCreateFromExternalSettings(
80
      CacheManagerImpl cacheManager, String cacheName) {
UNCOV
81
    return TypesafeConfigurator.<K, V>from(rootConfig(cacheManager), cacheName)
×
UNCOV
82
        .map(configuration -> createCache(cacheManager, cacheName, configuration))
×
UNCOV
83
        .orElse(null);
×
84
  }
85

86
  /**
87
   * Returns a fully constructed cache based on the cache
88
   *
89
   * @param cacheManager the owner of the cache instance
90
   * @param cacheName the name of the cache
91
   * @param configuration the full cache definition
92
   * @return a newly constructed cache instance
93
   */
94
  public static <K, V> CacheProxy<K, V> createCache(CacheManagerImpl cacheManager,
95
      String cacheName, Configuration<K, V> configuration) {
UNCOV
96
    CaffeineConfiguration<K, V> config = resolveConfigurationFor(cacheManager, configuration);
×
UNCOV
97
    return new Builder<>(cacheManager, cacheName, config).build();
×
98
  }
99

100
  /** Returns the resolved configuration. */
101
  private static Config rootConfig(CacheManager cacheManager) {
UNCOV
102
    return requireNonNull(TypesafeConfigurator.configSource().get(
×
UNCOV
103
        cacheManager.getURI(), cacheManager.getClassLoader()));
×
104
  }
105

106
  /** Copies the configuration and overlays it on top of the default settings. */
107
  private static <K, V> CaffeineConfiguration<K, V> resolveConfigurationFor(
108
      CacheManager cacheManager, Configuration<K, V> configuration) {
UNCOV
109
    if (configuration instanceof CaffeineConfiguration<?, ?>) {
×
UNCOV
110
      return new CaffeineConfiguration<>((CaffeineConfiguration<K, V>) configuration);
×
111
    }
112

UNCOV
113
    CaffeineConfiguration<K, V> template = TypesafeConfigurator.defaults(rootConfig(cacheManager));
×
UNCOV
114
    if (configuration instanceof CompleteConfiguration<?, ?>) {
×
UNCOV
115
      var complete = (CompleteConfiguration<K, V>) configuration;
×
UNCOV
116
      template.setReadThrough(complete.isReadThrough());
×
UNCOV
117
      template.setWriteThrough(complete.isWriteThrough());
×
UNCOV
118
      template.setManagementEnabled(complete.isManagementEnabled());
×
UNCOV
119
      template.setStatisticsEnabled(complete.isStatisticsEnabled());
×
UNCOV
120
      var existing = new ArrayList<CacheEntryListenerConfiguration<K, V>>();
×
UNCOV
121
      template.getCacheEntryListenerConfigurations().forEach(existing::add);
×
UNCOV
122
      existing.forEach(template::removeCacheEntryListenerConfiguration);
×
UNCOV
123
      complete.getCacheEntryListenerConfigurations()
×
UNCOV
124
          .forEach(template::addCacheEntryListenerConfiguration);
×
UNCOV
125
      template.setCacheLoaderFactory(complete.getCacheLoaderFactory());
×
UNCOV
126
      template.setCacheWriterFactory(complete.getCacheWriterFactory());
×
UNCOV
127
      template.setExpiryPolicyFactory(complete.getExpiryPolicyFactory());
×
128
    }
129

UNCOV
130
    template.setTypes(configuration.getKeyType(), configuration.getValueType());
×
UNCOV
131
    template.setStoreByValue(configuration.isStoreByValue());
×
UNCOV
132
    return template;
×
133
  }
134

135
  /** A one-shot builder for creating a cache instance. */
136
  private static final class Builder<K, V> {
137
    final Ticker ticker;
138
    final String cacheName;
139
    final Executor executor;
140
    final Scheduler scheduler;
141
    final ExpiryPolicy expiryPolicy;
142
    final CacheManagerImpl cacheManager;
143
    final EventDispatcher<K, V> dispatcher;
144
    final JCacheStatisticsMXBean statistics;
145
    final Caffeine<Object, Object> caffeine;
146
    final CaffeineConfiguration<K, V> config;
147

UNCOV
148
    Builder(CacheManagerImpl cacheManager, String cacheName, CaffeineConfiguration<K, V> config) {
×
UNCOV
149
      this.config = config;
×
UNCOV
150
      this.cacheName = cacheName;
×
UNCOV
151
      this.cacheManager = cacheManager;
×
UNCOV
152
      this.caffeine = Caffeine.newBuilder();
×
UNCOV
153
      this.statistics = new JCacheStatisticsMXBean();
×
UNCOV
154
      this.ticker = config.getTickerFactory().create();
×
UNCOV
155
      this.executor = config.getExecutorFactory().create();
×
UNCOV
156
      this.scheduler = config.getSchedulerFactory().create();
×
UNCOV
157
      this.expiryPolicy = config.getExpiryPolicyFactory().create();
×
UNCOV
158
      this.dispatcher = new EventDispatcher<>(executor);
×
159

UNCOV
160
      caffeine.ticker(ticker);
×
UNCOV
161
      caffeine.executor(executor);
×
UNCOV
162
      caffeine.scheduler(scheduler);
×
UNCOV
163
      config.getCacheEntryListenerConfigurations().forEach(dispatcher::register);
×
UNCOV
164
    }
×
165

166
    /** Creates a configured cache. */
167
    CacheProxy<K, V> build() {
UNCOV
168
      @Var boolean evicts = false;
×
UNCOV
169
      evicts |= configureMaximumSize();
×
UNCOV
170
      evicts |= configureMaximumWeight();
×
171

UNCOV
172
      @Var boolean expires = false;
×
UNCOV
173
      expires |= configureExpireAfterWrite();
×
UNCOV
174
      expires |= configureExpireAfterAccess();
×
UNCOV
175
      expires |= configureExpireVariably();
×
UNCOV
176
      if (!expires) {
×
UNCOV
177
        expires = configureJCacheExpiry();
×
178
      }
179

UNCOV
180
      if (config.isNativeStatisticsEnabled()) {
×
UNCOV
181
        caffeine.recordStats();
×
182
      }
183

UNCOV
184
      @Var JCacheEvictionListener<K, V> evictionListener = null;
×
UNCOV
185
      if (evicts || expires) {
×
UNCOV
186
        evictionListener = new JCacheEvictionListener<>(dispatcher, statistics);
×
UNCOV
187
        caffeine.evictionListener(evictionListener);
×
188
      }
189

190
      CacheProxy<K, V> cache;
UNCOV
191
      if (isReadThrough()) {
×
UNCOV
192
        configureRefreshAfterWrite();
×
UNCOV
193
        cache = newLoadingCacheProxy();
×
194
      } else {
UNCOV
195
        cache = newCacheProxy();
×
196
      }
197

UNCOV
198
      if (evictionListener != null) {
×
UNCOV
199
        evictionListener.setCache(cache);
×
200
      }
UNCOV
201
      return cache;
×
202
    }
203

204
    /** Determines if the cache should operate in read through mode. */
205
    private boolean isReadThrough() {
UNCOV
206
      return config.isReadThrough() && (config.getCacheLoaderFactory() != null);
×
207
    }
208

209
    /** Creates a cache that does not read through on a cache miss. */
210
    private CacheProxy<K, V> newCacheProxy() {
UNCOV
211
      var cacheLoaderFactory = config.getCacheLoaderFactory();
×
UNCOV
212
      var cacheLoader = (cacheLoaderFactory == null) ? null : cacheLoaderFactory.create();
×
UNCOV
213
      return new CacheProxy<>(cacheName, executor, cacheManager, config, caffeine.build(),
×
UNCOV
214
          dispatcher, Optional.ofNullable(cacheLoader), expiryPolicy, ticker, statistics);
×
215
    }
216

217
    /** Creates a cache that reads through on a cache miss. */
218
    private CacheProxy<K, V> newLoadingCacheProxy() {
UNCOV
219
      CacheLoader<K, V> cacheLoader = requireNonNull(config.getCacheLoaderFactory()).create();
×
UNCOV
220
      var adapter = new JCacheLoaderAdapter<>(
×
221
          cacheLoader, dispatcher, expiryPolicy, ticker, statistics);
UNCOV
222
      var cache = caffeine.build(adapter);
×
UNCOV
223
      var jcache = new LoadingCacheProxy<>(cacheName, executor, cacheManager, config,
×
224
          cache, dispatcher, cacheLoader, expiryPolicy, ticker, statistics);
UNCOV
225
      adapter.setCache(jcache);
×
UNCOV
226
      return jcache;
×
227
    }
228

229
    /** Configures the maximum size and returns if set. */
230
    private boolean configureMaximumSize() {
UNCOV
231
      if (config.getMaximumSize().isPresent()) {
×
UNCOV
232
        caffeine.maximumSize(config.getMaximumSize().getAsLong());
×
233
      }
UNCOV
234
      return config.getMaximumSize().isPresent();
×
235
    }
236

237
    /** Configures the maximum weight and returns if set. */
238
    private boolean configureMaximumWeight() {
UNCOV
239
      if (config.getMaximumWeight().isPresent()) {
×
UNCOV
240
        caffeine.maximumWeight(config.getMaximumWeight().getAsLong());
×
UNCOV
241
        Weigher<K, V> weigher = config.getWeigherFactory().map(Factory::create)
×
UNCOV
242
            .orElseThrow(() -> new IllegalStateException("Weigher not configured"));
×
UNCOV
243
        caffeine.weigher((K key, Expirable<V> expirable) -> {
×
UNCOV
244
          return weigher.weigh(key, expirable.get());
×
245
        });
246
      }
UNCOV
247
      return config.getMaximumWeight().isPresent();
×
248
    }
249

250
    /** Configures write expiration and returns if set. */
251
    private boolean configureExpireAfterWrite() {
UNCOV
252
      if (config.getExpireAfterWrite().isEmpty()) {
×
UNCOV
253
        return false;
×
254
      }
UNCOV
255
      caffeine.expireAfterWrite(Duration.ofNanos(config.getExpireAfterWrite().getAsLong()));
×
UNCOV
256
      return true;
×
257
    }
258

259
    /** Configures access expiration and returns if set. */
260
    private boolean configureExpireAfterAccess() {
UNCOV
261
      if (config.getExpireAfterAccess().isEmpty()) {
×
UNCOV
262
        return false;
×
263
      }
UNCOV
264
      caffeine.expireAfterAccess(Duration.ofNanos(config.getExpireAfterAccess().getAsLong()));
×
UNCOV
265
      return true;
×
266
    }
267

268
    /** Configures the custom expiration and returns if set. */
269
    private boolean configureExpireVariably() {
UNCOV
270
      if (config.getExpiryFactory().isEmpty()) {
×
UNCOV
271
        return false;
×
272
      }
UNCOV
273
      caffeine.expireAfter(new ExpiryAdapter<>(config.getExpiryFactory().orElseThrow().create()));
×
UNCOV
274
      return true;
×
275
    }
276

277
    private boolean configureJCacheExpiry() {
UNCOV
278
      if (expiryPolicy instanceof EternalExpiryPolicy) {
×
UNCOV
279
        return false;
×
280
      }
UNCOV
281
      caffeine.expireAfter(new ExpirableToExpiry<>(ticker));
×
UNCOV
282
      return true;
×
283
    }
284

285
    private void configureRefreshAfterWrite() {
UNCOV
286
      if (config.getRefreshAfterWrite().isPresent()) {
×
UNCOV
287
        caffeine.refreshAfterWrite(Duration.ofNanos(config.getRefreshAfterWrite().getAsLong()));
×
288
      }
UNCOV
289
    }
×
290
  }
291

292
  private static final class ExpiryAdapter<K, V> implements Expiry<K, Expirable<V>> {
293
    private final Expiry<K, V> expiry;
294

UNCOV
295
    ExpiryAdapter(Expiry<K, V> expiry) {
×
UNCOV
296
      this.expiry = requireNonNull(expiry);
×
UNCOV
297
    }
×
298
    @Override public long expireAfterCreate(K key, Expirable<V> expirable, long currentTime) {
UNCOV
299
      return expiry.expireAfterCreate(key, expirable.get(), currentTime);
×
300
    }
301
    @Override public long expireAfterUpdate(K key, Expirable<V> expirable,
302
        long currentTime, long currentDuration) {
UNCOV
303
      return expiry.expireAfterUpdate(key, expirable.get(), currentTime, currentDuration);
×
304
    }
305
    @Override public long expireAfterRead(K key, Expirable<V> expirable,
306
        long currentTime, long currentDuration) {
UNCOV
307
      return expiry.expireAfterRead(key, expirable.get(), currentTime, currentDuration);
×
308
    }
309
  }
310

311
  private static final class ExpirableToExpiry<K, V> implements Expiry<K, Expirable<V>> {
312
    private final Ticker ticker;
313

UNCOV
314
    ExpirableToExpiry(Ticker ticker) {
×
UNCOV
315
      this.ticker = requireNonNull(ticker);
×
UNCOV
316
    }
×
317
    @Override public long expireAfterCreate(K key, Expirable<V> expirable, long currentTime) {
UNCOV
318
      return toNanos(expirable);
×
319
    }
320
    @Override public long expireAfterUpdate(K key, Expirable<V> expirable,
321
        long currentTime, long currentDuration) {
UNCOV
322
      return toNanos(expirable);
×
323
    }
324
    @Override public long expireAfterRead(K key, Expirable<V> expirable,
325
        long currentTime, long currentDuration) {
UNCOV
326
      return toNanos(expirable);
×
327
    }
328
    private long toNanos(Expirable<V> expirable) {
UNCOV
329
      if (expirable.getExpireTimeMillis() == 0L) {
×
UNCOV
330
        return -1L;
×
UNCOV
331
      } else if (expirable.isEternal()) {
×
UNCOV
332
        return Long.MAX_VALUE;
×
333
      }
UNCOV
334
      return TimeUnit.MILLISECONDS.toNanos(expirable.getExpireTimeMillis()) - ticker.read();
×
335
    }
336
  }
337
}
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