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

Hyshmily / hotkey / 28026418224

23 Jun 2026 12:32PM UTC coverage: 92.562% (-0.6%) from 93.188%
28026418224

push

github

Hyshmily
Release 1.1.51

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

1176 of 1323 branches covered (88.89%)

Branch coverage included in aggregate %.

61 of 76 new or added lines in 6 files covered. (80.26%)

3 existing lines in 1 file now uncovered.

3366 of 3584 relevant lines covered (93.92%)

4.24 hits per line

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

80.53
/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 java.util.*;
33
import java.util.concurrent.BlockingQueue;
34
import java.util.concurrent.LinkedBlockingQueue;
35
import java.util.concurrent.TimeUnit;
36
import java.util.function.Function;
37
import java.util.function.Supplier;
38
import org.jspecify.annotations.Nullable;
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 {
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
   * Create a HotKey facade with all three optional components.
91
   *
92
   * <p>Each parameter may be {@code null} depending on the deployment mode:
93
   * <ul>
94
   *   <li><b>App-only:</b> {@code hotKeyCache} and {@code appHotKeyDetector} are present,
95
   *       {@code workerTopKAlgorithm} is {@code null}</li>
96
   *   <li><b>Worker-only:</b> only {@code workerTopKAlgorithm} is present</li>
97
   *   <li><b>Coexistence:</b> all three are present</li>
98
   * </ul>
99
   *
100
   * @param hotKeyCache         the cache orchestrator (maybe {@code null} in Worker-only mode)
101
   * @param appHotKeyDetector   the app-side local TopK detector (maybe {@code null} in Worker-only mode)
102
   * @param workerTopKAlgorithm the Worker-side global TopK detector (maybe {@code null} in app-only mode)
103
   */
104
  public HotKey(HotKeyCache hotKeyCache, HotKeyDetector appHotKeyDetector, TopK workerTopKAlgorithm) {
105
    this(hotKeyCache, appHotKeyDetector, workerTopKAlgorithm, null);
6✔
106
  }
1✔
107

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

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

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

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

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

256
    return result;
2✔
257
  }
258

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

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

299
    return result;
2✔
300
  }
301

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

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

335
    return result;
2✔
336
  }
337

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

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

374
    return result;
2✔
375
  }
376

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

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

416
    return result;
2✔
417
  }
418

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

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

447
    return result;
2✔
448
  }
449

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

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

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

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

520
  /**
521
   * Get with soft-expire and explicit hard/soft TTL overrides.
522
   *
523
   * @param cacheKey  the key to retrieve
524
   * @param reader    the value supplier for cache misses / refreshes
525
   * @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})
526
   * @param softTtlMs soft TTL override (0 = use configured default)
527
   * @param <T>       the value type
528
   * @return an {@link Optional} containing the cached (possibly stale) or loaded value
529
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
530
   * @throws HotKeyBlockedException when the key matches a blacklist rule
531
   */
532
  public <T> Optional<T> getWithSoftExpire(String cacheKey, Supplier<T> reader, long hardTtlMs, long softTtlMs) {
533
    requireAppCache("get");
3✔
534
    requireAppDetector("get");
3✔
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
    requireAppCache("invalidate");
3✔
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
    requireAppCache("invalidateAll");
3✔
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
    requireAppCache("evictLocal");
3✔
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
    requireAppCache("putThrough");
3✔
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
    requireAppCache("putThrough");
3✔
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
    if (hotKeyCache == null) {
3!
NEW
645
      return; // Worker-only mode: silently no-op (per Javadoc)
×
646
    }
647
    putLocal(cacheKey, value, 0, 0);
6✔
648
  }
1✔
649

650
  /**
651
   * Batch variant of {@link #putLocal(String, Object)}. Writes all entries into
652
   * L1 without version bump, broadcast, hot-key detection, or reporting.
653
   *
654
   * <p>In Worker-only mode this method silently no-ops.
655
   *
656
   * @param entries a map of key → value to store locally
657
   */
