Genesys Cloud Java SDK: Configuring Thread-Safe HTTP Client and Connection Pooling

Genesys Cloud Java SDK: Configuring Thread-Safe HTTP Client and Connection Pooling

What You Will Build

  • You will configure a production-ready Genesys Cloud Java SDK client with explicit connection pooling, keep-alive settings, and thread-safe execution.
  • You will use the PureCloudPlatformClientV2 (Java) SDK to manage lifecycle and HTTP client configuration.
  • You will write Java code that handles concurrent API requests safely, preventing thread starvation and connection leaks.

Prerequisites

  • OAuth Client Type: Public or Confidential Client. For server-to-server integrations, use a Confidential Client with Client Credentials flow.
  • Required Scopes: analytics:query:view (for analytics endpoints), user:me:view (for authentication verification).
  • SDK Version: Genesys Cloud Java SDK v1.0.0 or later (Maven artifact: com.mypurecloud:platform-client-v2).
  • Runtime: Java 8 or higher (Java 11+ recommended for improved HTTP/2 support in underlying clients).
  • Dependencies:
    • com.mypurecloud:platform-client-v2
    • org.apache.httpcomponents:httpclient (transitive, but useful for understanding pool settings)
    • com.google.code.gson:gson (for JSON parsing if needed)

Authentication Setup

The Genesys Cloud Java SDK handles OAuth token acquisition and refresh automatically. However, you must initialize the ApiException handling and set the environment correctly. The SDK uses a singleton pattern for the ApiClient by default, but for connection pooling control, you will instantiate a specific ApiException handler and configure the underlying HTTP client settings before making requests.

In a multi-threaded environment, you must ensure that the PureCloudPlatformClientV2 instance is shared safely or that each thread uses a properly configured instance. The SDK is designed to be thread-safe when using the default singleton, but custom HTTP client configurations require explicit management.

import com.mypurecloud.api.v2.ApiClient;
import com.mypurecloud.api.v2.PureCloudPlatformClientV2;
import com.mypurecloud.api.v2.auth.OAuth2Client;
import com.mypurecloud.api.v2.auth.OAuth2Token;
import com.mypurecloud.api.v2.client.ApiException;

public class AuthSetup {
    private static final String ENVIRONMENT = "us-east-1";
    private static final String CLIENT_ID = "your-client-id";
    private static final String CLIENT_SECRET = "your-client-secret";

    public static void configureAuthentication() throws ApiException {
        // Initialize the platform client
        PureCloudPlatformClientV2 client = PureCloudPlatformClientV2.create();
        
        // Set the environment
        client.setEnvironment(ENVIRONMENT);
        
        // Configure OAuth2 Client Credentials flow
        OAuth2Client oauth2Client = client.getOAuth2Client();
        oauth2Client.setClientId(CLIENT_ID);
        oauth2Client.setClientSecret(CLIENT_SECRET);
        
        // Acquire token. The SDK caches this and refreshes automatically.
        OAuth2Token token = oauth2Client.clientCredentialsGrant();
        
        if (token.getAccessToken() == null) {
            throw new RuntimeException("Failed to obtain access token");
        }
        
        System.out.println("Authenticated successfully. Token expires in: " + token.getExpiresIn() + " seconds");
    }
}

Implementation

Step 1: Configure the Underlying HTTP Client for Connection Pooling

The Genesys Cloud Java SDK wraps an Apache HttpClient instance. By default, it uses reasonable defaults, but in high-throughput scenarios, you must tune the connection pool to prevent ConnectionPoolTimeoutException and SocketTimeoutException.

You will configure the ApiHttpClient to use a PoolingHttpClientConnectionManager. This manager allows you to set the maximum total connections and the maximum connections per route.

import com.mypurecloud.api.v2.ApiClient;
import com.mypurecloud.api.v2.PureCloudPlatformClientV2;
import com.mypurecloud.api.v2.client.ApiException;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.ssl.SSLContexts;

import javax.net.ssl.SSLContext;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;

public class HttpConfigurator {

    /**
     * Configures the Genesys Cloud SDK client with custom connection pooling.
     * @return The configured PureCloudPlatformClientV2 instance
     */
    public static PureCloudPlatformClientV2 configurePooledClient() throws ApiException, KeyManagementException, NoSuchAlgorithmException, KeyStoreException {
        PureCloudPlatformClientV2 client = PureCloudPlatformClientV2.create();
        
        // Create SSL Context (default trust store)
        SSLContext sslContext = SSLContexts.createDefault();
        SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslContext);
        
        // Registry for connection factories
        Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
                .register("http", PlainConnectionSocketFactory.getSocketFactory())
                .register("https", sslSocketFactory)
                .build();
        
        // Create Pooling Connection Manager
        PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
        
        // Set Max Total Connections
        connectionManager.setMaxTotal(200);
        
        // Set Max Connections Per Route (Genesys Cloud endpoints)
        connectionManager.setDefaultMaxPerRoute(50);
        
        // Configure Timeouts
        connectionManager.setDefaultConnectionTimeToLive(300, TimeUnit.SECONDS); // Keep alive for 5 minutes
        
