Skip to content

Instantly share code, notes, and snippets.

@mike-seger
Last active June 28, 2024 17:31
Show Gist options
  • Save mike-seger/e77fc5caba21c986dec2a2941a2d0063 to your computer and use it in GitHub Desktop.
Save mike-seger/e77fc5caba21c986dec2a2941a2d0063 to your computer and use it in GitHub Desktop.
Retry Executor

You:

when i have the yaml config values, can't i save on java

ChatGPT:

Yes, you can leverage Spring Boot's auto-configuration capabilities with resilience4j to avoid manually creating beans for retry and circuit breaker configurations. Spring Boot automatically configures resilience4j beans based on the properties defined in your application.yml file. Here's how you can streamline your configuration:

  1. Add resilience4j Dependencies: Ensure you have the necessary dependencies in your pom.xml:
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-spring-boot2</artifactId>
    <version>1.7.1</version>
</dependency>
  1. Configure resilience4j in application.yml: Define the retry and circuit breaker properties directly in your application.yml:
resilience4j:
  retry:
    instances:
      myRetry:
        max-attempts: 3
        wait-duration: 1s  # wait duration in seconds
  circuitbreaker:
    instances:
      myCircuitBreaker:
        registerHealthIndicator: true
        sliding-window-size: 10
        minimum-number-of-calls: 5
        failure-rate-threshold: 50
        wait-duration-in-open-state: 5s  # wait duration in seconds
  1. Use the retry and circuit breaker mechanisms in your filter: Modify your filter to use the auto-configured retry and circuit breaker beans:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.retry.Retry;
import io.github.resilience4j.spring.ratelimiter.configure.RateLimiterConfigurationProperties;
import reactor.core.publisher.Mono;

@Component
public class MyGatewayFilter extends AbstractGatewayFilterFactory<MyGatewayFilter.Config> {

    private final WebClient webClient;
    private final Retry retry;
    private final CircuitBreaker circuitBreaker;
    
    @Autowired
    public MyGatewayFilter(WebClient.Builder webClientBuilder, RetryRegistry retryRegistry, CircuitBreakerRegistry circuitBreakerRegistry) {
        this.webClient = webClientBuilder.build();
        this.retry = retryRegistry.retry("myRetry");
        this.circuitBreaker = circuitBreakerRegistry.circuitBreaker("myCircuitBreaker");
    }

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            return chain.filter(exchange).then(Mono.fromRunnable(() -> {
                sendDetailsAsync(exchange.getRequest(), exchange.getResponse());
            }));
        };
    }

    private void sendDetailsAsync(ServerHttpRequest request, ServerHttpResponse response) {
        // Your request/response details
        String details = ...;

        Runnable decoratedRunnable = CircuitBreaker.decorateRunnable(circuitBreaker, 
            Retry.decorateRunnable(retry, () -> {
                webClient.post()
                        .uri("http://other-service/store")
                        .bodyValue(details)
                        .retrieve()
                        .bodyToMono(Void.class)
                        .block();
            })
        );

        try {
            decoratedRunnable.run();
        } catch (Exception e) {
            // Handle the exception (e.g., log it)
        }
    }

    public static class Config {
        // Configuration properties for your filter
    }
}

In this approach, you leverage the auto-configuration provided by Spring Boot and resilience4j to avoid manually defining beans in your configuration class. The properties defined in application.yml are automatically picked up by Spring Boot, simplifying your configuration and making it more maintainable.

Gradle Build Configuration

First, create a build.gradle file with the necessary dependencies:

plugins {
    id 'org.springframework.boot' version '2.6.3'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
    implementation 'io.github.resilience4j:resilience4j-spring-boot2:1.7.1'
    
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.mockito:mockito-core'
    testImplementation 'org.mockito:mockito-junit-jupiter'
    testImplementation 'org.spockframework:spock-core:2.0-groovy-3.0'
}

test {
    useJUnitPlatform()
}

Groovy Test

