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

Hyshmily / hotkey / 28356107387

29 Jun 2026 07:35AM UTC coverage: 91.168% (-0.01%) from 91.178%
28356107387

push

github

Hyshmily
test : fix CI tests

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

1276 of 1461 branches covered (87.34%)

Branch coverage included in aggregate %.

10 of 10 new or added lines in 4 files covered. (100.0%)

117 existing lines in 15 files now uncovered.

3596 of 3883 relevant lines covered (92.61%)

4.24 hits per line

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

90.91
/common/src/main/java/io/github/hyshmily/hotkey/cache/HotKeyCircuitBreaker.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.cache;
17

18
import static io.github.hyshmily.hotkey.util.TimeSource.currentTimeMillis;
19

20
import io.github.hyshmily.hotkey.autoconfigure.HotKeyProperties;
21
import io.github.hyshmily.hotkey.util.HotKeyThreadFactory;
22
import lombok.extern.slf4j.Slf4j;
23

24
import java.util.concurrent.ScheduledExecutorService;
25
import java.util.concurrent.ScheduledFuture;
26
import java.util.concurrent.ScheduledThreadPoolExecutor;
27
import java.util.concurrent.TimeUnit;
28
import java.util.concurrent.atomic.AtomicBoolean;
29
import java.util.concurrent.atomic.AtomicLong;
30
import java.util.concurrent.atomic.LongAdder;
31

32
/**
33
 * Sliding-window circuit breaker for protecting remote calls from cascading failures.
34
 *
35
 * <p>Uses a rolling time window divided into buckets ({@link LongAdder}s) to count
36
 * successes and failures. When the failure rate exceeds {@code failThreshold} and the
37
 * total request volume meets {@code requestVolumeThreshold}, the breaker opens.
38
 * After {@code singleTestIntervalMs} in the open state, a single probe request is
39
 * allowed through (half-open). If that probe succeeds, the breaker closes; if it
40
 * fails, the breaker stays open.
41
 *
42
 * <p>This class is thread-safe.
43
 */