658
  public void putLocal(Map<String, Object> entries) {
659
    if (hotKeyCache == null) {
3!
NEW
660
      return; // Worker-only mode: silently no-op
×
661
    }
662
    entries.forEach(this::putLocal);
4✔
663
  }
1✔
664

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

686
  /**
687
   * Execute a mutation, then invalidate L1 and broadcast.
688
   * Next {@link #get} will re-fetch from the reader.
689
   *
690
   * @param cacheKey the key to invalidate after mutation
691
   * @param mutation the mutation to execute
692
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
693
   */
694
  public void putBeforeInvalidate(String cacheKey, Runnable mutation) {
695
    requireAppCache("putBeforeInvalidate");
3✔
696
    hotKeyCache.putBeforeInvalidate(cacheKey, mutation);
5✔
697
  }
1✔
698

699
  /**
700
   * Batch variant of {@link #putBeforeInvalidate(String, Runnable)}. Executes
701
   * all mutations and invalidates the corresponding keys from L1 with broadcast.
702
   *
703
   * <p><b>Batch execution:</b> Iterates sequentially; for large batches
704
   * consider parallelizing in caller code.
705
   *
706
   * @param mutations a map of key → mutation to execute
707
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
708
   */
709
  public void putBeforeInvalidateAll(Map<String, Runnable> mutations) {
710
    requireAppCache("putBeforeInvalidateAll");
3✔
711
    mutations.forEach(this::putBeforeInvalidate);
4✔
712
  }
1✔
713

714
  /**
715
   * Estimated number of entries currently in the L1 cache.
716
   * <p>
717
   * This is a lightweight, best-effort estimate from the underlying Caffeine
718
   * cache, suitable for monitoring dashboards and capacity planning.
719
   *
720
   * @return estimated entry count, or {@code 0} in Worker-only mode
721
   */
722
  public long estimatedSize() {
723
    if (hotKeyCache == null) {
3✔
724
      return 0L;
2✔
725
    }
726
    return hotKeyCache.estimatedSize();
4✔
727
  }
728

729
  /**
730
   * Return a snapshot of basic L1 cache statistics.
731
   * <p>
732
   * Hit/miss/eviction counters are populated only when Caffeine's
733
   * {@code recordStats()} is enabled. {@code estimatedSize} is always
734
   * available.
735
   *
736
   * @return a {@link HotKeyCacheStats} record, or {@code null} in Worker-only mode;
737
   *         hit/miss counters are {@code 0} if stats recording is not enabled
738
   */
739
  public HotKeyCacheStats stats() {
740
    if (hotKeyCache == null) {
3✔
741
      return null;
2✔
742
    }
743
    return hotKeyCache.stats();
4✔
744
  }
745

746
  /**
747
   * Return the underlying Caffeine cache for direct access.
748
   *
749
   * <p>Useful for Caffeine-specific operations such as {@code asMap()},
750
   * {@code policy()}, and {@code cleanUp()}. Use with caution — bypassing
751
   * the HotKey orchestration layer can lead to inconsistent cache state.
752
   *
753
   * @return the raw Caffeine {@link Cache} instance
754
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
755
   */
756
  public Cache<String, Object> getLocalCache() {
757
    requireAppCache("getLocalCache");
3✔
758
    return hotKeyCache.getLocalCache();
4✔
759
  }
760

761
  /**
762
   * Invalidate all entries from the L1 cache without broadcasting.
763
   * <p>
764
   * This is an emergency flush — all cached values are removed immediately.
765
   * No cross-instance sync messages are sent. Subsequent {@link #get} calls
766
   * will reload data from the reader.
767
   * <p>
768
   * Use with caution: flushing the local cache increases load on the backend
769
   * until entries are re-cached.
770
   *
771
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
772
   */
