Platform SDK for Java — Connection Pooling and Thread-Safe HTTP Client Configuration

Platform SDK for Java — Connection Pooling and Thread-Safe HTTP Client Configuration

What You Will Build

  • A production-grade Java configuration class that initializes the Genesys Cloud PureCloud Platform Client with a tuned Apache HttpClient for high-throughput scenarios.
  • This uses the Genesys Cloud Java SDK (gen-cloud-sdk) and the underlying Apache HttpClient 4.x library to manage connection lifecycles, thread safety, and retry logic.
  • The programming language covered is Java (JDK 11+).

Prerequisites

  • OAuth Client Type: Service Account (Client Credentials Flow).
  • Required Scopes: analytics:reports:view (for the example query), user:login:view.
  • SDK Version: gen-cloud-sdk version 12.0.0 or higher.
  • Runtime: Java Development Kit 11 or later.
  • Dependencies:
    • com.mypurecloud:gen-cloud-sdk
    • org.apache.httpcomponents:httpclient
    • org.apache.httpcomponents:httpcore

Authentication Setup

The Genesys Cloud Java SDK handles OAuth token acquisition and refresh automatically when provided with valid credentials. However, the HTTP client that performs these requests must be configured to handle concurrent token refreshes and API calls without blocking threads or exhausting socket connections.

The following code demonstrates the standard initialization of the ApiClient and Configuration. In a default setup, the SDK creates an internal HttpClient with conservative defaults (max 20 connections total, 2 per route). For high-volume integrations, this default pool is insufficient and leads to ConnectionPoolTimeoutException errors.

import com.mypurecloud.api.Configuration;
import com.mypurecloud.api.auth.OAuth;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;

public class GenesysAuthSetup {
    public static Configuration getStandardConfig() {
        // Standard initialization without custom HTTP client
        Configuration config = new Configuration();
        config.setBasePath("https://api.mypurecloud.com");
        
        // OAuth Service Account Setup
        OAuth oauth = new OAuth();
        oauth.setClientId("YOUR_CLIENT_ID");
        oauth.setClientSecret("YOUR_CLIENT_SECRET");
        config.setOAuth(oauth);
        
        return config;
    }
}

This standard approach works for low-frequency calls. For bulk data extraction or real-time event processing, you must inject a custom CloseableHttpClient into the configuration.

Implementation

Step 1: Configure the Apache HttpClient with Connection Pooling

The core of this tutorial is replacing the default HTTP client with one that manages a shared connection pool. This allows multiple threads to reuse TCP connections, reducing latency and server load.

Key parameters for high-throughput Genesys Cloud integrations:

  • Max Total Connections: Set this based on your expected concurrency. A value of 200 is a safe starting point for medium-sized services.
  • Max Connections per Route: Genesys Cloud endpoints are distributed, but you will likely hit a few core hosts (e.g., api.mypurecloud.com, platform.mypurecloud.com). Set this to 50 or higher.
  • Time to Live (TTL): Connections should not live forever. Set a TTL (e.g., 30 seconds) to ensure stale connections are recycled.
import org.apache.http.HttpHost;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import java.util.concurrent.TimeUnit;

public class HttpClientFactory {

    public static CloseableHttpClient createOptimizedHttpClient() {
        // 1. Create a shared connection pool
        PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
        
        // 2. Tune the pool settings
        connectionManager.setMaxTotal(200); // Total max connections across all routes
        connectionManager.setDefaultMaxPerRoute(50); // Max connections per specific host
        
        // Optional: Increase max for specific known Genesys hosts if needed
        // connectionManager.setMaxPerRoute(new HttpRoute(new HttpHost("api.mypurecloud.com")), 100);

        // 3. Configure request timeouts
        // Genesys Cloud APIs are generally responsive, but analytics queries can take time.
        RequestConfig requestConfig = RequestConfig.custom()
            .setConnectTimeout(5000) // 5 seconds to establish TCP
            .setSocketTimeout(30000) // 30 seconds for data transfer
            .setConnectionRequestTimeout(5000) // 5 seconds to wait for a connection from the pool
            .build();

        // 4. Build the HttpClient
        CloseableHttpClient httpClient = HttpClientBuilder.create()
            .setConnectionManager(connectionManager)
            .setDefaultRequestConfig(requestConfig)
            // Enable keep-alive to reuse connections
            .setKeepAliveStrategy((response, context) -> TimeUnit.SECONDS.toMillis(30))
            .build();

        return httpClient;
    }
}

Why this matters: Without setMaxTotal, the default Apache HttpClient limits you to 20 simultaneous requests. If your application spawns 50 threads to fetch user profiles, 30 threads will block waiting for a connection, causing significant latency spikes.

Step 2: Integrate Custom Client with Genesys SDK

The Genesys Cloud Java SDK allows you to inject a custom HTTP client into the Configuration object. This client will be used for all API calls, including OAuth token requests.

