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

Hyshmily / hotkey / 28290463292

27 Jun 2026 01:22PM UTC coverage: 91.227% (-0.7%) from 91.882%
28290463292

push

github

Hyshmily
test: fix CI tests

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

1250 of 1427 branches covered (87.6%)

Branch coverage included in aggregate %.

3544 of 3828 relevant lines covered (92.58%)

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

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

90
  private final WorkerProperties properties;
91

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

100
  /**
101
   * Worker TopK snapshot service that persists the current hot-key list to
102
   * Redis and restores it on startup.
103
   *
104
   * <p>Only active when {@code hotkey.worker.persistence.enabled=true}.
105
   */
106
  @Bean
107
  @ConditionalOnProperty(prefix = "hotkey.worker.persistence", name = "enabled", havingValue = "true")
108
  public TopKPersistService topKPersistService(
109
    @Qualifier("workerTopK") TopK workerTopK,
110
    StringRedisTemplate redisTemplate,
111
    WorkerProperties properties
112
  ) {
113
    return new TopKPersistService(
6✔
114
      workerTopK,
115
      redisTemplate,
116
      properties.getRouting().getAppName(),
5✔
117
      nodeId,
118
      properties.getPersistence()
2✔
119
    );
120
  }
121

122
  /**
123
   * Schedules periodic TopK snapshot persistence. The returned placeholder bean
124
   * keeps the task alive in the context.
125
   */
126
  @Bean
127
  @ConditionalOnBean(TopKPersistService.class)
128
  public Object topKPersistTask(
129
    TopKPersistService service,
130
    WorkerProperties properties,
131
    @Qualifier("hotKeyScheduler") ScheduledExecutorService scheduler
132
  ) {
133
    long interval = properties.getPersistence().getPersistIntervalMs();
4✔
134
    try {
135
      scheduler.scheduleAtFixedRate(service::persistToRedis, interval, interval, TimeUnit.MILLISECONDS);
11✔
136
    } catch (Exception e) {
×
137
      log.error("Failed to schedule TopK persistence task; Worker TopK snapshots will not be persisted.", e);
×
138
    }
1✔
139
    return new Object();
4✔
140
  }
141

142
  /**
143
   * Validates that the sliding-window duration is evenly divisible by the
144
   * number of slices to avoid rounding inaccuracies.
145
   */
146
  @PostConstruct
147
  void validateWindowConfig() {
148
    if (properties.getSlidingWindow().getDurationMs() % properties.getSlidingWindow().getSlices() != 0) {
13✔
149
      log.warn(
5✔
150
        "windowDurationMs ({}) is not evenly divisible by windowSlices ({}). " +
151
          "This introduces rounding inaccuracies in window calculations.",
152
        properties.getSlidingWindow().getDurationMs(),
5✔
153
        properties.getSlidingWindow().getSlices()
3✔
154
      );
155
    }
156
  }
1✔
157

158
  /**
159
   * Creates the sliding‑window detector.
160
   *
161
   * <p>If an absolute hot threshold is configured via
162
   * {@code hotkey.worker.hot-threshold} it is used directly; otherwise the
163
   * value is set to {@code Long.MAX_VALUE} and the ratio‑based threshold
164
   * will be calculated later by the dynamic‑threshold logic (not shown here).
165
   */
166
  @Bean
167
  public SlidingWindowDetector slidingWindowDetector(WorkerProperties properties) {
168
    long threshold = properties.getThreshold().getHotThreshold();
4✔
169
    if (threshold <= 0) {
4✔
170
      threshold = Long.MAX_VALUE;
2✔
171
    }
172
    return new SlidingWindowDetector(
4✔
173
      properties.getSlidingWindow().getDurationMs(),
3✔
174
      properties.getSlidingWindow().getSlices(),
4✔
175
      threshold
176
    );
177
  }
178

179
  /**
180
   * Creates the per‑key lifecycle state machine.
181
   *
182
   * @param properties worker configuration properties providing confirm, cool, and
183
   *                   pre-cool grace window counts (converted from durations via
184
   *                   {@link WorkerProperties#getConfirmWindows()},
185
   *                   {@link WorkerProperties#getCoolWindows()}, and
186
   *                   {@link WorkerProperties#getPreCoolGraceWindows()})
187
   * @return a new {@link HotKeyStateMachine} instance
188
   */