773
  public void invalidateAllLocal() {
774
    requireAppCache("invalidateAllLocal");
3✔
775
    hotKeyCache.invalidateAllLocal();
3✔
776
  }
1✔
777

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

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

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

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

902
  //------------------------------------------------------------------------
903

904
  /**
905
   * Check whether a key is currently tracked as a local hot key in L1.
906
   *
907
   * @param cacheKey the key to inspect
908
   * @return {@code true} if the key exists in L1 with {@link io.github.hyshmily.hotkey.model.KeyState#HOT}
909
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
910
   */
911
  public boolean isLocalHotKey(String cacheKey) {
912
    requireAppCache("isLocalHotKey");
3✔
913
    requireAppDetector("isLocalHotKey");
3✔
914
    return hotKeyCache.isLocalHotKey(cacheKey);
5✔
915
  }
916

917
  /**
918
   * Batch variant of {@link #isLocalHotKey(String)}. Returns a map of key →
919
   * hot status for all given keys.
920
   *
921
   * <p><b>Batch execution:</b> Iterates sequentially; for large batches
922
   * consider parallelizing in caller code.
923
   *
924
   * @param cacheKeys the keys to inspect
925
   * @return a map of key → whether it is a local hot key
926
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
927
   */
928
  public Map<String, Boolean> areLocalHotKeys(Collection<String> cacheKeys) {
929
    Map<String, Boolean> result = new HashMap<>();
4✔
930
    cacheKeys.forEach(key -> result.put(key, isLocalHotKey(key)));
14✔
931
    return result;
2✔
932
  }
933

934
  /**
935
   * Increment the local TopK detector directly, bypassing the buffer and the
936
   * report-to-Worker path. Useful for bulk-loading historical access patterns
937
   * or correcting frequency counts.
938
   *
939
   * @param cacheKey the key to record
940
   * @param count    the number of accesses to add
941
   */
942
  public void notifyLocalDetectorDirect(String cacheKey, int count) {
943
    requireAppDetector("notifyLocalDetectorDirect");
3✔
944
    appHotKeyDetector.addDirect(cacheKey, count);
6✔
945
  }
1✔
946

947
  /**
948
   * Batch-increment the local TopK detector directly, bypassing buffer and reports.
949
   *
950
   * @param keyCounts a map of key → access count
951
   */
952
  public void notifyLocalDetectorDirect(Map<String, Long> keyCounts) {
953
    requireAppDetector("notifyLocalDetectorDirect");
3✔
954
    appHotKeyDetector.addDirect(keyCounts);
5✔
955
  }
1✔
956

957
  /**
958
   * Notify the local TopK detector that a key was accessed, without triggering
959
   * a report to the Worker. Used by {@code @Intercept} path to keep the local
960
   * frequency sketch accurate without flooding the Worker with reports.
961
   * Null keys are silently ignored.
962
   *
963
   * @param cacheKey the accessed key (maybe {@code null}, silently ignored)
964
   */
965
  public void notifyLocalDetector(String cacheKey) {
966
    requireAppDetector("notifyLocalDetector");
3✔
967
    appHotKeyDetector.add(cacheKey);
4✔
968
  }
1✔
969

970
  /**
971
   * Notify the local detector of key access with a custom delta, routing
972
   * through the buffered counter. Null keys are silently ignored.
973
   *
974
   * @param cacheKey the accessed key (maybe {@code null}, silently ignored)
975
   * @param count    the number of accesses to record
976
   */
977
  public void notifyLocalDetector(String cacheKey, long count) {
978
    requireAppDetector("notifyLocalDetector");
3✔
979
    appHotKeyDetector.add(cacheKey, count);
5✔
980
  }
1✔
981

982
  /**
983
   * Batch-notify the local detector of multiple key accesses, routing through
984
   * the buffered counter. Null keys in the map are silently ignored.
985
   *
986
   * @param keyCounts a map of key → access count
987
   */
