Skip to content

Commit

Permalink
Canary Rate Strategy Implementation (#570)
Browse files Browse the repository at this point in the history
* creating empty strategy

* creating tests to validate reslience4j behavior

* more tests

* new test

* new tests and features

* notes

* refactoring and testing

* creating healthchecker

* refactoring

* implementing

* adjusting test

* clean code

* refactoring

* release notes

* [Gradle Release Plugin] - new version commit:  '3.30.1-snapshot'.
  • Loading branch information
mageddo authored Sep 16, 2024
1 parent 53cc60c commit 7c0303c
Show file tree
Hide file tree
Showing 36 changed files with 1,045 additions and 3 deletions.
3 changes: 3 additions & 0 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
version=3.30.0-snapshot
version=3.30.1-snapshot
3 changes: 3 additions & 0 deletions src/main/java/com/mageddo/concurrent/ThreadsV2.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@ public class ThreadsV2 {
public static boolean isInterrupted() {
return Thread.currentThread().isInterrupted();
}
public static boolean isNotInterrupted() {
return !isInterrupted();
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,9 @@
public enum CircuitStatus {
OPEN,
CLOSED,
HALF_OPEN
HALF_OPEN;

public static boolean isOpen(CircuitStatus status) {
return OPEN == status;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Result> sup) {
final var circuitBreaker = this.findCircuitBreaker(remoteAddress);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ public interface CircuitBreakerDelegate {
Result execute(Supplier<Result> sup);

CircuitStatus findStatus();

void transitionToHalfOpenState();
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,9 @@ public Result execute(Supplier<Result> sup) {
public CircuitStatus findStatus() {
return null;
}

@Override
public void transitionToHalfOpenState() {

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,9 @@ public CircuitStatus findStatus() {
CircuitStatusRefresh.refresh(this.circuitBreaker);
return CircuitBreakerStateMapper.fromFailSafeCircuitBreaker(this.circuitBreaker);
}

@Override
public void transitionToHalfOpenState() {
this.circuitBreaker.halfOpen();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.mageddo.dnsproxyserver.solver.remote.circuitbreaker.application;

public interface HealthChecker {
boolean isHealthy();
}
Original file line number Diff line number Diff line change
@@ -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<Result> 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();
}
}
Original file line number Diff line number Diff line change
@@ -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<Result> 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;
}

}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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<Result> 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<Result> sup) {
this.lastCall = sup;
return this.delegate.execute(sup);
}

@Override
public CircuitStatus findStatus() {
return this.delegate.findStatus();
}

@Override
public void transitionToHalfOpenState() {
this.delegate.transitionToHalfOpenState();
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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());
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading

0 comments on commit 7c0303c

Please sign in to comment.