Add customizable retry for SDK http client (#2523)

Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
Avgustin Marinov
2025-07-02 09:28:15 +03:00
committed by GitHub
parent 2f2f947da0
commit 3e35d0c5c1

View File

@@ -58,15 +58,20 @@ import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.hc.client5.http.HttpRequestRetryStrategy;
import org.apache.hc.client5.http.classic.HttpClient; import org.apache.hc.client5.http.classic.HttpClient;
import org.apache.hc.client5.http.classic.methods.HttpGet; import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.impl.DefaultHttpRequestRetryStrategy;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
import org.apache.hc.client5.http.impl.classic.HttpClients; import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy; import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
import org.apache.hc.client5.http.ssl.TlsSocketStrategy;
import org.apache.hc.client5.http.ssl.TrustAllStrategy; import org.apache.hc.client5.http.ssl.TrustAllStrategy;
import org.apache.hc.core5.http.ClassicHttpResponse; import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.ssl.SSLContextBuilder; import org.apache.hc.core5.ssl.SSLContextBuilder;
import org.apache.hc.core5.util.TimeValue;
import org.springframework.hateoas.Link; import org.springframework.hateoas.Link;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
@@ -93,7 +98,7 @@ public class HawkbitClient {
"Basic " + Base64.getEncoder().encodeToString( "Basic " + Base64.getEncoder().encodeToString(
(Objects.requireNonNull(tenant.getUsername(), "User is null!") + (Objects.requireNonNull(tenant.getUsername(), "User is null!") +
":" + ":" +
Objects.requireNonNull(tenant.getPassword(),"Password is not available!")) Objects.requireNonNull(tenant.getPassword(), "Password is not available!"))
.getBytes(StandardCharsets.ISO_8859_1))) .getBytes(StandardCharsets.ISO_8859_1)))
: template -> { : template -> {
if (!ObjectUtils.isEmpty(tenant.getGatewayToken())) { if (!ObjectUtils.isEmpty(tenant.getGatewayToken())) {
@@ -108,6 +113,12 @@ public class HawkbitClient {
log.trace("REST API call failed!", e); log.trace("REST API call failed!", e);
return e; return e;
}; };
private static final HttpRequestRetryStrategy DEFAULT_HTTP_REQUEST_RETRY_STRATEGY =
new DefaultHttpRequestRetryStrategy(
Integer.getInteger("hawkbit.sdk.http.maxRetry", 3),
TimeValue.ofSeconds(Integer.getInteger("hawkbit.sdk.http.defaultRetryIntervalSec", 10)));
private final HawkbitServer hawkBitServer; private final HawkbitServer hawkBitServer;
private final Encoder encoder; private final Encoder encoder;
@@ -117,18 +128,23 @@ public class HawkbitClient {
private final ErrorDecoder errorDecoder; private final ErrorDecoder errorDecoder;
private final BiFunction<Tenant, Controller, RequestInterceptor> requestInterceptorFn; private final BiFunction<Tenant, Controller, RequestInterceptor> requestInterceptorFn;
private final HttpRequestRetryStrategy httpRequestRetryStrategy;
public HawkbitClient( public HawkbitClient(
final HawkbitServer hawkBitServer, final Encoder encoder, final Decoder decoder, final Contract contract) { final HawkbitServer hawkBitServer, final Encoder encoder, final Decoder decoder, final Contract contract) {
this(hawkBitServer, encoder, decoder, contract, null, null); this(hawkBitServer, encoder, decoder, contract, null, null);
} }
/**
* Customizers gets default ones and could
*/
public HawkbitClient( public HawkbitClient(
final HawkbitServer hawkBitServer, final Encoder encoder, final Decoder decoder, final Contract contract, final HawkbitServer hawkBitServer, final Encoder encoder, final Decoder decoder, final Contract contract,
final ErrorDecoder errorDecoder, final ErrorDecoder errorDecoder, final BiFunction<Tenant, Controller, RequestInterceptor> requestInterceptorFn) {
final BiFunction<Tenant, Controller, RequestInterceptor> requestInterceptorFn) { this(hawkBitServer, encoder, decoder, contract, errorDecoder, requestInterceptorFn, null);
}
public HawkbitClient(
final HawkbitServer hawkBitServer, final Encoder encoder, final Decoder decoder, final Contract contract,
final ErrorDecoder errorDecoder, final BiFunction<Tenant, Controller, RequestInterceptor> requestInterceptorFn,
final HttpRequestRetryStrategy httpRequestRetryStrategy) {
this.hawkBitServer = hawkBitServer; this.hawkBitServer = hawkBitServer;
this.encoder = encoder; this.encoder = encoder;
this.decoder = decoder; this.decoder = decoder;
@@ -136,6 +152,8 @@ public class HawkbitClient {
this.errorDecoder = errorDecoder == null ? DEFAULT_ERROR_DECODER : errorDecoder; this.errorDecoder = errorDecoder == null ? DEFAULT_ERROR_DECODER : errorDecoder;
this.requestInterceptorFn = requestInterceptorFn == null ? DEFAULT_REQUEST_INTERCEPTOR_FN : requestInterceptorFn; this.requestInterceptorFn = requestInterceptorFn == null ? DEFAULT_REQUEST_INTERCEPTOR_FN : requestInterceptorFn;
this.httpRequestRetryStrategy = httpRequestRetryStrategy == null ? DEFAULT_HTTP_REQUEST_RETRY_STRATEGY : httpRequestRetryStrategy;
} }
public <T> T mgmtService(final Class<T> serviceType, final Tenant tenantProperties) { public <T> T mgmtService(final Class<T> serviceType, final Tenant tenantProperties) {
@@ -176,9 +194,9 @@ public class HawkbitClient {
final T result; final T result;
if (linkType.isAssignableFrom(ClassicHttpResponse.class)) { if (linkType.isAssignableFrom(ClassicHttpResponse.class)) {
result = (T)response; result = (T) response;
} else if (linkType == InputStream.class) { } else if (linkType == InputStream.class) {
result = (T)response.getEntity().getContent(); result = (T) response.getEntity().getContent();
} else { } else {
result = new ObjectMapper().readValue(response.getEntity().getContent(), linkType); result = new ObjectMapper().readValue(response.getEntity().getContent(), linkType);
} }
@@ -294,7 +312,7 @@ public class HawkbitClient {
throw toFeignException(responseCode, conn, Request.create( throw toFeignException(responseCode, conn, Request.create(
Request.HttpMethod.POST, conn.getURL().toString(), Request.HttpMethod.POST, conn.getURL().toString(),
conn.getHeaderFields().entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)), conn.getHeaderFields().entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)),
(Request.Body)null, null)); (Request.Body) null, null));
} }
return method.getReturnType() == ResponseEntity.class return method.getReturnType() == ResponseEntity.class
@@ -307,7 +325,8 @@ public class HawkbitClient {
: deserialize(conn.getInputStream(), method.getReturnType(), objectMapper); : deserialize(conn.getInputStream(), method.getReturnType(), objectMapper);
} }
private static FeignException toFeignException(final int responseCode, final HttpURLConnection conn, final Request request) throws IOException { private static FeignException toFeignException(final int responseCode, final HttpURLConnection conn, final Request request)
throws IOException {
if (responseCode >= 500 && responseCode < 600) { if (responseCode >= 500 && responseCode < 600) {
// server error // server error
return switch (responseCode) { return switch (responseCode) {
@@ -395,19 +414,23 @@ public class HawkbitClient {
final HttpClientWrapper httpClientWrapper = HTTP_CLIENTS.get(key); final HttpClientWrapper httpClientWrapper = HTTP_CLIENTS.get(key);
HttpClient client = httpClientWrapper == null ? null : httpClientWrapper.get(); HttpClient client = httpClientWrapper == null ? null : httpClientWrapper.get();
if (client == null) { // create if (client == null) { // create
final CloseableHttpClient newClient; final HttpClientBuilder builder = HttpClients.custom().setRetryStrategy(DEFAULT_HTTP_REQUEST_RETRY_STRATEGY);
if (key.isHttps()) { if (key.isHttps()) {
// mTLS could be requested // mTLS could be used / setup
try { try {
newClient = tlsClient(key.getClientCertificate(), key.getServerCertificates()); builder.setConnectionManager(
PoolingHttpClientConnectionManagerBuilder.create()
.setTlsSocketStrategy(getTlsSocketStragegy(key.getClientCertificate(), key.getServerCertificates()))
.build());
} catch (final RuntimeException e) { } catch (final RuntimeException e) {
throw e; throw e;
} catch (final Exception e) { } catch (final Exception e) {
throw new IllegalStateException("Failed to create mTLS client", e); throw new IllegalStateException("Failed to create mTLS client", e);
} }
} else {
newClient = HttpClients.createDefault();
} }
final CloseableHttpClient newClient = builder.build();
HTTP_CLIENTS.put(key, new HttpClientWrapper(key, newClient)); HTTP_CLIENTS.put(key, new HttpClientWrapper(key, newClient));
return newClient; return newClient;
} else { } else {
@@ -415,7 +438,8 @@ public class HawkbitClient {
} }
} }
} }
private static CloseableHttpClient tlsClient(final Certificate clientCertificate, final X509Certificate[] serverCertificates)
private static TlsSocketStrategy getTlsSocketStragegy(final Certificate clientCertificate, final X509Certificate[] serverCertificates)
throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException, KeyManagementException, CertificateException, throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException, KeyManagementException, CertificateException,
IOException { IOException {
final SSLContextBuilder sslContextBuilder = SSLContextBuilder.create(); final SSLContextBuilder sslContextBuilder = SSLContextBuilder.create();
@@ -433,13 +457,7 @@ public class HawkbitClient {
} }
sslContextBuilder.loadTrustMaterial(trustStore, null); sslContextBuilder.loadTrustMaterial(trustStore, null);
} }
return HttpClients return new DefaultClientTlsStrategy(sslContextBuilder.build());
.custom()
.setConnectionManager(
PoolingHttpClientConnectionManagerBuilder.create()
.setTlsSocketStrategy(new DefaultClientTlsStrategy(sslContextBuilder.build()))
.build())
.build();
} }
@AllArgsConstructor @AllArgsConstructor