988
  public void notifyLocalDetector(Map<String, Long> keyCounts) {
989
    requireAppDetector("notifyLocalDetector");
3✔
990
    appHotKeyDetector.add(keyCounts);
4✔
991
  }
1✔
992

993
  /**
994
   * Evict the given key locally, then load and cache via the supplier.
995
   * Uses default TTLs. No broadcast is sent for the eviction.
996
   *
997
   * @param cacheKey the key to refresh
998
   * @param loader   the value supplier
999
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
1000
   */
1001
  public void refresh(String cacheKey, Supplier<?> loader) {
1002
    requireAppCache("refresh");
3✔
1003
    evictLocal(cacheKey);
3✔
1004
    putThrough(cacheKey, loader.get(), () -> {});
6✔
1005
  }
1✔
1006

1007
  /**
1008
   * Batch variant of {@link #refresh(String, Supplier)}. Refreshes all entries
1009
   * by evicting locally then loading and caching via the provided suppliers.
1010
   *
1011
   * @param loaders a map of key → value supplier
1012
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
1013
   */
1014
  public void refreshAll(Map<String, Supplier<?>> loaders) {
1015
    requireAppCache("refreshAll");
3✔
1016
    loaders.forEach(this::refresh);
4✔
1017
  }
1✔
1018

1019
  /**
1020
   * Evict the given key locally, then load and cache with explicit TTL overrides.
1021
   * No broadcast is sent for the eviction.
1022
   *
1023
   * @param cacheKey     the key to refresh
1024
   * @param loader       the value supplier
1025
   * @param hotHardTtlMs hard TTL override (0 = use configured default; {@link Long#MAX_VALUE} for permanent entry)
1026
   * @param hotSoftTtlMs soft TTL override (0 = use configured default)
1027
   * @param <V>          the value type
1028
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
1029
   */
1030
  public <V> void refresh(String cacheKey, Supplier<V> loader, long hotHardTtlMs, long hotSoftTtlMs) {
1031
    requireAppCache("refresh");
3✔
1032
    evictLocal(cacheKey);
3✔
1033
    putThrough(cacheKey, loader.get(), () -> {}, hotHardTtlMs, hotSoftTtlMs);
8✔
1034
  }
1✔
1035

1036
  /**
1037
   * Check whether a key is currently tracked as a cluster-wide hot key by the
1038
   * Worker-side global detector.
1039
   *
1040
   * @param cacheKey the key to inspect
1041
   * @return {@code true} if the key appears in the Worker TopK list;
1042
   *         {@code false} if the key is {@code null} or no Worker is active
1043
   */
1044
  public boolean isWorkerHotKey(String cacheKey) {
1045
    return workerTopKAlgorithm != null && cacheKey != null && workerTopKAlgorithm.contains(cacheKey);
14✔
1046
  }
1047

1048
  /**
1049
   * Batch variant of {@link #isWorkerHotKey(String)}. Returns a map of key →
1050
   * Worker hot status for all given keys.
1051
   *
1052
   * <p><b>Batch execution:</b> Iterates sequentially; for large batches
1053
   * consider parallelizing in caller code.
1054
   *
1055
   * @param cacheKeys the keys to inspect
1056
   * @return a map of key → whether it is a cluster-wide hot key;
1057
   *         all entries are {@code false} when no Worker is active
1058
   */
1059
  public Map<String, Boolean> areWorkerHotKeys(Collection<String> cacheKeys) {
1060
    Map<String, Boolean> result = new HashMap<>();
4✔
1061
    cacheKeys.forEach(key -> result.put(key, isWorkerHotKey(key)));
14✔
1062
    return result;
2✔
1063
  }
1064

1065
  /**
1066
   * Return the top N hot keys from the local detector, ordered by frequency.
1067
   * Useful when callers need more or fewer items than the configured TopK capacity.
1068
   *
1069
   * @param n the number of top keys to return
1070
   * @return the top N items, or an empty list if no detector is available
1071
   */
