diff --git a/docs/src/main/asciidoc/_configprops.adoc b/docs/src/main/asciidoc/_configprops.adoc index a3de5dc58..af96fab57 100644 --- a/docs/src/main/asciidoc/_configprops.adoc +++ b/docs/src/main/asciidoc/_configprops.adoc @@ -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 AWS services that support IPv6 +|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. diff --git a/docs/src/main/asciidoc/sqs.adoc b/docs/src/main/asciidoc/sqs.adoc index bf120b162..233bd7555 100644 --- a/docs/src/main/asciidoc/sqs.adoc +++ b/docs/src/main/asciidoc/sqs.adoc @@ -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 diff --git a/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/sqs/SqsAutoConfiguration.java b/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/sqs/SqsAutoConfiguration.java index c6c89301e..7474074b5 100644 --- a/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/sqs/SqsAutoConfiguration.java +++ b/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/sqs/SqsAutoConfiguration.java @@ -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; @@ -64,6 +65,7 @@ * @author Maciej Walkowiak * @author Wei Jiang * @author Dongha Kim + * @author Jeongmin Kim * @since 3.0 */ @AutoConfiguration @@ -159,10 +161,15 @@ private void configureProperties(SqsContainerOptionsBuilder options) { static class SqsJacksonConfiguration { @ConditionalOnMissingBean @Bean - public MessagingMessageConverter messageConverter(ObjectProvider jsonMapperProvider) { - JsonMapper jsonMapper = jsonMapperProvider.getIfAvailable(); - return jsonMapper != null ? new SqsMessagingMessageConverter(jsonMapper) + public MessagingMessageConverter messageConverter(ObjectProvider 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 @@ -179,8 +186,12 @@ public JacksonMessageConverterMigration jsonMapperWrapper(ObjectProvider messageConverter() { - return new LegacyJackson2SqsMessagingMessageConverter(); + public MessagingMessageConverter messageConverter(SqsProperties sqsProperties) { + LegacyJackson2SqsMessagingMessageConverter converter = new LegacyJackson2SqsMessagingMessageConverter(); + SqsHeaderMapper headerMapper = new SqsHeaderMapper(); + headerMapper.setConvertMessageIdToUuid(sqsProperties.getConvertMessageIdToUuid()); + converter.setHeaderMapper(headerMapper); + return converter; } @Bean diff --git a/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/sqs/SqsProperties.java b/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/sqs/SqsProperties.java index 509768d2c..fb37f35bb 100644 --- a/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/sqs/SqsProperties.java +++ b/spring-cloud-aws-autoconfigure/src/main/java/io/awspring/cloud/autoconfigure/sqs/SqsProperties.java @@ -26,6 +26,7 @@ * * @author Tomaz Fernandes * @author Wei Jiang + * @author Jeongmin Kim * @since 3.0 */ @ConfigurationProperties(prefix = SqsProperties.PREFIX) @@ -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} diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/MessageHeaderUtils.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/MessageHeaderUtils.java index 5ee1e238c..6f167157c 100644 --- a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/MessageHeaderUtils.java +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/MessageHeaderUtils.java @@ -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; @@ -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 { @@ -150,4 +152,22 @@ public static Message removeHeaderIfPresent(Message 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 String getRawMessageId(Collection> messages) { + return messages.stream().map(MessageHeaderUtils::getRawMessageId).collect(Collectors.joining("; ")); + } } diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/SqsHeaders.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/SqsHeaders.java index 41fcc166a..87101b1f2 100644 --- a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/SqsHeaders.java +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/SqsHeaders.java @@ -23,6 +23,7 @@ * * @author Tomaz Fernandes * @author Artem Bilan + * @author Jeongmin Kim * * @since 3.0 * @@ -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() { diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/operations/SqsTemplate.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/operations/SqsTemplate.java index dd577996c..bd790c5cd 100644 --- a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/operations/SqsTemplate.java +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/operations/SqsTemplate.java @@ -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; @@ -77,6 +78,7 @@ * @author Tomaz Fernandes * @author Zhong Xi Lu * @author Hyunggeol Lee + * @author Jeongmin Kim * * @since 3.0 */ @@ -327,16 +329,36 @@ private CompletableFuture handleAutoDeduplication(String endpointName) protected CompletableFuture> doSendAsync(String endpointName, Message message, org.springframework.messaging.Message 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 SendResult createSendResult(UUID messageId, @Nullable String sequenceNumber, String endpointName, - org.springframework.messaging.Message originalMessage) { + private SendResult createSendResult(String rawMessageId, @Nullable String sequenceNumber, + String endpointName, org.springframework.messaging.Message originalMessage) { + Map 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 createSendMessageRequest(String endpointName, Message message) { @@ -358,8 +380,8 @@ protected CompletableFuture> doSendBatchAsync(String end Collection messages, Collection> 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 SendResult.Batch createSendResultBatch(SendMessageBatchResponse response, String endpointName, @@ -379,10 +401,8 @@ private Collection> createSendResultFailed(SendMessageB private Collection> doCreateSendResultBatch(SendMessageBatchResponse response, String endpointName, Map> 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 org.springframework.messaging.Message getOriginalMessage( @@ -540,7 +560,7 @@ private Map addMissingFifoReceiveHeaders(Map hea private CompletableFuture deleteMessages(String endpointName, Collection> 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())) @@ -559,7 +579,8 @@ private Collection> getFailedAckMessage DeleteMessageBatchResponse response, Collection> 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))) @@ -570,7 +591,8 @@ private Collection> getSuccessfulAckMes DeleteMessageBatchResponse response, Collection> 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))) @@ -588,7 +610,7 @@ private void logAcknowledgement(String endpointName, Collection createDeleteMessageEntries( Collection> 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()) diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/operations/SqsTemplateParameters.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/operations/SqsTemplateParameters.java index 6c3e3662e..801ad5ce3 100644 --- a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/operations/SqsTemplateParameters.java +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/operations/SqsTemplateParameters.java @@ -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"; + } diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/support/converter/MessagingMessageHeaders.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/support/converter/MessagingMessageHeaders.java index 99ad15a0b..883e419c4 100644 --- a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/support/converter/MessagingMessageHeaders.java +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/support/converter/MessagingMessageHeaders.java @@ -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; @@ -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 { @@ -53,4 +55,15 @@ public MessagingMessageHeaders(@Nullable Map headers, @Nullable public MessagingMessageHeaders(@Nullable Map 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 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)); + } } diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/support/converter/SqsHeaderMapper.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/support/converter/SqsHeaderMapper.java index c9f454616..fa58a7111 100644 --- a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/support/converter/SqsHeaderMapper.java +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/support/converter/SqsHeaderMapper.java @@ -51,6 +51,7 @@ * @author Tomaz Fernandes * @author Alain Sahli * @author Maciej Walkowiak + * @author Jeongmin Kim * * @since 3.0 * @see LegacyJackson2SqsMessagingMessageConverter @@ -62,12 +63,18 @@ public class SqsHeaderMapper implements ContextAwareHeaderMapper { private BiFunction additionalHeadersFunction = ((message, accessor) -> accessor.toMessageHeaders()); + private boolean convertMessageIdToUuid = true; + public void setAdditionalHeadersFunction( BiFunction headerFunction) { Assert.notNull(headerFunction, "headerFunction cannot be null"); this.additionalHeadersFunction = headerFunction; } + public void setConvertMessageIdToUuid(boolean convertMessageIdToUuid) { + this.convertMessageIdToUuid = convertMessageIdToUuid; + } + @Override public Message fromHeaders(MessageHeaders headers) { Message.Builder builder = Message.builder(); @@ -157,9 +164,36 @@ public MessageHeaders toHeaders(Message source) { accessor.copyHeadersIfAbsent(getMessageAttributesAsHeaders(source)); accessor.copyHeadersIfAbsent(createDefaultHeaders(source)); accessor.copyHeadersIfAbsent(createAdditionalHeaders(source)); - MessageHeaders messageHeaders = accessor.toMessageHeaders(); - logger.trace("Mapped headers {} for message {}", messageHeaders, source.messageId()); - return new MessagingMessageHeaders(messageHeaders, UUID.fromString(source.messageId())); + + if (convertMessageIdToUuid) { + if (!isValidUuid(source.messageId())) { + 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.", + source.messageId(), SqsHeaders.SQS_RAW_MESSAGE_ID_HEADER)); + } + MessageHeaders messageHeaders = accessor.toMessageHeaders(); + logger.trace("Mapped headers {} for message {}", messageHeaders, source.messageId()); + return new MessagingMessageHeaders(messageHeaders, UUID.fromString(source.messageId())); + } + else { + accessor.setHeader(SqsHeaders.SQS_RAW_MESSAGE_ID_HEADER, source.messageId()); + MessageHeaders messageHeaders = accessor.toMessageHeaders(); + logger.trace("Mapped headers {} for message {}", messageHeaders, source.messageId()); + return new MessagingMessageHeaders(messageHeaders, source.messageId()); + } + + } + + private boolean isValidUuid(String messageId) { + try { + UUID.fromString(messageId); + return true; + } + catch (IllegalArgumentException e) { + return false; + } } private MessageHeaders createAdditionalHeaders(Message source) { diff --git a/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/MessageHeaderUtilsTest.java b/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/MessageHeaderUtilsTest.java index e156e9ec8..784bbf301 100644 --- a/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/MessageHeaderUtilsTest.java +++ b/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/MessageHeaderUtilsTest.java @@ -17,6 +17,9 @@ import static org.assertj.core.api.Assertions.assertThat; +import io.awspring.cloud.sqs.listener.SqsHeaders; +import java.util.Collection; +import java.util.List; import org.junit.jupiter.api.Test; import org.springframework.messaging.Message; import org.springframework.messaging.support.MessageBuilder; @@ -25,6 +28,7 @@ * Tests for {@link MessageHeaderUtils}. * * @author Tomaz Fernandes + * @author Jeongmin Kim */ class MessageHeaderUtilsTest { @@ -93,4 +97,51 @@ void shouldPreserveOtherHeaders() { assertThat(result.getHeaders().get("another-header")).isEqualTo("another-value"); assertThat(result.getHeaders().size()).isEqualTo(message.getHeaders().size() - 1); } + + @Test + void shouldReturnRawMessageIdWhenHeaderPresent() { + // given + String rawMessageId = "92898073-7bd6a160-5797b060-54a7e539"; + Message message = MessageBuilder.withPayload("test-payload") + .setHeader(SqsHeaders.SQS_RAW_MESSAGE_ID_HEADER, rawMessageId).build(); + + // when + String result = MessageHeaderUtils.getRawMessageId(message); + + // then + assertThat(result).isEqualTo(rawMessageId); + } + + @Test + void shouldFallbackToSpringMessageIdWhenRawHeaderNotPresent() { + // given + Message message = MessageBuilder.withPayload("test-payload").build(); + String expectedId = message.getHeaders().getId().toString(); + + // when + String result = MessageHeaderUtils.getRawMessageId(message); + + // then + assertThat(result).isEqualTo(expectedId); + } + + @Test + void shouldConcatenateRawMessageIdsFromCollection() { + // given + String rawMessageId1 = "raw-id-1"; + String rawMessageId2 = "raw-id-2"; + + Message message1 = MessageBuilder.withPayload("payload1") + .setHeader(SqsHeaders.SQS_RAW_MESSAGE_ID_HEADER, rawMessageId1).build(); + Message message2 = MessageBuilder.withPayload("payload2") + .setHeader(SqsHeaders.SQS_RAW_MESSAGE_ID_HEADER, rawMessageId2).build(); + + Collection> messages = List.of(message1, message2); + + // when + String result = MessageHeaderUtils.getRawMessageId(messages); + + // then + assertThat(result).isEqualTo("raw-id-1; raw-id-2"); + } } diff --git a/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/operations/SqsTemplateTests.java b/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/operations/SqsTemplateTests.java index a31d65fe1..bb297c702 100644 --- a/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/operations/SqsTemplateTests.java +++ b/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/operations/SqsTemplateTests.java @@ -1192,6 +1192,56 @@ void shouldReceiveBatchFifo() { } + @Test + void shouldHandleNonUuidMessageIdInSendResponse() { + String queue = "test-queue"; + GetQueueUrlResponse urlResponse = GetQueueUrlResponse.builder().queueUrl(queue).build(); + given(mockClient.getQueueUrl(any(GetQueueUrlRequest.class))) + .willReturn(CompletableFuture.completedFuture(urlResponse)); + mockQueueAttributes(mockClient, Map.of()); + String nonUuidMessageId = "92898073-7bd6a160-5797b060-54a7e539"; + SendMessageResponse response = SendMessageResponse.builder().messageId(nonUuidMessageId).build(); + given(mockClient.sendMessage(any(SendMessageRequest.class))) + .willReturn(CompletableFuture.completedFuture(response)); + SqsOperations template = SqsTemplate.newTemplate(mockClient); + String payload = "test-payload"; + SendResult result = template.send(to -> to.queue(queue).payload(payload)); + assertThat(result.messageId()) + .isEqualTo(UUID.nameUUIDFromBytes(nonUuidMessageId.getBytes(java.nio.charset.StandardCharsets.UTF_8))); + assertThat(result.additionalInformation().get(SqsTemplateParameters.RAW_MESSAGE_ID_PARAMETER_NAME)) + .isEqualTo(nonUuidMessageId); + } + + @Test + void shouldHandleNonUuidMessageIdInBatchSendResponse() { + String queue = "test-queue"; + String payload1 = "test-payload-1"; + String payload2 = "test-payload-2"; + Message message1 = MessageBuilder.withPayload(payload1).build(); + Message message2 = MessageBuilder.withPayload(payload2).build(); + List> messages = List.of(message1, message2); + + GetQueueUrlResponse urlResponse = GetQueueUrlResponse.builder().queueUrl(queue).build(); + given(mockClient.getQueueUrl(any(GetQueueUrlRequest.class))) + .willReturn(CompletableFuture.completedFuture(urlResponse)); + mockQueueAttributes(mockClient, Map.of()); + String nonUuidMessageId1 = "92898073-7bd6a160-5797b060-54a7e539"; + String nonUuidMessageId2 = "a2898073-8bd6a160-6797b060-64a7e539"; + SendMessageBatchResponse response = SendMessageBatchResponse.builder() + .successful( + builder -> builder.id(message1.getHeaders().getId().toString()).messageId(nonUuidMessageId1), + builder -> builder.id(message2.getHeaders().getId().toString()).messageId(nonUuidMessageId2)) + .build(); + given(mockClient.sendMessageBatch(any(SendMessageBatchRequest.class))) + .willReturn(CompletableFuture.completedFuture(response)); + SqsOperations template = SqsTemplate.newSyncTemplate(mockClient); + SendResult.Batch results = template.sendMany(queue, messages); + assertThat(results.successful()).hasSize(2); + results.successful().forEach(result -> { + assertThat(result.additionalInformation()).containsKey(SqsTemplateParameters.RAW_MESSAGE_ID_PARAMETER_NAME); + }); + } + @Test void shouldPropagateTracingAsMessageSystemAttribute() { String queue = "test-queue"; diff --git a/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/support/converter/SqsHeaderMapperTests.java b/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/support/converter/SqsHeaderMapperTests.java index ffbe0683a..5df507bd5 100644 --- a/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/support/converter/SqsHeaderMapperTests.java +++ b/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/support/converter/SqsHeaderMapperTests.java @@ -16,6 +16,7 @@ package io.awspring.cloud.sqs.support.converter; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import io.awspring.cloud.sqs.listener.SqsHeaders; import java.math.BigDecimal; @@ -27,6 +28,7 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.MessagingException; import software.amazon.awssdk.core.SdkBytes; import software.amazon.awssdk.services.sqs.model.Message; import software.amazon.awssdk.services.sqs.model.MessageAttributeValue; @@ -37,6 +39,7 @@ * * @author Tomaz Fernandes * @author Maciej Walkowiak + * @author Jeongmin Kim */ class SqsHeaderMapperTests { @@ -177,6 +180,39 @@ void createsMessageWithNumberHeader(String value, String type, Number expected) assertThat(headers.get(headerName)).isEqualTo(expected); } + @Test + void shouldConvertUuidMessageIdWhenConvertMessageIdToUuidIsTrue() { + SqsHeaderMapper mapper = new SqsHeaderMapper(); + mapper.setConvertMessageIdToUuid(true); + String uuidMessageId = "550e8400-e29b-41d4-a716-446655440000"; + Message message = Message.builder().body("payload").messageId(uuidMessageId).build(); + MessageHeaders headers = mapper.toHeaders(message); + assertThat(headers.getId()).isEqualTo(UUID.fromString(uuidMessageId)); + assertThat(headers.get(SqsHeaders.SQS_RAW_MESSAGE_ID_HEADER)).isNull(); + } + + @Test + void shouldThrowWhenConvertMessageIdToUuidIsTrueAndMessageIdIsNotValidUuid() { + SqsHeaderMapper mapper = new SqsHeaderMapper(); + mapper.setConvertMessageIdToUuid(true); + String nonUuidMessageId = "92898073-7bd6a160-5797b060-54a7e539"; + Message message = Message.builder().body("payload").messageId(nonUuidMessageId).build(); + assertThatThrownBy(() -> mapper.toHeaders(message)).isInstanceOf(MessagingException.class) + .hasMessageContaining("not a valid UUID").hasMessageContaining("convert-message-id-to-uuid"); + } + + @Test + void shouldStoreAwsMessageIdInHeaderWhenConvertMessageIdToUuidIsFalse() { + SqsHeaderMapper mapper = new SqsHeaderMapper(); + mapper.setConvertMessageIdToUuid(false); + String nonUuidMessageId = "92898073-7bd6a160-5797b060-54a7e539"; + Message message = Message.builder().body("payload").messageId(nonUuidMessageId).build(); + MessageHeaders headers = mapper.toHeaders(message); + assertThat(headers.get(SqsHeaders.SQS_RAW_MESSAGE_ID_HEADER)).isEqualTo(nonUuidMessageId); + assertThat(headers.getId()).isNotEqualTo(nonUuidMessageId); + assertThat(headers.getId()).isNotNull(); + } + private static Stream validArguments() { return Stream.of(Arguments.of("10", "Number", BigDecimal.valueOf(10)), Arguments.of("3", "Number.byte", (byte) 3), Arguments.of("3", "Number.Byte", (byte) 3),