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

Hyshmily / hotkey / 28735434673

05 Jul 2026 08:55AM UTC coverage: 90.421% (+0.02%) from 90.4%
28735434673

push

github

Hyshmily
perf : simplify the code

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

1295 of 1498 branches covered (86.45%)

Branch coverage included in aggregate %.

121 of 134 new or added lines in 11 files covered. (90.3%)

2 existing lines in 1 file now uncovered.

3689 of 4014 relevant lines covered (91.9%)

4.19 hits per line

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

81.15
/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.fluentAPI.HotKeyReadQuery;
21
import io.github.hyshmily.hotkey.cache.fluentAPI.HotKeyWriteCommand;
22
import io.github.hyshmily.hotkey.exception.HotKeyBlockedException;
23
import io.github.hyshmily.hotkey.exception.HotKeyModeException;
24
import io.github.hyshmily.hotkey.hotkeydetector.HotKeyDetector;
25
import io.github.hyshmily.hotkey.hotkeydetector.heavykeeper.Item;
26
import io.github.hyshmily.hotkey.hotkeydetector.heavykeeper.TopK;
27
import io.github.hyshmily.hotkey.model.HotKeyCacheStats;
28
import io.github.hyshmily.hotkey.rule.Rule;
29
import io.github.hyshmily.hotkey.rule.RuleMatcher;
30
import io.github.hyshmily.hotkey.sync.distributedlock.AutoReleaseLock;
31
import io.github.hyshmily.hotkey.sync.distributedlock.LockProvider;
32
import io.github.hyshmily.hotkey.util.HotKeyThreadFactory;
33
import java.util.*;
34
import java.util.concurrent.*;
35
import java.util.function.Function;
36
import java.util.function.Supplier;
37
import org.jspecify.annotations.Nullable;
38
import org.springframework.beans.factory.DisposableBean;
39

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

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

89
  /**
90
   * Scheduler for timed background refresh tasks (lazily initialized).
91
   */
92
  private volatile ScheduledExecutorService refreshScheduler;
93

94
  /**
95
   * Per-key scheduled refresh futures, keyed by cache key.
96
   */
97
  private final ConcurrentHashMap<String, ScheduledFuture<?>> refreshFutures;
98

99
  /**
100
   * Create a HotKey facade with all three optional components.
101
   *
102
   * <p>Each parameter may be {@code null} depending on the deployment mode:
103
   * <ul>
104
   *   <li><b>App-only:</b> {@code hotKeyCache} and {@code appHotKeyDetector} are present,
105
   *       {@code workerTopKAlgorithm} is {@code null}</li>
106
   *   <li><b>Worker-only:</b> only {@code workerTopKAlgorithm} is present</li>
107
   *   <li><b>Coexistence:</b> all three are present</li>
108
   * </ul>
109
   *
110
   * @param hotKeyCache         the cache orchestrator (maybe {@code null} in Worker-only mode)
111
   * @param appHotKeyDetector   the app-side local TopK detector (maybe {@code null} in Worker-only mode)
112
   * @param workerTopKAlgorithm the Worker-side global TopK detector (maybe {@code null} in app-only mode)
113
   */
114
  public HotKey(HotKeyCache hotKeyCache, HotKeyDetector appHotKeyDetector, TopK workerTopKAlgorithm) {
115
    this(hotKeyCache, appHotKeyDetector, workerTopKAlgorithm, null);
6✔
116
  }
1✔
117

118
  /**
119
   * Create a HotKey facade with an optional distributed lock provider.
120
   *
121
   * <p>Each parameter may be {@code null} depending on the deployment mode:
122
   * <ul>
123
   *   <li><b>App-only:</b> {@code hotKeyCache} and {@code appHotKeyDetector} are present,
124
   *       {@code workerTopKAlgorithm} and {@code lockProvider} may be absent</li>
125
   *   <li><b>Worker-only:</b> only {@code workerTopKAlgorithm} is present</li>
126
   *   <li><b>Coexistence:</b> all four may be present</li>
127
   * </ul>
128
   *
129
   * @param hotKeyCache         the cache orchestrator (maybe {@code null} in Worker-only mode)
130
   * @param appHotKeyDetector   the app-side local TopK detector (maybe {@code null} in Worker-only mode)
131
   * @param workerTopKAlgorithm the Worker-side global TopK detector (maybe {@code null} in app-only mode)
132
   * @param lockProvider        the distributed lock provider (maybe {@code null} when no Redis)
133
   */
134
  public HotKey(
135
    HotKeyCache hotKeyCache,
136
    HotKeyDetector appHotKeyDetector,
137
    TopK workerTopKAlgorithm,
138
    LockProvider lockProvider
139
  ) {
2✔
140
    this.hotKeyCache = hotKeyCache;
3✔
141
    this.appHotKeyDetector = appHotKeyDetector;
3✔
142
    this.workerTopKAlgorithm = workerTopKAlgorithm;
3✔
143
    this.lockProvider = lockProvider;
3✔
144
    this.refreshFutures = new ConcurrentHashMap<>();
5✔
145
  }
1✔
146

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

176
  /**
177
   * Create a fluent write command for the given key.
178
   *
179
   * <p>Returns a {@link HotKeyWriteCommand} builder that lets you configure
180
   * TTL overrides before executing a write-through, invalidate-before-write,
181
   * or plain invalidation.
182
   *
183
   * <p>Example:
184
   * <pre>
185
   *   hotKey.write("user:42")
186
   *       .withHardTtl(30_000)
187
   *       .putThrough(newValue, dbWriter);
188
   * </pre>
189
   *
190
   * @param cacheKey the cache key to operate on
191
   * @param <T>      the value type
192
   * @return a new {@link HotKeyWriteCommand} instance (single-use)
193
   */
194
  public <T> HotKeyWriteCommand<T> write(String cacheKey) {
195
    return new HotKeyWriteCommand<>(this, cacheKey);
6✔
196
  }
197

198
  /**
199
   * Convenience shorthand for {@link #get get(cacheKey, loader).orElse(null)}.
200
   *
201
   * <p>Loads the value via the supplier on cache miss, using configured default TTLs.
202
   * Returns {@code null} when the loader itself returns {@code null}.
203
   *
204
   * @param cacheKey the key to retrieve
205
   * @param loader   the value supplier for cache misses
206
   * @param <V>      the value type
207
   * @return the cached or loaded value, or {@code null} if the loader returned {@code null}
208
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
209
   * @throws HotKeyBlockedException when the key matches a blacklist rule
210
   */
211
  public <V> V computeIfAbsent(String cacheKey, Supplier<V> loader) {
212
    return get(cacheKey, loader).orElse(null);
7✔
213
  }
214

