• 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/configuration/TypesafeConfigurator.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.configuration;
17

18
import static java.util.Objects.requireNonNull;
19
import static java.util.concurrent.TimeUnit.MILLISECONDS;
20
import static java.util.concurrent.TimeUnit.NANOSECONDS;
21

22
import java.io.File;
23
import java.lang.System.Logger;
24
import java.lang.System.Logger.Level;
25
import java.net.MalformedURLException;
26
import java.net.URI;
27
import java.util.Collections;
28
import java.util.Objects;
29
import java.util.Optional;
30
import java.util.OptionalLong;
31
import java.util.Set;
32
import java.util.concurrent.atomic.AtomicReference;
33
import java.util.function.Supplier;
34

35
import javax.cache.CacheManager;
36
import javax.cache.configuration.Factory;
37
import javax.cache.configuration.FactoryBuilder;
38
import javax.cache.configuration.MutableCacheEntryListenerConfiguration;
39
import javax.cache.event.CacheEntryEventFilter;
40
import javax.cache.event.CacheEntryListener;
41
import javax.cache.expiry.Duration;
42
import javax.cache.expiry.EternalExpiryPolicy;
43
import javax.cache.expiry.ExpiryPolicy;
44

45
import org.jspecify.annotations.NullMarked;
46
import org.jspecify.annotations.Nullable;
47

48
import com.github.benmanes.caffeine.jcache.expiry.JCacheExpiryPolicy;
49
import com.google.errorprone.annotations.Var;
50
import com.typesafe.config.Config;
51
import com.typesafe.config.ConfigException;
52
import com.typesafe.config.ConfigFactory;
53
import com.typesafe.config.ConfigParseOptions;
54
import com.typesafe.config.ConfigSyntax;
55

56
import jakarta.inject.Inject;
57

58
/**
59
 * Static utility methods pertaining to externalized {@link CaffeineConfiguration} entries using the
60
 * Typesafe Config library.
61
 *
62
 * @author ben.manes@gmail.com (Ben Manes)
63
 */
