Skip to content

Expose slowCallDurationThreshold and slowCallRateThreshold in CircuitBreakerConfiguration #1148

@dira-AUR

Description

@dira-AUR

Describe the Problem

ResilienceConfiguration.CircuitBreakerConfiguration currently exposes only a subset of the properties available in Resilience4j's CircuitBreakerConfig. Notably missing are:

  • slowCallDurationThreshold (Resilience4j default: 60 seconds)
  • slowCallRateThreshold (Resilience4j default: 100%)

This is problematic for applications that perform long-running operations (e.g., large SAP OData batch imports that routinely take 2–10 minutes per call). With the default 60-second slow-call threshold, these calls are classified as "slow", and once the slow-call rate exceeds the threshold, the circuit breaker transitions to OPEN — even though all calls succeed.

Since CircuitBreakerConfig is immutable after construction and DefaultCircuitBreakerProvider does not set these properties from the SDK configuration, there is no clean way to adjust the slow-call behavior without resorting to reflection hacks.

Propose a Solution

Add two optional properties to CircuitBreakerConfiguration:

@NoArgsConstructor(staticName = "of")
@Accessors(fluent = true)
@EqualsAndHashCode
@Getter
@Setter
public static final class CircuitBreakerConfiguration {
    // ...existing fields...

    @Nonnull
    private Duration slowCallDurationThreshold = Duration.ofSeconds(60); // Resilience4j default

    private float slowCallRateThreshold = 100f; // Resilience4j default
}

Then wire them through in DefaultCircuitBreakerProvider.getCircuitBreaker():

CircuitBreakerConfig.custom()
    .failureRateThreshold(config.failureRateThreshold())
    .waitDurationInOpenState(config.waitDuration())
    .slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED)
    .slidingWindowSize(config.closedBufferSize())
    .minimumNumberOfCalls(config.closedBufferSize())
    .permittedNumberOfCallsInHalfOpenState(config.halfOpenBufferSize())
    .slowCallDurationThreshold(config.slowCallDurationThreshold())     // NEW
    .slowCallRateThreshold(config.slowCallRateThreshold())             // NEW
    .build();

This is fully backwards-compatible since the proposed defaults match Resilience4j's existing defaults.

Describe Alternatives

The only current workaround is using reflection to modify the CircuitBreakerConfig after construction:

// Step 1: Access DefaultCircuitBreakerProvider via reflection on Resilience4jDecorationStrategy.decorators (protected field)
Field decoratorsField = Resilience4jDecorationStrategy.class.getDeclaredField("decorators");
decoratorsField.setAccessible(true);
List<GenericDecorator> decorators = (List<GenericDecorator>) decoratorsField.get(strategy);
DefaultCircuitBreakerProvider cbProvider = decorators.stream()
    .filter(DefaultCircuitBreakerProvider.class::isInstance)
    .map(DefaultCircuitBreakerProvider.class::cast)
    .findFirst().orElseThrow();

// Step 2: Obtain CircuitBreaker instance and mutate the immutable config via reflection
CircuitBreaker cb = cbProvider.getCircuitBreaker(myResilienceConfiguration);
Field field = cb.getCircuitBreakerConfig().getClass().getDeclaredField("slowCallDurationThreshold");
field.setAccessible(true);
field.set(cb.getCircuitBreakerConfig(), Duration.ofMinutes(5));

This approach is fragile because:

  1. It accesses protected internals of Resilience4jDecorationStrategy
  2. It mutates an immutable Resilience4j config object via reflection
  3. It can break silently on any minor version update of either the SAP Cloud SDK or Resilience4j

Affected Development Phase

Production

Impact

Inconvenience

Timeline

No immediate deadline — this is a long-standing workaround we'd like to eliminate. Happy to contribute a PR if the team agrees with the approach.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions