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

Hyshmily / hotkey / 28916241081

08 Jul 2026 03:53AM UTC coverage: 89.468% (-0.7%) from 90.12%
28916241081

push

github

Hyshmily
refactor(core): extract central dispatcher and broadcast buffer

Introduce CentralDispatcher for centralized event routing and BroadcastBuffer for cache synchronization. Refactor HotKeyCache, SlidingWindowDetector, and related components to use the new dispatching mechanism, improving separation of concerns and reducing coupling.

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

1328 of 1564 branches covered (84.91%)

Branch coverage included in aggregate %.

102 of 134 new or added lines in 17 files covered. (76.12%)

3 existing lines in 2 files now uncovered.

3769 of 4133 relevant lines covered (91.19%)

4.21 hits per line

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

96.3
/worker/src/main/java/io/github/hyshmily/hotkey/worker/config/WorkerAutoConfiguration.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.worker.config;
17

18
import io.github.hyshmily.hotkey.constants.HotKeyConstants;
19
import io.github.hyshmily.hotkey.detection.HotKeyStateMachine;
20
import io.github.hyshmily.hotkey.detection.impl.HotKeyStateMachineImpl;
21
import io.github.hyshmily.hotkey.hotkeydetector.heavykeeper.HeavyKeeper;
22
import io.github.hyshmily.hotkey.hotkeydetector.heavykeeper.TopK;
23
import io.github.hyshmily.hotkey.util.InstanceIdGenerator;
24
import io.github.hyshmily.hotkey.worker.detection.GlobalQpsEstimator;
25
import io.github.hyshmily.hotkey.worker.detection.SlidingWindowDetector;
26
import io.github.hyshmily.hotkey.worker.detection.ThresholdLearner;
27
import io.github.hyshmily.hotkey.worker.detection.TopKValidator;
28
import io.github.hyshmily.hotkey.worker.dispatch.VerifyConsumer;
29
import io.github.hyshmily.hotkey.worker.dispatch.WorkerBroadcaster;
30
import io.github.hyshmily.hotkey.worker.dispatch.WorkerHeartbeatProducer;
31
import io.github.hyshmily.hotkey.worker.ingest.ReportConsumer;
32
import io.github.hyshmily.hotkey.worker.persistence.TopKPersistService;
33
import jakarta.annotation.PostConstruct;
34
import java.util.concurrent.ScheduledExecutorService;
35
import java.util.concurrent.TimeUnit;
36
import java.util.concurrent.atomic.AtomicLong;
37
import lombok.RequiredArgsConstructor;
38
import lombok.extern.slf4j.Slf4j;
39
import org.springframework.amqp.core.*;
40
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
41
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
42
import org.springframework.amqp.rabbit.core.RabbitTemplate;
43
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
44
import org.springframework.amqp.support.converter.MessageConverter;
45
import org.springframework.amqp.support.converter.SimpleMessageConverter;
46
import org.springframework.beans.factory.annotation.Qualifier;
47
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
48
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
49
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
50
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
51
import org.springframework.boot.context.properties.EnableConfigurationProperties;
52
import org.springframework.context.annotation.Bean;
53
import org.springframework.context.annotation.Configuration;
54
import org.springframework.data.redis.connection.RedisConnectionFactory;
55
import org.springframework.data.redis.core.StringRedisTemplate;
56
import org.springframework.scheduling.annotation.EnableScheduling;
57
import org.springframework.scheduling.annotation.Scheduled;
58

59
/**
60
 * Auto‑configuration for the <b>hot‑key Worker</b>.
61
 *
62
 * <p>Activates when {@code hotkey.worker.enabled=true} and RabbitMQ is on the
63
 * classpath.  The Worker consumes per‑key access counts reported by application
64
 * instances, applies a sliding‑window + state‑machine pipeline, and broadcasts
65
 * HOT / COOL decisions back to every instance.
66
 *
67
 * <h2>Provisioned beans</h2>
68
 * <ul>
69
 *   <li>{@link SlidingWindowDetector} – sliding‑window counter.</li>
70
 *   <li>{@link HotKeyStateMachine} – per‑key lifecycle state machine.</li>
71
 *   <li>{@link TopK} (worker‑scoped) – global frequency estimator for
72
 *       Top‑K cross‑validation and pre‑warming.</li>
73
 *   <li>{@link TopKValidator} – periodic Top‑K inspection that pre‑warms
74
 *       stable hot keys.</li>
75
 *   <li>{@link ReportConsumer} – AMQP listener that drives the pipeline.</li>
76
 *   <li>{@link WorkerBroadcaster} – publishes HOT / COOL broadcasts.</li>
77
 *   <li>RabbitMQ topology ({@code reportExchange}, shard‑specific
78
 *       {@code reportQueue}, binding).</li>
79
 *   <li>Scheduled tasks for stale‑state eviction and Top‑K validation.</li>
80
 * </ul>
81
 *
82
 * Default constructor.
83
 */