189
  @Bean
190
  public HotKeyStateMachine hotKeyStateMachine(WorkerProperties properties) {
191
    return new HotKeyStateMachine(
4✔
192
      properties.getConfirmWindows(),
2✔
193
      properties.getCoolWindows(),
2✔
194
      properties.getPreCoolGraceWindows()
2✔
195
    );
196
  }
197

198
  /**
199
   * Worker‑scoped Top‑K instance backed by a {@link HeavyKeeper} sketch.
200
   *
201
   * <p>This is intentionally a <b>separate bean</b> qualified with
202
   * {@code "workerTopK"} so that it can be distinguished from any other
203
   * Top‑K instance that might be defined in the application context
204
   * (e.g. for dashboard queries).
205
   *
206
   * @param properties worker configuration providing HeavyKeeper algorithm parameters
207
   *                   (top-K count, width, depth, decay, minimum count)
208
   * @return a new {@link HeavyKeeper} instance qualified as {@code workerTopK}
209
   */
210
  @Bean
211
  public TopK workerTopK(WorkerProperties properties) {
212
    return new HeavyKeeper(
4✔
213
      properties.getHeavyKeeper().getTopK(),
3✔
214
      properties.getHeavyKeeper().getWidth(),
3✔
215
      properties.getHeavyKeeper().getDepth(),
3✔
216
      properties.getHeavyKeeper().getDecay(),
3✔
217
      properties.getHeavyKeeper().getMinCount()
3✔
218
    );
219
  }
220

221
  /**
222
   * Top‑K validator that periodically inspects the worker's Top‑K list
223
   * and pre‑warms keys that appear consistently.
224
   *
225
   * @param workerTopK  the worker-scoped HeavyKeeper sketch for frequency estimation
226
   * @param broadcaster the worker broadcaster used to emit pre-warm HOT decisions
227
   * @param properties  worker configuration providing TopK validation parameters
228
   *                    (pre-warm count and minimum consecutive appearances)
229
   * @return a new {@link TopKValidator} instance
230
   */
231
  @Bean
232
  public TopKValidator topKValidator(
233
    @Qualifier("workerTopK") TopK workerTopK,
234
    WorkerBroadcaster broadcaster,
235
    WorkerProperties properties
236
  ) {
237
    return new TopKValidator(
6✔
238
      workerTopK,
239
      broadcaster,
240
      properties.getTopKValidation().getPreWarmCount(),
3✔
241
      properties.getTopKValidation().getPreWarmMinAppearances()
3✔
242
    );
243
  }
244

245
  /**
246
   * Report consumer – the main AMQP entry point.
247
   *
248
   * <p>Injects the worker‑scoped Top‑K so that every consumed report also
249
   * feeds the frequency estimator.
250
   *
251
   * @param detector           the sliding-window detector for per-key access tracking
252
   * @param stateMachine       the per-key lifecycle state machine for HOT/COOL transitions
253
   * @param broadcaster        publishes HOT and COOL decisions to all application instances
254
   * @param topKValidator      pre-warm validator for cross-instance frequency-based confirmation
255
   * @param workerTopK         the worker-scoped HeavyKeeper sketch for frequency estimation
256
   * @param globalQpsEstimator the global QPS estimator tracking overall throughput
257
   * @return a new {@link ReportConsumer} instance
258
   */
259
  @Bean
260
  public ReportConsumer reportConsumer(
261
    SlidingWindowDetector detector,
262
    HotKeyStateMachine stateMachine,
263
    WorkerBroadcaster broadcaster,
264
    TopKValidator topKValidator,
265
    @Qualifier("workerTopK") TopK workerTopK,
266
    GlobalQpsEstimator globalQpsEstimator
267
  ) {
268
    return new ReportConsumer(detector, stateMachine, broadcaster, topKValidator, workerTopK, globalQpsEstimator);
10✔
269
  }
270

