Configuring Thread-Safe HTTP Client and Connection Pooling for Genesys Cloud Java SDK
What You Will Build
- You will build a production-grade Java application that initializes the Genesys Cloud SDK with a custom, thread-safe
ApacheHttpClientconfigured for optimal connection pooling. - This tutorial uses the Genesys Cloud Platform Client V2 (
platform-client-java) SDK. - The implementation is in Java 17+ using Apache HttpClient 5 and the official Genesys SDK.
Prerequisites
- OAuth Client Type: Public Client or Confidential Client (depending on your app architecture).
- Required Scopes:
analytics:reports:view,user:read(for verification). - SDK Version:
platform-client-javav100+ (current stable). - Runtime: Java 17 or later (LTS recommended).
- Dependencies:
com.mulesoft.muleesb.platform-client-java(Genesys SDK)org.apache.httpcomponents.client5:httpclient5(Apache HttpClient 5)com.fasterxml.jackson.core:jackson-databind(JSON processing)
Authentication Setup
The Genesys Cloud Java SDK uses an OAuthClient to manage token acquisition and refresh. To ensure thread safety across a multi-threaded application (such as a web server or batch processor), you must initialize the OAuthClient once and share it across threads, or use the SDK’s built-in static initializer which handles basic caching. For advanced control over the underlying HTTP client and connection pooling, we will bypass the default static initialization and construct our own ApiClient instance.
The OAuthClient requires a ApiClient instance. This is where we inject our custom HTTP configuration.
import com.mulesoft.muleesb.platform.client.v2.ApiClient;
import com.mulesoft.muleesb.platform.client.v2.auth.OAuthClient;
import com.mulesoft.muleesb.platform.client.v2.auth.OAuthConfig;
public class GenesysAuth {
private static final String CLIENT_ID = "your_client_id";
private static final String CLIENT_SECRET = "your_client_secret";
private static final String GRANT_TYPE = "client_credentials";
/**
* Creates a thread-safe OAuthClient instance.
* This instance can be shared across multiple threads.
*/
public static OAuthClient createOAuthClient(ApiClient customApiClient) {
OAuthConfig config = OAuthConfig.builder()
.clientId(CLIENT_ID)
.clientSecret(CLIENT_SECRET)
.grantType(GRANT_TYPE)
.build();
// The OAuthClient is thread-safe for token retrieval
return new OAuthClient(customApiClient, config);
}
}
Note: The OAuthClient class in the Genesys SDK is designed to be thread-safe. It caches tokens and handles refresh logic internally. You should never create a new OAuthClient for every API call. Create one instance per application lifecycle.
Implementation
Step 1: Configure the Apache HttpClient with Connection Pooling
The Genesys Cloud Java SDK allows you to pass a custom java.net.http.HttpClient or an ApacheHttpClient wrapper. However, for fine-grained control over connection pooling, timeouts, and thread safety, the SDK provides a bridge to Apache HttpClient 5.
Default HTTP clients often open a new connection for every request or use a small default pool. In high-throughput scenarios, this causes connection exhaustion and increased latency. We will configure a PoolingHttpClientConnectionManager with explicit pool limits.
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.core5.http.io.SocketConfig;
import org.apache.hc.core5.util.Timeout;
import java.time.Duration;
public class HttpConfigurator {
/**
* Builds a thread-safe Apache HttpClient with connection pooling.
*
* @return Configured CloseableHttpClient
*/
public static CloseableHttpClient buildThreadSafeHttpClient() {
// 1. Define Timeouts
// Connect timeout: Time to establish a TCP connection
Timeout connectTimeout = Timeout.of(Duration.ofSeconds(5));
// Socket timeout: Time waiting for data after connection is established
Timeout socketTimeout = Timeout.of(Duration.ofSeconds(30));
// Request timeout: Total time for the entire request execution
Timeout requestTimeout = Timeout.of(Duration.ofSeconds(45));
// 2. Configure Socket Settings
SocketConfig socketConfig = SocketConfig.custom()
.setSoTimeout(socketTimeout)
.build();
// 3. Configure Connection Pool
// Max total connections: Total number of connections allowed in the pool
// Default max per route: Max connections to a single host (e.g., api.mypurecloud.com)
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
connectionManager.setMaxTotal(200); // Adjust based on expected throughput
connectionManager.setDefaultMaxPerRoute(50); // Genesys endpoints usually allow high concurrency
// 4. Build the Client
return HttpClients.custom()
.setConnectionManager(connectionManager)
.setDefaultSocketConfig(socketConfig)
.setConnectionTimeToLive(Duration.ofMinutes(5)) // Keep connections alive for 5 minutes
.disableAutomaticRetries() // Genesys SDK handles retries for 429s; disable HTTP level retries to avoid double-processing
.build();
}
}
Why this configuration matters:
setMaxTotal(200): Prevents the application from opening thousands of sockets, which can exhaust OS file descriptors.setDefaultMaxPerRoute(50): Ensures that if one Genesys API endpoint becomes slow, it does not block all other API calls to different endpoints.disableAutomaticRetries(): The Genesys SDK has its own retry logic for transient errors (like 429 Too Many Requests). Enabling HTTP-level retries can lead to unexpected behavior where the SDK receives a successful response from a retried request that was logically intended to fail or be handled differently.
Step 2: Initialize the Genesys ApiClient with Custom HTTP
Now that we have a configured CloseableHttpClient, we need to wrap it in the Genesys SDK’s ApiClient. The SDK provides a factory method or constructor that accepts an Apache HttpClient.
import com.mulesoft.muleesb.platform.client.v2.ApiClient;
import com.mulesoft.muleesb.platform.client.v2.auth.OAuthClient;
import com.mulesoft.muleesb.platform.client.v2.auth.OAuthConfig;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
public class GenesysClientFactory {
private static final String CLIENT_ID = "your_client_id";
private static final String CLIENT_SECRET = "your_client_secret";
private static final String BASE_URL = "https://api.mypurecloud.com";
private static volatile ApiClient apiClient;
private static volatile OAuthClient oauthClient;
/**
* Singleton-like initialization for thread-safe access.
* In a Spring/Web environment, inject these as beans.
*/
public static synchronized ApiClient getApiClient() {
if (apiClient == null) {
initClient();
}
return apiClient;
}
public static synchronized OAuthClient getOAuthClient() {
if (oauthClient == null) {
initClient();
}
return oauthClient;
}
private static void initClient() {
// 1. Get the custom HTTP client
CloseableHttpClient httpClient = HttpConfigurator.buildThreadSafeHttpClient();
// 2. Create the ApiClient with the custom HTTP client
// The SDK internally wraps this to handle JSON serialization/deserialization
apiClient = ApiClient.builder()
.withHttpClient(httpClient)
.withBasePath(BASE_URL)
.build();
// 3. Configure OAuth
OAuthConfig oAuthConfig = OAuthConfig.builder()
.clientId(CLIENT_ID)
.clientSecret(CLIENT_SECRET)
.grantType("client_credentials")
.build();
// 4. Create the OAuth Client
oauthClient = new OAuthClient(apiClient, oAuthConfig);
}
}
Critical Note on Thread Safety:
The ApiClient class in the Genesys Java SDK is not thread-safe for all operations if you manually modify its configuration (like headers) after initialization. However, if you initialize it once with a fixed configuration and a thread-safe HTTP client (as we did above), the resulting ApiClient instance is safe to use across multiple threads for making requests. The underlying CloseableHttpClient is thread-safe.
Step 3: Making Thread-Safe API Calls
With the client initialized, you can now make API calls. The SDK generates API classes (e.g., AnalyticsApi, UsersApi) that use the ApiClient for HTTP transport.
We will demonstrate fetching a user by ID and querying analytics data. These operations are independent and can be run in parallel.
import com.mulesoft.muleesb.platform.client.v2.api.AnalyticsApi;
import com.mulesoft.muleesb.platform.client.v2.api.UsersApi;
import com.mulesoft.muleesb.platform.client.v2.auth.OAuthClient;
import com.mulesoft.muleesb.platform.client.v2.model.User;
import com.mulesoft.muleesb.platform.client.v2.model.AnalyticsConversationDetailsQuery;
import com.mulesoft.muleesb.platform.client.v2.model.AnalyticsConversationDetailsResponse;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class GenesysWorker {
private final UsersApi usersApi;
private final AnalyticsApi analyticsApi;
private final OAuthClient oauthClient;
public GenesysWorker() {
this.oauthClient = GenesysClientFactory.getOAuthClient();
// APIs use the ApiClient internally via the OAuthClient
this.usersApi = new UsersApi();
this.analyticsApi = new AnalyticsApi();
}
/**
* Demonstrates parallel API calls using a thread-safe setup.
*/
public void runParallelCalls() throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(2);
// Task 1: Get User
CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(() -> {
try {
// Ensure token is valid before calling
oauthClient.getAccessToken();
return usersApi.getUser("user-id-here", null, null, null, null);
} catch (Exception e) {
throw new RuntimeException("Failed to get user", e);
}
}, executor);
// Task 2: Query Analytics
CompletableFuture<AnalyticsConversationDetailsResponse> analyticsFuture = CompletableFuture.supplyAsync(() -> {
try {
oauthClient.getAccessToken();
AnalyticsConversationDetailsQuery query = AnalyticsConversationDetailsQuery.builder()
.view("default")
.groupBy("conversationId")
.dateFrom("2023-01-01T00:00:00Z")
.dateTo("2023-01-31T23:59:59Z")
.build();
return analyticsApi.postAnalyticsConversationsDetailsQuery(query);
} catch (Exception e) {
throw new RuntimeException("Failed to query analytics", e);
}
}, executor);
// Await results
User user = userFuture.get();
AnalyticsConversationDetailsResponse analytics = analyticsFuture.get();
System.out.println("Retrieved user: " + user.getName());
System.out.println("Analytics data points: " + analytics.getEntities().size());
executor.shutdown();
}
}
Error Handling in Parallel Calls:
Notice the try-catch blocks inside the CompletableFuture suppliers. When using parallel calls, you must handle exceptions locally within each thread. If an exception is thrown, it will be wrapped in an ExecutionException when you call .get(). Proper handling involves logging the specific API error (401, 403, 429) and deciding whether to retry or fail gracefully.
Complete Working Example
This is a single-file, runnable Java class that demonstrates the entire flow: configuration, initialization, and execution.
import com.mulesoft.muleesb.platform.client.v2.ApiClient;
import com.mulesoft.muleesb.platform.client.v2.api.UsersApi;
import com.mulesoft.muleesb.platform.client.v2.auth.OAuthClient;
import com.mulesoft.muleesb.platform.client.v2.auth.OAuthConfig;
import com.mulesoft.muleesb.platform.client.v2.model.User;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.core5.http.io.SocketConfig;
import org.apache.hc.core5.util.Timeout;
import java.time.Duration;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class GenesysConnectionPoolDemo {
// Configuration Constants
private static final String CLIENT_ID = "your_client_id";
private static final String CLIENT_SECRET = "your_client_secret";
private static final String BASE_URL = "https://api.mypurecloud.com";
private static final String USER_ID = "your_test_user_id";
private static ApiClient apiClient;
private static OAuthClient oauthClient;
public static void main(String[] args) {
try {
initializeClients();
runParallelOperations();
} catch (Exception e) {
e.printStackTrace();
}
}
private static void initializeClients() throws Exception {
// 1. Build Thread-Safe HttpClient with Connection Pooling
CloseableHttpClient httpClient = buildPooledHttpClient();
// 2. Create ApiClient
apiClient = ApiClient.builder()
.withHttpClient(httpClient)
.withBasePath(BASE_URL)
.build();
// 3. Create OAuthClient
OAuthConfig config = OAuthConfig.builder()
.clientId(CLIENT_ID)
.clientSecret(CLIENT_SECRET)
.grantType("client_credentials")
.build();
oauthClient = new OAuthClient(apiClient, config);
// Pre-fetch token to ensure it is cached
oauthClient.getAccessToken();
System.out.println("OAuth Token acquired and cached.");
}
private static CloseableHttpClient buildPooledHttpClient() {
Timeout connectTimeout = Timeout.of(Duration.ofSeconds(5));
Timeout socketTimeout = Timeout.of(Duration.ofSeconds(30));
SocketConfig socketConfig = SocketConfig.custom()
.setSoTimeout(socketTimeout)
.build();
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
cm.setMaxTotal(100);
cm.setDefaultMaxPerRoute(20);
return HttpClients.custom()
.setConnectionManager(cm)
.setDefaultSocketConfig(socketConfig)
.setConnectionTimeToLive(Duration.ofMinutes(5))
.disableAutomaticRetries()
.build();
}
private static void runParallelOperations() throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(2);
UsersApi usersApi = new UsersApi();
// Simulate multiple concurrent requests
CompletableFuture<Void> task1 = CompletableFuture.runAsync(() -> {
try {
User user = usersApi.getUser(USER_ID, null, null, null, null);
System.out.println(Thread.currentThread().getName() + ": Got user " + user.getName());
} catch (Exception e) {
System.err.println(Thread.currentThread().getName() + ": Error - " + e.getMessage());
}
}, executor);
CompletableFuture<Void> task2 = CompletableFuture.runAsync(() -> {
try {
User user = usersApi.getUser(USER_ID, null, null, null, null);
System.out.println(Thread.currentThread().getName() + ": Got user " + user.getName());
} catch (Exception e) {
System.err.println(Thread.currentThread().getName() + ": Error - " + e.getMessage());
}
}, executor);
CompletableFuture.allOf(task1, task2).join();
executor.shutdown();
}
}
Common Errors & Debugging
Error: java.net.SocketTimeoutException: Read timed out
- What causes it: The server is taking longer to respond than the
socketTimeoutconfigured in theHttpClient. This is common with large Analytics queries. - How to fix it: Increase the
socketTimeoutin theSocketConfigbuilder. For large data exports, increase it to 60-120 seconds. - Code Fix:
Timeout socketTimeout = Timeout.of(Duration.ofSeconds(120));
Error: org.apache.hc.client5.http.classic.HttpHostConnectException
- What causes it: The application cannot reach
api.mypurecloud.com. This is often due to firewall rules, proxy misconfiguration, or DNS issues. - How to fix it: Ensure your network allows outbound HTTPS traffic to port 4443 or 443. If using a proxy, configure the
HttpClientwith aHttpClientBuilderproxy config.
Error: 429 Too Many Requests
- What causes it: You have exceeded the Genesys Cloud API rate limits.
- How to fix it: The Genesys SDK includes a built-in retry mechanism for 429s. Ensure you are not disabling retries globally if you want automatic backoff. If you need more control, implement an exponential backoff strategy in your application logic before calling the API.
- Code Fix (Manual Backoff):
// Pseudo-code for manual retry int retries = 3; for (int i = 0; i < retries; i++) { try { return usersApi.getUser(id, ...); } catch (ApiException e) { if (e.getCode() == 429) { Thread.sleep((long) Math.pow(2, i) * 1000); // Exponential backoff continue; } throw e; } }
Error: ConcurrentModificationException or Thread Safety Issues
- What causes it: Sharing a non-thread-safe object across threads.
- How to fix it: Ensure you are using the
PoolingHttpClientConnectionManageras shown. Do not create a newHttpClientfor each request. Do not modify theApiClientconfiguration after initialization. TheOAuthClientis thread-safe for token retrieval, but do not callsetAccessTokenmanually from multiple threads simultaneously.