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

Hyshmily / hotkey / 28159619024

25 Jun 2026 09:10AM UTC coverage: 92.667% (-0.02%) from 92.685%
28159619024

push

github

Hyshmily
release: v1.1.52

1199 of 1351 branches covered (88.75%)

Branch coverage included in aggregate %.

3426 of 3640 relevant lines covered (94.12%)

4.25 hits per line

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

95.92
/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.SimpleMessageConverter;
44
import org.springframework.beans.factory.annotation.Qualifier;
45
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
46
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
47
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
48
import org.springframework.boot.context.properties.EnableConfigurationProperties;
49
import org.springframework.context.annotation.Bean;
50
import org.springframework.context.annotation.Configuration;
51
import org.springframework.data.redis.connection.RedisConnectionFactory;
52
import org.springframework.data.redis.core.StringRedisTemplate;
53
import org.springframework.scheduling.annotation.EnableScheduling;
54
import org.springframework.scheduling.annotation.Scheduled;
55

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

88
  private final WorkerProperties properties;
89

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

453
    private final SlidingWindowDetector detector;
454
    private final HotKeyStateMachine stateMachine;
455
    private final WorkerProperties properties;
456

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

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

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

494
    private final TopKValidator topKValidator;
495

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

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

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

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

564
  /**
565
   * Listens for heartbeat-based config updates from peer Workers and applies
566
   * them if the received config timestamp is newer than the local one.
567
   *
568
   * <p>On startup, waits up to 3 seconds for the first heartbeat to arrive.
569
   * If none is received, the Worker continues with the values from
570
   * {@link WorkerProperties}.
571
   *
572
   * @param stateMachine           the worker's state machine
573
   * @param configTimestampCounter the shared config-change timestamp counter
574
   * @return a new {@link WorkerConfigNegotiator} instance
575
   */
576
  @Bean
577
  public WorkerConfigNegotiator workerConfigNegotiator(
578
    HotKeyStateMachine stateMachine,
579
    @Qualifier("configTimestampCounter") AtomicLong configTimestampCounter
580
  ) {
581
    return new WorkerConfigNegotiator(stateMachine, configTimestampCounter, nodeId);
8✔
582
  }
583

584
  /**
585
   * Schedules periodic threshold recalculation using the {@link ThresholdLearner}.
586
   *
587
   * <p>The learner runs at the interval specified by
588
   * {@code hotkey.worker.global-qps-dynamic-threshold.recalculate-interval-ms}.
589
   * The returned placeholder bean keeps the scheduled task alive in the context.
590
   *
591
   * @param learner    the threshold learner to schedule
592
   * @param properties worker configuration for the recalculation interval
593
   * @param scheduler  the shared worker scheduler
594
   * @return a placeholder {@link Object} bean that keeps the scheduled task alive
595
   */
596
  @Bean
597
  public Object thresholdLearningTask(
598
    ThresholdLearner learner,
599
    WorkerProperties properties,
600
    @Qualifier("hotKeyScheduler") ScheduledExecutorService scheduler
601
  ) {
602
    try {
603
      scheduler.scheduleAtFixedRate(
5✔
604
        learner,
605
        properties.getGlobalQpsDynamicThreshold().getRecalculateIntervalMs(),
3✔
606
        properties.getGlobalQpsDynamicThreshold().getRecalculateIntervalMs(),
3✔
607
        TimeUnit.MILLISECONDS
608
      );
609
    } catch (Exception e) {
×
610
      log.error("Failed to schedule threshold learning task; dynamic threshold " +
×
611
          "adjustment will not run.", e);
612
    }
1✔
613
    return new Object(); // placeholder bean
4✔
614
  }
615

616
  /**
617
   * Auto-delete queue for on-demand verification requests (PING) from Apps.
618
   * Queue name: {@code hotkey.verify.ping.<nodeId>}.
619
   *
620
   * @return a non-durable, auto-delete {@link Queue} for PING/PONG verification
621
   */
622
  @Bean
623
  public Queue verifyPingQueue() {
624
    return QueueBuilder.nonDurable("hotkey.verify.ping." + nodeId).autoDelete().build();
7✔
625
  }
626

627
  /**
628
   * Handles PING requests from Apps, replies PONG via Direct reply-to.
629
   *
630
   * @param rabbitTemplate the RabbitMQ template used to send PONG responses
631
   * @return a new {@link VerifyConsumer} instance for this Worker node
632
   */
633
  @Bean
634
  public VerifyConsumer verifyConsumer(RabbitTemplate rabbitTemplate) {
635
    return new VerifyConsumer(rabbitTemplate, nodeId);
7✔
636
  }
637

638
  /**
639
   * Container for the verification ping queue (NONE ack, on-demand only).
640
   *
641
   * @param connectionFactory the RabbitMQ connection factory
642
   * @param verifyPingQueue   the auto-delete ping queue
643
   * @param verifyConsumer    the consumer whose {@link VerifyConsumer#handlePing} handles messages
644
   * @return a configured {@link SimpleMessageListenerContainer} with NONE acknowledgement mode
645
   */
646
  @Bean
647
  public SimpleMessageListenerContainer verifyPingContainer(
648
    ConnectionFactory connectionFactory,
649
    Queue verifyPingQueue,
650
    VerifyConsumer verifyConsumer
651
  ) {
652
    SimpleMessageListenerContainer c = new SimpleMessageListenerContainer(connectionFactory);
5✔
653
    c.setQueues(verifyPingQueue);
8✔
654
    c.setMessageListener(verifyConsumer::handlePing);
7✔
655
    c.setAcknowledgeMode(AcknowledgeMode.NONE);
3✔
656
    c.setErrorHandler(t -> log.warn("Verify ping listener uncaught exception (PONG will not be sent)", t));
3✔
657
    return c;
2✔
658
  }
659

660
  /**
661
   * Enhanced heartbeat producer that sends structured heartbeats with epoch,
662
   * load factor, decision-version watermark, and state-machine config gossip.
663
   *
664
   * <p>Replaces the old ping-only heartbeat approach. Uses its own internal scheduler.
665
   *
666
   * @param rabbitTemplate         the RabbitMQ template for publishing heartbeat messages
667
   * @param properties             worker configuration providing exchange and interval settings
668
   * @param stateMachine           the state machine providing config gossip fields
669
   * @param broadcaster            the broadcaster for reading the current decision version watermark
670
   * @param configTimestampCounter the shared monotonic counter for config-change timestamps
671
   * @return a new {@link WorkerHeartbeatProducer} instance
672
   */
673
  @Bean
674
  public WorkerHeartbeatProducer workerHeartbeatProducer(
675
    RabbitTemplate rabbitTemplate,
676
    WorkerProperties properties,
677
    HotKeyStateMachine stateMachine,
678
    WorkerBroadcaster broadcaster,
679
    RedisConnectionFactory redisConnectionFactory,
680
    @Qualifier("configTimestampCounter") AtomicLong configTimestampCounter,
681
    @Qualifier("hotKeyScheduler") ScheduledExecutorService scheduler
682
  ) {
683
    return new WorkerHeartbeatProducer(
5✔
684
      rabbitTemplate,
685
      properties.getMessaging().getHeartbeatExchange(),
9✔
686
      nodeId,
687
      stateMachine,
688
      broadcaster,
689
      configTimestampCounter,
690
      redisConnectionFactory,
691
      properties.getHeartbeat().getPingIntervalMs(),
5✔
692
      scheduler
693
    );
694
  }
695
}
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