84
@Configuration
85
@ConditionalOnClass(name = "org.springframework.amqp.rabbit.core.RabbitTemplate")
86
@ConditionalOnProperty(prefix = "hotkey.worker", name = "enabled", havingValue = "true")
87
@EnableConfigurationProperties(WorkerProperties.class)
88
@EnableScheduling
89
@RequiredArgsConstructor
90
@Slf4j
4✔
91
public class WorkerAutoConfiguration {
92

93
  private final WorkerProperties properties;
94

95
  /**
96
   * Worker node identity — auto-generated once per JVM.
97
   *
98
   * <p>Used in queue names ({@code hotkey.worker.config.<nodeId>}) and heartbeat
99
   * messages to identify this Worker instance uniquely.
100
   */
101
  private final String nodeId = InstanceIdGenerator.get();
102

103
  /**
104
   * Worker TopK snapshot service that persists the current hot-key list to
105
   * Redis and restores it on startup.
106
   *
107
   * <p>Only active when {@code hotkey.worker.persistence.enabled=true}.
108
   *
109
   * @param workerTopK  the worker-scoped HeavyKeeper sketch to persist
110
   * @param redisTemplate the Redis template used for persistence
111
   * @param properties  worker configuration providing persistence settings
112
   * @return a new {@link TopKPersistService} instance
113
   */
114
  @Bean
115
  @ConditionalOnProperty(prefix = "hotkey.worker.persistence", name = "enabled", havingValue = "true")
116
  public TopKPersistService topKPersistService(
117
    @Qualifier("workerTopK") TopK workerTopK,
118
    StringRedisTemplate redisTemplate,
119
    WorkerProperties properties
120
  ) {
121
    return new TopKPersistService(
6✔
122
      workerTopK,
123
      redisTemplate,
124
      properties.getRouting().getAppName(),
5✔
125
      nodeId,
126
      properties.getPersistence()
2✔
127
    );
128
  }
129

130
  /**
131
   * Schedules periodic TopK snapshot persistence. The returned placeholder bean
132
   * keeps the task alive in the context.
133
   *
134
   * @param service    the TopK persistence service whose {@code persistToRedis} is scheduled
135
   * @param properties worker configuration providing the persistence interval
136
   * @param scheduler  the shared worker scheduler
137
   * @return a placeholder {@link Object} bean that keeps the scheduled task alive
138
   */
139
  @Bean
140
  @ConditionalOnBean(TopKPersistService.class)
141
  public Object topKPersistTask(
142
    TopKPersistService service,
143
    WorkerProperties properties,
144
    @Qualifier("hotKeyScheduler") ScheduledExecutorService scheduler
145
  ) {
146
    long interval = properties.getPersistence().getPersistIntervalMs();
4✔
147
    try {
148
      scheduler.scheduleAtFixedRate(service::persistToRedis, interval, interval, TimeUnit.MILLISECONDS);
11✔
149
    } catch (Exception e) {
×
150
      log.error("Failed to schedule TopK persistence task; Worker TopK snapshots will not be persisted.", e);
×
151
    }
1✔
152
    return new Object();
4✔
153
  }
154

155
  /**
156
   * Validates that the sliding-window duration is evenly divisible by the
157
   * number of slices to avoid rounding inaccuracies.
158
   */
159
  @PostConstruct
160
  void validateWindowConfig() {
161
    if (properties.getSlidingWindow().getDurationMs() % properties.getSlidingWindow().getSlices() != 0) {
13✔
162
      log.warn(
5✔
163
        "windowDurationMs ({}) is not evenly divisible by windowSlices ({}). " +
164
          "This introduces rounding inaccuracies in window calculations.",
165
        properties.getSlidingWindow().getDurationMs(),
5✔
166
        properties.getSlidingWindow().getSlices()
3✔
167
      );
168
    }
169
  }
1✔
170

171
  /**
172
   * Creates the sliding‑window detector.
173
   *
174
   * <p>If an absolute hot threshold is configured via
175
   * {@code hotkey.worker.hot-threshold} it is used directly; otherwise the
176
   * value is set to {@code Long.MAX_VALUE} and the ratio‑based threshold
177
   * will be calculated later by the dynamic‑threshold logic (not shown here).
178
   *
179
   * @param properties worker configuration providing sliding-window parameters
180
   * @return a new {@link SlidingWindowDetector} instance
181
   */
182
  @Bean
183
  public SlidingWindowDetector slidingWindowDetector(WorkerProperties properties) {
184
    long threshold = properties.getThreshold().getHotThreshold();
4✔
185
    if (threshold <= 0) {
4✔
186
      threshold = Long.MAX_VALUE;
2✔
187
    }
188
    return new SlidingWindowDetector(
4✔
189
      properties.getSlidingWindow().getDurationMs(),
3✔
190
      properties.getSlidingWindow().getSlices(),
4✔
191
      threshold
192
    );
193
  }
194

195
  /**
196
   * Creates the per‑key lifecycle state machine.
197
   *
198
   * @param properties worker configuration properties providing confirm, cool, and
199
   *                   pre-cool grace window counts (converted from durations via
200
   *                   {@link WorkerProperties#getConfirmWindows()},
201
   *                   {@link WorkerProperties#getCoolWindows()}, and
202
   *                   {@link WorkerProperties#getPreCoolGraceWindows()})
203
   * @return a new {@link HotKeyStateMachine} instance
204
   */
205
  @Bean
206
  public HotKeyStateMachine hotKeyStateMachine(WorkerProperties properties) {
207
    return new HotKeyStateMachineImpl(
4✔
208
      properties.getConfirmWindows(),
2✔
209
      properties.getCoolWindows(),
2✔
210
      properties.getPreCoolGraceWindows()
2✔
211
    );
212
  }
213

214
  /**
215
   * Worker‑scoped Top‑K instance backed by a {@link HeavyKeeper} sketch.
216
   *
217
   * <p>This is intentionally a <b>separate bean</b> qualified with
218
   * {@code "workerTopK"} so that it can be distinguished from any other
219
   * Top‑K instance that might be defined in the application context
220
   * (e.g. for dashboard queries).
221
   *
222
   * @param properties worker configuration providing HeavyKeeper algorithm parameters
223
   *                   (top-K count, width, depth, decay, minimum count)
224
   * @return a new {@link HeavyKeeper} instance qualified as {@code workerTopK}
225
   */
226
  @Bean
227
  public TopK workerTopK(WorkerProperties properties) {
228
    return new HeavyKeeper(
4✔
229
      properties.getHeavyKeeper().getTopK(),
3✔
230
      properties.getHeavyKeeper().getWidth(),
3✔
231
      properties.getHeavyKeeper().getDepth(),
3✔
232
      properties.getHeavyKeeper().getDecay(),
3✔
233
      properties.getHeavyKeeper().getMinCount()
3✔
234
    );
235
  }
236

237
  /**
238
   * Top‑K validator that periodically inspects the worker's Top‑K list
239
   * and pre‑warms keys that appear consistently.
240
   *
241
   * @param workerTopK  the worker-scoped HeavyKeeper sketch for frequency estimation
242
   * @param broadcaster the worker broadcaster used to emit pre-warm HOT decisions
243
   * @param properties  worker configuration providing TopK validation parameters
244
   *                    (pre-warm count and minimum consecutive appearances)
245
   * @return a new {@link TopKValidator} instance
246
   */
247
  @Bean
248
  public TopKValidator topKValidator(
249
    @Qualifier("workerTopK") TopK workerTopK,
250
    WorkerBroadcaster broadcaster,
251
    WorkerProperties properties
252
  ) {
253
    return new TopKValidator(
6✔
254
      workerTopK,
255
      broadcaster,
256
      properties.getTopKValidation().getPreWarmCount(),
3✔
257
      properties.getTopKValidation().getPreWarmMinAppearances()
3✔
258
    );
259
  }
260

261
  /**
262
   * Report consumer – the main AMQP entry point.
263
   *
264
   * <p>Injects the worker‑scoped Top‑K so that every consumed report also
265
   * feeds the frequency estimator.
266
   *
267
   * @param detector           the sliding-window detector for per-key access tracking
268
   * @param stateMachine       the per-key lifecycle state machine for HOT/COOL transitions
269
   * @param broadcaster        publishes HOT and COOL decisions to all application instances
270
   * @param topKValidator      pre-warm validator for cross-instance frequency-based confirmation
271
   * @param workerTopK         the worker-scoped HeavyKeeper sketch for frequency estimation
272
   * @param globalQpsEstimator the global QPS estimator tracking overall throughput
273
   * @return a new {@link ReportConsumer} instance
274
   */
275
  @Bean
276
  public ReportConsumer reportConsumer(
277
    SlidingWindowDetector detector,
278
    HotKeyStateMachine stateMachine,
279
    WorkerBroadcaster broadcaster,
280
    TopKValidator topKValidator,
281
    @Qualifier("workerTopK") TopK workerTopK,
282
    GlobalQpsEstimator globalQpsEstimator
283
  ) {
284
    return new ReportConsumer(detector, stateMachine, broadcaster, topKValidator, workerTopK, globalQpsEstimator);
10✔
285
  }
286

287
  /**
288
   * Broadcasts HOT / COOL decisions to all application instances.
289
   *
290
   * @param rabbitTemplate         the RabbitMQ template used to publish messages
291
   * @param properties             worker configuration providing exchange and routing settings
292
   * @param nodeId                 the Worker's node identity, injected via {@code @Qualifier("workerNodeId")}
293
   * @param epochCounter           the Worker's epoch counter, injected via {@code @Qualifier("workerEpochCounter")}
294
   * @param configTimestampCounter the shared monotonic counter for config-change timestamps
295
   * @return a new {@link WorkerBroadcaster} instance
296
   */
297
  @Bean
298
  public WorkerBroadcaster workerBroadcaster(
299
    RabbitTemplate rabbitTemplate,
300
    WorkerProperties properties,
301
    @Qualifier("workerNodeId") String nodeId,
302
    @Qualifier("workerEpochCounter") AtomicLong epochCounter,
303
    @Qualifier("configTimestampCounter") AtomicLong configTimestampCounter
304
  ) {
305
    return new WorkerBroadcaster(
5✔
306
      rabbitTemplate,
307
      properties.getMessaging().getBroadcastExchange(),
3✔
308
      properties.getRouting().getAppName(),
5✔
309
      nodeId,
310
      epochCounter
311
    );
312
  }
313

314
  /**
315
   * Direct exchange to which clients publish report messages.
316
   * Routing keys follow the pattern {@code report.<appName>.<nodeId>}.
317
   *
318
   * @param properties worker configuration providing the report exchange name
319
   * @return a durable, non-auto-delete {@link DirectExchange}
320
   */
321
  @Bean
322
  public DirectExchange reportExchange(WorkerProperties properties) {
323
    return new DirectExchange(properties.getMessaging().getReportExchange(), true, false);
9✔
324
  }
325

326
  /**
327
   * Queue that this Worker binds to.
328
   * Queue name is {@code hotkey.report.<appName>.<nodeId>},
329
   * guaranteeing that a key always lands on the same Worker.
330
   *
331
   * <p>The queue has a 7-day idle expiry ({@code x-expires}) since shard queues
332
   * are fixed-count and long-lived.
333
   *
334
   * @param properties worker configuration providing the routing app name
335
   * @return a durable {@link Queue} with the shard-specific name
336
   */
337
  @Bean
338
  public Queue reportQueue(WorkerProperties properties) {
339
    String queueName = HotKeyConstants.QUEUE_PREFIX_REPORT + properties.getRouting().getAppName() + "." + nodeId;
7✔
340
    return QueueBuilder.durable(queueName).withArgument("x-expires", 604_800_000).build();
8✔
341
  }
342

343
  /**
344
   * Binds the report queue to the report exchange using the shard-specific
345
   * routing key {@code report.<appName>.<nodeId>}.
346
   *
347
   * @param reportQueue    the shard-specific report queue
348
   * @param reportExchange the report exchange
349
   * @param properties     worker configuration providing the routing app name
350
   * @return a {@link Binding} connecting the queue to the exchange
351
   */
352
  @Bean
353
  public Binding reportBinding(Queue reportQueue, DirectExchange reportExchange, WorkerProperties properties) {
354
    String routingKey = HotKeyConstants.ROUTING_KEY_REPORT + properties.getRouting().getAppName() + "." + nodeId;
7✔
355
    return BindingBuilder.bind(reportQueue).to(reportExchange).with(routingKey);
7✔
356
  }
357

358
  /**
359
   * Auto-delete queue for receiving heartbeat-based config updates from
360
   * peer Workers.
361
   *
362
   * <p>Each Worker declares its own queue named
363
   * {@code hotkey.worker.config.<nodeId>}, which is automatically removed
364
   * when the Worker disconnects.  The queue is bound to the
365
   * {@code heartbeatExchange} with routing key {@code heartbeat.*}.
366
   *
367
   * @return a non-durable, auto-delete {@link Queue} unique to this Worker node
368
   */
369
  @Bean
370
  public Queue workerConfigQueue() {
371
    return QueueBuilder.nonDurable("hotkey.worker.config." + nodeId).autoDelete().build();
7✔
372
  }
373

374
  /**
375
   * Binds the per-Worker config queue to the heartbeat exchange with the
376
   * routing key pattern {@code heartbeat.*}.
377
   *
378
   * @param workerConfigQueue the per-Worker config queue to bind
379
   * @param heartbeatExchange the heartbeat topic exchange
380
   * @return a {@link Binding} connecting the config queue to the heartbeat exchange
381
   */
382
  @Bean
383
  public Binding workerConfigBinding(Queue workerConfigQueue, TopicExchange heartbeatExchange) {
384
    return BindingBuilder.bind(workerConfigQueue)
4✔
385
      .to(heartbeatExchange)
2✔
386
      .with(HotKeyConstants.ROUTING_KEY_HEARTBEAT + "*");
1✔
387
  }
388

389
  /**
390
   * Topic exchange for epoch-aware structured heartbeats from Workers.
391
   *
392
   * @param properties worker configuration providing the heartbeat exchange name
393
   * @return a durable, non-auto-delete {@link TopicExchange}
394
   */
395
  @Bean
396
  public TopicExchange heartbeatExchange(WorkerProperties properties) {
397
    return new TopicExchange(properties.getMessaging().getHeartbeatExchange(), true, false);
9✔
398
  }
399

400
  /**
401
   * Exposes the Worker's node ID as a Spring bean for injection.
402
   *
403
   * @return the unique node identifier for this Worker instance
404
   */
405
  @Bean
406
  @Qualifier("workerNodeId")
407
  public String workerNodeId() {
408
    return nodeId;
3✔
409
  }
410

411
  /**
412
   * Monotonically increasing epoch counter for this Worker instance.
413
   *
414
   * <p>The epoch is incremented on every Worker restart and transmitted in
415
   * broadcast messages (see {@link WorkerBroadcaster#sendBroadcast}). Receiving
416
   * apps use the epoch to detect Worker restarts and unconditionally accept
417
   * decisions from a higher epoch (ADR-0010).
418
   *
419
   * @return a new {@link AtomicLong} initialised to {@code 0}
420
   */
421
  @Bean
422
  @Qualifier("workerEpochCounter")
423
  public AtomicLong workerEpochCounter() {
424
    return new AtomicLong(0);
5✔
425
  }
426

427
  /**
428
   * Monotonically increasing counter for config-change timestamps.
429
   *
430
   * <p>Every {@code POST /actuator/worker/state} call increments this counter.
431
   * Receiving Workers compare their own counter against the incoming value;
432
   * only strictly newer configs are applied.
433
   *
434
   * @return a new {@link AtomicLong} initialised to {@code 0}
435
   */
436
  @Bean
437
  @Qualifier("configTimestampCounter")
438
  public AtomicLong configTimestampCounter() {
439
    return new AtomicLong(0);
5✔
440
  }
441

442
  /**
443
   * Periodically evicts stale keys from the sliding‑window detector and
444
   * the state machine.
445
   *
446
   * <p>The eviction threshold is set to {@code 2 * coolDurationMs},
447
   * giving keys a generous grace period after their last access before
448
   * their data structures are reclaimed.
449
   *
450
   * @param detector     the sliding-window detector whose idle keys will be evicted
451
   * @param stateMachine the state machine whose idle entries will be evicted
452
   * @param properties   worker configuration providing the cool duration for stale threshold
453
   * @return a new {@link EvictStaleTask} instance
454
   */
455
  @Bean
456
  public EvictStaleTask evictStaleTask(
457
    SlidingWindowDetector detector,
458
    HotKeyStateMachine stateMachine,
459
    WorkerProperties properties
460
  ) {
461
    return new EvictStaleTask(detector, stateMachine, properties);
7✔
462
  }
463

464
  /**
465
   * Scheduled task that evicts stale keys from the sliding-window detector
466
   * and state machine.
467
   *
468
   * Default constructor.
469
   */
470
  @RequiredArgsConstructor
471
  public static class EvictStaleTask {
472

473
    private final SlidingWindowDetector detector;
474
    private final HotKeyStateMachine stateMachine;
475
    private final WorkerProperties properties;
476

477
    /**
478
     * Evicts keys that have not been accessed within {@code 2 * coolDurationMs}.
479
     */
480
    @Scheduled(fixedDelayString = "${hotkey.worker.state-machine.evict-interval-ms:30000}")
481
    public void evictStale() {
482
      try {
483
        long staleAfterMs = properties.getStateMachine().getCoolDurationMs() * 2;
7✔
484
        detector.evictStale(staleAfterMs);
4✔
485
        stateMachine.evictStale(staleAfterMs);
4✔
486
        log.debug("EvictStale tick: staleAfterMs={}", staleAfterMs);
5✔
487
      } catch (Exception e) {
1✔
488
        log.error("Scheduled evictStale failed", e);
4✔
489
      }
1✔
490
    }
1✔
491
  }
492

493
  /**
494
   * Periodically runs Top‑K cross‑validation to detect slow‑warming hot
495
   * keys that the sliding window might miss.
496
   *
497
   * <p>Interval is controlled by {@code hotkey.worker.topk-validation.validate-interval-ms}
498
   * (default 60 seconds).
499
   *
500
   * @param topKValidator the TopK validator whose {@link TopKValidator#validate()} will be scheduled
501
   * @return a new {@link TopKValidationTask} instance
502
   */
503
  @Bean
504
  public TopKValidationTask topKValidationTask(TopKValidator topKValidator) {
505
    return new TopKValidationTask(topKValidator);
5✔
506
  }
507

508
  /**
509
   * Scheduled task that inspects the Worker TopK and pre-warms stable hot keys.
510
   *
511
   * Default constructor.
512
   */
513
  @RequiredArgsConstructor
514
  public static class TopKValidationTask {
515

516
    private final TopKValidator topKValidator;
517

518
    /**
519
     * Runs TopK validation and pre-warm logic.
520
     */
521
    @Scheduled(fixedDelayString = "${hotkey.worker.topk-validation.validate-interval-ms:60000}")
522
    public void validate() {
523
      try {
524
        topKValidator.validate();
3✔
525
        log.debug("TopK validation tick completed");
3✔
526
      } catch (Exception e) {
1✔
527
        log.error("Scheduled TopK validation failed", e);
4✔
528
      }
1✔
529
    }
1✔
530
  }
531

532
  /**
533
   * Global QPS estimator that tracks overall throughput across all keys in the
534
   * current shard using a sliding window.
535
   *
536
   * @param properties worker configuration properties (sliding-window duration
537
   *                   and slice count are extracted from here)
538
   * @return a new {@link GlobalQpsEstimator} instance
539
   */
540
  @Bean
541
  public GlobalQpsEstimator globalQpsEstimator(WorkerProperties properties) {
542
    return new GlobalQpsEstimator(
4✔
543
      properties.getSlidingWindow().getDurationMs(),
3✔
544
      properties.getSlidingWindow().getSlices()
3✔
545
    );
546
  }
547

548
  /**
549
   * Threshold learner that periodically recalculates the hot-key threshold
550
   * based on estimated global QPS and updates the sliding-window detector.
551
   *
552
   * @param estimator the global QPS estimator
553
   * @param detector  the sliding-window detector whose threshold will be
554
   *                  dynamically adjusted
555
   * @param properties worker configuration properties for threshold tuning
556
   *                   parameters (ratio, tolerance, learning period)
557
   * @return a new {@link ThresholdLearner} instance
558
   */
559
  @Bean
560
  public ThresholdLearner thresholdLearner(
561
    GlobalQpsEstimator estimator,
562
    SlidingWindowDetector detector,
563
    WorkerProperties properties
564
  ) {
565
    return new ThresholdLearner(estimator, detector, properties);
7✔
566
  }
567

568
  /**
569
   * {@link SimpleRabbitListenerContainerFactory} using {@link SimpleMessageConverter}
570
   * for the Worker config listener, avoiding Jackson JSON conversion of heartbeat
571
   * messages (which use a custom header-based format).
572
   *
573
   * @param connectionFactory the RabbitMQ connection factory
574
   * @return a configured {@link SimpleRabbitListenerContainerFactory} instance
575
   */
576
  @Bean
577
  public SimpleRabbitListenerContainerFactory workerConfigListenerContainerFactory(
578
    ConnectionFactory connectionFactory
579
  ) {
580
    SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
4✔
581
    factory.setConnectionFactory(connectionFactory);
3✔
582
    factory.setMessageConverter(new SimpleMessageConverter());
5✔
583
    return factory;
2✔
584
  }
585

586
  /**
587
   * Fallback JSON message converter for report messages. Active only when the
588
   * common module's {@code reportMessageConverter} is absent (e.g. in tests).
589
   *
590
   * @return a {@link org.springframework.amqp.support.converter.Jackson2JsonMessageConverter} instance
591
   */
592
  @Bean("reportMessageConverter")
593
  @ConditionalOnMissingBean(name = "reportMessageConverter")
594
  public MessageConverter reportMessageConverter() {
595
    return new org.springframework.amqp.support.converter.Jackson2JsonMessageConverter();
4✔
596
  }
597

598
  /**
599
   * Container factory for the {@link ReportConsumer}'s {@code @RabbitListener}.
600
   * Lifts throughput above Spring Boot's default (concurrency=1) by exposing
601
   * concurrent-consumers and prefetch via {@code hotkey.worker.report-consumer.*}.
602
   *
603
   * @param connectionFactory the RabbitMQ connection factory
604
   * @param reportMessageConverter the JSON message converter for report messages
605
   * @param properties worker configuration properties
606
   * @return a configured {@link SimpleRabbitListenerContainerFactory} instance
607
   */
608
  @Bean
609
  public SimpleRabbitListenerContainerFactory reportListenerContainerFactory(
610
    ConnectionFactory connectionFactory,
611
    @Qualifier("reportMessageConverter") MessageConverter reportMessageConverter,
612
    WorkerProperties properties
613
  ) {
614
    SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
4✔
615
    factory.setConnectionFactory(connectionFactory);
3✔
616
    factory.setMessageConverter(reportMessageConverter);
3✔
617
    factory.setConcurrentConsumers(properties.getReportConsumer().getConcurrentConsumers());
6✔
618
    factory.setMaxConcurrentConsumers(properties.getReportConsumer().getConcurrentConsumers());
6✔
619
    factory.setPrefetchCount(properties.getReportConsumer().getPrefetchCount());
6✔
620
    factory.setAcknowledgeMode(AcknowledgeMode.AUTO);
3✔
621
    factory.setDefaultRequeueRejected(false);
4✔
622
    return factory;
2✔
623
  }
624

625
  /**
626
   * Listens for heartbeat-based config updates from peer Workers and applies
627
   * them if the received config timestamp is newer than the local one.
628
   *
629
   * <p>On startup, waits up to 3 seconds for the first heartbeat to arrive.
630
   * If none is received, the Worker continues with the values from
631
   * {@link WorkerProperties}.
632
   *
633
   * @param stateMachine           the worker's state machine
634
   * @param configTimestampCounter the shared config-change timestamp counter
635
   * @return a new {@link WorkerConfigNegotiator} instance
636
   */
637
  @Bean
638
  public WorkerConfigNegotiator workerConfigNegotiator(
639
    HotKeyStateMachine stateMachine,
640
    @Qualifier("configTimestampCounter") AtomicLong configTimestampCounter
641
  ) {
642
    return new WorkerConfigNegotiator(stateMachine, configTimestampCounter, nodeId);
8✔
643
  }
644

645
  /**
646
   * Schedules periodic threshold recalculation using the {@link ThresholdLearner}.
647
   *
648
   * <p>The learner runs at the interval specified by
649
   * {@code hotkey.worker.global-qps-dynamic-threshold.recalculate-interval-ms}.
650
   * The returned placeholder bean keeps the scheduled task alive in the context.
651
   *
652
   * @param learner    the threshold learner to schedule
653
   * @param properties worker configuration for the recalculation interval
654
   * @param scheduler  the shared worker scheduler
655
   * @return a placeholder {@link Object} bean that keeps the scheduled task alive
656
   */
657
  @Bean
658
  public Object thresholdLearningTask(
659
    ThresholdLearner learner,
660
    WorkerProperties properties,
661
    @Qualifier("hotKeyScheduler") ScheduledExecutorService scheduler
662
  ) {
663
    try {
664
      scheduler.scheduleAtFixedRate(
5✔
665
        learner,
666
        properties.getGlobalQpsDynamicThreshold().getRecalculateIntervalMs(),
3✔
667
        properties.getGlobalQpsDynamicThreshold().getRecalculateIntervalMs(),
3✔
668
        TimeUnit.MILLISECONDS
669
      );
670
    } catch (Exception e) {
×
NEW
671
      log.error("Failed to schedule threshold learning task; dynamic threshold " + "adjustment will not run.", e);
×
672
    }
1✔
673
    return new Object(); // placeholder bean
4✔
674
  }
675

676
  /**
677
   * Auto-delete queue for on-demand verification requests (PING) from Apps.
678
   * Queue name: {@code hotkey.verify.ping.<nodeId>}.
679
   *
680
   * @return a non-durable, auto-delete {@link Queue} for PING/PONG verification
681
   */
682
  @Bean
683
  public Queue verifyPingQueue() {
684
    return QueueBuilder.nonDurable("hotkey.verify.ping." + nodeId).autoDelete().build();
7✔
685
  }
686

687
  /**
688
   * Handles PING requests from Apps, replies PONG via Direct reply-to.
689
   *
690
   * @param rabbitTemplate the RabbitMQ template used to send PONG responses
691
   * @return a new {@link VerifyConsumer} instance for this Worker node
692
   */
693
  @Bean
694
  public VerifyConsumer verifyConsumer(RabbitTemplate rabbitTemplate) {
695
    return new VerifyConsumer(rabbitTemplate, nodeId);
7✔
696
  }
697

698
  /**
699
   * Container for the verification ping queue (NONE ack, on-demand only).
700
   *
701
   * @param connectionFactory the RabbitMQ connection factory
702
   * @param verifyPingQueue   the auto-delete ping queue
703
   * @param verifyConsumer    the consumer whose {@link VerifyConsumer#handlePing} handles messages
704
   * @return a configured {@link SimpleMessageListenerContainer} with NONE acknowledgement mode
705
   */
706
  @Bean
707
  public SimpleMessageListenerContainer verifyPingContainer(
708
    ConnectionFactory connectionFactory,
709
    Queue verifyPingQueue,
710
    VerifyConsumer verifyConsumer
711
  ) {
712
    SimpleMessageListenerContainer c = new SimpleMessageListenerContainer(connectionFactory);
5✔
713
    c.setQueues(verifyPingQueue);
8✔
714
    c.setMessageListener(verifyConsumer::handlePing);
7✔
715
    c.setAcknowledgeMode(AcknowledgeMode.NONE);
3✔
716
    c.setErrorHandler(t -> log.warn("Verify ping listener uncaught exception (PONG will not be sent)", t));
3✔
717
    return c;
2✔
718
  }
719

720
  /**
721
   * Enhanced heartbeat producer that sends structured heartbeats with epoch,
722
   * load factor, decision-version watermark, and state-machine config gossip.
723
   *
724
   * <p>Replaces the old ping-only heartbeat approach. Uses its own internal scheduler.
725
   *
726
   * @param rabbitTemplate         the RabbitMQ template for publishing heartbeat messages
727
   * @param properties             worker configuration providing exchange and interval settings
728
   * @param stateMachine           the state machine providing config gossip fields
729
   * @param broadcaster            the broadcaster for reading the current decision version watermark
730
   * @param redisConnectionFactory the Redis connection factory for epoch initialization
731
   * @param configTimestampCounter the shared monotonic counter for config-change timestamps
732
   * @param scheduler              the shared worker scheduler for periodic heartbeat sends
733
   * @return a new {@link WorkerHeartbeatProducer} instance
734
   */
735
  @Bean
736
  public WorkerHeartbeatProducer workerHeartbeatProducer(
737
    RabbitTemplate rabbitTemplate,
738
    WorkerProperties properties,
739
    HotKeyStateMachine stateMachine,
740
    WorkerBroadcaster broadcaster,
741
    RedisConnectionFactory redisConnectionFactory,
742
    @Qualifier("configTimestampCounter") AtomicLong configTimestampCounter,
743
    @Qualifier("hotKeyScheduler") ScheduledExecutorService scheduler
744
  ) {
745
    return new WorkerHeartbeatProducer(
5✔
746
      rabbitTemplate,
747
      properties.getMessaging().getHeartbeatExchange(),
9✔
748
      nodeId,
749
      stateMachine,
750
      broadcaster,
751
      configTimestampCounter,
752
      redisConnectionFactory,
753
      properties.getHeartbeat().getPingIntervalMs(),
5✔
754
      scheduler
755
    );
756
  }
757
}
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