• 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/integration/JCacheLoaderAdapter.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.integration;
17

18
import static java.util.Objects.requireNonNull;
19

20
import java.lang.System.Logger;
21
import java.lang.System.Logger.Level;
22
import java.util.HashMap;
23
import java.util.Map;
24
import java.util.Set;
25
import java.util.concurrent.TimeUnit;
26
import java.util.function.Supplier;
27

28
import javax.cache.expiry.Duration;
29
import javax.cache.expiry.ExpiryPolicy;
30
import javax.cache.integration.CacheLoader;
31
import javax.cache.integration.CacheLoaderException;
32

33
import org.jspecify.annotations.Nullable;
34

35
import com.github.benmanes.caffeine.cache.Ticker;
36
import com.github.benmanes.caffeine.jcache.CacheProxy;
37
import com.github.benmanes.caffeine.jcache.Expirable;
38
import com.github.benmanes.caffeine.jcache.event.EventDispatcher;
39
import com.github.benmanes.caffeine.jcache.management.JCacheStatisticsMXBean;
40
import com.google.errorprone.annotations.Var;
41

42
/**
43
 * An adapter from a JCache cache loader to Caffeine's.
44
 *
45
 * @author ben.manes@gmail.com (Ben Manes)
46
 */
47
public final class JCacheLoaderAdapter<K, V>
48
    implements com.github.benmanes.caffeine.cache.CacheLoader<K, @Nullable Expirable<V>> {
UNCOV
49
  private static final Logger logger = System.getLogger(JCacheLoaderAdapter.class.getName());
×
50

51
  private final JCacheStatisticsMXBean statistics;
52
  private final EventDispatcher<K, V> dispatcher;
53
  private final CacheLoader<K, V> delegate;
54
  private final ExpiryPolicy expiry;
55
  private final Ticker ticker;
56

57
  private @Nullable CacheProxy<K, V> cache;
58

59
  public JCacheLoaderAdapter(CacheLoader<K, V> delegate, EventDispatcher<K, V> dispatcher,
UNCOV
60
      ExpiryPolicy expiry, Ticker ticker, JCacheStatisticsMXBean statistics) {
×
UNCOV
61
    this.dispatcher = requireNonNull(dispatcher);
×
UNCOV
62
    this.statistics = requireNonNull(statistics);
×
UNCOV
63
    this.delegate = requireNonNull(delegate);
×
UNCOV
64
    this.expiry = requireNonNull(expiry);
×
UNCOV
65
    this.ticker = requireNonNull(ticker);
×
UNCOV
66
  }
×
67

68
  /**
69
   * Sets the cache instance that was created with this loader.
70
   *
71
   * @param cache the cache that uses this loader
72
   */
73
  public void setCache(CacheProxy<K, V> cache) {
UNCOV
74
    this.cache = requireNonNull(cache);
×
UNCOV
75
  }
×
76

77
  @Override
78
  @SuppressWarnings("ConstantValue")
79
  public @Nullable Expirable<V> load(K key) {
80
    try {
UNCOV
81
      boolean statsEnabled = statistics.isEnabled();
×
UNCOV
82
      long start = statsEnabled ? ticker.read() : 0L;
×
83

UNCOV
84
      V value = delegate.load(key);
×
UNCOV
85
      @Var Expirable<V> expirable = null;
×
UNCOV
86
      if (value != null) {
×
UNCOV
87
        requireNonNull(cache);
×
UNCOV
88
        long expireTime = expireTimeMillis(expiry::getExpiryForCreation);
×
UNCOV
89
        if (expireTime == 0L) {
×
90
          // Per JSR-107 1.1.1 p.55: ZERO duration → entry is already expired
91
          // and will not be added to the Cache. Match the put-path convention
92
          // of publishing EXPIRED in place of CREATED.
UNCOV
93
          dispatcher.publishExpired(cache, key, value);
×
94
        } else {
UNCOV
95
          expirable = new Expirable<>(value, expireTime);
×
UNCOV
96
          dispatcher.publishCreated(cache, key, value);
×
97
        }
98
      }
99

UNCOV
100
      if (statsEnabled) {
×
101
        // Subtracts the load time from the get time
UNCOV
102
        statistics.recordGetTime(start - ticker.read());
×
103
      }
UNCOV
104
      return expirable;
×
UNCOV
105
    } catch (CacheLoaderException e) {
×
UNCOV
106
      throw e;
×
UNCOV
107
    } catch (RuntimeException e) {
×
UNCOV
108
      throw new CacheLoaderException(e);
×
109
    }
110
  }
111

112
  @Override
113
  public Map<K, Expirable<V>> loadAll(Set<? extends K> keys) {
114
    try {
UNCOV
115
      boolean statsEnabled = statistics.isEnabled();
×
UNCOV
116
      long start = statsEnabled ? ticker.read() : 0L;
×
UNCOV
117
      requireNonNull(cache);
×
118

119
      @SuppressWarnings("ConstantValue")
UNCOV
120
      Map<K, V> loaded = delegate.loadAll(keys);
×
UNCOV
121
      var result = new HashMap<K, Expirable<V>>(loaded.size());
×
UNCOV
122
      for (var entry : loaded.entrySet()) {
×
UNCOV
123
        K key = entry.getKey();
×
UNCOV
124
        V value = entry.getValue();
×
UNCOV
125
        if ((key == null) || (value == null)) {
×
UNCOV
126
          continue;
×
127
        }
UNCOV
128
        long expireTime = expireTimeMillis(expiry::getExpiryForCreation);
×
UNCOV
129
        if (expireTime == 0L) {
×
130
          // ZERO → already expired and not added; match the put-path convention.
UNCOV
131
          dispatcher.publishExpired(cache, key, value);
×
132
        } else {
UNCOV
133
          result.put(key, new Expirable<>(value, expireTime));
×
134
        }
UNCOV
135
      }
×
UNCOV
136
      for (var entry : result.entrySet()) {
×
UNCOV
137
        dispatcher.publishCreated(cache, entry.getKey(), entry.getValue().get());
×
UNCOV
138
      }
×
139

UNCOV
140
      if (statsEnabled) {
×
141
        // Subtracts the load time from the get time
UNCOV
142
        statistics.recordGetTime(start - ticker.read());
×
143
      }
UNCOV
144
      return result;
×
UNCOV
145
    } catch (CacheLoaderException e) {
×
UNCOV
146
      throw e;
×
UNCOV
147
    } catch (RuntimeException e) {
×
UNCOV
148
      throw new CacheLoaderException(e);
×
149
    }
150
  }
151

152
  @Override
153
  public @Nullable Expirable<V> reload(K key, Expirable<V> oldValue) {
154
    try {
UNCOV
155
      V value = delegate.load(key);
×
UNCOV
156
      if (value == null) {
×
UNCOV
157
        return null;
×
158
      }
UNCOV
159
      requireNonNull(cache);
×
UNCOV
160
      long expireTime = expireTimeMillis(expiry::getExpiryForUpdate);
×
UNCOV
161
      if (expireTime == 0L) {
×
UNCOV
162
        dispatcher.publishExpiredQuietly(cache, key, value);
×
UNCOV
163
        return null;
×
164
      }
UNCOV
165
      dispatcher.publishUpdatedQuietly(cache, key, oldValue.get(), value);
×
UNCOV
166
      return new Expirable<>(value, expireTime);
×
UNCOV
167
    } catch (CacheLoaderException e) {
×
UNCOV
168
      throw e;
×
UNCOV
169
    } catch (RuntimeException e) {
×
UNCOV
170
      throw new CacheLoaderException(e);
×
171
    }
172
  }
173

174
  private long expireTimeMillis(Supplier<Duration> durationSupplier) {
175
    try {
UNCOV
176
      Duration duration = durationSupplier.get();
×
UNCOV
177
      if (duration.isZero()) {
×
UNCOV
178
        return 0;
×
UNCOV
179
      } else if (duration.isEternal()) {
×
UNCOV
180
        return Long.MAX_VALUE;
×
181
      }
UNCOV
182
      long millis = TimeUnit.NANOSECONDS.toMillis(ticker.read());
×
UNCOV
183
      long expireTime = duration.getAdjustedTime(millis);
×
UNCOV
184
      return ((expireTime == 0L) || (expireTime == Long.MAX_VALUE)) ? (expireTime - 1) : expireTime;
×
UNCOV
185
    } catch (RuntimeException e) {
×
186
      // Per JSR-107 1.1.1 p.55: if the expiry policy throws, an implementation
187
      // specific default Duration will be used. We treat as eternal so the
188
      // loaded entry is not lost.
UNCOV
189
      logger.log(Level.WARNING, "Exception thrown by expiry policy", e);
×
UNCOV
190
      return Long.MAX_VALUE;
×
191
    }
192
  }
193
}
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