Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions grpc-circuitbreaker-utils/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
plugins {
`java-library`
jacoco
id("org.hypertrace.publish-plugin")
id("org.hypertrace.jacoco-report-plugin")
}

dependencies {

api(platform("io.grpc:grpc-bom:1.68.3"))
api("io.grpc:grpc-api")
api(project(":grpc-context-utils"))

implementation("org.slf4j:slf4j-api:1.7.36")
implementation("io.github.resilience4j:resilience4j-circuitbreaker:1.7.1")
implementation("com.typesafe:config:1.4.2")

annotationProcessor("org.projectlombok:lombok:1.18.24")
compileOnly("org.projectlombok:lombok:1.18.24")

testImplementation("org.junit.jupiter:junit-jupiter:5.8.2")
Comment thread
pavan-traceable marked this conversation as resolved.
testImplementation("org.mockito:mockito-core:5.8.0")
testImplementation("org.mockito:mockito-junit-jupiter:5.8.0")
}

tasks.test {
useJUnitPlatform()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package org.hypertrace.circuitbreaker.grpcutils;

import com.typesafe.config.Config;
import java.time.Duration;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class CircuitBreakerConfigParser {

Check warning on line 11 in grpc-circuitbreaker-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerConfigParser.java

View check run for this annotation

Codecov / codecov/patch

grpc-circuitbreaker-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerConfigParser.java#L10-L11

Added lines #L10 - L11 were not covered by tests

private static final Set<String> nonThresholdKeys =
Set.of("enabled", "defaultCircuitBreakerKey", "defaultThresholds");

Check warning on line 14 in grpc-circuitbreaker-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerConfigParser.java

View check run for this annotation

Codecov / codecov/patch

grpc-circuitbreaker-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerConfigParser.java#L13-L14

Added lines #L13 - L14 were not covered by tests
Comment thread
aaron-steinfeld marked this conversation as resolved.
Outdated

// Percentage of failures to trigger OPEN state
private static final String FAILURE_RATE_THRESHOLD = "failureRateThreshold";
// Percentage of slow calls to trigger OPEN state
private static final String SLOW_CALL_RATE_THRESHOLD = "slowCallRateThreshold";
// Define what a "slow" call is
private static final String SLOW_CALL_DURATION_THRESHOLD = "slowCallDurationThreshold";
// Number of calls to consider in the sliding window
private static final String SLIDING_WINDOW_SIZE = "slidingWindowSize";
// Time before retrying after OPEN state
private static final String WAIT_DURATION_IN_OPEN_STATE = "waitDurationInOpenState";
// Minimum calls before evaluating failure rate
private static final String MINIMUM_NUMBER_OF_CALLS = "minimumNumberOfCalls";
// Calls allowed in HALF_OPEN state before deciding to
// CLOSE or OPEN again
private static final String PERMITTED_NUMBER_OF_CALLS_IN_HALF_OPEN_STATE =
"permittedNumberOfCallsInHalfOpenState";
private static final String SLIDING_WINDOW_TYPE = "slidingWindowType";
public static final String ENABLED = "enabled";
public static final String DEFAULT_THRESHOLDS = "defaultThresholds";

public static <T> CircuitBreakerConfiguration.CircuitBreakerConfigurationBuilder<T> parseConfig(
Config config) {
CircuitBreakerConfiguration.CircuitBreakerConfigurationBuilder<T> builder =
CircuitBreakerConfiguration.builder();

Check warning on line 39 in grpc-circuitbreaker-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerConfigParser.java

View check run for this annotation

Codecov / codecov/patch

grpc-circuitbreaker-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerConfigParser.java#L39

Added line #L39 was not covered by tests
if (config.hasPath(ENABLED)) {
builder.enabled(config.getBoolean(ENABLED));

Check warning on line 41 in grpc-circuitbreaker-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerConfigParser.java

View check run for this annotation

Codecov / codecov/patch

grpc-circuitbreaker-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerConfigParser.java#L41

Added line #L41 was not covered by tests
}

if (config.hasPath(DEFAULT_THRESHOLDS)) {
builder.defaultThresholds(
buildCircuitBreakerThresholds(config.getConfig(DEFAULT_THRESHOLDS)));

Check warning on line 46 in grpc-circuitbreaker-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerConfigParser.java

View check run for this annotation

Codecov / codecov/patch

grpc-circuitbreaker-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerConfigParser.java#L45-L46

Added lines #L45 - L46 were not covered by tests
} else {
builder.defaultThresholds(buildCircuitBreakerDefaultThresholds());

Check warning on line 48 in grpc-circuitbreaker-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerConfigParser.java

View check run for this annotation

Codecov / codecov/patch

grpc-circuitbreaker-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerConfigParser.java#L48

Added line #L48 was not covered by tests
}

Map<String, CircuitBreakerThresholds> circuitBreakerThresholdsMap =
config.root().keySet().stream()

Check warning on line 52 in grpc-circuitbreaker-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerConfigParser.java

View check run for this annotation

Codecov / codecov/patch

grpc-circuitbreaker-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerConfigParser.java#L51-L52

Added lines #L51 - L52 were not covered by tests
.filter(key -> !nonThresholdKeys.contains(key)) // Filter out non-threshold keys
.collect(
Collectors.toMap(
key -> key, // Circuit breaker key
key -> buildCircuitBreakerThresholds(config.getConfig(key))));
builder.circuitBreakerThresholdsMap(circuitBreakerThresholdsMap);
log.debug("Loaded circuit breaker configs: {}", builder);
return builder;

Check warning on line 60 in grpc-circuitbreaker-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerConfigParser.java

View check run for this annotation

Codecov / codecov/patch

grpc-circuitbreaker-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerConfigParser.java#L54-L60

Added lines #L54 - L60 were not covered by tests
}

private static CircuitBreakerThresholds buildCircuitBreakerThresholds(Config config) {
return CircuitBreakerThresholds.builder()
.failureRateThreshold((float) config.getDouble(FAILURE_RATE_THRESHOLD))
Comment thread
aaron-steinfeld marked this conversation as resolved.
Outdated
.slowCallRateThreshold((float) config.getDouble(SLOW_CALL_RATE_THRESHOLD))
.slowCallDurationThreshold(config.getDuration(SLOW_CALL_DURATION_THRESHOLD))
.slidingWindowType(getSlidingWindowType(config.getString(SLIDING_WINDOW_TYPE)))
.slidingWindowSize(config.getInt(SLIDING_WINDOW_SIZE))
.waitDurationInOpenState(config.getDuration(WAIT_DURATION_IN_OPEN_STATE))
.permittedNumberOfCallsInHalfOpenState(
config.getInt(PERMITTED_NUMBER_OF_CALLS_IN_HALF_OPEN_STATE))
.minimumNumberOfCalls(config.getInt(MINIMUM_NUMBER_OF_CALLS))
.build();

Check warning on line 74 in grpc-circuitbreaker-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerConfigParser.java

View check run for this annotation

Codecov / codecov/patch

grpc-circuitbreaker-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerConfigParser.java#L64-L74

Added lines #L64 - L74 were not covered by tests
}

public static CircuitBreakerThresholds buildCircuitBreakerDefaultThresholds() {
return CircuitBreakerThresholds.builder()
.failureRateThreshold(50f)
.slowCallRateThreshold(50f)
.slowCallDurationThreshold(Duration.ofSeconds(2))
.slidingWindowType(CircuitBreakerThresholds.SlidingWindowType.TIME_BASED)
.slidingWindowSize(60)
.waitDurationInOpenState(Duration.ofSeconds(60))
.permittedNumberOfCallsInHalfOpenState(5)
.minimumNumberOfCalls(10)
.build();

Check warning on line 87 in grpc-circuitbreaker-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerConfigParser.java

View check run for this annotation

Codecov / codecov/patch

grpc-circuitbreaker-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerConfigParser.java#L78-L87

Added lines #L78 - L87 were not covered by tests
}

private static CircuitBreakerThresholds.SlidingWindowType getSlidingWindowType(
String slidingWindowType) {
return CircuitBreakerThresholds.SlidingWindowType.valueOf(slidingWindowType);

Check warning on line 92 in grpc-circuitbreaker-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerConfigParser.java

View check run for this annotation

Codecov / codecov/patch

grpc-circuitbreaker-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerConfigParser.java#L92

Added line #L92 was not covered by tests
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package org.hypertrace.circuitbreaker.grpcutils;

import io.grpc.Status;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.Function;
import lombok.Builder;
import lombok.Value;
import org.hypertrace.core.grpcutils.context.RequestContext;

@Value
@Builder
public class CircuitBreakerConfiguration<T> {
Class<T> requestClass;
BiFunction<RequestContext, T, String> keyFunction;
@Builder.Default boolean enabled = false;

Check warning on line 16 in grpc-circuitbreaker-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerConfiguration.java

View check run for this annotation

Codecov / codecov/patch

grpc-circuitbreaker-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerConfiguration.java#L14-L16

Added lines #L14 - L16 were not covered by tests
// Default value be "global" if not override.
String defaultCircuitBreakerKey = "global";

Check warning on line 18 in grpc-circuitbreaker-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerConfiguration.java

View check run for this annotation

Codecov / codecov/patch

grpc-circuitbreaker-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerConfiguration.java#L18

Added line #L18 was not covered by tests
Comment thread
aaron-steinfeld marked this conversation as resolved.
Outdated
// Standard/default thresholds
CircuitBreakerThresholds defaultThresholds;

Check warning on line 20 in grpc-circuitbreaker-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerConfiguration.java

View check run for this annotation

Codecov / codecov/patch

grpc-circuitbreaker-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerConfiguration.java#L20

Added line #L20 was not covered by tests
// Custom overrides for specific cases (less common)
@Builder.Default Map<String, CircuitBreakerThresholds> circuitBreakerThresholdsMap = Map.of();

Check warning on line 22 in grpc-circuitbreaker-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerConfiguration.java

View check run for this annotation

Codecov / codecov/patch

grpc-circuitbreaker-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerConfiguration.java#L22

Added line #L22 was not covered by tests

// New exception builder logic
@Builder.Default
Function<String, RuntimeException> exceptionBuilder =
reason -> Status.RESOURCE_EXHAUSTED.withDescription(reason).asRuntimeException();

Check warning on line 27 in grpc-circuitbreaker-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerConfiguration.java

View check run for this annotation

Codecov / codecov/patch

grpc-circuitbreaker-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerConfiguration.java#L26-L27

Added lines #L26 - L27 were not covered by tests
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.hypertrace.circuitbreaker.grpcutils;

import io.grpc.CallOptions;
import io.grpc.Channel;
import io.grpc.ClientCall;
import io.grpc.ClientInterceptor;
import io.grpc.MethodDescriptor;

public abstract class CircuitBreakerInterceptor implements ClientInterceptor {
@Override
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
if (!isCircuitBreakerEnabled()) {
return next.newCall(method, callOptions);

Check warning on line 14 in grpc-circuitbreaker-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerInterceptor.java

View check run for this annotation

Codecov / codecov/patch

grpc-circuitbreaker-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerInterceptor.java#L14

Added line #L14 was not covered by tests
}
return createInterceptedCall(method, callOptions, next);

Check warning on line 16 in grpc-circuitbreaker-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerInterceptor.java

View check run for this annotation

Codecov / codecov/patch

grpc-circuitbreaker-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerInterceptor.java#L16

Added line #L16 was not covered by tests
}

protected abstract boolean isCircuitBreakerEnabled();

protected abstract <ReqT, RespT> ClientCall<ReqT, RespT> createInterceptedCall(
MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.hypertrace.circuitbreaker.grpcutils;

import java.time.Duration;
import lombok.Builder;
import lombok.Value;

@Value
@Builder
public class CircuitBreakerThresholds {
// Percentage of failures to trigger OPEN state
float failureRateThreshold;
// Percentage of slow calls to trigger OPEN state
float slowCallRateThreshold;
// Define what a "slow" call is
Duration slowCallDurationThreshold;
// Number of calls to consider in the sliding window
SlidingWindowType slidingWindowType;
int slidingWindowSize;
// Time before retrying after OPEN state
Duration waitDurationInOpenState;
// Minimum calls before evaluating failure rate
int minimumNumberOfCalls;
// Calls allowed in HALF_OPEN state before deciding to
// CLOSE or OPEN again
int permittedNumberOfCallsInHalfOpenState;

public enum SlidingWindowType {
COUNT_BASED,
TIME_BASED
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package org.hypertrace.circuitbreaker.grpcutils.resilience;

import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import java.util.Map;
import java.util.stream.Collectors;
import org.hypertrace.circuitbreaker.grpcutils.CircuitBreakerThresholds;

/** Utility class to parse CircuitBreakerConfiguration to Resilience4j CircuitBreakerConfig */
class ResilienceCircuitBreakerConfigConverter {

Check warning on line 9 in grpc-circuitbreaker-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/resilience/ResilienceCircuitBreakerConfigConverter.java

View check run for this annotation

Codecov / codecov/patch

grpc-circuitbreaker-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/resilience/ResilienceCircuitBreakerConfigConverter.java#L9

Added line #L9 was not covered by tests

public static Map<String, CircuitBreakerConfig> getCircuitBreakerConfigs(
Map<String, CircuitBreakerThresholds> configurationMap) {
return configurationMap.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, entry -> convertConfig(entry.getValue())));
}

static CircuitBreakerConfig convertConfig(CircuitBreakerThresholds configuration) {
return CircuitBreakerConfig.custom()
.failureRateThreshold(configuration.getFailureRateThreshold())
.slowCallRateThreshold(configuration.getSlowCallRateThreshold())
.slowCallDurationThreshold(configuration.getSlowCallDurationThreshold())
.slidingWindowType(getSlidingWindowType(configuration.getSlidingWindowType()))
.slidingWindowSize(configuration.getSlidingWindowSize())
.waitDurationInOpenState(configuration.getWaitDurationInOpenState())
.permittedNumberOfCallsInHalfOpenState(
configuration.getPermittedNumberOfCallsInHalfOpenState())
.minimumNumberOfCalls(configuration.getMinimumNumberOfCalls())
.build();
}

private static CircuitBreakerConfig.SlidingWindowType getSlidingWindowType(
CircuitBreakerThresholds.SlidingWindowType slidingWindowType) {
switch (slidingWindowType) {
case COUNT_BASED:
return CircuitBreakerConfig.SlidingWindowType.COUNT_BASED;

Check warning on line 35 in grpc-circuitbreaker-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/resilience/ResilienceCircuitBreakerConfigConverter.java

View check run for this annotation

Codecov / codecov/patch

grpc-circuitbreaker-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/resilience/ResilienceCircuitBreakerConfigConverter.java#L35

Added line #L35 was not covered by tests
case TIME_BASED:
default:
return CircuitBreakerConfig.SlidingWindowType.TIME_BASED;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package org.hypertrace.circuitbreaker.grpcutils.resilience;

import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
import java.time.Clock;
import java.util.Map;
import org.hypertrace.circuitbreaker.grpcutils.CircuitBreakerConfiguration;

public class ResilienceCircuitBreakerFactory {

Check warning on line 9 in grpc-circuitbreaker-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/resilience/ResilienceCircuitBreakerFactory.java

View check run for this annotation

Codecov / codecov/patch

grpc-circuitbreaker-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/resilience/ResilienceCircuitBreakerFactory.java#L9

Added line #L9 was not covered by tests
public static ResilienceCircuitBreakerInterceptor getResilienceCircuitBreakerInterceptor(
CircuitBreakerConfiguration<?> circuitBreakerConfiguration, Clock clock) {
Map<String, CircuitBreakerConfig> resilienceCircuitBreakerConfigMap =
ResilienceCircuitBreakerConfigConverter.getCircuitBreakerConfigs(
circuitBreakerConfiguration.getCircuitBreakerThresholdsMap());
CircuitBreakerRegistry resilicenceCircuitBreakerRegistry =

Check warning on line 15 in grpc-circuitbreaker-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/resilience/ResilienceCircuitBreakerFactory.java

View check run for this annotation

Codecov / codecov/patch

grpc-circuitbreaker-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/resilience/ResilienceCircuitBreakerFactory.java#L12-L15

Added lines #L12 - L15 were not covered by tests
new ResilienceCircuitBreakerRegistryProvider(
circuitBreakerConfiguration.getDefaultThresholds())
Comment thread
aaron-steinfeld marked this conversation as resolved.
.getCircuitBreakerRegistry();
ResilienceCircuitBreakerProvider resilienceCircuitBreakerProvider =

Check warning on line 19 in grpc-circuitbreaker-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/resilience/ResilienceCircuitBreakerFactory.java

View check run for this annotation

Codecov / codecov/patch

grpc-circuitbreaker-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/resilience/ResilienceCircuitBreakerFactory.java#L17-L19

Added lines #L17 - L19 were not covered by tests
new ResilienceCircuitBreakerProvider(
resilicenceCircuitBreakerRegistry, resilienceCircuitBreakerConfigMap);
return new ResilienceCircuitBreakerInterceptor(

Check warning on line 22 in grpc-circuitbreaker-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/resilience/ResilienceCircuitBreakerFactory.java

View check run for this annotation

Codecov / codecov/patch

grpc-circuitbreaker-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/resilience/ResilienceCircuitBreakerFactory.java#L22

Added line #L22 was not covered by tests
circuitBreakerConfiguration, clock, resilienceCircuitBreakerProvider);
}
}
Loading
Loading