1072
  public List<Item> returnLocalTopNHotKeys(int n) {
1073
    return appHotKeyDetector != null ? appHotKeyDetector.listTopN(n) : List.of();
10✔
1074
  }
1075

1076
  /**
1077
   * Return a blocking queue of recently expelled hot keys from the local detector.
1078
   * Consumers can drain this queue to react to keys that are no longer hot.
1079
   *
1080
   * @return the expelled queue, or an empty queue if no detector is available
1081
   */
1082
  public BlockingQueue<Item> returnLocalExpelledHotKeys() {
1083
    return appHotKeyDetector != null ? appHotKeyDetector.expelled() : new LinkedBlockingQueue<>();
11✔
1084
  }
1085

1086
  /**
1087
   * Return the total number of data streams (accesses) tracked by the local detector.
1088
   *
1089
   * @return the total count, or {@code 0} if no detector is available
1090
   */
1091
  public long returnLocalTotalDataStreams() {
1092
    return appHotKeyDetector != null ? appHotKeyDetector.total() : 0L;
9✔
1093
  }
1094

1095
  /**
1096
   * Return the current top-K hot keys (key + count) from the local detector,
1097
   * ordered by frequency.
1098
   *
1099
   * @return the local top-K list, or an empty list if no detector is available;
1100
   *         the returned list is a point-in-time snapshot
1101
   */
1102
  public List<Item> returnLocalHotKeys() {
1103
    return appHotKeyDetector != null ? appHotKeyDetector.list() : List.of();
9✔
1104
  }
1105

1106
  /**
1107
   * Return the current top-K hot keys from the Worker-side global detector,
1108
   * ordered by frequency. These keys reflect cross-instance aggregated access
1109
   * counts and are updated via periodic reports from all application instances.
1110
   *
1111
   * @return the cluster-wide top-K list, or an empty list if no Worker is active;
1112
   *         the returned list is a point-in-time snapshot
1113
   */
1114
  public List<Item> returnWorkerHotKeys() {
1115
    return workerTopKAlgorithm != null ? workerTopKAlgorithm.list() : List.of();
9✔
1116
  }
1117

1118
  /**
1119
   * Return a blocking queue of recently expelled hot keys from the Worker-side
1120
   * global detector. Consumers can drain this queue to react to keys that are
1121
   * no longer considered cluster-wide hot.
1122
   *
1123
   * @return the expelled queue, or an empty queue if no Worker is active
1124
   */
1125
  public BlockingQueue<Item> returnWorkerExpelledHotKeys() {
1126
    return workerTopKAlgorithm != null ? workerTopKAlgorithm.expelled() : new LinkedBlockingQueue<>();
11✔
1127
  }
1128

1129
  /**
1130
   * Return the total number of data streams tracked by the Worker-side
1131
   * global detector.
1132
   *
1133
   * @return the total count, or {@code 0} if no Worker is active
1134
   */
1135
  public long returnWorkerTotalDataStreams() {
1136
    return workerTopKAlgorithm != null ? workerTopKAlgorithm.total() : 0L;
9✔
1137
  }
1138

1139
  //-------------------------------------------------------------------------------------
1140

1141
  /**
1142
   * Add a key pattern to the blacklist. Keys matching this pattern will be
1143
   * blocked from cache get/put operations (returns {@link Optional#empty()}).
1144
   * <p>The pattern is auto-detected by {@link RuleMatcher#of}: exact match,
1145
   * prefix (trailing {@code *}), wildcard (containing {@code *} or {@code ?}),
1146
   * or regex (prefixed with {@code regex:}).
1147
   *
1148
   * @param keyPattern the key pattern to blacklist
1149
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
1150
   */
1151
  public void addBlacklist(String keyPattern) {
1152
    requireAppCache("addBlacklist");
3✔
1153
    hotKeyCache.addBlacklist(keyPattern);
4✔
1154
  }
1✔
1155

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

