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

Hyshmily / hotkey / 28735434673

05 Jul 2026 08:55AM UTC coverage: 90.421% (+0.02%) from 90.4%
28735434673

push

github

Hyshmily
perf : simplify the code

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

1295 of 1498 branches covered (86.45%)

Branch coverage included in aggregate %.

121 of 134 new or added lines in 11 files covered. (90.3%)

2 existing lines in 1 file now uncovered.

3689 of 4014 relevant lines covered (91.9%)

4.19 hits per line

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

59.7
/common/src/main/java/io/github/hyshmily/hotkey/autoconfigure/HotKeyAutoConfiguration.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.util.TimeSource.currentTimeMillis;
19

20
import com.github.benmanes.caffeine.cache.Cache;
21
import com.github.benmanes.caffeine.cache.Caffeine;
22
import com.github.benmanes.caffeine.cache.Expiry;
23
import io.github.hyshmily.hotkey.Internal;
24
import io.github.hyshmily.hotkey.cache.HotKeyCache;
25
import io.github.hyshmily.hotkey.cache.cachesupport.CacheExpireManager;
26
import io.github.hyshmily.hotkey.cache.cachesupport.HotKeyCircuitBreaker;
27
import io.github.hyshmily.hotkey.cache.cachesupport.SingleFlight;
28
import io.github.hyshmily.hotkey.constants.HotKeyConstants;
29
import io.github.hyshmily.hotkey.hotkeydetector.HotKeyDetector;
30
import io.github.hyshmily.hotkey.hotkeydetector.heavykeeper.HeavyKeeper;
31
import io.github.hyshmily.hotkey.hotkeydetector.heavykeeper.TopK;
32
import io.github.hyshmily.hotkey.model.CacheEntry;
33
import io.github.hyshmily.hotkey.reporting.HotKeyReporter;
34
import io.github.hyshmily.hotkey.rule.RuleMatcher;
35
import io.github.hyshmily.hotkey.sharding.ClusterHealthView;
36
import io.github.hyshmily.hotkey.sync.local.CacheSyncPublisher;
37
import io.github.hyshmily.hotkey.util.version.VersionController;
38
import java.util.Optional;
39
import java.util.concurrent.Executor;
40
import java.util.concurrent.RejectedExecutionException;
41
import java.util.concurrent.ScheduledExecutorService;
42
import java.util.concurrent.TimeUnit;
43
import lombok.extern.slf4j.Slf4j;
44
import org.jspecify.annotations.NonNull;
45
import org.springframework.beans.factory.ObjectProvider;
46
import org.springframework.beans.factory.annotation.Qualifier;
47
import org.springframework.boot.autoconfigure.AutoConfiguration;
48
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
49
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
50
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
51
import org.springframework.boot.context.properties.EnableConfigurationProperties;
52
import org.springframework.context.annotation.Bean;
53
import org.springframework.data.redis.core.StringRedisTemplate;
54
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
55

56
/**
57
 * App-side autoconfiguration for the HotKey library.
58
 *
59
 * <p>Creates the app-side {@link TopK} detector (HeavyKeeper), L1 Caffeine
60
 * cache, {@link SingleFlight} deduplication layer, executor, and the
61
 * primary {@link HotKeyCache} (without Redis version tracking when Redis
62
 * is absent).
63
 *
64
 * <p><b>Condition:</b> this configuration is <em>skipped</em> when the
65
 * Worker is active ({@code hotkey.worker.enabled=true}). It runs when
66
 * Worker is disabled or the property is absent.
67
 */