215
  /**
216
   * Batch variant of {@link #computeIfAbsent(String, Supplier)}. Iterates over
217
   * the given keys and loads each one via the provided function on cache miss.
218
   *
219
   * @param cachKeys the keys to retrieve
220
   * @param loader   the per-key value supplier for cache misses
221
   * @param <V>      the value type
222
   * @return a map of key → loaded value (only keys whose loader returned non-null are included)
223
   */
224
  public <V> Map<String, V> computeIfAbsent(Collection<String> cachKeys, Function<String, V> loader) {
225
    Map<String, V> result = new HashMap<>();
4✔
226
    cachKeys.forEach(key -> {
6✔
227
      V value = computeIfAbsent(key, () -> loader.apply(key));
7✔
228
      result.put(key, value);
5✔
229
    });
1✔
230

231
    return result;
2✔
232
  }
233

234
  /**
235
   * Convenience shorthand with explicit hard TTL.
236
   *
237
   * @param cacheKey the key to retrieve
238
   * @param loader   the value supplier for cache misses
239
   * @param hardTtlMs hard TTL override (0 = use configured default; {@link Long#MAX_VALUE} for permanent entry)
240
   * @param <V>      the value type
241
   * @return the cached or loaded value, or {@code null} if the loader returned {@code null}
242
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
243
   * @throws HotKeyBlockedException when the key matches a blacklist rule
244
   * @see #get(String, Supplier, long, long)
245
   */
246
  public <V> V computeIfAbsent(String cacheKey, Supplier<V> loader, long hardTtlMs) {
247
    return get(cacheKey, loader, hardTtlMs, 0).orElse(null);
9✔
248
  }
249

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

267
    return result;
2✔
268
  }
269

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

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

310
    return result;
2✔
311
  }
312

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

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

346
    return result;
2✔
347
  }
348

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

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

385
    return result;
2✔
386
  }
387

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

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

427
    return result;
2✔
428
  }
429

430
  /**
431
   * Look up a cached value without loading or triggering hot-key detection.
432
   *
433
   * @param cacheKey the key to look up
434
   * @param <T>      the value type
435
   * @return an {@link Optional} containing the raw value if present
436
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
437
   */
438
  public <T> Optional<T> peek(String cacheKey) {
439
    requireAppCache("peek");
3✔
440
    return hotKeyCache.peek(cacheKey);
5✔
441
  }
442

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

458
    return result;
2✔
459
  }
460

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

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

497
  /**
498
   * Get with soft-expire (stale-while-revalidate). Returns cached value immediately
499
   * even if soft TTL expired, while triggering async refresh in background.
500
   *
501
   * @param cacheKey the key to retrieve
502
   * @param reader   the value supplier for cache misses / refreshes
503
   * @param <T>      the value type
504
   * @return an {@link Optional} containing the cached (possibly stale) or loaded value
505
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
506
   * @throws HotKeyBlockedException when the key matches a blacklist rule
507
   */
508
  public <T> Optional<T> getWithSoftExpire(String cacheKey, Supplier<T> reader) {
509
    requireAppCache("get");
3✔
510
    requireAppDetector("get");
3✔
511
    return hotKeyCache.getWithSoftExpire(cacheKey, reader);
6✔
512
  }
513

514
  /**
515
   * Get with soft-expire and explicit soft TTL override.
516
   *
517
   * @param cacheKey  the key to retrieve
518
   * @param reader    the value supplier for cache misses / refreshes
519
   * @param softTtlMs soft TTL override (0 = use configured default)
520
   * @param <T>       the value type
521
   * @return an {@link Optional} containing the cached (possibly stale) or loaded value
522
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
523
   * @throws HotKeyBlockedException when the key matches a blacklist rule
524
   */
525
  public <T> Optional<T> getWithSoftExpire(String cacheKey, Supplier<T> reader, long softTtlMs) {
526
    requireAppCache("get");
3✔
527
    requireAppDetector("get");
3✔
528
    return hotKeyCache.getWithSoftExpire(cacheKey, reader, softTtlMs);
7✔
529
  }
530

531
  /**
532
   * Get with soft-expire and explicit hard/soft TTL overrides.
533
   *
534
   * @param cacheKey  the key to retrieve
535
   * @param reader    the value supplier for cache misses / refreshes
536
   * @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})
537
   * @param softTtlMs soft TTL override (0 = use configured default)
538
   * @param <T>       the value type
539
   * @return an {@link Optional} containing the cached (possibly stale) or loaded value
540
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
541
   * @throws HotKeyBlockedException when the key matches a blacklist rule
542
   */
543
  public <T> Optional<T> getWithSoftExpire(String cacheKey, Supplier<T> reader, long hardTtlMs, long softTtlMs) {
544
    requireAppCache("get");
3✔
545
    requireAppDetector("get");
3✔
546
    return hotKeyCache.getWithSoftExpire(cacheKey, reader, hardTtlMs, softTtlMs);
8✔
547
  }
548

549
  /**
550
   * Invalidate a single key from L1 and broadcast REFRESH to peers.
551
   * The next {@link #get} will re-fetch from the reader.
552
   *
553
   * @param cacheKey the key to invalidate
554
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
555
   */
556
  public void invalidate(String cacheKey) {
557
    requireAppCache("invalidate");
3✔
558
    hotKeyCache.invalidate(cacheKey);
4✔
559
  }
1✔
560

561
  /**
562
   * Invalidate one or more 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
   * @see #invalidate(String)
567
   */
568
  public void invalidateAll(String... cacheKeys) {
569
    invalidateAll(Arrays.asList(cacheKeys));
4✔
570
  }
1✔
571

572
  /**
573
   * Invalidate a collection of keys from L1 and broadcast INVALIDATE for each.
574
   *
575
   * @param cacheKeys the keys to invalidate
576
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
577
   */
578
  public void invalidateAll(Collection<String> cacheKeys) {
579
    requireAppCache("invalidateAll");
3✔
580
    hotKeyCache.invalidateAll(cacheKeys);
4✔
581
  }
1✔
582

583
  /**
584
   * Convenience shorthand for {@link #evictLocal(Collection)} — evicts a single
585
   * key from the local cache without broadcasting.
586
   *
587
   * @param cacheKeys the key to evict locally
588
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
589
   */
590
  public void evictLocal(String cacheKeys) {
591
    evictLocal(Collections.singletonList(cacheKeys));
4✔
592
  }
1✔
593

594
  /**
595
   * Evict keys from the local cache only, without broadcasting to other
596
   * instances and without bumping version numbers.
597
   *
598
   * <p>Useful for emergency local cleanup, testing, or module offline
599
   * scenarios where only the current node needs to be cleared.
600
   *
601
   * @param cacheKeys the keys to evict locally
602
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
603
   */
604
  public void evictLocal(Collection<String> cacheKeys) {
605
    requireAppCache("evictLocal");
3✔
606
    hotKeyCache.evictLocal(cacheKeys);
4✔
607
  }
