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

Hyshmily / hotkey / 27856464831

20 Jun 2026 01:44AM UTC coverage: 93.742% (-0.9%) from 94.613%
27856464831

push

github

Hyshmily
feat: add distributed lock (tryLock/tryLockAndRun) with Redis-based AutoReleaseLock

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

1108 of 1235 branches covered (89.72%)

Branch coverage included in aggregate %.

69 of 97 new or added lines in 5 files covered. (71.13%)

2 existing lines in 2 files now uncovered.

3191 of 3351 relevant lines covered (95.23%)

4.32 hits per line

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

88.28
/common/src/main/java/io/github/hyshmily/hotkey/HotKey.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;
17

18
import com.github.benmanes.caffeine.cache.Cache;
19
import io.github.hyshmily.hotkey.cache.HotKeyCache;
20
import io.github.hyshmily.hotkey.cache.distributedlock.AutoReleaseLock;
21
import io.github.hyshmily.hotkey.cache.distributedlock.LockProvider;
22
import io.github.hyshmily.hotkey.cache.fluentAPI.HotKeyReadQuery;
23
import io.github.hyshmily.hotkey.cache.fluentAPI.HotKeyWriteCommand;
24
import io.github.hyshmily.hotkey.exception.HotKeyBlockedException;
25
import io.github.hyshmily.hotkey.hotkeydetector.HotKeyDetector;
26
import io.github.hyshmily.hotkey.hotkeydetector.heavykepper.Item;
27
import io.github.hyshmily.hotkey.hotkeydetector.heavykepper.TopK;
28
import io.github.hyshmily.hotkey.model.HotKeyCacheStats;
29
import io.github.hyshmily.hotkey.rule.Rule;
30
import io.github.hyshmily.hotkey.rule.RuleMatcher;
31
import java.util.*;
32
import java.util.concurrent.BlockingQueue;
33
import java.util.concurrent.LinkedBlockingQueue;
34
import java.util.concurrent.TimeUnit;
35
import java.util.function.Function;
36
import java.util.function.Supplier;
37
import org.jspecify.annotations.Nullable;
38

39
/**
40
 * Public facade for the HotKey library — the sole public API entry point.
41
 *
42
 * <p>All cache read/write/invalidation operations are dispatched through this
43
 * class, which delegates to {@link HotKeyCache} for L1 orchestration, version
44
 * tracking, and cross-instance broadcast, and to {@link TopK} (HeavyKeeper)
45
 * for local and cluster-wide hot-key detection queries.
46
 *
47
 * <p>Rule management (blacklist/whitelist) is also exposed here, with pattern
48
 * matching for exact, prefix, wildcard, and regex rules.
49
 *
50
 * <p><b>Thread safety:</b> All public methods are thread-safe. The underlying
51
 * Caffeine cache and HeavyKeeper sketch are designed for concurrent access.
52
 *
53
 * <p>Depending on the runtime mode, some dependencies may be absent:
54
 * <ul>
55
 *   <li><b>App-only mode</b> (default): all services available.</li>
56
 *   <li><b>Worker-only mode</b> ({@code hotkey.worker.enabled=true}):
57
 *       cache and app‑side TopK are absent; only the Worker TopK is available.</li>
58
 * </ul>
59
 * Methods whose backing service is absent throw
60
 * {@link UnsupportedOperationException} (cache read/write) or return
61
 * empty / zero (TopK queries).
62
 */
