diff options
author | tkogut <tomasz.kogut@nokia.com> | 2021-02-24 12:04:53 +0100 |
---|---|---|
committer | tkogut <tomasz.kogut@nokia.com> | 2021-02-25 11:35:09 +0100 |
commit | 573c53f8f8dbd220f34b718399ddd3bd1a3121a9 (patch) | |
tree | b18fad1f4e68273fbf17390f394b33b986e1df5d /rest-services/http-client/src/main | |
parent | dff666365ea56fe9a0d6b89cdb85bb4bf7849122 (diff) |
Honor the Retry-After HTTP header in case of 413 status code.
Issue-ID: DCAEGEN2-1483
Signed-off-by: tkogut <tomasz.kogut@nokia.com>
Change-Id: I429474d4a0565f5aa9bd542daa0db814c5504ef6
Diffstat (limited to 'rest-services/http-client/src/main')
8 files changed, 215 insertions, 41 deletions
diff --git a/rest-services/http-client/src/main/java/org/onap/dcaegen2/services/sdk/rest/services/adapters/http/HttpResponse.java b/rest-services/http-client/src/main/java/org/onap/dcaegen2/services/sdk/rest/services/adapters/http/HttpResponse.java index ce100478..b6cc7c2d 100644 --- a/rest-services/http-client/src/main/java/org/onap/dcaegen2/services/sdk/rest/services/adapters/http/HttpResponse.java +++ b/rest-services/http-client/src/main/java/org/onap/dcaegen2/services/sdk/rest/services/adapters/http/HttpResponse.java @@ -2,7 +2,7 @@ * ============LICENSE_START==================================== * DCAEGEN2-SERVICES-SDK * ========================================================= - * Copyright (C) 2019 Nokia. All rights reserved. + * Copyright (C) 2019-2021 Nokia. All rights reserved. * ========================================================= * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,10 +21,11 @@ package org.onap.dcaegen2.services.sdk.rest.services.adapters.http; import com.google.gson.Gson; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; +import io.vavr.collection.Multimap; import org.immutables.value.Value; import org.onap.dcaegen2.services.sdk.rest.services.adapters.http.exceptions.HttpException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; /** * @author <a href="mailto:piotr.jaszczyk@nokia.com">Piotr Jaszczyk</a> @@ -39,6 +40,8 @@ public interface HttpResponse { byte[] rawBody(); + Multimap<String, String> headers(); + @Value.Default default String statusReason() { return ""; diff --git a/rest-services/http-client/src/main/java/org/onap/dcaegen2/services/sdk/rest/services/adapters/http/NettyHttpResponse.java b/rest-services/http-client/src/main/java/org/onap/dcaegen2/services/sdk/rest/services/adapters/http/NettyHttpResponse.java index 3dcd7098..c4c8ac8d 100644 --- a/rest-services/http-client/src/main/java/org/onap/dcaegen2/services/sdk/rest/services/adapters/http/NettyHttpResponse.java +++ b/rest-services/http-client/src/main/java/org/onap/dcaegen2/services/sdk/rest/services/adapters/http/NettyHttpResponse.java @@ -2,7 +2,7 @@ * ============LICENSE_START==================================== * DCAEGEN2-SERVICES-SDK * ========================================================= - * Copyright (C) 2019 Nokia. All rights reserved. + * Copyright (C) 2019-2021 Nokia. All rights reserved. * ========================================================= * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,9 +20,18 @@ package org.onap.dcaegen2.services.sdk.rest.services.adapters.http; +import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpStatusClass; +import io.vavr.Tuple; +import io.vavr.Tuple2; +import io.vavr.collection.HashMultimap; +import io.vavr.collection.Multimap; +import reactor.netty.http.client.HttpClientResponse; + import java.nio.charset.Charset; +import java.util.List; +import java.util.stream.Collectors; /** * @author <a href="mailto:piotr.jaszczyk@nokia.com">Piotr Jaszczyk</a> @@ -32,11 +41,13 @@ class NettyHttpResponse implements HttpResponse { private final String url; private final HttpResponseStatus status; + private final HttpHeaders headers; private final byte[] body; - NettyHttpResponse(String url, HttpResponseStatus status, byte[] body) { + public NettyHttpResponse(String url, HttpClientResponse response, byte[] body) { this.url = url; - this.status = status; + this.status = response.status(); + this.headers = response.responseHeaders(); this.body = body; } @@ -66,6 +77,14 @@ class NettyHttpResponse implements HttpResponse { } @Override + public Multimap<String, String> headers() { + List<Tuple2<String, String>> httpHeaders = headers.entries().stream() + .map(entry -> Tuple.of(entry.getKey(), entry.getValue())) + .collect(Collectors.toList()); + return HashMultimap.withSeq().ofEntries(httpHeaders); + } + + @Override public String bodyAsString(Charset charset) { return new String(body, charset); } diff --git a/rest-services/http-client/src/main/java/org/onap/dcaegen2/services/sdk/rest/services/adapters/http/RxHttpClient.java b/rest-services/http-client/src/main/java/org/onap/dcaegen2/services/sdk/rest/services/adapters/http/RxHttpClient.java index d25d7469..341aaf56 100644 --- a/rest-services/http-client/src/main/java/org/onap/dcaegen2/services/sdk/rest/services/adapters/http/RxHttpClient.java +++ b/rest-services/http-client/src/main/java/org/onap/dcaegen2/services/sdk/rest/services/adapters/http/RxHttpClient.java @@ -20,23 +20,21 @@ package org.onap.dcaegen2.services.sdk.rest.services.adapters.http; -import io.netty.handler.codec.http.HttpResponseStatus; -import io.vavr.collection.HashSet; import io.vavr.collection.Stream; import io.vavr.control.Option; -import org.onap.dcaegen2.services.sdk.rest.services.adapters.http.config.RetryConfig; import org.onap.dcaegen2.services.sdk.rest.services.adapters.http.exceptions.RetryableException; +import org.onap.dcaegen2.services.sdk.rest.services.adapters.http.retry.RetryLogic; import org.onap.dcaegen2.services.sdk.rest.services.model.logging.RequestDiagnosticContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Mono; +import reactor.netty.ByteBufMono; import reactor.netty.http.client.HttpClient; import reactor.netty.http.client.HttpClient.ResponseReceiver; import reactor.netty.http.client.HttpClientRequest; import reactor.netty.http.client.HttpClientResponse; -import reactor.util.retry.Retry; -import reactor.util.retry.RetryBackoffSpec; +import java.util.Objects; import java.util.stream.Collectors; /** @@ -46,21 +44,21 @@ public class RxHttpClient { private static final Logger LOGGER = LoggerFactory.getLogger(RxHttpClient.class); private final HttpClient httpClient; - private RetryConfig retryConfig; + private RetryLogic retryLogic; RxHttpClient(HttpClient httpClient) { - this.httpClient = httpClient; + this.httpClient = Objects.requireNonNull(httpClient, "httpClient must not be null"); } - RxHttpClient(HttpClient httpClient, RetryConfig retryConfig) { + RxHttpClient(HttpClient httpClient, RetryLogic retryLogic) { this(httpClient); - this.retryConfig = retryConfig; + this.retryLogic = retryLogic; } public Mono<HttpResponse> call(HttpRequest request) { Mono<HttpResponse> httpResponseMono = response(request); - return Option.of(retryConfig) - .map(rc -> retryConfig(rc, request.diagnosticContext())) + return Option.of(retryLogic) + .map(rc -> rc.retry(request.diagnosticContext())) .map(httpResponseMono::retryWhen) .getOrElse(() -> httpResponseMono); } @@ -80,13 +78,13 @@ public class RxHttpClient { private Mono<HttpResponse> response(HttpRequest request) { return prepareRequest(request) - .responseSingle((resp, content) -> mapResponse(request.url(), resp.status(), content)); + .responseSingle((resp, content) -> mapResponse(request.url(), resp, content)); } - private Mono<HttpResponse> mapResponse(String url, HttpResponseStatus status, reactor.netty.ByteBufMono content) { + private Mono<HttpResponse> mapResponse(String url, HttpClientResponse response, ByteBufMono content) { return content.asByteArray() .defaultIfEmpty(new byte[0]) - .map(bytes -> new NettyHttpResponse(url, status, bytes)) + .map(bytes -> new NettyHttpResponse(url, response, bytes)) .map(this::validatedResponse); } @@ -98,10 +96,9 @@ public class RxHttpClient { } private boolean shouldRetry(int code) { - return Option.of(retryConfig) - .map(RetryConfig::retryableHttpResponseCodes) - .getOrElse(HashSet::empty) - .contains(code); + return Option.of(retryLogic) + .map(rc -> rc.shouldRetry(code)) + .getOrElse(Boolean.FALSE); } private ResponseReceiver<?> prepareBody(HttpRequest request, HttpClient theClient) { @@ -153,19 +150,4 @@ public class RxHttpClient { context.withSlf4jMdc(LOGGER.isDebugEnabled(), () -> LOGGER.debug("Response status: {}", httpClientResponse.status())); } - - private RetryBackoffSpec retryConfig(RetryConfig retryConfig, RequestDiagnosticContext context) { - return Retry - .fixedDelay(retryConfig.retryCount(), retryConfig.retryInterval()) - .doBeforeRetry(retrySignal -> context.withSlf4jMdc( - LOGGER.isTraceEnabled(), () -> LOGGER.trace("Retry: {}", retrySignal))) - .filter(ex -> isRetryable(retryConfig, ex)) - .onRetryExhaustedThrow((retryBackoffSpec, retrySignal) -> retrySignal.failure()); - } - - private boolean isRetryable(RetryConfig retryConfig, Throwable ex) { - return retryConfig.retryableExceptions() - .toStream() - .exists(clazz -> clazz.isAssignableFrom(ex.getClass())); - } } diff --git a/rest-services/http-client/src/main/java/org/onap/dcaegen2/services/sdk/rest/services/adapters/http/RxHttpClientFactory.java b/rest-services/http-client/src/main/java/org/onap/dcaegen2/services/sdk/rest/services/adapters/http/RxHttpClientFactory.java index 90b8ff16..8634f146 100644 --- a/rest-services/http-client/src/main/java/org/onap/dcaegen2/services/sdk/rest/services/adapters/http/RxHttpClientFactory.java +++ b/rest-services/http-client/src/main/java/org/onap/dcaegen2/services/sdk/rest/services/adapters/http/RxHttpClientFactory.java @@ -22,6 +22,7 @@ package org.onap.dcaegen2.services.sdk.rest.services.adapters.http; import io.vavr.control.Option; import org.onap.dcaegen2.services.sdk.rest.services.adapters.http.config.RxHttpClientConfig; +import org.onap.dcaegen2.services.sdk.rest.services.adapters.http.retry.RetryLogicFactory; import org.onap.dcaegen2.services.sdk.security.ssl.SecurityKeys; import org.onap.dcaegen2.services.sdk.security.ssl.TrustStoreKeys; import reactor.netty.http.client.HttpClient; @@ -81,7 +82,7 @@ public final class RxHttpClientFactory { private static RxHttpClient createWithConfig(HttpClient httpClient, RxHttpClientConfig config) { return Option.of(config.retryConfig()) - .map(retryConfig -> new RxHttpClient(httpClient, retryConfig)) + .map(retryConfig -> new RxHttpClient(httpClient, RetryLogicFactory.create(retryConfig))) .getOrElse(() -> new RxHttpClient(httpClient)); } } diff --git a/rest-services/http-client/src/main/java/org/onap/dcaegen2/services/sdk/rest/services/adapters/http/exceptions/RetryableException.java b/rest-services/http-client/src/main/java/org/onap/dcaegen2/services/sdk/rest/services/adapters/http/exceptions/RetryableException.java index aa48497a..6d286a6f 100644 --- a/rest-services/http-client/src/main/java/org/onap/dcaegen2/services/sdk/rest/services/adapters/http/exceptions/RetryableException.java +++ b/rest-services/http-client/src/main/java/org/onap/dcaegen2/services/sdk/rest/services/adapters/http/exceptions/RetryableException.java @@ -22,12 +22,14 @@ package org.onap.dcaegen2.services.sdk.rest.services.adapters.http.exceptions; import org.onap.dcaegen2.services.sdk.rest.services.adapters.http.HttpResponse; +import java.util.Objects; + public class RetryableException extends RuntimeException { private final HttpResponse response; public RetryableException(HttpResponse response) { - this.response = response; + this.response = Objects.requireNonNull(response, "response must not be null"); } public HttpResponse getResponse() { diff --git a/rest-services/http-client/src/main/java/org/onap/dcaegen2/services/sdk/rest/services/adapters/http/retry/RetryIntervalExtractor.java b/rest-services/http-client/src/main/java/org/onap/dcaegen2/services/sdk/rest/services/adapters/http/retry/RetryIntervalExtractor.java new file mode 100644 index 00000000..0e2c875a --- /dev/null +++ b/rest-services/http-client/src/main/java/org/onap/dcaegen2/services/sdk/rest/services/adapters/http/retry/RetryIntervalExtractor.java @@ -0,0 +1,59 @@ +/* + * ============LICENSE_START==================================== + * DCAEGEN2-SERVICES-SDK + * ========================================================= + * Copyright (C) 2021 Nokia. All rights reserved. + * ========================================================= + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================== + */ + +package org.onap.dcaegen2.services.sdk.rest.services.adapters.http.retry; + +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vavr.Tuple; +import io.vavr.Value; +import io.vavr.collection.Multimap; +import io.vavr.control.Option; +import io.vavr.control.Try; +import org.onap.dcaegen2.services.sdk.rest.services.adapters.http.HttpResponse; + +import java.time.Duration; + +class RetryIntervalExtractor { + + private static final String RETRY_AFTER_HEADER = HttpHeaderNames.RETRY_AFTER.toString(); + private static final int PAYLOAD_TOO_LARGE_HTTP_CODE = HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE.code(); + + Option<Duration> extractDelay(HttpResponse response) { + return response.statusCode() == PAYLOAD_TOO_LARGE_HTTP_CODE + ? extractDelay(response.headers()) + : Option.none(); + } + + private Option<Duration> extractDelay(Multimap<String, String> headers) { + return headers + .map((key, value) -> Tuple.of(key.toLowerCase(), value)) + .get(RETRY_AFTER_HEADER) + .toStream() + .flatMap(Value::toStream) + .map(this::parse) + .find(d -> d >= 0) + .map(Duration::ofSeconds); + } + + private int parse(String str) { + return Try.of(() -> Integer.parseInt(str)).getOrElse(-1); + } +} diff --git a/rest-services/http-client/src/main/java/org/onap/dcaegen2/services/sdk/rest/services/adapters/http/retry/RetryLogic.java b/rest-services/http-client/src/main/java/org/onap/dcaegen2/services/sdk/rest/services/adapters/http/retry/RetryLogic.java new file mode 100644 index 00000000..fd420843 --- /dev/null +++ b/rest-services/http-client/src/main/java/org/onap/dcaegen2/services/sdk/rest/services/adapters/http/retry/RetryLogic.java @@ -0,0 +1,75 @@ +/* + * ============LICENSE_START==================================== + * DCAEGEN2-SERVICES-SDK + * ========================================================= + * Copyright (C) 2021 Nokia. All rights reserved. + * ========================================================= + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================== + */ + +package org.onap.dcaegen2.services.sdk.rest.services.adapters.http.retry; + +import org.onap.dcaegen2.services.sdk.rest.services.adapters.http.config.RetryConfig; +import org.onap.dcaegen2.services.sdk.rest.services.adapters.http.exceptions.RetryableException; +import org.onap.dcaegen2.services.sdk.rest.services.model.logging.RequestDiagnosticContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Mono; +import reactor.util.retry.Retry; + +import java.time.Duration; +import java.util.Objects; + +public class RetryLogic { + + private static final Logger LOGGER = LoggerFactory.getLogger(RetryLogic.class); + + private final RetryConfig retryConfig; + private final RetryIntervalExtractor delayExtractor; + + public RetryLogic(RetryConfig retryConfig, RetryIntervalExtractor delayExtractor) { + this.retryConfig = Objects.requireNonNull(retryConfig, "retryConfig must not be null"); + this.delayExtractor = Objects.requireNonNull(delayExtractor, "delayExtractor must not be null"); + } + + public Retry retry(RequestDiagnosticContext requestDiagnosticContext) { + return Retry + .max(retryConfig.retryCount()) + .doAfterRetryAsync(rc -> Mono.delay(calculateDelay(rc.failure())).then()) + .doBeforeRetry(retrySignal -> requestDiagnosticContext.withSlf4jMdc( + LOGGER.isTraceEnabled(), () -> LOGGER.trace("Retry: {}", retrySignal))) + .filter(ex -> isRetryable(retryConfig, ex)) + .onRetryExhaustedThrow((retryBackoffSpec, retrySignal) -> retrySignal.failure()); + } + + public boolean shouldRetry(int code) { + return retryConfig.retryableHttpResponseCodes().contains(code); + } + + private Duration calculateDelay(Throwable tx) { + Duration retryInterval = retryConfig.retryInterval(); + if (tx instanceof RetryableException) { + RetryableException ex = (RetryableException) tx; + retryInterval = delayExtractor.extractDelay(ex.getResponse()) + .getOrElse(retryInterval); + } + return retryInterval; + } + + private boolean isRetryable(RetryConfig retryConfig, Throwable ex) { + return retryConfig.retryableExceptions() + .toStream() + .exists(clazz -> clazz.isAssignableFrom(ex.getClass())); + } +} diff --git a/rest-services/http-client/src/main/java/org/onap/dcaegen2/services/sdk/rest/services/adapters/http/retry/RetryLogicFactory.java b/rest-services/http-client/src/main/java/org/onap/dcaegen2/services/sdk/rest/services/adapters/http/retry/RetryLogicFactory.java new file mode 100644 index 00000000..51acbe12 --- /dev/null +++ b/rest-services/http-client/src/main/java/org/onap/dcaegen2/services/sdk/rest/services/adapters/http/retry/RetryLogicFactory.java @@ -0,0 +1,33 @@ +/* + * ============LICENSE_START==================================== + * DCAEGEN2-SERVICES-SDK + * ========================================================= + * Copyright (C) 2021 Nokia. All rights reserved. + * ========================================================= + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END===================================== + */ + +package org.onap.dcaegen2.services.sdk.rest.services.adapters.http.retry; + +import org.onap.dcaegen2.services.sdk.rest.services.adapters.http.config.RetryConfig; + +public final class RetryLogicFactory { + + private RetryLogicFactory() { + } + + public static RetryLogic create(RetryConfig config) { + return new RetryLogic(config, new RetryIntervalExtractor()); + } +} |