1✔
608

609
  /**
610
   * Write-through: execute the writer, then update L1 and broadcast.
611
   * Uses effective hard/soft TTL from configuration.
612
   *
613
   * @param cacheKey the key to write
614
   * @param value    the value to cache
615
   * @param writer   the data-source mutation to execute before caching
616
   * @param <T>      the value type
617
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
618
   */
619
  public <T> void putThrough(String cacheKey, T value, Runnable writer) {
620
    requireAppCache("putThrough");
3✔
621
    hotKeyCache.putThrough(cacheKey, value, writer);
6✔
622
  }
1✔
623

624
  /**
625
   * Write-through with explicit TTL overrides.
626
   * Pass 0 to use the configured default for that TTL type.
627
   *
628
   * @param cacheKey  the key to write
629
   * @param value     the value to cache
630
   * @param writer    the data-source mutation to execute before caching
631
   * @param hardTtlMs hard TTL override (0 = use configured default; {@link Long#MAX_VALUE} for permanent entry — no hard TTL eviction)
632
   * @param softTtlMs soft TTL override (0 = use configured default)
633
   * @param <T>       the value type
634
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
635
   */
636
  public <T> void putThrough(String cacheKey, T value, Runnable writer, long hardTtlMs, long softTtlMs) {
637
    requireAppCache("putThrough");
3✔
638
    hotKeyCache.putThrough(cacheKey, value, writer, hardTtlMs, softTtlMs);
8✔
639
  }
1✔
640

641
  /**
642
   * Write a value directly into the local L1 cache without version bump,
643
   * without broadcast, without hot-key detection, and without reporting.
644
   * <p>
645
   * Existing entry metadata is preserved.  If no entry exists, a fresh
646
   * {@link io.github.hyshmily.hotkey.model.CacheEntry} is created with {@link io.github.hyshmily.hotkey.model.KeyState#NORMAL}.
647
   * <p>
648
   * Never throws {@code UnsupportedOperationException} in Worker-only mode;
649
   * silently no-ops instead.
650
   *
651
   * @param cacheKey the key to store
652
   * @param value    the value to cache
653
   */
654
  public void putLocal(String cacheKey, Object value) {
655
    if (hotKeyCache == null) {
3!
656
      return; // Worker-only mode: silently no-op (per Javadoc)
×
657
    }
658
    putLocal(cacheKey, value, 0, 0);
6✔
659
  }
1✔
660

661
  /**
662
   * Batch variant of {@link #putLocal(String, Object)}. Writes all entries into
663
   * L1 without version bump, broadcast, hot-key detection, or reporting.
664
   *
665
   * <p>In Worker-only mode this method silently no-ops.
666
   *
667
   * @param entries a map of key → value to store locally
668
   */
669
  public void putLocal(Map<String, Object> entries) {
670
    if (hotKeyCache == null) {
3!
671
      return; // Worker-only mode: silently no-op
×
672
    }
673
    entries.forEach(this::putLocal);
4✔
674
  }
1✔
675

676
  /**
677
   * Write a value directly into the local L1 cache with explicit TTL overrides.
678
   * Delegates to {@link #putLocal(String, Object)} semantics — no version bump,
679
   * no broadcast, no hot-key detection, and no reporting.
680
   * <p>
681
   * Pass {@code 0} for either TTL to use the configured default.
682
   *
683
   * <p>In Worker-only mode this method silently no-ops.
684
   *
685
   * @param cacheKey  the key to store
686
   * @param value     the value to cache
687
   * @param hardTtlMs hard TTL override (0 = use configured default)
688
   * @param softTtlMs soft TTL override (0 = use configured default)
689
   */
690
  public void putLocal(String cacheKey, Object value, long hardTtlMs, long softTtlMs) {
691
    if (hotKeyCache == null) {
3!
692
      return; // Worker-only mode: silently no-op
×
693
    }
694
    hotKeyCache.putLocal(cacheKey, value, hardTtlMs, softTtlMs);
7✔
695
  }
1✔
696

697
  /**
698
   * Execute a mutation, then invalidate L1 and broadcast.
699
   * Next {@link #get} will re-fetch from the reader.
700
   *
701
   * @param cacheKey the key to invalidate after mutation
702
   * @param mutation the mutation to execute
703
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
704
   */
705
  public void putBeforeInvalidate(String cacheKey, Runnable mutation) {
706
    requireAppCache("putBeforeInvalidate");
3✔
707
    hotKeyCache.putBeforeInvalidate(cacheKey, mutation);
5✔
708
  }
1✔
709

710
  /**
711
   * Batch variant of {@link #putBeforeInvalidate(String, Runnable)}. Executes
712
   * all mutations and invalidates the corresponding keys from L1 with broadcast.
713
   *
714
   * <p><b>Batch execution:</b> Iterates sequentially; for large batches
715
   * consider parallelizing in caller code.
716
   *
717
   * @param mutations a map of key → mutation to execute
718
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
719
   */
720
  public void putBeforeInvalidateAll(Map<String, Runnable> mutations) {
721
    requireAppCache("putBeforeInvalidateAll");
3✔
722
    mutations.forEach(this::putBeforeInvalidate);
4✔
723
  }
1✔
724

725
  /**
726
   * Estimated number of entries currently in the L1 cache.
727
   * <p>
728
   * This is a lightweight, best-effort estimate from the underlying Caffeine
729
   * cache, suitable for monitoring dashboards and capacity planning.
730
   *
731
   * @return estimated entry count, or {@code 0} in Worker-only mode
732
   */
733
  public long estimatedSize() {
734
    if (hotKeyCache == null) {
3✔
735
      return 0L;
2✔
736
    }
737
    return hotKeyCache.estimatedSize();
4✔
738
  }
739

740
  /**
741
   * Return a snapshot of basic L1 cache statistics.
742
   * <p>
743
   * Hit/miss/eviction counters are populated only when Caffeine's
744
   * {@code recordStats()} is enabled. {@code estimatedSizeOfKeysCount} is always
745
   * available.
746
   *
747
   * @return a {@link HotKeyCacheStats} record, or {@code null} in Worker-only mode;
748
   *         hit/miss counters are {@code 0} if stats recording is not enabled
749
   */
750
  public HotKeyCacheStats stats() {
751
    if (hotKeyCache == null) {
3✔
752
      return null;
2✔
753
    }
754
    return hotKeyCache.stats();
4✔
755
  }
756

757
  /**
758
   * Return the underlying Caffeine cache for direct access.
759
   *
760
   * <p>Useful for Caffeine-specific operations such as {@code asMap()},
761
   * {@code policy()}, and {@code cleanUp()}. Use with caution — bypassing
762
   * the HotKey orchestration layer can lead to inconsistent cache state.
763
   *
764
   * @return the raw Caffeine {@link Cache} instance
765
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
766
   */
