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

Hyshmily / hotkey / 28637103998

03 Jul 2026 03:48AM UTC coverage: 90.399% (-0.2%) from 90.63%
28637103998

push

github

Hyshmily
fix: add packaging to gitignore, fix flaky jitter test, update docs references

Signed-off-by: Hyshmily <cxm8607@outlook.com>

1264 of 1461 branches covered (86.52%)

Branch coverage included in aggregate %.

3604 of 3924 relevant lines covered (91.85%)

4.19 hits per line

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

95.77
/common/src/main/java/io/github/hyshmily/hotkey/util/version/VersionGuard.java
1
/*
2
 * Copyright 2026 Hyshmily. 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 io.github.hyshmily.hotkey.util.version;
17

18
import com.github.benmanes.caffeine.cache.Cache;
19
import io.github.hyshmily.hotkey.Internal;
20
import io.github.hyshmily.hotkey.model.CacheEntry;
21
import io.github.hyshmily.hotkey.sync.local.CacheSyncListener;
22
import io.github.hyshmily.hotkey.sync.worker.WorkerListener;
23
import java.util.Objects;
24

25
/**
26
 * Shared version comparison logic for broadcast message guards used by both
27
 * the Worker decision listener and the instance-to-instance cache sync listener.
28
 *
29
 * <p>This class provides two families of guard methods, each with a fast-path
30
 * variant (accepting a {@link Cache} reference) and an entry-level variant
31
 * (accepting an existing {@link CacheEntry}) for use inside atomic
32
 * {@code compute} blocks:
33
 *
34
 * <ul>
35
 *   <li><b>{@link #shouldSkipForWorker}</b> — Compares {@code decisionVersion}
36
 *       from Worker HOT/COOL broadcasts. Degraded entries (created during a
37
 *       Redis outage) unconditionally accept incoming decisions, providing a
38
 *       safety net against Worker restarts that reset the {@code AtomicLong}
39
 *       counter (see ADR-0008, ADR-0009).</li>
40
 *   <li><b>{@link #shouldSkipForSync}</b> — Compares {@code dataVersion}
41
 *       from application-level data-mutation broadcasts. Uses a 4-case degraded
42
 *       comparison matrix:
43
 *       <ol>
44
 *         <li>Both normal: skip if existing {@code >=} incoming</li>
45
 *         <li>Existing normal, incoming degraded: always skip (normal wins)</li>
46
 *         <li>Both degraded: skip if existing {@code >=} incoming</li>
47
 *         <li>Existing degraded, incoming normal: never skip (normal overwrites degraded)</li>
48
 *       </ol>
49
 *   </li>
50
 * </ul>
51
 *
52
 * <p>All methods are stateless and thread-safe. Instances of this utility class
53
 * must never be created.
54
 *
55
 * @see WorkerListener
56
 * @see CacheSyncListener
57
 */