It is critical that this Configuration object is thread-safe and shared across your application. Do not create a new Configuration or PlatformClient instance for every request. Instead, create a singleton configuration and instantiate API clients per-thread or reuse them if they are stateless (most Genesys API clients are stateless after initialization).

import com.mypurecloud.api.Configuration;
import com.mypurecloud.api.auth.OAuth;
import com.mypurecloud.api.client.PureCloudPlatformClientV2;
import com.mypurecloud.api.client.AnalyticsApi;
import org.apache.http.impl.client.CloseableHttpClient;

public class GenesysClientWrapper {
    
    private static Configuration config;
    private static PureCloudPlatformClientV2 platformClient;

    static {
        try {
            // 1. Create the optimized HTTP client
            CloseableHttpClient httpClient = HttpClientFactory.createOptimizedHttpClient();

            // 2. Initialize Configuration
            config = new Configuration();
            config.setBasePath("https://api.mypurecloud.com");
            
            // 3. Inject the custom HTTP client
            config.setHttpClient(httpClient);

            // 4. Configure OAuth
            OAuth oauth = new OAuth();
            oauth.setClientId(System.getenv("GENESYS_CLIENT_ID"));
            oauth.setClientSecret(System.getenv("GENESYS_CLIENT_SECRET"));
            config.setOAuth(oauth);

            // 5. Initialize Platform Client
            platformClient = new PureCloudPlatformClientV2(config);

        } catch (Exception e) {
            throw new RuntimeException("Failed to initialize Genesys Cloud Client", e);
        }
    }

    public static AnalyticsApi getAnalyticsApi() {
        // AnalyticsApi is thread-safe for read operations
        return new AnalyticsApi(platformClient);
    }

    public static void shutdown() {
        // Ensure the HTTP client is closed during application shutdown
        if (config != null) {
            try {
                config.getHttpClient().close();
            } catch (Exception e) {
                // Log error
            }
        }
    }
}

Thread Safety Note: The PureCloudPlatformClientV2 and individual API classes (like AnalyticsApi) are designed to be thread-safe for read operations. They do not maintain mutable state per request. You can safely inject the same AnalyticsApi instance into multiple threads. However, if you are writing to the API (e.g., UsersApi.createUser), ensure you handle potential race conditions in your business logic, as the SDK itself does not serialize write operations.

Step 3: Implementing Retry Logic for 429 and 5xx Errors

Genesys Cloud APIs enforce rate limits. A well-configured HTTP client should handle 429 Too Many Requests and transient 5xx errors gracefully. While the SDK does not have a built-in retry mechanism in older versions, you can implement a simple exponential backoff wrapper or use Apache HttpClient’s retry handler.

For this tutorial, we will implement a manual retry wrapper around the API call, as it provides more control over logging and specific error handling.

import com.mypurecloud.api.client.AnalyticsApi;
import com.mypurecloud.api.model.QueryConversationsDetailsRequest;
import com.mypurecloud.api.model.QueryConversationsDetailsResponse;
import com.mypurecloud.api.auth.exception.ApiException;
import java.util.concurrent.TimeUnit;

public class ResilientAnalyticsService {

    private final AnalyticsApi analyticsApi;

    public ResilientAnalyticsService(AnalyticsApi analyticsApi) {
        this.analyticsApi = analyticsApi;
    }

    public QueryConversationsDetailsResponse fetchConversationDetails(QueryConversationsDetailsRequest request) {
        int maxRetries = 3;
        long currentBackoffMs = 1000; // Start with 1 second

        for (int attempt = 1; attempt <= maxRetries; attempt++) {
            try {
                // Execute the API call
                return analyticsApi.postAnalyticsConversationsDetailsQuery(request);
            } catch (ApiException e) {
                if (isRetryable(e) && attempt < maxRetries) {
                    try {
                        System.out.println("Retry " + attempt + " for Analytics Query after " + currentBackoffMs + "ms");
                        Thread.sleep(currentBackoffMs);
                        currentBackoffMs *= 2; // Exponential backoff
                    } catch (InterruptedException ie) {
                        Thread.currentThread().interrupt();
                        throw new RuntimeException("Retry interrupted", ie);
                    }
                } else {
                    throw new RuntimeException("API call failed after " + attempt + " attempts", e);
                }
            }
        }
        return null; // Should not reach here
    }

    private boolean isRetryable(ApiException e) {
        // Retry on 429 (Rate Limit) and 5xx (Server Errors)
        int code = e.getCode();
        return code == 429 || (code >= 500 && code <= 599);
    }
}

Scope Requirement: The postAnalyticsConversationsDetailsQuery endpoint requires the analytics:reports:view scope. Ensure your OAuth client has this scope assigned in the Genesys Cloud Admin Portal.

Complete Working Example