767
  public Cache<String, Object> getLocalCache() {
768
    requireAppCache("getLocalCache");
3✔
769
    return hotKeyCache.getLocalCache();
4✔
770
  }
771

772
  /**
773
   * Invalidate all entries from the L1 cache without broadcasting.
774
   * <p>
775
   * This is an emergency flush — all cached values are removed immediately.
776
   * No cross-instance sync messages are sent. Subsequent {@link #get} calls
777
   * will reload data from the reader.
778
   * <p>
779
   * Use with caution: flushing the local cache increases load on the backend
780
   * until entries are re-cached.
781
   *
782
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
783
   */
784
  public void invalidateAllLocal() {
785
    requireAppCache("invalidateAllLocal");
3✔
786
    hotKeyCache.invalidateAllLocal();
3✔
787
  }
1✔
788

789
  /**
790
   * Attempt to acquire a distributed lock with the provider's default
791
   * retry counts.
792
   *
793
   * <p>Equivalent to {@code tryLock(key, expire, unit, -1, -1, -1)}.
794
   *
795
   * <p>Usage:
796
   * <pre>{@code
797
   * try (AutoReleaseLock lock = hotKey.tryLock("order:42", 5, TimeUnit.SECONDS)) {
798
   *     if (lock != null) {
799
   *         // critical section
800
   *     }
801
   * }
802
   * }</pre>
803
   *
804
   * @param key    the lock key
805
   * @param expire the time-to-live for the lock
806
   * @param unit   the time unit for {@code expire}
807
   * @return a lock handle if acquired, or {@code null} if the lock is held
808
   *         by another caller or the provider is unavailable
809
   */
810
  @Nullable
811
  public AutoReleaseLock tryLock(String key, long expire, TimeUnit unit) {
812
    return lockProvider != null ? lockProvider.tryLock(key, expire, unit) : null;
×
813
  }
814

815
  /**
816
   * Acquire a distributed lock, execute the action, and release
817
   * with the provider's default retry counts.
818
   *
819
   * <p>Equivalent to {@code tryLockAndRun(key, expire, unit, action, -1, -1, -1)}.
820
   *
821
   * @param key    the lock key
822
   * @param expire the time-to-live for the lock
823
   * @param unit   the time unit for {@code expire}
824
   * @param action the action to execute while holding the lock
825
   * @return {@code true} if the lock was acquired and the action ran,
826
   *         {@code false} otherwise
827
   */
828
  public boolean tryLockAndRun(String key, long expire, TimeUnit unit, Runnable action) {
829
    Objects.requireNonNull(action, "action must not be null");
×
830
    if (lockProvider == null) {
×
831
      return false;
×
832
    }
833
    try (AutoReleaseLock lock = tryLock(key, expire, unit)) {
×
834
      if (lock != null) {
×
835
        action.run();
×
836
        return true;
×
837
      }
838
      return false;
×
839
    }
×
840
  }
841

842
  /**
843
   * Attempt to acquire a distributed lock with explicit retry counts.
844
   *
845
   * <p>Any negative count falls back to the provider's configured default.
846
   * Returns {@code null} when the lock is held by another caller or when
847
   * no Redis is available (graceful degradation).
848
   *
849
   * @param key                  the lock key
850
   * @param expire               the time-to-live for the lock
851
   * @param unit                 the time unit for {@code expire}
852
   * @param tryLockLockCount     the number of {@code SET NX} retries;
853
   *                             negative → default
854
   * @param tryLockInquiryCount  the number of {@code GET} inquiries;
855
   *                             negative → default
856
   * @param tryLockUnlockCount   the number of {@code DEL} retries;
857
   *                             negative → default
858
   * @return a lock handle if acquired, or {@code null} if the lock is held
859
   *         by another caller or the provider is unavailable
860
   */
861
  @Nullable
862
  public AutoReleaseLock tryLock(
863
    String key,
864
    long expire,
865
    TimeUnit unit,
866
    int tryLockLockCount,
867
    int tryLockInquiryCount,
868
    int tryLockUnlockCount
869
  ) {
870
    return lockProvider != null
×
871
      ? lockProvider.tryLock(key, expire, unit, tryLockLockCount, tryLockInquiryCount, tryLockUnlockCount)
×
872
      : null;
×
873
  }
874

875
  /**
876
   * Acquire a distributed lock with explicit retry counts, execute the
877
   * action, and release.
878
   *
879
   * <p>Any negative count falls back to the provider's configured default.
880
   *
881
   * @param key                  the lock key
882
   * @param expire               the time-to-live for the lock
883
   * @param unit                 the time unit for {@code expire}
884
   * @param action               the action to execute while holding the lock
885
   * @param tryLockLockCount     the number of {@code SET NX} retries;
886
   *                             negative → default
887
   * @param tryLockInquiryCount  the number of {@code GET} inquiries;
888
   *                             negative → default
889
   * @param tryLockUnlockCount   the number of {@code DEL} retries;
890
   *                             negative → default
891
   * @return {@code true} if the lock was acquired and the action ran,
892
   *         {@code false} otherwise
893
   */
894
  public boolean tryLockAndRun(
895
    String key,
896
    long expire,
897
    TimeUnit unit,
898
    Runnable action,
899
    int tryLockLockCount,
900
    int tryLockInquiryCount,
901
    int tryLockUnlockCount
902
  ) {
903
    Objects.requireNonNull(action, "action must not be null");
×
904
    try (AutoReleaseLock lock = tryLock(key, expire, unit, tryLockLockCount, tryLockInquiryCount, tryLockUnlockCount)) {
×
905
      if (lock != null) {
×
906
        action.run();
×
907
        return true;
×
908
      }
909
      return false;
×
910
    }
×
911
  }
912

913
  /**
914
   * Lazy-initialize the refresh scheduler.
915
   * Uses double-checked locking for thread safety.
916
   */
917
  private ScheduledExecutorService getScheduler() {
918
    ScheduledExecutorService s = refreshScheduler;
3✔
919
    if (s == null) {
2✔
920
      synchronized (this) {
4✔
921
        s = refreshScheduler;
3✔
922
        if (s == null) {
2!
923
          s = Executors.newScheduledThreadPool(2, new HotKeyThreadFactory("hotkey-refresh"));
7✔
924
          refreshScheduler = s;
3✔
925
        }
926
      }
3✔
927
    }
928
    return s;
2✔
929
  }
930

