diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 68dc5109f..f11babbcc 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,3 +1,6 @@ +## 3.30.1 +* Implemented but not exposed Canary Rate Threshold Circuit Breaker Strategy. #533 + ## 3.30.0 * Module to beans which need to initialize on app startup, different of StartupEvent, Eager are not coupled to DPS logic. diff --git a/gradle.properties b/gradle.properties index 6b4fbd30b..138305a02 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=3.30.0-snapshot +version=3.30.1-snapshot diff --git a/src/main/java/com/mageddo/concurrent/ThreadsV2.java b/src/main/java/com/mageddo/concurrent/ThreadsV2.java index 4c59f6b7f..056f102f5 100644 --- a/src/main/java/com/mageddo/concurrent/ThreadsV2.java +++ b/src/main/java/com/mageddo/concurrent/ThreadsV2.java @@ -4,4 +4,7 @@ public class ThreadsV2 { public static boolean isInterrupted() { return Thread.currentThread().isInterrupted(); } + public static boolean isNotInterrupted() { + return !isInterrupted(); + } } diff --git a/src/main/java/com/mageddo/dnsproxyserver/config/CanaryRateThresholdCircuitBreakerStrategyConfig.java b/src/main/java/com/mageddo/dnsproxyserver/config/CanaryRateThresholdCircuitBreakerStrategyConfig.java new file mode 100644 index 000000000..76dff4934 --- /dev/null +++ b/src/main/java/com/mageddo/dnsproxyserver/config/CanaryRateThresholdCircuitBreakerStrategyConfig.java @@ -0,0 +1,18 @@ +package com.mageddo.dnsproxyserver.config; + +import lombok.Builder; +import lombok.Value; + +@Value +@Builder +public class CanaryRateThresholdCircuitBreakerStrategyConfig implements CircuitBreakerStrategyConfig { + + private float failureRateThreshold; + private int minimumNumberOfCalls; + private int permittedNumberOfCallsInHalfOpenState; + + @Override + public Name name() { + return Name.CANARY_RATE_THRESHOLD; + } +} diff --git a/src/main/java/com/mageddo/dnsproxyserver/solver/remote/CircuitStatus.java b/src/main/java/com/mageddo/dnsproxyserver/solver/remote/CircuitStatus.java index df2ad02d8..ef0a964fe 100644 --- a/src/main/java/com/mageddo/dnsproxyserver/solver/remote/CircuitStatus.java +++ b/src/main/java/com/mageddo/dnsproxyserver/solver/remote/CircuitStatus.java @@ -3,5 +3,9 @@ public enum CircuitStatus { OPEN, CLOSED, - HALF_OPEN + HALF_OPEN; + + public static boolean isOpen(CircuitStatus status) { + return OPEN == status; + } } diff --git a/src/main/java/com/mageddo/dnsproxyserver/solver/remote/application/failsafe/CircuitBreakerFactory.java b/src/main/java/com/mageddo/dnsproxyserver/solver/remote/application/failsafe/CircuitBreakerFactory.java index 0159b1595..9805d8792 100644 --- a/src/main/java/com/mageddo/dnsproxyserver/solver/remote/application/failsafe/CircuitBreakerFactory.java +++ b/src/main/java/com/mageddo/dnsproxyserver/solver/remote/application/failsafe/CircuitBreakerFactory.java @@ -32,6 +32,7 @@ public class CircuitBreakerFactory { private final ConfigService configService; private final CircuitBreakerPingCheckerService circuitBreakerCheckerService; private final FailsafeCircuitBreakerFactory failsafeCircuitBreakerFactory; + private final com.mageddo.dnsproxyserver.solver.remote.circuitbreaker.canaryratethreshold.CircuitBreakerFactory canaryThresholdFactory; public Result check(InetSocketAddress remoteAddress, Supplier sup) { final var circuitBreaker = this.findCircuitBreaker(remoteAddress); diff --git a/src/main/java/com/mageddo/dnsproxyserver/solver/remote/circuitbreaker/application/CircuitBreakerDelegate.java b/src/main/java/com/mageddo/dnsproxyserver/solver/remote/circuitbreaker/application/CircuitBreakerDelegate.java index 095796ed8..1e1590724 100644 --- a/src/main/java/com/mageddo/dnsproxyserver/solver/remote/circuitbreaker/application/CircuitBreakerDelegate.java +++ b/src/main/java/com/mageddo/dnsproxyserver/solver/remote/circuitbreaker/application/CircuitBreakerDelegate.java @@ -10,4 +10,6 @@ public interface CircuitBreakerDelegate { Result execute(Supplier sup); CircuitStatus findStatus(); + + void transitionToHalfOpenState(); } diff --git a/src/main/java/com/mageddo/dnsproxyserver/solver/remote/circuitbreaker/application/CircuitBreakerDelegateNonResilient.java b/src/main/java/com/mageddo/dnsproxyserver/solver/remote/circuitbreaker/application/CircuitBreakerDelegateNonResilient.java index 96f7f10eb..c3adae278 100644 --- a/src/main/java/com/mageddo/dnsproxyserver/solver/remote/circuitbreaker/application/CircuitBreakerDelegateNonResilient.java +++ b/src/main/java/com/mageddo/dnsproxyserver/solver/remote/circuitbreaker/application/CircuitBreakerDelegateNonResilient.java @@ -16,4 +16,9 @@ public Result execute(Supplier sup) { public CircuitStatus findStatus() { return null; } + + @Override + public void transitionToHalfOpenState() { + + } } diff --git a/src/main/java/com/mageddo/dnsproxyserver/solver/remote/circuitbreaker/application/CircuitBreakerDelegateStaticThresholdFailsafe.java b/src/main/java/com/mageddo/dnsproxyserver/solver/remote/circuitbreaker/application/CircuitBreakerDelegateStaticThresholdFailsafe.java index abfb15d0f..2381615a4 100644 --- a/src/main/java/com/mageddo/dnsproxyserver/solver/remote/circuitbreaker/application/CircuitBreakerDelegateStaticThresholdFailsafe.java +++ b/src/main/java/com/mageddo/dnsproxyserver/solver/remote/circuitbreaker/application/CircuitBreakerDelegateStaticThresholdFailsafe.java @@ -35,4 +35,9 @@ public CircuitStatus findStatus() { CircuitStatusRefresh.refresh(this.circuitBreaker); return CircuitBreakerStateMapper.fromFailSafeCircuitBreaker(this.circuitBreaker); } + + @Override + public void transitionToHalfOpenState() { + this.circuitBreaker.halfOpen(); + } } diff --git a/src/main/java/com/mageddo/dnsproxyserver/solver/remote/circuitbreaker/application/HealthChecker.java b/src/main/java/com/mageddo/dnsproxyserver/solver/remote/circuitbreaker/application/HealthChecker.java new file mode 100644 index 000000000..abe570775 --- /dev/null +++ b/src/main/java/com/mageddo/dnsproxyserver/solver/remote/circuitbreaker/application/HealthChecker.java @@ -0,0 +1,5 @@ +package com.mageddo.dnsproxyserver.solver.remote.circuitbreaker.application; + +public interface HealthChecker { + boolean isHealthy(); +} diff --git a/src/main/java/com/mageddo/dnsproxyserver/solver/remote/circuitbreaker/canaryratethreshold/CircuitBreakerDelegateCanaryRateThreshold.java b/src/main/java/com/mageddo/dnsproxyserver/solver/remote/circuitbreaker/canaryratethreshold/CircuitBreakerDelegateCanaryRateThreshold.java new file mode 100644 index 000000000..f78c69705 --- /dev/null +++ b/src/main/java/com/mageddo/dnsproxyserver/solver/remote/circuitbreaker/canaryratethreshold/CircuitBreakerDelegateCanaryRateThreshold.java @@ -0,0 +1,41 @@ +package com.mageddo.dnsproxyserver.solver.remote.circuitbreaker.canaryratethreshold; + +import com.mageddo.commons.circuitbreaker.CircuitIsOpenException; +import com.mageddo.dnsproxyserver.solver.remote.CircuitStatus; +import com.mageddo.dnsproxyserver.solver.remote.Result; +import com.mageddo.dnsproxyserver.solver.remote.circuitbreaker.application.CircuitBreakerDelegate; +import com.mageddo.dnsproxyserver.solver.remote.mapper.Resilience4jStatusMapper; +import io.github.resilience4j.circuitbreaker.CallNotPermittedException; +import io.github.resilience4j.circuitbreaker.CircuitBreaker; +import lombok.extern.slf4j.Slf4j; + +import java.util.function.Supplier; + +@Slf4j +public class CircuitBreakerDelegateCanaryRateThreshold implements CircuitBreakerDelegate { + + private final CircuitBreaker circuitBreaker; + + public CircuitBreakerDelegateCanaryRateThreshold(CircuitBreaker circuitBreaker) { + this.circuitBreaker = circuitBreaker; + } + + @Override + public Result execute(Supplier sup) { + try { + return this.circuitBreaker.executeSupplier(sup); + } catch (CallNotPermittedException e) { + throw new CircuitIsOpenException(e); + } + } + + @Override + public CircuitStatus findStatus() { + return Resilience4jStatusMapper.toCircuitStatus(this.circuitBreaker.getState()); + } + + @Override + public void transitionToHalfOpenState() { + this.circuitBreaker.transitionToHalfOpenState(); + } +} diff --git a/src/main/java/com/mageddo/dnsproxyserver/solver/remote/circuitbreaker/canaryratethreshold/CircuitBreakerDelegateSelfObservable.java b/src/main/java/com/mageddo/dnsproxyserver/solver/remote/circuitbreaker/canaryratethreshold/CircuitBreakerDelegateSelfObservable.java new file mode 100644 index 000000000..e95707484 --- /dev/null +++ b/src/main/java/com/mageddo/dnsproxyserver/solver/remote/circuitbreaker/canaryratethreshold/CircuitBreakerDelegateSelfObservable.java @@ -0,0 +1,90 @@ +package com.mageddo.dnsproxyserver.solver.remote.circuitbreaker.canaryratethreshold; + +import com.mageddo.commons.concurrent.Threads; +import com.mageddo.concurrent.ThreadsV2; +import com.mageddo.dnsproxyserver.solver.remote.CircuitStatus; +import com.mageddo.dnsproxyserver.solver.remote.Result; +import com.mageddo.dnsproxyserver.solver.remote.circuitbreaker.application.CircuitBreakerDelegate; +import com.mageddo.dnsproxyserver.solver.remote.circuitbreaker.application.HealthChecker; +import lombok.extern.slf4j.Slf4j; + +import java.time.Duration; +import java.util.function.Supplier; + +@Slf4j +public class CircuitBreakerDelegateSelfObservable implements CircuitBreakerDelegate, AutoCloseable { + + private final CircuitBreakerDelegate delegate; + private final Duration sleepDuration; + private final HealthChecker healthChecker; + private boolean open = true; + + public CircuitBreakerDelegateSelfObservable( + CircuitBreakerDelegate delegate, HealthChecker healthChecker + ) { + this(delegate, Duration.ofSeconds(1), healthChecker); + } + + public CircuitBreakerDelegateSelfObservable( + CircuitBreakerDelegate delegate, Duration sleepDuration, HealthChecker healthChecker + ) { + this.delegate = delegate; + this.sleepDuration = sleepDuration; + this.healthChecker = healthChecker; + this.startOpenCircuitHealthCheckWorker(); + } + + @Override + public Result execute(Supplier sup) { + return this.delegate.execute(sup); + } + + @Override + public CircuitStatus findStatus() { + return this.delegate.findStatus(); + } + + @Override + public void transitionToHalfOpenState() { + this.delegate.transitionToHalfOpenState(); + } + + private void startOpenCircuitHealthCheckWorker() { + Thread + .ofVirtual() + .start(() -> { + while (this.shouldRun()) { + Threads.sleep(this.sleepDuration); + this.healthCheckWhenInOpenState(); + } + }); + } + + private boolean shouldRun() { + return ThreadsV2.isNotInterrupted() && this.open; + } + + private void healthCheckWhenInOpenState() { + final var status = this.findStatus(); + log.trace("status=checking, status={}", status); + if (!CircuitStatus.isOpen(status)) { + log.trace("status=notOpenStatus, status={}", status); + return; + } + final var success = this.isHealthy(); + if (success) { + this.transitionToHalfOpenState(); + log.debug("status=halfOpenStatus, circuitBreaker={}", this); + } + } + + private boolean isHealthy() { + return this.healthChecker.isHealthy(); + } + + @Override + public void close() throws Exception { + this.open = false; + } + +} diff --git a/src/main/java/com/mageddo/dnsproxyserver/solver/remote/circuitbreaker/canaryratethreshold/CircuitBreakerFactory.java b/src/main/java/com/mageddo/dnsproxyserver/solver/remote/circuitbreaker/canaryratethreshold/CircuitBreakerFactory.java new file mode 100644 index 000000000..3e3b5671d --- /dev/null +++ b/src/main/java/com/mageddo/dnsproxyserver/solver/remote/circuitbreaker/canaryratethreshold/CircuitBreakerFactory.java @@ -0,0 +1,29 @@ +package com.mageddo.dnsproxyserver.solver.remote.circuitbreaker.canaryratethreshold; + +import com.mageddo.dnsproxyserver.config.CanaryRateThresholdCircuitBreakerStrategyConfig; +import io.github.resilience4j.circuitbreaker.CircuitBreaker; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import javax.inject.Inject; +import javax.inject.Singleton; + +@Slf4j +@Singleton +@RequiredArgsConstructor(onConstructor = @__({@Inject})) +public class CircuitBreakerFactory { + + public CircuitBreakerDelegateSelfObservable build(CanaryRateThresholdCircuitBreakerStrategyConfig config){ + final var circuitBreakerDelegate = new CircuitBreakerDelegateCanaryRateThreshold( + this.createResilienceCircuitBreakerFrom(config) + ); + final var healthChecker = new CircuitExecutionsAsHealthChecker(circuitBreakerDelegate); + return new CircuitBreakerDelegateSelfObservable( + healthChecker, healthChecker + ); + } + + private CircuitBreaker createResilienceCircuitBreakerFrom(CanaryRateThresholdCircuitBreakerStrategyConfig config) { + return Resilience4jMapper.from(config); + } +} diff --git a/src/main/java/com/mageddo/dnsproxyserver/solver/remote/circuitbreaker/canaryratethreshold/CircuitExecutionsAsHealthChecker.java b/src/main/java/com/mageddo/dnsproxyserver/solver/remote/circuitbreaker/canaryratethreshold/CircuitExecutionsAsHealthChecker.java new file mode 100644 index 000000000..b0aff80db --- /dev/null +++ b/src/main/java/com/mageddo/dnsproxyserver/solver/remote/circuitbreaker/canaryratethreshold/CircuitExecutionsAsHealthChecker.java @@ -0,0 +1,55 @@ +package com.mageddo.dnsproxyserver.solver.remote.circuitbreaker.canaryratethreshold; + +import com.mageddo.commons.circuitbreaker.CircuitCheckException; +import com.mageddo.dnsproxyserver.solver.remote.CircuitStatus; +import com.mageddo.dnsproxyserver.solver.remote.Result; +import com.mageddo.dnsproxyserver.solver.remote.circuitbreaker.application.CircuitBreakerDelegate; +import com.mageddo.dnsproxyserver.solver.remote.circuitbreaker.application.HealthChecker; +import lombok.extern.slf4j.Slf4j; + +import java.util.function.Supplier; + +@Slf4j +public class CircuitExecutionsAsHealthChecker implements HealthChecker, CircuitBreakerDelegate { + + private final CircuitBreakerDelegate delegate; + private final boolean healthWhenNoCallTemplateToDo; + private Supplier lastCall = null; + + public CircuitExecutionsAsHealthChecker(CircuitBreakerDelegate delegate) { + this.delegate = delegate; + this.healthWhenNoCallTemplateToDo = false; + } + + @Override + public boolean isHealthy() { + try { + if (this.lastCall == null) { + log.trace("status=noLastCall, answer={}", this.healthWhenNoCallTemplateToDo); + return this.healthWhenNoCallTemplateToDo; + } + final var res = this.lastCall.get(); + log.trace("status=delegateToLastCall, answer={}", res); + return true; + } catch (CircuitCheckException e) { + log.trace("status=callFailed, answer=false, msg={}", e.getMessage()); + return false; + } + } + + @Override + public Result execute(Supplier sup) { + this.lastCall = sup; + return this.delegate.execute(sup); + } + + @Override + public CircuitStatus findStatus() { + return this.delegate.findStatus(); + } + + @Override + public void transitionToHalfOpenState() { + this.delegate.transitionToHalfOpenState(); + } +} diff --git a/src/main/java/com/mageddo/dnsproxyserver/solver/remote/circuitbreaker/canaryratethreshold/Resilience4jMapper.java b/src/main/java/com/mageddo/dnsproxyserver/solver/remote/circuitbreaker/canaryratethreshold/Resilience4jMapper.java new file mode 100644 index 000000000..13898140f --- /dev/null +++ b/src/main/java/com/mageddo/dnsproxyserver/solver/remote/circuitbreaker/canaryratethreshold/Resilience4jMapper.java @@ -0,0 +1,28 @@ +package com.mageddo.dnsproxyserver.solver.remote.circuitbreaker.canaryratethreshold; + +import com.mageddo.commons.circuitbreaker.CircuitCheckException; +import com.mageddo.dnsproxyserver.config.CanaryRateThresholdCircuitBreakerStrategyConfig; +import io.github.resilience4j.circuitbreaker.CircuitBreaker; +import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig; + +import java.time.Duration; + +public class Resilience4jMapper { + public static CircuitBreaker from(CanaryRateThresholdCircuitBreakerStrategyConfig config) { + final var circuitBreaker = CircuitBreaker.of( + "defaultCircuitBreaker", + CircuitBreakerConfig + .custom() + + .failureRateThreshold(config.getFailureRateThreshold()) + .minimumNumberOfCalls(config.getMinimumNumberOfCalls()) + .permittedNumberOfCallsInHalfOpenState(config.getPermittedNumberOfCallsInHalfOpenState()) + + .waitDurationInOpenState(Duration.ofDays(365)) + .recordExceptions(CircuitCheckException.class) + + .build() + ); + return circuitBreaker; + } +} diff --git a/src/main/java/com/mageddo/dnsproxyserver/solver/remote/mapper/Resilience4jStatusMapper.java b/src/main/java/com/mageddo/dnsproxyserver/solver/remote/mapper/Resilience4jStatusMapper.java new file mode 100644 index 000000000..a32df4624 --- /dev/null +++ b/src/main/java/com/mageddo/dnsproxyserver/solver/remote/mapper/Resilience4jStatusMapper.java @@ -0,0 +1,11 @@ +package com.mageddo.dnsproxyserver.solver.remote.mapper; + +import com.mageddo.dnsproxyserver.solver.remote.CircuitStatus; +import io.github.resilience4j.circuitbreaker.CircuitBreaker; +import org.apache.commons.lang3.EnumUtils; + +public class Resilience4jStatusMapper { + public static CircuitStatus toCircuitStatus(CircuitBreaker.State state){ + return EnumUtils.getEnum(CircuitStatus.class, state.name()); + } +} diff --git a/src/test/java/com/mageddo/dnsproxyserver/solver/remote/circuitbreaker/application/CircuitExecutionsAsHealthCheckerTest.java b/src/test/java/com/mageddo/dnsproxyserver/solver/remote/circuitbreaker/application/CircuitExecutionsAsHealthCheckerTest.java new file mode 100644 index 000000000..736a1a0fe --- /dev/null +++ b/src/test/java/com/mageddo/dnsproxyserver/solver/remote/circuitbreaker/application/CircuitExecutionsAsHealthCheckerTest.java @@ -0,0 +1,39 @@ +package com.mageddo.dnsproxyserver.solver.remote.circuitbreaker.application; + +import com.mageddo.dnsproxyserver.solver.remote.circuitbreaker.canaryratethreshold.CircuitExecutionsAsHealthChecker; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import testing.templates.solver.remote.ResultSupplierTemplates; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +@ExtendWith(MockitoExtension.class) +class CircuitExecutionsAsHealthCheckerTest { + + @Mock + CircuitBreakerDelegate circuitBreaker; + + @InjectMocks + CircuitExecutionsAsHealthChecker obj; + + @Test + void mustUseTheLastCallAsHealthCheck() { + + final var sup = ResultSupplierTemplates.withCallsCounterNullRes(); + + this.obj.execute(sup); + this.obj.isHealthy(); + + assertEquals(1, sup.getCalls()); + } + + @Test + void mustAnswerHealthWhenExecuteNeverCalled(){ + final var healthy = this.obj.isHealthy(); + assertFalse(healthy); + } +} diff --git a/src/test/java/com/mageddo/dnsproxyserver/solver/remote/circuitbreaker/application/DpsCircuitBreakerWithManualHalfOpenTest.java b/src/test/java/com/mageddo/dnsproxyserver/solver/remote/circuitbreaker/application/DpsCircuitBreakerWithManualHalfOpenTest.java new file mode 100644 index 000000000..a01b80c87 --- /dev/null +++ b/src/test/java/com/mageddo/dnsproxyserver/solver/remote/circuitbreaker/application/DpsCircuitBreakerWithManualHalfOpenTest.java @@ -0,0 +1,60 @@ +package com.mageddo.dnsproxyserver.solver.remote.circuitbreaker.application; + +import com.mageddo.supporting.circuitbreaker.testing.sandbox.Result; +import io.github.resilience4j.circuitbreaker.CircuitBreaker; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import testing.templates.circuitbreaker.Resilience4jCircuitBreakerTemplates; + +import static com.mageddo.supporting.circuitbreaker.testing.sandbox.resilience4j.Resilience4jCircuitBreakerSandBox.testCircuitOnError; +import static com.mageddo.supporting.circuitbreaker.testing.sandbox.resilience4j.Resilience4jCircuitBreakerSandBox.testCircuitOnSuccess; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@Slf4j +public class DpsCircuitBreakerWithManualHalfOpenTest { + + @Test + void mustNotHalfOpenAutomatically() { + + // arrange + final var circuit = dpsConfig(); + circuit.transitionToOpenState(); + + assertEquals(CircuitBreaker.State.OPEN, circuit.getState()); + testCircuitOnSuccess(Result.CIRCUIT_OPEN, CircuitBreaker.State.OPEN, circuit, 1); + + } + + @Test + void serverGotUp() { + + // arrange + final var circuit = dpsConfig(); + circuit.transitionToOpenState(); + circuit.transitionToHalfOpenState(); + + // act // assert + testCircuitOnSuccess(Result.SUCCESS, CircuitBreaker.State.HALF_OPEN, circuit, 8); + testCircuitOnError(Result.ERROR, CircuitBreaker.State.HALF_OPEN, circuit, 1); + testCircuitOnError(Result.ERROR, CircuitBreaker.State.CLOSED, circuit, 1); + + } + + @Test + void serverGoesDownAndDecideToOpenTheCircuitAfterMinimumNumberOfCalls() { + + // arrange + final var circuit = dpsConfig(); + + // act // assert + testCircuitOnError(Result.ERROR, CircuitBreaker.State.CLOSED, circuit, 4); + testCircuitOnSuccess(Result.SUCCESS, CircuitBreaker.State.CLOSED, circuit, 1); + testCircuitOnError(Result.ERROR, CircuitBreaker.State.CLOSED, circuit, 94); + testCircuitOnSuccess(Result.SUCCESS, CircuitBreaker.State.OPEN, circuit, 1); + + } + + static CircuitBreaker dpsConfig() { + return Resilience4jCircuitBreakerTemplates.theDefaultHandlingIoException(); + } +} diff --git a/src/test/java/com/mageddo/dnsproxyserver/solver/remote/circuitbreaker/canaryratethreshold/CircuitBreakerDelegateCanaryRateThresholdTest.java b/src/test/java/com/mageddo/dnsproxyserver/solver/remote/circuitbreaker/canaryratethreshold/CircuitBreakerDelegateCanaryRateThresholdTest.java new file mode 100644 index 000000000..4cca0a01b --- /dev/null +++ b/src/test/java/com/mageddo/dnsproxyserver/solver/remote/circuitbreaker/canaryratethreshold/CircuitBreakerDelegateCanaryRateThresholdTest.java @@ -0,0 +1,86 @@ +package com.mageddo.dnsproxyserver.solver.remote.circuitbreaker.canaryratethreshold; + +import com.mageddo.commons.circuitbreaker.CircuitCheckException; +import com.mageddo.commons.circuitbreaker.CircuitIsOpenException; +import com.mageddo.dnsproxyserver.solver.remote.CircuitStatus; +import com.mageddo.dnsproxyserver.solver.remote.Result; +import io.github.resilience4j.circuitbreaker.CircuitBreaker; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import testing.templates.circuitbreaker.Resilience4jCircuitBreakerTemplates; +import testing.templates.solver.remote.ResultTemplates; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class CircuitBreakerDelegateCanaryRateThresholdTest { + + CircuitBreaker circuitBreaker = Resilience4jCircuitBreakerTemplates.fastFail(); + + CircuitBreakerDelegateCanaryRateThreshold delegate; + + @BeforeEach + void beforeEach() { + this.delegate = new CircuitBreakerDelegateCanaryRateThreshold(this.circuitBreaker); + } + + @Test + void mustExecuteSupplierWithSuccess() { + // arrange + final var counter = new AtomicInteger(); + + // act + final var result = this.delegate.execute(() -> { + counter.incrementAndGet(); + return ResultTemplates.success(); + }); + + // assert + assertNotNull(result); + assertTrue(result.hasSuccessMessage()); + assertEquals(1, counter.get()); + } + + + @Test + void mustThrowSpecificExceptionWhenGetOpenCircuit() { + // arrange + final Supplier sup = () -> { + throw new CircuitCheckException("Mocked Error"); + }; + + // act // assert + assertThrows(CircuitCheckException.class, () -> this.delegate.execute(sup)); + assertThrows(CircuitIsOpenException.class, () -> this.delegate.execute(sup)); + + } + + @Test + void mustProvideCircuitBreakerStatus() { + + final var status = this.delegate.findStatus(); + + assertEquals(CircuitStatus.CLOSED, status); + + } + + + @Test + void mustProvideNullStatusWhenItsNotMapped() { + + this.circuitBreaker.transitionToForcedOpenState(); + + final var status = this.delegate.findStatus(); + + assertNull(status); + + } + + +} diff --git a/src/test/java/com/mageddo/dnsproxyserver/solver/remote/circuitbreaker/canaryratethreshold/CircuitBreakerDelegateSelfObservableTest.java b/src/test/java/com/mageddo/dnsproxyserver/solver/remote/circuitbreaker/canaryratethreshold/CircuitBreakerDelegateSelfObservableTest.java new file mode 100644 index 000000000..0c6687d2a --- /dev/null +++ b/src/test/java/com/mageddo/dnsproxyserver/solver/remote/circuitbreaker/canaryratethreshold/CircuitBreakerDelegateSelfObservableTest.java @@ -0,0 +1,85 @@ +package com.mageddo.dnsproxyserver.solver.remote.circuitbreaker.canaryratethreshold; + +import com.mageddo.commons.concurrent.Threads; +import com.mageddo.dnsproxyserver.solver.remote.CircuitStatus; +import com.mageddo.dnsproxyserver.solver.remote.circuitbreaker.application.HealthChecker; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.Duration; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class CircuitBreakerDelegateSelfObservableTest { + + CircuitBreakerDelegateSelfObservable strategy; + + @Mock + CircuitBreakerDelegateCanaryRateThreshold delegate; + + @Mock + HealthChecker healthChecker; + + @BeforeEach + void beforeEach() { + this.strategy = new CircuitBreakerDelegateSelfObservable( + this.delegate, + Duration.ofMillis(1000 / 30), + this.healthChecker + ); + } + + @Test + void mustStartBackgroundTaskWhenCreatingObject() { + + // act + Threads.sleep(300); + + // assert + verify(this.delegate, atLeastOnce()).findStatus(); + + } + + @Test + void mustHalfOpenCircuitAfterConfiguredTimeAndSatisfyHealthCheck() { + + // arrange + doReturn(CircuitStatus.OPEN) + .when(this.delegate) + .findStatus() + ; + doReturn(true) + .when(this.healthChecker) + .isHealthy() + ; + + // act + Threads.sleep(300); + + // assert + verify(this.delegate, atLeastOnce()).transitionToHalfOpenState(); + + } + + @Test + void mustNotHalfOpenCircuitAfterHealthCheckRunAndGetNoSuccess() { + // arrange + doReturn(CircuitStatus.OPEN) + .when(this.delegate) + .findStatus() + ; + + // act + Threads.sleep(300); + + // assert + assertEquals(CircuitStatus.OPEN, this.strategy.findStatus()); + } +} diff --git a/src/test/java/com/mageddo/dnsproxyserver/solver/remote/circuitbreaker/canaryratethreshold/CircuitBreakerFactoryCompTest.java b/src/test/java/com/mageddo/dnsproxyserver/solver/remote/circuitbreaker/canaryratethreshold/CircuitBreakerFactoryCompTest.java new file mode 100644 index 000000000..bf6e880c5 --- /dev/null +++ b/src/test/java/com/mageddo/dnsproxyserver/solver/remote/circuitbreaker/canaryratethreshold/CircuitBreakerFactoryCompTest.java @@ -0,0 +1,62 @@ +package com.mageddo.dnsproxyserver.solver.remote.circuitbreaker.canaryratethreshold; + +import com.mageddo.commons.circuitbreaker.CircuitCheckException; +import com.mageddo.commons.circuitbreaker.CircuitIsOpenException; +import com.mageddo.commons.concurrent.Threads; +import com.mageddo.dnsproxyserver.solver.remote.CircuitStatus; +import dagger.sheath.junit.DaggerTest; +import org.junit.jupiter.api.Test; +import testing.ContextSupplier; +import testing.Events; +import testing.templates.CircuitBreakerConfigTemplates; +import testing.templates.solver.remote.ResultSupplierTemplates; + +import javax.inject.Inject; + +import java.time.Duration; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@DaggerTest(initializer = ContextSupplier.class, eventsHandler = Events.class) +class CircuitBreakerFactoryCompTest { + + @Inject + CircuitBreakerFactory factory; + + @Test + void mustExecuteHappyPath() { + + final var sup = ResultSupplierTemplates.withCallsCounterNullRes(); + final var config = CircuitBreakerConfigTemplates.fastCanaryRateThreshold(); + + final var circuitBreaker = this.factory.build(config); + final var result = circuitBreaker.execute(sup); + + assertEquals(1, sup.getCalls()); + assertNull(result); + + } + + @Test + void mustTestCircuitWhenItIsOpen() { + + final var sup = ResultSupplierTemplates.withCallsCounterNullRes(); + final var config = CircuitBreakerConfigTemplates.fastCanaryRateThreshold(); + + final var circuitBreaker = this.factory.build(config); + assertEquals(CircuitStatus.CLOSED, circuitBreaker.findStatus()); + + assertThrows(CircuitCheckException.class, () -> circuitBreaker.execute(ResultSupplierTemplates.alwaysFail())); + assertEquals(CircuitStatus.OPEN, circuitBreaker.findStatus()); + + assertThrows(CircuitIsOpenException.class, () -> circuitBreaker.execute(sup)); + + Threads.sleep(Duration.ofMillis(1200)); + + assertEquals(CircuitStatus.HALF_OPEN, circuitBreaker.findStatus()); + assertEquals(1, sup.getCalls()); + + } +} diff --git a/src/test/java/com/mageddo/supporting/circuitbreaker/testing/sandbox/AbstractCircuitBreakerSandBox.java b/src/test/java/com/mageddo/supporting/circuitbreaker/testing/sandbox/AbstractCircuitBreakerSandBox.java new file mode 100644 index 000000000..abaeece02 --- /dev/null +++ b/src/test/java/com/mageddo/supporting/circuitbreaker/testing/sandbox/AbstractCircuitBreakerSandBox.java @@ -0,0 +1,55 @@ +package com.mageddo.supporting.circuitbreaker.testing.sandbox; + +import org.apache.commons.lang3.time.StopWatch; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public abstract class AbstractCircuitBreakerSandBox implements CircuitBreakerSandbox { + + protected abstract State getCircuitBreakerState(); + + @Override + public void testCircuitOnError( + final Result expectedResult, final State expectedState, final int times + ) { + testCircuitOn( + expectedResult, expectedState, times, this::runError + ); + } + + @Override + public void testCircuitOnSuccess( + final Result expectedResult, final State expectedState, final int times + ) { + testCircuitOn(expectedResult, expectedState, times, this::runSuccess); + } + + private void testCircuitOn( + final Result expectedResult, final State expectedState, + final int times, final Runnable runnable + ) { + final var stats = new Stats(); + final var stopWatch = StopWatch.createStarted(); + for (int i = 0; i < times; i++) { + assertEquals(expectedResult, this.calcStats(stats, runnable)); + assertCircuitState(i, stopWatch, expectedState, this.getCircuitBreakerState()); + } + } + + private static void assertCircuitState( + int i, StopWatch stopWatch, + final State expectedState, + final State actualState + ) { + assertEquals( + expectedState, + actualState, + formatMessage(i, stopWatch) + ); + } + + private static String formatMessage(int i, StopWatch stopWatch) { + return String.format("try=%d, time=%d", i, stopWatch.getTime()); + } + +} diff --git a/src/test/java/com/mageddo/supporting/circuitbreaker/testing/sandbox/CircuitBreakerSandBoxFactory.java b/src/test/java/com/mageddo/supporting/circuitbreaker/testing/sandbox/CircuitBreakerSandBoxFactory.java new file mode 100644 index 000000000..e1f8c4ad5 --- /dev/null +++ b/src/test/java/com/mageddo/supporting/circuitbreaker/testing/sandbox/CircuitBreakerSandBoxFactory.java @@ -0,0 +1,4 @@ +package com.mageddo.supporting.circuitbreaker.testing.sandbox; + +public class CircuitBreakerSandBoxFactory { +} diff --git a/src/test/java/com/mageddo/supporting/circuitbreaker/testing/sandbox/CircuitBreakerSandbox.java b/src/test/java/com/mageddo/supporting/circuitbreaker/testing/sandbox/CircuitBreakerSandbox.java new file mode 100644 index 000000000..218427a56 --- /dev/null +++ b/src/test/java/com/mageddo/supporting/circuitbreaker/testing/sandbox/CircuitBreakerSandbox.java @@ -0,0 +1,19 @@ +package com.mageddo.supporting.circuitbreaker.testing.sandbox; + +public interface CircuitBreakerSandbox { + + void testCircuitOnError( + final Result expectedResult, final State expectedState, final int times + ); + + void testCircuitOnSuccess( + final Result expectedResult, final State expectedState, final int times + ); + + Result calcStats(Stats stats, Runnable r); + + String runError(); + + String runSuccess(); + +} diff --git a/src/test/java/com/mageddo/supporting/circuitbreaker/testing/sandbox/Result.java b/src/test/java/com/mageddo/supporting/circuitbreaker/testing/sandbox/Result.java new file mode 100644 index 000000000..32bd11676 --- /dev/null +++ b/src/test/java/com/mageddo/supporting/circuitbreaker/testing/sandbox/Result.java @@ -0,0 +1,7 @@ +package com.mageddo.supporting.circuitbreaker.testing.sandbox; + +public enum Result { + SUCCESS, + ERROR, + CIRCUIT_OPEN +} diff --git a/src/test/java/com/mageddo/supporting/circuitbreaker/testing/sandbox/State.java b/src/test/java/com/mageddo/supporting/circuitbreaker/testing/sandbox/State.java new file mode 100644 index 000000000..24ea1063c --- /dev/null +++ b/src/test/java/com/mageddo/supporting/circuitbreaker/testing/sandbox/State.java @@ -0,0 +1,7 @@ +package com.mageddo.supporting.circuitbreaker.testing.sandbox; + +public enum State { + OPEN, + CLOSED, + HALF_OPEN +} diff --git a/src/test/java/com/mageddo/supporting/circuitbreaker/testing/sandbox/Stats.java b/src/test/java/com/mageddo/supporting/circuitbreaker/testing/sandbox/Stats.java new file mode 100644 index 000000000..316179ad7 --- /dev/null +++ b/src/test/java/com/mageddo/supporting/circuitbreaker/testing/sandbox/Stats.java @@ -0,0 +1,7 @@ +package com.mageddo.supporting.circuitbreaker.testing.sandbox; + +public class Stats { + public int success; + public int error; + public int openCircuit; +} diff --git a/src/test/java/com/mageddo/supporting/circuitbreaker/testing/sandbox/failsafe/FailSafeCircuitBreakerSandBox.java b/src/test/java/com/mageddo/supporting/circuitbreaker/testing/sandbox/failsafe/FailSafeCircuitBreakerSandBox.java new file mode 100644 index 000000000..1dec37c9f --- /dev/null +++ b/src/test/java/com/mageddo/supporting/circuitbreaker/testing/sandbox/failsafe/FailSafeCircuitBreakerSandBox.java @@ -0,0 +1,72 @@ +package com.mageddo.supporting.circuitbreaker.testing.sandbox.failsafe; + +import com.mageddo.supporting.circuitbreaker.testing.sandbox.AbstractCircuitBreakerSandBox; +import com.mageddo.supporting.circuitbreaker.testing.sandbox.Result; +import com.mageddo.supporting.circuitbreaker.testing.sandbox.State; +import com.mageddo.supporting.circuitbreaker.testing.sandbox.Stats; +import dev.failsafe.CircuitBreaker; +import dev.failsafe.Failsafe; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.time.LocalDateTime; + + +public class FailSafeCircuitBreakerSandBox extends AbstractCircuitBreakerSandBox { + + private final CircuitBreaker circuitBreaker; + + public FailSafeCircuitBreakerSandBox(final CircuitBreaker circuitBreaker) { + this.circuitBreaker = circuitBreaker; + } + + @Override + protected State getCircuitBreakerState() { + return StateMapper.from(this.circuitBreaker.getState()); + } + + @Override + public Result calcStats(Stats stats, Runnable r) { + return StatsCalculator.calcStats(stats, r); + } + + @Override + public String runError() { + return runError(this.circuitBreaker); + } + + @Override + public String runSuccess() { + return runSuccess(this.circuitBreaker); + } + + public static String runError(CircuitBreaker breaker) { + return Failsafe.with(breaker) + .get(() -> { + throw new UncheckedIOException(new IOException("an error")); + }); + } + + public static String runSuccess(CircuitBreaker breaker) { + return Failsafe.with(breaker) + .get(() -> LocalDateTime.now().toString()); + } + + public static void testCircuitOnError( + final Result expectedResult, final CircuitBreaker.State expectedState, + final CircuitBreaker circuit, final int times + ) { + of(circuit).testCircuitOnError(expectedResult, StateMapper.from(expectedState), times); + } + + public static void testCircuitOnSuccess( + final Result expectedResult, final CircuitBreaker.State expectedState, + final CircuitBreaker circuit, final int times + ) { + of(circuit).testCircuitOnSuccess(expectedResult, StateMapper.from(expectedState), times); + } + + public static FailSafeCircuitBreakerSandBox of(CircuitBreaker circuitBreaker) { + return new FailSafeCircuitBreakerSandBox(circuitBreaker); + } +} diff --git a/src/test/java/com/mageddo/supporting/circuitbreaker/testing/sandbox/failsafe/StateMapper.java b/src/test/java/com/mageddo/supporting/circuitbreaker/testing/sandbox/failsafe/StateMapper.java new file mode 100644 index 000000000..c3bd8b1f9 --- /dev/null +++ b/src/test/java/com/mageddo/supporting/circuitbreaker/testing/sandbox/failsafe/StateMapper.java @@ -0,0 +1,11 @@ +package com.mageddo.supporting.circuitbreaker.testing.sandbox.failsafe; + +import com.mageddo.supporting.circuitbreaker.testing.sandbox.State; +import dev.failsafe.CircuitBreaker; + +public class StateMapper { + + public static State from(CircuitBreaker.State state) { + return State.valueOf(state.name()); + } +} diff --git a/src/test/java/com/mageddo/supporting/circuitbreaker/testing/sandbox/failsafe/StatsCalculator.java b/src/test/java/com/mageddo/supporting/circuitbreaker/testing/sandbox/failsafe/StatsCalculator.java new file mode 100644 index 000000000..b73072a02 --- /dev/null +++ b/src/test/java/com/mageddo/supporting/circuitbreaker/testing/sandbox/failsafe/StatsCalculator.java @@ -0,0 +1,23 @@ +package com.mageddo.supporting.circuitbreaker.testing.sandbox.failsafe; + +import com.mageddo.supporting.circuitbreaker.testing.sandbox.Result; +import com.mageddo.supporting.circuitbreaker.testing.sandbox.Stats; +import dev.failsafe.CircuitBreakerOpenException; + +import java.io.UncheckedIOException; + +public class StatsCalculator { + public static Result calcStats(Stats stats, Runnable r) { + try { + r.run(); + stats.success++; + return Result.SUCCESS; + } catch (CircuitBreakerOpenException e) { + stats.openCircuit++; + return Result.CIRCUIT_OPEN; + } catch (UncheckedIOException e) { + stats.error++; + return Result.ERROR; + } + } +} diff --git a/src/test/java/com/mageddo/supporting/circuitbreaker/testing/sandbox/resilience4j/Resilience4jCircuitBreakerSandBox.java b/src/test/java/com/mageddo/supporting/circuitbreaker/testing/sandbox/resilience4j/Resilience4jCircuitBreakerSandBox.java new file mode 100644 index 000000000..9994dc8bd --- /dev/null +++ b/src/test/java/com/mageddo/supporting/circuitbreaker/testing/sandbox/resilience4j/Resilience4jCircuitBreakerSandBox.java @@ -0,0 +1,68 @@ +package com.mageddo.supporting.circuitbreaker.testing.sandbox.resilience4j; + +import com.mageddo.supporting.circuitbreaker.testing.sandbox.Result; +import com.mageddo.supporting.circuitbreaker.testing.sandbox.State; +import com.mageddo.supporting.circuitbreaker.testing.sandbox.Stats; +import com.mageddo.supporting.circuitbreaker.testing.sandbox.AbstractCircuitBreakerSandBox; +import io.github.resilience4j.circuitbreaker.CircuitBreaker; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.time.LocalDateTime; + +public class Resilience4jCircuitBreakerSandBox extends AbstractCircuitBreakerSandBox { + + private final CircuitBreaker circuitBreaker; + + public Resilience4jCircuitBreakerSandBox(final CircuitBreaker circuitBreaker) { + this.circuitBreaker = circuitBreaker; + } + + @Override + protected State getCircuitBreakerState() { + return StateMapper.from(this.circuitBreaker.getState()); + } + + @Override + public Result calcStats(Stats stats, Runnable r) { + return StatsCalculator.calcStats(stats, r); + } + + @Override + public String runError() { + return runError(this.circuitBreaker); + } + + @Override + public String runSuccess() { + return runSuccess(this.circuitBreaker); + } + + public static String runError(CircuitBreaker breaker) { + return breaker.executeSupplier(() -> { + throw new UncheckedIOException(new IOException("an error")); + }); + } + + public static String runSuccess(CircuitBreaker breaker) { + return breaker.executeSupplier(() -> LocalDateTime.now().toString()); + } + + public static void testCircuitOnError( + final Result expectedResult, final CircuitBreaker.State expectedState, + final CircuitBreaker circuit, final int times + ) { + of(circuit).testCircuitOnError(expectedResult, StateMapper.from(expectedState), times); + } + + public static void testCircuitOnSuccess( + final Result expectedResult, final CircuitBreaker.State expectedState, + final CircuitBreaker circuit, final int times + ) { + of(circuit).testCircuitOnSuccess(expectedResult, StateMapper.from(expectedState), times); + } + + public static Resilience4jCircuitBreakerSandBox of(CircuitBreaker circuitBreaker) { + return new Resilience4jCircuitBreakerSandBox(circuitBreaker); + } +} diff --git a/src/test/java/com/mageddo/supporting/circuitbreaker/testing/sandbox/resilience4j/StateMapper.java b/src/test/java/com/mageddo/supporting/circuitbreaker/testing/sandbox/resilience4j/StateMapper.java new file mode 100644 index 000000000..28e3ecae2 --- /dev/null +++ b/src/test/java/com/mageddo/supporting/circuitbreaker/testing/sandbox/resilience4j/StateMapper.java @@ -0,0 +1,11 @@ +package com.mageddo.supporting.circuitbreaker.testing.sandbox.resilience4j; + +import com.mageddo.supporting.circuitbreaker.testing.sandbox.State; +import io.github.resilience4j.circuitbreaker.CircuitBreaker; + +public class StateMapper { + public static State from(CircuitBreaker.State state) { + return State.valueOf(state.name()); + } + +} diff --git a/src/test/java/com/mageddo/supporting/circuitbreaker/testing/sandbox/resilience4j/StatsCalculator.java b/src/test/java/com/mageddo/supporting/circuitbreaker/testing/sandbox/resilience4j/StatsCalculator.java new file mode 100644 index 000000000..b80488896 --- /dev/null +++ b/src/test/java/com/mageddo/supporting/circuitbreaker/testing/sandbox/resilience4j/StatsCalculator.java @@ -0,0 +1,23 @@ +package com.mageddo.supporting.circuitbreaker.testing.sandbox.resilience4j; + +import com.mageddo.supporting.circuitbreaker.testing.sandbox.Result; +import com.mageddo.supporting.circuitbreaker.testing.sandbox.Stats; +import io.github.resilience4j.circuitbreaker.CallNotPermittedException; + +import java.io.UncheckedIOException; + +public class StatsCalculator { + public static Result calcStats(Stats stats, Runnable r) { + try { + r.run(); + stats.success++; + return Result.SUCCESS; + } catch (CallNotPermittedException e) { + stats.openCircuit++; + return Result.CIRCUIT_OPEN; + } catch (UncheckedIOException e) { + stats.error++; + return Result.ERROR; + } + } +} diff --git a/src/test/java/testing/templates/CircuitBreakerConfigTemplates.java b/src/test/java/testing/templates/CircuitBreakerConfigTemplates.java index 287c7d2b2..4588227f8 100644 --- a/src/test/java/testing/templates/CircuitBreakerConfigTemplates.java +++ b/src/test/java/testing/templates/CircuitBreakerConfigTemplates.java @@ -1,5 +1,6 @@ package testing.templates; +import com.mageddo.dnsproxyserver.config.CanaryRateThresholdCircuitBreakerStrategyConfig; import com.mageddo.dnsproxyserver.config.CircuitBreakerStrategyConfig; import com.mageddo.dnsproxyserver.config.NonResilientCircuitBreakerStrategyConfig; import com.mageddo.dnsproxyserver.config.StaticThresholdCircuitBreakerStrategyConfig; @@ -9,7 +10,7 @@ public class CircuitBreakerConfigTemplates { - public static StaticThresholdCircuitBreakerStrategyConfig buildDefault(){ + public static StaticThresholdCircuitBreakerStrategyConfig buildDefault() { return ConfigMapper.defaultCircuitBreaker(); } @@ -26,4 +27,12 @@ public static StaticThresholdCircuitBreakerStrategyConfig oneTryFailSuccess() { public static CircuitBreakerStrategyConfig buildNonResilientConfig() { return new NonResilientCircuitBreakerStrategyConfig(); } + + public static CanaryRateThresholdCircuitBreakerStrategyConfig fastCanaryRateThreshold() { + return CanaryRateThresholdCircuitBreakerStrategyConfig.builder() + .permittedNumberOfCallsInHalfOpenState(10) + .minimumNumberOfCalls(1) + .failureRateThreshold(1) + .build(); + } } diff --git a/src/test/java/testing/templates/circuitbreaker/Resilience4jCircuitBreakerTemplates.java b/src/test/java/testing/templates/circuitbreaker/Resilience4jCircuitBreakerTemplates.java new file mode 100644 index 000000000..753ef62a8 --- /dev/null +++ b/src/test/java/testing/templates/circuitbreaker/Resilience4jCircuitBreakerTemplates.java @@ -0,0 +1,65 @@ +package testing.templates.circuitbreaker; + +import com.mageddo.commons.circuitbreaker.CircuitCheckException; +import io.github.resilience4j.circuitbreaker.CircuitBreaker; +import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig; +import lombok.extern.slf4j.Slf4j; + +import java.io.UncheckedIOException; +import java.time.Duration; + +@Slf4j +public class Resilience4jCircuitBreakerTemplates { + + + public static CircuitBreaker theDefault() { + return CircuitBreaker.of( + "defaultCircuitBreaker", + CircuitBreakerConfig + .custom() + + .failureRateThreshold(21f) + .minimumNumberOfCalls(100) + .permittedNumberOfCallsInHalfOpenState(10) + + .waitDurationInOpenState(Duration.ofDays(365)) + .recordExceptions(CircuitCheckException.class) + + .build() + ); + } + + public static CircuitBreaker theDefaultHandlingIoException() { + return CircuitBreaker.of( + "defaultCircuitBreaker", + CircuitBreakerConfig + .custom() + + .failureRateThreshold(21f) + .minimumNumberOfCalls(100) + .permittedNumberOfCallsInHalfOpenState(10) + + .waitDurationInOpenState(Duration.ofDays(365)) + .recordExceptions(UncheckedIOException.class) + + .build() + ); + } + + public static CircuitBreaker fastFail() { + return CircuitBreaker.of( + "defaultCircuitBreaker", + CircuitBreakerConfig + .custom() + + .failureRateThreshold(21f) + .minimumNumberOfCalls(1) + .permittedNumberOfCallsInHalfOpenState(1) + + .waitDurationInOpenState(Duration.ofDays(365)) + .recordExceptions(CircuitCheckException.class) + + .build() + ); + } +} diff --git a/src/test/java/testing/templates/solver/remote/ResultSupplierTemplates.java b/src/test/java/testing/templates/solver/remote/ResultSupplierTemplates.java index c2ac5291c..422cc02a4 100644 --- a/src/test/java/testing/templates/solver/remote/ResultSupplierTemplates.java +++ b/src/test/java/testing/templates/solver/remote/ResultSupplierTemplates.java @@ -3,6 +3,7 @@ import com.mageddo.commons.circuitbreaker.CircuitCheckException; import com.mageddo.dnsproxyserver.solver.remote.Result; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Supplier; public class ResultSupplierTemplates { @@ -17,4 +18,32 @@ public static Supplier alwaysSuccess() { return null; }; } + + public static WithCallsCounter withCallsCounterNullRes() { + return withCallsCounter(() -> null); + } + + public static WithCallsCounter withCallsCounter(Supplier sup) { + return new WithCallsCounter(sup); + } + + public static class WithCallsCounter implements Supplier { + + final AtomicInteger calls = new AtomicInteger(); + Supplier delegate; + + public WithCallsCounter(Supplier delegate) { + this.delegate = delegate; + } + + @Override + public Result get() { + this.calls.incrementAndGet(); + return this.delegate.get(); + } + + public int getCalls() { + return this.calls.get(); + } + } }