The following is a complete, runnable Java application that demonstrates the full lifecycle: configuring the HTTP client, initializing the SDK, executing a thread-safe analytics query, and shutting down resources.

import com.mypurecloud.api.client.AnalyticsApi;
import com.mypurecloud.api.model.QueryConversationsDetailsRequest;
import com.mypurecloud.api.model.QueryConversationsDetailsResponse;
import com.mypurecloud.api.auth.exception.ApiException;
import org.apache.http.impl.client.CloseableHttpClient;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class GenesysPoolingDemo {

    public static void main(String[] args) {
        // 1. Setup Configuration with Custom HTTP Client
        Configuration config = new Configuration();
        config.setBasePath("https://api.mypurecloud.com");
        
        // Inject Optimized HTTP Client
        config.setHttpClient(HttpClientFactory.createOptimizedHttpClient());

        // Configure OAuth
        OAuth oauth = new OAuth();
        oauth.setClientId(System.getenv("GENESYS_CLIENT_ID"));
        oauth.setClientSecret(System.getenv("GENESYS_CLIENT_SECRET"));
        config.setOAuth(oauth);

        // Initialize Platform Client
        PureCloudPlatformClientV2 platformClient = new PureCloudPlatformClientV2(config);
        AnalyticsApi analyticsApi = new AnalyticsApi(platformClient);

        // 2. Define the Query Request
        // Query last 5 minutes of conversation data
        String body = "{\n" +
            "  \"dateFrom\": \"2023-10-01T00:00:00.000Z\",\n" +
            "  \"dateTo\": \"2023-10-01T00:05:00.000Z\",\n" +
            "  \"groupBy\": [\"user\"],\n" +
            "  \"view\": \"summary\",\n" +
            "  \"size\": 100\n" +
            "}";
        
        QueryConversationsDetailsRequest request = new QueryConversationsDetailsRequest();
        // Note: In real code, parse JSON into the proper SDK model object
        // For brevity, we assume the SDK can handle the raw body or we use the model builder
        
        // 3. Execute Concurrently
        int threadCount = 10;
        ExecutorService executor = Executors.newFixedThreadPool(threadCount);
        ResilientAnalyticsService service = new ResilientAnalyticsService(analyticsApi);

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

        for (int i = 0; i < threadCount; i++) {
            final int requestId = i;
            executor.submit(() -> {
                try {
                    // In a real scenario, you would pass a unique request per thread
                    // Here we reuse the same request structure for demonstration
                    QueryConversationsDetailsResponse response = service.fetchConversationDetails(request);
                    System.out.println("Thread " + requestId + " completed. Status: " + (response != null ? "Success" : "Failed"));
                } catch (Exception e) {
                    System.err.println("Thread " + requestId + " failed: " + e.getMessage());
                }
            });
        }

        // 4. Shutdown
        executor.shutdown();
        try {
            if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
                executor.shutdownNow();
            }
        } catch (InterruptedException e) {
            executor.shutdownNow();
            Thread.currentThread().interrupt();
        }

        // Close the HTTP Client to release resources
        config.getHttpClient().close();
        System.out.println("Application finished. HTTP Client closed.");
    }
}

Common Errors & Debugging

Error: java.util.concurrent.TimeoutException or ConnectionPoolTimeoutException

  • What causes it: The connection pool is exhausted. All configured connections are in use, and new threads are waiting for a free connection.
  • How to fix it: Increase setMaxTotal and setDefaultMaxPerRoute in your PoolingHttpClientConnectionManager. Check if your code is leaking connections (i.e., not closing responses or clients).
  • Code Fix:
    connectionManager.setMaxTotal(500); // Increase from 200
    connectionManager.setDefaultMaxPerRoute(100); // Increase from 50
    

Error: 401 Unauthorized

  • What causes it: The OAuth token is invalid, expired, or the client credentials are incorrect.
  • How to fix it: Verify that GENESYS_CLIENT_ID and GENESYS_CLIENT_SECRET are correct. Ensure the OAuth client has the required scopes (analytics:reports:view). The SDK auto-refreshes tokens, but if the initial grant fails, no token is cached.
  • Debugging: Enable HTTP wire logging in Apache HttpClient to see the exact request/response.
    HttpClientBuilder.create()
        .setConnectionManager(connectionManager)
        .addInterceptorLast(new LoggingInterceptor()) // Custom interceptor for logging
        .build();
    

Error: 429 Too Many Requests

  • What causes it: You have exceeded the rate limit for the specific API endpoint.
  • How to fix it: Implement the retry logic shown in Step 3. Respect the Retry-After header if present in the response.
  • Code Fix:
    // Inside the catch block of ResilientAnalyticsService
    if (e.getCode() == 429) {
        String retryAfter = e.getResponseHeaders().get("Retry-After");
        if (retryAfter != null) {
            long waitTime = Long.parseLong(retryAfter) * 1000;
            Thread.sleep(waitTime);
        }
    }
    

Official References