271
  /**
272
   * Broadcasts HOT / COOL decisions to all application instances.
273
   *
274
   * @param rabbitTemplate         the RabbitMQ template used to publish messages
275
   * @param properties             worker configuration providing exchange and routing settings
276
   * @param nodeId                 the Worker's node identity, injected via {@code @Qualifier("workerNodeId")}
277
   * @param epochCounter           the Worker's epoch counter, injected via {@code @Qualifier("workerEpochCounter")}
278
   * @param configTimestampCounter the shared monotonic counter for config-change timestamps
279
   * @return a new {@link WorkerBroadcaster} instance
280
   */
281
  @Bean
282
  public WorkerBroadcaster workerBroadcaster(
283
    RabbitTemplate rabbitTemplate,
284
    WorkerProperties properties,
285
    @Qualifier("workerNodeId") String nodeId,
286
    @Qualifier("workerEpochCounter") AtomicLong epochCounter,
287
    @Qualifier("configTimestampCounter") AtomicLong configTimestampCounter
288
  ) {
289
    return new WorkerBroadcaster(
5✔
290
      rabbitTemplate,
291
      properties.getMessaging().getBroadcastExchange(),
3✔
292
      properties.getRouting().getAppName(),
5✔
293
      nodeId,
294
      epochCounter
295
    );
296
  }
297

298
  /**
299
   * Direct exchange to which clients publish report messages.
300
   * Routing keys follow the pattern {@code report.<appName>.<nodeId>}.
301
   *
302
   * @param properties worker configuration providing the report exchange name
303
   * @return a durable, non-auto-delete {@link DirectExchange}
304
   */
305
  @Bean
306
  public DirectExchange reportExchange(WorkerProperties properties) {
307
    return new DirectExchange(properties.getMessaging().getReportExchange(), true, false);
9✔
308
  }
309

310
  /**
311
   * Queue that this Worker binds to.
312
   * Queue name is {@code hotkey.report.<appName>.<nodeId>},
313
   * guaranteeing that a key always lands on the same Worker.
314
   *
315
   * <p>The queue has a 7-day idle expiry ({@code x-expires}) since shard queues
316
   * are fixed-count and long-lived.
317
   *
318
   * @param properties worker configuration providing the routing app name
319
   * @return a durable {@link Queue} with the shard-specific name
320
   */
321
  @Bean
322
  public Queue reportQueue(WorkerProperties properties) {
323
    String queueName = HotKeyConstants.QUEUE_PREFIX_REPORT + properties.getRouting().getAppName() + "." + nodeId;
7✔
324
    return QueueBuilder.durable(queueName).withArgument("x-expires", 604_800_000).build();
8✔
325
  }
326

327
  /**
328
   * Binds the report queue to the report exchange using the shard-specific
329
   * routing key {@code report.<appName>.<nodeId>}.
330
   *
331
   * @param reportQueue    the shard-specific report queue
332
   * @param reportExchange the report exchange
333
   * @param properties     worker configuration providing the routing app name
334
   * @return a {@link Binding} connecting the queue to the exchange
335
   */
336
  @Bean
337
  public Binding reportBinding(Queue reportQueue, DirectExchange reportExchange, WorkerProperties properties) {
338
    String routingKey = HotKeyConstants.ROUTING_KEY_REPORT + properties.getRouting().getAppName() + "." + nodeId;
7✔
339
    return BindingBuilder.bind(reportQueue).to(reportExchange).with(routingKey);
7✔
340
  }
341

342
  /**
343
   * Auto-delete queue for receiving heartbeat-based config updates from
344
   * peer Workers.
345
   *
346
   * <p>Each Worker declares its own queue named
347
   * {@code hotkey.worker.config.<nodeId>}, which is automatically removed
348
   * when the Worker disconnects.  The queue is bound to the
349
   * {@code heartbeatExchange} with routing key {@code heartbeat.*}.
350
   *
351
   * @return a non-durable, auto-delete {@link Queue} unique to this Worker node
352
   */
353
  @Bean
354
  public Queue workerConfigQueue() {
355
    return QueueBuilder.nonDurable("hotkey.worker.config." + nodeId).autoDelete().build();
7✔
356
  }
357

