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

ben-manes / caffeine / #5577

04 Jul 2026 05:08AM UTC coverage: 99.904% (-0.08%) from 99.988%
#5577

push

github

ben-manes
Publish UPDATED for jcache refresh reloads

The loader adapter had no reload override, so refreshAfterWrite routed
through load(): a refresh of an existing entry published CREATED (never
UPDATED), stamped it with the creation expiry rather than the update
expiry, and — since load runs on the executor thread with no
awaitSynchronous() drain — leaked each synchronous listener's future
into that thread's pending list. Override reload to publish UPDATED
quietly with getExpiryForUpdate; quiet keeps the future off the pending
list, and a background refresh can't meaningfully block a synchronous
listener anyway.

4076 of 4090 branches covered (99.66%)

12 of 19 new or added lines in 2 files covered. (63.16%)

8312 of 8320 relevant lines covered (99.9%)

1.0 hits per line

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

91.03
/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>> {
49
  private static final Logger logger = System.getLogger(JCacheLoaderAdapter.class.getName());
1✔
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,
60
      ExpiryPolicy expiry, Ticker ticker, JCacheStatisticsMXBean statistics) {
1✔
61
    this.dispatcher = requireNonNull(dispatcher);
1✔
62
    this.statistics = requireNonNull(statistics);
1✔
63
    this.delegate = requireNonNull(delegate);
1✔
64
    this.expiry = requireNonNull(expiry);
1✔
65
    this.ticker = requireNonNull(ticker);
1✔
66
  }
1✔
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) {
74
    this.cache = requireNonNull(cache);
1✔
75
  }
1✔
76

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

84
      V value = delegate.load(key);
1✔
85
      @Var Expirable<V> expirable = null;
1✔
86
      if (value != null) {
1✔
87
        requireNonNull(cache);
1✔
88
        long expireTime = expireTimeMillis(expiry::getExpiryForCreation);
1✔
89
        if (expireTime == 0L) {
1✔
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.
93
          dispatcher.publishExpired(cache, key, value);
1✔
94
        } else {
95
          expirable = new Expirable<>(value, expireTime);
1✔
96
          dispatcher.publishCreated(cache, key, value);
1✔
97
        }
98
      }
99

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

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

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

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

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

174
  private long expireTimeMillis(Supplier<Duration> durationSupplier) {
175
    try {
176
      Duration duration = durationSupplier.get();
1✔
177
      if (duration.isZero()) {
1✔
178
        return 0;
1✔
179
      } else if (duration.isEternal()) {
1✔
180
        return Long.MAX_VALUE;
1✔
181
      }
182
      long millis = TimeUnit.NANOSECONDS.toMillis(ticker.read());
1✔
183
      long expireTime = duration.getAdjustedTime(millis);
1✔
184
      return ((expireTime == 0L) || (expireTime == Long.MAX_VALUE)) ? (expireTime - 1) : expireTime;
1!
185
    } catch (RuntimeException e) {
1✔
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.
189
      logger.log(Level.WARNING, "Exception thrown by expiry policy", e);
1✔
190
      return Long.MAX_VALUE;
1✔
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