931
  /**
932
   * Register a timed background refresh for the given key.
933
   * <p>
934
   * Schedules {@link #getWithSoftExpire} at an interval slightly longer than
935
   * the soft TTL ({@code softTtlMs × 1.1}) to guarantee the entry is stale
936
   * when the timer fires, ensuring every cycle triggers an async refresh via
937
   * {@code triggerBackgroundRefresh}.
938
   * <p>
939
   * The scheduler is created lazily on first registration (no threads before
940
   * first use). Uses a 2-thread pool to prevent a slow supplier from blocking
941
   * other refresh keys.
942
   * <p>
943
   * If a previous registration exists for the same key, it is cancelled and
944
   * replaced.
945
   *
946
   * @param key       the cache key to refresh
947
   * @param supplier  the value supplier for refresh
948
   * @param hardTtlMs hard TTL override (0 = use configured default)
949
   * @param softTtlMs soft TTL override (also used as the base interval)
950
   * @param <T>       the value type
951
   */
952
  public <T> void registerRefresh(String key, Supplier<T> supplier, long hardTtlMs, long softTtlMs) {
953
    long intervalMs = (long) (softTtlMs * 1.1);
6✔
954
    if (intervalMs < 1) {
4!
955
      intervalMs = 1;
×
956
    }
957
    ScheduledFuture<?> prev = refreshFutures.put(
7✔
958
      key,
959
      getScheduler().scheduleWithFixedDelay(
11✔
960
        () -> getWithSoftExpire(key, supplier, hardTtlMs, softTtlMs),
8✔
961
        intervalMs,
962
        intervalMs,
963
        TimeUnit.MILLISECONDS
964
      )
965
    );
966
    if (prev != null) {
2✔
967
      prev.cancel(false);
4✔
968
    }
969
  }
1✔
970

971
  /**
972
   * Update the refresh configuration for a key at runtime.
973
   * <p>
974
   * Cancels any existing scheduled task and registers a new one with the
975
   * given TTLs. If no prior registration exists, this behaves identically
976
   * to {@link #registerRefresh}.
977
   *
978
   * @param key       the cache key
979
   * @param supplier  the value supplier for refresh
980
   * @param hardTtlMs new hard TTL override
981
   * @param softTtlMs new soft TTL override (also used as the base interval)
982
   * @param <T>       the value type
983
   */
984
  public <T> void updateRefresh(String key, Supplier<T> supplier, long hardTtlMs, long softTtlMs) {
985
    registerRefresh(key, supplier, hardTtlMs, softTtlMs);
×
986
  }
×
987

988
  /**
989
   * Cancel the timed refresh for the given key.
990
   *
991
   * @param key the cache key to stop refreshing
992
   */
993
  public void unregisterRefresh(String key) {
994
    ScheduledFuture<?> f = refreshFutures.remove(key);
6✔
995
    if (f != null) {
2!
996
      f.cancel(false);
4✔
997
    }
998
  }
1✔
999

1000
  /**
1001
   * Cancel all timed refreshes and shut down the refresh scheduler.
1002
   * Called automatically by Spring when this bean is destroyed.
1003
   */
1004
  @Override
1005
  public void destroy() {
1006
    refreshFutures.values().forEach(f -> f.cancel(false));
10✔
1007
    refreshFutures.clear();
3✔
1008
    ScheduledExecutorService s = refreshScheduler;
3✔
1009
    if (s != null) {
2✔
1010
      s.shutdown();
2✔
1011
    }
1012
  }
1✔
1013

1014
  //------------------------------------------------------------------------
1015

1016
  /**
1017
   * Check whether a key is currently tracked as a local hot key in L1.
1018
   *
1019
   * @param cacheKey the key to inspect
1020
   * @return {@code true} if the key exists in L1 with {@link io.github.hyshmily.hotkey.model.KeyState#HOT}
1021
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
1022
   */
1023
  public boolean isLocalHotKey(String cacheKey) {
1024
    requireAppCache("isLocalHotKey");
3✔
1025
    requireAppDetector("isLocalHotKey");
3✔
1026
    return hotKeyCache.isLocalHotKey(cacheKey);
5✔
1027
  }
1028

1029
  /**
1030
   * Batch variant of {@link #isLocalHotKey(String)}. Returns a map of key →
1031
   * hot status for all given keys.
1032
   *
1033
   * <p><b>Batch execution:</b> Iterates sequentially; for large batches
1034
   * consider parallelizing in caller code.
1035
   *
1036
   * @param cacheKeys the keys to inspect
1037
   * @return a map of key → whether it is a local hot key
1038
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
1039
   */
1040
  public Map<String, Boolean> areLocalHotKeys(Collection<String> cacheKeys) {
1041
    Map<String, Boolean> result = new HashMap<>();
4✔
1042
    cacheKeys.forEach(key -> result.put(key, isLocalHotKey(key)));
14✔
1043
    return result;
2✔
1044
  }
1045

1046
  /**
1047
   * Increment the local TopK detector directly, bypassing the buffer and the
1048
   * report-to-Worker path. Useful for bulk-loading historical access patterns
1049
   * or correcting frequency counts.
1050
   *
1051
   * @param cacheKey the key to record
1052
   * @param count    the number of accesses to add
1053
   */
1054
  public void notifyLocalDetectorDirect(String cacheKey, int count) {
1055
    requireAppDetector("notifyLocalDetectorDirect");
3✔
1056
    appHotKeyDetector.addDirect(cacheKey, count);
6✔
1057
  }
1✔
1058

1059
  /**
1060
   * Batch-increment the local TopK detector directly, bypassing buffer and reports.
1061
   *
1062
   * @param keyCounts a map of key → access count
1063
   */
1064
  public void notifyLocalDetectorDirect(Map<String, Long> keyCounts) {
1065
    requireAppDetector("notifyLocalDetectorDirect");
3✔
1066
    appHotKeyDetector.addDirect(keyCounts);
5✔
1067
  }
1✔
1068

1069
  /**
1070
   * Notify the local TopK detector that a key was accessed, without triggering
1071
   * a report to the Worker. Used by {@code @Intercept} path to keep the local
1072
   * frequency sketch accurate without flooding the Worker with reports.
1073
   * Null keys are silently ignored.
1074
   *
1075
   * @param cacheKey the accessed key (maybe {@code null}, silently ignored)
1076
   */
1077
  public void notifyLocalDetector(String cacheKey) {
1078
    requireAppDetector("notifyLocalDetector");
3✔
1079
    appHotKeyDetector.add(cacheKey);
4✔
1080
  }
1✔
1081

1082
  /**
1083
   * Notify the local detector of key access with a custom delta, routing
1084
   * through the buffered counter. Null keys are silently ignored.
1085
   *
1086
   * @param cacheKey the accessed key (maybe {@code null}, silently ignored)
1087
   * @param count    the number of accesses to record
1088
   */
1089
  public void notifyLocalDetector(String cacheKey, long count) {
1090
    requireAppDetector("notifyLocalDetector");
3✔
1091
    appHotKeyDetector.add(cacheKey, count);
5✔
1092
  }