1168
  /**
1169
   * Remove a key pattern from the blacklist.
1170
   *
1171
   * @param keyPattern the key pattern to remove from the blacklist
1172
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
1173
   */
1174
  public void removeBlacklist(String keyPattern) {
1175
    requireAppCache("removeBlacklist");
3✔
1176
    hotKeyCache.unBlacklist(keyPattern);
4✔
1177
  }
1✔
1178

1179
  /**
1180
   * Batch variant of {@link #removeBlacklist(String)}. Removes multiple key
1181
   * patterns from the blacklist.
1182
   *
1183
   * @param keyPatterns the key patterns to remove from the blacklist
1184
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
1185
   */
1186
  public void removeBlacklist(Collection<String> keyPatterns) {
1187
    requireAppCache("removeBlacklist");
3✔
1188
    keyPatterns.forEach(hotKeyCache::unBlacklist);
8✔
1189
  }
1✔
1190

1191
  /**
1192
   * Add a key pattern to the whitelist. Keys matching this pattern will
1193
   * skip report recording (no Worker report sent) but still participate in
1194
   * normal cache get/put and local hot-key detection.
1195
   * <p>The pattern is auto-detected by {@link RuleMatcher#of}.
1196
   *
1197
   * @param keyPattern the key pattern to whitelist
1198
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
1199
   */
1200
  public void addWhitelist(String keyPattern) {
1201
    requireAppCache("addWhitelist");
3✔
1202
    hotKeyCache.addWhitelist(keyPattern);
4✔
1203
  }
1✔
1204

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

1217
  /**
1218
   * Remove a key pattern from the whitelist.
1219
   *
1220
   * @param keyPattern the key pattern to remove from the whitelist
1221
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
1222
   */
1223
  public void removeWhitelist(String keyPattern) {
1224
    requireAppCache("removeWhitelist");
3✔
1225
    hotKeyCache.unWhitelist(keyPattern);
4✔
1226
  }
1✔
1227

1228
  /**
1229
   * Batch variant of {@link #removeWhitelist(String)}. Removes multiple key
1230
   * patterns from the whitelist.
1231
   *
1232
   * @param keyPatterns the key patterns to remove from the whitelist
1233
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
1234
   */
1235
  public void removeWhitelist(Collection<String> keyPatterns) {
1236
    requireAppCache("removeWhitelist");
3✔
1237
    keyPatterns.forEach(hotKeyCache::unWhitelist);
8✔
1238
  }
1✔
1239

1240
  /**
1241
   * Evaluate all rules against the given key and return the first matching action.
1242
   *
1243
   * @param cacheKey the key to evaluate
1244
   * @return the matching {@link Rule.RuleAction}, or {@code ALLOW} if no rule matches
1245
   *         or no cache is available (Worker-only mode)
1246
   */
1247
  public Rule.RuleAction evaluateRule(String cacheKey) {
1248
    if (hotKeyCache == null) {
3✔
1249
      return Rule.RuleAction.ALLOW;
2✔
1250
    }
1251
    return hotKeyCache.evaluateRule(cacheKey);
5✔
1252
  }
1253

1254
  /**
1255
   * Batch variant of {@link #evaluateRule(String)}. Evaluates all rules against
1256
   * the given keys and returns the first matching action for each.
1257
   *
1258
   * @param cacheKeys the keys to evaluate
1259
   * @return a map of key → matching {@link Rule.RuleAction} (or {@code ALLOW} if no rule matches)
1260
   */
1261
  public Map<String, Rule.RuleAction> evaluateRules(Collection<String> cacheKeys) {
1262
    Map<String, Rule.RuleAction> result = new HashMap<>();
4✔
1263
    cacheKeys.forEach(key -> {
5✔
1264
      Rule.RuleAction action = evaluateRule(key);
4✔
1265
      result.put(key, action);
5✔
1266
    });
1✔
1267

1268
    return result;
2✔
1269
  }
