• 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

80.0
/common/src/main/java/io/github/hyshmily/hotkey/autoconfigure/HotKeyAmqpAutoConfiguration.java
1
/*
2
 * Copyright 2026 Hyshmily. All Rights Reserved.
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *     http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16
package io.github.hyshmily.hotkey.autoconfigure;
17

18
import static io.github.hyshmily.hotkey.constants.HotKeyConstants.ROUTING_KEY_HEARTBEAT;
19

20
import io.github.hyshmily.hotkey.constants.HotKeyConstants;
21

22
import com.github.benmanes.caffeine.cache.Cache;
23
import io.github.hyshmily.hotkey.cache.CacheExpireManager;
24
import io.github.hyshmily.hotkey.reporting.BbrRateLimiter;
25
import io.github.hyshmily.hotkey.reporting.HotKeyReporter;
26
import io.github.hyshmily.hotkey.reporting.ReportPublisher;
27
import io.github.hyshmily.hotkey.rule.RuleMatcher;
28
import io.github.hyshmily.hotkey.sharding.RingManager;
29
import io.github.hyshmily.hotkey.sharding.ClusterHealthView;
30
import io.github.hyshmily.hotkey.sync.local.CacheSyncListener;
31
import io.github.hyshmily.hotkey.sync.local.CacheSyncProperties;
32
import io.github.hyshmily.hotkey.sync.local.CacheSyncPublisher;
33
import io.github.hyshmily.hotkey.sync.worker.WorkerHeartbeatMessage;
34
import io.github.hyshmily.hotkey.sync.worker.WorkerHeartbeatVerifier;
35
import io.github.hyshmily.hotkey.sync.worker.WorkerListener;
36
import io.github.hyshmily.hotkey.sync.worker.WorkerListenerProperties;
37
import io.github.hyshmily.hotkey.util.InstanceIdGenerator;
38
import io.github.hyshmily.hotkey.util.SystemLoadMonitor;
39
import io.github.hyshmily.hotkey.util.ratelimit.SreRateLimiter;
40
import java.util.concurrent.Executors;
41
import java.util.concurrent.ScheduledExecutorService;
42
import java.util.function.Function;
43
import org.springframework.amqp.core.*;
44
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
45
import org.springframework.amqp.rabbit.core.RabbitTemplate;
46
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
47
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
48
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
49
import org.springframework.amqp.support.converter.MessageConverter;
50
import org.springframework.beans.factory.ObjectProvider;
51
import org.springframework.beans.factory.annotation.Qualifier;
52
import org.springframework.boot.autoconfigure.AutoConfiguration;
53
import org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration;
54
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
55
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
56
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
57
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
58
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
59
import org.springframework.boot.context.properties.EnableConfigurationProperties;
60
import org.springframework.context.annotation.Bean;
61
import org.springframework.context.annotation.Configuration;
62
import org.springframework.data.redis.core.RedisTemplate;
63
import org.springframework.data.redis.core.StringRedisTemplate;
64

65
/**
66
 * Unified AMQP auto-configuration for HotKey messaging: app-to-Worker reporting,
67
 * instance-to-instance cache sync, and Worker decision listening.
68
 *
69
 * <p>Conditionally activates when {@link RabbitTemplate} is on the classpath.
70
 * Sub-groups for cache sync and Worker listener additionally require Redis.
71
 *
72
 * <p><b>Report</b> ({@code hotkey.report.enabled}, default {@code true}):
73
 * app instance aggregates access counts and sends them to the Worker via
74
 * {@link DirectExchange}. No Redis dependency.
75
 *
76
 * <p><b>Cache Sync</b> ({@code hotkey.sync.enabled=true}):
77
 * instance-to-instance INVALIDATE / REFRESH broadcasts via {@link FanoutExchange}.
78
 * Requires Redis for version tracking.
79
 *
80
 * <p><b>Worker Listener</b> ({@code hotkey.worker-listener.enabled=true}):
81
 * receives HOT/COOL decisions from the Worker via {@link FanoutExchange}.
82
 * Requires Redis.
83
 */