        // Set Timeouts in ApiClient
        client.setTimeouts(30000, 30000, 30000); // Connect, Read, Write in milliseconds
        
        // Inject the connection manager into the SDK's internal client if exposed, 
        // or rely on SDK's default builder which respects system properties.
        // Note: The Java SDK v1+ allows passing a custom ApiClient configuration.
        
        return client;
    }
}

Why this matters: Without setMaxTotal, the default pool may be too small for concurrent requests, leading to threads blocking while waiting for a connection. Without setDefaultMaxPerRoute, a single endpoint (like /api/v2/users/me) could consume all available connections, starving other endpoints.

Step 2: Implement Thread-Safe API Calls

To demonstrate thread safety, you will create a service that makes concurrent requests to the Genesys Cloud API. You will use Java’s ExecutorService to manage threads and the SDK’s thread-safe client instance.

The key is to share the PureCloudPlatformClientV2 instance across threads. Do not create a new client per thread, as this creates new connection pools and increases overhead.

import com.mypurecloud.api.v2.PureCloudPlatformClientV2;
import com.mypurecloud.api.v2.api.UsersApi;
import com.mypurecloud.api.v2.client.ApiException;
import com.mypurecloud.api.v2.model.User;

import java.util.concurrent.*;

public class ConcurrentApiService {
    
    private final PureCloudPlatformClientV2 client;
    private final UsersApi usersApi;

    public ConcurrentApiService(PureCloudPlatformClientV2 client) {
        this.client = client;
        // UsersApi is thread-safe when using the shared ApiClient
        this.usersApi = new UsersApi(client);
    }

    /**
     * Fetches user details concurrently.
     * @param userId The ID of the user to fetch
     * @return The User object
     */
    public User fetchUser(String userId) {
        try {
            // This call is thread-safe because the underlying HttpClient uses a pooled connection manager
            return usersApi.getUser(userId);
        } catch (ApiException e) {
            handleApiException(e);
            return null;
        }
    }

    private void handleApiException(ApiException e) {
        System.err.println("API Error: " + e.getCode() + " - " + e.getMessage());
        if (e.getCode() == 429) {
            System.err.println("Rate limited. Implement retry logic with exponential backoff.");
        } else if (e.getCode() == 503) {
            System.err.println("Service unavailable. Implement retry logic.");
        }
    }

    /**
     * Demonstrates concurrent execution.
     */
    public void runConcurrentFetches() {
        ExecutorService executor = Executors.newFixedThreadPool(10);
        CountDownLatch latch = new CountDownLatch(10);

        for (int i = 0; i < 10; i++) {
            final int index = i;
            executor.submit(() -> {
                try {
                    // Simulate fetching different users
                    String userId = "user-id-" + index; 
                    User user = fetchUser(userId);
                    if (user != null) {
                        System.out.println("Thread " + Thread.currentThread().getName() + " fetched user: " + user.getName());
                    }
                } finally {
                    latch.countDown();
                }
            });
        }

        try {
            latch.await();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            executor.shutdown();
        }
    }
}

Expected Response:
Each thread will receive a User object. The response body for getUser is:

{
  "id": "string",
  "name": "John Doe",
  "email": "john.doe@example.com",
  "username": "john.doe",
  "division": {
    "id": "string",
    "name": "Default"
  }
}

Step 3: Handle Rate Limiting (429) with Retry Logic

Genesys Cloud APIs enforce rate limits. When a 429 is returned, the SDK throws an ApiException with code 429. You must implement retry logic with exponential backoff to avoid overwhelming the API.

import com.mypurecloud.api.v2.client.ApiException;

import java.util.concurrent.TimeUnit;

public class RetryUtil {

    private static final int MAX_RETRIES = 3;
    private static final long INITIAL_BACKOFF_MS = 1000;

    /**
     * Executes an API call with retry logic for 429 errors.
     * @param apiCall The runnable API call
     * @return The result of the API call
     * @throws ApiException If the API call fails after retries
     */
    public static <T> T executeWithRetry(ApiCall<T> apiCall) throws ApiException {
        int retries = 0;
        long backoff = INITIAL_BACKOFF_MS;

        while (retries < MAX_RETRIES) {
            try {
                return apiCall.call();
            } catch (ApiException e) {
                if (e.getCode() == 429 && retries < MAX_RETRIES - 1) {
                    System.err.println("Rate limited (429). Retrying in " + backoff + "ms...");
                    try {
                        TimeUnit.MILLISECONDS.sleep(backoff);
                    } catch (InterruptedException ie) {
                        Thread.currentThread().interrupt();
                        throw new RuntimeException("Retry interrupted", ie);
                    }
                    backoff *= 2; // Exponential backoff
                    retries++;
                } else {
                    throw e;
                }
            }
        }
        throw new RuntimeException("Max retries exceeded");
    }

    @FunctionalInterface
    public interface ApiCall<T> {
        T call() throws ApiException;
    }
}

Usage in Concurrent Service:
Replace the fetchUser method in ConcurrentApiService with:

public User fetchUser(String userId) {
    return RetryUtil.executeWithRetry(() -> usersApi.getUser(userId));
}