1✔
1093

1094
  /**
1095
   * Batch-notify the local detector of multiple key accesses, routing through
1096
   * the buffered counter. Null keys in the map are silently ignored.
1097
   *
1098
   * @param keyCounts a map of key → access count
1099
   */
1100
  public void notifyLocalDetector(Map<String, Long> keyCounts) {
1101
    requireAppDetector("notifyLocalDetector");
3✔
1102
    appHotKeyDetector.add(keyCounts);
4✔
1103
  }
1✔
1104

1105
  /**
1106
   * Evict the given key locally, then load and cache via the supplier.
1107
   * Uses default TTLs. No broadcast is sent for the eviction.
1108
   *
1109
   * @param cacheKey the key to refresh
1110
   * @param loader   the value supplier
1111
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
1112
   */
1113
  public void refresh(String cacheKey, Supplier<?> loader) {
1114
    requireAppCache("refresh");
3✔
1115
    evictLocal(cacheKey);
3✔
1116
    putThrough(cacheKey, loader.get(), () -> {});
6✔
1117
  }
1✔
1118

1119
  /**
1120
   * Batch variant of {@link #refresh(String, Supplier)}. Refreshes all entries
1121
   * by evicting locally then loading and caching via the provided suppliers.
1122
   *
1123
   * @param loaders a map of key → value supplier
1124
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
1125
   */
1126
  public void refreshAll(Map<String, Supplier<?>> loaders) {
1127
    requireAppCache("refreshAll");
3✔
1128
    loaders.forEach(this::refresh);
4✔
1129
  }
1✔
1130

1131
  /**
1132
   * Evict the given key locally, then load and cache with explicit TTL overrides.
1133
   * No broadcast is sent for the eviction.
1134
   *
1135
   * @param cacheKey     the key to refresh
1136
   * @param loader       the value supplier
1137
   * @param hotHardTtlMs hard TTL override (0 = use configured default; {@link Long#MAX_VALUE} for permanent entry)
1138
   * @param hotSoftTtlMs soft TTL override (0 = use configured default)
1139
   * @param <V>          the value type
1140
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
1141
   */
1142
  public <V> void refresh(String cacheKey, Supplier<V> loader, long hotHardTtlMs, long hotSoftTtlMs) {
1143
    requireAppCache("refresh");
3✔
1144
    evictLocal(cacheKey);
3✔
1145
    putThrough(cacheKey, loader.get(), () -> {}, hotHardTtlMs, hotSoftTtlMs);
8✔
1146
  }
1✔
1147

1148
  /**
1149
   * Check whether a key is currently tracked as a cluster-wide hot key by the
1150
   * Worker-side global detector.
1151
   *
1152
   * @param cacheKey the key to inspect
1153
   * @return {@code true} if the key appears in the Worker TopK list;
1154
   *         {@code false} if the key is {@code null} or no Worker is active
1155
   */
1156
  public boolean isWorkerHotKey(String cacheKey) {
1157
    return workerTopKAlgorithm != null && cacheKey != null && workerTopKAlgorithm.contains(cacheKey);
14✔
1158
  }
1159

1160
  /**
1161
   * Batch variant of {@link #isWorkerHotKey(String)}. Returns a map of key →
1162
   * Worker hot status for all given keys.
1163
   *
1164
   * <p><b>Batch execution:</b> Iterates sequentially; for large batches
1165
   * consider parallelizing in caller code.
1166
   *
1167
   * @param cacheKeys the keys to inspect
1168
   * @return a map of key → whether it is a cluster-wide hot key;
1169
   *         all entries are {@code false} when no Worker is active
1170
   */
1171
  public Map<String, Boolean> areWorkerHotKeys(Collection<String> cacheKeys) {
1172
    Map<String, Boolean> result = new HashMap<>();
4✔
1173
    cacheKeys.forEach(key -> result.put(key, isWorkerHotKey(key)));
14✔
1174
    return result;
2✔
1175
  }
1176

1177
  /**
1178
   * Return the top N hot keys from the local detector, ordered by frequency.
1179
   * Useful when callers need more or fewer items than the configured TopK capacity.
1180
   *
1181
   * @param n the number of top keys to return
1182
   * @return the top N items, or an empty list if no detector is available
1183
   */
1184
  public List<Item> returnLocalTopNHotKeys(int n) {
1185
    return appHotKeyDetector != null ? appHotKeyDetector.listTopN(n) : List.of();
10✔
1186
  }
1187

1188
  /**
1189
   * Return a blocking queue of recently expelled hot keys from the local detector.
1190
   * Consumers can drain this queue to react to keys that are no longer hot.
1191
   *
1192
   * @return the expelled queue, or an empty queue if no detector is available
1193
   */
1194
  public BlockingQueue<Item> returnLocalExpelledHotKeys() {
1195
    return appHotKeyDetector != null ? appHotKeyDetector.expelled() : new LinkedBlockingQueue<>();
11✔
1196
  }
1197

1198
  /**
1199
   * Return the total number of data streams (accesses) tracked by the local detector.
1200
   *
1201
   * @return the total count, or {@code 0} if no detector is available
1202
   */
1203
  public long returnLocalTotalDataStreams() {
1204
    return appHotKeyDetector != null ? appHotKeyDetector.total() : 0L;
9✔
1205
  }
1206

1207
  /**
1208
   * Return the current top-K hot keys (key + count) from the local detector,
1209
   * ordered by frequency.
1210
   *
1211
   * @return the local top-K list, or an empty list if no detector is available;
1212
   *         the returned list is a point-in-time snapshot
1213
   */
1214
  public List<Item> returnLocalHotKeys() {
1215
    return appHotKeyDetector != null ? appHotKeyDetector.list() : List.of();
9✔
1216
  }
1217

1218
  /**
1219
   * Return the current top-K hot keys from the Worker-side global detector,
1220
   * ordered by frequency. These keys reflect cross-instance aggregated access
1221
   * counts and are updated via periodic reports from all application instances.
1222
   *
1223
   * @return the cluster-wide top-K list, or an empty list if no Worker is active;
1224
   *         the returned list is a point-in-time snapshot
1225
   */
1226
  public List<Item> returnWorkerHotKeys() {
1227
    return workerTopKAlgorithm != null ? workerTopKAlgorithm.list() : List.of();
9✔
1228
  }
1229

1230
  /**
1231
   * Return a blocking queue of recently expelled hot keys from the Worker-side
1232
   * global detector. Consumers can drain this queue to react to keys that are
1233
   * no longer considered cluster-wide hot.
1234
   *
1235
   * @return the expelled queue, or an empty queue if no Worker is active
1236
   */