358
  /**
359
   * Binds the per-Worker config queue to the heartbeat exchange with the
360
   * routing key pattern {@code heartbeat.*}.
361
   *
362
   * @param workerConfigQueue the per-Worker config queue to bind
363
   * @param heartbeatExchange the heartbeat topic exchange
364
   * @return a {@link Binding} connecting the config queue to the heartbeat exchange
365
   */
366
  @Bean
367
  public Binding workerConfigBinding(Queue workerConfigQueue, TopicExchange heartbeatExchange) {
368
    return BindingBuilder.bind(workerConfigQueue)
4✔
369
      .to(heartbeatExchange)
2✔
370
      .with(HotKeyConstants.ROUTING_KEY_HEARTBEAT + "*");
1✔
371
  }
372

373
  /**
374
   * Topic exchange for epoch-aware structured heartbeats from Workers.
375
   *
376
   * @param properties worker configuration providing the heartbeat exchange name
377
   * @return a durable, non-auto-delete {@link TopicExchange}
378
   */
379
  @Bean
380
  public TopicExchange heartbeatExchange(WorkerProperties properties) {
381
    return new TopicExchange(properties.getMessaging().getHeartbeatExchange(), true, false);
9✔
382
  }
383

384
  /**
385
   * Exposes the Worker's node ID as a Spring bean for injection.
386
   *
387
   * @return the unique node identifier for this Worker instance
388
   */
389
  @Bean
390
  @Qualifier("workerNodeId")
391
  public String workerNodeId() {
392
    return nodeId;
3✔
393
  }
394

395
  /**
396
   * Monotonically increasing epoch counter for this Worker instance.
397
   *
398
   * <p>The epoch is incremented on every Worker restart and transmitted in
399
   * broadcast messages (see {@link WorkerBroadcaster#sendBroadcast}). Receiving
400
   * apps use the epoch to detect Worker restarts and unconditionally accept
401
   * decisions from a higher epoch (ADR-0010).
402
   *
403
   * @return a new {@link AtomicLong} initialised to {@code 0}
404
   */
405
  @Bean
406
  @Qualifier("workerEpochCounter")
407
  public AtomicLong workerEpochCounter() {
408
    return new AtomicLong(0);
5✔
409
  }
410

411
  /**
412
   * Monotonically increasing counter for config-change timestamps.
413
   *
414
   * <p>Every {@code POST /actuator/worker/state} call increments this counter.
415
   * Receiving Workers compare their own counter against the incoming value;
416
   * only strictly newer configs are applied.
417
   *
418
   * @return a new {@link AtomicLong} initialised to {@code 0}
419
   */
420
  @Bean
421
  @Qualifier("configTimestampCounter")
422
  public AtomicLong configTimestampCounter() {
423
    return new AtomicLong(0);
5✔
424
  }
425

426
  /**
427
   * Periodically evicts stale keys from the sliding‑window detector and
428
   * the state machine.
429
   *
430
   * <p>The eviction threshold is set to {@code 2 * coolDurationMs},
431
   * giving keys a generous grace period after their last access before
432
   * their data structures are reclaimed.
433
   *
434
   * @param detector     the sliding-window detector whose idle keys will be evicted
435
   * @param stateMachine the state machine whose idle entries will be evicted
436
   * @param properties   worker configuration providing the cool duration for stale threshold
437
   * @return a new {@link EvictStaleTask} instance
438
   */
439
  @Bean
440
  public EvictStaleTask evictStaleTask(
441
    SlidingWindowDetector detector,
442
    HotKeyStateMachine stateMachine,
443
    WorkerProperties properties
444
  ) {
445
    return new EvictStaleTask(detector, stateMachine, properties);
7✔
446
  }
447

448
  /**
449
   * Scheduled task that evicts stale keys from the sliding-window detector
450
   * and state machine.
451
   */
452
  @RequiredArgsConstructor
453
  public static class EvictStaleTask {
454

455
    private final SlidingWindowDetector detector;
456
    private final HotKeyStateMachine stateMachine;
457
    private final WorkerProperties properties;
458

459
    /**
460
     * Evicts keys that have not been accessed within {@code 2 * coolDurationMs}.
461
     */
462
    @Scheduled(fixedDelayString = "${hotkey.worker.state-machine.evict-interval-ms:30000}")
463
    public void evictStale() {
464
      try {
465
        long staleAfterMs = properties.getStateMachine().getCoolDurationMs() * 2;
7✔
466
        detector.evictStale(staleAfterMs);
4✔
467
        stateMachine.evictStale(staleAfterMs);
4✔
468
        log.debug("EvictStale tick: staleAfterMs={}", staleAfterMs);
5✔
469
      } catch (Exception e) {
1✔
470
        log.error("Scheduled evictStale failed", e);
4✔
471
      }
1✔
472
    }
1✔
473
  }