64
@NullMarked
65
public final class TypesafeConfigurator {
UNCOV
66
  static final Logger logger = System.getLogger(TypesafeConfigurator.class.getName());
×
67

UNCOV
68
  static final AtomicReference<ConfigSource> configSource =
×
69
      new AtomicReference<>(TypesafeConfigurator::resolveConfig);
UNCOV
70
  static final AtomicReference<FactoryCreator> factoryCreator =
×
71
      new AtomicReference<>(FactoryBuilder::factoryOf);
72

73
  private TypesafeConfigurator() {}
74

75
  /**
76
   * Retrieves the names of the caches defined in the configuration resource.
77
   *
78
   * @param config the configuration resource
79
   * @return the names of the configured caches
80
   */
81
  public static Set<String> cacheNames(Config config) {
UNCOV
82
    return config.hasPath("caffeine.jcache")
×
UNCOV
83
        ? Collections.unmodifiableSet(config.getObject("caffeine.jcache").keySet())
×
UNCOV
84
        : Collections.emptySet();
×
85
  }
86

87
  /**
88
   * Retrieves the default cache settings from the configuration resource.
89
   *
90
   * @param config the configuration resource
91
   * @param <K> the type of keys maintained the cache
92
   * @param <V> the type of cached values
93
   * @return the default configuration for a cache
94
   */
95
  public static <K, V> CaffeineConfiguration<K, V> defaults(Config config) {
UNCOV
96
    return new Configurator<K, V>(config, "default").configure();
×
97
  }
98

99
  /**
100
   * Retrieves the cache's settings from the configuration resource if defined.
101
   *
102
   * @param config the configuration resource
103
   * @param cacheName the name of the cache
104
   * @param <K> the type of keys maintained the cache
105
   * @param <V> the type of cached values
106
   * @return the configuration for the cache
107
   */
108
  public static <K, V> Optional<CaffeineConfiguration<K, V>> from(Config config, String cacheName) {
109
    try {
UNCOV
110
      requireNonNull(cacheName);
×
UNCOV
111
      return config.hasPath("caffeine.jcache." + cacheName)
×
UNCOV
112
          ? Optional.of(new Configurator<K, V>(config, cacheName).configure())
×
UNCOV
113
          : Optional.empty();
×
UNCOV
114
    } catch (ConfigException.BadPath e) {
×
UNCOV
115
      logger.log(Level.WARNING, "Failed to load cache configuration", e);
×
UNCOV
116
      return Optional.empty();
×
117
    }
118
  }
119

120
  /**
121
   * Specifies how {@link Factory} instances are created for a given class name. The default
122
   * strategy uses {@link Class#newInstance()} and requires the class has a no-args constructor.
123
   *
124
   * @param factoryCreator the strategy for creating a factory
125
   */
126
  @Inject
127
  @SuppressWarnings({"deprecation", "UnnecessarilyVisible"})
128
  public static void setFactoryCreator(FactoryCreator factoryCreator) {
UNCOV
129
    TypesafeConfigurator.factoryCreator.set(requireNonNull(factoryCreator));
×
UNCOV
130
  }
×
131

132
  /** Returns the strategy for how factory instances are created. */
133
  public static FactoryCreator factoryCreator() {
UNCOV
134
    return requireNonNull(factoryCreator.get());
×
135
  }
136

137
  /**
138
   * Specifies how the {@link Config} instance should be loaded. The default strategy uses the uri
139
   * provided by {@link CacheManager#getURI()} as an optional override location to parse from a
140
   * file system or classpath resource, or else returns {@link ConfigFactory#load(ClassLoader)}.
141
   * The configuration is retrieved on-demand, allowing for it to be reloaded, and it is assumed
142
   * that the source caches it as needed.
143
   *
144
   * @param configSource the strategy for loading the configuration
145
   */
146
  public static void setConfigSource(Supplier<Config> configSource) {
UNCOV
147
    requireNonNull(configSource);
×
UNCOV
148
    setConfigSource((uri, classloader) -> configSource.get());
×
UNCOV
149
  }
×
150

151
  /**
152
   * Specifies how the {@link Config} instance should be loaded. The default strategy uses the uri
153
   * provided by {@link CacheManager#getURI()} as an optional override location to parse from a
154
   * file system or classpath resource, or else returns {@link ConfigFactory#load(ClassLoader)}.
155
   * The configuration is retrieved on-demand, allowing for it to be reloaded, and it is assumed
156
   * that the source caches it as needed.
157
   *
158
   * @param configSource the strategy for loading the configuration from a URI
159
   */
160
  public static void setConfigSource(ConfigSource configSource) {
UNCOV
161
    TypesafeConfigurator.configSource.set(requireNonNull(configSource));
×
UNCOV
162
  }
×
163

164
  /** Returns the strategy for loading the configuration. */
165
  public static ConfigSource configSource() {
UNCOV
166
    return requireNonNull(configSource.get());
×
167
  }
168

169
  /** Returns the configuration by applying the default strategy. */
170
  private static Config resolveConfig(URI uri, ClassLoader classloader) {
UNCOV
171
    requireNonNull(uri);
×
UNCOV
172
    requireNonNull(classloader);
×
UNCOV
173
    var options = ConfigParseOptions.defaults()
×
UNCOV
174
        .setClassLoader(classloader)
×
UNCOV
175
        .setAllowMissing(false);
×
UNCOV
176
    if ((uri.getScheme() != null) && uri.getScheme().equalsIgnoreCase("file")) {
×
UNCOV
177
      return ConfigFactory.defaultOverrides(classloader)
×
UNCOV
178
          .withFallback(ConfigFactory.parseFile(new File(uri), options))
×
UNCOV
179
          .withFallback(ConfigFactory.defaultReferenceUnresolved(classloader))
×
UNCOV
180
          .resolve();
×
UNCOV
181
    } else if ((uri.getScheme() != null) && uri.getScheme().equalsIgnoreCase("jar")) {
×
182
      try {
UNCOV
183
        return ConfigFactory.defaultOverrides(classloader)
×
UNCOV
184
            .withFallback(ConfigFactory.parseURL(uri.toURL(), options))
×
UNCOV
185
            .withFallback(ConfigFactory.defaultReferenceUnresolved(classloader))
×
UNCOV
186
            .resolve();
×
UNCOV
187
      } catch (MalformedURLException e) {
×
UNCOV
188
        throw new ConfigException.BadPath(uri.toString(), "Failed to load cache configuration", e);
×
189
      }
UNCOV
190
    } else if (isResource(uri)) {
×
UNCOV
191
      return ConfigFactory.defaultOverrides(classloader)
×
UNCOV
192
          .withFallback(ConfigFactory.parseResources(uri.getSchemeSpecificPart(), options))
×
UNCOV
193
          .withFallback(ConfigFactory.defaultReferenceUnresolved(classloader))
×
UNCOV
194
          .resolve();
×
195
    }
UNCOV
196
    return ConfigFactory.load(classloader);
×
197
  }
198

199
  /** Returns if the uri is a file or classpath resource. */
200
  private static boolean isResource(URI uri) {
UNCOV
201
    if ((uri.getScheme() != null) && !uri.getScheme().equalsIgnoreCase("classpath")) {
×
UNCOV
202
      return false;
×
203
    }
UNCOV
204
    var path = uri.getSchemeSpecificPart();
×
UNCOV
205
    int dotIndex = path.lastIndexOf('.');
×
UNCOV
206
    if (dotIndex != -1) {
×
UNCOV
207
      var extension = path.substring(dotIndex + 1);
×
UNCOV
208
      for (var format : ConfigSyntax.values()) {
×
UNCOV
209
        if (format.toString().equalsIgnoreCase(extension)) {
×
UNCOV
210
          return true;
×
211
        }
212
      }
213
    }
UNCOV
214
    return false;
×
215
  }
216

217
  /** A one-shot builder for creating a configuration instance. */
218
  static final class Configurator<K, V> {
219
    final CaffeineConfiguration<K, V> configuration;
220
    final Config customized;
221
    final Config merged;
222
    final Config root;
223

UNCOV
224
    Configurator(Config config, String cacheName) {
×
UNCOV
225
      this.root = requireNonNull(config);
×
UNCOV
226
      this.configuration = new CaffeineConfiguration<>();
×
UNCOV
227
      this.customized = root.getConfig("caffeine.jcache." + requireNonNull(cacheName));
×
UNCOV
228
      this.merged = customized.withFallback(root.getConfig("caffeine.jcache.default"));
×
UNCOV
229
    }
×
230

231
    /** Returns a configuration built from the external settings. */
232
    CaffeineConfiguration<K, V> configure() {
UNCOV
233
      addKeyValueTypes();
×
UNCOV
234
      addStoreByValue();
×
UNCOV
235
      addExecutor();
×
UNCOV
236
      addScheduler();
×
UNCOV
237
      addListeners();
×
UNCOV
238
      addReadThrough();
×
UNCOV
239
      addWriteThrough();
×
UNCOV
240
      addMonitoring();
×
UNCOV
241
      addLazyExpiration();
×
UNCOV
242
      addEagerExpiration();
×
UNCOV
243
      addRefresh();
×
UNCOV
244
      addMaximum();
×
245

UNCOV
246
      return configuration;
×
247
    }
248

249
    /** Adds the key and value class types. */
250
    private void addKeyValueTypes() {
251
      try {
252
        @SuppressWarnings("unchecked")
UNCOV
253
        var keyType = (Class<K>) Class.forName(merged.getString("key-type"));
×
254
        @SuppressWarnings("unchecked")
UNCOV
255
        var valueType = (Class<V>) Class.forName(merged.getString("value-type"));
×
UNCOV
256
        configuration.setTypes(keyType, valueType);
×
UNCOV
257
      } catch (ClassNotFoundException e) {
×
UNCOV
258
        throw new IllegalStateException(e);
×
UNCOV
259
      }
×
UNCOV
260
    }
×
261

262
    /** Adds the store-by-value settings. */
263
    private void addStoreByValue() {
UNCOV
264
      configuration.setStoreByValue(merged.getBoolean("store-by-value.enabled"));
×
UNCOV
265
      if (isSet("store-by-value.strategy")) {
×
UNCOV
266
        configuration.setCopierFactory(factoryCreator().factoryOf(
×
UNCOV
267
            merged.getString("store-by-value.strategy")));
×
268
      }
UNCOV
269
    }
×
270

271
    /** Adds the executor settings. */
272
    public void addExecutor() {
UNCOV
273
      if (isSet("executor")) {
×
UNCOV
274
        configuration.setExecutorFactory(factoryCreator()
×
UNCOV
275
            .factoryOf(merged.getString("executor")));
×
276
      }
UNCOV
277
    }
×
278

279
    /** Adds the scheduler settings. */
280
    public void addScheduler() {
UNCOV
281
      if (isSet("scheduler")) {
×
UNCOV
282
        configuration.setSchedulerFactory(factoryCreator()
×
UNCOV
283
            .factoryOf(merged.getString("scheduler")));
×
284
      }
UNCOV
285
    }
×
286

287
    /** Adds the entry listeners settings. */
288
    private void addListeners() {
UNCOV
289
      for (String path : merged.getStringList("listeners")) {
×
UNCOV
290
        Config listener = root.getConfig(path);
×
291

292
        Factory<? extends CacheEntryListener<? super K, ? super V>> listenerFactory =
UNCOV
293
            factoryCreator().factoryOf(listener.getString("class"));
×
UNCOV
294
        @Var Factory<? extends CacheEntryEventFilter<? super K, ? super V>> filterFactory = null;
×
UNCOV
295
        if (listener.hasPath("filter")) {
×
UNCOV
296
          filterFactory = factoryCreator().factoryOf(listener.getString("filter"));
×
297
        }
UNCOV
298
        boolean oldValueRequired = listener.getBoolean("old-value-required");
×
UNCOV
299
        boolean synchronous = listener.getBoolean("synchronous");
×
UNCOV
300
        configuration.addCacheEntryListenerConfiguration(
×
301
            new MutableCacheEntryListenerConfiguration<>(
302
                listenerFactory, filterFactory, oldValueRequired, synchronous));
UNCOV
303
      }
×
UNCOV
304
    }
×
305

306
    /** Adds the read through settings. */
307
    private void addReadThrough() {
UNCOV
308
      configuration.setReadThrough(merged.getBoolean("read-through.enabled"));
×
UNCOV
309
      if (isSet("read-through.loader")) {
×
UNCOV
310
        configuration.setCacheLoaderFactory(factoryCreator().factoryOf(
×
UNCOV
311
            merged.getString("read-through.loader")));
×
312
      }
UNCOV
313
    }
×
314

315
    /** Adds the write-through settings. */
316
    private void addWriteThrough() {
UNCOV
317
      configuration.setWriteThrough(merged.getBoolean("write-through.enabled"));
×
UNCOV
318
      if (isSet("write-through.writer")) {
×
UNCOV
319
        configuration.setCacheWriterFactory(factoryCreator().factoryOf(
×
UNCOV
320
            merged.getString("write-through.writer")));
×
321
      }
UNCOV
322
    }
×
323

324
    /** Adds the monitoring settings. */
325
    private void addMonitoring() {
UNCOV
326
      configuration.setNativeStatisticsEnabled(merged.getBoolean("monitoring.native-statistics"));
×
UNCOV
327
      configuration.setStatisticsEnabled(merged.getBoolean("monitoring.statistics"));
×
UNCOV
328
      configuration.setManagementEnabled(merged.getBoolean("monitoring.management"));
×
UNCOV
329
    }
×
330

331
    /** Adds the JCache specification's lazy expiration settings. */
332
    public void addLazyExpiration() {
UNCOV
333
      Duration creation = getDurationFor("policy.lazy-expiration.creation");
×
UNCOV
334
      Duration update = getDurationFor("policy.lazy-expiration.update");
×
UNCOV
335
      Duration access = getDurationFor("policy.lazy-expiration.access");
×
UNCOV
336
      requireNonNull(creation, "policy.lazy-expiration.creation may not be null");
×
337

UNCOV
338
      boolean eternal = Objects.equals(creation, Duration.ETERNAL)
×
UNCOV
339
          && Objects.equals(update, Duration.ETERNAL)
×
UNCOV
340
          && Objects.equals(access, Duration.ETERNAL);
×
UNCOV
341
      Factory<? extends ExpiryPolicy> factory = eternal
×
UNCOV
342
          ? EternalExpiryPolicy.factoryOf()
×
UNCOV
343
          : FactoryBuilder.factoryOf(new JCacheExpiryPolicy(creation, update, access));
×
UNCOV
344
      configuration.setExpiryPolicyFactory(factory);
×
UNCOV
345
    }
×
346

347
    /** Returns the duration for the expiration time. */
348
    private @Nullable Duration getDurationFor(String path) {
UNCOV
349
      if (!isSet(path)) {
×
UNCOV
350
        return null;
×
351
      }
UNCOV
352
      if (merged.getString(path).equalsIgnoreCase("eternal")) {
×
UNCOV
353
        return Duration.ETERNAL;
×
354
      }
UNCOV
355
      long millis = merged.getDuration(path, MILLISECONDS);
×
UNCOV
356
      return new Duration(MILLISECONDS, millis);
×
357
    }
358

359
    /** Adds the Caffeine eager expiration settings. */
360
    public void addEagerExpiration() {
UNCOV
361
      if (isSet("policy.eager-expiration.after-write")) {
×
UNCOV
362
        long nanos = merged.getDuration("policy.eager-expiration.after-write", NANOSECONDS);
×
UNCOV
363
        configuration.setExpireAfterWrite(OptionalLong.of(nanos));
×
364
      }
UNCOV
365
      if (isSet("policy.eager-expiration.after-access")) {
×
UNCOV
366
        long nanos = merged.getDuration("policy.eager-expiration.after-access", NANOSECONDS);
×
UNCOV
367
        configuration.setExpireAfterAccess(OptionalLong.of(nanos));
×
368
      }
UNCOV
369
      if (isSet("policy.eager-expiration.variable")) {
×
UNCOV
370
        configuration.setExpiryFactory(Optional.of(factoryCreator().factoryOf(
×
UNCOV
371
            merged.getString("policy.eager-expiration.variable"))));
×
372
      }
UNCOV
373
    }
×
374

375
    /** Adds the Caffeine refresh settings. */
376
    public void addRefresh() {
UNCOV
377
      if (isSet("policy.refresh.after-write")) {
×
UNCOV
378
        long nanos = merged.getDuration("policy.refresh.after-write", NANOSECONDS);
×
UNCOV
379
        configuration.setRefreshAfterWrite(OptionalLong.of(nanos));
×
380
      }
UNCOV
381
    }
×
382

383
    /** Adds the maximum size and weight bounding settings. */
384
    private void addMaximum() {
UNCOV
385
      if (isSet("policy.maximum.size")) {
×
UNCOV
386
        configuration.setMaximumSize(OptionalLong.of(merged.getLong("policy.maximum.size")));
×
387
      }
UNCOV
388
      if (isSet("policy.maximum.weight")) {
×
UNCOV
389
        configuration.setMaximumWeight(OptionalLong.of(merged.getLong("policy.maximum.weight")));
×
390
      }
UNCOV
391
      if (isSet("policy.maximum.weigher")) {
×
UNCOV
392
        configuration.setWeigherFactory(Optional.of(
×
UNCOV
393
            factoryCreator().factoryOf(merged.getString("policy.maximum.weigher"))));
×
394
      }
UNCOV
395
    }
×
396

397
    /** Returns if the value is present (not unset by the cache configuration). */
398
    private boolean isSet(String path) {
UNCOV
399
      if (!merged.hasPath(path)) {
×
UNCOV
400
        return false;
×
UNCOV
401
      } else if (customized.hasPathOrNull(path)) {
×
UNCOV
402
        return !customized.getIsNull(path);
×
403
      }
UNCOV
404
      return true;
×
405
    }
406
  }
407
}
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