58
@Internal
59
public final class VersionGuard {
60

61
  /**
62
   * Utility class — prevent instantiation.
63
   */
64
  private VersionGuard() {}
65

66
  /**
67
   * WorkerListener guard with epoch and node-id awareness.
68
   * <p>Decision logic:
69
   * <ol>
70
   *   <li>No existing entry → accept (return false)</li>
71
   *   <li>Existing entry is degraded → accept unconditionally (safety net)</li>
72
   *   <li>Incoming epoch &gt; existing epoch → accept unconditionally
73
   *       (Worker restart detected — ADR-0010)</li>
74
   *   <li>Incoming epoch &lt; existing epoch → skip (stale incarnation message)</li>
75
   *   <li>Same epoch, same nodeId → normal ordering (skip if existing dv &gt;= incoming dv)</li>
76
   *   <li>Different nodeId, same epoch → cross-Worker ownership transfer;
77
   *       accept (return false) to allow new owner to assert authority</li>
78
   * </ol>
79
   *
80
   * @param existing              the existing cache entry; may be null
81
   * @param incomingDecisionVersion the decision version from the incoming Worker message
82
   * @param incomingNodeId        the originating Worker's node ID
83
   * @param incomingEpoch         the originating Worker's epoch
84
   * @return true if the incoming message should be skipped, false if it should be applied
85
   */
86
  public static boolean shouldSkipForWorker(
87
    CacheEntry existing,
88
    long incomingDecisionVersion,
89
    String incomingNodeId,
90
    long incomingEpoch
91
  ) {
92
    if (existing == null) {
2✔
93
      return false;
2✔
94
    }
95
    if (existing.isVersionDegraded()) {
3✔
96
      return false;
2✔
97
    }
98

99
    if (incomingEpoch > existing.getDecisionEpoch()) {
5✔
100
      return false;
2✔
101
    }
102
    if (incomingEpoch < existing.getDecisionEpoch()) {
5✔
103
      return true;
2✔
104
    }
105

106
    if (Objects.equals(incomingNodeId, existing.getDecisionNodeId())) {
5✔
107
      return existing.getDecisionVersion() >= incomingDecisionVersion;
9✔
108
    }
109

110
    return false;
2✔
111
  }
112

113
  /**
114
   * WorkerListener guard for use inside an atomic {@code compute} block: the caller
115
   * already holds the existing entry reference, so no redundant {@code getIfPresent}
116
   * is needed.
117
   *
118
   * <p>Returns {@code false} (accept) for any of the following:
119
   * <ul>
120
   *   <li>No existing entry ({@code null})</li>
121
   *   <li>Existing entry is degraded ({@code isVersionDegraded == true}) —
122
   *       yields to any incoming decision, even one with a lower version,
123
   *       because the degraded entry was written during a Redis outage</li>
124
   * </ul>
125
   *
126
   * Otherwise, skips when the existing entry's {@code decisionVersion} is
127
   * {@code >=} the incoming version.
128
   *
129
   * @param existing                 the existing cache entry; may be {@code null}
130
   * @param incomingDecisionVersion  the decision version from the incoming Worker message;
131
   *                                 must be non-negative in normal operation
132
   * @return {@code true} if the incoming message should be skipped, {@code false}
133
   *         if the decision should be applied
134
   */
135
  public static boolean shouldSkipForWorker(CacheEntry existing, long incomingDecisionVersion) {
136
    if (existing == null) {
2✔
137
      return false;
2✔
138
    }
139
    if (existing.isVersionDegraded()) {
3!
140
      return false;
×
141
    }
142
    return existing.getDecisionVersion() >= incomingDecisionVersion;
8!
143
  }
144

145
  /**
146
   * WorkerListener guard with a cache-level fast path using legacy 3-argument signature.
147
   * Delegates to the 5-argument overload with {@code null} node ID and {@code 0} epoch.
148
   *
149
   * @param cache                    the local Caffeine L1 cache; must not be null
150
   * @param cacheKey                 the cache key to look up; must not be null
151
   * @param incomingDecisionVersion  the decision version from the incoming Worker message
152
   * @return {@code true} if the incoming message should be skipped (existing entry
153
   *         is already up-to-date); {@code false} if the decision may need to be applied
154
   */
155
  public static boolean shouldSkipForWorker(
156
    Cache<String, Object> cache,
157
    String cacheKey,
158
    long incomingDecisionVersion
159
  ) {
160
    return shouldSkipForWorker(cache, cacheKey, incomingDecisionVersion, null, 0);
7✔
161
  }
162

163
  /**
164
   * WorkerListener guard with a cache-level fast path: fetches the existing entry
165
   * from the L1 cache and delegates to the entry-level overload.
166
   *
167
   * <p>This variant is used <em>outside</em> atomic {@code compute} blocks as a
168
   * cheap first-pass check. If it returns {@code true} (skip), the caller can avoid
169
   * the more expensive Redis fetch entirely. A second guard inside the {@code compute}
170
   * block is still needed for correctness (DCL pattern).
171
   *
172
   * @param cache                    the local Caffeine L1 cache; must not be null
173
   * @param cacheKey                 the cache key to look up; must not be null
174
   * @param incomingDecisionVersion  the decision version from the incoming Worker message
175
   * @param incomingNodeId           the originating Worker's node ID, may be null
176
   * @param incomingEpoch            the originating Worker's epoch
177
   * @return {@code true} if the incoming message should be skipped (existing entry
178
   *         is already up-to-date); {@code false} if the decision may need to be applied
179
   */
180
  public static boolean shouldSkipForWorker(
181
    Cache<String, Object> cache,
182
    String cacheKey,
183
    long incomingDecisionVersion,
184
    String incomingNodeId,
185
    long incomingEpoch
186
  ) {
187
    Object existing = cache.getIfPresent(cacheKey);
4✔
188
    if (existing instanceof CacheEntry existingCacheEntry) {
6✔
189
      return shouldSkipForWorker(existingCacheEntry, incomingDecisionVersion, incomingNodeId, incomingEpoch);
6✔
190
    }
191
    return false;
2✔
192
  }
193

194
  /**
195
   * CacheSyncListener guard for use inside an atomic {@code compute} block: the caller
196
   * already holds the existing entry reference.
197
   *
198
   * <p>Applies the 4-case degraded comparison matrix:
199
   * <ol>
200
   *   <li>Both normal: skip if existing {@code >=} incoming</li>
201
   *   <li>Existing normal, incoming degraded: always skip (normal wins)</li>
202
   *   <li>Both degraded: skip if existing {@code >=} incoming</li>
203
   *   <li>Existing degraded, incoming normal: never skip (normal overwrites degraded)</li>
204
   * </ol>
205
   *
206
   * <p>This design ensures that a single healthy Redis-backed instance can
207
   * always overwrite degraded entries from other instances, while preventing
208
   * degraded broadcasts from reverting healthy entries.
209
   *
210
   * @param existing             the existing cache entry; may be {@code null} (returns {@code false})
211
   * @param incomingDataVersion  the data version from the incoming sync message
212
   * @param incomingDegraded     {@code true} if the incoming sync message was sent in degraded mode
213
   * @return {@code true} if the incoming refresh should be skipped;
214
   *         {@code false} if it should be applied
215
   */
216
  public static boolean shouldSkipForSync(CacheEntry existing, long incomingDataVersion, boolean incomingDegraded) {
217
    if (existing == null) {
2✔
218
      return false;
2✔
219
    }
220

221
    boolean existingDegraded = existing.isVersionDegraded();
3✔
222

223
    // Both normal
224
    if (!existingDegraded && !incomingDegraded) {
4✔
225
      return existing.getDataVersion() >= incomingDataVersion;
9✔
226
    }
227
    // Existing normal, incoming degraded — normal wins
228
    if (!existingDegraded) {
2✔
229
      return true;
2✔
230
    }
231
    // Both degraded
232
    if (incomingDegraded) {
2✔
233
      return existing.getDataVersion() >= incomingDataVersion;
9✔
234
    }
235
    // Existing degraded, incoming normal — normal overwrites
236
    return false;
2✔
237
  }
238

239
  /**
240
   * CacheSyncListener guard with a cache-level fast path: fetches the existing entry
241
   * from the L1 cache and delegates to the entry-level overload.
242
   *
243
   * <p>Used <em>outside</em> atomic {@code compute} blocks as a cheap first-pass
244
   * check (DCL pattern). A second guard inside the {@code compute} block is still
245
   * needed for correctness.
246
   *
247
   * @param cache               the local Caffeine L1 cache; must not be null
248
   * @param cacheKey            the cache key to look up; must not be null
249
   * @param incomingDataVersion the data version from the incoming sync message
250
   * @param incomingDegraded    {@code true} if the incoming sync message was sent in degraded mode
251
   * @return {@code true} if the incoming refresh should be skipped;
252
   *         {@code false} if the update may be needed
253
   */
254
  public static boolean shouldSkipForSync(
255
    Cache<String, Object> cache,
256
    String cacheKey,
257
    long incomingDataVersion,
258
    boolean incomingDegraded
259
  ) {
260
    Object existing = cache.getIfPresent(cacheKey);
4✔
261
    if (existing instanceof CacheEntry existingCacheEntry) {
6✔
262
      return shouldSkipForSync(existingCacheEntry, incomingDataVersion, incomingDegraded);
5✔
263
    }
264
    return false;
2✔
265
  }
266
}
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