474

475
  /**
476
   * Periodically runs Top‑K cross‑validation to detect slow‑warming hot
477
   * keys that the sliding window might miss.
478
   *
479
   * <p>Interval is controlled by {@code hotkey.worker.topk-validation.validate-interval-ms}
480
   * (default 60 seconds).
481
   *
482
   * @param topKValidator the TopK validator whose {@link TopKValidator#validate()} will be scheduled
483
   * @return a new {@link TopKValidationTask} instance
484
   */
485
  @Bean
486
  public TopKValidationTask topKValidationTask(TopKValidator topKValidator) {
487
    return new TopKValidationTask(topKValidator);
5✔
488
  }
489

490
  /**
491
   * Scheduled task that inspects the Worker TopK and pre-warms stable hot keys.
492
   */
493
  @RequiredArgsConstructor
494
  public static class TopKValidationTask {
495

496
    private final TopKValidator topKValidator;
497

498
    /**
499
     * Runs TopK validation and pre-warm logic.
500
     */
501
    @Scheduled(fixedDelayString = "${hotkey.worker.topk-validation.validate-interval-ms:60000}")
502
    public void validate() {
503
      try {
504
        topKValidator.validate();
3✔
505
        log.debug("TopK validation tick completed");
3✔
506
      } catch (Exception e) {
1✔
507
        log.error("Scheduled TopK validation failed", e);
4✔
508
      }
1✔
509
    }
1✔
510
  }
511

512
  /**
513
   * Global QPS estimator that tracks overall throughput across all keys in the
514
   * current shard using a sliding window.
515
   *
516
   * @param properties worker configuration properties (sliding-window duration
517
   *                   and slice count are extracted from here)
518
   * @return a new {@link GlobalQpsEstimator} instance
519
   */
520
  @Bean
521
  public GlobalQpsEstimator globalQpsEstimator(WorkerProperties properties) {
522
    return new GlobalQpsEstimator(
4✔
523
      properties.getSlidingWindow().getDurationMs(),
3✔
524
      properties.getSlidingWindow().getSlices()
3✔
525
    );
526
  }
527

528
  /**
529
   * Threshold learner that periodically recalculates the hot-key threshold
530
   * based on estimated global QPS and updates the sliding-window detector.
531
   *
532
   * @param estimator the global QPS estimator
533
   * @param detector  the sliding-window detector whose threshold will be
534
   *                  dynamically adjusted
535
   * @param properties worker configuration properties for threshold tuning
536
   *                   parameters (ratio, tolerance, learning period)
537
   * @return a new {@link ThresholdLearner} instance
538
   */
539
  @Bean
540
  public ThresholdLearner thresholdLearner(
541
    GlobalQpsEstimator estimator,
542
    SlidingWindowDetector detector,
543
    WorkerProperties properties
544
  ) {
545
    return new ThresholdLearner(estimator, detector, properties);
7✔
546
  }
547

548
  /**
549
   * {@link SimpleRabbitListenerContainerFactory} using {@link SimpleMessageConverter}
550
   * for the Worker config listener, avoiding Jackson JSON conversion of heartbeat
551
   * messages (which use a custom header-based format).
552
   *
553
   * @param connectionFactory the RabbitMQ connection factory
554
   * @return a configured {@link SimpleRabbitListenerContainerFactory} instance
555
   */
556
  @Bean
557
  public SimpleRabbitListenerContainerFactory workerConfigListenerContainerFactory(
558
    ConnectionFactory connectionFactory
559
  ) {
560
    SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
4✔
561
    factory.setConnectionFactory(connectionFactory);
3✔
562
    factory.setMessageConverter(new SimpleMessageConverter());
5✔
563
    return factory;
2✔
564
  }
565