84
@AutoConfiguration(after = { RedisAutoConfiguration.class, RabbitAutoConfiguration.class })
85
@ConditionalOnClass(name = "org.springframework.amqp.rabbit.core.RabbitTemplate")
86
@EnableConfigurationProperties({ HotKeyProperties.class, CacheSyncProperties.class, WorkerListenerProperties.class })
87
public class HotKeyAmqpAutoConfiguration {
3✔
88

89
  /**
90
   * Inner configuration for app-to-Worker report routing via DirectExchange.
91
   * Creates the exchange, publisher, ring manager (optional), reporter, and report scheduler.
92
   * Active by default when a {@link RabbitTemplate} bean is present.
93
   */
94
  @Configuration
95
  @ConditionalOnBean(RabbitTemplate.class)
96
  @ConditionalOnProperty(prefix = "hotkey.report", name = "enabled", havingValue = "true", matchIfMissing = true)
97
  static class ReportConfiguration {
3✔
98

99
    /**
100
     * Declare the DirectExchange for report routing (app → Worker).
101
     * Routing keys ({@code report.<appName>.<nodeId>}) ensure each key's
102
     * messages land on the correct worker queue.
103
     *
104
     * @param properties the HotKey configuration properties
105
     * @return a durable, non-auto-delete {@link DirectExchange}
106
     */
107
    @Bean
108
    public DirectExchange hotkeyReportExchange(HotKeyProperties properties) {
109
      return new DirectExchange(properties.getReportExchange(), true, false);
8✔
110
    }
111

112
    /**
113
     * Create the {@link MessageConverter} for serializing report messages to JSON.
114
     * <p>
115
     * Uses Jackson JSON serialization (not Java serialization) for efficiency and cross-version
116
     * compatibility.
117
     *
118
     * @return a new {@link Jackson2JsonMessageConverter} instance
119
     */
120
    @Bean
121
    @ConditionalOnMissingBean(MessageConverter.class)
122
    public MessageConverter reportMessageConverter() {
123
      return new Jackson2JsonMessageConverter();
4✔
124
    }
125

126
    /**
127
     * Create the {@link ReportPublisher} for sending batched access-count reports to the Worker.
128
     *
129
     * @param rabbitTemplate the RabbitMQ template for publishing messages
130
     * @param properties     the HotKey configuration properties
131
     * @return a new {@link ReportPublisher} instance
132
     */
133
    @Bean
134
    @ConditionalOnMissingBean
135
    public ReportPublisher reportPublisher(RabbitTemplate rabbitTemplate, HotKeyProperties properties) {
136
      return new ReportPublisher(rabbitTemplate, properties.getReportExchange(), properties.getAppName());
9✔
137
    }
138

139
    /**
140
     * Create the {@link RingManager} for consistent-hashing report routing.
141
     *
142
     * @param properties the HotKey configuration properties
143
     * @return a new {@link RingManager} instance
144
     */
145
    @Bean
146
    @ConditionalOnMissingBean
147
    public RingManager ringManager(HotKeyProperties properties) {
148
      return new RingManager(properties.getConsistentHashing().getVirtualNodes());
7✔
149
    }
150

151
    /**
152
     * Create the system CPU monitor with EMA smoothing.
153
     * <p>
154
     * Uses the JDK platform MXBean ({@link com.sun.management.OperatingSystemMXBean})
155
     * which is already used by the Worker-side heartbeat producer. The monitor
156
     * starts sampling on creation and stops on context close.
157
     *
158
     * @param properties the HotKey configuration properties
159
     * @return a new {@link SystemLoadMonitor} instance
160
     */
161
    @Bean(initMethod = "start", destroyMethod = "stop")
162
    @ConditionalOnMissingBean
163
    public SystemLoadMonitor hotKeyCpuMonitor(
164
      HotKeyProperties properties,
165
      @Qualifier("hotKeyScheduler") ScheduledExecutorService hotKeyScheduler
166
    ) {
167
      HotKeyProperties.ReporterLimiter cfg = properties.getReporter();
3✔
168
      return new SystemLoadMonitor(hotKeyScheduler, cfg.getCpuPollIntervalMs(), cfg.getCpuDecay());
9✔
169
    }
170

171
    /**
172
     * Create the BBR adaptive rate limiter for the report publisher.
173
     * <p>
174
     * Uses the CPU monitor and the configured BBR parameters. When disabled
175
     * (or when the CPU monitor itself hasn't been fully initialized yet),
176
     * the limiter falls back to a permissive mode.
177
     *
178
     * @param cpuMonitor the system CPU load monitor
179
     * @param properties the HotKey configuration properties
180
     * @return a new {@link BbrRateLimiter} instance
181
     */
182
    @Bean
183
    @ConditionalOnMissingBean
184
    @ConditionalOnProperty(
185
      prefix = "hotkey.local.reporter",
186
      name = "enabled",
187
      havingValue = "true",
188
      matchIfMissing = true
189
    )
190
    public BbrRateLimiter hotKeyBbrRateLimiter(SystemLoadMonitor cpuMonitor, HotKeyProperties properties) {
191
      HotKeyProperties.ReporterLimiter cfg = properties.getReporter();
3✔
192
      return new BbrRateLimiter(
5✔
193
        cpuMonitor,
194
        cfg.getCpuThreshold(),
2✔
195
        cfg.getBbrWindowMs(),
2✔
196
        cfg.getBbrWindowBuckets(),
2✔
197
        cfg.getBbrCooldownMs()
2✔
198
      );
199
    }
200

201
    /**
202
     * Create the {@link HotKeyReporter} that aggregates per-key counts and flushes them
203
     * at the configured interval.
204
     *
205
     * @param reportPublisher       the report publisher for sending batches
206
     * @param properties            the HotKey configuration properties
207
     * @param ringManager           the consistent-hash ring manager
208
     * @param healthViewProvider    optional provider for the cluster health view
209
     * @param bbrRateLimiterProvider optional provider for the BBR rate limiter
210
     * @return a new {@link HotKeyReporter} instance
211
     */
212
    @Bean(initMethod = "start", destroyMethod = "stop")
213
    @ConditionalOnMissingBean
214
    public HotKeyReporter hotKeyReporter(
215
      ReportPublisher reportPublisher,
216
      @Qualifier("hotKeyScheduler") ScheduledExecutorService hotKeyScheduler,
217
      HotKeyProperties properties,
218
      RingManager ringManager,
219
      ObjectProvider<ClusterHealthView> healthViewProvider,
220
      ObjectProvider<BbrRateLimiter> bbrRateLimiterProvider
221
    ) {
222
      HotKeyReporter reporter = new HotKeyReporter(
5✔
223
        reportPublisher,
224
        hotKeyScheduler,
225
        properties.getReportIntervalMs(),
2✔
226
        properties.getAppName(),
2✔
227
        properties.getQueueCapacity(),
2✔
228
        properties.getQueueOfferTimeoutMs(),
2✔
229
        properties.effectiveConsumerCount(),
5✔
230
        ringManager,
231
        healthViewProvider.getIfAvailable(() ->
4✔
232
          new ClusterHealthView(
×
233
            properties.getExpectedWorkerCount(),
×
234
            properties.getHeartbeat().getTimeoutMs(),
×
235
            properties.getHeartbeat().getDegradeAfterFailures()
×
236
          )
237
        )
238
      );
239
      bbrRateLimiterProvider.ifAvailable(reporter::setBbrRateLimiter);
7✔
240
      return reporter;
2✔
241
    }
242
  }
243

244
  /**
245
   * Inner configuration for instance-to-instance cache synchronization.
246
   * Creates a FanoutExchange, per-instance queue with TTL, binding, publisher,
247
   * Redis loader, sync listener, and a dedicated scheduled executor.
248
   * Requires Redis and {@code hotkey.sync.enabled=true}.
249
   */
250
  @Configuration
251
  @ConditionalOnClass(name = "org.springframework.data.redis.core.RedisTemplate")
252
  @ConditionalOnProperty(prefix = "hotkey.sync", name = "enabled", havingValue = "true")
253
  @lombok.extern.slf4j.Slf4j
4✔
254
  static class SyncConfiguration {
3✔
255

256
    /**
257
     * Create the FanoutExchange for broadcasting INVALIDATE/REFRESH messages
258
     * to all app instances.
259
     *
260
     * @param properties the cache sync configuration properties
261
     * @return a durable, non-auto-delete {@link FanoutExchange}
262
     */
263
    @Bean
264
    @ConditionalOnClass(name = "org.springframework.amqp.core.FanoutExchange")
265
    public FanoutExchange hotkeySyncExchange(CacheSyncProperties properties) {
266
      return new FanoutExchange(properties.getExchangeName(), true, false);
8✔
267
    }
268

269
    /**
270
     * Create the per-instance sync queue with a 60-second message TTL and 24-hour idle expiry.
271
     *
272
     * @param properties the cache sync configuration properties
273
     * @return a durable {@link Queue} with {@code x-message-ttl} of 60 seconds
274
     *         and {@code x-expires} of 24 hours
275
     */
276
    @Bean
277
    @ConditionalOnClass(name = "org.springframework.amqp.core.Queue")
278
    public Queue hotkeySyncQueue(CacheSyncProperties properties) {
279
      return QueueBuilder.durable(properties.getQueueName())
6✔
280
        .withArgument("x-message-ttl", 60_000)
4✔
281
        .withArgument("x-expires", 86_400_000)
2✔
282
        .build();
1✔
283
    }
284

285
    /**
286
     * Bind the per-instance queue to the sync exchange.
287
     *
288
     * @param hotkeySyncQueue    the per-instance sync queue
289
     * @param hotkeySyncExchange the sync FanoutExchange
290
     * @return a {@link Binding} connecting the queue to the exchange
291
     */
292
    @Bean
293
    public Binding hotkeySyncBinding(Queue hotkeySyncQueue, FanoutExchange hotkeySyncExchange) {
294
      return BindingBuilder.bind(hotkeySyncQueue).to(hotkeySyncExchange);
5✔
295
    }
296

297
    /**
298
     * Create the cache sync publisher for sending INVALIDATE/REFRESH messages.
299
     *
300
     * @param rabbitTemplate the RabbitMQ template for publishing messages
301
     * @param properties     the cache sync configuration properties
302
     * @return a new {@link CacheSyncPublisher} instance
303
     */
304
    @Bean
305
    @ConditionalOnMissingBean
306
    public CacheSyncPublisher cacheSyncPublisher(RabbitTemplate rabbitTemplate, CacheSyncProperties properties) {
307
      return new CacheSyncPublisher(rabbitTemplate, properties);
6✔
308
    }
309

310
    /**
311
     * Dedicated scheduler for jitter-delayed cache-update tasks received from
312
     * peer instances. Isolated from {@code hotKeyScheduler} so that synchronous
313
     * Redis GETs performed by {@code handleRefresh} can never starve the
314
     * 50 ms reporter flush tick.
315
     *
316
     * <p>Pool size is {@code hotkey.sync.scheduler-pool-size} (default 4) but
317
     * should be at least {@code hotkey.sync.concurrent-consumers × 2}.
318
     *
319
     * @param properties the cache sync configuration properties
320
     * @return a daemon-thread scheduled executor named {@code hotkey-sync-sched-N}
321
     */
322
    @Bean(name = "hotKeySyncScheduler", destroyMethod = "shutdown")
323
    @ConditionalOnMissingBean(name = "hotKeySyncScheduler")
324
    public ScheduledExecutorService hotKeySyncScheduler(CacheSyncProperties properties) {
325
      int poolSize = Math.max(properties.getSchedulerPoolSize(), properties.getConcurrentConsumers() * 2);
×
326
      return Executors.newScheduledThreadPool(poolSize, r -> {
×
327
        Thread t = new Thread(r, HotKeyConstants.THREAD_PREFIX_SCHEDULER + "-sync");
×
328
        t.setDaemon(true);
×
329
        return t;
×
330
      });
331
    }
332

333
    /**
334
     * Default Redis loader used by the sync listener to refresh cache entries via {@code GET}.
335
     *
336
     * @param stringRedisTemplate the String-based Redis template for reading values
337
     * @return a {@link Function} that reads a key from Redis and returns its value
338
     */
339
    @Bean
340
    @ConditionalOnMissingBean(name = "hotKeyRedisLoader")
341
    public Function<String, Object> hotKeyRedisLoader(StringRedisTemplate stringRedisTemplate) {
342
      return key -> stringRedisTemplate.opsForValue().get(key);
3✔
343
    }
344

345
    /**
346
     * Create the sync listener that handles incoming INVALIDATE/REFRESH messages from peers.
347
     *
348
     * @param hotLocalCache       the L1 Caffeine cache
349
     * @param hotKeyRedisLoader   the function for loading values from Redis
350
     * @param properties          the cache sync configuration properties
351
     * @param expireManager       the cache expiry manager
352
     * @param ruleMatcher         the rule matcher for key matching
353
     * @return a new {@link CacheSyncListener} instance
354
     */
355
    @Bean
356
    @ConditionalOnMissingBean
357
    public CacheSyncListener cacheSyncListener(
358
      Cache<String, Object> hotLocalCache,
359
      Function<String, Object> hotKeyRedisLoader,
360
      CacheSyncProperties properties,
361
      @Qualifier("hotKeySyncScheduler") ScheduledExecutorService syncScheduler,
362
      CacheExpireManager expireManager,
363
      RuleMatcher ruleMatcher
364
    ) {
365
      return new CacheSyncListener(
10✔
366
        hotLocalCache,
367
        hotKeyRedisLoader,
368
        properties,
369
        syncScheduler,
370
        expireManager,
371
        ruleMatcher
372
      );
373
    }
374

375
    /**
376
     * Create the AMQP message listener container that drives the sync listener.
377
     *
378
     * @param connectionFactory  the RabbitMQ connection factory
379
     * @param cacheSyncListener  the sync message handler
380
     * @param properties         the cache sync configuration properties
381
     * @return a configured {@link SimpleMessageListenerContainer}
382
     */
383
    @Bean
384
    @ConditionalOnBean(ConnectionFactory.class)
385
    public SimpleMessageListenerContainer syncListenerContainer(
386
      ConnectionFactory connectionFactory,
387
      CacheSyncListener cacheSyncListener,
388
      CacheSyncProperties properties
389
    ) {
390
      SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory);
5✔
391
      container.setQueueNames(properties.getQueueName());
9✔
392
      container.setAutoStartup(properties.isAutoStartup());
4✔
393
      container.setAcknowledgeMode(AcknowledgeMode.MANUAL);
3✔
394
      container.setConcurrentConsumers(properties.getConcurrentConsumers());
4✔
395
      container.setPrefetchCount(properties.getPrefetchCount());
4✔
396
      container.setErrorHandler(t -> log.warn("Sync listener uncaught exception (message will be requeued by container)", t));
3✔
397
      container.setMessageListener(
4✔
398
        (ChannelAwareMessageListener) (msg, channel) -> cacheSyncListener.handleSyncMessage(channel, msg)
×
399
      );
400
      return container;
2✔
401
    }
402
  }
