HttpClient asyncRequest and exponential backoff - java

I need to implement an exponential backoff on a request that might fail. However, it's implemented as an async request. Had this been done synchronously, I'd have a better idea on where to put the delay. Roughly, I'm thinking it'd work something like this:
// These would be configurable in application.yml
currentAttempt = 0;
maxAttempts = 3;
timeoutGrowth = 2;
currentDelayTime = 5ms;
repeatNeeded = false;
while(repeatNeeded && currentAttempt < maxAttempts) {
httpStatusCode = makeRequest(someService)
if(httpStatusCode == 503) {
repeatNeeded=true;
currentAttempt++;
currentDelayTime*=timeoutGrowthRate;
sleep(currentDelayTime)
}
}
However, with an async call, the caller to the function is given the time back to do something else until the Future is has something. Do I code the backoff within the getObservations() method below, or do I code this in the caller of that getObservations() method? Below is the call as it currently is:
public CompletableFuture getObservations(String text, Map<String, Object> bodyParams) throws URISyntaxException {
URI uri = getUri(text);
HttpRequest request = getRequest(uri, text, bodyParams);
Map<String, String> contextMap = Optional.ofNullable(MDC.getCopyOfContextMap()).orElse(Collections.emptyMap());
Instant startTime = Instant.now();
return httpClient.sendAsync(request, BodyHandlers.ofString())
.exceptionally(ex -> {
throw new ExternalToolException(externalServiceConfig.getName(), ex);
})
.thenApply(response -> {
long toolRequestDurationMillis = ChronoUnit.MILLIS.between(startTime, Instant.now());
if (HttpStatus.valueOf(response.statusCode()).is2xxSuccessful()) {
ToolResponse toolResponse = processResponse(response, toolRequestDurationMillis);
logToolResponse(toolResponse);
return toolResponse;
}
log.error("{} returned non-200 response code: {}", externalServiceConfig.getName(), response.statusCode());
throw new ExternalToolException(externalServiceConfig.getName(), response.statusCode());
});
}

If you could consider using reactive java that has very powerful API including retries. For example,
request()
.retryWhen(Retry.backoff(3, Duration.ofSeconds(1)));
there are more options like retries for the specific exceptions only or defining max backoff
request()
.retryWhen(Retry.backoff(3, Duration.ofSeconds(2)));
.maxBackoff(5)
.filter(throwable -> isRetryableError(throwable))
You could use WebClient that is a non-blocking client exposing a fluent, reactive API over underlying HTTP client libraries such as Reactor Netty
webClient.get()
.uri("/api")
.retrieve()
.bodyToMono(String.class)
.retryWhen(Retry.backoff(3, Duration.ofSeconds(1)));
if for some reason, you still want to use HttpClient you can wrap CompletableFuture
Mono.fromFuture(httpClient.sendAsync(request, BodyHandlers.ofString()))
.retryWhen(Retry.backoff(3, Duration.ofSeconds(1)));

Related

WebClient how to make calls in Parallel and wait for response

First time trying Webclient and a bit lost. I'm trying to call an API potentially up to 20 times, and I want the calls to happen in parallel and process the response objects as they come in. Then returning the response. I have it almost working, the API is properly iterating through all the responses as they come in and building my response object. However it's not blocking, meaning when the response is finished building, my API has already returned an empty response such as: {}
API:
public GetHistoricalRes getHistoricalDaily(GetHistoricalReq getHistoricalReq) {
GetHistoricalRes historicalDailyQuotesRes = new GetHistoricalRes();
List<Mono<GenHistoricalRes>> genHistoricalDailyQuotes = new ArrayList<>();
for (String ticker : getHistoricalReq.getTickers()) {
genHistoricalDailyQuotes.add(MrMarketClient.getHistoricalDailyQuotes(ticker, getHistoricalReq.getTo(), getHistoricalReq.getFrom()));
}
Flux.merge(genHistoricalDailyQuotes).subscribe((genHistoricalRes) -> {
historicalDailyQuotesRes.getQuotes().put(genHistoricalRes.getSymbol(), genHistoricalRes);
});
return historicalDailyQuotesRes;
}
Webclient:
public Mono<GenHistoricalRes> getHistoricalDailyQuotes(String ticker, String to, String from) {
String historicalPricePath = "/historical-price-full/" + ticker;
return this.getClient()
.get()
.uri(builder -> builder
.path(historicalPricePath)
.queryParam("apikey", apiKey)
.queryParam("from", from)
.queryParam("to", to)
.build())
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.accept(MediaType.APPLICATION_JSON)
.exchangeToMono(
response -> {
if (response.statusCode().equals(HttpStatus.OK)) {
return response.bodyToMono(GenHistoricalRes.class)
.log();
} else {
return response.createException()
.flatMap(Mono::error);
}
});
}

CompletableFuture - skip errors and take only validate data