1237
  public BlockingQueue<Item> returnWorkerExpelledHotKeys() {
1238
    return workerTopKAlgorithm != null ? workerTopKAlgorithm.expelled() : new LinkedBlockingQueue<>();
11✔
1239
  }
1240

1241
  /**
1242
   * Return the total number of data streams tracked by the Worker-side
1243
   * global detector.
1244
   *
1245
   * @return the total count, or {@code 0} if no Worker is active
1246
   */
1247
  public long returnWorkerTotalDataStreams() {
1248
    return workerTopKAlgorithm != null ? workerTopKAlgorithm.total() : 0L;
9✔
1249
  }
1250

1251
  //-------------------------------------------------------------------------------------
1252

1253
  /**
1254
   * Add a key pattern to the blacklist. Keys matching this pattern will be
1255
   * blocked from cache get/put operations (returns {@link Optional#empty()}).
1256
   * <p>The pattern is auto-detected by {@link RuleMatcher#of}: exact match,
1257
   * prefix (trailing {@code *}), wildcard (containing {@code *} or {@code ?}),
1258
   * or regex (prefixed with {@code regex:}).
1259
   *
1260
   * @param keyPattern the key pattern to blacklist
1261
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
1262
   */
1263
  public void addBlacklist(String keyPattern) {
1264
    requireAppCache("addBlacklist");
3✔
1265
    hotKeyCache.addBlacklist(keyPattern);
4✔
1266
  }
1✔
1267

1268
  /**
1269
   * Batch variant of {@link #addBlacklist(String)}. Adds multiple key patterns
1270
   * to the blacklist.
1271
   *
1272
   * @param keyPatterns the key patterns to blacklist
1273
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
1274
   */
1275
  public void addBlacklist(Collection<String> keyPatterns) {
1276
    requireAppCache("addBlacklist");
3✔
1277
    keyPatterns.forEach(hotKeyCache::addBlacklist);
8✔
1278
  }
1✔
1279

1280
  /**
1281
   * Remove a key pattern from the blacklist.
1282
   *
1283
   * @param keyPattern the key pattern to remove from the blacklist
1284
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
1285
   */
1286
  public void removeBlacklist(String keyPattern) {
1287
    requireAppCache("removeBlacklist");
3✔
1288
    hotKeyCache.unBlacklist(keyPattern);
4✔
1289
  }
1✔
1290

1291
  /**
1292
   * Batch variant of {@link #removeBlacklist(String)}. Removes multiple key
1293
   * patterns from the blacklist.
1294
   *
1295
   * @param keyPatterns the key patterns to remove from the blacklist
1296
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
1297
   */
1298
  public void removeBlacklist(Collection<String> keyPatterns) {
1299
    requireAppCache("removeBlacklist");
3✔
1300
    keyPatterns.forEach(hotKeyCache::unBlacklist);
8✔
1301
  }
1✔
1302

1303
  /**
1304
   * Add a key pattern to the whitelist. Keys matching this pattern will
1305
   * skip report recording (no Worker report sent) but still participate in
1306
   * normal cache get/put and local hot-key detection.
1307
   * <p>The pattern is auto-detected by {@link RuleMatcher#of}.
1308
   *
1309
   * @param keyPattern the key pattern to whitelist
1310
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
1311
   */
1312
  public void addWhitelist(String keyPattern) {
1313
    requireAppCache("addWhitelist");
3✔
1314
    hotKeyCache.addWhitelist(keyPattern);
4✔
1315
  }
1✔
1316

1317
  /**
1318
   * Batch variant of {@link #addWhitelist(String)}. Adds multiple key patterns
1319
   * to the whitelist.
1320
   *
1321
   * @param keyPatterns the key patterns to whitelist
1322
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
1323
   */
1324
  public void addWhitelist(Collection<String> keyPatterns) {
1325
    requireAppCache("addWhitelist");
3✔
1326
    keyPatterns.forEach(hotKeyCache::addWhitelist);
8✔
1327
  }
1✔
1328

1329
  /**
1330
   * Remove a key pattern from the whitelist.
1331
   *
1332
   * @param keyPattern the key pattern to remove from the whitelist
1333
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
1334
   */
1335
  public void removeWhitelist(String keyPattern) {
1336
    requireAppCache("removeWhitelist");
3✔
1337
    hotKeyCache.unWhitelist(keyPattern);
4✔
1338
  }
1✔
1339

1340
  /**
1341
   * Batch variant of {@link #removeWhitelist(String)}. Removes multiple key
1342
   * patterns from the whitelist.
1343
   *
1344
   * @param keyPatterns the key patterns to remove from the whitelist
1345
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
1346
   */
1347
  public void removeWhitelist(Collection<String> keyPatterns) {
1348
    requireAppCache("removeWhitelist");
3✔
1349
    keyPatterns.forEach(hotKeyCache::unWhitelist);
8✔
1350
  }
1✔
1351

1352
  /**
1353
   * Evaluate all rules against the given key and return the first matching action.
1354
   *
1355
   * @param cacheKey the key to evaluate
1356
   * @return the matching {@link Rule.RuleAction}, or {@code ALLOW} if no rule matches
1357
   *         or no cache is available (Worker-only mode)
1358
   */
1359
  public Rule.RuleAction evaluateRule(String cacheKey) {
1360
    if (hotKeyCache == null) {
3✔
1361
      return Rule.RuleAction.ALLOW;
2✔
1362
    }
1363
    return hotKeyCache.evaluateRule(cacheKey);
5✔
1364
  }
1365

1366
  /**
1367
   * Batch variant of {@link #evaluateRule(String)}. Evaluates all rules against
1368
   * the given keys and returns the first matching action for each.
1369
   *
1370
   * @param cacheKeys the keys to evaluate
1371
   * @return a map of key → matching {@link Rule.RuleAction} (or {@code ALLOW} if no rule matches)
1372
   */
1373
  public Map<String, Rule.RuleAction> evaluateRules(Collection<String> cacheKeys) {
1374
    Map<String, Rule.RuleAction> result = new HashMap<>();
4✔
1375
    cacheKeys.forEach(key -> {
5✔
1376
      Rule.RuleAction action = evaluateRule(key);
4✔
1377
      result.put(key, action);
5✔
1378
    });
1✔
1379

1380
    return result;
2✔
1381
  }
1382

1383
  /**
1384
   * Quickly check whether the given key is blocked by any blacklist rule.
1385
   * <p>
1386
   * Equivalent to {@code evaluateRule(key) == BLOCK}, provided as a convenience
1387
   * for guard clauses before expensive operations.
1388
   *
1389
   * @param cacheKey the key to check
1390
   * @return {@code true} if a blacklist rule matches the key, {@code false} if no
1391
   *         rule matches or no cache is available (Worker-only mode)
1392
   */