403

404
  /**
405
   * Inner configuration for receiving Worker HOT/COOL decisions.
406
   * Creates a FanoutExchange, per-instance queue with TTL, binding, worker listener,
407
   * listener container, and a dedicated scheduled executor.
408
   * Requires Redis and {@code hotkey.worker-listener.enabled=true}.
409
   */
410
  @Configuration
411
  @ConditionalOnClass(name = "org.springframework.data.redis.core.RedisTemplate")
412
  @ConditionalOnBean(RedisTemplate.class)
413
  @ConditionalOnProperty(prefix = "hotkey.worker-listener", name = "enabled", havingValue = "true")
414
  @lombok.extern.slf4j.Slf4j
4✔
415
  static class WorkerListenerConfiguration {
3✔
416

417
    /**
418
     * Create the FanoutExchange for broadcasting Worker HOT/COOL decisions
419
     * to all app instances.
420
     *
421
     * @param properties the Worker listener configuration properties
422
     * @return a durable, non-auto-delete {@link FanoutExchange}
423
     */
424
    @Bean
425
    @ConditionalOnMissingBean(name = "hotkeyWorkerExchange")
426
    public FanoutExchange hotkeyWorkerExchange(WorkerListenerProperties properties) {
427
      return new FanoutExchange(properties.getExchangeName(), true, false);
8✔
428
    }
429

430
    /**
431
     * Create the per-instance Worker listener queue with a 60-second message TTL and 24-hour idle expiry.
432
     *
433
     * @param properties the Worker listener configuration properties
434
     * @return a durable {@link Queue} with {@code x-message-ttl} of 60 seconds
435
     *         and {@code x-expires} of 24 hours
436
     */
437
    @Bean
438
    @ConditionalOnMissingBean(name = "hotkeyWorkerQueue")
439
    public Queue hotkeyWorkerQueue(WorkerListenerProperties properties) {
440
      return QueueBuilder.durable(properties.getQueueName())
6✔
441
        .withArgument("x-message-ttl", 60_000)
4✔
442
        .withArgument("x-expires", 86_400_000)
2✔
443
        .build();
1✔
444
    }
445

446
    /**
447
     * Bind the per-instance queue to the Worker exchange.
448
     *
449
     * @param hotkeyWorkerQueue    the per-instance Worker listener queue
450
     * @param hotkeyWorkerExchange the Worker FanoutExchange
451
     * @return a {@link Binding} connecting the queue to the exchange
452
     */
453
    @Bean
454
    public Binding hotkeyWorkerBinding(Queue hotkeyWorkerQueue, FanoutExchange hotkeyWorkerExchange) {
455
      return BindingBuilder.bind(hotkeyWorkerQueue).to(hotkeyWorkerExchange);
5✔
456
    }
457

458
    /**
459
     * Create the TopicExchange for Worker heartbeat broadcasts.
460
     *
461
     * @param properties the HotKey configuration properties
462
     * @return a durable, non-auto-delete {@link TopicExchange}
463
     */
464
    @Bean
465
    @ConditionalOnMissingBean(name = "hotkeyHeartbeatExchange")
466
    public TopicExchange hotkeyHeartbeatExchange(HotKeyProperties properties) {
467
      return new TopicExchange(properties.getHeartbeat().getExchangeName(), true, false);
9✔
468
    }
469

470
    /**
471
     * Create the per-instance non-durable heartbeat queue that auto-deletes on disconnect.
472
     *
473
     * @return a non-durable, auto-delete {@link Queue}
474
     */
475
    @Bean
476
    public Queue hotkeyHeartbeatQueue() {
477
      return QueueBuilder.nonDurable("hotkey.heartbeat:" + InstanceIdGenerator.get()).autoDelete().build();
6✔
478
    }
479

480
    /**
481
     * Bind the per-instance heartbeat queue to the heartbeat exchange with routing key {@code heartbeat.*}.
482
     *
483
     * @param hotkeyHeartbeatQueue    the per-instance heartbeat queue
484
     * @param hotkeyHeartbeatExchange the heartbeat TopicExchange
485
     * @return a {@link Binding} connecting the queue to the exchange
486
     */
487
    @Bean
488
    public Binding hotkeyHeartbeatBinding(Queue hotkeyHeartbeatQueue, TopicExchange hotkeyHeartbeatExchange) {
489
      return BindingBuilder.bind(hotkeyHeartbeatQueue).to(hotkeyHeartbeatExchange).with(ROUTING_KEY_HEARTBEAT + "*");
7✔
490
    }
491

492
    /**
493
     * Create the {@link ClusterHealthView} for tracking Worker cluster health.
494
     *
495
     * @param ringManager the consistent-hash ring manager
496
     * @param properties  the HotKey configuration properties
497
     * @return a new {@link ClusterHealthView} instance
498
     */
499
    @Bean
500
    @ConditionalOnMissingBean
501
    public ClusterHealthView clusterHealthView(RingManager ringManager, HotKeyProperties properties) {
502
      ClusterHealthView view = new ClusterHealthView(
3✔
503
        properties.getExpectedWorkerCount(),
2✔
504
        properties.getHeartbeat().getTimeoutMs(),
4✔
505
        properties.getHeartbeat().getDegradeAfterFailures()
4✔
506
      );
507
      view.setMinAliveWorkers(properties.getHeartbeat().getMinAliveWorkers());
5✔
508
      ringManager.setOnRingReconciled(aliveCount -> {
3✔
509
        // knownWorkerCount is managed by the user via
510
        // hotkey.local.expected-worker-count. When <=0 (dynamic mode),
511
        // isClusterHealthy() uses "any alive" logic so no adjustment needed.
512
      });
×
513
      return view;
2✔
514
    }
515

516
    /**
517
     * Create the SRE adaptive rate limiter for WorkerListener HOT-path throttling.
518
     * <p>
519
     * Disabled when {@code hotkey.worker-listener.sre.enabled=false}.
520
     *
521
     * @param properties the Worker listener configuration properties
522
     * @return a new {@link SreRateLimiter} instance
523
     */
524
    @Bean
525
    @ConditionalOnMissingBean
526
    @ConditionalOnProperty(
527
      prefix = "hotkey.worker-listener.sre",
528
      name = "enabled",
529
      havingValue = "true",
530
      matchIfMissing = true
531
    )
532
    public SreRateLimiter hotKeySreRateLimiter(WorkerListenerProperties properties) {
533
      WorkerListenerProperties.Sre sreConfig = properties.getSre();
3✔
534
      return new SreRateLimiter(
4✔
535
        sreConfig.getWindowMs(),
2✔
536
        sreConfig.getBuckets(),
3✔
537
        1.0 / sreConfig.getSuccessThreshold(),
3✔
538
        sreConfig.getMinSamples()
2✔
539
      );
540
    }
541

542
    /**
543
     * Dedicated scheduler for jitter-delayed cache-update tasks received from
544
     * the Worker (HOT/COOL decisions). Isolated from {@code hotKeyScheduler}
545
     * so that synchronous Redis GETs performed by {@code handleHot} can never
546
     * starve the reporter flush tick.
547
     *
548
     * @param properties the Worker listener configuration properties
549
     * @return a daemon-thread scheduled executor named {@code hotkey-worker-sched-N}
550
     */
551
    @Bean(name = "hotKeyWorkerSchedScheduler", destroyMethod = "shutdown")
552
    @ConditionalOnMissingBean(name = "hotKeyWorkerSchedScheduler")
553
    public ScheduledExecutorService hotKeyWorkerSchedScheduler(WorkerListenerProperties properties) {
554
      int poolSize = Math.max(properties.getSchedulerPoolSize(), properties.getConcurrentConsumers() * 2);
×
555
      return Executors.newScheduledThreadPool(poolSize, r -> {
×
556
        Thread t = new Thread(r, HotKeyConstants.THREAD_PREFIX_SCHEDULER + "-worker");
×
557
        t.setDaemon(true);
×
558
        return t;
×
559
      });
560
    }
561

562
    /**
563
     * Create the listener that processes HOT/COOL decisions broadcast by the Worker.
564
     *
565
     * @param hotLocalCache          the L1 Caffeine cache
566
     * @param hotKeyRedisLoader      the function for loading values from Redis
567
     * @param properties             the Worker listener configuration properties
568
     * @param expireManager          the cache expiry manager
569
     * @param sreRateLimiterProvider optional provider for the SRE rate limiter
570
     * @return a new {@link WorkerListener} instance
571
     */
572
    @Bean
573
    @ConditionalOnMissingBean
574
    public WorkerListener workerListener(
575
      Cache<String, Object> hotLocalCache,
576
      Function<String, Object> hotKeyRedisLoader,
577
      WorkerListenerProperties properties,
578
      @Qualifier("hotKeyWorkerSchedScheduler") ScheduledExecutorService workerSchedScheduler,
579
      CacheExpireManager expireManager,
580
      ObjectProvider<SreRateLimiter> sreRateLimiterProvider
581
    ) {
582
      return new WorkerListener(
9✔
583
        hotLocalCache,
584
        hotKeyRedisLoader,
585
        properties,
586
        workerSchedScheduler,
587
        expireManager,
588
        sreRateLimiterProvider.getIfAvailable()
3✔
589
      );
590
    }
591

592
    /**
593
     * Create the AMQP message listener container that processes Worker heartbeat messages.
594
     *
595
     * @param connectionFactory   the RabbitMQ connection factory
596
     * @param healthView          the cluster health view to update on heartbeat reception
597
     * @param hotkeyHeartbeatQueue the heartbeat queue
598
     * @return a configured {@link SimpleMessageListenerContainer}
599
     */
600
    @Bean
601
    @ConditionalOnMissingBean(name = "hotkeyHeartbeatContainer")
602
    public SimpleMessageListenerContainer heartbeatContainer(
603
      ConnectionFactory connectionFactory,
604
      ClusterHealthView healthView,
605
      Queue hotkeyHeartbeatQueue
606
    ) {
607
      SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory);
5✔
608
      container.setQueueNames(hotkeyHeartbeatQueue.getName());
9✔
609
      container.setAcknowledgeMode(AcknowledgeMode.NONE);
3✔
610
      container.setConcurrentConsumers(1);
3✔
611
      container.setPrefetchCount(100);
3✔
612
      container.setErrorHandler(t -> log.warn("Heartbeat listener uncaught exception (message discarded)", t));
3✔
613
      container.setMessageListener(msg -> {
4✔
614
        WorkerHeartbeatMessage hb = WorkerHeartbeatMessage.from(msg);
×
615
        if (hb != null) {
×
616
          healthView.onHeartbeat(hb);
×
617
        }
618
      });
×
619
      return container;
2✔
620
    }
621

622
    /**
623
     * Create the AMQP message listener container that processes Worker HOT/COOL decisions
624
     * via the {@link WorkerListener}.
625
     *
626
     * <p>The container uses {@link AcknowledgeMode#MANUAL} because the
627
     * {@link WorkerListener#handleWorkerMessage} performs its own ack/nack
628
     * (ack-before-update pattern, see ADR-0004).
629
     *
630
     * @param connectionFactory the RabbitMQ connection factory
631
     * @param hotkeyWorkerQueue the per-instance Worker listener queue
632
     * @param workerListener    the Worker decision listener
633
     * @param properties        the Worker listener configuration properties
634
     * @return a configured {@link SimpleMessageListenerContainer}
635
     */
636
    @Bean
637
    @ConditionalOnMissingBean(name = "workerListenerContainer")
638
    public SimpleMessageListenerContainer workerListenerContainer(
639
      ConnectionFactory connectionFactory,
640
      Queue hotkeyWorkerQueue,
641
      WorkerListener workerListener,
642
      WorkerListenerProperties properties
643
    ) {
644
      SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory);
5✔
645
      container.setQueueNames(hotkeyWorkerQueue.getName());
9✔
646
      container.setAcknowledgeMode(AcknowledgeMode.MANUAL);
3✔
647
      container.setMessageListener(
4✔
648
        (ChannelAwareMessageListener) (msg, channel) -> workerListener.handleWorkerMessage(channel, msg)
×
649
      );
