Skip to content

Add registry decorators for deadlines and service-keyed circuit breakers - #94

Draft
suresh-prakash wants to merge 9 commits into
mainfrom
grpc-registry-decorators
Draft

suresh-prakash wants to merge 9 commits into
mainfrom
grpc-registry-decorators

Conversation

@suresh-prakash

Copy link
Copy Markdown

Summary

  • Add InterceptingChannelRegistry so an already-built GrpcChannelRegistry can carry one client interceptor, and TimeoutChannelRegistry so grpc.client.timeout applies a default deadline when a call has none.
  • Add a service-keyed circuit breaker: CircuitBreakerInterceptorFactory.forService and CircuitBreakerChannelRegistry.wrap. The breaker name is the protobuf service name. failureStatusCodes defaults to DEADLINE_EXCEEDED, UNAVAILABLE, and RESOURCE_EXHAUSTED.
  • Leave the existing request-keyed interceptor unchanged. Callers compose the timeout and circuit-breaker decorators themselves.

Test plan

  • ./gradlew -Pversion=0.13.23 :grpc-client-utils:test :grpc-circuitbreaker-utils:test
  • Confirm an absent or disabled config returns the original registry
  • Confirm an enabled service opens on the configured failure statuses and rejects with RESOURCE_EXHAUSTED before the downstream call
  • Confirm a quoted dotted service name parses, and an unknown status name fails startup

Made with Cursor

suresh-prakash and others added 7 commits October 23, 2026 15:26
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…gistry.

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Test Results

132 tests  +33   132 ✅ +33   23s ⏱️ ±0s
 22 suites + 5     0 💤 ± 0 
 22 files   + 5     0 ❌ ± 0 

Results for commit 68ee8a5. ± Comparison against base commit 48511e9.

♻️ This comment has been updated with latest results.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gitignore "docs/superpowers".


private CircuitBreakerChannelRegistry() {}

public static GrpcChannelRegistry wrap(GrpcChannelRegistry registry, Config config) {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer to use final wherever feasible (to avoid errors like accidentally assigning a local variable instead of the field). Let's use that throughout this PR.


static final String CIRCUIT_BREAKER_CONFIG = "circuit.breaker.config";

private CircuitBreakerChannelRegistry() {}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Can also use lombok?

for (String serviceName : config.root().keySet()) {
if (CircuitBreakerConfigParser.ENABLED.equals(serviceName)
|| CircuitBreakerConfigParser.DEFAULT_THRESHOLDS.equals(serviceName)) {
continue;

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be helpful to have "debug" logs at these early exist points so it can come handy for debugging.

.map(Status.Code::name)
.collect(Collectors.joining(", "));
return ConfigFactory.parseString(
"failureRateThreshold = "

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be better if these configs were built more first-class instead of string concatenation. It's okay if that's not possible.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe, once we have the first-class model, the defaults can be specified directly in the model.

Objects.requireNonNull(registry), Objects.requireNonNull(interceptor));
}

@Delegate(excludes = ForAddress.class)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need the excludes specifically? Or, can lombok automatically skip methods that are explicitly overridden?

}

private static final class InterceptedManagedChannel extends ManagedChannel {
@Delegate(excludes = InterceptedCall.class)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.


Config timeoutConfig = config.getConfig(TIMEOUT_PATH);
if (!timeoutConfig.hasPath(ENABLED_KEY) || !timeoutConfig.getBoolean(ENABLED_KEY)) {
return registry;

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a debug log at all early exist points, especially since the would be invoked just once per restart.

return InterceptingChannelRegistry.wrap(registry, new DefaultTimeoutClientInterceptor(timeout));
}

private TimeoutChannelRegistry() {}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Can be replaced with Lombok?

Comment thread .gitignore

.idea

.superpowers/

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docs/superpowers can also be ignored.

Parse service breakers into CircuitBreakerThresholds instead of rebuilding defaults as HOCON, and log when a decorator is skipped or a call is rejected. Lombok still excludes forAddress because that method is inherited, not overridden.

Co-authored-by: Cursor <cursoragent@cursor.com>

static final String CIRCUIT_BREAKER_CONFIG = CircuitBreakerConfigParser.CIRCUIT_BREAKER_CONFIG;

public static GrpcChannelRegistry wrap(final GrpcChannelRegistry registry, final Config config) {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not take in the first-class object here instead of typesafe?

return new ServiceCircuitBreakerInterceptor(Map.of(), Map.of());
}

// Resilience4j records onError as a success when recordException returns false.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment seems off place still.

Callers can install the breaker from CircuitBreakerThresholds without passing Typesafe Config. Parameters in the new code are final, and a breaker logs when it changes state.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant