Garbage-Collecting Genesys Cloud EventBridge Dead Letters in Java
What You Will Build
- A Java service that polls an AWS SQS Dead Letter Queue receiving failed Genesys Cloud EventBridge events, evaluates age and retry thresholds, validates payloads against schema constraints, executes atomic batch deletions, records audit logs, and forwards sweep metrics to an external log aggregator.
- This implementation uses the AWS SDK for Java 2.x for SQS/EventBridge management and the Genesys Cloud Java SDK for event schema validation.
- The programming language covered is Java 17+.
Prerequisites
- AWS IAM credentials with
sqs:ReceiveMessage,sqs:DeleteMessageBatch,sqs:GetQueueAttributes, andevents:DescribeArchivepermissions. - Genesys Cloud OAuth confidential client with
event:readscope for schema validation. - Java 17 or later with Maven or Gradle.
- Dependencies:
software.amazon.awssdk:sqs,software.amazon.awssdk:events,software.amazon.awssdk:auth,com.networknt:json-schema-validator,com.google.code.gson:gson,org.slf4j:slf4j-api.
Authentication Setup
Genesys Cloud EventBridge integration streams events to AWS EventBridge. Failed events route to an SQS Dead Letter Queue. The Java application authenticates to AWS using the default credential provider chain and to Genesys Cloud using OAuth2 client credentials.
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.sqs.SqsClient;
import com.mypurecloud.sdk.client.ApiClient;
import com.mypurecloud.sdk.client.auth.oauth.ClientCredentialsProvider;
import com.mypurecloud.sdk.client.auth.oauth.OAuthClientCredentialsProvider;
import java.util.List;
public class AuthSetup {
public static SqsClient buildSqsClient(String region) {
return SqsClient.builder()
.region(Region.of(region))
.credentialsProvider(DefaultCredentialsProvider.create())
.build();
}
public static ApiClient buildGenesysApiClient(String environment, String clientId, String clientSecret) {
ApiClient apiClient = new ApiClient();
apiClient.setBasePath("https://" + environment + ".mypurecloud.com");
OAuthClientCredentialsProvider credentialsProvider = new OAuthClientCredentialsProvider(
clientId,
clientSecret,
List.of("event:read"),
"https://" + environment + ".mypurecloud.com"
);
apiClient.setCredentials(credentialsProvider);
return apiClient;
}
}
The event:read scope allows the service to fetch Genesys Cloud event schemas for payload validation. AWS credentials are resolved automatically from environment variables, IAM roles, or the shared credentials file.
Implementation
Step 1: Initialize Purge Configuration and Constraints
Define the purge directive, age threshold, maximum purge window, and retry exhaustion limits. These parameters control the garbage collection behavior.
import java.time.Instant;
public record PurgeDirective(
String dlqRef,
long ageThresholdSeconds,
long maximumPurgeWindowSeconds,
int retryExhaustionLimit,
String eventbridgeConstraintsSchemaUrl,
String externalLogAggregatorUrl
) {}
public class PurgeValidator {
private final PurgeDirective directive;
private final Instant sweepStartTime;
public PurgeValidator(PurgeDirective directive) {
this.directive = directive;
this.sweepStartTime = Instant.now();
}
public boolean isWithinPurgeWindow(Instant eventTimestamp) {
long ageSeconds = Instant.now().getEpochSecond() - eventTimestamp.getEpochSecond();
return ageSeconds >= directive.ageThresholdSeconds()
&& ageSeconds <= directive.maximumPurgeWindowSeconds();
}
public boolean isRetryExhausted(int approximateReceiveCount) {
return approximateReceiveCount >= directive.retryExhaustionLimit();
}
public long getElapsedMilliseconds() {
return Instant.now().getEpochSecond() * 1000 - sweepStartTime.getEpochSecond() * 1000;
}
}
The PurgeDirective maps directly to the dlq-ref, age-threshold, maximum-purge-window-seconds, and retry-exhaustion requirements. The PurgeValidator enforces the eventbridge-constraints window and tracks garbage collection latency.
Step 2: Poll DLQ and Evaluate Active Consumer State
SQS does not expose an explicit active consumer flag. We simulate active consumer checking by verifying the message visibility window and ensuring no concurrent sweep is processing the same batch. We fetch messages with a long poll and extract metadata.
import software.amazon.awssdk.services.sqs.SqsClient;
import software.amazon.awssdk.services.sqs.model.*;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class DlgPoller {
private static final Gson gson = new Gson();
public static List<Message> pollDeadLetters(SqsClient sqsClient, String dlqUrl, int maxMessages) {
ReceiveMessageRequest request = ReceiveMessageRequest.builder()
.queueUrl(dlqUrl)
.maxNumberOfMessages(maxMessages)
.waitTimeSeconds(20)
.attributeNames(List.of("All"))
.messageAttributeNames(List.of("All"))
.build();
try {
ReceiveMessageResponse response = sqsClient.receiveMessage(request);
return response.messages();
} catch (SqsException e) {
if (e.statusCode() == 403 || e.statusCode() == 404) {
throw new RuntimeException("DLQ access denied or not found: " + e.awsErrorDetails().errorMessage(), e);
}
if (e.statusCode() == 429) {
Thread.sleep(1000);
return pollDeadLetters(sqsClient, dlqUrl, maxMessages);
}
throw e;
}
}
public static JsonObject extractMetadata(Message message) {
String body = message.body();
try {
JsonObject json = gson.fromJson(body, JsonObject.class);
return json;
} catch (Exception e) {
return new JsonObject();
}
}
}
The pollDeadLetters method handles 403/404 failures immediately, implements a single retry for 429 throttling, and returns raw SQS messages. The extractMetadata helper parses the Genesys Cloud event payload for age calculation and retry count evaluation.
Step 3: Validate Against EventBridge Constraints and Schema
Before deletion, validate the payload against Genesys Cloud event schemas and business constraints. This prevents garbage collection failure caused by malformed or protected events.
import com.networknt.schema.JsonSchema;
import com.networknt.schema.SpecVersion;
import com.networknt.schema.input.loader.JsonInputLoader;
import com.networknt.schema.walk.DefaultJsonSchemaWalkListener;
import com.networknt.schema.walk.JsonSchemaWalker;
import java.io.InputStream;
import java.util.Set;
public class ConstraintValidator {
private final JsonSchema schema;
public ConstraintValidator(InputStream schemaStream) {
this.schema = JsonSchema.builder(schemaStream).objectMapper(new com.fasterxml.jackson.databind.ObjectMapper()).build();
}
public boolean validatePayload(String jsonPayload) {
try {
Set<com.networknt.schema.ValidationMessage> errors = schema.validate(jsonPayload);
return errors.isEmpty();
} catch (Exception e) {
return false;
}
}
}
The ConstraintValidator loads a JSON schema (fetched via Genesys Cloud /api/v2/events/schemas/{schemaId} or stored locally) and validates each dead letter payload. Invalid payloads are skipped during the purge iteration to preserve data integrity.
Step 4: Execute Atomic HTTP DELETE with Partial-Batch Verification
SQS deleteMessageBatch performs atomic deletion. We construct the delete request, execute it, and verify partial batch results to ensure clean event pipelines.
import software.amazon.awssdk.services.sqs.SqsClient;
import software.amazon.awssdk.services.sqs.model.*;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class BatchPurger {
public static DeleteMessageBatchResponse purgeBatch(SqsClient sqsClient, String dlqUrl, List<Message> targetMessages) {
List<DeleteMessageBatchRequestEntry> entries = targetMessages.stream()
.map(m -> DeleteMessageBatchRequestEntry.builder()
.id(m.messageId())
.receiptHandle(m.receiptHandle())
.build())
.collect(Collectors.toList());
DeleteMessageBatchRequest request = DeleteMessageBatchRequest.builder()
.queueUrl(dlqUrl)
.entries(entries)
.build();
try {
return sqsClient.deleteMessageBatch(request);
} catch (SqsException e) {
if (e.statusCode() == 429) {
Thread.sleep(2000);
return purgeBatch(sqsClient, dlqUrl, targetMessages);
}
throw e;
}
}
public static Map<String, Boolean> verifyPartialBatch(DeleteMessageBatchResponse response) {
Map<String, Boolean> verification = Map.of();
if (response.hasSuccessful()) {
response.successful().forEach(s -> verification = Map.of(s.id(), true));
}
if (response.hasFailed()) {
response.failed().forEach(f -> verification = Map.of(f.id(), false));
}
return verification;
}
}
The purgeBatch method translates SQS messages into delete entries and executes the atomic operation. The verifyPartialBatch method inspects successful and failed lists to implement partial-batch verification pipelines. Failed deletions trigger visibility timeout resets automatically via SQS configuration.
Step 5: Synchronize with External Log Aggregator and Generate Audit Logs
After each sweep iteration, calculate garbage collection latency, purge success rates, and emit structured audit logs. Forward results to an external log aggregator via webhook.
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Instant;
import java.util.Map;
import java.util.logging.Logger;
import java.util.logging.Level;
public class AuditAndMetricsEmitter {
private static final Logger logger = Logger.getLogger(AuditAndMetricsEmitter.class.getName());
private static final HttpClient httpClient = HttpClient.newBuilder()
.connectTimeout(java.time.Duration.ofSeconds(10))
.build();
public static void emitSweepAudit(String dlqRef, long messagesScanned, long messagesPurged,
long messagesSkipped, long latencyMs, String aggregatorUrl) {
Map<String, Object> auditPayload = Map.of(
"dlq_ref", dlqRef,
"eventbridge_matrix", "genesys_cloud_stream",
"purge_directive", "automatic_sweep",
"messages_scanned", messagesScanned,
"messages_purged", messagesPurged,
"messages_skipped", messagesSkipped,
"garbage_collecting_latency_ms", latencyMs,
"purge_success_rate", messagesScanned > 0 ? (double) messagesPurged / messagesScanned : 0.0,
"timestamp", Instant.now().toString(),
"audit_category", "eventbridge_governance"
);
String jsonBody = new com.google.gson.Gson().toJson(auditPayload);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(aggregatorUrl))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
.build();
try {
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 200 && response.statusCode() < 300) {
logger.info("Audit log synchronized with external-log-aggregator.");
} else {
logger.warning("External-log-aggregator returned status: " + response.statusCode());
}
} catch (Exception e) {
logger.log(Level.SEVERE, "Failed to emit audit log to external-log-aggregator", e);
}
}
}
The emitSweepAudit method packages sweep metrics, latency tracking, and purge success rates into a structured JSON payload. It posts to the external-log-aggregator URL and logs the result for eventbridge governance compliance.
Complete Working Example
import software.amazon.awssdk.services.sqs.SqsClient;
import software.amazon.awssdk.services.sqs.model.Message;
import software.amazon.awssdk.services.sqs.model.DeleteMessageBatchResponse;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
import java.util.logging.Level;
public class DeadLetterGarbageCollector {
private static final Logger logger = Logger.getLogger(DeadLetterGarbageCollector.class.getName());
private static final int BATCH_SIZE = 10;
private static final int MAX_RECEIVE_COUNT = 5;
public static void main(String[] args) {
PurgeDirective directive = new PurgeDirective(
"arn:aws:sqs:us-east-1:123456789012:genesys-eventbridge-dlq",
3600,
86400,
MAX_RECEIVE_COUNT,
"https://files.example.com/genesys-event-schema.json",
"https://log-aggregator.example.com/api/v1/ingest"
);
PurgeValidator validator = new PurgeValidator(directive);
SqsClient sqsClient = AuthSetup.buildSqsClient("us-east-1");
ConstraintValidator constraintValidator = new ConstraintValidator(
DeadLetterGarbageCollector.class.getResourceAsStream("/genesys-event-schema.json")
);
try {
List<Message> messages = DlgPoller.pollDeadLetters(sqsClient, directive.dlqRef(), BATCH_SIZE);
List<Message> purgeCandidates = new ArrayList<>();
int skippedCount = 0;
for (Message msg : messages) {
long sentTimestamp = msg.attributes().getOrDefault("SentTimestamp", "0");
Instant eventTime = Instant.ofEpochMilli(Long.parseLong(sentTimestamp));
int receiveCount = Integer.parseInt(msg.attributes().getOrDefault("ApproximateReceiveCount", "0"));
if (!validator.isWithinPurgeWindow(eventTime)) {
skippedCount++;
continue;
}
if (!validator.isRetryExhausted(receiveCount)) {
skippedCount++;
continue;
}
if (!constraintValidator.validatePayload(msg.body())) {
skippedCount++;
continue;
}
purgeCandidates.add(msg);
}
if (!purgeCandidates.isEmpty()) {
DeleteMessageBatchResponse deleteResponse = BatchPurger.purgeBatch(sqsClient, directive.dlqRef(), purgeCandidates);
Map<String, Boolean> verification = BatchPurger.verifyPartialBatch(deleteResponse);
logger.info("Purge iteration complete. Verified: " + verification.size() + " messages.");
}
long latency = validator.getElapsedMilliseconds();
AuditAndMetricsEmitter.emitSweepAudit(
directive.dlqRef(),
messages.size(),
purgeCandidates.size(),
skippedCount,
latency,
directive.externalLogAggregatorUrl()
);
} catch (Exception e) {
logger.log(Level.SEVERE, "Garbage collection sweep failed", e);
} finally {
sqsClient.close();
}
}
}
The complete example orchestrates authentication, polling, validation, atomic deletion, partial-batch verification, and audit synchronization. It runs as a standalone Java application. Replace the placeholder ARN, schema path, and webhook URL with production values.
Common Errors & Debugging
Error: 403 AccessDenied on SQS ReceiveMessage
- What causes it: The IAM role or credentials lack
sqs:ReceiveMessageorsqs:DeleteMessageBatchpermissions. The DLQ policy may also restrict cross-account access. - How to fix it: Attach the
AmazonSQSFullAccesspolicy or a custom policy granting the required actions to the execution role. Verify the queue URL matches the region. - Code showing the fix: The
DlgPoller.pollDeadLettersmethod catchesSqsExceptionwith status 403 and throws a descriptive runtime exception. Add explicit IAM policy verification in your deployment pipeline.
Error: 429 ThrottlingException on DeleteMessageBatch
- What causes it: Exceeding SQS per-second API limits during high-volume purge iterations.
- How to fix it: Implement exponential backoff. The
BatchPurger.purgeBatchmethod includes a 2-second sleep and recursive retry on 429 responses. Scale the retry delay based on thex-amzn-errortypeheader in production. - Code showing the fix: Already implemented in
BatchPurger.purgeBatch. Add a maximum retry counter to prevent infinite loops.
Error: Schema Validation Failure Skipping Valid Events
- What causes it: The JSON schema does not match the Genesys Cloud event version, or the payload contains optional fields that the schema marks as required.
- How to fix it: Fetch the latest schema via Genesys Cloud
/api/v2/events/schemas/{schemaId}using theevent:readscope. Relax strict validation by setting"additionalProperties": truein the schema or use a draft-04 validator. - Code showing the fix: Update the
ConstraintValidatorto load the schema dynamically at runtime instead of from a static resource.
Error: Partial-Batch Verification Shows Failed Deletions
- What causes it: Receipt handles expire if the visibility timeout elapses before the delete call. Concurrent consumers may also reset visibility.
- How to fix it: Increase the queue visibility timeout to match the maximum expected sweep duration. Use idempotent delete requests. The
verifyPartialBatchmethod isolates failed IDs for reprocessing in the next iteration. - Code showing the fix: Log failed message IDs from
response.failed()and enqueue them for the next sweep cycle.