650
      container.setConcurrentConsumers(properties.getConcurrentConsumers());
4✔
651
      container.setPrefetchCount(properties.getPrefetchCount());
4✔
652
      container.setErrorHandler(t -> log.warn("Worker listener uncaught exception (message will be requeued by container)", t));
3✔
653
      container.setAutoStartup(properties.isAutoStartup());
4✔
654
      return container;
2✔
655
    }
656

657
    /**
658
     * Create the {@link WorkerHeartbeatVerifier} that periodically PINGs Workers
659
     * to verify they are alive.
660
     *
661
     * @param rabbitTemplate the RabbitMQ template for sending PING messages
662
     * @param healthView     the cluster health view to update on verification results
663
     * @param properties     the HotKey configuration properties
664
     * @return a new {@link WorkerHeartbeatVerifier} instance
665
     */
666
    @Bean(initMethod = "start", destroyMethod = "stop")
667
    @ConditionalOnMissingBean
668
    public WorkerHeartbeatVerifier workerHeartbeatVerifier(
669
      RabbitTemplate rabbitTemplate,
670
      ClusterHealthView healthView,
671
      HotKeyProperties properties,
672
      @Qualifier("hotKeyScheduler") ScheduledExecutorService hotKeyScheduler
673
    ) {
674
      return new WorkerHeartbeatVerifier(
6✔
675
        rabbitTemplate,
676
        healthView,
677
        properties.getInstanceId(),
4✔
678
        new WorkerHeartbeatVerifier.VerifierConfig(
679
          properties.getHeartbeat().getVerifyIntervalMs(),
3✔
680
          properties.getHeartbeat().getPingTimeoutMs(),
3✔
681
          properties.getHeartbeat().getDegradeAfterFailures(),
3✔
682
          properties.getHeartbeat().getVerifyMaxBackoffMs()
5✔
683
        ),
684
        hotKeyScheduler
685
      );
686
    }
687
  }
688
}
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