I have a specific code which is working fine if webclient response is OK. If any error, then the get() method throwing error and the thread blocked forever.
#SneakyThrows
public List<ResponseData> validateExpression(List<RequestData> RequestDataList, Data data) {
System.out.println(Instant.now());
final List<Mono<ResponseData>> monoList = new ArrayList<>();
RequestDataList.parallelStream().forEach(requestData -> {
try {
ObjectMapper mapper = new ObjectMapper();
log.info("Diversity API request data:");
log.info(mapper.writeValueAsString(requestData));
Mono<ResponseData> monoResponse = webClient
.post()
.uri("http://...")
.contentType(MediaType.APPLICATION_JSON)
.header(API_KEY_HEADER, config.getApiKey())
.body(Mono.just(requestData), RequestData.class)
.retrieve()
.bodyToMono(ResponseData.class);
System.out.println("create mono response lazy initialization");
monoList.add(monoResponse);
} catch (Exception e) {
log.info(e.getMessage());
}
});
System.out.println(Instant.now());
CompletableFuture<List<ResponseData>> futureCount = new CompletableFuture<>();
List<ResponseData> responseDataList = new ArrayList<>();
Mono.zip(monoList, Arrays::asList)
.flatMapIterable(objects -> objects)
.doOnComplete(() -> {
futureCount.complete(responseDataList);
}).subscribe(responseData -> {
responseDataList.add((ResponseData) responseData);
});
return futureCount.get();
}
It is working fine with successful case. If there is any error from the webclient it is throwing error and thread blocked forever.
How to skip the errors and get only validate response data ?
How to avoid deadLock on this case?
You should look at CompletableFuture::get method's doc:
It throws three different checked exceptions. Lombok's #SneakyThrows annotation hides them so they aren't managed by your method. You should probably add a try/catch block to manage these exceptions and skip the errors if you want so.

Spring WebClient - how to retry with delay based on response header

A little background
I've been learning Spring Webflux and reactive programming and have gotten stuck on a problem I'm trying to solve around retry logic using Spring Webclient. I've created a client and made successful calls to an external web-service GET endpoint that returns some JSON data.
Problem
When the external service responds with a 503 - Service Unavailable status, the response includes a Retry-After header with a value that indicates how long I should wait before retrying the request. I want to find a way within Spring Webflux/Reactor to tell the webClient to retry it's request after X period, where X is the difference between now and the DateTime that I parse out of the response header.
Simple WebClient GET request
public <T> Mono<T> get(final String url, Class<T> clazz) {
return webClient
.get().uri(url)
.retrieve()
.bodyToMono(clazz);
}
WebClient Builder
I use a builder to create the webClient variable used in the above method, and it's stored as an instance variable in the class.
webClientBuilder = WebClient.builder();
webClientBuilder.codecs(clientCodecConfigurer -> {
clientCodecConfigurer.defaultCodecs();
clientCodecConfigurer.customCodecs().register(new Jackson2JsonDecoder());
clientCodecConfigurer.customCodecs().register(new Jackson2JsonEncoder());
});
webClient = webClientBuilder.build();
Retry When
I've tried to understand and use the retryWhen method with the Retry class, but can't figure out if I can access or pass through the response header value there.
public <T> Mono<T> get(final String url, Class<T> clazz) {
return webClient
.get().uri(url)
.retrieve()
.bodyToMono(clazz);
.retryWhen(new Retry() {
#Override
public Publisher<?> generateCompanion(final Flux<RetrySignal> retrySignals) {
// Can I use retrySignals or retryContext to find the response header somehow?
// If I can find the response header, how to return a "yes-retry" response?
}
})
}
Filter(s) with Extra Logic and DB Interaction
I've also tried to do some extra logic and use filters with the WebClient.Builder, but that only gets me to a point of halting a new request (call to #get) until a previously established Retry-After value has elapsed.
webClientBuilder = WebClient.builder();
webClientBuilder.codecs(clientCodecConfigurer -> {
clientCodecConfigurer.defaultCodecs();
clientCodecConfigurer.customCodecs().register(new Jackson2JsonDecoder());
clientCodecConfigurer.customCodecs().register(new Jackson2JsonEncoder());
});
webClientBuilder.filter(ExchangeFilterFunction.ofRequestProcessor(clientRequest -> {
final Clock clock = Clock.systemUTC();
final int id = (int) clientRequest.attribute("id"); // id is saved as an attribute for the request, pull it out here
final long retryAfterEpochMillis = // get epoch millisecond from DB for id
if(epoch is in the past) {
return Mono.just(clientRequest);
} else { // have to wait until epoch passes to send request
return Mono.just(clientRequest).delayElement(Duration.between(clock.instant(), Instant.ofEpochMilli(retryAfterEpochMillis)));
}
})
);
webClient = webClientBuilder.build();
.onStatus(HttpStatus::isError, response -> {
final List<String> retryAfterHeaders = response.headers().header("Retry-After");
if(retryAfterHeaders.size() > 0) {
final long retryAfterEpochMillis = // parse millisecond epoch time from header
// Save millisecond time to DB associated to specific id
}
return response.bodyToMono(String.class).flatMap(body ->
Mono.error(new RuntimeException(
String.format("Request url {%s} failed with status {%s} and reason {%s}",
url,
response.rawStatusCode(),
body))));
})
Any help is appreciated, and if I can provide more contextual data to help, I will.
1. Retrieve header in retry builder
public class WebClientStatefulRetry3 {
public static void main(String[] args) {
WebClient webClient = WebClient.create();
call(webClient)
.retryWhen(Retry.indefinitely()
.filter(ex -> ex instanceof WebClientResponseException.ServiceUnavailable)
.doBeforeRetryAsync(signal -> Mono.delay(calculateDelay(signal.failure())).then()))
.block();
}
private static Mono<String> call(WebClient webClient) {
return webClient.get()
.uri("http://mockbin.org/bin/b2a26614-0219-4018-9446-c03bc1868ebf")
.retrieve()
.bodyToMono(String.class);
}
private static Duration calculateDelay(Throwable failure) {
String headerValue = ((WebClientResponseException.ServiceUnavailable) failure).getHeaders().get("Retry-After").get(0);
return // calculate delay here from header and current time;
}
}
2. Use expand operator to access the previous response and generate the next one
public class WebClientRetryWithExpand {
public static void main(String[] args) {
WebClient webClient = WebClient.create();
call(webClient)
.expand(prevResponse -> {
List<String> header = prevResponse.headers.header("Retry-After");
if (header.isEmpty()) {
return Mono.empty();
}
long delayInMillis = // calculate delay from header and current time
return Mono.delay(Duration.ofMillis(delayInMillis))
.then(call(webClient));
})
.last()
.block();
}
private static Mono<ResponseWithHeaders> call(WebClient webClient) {
return webClient.get()
.uri("https://example.com")
.exchangeToMono(response -> response.bodyToMono(String.class)
.map(rawResponse -> new ResponseWithHeaders(rawResponse, response.headers())));
}
#Data
static class ResponseWithHeaders {
private final String rawResponse;
private final ClientResponse.Headers headers;
}
}