Next, create a Groovy test class for MyGatewayFilter.Create a directory structure: src/test/groovy/com/example and inside it create MyGatewayFilterTest.groovy:

package com.example

import io.github.resilience4j.circuitbreaker.CircuitBreaker
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry
import io.github.resilience4j.retry.Retry
import io.github.resilience4j.retry.RetryConfig
import io.github.resilience4j.retry.RetryRegistry
import org.springframework.http.HttpHeaders
import org.springframework.http.MediaType
import org.springframework.mock.http.server.reactive.MockServerHttpRequest
import org.springframework.mock.web.server.MockServerWebExchange
import org.springframework.web.reactive.function.client.WebClient
import reactor.core.publisher.Mono
import spock.lang.Specification
import java.time.Duration

class MyGatewayFilterTest extends Specification {

    WebClient webClient = Mock()

    RetryRegistry retryRegistry
    CircuitBreakerRegistry circuitBreakerRegistry

    MyGatewayFilter myGatewayFilter

    def setup() {
        // Initialize real Retry and CircuitBreaker instances
        RetryConfig retryConfig = RetryConfig.custom()
                .maxAttempts(3)
                .waitDuration(Duration.ofSeconds(1))
                .build()
        retryRegistry = RetryRegistry.of(retryConfig)

        CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
                .failureRateThreshold(50)
                .waitDurationInOpenState(Duration.ofSeconds(5))
                .slidingWindowSize(10)
                .minimumNumberOfCalls(5)
                .build()
        circuitBreakerRegistry = CircuitBreakerRegistry.of(circuitBreakerConfig)

        // Create the MyGatewayFilter instance with the mocked WebClient, RetryRegistry, and CircuitBreakerRegistry
        myGatewayFilter = new MyGatewayFilter(webClient, retryRegistry, circuitBreakerRegistry)
    }

    def "test sendDetailsAsync"() {
        given:
        // Create mocks for each step in the WebClient chain
        WebClient.RequestBodyUriSpec requestBodyUriSpec = Mock(WebClient.RequestBodyUriSpec)
        WebClient.RequestHeadersUriSpec requestHeadersUriSpec = Mock(WebClient.RequestHeadersUriSpec)
        WebClient.RequestHeadersSpec requestHeadersSpec = Mock(WebClient.RequestHeadersSpec)
        WebClient.ResponseSpec responseSpec = Mock(WebClient.ResponseSpec)

        // Mock the request and response exchange
        def mockRequest = MockServerHttpRequest.post("/")
                .header(HttpHeaders.AUTHORIZATION, "Bearer JWT_TOKEN")
                .header("header1", "hv1")
                .contentType(MediaType.APPLICATION_JSON)
                .body("{}")
        def exchange = MockServerWebExchange.from(mockRequest)

        // Mock the WebClient method chain
        webClient.post() >> requestBodyUriSpec

        // Mock all uri signatures
        requestBodyUriSpec.metaClass.invokeMethod = { String name, args ->
            if (name == 'uri') {
                return requestHeadersUriSpec
            }
            delegate.metaClass.getMetaMethod(name, args).invoke(delegate, args)
        }

        requestHeadersUriSpec.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) >> requestHeadersUriSpec
        requestHeadersUriSpec.bodyValue(_) >> requestHeadersSpec
        requestHeadersSpec.retrieve() >> responseSpec
        responseSpec.bodyToMono(Void.class) >> Mono.empty()

        when:
        // Call the method under test
        myGatewayFilter.sendDetailsAsync(exchange, exchange.getRequest(), exchange.getResponse())

        then:
        // Verify the method calls on the WebClient chain
        1 * webClient.post()
        1 * requestBodyUriSpec.uri(_, _)
        1 * requestHeadersUriSpec.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
        1 * requestHeadersUriSpec.bodyValue(_)
        1 * requestHeadersSpec.retrieve()
        1 * responseSpec.bodyToMono(Void.class)
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment