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

Hyshmily / hotkey / 28216525937

26 Jun 2026 04:08AM UTC coverage: 91.258% (-1.4%) from 92.707%
28216525937

push

github

Hyshmily
feat: add circuit breaker, null-value caching, and heartbeat refinements

- Introduce HotKeyCircuitBreaker with sliding-window failure detection
- Cache null/miss results with configurable TTL (null-value-ttl-seconds)
- Add per-Worker exponential backoff to heartbeat verifier (verify-max-backoff-ms)
- Support min-alive-workers config for cluster health threshold
- Migrate HotKeyEndpoint from Actuator @Endpoint to Spring MVC @RestController
- Add NumberFormatException handling in StateMachineEndpoint

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

1226 of 1419 branches covered (86.4%)

Branch coverage included in aggregate %.

181 of 229 new or added lines in 12 files covered. (79.04%)

1 existing line in 1 file now uncovered.

3503 of 3763 relevant lines covered (93.09%)

4.22 hits per line

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

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

18
import io.github.hyshmily.hotkey.sharding.ClusterHealthView;
19
import jakarta.validation.Valid;
20
import jakarta.validation.constraints.Max;
21
import jakarta.validation.constraints.Min;
22
import jakarta.validation.constraints.Positive;
23
import lombok.AccessLevel;
24
import lombok.Data;
25
import lombok.Getter;
26
import org.springframework.boot.context.properties.ConfigurationProperties;
27
import org.springframework.validation.annotation.Validated;
28

29
/**
30
 * Configuration properties bound from the {@code hotkey.*} prefix.
31
 *
32
 * <p>Groups: TopK algorithm parameters (capacity, sketch width/depth,
33
 * decay, minimum count), L1 Caffeine cache limits, SingleFlight
34
 * deduplication settings, executor thread pool, TTL overrides (hard
35
 * and soft, for normal and hot keys), and reporting shard topology.
36
 */