Limit max threads while using ParallelFlux

How can i limit threads for task, that being executed in parallel? The issue is simple - while my scheduler working, i can't do anything else (fetch some info using postman etc). Is there any way to solve this problem?
Also, i've tryed to set number of threads in flux, for example, using parallel(3).runOn(Schedulers.parallel()) and still my programm is blocked.
#Scheduled(fixedRate = 60000L)
#PostConstruct
public void fillMap() {
Flux.fromIterable(proxyParserService.getProxyList())
.parallel()
.runOn(Schedulers.parallel())
.flatMap(geoDataService::getData)
//some logic here...
Also worh mentioning, that i have flatmap method with opening connections in parallel:
public Mono<Address> getData(Address proxy) {
WebClient webClient = WebClient.builder()
.baseUrl(String.format(URL, proxy.getHost()))
.build();
WebClient.RequestBodyUriSpec request = webClient.method(HttpMethod.GET);
return request.retrieve()
.onStatus(HttpStatus::isError, clientResponse -> {
log.error("Error while calling endpoint {} with status code {}",
URL, clientResponse.statusCode());
throw new RuntimeException("Error while calling geolocation endpoint");
})
.bodyToMono(Address.class)

How to handle error responses in a chain of CompletableFutures?

I have a long chain of completable futures in my project, with each step calling a backend API, which can give multiple error responses and one success response. Now, after parsing the response, I need to judge if it's an error, then I need to show to the user. I also need to know which stage in my chain, produced this error.
My approach right now (shown below) is to throw a Runtime Exception whenever I encounter an error response, and then append exceptionally block to my chain. I feel that this is not the best way to do it, since a runtime exception doesn't fit in this scenario. It also makes my code ugly, since I have to do it whenever I process a response, leading to an extra exception check. Is there a better way to do it?
CompletableFuture.supplyAsync(() -> {
//some api call
Response response = request.send();
if(response.hasError()){ //this is what I am doing right now
logger.error("this is error response");
throw new ResponseErrorException("Error response received for request");
}
})
This is basically repeated for every step in the chain.
Summary: If I get a failure response in any of the steps in a CompletableFuture chain, what's a good way to propagate it to the user?
Edit: If there's no better approach, please feel free to share your views on my approach.
My suggestion is using Decorator pattern for the responses. Suggest you have something like this
CompletableFuture
.supplyAsync(() -> {
//some api call
Response response = request.send();
if(response.hasError()){ //this is what I am doing right now
throw new ResponseErrorException("Error response received for request");
}
})
.thenApply(() -> {
//some api call
Response response = request.send();
if(response.hasError()){ //this is what I am doing right now
throw new ResponseErrorException("Another Error response received for request");
}
})
.exceptionally(ex -> "Error: " + ex.getMessage());
and if you would like to avoid duplication in throwing exceptions you could use following approach
CompletableFuture
.supplyAsync(() -> {
//some api call
Response response = ThrowExceptionOnErrorResponse(request.send());
})
.thenApply(() -> {
//some api call
Response response = ThrowExceptionOnErrorResponse(request.send());
}
})
.exceptionally(ex -> "Error: " + ex.getMessage());
class ThrowExceptionOnError implements Response {
Response originalResponse;
ThrowExceptionOnError(Response originalResp) {
if(response.hasError()) {
throw new ResponseErrorException("Another Error response received for request");
}
this.originalResponse = originalResponse;
}

Categories