Configure Thread-Safe Connection Pooling for Genesys Cloud Java SDK

Configure Thread-Safe Connection Pooling for Genesys Cloud Java SDK

What You Will Build

  • A Java application that initializes a single, thread-safe instance of the Genesys Cloud Platform Client with optimized Apache HttpClient connection pooling.
  • This tutorial uses the Genesys Cloud Java SDK (com.mypurecloud.platform:platform-java) and the underlying Apache HttpClient 5 infrastructure.
  • The code is written in Java 17+ using modern syntax and best practices for enterprise integration.

Prerequisites

  • OAuth Client Type: Confidential Client (Client Credentials Grant) or Public Client (Authorization Code Grant with PKCE). This example assumes Client Credentials for server-side integration.
  • Required Scopes: analytics:conversation:read (for the example query), user:read (for basic validation).
  • SDK Version: com.mypurecloud.platform:platform-java version 100.0.0 or later.
  • Runtime: Java 17 or higher (LTS recommended).
  • External Dependencies:
    • org.apache.httpcomponents.client5:httpclient5 (Transitive dependency of the SDK, but explicit versioning is recommended).
    • org.apache.httpcomponents.core5:httpcore5.

Authentication Setup

The Genesys Cloud Java SDK handles OAuth token acquisition and refresh automatically when configured correctly. However, the default configuration often relies on a naive HTTP client implementation that does not scale well under high concurrency. To fix this, we must inject a custom HttpClient configuration into the PlatformClient builder.

The SDK uses org.openapitools.client.ApiClient internally. This client wraps an org.apache.hc.client5.http.impl.classic.CloseableHttpClient.

Below is the secure way to initialize the client with credentials. Never hardcode secrets. Use environment variables.

import com.mypurecloud.platform.client.ApiClient;
import com.mypurecloud.platform.client.auth.OAuth;
import com.mypurecloud.platform.client.auth.OAuthClientCredentialsFlow;
import com.mypurecloud.platform.client.auth.OAuthScope;
import com.mypurecloud.platform.client.auth.OAuthStrategy;
import com.mypurecloud.platform.client.auth.OAuthToken;
import com.mypurecloud.platform.client.exception.ApiException;
import com.mypurecloud.platform.client.exception.ConfigurationException;
import com.mypurecloud.platform.client.httpclient.HttpClientConfig;
import com.mypurecloud.platform.client.httpclient.StandardHttpClient;
import com.mypurecloud.platform.client.httpclient.StandardHttpClientConfig;
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.client5.http.impl.routing.DefaultProxyRoutePlanner;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.pool.PoolConcurrencyPolicy;
import org.apache.hc.core5.pool.PoolReusePolicy;

import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;

public class GenesysConfig {

    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 REGION = System.getenv("GENESYS_REGION"); // e.g., "mypurecloud.com" or "au.mypurecloud.com"

    /**
     * Creates a thread-safe, pooled HTTP client for use with the Genesys SDK.
     * 
     * @return Configured ApiClient instance
     * @throws ConfigurationException if credentials are missing
     */
    public static ApiClient createThreadSafeApiClient() throws ConfigurationException {
        
        // 1. Validate Credentials
        if (CLIENT_ID == null || CLIENT_SECRET == null || REGION == null) {
            throw new ConfigurationException("Missing required environment variables: GENESYS_CLIENT_ID, GENESYS_CLIENT_SECRET, GENESYS_REGION");
        }

        // 2. Configure Connection Pooling
        // This is the critical step for thread safety and performance.
        PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(
            org.apache.hc.core5.http.io.SocketConfig.custom()
                .setSoTimeout(30, TimeUnit.SECONDS)
                .build(),
            null,
            null,
            org.apache.hc.core5.http.message.BasicElementCodec.createDefault(),
            org.apache.hc.core5.http.message.BasicHeaderValueParser.createDefault(),
            org.apache.hc.core5.http2.Http2UpgradeHandler.createDefault()
        );

        // Set maximum total connections
        connectionManager.setMaxTotal(200);
        // Set maximum connections per route (host)
        connectionManager.setDefaultMaxPerRoute(50);

        // Configure the HTTP Client Builder
        CloseableHttpClient httpClient = HttpClients.custom()
            .setConnectionManager(connectionManager)
            .setKeepAliveStrategy((response, context) -> 60 * 1000L) // Keep connections alive for 60 seconds
            .setDefaultRequestConfig(
                org.apache.hc.client5.http.config.RequestConfig.custom()
                    .setConnectTimeout(5, TimeUnit.SECONDS)
                    .setResponseTimeout(30, TimeUnit.SECONDS)
                    .build()
            )
            .build();

        // 3. Configure the SDK's HttpClient wrapper
        // The SDK provides StandardHttpClient which wraps Apache HttpClient
        HttpClientConfig sdkHttpClientConfig = new StandardHttpClientConfig.Builder()
            .setHttpClient(httpClient)
            .build();

        // 4. Build the ApiClient
        ApiClient apiClient = new ApiClient.Builder(sdkHttpClientConfig)
            .setRegion(REGION)
            .build();

        // 5. Configure OAuth
        OAuth oauth = apiClient.getOAuth();
        
        // Define scopes required for your integration
        List<String> scopes = Collections.singletonList(OAuthScope.ANALYTICS_CONVERSATION_READ);
        
        // Set up Client Credentials Flow
        OAuthClientCredentialsFlow flow = new OAuthClientCredentialsFlow(
            CLIENT_ID,
            CLIENT_SECRET,
            scopes
        );

        // Register the flow with the OAuth manager
        oauth.setStrategy(new OAuthStrategy.Builder(flow).build());

        return apiClient;
    }
}