63
public class HotKey {
64

65
  /**
66
   * Cache orchestrator that manages L1 (Caffeine), version tracking, TTL
67
   * enforcement, cross-instance broadcast, and rule evaluation.
68
   * {@code null} in Worker-only mode.
69
   */
70
  private final HotKeyCache hotKeyCache;
71
  /**
72
   * App-side local hot-key detector (HeavyKeeper). Records every cache
73
   * access to maintain local frequency sketches. Never {@code null} in
74
   * app mode.
75
   */
76
  private final HotKeyDetector appHotKeyDetector;
77
  /**
78
   * Worker-side global hot-key detector receiving aggregated reports from
79
   * all application instances. {@code null} when no Worker is connected.
80
   */
81
  private final TopK workerTopKAlgorithm;
82
  /**
83
   * Distributed lock provider.  {@code null} when no Redis is available
84
   * (graceful degradation — {@link #tryLock} returns {@code null}).
85
   */
86
  private final LockProvider lockProvider;
87

88
  /**
89
   * Create a HotKey facade with all three optional components.
90
   *
91
   * <p>Each parameter may be {@code null} depending on the deployment mode:
92
   * <ul>
93
   *   <li><b>App-only:</b> {@code hotKeyCache} and {@code appHotKeyDetector} are present,
94
   *       {@code workerTopKAlgorithm} is {@code null}</li>
95
   *   <li><b>Worker-only:</b> only {@code workerTopKAlgorithm} is present</li>
96
   *   <li><b>Coexistence:</b> all three are present</li>
97
   * </ul>
98
   *
99
   * @param hotKeyCache         the cache orchestrator (maybe {@code null} in Worker-only mode)
100
   * @param appHotKeyDetector   the app-side local TopK detector (maybe {@code null} in Worker-only mode)
101
   * @param workerTopKAlgorithm the Worker-side global TopK detector (maybe {@code null} in app-only mode)
102
   */
103
  public HotKey(HotKeyCache hotKeyCache, HotKeyDetector appHotKeyDetector, TopK workerTopKAlgorithm) {
104
    this(hotKeyCache, appHotKeyDetector, workerTopKAlgorithm, null);
6✔
105
  }
1✔
106

107
  /**
108
   * Create a HotKey facade with an optional distributed lock provider.
109
   *
110
   * <p>Each parameter may be {@code null} depending on the deployment mode:
111
   * <ul>
112
   *   <li><b>App-only:</b> {@code hotKeyCache} and {@code appHotKeyDetector} are present,
113
   *       {@code workerTopKAlgorithm} and {@code lockProvider} may be absent</li>
114
   *   <li><b>Worker-only:</b> only {@code workerTopKAlgorithm} is present</li>
115
   *   <li><b>Coexistence:</b> all four may be present</li>
116
   * </ul>
117
   *
118
   * @param hotKeyCache         the cache orchestrator (maybe {@code null} in Worker-only mode)
119
   * @param appHotKeyDetector   the app-side local TopK detector (maybe {@code null} in Worker-only mode)
120
   * @param workerTopKAlgorithm the Worker-side global TopK detector (maybe {@code null} in app-only mode)
121
   * @param lockProvider        the distributed lock provider (maybe {@code null} when no Redis)
122
   */
123
  public HotKey(
124
    HotKeyCache hotKeyCache,
125
    HotKeyDetector appHotKeyDetector,
126
    TopK workerTopKAlgorithm,
127
    LockProvider lockProvider
128
  ) {
2✔
129
    this.hotKeyCache = hotKeyCache;
3✔
130
    this.appHotKeyDetector = appHotKeyDetector;
3✔
131
    this.workerTopKAlgorithm = workerTopKAlgorithm;
3✔
132
    this.lockProvider = lockProvider;
3✔
133
  }
1✔
134

135
  /**
136
   * Create a fluent read query for the given key.
137
   *
138
   * <p>Returns a {@link HotKeyReadQuery} builder that lets you configure
139
   * the primary reader, fallback chain, cache mode, TTL overrides, null
140
   * caching policy, and broadcast behaviour before executing.
141
   *
142
   * <p>Useful for read-heavy call-sites that prefer a declarative style
143
   * over manual {@code get()}/{@code getWithSoftExpire()} orchestration.
144
   *
145
   * <p>Example:
146
   * <pre>
147
   *   Optional&lt;User&gt; user = hotKey.read("user:42")
148
   *       .withPrimary(userRepo::findById)
149
   *       .thenExecute(backupRepo::findById)
150
   *       .withHardTtl(30_000)
151
   *       .withSoftTtl(10_000)
152
   *       .allowBroadcast()
153
   *       .execute();
154
   * </pre>
155
   *
156
   * @param cacheKey the cache key to read
157
   * @param <T>      the value type
158
   * @return a new {@link HotKeyReadQuery} instance (single-use)
159
   */
160
  public <T> HotKeyReadQuery<T> read(String cacheKey) {
161
    return new HotKeyReadQuery<>(this, cacheKey);
6✔
162
  }
163

164
  /**
165
   * Create a fluent write command for the given key.
166
   *
167
   * <p>Returns a {@link HotKeyWriteCommand} builder that lets you configure
168
   * TTL overrides before executing a write-through, invalidate-before-write,
169
   * or plain invalidation.
170
   *
171
   * <p>Example:
172
   * <pre>
173
   *   hotKey.write("user:42")
174
   *       .withHardTtl(30_000)
175
   *       .putThrough(newValue, dbWriter);
176
   * </pre>
177
   *
178
   * @param cacheKey the cache key to operate on
179
   * @param <T>      the value type
180
   * @return a new {@link HotKeyWriteCommand} instance (single-use)
181
   */
182
  public <T> HotKeyWriteCommand<T> write(String cacheKey) {
183
    return new HotKeyWriteCommand<>(this, cacheKey);
6✔
184
  }
185

186
  /**
187
   * Convenience shorthand for {@link #get get(cacheKey, loader).orElse(null)}.
188
   *
189
   * <p>Loads the value via the supplier on cache miss, using configured default TTLs.
190
   * Returns {@code null} when the loader itself returns {@code null}.
191
   *
192
   * @param cacheKey the key to retrieve
193
   * @param loader   the value supplier for cache misses
194
   * @param <V>      the value type
195
   * @return the cached or loaded value, or {@code null} if the loader returned {@code null}
196
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
197
   * @throws HotKeyBlockedException when the key matches a blacklist rule
198
   */
199
  public <V> V computeIfAbsent(String cacheKey, Supplier<V> loader) {
200
    requireCache();
2✔
201
    return get(cacheKey, loader).orElse(null);
7✔
202
  }
203

204
  /**
205
   * Batch variant of {@link #computeIfAbsent(String, Supplier)}. Iterates over
206
   * the given keys and loads each one via the provided function on cache miss.
207
   *
208
   * @param cachKeys the keys to retrieve
209
   * @param loader   the per-key value supplier for cache misses
210
   * @param <V>      the value type
211
   * @return a map of key → loaded value (only keys whose loader returned non-null are included)
212
   */
213
  public <V> Map<String, V> computeIfAbsent(Collection<String> cachKeys, Function<String, V> loader) {
214
    Map<String, V> result = new HashMap<>();
4✔
215
    cachKeys.forEach(key -> {
6✔
216
      V value = computeIfAbsent(key, () -> loader.apply(key));
7✔
217
      result.put(key, value);
5✔
218
    });
1✔
219

220
    return result;
2✔
221
  }
222

223
  /**
224
   * Convenience shorthand with explicit hard TTL.
225
   *
226
   * @param cacheKey the key to retrieve
227
   * @param loader   the value supplier for cache misses
228
   * @param hardTtlMs hard TTL override (0 = use configured default; {@link Long#MAX_VALUE} for permanent entry)
229
   * @param <V>      the value type
230
   * @return the cached or loaded value, or {@code null} if the loader returned {@code null}
231
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
232
   * @throws HotKeyBlockedException when the key matches a blacklist rule
233
   * @see #get(String, Supplier, long, long)
234
   */
235
  public <V> V computeIfAbsent(String cacheKey, Supplier<V> loader, long hardTtlMs) {
236
    requireCache();
2✔
237
    return get(cacheKey, loader, hardTtlMs, 0).orElse(null);
9✔
238
  }
239

240
  /**
241
   * Batch variant of {@link #computeIfAbsent(String, Supplier, long)}. Iterates
242
   * over the given keys with an explicit hard TTL override.
243
   *
244
   * @param cachKeys  the keys to retrieve
245
   * @param loader    the per-key value supplier for cache misses
246
   * @param hardTtlMs hard TTL override (0 = use configured default; {@link Long#MAX_VALUE} for permanent entry)
247
   * @param <V>       the value type
248
   * @return a map of key → loaded value
249
   */
250
  public <V> Map<String, V> computeIfAbsent(Collection<String> cachKeys, Function<String, V> loader, long hardTtlMs) {
251
    Map<String, V> result = new HashMap<>();
4✔
252
    cachKeys.forEach(key -> {
7✔
253
      V value = computeIfAbsent(key, () -> loader.apply(key), hardTtlMs);
8✔
254
      result.put(key, value);
5✔
255
    });
1✔
256

257
    return result;
2✔
258
  }
259

260
  /**
261
   * Convenience shorthand with explicit hard and soft TTLs.
262
   *
263
   * @param cacheKey  the key to retrieve
264
   * @param loader    the value supplier for cache misses
265
   * @param hardTtlMs hard TTL override (0 = use configured default; {@link Long#MAX_VALUE} for permanent entry)
266
   * @param softTtlMs soft TTL override (0 = use configured default)
267
   * @param <V>       the value type
268
   * @return the cached or loaded value, or {@code null} if the loader returned {@code null}
269
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
270
   * @throws HotKeyBlockedException when the key matches a blacklist rule
271
   * @see #get(String, Supplier, long, long)
272
   */
273
  public <V> V computeIfAbsent(String cacheKey, Supplier<V> loader, long hardTtlMs, long softTtlMs) {
274
    requireCache();
2✔
275
    return get(cacheKey, loader, hardTtlMs, softTtlMs).orElse(null);
9✔
276
  }
277

278
  /**
279
   * Batch variant of {@link #computeIfAbsent(String, Supplier, long, long)}.
280
   * Iterates over the given keys with explicit hard and soft TTL overrides.
281
   *
282
   * @param cachKeys  the keys to retrieve
283
   * @param loader    the per-key value supplier for cache misses
284
   * @param hardTtlMs hard TTL override (0 = use configured default; {@link Long#MAX_VALUE} for permanent entry)
285
   * @param softTtlMs soft TTL override (0 = use configured default)
286
   * @param <V>       the value type
287
   * @return a map of key → loaded value
288
   */
289
  public <V> Map<String, V> computeIfAbsent(
290
    Collection<String> cachKeys,
291
    Function<String, V> loader,
292
    long hardTtlMs,
293
    long softTtlMs
294
  ) {
295
    Map<String, V> result = new HashMap<>();
4✔
296
    cachKeys.forEach(key -> {
8✔
297
      V value = computeIfAbsent(key, () -> loader.apply(key), hardTtlMs, softTtlMs);
9✔
298
      result.put(key, value);
5✔
299
    });
1✔
300

301
    return result;
2✔
302
  }
303

304
  /**
305
   * Convenience shorthand for {@link #getWithSoftExpire getWithSoftExpire(cacheKey, loader).orElse(null)}.
306
   *
307
   * <p>Returns stale data immediately when the soft TTL has expired, triggering
308
   * an async refresh in the background.
309
   *
310
   * @param cacheKey the key to retrieve
311
   * @param loader   the value supplier for cache misses / refreshes
312
   * @param <V>      the value type
313
   * @return the cached (possibly stale) or loaded value, or {@code null} if the loader returned {@code null}
314
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
315
   * @throws HotKeyBlockedException when the key matches a blacklist rule
316
   */
317
  public <V> V computeIfAbsentWithSoftExpire(String cacheKey, Supplier<V> loader) {
318
    requireCache();
2✔
319
    return getWithSoftExpire(cacheKey, loader).orElse(null);
7✔
320
  }
321

322
  /**
323
   * Batch variant of {@link #computeIfAbsentWithSoftExpire(String, Supplier)}.
324
   * Iterates over the given keys with soft-expire semantics.
325
   *
326
   * @param cachKeys the keys to retrieve
327
   * @param loader   the per-key value supplier for cache misses / refreshes
328
   * @param <V>      the value type
329
   * @return a map of key → (possibly stale) loaded value
330
   */
331
  public <V> Map<String, V> computeIfAbsentWithSoftExpire(Collection<String> cachKeys, Function<String, V> loader) {
332
    Map<String, V> result = new HashMap<>();
4✔
333
    cachKeys.forEach(key -> {
6✔
334
      V value = computeIfAbsentWithSoftExpire(key, () -> loader.apply(key));
7✔
335
      result.put(key, value);
5✔
336
    });
1✔
337

338
    return result;
2✔
339
  }
340

341
  /**
342
   * Convenience shorthand with explicit soft TTL.
343
   *
344
   * @param cacheKey  the key to retrieve
345
   * @param loader    the value supplier for cache misses / refreshes
346
   * @param softTtlMs soft TTL override (0 = use configured default)
347
   * @param <V>       the value type
348
   * @return the cached (possibly stale) or loaded value, or {@code null} if the loader returned {@code null}
349
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
350
   * @throws HotKeyBlockedException when the key matches a blacklist rule
351
   */
352
  public <V> V computeIfAbsentWithSoftExpire(String cacheKey, Supplier<V> loader, long softTtlMs) {
353
    requireCache();
2✔
354
    return getWithSoftExpire(cacheKey, loader, softTtlMs).orElse(null);
8✔
355
  }
356

357
  /**
358
   * Batch variant of {@link #computeIfAbsentWithSoftExpire(String, Supplier, long)}.
359
   * Iterates over the given keys with explicit soft TTL override.
360
   *
361
   * @param cachKeys  the keys to retrieve
362
   * @param loader    the per-key value supplier for cache misses / refreshes
363
   * @param softTtlMs soft TTL override (0 = use configured default)
364
   * @param <V>       the value type
365
   * @return a map of key → (possibly stale) loaded value
366
   */
367
  public <V> Map<String, V> computeIfAbsentWithSoftExpire(
368
    Collection<String> cachKeys,
369
    Function<String, V> loader,
370
    long softTtlMs
371
  ) {
372
    Map<String, V> result = new HashMap<>();
4✔
373
    cachKeys.forEach(key -> {
7✔
374
      V value = computeIfAbsentWithSoftExpire(key, () -> loader.apply(key), softTtlMs);
8✔
375
      result.put(key, value);
5✔
376
    });
1✔
377

378
    return result;
2✔
379
  }
380

381
  /**
382
   * Convenience shorthand with explicit hard and soft TTLs for soft-expire semantics.
383
   *
384
   * @param cacheKey  the key to retrieve
385
   * @param loader    the value supplier for cache misses / refreshes
386
   * @param hardTtlMs hard TTL override (0 = use configured default; {@link Long#MAX_VALUE} for pure logical expiry)
387
   * @param softTtlMs soft TTL override (0 = use configured default)
388
   * @param <V>       the value type
389
   * @return the cached (possibly stale) or loaded value, or {@code null} if the loader returned {@code null}
390
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
391
   * @throws HotKeyBlockedException when the key matches a blacklist rule
392
   */
393
  public <V> V computeIfAbsentWithSoftExpire(String cacheKey, Supplier<V> loader, long hardTtlMs, long softTtlMs) {
394
    requireCache();
2✔
395
    return getWithSoftExpire(cacheKey, loader, hardTtlMs, softTtlMs).orElse(null);
9✔
396
  }
397

398
  /**
399
   * Batch variant of {@link #computeIfAbsentWithSoftExpire(String, Supplier, long, long)}.
400
   * Iterates over the given keys with explicit hard and soft TTL overrides.
401
   *
402
   * @param cachKeys  the keys to retrieve
403
   * @param loader    the per-key value supplier for cache misses / refreshes
404
   * @param hardTtlMs hard TTL override (0 = use configured default; {@link Long#MAX_VALUE} for pure logical expiry)
405
   * @param softTtlMs soft TTL override (0 = use configured default)
406
   * @param <V>       the value type
407
   * @return a map of key → (possibly stale) loaded value
408
   */
409
  public <V> Map<String, V> computeIfAbsentWithSoftExpire(
410
    Collection<String> cachKeys,
411
    Function<String, V> loader,
412
    long hardTtlMs,
413
    long softTtlMs
414
  ) {
415
    Map<String, V> result = new HashMap<>();
4✔
416
    cachKeys.forEach(key -> {
8✔
417
      V value = computeIfAbsentWithSoftExpire(key, () -> loader.apply(key), hardTtlMs, softTtlMs);
9✔
418
      result.put(key, value);
5✔
419
    });
1✔
420

421
    return result;
2✔
422
  }
423

424
  /**
425
   * Look up a cached value without loading or triggering hot-key detection.
426
   *
427
   * @param cacheKey the key to look up
428
   * @param <T>      the value type
429
   * @return an {@link Optional} containing the raw value if present
430
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
431
   */
432
  public <T> Optional<T> peek(String cacheKey) {
433
    requireCache();
2✔
434
    return hotKeyCache.peek(cacheKey);
5✔
435
  }
436

437
  /**
438
   * Batch variant of {@link #peek(String)}. Returns a map of key → value for
439
   * all keys that are present in L1. Missing keys are silently omitted.
440
   *
441
   * @param cacheKeys the keys to look up
442
   * @return a map of present key-value pairs (never {@code null})
443
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
444
   */
445
  public Map<String, Object> peekAll(Collection<String> cacheKeys) {
446
    Map<String, Object> result = new HashMap<>();
4✔
447
    cacheKeys.forEach(key -> {
5✔
448
      Optional<Object> value = peek(key);
4✔
449
      value.ifPresent(v -> result.put(key, v));
11✔
450
    });
1✔
451

452
    return result;
2✔
453
  }
454

455
  /**
456
   * Get a value from L1 or load it via the reader.
457
   * Hot keys are promoted to L1 with configured hot TTLs; normal keys use default TTLs.
458
   *
459
   * @param cacheKey the key to retrieve
460
   * @param reader   the value supplier for cache misses
461
   * @param <T>      the value type
462
   * @return an {@link Optional} containing the cached or loaded value
463
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
464
   * @throws HotKeyBlockedException when the key matches a blacklist rule
465
   */
466
  public <T> Optional<T> get(String cacheKey, Supplier<T> reader) {
467
    requireCache();
2✔
468
    return hotKeyCache.get(cacheKey, reader);
6✔
469
  }
470

471
  /**
472
   * Get with explicit TTL overrides.
473
   * Pass 0 to use the configured default for that TTL type.
474
   *
475
   * @param cacheKey  the key to retrieve
476
   * @param reader    the value supplier for cache misses
477
   * @param hardTtlMs hard TTL override (0 = use configured default; {@link Long#MAX_VALUE} for permanent entry — no hard TTL eviction)
478
   * @param softTtlMs soft TTL override (0 = use configured default)
479
   * @param <T>       the value type
480
   * @return an {@link Optional} containing the cached or loaded value
481
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
482
   * @throws HotKeyBlockedException when the key matches a blacklist rule
483
   */
484
  public <T> Optional<T> get(String cacheKey, Supplier<T> reader, long hardTtlMs, long softTtlMs) {
485
    requireCache();
2✔
486
    return hotKeyCache.get(cacheKey, reader, hardTtlMs, softTtlMs);
8✔
487
  }
488

489
  /**
490
   * Get with soft-expire (stale-while-revalidate). Returns cached value immediately
491
   * even if soft TTL expired, while triggering async refresh in background.
492
   *
493
   * @param cacheKey the key to retrieve
494
   * @param reader   the value supplier for cache misses / refreshes
495
   * @param <T>      the value type
496
   * @return an {@link Optional} containing the cached (possibly stale) or loaded value
497
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
498
   * @throws HotKeyBlockedException when the key matches a blacklist rule
499
   */
500
  public <T> Optional<T> getWithSoftExpire(String cacheKey, Supplier<T> reader) {
501
    requireCache();
2✔
502
    return hotKeyCache.getWithSoftExpire(cacheKey, reader);
6✔
503
  }
504

505
  /**
506
   * Get with soft-expire and explicit soft TTL override.
507
   *
508
   * @param cacheKey  the key to retrieve
509
   * @param reader    the value supplier for cache misses / refreshes
510
   * @param softTtlMs soft TTL override (0 = use configured default)
511
   * @param <T>       the value type
512
   * @return an {@link Optional} containing the cached (possibly stale) or loaded value
513
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
514
   * @throws HotKeyBlockedException when the key matches a blacklist rule
515
   */
516
  public <T> Optional<T> getWithSoftExpire(String cacheKey, Supplier<T> reader, long softTtlMs) {
517
    requireCache();
2✔
518
    return hotKeyCache.getWithSoftExpire(cacheKey, reader, softTtlMs);
7✔
519
  }
520

521
  /**
522
   * Get with soft-expire and explicit hard/soft TTL overrides.
523
   *
524
   * @param cacheKey  the key to retrieve
525
   * @param reader    the value supplier for cache misses / refreshes
526
   * @param hardTtlMs hard TTL override (0 = use configured default; {@link Long#MAX_VALUE} for pure logical expiry — entry never hard-evicted, only soft-expire or Caffeine {@code maximumSize})
527
   * @param softTtlMs soft TTL override (0 = use configured default)
528
   * @param <T>       the value type
529
   * @return an {@link Optional} containing the cached (possibly stale) or loaded value
530
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
531
   * @throws HotKeyBlockedException when the key matches a blacklist rule
532
   */
533
  public <T> Optional<T> getWithSoftExpire(String cacheKey, Supplier<T> reader, long hardTtlMs, long softTtlMs) {
534
    requireCache();
2✔
535
    return hotKeyCache.getWithSoftExpire(cacheKey, reader, hardTtlMs, softTtlMs);
8✔
536
  }
537

538
  /**
539
   * Invalidate a single key from L1 and broadcast REFRESH to peers.
540
   * The next {@link #get} will re-fetch from the reader.
541
   *
542
   * @param cacheKey the key to invalidate
543
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
544
   */
545
  public void invalidate(String cacheKey) {
546
    requireCache();
2✔
547
    hotKeyCache.invalidate(cacheKey);
4✔
548
  }
1✔
549

550
  /**
551
   * Invalidate one or more keys from L1 and broadcast INVALIDATE for each.
552
   *
553
   * @param cacheKeys the keys to invalidate
554
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
555
   * @see #invalidate(String)
556
   */
557
  public void invalidateAll(String... cacheKeys) {
558
    invalidateAll(Arrays.asList(cacheKeys));
4✔
559
  }
1✔
560

561
  /**
562
   * Invalidate a collection of keys from L1 and broadcast INVALIDATE for each.
563
   *
564
   * @param cacheKeys the keys to invalidate
565
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
566
   */
567
  public void invalidateAll(Collection<String> cacheKeys) {
568
    requireCache();
2✔
569
    hotKeyCache.invalidateAll(cacheKeys);
4✔
570
  }
1✔
571

572
  /**
573
   * Convenience shorthand for {@link #evictLocal(Collection)} — evicts a single
574
   * key from the local cache without broadcasting.
575
   *
576
   * @param cacheKeys the key to evict locally
577
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
578
   */
579
  public void evictLocal(String cacheKeys) {
580
    evictLocal(Collections.singletonList(cacheKeys));
4✔
581
  }
1✔
582

583
  /**
584
   * Evict keys from the local cache only, without broadcasting to other
585
   * instances and without bumping version numbers.
586
   *
587
   * <p>Useful for emergency local cleanup, testing, or module offline
588
   * scenarios where only the current node needs to be cleared.
589
   *
590
   * @param cacheKeys the keys to evict locally
591
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
592
   */
593
  public void evictLocal(Collection<String> cacheKeys) {
594
    requireCache();
2✔
595
    hotKeyCache.evictLocal(cacheKeys);
4✔
596
  }
1✔
597

598
  /**
599
   * Write-through: execute the writer, then update L1 and broadcast.
600
   * Uses effective hard/soft TTL from configuration.
601
   *
602
   * @param cacheKey the key to write
603
   * @param value    the value to cache
604
   * @param writer   the data-source mutation to execute before caching
605
   * @param <T>      the value type
606
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
607
   */
608
  public <T> void putThrough(String cacheKey, T value, Runnable writer) {
609
    requireCache();
2✔
610
    hotKeyCache.putThrough(cacheKey, value, writer);
6✔
611
  }
1✔
612

613
  /**
614
   * Write-through with explicit TTL overrides.
615
   * Pass 0 to use the configured default for that TTL type.
616
   *
617
   * @param cacheKey  the key to write
618
   * @param value     the value to cache
619
   * @param writer    the data-source mutation to execute before caching
620
   * @param hardTtlMs hard TTL override (0 = use configured default; {@link Long#MAX_VALUE} for permanent entry — no hard TTL eviction)
621
   * @param softTtlMs soft TTL override (0 = use configured default)
622
   * @param <T>       the value type
623
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
624
   */
625
  public <T> void putThrough(String cacheKey, T value, Runnable writer, long hardTtlMs, long softTtlMs) {
626
    requireCache();
2✔
627
    hotKeyCache.putThrough(cacheKey, value, writer, hardTtlMs, softTtlMs);
8✔
628
  }
1✔
629

630
  /**
631
   * Write a value directly into the local L1 cache without version bump,
632
   * without broadcast, without hot-key detection, and without reporting.
633
   * <p>
634
   * Existing entry metadata is preserved.  If no entry exists, a fresh
635
   * {@link io.github.hyshmily.hotkey.model.CacheEntry} is created with {@link io.github.hyshmily.hotkey.model.KeyState#NORMAL}.
636
   * <p>
637
   * Never throws {@code UnsupportedOperationException} in Worker-only mode;
638
   * silently no-ops instead.
639
   *
640
   * @param cacheKey the key to store
641
   * @param value    the value to cache
642
   */
643
  public void putLocal(String cacheKey, Object value) {
644
    requireCache();
2✔
645
    putLocal(cacheKey, value, 0, 0);
6✔
646
  }
1✔
647

648
  /**
649
   * Batch variant of {@link #putLocal(String, Object)}. Writes all entries into
650
   * L1 without version bump, broadcast, hot-key detection, or reporting.
651
   *
652
   * @param entries a map of key → value to store locally
653
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
654
   */
655
  public void putLocal(Map<String, Object> entries) {
656
    requireCache();
2✔
657
    entries.forEach(this::putLocal);
4✔
658
  }
1✔
659

660
  /**
661
   * Write a value directly into the local L1 cache with explicit TTL overrides.
662
   * Delegates to {@link #putLocal(String, Object)} semantics — no version bump,
663
   * no broadcast, no hot-key detection, and no reporting.
664
   * <p>
665
   * Pass {@code 0} for either TTL to use the configured default.
666
   *
667
   * @param cacheKey  the key to store
668
   * @param value     the value to cache
669
   * @param hardTtlMs hard TTL override (0 = use configured default)
670
   * @param softTtlMs soft TTL override (0 = use configured default)
671
   */
672
  public void putLocal(String cacheKey, Object value, long hardTtlMs, long softTtlMs) {
673
    requireCache();
2✔
674
    hotKeyCache.putLocal(cacheKey, value, hardTtlMs, softTtlMs);
7✔
675
  }
1✔
676

677
  /**
678
   * Execute a mutation, then invalidate L1 and broadcast.
679
   * Next {@link #get} will re-fetch from the reader.
680
   *
681
   * @param cacheKey the key to invalidate after mutation
682
   * @param mutation the mutation to execute
683
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
684
   */
685
  public void putBeforeInvalidate(String cacheKey, Runnable mutation) {
686
    requireCache();
2✔
687
    hotKeyCache.putBeforeInvalidate(cacheKey, mutation);
5✔
688
  }
1✔
689

690
  /**
691
   * Batch variant of {@link #putBeforeInvalidate(String, Runnable)}. Executes
692
   * all mutations and invalidates the corresponding keys from L1 with broadcast.
693
   *
694
   * @param mutations a map of key → mutation to execute
695
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
696
   */
697
  public void putBeforeInvalidateAll(Map<String, Runnable> mutations) {
698
    requireCache();
2✔
699
    mutations.forEach(this::putBeforeInvalidate);
4✔
700
  }
1✔
701

702
  /**
703
   * Estimated number of entries currently in the L1 cache.
704
   * <p>
705
   * This is a lightweight, best-effort estimate from the underlying Caffeine
706
   * cache, suitable for monitoring dashboards and capacity planning.
707
   *
708
   * @return estimated entry count, or {@code 0} in Worker-only mode
709
   */
710
  public long estimatedSize() {
711
    if (hotKeyCache == null) {
3✔
712
      return 0L;
2✔
713
    }
714
    return hotKeyCache.estimatedSize();
4✔
715
  }
716

717
  /**
718
   * Return a snapshot of basic L1 cache statistics.
719
   * <p>
720
   * Hit/miss/eviction counters are populated only when Caffeine's
721
   * {@code recordStats()} is enabled. {@code estimatedSize} is always
722
   * available.
723
   *
724
   * @return a {@link HotKeyCacheStats} record, or {@code null} in Worker-only mode;
725
   *         hit/miss counters are {@code 0} if stats recording is not enabled
726
   */
727
  public HotKeyCacheStats stats() {
728
    if (hotKeyCache == null) {
3✔
729
      return null;
2✔
730
    }
731
    return hotKeyCache.stats();
4✔
732
  }
733

734
  /**
735
   * Return the underlying Caffeine cache for direct access.
736
   *
737
   * <p>Useful for Caffeine-specific operations such as {@code asMap()},
738
   * {@code policy()}, and {@code cleanUp()}. Use with caution — bypassing
739
   * the HotKey orchestration layer can lead to inconsistent cache state.
740
   *
741
   * @return the raw Caffeine {@link Cache} instance
742
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
743
   */
744
  public Cache<String, Object> getLocalCache() {
745
    requireCache();
2✔
746
    return hotKeyCache.getLocalCache();
4✔
747
  }
748

749
  /**
750
   * Invalidate all entries from the L1 cache without broadcasting.
751
   * <p>
752
   * This is an emergency flush — all cached values are removed immediately.
753
   * No cross-instance sync messages are sent. Subsequent {@link #get} calls
754
   * will reload data from the reader.
755
   * <p>
756
   * Use with caution: flushing the local cache increases load on the backend
757
   * until entries are re-cached.
758
   *
759
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
760
   */
761
  public void invalidateAll() {
762
    requireCache();
2✔
763
    hotKeyCache.invalidateAll();
3✔
764
  }
1✔
765

766
  /**
767
   * Attempt to acquire a distributed lock with the provider's default
768
   * retry counts.
769
   *
770
   * <p>Equivalent to {@code tryLock(key, expire, unit, -1, -1, -1)}.
771
   *
772
   * <p>Usage:
773
   * <pre>{@code
774
   * try (AutoReleaseLock lock = hotKey.tryLock("order:42", 5, TimeUnit.SECONDS)) {
775
   *     if (lock != null) {
776
   *         // critical section
777
   *     }
778
   * }
779
   * }</pre>
780
   *
781
   * @param key    the lock key
782
   * @param expire the time-to-live for the lock
783
   * @param unit   the time unit for {@code expire}
784
   * @return a lock handle if acquired, or {@code null} if the lock is held
785
   *         by another caller or the provider is unavailable
786
   */
787
  @Nullable
788
  public AutoReleaseLock tryLock(String key, long expire, TimeUnit unit) {
NEW
789
    return lockProvider != null ? lockProvider.tryLock(key, expire, unit) : null;
×
790
  }
791

792
  /**
793
   * Acquire a distributed lock, execute the action, and release
794
   * with the provider's default retry counts.
795
   *
796
   * <p>Equivalent to {@code tryLockAndRun(key, expire, unit, action, -1, -1, -1)}.
797
   *
798
   * @param key    the lock key
799
   * @param expire the time-to-live for the lock
800
   * @param unit   the time unit for {@code expire}
801
   * @param action the action to execute while holding the lock
802
   * @return {@code true} if the lock was acquired and the action ran,
803
   *         {@code false} otherwise
804
   */
805
  public boolean tryLockAndRun(String key, long expire, TimeUnit unit, Runnable action) {
NEW
806
    Objects.requireNonNull(action, "action must not be null");
×
NEW
807
    if (lockProvider == null) {
×
NEW
808
      return false;
×
809
    }
NEW
810
    try (AutoReleaseLock lock = tryLock(key, expire, unit)) {
×
NEW
811
      if (lock != null) {
×
NEW
812
        action.run();
×
NEW
813
        return true;
×
814
      }
NEW
815
      return false;
×
NEW
816
    }
×
817
  }
818

819
  /**
820
   * Attempt to acquire a distributed lock with explicit retry counts.
821
   *
822
   * <p>Any negative count falls back to the provider's configured default.
823
   * Returns {@code null} when the lock is held by another caller or when
824
   * no Redis is available (graceful degradation).
825
   *
826
   * @param key                  the lock key
827
   * @param expire               the time-to-live for the lock
828
   * @param unit                 the time unit for {@code expire}
829
   * @param tryLockLockCount     the number of {@code SET NX} retries;
830
   *                             negative → default
831
   * @param tryLockInquiryCount  the number of {@code GET} inquiries;
832
   *                             negative → default
833
   * @param tryLockUnlockCount   the number of {@code DEL} retries;
834
   *                             negative → default
835
   * @return a lock handle if acquired, or {@code null} if the lock is held
836
   *         by another caller or the provider is unavailable
837
   */
838
  @Nullable
839
  public AutoReleaseLock tryLock(
840
    String key,
841
    long expire,
842
    TimeUnit unit,
843
    int tryLockLockCount,
844
    int tryLockInquiryCount,
845
    int tryLockUnlockCount
846
  ) {
NEW
847
    return lockProvider != null
×
NEW
848
      ? lockProvider.tryLock(key, expire, unit, tryLockLockCount, tryLockInquiryCount, tryLockUnlockCount)
×
NEW
849
      : null;
×
850
  }
851

852
  /**
853
   * Acquire a distributed lock with explicit retry counts, execute the
854
   * action, and release.
855
   *
856
   * <p>Any negative count falls back to the provider's configured default.
857
   *
858
   * @param key                  the lock key
859
   * @param expire               the time-to-live for the lock
860
   * @param unit                 the time unit for {@code expire}
861
   * @param action               the action to execute while holding the lock
862
   * @param tryLockLockCount     the number of {@code SET NX} retries;
863
   *                             negative → default
864
   * @param tryLockInquiryCount  the number of {@code GET} inquiries;
865
   *                             negative → default
866
   * @param tryLockUnlockCount   the number of {@code DEL} retries;
867
   *                             negative → default
868
   * @return {@code true} if the lock was acquired and the action ran,
869
   *         {@code false} otherwise
870
   */
871
  public boolean tryLockAndRun(
872
    String key,
873
    long expire,
874
    TimeUnit unit,
875
    Runnable action,
876
    int tryLockLockCount,
877
    int tryLockInquiryCount,
878
    int tryLockUnlockCount
879
  ) {
NEW
880
    Objects.requireNonNull(action, "action must not be null");
×
NEW
881
    try (AutoReleaseLock lock = tryLock(key, expire, unit, tryLockLockCount, tryLockInquiryCount, tryLockUnlockCount)) {
×
NEW
882
      if (lock != null) {
×
NEW
883
        action.run();
×
NEW
884
        return true;
×
885
      }
NEW
886
      return false;
×
NEW
887
    }
×
888
  }
889

890
  //------------------------------------------------------------------------
891

892
  /**
893
   * Check whether a key is currently tracked as a local hot key in L1.
894
   *
895
   * @param cacheKey the key to inspect
896
   * @return {@code true} if the key exists in L1 with {@link io.github.hyshmily.hotkey.model.KeyState#HOT}
897
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
898
   */
899
  public boolean isLocalHotKey(String cacheKey) {
900
    requireCache();
2✔
901
    return hotKeyCache.isLocalHotKey(cacheKey);
5✔
902
  }
903

904
  /**
905
   * Batch variant of {@link #isLocalHotKey(String)}. Returns a map of key →
906
   * hot status for all given keys.
907
   *
908
   * @param cacheKeys the keys to inspect
909
   * @return a map of key → whether it is a local hot key
910
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
911
   */
912
  public Map<String, Boolean> areLocalHotKeys(Collection<String> cacheKeys) {
913
    requireCache();
2✔
914
    Map<String, Boolean> result = new HashMap<>();
4✔
915
    cacheKeys.forEach(key -> result.put(key, isLocalHotKey(key)));
14✔
916
    return result;
2✔
917
  }
918

919
  /**
920
   * Increment the local TopK detector directly, bypassing the buffer and the
921
   * report-to-Worker path. Useful for bulk-loading historical access patterns
922
   * or correcting frequency counts.
923
   *
924
   * @param cacheKey the key to record
925
   * @param count    the number of accesses to add
926
   */
927
  public void notifyLocalDetectorDirect(String cacheKey, int count) {
928
    appHotKeyDetector.addDirect(cacheKey, count);
6✔
929
  }
1✔
930

931
  /**
932
   * Batch-increment the local TopK detector directly, bypassing buffer and reports.
933
   *
934
   * @param keyCounts a map of key → access count
935
   */
936
  public void notifyLocalDetectorDirect(Map<String, Long> keyCounts) {
937
    appHotKeyDetector.addDirect(keyCounts);
5✔
938
  }
1✔
939

940
  /**
941
   * Notify the local TopK detector that a key was accessed, without triggering
942
   * a report to the Worker. Used by {@code @Intercept} path to keep the local
943
   * frequency sketch accurate without flooding the Worker with reports.
944
   * Null keys are silently ignored.
945
   *
946
   * @param cacheKey the accessed key (maybe {@code null}, silently ignored)
947
   */
948
  public void notifyLocalDetector(String cacheKey) {
949
    appHotKeyDetector.add(cacheKey);
4✔
950
  }
1✔
951

952
  /**
953
   * Notify the local detector of key access with a custom delta, routing
954
   * through the buffered counter. Null keys are silently ignored.
955
   *
956
   * @param cacheKey the accessed key (maybe {@code null}, silently ignored)
957
   * @param count    the number of accesses to record
958
   */
959
  public void notifyLocalDetector(String cacheKey, long count) {
960
    appHotKeyDetector.add(cacheKey, count);
5✔
961
  }
1✔
962

963
  /**
964
   * Batch-notify the local detector of multiple key accesses, routing through
965
   * the buffered counter. Null keys in the map are silently ignored.
966
   *
967
   * @param keyCounts a map of key → access count
968
   */
969
  public void notifyLocalDetector(Map<String, Long> keyCounts) {
970
    appHotKeyDetector.add(keyCounts);
4✔
971
  }
1✔
972

973
  /**
974
   * Evict the given key locally, then load and cache via the supplier.
975
   * Uses default TTLs. No broadcast is sent for the eviction.
976
   *
977
   * @param cacheKey the key to refresh
978
   * @param loader   the value supplier
979
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
980
   */
981
  public void refresh(String cacheKey, Supplier<?> loader) {
982
    requireCache();
2✔
983
    evictLocal(cacheKey);
3✔
984
    putThrough(cacheKey, loader.get(), () -> {});
6✔
985
  }
1✔
986

987
  /**
988
   * Batch variant of {@link #refresh(String, Supplier)}. Refreshes all entries
989
   * by evicting locally then loading and caching via the provided suppliers.
990
   *
991
   * @param loaders a map of key → value supplier
992
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
993
   */
994
  public void refreshAll(Map<String, Supplier<?>> loaders) {
995
    requireCache();
2✔
996
    loaders.forEach(this::refresh);
4✔
997
  }
1✔
998

999
  /**
1000
   * Evict the given key locally, then load and cache with explicit TTL overrides.
1001
   * No broadcast is sent for the eviction.
1002
   *
1003
   * @param cacheKey     the key to refresh
1004
   * @param loader       the value supplier
1005
   * @param hotHardTtlMs hard TTL override (0 = use configured default; {@link Long#MAX_VALUE} for permanent entry)
1006
   * @param hotSoftTtlMs soft TTL override (0 = use configured default)
1007
   * @param <V>          the value type
1008
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
1009
   */
1010
  public <V> void refresh(String cacheKey, Supplier<V> loader, long hotHardTtlMs, long hotSoftTtlMs) {
1011
    requireCache();
2✔
1012
    evictLocal(cacheKey);
3✔
1013
    putThrough(cacheKey, loader.get(), () -> {}, hotHardTtlMs, hotSoftTtlMs);
8✔
1014
  }
1✔
1015

1016
  /**
1017
   * Check whether a key is currently tracked as a cluster-wide hot key by the
1018
   * Worker-side global detector.
1019
   *
1020
   * @param cacheKey the key to inspect
1021
   * @return {@code true} if the key appears in the Worker TopK list;
1022
   *         {@code false} if the key is {@code null} or no Worker is active
1023
   */
1024
  public boolean isWorkerHotKey(String cacheKey) {
1025
    return workerTopKAlgorithm != null && cacheKey != null && workerTopKAlgorithm.contains(cacheKey);
14✔
1026
  }
1027

1028
  /**
1029
   * Batch variant of {@link #isWorkerHotKey(String)}. Returns a map of key →
1030
   * Worker hot status for all given keys.
1031
   *
1032
   * @param cacheKeys the keys to inspect
1033
   * @return a map of key → whether it is a cluster-wide hot key;
1034
   *         all entries are {@code false} when no Worker is active
1035
   */
1036
  public Map<String, Boolean> areWorkerHotKeys(Collection<String> cacheKeys) {
1037
    Map<String, Boolean> result = new HashMap<>();
4✔
1038
    cacheKeys.forEach(key -> result.put(key, isWorkerHotKey(key)));
14✔
1039
    return result;
2✔
1040
  }
1041

1042
  /**
1043
   * Return the top N hot keys from the local detector, ordered by frequency.
1044
   * Useful when callers need more or fewer items than the configured TopK capacity.
1045
   *
1046
   * @param n the number of top keys to return
1047
   * @return the top N items, or an empty list if no detector is available
1048
   */
1049
  public List<Item> returnLocalTopNHotKeys(int n) {
1050
    return appHotKeyDetector != null ? appHotKeyDetector.listTopN(n) : List.of();
10✔
1051
  }
1052

1053
  /**
1054
   * Return a blocking queue of recently expelled hot keys from the local detector.
1055
   * Consumers can drain this queue to react to keys that are no longer hot.
1056
   *
1057
   * @return the expelled queue, or an empty queue if no detector is available
1058
   */
1059
  public BlockingQueue<Item> returnLocalExpelledHotKeys() {
1060
    return appHotKeyDetector != null ? appHotKeyDetector.expelled() : new LinkedBlockingQueue<>();
11✔
1061
  }
1062

1063
  /**
1064
   * Return the total number of data streams (accesses) tracked by the local detector.
1065
   *
1066
   * @return the total count, or {@code 0} if no detector is available
1067
   */
1068
  public long returnLocalTotalDataStreams() {
1069
    return appHotKeyDetector != null ? appHotKeyDetector.total() : 0L;
9✔
1070
  }
1071

1072
  /**
1073
   * Return the current top-K hot keys (key + count) from the local detector,
1074
   * ordered by frequency.
1075
   *
1076
   * @return the local top-K list, or an empty list if no detector is available;
1077
   *         the returned list is a point-in-time snapshot
1078
   */
1079
  public List<Item> returnLocalHotKeys() {
1080
    return appHotKeyDetector != null ? appHotKeyDetector.list() : List.of();
9✔
1081
  }
1082

1083
  /**
1084
   * Return the current top-K hot keys from the Worker-side global detector,
1085
   * ordered by frequency. These keys reflect cross-instance aggregated access
1086
   * counts and are updated via periodic reports from all application instances.
1087
   *
1088
   * @return the cluster-wide top-K list, or an empty list if no Worker is active;
1089
   *         the returned list is a point-in-time snapshot
1090
   */
1091
  public List<Item> returnWorkerHotKeys() {
1092
    return workerTopKAlgorithm != null ? workerTopKAlgorithm.list() : List.of();
9✔
1093
  }
1094

1095
  /**
1096
   * Return a blocking queue of recently expelled hot keys from the Worker-side
1097
   * global detector. Consumers can drain this queue to react to keys that are
1098
   * no longer considered cluster-wide hot.
1099
   *
1100
   * @return the expelled queue, or an empty queue if no Worker is active
1101
   */
1102
  public BlockingQueue<Item> returnWorkerExpelledHotKeys() {
1103
    return workerTopKAlgorithm != null ? workerTopKAlgorithm.expelled() : new LinkedBlockingQueue<>();
11✔
1104
  }
1105

1106
  /**
1107
   * Return the total number of data streams tracked by the Worker-side
1108
   * global detector.
1109
   *
1110
   * @return the total count, or {@code 0} if no Worker is active
1111
   */
1112
  public long returnWorkerTotalDataStreams() {
1113
    return workerTopKAlgorithm != null ? workerTopKAlgorithm.total() : 0L;
9✔
1114
  }
1115

1116
  //-------------------------------------------------------------------------------------
1117

1118
  /**
1119
   * Add a key pattern to the blacklist. Keys matching this pattern will be
1120
   * blocked from cache get/put operations (returns {@link Optional#empty()}).
1121
   * <p>The pattern is auto-detected by {@link RuleMatcher#of}: exact match,
1122
   * prefix (trailing {@code *}), wildcard (containing {@code *} or {@code ?}),
1123
   * or regex (prefixed with {@code regex:}).
1124
   *
1125
   * @param keyPattern the key pattern to blacklist
1126
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
1127
   */
1128
  public void addBlacklist(String keyPattern) {
1129
    requireCache();
2✔
1130
    hotKeyCache.addBlacklist(keyPattern);
4✔
1131
  }
1✔
1132

1133
  /**
1134
   * Batch variant of {@link #addBlacklist(String)}. Adds multiple key patterns
1135
   * to the blacklist.
1136
   *
1137
   * @param keyPatterns the key patterns to blacklist
1138
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
1139
   */
1140
  public void addBlacklist(Collection<String> keyPatterns) {
1141
    requireCache();
2✔
1142
    keyPatterns.forEach(hotKeyCache::addBlacklist);
8✔
1143
  }
1✔
1144

1145
  /**
1146
   * Remove a key pattern from the blacklist.
1147
   *
1148
   * @param keyPattern the key pattern to remove from the blacklist
1149
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
1150
   */
1151
  public void removeBlacklist(String keyPattern) {
1152
    requireCache();
2✔
1153
    hotKeyCache.unBlacklist(keyPattern);
4✔
1154
  }
1✔
1155

1156
  /**
1157
   * Batch variant of {@link #removeBlacklist(String)}. Removes multiple key
1158
   * patterns from the blacklist.
1159
   *
1160
   * @param keyPatterns the key patterns to remove from the blacklist
1161
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
1162
   */
1163
  public void removeBlacklist(Collection<String> keyPatterns) {
1164
    requireCache();
2✔
1165
    keyPatterns.forEach(hotKeyCache::unBlacklist);
8✔
1166
  }
1✔
1167

1168
  /**
1169
   * Add a key pattern to the whitelist. Keys matching this pattern will
1170
   * skip report recording (no Worker report sent) but still participate in
1171
   * normal cache get/put and local hot-key detection.
1172
   * <p>The pattern is auto-detected by {@link RuleMatcher#of}.
1173
   *
1174
   * @param keyPattern the key pattern to whitelist
1175
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
1176
   */
1177
  public void addWhitelist(String keyPattern) {
1178
    requireCache();
2✔
1179
    hotKeyCache.addWhitelist(keyPattern);
4✔
1180
  }
1✔
1181

1182
  /**
1183
   * Batch variant of {@link #addWhitelist(String)}. Adds multiple key patterns
1184
   * to the whitelist.
1185
   *
1186
   * @param keyPatterns the key patterns to whitelist
1187
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
1188
   */
1189
  public void addWhitelist(Collection<String> keyPatterns) {
1190
    requireCache();
2✔
1191
    keyPatterns.forEach(hotKeyCache::addWhitelist);
8✔
1192
  }
1✔
1193

1194
  /**
1195
   * Remove a key pattern from the whitelist.
1196
   *
1197
   * @param keyPattern the key pattern to remove from the whitelist
1198
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
1199
   */
1200
  public void removeWhitelist(String keyPattern) {
1201
    requireCache();
2✔
1202
    hotKeyCache.unWhitelist(keyPattern);
4✔
1203
  }
1✔
1204

1205
  /**
1206
   * Batch variant of {@link #removeWhitelist(String)}. Removes multiple key
1207
   * patterns from the whitelist.
1208
   *
1209
   * @param keyPatterns the key patterns to remove from the whitelist
1210
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
1211
   */
1212
  public void removeWhitelist(Collection<String> keyPatterns) {
1213
    requireCache();
2✔
1214
    keyPatterns.forEach(hotKeyCache::unWhitelist);
8✔
1215
  }
1✔
1216

1217
  /**
1218
   * Evaluate all rules against the given key and return the first matching action.
1219
   *
1220
   * @param cacheKey the key to evaluate
1221
   * @return the matching {@link Rule.RuleAction}, or {@code ALLOW} if no rule matches
1222
   *         or no cache is available (Worker-only mode)
1223
   */
1224
  public Rule.RuleAction evaluateRule(String cacheKey) {
1225
    if (hotKeyCache == null) {
3✔
1226
      return Rule.RuleAction.ALLOW;
2✔
1227
    }
1228
    return hotKeyCache.evaluateRule(cacheKey);
5✔
1229
  }
1230

1231
  /**
1232
   * Batch variant of {@link #evaluateRule(String)}. Evaluates all rules against
1233
   * the given keys and returns the first matching action for each.
1234
   *
1235
   * @param cacheKeys the keys to evaluate
1236
   * @return a map of key → matching {@link Rule.RuleAction} (or {@code ALLOW} if no rule matches)
1237
   */
1238
  public Map<String, Rule.RuleAction> evaluateRules(Collection<String> cacheKeys) {
1239
    Map<String, Rule.RuleAction> result = new HashMap<>();
4✔
1240
    cacheKeys.forEach(key -> {
5✔
1241
      Rule.RuleAction action = evaluateRule(key);
4✔
1242
      result.put(key, action);
5✔
1243
    });
1✔
1244

1245
    return result;
2✔
1246
  }
1247

1248
  /**
1249
   * Quickly check whether the given key is blocked by any blacklist rule.
1250
   * <p>
1251
   * Equivalent to {@code evaluateRule(key) == BLOCK}, provided as a convenience
1252
   * for guard clauses before expensive operations.
1253
   *
1254
   * @param cacheKey the key to check
1255
   * @return {@code true} if a blacklist rule matches the key, {@code false} if no
1256
   *         rule matches or no cache is available (Worker-only mode)
1257
   */
1258
  public boolean isBlacklisted(String cacheKey) {
1259
    if (hotKeyCache == null) {
3✔
1260
      return false;
2✔
1261
    }
1262
    return hotKeyCache.isBlacklisted(cacheKey);
5✔
1263
  }
1264

1265
  /**
1266
   * Batch variant of {@link #isBlacklisted(String)}. Checks multiple keys
1267
   * against blacklist rules.
1268
   *
1269
   * @param cacheKeys the keys to check
1270
   * @return a map of key → whether it is blocked by a blacklist rule
1271
   */
1272
  public Map<String, Boolean> isBlacklisted(Collection<String> cacheKeys) {
1273
    Map<String, Boolean> result = new HashMap<>();
4✔
1274
    cacheKeys.forEach(key -> {
5✔
1275
      boolean isBlacklisted = isBlacklisted(key);
4✔
1276
      result.put(key, isBlacklisted);
6✔
1277
    });
1✔
1278

1279
    return result;
2✔
1280
  }
1281

1282
  /**
1283
   * Quickly check whether the given key is whitelisted (skips Worker reporting).
1284
   * <p>
1285
   * Equivalent to {@code evaluateRule(key) == ALLOW_NO_REPORT}, provided as a
1286
   * convenience for debugging and monitoring.
1287
   *
1288
   * @param cacheKey the key to check
1289
   * @return {@code true} if a whitelist rule matches the key, {@code false} if no
1290
   *         rule matches or no cache is available (Worker-only mode)
1291
   */
1292
  public boolean isWhitelisted(String cacheKey) {
1293
    if (hotKeyCache == null) {
3✔
1294
      return false;
2✔
1295
    }
1296
    return hotKeyCache.isWhitelisted(cacheKey);
5✔
1297
  }
1298

1299
  /**
1300
   * Batch variant of {@link #isWhitelisted(String)}. Checks multiple keys
1301
   * against whitelist rules.
1302
   *
1303
   * @param cacheKeys the keys to check
1304
   * @return a map of key → whether it is whitelisted (skips Worker reporting)
1305
   */
1306
  public Map<String, Boolean> isWhitelisted(Collection<String> cacheKeys) {
1307
    Map<String, Boolean> result = new HashMap<>();
4✔
1308
    cacheKeys.forEach(key -> {
5✔
1309
      boolean isWhitelisted = isWhitelisted(key);
4✔
1310
      result.put(key, isWhitelisted);
6✔
1311
    });
1✔
1312

1313
    return result;
2✔
1314
  }
1315

1316
  /**
1317
   * Return a snapshot of all current rules in evaluation order.
1318
   *
1319
   * @return list of rules, or an empty list if no cache is available;
1320
   *         the returned list is unmodifiable
1321
   */
1322
  public List<Rule> getAllRules() {
1323
    if (hotKeyCache == null) {
3✔
1324
      return List.of();
2✔
1325
    }
1326
    return hotKeyCache.getAllRules();
4✔
1327
  }
1328

1329
  /**
1330
   * Remove all blacklist and whitelist rules.
1331
   *
1332
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
1333
   */
1334
  public void clearAllRules() {
1335
    requireCache();
2✔
1336
    hotKeyCache.clearAllRules();
3✔
1337
  }
1✔
1338

1339
  /**
1340
   * Broadcast all local rules to peer instances via the sync exchange.
1341
   * Useful for initial synchronization when a new instance joins the cluster.
1342
   *
1343
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
1344
   */
1345
  public void broadcastAllLocalRulesManually() {
1346
    requireCache();
2✔
1347
    hotKeyCache.broadcastAllLocalRulesManually();
3✔
1348
  }
1✔
1349

1350
  /**
1351
   * Verify that the cache is available; throws {@link UnsupportedOperationException}
1352
   * when running in Worker-only mode where no app-side cache exists.
1353
   */
1354
  private void requireCache() {
1355
    if (hotKeyCache == null) {
3✔
1356
      throw new UnsupportedOperationException(
5✔
1357
        "HotKey cache is not available in Worker-only mode. " + "Run the Worker as a separate Spring Boot process."
1358
      );
1359
    }
1360
  }
1✔
1361
}
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