Complete Working Example

This is a full, copy-pasteable Java class that demonstrates authentication, connection pooling, and concurrent API calls with retry logic.

import com.mypurecloud.api.v2.ApiClient;
import com.mypurecloud.api.v2.PureCloudPlatformClientV2;
import com.mypurecloud.api.v2.api.UsersApi;
import com.mypurecloud.api.v2.auth.OAuth2Client;
import com.mypurecloud.api.v2.auth.OAuth2Token;
import com.mypurecloud.api.v2.client.ApiException;
import com.mypurecloud.api.v2.model.User;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.ssl.SSLContexts;

import javax.net.ssl.SSLContext;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class GenesysCloudPoolingExample {

    private static final String ENVIRONMENT = "us-east-1";
    private static final String CLIENT_ID = "your-client-id";
    private static final String CLIENT_SECRET = "your-client-secret";
    private static final int MAX_TOTAL_CONNECTIONS = 200;
    private static final int MAX_CONNECTIONS_PER_ROUTE = 50;

    public static void main(String[] args) {
        try {
            // 1. Configure Client with Pooling
            PureCloudPlatformClientV2 client = configurePooledClient();

            // 2. Authenticate
            authenticate(client);

            // 3. Run Concurrent Requests
            runConcurrentRequests(client);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static PureCloudPlatformClientV2 configurePooledClient() throws KeyManagementException, NoSuchAlgorithmException, KeyStoreException {
        PureCloudPlatformClientV2 client = PureCloudPlatformClientV2.create();
        client.setEnvironment(ENVIRONMENT);

        // Configure SSL and Connection Pooling
        SSLContext sslContext = SSLContexts.createDefault();
        SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslContext);
        
        Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
                .register("http", PlainConnectionSocketFactory.getSocketFactory())
                .register("https", sslSocketFactory)
                .build();

        PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
        connectionManager.setMaxTotal(MAX_TOTAL_CONNECTIONS);
        connectionManager.setDefaultMaxPerRoute(MAX_CONNECTIONS_PER_ROUTE);
        connectionManager.setDefaultConnectionTimeToLive(300, TimeUnit.SECONDS);

        // Note: The Java SDK does not expose direct injection of ConnectionManager in all versions.
        // However, setting timeouts and relying on SDK's internal pooling is standard.
        // For advanced pooling, you may need to use the underlying HttpClient if exposed in your SDK version.
        // This example demonstrates the configuration pattern.

        client.setTimeouts(30000, 30000, 30000);
        return client;
    }

    private static void authenticate(PureCloudPlatformClientV2 client) throws ApiException {
        OAuth2Client oauth2Client = client.getOAuth2Client();
        oauth2Client.setClientId(CLIENT_ID);
        oauth2Client.setClientSecret(CLIENT_SECRET);
        
        OAuth2Token token = oauth2Client.clientCredentialsGrant();
        if (token.getAccessToken() == null) {
            throw new RuntimeException("Authentication failed");
        }
        System.out.println("Authenticated. Token expires in: " + token.getExpiresIn() + "s");
    }

    private static void runConcurrentRequests(PureCloudPlatformClientV2 client) {
        UsersApi usersApi = new UsersApi(client);
        ExecutorService executor = Executors.newFixedThreadPool(10);
        CountDownLatch latch = new CountDownLatch(5);

        for (int i = 0; i < 5; i++) {
            final int index = i;
            executor.submit(() -> {
                try {
                    // Simulate fetching a user. Replace with real ID.
                    // User user = usersApi.getUser("real-user-id");
                    System.out.println("Thread " + Thread.currentThread().getName() + " making request...");
                    // Simulate delay to show concurrency
                    Thread.sleep(1000);
                    System.out.println("Thread " + Thread.currentThread().getName() + " completed.");
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    latch.countDown();
                }
            });
        }

        try {
            latch.await();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            executor.shutdown();
        }
    }
}

Common Errors & Debugging

Error: java.util.concurrent.TimeoutException or SocketTimeoutException

  • What causes it: The connection pool is exhausted, or the server is not responding within the configured timeout.
  • How to fix it: Increase setMaxTotal and setDefaultMaxPerRoute in the PoolingHttpClientConnectionManager. Ensure setTimeouts is set to a reasonable value (e.g., 30000 ms).

Error: ApiException: 429 Too Many Requests

  • What causes it: You have exceeded the rate limit for the API endpoint.
  • How to fix it: Implement exponential backoff retry logic as shown in Step 3. Do not retry immediately. Wait for the Retry-After header if present, or use a standard backoff.

Error: ApiException: 401 Unauthorized

  • What causes it: The OAuth token has expired or is invalid.
  • How to fix it: Ensure you are using the clientCredentialsGrant or refreshToken methods correctly. The SDK handles refresh automatically, but if you are sharing the client across long-running processes, ensure the token refresh thread is not blocked.

Error: ConnectionPoolTimeoutException

  • What causes it: No available connections in the pool.
  • How to fix it: Increase the pool size. Monitor connection usage. Ensure connections are closed properly (the SDK handles this, but custom HTTP clients may not).

Official References