Implementation

Step 1: Initialize the Analytics API Client

With the ApiClient configured, we can now instantiate the specific API service. In this example, we use the AnalyticsApi to query conversation details. The AnalyticsApi is thread-safe and can be shared across multiple threads because it relies on the underlying thread-safe HttpClient.

import com.mypurecloud.platform.analytics.api.AnalyticsApi;
import com.mypurecloud.platform.analytics.model.ConversationDetailsQuery;
import com.mypurecloud.platform.analytics.model.ConversationDetailsQueryFilter;
import com.mypurecloud.platform.analytics.model.ConversationDetailsQueryFilterType;
import com.mypurecloud.platform.analytics.model.ConversationDetailsQueryGroupBy;
import com.mypurecloud.platform.analytics.model.ConversationDetailsQueryGroupByType;
import com.mypurecloud.platform.analytics.model.ConversationDetailsQueryOrderBy;
import com.mypurecloud.platform.analytics.model.ConversationDetailsQueryOrderByType;
import com.mypurecloud.platform.analytics.model.ConversationDetailsQueryType;
import com.mypurecloud.platform.analytics.model.ConversationDetailsResponse;
import com.mypurecloud.platform.client.exception.ApiException;

import java.time.OffsetDateTime;
import java.util.Collections;
import java.util.List;

public class ConversationAnalyzer {

    private final AnalyticsApi analyticsApi;

    public ConversationAnalyzer(ApiClient apiClient) {
        // Inject the shared ApiClient
        this.analyticsApi = new AnalyticsApi(apiClient);
    }

    /**
     * Queries recent voice conversations.
     * 
     * @return List of conversation details
     * @throws ApiException if the API call fails
     */
    public ConversationDetailsResponse getRecentVoiceConversations() throws ApiException {
        
        // Define the time range (last 24 hours)
        OffsetDateTime endTime = OffsetDateTime.now();
        OffsetDateTime startTime = endTime.minusDays(1);

        // Build the query object
        ConversationDetailsQuery query = new ConversationDetailsQuery();
        
        // 1. Set Type
        query.setType(ConversationDetailsQueryType.DETAIL);

        // 2. Set Time Interval
        query.setIntervalType(ConversationDetailsQueryIntervalType.FIXED);
        query.setFixedIntervalStart(startTime.toString());
        query.setFixedIntervalEnd(endTime.toString());

        // 3. Set Filters (Voice only)
        ConversationDetailsQueryFilter filter = new ConversationDetailsQueryFilter();
        filter.setType(ConversationDetailsQueryFilterType.INCLUDE);
        filter.setField("mediaType");
        filter.setValues(Collections.singletonList("voice"));
        query.setFilters(Collections.singletonList(filter));

        // 4. Set Order By (Most recent first)
        ConversationDetailsQueryOrderBy orderBy = new ConversationDetailsQueryOrderBy();
        orderBy.setField("startDateTime");
        orderBy.setType(ConversationDetailsQueryOrderByType.DESC);
        query.setOrderBy(Collections.singletonList(orderBy));

        // 5. Set Group By (None for detail query)
        query.setGroupBy(Collections.emptyList());

        // 6. Set Page Size
        query.setPageSize(25);

        // Execute the query
        return analyticsApi.postAnalyticsConversationsDetailsQuery(query);
    }
}