566
  /**
567
   * Fallback JSON message converter for report messages. Active only when the
568
   * common module's {@code reportMessageConverter} is absent (e.g. in tests).
569
   *
570
   * @return a {@link Jackson2JsonMessageConverter} instance
571
   */
572
  @Bean("reportMessageConverter")
573
  @ConditionalOnMissingBean(name = "reportMessageConverter")
574
  public MessageConverter reportMessageConverter() {
575
    return new org.springframework.amqp.support.converter.Jackson2JsonMessageConverter();
4✔
576
  }
577

578
  /**
579
   * Container factory for the {@link ReportConsumer}'s {@code @RabbitListener}.
580
   * Lifts throughput above Spring Boot's default (concurrency=1) by exposing
581
   * concurrent-consumers and prefetch via {@code hotkey.worker.report-consumer.*}.
582
   *
583
   * @param connectionFactory the RabbitMQ connection factory
584
   * @param reportMessageConverter the JSON message converter for report messages
585
   * @param properties worker configuration properties
586
   * @return a configured {@link SimpleRabbitListenerContainerFactory} instance
587
   */
588
  @Bean
589
  public SimpleRabbitListenerContainerFactory reportListenerContainerFactory(
590
      ConnectionFactory connectionFactory,
591
      @Qualifier("reportMessageConverter") MessageConverter reportMessageConverter,
592
      WorkerProperties properties
593
  ) {
594
    SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
4✔
595
    factory.setConnectionFactory(connectionFactory);
3✔
596
    factory.setMessageConverter(reportMessageConverter);
3✔
597
    factory.setConcurrentConsumers(properties.getReportConsumer().getConcurrentConsumers());
6✔
598
    factory.setMaxConcurrentConsumers(properties.getReportConsumer().getConcurrentConsumers());
6✔
599
    factory.setPrefetchCount(properties.getReportConsumer().getPrefetchCount());
6✔
600
    factory.setAcknowledgeMode(AcknowledgeMode.AUTO);
3✔
601
    factory.setDefaultRequeueRejected(false);
4✔
602
    return factory;
2✔
603
  }
604

605
  /**
606
   * Listens for heartbeat-based config updates from peer Workers and applies
607
   * them if the received config timestamp is newer than the local one.
608
   *
609
   * <p>On startup, waits up to 3 seconds for the first heartbeat to arrive.
610
   * If none is received, the Worker continues with the values from
611
   * {@link WorkerProperties}.
612
   *
613
   * @param stateMachine           the worker's state machine
614
   * @param configTimestampCounter the shared config-change timestamp counter
615
   * @return a new {@link WorkerConfigNegotiator} instance
616
   */
617
  @Bean
618
  public WorkerConfigNegotiator workerConfigNegotiator(
619
    HotKeyStateMachine stateMachine,
620
    @Qualifier("configTimestampCounter") AtomicLong configTimestampCounter
621
  ) {
622
    return new WorkerConfigNegotiator(stateMachine, configTimestampCounter, nodeId);
8✔
623
  }
624

625
  /**
626
   * Schedules periodic threshold recalculation using the {@link ThresholdLearner}.
627
   *
628
   * <p>The learner runs at the interval specified by
629
   * {@code hotkey.worker.global-qps-dynamic-threshold.recalculate-interval-ms}.
630
   * The returned placeholder bean keeps the scheduled task alive in the context.
631
   *
632
   * @param learner    the threshold learner to schedule
633
   * @param properties worker configuration for the recalculation interval
634
   * @param scheduler  the shared worker scheduler
635
   * @return a placeholder {@link Object} bean that keeps the scheduled task alive
636
   */
637
  @Bean
638
  public Object thresholdLearningTask(
639
    ThresholdLearner learner,
640
    WorkerProperties properties,
641
    @Qualifier("hotKeyScheduler") ScheduledExecutorService scheduler
642
  ) {
643
    try {
644
      scheduler.scheduleAtFixedRate(
5✔
645
        learner,
646
        properties.getGlobalQpsDynamicThreshold().getRecalculateIntervalMs(),
3✔
647
        properties.getGlobalQpsDynamicThreshold().getRecalculateIntervalMs(),
3✔
648
        TimeUnit.MILLISECONDS
649
      );
650
    } catch (Exception e) {
×
651
      log.error("Failed to schedule threshold learning task; dynamic threshold " +
×
652
          "adjustment will not run.", e);
653
    }
1✔
654
    return new Object(); // placeholder bean
4✔
655
  }
