Type: Bug
Component:
SQS
Describe the bug
When using SQS listener acknowledgement batching with a Standard SQS queue, acknowledgement can fail if the same SQS message is delivered more than once before the acknowledgement batch is flushed.
Spring Cloud AWS version: 4.0.2
Standard SQS queues provide at-least-once delivery, so the same message can legitimately be received more than once. If duplicate deliveries of the same SQS message are included in one acknowledgement batch, Spring Cloud AWS appears to use the SQS message id as the DeleteMessageBatchRequestEntry.id.
AWS requires each DeleteMessageBatchRequestEntry.id to be unique within a batch request. If two entries use the same id, AWS rejects the request with BatchEntryIdsNotDistinctException, causing the acknowledgement batch to fail.
Relevant code path:
SqsHeaderMapper.toHeaders(...) maps the SQS message id into the Spring message header id: new MessagingMessageHeaders(messageHeaders, UUID.fromString(source.messageId()))
SqsAcknowledgementExecutor.toDeleteMessageEntry(...) uses that id as the delete batch entry id: .id(MessageHeaderUtils.getId(message))
Actual behavior:
If an acknowledgement batch contains duplicate deliveries of the same SQS message, the generated DeleteMessageBatchRequest contains duplicate entry ids and AWS rejects the batch.
Expected behavior:
Spring Cloud AWS should avoid generating duplicate DeleteMessageBatchRequestEntry.id values within a single batch request.
The batch entry id does not need to be the SQS message id. It only needs to be unique within the request and usable for correlating batch results. For example, Spring Cloud AWS could generate unique per-entry ids and keep an internal mapping back to the original message/receipt handle for partial failure handling.
Disabling acknowledgement batching works around the issue, but batching should not fail only because SQS redelivered the same message in the same batch window.
Sample
I do not have a standalone sample application yet, but the issue should be reproducible by testing the acknowledgement executor directly:
- Create two Spring
Message<?> instances representing two deliveries of the same SQS message.
- Give both messages the same Spring message id, matching the same SQS
messageId.
- Give each message a valid
SqsHeaders.SQS_RECEIPT_HANDLE_HEADER; the receipt handles may be different.
- Pass both messages to
SqsAcknowledgementExecutor.execute(...).
- Inspect the
DeleteMessageBatchRequest sent to SqsAsyncClient.deleteMessageBatch(...).
Expected request entries should have unique DeleteMessageBatchRequestEntry.id values.
Actual request entries have duplicate ids because both are derived from MessageHeaderUtils.getId(message).
Pseudo-test shape:
@Test
void batchedAcknowledgementShouldUseUniqueDeleteBatchEntryIdsForDuplicateSqsMessageIds() {
UUID sqsMessageId = UUID.randomUUID();
Message<String> firstDelivery = messageWithIdAndReceiptHandle(sqsMessageId, "receipt-handle-1");
Message<String> secondDelivery = messageWithIdAndReceiptHandle(sqsMessageId, "receipt-handle-2");
SqsAsyncClient sqsAsyncClient = mock(SqsAsyncClient.class);
ArgumentCaptor<DeleteMessageBatchRequest> requestCaptor =
ArgumentCaptor.forClass(DeleteMessageBatchRequest.class);
when(sqsAsyncClient.deleteMessageBatch(requestCaptor.capture()))
.thenReturn(CompletableFuture.completedFuture(DeleteMessageBatchResponse.builder().build()));
SqsAcknowledgementExecutor<String> executor = new SqsAcknowledgementExecutor<>();
executor.setSqsAsyncClient(sqsAsyncClient);
executor.setQueueAttributes(queueAttributes("test-queue", "https://sqs.example/test-queue"));
executor.execute(List.of(firstDelivery, secondDelivery)).join();
List<String> batchEntryIds = requestCaptor.getValue().entries().stream()
.map(DeleteMessageBatchRequestEntry::id)
.toList();
assertThat(batchEntryIds).doesNotHaveDuplicates();
}
The exact test setup may differ because Spring message ids are normally generated by the framework. Another way to reproduce this is to use the real SqsHeaderMapper.toHeaders(...) conversion path with two AWS SDK SQS Message objects that have the same messageId.
Type: Bug
Component:
SQS
Describe the bug
When using SQS listener acknowledgement batching with a Standard SQS queue, acknowledgement can fail if the same SQS message is delivered more than once before the acknowledgement batch is flushed.
Spring Cloud AWS version: 4.0.2
Standard SQS queues provide at-least-once delivery, so the same message can legitimately be received more than once. If duplicate deliveries of the same SQS message are included in one acknowledgement batch, Spring Cloud AWS appears to use the SQS message id as the
DeleteMessageBatchRequestEntry.id.AWS requires each
DeleteMessageBatchRequestEntry.idto be unique within a batch request. If two entries use the same id, AWS rejects the request withBatchEntryIdsNotDistinctException, causing the acknowledgement batch to fail.Relevant code path:
SqsHeaderMapper.toHeaders(...)maps the SQS message id into the Spring message header id:new MessagingMessageHeaders(messageHeaders, UUID.fromString(source.messageId()))SqsAcknowledgementExecutor.toDeleteMessageEntry(...)uses that id as the delete batch entry id:.id(MessageHeaderUtils.getId(message))Actual behavior:
If an acknowledgement batch contains duplicate deliveries of the same SQS message, the generated
DeleteMessageBatchRequestcontains duplicate entry ids and AWS rejects the batch.Expected behavior:
Spring Cloud AWS should avoid generating duplicate
DeleteMessageBatchRequestEntry.idvalues within a single batch request.The batch entry id does not need to be the SQS message id. It only needs to be unique within the request and usable for correlating batch results. For example, Spring Cloud AWS could generate unique per-entry ids and keep an internal mapping back to the original message/receipt handle for partial failure handling.
Disabling acknowledgement batching works around the issue, but batching should not fail only because SQS redelivered the same message in the same batch window.
Sample
I do not have a standalone sample application yet, but the issue should be reproducible by testing the acknowledgement executor directly:
Message<?>instances representing two deliveries of the same SQS message.messageId.SqsHeaders.SQS_RECEIPT_HANDLE_HEADER; the receipt handles may be different.SqsAcknowledgementExecutor.execute(...).DeleteMessageBatchRequestsent toSqsAsyncClient.deleteMessageBatch(...).Expected request entries should have unique
DeleteMessageBatchRequestEntry.idvalues.Actual request entries have duplicate ids because both are derived from
MessageHeaderUtils.getId(message).Pseudo-test shape:
The exact test setup may differ because Spring message ids are normally generated by the framework. Another way to reproduce this is to use the real
SqsHeaderMapper.toHeaders(...)conversion path with two AWS SDK SQSMessageobjects that have the samemessageId.