Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/src/main/asciidoc/_configprops.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
|spring.cloud.aws.sns.endpoint | | Overrides the default endpoint.
|spring.cloud.aws.sns.region | | Overrides the default region.
|spring.cloud.aws.sqs.dualstack-enabled | | Configure whether the AWS client should use the AWS dualstack endpoint. Note that not each AWS service supports dual-stack. For complete list check <a href="https://docs.aws.amazon.com/vpc/latest/userguide/aws-ipv6-support.html">AWS services that support IPv6</a>
|spring.cloud.aws.sqs.convert-message-id-to-uuid | `+++true+++` | Whether to convert SQS message IDs to UUIDs. Set to `false` for SQS-compatible providers that return non-UUID message IDs.
|spring.cloud.aws.sqs.enabled | `+++true+++` | Enables SQS integration.
|spring.cloud.aws.sqs.endpoint | | Overrides the default endpoint.
|spring.cloud.aws.sqs.listener.auto-startup | | Defines whether SQS listeners will start automatically or not.
Expand Down
21 changes: 21 additions & 0 deletions docs/src/main/asciidoc/sqs.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,27 @@ If `SendBatchFailureStrategy#DO_NOT_THROW` is configured, no exception is thrown

For convenience, the `additionalInformation` parameters can be found as constants in the `SqsTemplateParameters` class.

===== Non-UUID Message IDs

By default, Spring Cloud AWS SQS expects the message ID returned by SQS to be a valid UUID.
If a non-UUID message ID is received, an error is thrown with instructions to enable non-UUID support.

To enable non-UUID message ID support (e.g., for Yandex Message Queue or other SQS-compatible providers):

[source,properties]
----
spring.cloud.aws.sqs.convert-message-id-to-uuid=false
----

When disabled:

* **Receive side**: The raw provider message ID is stored in the `Sqs_RawMessageId` header.
A deterministic UUID derived from the raw ID is used as the Spring `MessageHeaders.ID`.
Access the raw ID via `MessageHeaderUtils.getRawMessageId(message)`.
* **Send side**: If the send response contains a non-UUID message ID,
`SendResult.messageId()` returns a deterministic UUID and the raw ID is available
in `SendResult.additionalInformation()` under the `rawMessageId` key.

