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

Hyshmily / hotkey / 28316658488

28 Jun 2026 08:34AM UTC coverage: 91.178% (-0.05%) from 91.227%
28316658488

push

github

Hyshmily
test :fix CI tests

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

1264 of 1445 branches covered (87.47%)

Branch coverage included in aggregate %.

3583 of 3871 relevant lines covered (92.56%)

4.21 hits per line

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

91.13
/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 lombok.extern.slf4j.Slf4j;
22

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

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

46
  private static final ScheduledExecutorService SCHEDULER = new ScheduledThreadPoolExecutor(
3✔
47
    Runtime.getRuntime().availableProcessors(),
5✔
48
    r -> {
49
      Thread t = new Thread(r, "hotkey-cb");
6✔
50
      t.setDaemon(true);
3✔
51
      return t;
2✔
52
    }
53
  );
54

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

65
  public HotKeyCircuitBreaker(HotKeyProperties.CircuitBreaker config) {
2✔
66
    this.config = config;
3✔
67
    this.bucketSize = config.getWindowBuckets();
4✔
68
    this.successBuckets = new LongAdder[bucketSize];
5✔
69
    this.failBuckets = new LongAdder[bucketSize];
5✔
70
    for (int i = 0; i < bucketSize; i++) {
8✔
71
      successBuckets[i] = new LongAdder();
7✔
72
      failBuckets[i] = new LongAdder();
7✔
73
    }
74
    long slideMs = config.getWindowTimeMs() / bucketSize;
7✔
75
    this.slideFuture = SCHEDULER.scheduleAtFixedRate(this::slide, slideMs, slideMs, TimeUnit.MILLISECONDS);
9✔
76
  }
1✔
77

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

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

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

124
  /** Record a failed call. */
125
  public void onFailure() {
126
    if (config.isEnabled()) {
4✔
127
      failBuckets[currentIndex].increment();
6✔
128
      evaluateThreshold();
2✔
129
    }
130
  }
1✔
131

132
  /** Whether the breaker is currently open. */
133
  public boolean isOpen() {
134
    return open.get() && config.isEnabled();
12!
135
  }
136

137
  private void slide() {
138
    int next = (currentIndex + 1) % bucketSize;
8✔
139
    successBuckets[next].reset();
5✔
140
    failBuckets[next].reset();
5✔
141
    currentIndex = next;
3✔
142
    evaluateThreshold();
2✔
143
  }
1✔
144

145
  private void evaluateThreshold() {
146
    long totalSuccess = 0, totalFail = 0;
4✔
147

148
    for (LongAdder a : successBuckets) {
17✔
149
      totalSuccess += a.sum();
5✔
150
    }
151
    for (LongAdder a : failBuckets) {
17✔
152
      totalFail += a.sum();
5✔
153
    }
154

155
    if (totalFail > 0 && totalSuccess + totalFail >= config.getRequestVolumeThreshold()) {
12✔
156
      double rate = (double) totalFail / (totalSuccess + totalFail);
8✔
157

158
      if (rate > config.getFailThreshold() && open.compareAndSet(false, true)) {
12✔
159
        openTimestamp = currentTimeMillis();
3✔
160

161
        if (config.isLogEnabled()) {
4✔
162
          log.info("CB OPEN (failRate={}, total={})", rate, totalSuccess + totalFail);
9✔
163
        }
164
      }
165
    }
166
  }
1✔
167

168
  private void resetAllBuckets() {
169
    for (LongAdder a : successBuckets) {
17✔
170
      a.reset();
2✔
171
    }
172
    for (LongAdder a : failBuckets) {
17✔
173
      a.reset();
2✔
174
    }
175
  }
1✔
176

177
  @Override
178
  public void close() {
179
    if (slideFuture != null) {
3!
180
      slideFuture.cancel(false);
5✔
181
    }
182
  }
1✔
183
}
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