This is an automated email from the ASF dual-hosted git repository.

Aias00 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shenyu.git


The following commit(s) were added to refs/heads/master by this push:
     new 0e6216d6ed fix: enforce single half-open circuit breaker probe (#7802)
0e6216d6ed is described below

commit 0e6216d6eda676025849ac38e70d7c144111a384
Author: IT-Althusser <[email protected]>
AuthorDate: Tue Oct 20 12:38:23 2026 +0800

    fix: enforce single half-open circuit breaker probe (#7802)
    
    * fix: enforce single half-open circuit breaker probe
    
    * fix: restore per-upstream circuit breaker recovery
---
 .../circuitbreaker/UpstreamCircuitBreaker.java     |  44 +++++++--
 .../circuitbreaker/UpstreamCircuitBreakerTest.java |  53 +++++++++++
 .../apache/shenyu/plugin/divide/DividePlugin.java  |  83 ++++++++++++-----
 .../divide/DividePluginCircuitBreakerTest.java     | 102 +++++++++++++++++++--
 4 files changed, 247 insertions(+), 35 deletions(-)

diff --git 
a/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/circuitbreaker/UpstreamCircuitBreaker.java
 
b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/circuitbreaker/UpstreamCircuitBreaker.java
index 18c3c40918..89388ef6bc 100644
--- 
a/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/circuitbreaker/UpstreamCircuitBreaker.java
+++ 
b/shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/circuitbreaker/UpstreamCircuitBreaker.java
@@ -100,8 +100,9 @@ public final class UpstreamCircuitBreaker {
     /**
      * Whether a request to the given upstream is currently allowed.
      *
-     * <p>Requests are allowed when the breaker is closed, half-open, or open
-     * but already past its half-open wait window (i.e. eligible to be probed).
+     * <p>Requests are allowed only when the breaker is closed. An open breaker
+     * must be acquired through {@link #tryAcquireHalfOpenProbe(String)} after
+     * its wait window has elapsed.
      *
      * @param key the breaker key
      * @return true if a request may be sent to the upstream
@@ -152,6 +153,18 @@ public final class UpstreamCircuitBreaker {
                 .recordFailure();
     }
 
+    /**
+     * Re-open a breaker when its acquired half-open probe is cancelled.
+     *
+     * @param key the breaker key
+     */
+    public void recordProbeCancellation(final String key) {
+        CircuitBreakerState state = this.breakers.get(key);
+        if (Objects.nonNull(state)) {
+            state.recordProbeCancellation();
+        }
+    }
+
     /**
      * Whether the breaker currently blocks requests for the given key.
      *
@@ -185,11 +198,27 @@ public final class UpstreamCircuitBreaker {
      * @return the wrapped mono with outcome recording attached
      */
     public static <T> Mono<T> recordOutcome(final Mono<T> source, final String 
key) {
+        return recordOutcome(source, key, false);
+    }
+
+    /**
+     * Wrap a forwarded {@link Mono} and record its outcome, including 
cancellation
+     * of a request that was granted the half-open probe.
+     *
+     * @param <T> the element type of the source mono
+     * @param source the mono executing the upstream call
+     * @param key the breaker key
+     * @param probeGranted whether this request acquired the half-open probe
+     * @return the wrapped mono with outcome recording attached
+     */
+    public static <T> Mono<T> recordOutcome(final Mono<T> source, final String 
key, final boolean probeGranted) {
         return source.doFinally(signal -> {
             if (signal == SignalType.ON_COMPLETE) {
                 INSTANCE.recordSuccess(key);
             } else if (signal == SignalType.ON_ERROR) {
                 INSTANCE.recordFailure(key);
+            } else if (signal == SignalType.CANCEL && probeGranted) {
+                INSTANCE.recordProbeCancellation(key);
             }
         });
     }
@@ -231,10 +260,7 @@ public final class UpstreamCircuitBreaker {
         }
 
         synchronized boolean isRequestAllowed() {
-            if (this.state == State.OPEN) {
-                return System.currentTimeMillis() - this.openedAtMillis >= 
this.halfOpenWaitMillis;
-            }
-            return true;
+            return this.state == State.CLOSED;
         }
 
         synchronized boolean isBlocking() {
@@ -266,6 +292,12 @@ public final class UpstreamCircuitBreaker {
             }
         }
 
+        synchronized void recordProbeCancellation() {
+            if (this.state == State.HALF_OPEN) {
+                open();
+            }
+        }
+
         private void open() {
             this.state = State.OPEN;
             this.openedAtMillis = System.currentTimeMillis();
diff --git 
a/shenyu-plugin/shenyu-plugin-base/src/test/java/org/apache/shenyu/plugin/base/circuitbreaker/UpstreamCircuitBreakerTest.java
 
b/shenyu-plugin/shenyu-plugin-base/src/test/java/org/apache/shenyu/plugin/base/circuitbreaker/UpstreamCircuitBreakerTest.java
index 35b3c718c3..1c29d1b063 100644
--- 
a/shenyu-plugin/shenyu-plugin-base/src/test/java/org/apache/shenyu/plugin/base/circuitbreaker/UpstreamCircuitBreakerTest.java
+++ 
b/shenyu-plugin/shenyu-plugin-base/src/test/java/org/apache/shenyu/plugin/base/circuitbreaker/UpstreamCircuitBreakerTest.java
@@ -22,7 +22,9 @@ import org.junit.jupiter.api.Test;
 import reactor.core.publisher.Mono;
 import reactor.test.StepVerifier;
 
+import java.lang.reflect.Field;
 import java.util.List;
+import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -90,6 +92,7 @@ public final class UpstreamCircuitBreakerTest {
         // within the wait window no probe is granted
         assertFalse(breaker.tryAcquireHalfOpenProbe(KEY));
         TimeUnit.MILLISECONDS.sleep(150L);
+        assertFalse(breaker.isRequestAllowed(KEY));
         // the first caller is granted the single probe
         assertTrue(breaker.tryAcquireHalfOpenProbe(KEY));
         // no second probe while the first one is in flight
@@ -114,6 +117,56 @@ public final class UpstreamCircuitBreakerTest {
         assertFalse(breaker.isBlocking(KEY));
     }
 
+    @Test
+    public void testCancelledHalfOpenProbeReopensBreaker() throws Exception {
+        UpstreamCircuitBreaker breaker = UpstreamCircuitBreaker.getInstance();
+        breaker.reset(KEY);
+        breaker.recordFailure(KEY);
+        breaker.recordFailure(KEY);
+        breaker.recordFailure(KEY);
+        elapseWaitWindow(breaker);
+        assertTrue(breaker.tryAcquireHalfOpenProbe(KEY));
+
+        StepVerifier.create(UpstreamCircuitBreaker.recordOutcome(Mono.never(), 
KEY))
+                .thenCancel()
+                .verify();
+        assertFalse(breaker.tryAcquireHalfOpenProbe(KEY));
+
+        StepVerifier.create(UpstreamCircuitBreaker.recordOutcome(Mono.never(), 
KEY, true))
+                .thenCancel()
+                .verify();
+
+        assertFalse(breaker.tryAcquireHalfOpenProbe(KEY));
+        elapseWaitWindow(breaker);
+        assertTrue(breaker.tryAcquireHalfOpenProbe(KEY));
+        breaker.reset(KEY);
+    }
+
+    @Test
+    public void testDefaultOutcomeRecordingIgnoresCancellation() {
+        UpstreamCircuitBreaker breaker = UpstreamCircuitBreaker.getInstance();
+        breaker.reset(KEY);
+        breaker.recordFailure(KEY);
+
+        StepVerifier.create(UpstreamCircuitBreaker.recordOutcome(Mono.never(), 
KEY))
+                .thenCancel()
+                .verify();
+        breaker.recordFailure(KEY);
+
+        assertTrue(breaker.isRequestAllowed(KEY));
+        breaker.reset(KEY);
+    }
+
+    private void elapseWaitWindow(final UpstreamCircuitBreaker breaker) throws 
Exception {
+        Field breakersField = 
UpstreamCircuitBreaker.class.getDeclaredField("breakers");
+        breakersField.setAccessible(true);
+        ConcurrentMap<String, ?> breakers = (ConcurrentMap<String, ?>) 
breakersField.get(breaker);
+        Object state = breakers.get(KEY);
+        Field openedAtField = 
state.getClass().getDeclaredField("openedAtMillis");
+        openedAtField.setAccessible(true);
+        openedAtField.setLong(state, System.currentTimeMillis() - 60000L);
+    }
+
     @Test
     public void testConcurrentFailuresOpenBreaker() throws 
InterruptedException {
         final int threads = 32;
diff --git 
a/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-divide/src/main/java/org/apache/shenyu/plugin/divide/DividePlugin.java
 
b/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-divide/src/main/java/org/apache/shenyu/plugin/divide/DividePlugin.java
index ef8deefe99..29df701bed 100644
--- 
a/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-divide/src/main/java/org/apache/shenyu/plugin/divide/DividePlugin.java
+++ 
b/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-divide/src/main/java/org/apache/shenyu/plugin/divide/DividePlugin.java
@@ -99,18 +99,19 @@ public class DividePlugin extends AbstractShenyuPlugin {
             Object error = ShenyuResultWrap.error(exchange, 
ShenyuResultEnum.CANNOT_FIND_HEALTHY_UPSTREAM_URL);
             return WebFluxResultUtils.result(exchange, error);
         }
-        final List<Upstream> breakeredUpstreamList = 
filterByCircuitBreaker(selector.getId(), upstreamList);
+        List<String> specifyDomains = 
exchange.getRequest().getHeaders().get(Constants.SPECIFY_DOMAIN);
+        String specifiedDomain = CollectionUtils.isNotEmpty(specifyDomains) ? 
specifyDomains.get(0) : null;
+        CircuitBreakerSelection circuitBreakerSelection = 
filterByCircuitBreaker(selector.getId(), upstreamList, specifiedDomain);
+        final List<Upstream> breakeredUpstreamList = 
circuitBreakerSelection.getAllowedUpstreams();
         if (CollectionUtils.isEmpty(breakeredUpstreamList)) {
             LOG.warn("all upstreams of selector {} are blocked by the circuit 
breaker, fail fast", selector.getId());
             Object error = ShenyuResultWrap.error(exchange, 
ShenyuResultEnum.CANNOT_FIND_HEALTHY_UPSTREAM_URL);
             return WebFluxResultUtils.result(exchange, error);
         }
-        List<String> specifyDomains = 
exchange.getRequest().getHeaders().get(Constants.SPECIFY_DOMAIN);
         Upstream upstream;
         if (CollectionUtils.isNotEmpty(specifyDomains)) {
-            String requested = specifyDomains.get(0);
             upstream = breakeredUpstreamList.stream()
-                    .filter(u -> u.getUrl().equals(requested))
+                    .filter(u -> u.getUrl().equals(specifiedDomain))
                     .findFirst()
                     .map(u -> Upstream.builder()
                             .url(u.getUrl())
@@ -139,7 +140,8 @@ public class DividePlugin extends AbstractShenyuPlugin {
         exchange.getAttributes().put(Constants.RETRY_STRATEGY, 
StringUtils.defaultIfEmpty(ruleHandle.getRetryStrategy(), 
RetryEnum.CURRENT.getName()));
         exchange.getAttributes().put(Constants.LOAD_BALANCE, 
StringUtils.defaultIfEmpty(ruleHandle.getLoadBalance(), 
LoadBalanceEnum.RANDOM.getName()));
         exchange.getAttributes().put(Constants.DIVIDE_SELECTOR_ID, 
selector.getId());
-        return forwardWithCircuitBreaker(exchange, chain, ruleHandle, 
selector.getId(), upstream);
+        return forwardWithCircuitBreaker(exchange, chain, ruleHandle, 
selector.getId(), upstream,
+                circuitBreakerSelection.isProbe(selector.getId(), upstream));
     }
 
     @Override
@@ -176,20 +178,22 @@ public class DividePlugin extends AbstractShenyuPlugin {
      * @param ruleHandle the divide rule handle
      * @param selectorId the selector id
      * @param upstream the chosen upstream
+     * @param probeGranted whether this request acquired the half-open probe
      * @return {@code Mono<Void>} to indicate when request processing is 
complete
      */
     private Mono<Void> forwardWithCircuitBreaker(final ServerWebExchange 
exchange, final ShenyuPluginChain chain,
-                                                 final DivideRuleHandle 
ruleHandle, final String selectorId, final Upstream upstream) {
+                                                 final DivideRuleHandle 
ruleHandle, final String selectorId, final Upstream upstream,
+                                                 final boolean probeGranted) {
         String breakerKey = UpstreamCircuitBreaker.buildKey(selectorId, 
upstream);
         if (ruleHandle.getLoadBalance().equals(P2C)) {
             return UpstreamCircuitBreaker.recordOutcome(chain.execute(exchange)
-                    .doFinally(signalType -> responseTrigger(upstream)), 
breakerKey);
+                    .doFinally(signalType -> responseTrigger(upstream)), 
breakerKey, probeGranted);
         } else if (ruleHandle.getLoadBalance().equals(SHORTEST_RESPONSE)) {
             long beginTime = System.currentTimeMillis();
             return UpstreamCircuitBreaker.recordOutcome(chain.execute(exchange)
-                    .doOnSuccess(e -> successResponseTrigger(upstream, 
beginTime)), breakerKey);
+                    .doOnSuccess(e -> successResponseTrigger(upstream, 
beginTime)), breakerKey, probeGranted);
         }
-        return UpstreamCircuitBreaker.recordOutcome(chain.execute(exchange), 
breakerKey);
+        return UpstreamCircuitBreaker.recordOutcome(chain.execute(exchange), 
breakerKey, probeGranted);
     }
 
     private DivideRuleHandle buildRuleHandle(final RuleData rule) {
@@ -199,30 +203,47 @@ public class DividePlugin extends AbstractShenyuPlugin {
     /**
      * Filter out upstreams blocked by the built-in circuit breaker.
      *
-     * <p>When every upstream of the selector is blocked, a single half-open
-     * probe request is granted to one of them so the breaker can recover;
-     * otherwise the caller should fail fast.
+     * <p>An upstream whose wait window has elapsed is selected alone for its
+     * single half-open probe, even when other upstreams remain available. This
+     * ensures the acquired probe is actually sent instead of being lost in
+     * load balancing. If no probe is ready, closed upstreams remain available.
+     * When a domain is explicitly specified, that upstream is preferred for
+     * probe acquisition. If it is unavailable and no closed upstream is
+     * available, an eligible upstream may acquire a probe as a fallback.
      *
      * @param selectorId the selector id the upstreams belong to
      * @param upstreamList the healthy upstream list from the cache
+     * @param specifiedDomain the explicitly requested upstream URL, if present
      * @return the upstreams allowed to receive requests, possibly empty
      */
-    private List<Upstream> filterByCircuitBreaker(final String selectorId, 
final List<Upstream> upstreamList) {
+    private CircuitBreakerSelection filterByCircuitBreaker(final String 
selectorId, final List<Upstream> upstreamList,
+                                                            final String 
specifiedDomain) {
         List<Upstream> allowed = new ArrayList<>(upstreamList.size());
+        Upstream probe = null;
+        String probeKey = null;
         for (Upstream upstream : upstreamList) {
-            if 
(UpstreamCircuitBreaker.getInstance().isRequestAllowed(UpstreamCircuitBreaker.buildKey(selectorId,
 upstream))) {
+            String key = UpstreamCircuitBreaker.buildKey(selectorId, upstream);
+            if (UpstreamCircuitBreaker.getInstance().isRequestAllowed(key)) {
                 allowed.add(upstream);
+            } else if (Objects.isNull(probe)
+                    && (Objects.isNull(specifiedDomain) || 
upstream.getUrl().equals(specifiedDomain))
+                    && 
UpstreamCircuitBreaker.getInstance().tryAcquireHalfOpenProbe(key)) {
+                probe = upstream;
+                probeKey = key;
             }
         }
-        if (CollectionUtils.isNotEmpty(allowed)) {
-            return allowed;
-        }
-        for (Upstream upstream : upstreamList) {
-            if 
(UpstreamCircuitBreaker.getInstance().tryAcquireHalfOpenProbe(UpstreamCircuitBreaker.buildKey(selectorId,
 upstream))) {
-                return Collections.singletonList(upstream);
+        if (CollectionUtils.isEmpty(allowed) && Objects.isNull(probe) && 
Objects.nonNull(specifiedDomain)) {
+            for (Upstream upstream : upstreamList) {
+                String key = UpstreamCircuitBreaker.buildKey(selectorId, 
upstream);
+                if 
(UpstreamCircuitBreaker.getInstance().tryAcquireHalfOpenProbe(key)) {
+                    probe = upstream;
+                    probeKey = key;
+                    break;
+                }
             }
         }
-        return Collections.emptyList();
+        List<Upstream> selected = Objects.nonNull(probe) ? 
Collections.singletonList(probe) : allowed;
+        return new CircuitBreakerSelection(selected, probeKey);
     }
 
     private void responseTrigger(final Upstream upstream) {
@@ -253,4 +274,24 @@ public class DividePlugin extends AbstractShenyuPlugin {
         upstream.getSucceeded().incrementAndGet();
     }
 
+    private static final class CircuitBreakerSelection {
+
+        private final List<Upstream> allowedUpstreams;
+
+        private final String probeKey;
+
+        private CircuitBreakerSelection(final List<Upstream> allowedUpstreams, 
final String probeKey) {
+            this.allowedUpstreams = allowedUpstreams;
+            this.probeKey = probeKey;
+        }
+
+        private List<Upstream> getAllowedUpstreams() {
+            return allowedUpstreams;
+        }
+
+        private boolean isProbe(final String selectorId, final Upstream 
upstream) {
+            return Objects.equals(probeKey, 
UpstreamCircuitBreaker.buildKey(selectorId, upstream));
+        }
+    }
+
 }
diff --git 
a/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-divide/src/test/java/org/apache/shenyu/plugin/divide/DividePluginCircuitBreakerTest.java
 
b/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-divide/src/test/java/org/apache/shenyu/plugin/divide/DividePluginCircuitBreakerTest.java
index 69e67575e8..1eea3d546d 100644
--- 
a/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-divide/src/test/java/org/apache/shenyu/plugin/divide/DividePluginCircuitBreakerTest.java
+++ 
b/shenyu-plugin/shenyu-plugin-proxy/shenyu-plugin-divide/src/test/java/org/apache/shenyu/plugin/divide/DividePluginCircuitBreakerTest.java
@@ -47,6 +47,7 @@ import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.ConcurrentMap;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
@@ -140,14 +141,7 @@ public final class DividePluginCircuitBreakerTest {
     @Test
     public void testHalfOpenProbeRecoversBreaker() throws Exception {
         openBreaker();
-        // rewind the open timestamp so the half-open wait window has elapsed
-        Field breakersField = 
UpstreamCircuitBreaker.class.getDeclaredField("breakers");
-        breakersField.setAccessible(true);
-        ConcurrentMap<String, ?> breakers = (ConcurrentMap<String, ?>) 
breakersField.get(UpstreamCircuitBreaker.getInstance());
-        Object state = breakers.get(breakerKey);
-        Field openedAtField = 
state.getClass().getDeclaredField("openedAtMillis");
-        openedAtField.setAccessible(true);
-        openedAtField.setLong(state, System.currentTimeMillis() - 60000L);
+        elapseWaitWindow(breakerKey);
         // the probe request is forwarded and its success closes the breaker
         when(chain.execute(exchange)).thenReturn(Mono.empty());
         StepVerifier.create(dividePlugin.doExecute(exchange, chain, 
selectorData, ruleData))
@@ -156,6 +150,88 @@ public final class DividePluginCircuitBreakerTest {
         
assertFalse(UpstreamCircuitBreaker.getInstance().isBlocking(breakerKey));
     }
 
+    @Test
+    public void testHalfOpenProbeIsSelectedWhileAnotherUpstreamIsHealthy() 
throws Exception {
+        openBreaker();
+        elapseWaitWindow(breakerKey);
+        Upstream healthy = 
Upstream.builder().protocol("http://";).url("127.0.0.1:9091").build();
+        UpstreamCacheManager cacheManager = mock(UpstreamCacheManager.class);
+        
when(cacheManager.findUpstreamListBySelectorId(SELECTOR_ID)).thenReturn(List.of(upstream,
 healthy));
+        
cacheMock.when(UpstreamCacheManager::getInstance).thenReturn(cacheManager);
+        when(chain.execute(exchange)).thenReturn(Mono.empty());
+
+        StepVerifier.create(dividePlugin.doExecute(exchange, chain, 
selectorData, ruleData))
+                .verifyComplete();
+
+        assertEquals(upstream.buildDomain(), 
exchange.getAttribute(Constants.HTTP_DOMAIN));
+        
assertFalse(UpstreamCircuitBreaker.getInstance().isBlocking(breakerKey));
+        verify(chain, times(1)).execute(any());
+    }
+
+    @Test
+    public void testSpecifiedHealthyUpstreamIsNotOverriddenByAnotherProbe() 
throws Exception {
+        openBreaker();
+        elapseWaitWindow(breakerKey);
+        Upstream healthy = 
Upstream.builder().protocol("http://";).url("127.0.0.1:9091").build();
+        UpstreamCacheManager cacheManager = mock(UpstreamCacheManager.class);
+        
when(cacheManager.findUpstreamListBySelectorId(SELECTOR_ID)).thenReturn(List.of(upstream,
 healthy));
+        
cacheMock.when(UpstreamCacheManager::getInstance).thenReturn(cacheManager);
+        this.exchange = 
MockServerWebExchange.from(MockServerHttpRequest.get("http://localhost/test";)
+                .header(Constants.SPECIFY_DOMAIN, healthy.getUrl()).build());
+        exchange.getAttributes().put(Constants.CONTEXT, 
mock(ShenyuContext.class));
+        when(chain.execute(exchange)).thenReturn(Mono.empty());
+
+        StepVerifier.create(dividePlugin.doExecute(exchange, chain, 
selectorData, ruleData))
+                .verifyComplete();
+
+        assertEquals(healthy.buildDomain(), 
exchange.getAttribute(Constants.HTTP_DOMAIN));
+        
assertTrue(UpstreamCircuitBreaker.getInstance().isBlocking(breakerKey));
+        verify(chain, times(1)).execute(any());
+    }
+
+    @Test
+    public void testUnavailableSpecifiedUpstreamFallsBackToAvailableProbe() 
throws Exception {
+        openBreaker();
+        elapseWaitWindow(breakerKey);
+        this.exchange = 
MockServerWebExchange.from(MockServerHttpRequest.get("http://localhost/test";)
+                .header(Constants.SPECIFY_DOMAIN, "127.0.0.1:9091").build());
+        exchange.getAttributes().put(Constants.CONTEXT, 
mock(ShenyuContext.class));
+        when(chain.execute(exchange)).thenReturn(Mono.empty());
+
+        StepVerifier.create(dividePlugin.doExecute(exchange, chain, 
selectorData, ruleData))
+                .verifyComplete();
+
+        assertEquals(upstream.buildDomain(), 
exchange.getAttribute(Constants.HTTP_DOMAIN));
+        
assertFalse(UpstreamCircuitBreaker.getInstance().isBlocking(breakerKey));
+        verify(chain, times(1)).execute(any());
+    }
+
+    @Test
+    public void testCancelledHalfOpenProbeReopensBreaker() throws Exception {
+        openBreaker();
+        elapseWaitWindow(breakerKey);
+        when(chain.execute(exchange)).thenReturn(Mono.never());
+
+        StepVerifier.create(dividePlugin.doExecute(exchange, chain, 
selectorData, ruleData))
+                .thenCancel()
+                .verify();
+
+        
assertFalse(UpstreamCircuitBreaker.getInstance().tryAcquireHalfOpenProbe(breakerKey));
+        elapseWaitWindow(breakerKey);
+        
assertTrue(UpstreamCircuitBreaker.getInstance().tryAcquireHalfOpenProbe(breakerKey));
+    }
+
+    @Test
+    public void testCancelledClosedRequestLeavesBreakerClosed() {
+        when(chain.execute(exchange)).thenReturn(Mono.never());
+
+        StepVerifier.create(dividePlugin.doExecute(exchange, chain, 
selectorData, ruleData))
+                .thenCancel()
+                .verify();
+
+        
assertTrue(UpstreamCircuitBreaker.getInstance().isRequestAllowed(breakerKey));
+    }
+
     @Test
     public void testHealthyUpstreamsBypassBreaker() {
         Upstream healthy = 
Upstream.builder().protocol("http://";).url("127.0.0.1:9091").build();
@@ -178,4 +254,14 @@ public final class DividePluginCircuitBreakerTest {
             UpstreamCircuitBreaker.getInstance().recordFailure(breakerKey);
         }
     }
+
+    private void elapseWaitWindow(final String key) throws Exception {
+        Field breakersField = 
UpstreamCircuitBreaker.class.getDeclaredField("breakers");
+        breakersField.setAccessible(true);
+        ConcurrentMap<String, ?> breakers = (ConcurrentMap<String, ?>) 
breakersField.get(UpstreamCircuitBreaker.getInstance());
+        Object state = breakers.get(key);
+        Field openedAtField = 
state.getClass().getDeclaredField("openedAtMillis");
+        openedAtField.setAccessible(true);
+        openedAtField.setLong(state, System.currentTimeMillis() - 60000L);
+    }
 }

Reply via email to