1270

1271
  /**
1272
   * Quickly check whether the given key is blocked by any blacklist rule.
1273
   * <p>
1274
   * Equivalent to {@code evaluateRule(key) == BLOCK}, provided as a convenience
1275
   * for guard clauses before expensive operations.
1276
   *
1277
   * @param cacheKey the key to check
1278
   * @return {@code true} if a blacklist rule matches the key, {@code false} if no
1279
   *         rule matches or no cache is available (Worker-only mode)
1280
   */
1281
  public boolean isBlacklisted(String cacheKey) {
1282
    if (hotKeyCache == null) {
3✔
1283
      return false;
2✔
1284
    }
1285
    return hotKeyCache.isBlacklisted(cacheKey);
5✔
1286
  }
1287

1288
  /**
1289
   * Batch variant of {@link #isBlacklisted(String)}. Checks multiple keys
1290
   * against blacklist rules.
1291
   *
1292
   * @param cacheKeys the keys to check
1293
   * @return a map of key → whether it is blocked by a blacklist rule
1294
   */
1295
  public Map<String, Boolean> isBlacklisted(Collection<String> cacheKeys) {
1296
    Map<String, Boolean> result = new HashMap<>();
4✔
1297
    cacheKeys.forEach(key -> {
5✔
1298
      boolean isBlacklisted = isBlacklisted(key);
4✔
1299
      result.put(key, isBlacklisted);
6✔
1300
    });
1✔
1301

1302
    return result;
2✔
1303
  }
1304

1305
  /**
1306
   * Quickly check whether the given key is whitelisted (skips Worker reporting).
1307
   * <p>
1308
   * Equivalent to {@code evaluateRule(key) == ALLOW_NO_REPORT}, provided as a
1309
   * convenience for debugging and monitoring.
1310
   *
1311
   * @param cacheKey the key to check
1312
   * @return {@code true} if a whitelist rule matches the key, {@code false} if no
1313
   *         rule matches or no cache is available (Worker-only mode)
1314
   */
1315
  public boolean isWhitelisted(String cacheKey) {
1316
    if (hotKeyCache == null) {
3✔
1317
      return false;
2✔
1318
    }
1319
    return hotKeyCache.isWhitelisted(cacheKey);
5✔
1320
  }
1321

1322
  /**
1323
   * Batch variant of {@link #isWhitelisted(String)}. Checks multiple keys
1324
   * against whitelist rules.
1325
   *
1326
   * @param cacheKeys the keys to check
1327
   * @return a map of key → whether it is whitelisted (skips Worker reporting)
1328
   */
1329
  public Map<String, Boolean> isWhitelisted(Collection<String> cacheKeys) {
1330
    Map<String, Boolean> result = new HashMap<>();
4✔
1331
    cacheKeys.forEach(key -> {
5✔
1332
      boolean isWhitelisted = isWhitelisted(key);
4✔
1333
      result.put(key, isWhitelisted);
6✔
1334
    });
1✔
1335

1336
    return result;
2✔
1337
  }
1338

1339
  /**
1340
   * Return a snapshot of all current rules in evaluation order.
1341
   *
1342
   * @return list of rules, or an empty list if no cache is available;
1343
   *         the returned list is unmodifiable
1344
   */
1345
  public List<Rule> getAllRules() {
1346
    if (hotKeyCache == null) {
3✔
1347
      return List.of();
2✔
1348
    }
1349
    return hotKeyCache.getAllRules();
4✔
1350
  }
1351

1352
  /**
1353
   * Remove all blacklist and whitelist rules.
1354
   *
1355
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
1356
   */
1357
  public void clearAllRules() {
1358
    requireAppCache("clearAllRules");
3✔
1359
    hotKeyCache.clearAllRules();
3✔
1360
  }
1✔
1361