[[template-message-conversion]]
==== Template Message Conversion

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import io.awspring.cloud.sqs.operations.SqsTemplate;
import io.awspring.cloud.sqs.operations.SqsTemplateBuilder;
import io.awspring.cloud.sqs.support.converter.MessagingMessageConverter;
import io.awspring.cloud.sqs.support.converter.SqsHeaderMapper;
import io.awspring.cloud.sqs.support.converter.SqsMessagingMessageConverter;
import io.awspring.cloud.sqs.support.converter.legacy.JacksonJsonMessageConverterMigration;
import io.awspring.cloud.sqs.support.converter.legacy.JacksonMessageConverterMigration;
Expand Down Expand Up @@ -64,6 +65,7 @@
* @author Maciej Walkowiak
* @author Wei Jiang
* @author Dongha Kim
* @author Jeongmin Kim
* @since 3.0
*/
@AutoConfiguration
Expand Down Expand Up @@ -159,10 +161,15 @@ private void configureProperties(SqsContainerOptionsBuilder options) {
static class SqsJacksonConfiguration {
@ConditionalOnMissingBean
@Bean
public MessagingMessageConverter<Message> messageConverter(ObjectProvider<JsonMapper> jsonMapperProvider) {
JsonMapper jsonMapper = jsonMapperProvider.getIfAvailable();
return jsonMapper != null ? new SqsMessagingMessageConverter(jsonMapper)
public MessagingMessageConverter<Message> messageConverter(ObjectProvider<JsonMapper> jsonMapperProvider,
SqsProperties sqsProperties) {
SqsMessagingMessageConverter converter = jsonMapperProvider.getIfAvailable() != null
? new SqsMessagingMessageConverter(jsonMapperProvider.getIfAvailable())
: new SqsMessagingMessageConverter();
SqsHeaderMapper headerMapper = new SqsHeaderMapper();
headerMapper.setConvertMessageIdToUuid(sqsProperties.getConvertMessageIdToUuid());
converter.setHeaderMapper(headerMapper);
return converter;
}

@Bean
Expand All @@ -179,8 +186,12 @@ public JacksonMessageConverterMigration jsonMapperWrapper(ObjectProvider<JsonMap
static class LegacySqsJackson2Configuration {
@ConditionalOnMissingBean
@Bean
public MessagingMessageConverter<Message> messageConverter() {
return new LegacyJackson2SqsMessagingMessageConverter();
public MessagingMessageConverter<Message> messageConverter(SqsProperties sqsProperties) {
LegacyJackson2SqsMessagingMessageConverter converter = new LegacyJackson2SqsMessagingMessageConverter();
SqsHeaderMapper headerMapper = new SqsHeaderMapper();
headerMapper.setConvertMessageIdToUuid(sqsProperties.getConvertMessageIdToUuid());
converter.setHeaderMapper(headerMapper);
return converter;
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
*
* @author Tomaz Fernandes
* @author Wei Jiang
* @author Jeongmin Kim
* @since 3.0
*/
@ConfigurationProperties(prefix = SqsProperties.PREFIX)
Expand All @@ -51,6 +52,20 @@ public void setListener(Listener listener) {

private Boolean observationEnabled = false;

/**
* Whether to convert SQS message IDs to UUIDs. Set to {@code false} for SQS-compatible providers that return
* non-UUID message IDs.
*/
private Boolean convertMessageIdToUuid = true;

public Boolean getConvertMessageIdToUuid() {
return convertMessageIdToUuid;
}

public void setConvertMessageIdToUuid(Boolean convertMessageIdToUuid) {
this.convertMessageIdToUuid = convertMessageIdToUuid;
}

/**
* Return the strategy to use if the queue is not found.
* @return the {@link QueueNotFoundStrategy}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package io.awspring.cloud.sqs;

import io.awspring.cloud.sqs.listener.SqsHeaders;
import io.awspring.cloud.sqs.support.converter.MessagingMessageHeaders;
import java.util.Collection;
import java.util.Map;
Expand All @@ -30,6 +31,7 @@
* Utility class for extracting {@link MessageHeaders} from a {@link Message}.
*
* @author Tomaz Fernandes
* @author Jeongmin Kim
* @since 3.0
*/
public class MessageHeaderUtils {
Expand Down Expand Up @@ -150,4 +152,22 @@ public static <T> Message<T> removeHeaderIfPresent(Message<T> message, String ke
return new GenericMessage<>(message.getPayload(), newHeaders);
}

/**
* Return the raw provider message ID, falling back to Spring message ID if not present.
* @param message the message.
* @return the raw provider ID or Spring ID.
*/
public static String getRawMessageId(Message<?> message) {
String rawMessageId = message.getHeaders().get(SqsHeaders.SQS_RAW_MESSAGE_ID_HEADER, String.class);
return rawMessageId != null ? rawMessageId : getId(message);
}

/**
* Return the messages' raw provider IDs as a concatenated {@link String}.
* @param messages the messages.
* @return the raw provider IDs.
*/
public static <T> String getRawMessageId(Collection<Message<T>> messages) {
return messages.stream().map(MessageHeaderUtils::getRawMessageId).collect(Collectors.joining("; "));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
*
* @author Tomaz Fernandes
* @author Artem Bilan
* @author Jeongmin Kim
*
* @since 3.0
*
Expand Down Expand Up @@ -88,6 +89,11 @@ private SqsHeaders() {
*/
public static final String SQS_DEFAULT_TYPE_HEADER = "JavaType";

/**
* Header for the raw provider message ID when not using UUID conversion.
*/
public static final String SQS_RAW_MESSAGE_ID_HEADER = SQS_HEADER_PREFIX + "RawMessageId";

public static class MessageSystemAttributes {

private MessageSystemAttributes() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import io.awspring.cloud.sqs.support.converter.SqsMessagingMessageConverter;
import io.awspring.cloud.sqs.support.converter.legacy.LegacyJackson2SqsMessagingMessageConverter;
import io.awspring.cloud.sqs.support.observation.SqsTemplateObservation;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Collection;
import java.util.Collections;
Expand Down Expand Up @@ -77,6 +78,7 @@
* @author Tomaz Fernandes
* @author Zhong Xi Lu
* @author Hyunggeol Lee
* @author Jeongmin Kim
*
* @since 3.0
*/
Expand Down Expand Up @@ -327,16 +329,36 @@ private CompletableFuture<Boolean> handleAutoDeduplication(String endpointName)
protected <T> CompletableFuture<SendResult<T>> doSendAsync(String endpointName, Message message,
org.springframework.messaging.Message<T> originalMessage) {
return createSendMessageRequest(endpointName, message).thenCompose(this.sqsAsyncClient::sendMessage)
.thenApply(response -> createSendResult(UUID.fromString(response.messageId()),
response.sequenceNumber(), endpointName, originalMessage));
.thenApply(response -> createSendResult(response.messageId(), response.sequenceNumber(), endpointName,
originalMessage));
}

private <T> SendResult<T> createSendResult(UUID messageId, @Nullable String sequenceNumber, String endpointName,
org.springframework.messaging.Message<T> originalMessage) {
private <T> SendResult<T> createSendResult(String rawMessageId, @Nullable String sequenceNumber,
String endpointName, org.springframework.messaging.Message<T> originalMessage) {
Map<String, Object> additionalInfo = new HashMap<>();
if (sequenceNumber != null) {
additionalInfo.put(SqsTemplateParameters.SEQUENCE_NUMBER_PARAMETER_NAME, sequenceNumber);
}
UUID messageId;
if (isValidUuid(rawMessageId)) {
messageId = UUID.fromString(rawMessageId);
}
else {
messageId = UUID.nameUUIDFromBytes(rawMessageId.getBytes(StandardCharsets.UTF_8));
additionalInfo.put(SqsTemplateParameters.RAW_MESSAGE_ID_PARAMETER_NAME, rawMessageId);
}
return new SendResult<>(messageId, endpointName, originalMessage,
sequenceNumber != null
? Collections.singletonMap(SqsTemplateParameters.SEQUENCE_NUMBER_PARAMETER_NAME, sequenceNumber)
: Collections.emptyMap());
additionalInfo.isEmpty() ? Collections.emptyMap() : additionalInfo);
}

private static boolean isValidUuid(String value) {
try {
UUID.fromString(value);
return true;
}
catch (IllegalArgumentException e) {
return false;
}
}

private CompletableFuture<SendMessageRequest> createSendMessageRequest(String endpointName, Message message) {
Expand All @@ -358,8 +380,8 @@ protected <T> CompletableFuture<SendResult.Batch<T>> doSendBatchAsync(String end
Collection<Message> messages, Collection<org.springframework.messaging.Message<T>> originalMessages) {
logger.debug("Sending messages {} to endpoint {}", messages, endpointName);
return createSendMessageBatchRequest(endpointName, messages).thenCompose(this.sqsAsyncClient::sendMessageBatch)
.thenApply(response -> createSendResultBatch(response, endpointName,
originalMessages.stream().collect(Collectors.toMap(MessageHeaderUtils::getId, msg -> msg))));
.thenApply(response -> createSendResultBatch(response, endpointName, originalMessages.stream()
.collect(Collectors.toMap(MessageHeaderUtils::getRawMessageId, msg -> msg))));
}

private <T> SendResult.Batch<T> createSendResultBatch(SendMessageBatchResponse response, String endpointName,
Expand All @@ -379,10 +401,8 @@ private <T> Collection<SendResult.Failed<T>> createSendResultFailed(SendMessageB

private <T> Collection<SendResult<T>> doCreateSendResultBatch(SendMessageBatchResponse response,
String endpointName, Map<String, org.springframework.messaging.Message<T>> originalMessagesById) {
return response
.successful().stream().map(entry -> createSendResult(UUID.fromString(entry.messageId()),
entry.sequenceNumber(), endpointName, getOriginalMessage(originalMessagesById, entry)))
.toList();
return response.successful().stream().map(entry -> createSendResult(entry.messageId(), entry.sequenceNumber(),
endpointName, getOriginalMessage(originalMessagesById, entry))).toList();
}

private <T> org.springframework.messaging.Message<T> getOriginalMessage(
Expand Down Expand Up @@ -540,7 +560,7 @@ private Map<String, Object> addMissingFifoReceiveHeaders(Map<String, Object> hea
private CompletableFuture<Void> deleteMessages(String endpointName,
Collection<org.springframework.messaging.Message<?>> messages) {
logger.trace("Acknowledging in queue {} messages {}", endpointName,
MessageHeaderUtils.getId(addTypeToMessages(messages)));
MessageHeaderUtils.getRawMessageId(addTypeToMessages(messages)));
return getQueueAttributes(endpointName)
.thenCompose(attributes -> this.sqsAsyncClient.deleteMessageBatch(DeleteMessageBatchRequest.builder()
.queueUrl(attributes.getQueueUrl()).entries(createDeleteMessageEntries(messages)).build()))
Expand All @@ -559,7 +579,8 @@ private Collection<org.springframework.messaging.Message<?>> getFailedAckMessage
DeleteMessageBatchResponse response, Collection<org.springframework.messaging.Message<?>> messages,
String endpointName) {
return response.failed().stream().map(BatchResultErrorEntry::id)
.map(id -> messages.stream().filter(msg -> MessageHeaderUtils.getId(msg).equals(id)).findFirst()
.map(id -> messages.stream().filter(msg -> MessageHeaderUtils.getRawMessageId(msg).equals(id))
.findFirst()
.orElseThrow(() -> new SqsAcknowledgementException(
"Could not correlate ids for acknowledgement failure", Collections.emptyList(),
messages, endpointName)))
Expand All @@ -570,7 +591,8 @@ private Collection<org.springframework.messaging.Message<?>> getSuccessfulAckMes
DeleteMessageBatchResponse response, Collection<org.springframework.messaging.Message<?>> messages,
String endpointName) {
return response.successful().stream().map(DeleteMessageBatchResultEntry::id)
.map(id -> messages.stream().filter(msg -> MessageHeaderUtils.getId(msg).equals(id)).findFirst()
.map(id -> messages.stream().filter(msg -> MessageHeaderUtils.getRawMessageId(msg).equals(id))
.findFirst()
.orElseThrow(() -> new SqsAcknowledgementException(
"Could not correlate ids for acknowledgement failure", Collections.emptyList(),
messages, endpointName)))
Expand All @@ -588,22 +610,22 @@ private void logAcknowledgement(String endpointName, Collection<org.springframew
DeleteMessageBatchResponse response, @Nullable Throwable t) {
if (t != null) {
logger.error("Error acknowledging in queue {} messages {}", endpointName,
MessageHeaderUtils.getId(addTypeToMessages(messages)));
MessageHeaderUtils.getRawMessageId(addTypeToMessages(messages)));
}
else if (!response.failed().isEmpty()) {
logger.warn("Some messages could not be acknowledged in queue {}: {}", endpointName,
response.failed().stream().map(BatchResultErrorEntry::id).toList());
}
else {
logger.trace("Acknowledged messages in queue {}: {}", endpointName,
MessageHeaderUtils.getId(addTypeToMessages(messages)));
MessageHeaderUtils.getRawMessageId(addTypeToMessages(messages)));
}
}

private Collection<DeleteMessageBatchRequestEntry> createDeleteMessageEntries(
Collection<org.springframework.messaging.Message<?>> messages) {
return messages.stream()
.map(message -> DeleteMessageBatchRequestEntry.builder().id(MessageHeaderUtils.getId(message))
.map(message -> DeleteMessageBatchRequestEntry.builder().id(MessageHeaderUtils.getRawMessageId(message))
.receiptHandle(
MessageHeaderUtils.getHeaderAsString(message, SqsHeaders.SQS_RECEIPT_HANDLE_HEADER))
.build())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,9 @@ public class SqsTemplateParameters {
*/
public static final String ERROR_CODE_PARAMETER_NAME = "code";

/**
* The raw provider message ID when it is not a valid UUID.
*/
public static final String RAW_MESSAGE_ID_PARAMETER_NAME = "rawMessageId";

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package io.awspring.cloud.sqs.support.converter;

import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.UUID;
import org.jspecify.annotations.Nullable;
Expand All @@ -23,6 +24,7 @@
/**
* {@link MessageHeaders} implementation that allows providing an external {@link UUID}.
* @author Tomaz Fernandes
* @author Jeongmin Kim
* @since 3.0
*/
public class MessagingMessageHeaders extends MessageHeaders {
Expand Down Expand Up @@ -53,4 +55,15 @@ public MessagingMessageHeaders(@Nullable Map<String, Object> headers, @Nullable
public MessagingMessageHeaders(@Nullable Map<String, Object> headers, @Nullable UUID id, @Nullable Long timestamp) {
super(headers, id, timestamp);
}

/**
* Create an instance with String ID converted to consistent UUID
*/
public MessagingMessageHeaders(@Nullable Map<String, Object> headers, @Nullable String stringId) {
super(headers, stringId != null ? generateConsistentUuid(stringId) : null, null);
}

private static UUID generateConsistentUuid(String stringId) {
return UUID.nameUUIDFromBytes(stringId.getBytes(StandardCharsets.UTF_8));
}
}
Loading