Step 2: Handle Pagination and Rate Limiting

The Genesys Cloud API enforces strict rate limits (429 Too Many Requests). The default HttpClient does not automatically retry these requests. While the SDK provides some basic error handling, production code must implement exponential backoff for 429 errors.

Additionally, large datasets require pagination. The ConversationDetailsResponse contains a nextPageToken if more data is available.

import com.mypurecloud.platform.analytics.model.ConversationDetailsResponse;
import com.mypurecloud.platform.client.exception.ApiException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

public class PaginatedAnalyzer {

    private static final Logger logger = LoggerFactory.getLogger(PaginatedAnalyzer.class);
    private final AnalyticsApi analyticsApi;
    private final int maxRetries = 3;

    public PaginatedAnalyzer(ApiClient apiClient) {
        this.analyticsApi = new AnalyticsApi(apiClient);
    }

    /**
     * Retrieves all conversations matching the query, handling pagination and retries.
     */
    public List<ConversationDetailsResponse> getAllConversations(ConversationDetailsQuery initialQuery) throws Exception {
        List<ConversationDetailsResponse> allResults = new ArrayList<>();
        ConversationDetailsResponse response;
        int currentPage = 1;
        ConversationDetailsQuery currentQuery = initialQuery;

        // Ensure pageSize is set for pagination
        if (currentQuery.getPageSize() == null) {
            currentQuery.setPageSize(100); // Max allowed is usually 1000 for detail queries, but 100 is safer for testing
        }

        while (true) {
            try {
                response = executeWithRetry(currentQuery);
                allResults.add(response);
                logger.info("Fetched page {}. Retrieved {} conversations.", currentPage, response.getEntities().size());

                // Check for next page
                if (response.getNextPageToken() != null) {
                    currentQuery.setPageToken(response.getNextPageToken());
                    currentPage++;
                } else {
                    break; // No more pages
                }

            } catch (ApiException e) {
                // Handle non-retryable errors
                if (e.getCode() != 429) {
                    logger.error("API Error: {} - {}", e.getCode(), e.getMessage());
                    throw e;
                }
                // Retry logic is handled inside executeWithRetry
            }
        }
        return allResults;
    }

    /**
     * Executes an API call with exponential backoff for 429 errors.
     */
    private ConversationDetailsResponse executeWithRetry(ConversationDetailsQuery query) throws Exception {
        Exception lastException = null;
        
        for (int attempt = 1; attempt <= maxRetries; attempt++) {
            try {
                return analyticsApi.postAnalyticsConversationsDetailsQuery(query);
            } catch (ApiException e) {
                if (e.getCode() == 429) {
                    lastException = e;
                    long waitTime = (long) Math.pow(2, attempt) * 1000; // Exponential backoff: 2s, 4s, 8s
                    logger.warn("Rate limited (429). Retrying in {} ms (Attempt {}/{})", waitTime, attempt, maxRetries);
                    
                    // Optional: Parse Retry-After header if present
                    String retryAfter = e.getResponseHeaders().get("Retry-After");
                    if (retryAfter != null) {
                        try {
                            waitTime = Long.parseLong(retryAfter) * 1000;
                        } catch (NumberFormatException ignored) {}
                    }
                    
                    TimeUnit.MILLISECONDS.sleep(waitTime);
                } else {
                    throw e;
                }
            }
        }
        throw lastException; // Throw the last 429 error if retries exhausted
    }
}

Step 3: Thread-Safe Execution Demo

To demonstrate the thread safety of the configuration, we will create a multi-threaded executor that shares the single ApiClient instance. This is a common anti-pattern if the client is not configured correctly (leading to connection leaks), but with the PoolingHttpClientConnectionManager, it is safe and efficient.

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

public class Main {