1393
  public boolean isBlacklisted(String cacheKey) {
1394
    if (hotKeyCache == null) {
3✔
1395
      return false;
2✔
1396
    }
1397
    return hotKeyCache.isBlacklisted(cacheKey);
5✔
1398
  }
1399

1400
  /**
1401
   * Batch variant of {@link #isBlacklisted(String)}. Checks multiple keys
1402
   * against blacklist rules.
1403
   *
1404
   * @param cacheKeys the keys to check
1405
   * @return a map of key → whether it is blocked by a blacklist rule
1406
   */
1407
  public Map<String, Boolean> isBlacklisted(Collection<String> cacheKeys) {
1408
    Map<String, Boolean> result = new HashMap<>();
4✔
1409
    cacheKeys.forEach(key -> {
5✔
1410
      boolean isBlacklisted = isBlacklisted(key);
4✔
1411
      result.put(key, isBlacklisted);
6✔
1412
    });
1✔
1413

1414
    return result;
2✔
1415
  }
1416

1417
  /**
1418
   * Quickly check whether the given key is whitelisted (skips Worker reporting).
1419
   * <p>
1420
   * Equivalent to {@code evaluateRule(key) == ALLOW_NO_REPORT}, provided as a
1421
   * convenience for debugging and monitoring.
1422
   *
1423
   * @param cacheKey the key to check
1424
   * @return {@code true} if a whitelist rule matches the key, {@code false} if no
1425
   *         rule matches or no cache is available (Worker-only mode)
1426
   */
1427
  public boolean isWhitelisted(String cacheKey) {
1428
    if (hotKeyCache == null) {
3✔
1429
      return false;
2✔
1430
    }
1431
    return hotKeyCache.isWhitelisted(cacheKey);
5✔
1432
  }
1433

1434
  /**
1435
   * Batch variant of {@link #isWhitelisted(String)}. Checks multiple keys
1436
   * against whitelist rules.
1437
   *
1438
   * @param cacheKeys the keys to check
1439
   * @return a map of key → whether it is whitelisted (skips Worker reporting)
1440
   */
1441
  public Map<String, Boolean> isWhitelisted(Collection<String> cacheKeys) {
1442
    Map<String, Boolean> result = new HashMap<>();
4✔
1443
    cacheKeys.forEach(key -> {
5✔
1444
      boolean isWhitelisted = isWhitelisted(key);
4✔
1445
      result.put(key, isWhitelisted);
6✔
1446
    });
1✔
1447

1448
    return result;
2✔
1449
  }
1450

1451
  /**
1452
   * Return a snapshot of all current rules in evaluation order.
1453
   *
1454
   * @return list of rules, or an empty list if no cache is available;
1455
   *         the returned list is unmodifiable
1456
   */
1457
  public List<Rule> getAllRules() {
1458
    if (hotKeyCache == null) {
3✔
1459
      return List.of();
2✔
1460
    }
1461
    return hotKeyCache.getAllRules();
4✔
1462
  }
1463

1464
  /**
1465
   * Remove all blacklist and whitelist rules.
1466
   *
1467
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
1468
   */
1469
  public void clearAllRules() {
1470
    requireAppCache("clearAllRules");
3✔
1471
    hotKeyCache.clearAllRules();
3✔
1472
  }
1✔
1473

1474
  /**
1475
   * Broadcast all local rules to peer instances via the sync exchange.
1476
   * Useful for initial synchronization when a new instance joins the cluster.
1477
   *
1478
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
1479
   */
1480
  public void broadcastAllLocalRulesManually() {
1481
    requireAppCache("broadcastAllLocalRulesManually");
3✔
1482
    hotKeyCache.broadcastAllLocalRulesManually();
3✔
1483
  }
1✔
1484

1485
  /**
1486
   * Whether this instance has app-side cache available.
1487
   *
1488
   * <p>Returns {@code true} in App-only and Coexistence modes,
1489
   * {@code false} in Worker-only mode. Callers can use this to guard
1490
   * cache-dependent operations without catching {@link HotKeyModeException}.
1491
   *
1492
   * @return {@code true} if cache-dependent APIs (get, put, invalidate, etc.) are usable
1493
   */
1494
  public boolean isApp() {
UNCOV
1495
    return hotKeyCache != null;
×
1496
  }
1497

1498
  /**
1499
   * Whether this instance has Worker-side TopK available.
1500
   *
1501
   * <p>Returns {@code true} in Worker-only and Coexistence modes,
1502
   * {@code false} in App-only mode.
1503
   *
1504
   * @return {@code true} if Worker TopK queries (returnWorkerHotKeys, etc.) are usable
1505
   */
1506
  public boolean isWorker() {
UNCOV
1507
    return workerTopKAlgorithm != null;
×
1508
  }
1509

1510
  /**
1511
   * Whether this instance is in pure App-only mode (cache present, Worker TopK absent).
1512
   *
1513
   * @return {@code true} if App-only mode
1514
   */
1515
  public boolean isAppOnly() {
1516
    return hotKeyCache != null && workerTopKAlgorithm == null;
×
1517
  }
1518

1519
  /**
1520
   * Whether this instance is in pure Worker-only mode (cache absent, Worker TopK present).
1521
   *
1522
   * @return {@code true} if Worker-only mode
1523
   */
1524
  public boolean isWorkerOnly() {
1525
    return hotKeyCache == null && workerTopKAlgorithm != null;
×
1526
  }
1527

1528
  /**
1529
   * Returns a human-readable label of the current deployment mode based on the
1530
   * presence/absence of the cache and Worker TopK fields.
1531
   */
1532
  private String currentModeLabel() {
1533
    if (hotKeyCache != null && workerTopKAlgorithm != null) {
3!
1534
      return "Coexistence mode";
×
1535
    }
1536
    if (hotKeyCache != null) {
3!
1537
      return "App-only mode";
×
1538
    }
1539
    if (workerTopKAlgorithm != null) {
3!
1540
      return "Worker-only mode";
2✔
1541
    }
1542
    return "Uninitialized mode (no cache, no TopK)";
×
1543
  }
1544

1545
  private void requireAppCache(String operation) {
1546
    if (hotKeyCache == null) {
3✔
1547
      throw new HotKeyModeException(operation, currentModeLabel(), "App-mode cache");
8✔
1548
    }
1549
  }
1✔
1550

1551
  /**
1552
   * Requires app-side TopK detector; throws {@link HotKeyModeException} when
1553
   * the app-side detector is unavailable (Worker-only mode).
1554
   *
1555
   * @param operation the API operation name
1556
   */
1557
  private void requireAppDetector(String operation) {
1558
    if (appHotKeyDetector == null) {
3!
1559
      throw new HotKeyModeException(operation, currentModeLabel(), "App-mode TopK detector");
×
1560
    }
1561
  }
1✔
1562
}
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