68
@Internal
69
@AutoConfiguration(after = RedisAutoConfiguration.class)
70
@ConditionalOnProperty(prefix = "hotkey.worker", name = "enabled", havingValue = "false", matchIfMissing = true)
71
@EnableConfigurationProperties(HotKeyProperties.class)
72
@Slf4j
4✔
73
public class HotKeyAutoConfiguration {
3✔
74

75
  /**
76
   * Create the app-side TopK instance (HeavyKeeper) as a standalone bean.
77
   *
78
   * <p>The HeavyKeeper uses a Count-Min Sketch augmented with a minimum-count
79
   * threshold and exponential decay to track the top-K hottest keys with high
80
   * accuracy and low memory footprint. Configuration parameters (width, depth,
81
   * decay, minCount, topK capacity, expelled queue capacity) are read from
82
   * {@link HotKeyProperties}.
83
   *
84
   * @param properties the HotKey configuration properties (never {@code null})
85
   * @return a new HeavyKeeper TopK instance
86
   */
87
  @Bean
88
  @ConditionalOnMissingBean
89
  public HeavyKeeper heavyKeeper(HotKeyProperties properties) {
90
    return new HeavyKeeper(
4✔
91
      properties.getTopK(),
2✔
92
      properties.getWidth(),
2✔
93
      properties.getDepth(),
2✔
94
      properties.getDecay(),
2✔
95
      properties.getMinCount(),
2✔
96
      properties.getExpelledQueueCapacity(),
2✔
97
      properties.getSketchWindowCount()
2✔
98
    );
99
  }
100

101
  /**
102
   * Create the app-side {@link HotKeyDetector} facade that wraps the HeavyKeeper TopK
103
   * and schedules periodic decay.
104
   *
105
   * <p>The detector provides the primary hot-key detection API ({@code add()}, {@code list()},
106
   * {@code total()}) and schedules the periodic HeavyKeeper decay using the shared scheduler.
107
   * This bean is the {@code @Qualifier("hotKeyDetector")} target for injection into
108
   * {@link HotKeyCache} and other components.
109
   *
110
   * @param heavyKeeper      the HeavyKeeper TopK instance (never {@code null})
111
   * @param hotKeyScheduler  the shared scheduler for periodic tasks (never {@code null})
112
   * @return a new HotKeyDetector instance
113
   */
114
  @Bean
115
  @ConditionalOnMissingBean
116
  @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
117
  public HotKeyDetector hotKeyDetector(
118
    HeavyKeeper heavyKeeper,
119
    @Qualifier("hotKeyScheduler") ScheduledExecutorService hotKeyScheduler
120
  ) {
121
    return new HotKeyDetector(heavyKeeper, hotKeyScheduler);
6✔
122
  }
123

124
  /**
125
   * Create the SingleFlight deduplication layer for concurrent cache-load requests.
126
   *
127
   * <p>When multiple threads request the same key simultaneously (a cache miss), the
128
   * first caller triggers the load and subsequent callers wait for the same result
129
   * rather than duplicating the load. Configuration parameters (max in-flight entries,
130
   * TTL, timeout) are read from {@link HotKeyProperties}.
131
   *
132
   * @param properties     the HotKey configuration properties (never {@code null})
133
   * @param hotKeyExecutor the dedicated HotKey executor for async load execution (never {@code null})
134
   * @return a new SingleFlight instance
135
   */
136
  @Bean
137
  @ConditionalOnMissingBean
138
  public SingleFlight singleFlight(HotKeyProperties properties, @Qualifier("hotKeyExecutor") Executor hotKeyExecutor) {
139
    return new SingleFlight(
4✔
140
      properties.getInflightMaxSize(),
2✔
141
      properties.getInflightTtlSeconds(),
2✔
142
      properties.getInflightTimeoutSeconds(),
5✔
143
      hotKeyExecutor,
144
      new HotKeyCircuitBreaker(properties.getCircuitBreaker())
3✔
145
    );
146
  }
147

148
  /**
149
   * Create the soft/hard expiration manager that manages time-based eviction.
150
   *
151
   * <p>The soft-expire mechanism triggers an asynchronous refresh when a configurable
152
   * portion of the TTL has elapsed, serving stale data while fetching a fresh value
153
   * in the background. The hard-expire is the absolute maximum TTL enforced at the
154
   * Caffeine level. The refresh pool size limits concurrent background refresh tasks
155
   * to prevent resource exhaustion under high cache-miss rates.
156
   *
157
   * @param hotLocalCache  the L1 Caffeine cache (never {@code null})
158
   * @param hotKeyExecutor the dedicated HotKey executor for async refresh tasks (never {@code null})
159
   * @param properties     the HotKey configuration properties (never {@code null})
160
   * @return a new CacheExpireManager instance
161
   */
162
  @Bean
163
  @ConditionalOnMissingBean
164
  public CacheExpireManager expireManager(
165
    Cache<String, Object> hotLocalCache,
166
    @Qualifier("hotKeyExecutor") Executor hotKeyExecutor,
167
    HotKeyProperties properties
168
  ) {
169
    return new CacheExpireManager(hotLocalCache, hotKeyExecutor, properties, properties.getRefreshMaxPools());
9✔
170
  }
171

172
  /**
173
   * Create the dedicated thread-pool executor for asynchronous cache operations.
174
   *
175
   * <p>This executor handles all async operations in the HotKey data path: cache loading
176
   * via SingleFlight, soft-expiry refresh tasks, and cross-instance broadcast callbacks.
177
   * Uses a bounded thread pool to limit concurrent AMQP channel usage (RabbitMQ's
178
   * {@code CachingConnectionFactory} associates channels with platform threads, so
179
   * unbounded virtual-thread concurrency causes channel-open timeouts). The pool is
180
   * configured with core/max pool size, bounded queue capacity, and a rejection policy
181
   * that throws {@link RejectedExecutionException} when the queue is full. On shutdown,
182
   * in-progress tasks are allowed to complete with a 60-second grace period.
183
   *
184
   * @param properties the HotKey configuration properties (never {@code null})
185
   * @return a configured {@link ThreadPoolTaskExecutor}
186
   */
187
  @Bean("hotKeyExecutor")
188
  @ConditionalOnMissingBean(name = "hotKeyExecutor")
189
  public Executor hotKeyExecutor(HotKeyProperties properties) {
190
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
4✔
191
    executor.setCorePoolSize(0);
3✔
192
    executor.setMaxPoolSize(properties.getExecutorMaxPoolSize());
4✔
193
    executor.setQueueCapacity(properties.getExecutorQueueCapacity());
4✔
194
    executor.setAllowCoreThreadTimeOut(true);
3✔
195
    executor.setThreadNamePrefix(HotKeyConstants.THREAD_PREFIX_HOTKEY);
3✔
196
    executor.setWaitForTasksToCompleteOnShutdown(true);
3✔
197
    executor.setAwaitTerminationSeconds(60);
3✔
198
    executor.setRejectedExecutionHandler((r, exe) -> {
4✔
199
      log.warn(
×
200
        "HotKey executor task rejected: corePool={}, maxPool={}, queueCapacity={}",
201
        properties.getExecutorCorePoolSize(),
×
202
        properties.getExecutorMaxPoolSize(),
×
203
        properties.getExecutorQueueCapacity()
×
204
      );
205
      throw new RejectedExecutionException("HotKey executor queue full");
×
206
    });
207
    executor.initialize();
2✔
208
    return executor;
2✔
209
  }
210

211
  /**
212
   * Create the {@link RuleMatcher} for key matching against user-defined rules.
213
   *
214
   * <p>This variant is wired with an empty Redis provider and an optional sync
215
   * publisher, since no {@link StringRedisTemplate} is available in the non-Redis
216
   * deployment mode. Rules are kept in-memory only and do not survive restarts.
217
   * When Redis is available, {@link HotKeyRedisAutoConfiguration#ruleMatcher}
218
   * takes over with Redis persistence.
219
   *
220
   * @param publisherProvider optional provider for the cache sync publisher (may be absent)
221
   * @return a new RuleMatcher instance (in-memory only)
222
   */
223
  @Bean
224
  @ConditionalOnMissingBean({ RuleMatcher.class, StringRedisTemplate.class })
225
  public RuleMatcher ruleMatcher(ObjectProvider<CacheSyncPublisher> publisherProvider) {
226
    return new RuleMatcher(Optional.empty(), Optional.ofNullable(publisherProvider.getIfAvailable()));
9✔
227
  }
228

229
  /**
230
   * Create the {@link HotKeyCache} (non-Redis variant).
231
   *
232
   * <p>Only active when {@code RedisTemplate} is absent; otherwise
233
   * {@link HotKeyRedisAutoConfiguration#hotKeyCache} takes over. Creates a
234
   * {@link VersionController} with an empty Redis provider (falling back to
235
   * node-local counters) and default ring manager / health view when none are
236
   * available from the application context.
237
   *
238
   * @param hotKeyDetector            the app-side TopK detector (never {@code null})
239
   * @param hotLocalCache             the L1 Caffeine cache (never {@code null})
240
   * @param singleFlight              the deduplication layer (never {@code null})
241
   * @param expireManager             the soft/hard expiration manager (never {@code null})
242
   * @param syncPublisher             optional cache sync publisher (may be absent)
243
   * @param hotKeyReporter            optional hot key reporter (may be absent; e.g. in Worker-only mode)
244
   * @param hotKeyExecutor            the dedicated HotKey executor (never {@code null})
245
   * @param properties                the HotKey configuration properties (never {@code null})
246
   * @param ruleMatcher               the rule matcher instance (never {@code null})
247
   * @param healthViewProvider        provider for the cluster health view (creates default if absent)
248
   * @return a new HotKeyCache instance with node-local version tracking
249
   */
250
  @Bean
251
  @ConditionalOnMissingBean(type = "org.springframework.data.redis.core.RedisTemplate")
252
  public HotKeyCache hotKeyCache(
253
    @Qualifier("hotKeyDetector") HotKeyDetector hotKeyDetector,
254
    Cache<String, Object> hotLocalCache,
255
    SingleFlight singleFlight,
256
    CacheExpireManager expireManager,
257
    Optional<CacheSyncPublisher> syncPublisher,
258
    Optional<HotKeyReporter> hotKeyReporter,
259
    @Qualifier("hotKeyExecutor") Executor hotKeyExecutor,
260
    HotKeyProperties properties,
261
    RuleMatcher ruleMatcher,
262
    ObjectProvider<ClusterHealthView> healthViewProvider
263
  ) {
264
    return new HotKeyCache(
13✔
265
      hotKeyDetector,
266
      hotLocalCache,
267
      singleFlight,
268
      expireManager,
269
      hotKeyExecutor,
270
      syncPublisher,
271
      hotKeyReporter,
272
      ruleMatcher,
273
      new VersionController(Optional.empty(), properties.getVersionKeyTtlMinutes()),
8✔
274
      properties,
275
      healthViewProvider.getIfAvailable(() ->
3✔
NEW
276
        new ClusterHealthView(
×
NEW
277
          properties.getExpectedWorkerCount(),
×
NEW
278
          properties.getHeartbeat().getTimeoutMs(),
×
NEW
279
          properties.getHeartbeat().getDegradeAfterFailures()
×
280
        )
281
      )
282
    );
283
  }
284

285
  /**
286
   * Create the L1 Caffeine cache instance.
287
   *
288
   * <p>Time-based expiry operates at the <em>Caffeine</em> level via a custom
289
   * {@link Expiry} implementation, computing remaining nanoseconds from
290
   * . Entries with
291
   * {@code hardExpireAtMs == Long.MAX_VALUE} are purely logical-expiry
292
   * — Caffeine never evicts them by time; they live until size eviction or
293
   * manual invalidation. Reads never extend the expiry duration (no read-based
294
   * refresh), ensuring predictable TTL behavior.
295
   *
296
   * <p>The maximum cache size is configured via {@code hotkey.local.local-cache-max-size}
297
   * (default 1000 entries). Time-based TTL for entries without an explicit hard-expire
298
   * timestamp defaults to {@code hotkey.local.local-cache-ttl-minutes} (default 5 minutes).
299
   *
300
   * @param properties the HotKey configuration properties (never {@code null})
301
   * @return a configured Caffeine {@link Cache} instance
302
   */
303
  @Bean
304
  @ConditionalOnMissingBean
305
  public Cache<String, Object> hotLocalCache(HotKeyProperties properties) {
306
    Caffeine<Object, Object> builder = Caffeine.newBuilder()
2✔
307
      .maximumSize(properties.getLocalCacheMaxSize())
8✔
308
      .expireAfter(
2✔
309
        new Expiry<>() {
9✔
310
          /**
311
           * Compute the time-to-live for a newly created cache entry.
312
           * Returns {@link Long#MAX_VALUE} for pure logical-expiry entries
313
           * (where {@code hardExpireAtMs == Long.MAX_VALUE}); otherwise
314
           * computes the remaining wall-clock time.
315
           *
316
           * @param key              the cache key
317
           * @param value            the cache value (expected to be a {@link CacheEntry})
318
           * @param currentTimeNanos the current time in nanoseconds (provided by Caffeine)
319
           * @return the expiry duration in nanoseconds, or {@link Long#MAX_VALUE} for no expiry
320
           */
321
          @Override
322
          public long expireAfterCreate(@NonNull Object key, @NonNull Object value, long currentTimeNanos) {
323
            if (value instanceof CacheEntry entry) {
3!
324
              if (entry.getHardExpireAtMs() == Long.MAX_VALUE) {
×
325
                // Pure logical expiry: Caffeine never time-evicts this entry.
326
                // See Expiry Javadoc: Long.MAX_VALUE signals "no expiration".
327
                return Long.MAX_VALUE;
×
328
              }
329
              long remainingMs = entry.getHardExpireAtMs() - currentTimeMillis();
×
330
              return TimeUnit.MILLISECONDS.toNanos(Math.max(1, remainingMs));
×
331
            }
332
            return TimeUnit.MINUTES.toNanos(properties.getLocalCacheTtlMinutes());
7✔
333
          }
334

335
          /**
336
           * Re-compute the expiry duration after an entry is updated.
337
           * Preserves pure logical expiry across updates; otherwise
338
           * recalculates from the entry's {@code hardExpireAtMs}.
339
           *
340
           * @param key              the cache key
341
           * @param value            the cache value (expected to be a {@link CacheEntry})
342
           * @param currentTimeNanos the current time in nanoseconds (provided by Caffeine)
343
           * @param currentDuration  the current expiry duration in nanoseconds
344
           * @return the updated expiry duration in nanoseconds, or {@link Long#MAX_VALUE} for no expiry
345
           */
346
          @Override
347
          public long expireAfterUpdate(
348
            @NonNull Object key,
349
            @NonNull Object value,
350
            long currentTimeNanos,
351
            long currentDuration
352
          ) {
353
            if (value instanceof CacheEntry entry) {
×
354
              if (entry.getHardExpireAtMs() == Long.MAX_VALUE) {
×
355
                // Preserve pure logical expiry across updates (e.g. broadcast refresh).
356
                return Long.MAX_VALUE;
×
357
              }
358
              long remainingMs = entry.getHardExpireAtMs() - currentTimeMillis();
×
359
              return TimeUnit.MILLISECONDS.toNanos(Math.max(1, remainingMs));
×
360
            }
361
            return currentDuration;
×
362
          }
363

364
          /**
365
           * Preserve the current expiry duration on read — reads never
366
           * extend or shorten the entry's time-to-live.
367
           *
368
           * @param key              the cache key
369
           * @param value            the cache value
370
           * @param currentTimeNanos the current time in nanoseconds (provided by Caffeine)
371
           * @param currentDuration  the current expiry duration in nanoseconds
372
           * @return the unchanged expiry duration in nanoseconds
373
           */
374
          @Override
375
          public long expireAfterRead(
376
            @NonNull Object key,
377
            @NonNull Object value,
378
            long currentTimeNanos,
379
            long currentDuration
380
          ) {
381
            return currentDuration;
×
382
          }
383
        }
384
      );
385
    return builder.build();
3✔
386
  }
387
}
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