-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add grpc circuit breaker utility using interceptors #68
base: main
Are you sure you want to change the base?
Conversation
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #68 +/- ##
=============================================
- Coverage 71.80% 60.52% -11.28%
- Complexity 173 193 +20
=============================================
Files 26 35 +9
Lines 532 755 +223
Branches 26 47 +21
=============================================
+ Hits 382 457 +75
- Misses 126 268 +142
- Partials 24 30 +6
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
...tils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerConfigProvider.java
Outdated
Show resolved
Hide resolved
...tils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerConfigProvider.java
Outdated
Show resolved
Hide resolved
...reaker-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerModule.java
Outdated
Show resolved
Hide resolved
...reaker-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerModule.java
Outdated
Show resolved
Hide resolved
...utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerEventListener.java
Outdated
Show resolved
Hide resolved
...r-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerInterceptor.java
Outdated
Show resolved
Hide resolved
...r-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerInterceptor.java
Outdated
Show resolved
Hide resolved
...r-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerInterceptor.java
Outdated
Show resolved
Hide resolved
...r-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerInterceptor.java
Outdated
Show resolved
Hide resolved
...tils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerConfigProvider.java
Outdated
Show resolved
Hide resolved
...tils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerConfigProvider.java
Outdated
Show resolved
Hide resolved
...utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerConfiguration.java
Outdated
Show resolved
Hide resolved
...org/hypertrace/circuitbreaker/grpcutils/resilience/ResilienceCircuitBreakerConfigParser.java
Outdated
Show resolved
Hide resolved
.../org/hypertrace/circuitbreaker/grpcutils/resilience/ResilienceCircuitBreakerInterceptor.java
Outdated
Show resolved
Hide resolved
.../org/hypertrace/circuitbreaker/grpcutils/resilience/ResilienceCircuitBreakerInterceptor.java
Outdated
Show resolved
Hide resolved
...utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerConfiguration.java
Outdated
Show resolved
Hide resolved
...-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerConfigParser.java
Outdated
Show resolved
Hide resolved
...-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerConfigParser.java
Outdated
Show resolved
Hide resolved
...org/hypertrace/circuitbreaker/grpcutils/resilience/ResilienceCircuitBreakerConfigParser.java
Outdated
Show resolved
Hide resolved
.../org/hypertrace/circuitbreaker/grpcutils/resilience/ResilienceCircuitBreakerInterceptor.java
Outdated
Show resolved
Hide resolved
.../org/hypertrace/circuitbreaker/grpcutils/resilience/ResilienceCircuitBreakerInterceptor.java
Outdated
Show resolved
Hide resolved
.../org/hypertrace/circuitbreaker/grpcutils/resilience/ResilienceCircuitBreakerInterceptor.java
Outdated
Show resolved
Hide resolved
.../org/hypertrace/circuitbreaker/grpcutils/resilience/ResilienceCircuitBreakerInterceptor.java
Outdated
Show resolved
Hide resolved
...hypertrace/circuitbreaker/grpcutils/resilience/ResilienceCircuitBreakerRegistryProvider.java
Outdated
Show resolved
Hide resolved
...-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerConfigParser.java
Outdated
Show resolved
Hide resolved
.../org/hypertrace/circuitbreaker/grpcutils/resilience/ResilienceCircuitBreakerInterceptor.java
Outdated
Show resolved
Hide resolved
...ava/org/hypertrace/circuitbreaker/grpcutils/resilience/ResilienceCircuitBreakerProvider.java
Outdated
Show resolved
Hide resolved
...hypertrace/circuitbreaker/grpcutils/resilience/ResilienceCircuitBreakerRegistryProvider.java
Outdated
Show resolved
Hide resolved
...hypertrace/circuitbreaker/grpcutils/resilience/ResilienceCircuitBreakerRegistryProvider.java
Outdated
Show resolved
Hide resolved
.../org/hypertrace/circuitbreaker/grpcutils/resilience/ResilienceCircuitBreakerInterceptor.java
Show resolved
Hide resolved
.../org/hypertrace/circuitbreaker/grpcutils/resilience/ResilienceCircuitBreakerInterceptor.java
Outdated
Show resolved
Hide resolved
...-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerConfigParser.java
Outdated
Show resolved
Hide resolved
...-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerConfigParser.java
Outdated
Show resolved
Hide resolved
...-utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerConfigParser.java
Outdated
Show resolved
Hide resolved
...utils/src/main/java/org/hypertrace/circuitbreaker/grpcutils/CircuitBreakerConfiguration.java
Outdated
Show resolved
Hide resolved
...ava/org/hypertrace/circuitbreaker/grpcutils/resilience/ResilienceCircuitBreakerProvider.java
Outdated
Show resolved
Hide resolved
.../org/hypertrace/circuitbreaker/grpcutils/resilience/ResilienceCircuitBreakerInterceptor.java
Show resolved
Hide resolved
.../org/hypertrace/circuitbreaker/grpcutils/resilience/ResilienceCircuitBreakerInterceptor.java
Show resolved
Hide resolved
...java/org/hypertrace/circuitbreaker/grpcutils/resilience/ResilienceCircuitBreakerFactory.java
Outdated
Show resolved
Hide resolved
...java/org/hypertrace/circuitbreaker/grpcutils/resilience/ResilienceCircuitBreakerFactory.java
Outdated
Show resolved
Hide resolved
circuitBreakerKey = config.getKeyFunction().apply(RequestContext.CURRENT.get(), message); | ||
circuitBreaker = resilienceCircuitBreakerProvider.getCircuitBreaker(circuitBreakerKey); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We discussed supporting the key function returning null too. And if a default exists,I think the logic should look something like
key = null
if (keyFunction not null) {
key = keyFunction(context, message)
}
circuitBreaker = key not null ? getCircuitBreaker(key) : getDefaultCircuitBreaker();
if (circuitBreaker is null) {
// short circuit
}
// continue with existing logic
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are setting default always why will circuitBreaker become null ?
builder.defaultThresholds(
config.hasPath(DEFAULT_THRESHOLDS) ?
buildCircuitBreakerThresholds(config.getConfig(DEFAULT_THRESHOLDS)) :
buildCircuitBreakerDefaultThresholds());
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If someone defines keyFunction, why key should be null ? To handle what scenarios ? Can you please list all scenarios that we need to handle, i will check accordingly
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are setting default always why will circuitBreaker become null ?
We're requiring config for it, but it can be disabled and would not have an associated circuit breaker.
If someone defines keyFunction, why key should be null ?
Imagine an API like "doAction(action)" where we want to circuit break some expensive action like "email" by tenant ID. A key function might look like action == "email" ? getTenantId() : null
. That would allow other actions to continue on without a circuit breaker, but emails to be circuit broken by tenant ID. Alternatively, we could add a separate optional filter method with a contract like (context, request) => boolean. Do you think that would be clearer?
I realize looking back my logic was a bit off here too.
Can you please list all scenarios that we need to handle, i will check accordingly
Basically any permutation - should be able to provide a key function (and filter/predicate, if we want to separate that - the class match is a bit of an implicit predicate but does not encompass all scenarios) which allows us to:
- route some requests to custom circuit breaker that's declared in config
- route others to a default circuit breaker that's declared in config
- have others skip the circuit breaker entirely
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hopefully i have fixed all different scenarios now. Please check
.../org/hypertrace/circuitbreaker/grpcutils/resilience/ResilienceCircuitBreakerInterceptor.java
Outdated
Show resolved
Hide resolved
|
||
if (!config.hasPath(DEFAULT_THRESHOLDS)) { | ||
builder.defaultThresholds(buildCircuitBreakerDefaultThresholds()); | ||
circuitBreakerThresholdsMap.put(DEFAULT_THRESHOLDS, buildCircuitBreakerDefaultThresholds()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are we writing this into the map? The the default thresholds don't need to be accessed by key - that should be what builder.defaultThresholds
is for. Also note if DEFAULT_THRESHOLDS
path does exist it would make it into the map, but it would not be set to builder.defaultThresholds
. I would expect this logic to look more like
builder.defaultThresholds(
config.hasPath(DEFAULT_THRESHOLDS) ?
buildCircuitBreakerThresholds(config.getConfig(DEFAULT_THRESHOLDS)) :
buildCircuitBreakerDefaultThresholds());
The config key then is not a concern of the specific impl. If you really want to use a magic key, then defaultThresholds
would be removed, we wouldn't want both since it's two ways of conveying the same thing - one just decouples concerns.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is done mainly to handle where circuit breaker is disabled by default but enabled for only tenant, in CircuitBreakerProvider we check enabled or based on CircuitConfigMap only, so thats why adding to map.
circuitBreakerConfiguration.getCircuitBreakerThresholdsMap()); | ||
CircuitBreakerRegistry resilicenceCircuitBreakerRegistry = | ||
new ResilienceCircuitBreakerRegistryProvider( | ||
circuitBreakerConfiguration.getDefaultThresholds()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So these default thresholds go into the registry directly, but the others are built as-needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need default config for CircuitBreakerRegistry, we will override this at circuitBreaker level
this.disabledKeys = disabledKeys; | ||
} | ||
|
||
public Optional<CircuitBreaker> getCircuitBreaker(String circuitBreakerKey) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So if this is some random key (that would map to the default) how do we handle there being no default here? It looks like the disabled key is only being respected for the overrides
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed it.
} | ||
|
||
public Optional<CircuitBreaker> getDefaultCircuitBreaker() { | ||
return getCircuitBreaker(CircuitBreakerConfigParser.DEFAULT_THRESHOLDS); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I see one part of the confusion here. You're using this special key for the "shared" circuit breaker that's used when there is no key. We can use a special key for that, but it should be local to this provider - it's indepdendent of the config.
I think part of the confusion is that the term default is being overloaded a bit here. There's two separate things:
- the config to use if no key matches (which can be disabled - if no specific key config, don't circuit break)
- the shared circuit breaker to use if there is no key (which would be an instance of the former case - the config to use if no key matches).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for your patience, i hope i have addressed all the cases now. Due to multiple scenarios, i feel implementation is slightly complicated, not sure if we use these different scenarios.
|
||
Map<String, CircuitBreakerThresholds> circuitBreakerThresholdsMap = | ||
config.root().keySet().stream() | ||
.filter(key -> !NON_THRESHOLD_KEYS.contains(key)) // Filter out non-threshold keys |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because NON_THRESHOLD_KEYS is not excluding the default, the default will be put into the map like every other config in addition to being set as defaultThresholds
.
Can avoid putting it in the map any only use it through defaultThresholds
? Because also note that if I choose to build the config in code (rather than parsing it from config) I would only set it in the one place.
this.circuitBreakerRegistry = circuitBreakerRegistry; | ||
this.circuitBreakerConfigMap = circuitBreakerConfigMap; | ||
this.disabledKeys = disabledKeys; | ||
this.defaultEnabled = !disabledKeys.contains(CircuitBreakerConfigParser.DEFAULT_THRESHOLDS); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This works because the defaults are still being put in the map along with every other config - but we can't rely on that as I pointed out in an earlier comment (it wouldn't hold if I built my config rather than parsed it, and it's that same abstraction leak we've been trying to avoid).
if (!defaultEnabled) { | ||
return Optional.empty(); | ||
} | ||
return Optional.of( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit - can this just be return getCircuitBreaker(SHARED_KEY);
?
() -> | ||
defaultEnabled | ||
? circuitBreakerRegistry.circuitBreaker(circuitBreakerKey) | ||
: null); // Return null if default is disabled |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit - given that most of this PR (including this same function) is working in optionals, please stick to optionals over null unless the third party code doesn't give us an option. This function can return an optional to convey it like
private Optional<CircuitBreaker> buildNewCircuitBreaker(String circuitBreakerKey, boolean defaultEnabled) {
return Optional.ofNullable(circuitBreakerConfigMap.get(circuitBreakerKey))
.map(config -> circuitBreakerRegistry.circuitBreaker(circuitBreakerKey, config))
.or(() -> defaultEnabled ? Optional.of(circuitBreakerRegistry.circuitBreaker(circuitBreakerKey)) : Optional.empty());
getCircuitBreakerFromConfigMap(circuitBreakerKey, defaultEnabled); | ||
// If no circuit breaker is created return empty | ||
if (circuitBreaker == null) { | ||
return null; // Ensures cache does not store null entries |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not store nulls? No need to recompute the disabled status for the same key. Could make the map value an optional to avoid the null behavior of computeIfAbsent.
private static final String SHARED_KEY = "SHARED_KEY"; | ||
private final CircuitBreakerRegistry circuitBreakerRegistry; | ||
private final Map<String, CircuitBreakerConfig> circuitBreakerConfigMap; | ||
private final Map<String, CircuitBreaker> circuitBreakerCache = new ConcurrentHashMap<>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are there any concerns about this growing too large? If we were to use a bad key (e.g. request ID) as our circuit breaker it would grow unbounded. Consider if we should use a true simple cache here like below to prevent a bad config from crashing a service.
CacheBuilder.newBuilder()
.expireAfterAccess(Duration.ofHours(1)) // or from config
.maximumSize(10_000)
.build(CacheLoader.from(this::buildCircuitBreaker));
Description
Added grpc utility to support circuit breaker for slow and failure calls
Testing
Tested changes on sandbox environment using this utility.