44
@Slf4j
3✔
45
public class HotKeyCircuitBreaker implements AutoCloseable {
46

47
  private static final ScheduledExecutorService SCHEDULER = new ScheduledThreadPoolExecutor(
3✔
48
    Runtime.getRuntime().availableProcessors(),
8✔
49
    new HotKeyThreadFactory("hotkey-cb")
50
  );
51

52
  private final HotKeyProperties.CircuitBreaker config;
53
  private final int bucketSize;
54
  private final LongAdder[] successBuckets;
55
  private final LongAdder[] failBuckets;
56
  private volatile int currentIndex;
57
  private final AtomicBoolean open = new AtomicBoolean(false);
6✔
58
  private volatile long openTimestamp;
59
  private final AtomicLong lastHalfOpenAttempt = new AtomicLong(0L);
6✔
60
  private final ScheduledFuture<?> slideFuture;
61

62
  /**
63
   * Creates a new circuit breaker with sliding-window failure tracking.
64
   * <p>
65
   * Initialises the success/failure bucket ring and schedules a periodic
66
   * slide task on the shared {@link #SCHEDULER} to advance the window.
67
   *
68
   * @param config the circuit breaker configuration (window size, thresholds, etc.)
69
   */
70
  public HotKeyCircuitBreaker(HotKeyProperties.CircuitBreaker config) {
2✔
71
    this.config = config;
3✔
72
    this.bucketSize = config.getWindowBuckets();
4✔
73
    this.successBuckets = new LongAdder[bucketSize];
5✔
74
    this.failBuckets = new LongAdder[bucketSize];
5✔
75
    for (int i = 0; i < bucketSize; i++) {
8✔
76
      successBuckets[i] = new LongAdder();
7✔
77
      failBuckets[i] = new LongAdder();
7✔
78
    }
79
    long slideMs = config.getWindowTimeMs() / bucketSize;
7✔
80
    this.slideFuture = SCHEDULER.scheduleAtFixedRate(this::slide, slideMs, slideMs, TimeUnit.MILLISECONDS);
9✔
81
  }
1✔
82

83
  /**
84
   * Check whether a request is allowed through.
85
   * When closed: always allowed. When open: allowed only for half-open probe.
86
   *
87
   * @return {@code true} if the request may proceed
88
   */
89
  public boolean allowRequest() {
90
    if (!config.isEnabled()) {
4✔
91
      if (open.get()) {
4!
UNCOV
92
        open.set(false);
×
93
      }
94
      return true;
2✔
95
    }
96
    if (!open.get()) {
4✔
97
      return true;
2✔
98
    }
99

100
    long now = currentTimeMillis();
2✔
101
    long lastTest = lastHalfOpenAttempt.get();
4✔
102
    if (
6✔
103
      now - openTimestamp > config.getSingleTestIntervalMs() &&
8✔
104
      now - lastTest > config.getSingleTestIntervalMs() &&
7!
105
      lastHalfOpenAttempt.compareAndSet(lastTest, now)
2!
106
    ) {
107
      if (config.isLogEnabled()) {
4!
UNCOV
108
        log.info("CB half-open probe");
×
109
      }
110
      return true;
2✔
111
    }
112
    return false;
2✔
113
  }
114

115
  /** Record a successful call. If the breaker was open, attempt to close it. */
116
  public void onSuccess() {
117
    if (!config.isEnabled()) {
4✔
118
      return;
1✔
119
    }
120
    successBuckets[currentIndex].increment();
6✔
121
    if (open.get() && open.compareAndSet(true, false)) {
10!
122
      resetAllBuckets();
2✔
123
      if (config.isLogEnabled()) {
4!
UNCOV
124
        log.info("CB closed");
×
125
      }
126
    }
127
  }
1✔
128

129
  /** Record a failed call. */
130
  public void onFailure() {
131
    if (config.isEnabled()) {
4✔
132
      failBuckets[currentIndex].increment();
6✔
133
      evaluateThreshold();
2✔
134
    }
135
  }
1✔
136

137
  /** Whether the breaker is currently open. */
138
  public boolean isOpen() {
139
    return open.get() && config.isEnabled();
12!
140
  }
141

142
  private void slide() {
143
    int next = (currentIndex + 1) % bucketSize;
8✔
144
    successBuckets[next].reset();
5✔
145
    failBuckets[next].reset();
5✔
146
    currentIndex = next;
3✔
147
    evaluateThreshold();
2✔
148
  }
1✔
149

150
  private void evaluateThreshold() {
151
    long totalSuccess = 0, totalFail = 0;
4✔
152

153
    for (LongAdder a : successBuckets) {
17✔
154
      totalSuccess += a.sum();
5✔
155
    }
156
    for (LongAdder a : failBuckets) {
17✔
157
      totalFail += a.sum();
5✔
158
    }
159

160
    if (totalFail > 0 && totalSuccess + totalFail >= config.getRequestVolumeThreshold()) {
12✔
161
      double rate = (double) totalFail / (totalSuccess + totalFail);
8✔
162

163
      if (rate > config.getFailThreshold() && open.compareAndSet(false, true)) {
12✔
164
        openTimestamp = currentTimeMillis();
3✔
165

166
        if (config.isLogEnabled()) {
4✔
167
          log.info("CB OPEN (failRate={}, total={})", rate, totalSuccess + totalFail);
9✔
168
        }
169
      }
170
    }
171
  }
1✔
172

173
  private void resetAllBuckets() {
174
    for (LongAdder a : successBuckets) {
17✔
175
      a.reset();
2✔
176
    }
177
    for (LongAdder a : failBuckets) {
17✔
178
      a.reset();
2✔
179
    }
180
  }
1✔
181

182
  @Override
183
  public void close() {
184
    if (slideFuture != null) {
3!
185
      slideFuture.cancel(false);
5✔
186
    }
187
  }
1✔
188
}
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