• 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

80.0
/caffeine/src/main/java/com/github/benmanes/caffeine/cache/SnapshotEntry.java
1
/*
2
 * Copyright 2022 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 java.util.Objects.requireNonNull;
19

20
import java.util.Map;
21

22
import org.jspecify.annotations.Nullable;
23

24
import com.github.benmanes.caffeine.cache.Policy.CacheEntry;
25
import com.google.errorprone.annotations.Immutable;
26

27
/**
28
 * An immutable entry that includes a snapshot of the policy metadata at its time of creation.
29
 *
30
 * @author ben.manes@gmail.com (Ben Manes)
31
 */
32
@Immutable(containerOf = {"K", "V"})
33
class SnapshotEntry<K, V> implements CacheEntry<K, V> {
34
  private final long snapshot;
35
  private final V value;
36
  private final K key;
37

38
  SnapshotEntry(K key, V value, long snapshot) {
1✔
39
    this.snapshot = snapshot;
1✔
40
    this.key = requireNonNull(key);
1✔
41
    this.value = requireNonNull(value);
1✔
42
  }
1✔
43
  @Override public final K getKey() {
44
    return key;
1✔
45
  }
46
  @Override public final V getValue() {
47
    return value;
1✔
48
  }
49
  @Override public V setValue(V value) {
50
    throw new UnsupportedOperationException();
1✔
51
  }
52
  @Override public int weight() {
53
    return 1;
1✔
54
  }
55
  @Override public long expiresAt() {
56
    return snapshot + Long.MAX_VALUE;
1✔
57
  }
58
  @Override public long refreshableAt() {
59
    return snapshot + Long.MAX_VALUE;
1✔
60
  }
61
  @Override public final long snapshotAt() {
62
    return snapshot;
1✔
63
  }
64
  @Override public final boolean equals(@Nullable Object o) {
UNCOV
65
    if (o == this) {
×
UNCOV
66
      return true;
×
UNCOV
67
    } else if (!(o instanceof Map.Entry)) {
×
UNCOV
68
      return false;
×
69
    }
UNCOV
70
    var entry = (Map.Entry<?, ?>) o;
×
UNCOV
71
    return key.equals(entry.getKey()) && value.equals(entry.getValue());
×
72
  }
73
  @Override public final int hashCode() {
UNCOV
74
    return key.hashCode() ^ value.hashCode();
×
75
  }
76
  @Override public final String toString() {
UNCOV
77
    return key + "=" + value;
×
78
  }
79

80
  /** Returns a cache entry containing the given key, value, and snapshot. */
81
  public static <K, V> SnapshotEntry<K, V> forEntry(K key, V value) {
UNCOV
82
    return new SnapshotEntry<>(key, value, /* snapshot= */ 0);
×
83
  }
84

85
  /** Returns a cache entry with the specified metadata. */
86
  @SuppressWarnings("StatementSwitchToExpressionSwitch")
87
  public static <K, V> SnapshotEntry<K, V> forEntry(K key, V value,
88
      long snapshot, int weight, long expiresAt, long refreshableAt) {
89
    long unsetTicks = snapshot + Long.MAX_VALUE;
1✔
90
    boolean refresh = (refreshableAt != unsetTicks);
1✔
91
    boolean expires = (expiresAt != unsetTicks);
1✔
92
    boolean weights = (weight != 1);
1✔
93
    int features = // truth table
94
          (weights ? 0b001 : 0b000)
1✔
95
        | (expires ? 0b010 : 0b000)
1✔
96
        | (refresh ? 0b100 : 0b000);
1✔
97
    switch (features) { // optimized for common cases
1✔
98
      case 0b000: return new SnapshotEntry<>(key, value, snapshot);
1✔
99
      case 0b001: return new WeightedEntry<>(key, value, snapshot, weight);
1✔
100
      case 0b010: return new ExpirableEntry<>(key, value, snapshot, expiresAt);
1✔
101
      case 0b011: return new ExpirableWeightedEntry<>(key, value, snapshot, weight, expiresAt);
1✔
102
      case 0b110: return new RefreshableExpirableEntry<>(
1✔
103
          key, value, snapshot, expiresAt, refreshableAt);
104
      default: return new CompleteEntry<>(key, value, snapshot, weight, expiresAt, refreshableAt);
1✔
105
    }
106
  }
107

108
  static class WeightedEntry<K, V> extends SnapshotEntry<K, V> {
109
    final int weight;
110

111
    WeightedEntry(K key, V value, long snapshot, int weight) {
112
      super(key, value, snapshot);
1✔
113
      this.weight = weight;
1✔
114
    }
1✔
115
    @Override public final int weight() {
116
      return weight;
1✔
117
    }
118
  }
119

120
  static class ExpirableEntry<K, V> extends SnapshotEntry<K, V> {
121
    final long expiresAt;
122

123
    ExpirableEntry(K key, V value, long snapshot, long expiresAt) {
124
      super(key, value, snapshot);
1✔
125
      this.expiresAt = expiresAt;
1✔
126
    }
1✔
127
    @Override public final long expiresAt() {
UNCOV
128
      return expiresAt;
×
129
    }
130
  }
131

132
  static class ExpirableWeightedEntry<K, V> extends WeightedEntry<K, V> {
133
    final long expiresAt;
134

135
    ExpirableWeightedEntry(K key, V value, long snapshot, int weight, long expiresAt) {
136
      super(key, value, snapshot, weight);
1✔
137
      this.expiresAt = expiresAt;
1✔
138
    }
1✔
139
    @Override public final long expiresAt() {
140
      return expiresAt;
1✔
141
    }
142
  }
143

144
  static class RefreshableExpirableEntry<K, V> extends ExpirableEntry<K, V> {
145
    final long refreshableAt;
146

147
    RefreshableExpirableEntry(K key, V value, long snapshot, long expiresAt, long refreshableAt) {
148
      super(key, value, snapshot, expiresAt);
1✔
149
      this.refreshableAt = refreshableAt;
1✔
150
    }
1✔
151
    @Override public final long refreshableAt() {
UNCOV
152
      return refreshableAt;
×
153
    }
154
  }
155

156
  static final class CompleteEntry<K, V> extends ExpirableWeightedEntry<K, V> {
157
    final long refreshableAt;
158

159
    CompleteEntry(K key, V value, long snapshot, int weight, long expiresAt, long refreshableAt) {
160
      super(key, value, snapshot, weight, expiresAt);
1✔
161
      this.refreshableAt = refreshableAt;
1✔
162
    }
1✔
163
    @Override public long refreshableAt() {
164
      return refreshableAt;
1✔
165
    }
166
  }
167
}
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