Configure Thread-Safe Java SDK Clients with Connection Pooling
What You Will Build
- You will build a Java application that initializes a single, thread-safe
PureCloudPlatformClientV2instance configured with an optimized Apache HTTP Client connection pool. - This configuration prevents port exhaustion and socket leaks in high-concurrency environments by reusing underlying TCP connections.
- The tutorial covers Java 17+ using the official Genesys Cloud CX Java SDK (
com.mypurecloud.api:platform-client-v2).
Prerequisites
- Java Version: Java 17 or later (LTS).
- SDK Version:
platform-client-v2version 167.0.0 or later. - Build Tool: Maven or Gradle.
- Dependencies:
com.mypurecloud.api:platform-client-v2:167.0.0org.apache.httpcomponents:httpclient:4.5.14(Transitive dependency, but good to pin for stability).
- Genesys Cloud Environment:
- An OAuth Client ID and Client Secret.
- A valid scope for the API calls used in this tutorial (e.g.,
user:readfor testing connectivity,analytics:queryfor data retrieval).
Authentication Setup
The Genesys Cloud Java SDK handles the OAuth2 Client Credentials flow internally. However, to ensure thread safety and performance, you must configure the underlying HTTP client before initializing the PureCloudPlatformClientV2. The SDK uses Apache HttpClient 4.x under the hood. By default, it uses a basic connection manager that is not thread-safe for concurrent requests. You must inject a PoolingHttpClientConnectionManager.
Step 1: Define the Connection Pool Configuration
Create a utility class to manage the HTTP client configuration. This class will define the socket timeouts, connection timeouts, and the pool size.
package com.example.genesys.config;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.impl.client.PoolingHttpClientConnectionManager;
import org.apache.http.ssl.SSLContextBuilder;
import javax.net.ssl.SSLContext;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
/**
* Configures the underlying Apache HTTP Client connection manager for the Genesys SDK.
* This ensures thread-safe connection pooling.
*/
public class HttpConfig {
private static final int MAX_TOTAL_CONNECTIONS = 100;
private static final int MAX_CONNECTIONS_PER_ROUTE = 20;
private static final int CONNECT_TIMEOUT_MS = 5000;
private static final int SOCKET_TIMEOUT_MS = 10000;
private static final int TIME_TO_LIVE_MS = 300000; // 5 minutes
/**
* Creates a thread-safe PoolingHttpClientConnectionManager.
*
* @return The configured connection manager.
*/
public static PoolingHttpClientConnectionManager createConnectionManager() {
try {
// Trust all certificates for development ease.
// In production, use a proper TrustManager or the default system trust store.
SSLContext sslContext = SSLContextBuilder.create()
.loadTrustMaterial(null, (TrustStrategy) (chain, authType) -> true)
.build();
SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslContext);
PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(sslSocketFactory);
// Set global max connections
connManager.setMaxTotal(MAX_TOTAL_CONNECTIONS);
// Set max connections per host (route). Genesys API endpoints are distributed across subdomains.
connManager.setDefaultMaxPerRoute(MAX_CONNECTIONS_PER_ROUTE);
return connManager;
} catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException e) {
throw new RuntimeException("Failed to initialize SSL Context for Genesys HTTP Client", e);
}
}
public static int getConnectTimeoutMs() {
return CONNECT_TIMEOUT_MS;
}
public static int getSocketTimeoutMs() {
return SOCKET_TIMEOUT_MS;
}
}
Step 2: Initialize the Thread-Safe SDK Client
The PureCloudPlatformClientV2 constructor allows you to pass a custom ApiClient or configure the underlying HttpClients builder. In recent SDK versions, the recommended approach is to configure the ApiClient directly or use the PlatformClientFactory with a custom builder if available. However, the most robust method for explicit control is to configure the ApiClient instance before passing it to the platform client, or to use the static initialization methods with custom properties.
For this tutorial, we will use the ApiClient configuration approach which is explicit and version-stable.
package com.example.genesys.config;
import com.mypurecloud.api.v2.ApiClient;
import com.mypurecloud.api.v2.PureCloudPlatformClientV2;
import com.mypurecloud.api.v2.auth.OAuthClientCredentials;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.PoolingHttpClientConnectionManager;
/**
* Factory for creating a thread-safe Genesys Cloud Client.
*/
public class GenesysClientFactory {
private static final String CLIENT_ID = System.getenv("GENESYS_CLIENT_ID");
private static final String CLIENT_SECRET = System.getenv("GENESYS_CLIENT_SECRET");
private static final String ENVIRONMENT = "mypurecloud.com"; // Use "mypurecloud.ie" for EU
private static PureCloudPlatformClientV2 platformClient;
/**
* Initializes the platform client with a shared, thread-safe connection pool.
* This method is idempotent and thread-safe.
*
* @return The configured PureCloudPlatformClientV2 instance.
*/
public static synchronized PureCloudPlatformClientV2 getClient() {
if (platformClient == null) {
try {
// 1. Create the connection manager
PoolingHttpClientConnectionManager connManager = HttpConfig.createConnectionManager();
// 2. Build the Apache HttpClient
CloseableHttpClient httpClient = HttpClientBuilder.create()
.setConnectionManager(connManager)
.setConnectTimeout(HttpConfig.getConnectTimeoutMs())
.setSocketTimeout(HttpConfig.getSocketTimeoutMs())
.build();
// 3. Create the ApiClient and inject the custom HttpClient
// Note: The constructor signature may vary slightly by SDK version.
// In v167+, you typically configure the ApiClient via setters or a builder.
// If the direct constructor is not available, use the default ApiClient and replace the httpClient.
ApiClient apiClient = new ApiClient();
apiClient.setHttpClient(httpClient); // Inject the thread-safe client
apiClient.setEnvironment(ENVIRONMENT);
// 4. Configure OAuth
OAuthClientCredentials oAuth = new OAuthClientCredentials();
oAuth.setClientId(CLIENT_ID);
oAuth.setClientSecret(CLIENT_SECRET);
oAuth.setGrantType("client_credentials");
// Define scopes. Add scopes as needed for your use case.
oAuth.setScopes("user:read", "analytics:query");
apiClient.setAuthenticator(oAuth);
// 5. Initialize the Platform Client
platformClient = new PureCloudPlatformClientV2(apiClient);
} catch (Exception e) {
throw new RuntimeException("Failed to initialize Genesys Platform Client", e);
}
}
return platformClient;
}
/**
* Shuts down the HTTP client and releases resources.
* Call this during application shutdown.
*/
public static synchronized void shutdown() {
if (platformClient != null) {
platformClient.close();
platformClient = null;
}
}
}
Implementation
Step 3: Verify Connectivity with a Simple API Call
Before implementing complex logic, verify that the thread-safe client can authenticate and retrieve data. We will use the UsersApi to list users. This is a lightweight operation that tests authentication and basic HTTP connectivity.
Required Scope: user:read
package com.example.genesys.demo;
import com.mypurecloud.api.v2.PureCloudPlatformClientV2;
import com.mypurecloud.api.v2.api.UsersApi;
import com.mypurecloud.api.v2.model.PagedEntityPresenceList;
import com.example.genesys.config.GenesysClientFactory;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class ConnectivityTest {
public static void main(String[] args) {
PureCloudPlatformClientV2 client = GenesysClientFactory.getClient();
// Create an API client instance for Users
UsersApi usersApi = client.getUsersApi();
try {
System.out.println("Testing connectivity...");
// Fetch the first page of users
// Page size 5, page number 1
PagedEntityPresenceList users = usersApi.getUsers(5, 1, null, null, null, null, null, null);
System.out.println("Successfully fetched " + users.getEntities().size() + " users.");
users.getEntities().forEach(user -> {
System.out.println("User: " + user.getName() + " (" + user.getId() + ")");
});
} catch (Exception e) {
System.err.println("Error during connectivity test:");
e.printStackTrace();
} finally {
GenesysClientFactory.shutdown();
}
}
}
Expected Response:
If successful, the console will print the names and IDs of up to 5 users. If authentication fails, you will receive a 401 Unauthorized exception. If the connection pool is misconfigured, you may encounter SocketTimeoutException or ConnectTimeoutException.
Step 4: Implement Concurrent Data Retrieval
The primary benefit of connection pooling is handling concurrent requests. In this step, we will create a multi-threaded scenario that retrieves analytics data for multiple queues simultaneously. This demonstrates that the single PureCloudPlatformClientV2 instance can serve multiple threads without creating a new TCP connection for each request.
Required Scope: analytics:query
We will use the AnalyticsApi to query conversation details. This is a heavier operation that benefits from connection reuse.
package com.example.genesys.demo;
import com.mypurecloud.api.v2.PureCloudPlatformClientV2;
import com.mypurecloud.api.v2.api.AnalyticsApi;
import com.mypurecloud.api.v2.api.AnalyticsConversationsApi;
import com.mypurecloud.api.v2.model.ConversationDetailsQuery;
import com.mypurecloud.api.v2.model.ConversationDetailsResponse;
import com.mypurecloud.api.v2.model.Sort;
import com.example.genesys.config.GenesysClientFactory;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class ConcurrentAnalyticsDemo {
private static final int THREAD_POOL_SIZE = 10;
private static final int NUM_QUEUES = 5;
private static final String QUEUE_ID_1 = "YOUR_QUEUE_ID_1"; // Replace with actual Queue UUID
private static final String QUEUE_ID_2 = "YOUR_QUEUE_ID_2"; // Replace with actual Queue UUID
public static void main(String[] args) {
PureCloudPlatformClientV2 client = GenesysClientFactory.getClient();
AnalyticsConversationsApi analyticsApi = client.getAnalyticsConversationsApi();
ExecutorService executor = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
List<Future<Long>> futures = new java.util.ArrayList<>();
// Simulate multiple concurrent queries
for (int i = 0; i < NUM_QUEUES; i++) {
// Alternate between two queue IDs for demonstration
String queueId = (i % 2 == 0) ? QUEUE_ID_1 : QUEUE_ID_2;
futures.add(executor.submit(() -> {
return fetchConversationCount(analyticsApi, queueId);
}));
}
// Collect results
for (Future<Long> future : futures) {
try {
long count = future.get(30, TimeUnit.SECONDS);
System.out.println("Queue processed. Conversations found: " + count);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
System.err.println("Error processing queue: " + e.getMessage());
}
}
executor.shutdown();
try {
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
}
GenesysClientFactory.shutdown();
}
/**
* Queries the Analytics API for conversation details.
*
* @param analyticsApi The configured Analytics API client.
* @param queueId The UUID of the queue to query.
* @return The total number of conversations matching the query.
*/
private static long fetchConversationCount(AnalyticsConversationsApi analyticsApi, String queueId) {
try {
// Construct the query body
ConversationDetailsQuery query = new ConversationDetailsQuery();
// Set date range: Last 24 hours
java.time.ZonedDateTime now = java.time.ZonedDateTime.now();
java.time.ZonedDateTime start = now.minusHours(24);
query.setStartTime(start.toString());
query.setEndTime(now.toString());
// Filter by queue ID
query.setFilter(
new com.mypurecloud.api.v2.model.Filter()
.field("queueId")
.op("eq")
.value(queueId)
);
// Set sort order
query.setSort(
new Sort()
.field("startTime")
.order("desc")
);
// Execute the query
// Note: The SDK method signature may vary.
// In v167+, it is typically: postAnalyticsConversationsDetailsQuery(ConversationDetailsQuery body)
ConversationDetailsResponse response = analyticsApi.postAnalyticsConversationsDetailsQuery(query);
if (response != null && response.getEntities() != null) {
return response.getEntities().size();
}
return 0;
} catch (Exception e) {
System.err.println("API Error for Queue " + queueId + ": " + e.getMessage());
// Log the full stack trace for debugging 4xx/5xx errors
e.printStackTrace();
return -1;
}
}
}
Key Implementation Details:
- Shared Client: The
analyticsApiinstance is derived from the sameplatformClientinstance used in Step 3. This ensures all threads share the same connection pool. - Thread Safety: The
PoolingHttpClientConnectionManageris thread-safe. Multiple threads callingfetchConversationCountwill borrow connections from the pool, use them, and return them. - Pagination: The
postAnalyticsConversationsDetailsQueryendpoint returns paginated results. This example only fetches the first page. For production, you must implement pagination logic using thenextPageURI provided in the response headers or body.
Step 5: Handling Pagination and Retry Logic
Genesys Cloud APIs use standard HTTP pagination. When processing large datasets, you must handle pagination to avoid missing data. Additionally, you should implement retry logic for transient errors (429 Too Many Requests, 5xx Server Errors).
The Java SDK does not automatically retry 429 errors. You must implement this in your application code.
package com.example.genesys.demo;
import com.mypurecloud.api.v2.PureCloudPlatformClientV2;
import com.mypurecloud.api.v2.api.AnalyticsConversationsApi;
import com.mypurecloud.api.v2.model.ConversationDetailsQuery;
import com.mypurecloud.api.v2.model.ConversationDetailsResponse;
import com.mypurecloud.api.v2.model.Sort;
import com.example.genesys.config.GenesysClientFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class PaginatedAnalyticsFetcher {
private static final Logger logger = LoggerFactory.getLogger(PaginatedAnalyticsFetcher.class);
private static final int MAX_RETRIES = 3;
private static final long RETRY_DELAY_MS = 1000;
public static void main(String[] args) {
PureCloudPlatformClientV2 client = GenesysClientFactory.getClient();
AnalyticsConversationsApi analyticsApi = client.getAnalyticsConversationsApi();
String queueId = "YOUR_QUEUE_ID_1"; // Replace with actual Queue UUID
try {
List<String> allConversationIds = fetchAllConversations(analyticsApi, queueId);
System.out.println("Total conversations fetched: " + allConversationIds.size());
} catch (Exception e) {
logger.error("Failed to fetch conversations", e);
} finally {
GenesysClientFactory.shutdown();
}
}
/**
* Fetches all conversations for a queue, handling pagination and retries.
*/
public static List<String> fetchAllConversations(AnalyticsConversationsApi analyticsApi, String queueId) {
List<String> conversationIds = new ArrayList<>();
String nextPageUri = null;
int retryCount = 0;
// Initial query
ConversationDetailsQuery query = buildQuery(queueId);
do {
boolean success = false;
ConversationDetailsResponse response = null;
// Retry loop for transient errors
while (retryCount < MAX_RETRIES) {
try {
if (nextPageUri == null) {
// First page
response = analyticsApi.postAnalyticsConversationsDetailsQuery(query);
} else {
// Subsequent pages
// Note: The SDK may not have a direct method for nextPageUri.
// You may need to use the generic ApiClient call or construct the URL manually.
// For simplicity, we assume the SDK handles pagination via a specific method if available.
// If not, you must use the ApiClient's generic execute method.
// Here we assume a hypothetical method for clarity. In reality, check SDK docs for pagination helpers.
throw new UnsupportedOperationException("Pagination via nextPageUri requires custom HTTP call in this SDK version.");
}
success = true;
break; // Exit retry loop on success
} catch (Exception e) {
int statusCode = extractStatusCode(e);
if (statusCode == 429 || (statusCode >= 500 && statusCode < 600)) {
retryCount++;
logger.warn("Transient error (Status: {}). Retrying in {} ms...", statusCode, RETRY_DELAY_MS);
try {
Thread.sleep(RETRY_DELAY_MS * retryCount); // Exponential backoff
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException("Interrupted during retry", ie);
}
} else {
throw e; // Non-retryable error
}
}
}
if (!success) {
throw new RuntimeException("Max retries exceeded for conversation query.");
}
// Process results
if (response != null && response.getEntities() != null) {
response.getEntities().forEach(entity -> {
conversationIds.add(entity.getId());
});
// Check for next page
// The SDK response object may contain a 'nextPage' field or header.
// Check the specific SDK model for ConversationDetailsResponse.
if (response.getNextPage() != null) {
nextPageUri = response.getNextPage().toString();
} else {
nextPageUri = null;
}
}
} while (nextPageUri != null);
return conversationIds;
}
private static ConversationDetailsQuery buildQuery(String queueId) {
ConversationDetailsQuery query = new ConversationDetailsQuery();
java.time.ZonedDateTime now = java.time.ZonedDateTime.now();
query.setStartTime(now.minusHours(24).toString());
query.setEndTime(now.toString());
query.setFilter(
new com.mypurecloud.api.v2.model.Filter()
.field("queueId")
.op("eq")
.value(queueId)
);
query.setPageSize(100); // Max page size
return query;
}
private static int extractStatusCode(Exception e) {
// Helper to extract status code from SDK exceptions
// Implementation depends on specific SDK exception classes
if (e instanceof com.mypurecloud.api.v2.ApiException) {
return ((com.mypurecloud.api.v2.ApiException) e).getCode();
}
return -1;
}
}
Note on Pagination: The Genesys Cloud Java SDK’s handling of pagination can be verbose. Always check the ApiResponse or the specific model class for nextPage or links fields. If the SDK does not provide a helper method, you must use the ApiClient’s generic execute method with the nextPage URI.
Complete Working Example
Below is the complete pom.xml and the main class structure for a standalone application.
Maven pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>genesys-sdk-pooling</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<genesys.sdk.version>167.0.0</genesys.sdk.version>
</properties>
<dependencies>
<!-- Genesys Cloud Java SDK -->
<dependency>
<groupId>com.mypurecloud.api</groupId>
<artifactId>platform-client-v2</artifactId>
<version>${genesys.sdk.version}</version>
</dependency>
<!-- SLF4J for Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.9</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
</plugin>
</plugins>
</build>
</project>
Main Application Class
package com.example.genesys;
import com.mypurecloud.api.v2.PureCloudPlatformClientV2;
import com.mypurecloud.api.v2.api.UsersApi;
import com.mypurecloud.api.v2.model.PagedEntityPresenceList;
import com.example.genesys.config.GenesysClientFactory;
public class Application {
public static void main(String[] args) {
System.out.println("Starting Genesys Cloud SDK Demo...");
try {
// 1. Get the thread-safe client
PureCloudPlatformClientV2 client = GenesysClientFactory.getClient();
UsersApi usersApi = client.getUsersApi();
// 2. Execute a simple API call
System.out.println("Fetching users...");
PagedEntityPresenceList users = usersApi.getUsers(5, 1, null, null, null, null, null, null);
System.out.println("Fetched " + users.getEntities().size() + " users.");
users.getEntities().forEach(user ->
System.out.println(" - " + user.getName() + " (" + user.getId() + ")")
);
} catch (Exception e) {
System.err.println("Application error:");
e.printStackTrace();
} finally {
// 3. Shutdown to release resources
GenesysClientFactory.shutdown();
System.out.println("Application shutdown complete.");
}
}
}
Common Errors & Debugging
Error: javax.net.ssl.SSLHandshakeException
- Cause: The SSL context is not configured correctly, or the trust store does not trust the Genesys Cloud certificate.
- Fix: Ensure the
SSLContextBuilderinHttpConfigis correctly initialized. In production, remove theTrustStrategythat accepts all certificates and use the default system trust store.
Error: java.util.concurrent.TimeoutException
- Cause: The request took longer than the configured timeout.
- Fix: Increase
SOCKET_TIMEOUT_MSandCONNECT_TIMEOUT_MSinHttpConfig. Analytics queries can take several seconds.
Error: 429 Too Many Requests
- Cause: You have exceeded the API rate limit.
- Fix: Implement retry logic with exponential backoff as shown in Step 5. Monitor the
Retry-Afterheader in the response.
Error: ConnectionPoolTimeoutException
- Cause: The connection pool is exhausted. All connections are in use, and no new connections can be created because
MAX_TOTAL_CONNECTIONShas been reached. - Fix: Increase
MAX_TOTAL_CONNECTIONSandMAX_CONNECTIONS_PER_ROUTEinHttpConfig. Ensure that connections are being returned to the pool (i.e., you are not holding onto HTTP responses indefinitely).
Error: 401 Unauthorized
- Cause: Invalid Client ID/Secret or missing scopes.
- Fix: Verify the environment variables
GENESYS_CLIENT_IDandGENESYS_CLIENT_SECRET. Ensure the OAuth client in the Genesys Admin console has the correct scopes assigned.