656

657
  /**
658
   * Auto-delete queue for on-demand verification requests (PING) from Apps.
659
   * Queue name: {@code hotkey.verify.ping.<nodeId>}.
660
   *
661
   * @return a non-durable, auto-delete {@link Queue} for PING/PONG verification
662
   */
663
  @Bean
664
  public Queue verifyPingQueue() {
665
    return QueueBuilder.nonDurable("hotkey.verify.ping." + nodeId).autoDelete().build();
7✔
666
  }
667

668
  /**
669
   * Handles PING requests from Apps, replies PONG via Direct reply-to.
670
   *
671
   * @param rabbitTemplate the RabbitMQ template used to send PONG responses
672
   * @return a new {@link VerifyConsumer} instance for this Worker node
673
   */
674
  @Bean
675
  public VerifyConsumer verifyConsumer(RabbitTemplate rabbitTemplate) {
676
    return new VerifyConsumer(rabbitTemplate, nodeId);
7✔
677
  }
678

679
  /**
680
   * Container for the verification ping queue (NONE ack, on-demand only).
681
   *
682
   * @param connectionFactory the RabbitMQ connection factory
683
   * @param verifyPingQueue   the auto-delete ping queue
684
   * @param verifyConsumer    the consumer whose {@link VerifyConsumer#handlePing} handles messages
685
   * @return a configured {@link SimpleMessageListenerContainer} with NONE acknowledgement mode
686
   */
687
  @Bean
688
  public SimpleMessageListenerContainer verifyPingContainer(
689
    ConnectionFactory connectionFactory,
690
    Queue verifyPingQueue,
691
    VerifyConsumer verifyConsumer
692
  ) {
693
    SimpleMessageListenerContainer c = new SimpleMessageListenerContainer(connectionFactory);
5✔
694
    c.setQueues(verifyPingQueue);
8✔
695
    c.setMessageListener(verifyConsumer::handlePing);
7✔
696
    c.setAcknowledgeMode(AcknowledgeMode.NONE);
3✔
697
    c.setErrorHandler(t -> log.warn("Verify ping listener uncaught exception (PONG will not be sent)", t));
3✔
698
    return c;
2✔
699
  }
700

701
  /**
702
   * Enhanced heartbeat producer that sends structured heartbeats with epoch,
703
   * load factor, decision-version watermark, and state-machine config gossip.
704
   *
705
   * <p>Replaces the old ping-only heartbeat approach. Uses its own internal scheduler.
706
   *
707
   * @param rabbitTemplate         the RabbitMQ template for publishing heartbeat messages
708
   * @param properties             worker configuration providing exchange and interval settings
709
   * @param stateMachine           the state machine providing config gossip fields
710
   * @param broadcaster            the broadcaster for reading the current decision version watermark
711
   * @param configTimestampCounter the shared monotonic counter for config-change timestamps
712
   * @return a new {@link WorkerHeartbeatProducer} instance
713
   */
714
  @Bean
715
  public WorkerHeartbeatProducer workerHeartbeatProducer(
716
    RabbitTemplate rabbitTemplate,
717
    WorkerProperties properties,
718
    HotKeyStateMachine stateMachine,
719
    WorkerBroadcaster broadcaster,
720
    RedisConnectionFactory redisConnectionFactory,
721
    @Qualifier("configTimestampCounter") AtomicLong configTimestampCounter,
722
    @Qualifier("hotKeyScheduler") ScheduledExecutorService scheduler
723
  ) {
724
    return new WorkerHeartbeatProducer(
5✔
725
      rabbitTemplate,
726
      properties.getMessaging().getHeartbeatExchange(),
9✔
727
      nodeId,
728
      stateMachine,
729
      broadcaster,
730
      configTimestampCounter,
731
      redisConnectionFactory,
732
      properties.getHeartbeat().getPingIntervalMs(),
5✔
733
      scheduler
734
    );
735
  }
736
}
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