From b852d6681d8404623e9976c017d4650ab39747a6 Mon Sep 17 00:00:00 2001 From: Chris Eager Date: Fri, 23 Jun 2023 10:48:38 -0500 Subject: [PATCH] FaultTolerantHttpClient: used managed ScheduledExecutorService for retries --- .../org/signal/integration/Operations.java | 1 + .../textsecuregcm/WhisperServerService.java | 19 +- .../http/FaultTolerantHttpClient.java | 60 +++--- .../securebackup/SecureBackupClient.java | 59 +++--- .../securestorage/SecureStorageClient.java | 59 +++--- .../SecureValueRecovery2Client.java | 5 +- .../subscriptions/BraintreeManager.java | 5 +- .../workers/AssignUsernameCommand.java | 15 +- .../workers/CommandDependencies.java | 10 +- .../securebackup/SecureBackupClientTest.java | 103 ++++----- .../SecureStorageClientTest.java | 196 +++++++++--------- .../SecureValueRecovery2ClientTest.java | 8 +- .../http/FaultTolerantHttpClientTest.java | 58 ++++-- 13 files changed, 346 insertions(+), 252 deletions(-) diff --git a/integration-tests/src/main/java/org/signal/integration/Operations.java b/integration-tests/src/main/java/org/signal/integration/Operations.java index 3ed873e2d..f603f2fda 100644 --- a/integration-tests/src/main/java/org/signal/integration/Operations.java +++ b/integration-tests/src/main/java/org/signal/integration/Operations.java @@ -311,6 +311,7 @@ public final class Operations { return FaultTolerantHttpClient.newBuilder() .withName("integration-test") .withExecutor(Executors.newFixedThreadPool(16)) + .withRetryExecutor(Executors.newSingleThreadScheduledExecutor()) .withCircuitBreaker(new CircuitBreakerConfiguration()) .withTrustedServerCertificates(CONFIG.rootCert()) .build(); diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java index cbf8bc8d8..709b85aeb 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java @@ -379,6 +379,10 @@ public class WhisperServerService extends ApplicationtoRetryConfigBuilder().retryOnResult(o -> o.statusCode() >= 500).build(); + if (this.retryExecutor == null) { + throw new IllegalArgumentException("retryExecutor must be specified with retryConfiguration"); + } + RetryConfig retryConfig = retryConfiguration.toRetryConfigBuilder() + .retryOnResult(o -> o.statusCode() >= 500).build(); this.retry = Retry.of(name + "-retry", retryConfig); CircuitBreakerUtil.registerMetrics(retry, FaultTolerantHttpClient.class); } else { @@ -76,19 +80,20 @@ public class FaultTolerantHttpClient { public static class Builder { + private HttpClient.Version version = HttpClient.Version.HTTP_2; + private HttpClient.Redirect redirect = HttpClient.Redirect.NEVER; + private Duration connectTimeout = Duration.ofSeconds(10); - private HttpClient.Version version = HttpClient.Version.HTTP_2; - private HttpClient.Redirect redirect = HttpClient.Redirect.NEVER; - private Duration connectTimeout = Duration.ofSeconds(10); - - private String name; - private Executor executor; - private KeyStore trustStore; - private String securityProtocol = SECURITY_PROTOCOL_TLS_1_2; - private RetryConfiguration retryConfiguration; + private String name; + private Executor executor; + private ScheduledExecutorService retryExecutor; + private KeyStore trustStore; + private String securityProtocol = SECURITY_PROTOCOL_TLS_1_2; + private RetryConfiguration retryConfiguration; private CircuitBreakerConfiguration circuitBreakerConfiguration; - private Builder() {} + private Builder() { + } public Builder withName(String name) { this.name = name; @@ -120,6 +125,11 @@ public class FaultTolerantHttpClient { return this; } + public Builder withRetryExecutor(ScheduledExecutorService retryExecutor) { + this.retryExecutor = retryExecutor; + return this; + } + public Builder withCircuitBreaker(CircuitBreakerConfiguration circuitBreakerConfiguration) { this.circuitBreakerConfiguration = circuitBreakerConfiguration; return this; @@ -141,10 +151,10 @@ public class FaultTolerantHttpClient { } final HttpClient.Builder builder = HttpClient.newBuilder() - .connectTimeout(connectTimeout) - .followRedirects(redirect) - .version(version) - .executor(executor); + .connectTimeout(connectTimeout) + .followRedirects(redirect) + .version(version) + .executor(executor); final SslConfigurator sslConfigurator = SslConfigurator.newInstance().securityProtocol(securityProtocol); @@ -154,9 +164,9 @@ public class FaultTolerantHttpClient { builder.sslContext(sslConfigurator.createSSLContext()); - return new FaultTolerantHttpClient(name, builder.build(), retryConfiguration, circuitBreakerConfiguration); + return new FaultTolerantHttpClient(name, builder.build(), retryExecutor, retryConfiguration, + circuitBreakerConfiguration); } - } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/securebackup/SecureBackupClient.java b/service/src/main/java/org/whispersystems/textsecuregcm/securebackup/SecureBackupClient.java index 52a4628de..e6ecd801e 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/securebackup/SecureBackupClient.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/securebackup/SecureBackupClient.java @@ -18,6 +18,7 @@ import java.time.Duration; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; +import java.util.concurrent.ScheduledExecutorService; import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials; import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator; import org.whispersystems.textsecuregcm.configuration.SecureBackupServiceConfiguration; @@ -29,37 +30,41 @@ import org.whispersystems.textsecuregcm.util.HttpUtils; */ public class SecureBackupClient { - private final ExternalServiceCredentialsGenerator secureBackupCredentialsGenerator; - private final URI deleteUri; - private final FaultTolerantHttpClient httpClient; + private final ExternalServiceCredentialsGenerator secureBackupCredentialsGenerator; + private final URI deleteUri; + private final FaultTolerantHttpClient httpClient; - @VisibleForTesting - static final String DELETE_PATH = "/v1/backup"; + @VisibleForTesting + static final String DELETE_PATH = "/v1/backup"; - public SecureBackupClient(final ExternalServiceCredentialsGenerator secureBackupCredentialsGenerator, final Executor executor, final SecureBackupServiceConfiguration configuration) throws CertificateException { - this.secureBackupCredentialsGenerator = secureBackupCredentialsGenerator; - this.deleteUri = URI.create(configuration.getUri()).resolve(DELETE_PATH); - this.httpClient = FaultTolerantHttpClient.newBuilder() - .withCircuitBreaker(configuration.getCircuitBreakerConfiguration()) - .withRetry(configuration.getRetryConfiguration()) - .withVersion(HttpClient.Version.HTTP_1_1) - .withConnectTimeout(Duration.ofSeconds(10)) - .withRedirect(HttpClient.Redirect.NEVER) - .withExecutor(executor) - .withName("secure-backup") - .withSecurityProtocol(FaultTolerantHttpClient.SECURITY_PROTOCOL_TLS_1_2) - .withTrustedServerCertificates(configuration.getBackupCaCertificates().toArray(new String[0])) - .build(); - } + public SecureBackupClient(final ExternalServiceCredentialsGenerator secureBackupCredentialsGenerator, + final Executor executor, final + ScheduledExecutorService retryExecutor, final SecureBackupServiceConfiguration configuration) + throws CertificateException { + this.secureBackupCredentialsGenerator = secureBackupCredentialsGenerator; + this.deleteUri = URI.create(configuration.getUri()).resolve(DELETE_PATH); + this.httpClient = FaultTolerantHttpClient.newBuilder() + .withCircuitBreaker(configuration.getCircuitBreakerConfiguration()) + .withRetry(configuration.getRetryConfiguration()) + .withRetryExecutor(retryExecutor) + .withVersion(HttpClient.Version.HTTP_1_1) + .withConnectTimeout(Duration.ofSeconds(10)) + .withRedirect(HttpClient.Redirect.NEVER) + .withExecutor(executor) + .withName("secure-backup") + .withSecurityProtocol(FaultTolerantHttpClient.SECURITY_PROTOCOL_TLS_1_2) + .withTrustedServerCertificates(configuration.getBackupCaCertificates().toArray(new String[0])) + .build(); + } - public CompletableFuture deleteBackups(final UUID accountUuid) { - final ExternalServiceCredentials credentials = secureBackupCredentialsGenerator.generateForUuid(accountUuid); + public CompletableFuture deleteBackups(final UUID accountUuid) { + final ExternalServiceCredentials credentials = secureBackupCredentialsGenerator.generateForUuid(accountUuid); - final HttpRequest request = HttpRequest.newBuilder() - .uri(deleteUri) - .DELETE() - .header(HttpHeaders.AUTHORIZATION, basicAuthHeader(credentials)) - .build(); + final HttpRequest request = HttpRequest.newBuilder() + .uri(deleteUri) + .DELETE() + .header(HttpHeaders.AUTHORIZATION, basicAuthHeader(credentials)) + .build(); return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()).thenApply(response -> { if (HttpUtils.isSuccessfulResponse(response.statusCode())) { diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/securestorage/SecureStorageClient.java b/service/src/main/java/org/whispersystems/textsecuregcm/securestorage/SecureStorageClient.java index 2918a5179..6fe2f7334 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/securestorage/SecureStorageClient.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/securestorage/SecureStorageClient.java @@ -18,6 +18,7 @@ import java.time.Duration; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; +import java.util.concurrent.ScheduledExecutorService; import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials; import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator; import org.whispersystems.textsecuregcm.configuration.SecureStorageServiceConfiguration; @@ -29,37 +30,41 @@ import org.whispersystems.textsecuregcm.util.HttpUtils; */ public class SecureStorageClient { - private final ExternalServiceCredentialsGenerator storageServiceCredentialsGenerator; - private final URI deleteUri; - private final FaultTolerantHttpClient httpClient; + private final ExternalServiceCredentialsGenerator storageServiceCredentialsGenerator; + private final URI deleteUri; + private final FaultTolerantHttpClient httpClient; - @VisibleForTesting - static final String DELETE_PATH = "/v1/storage"; + @VisibleForTesting + static final String DELETE_PATH = "/v1/storage"; - public SecureStorageClient(final ExternalServiceCredentialsGenerator storageServiceCredentialsGenerator, final Executor executor, final SecureStorageServiceConfiguration configuration) throws CertificateException { - this.storageServiceCredentialsGenerator = storageServiceCredentialsGenerator; - this.deleteUri = URI.create(configuration.uri()).resolve(DELETE_PATH); - this.httpClient = FaultTolerantHttpClient.newBuilder() - .withCircuitBreaker(configuration.circuitBreaker()) - .withRetry(configuration.retry()) - .withVersion(HttpClient.Version.HTTP_1_1) - .withConnectTimeout(Duration.ofSeconds(10)) - .withRedirect(HttpClient.Redirect.NEVER) - .withExecutor(executor) - .withName("secure-storage") - .withSecurityProtocol(FaultTolerantHttpClient.SECURITY_PROTOCOL_TLS_1_3) - .withTrustedServerCertificates(configuration.storageCaCertificates().toArray(new String[0])) - .build(); - } + public SecureStorageClient(final ExternalServiceCredentialsGenerator storageServiceCredentialsGenerator, + final Executor executor, final + ScheduledExecutorService retryExecutor, final SecureStorageServiceConfiguration configuration) + throws CertificateException { + this.storageServiceCredentialsGenerator = storageServiceCredentialsGenerator; + this.deleteUri = URI.create(configuration.uri()).resolve(DELETE_PATH); + this.httpClient = FaultTolerantHttpClient.newBuilder() + .withCircuitBreaker(configuration.circuitBreaker()) + .withRetry(configuration.retry()) + .withRetryExecutor(retryExecutor) + .withVersion(HttpClient.Version.HTTP_1_1) + .withConnectTimeout(Duration.ofSeconds(10)) + .withRedirect(HttpClient.Redirect.NEVER) + .withExecutor(executor) + .withName("secure-storage") + .withSecurityProtocol(FaultTolerantHttpClient.SECURITY_PROTOCOL_TLS_1_3) + .withTrustedServerCertificates(configuration.storageCaCertificates().toArray(new String[0])) + .build(); + } - public CompletableFuture deleteStoredData(final UUID accountUuid) { - final ExternalServiceCredentials credentials = storageServiceCredentialsGenerator.generateForUuid(accountUuid); + public CompletableFuture deleteStoredData(final UUID accountUuid) { + final ExternalServiceCredentials credentials = storageServiceCredentialsGenerator.generateForUuid(accountUuid); - final HttpRequest request = HttpRequest.newBuilder() - .uri(deleteUri) - .DELETE() - .header(HttpHeaders.AUTHORIZATION, basicAuthHeader(credentials)) - .build(); + final HttpRequest request = HttpRequest.newBuilder() + .uri(deleteUri) + .DELETE() + .header(HttpHeaders.AUTHORIZATION, basicAuthHeader(credentials)) + .build(); return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()).thenApply(response -> { if (HttpUtils.isSuccessfulResponse(response.statusCode())) { diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/securevaluerecovery/SecureValueRecovery2Client.java b/service/src/main/java/org/whispersystems/textsecuregcm/securevaluerecovery/SecureValueRecovery2Client.java index 1d5233fc7..465958be5 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/securevaluerecovery/SecureValueRecovery2Client.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/securevaluerecovery/SecureValueRecovery2Client.java @@ -18,6 +18,7 @@ import java.time.Duration; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; +import java.util.concurrent.ScheduledExecutorService; import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentials; import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialsGenerator; import org.whispersystems.textsecuregcm.configuration.SecureValueRecovery2Configuration; @@ -37,13 +38,15 @@ public class SecureValueRecovery2Client { static final String DELETE_PATH = "/v1/delete"; public SecureValueRecovery2Client(final ExternalServiceCredentialsGenerator secureValueRecoveryCredentialsGenerator, - final Executor executor, final SecureValueRecovery2Configuration configuration) + final Executor executor, final ScheduledExecutorService retryExecutor, + final SecureValueRecovery2Configuration configuration) throws CertificateException { this.secureValueRecoveryCredentialsGenerator = secureValueRecoveryCredentialsGenerator; this.deleteUri = URI.create(configuration.uri()).resolve(DELETE_PATH); this.httpClient = FaultTolerantHttpClient.newBuilder() .withCircuitBreaker(configuration.circuitBreaker()) .withRetry(configuration.retry()) + .withRetryExecutor(retryExecutor) .withVersion(HttpClient.Version.HTTP_1_1) .withConnectTimeout(Duration.ofSeconds(10)) .withRedirect(HttpClient.Redirect.NEVER) diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/BraintreeManager.java b/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/BraintreeManager.java index 5be93ac18..5a3405599 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/BraintreeManager.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/BraintreeManager.java @@ -32,6 +32,7 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.Executor; +import java.util.concurrent.ScheduledExecutorService; import javax.annotation.Nullable; import javax.ws.rs.ClientErrorException; import javax.ws.rs.WebApplicationException; @@ -61,7 +62,8 @@ public class BraintreeManager implements SubscriptionProcessorManager { final Map currenciesToMerchantAccounts, final String graphqlUri, final CircuitBreakerConfiguration circuitBreakerConfiguration, - final Executor executor) { + final Executor executor, + final ScheduledExecutorService retryExecutor) { this(new BraintreeGateway(braintreeEnvironment, braintreeMerchantId, braintreePublicKey, braintreePrivateKey), @@ -71,6 +73,7 @@ public class BraintreeManager implements SubscriptionProcessorManager { .withName("braintree-graphql") .withCircuitBreaker(circuitBreakerConfiguration) .withExecutor(executor) + .withRetryExecutor(retryExecutor) .build(), graphqlUri, braintreePublicKey, braintreePrivateKey), executor); } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/workers/AssignUsernameCommand.java b/service/src/main/java/org/whispersystems/textsecuregcm/workers/AssignUsernameCommand.java index 8e9ec24a4..ac1d78d5a 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/workers/AssignUsernameCommand.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/workers/AssignUsernameCommand.java @@ -18,6 +18,7 @@ import java.util.List; import java.util.UUID; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparser; import org.whispersystems.textsecuregcm.WhisperServerConfiguration; @@ -110,6 +111,10 @@ public class AssignUsernameCommand extends EnvironmentCommand secureStorageClient.deleteStoredData(accountUuid).join()); - assertTrue(completionException.getCause() instanceof SecureStorageException); - } + final String username = RandomStringUtils.randomAlphabetic(16); + final String password = RandomStringUtils.randomAlphanumeric(32); + + when(credentialsGenerator.generateForUuid(accountUuid)).thenReturn( + new ExternalServiceCredentials(username, password)); + + wireMock.stubFor(delete(urlEqualTo(SecureStorageClient.DELETE_PATH)) + .withBasicAuth(username, password) + .willReturn(aResponse().withStatus(400))); + + final CompletionException completionException = assertThrows(CompletionException.class, + () -> secureStorageClient.deleteStoredData(accountUuid).join()); + assertTrue(completionException.getCause() instanceof SecureStorageException); + } } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/securevaluerecovery/SecureValueRecovery2ClientTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/securevaluerecovery/SecureValueRecovery2ClientTest.java index 31f941c55..9e21628d8 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/securevaluerecovery/SecureValueRecovery2ClientTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/securevaluerecovery/SecureValueRecovery2ClientTest.java @@ -23,6 +23,7 @@ import java.util.UUID; import java.util.concurrent.CompletionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import org.apache.commons.lang3.RandomStringUtils; import org.junit.jupiter.api.AfterEach; @@ -38,6 +39,7 @@ class SecureValueRecovery2ClientTest { private UUID accountUuid; private ExternalServiceCredentialsGenerator credentialsGenerator; private ExecutorService httpExecutor; + private ScheduledExecutorService retryExecutor; private SecureValueRecovery2Client secureValueRecovery2Client; @@ -51,6 +53,7 @@ class SecureValueRecovery2ClientTest { accountUuid = UUID.randomUUID(); credentialsGenerator = mock(ExternalServiceCredentialsGenerator.class); httpExecutor = Executors.newSingleThreadExecutor(); + retryExecutor = Executors.newSingleThreadScheduledExecutor(); final SecureValueRecovery2Configuration config = new SecureValueRecovery2Configuration( "http://localhost:" + wireMock.getPort(), @@ -104,13 +107,16 @@ class SecureValueRecovery2ClientTest { """), null, null); - secureValueRecovery2Client = new SecureValueRecovery2Client(credentialsGenerator, httpExecutor, config); + secureValueRecovery2Client = new SecureValueRecovery2Client(credentialsGenerator, httpExecutor, retryExecutor, + config); } @AfterEach void tearDown() throws InterruptedException { httpExecutor.shutdown(); httpExecutor.awaitTermination(1, TimeUnit.SECONDS); + retryExecutor.shutdown(); + retryExecutor.awaitTermination(1, TimeUnit.SECONDS); } @Test diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/tests/http/FaultTolerantHttpClientTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/tests/http/FaultTolerantHttpClientTest.java index a3d963b9b..9d882ce8f 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/tests/http/FaultTolerantHttpClientTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/tests/http/FaultTolerantHttpClientTest.java @@ -20,7 +20,12 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.whispersystems.textsecuregcm.configuration.CircuitBreakerConfiguration; @@ -34,21 +39,38 @@ class FaultTolerantHttpClientTest { .options(wireMockConfig().dynamicPort().dynamicHttpsPort()) .build(); + private ExecutorService httpExecutor; + private ScheduledExecutorService retryExecutor; + + @BeforeEach + void setUp() { + httpExecutor = Executors.newSingleThreadExecutor(); + retryExecutor = Executors.newSingleThreadScheduledExecutor(); + } + + @AfterEach + void tearDown() throws InterruptedException { + httpExecutor.shutdown(); + httpExecutor.awaitTermination(1, TimeUnit.SECONDS); + retryExecutor.shutdown(); + retryExecutor.awaitTermination(1, TimeUnit.SECONDS); + } + @Test void testSimpleGet() { wireMock.stubFor(get(urlEqualTo("/ping")) - .willReturn(aResponse() - .withHeader("Content-Type", "text/plain") - .withBody("Pong!"))); - + .willReturn(aResponse() + .withHeader("Content-Type", "text/plain") + .withBody("Pong!"))); FaultTolerantHttpClient client = FaultTolerantHttpClient.newBuilder() - .withCircuitBreaker(new CircuitBreakerConfiguration()) - .withRetry(new RetryConfiguration()) - .withExecutor(Executors.newSingleThreadExecutor()) - .withName("test") - .withVersion(HttpClient.Version.HTTP_2) - .build(); + .withCircuitBreaker(new CircuitBreakerConfiguration()) + .withRetry(new RetryConfiguration()) + .withExecutor(httpExecutor) + .withRetryExecutor(retryExecutor) + .withName("test") + .withVersion(HttpClient.Version.HTTP_2) + .build(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("http://localhost:" + wireMock.getPort() + "/ping")) @@ -72,12 +94,13 @@ class FaultTolerantHttpClientTest { .withBody("Pong!"))); FaultTolerantHttpClient client = FaultTolerantHttpClient.newBuilder() - .withCircuitBreaker(new CircuitBreakerConfiguration()) - .withRetry(new RetryConfiguration()) - .withExecutor(Executors.newSingleThreadExecutor()) - .withName("test") - .withVersion(HttpClient.Version.HTTP_2) - .build(); + .withCircuitBreaker(new CircuitBreakerConfiguration()) + .withRetry(new RetryConfiguration()) + .withExecutor(httpExecutor) + .withRetryExecutor(retryExecutor) + .withName("test") + .withVersion(HttpClient.Version.HTTP_2) + .build(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("http://localhost:" + wireMock.getPort() + "/failure")) @@ -104,7 +127,8 @@ class FaultTolerantHttpClientTest { FaultTolerantHttpClient client = FaultTolerantHttpClient.newBuilder() .withCircuitBreaker(circuitBreakerConfiguration) .withRetry(new RetryConfiguration()) - .withExecutor(Executors.newSingleThreadExecutor()) + .withRetryExecutor(retryExecutor) + .withExecutor(httpExecutor) .withName("test") .withVersion(HttpClient.Version.HTTP_2) .build();