Add configurable UUID conversion for non-AWS SQS-compatible services#1433
Add configurable UUID conversion for non-AWS SQS-compatible services#1433jm0514 wants to merge 8 commits intoawspring:mainfrom
Conversation
| accessor.setHeader(SqsHeaders.SQS_AWS_MESSAGE_ID_HEADER, source.messageId()); | ||
| MessageHeaders messageHeaders = accessor.toMessageHeaders(); | ||
| logger.trace("Mapped headers {} for message {}", messageHeaders, source.messageId()); | ||
| return new MessagingMessageHeaders(messageHeaders, UUID.randomUUID()); |
There was a problem hiding this comment.
Passing a random UUID does not feel right. Perhaps MessagingMessageHeaders can have id set as String instead of UUID? cc @tomazfernandes
There was a problem hiding this comment.
Thanks for the suggestion! Updated to generate UUID from Message ID instead of random UUID.
|
Hey @jm0514, thanks for the PR!
@maciejwalkowiak, the issue there is that the Might be worth raising with the Spring Messaging team? In the meantime, I think we can make the solution simpler if we leverage the fact that most ID-related operations are made through the Instead of requiring a configuration change to support other ID types, we could simplify the logic by trying to parse the UUID, and if it fails we add the new header as suggested in the PR. Then in the The caveat would be that for non UUID ids, the result from getting the ID header directly would be a random UUID, but I don't think there's a way around it with the existing What do you folks think? |
|
tomazfernandes
left a comment
There was a problem hiding this comment.
Hey @jm0514, thanks for the updates. Overall direction looks solid.
I have a couple of suggestions to simplify the design and reduce duplication.
| public MessagingMessageConverter<Message> messageConverter(ObjectProvider<JsonMapper> jsonMapperProvider) { | ||
| JsonMapper jsonMapper = jsonMapperProvider.getIfAvailable(); | ||
| return jsonMapper != null ? new SqsMessagingMessageConverter(jsonMapper) | ||
| public MessagingMessageConverter<Message> messageConverter(ObjectProvider<JsonMapper> jsonMapperProvider, |
There was a problem hiding this comment.
Currently the auto-config manually creates a SqsHeaderMapper and sets the flag on it. Instead, let's add convertMessageIdToUuid as a property on both SqsContainerOptions and SqsTemplateOptions, so auto-config just does:
factory.configure(options -> options.convertMessageIdToUuid(sqsProperties.getConvertMessageIdToUuid()));
builder.configure(options -> options.convertMessageIdToUuid(sqsProperties.getConvertMessageIdToUuid()));
This keeps the configuration surface consistent between container and template, removes the manual SqsHeaderMapper creation from auto-config, and gives programmatic users (without auto-config) a clean way to set it through the standard options API.
There was a problem hiding this comment.
Thanks for the great suggestion! This makes the configuration much cleaner and consistent with the existing patterns.
Applied in 1d32b0c. Moved convertMessageIdToUuid to both SqsContainerOptions and SqsTemplateOptions, so auto-config now uses factory.configure() and builder.configure() instead of manually creating SqsHeaderMapper.
Added getHeaderMapper() to AbstractMessagingMessageConverter to propagate the option to the header mapper.
| MessageHeaders messageHeaders = accessor.toMessageHeaders(); | ||
| logger.trace("Mapped headers {} for message {}", messageHeaders, source.messageId()); | ||
| return new MessagingMessageHeaders(messageHeaders, UUID.fromString(source.messageId())); | ||
|
|
There was a problem hiding this comment.
Let's consolidate this logic so we get consistent behavior across both listener and template.
My suggestion would be to create a SqsMessageIdResolver utility class with a method such as:
public static MessageHeaders resolveAndAddMessageId(String messageId, MessageHeaders headers,
boolean convertMessageIdToUuid) {
MessageHeaders withRawId = MessageHeaderUtils.addHeaderIfAbsent(
headers, SqsHeaders.SQS_RAW_MESSAGE_ID_HEADER, messageId);
if (isValidUuid(messageId)) {
return new MessagingMessageHeaders(withRawId, UUID.fromString(messageId));
}
if (convertMessageIdToUuid) {
throw new MessagingException(String.format(
"Message ID '%s' is not a valid UUID. To support non-UUID message IDs, "
+ "set 'spring.cloud.aws.sqs.convert-message-id-to-uuid=false'. "
+ "The raw message ID will be available via the '%s' header.",
messageId, SqsHeaders.SQS_RAW_MESSAGE_ID_HEADER));
}
return new MessagingMessageHeaders(withRawId,
UUID.nameUUIDFromBytes(messageId.getBytes(StandardCharsets.UTF_8)));
}
private static boolean isValidUuid(String value) {
try {
UUID.fromString(value);
return true;
}
catch (IllegalArgumentException e) {
return false;
}
}Then we'd use it in both SqsHeaderMapper and SqsTemplate.
Open to suggestions if you have a different idea.
There was a problem hiding this comment.
Your suggestion was clear and well thought out, so I followed it as proposed.
Applied in f1d7a50. Created SqsMessageIdResolver with resolveAndAddMessageId() for the listener side and resolveUuid() / isValidUuid() for the template side.
Both SqsHeaderMapper and SqsTemplate now delegate to this utility class.
There was a problem hiding this comment.
@jm0514, thanks for the updates. I left a few comments, we're getting closer.
The main point is that there's a mismatch between what Spring Messaging exposes, which is a UUID-based Message, and the SQS contract, which uses String ids.
Therefore we need a consistent message-id translation layer exposed consistently to users regardless of whether they're using SQS directly or another compatible implementation.
This also makes it easier to move between SQS and SQS-compatible services over time.
Also, please add @since 4.1.0 to the new classes.
| if (sequenceNumber != null) { | ||
| additionalInfo.put(SqsTemplateParameters.SEQUENCE_NUMBER_PARAMETER_NAME, sequenceNumber); | ||
| } | ||
| UUID messageId = SqsMessageIdResolver.resolveUuid(rawMessageId); |
There was a problem hiding this comment.
I'd suggest always adding the new header to originalMessage through resolveAndAddMessageId, then reading the UUID value back from the resulting message.
That keeps the behavior consistent between receive and send paths and avoids silently handling the UUID conversion when not explicitly configured.
That also allows us to make the remaining methods of SqsMessageIdResolver private.
| /** | ||
| * The raw provider message ID when it is not a valid UUID. | ||
| */ | ||
| public static final String RAW_MESSAGE_ID_PARAMETER_NAME = "rawMessageId"; |
There was a problem hiding this comment.
Let's name it "SQS_RAW_MESSAGE_ID_PARAMETER_NAME" for consistency with the header.
| } | ||
| UUID messageId = SqsMessageIdResolver.resolveUuid(rawMessageId); | ||
| if (!SqsMessageIdResolver.isValidUuid(rawMessageId)) { | ||
| additionalInfo.put(SqsTemplateParameters.RAW_MESSAGE_ID_PARAMETER_NAME, rawMessageId); |
There was a problem hiding this comment.
If this id is already added to the MessageHeaders, we may not need to also expose it through additionalInfo.
If we keep this parameter, we should always add it so users can rely on it regardless of whether they are using SQS or another implementation.
| * {@link software.amazon.awssdk.services.sqs.model.Message} instances. | ||
| * @return the header mapper instance. | ||
| */ | ||
| public HeaderMapper<S> getHeaderMapper() { |
There was a problem hiding this comment.
We could change to a configuration-style API here, for example configureHeaderMapper(Consumer<HeaderMapper<S>> configurer).
That preserves encapsulation and keeps the API aligned with the configuration patterns used in the project.
| configureHeaderMapper(sqsContainerOptions); | ||
| } | ||
|
|
||
| private void configureHeaderMapper(SqsContainerOptions sqsContainerOptions) { |
There was a problem hiding this comment.
Let's move the configuration logic to SqsMessageIdResolver and centralize all message id resolution logic there and keep behavior consistent between the template and message source.
I suggest something along these lines:
public static void configureMessageIdResolution(
MessagingMessageConverter<Message> converter, boolean convertMessageIdToUuid)`.| /** | ||
| * Sqs specific options for the {@link SqsTemplate}. | ||
| * | ||
| * @author Jeongmin Kim |
There was a problem hiding this comment.
Please add me as an author here as well, along with @since 3.0.0. I missed it when the class was originally created.
Add configurable UUID conversion for non-AWS SQS-compatible services
📢 Type of change
📜 Description
Added configurable support for SQS-compatible cloud services (like Yandex Message Queue) that use non-UUID MessageId formats. The framework now supports both UUID and non-UUID MessageId handling through a new configuration option.
Configuration:
💡 Motivation and Context
Solves #814
💚 How did you test it?
SqsHeaderMapperandMessageHeaderUtils📝 Checklist
🔮 Next steps