37
@Data
38
@Validated
39
@ConfigurationProperties(prefix = "hotkey.local")
40
public class HotKeyProperties {
41

42
  /** Number of top hot keys to track. */
43
  @Min(1)
44
  private int topK = 100;
45

46
  /** Width of the HeavyKeeper Count-Min Sketch (number of columns). */
47
  @Min(1)
48
  @Max(200_000)
49
  private int width = 50_000;
50

51
  /** Depth of the HeavyKeeper Count-Min Sketch (number of hash functions). */
52
  @Min(1)
53
  @Max(10)
54
  private int depth = 5;
55

56
  /** Decay factor for HeavyKeeper counter fading. */
57
  @Positive
58
  private double decay = 0.92;
59

60
  /** Minimum access count before a key is considered hot. */
61
  @Min(1)
62
  private int minCount = 10;
63

64
  /** Maximum number of entries in the L1 Caffeine cache. */
65
  @Min(1)
66
  private int localCacheMaxSize = 1000;
67

68
  /** Default TTL in minutes for non-hot entries in the L1 cache. */
69
  @Min(1)
70
  private int localCacheTtlMinutes = 5;
71

72
  /** Maximum number of in-flight deduplication entries. */
73
  @Min(1)
74
  private int inflightMaxSize = 50_000;
75

76
  /** TTL in seconds for in-flight deduplication entries. */
77
  @Min(1)
78
  private int inflightTtlSeconds = 5;
79

80
  /** Timeout in seconds for waiting on an in-flight cache load. */
81
  @Min(1)
82
  private int inflightTimeoutSeconds = 3;
83

84
  /** Core pool size for the HotKey async executor. */
85
  @Min(1)
86
  private int executorCorePoolSize = 8;
87

88
  /** Maximum pool size for the HotKey async executor. */
89
  @Min(1)
90
  private int executorMaxPoolSize = 32;
91

92
  /** Queue capacity for the HotKey async executor before rejection. */
93
  @Min(1)
94
  private int executorQueueCapacity = 2000;
95

96
  /** Pool size for the shared HotKey scheduler (periodic tasks). */
97
  @Min(1)
98
  private int schedulerPoolSize = 8;
99

100
  /**
101
   * Expected number of Worker nodes in the cluster.
102
   * <p>Used by {@link ClusterHealthView} for majority-quorum health checks.
103
   * If set to 0 (default), the cluster is always considered unhealthy until
104
   * Worker heartbeats dynamically update the count.
105
   * <p>For production deployments with a fixed Worker count, set this to
106
   * the expected number of Worker instances for accurate health detection.
107
   */
108
  @Min(0)
109
  private int expectedWorkerCount = 0;
110

111
  /** Capacity of the expelled-key queue in HeavyKeeper. */
112
  @Min(1)
113
  private int expelledQueueCapacity = 50_000;
114

115
  /** Default hard TTL (ms) for normal keys — fallback when {@link #hardTtlMs} is not set. */
116
  private long defaultHardTtlMs = 300_000;
117

118
  /** Override hard TTL (ms) for normal keys. 0 means use {@link #defaultHardTtlMs}. */
119
  private long hardTtlMs;
120

121
  /** Default hard TTL (ms) for hot keys — fallback when {@link #hotHardTtlMs} is not set. */
122
  private long defaultHotHardTtlMs = 3_600_000;
123

124
  /** Override hard TTL (ms) for hot keys. 0 means use {@link #defaultHotHardTtlMs}. */
125
  private long hotHardTtlMs;
126

127
  /** Default soft TTL (ms) for normal keys — fallback when {@link #softTtlMs} is not set. */
128
  private long defaultSoftTtlMs = 30_000;
129

130
  /** Override soft TTL (ms) for normal keys. 0 means use {@link #defaultSoftTtlMs}. */
131
  private long softTtlMs;
132

133
  /** Default soft TTL (ms) for hot keys — fallback when {@link #hotSoftTtlMs} is not set. */
134
  private long defaultHotSoftTtlMs = 300_000;
135

136
  /** Override soft TTL (ms) for hot keys. 0 means use {@link #defaultHotSoftTtlMs}. */
137
  private long hotSoftTtlMs;
138

139
  /** TTL (seconds) for null/cache-miss entries. Kept short to avoid caching negative results. */
140
  private int nullValueTtlSeconds = 10;
141

142
  public long effectiveNullTtlMs() {
NEW
143
    return nullValueTtlSeconds > 0 ? nullValueTtlSeconds * 1000L : Long.MAX_VALUE;
×
144
  }
145

146
  /**
147
   * Effective hard TTL for normal keys.
148
   * Returns the override value if set, otherwise the default.
149
   *
150
   * @return the effective hard TTL in milliseconds
151
   */
152
  public long effectiveHardTtlMs() {
153
    return hardTtlMs > 0 ? hardTtlMs : defaultHardTtlMs;
11✔
154
  }
155

156
  /**
157
   * Effective hard TTL for hot keys.
158
   * Returns the override value if set, otherwise the default.
159
   *
160
   * @return the effective hot-key hard TTL in milliseconds
161
   */
162
  public long effectiveHotHardTtlMs() {
163
    return hotHardTtlMs > 0 ? hotHardTtlMs : defaultHotHardTtlMs;
11✔
164
  }
165

166
  /**
167
   * Effective soft TTL for normal keys.
168
   * Returns the override value if set, otherwise the default. Returns 0 if disabled.
169
   *
170
   * @return the effective soft TTL in milliseconds, or 0 if disabled
171
   */
172
  public long effectiveSoftTtlMs() {
173
    return softTtlMs > 0 ? softTtlMs : defaultSoftTtlMs;
11✔
174
  }
175

176
  /**
177
   * Effective soft TTL for hot keys.
178
   * Returns the override value if set, otherwise the default. Returns 0 if disabled.
179
   *
180
   * @return the effective hot-key soft TTL in milliseconds, or 0 if disabled
181
   */
182
  public long effectiveHotSoftTtlMs() {
183
    return hotSoftTtlMs > 0 ? hotSoftTtlMs : defaultHotSoftTtlMs;
11✔
184
  }
185

186
  /**
187
   * Whether any soft TTL is configured (normal or hot).
188
   *
189
   * @return {@code true} if either normal or hot key soft TTL is enabled
190
   */
191
  public boolean isSoftExpireEnabled() {
192
    return effectiveSoftTtlMs() > 0 || effectiveHotSoftTtlMs() > 0;
14!
193
  }
194

195
  /** Whether to apply random jitter (±ttlJitterRatio) to TTL expiry timestamps to prevent cache stampedes. */
196
  private boolean ttlJitterEnabled = true;
197

198
  /** Jitter ratio (0.0–1.0) applied to TTLs — e.g. 0.1 means ±10% random offset. */
199
  @Min(0)
200
  @Max(1)
201
  private double ttlJitterRatio = 0.1;
202

203
  /** Maximum number of concurrent refresh tasks for soft-expiry management. */
204
  @Min(1)
205
  private int refreshMaxPools = 100;
206

207
  /** TTL in minutes for Redis version keys used in stale-detection. */
208
  @Min(1)
209
  private int versionKeyTtlMinutes = 60;
210

211
  /** Application name used for queue naming and routing keys. */
212
  private String appName = "default";
213

214
  /** Exchange name for app-to-Worker report routing. */
215
  private String reportExchange = "hotkey.report.exchange";
216

217
  /** Interval in ms at which the reporter flushes batches to RabbitMQ. */
218
  private long reportIntervalMs = 50;
219

220
  /** Number of shards for report partitioning (only used when consistent-hashing is disabled). */
221
  private int shardCount = 1;
222

223
  /** Capacity of the per-shard report queue. */
224
  @Min(1)
225
  private int queueCapacity = 10_000;
226

227
  /** Timeout in ms for offering new entries to the report queue. */
228
  @Min(1)
229
  private int queueOfferTimeoutMs = 100;
230

231
  /** Number of consumer threads for report processing. 0 means auto-compute. */
232
  @Min(0)
233
  private int consumerCount = 0;
234

235
  /**
236
   * Effective consumer thread count: configured value, or max(4, availableProcessors/2).
237
   * Uses available CPU cores as a baseline — scales with machine capacity without
238
   * requiring static shard configuration in a dynamic topology.
239
   * A floor of 4 ensures adequate parallelism for report dispatch even on small instances.
240
   *
241
   * @return the effective number of consumer threads
242
   */
243
  public int effectiveConsumerCount() {
244
    return consumerCount > 0 ? consumerCount : Math.max(4, Runtime.getRuntime().availableProcessors() / 2);
13✔
245
  }
246

247
  /**
248
   * Configuration for consistent-hashing based report routing.
249
   * <p>
250
   * When enabled, the app uses a consistent-hash ring to select the target
251
   * shard for each key, ensuring the same key always maps to the same Worker
252
   * shard even when the shard set changes.
253
   */
254
  @Data
255
  public static class ConsistentHashing {
256

257
    /** Whether consistent-hashing mode is enabled. */
258
    private boolean enabled = true;
259

260
    /** Number of virtual nodes per physical shard on the hash ring. */
261
    @Min(1)
262
    private int virtualNodes = 500;
263
  }
264

265
  /** Consistent-hashing configuration for report routing. */
266
  @Valid
267
  private ConsistentHashing consistentHashing = new ConsistentHashing();
268

269
  /** Reporting BBR adaptive rate-limiter and CPU monitor configuration. */
270
  @Valid
271
  private ReporterLimiter reporter = new ReporterLimiter();
272

273
  /**
274
   * Configuration for the BBR adaptive rate-limiter and CPU monitor.
275
   * <p>
276
   * Controls CPU threshold, polling interval, EMA decay factor, BBR sliding
277
   * window parameters, and the cooldown period after a rate-limit drop.
278
   */
279
  @Data
280
  public static class ReporterLimiter {
281

282
    /** Whether BBR adaptive rate-limiting is enabled. */
283
    @Getter(AccessLevel.NONE)
284
    private boolean enabled = true;
285

286
    /** CPU threshold (0–1000, default 800 = 80 %). */
287
    @Min(0)
288
    @Max(1000)
289
    private int cpuThreshold = 800;
290

291
    /** CPU polling interval in milliseconds. */
292
    @Min(100)
293
    private long cpuPollIntervalMs = 500;
294

295
    /** EMA decay factor for CPU smoothing (0.0 – 1.0). */
296
    private double cpuDecay = 0.95;
297

298
    /** BBR sliding window size in milliseconds. */
299
    @Min(100)
300
    private long bbrWindowMs = 10_000;
301

302
    /** Number of buckets in the BBR sliding window. */
303
    @Min(2)
304
    private int bbrWindowBuckets = 100;
305

306
    /** Cooldown period in ms after a drop before allowing again. */
307
    @Min(0)
308
    private long bbrCooldownMs = 1_000;
309
  }
310

311
  /**
312
   * Explicit instance ID for queue naming.
313
   * Falls back to {@code server.port-HOSTNAME} (or {@code server.port-UUID} if {@code HOSTNAME} is unset) if empty.
314
   */
315
  private String instanceId = "";
316

317
  /**
318
   * Configuration for Worker heartbeat exchange and liveliness detection.
319
   * <p>
320
   * Controls the exchange name, timeout intervals for heartbeat reception,
321
   * PING verification, and the number of consecutive failures before
322
   * graceful degradation is triggered.
323
   */
324
  @Data
325
  public static class Heartbeat {
326

327
    /** Heartbeat exchange name. */
328
    private String exchangeName = "hotkey.heartbeat.exchange";
329
    /** Heartbeat timeout (ms). */
330
    private int timeoutMs = 15000;
331
    /** Verify interval (ms). */
332
    private long verifyIntervalMs = 5000;
333
    /** PING timeout (ms). */
334
    private long pingTimeoutMs = 3000;
335
    /** Degrade after consecutive failures. */
336
    private int degradeAfterFailures = 3;
337
    /** Max backoff (ms) for per-Worker exponential backoff between verification probes. */
338
    private long verifyMaxBackoffMs = 60_000;
339
    /**
340
     * Minimum alive workers for cluster health. 0 = use majority formula (knownWorkerCount / 2 + 1).
341
     * Set to a positive value to override the default majority threshold.
342
     */
343
    private int minAliveWorkers;
344
  }
345

346
  /** Worker heartbeat and liveliness detection configuration. */
347
  @Valid
348
  private Heartbeat heartbeat = new Heartbeat();
349

350
  /**
351
   * Configuration for the sliding-window circuit breaker.
352
   *
353
   * <p>When enabled, protects remote calls (reader suppliers) from cascading failures.
354
   * Default is disabled — users should only enable when their cache-load suppliers
355
   * (e.g. database queries, remote API calls) are prone to timeout or error cascades.
356
   */
357
  @Data
358
  public static class CircuitBreaker {
359

360
    /** Whether circuit breaker is enabled. Default {@code false}. */
361
    private boolean enabled = false;
362

363
    /** Sliding window duration in milliseconds. */
364
    private long windowTimeMs = 10_000;
365

366
    /** Number of buckets in the sliding window. */
367
    private int windowBuckets = 10;
368

369
    /** Failure rate threshold (0.0–1.0) — opens when exceeded. */
370
    private double failThreshold = 0.5;
371

372
    /** Minimum total requests in the window before the breaker evaluates the failure rate. */
373
    private long requestVolumeThreshold = 20;
374

375
    /** How long to wait (ms) before allowing a half-open probe request. */
376
    private long singleTestIntervalMs = 5_000;
377

378
    /** Whether to log state transitions. */
379
    private boolean logEnabled = true;
380
  }
381

382
  /** Circuit breaker configuration. */
383
  @Valid
384
  private CircuitBreaker circuitBreaker = new CircuitBreaker();
385

386
  /**
387
   * Configuration for Spring {@code @Cacheable} / {@code @CachePut} / {@code @CacheEvict} integration.
388
   *
389
   * <p>When enabled, HotKey detects hot keys on all Spring Cache reads and applies
390
   * dynamic TTL via Caffeine L1.
391
   */
392
  @Data
393
  public static class SpringCache {
394

395
    /** Whether Spring Cache integration is enabled ({@code hotkey.spring-cache.enabled}). */
396
    private boolean enabled = false;
397

398
    /** Separator between cache name and key in the internal cache key string. */
399
    private String keySeparator = "::";
400
  }
401

402
  /** Spring Cache integration configuration. */
403
  @Valid
404
  private SpringCache springCache = new SpringCache();
405

406
  /** Number of {@code SET NX} retries for distributed lock acquisition. */
407
  private int tryLockLockCount = 2;
408

409
  /** Number of {@code GET} inquiries after a transient {@code SET NX} failure. */
410
  private int tryLockInquiryCount = 1;
411

412
  /** Number of {@code DEL} retries for distributed lock release. */
413
  private int tryLockUnlockCount = 2;
414
}
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