1362
  /**
1363
   * Broadcast all local rules to peer instances via the sync exchange.
1364
   * Useful for initial synchronization when a new instance joins the cluster.
1365
   *
1366
   * @throws UnsupportedOperationException when no cache is available (Worker-only mode)
1367
   */
1368
  public void broadcastAllLocalRulesManually() {
1369
    requireAppCache("broadcastAllLocalRulesManually");
3✔
1370
    hotKeyCache.broadcastAllLocalRulesManually();
3✔
1371
  }
1✔
1372

1373
  /**
1374
   * Whether this instance has app-side cache available.
1375
   *
1376
   * <p>Returns {@code true} in App-only and Coexistence modes,
1377
   * {@code false} in Worker-only mode. Callers can use this to guard
1378
   * cache-dependent operations without catching {@link HotKeyModeException}.
1379
   *
1380
   * @return {@code true} if cache-dependent APIs (get, put, invalidate, etc.) are usable
1381
   */
1382
  public boolean isApp() {
NEW
1383
    return hotKeyCache != null;
×
1384
  }
1385

1386
  /**
1387
   * Whether this instance has Worker-side TopK available.
1388
   *
1389
   * <p>Returns {@code true} in Worker-only and Coexistence modes,
1390
   * {@code false} in App-only mode.
1391
   *
1392
   * @return {@code true} if Worker TopK queries (returnWorkerHotKeys, etc.) are usable
1393
   */
1394
  public boolean isWorker() {
NEW
1395
    return workerTopKAlgorithm != null;
×
1396
  }
1397

1398
  /**
1399
   * Whether this instance is in pure App-only mode (cache present, Worker TopK absent).
1400
   *
1401
   * @return {@code true} if App-only mode
1402
   */
1403
  public boolean isAppOnly() {
NEW
1404
    return hotKeyCache != null && workerTopKAlgorithm == null;
×
1405
  }
1406

1407
  /**
1408
   * Whether this instance is in pure Worker-only mode (cache absent, Worker TopK present).
1409
   *
1410
   * @return {@code true} if Worker-only mode
1411
   */
1412
  public boolean isWorkerOnly() {
NEW
1413
    return hotKeyCache == null && workerTopKAlgorithm != null;
×
1414
  }
1415

1416
  /**
1417
   * Returns a human-readable label of the current deployment mode based on the
1418
   * presence/absence of the cache and Worker TopK fields.
1419
   */
1420
  private String currentModeLabel() {
1421
    if (hotKeyCache != null && workerTopKAlgorithm != null) {
3!
NEW
1422
      return "Coexistence mode";
×
1423
    }
1424
    if (hotKeyCache != null) {
3!
NEW
1425
      return "App-only mode";
×
1426
    }
1427
    if (workerTopKAlgorithm != null) {
3!
1428
      return "Worker-only mode";
2✔
1429
    }
NEW
1430
    return "Uninitialized mode (no cache, no TopK)";
×
1431
  }
1432

1433
  /**
1434
   * Requires app-side cache; throws {@link HotKeyModeException} with an
1435
   * operation-tagged message when running in Worker-only mode.
1436
   *
1437
   * @param operation the API operation name (e.g. {@code "get"}, {@code "putThrough"})
1438
   */
1439
  private void requireAppCache(String operation) {
1440
    if (hotKeyCache == null) {
3✔
1441
      throw new HotKeyModeException(operation, currentModeLabel(), "App-mode cache");
8✔
1442
    }
1443
  }
1✔
1444

1445
  /**
1446
   * Requires app-side TopK detector; throws {@link HotKeyModeException} when
1447
   * the app-side detector is unavailable (Worker-only mode).
1448
   *
1449
   * @param operation the API operation name
1450
   */
1451
  private void requireAppDetector(String operation) {
1452
    if (appHotKeyDetector == null) {
3!
NEW
1453
      throw new HotKeyModeException(operation, currentModeLabel(), "App-mode TopK detector");
×
1454
    }
1455
  }
1✔
1456

1457

1458
}
1459

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