## resilience4j This module provides support to use [Resilience4j](https://resilience4j.readme.io/) with `julian-http-client`. Resilience4j is a fault tolerance library. ## Install ### Maven ```xml com.github.ljtfreitas.julian-http-client julian-http-client-http-client-resilience4j ${julian-http-client-version} ``` ### Gradle ```kotlin dependencies { implementation("com.github.ljtfreitas.julian-http-client:julian-http-client-resilience4j:$julianHttpClientVersion") } ``` ## Usage `julian-http-client` provides support for several features from Resilience4j, using [interceptors](../README.md#http-request-interceptors). ### Circuit Breaker `CircuitBreakerHTTPRequestInterceptor` wraps the HTTP request inside a [CircuitBreaker](https://resilience4j.readme.io/docs/circuitbreaker). ```java import com.github.ljtfreitas.julian.http.resilience4j.CircuitBreakerHTTPRequestInterceptor; import io.github.resilience4j.circuitbreaker.CircuitBreaker; interface MyApi {} CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("my-circuit-breaker"); CircuitBreakerHTTPRequestInterceptor circuitBreakerInterceptor = new CircuitBreakerHTTPRequestInterceptor(circuitBreaker); MyApi myApi = new ProxyBuilder() .http() .interceptors() .add(circuitBreakerInterceptor) .and() .and() .build(MyApi.class, "http://my.api.com"); ``` 2xx HTTP responses will be counted as success and 4xx/5xx responses will be counted as errors. We can customize this behavior passing a predicate in order to implement a fine control over what HTTP response codes are counted as an "error": ```java import com.github.ljtfreitas.julian.http.resilience4j.CircuitBreakerHTTPRequestInterceptor; import io.github.resilience4j.circuitbreaker.CircuitBreaker; interface MyApi {} CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("my-circuit-breaker"); Predicate> myPredicate = r -> r.status().isSuccess() || r.status().is(HTTPStatusCode.NOT_FOUND); // 404 (NotFound) responses will not be counted as errors on circuit breaker CircuitBreakerHTTPRequestInterceptor circuitBreakerInterceptor = new CircuitBreakerHTTPRequestInterceptor(circuitBreaker); MyApi myApi = new ProxyBuilder() .http() .interceptors() .add(circuitBreakerInterceptor) .and() .and() .build(MyApi.class, "http://my.api.com"); ``` With the circuit breaker in place, we can just call our interface method. In case circuit breaker is OPEN, an exception will be throw, so we just need to be careful about error handling: ```java import com.github.ljtfreitas.julian.Attempt; import com.github.ljtfreitas.julian.http.resilience4j.CircuitBreakerHTTPRequestInterceptor; import io.github.resilience4j.circuitbreaker.CircuitBreaker; interface MyApi { @GET("/get-something") Attempt get(); } CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("my-circuit-breaker"); CircuitBreakerHTTPRequestInterceptor circuitBreakerInterceptor = new CircuitBreakerHTTPRequestInterceptor(circuitBreaker); MyApi myApi = new ProxyBuilder() .http() .interceptors() .add(circuitBreakerInterceptor) .and() .and() .build(MyApi.class, "http://my.api.com"); Attempt result = myApi.get(); // success or failure ``` ### Rate limiter `RateLimiterHTTPRequestInterceptor` wraps the HTTP request inside a [RateLimiter](https://resilience4j.readme.io/docs/ratelimiter). ```java import com.github.ljtfreitas.julian.http.resilience4j.RateLimiterHTTPRequestInterceptor; import io.github.resilience4j.ratelimiter.RateLimiter; interface MyApi {} RateLimiter rateLimiter = RateLimiter.ofDefaults("my-rate-limiter"); RateLimiterHTTPRequestInterceptor rateLimiterInterceptor = new RateLimiterHTTPRequestInterceptor(rateLimiter); MyApi myApi = new ProxyBuilder() .http() .interceptors() .add(rateLimiterInterceptor) .and() .and() .build(MyApi.class, "http://my.api.com"); ``` In case of `RateLimiter` thows a `RequestNotPermitted` exception, `julian-http-client` will return a 409 (Too Many Requests) HTTP response. ### Retry `RetryHTTPRequestInterceptor` wraps the HTTP request inside a [Retry](https://resilience4j.readme.io/docs/retry) component. Because `julian-http-client` requests are async and don't block the main thread, we need to run retries using a [ScheduledExecutorService](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/concurrent/ScheduledExecutorService.html). ```java import com.github.ljtfreitas.julian.http.resilience4j.RetryHTTPRequestInterceptor; import io.github.resilience4j.retry.Retry; interface MyApi {} ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); Retry retry = Retry.ofDefaults("my-retry"); RetryHTTPRequestInterceptor retryInterceptor = new RetryHTTPRequestInterceptor(retry, scheduler); MyApi myApi = new ProxyBuilder() .http() .interceptors() .add(retryInterceptor) .and() .and() .build(MyApi.class, "http://my.api.com"); ``` By default, just exceptions are retried, and `julian-http-client` doesn't handle HTTP response failures (4xx or 5xx) as exceptions. In case we need to retry this kind of response too, we can configure `Retry`: ```java import com.github.ljtfreitas.julian.http.resilience4j.RetryHTTPRequestInterceptor; import io.github.resilience4j.retry.Retry; import io.github.resilience4j.retry.RetryConfig; interface MyApi {} ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); Retry retry = Retry.of("my-retry", RetryConfig.> custom() .retryOnResult(r -> r.status().is(HTTPStatusGroup.SERVER_ERROR)) // retry for any 5xx (server errors) responses .build()); RetryHTTPRequestInterceptor retryInterceptor = new RetryHTTPRequestInterceptor(retry, scheduler); MyApi myApi = new ProxyBuilder() .http() .interceptors() .add(retryInterceptor) .and() .and() .build(MyApi.class, "http://my.api.com"); ``` For exceptions, we can choose what errors should be retried: ```java import com.github.ljtfreitas.julian.http.resilience4j.RetryHTTPRequestInterceptor; import io.github.resilience4j.retry.Retry; import io.github.resilience4j.retry.RetryConfig; interface MyApi {} ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); Retry retry = Retry.of("my-retry", RetryConfig.> custom() .retryOnException(e -> e instanceof HTTPClientException) // just retry HTTPClientException failures .build()); RetryHTTPRequestInterceptor retryInterceptor = new RetryHTTPRequestInterceptor(retry, scheduler); MyApi myApi = new ProxyBuilder() .http() .interceptors() .add(retryInterceptor) .and() .and() .build(MyApi.class, "http://my.api.com"); ``` ### TimeLimiter `TimeLimiterHTTPRequestInterceptor` wraps the HTTP request inside a [TimeLimter](https://resilience4j.readme.io/docs/timeout) component. Because `julian-http-client` requests are async and don't block the main thread, we need to use a [ScheduledExecutorService](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/concurrent/ScheduledExecutorService.html) in order to schedule a timeout. ```java import com.github.ljtfreitas.julian.http.resilience4j.TimeLimiterHTTPRequestInterceptor; import io.github.resilience4j.timelimiter.TimeLimiter; interface MyApi {} ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); TimeLimiter timeLimiter = TimeLimiter.of(Duration.ofMillis(2000)); TimeLimiterHTTPRequestInterceptor timeLimiter = new TimeLimiterHTTPRequestInterceptor(timeLimiter, scheduler); MyApi myApi = new ProxyBuilder() .http() .interceptors() .add(timeLimiter) .and() .and() .build(MyApi.class, "http://my.api.com"); ``` In case of `TimeLimiter` timeout is exceeded, a `java.util.concurrent.TimeoutException` will be throw.