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

Hyshmily / hotkey / 27925388821

22 Jun 2026 02:14AM UTC coverage: 92.403% (-1.2%) from 93.615%
27925388821

push

github

Hyshmily
fix:fix known bugs and promote performance

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

1143 of 1289 branches covered (88.67%)

Branch coverage included in aggregate %.

184 of 236 new or added lines in 19 files covered. (77.97%)

6 existing lines in 3 files now uncovered.

3321 of 3542 relevant lines covered (93.76%)

4.22 hits per line

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

95.74
/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.heavykepper.HeavyKeeper;
21
import io.github.hyshmily.hotkey.hotkeydetector.heavykepper.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.connection.ConnectionFactory;
40
import org.springframework.amqp.rabbit.core.RabbitTemplate;
41
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
42
import org.springframework.beans.factory.annotation.Qualifier;
43
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
44
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
45
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
46
import org.springframework.boot.context.properties.EnableConfigurationProperties;
47
import org.springframework.context.annotation.Bean;
48
import org.springframework.context.annotation.Configuration;
49
import org.springframework.data.redis.connection.RedisConnectionFactory;
50
import org.springframework.data.redis.core.StringRedisTemplate;
51
import org.springframework.scheduling.annotation.EnableScheduling;
52
import org.springframework.scheduling.annotation.Scheduled;
53

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

86
  private final WorkerProperties properties;
87

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

451
    private final SlidingWindowDetector detector;
452
    private final HotKeyStateMachine stateMachine;
453
    private final WorkerProperties properties;
454

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

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

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

492
    private final TopKValidator topKValidator;
493

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

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

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

544
  /**
545
   * Listens for heartbeat-based config updates from peer Workers and applies
546
   * them if the received config timestamp is newer than the local one.
547
   *
548
   * <p>On startup, waits up to 3 seconds for the first heartbeat to arrive.
549
   * If none is received, the Worker continues with the values from
550
   * {@link WorkerProperties}.
551
   *
552
   * @param stateMachine           the worker's state machine
553
   * @param configTimestampCounter the shared config-change timestamp counter
554
   * @return a new {@link WorkerConfigNegotiator} instance
555
   */
556
  @Bean
557
  public WorkerConfigNegotiator workerConfigNegotiator(
558
    HotKeyStateMachine stateMachine,
559
    @Qualifier("configTimestampCounter") AtomicLong configTimestampCounter
560
  ) {
561
    return new WorkerConfigNegotiator(stateMachine, configTimestampCounter, nodeId);
8✔
562
  }
563

564
  /**
565
   * Schedules periodic threshold recalculation using the {@link ThresholdLearner}.
566
   *
567
   * <p>The learner runs at the interval specified by
568
   * {@code hotkey.worker.global-qps-dynamic-threshold.recalculate-interval-ms}.
569
   * The returned placeholder bean keeps the scheduled task alive in the context.
570
   *
571
   * @param learner    the threshold learner to schedule
572
   * @param properties worker configuration for the recalculation interval
573
   * @param scheduler  the shared worker scheduler
574
   * @return a placeholder {@link Object} bean that keeps the scheduled task alive
575
   */
576
  @Bean
577
  public Object thresholdLearningTask(
578
    ThresholdLearner learner,
579
    WorkerProperties properties,
580
    @Qualifier("hotKeyScheduler") ScheduledExecutorService scheduler
581
  ) {
582
    try {
583
      scheduler.scheduleAtFixedRate(
5✔
584
        learner,
585
        properties.getGlobalQpsDynamicThreshold().getRecalculateIntervalMs(),
3✔
586
        properties.getGlobalQpsDynamicThreshold().getRecalculateIntervalMs(),
3✔
587
        TimeUnit.MILLISECONDS
588
      );
NEW
589
    } catch (Exception e) {
×
NEW
590
      log.error("Failed to schedule threshold learning task; dynamic threshold " +
×
591
          "adjustment will not run.", e);
592
    }
1✔
593
    return new Object(); // placeholder bean
4✔
594
  }
595

596
  /**
597
   * Auto-delete queue for on-demand verification requests (PING) from Apps.
598
   * Queue name: {@code hotkey.verify.ping.<nodeId>}.
599
   *
600
   * @return a non-durable, auto-delete {@link Queue} for PING/PONG verification
601
   */
602
  @Bean
603
  public Queue verifyPingQueue() {
604
    return QueueBuilder.nonDurable("hotkey.verify.ping." + nodeId).autoDelete().build();
7✔
605
  }
606

607
  /**
608
   * Handles PING requests from Apps, replies PONG via Direct reply-to.
609
   *
610
   * @param rabbitTemplate the RabbitMQ template used to send PONG responses
611
   * @return a new {@link VerifyConsumer} instance for this Worker node
612
   */
613
  @Bean
614
  public VerifyConsumer verifyConsumer(RabbitTemplate rabbitTemplate) {
615
    return new VerifyConsumer(rabbitTemplate, nodeId);
7✔
616
  }
617

618
  /**
619
   * Container for the verification ping queue (NONE ack, on-demand only).
620
   *
621
   * @param connectionFactory the RabbitMQ connection factory
622
   * @param verifyPingQueue   the auto-delete ping queue
623
   * @param verifyConsumer    the consumer whose {@link VerifyConsumer#handlePing} handles messages
624
   * @return a configured {@link SimpleMessageListenerContainer} with NONE acknowledgement mode
625
   */
626
  @Bean
627
  public SimpleMessageListenerContainer verifyPingContainer(
628
    ConnectionFactory connectionFactory,
629
    Queue verifyPingQueue,
630
    VerifyConsumer verifyConsumer
631
  ) {
632
    SimpleMessageListenerContainer c = new SimpleMessageListenerContainer(connectionFactory);
5✔
633
    c.setQueues(verifyPingQueue);
8✔
634
    c.setMessageListener(verifyConsumer::handlePing);
7✔
635
    c.setAcknowledgeMode(AcknowledgeMode.NONE);
3✔
636
    c.setErrorHandler(t -> log.warn("Verify ping listener uncaught exception (PONG will not be sent)", t));
3✔
637
    return c;
2✔
638
  }
639

640
  /**
641
   * Enhanced heartbeat producer that sends structured heartbeats with epoch,
642
   * load factor, decision-version watermark, and state-machine config gossip.
643
   *
644
   * <p>Replaces the old ping-only heartbeat approach. Uses its own internal scheduler.
645
   *
646
   * @param rabbitTemplate         the RabbitMQ template for publishing heartbeat messages
647
   * @param properties             worker configuration providing exchange and interval settings
648
   * @param stateMachine           the state machine providing config gossip fields
649
   * @param broadcaster            the broadcaster for reading the current decision version watermark
650
   * @param configTimestampCounter the shared monotonic counter for config-change timestamps
651
   * @return a new {@link WorkerHeartbeatProducer} instance
652
   */
653
  @Bean
654
  public WorkerHeartbeatProducer workerHeartbeatProducer(
655
    RabbitTemplate rabbitTemplate,
656
    WorkerProperties properties,
657
    HotKeyStateMachine stateMachine,
658
    WorkerBroadcaster broadcaster,
659
    RedisConnectionFactory redisConnectionFactory,
660
    @Qualifier("configTimestampCounter") AtomicLong configTimestampCounter,
661
    @Qualifier("hotKeyScheduler") ScheduledExecutorService scheduler
662
  ) {
663
    return new WorkerHeartbeatProducer(
5✔
664
      rabbitTemplate,
665
      properties.getMessaging().getHeartbeatExchange(),
9✔
666
      nodeId,
667
      stateMachine,
668
      broadcaster,
669
      configTimestampCounter,
670
      redisConnectionFactory,
671
      properties.getHeartbeat().getPingIntervalMs(),
5✔
672
      scheduler
673
    );
674
  }
675
}
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