    public static void main(String[] args) {
        try {
            // 1. Initialize the thread-safe client ONCE
            ApiClient apiClient = GenesysConfig.createThreadSafeApiClient();
            
            // 2. Create the analyzer instance (also thread-safe)
            ConversationAnalyzer analyzer = new ConversationAnalyzer(apiClient);
            PaginatedAnalyzer paginatedAnalyzer = new PaginatedAnalyzer(apiClient);

            // 3. Set up thread pool
            int threadCount = 5;
            ExecutorService executor = Executors.newFixedThreadPool(threadCount);
            AtomicInteger successCount = new AtomicInteger(0);
            AtomicInteger errorCount = new AtomicInteger(0);

            System.out.println("Starting concurrent requests...");

            // 4. Submit tasks
            for (int i = 0; i < 10; i++) {
                final int taskId = i;
                executor.submit(() -> {
                    try {
                        // Each thread uses the same apiClient instance
                        ConversationDetailsResponse response = analyzer.getRecentVoiceConversations();
                        if (response != null && response.getEntities() != null) {
                            System.out.println("Task " + taskId + ": Success. Entities: " + response.getEntities().size());
                            successCount.incrementAndGet();
                        }
                    } catch (Exception e) {
                        System.err.println("Task " + taskId + ": Error - " + e.getMessage());
                        errorCount.incrementAndGet();
                    }
                });
            }

            // 5. Shutdown and wait
            executor.shutdown();
            if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
                executor.shutdownNow();
            }

            System.out.println("Completed. Successes: " + successCount.get() + ", Errors: " + errorCount.get());

        } catch (ConfigurationException e) {
            System.err.println("Configuration Error: " + e.getMessage());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Complete Working Example

Below is the complete pom.xml dependencies and the main class structure required to run this tutorial.

pom.xml

<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-pooling-demo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <platform-java.version>100.0.0</platform-java.version>
    </properties>
    <dependencies>
        <!-- Genesys Cloud Java SDK -->
        <dependency>
            <groupId>com.mypurecloud.platform</groupId>
            <artifactId>platform-java</artifactId>
            <version>${platform-java.version}</version>
        </dependency>
        
        <!-- Logging -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>2.0.7</version>
        </dependency>

        <!-- Apache HttpClient 5 (Transitive, but explicit for version control) -->
        <dependency>
            <groupId>org.apache.httpcomponents.client5</groupId>
            <artifactId>httpclient5</artifactId>
            <version>5.2.1</version>
        </dependency>
    </dependencies>
</project>

Main.java

Combine the GenesysConfig, ConversationAnalyzer, PaginatedAnalyzer, and Main classes from the previous steps into a single package. Ensure environment variables are set:

export GENESYS_CLIENT_ID="your_client_id"
export GENESYS_CLIENT_SECRET="your_client_secret"
export GENESYS_REGION="mypurecloud.com"

Common Errors & Debugging

Error: java.net.SocketTimeoutException: Read timed out

  • Cause: The default socket timeout is often too short for large analytics queries.
  • Fix: Increase the SoTimeout in the PoolingHttpClientConnectionManager configuration in Step 1.
  • Code Fix:
    .setSoTimeout(60, TimeUnit.SECONDS) // Increase from 30 to 60
    

Error: org.apache.hc.core5.http.ConnectionPoolTimeoutException: Timeout waiting for connection

  • Cause: The connection pool is exhausted. This happens when maxTotal or maxPerRoute is too low for the number of concurrent threads.
  • Fix: Increase setMaxTotal and setDefaultMaxPerRoute.
  • Code Fix:
    connectionManager.setMaxTotal(500); // Increase capacity
    connectionManager.setDefaultMaxPerRoute(100);
    

Error: 401 Unauthorized or 403 Forbidden

  • Cause: Invalid OAuth credentials or missing scopes.
  • Fix: Verify GENESYS_CLIENT_ID and GENESYS_CLIENT_SECRET. Check that the OAuth Client in Genesys Cloud Admin has the required scopes enabled.
  • Debugging: Enable SDK logging to see the exact token request and response.
    // Add to ApiClient builder
    .setLoggingEnabled(true)
    

Error: 429 Too Many Requests

  • Cause: Exceeding the API rate limit.
  • Fix: Implement the retry logic shown in Step 2. Do not ignore 429 errors. Always respect the Retry-After header if provided.

Official References