From a96b22ec90798ef95947d265312936ca0f29062e Mon Sep 17 00:00:00 2001 From: Tomaz Fernandes Date: Fri, 6 May 2022 03:12:15 -0300 Subject: [PATCH 1/5] Add SQS Support Resolves #344 Adds support for listening to SQS queues and handling messages --- pom.xml | 2 + spring-cloud-aws-messaging-support/pom.xml | 65 +++ .../messaging/support/MessagingUtils.java | 52 ++ .../config/AbstractFactoryOptions.java | 76 +++ ...stractMessageListenerContainerFactory.java | 103 ++++ .../support/config/FactoryOptions.java | 24 + .../MessageListenerContainerFactory.java | 29 + .../MessagingBootstrapConfiguration.java | 45 ++ .../support/config/MessagingConfigUtils.java | 34 ++ .../endpoint/DefaultEndpointProcessor.java | 84 +++ .../messaging/support/endpoint/Endpoint.java | 30 + .../support/endpoint/EndpointProcessor.java | 26 + .../support/endpoint/EndpointRegistry.java | 28 + .../listener/AbstractContainerOptions.java | 70 +++ .../AbstractMessageListenerContainer.java | 225 ++++++++ .../support/listener/AsyncErrorHandler.java | 30 + .../listener/AsyncMessageInterceptor.java | 29 + .../listener/AsyncMessageListener.java | 30 + .../listener/AsyncMessageProducer.java | 32 ++ .../listener/CallbackMessageListener.java | 30 + .../support/listener/ContainerOptions.java | 28 + .../DefaultListenerContainerRegistry.java | 79 +++ .../support/listener/LoggingErrorHandler.java | 36 ++ .../support/listener/MessageHeaders.java | 26 + .../listener/MessageListenerContainer.java | 26 + .../MessageListenerContainerRegistry.java | 31 ++ .../acknowledgement/AckFrequencyHandler.java | 42 ++ .../acknowledgement/AsyncAckHandler.java | 32 ++ .../acknowledgement/AsyncAcknowledgement.java | 28 + .../acknowledgement/OnSuccessAckHandler.java | 42 ++ spring-cloud-aws-sqs/pom.xml | 49 ++ .../cloud/sqs/annotation/EnableSqs.java | 38 ++ .../cloud/sqs/annotation/SqsListener.java | 85 +++ .../cloud/sqs/config/SqsConfigUtils.java | 28 + .../sqs/config/SqsConfigurationSupport.java | 44 ++ .../cloud/sqs/config/SqsFactoryOptions.java | 77 +++ .../SqsMessageListenerContainerFactory.java | 109 ++++ .../cloud/sqs/endpoint/SqsEndpoint.java | 151 +++++ .../invocation/EndpointMessageHandler.java | 316 +++++++++++ .../AsyncMessageHandlerMessageListener.java | 64 +++ .../MessageHandlerMessageListener.java | 60 ++ .../MessageVisibilityExtenderInterceptor.java | 77 +++ .../cloud/sqs/listener/QueueAttributes.java | 54 ++ .../sqs/listener/QueueMessageVisibility.java | 51 ++ .../cloud/sqs/listener/SqsAcknowledge.java | 44 ++ .../sqs/listener/SqsContainerOptions.java | 53 ++ .../cloud/sqs/listener/SqsMessageHeaders.java | 108 ++++ .../listener/SqsMessageListenerContainer.java | 64 +++ .../sqs/listener/SqsMessageProducer.java | 161 ++++++ .../cloud/sqs/listener/Visibility.java | 39 ++ ...ledgmentHandlerMethodArgumentResolver.java | 52 ++ .../CallbackFutureReturnValueHandler.java | 55 ++ .../SqsHeadersMethodArgumentResolver.java | 57 ++ .../SqsMessageMethodArgumentResolver.java | 40 ++ ...sibilityHandlerMethodArgumentResolver.java | 51 ++ .../cloud/sqs/BaseSqsIntegrationTest.java | 63 +++ .../cloud/sqs/SqsIntegrationTests.java | 519 ++++++++++++++++++ .../src/test/resources/logback.xml | 32 ++ 58 files changed, 3955 insertions(+) create mode 100644 spring-cloud-aws-messaging-support/pom.xml create mode 100644 spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/MessagingUtils.java create mode 100644 spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/config/AbstractFactoryOptions.java create mode 100644 spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/config/AbstractMessageListenerContainerFactory.java create mode 100644 spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/config/FactoryOptions.java create mode 100644 spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/config/MessageListenerContainerFactory.java create mode 100644 spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/config/MessagingBootstrapConfiguration.java create mode 100644 spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/config/MessagingConfigUtils.java create mode 100644 spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/endpoint/DefaultEndpointProcessor.java create mode 100644 spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/endpoint/Endpoint.java create mode 100644 spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/endpoint/EndpointProcessor.java create mode 100644 spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/endpoint/EndpointRegistry.java create mode 100644 spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/AbstractContainerOptions.java create mode 100644 spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/AbstractMessageListenerContainer.java create mode 100644 spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/AsyncErrorHandler.java create mode 100644 spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/AsyncMessageInterceptor.java create mode 100644 spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/AsyncMessageListener.java create mode 100644 spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/AsyncMessageProducer.java create mode 100644 spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/CallbackMessageListener.java create mode 100644 spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/ContainerOptions.java create mode 100644 spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/DefaultListenerContainerRegistry.java create mode 100644 spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/LoggingErrorHandler.java create mode 100644 spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/MessageHeaders.java create mode 100644 spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/MessageListenerContainer.java create mode 100644 spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/MessageListenerContainerRegistry.java create mode 100644 spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/acknowledgement/AckFrequencyHandler.java create mode 100644 spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/acknowledgement/AsyncAckHandler.java create mode 100644 spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/acknowledgement/AsyncAcknowledgement.java create mode 100644 spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/acknowledgement/OnSuccessAckHandler.java create mode 100644 spring-cloud-aws-sqs/pom.xml create mode 100644 spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/annotation/EnableSqs.java create mode 100644 spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/annotation/SqsListener.java create mode 100644 spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/config/SqsConfigUtils.java create mode 100644 spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/config/SqsConfigurationSupport.java create mode 100644 spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/config/SqsFactoryOptions.java create mode 100644 spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/config/SqsMessageListenerContainerFactory.java create mode 100644 spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/endpoint/SqsEndpoint.java create mode 100644 spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/invocation/EndpointMessageHandler.java create mode 100644 spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/AsyncMessageHandlerMessageListener.java create mode 100644 spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/MessageHandlerMessageListener.java create mode 100644 spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/MessageVisibilityExtenderInterceptor.java create mode 100644 spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/QueueAttributes.java create mode 100644 spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/QueueMessageVisibility.java create mode 100644 spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/SqsAcknowledge.java create mode 100644 spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/SqsContainerOptions.java create mode 100644 spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/SqsMessageHeaders.java create mode 100644 spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/SqsMessageListenerContainer.java create mode 100644 spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/SqsMessageProducer.java create mode 100644 spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/Visibility.java create mode 100644 spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/support/AsyncAcknowledgmentHandlerMethodArgumentResolver.java create mode 100644 spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/support/CallbackFutureReturnValueHandler.java create mode 100644 spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/support/SqsHeadersMethodArgumentResolver.java create mode 100644 spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/support/SqsMessageMethodArgumentResolver.java create mode 100644 spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/support/VisibilityHandlerMethodArgumentResolver.java create mode 100644 spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/BaseSqsIntegrationTest.java create mode 100644 spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/SqsIntegrationTests.java create mode 100644 spring-cloud-aws-sqs/src/test/resources/logback.xml diff --git a/pom.xml b/pom.xml index d46bab9bd..d6eff0972 100644 --- a/pom.xml +++ b/pom.xml @@ -48,6 +48,8 @@ spring-cloud-aws-starters spring-cloud-aws-samples docs + spring-cloud-aws-messaging-support + spring-cloud-aws-sqs + + com.amazonaws + aws-java-sdk-core + test + + + + diff --git a/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/MessagingUtils.java b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/MessagingUtils.java new file mode 100644 index 000000000..b4709607d --- /dev/null +++ b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/MessagingUtils.java @@ -0,0 +1,52 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.messaging.support; + +import java.util.Arrays; +import java.util.Objects; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +/** + * @author Tomaz Fernandes + * @since 3.0 + */ +public class MessagingUtils { + + public static final MessagingUtils INSTANCE = new MessagingUtils(); + + private MessagingUtils() { + } + + public MessagingUtils acceptIfNotNull(T value, Consumer consumer) { + if (value != null) { + consumer.accept(value); + } + return this; + } + + public MessagingUtils acceptBothIfNoneNull(T firstValue, V secondValue, BiConsumer consumer) { + if (firstValue != null && secondValue != null) { + consumer.accept(firstValue, secondValue); + } + return this; + } + + public MessagingUtils acceptFirstNonNull(Consumer consumer, T... values) { + Arrays.stream(values).filter(Objects::nonNull).findFirst().ifPresent(consumer); + return this; + } +} diff --git a/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/config/AbstractFactoryOptions.java b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/config/AbstractFactoryOptions.java new file mode 100644 index 000000000..281dd8629 --- /dev/null +++ b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/config/AbstractFactoryOptions.java @@ -0,0 +1,76 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.messaging.support.config; + +import io.awspring.cloud.messaging.support.listener.AsyncErrorHandler; +import io.awspring.cloud.messaging.support.listener.AsyncMessageInterceptor; +import io.awspring.cloud.messaging.support.listener.acknowledgement.AsyncAckHandler; + +/** + * @author Tomaz Fernandes + * @since 3.0 + */ +public abstract class AbstractFactoryOptions> implements FactoryOptions { + + private AsyncErrorHandler errorHandler; + + private AsyncAckHandler ackHandler; + + private AsyncMessageInterceptor messageInterceptor; + + private Integer maxWorksPerContainer; + + Integer getMaxWorkersPerContainer() { + return this.maxWorksPerContainer; + } + + AsyncErrorHandler getErrorHandler() { + return this.errorHandler; + } + + AsyncAckHandler getAckHandler() { + return this.ackHandler; + } + + AsyncMessageInterceptor getMessageInterceptor() { + return this.messageInterceptor; + } + + @SuppressWarnings("unchecked") + private O self() { + return (O) this; + } + + public O errorHandler(AsyncErrorHandler errorHandler) { + this.errorHandler = errorHandler; + return self(); + } + + public O ackHandler(AsyncAckHandler ackHandler) { + this.ackHandler = ackHandler; + return self(); + } + + public O interceptor(AsyncMessageInterceptor messageInterceptor) { + this.messageInterceptor = messageInterceptor; + return self(); + } + + public O concurrentWorkersPerContainer(int maxWorksPerContainer) { + this.maxWorksPerContainer = maxWorksPerContainer; + return self(); + } +} diff --git a/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/config/AbstractMessageListenerContainerFactory.java b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/config/AbstractMessageListenerContainerFactory.java new file mode 100644 index 000000000..d9dc98659 --- /dev/null +++ b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/config/AbstractMessageListenerContainerFactory.java @@ -0,0 +1,103 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.messaging.support.config; + +import io.awspring.cloud.messaging.support.MessagingUtils; +import io.awspring.cloud.messaging.support.endpoint.Endpoint; +import io.awspring.cloud.messaging.support.listener.AbstractMessageListenerContainer; +import io.awspring.cloud.messaging.support.listener.AsyncMessageListener; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.SmartInitializingSingleton; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.util.Assert; + +/** + * @author Tomaz Fernandes + * @since 3.0 + */ +public abstract class AbstractMessageListenerContainerFactory, E extends Endpoint> + implements MessageListenerContainerFactory, SmartInitializingSingleton, BeanFactoryAware { + + private static final Integer DEFAULT_THREADPOOL_SIZE = 11; + private final AbstractFactoryOptions factoryOptions; + private BeanFactory beanFactory; + private AsyncMessageListener messageListener; + + protected AbstractMessageListenerContainerFactory(AbstractFactoryOptions factoryOptions) { + this.factoryOptions = factoryOptions; + } + + @Override + public C create(E endpoint) { + C container = createContainerInstance(endpoint); + MessagingUtils.INSTANCE.acceptIfNotNull(this.factoryOptions.getErrorHandler(), container::setErrorHandler) + .acceptIfNotNull(this.factoryOptions.getAckHandler(), container::setAckHandler) + .acceptIfNotNull(this.factoryOptions.getMessageInterceptor(), container::setMessageInterceptor); + initializeContainer(container); + return container; + } + + protected abstract C createContainerInstance(E endpoint); + + protected void initializeContainer(C container) { + } + + protected ThreadPoolTaskExecutor createTaskExecutor() { + ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); + int poolSize = this.factoryOptions.getMaxWorkersPerContainer() != null + ? this.factoryOptions.getMaxWorkersPerContainer() + 1 + : DEFAULT_THREADPOOL_SIZE; + taskExecutor.setMaxPoolSize(poolSize); + taskExecutor.setCorePoolSize(poolSize); + return taskExecutor; + } + + public void setMessageListener(AsyncMessageListener messageListener) { + Assert.notNull(messageListener, "messageListener cannot be null"); + this.messageListener = messageListener; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = beanFactory; + } + + protected BeanFactory getBeanFactory() { + return this.beanFactory; + } + + protected AsyncMessageListener getMessageListener() { + return this.messageListener; + } + + @SuppressWarnings("unchecked") + @Override + public void afterSingletonsInstantiated() { + if (this.messageListener == null) { + Assert.isTrue(this.beanFactory.containsBean(MessagingConfigUtils.MESSAGE_LISTENER_BEAN_NAME), + "An AsyncMessageListener must be registered with name " + + MessagingConfigUtils.MESSAGE_LISTENER_BEAN_NAME); + this.messageListener = this.beanFactory.getBean(MessagingConfigUtils.MESSAGE_LISTENER_BEAN_NAME, + AsyncMessageListener.class); + } + initializeFactory(); + } + + protected void initializeFactory() { + } +} diff --git a/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/config/FactoryOptions.java b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/config/FactoryOptions.java new file mode 100644 index 000000000..059e271aa --- /dev/null +++ b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/config/FactoryOptions.java @@ -0,0 +1,24 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.messaging.support.config; + +/** + * @author Tomaz Fernandes + * @since 3.0 + */ +public interface FactoryOptions { + +} diff --git a/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/config/MessageListenerContainerFactory.java b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/config/MessageListenerContainerFactory.java new file mode 100644 index 000000000..1a3f38a32 --- /dev/null +++ b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/config/MessageListenerContainerFactory.java @@ -0,0 +1,29 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.messaging.support.config; + +import io.awspring.cloud.messaging.support.endpoint.Endpoint; +import io.awspring.cloud.messaging.support.listener.MessageListenerContainer; + +/** + * @author Tomaz Fernandes + * @since 3.0 + */ +public interface MessageListenerContainerFactory, E extends Endpoint> { + + C create(E endpoint); + +} diff --git a/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/config/MessagingBootstrapConfiguration.java b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/config/MessagingBootstrapConfiguration.java new file mode 100644 index 000000000..ec3d4c500 --- /dev/null +++ b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/config/MessagingBootstrapConfiguration.java @@ -0,0 +1,45 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.messaging.support.config; + +import io.awspring.cloud.messaging.support.endpoint.DefaultEndpointProcessor; +import io.awspring.cloud.messaging.support.listener.DefaultListenerContainerRegistry; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.core.type.AnnotationMetadata; + +/** + * @author Tomaz Fernandes + * @since 3.0 + */ +public class MessagingBootstrapConfiguration implements ImportBeanDefinitionRegistrar { + + @Override + public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { + if (!registry.containsBeanDefinition(MessagingConfigUtils.MESSAGE_LISTENER_CONTAINER_REGISTRY_BEAN_NAME)) { + + registry.registerBeanDefinition(MessagingConfigUtils.MESSAGE_LISTENER_CONTAINER_REGISTRY_BEAN_NAME, + new RootBeanDefinition(DefaultListenerContainerRegistry.class)); + } + + if (!registry.containsBeanDefinition(MessagingConfigUtils.ENDPOINT_PROCESSOR_BEAN_NAME)) { + registry.registerBeanDefinition(MessagingConfigUtils.ENDPOINT_PROCESSOR_BEAN_NAME, + new RootBeanDefinition(DefaultEndpointProcessor.class)); + } + } + +} diff --git a/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/config/MessagingConfigUtils.java b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/config/MessagingConfigUtils.java new file mode 100644 index 000000000..fd2fcb5f7 --- /dev/null +++ b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/config/MessagingConfigUtils.java @@ -0,0 +1,34 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.messaging.support.config; + +/** + * @author Tomaz Fernandes + * @since 3.0 + */ +public class MessagingConfigUtils { + + public static final String DEFAULT_LISTENER_CONTAINER_FACTORY_BEAN_NAME = "defaultListenerContainerFactory"; + + public static final String ENDPOINT_REGISTRY_BEAN_NAME = "io.awspring.cloud.messaging.internalEndpointRegistryBeanName"; + + public static final String MESSAGE_LISTENER_BEAN_NAME = "io.awspring.cloud.messaging.internalMessageListener"; + + public static final String MESSAGE_LISTENER_CONTAINER_REGISTRY_BEAN_NAME = "io.awspring.cloud.messaging.internalMessageListenerContainerRegistry"; + + public static final String ENDPOINT_PROCESSOR_BEAN_NAME = "io.awspring.cloud.messaging.internalEndpointProcessor"; + +} diff --git a/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/endpoint/DefaultEndpointProcessor.java b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/endpoint/DefaultEndpointProcessor.java new file mode 100644 index 000000000..ff541e366 --- /dev/null +++ b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/endpoint/DefaultEndpointProcessor.java @@ -0,0 +1,84 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.messaging.support.endpoint; + +import io.awspring.cloud.messaging.support.config.MessageListenerContainerFactory; +import io.awspring.cloud.messaging.support.config.MessagingConfigUtils; +import io.awspring.cloud.messaging.support.listener.MessageListenerContainer; +import io.awspring.cloud.messaging.support.listener.MessageListenerContainerRegistry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.SmartInitializingSingleton; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * @author Tomaz Fernandes + * @since 3.0 + */ +public class DefaultEndpointProcessor implements EndpointProcessor, BeanFactoryAware, SmartInitializingSingleton { + + private static final Logger logger = LoggerFactory.getLogger(DefaultEndpointProcessor.class); + + public static final String DEFAULT_LISTENER_CONTAINER_FACTORY_BEAN_NAME = "defaultListenerContainerFactory"; + + public static final String ENDPOINT_REGISTRY_BEAN_NAME = "defaultEndpointRegistry"; + + private BeanFactory beanFactory; + + private MessageListenerContainerRegistry listenerContainerRegistry; + + @Override + public void afterSingletonsInstantiated() { + Assert.isTrue(beanFactory.containsBean(MessagingConfigUtils.ENDPOINT_REGISTRY_BEAN_NAME), + () -> "A MessageListenerContainerRegistry implementation must be registered with name " + + MessagingConfigUtils.ENDPOINT_REGISTRY_BEAN_NAME); + this.listenerContainerRegistry = beanFactory.getBean( + MessagingConfigUtils.MESSAGE_LISTENER_CONTAINER_REGISTRY_BEAN_NAME, + MessageListenerContainerRegistry.class); + this.beanFactory.getBean(MessagingConfigUtils.ENDPOINT_REGISTRY_BEAN_NAME, EndpointRegistry.class) + .retrieveEndpoints().forEach(this::process); + } + + @Override + public void process(Endpoint endpoint) { + logger.debug("Processing endpoint: " + endpoint); + this.listenerContainerRegistry.registerListenerContainer(createContainerFor(endpoint)); + } + + @SuppressWarnings("unchecked") + private MessageListenerContainer createContainerFor(Endpoint endpoint) { + String factoryBeanName = getListenerContainerFactoryName(endpoint); + Assert.isTrue(this.beanFactory.containsBean(factoryBeanName), + () -> "No bean with name " + factoryBeanName + " found for MessageListenerContainerFactory."); + return this.beanFactory.getBean(factoryBeanName, MessageListenerContainerFactory.class).create(endpoint); + } + + private String getListenerContainerFactoryName(Endpoint endpoint) { + return StringUtils.hasText(endpoint.getListenerContainerFactoryName()) + ? endpoint.getListenerContainerFactoryName() + : MessagingConfigUtils.DEFAULT_LISTENER_CONTAINER_FACTORY_BEAN_NAME; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = beanFactory; + } + +} diff --git a/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/endpoint/Endpoint.java b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/endpoint/Endpoint.java new file mode 100644 index 000000000..9cbf5fd87 --- /dev/null +++ b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/endpoint/Endpoint.java @@ -0,0 +1,30 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.messaging.support.endpoint; + +import java.util.Collection; + +/** + * @author Tomaz Fernandes + * @since 3.0 + */ +public interface Endpoint { + + Collection getLogicalEndpointNames(); + + String getListenerContainerFactoryName(); + +} diff --git a/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/endpoint/EndpointProcessor.java b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/endpoint/EndpointProcessor.java new file mode 100644 index 000000000..20560196c --- /dev/null +++ b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/endpoint/EndpointProcessor.java @@ -0,0 +1,26 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.messaging.support.endpoint; + +/** + * @author Tomaz Fernandes + * @since 3.0 + */ +public interface EndpointProcessor { + + void process(Endpoint endpoint); + +} diff --git a/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/endpoint/EndpointRegistry.java b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/endpoint/EndpointRegistry.java new file mode 100644 index 000000000..3588241d2 --- /dev/null +++ b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/endpoint/EndpointRegistry.java @@ -0,0 +1,28 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.messaging.support.endpoint; + +import java.util.Collection; + +/** + * @author Tomaz Fernandes + * @since 3.0 + */ +public interface EndpointRegistry { + + Collection retrieveEndpoints(); + +} diff --git a/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/AbstractContainerOptions.java b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/AbstractContainerOptions.java new file mode 100644 index 000000000..62c7a6617 --- /dev/null +++ b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/AbstractContainerOptions.java @@ -0,0 +1,70 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.messaging.support.listener; + +import java.time.Duration; + +/** + * @author Tomaz Fernandes + * @since 3.0 + */ +public abstract class AbstractContainerOptions> implements ContainerOptions { + + private static final int DEFAULT_SIMULTANEOUS_PRODUCE_CALLS = 2; + + private static final int DEFAULT_MESSAGES_PER_PRODUCE = 10; + + private static final int DEFAULT_PRODUCE_TIMEOUT = 10; + + private int simultaneousProduceCalls = DEFAULT_SIMULTANEOUS_PRODUCE_CALLS; + + private int messagesPerProduce = DEFAULT_MESSAGES_PER_PRODUCE; + + private int produceTimeout = DEFAULT_PRODUCE_TIMEOUT; + + @SuppressWarnings("unchecked") + private O self() { + return (O) this; + } + + public O simultaneousProduceCalls(int simultaneousProduceCalls) { + this.simultaneousProduceCalls = simultaneousProduceCalls; + return self(); + } + + public O messagesPerProduce(int messagesPerProduce) { + this.messagesPerProduce = messagesPerProduce; + return self(); + } + + public O produceTimeout(Integer produceTimeout) { + this.produceTimeout = produceTimeout; + return self(); + } + + public int getSimultaneousProduceCalls() { + return this.simultaneousProduceCalls; + } + + public int getMessagesPerProduce() { + return this.messagesPerProduce; + } + + public Duration getProduceTimeout() { + return Duration.ofSeconds(this.produceTimeout); + } + +} diff --git a/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/AbstractMessageListenerContainer.java b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/AbstractMessageListenerContainer.java new file mode 100644 index 000000000..a1ee1a3ae --- /dev/null +++ b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/AbstractMessageListenerContainer.java @@ -0,0 +1,225 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.messaging.support.listener; + +import io.awspring.cloud.messaging.support.listener.acknowledgement.AsyncAckHandler; +import io.awspring.cloud.messaging.support.listener.acknowledgement.OnSuccessAckHandler; +import java.util.Collection; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Semaphore; +import java.util.function.BiFunction; +import java.util.function.Function; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.task.TaskExecutor; +import org.springframework.messaging.Message; +import org.springframework.util.Assert; + +/** + * @author Tomaz Fernandes + * @since 3.0 + */ +public abstract class AbstractMessageListenerContainer implements MessageListenerContainer, InitializingBean { + + private static final Logger logger = LoggerFactory.getLogger(AbstractMessageListenerContainer.class); + + private final AbstractContainerOptions options; + + private boolean isRunning; + + private final TaskExecutor taskExecutor; + + private final Semaphore producersSemaphore; + + private final Object lifecycleMonitor = new Object(); + + private final Collection> messageProducers; + + private final AsyncMessageListener messageListener; + + private AsyncErrorHandler errorHandler = new LoggingErrorHandler<>(); + + private AsyncAckHandler ackHandler = new OnSuccessAckHandler<>(); + + private AsyncMessageInterceptor messageInterceptor = null; + + public AbstractMessageListenerContainer(AbstractContainerOptions options, TaskExecutor taskExecutor, + AsyncMessageListener messageListener, Collection> producers) { + this.messageListener = messageListener; + handleCallbackListener(messageListener); + this.messageProducers = producers; + this.options = options; + this.taskExecutor = taskExecutor; + this.producersSemaphore = new Semaphore(options.getSimultaneousProduceCalls()); + } + + private void handleCallbackListener(AsyncMessageListener messageListener) { + if (messageListener instanceof CallbackMessageListener) { + ((CallbackMessageListener) messageListener).addResultCallback(this::handleResult); + } + } + + public void setErrorHandler(AsyncErrorHandler errorHandler) { + Assert.notNull(errorHandler, "errorHandler cannot be null"); + this.errorHandler = errorHandler; + } + + public void setAckHandler(AsyncAckHandler ackHandler) { + Assert.notNull(ackHandler, "ackHandler cannot be null"); + this.ackHandler = ackHandler; + } + + public void setMessageInterceptor(AsyncMessageInterceptor messageInterceptor) { + Assert.notNull(messageInterceptor, "messageInterceptor cannot be null"); + this.messageInterceptor = messageInterceptor; + } + + public AsyncMessageInterceptor getMessageInterceptor() { + return messageInterceptor; + } + + @Override + public void start() { + logger.debug("Starting container {}", this); + synchronized (this.lifecycleMonitor) { + this.isRunning = true; + doStart(); + this.taskExecutor.execute(this::produceAndProcessMessages); + } + logger.debug("Container started {}", this); + } + + protected void doStart() { + } + + private void produceAndProcessMessages() { + while (this.isRunning) { + this.messageProducers.forEach(producer -> { + try { + acquireSemaphore(); + producer.produce(options.getMessagesPerProduce(), options.getProduceTimeout()) + .thenComposeAsync(this::splitAndProcessMessages).handle(handleProcessingResult()) + .thenRun(releaseSemaphore()); + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + logger.debug("Thread interrupted", e); + } + catch (Exception e) { + logger.error("Error in ListenerContainer", e); + } + }); + } + } + + protected CompletableFuture splitAndProcessMessages(Collection> messages) { + logger.trace("Received {} messages in Thread {}", messages.size(), threadName()); + return CompletableFuture + .allOf(messages.stream().map(this::processMessageAsync).toArray(CompletableFuture[]::new)); + } + + protected CompletableFuture processMessageAsync(Message msg) { + return CompletableFuture.supplyAsync(() -> processMessage(msg), this.taskExecutor).thenCompose(x -> x); + } + + protected CompletableFuture processMessage(Message message) { + logger.debug("Processing message {} in thread {}", message, threadName()); + CompletableFuture messageListenerResult = maybeIntercept(message, this.messageListener::onMessage); + return this.messageListener instanceof CallbackMessageListener ? CompletableFuture.completedFuture(null) + : messageListenerResult.handle((val, t) -> handleResult(message, t)); + } + + protected CompletableFuture handleResult(Message message, Throwable throwable) { + return throwable == null ? this.ackHandler.onSuccess(message) + : this.errorHandler.handleError(message, throwable) + .thenCompose(val -> this.ackHandler.onError(message, throwable)); + } + + private CompletableFuture maybeIntercept(Message message, + Function, CompletableFuture> listener) { + return this.messageInterceptor != null ? this.messageInterceptor.intercept(message).thenComposeAsync(listener) + : listener.apply(message); + } + + private String threadName() { + return Thread.currentThread().getName(); + } + + private void acquireSemaphore() throws InterruptedException { + producersSemaphore.acquire(); + logger.trace("Semaphore acquired for producer {} in thread {} ", this, threadName()); + } + + private Runnable releaseSemaphore() { + return () -> { + this.producersSemaphore.release(); + logger.trace("Semaphore released for producer {} in thread {} ", this, threadName()); + }; + } + + protected BiFunction handleProcessingResult() { + return (value, t) -> { + if (t != null) { + logger.error("Error handling messages in container {} ", this); + } + return null; + }; + } + + @Override + public void stop() { + logger.debug("Stopping container {}", this); + synchronized (this.lifecycleMonitor) { + this.isRunning = false; + doStop(); + if (this.taskExecutor instanceof DisposableBean) { + try { + ((DisposableBean) this.taskExecutor).destroy(); + } + catch (Exception e) { + throw new IllegalStateException("Error shutting down TaskExecutor", e); + } + } + } + logger.debug("Container stopped {}", this); + } + + protected void doStop() { + } + + protected Collection> getMessageProducers() { + return this.messageProducers; + } + + @Override + public boolean isRunning() { + return this.isRunning; + } + + @Override + public void afterPropertiesSet() { + if (this.taskExecutor instanceof InitializingBean) { + try { + ((InitializingBean) this.taskExecutor).afterPropertiesSet(); + } + catch (Exception e) { + throw new IllegalStateException("Could not initialize TaskExecutor", e); + } + } + } +} diff --git a/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/AsyncErrorHandler.java b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/AsyncErrorHandler.java new file mode 100644 index 000000000..2c016236e --- /dev/null +++ b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/AsyncErrorHandler.java @@ -0,0 +1,30 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.messaging.support.listener; + +import java.util.concurrent.CompletableFuture; +import org.springframework.messaging.Message; + +/** + * @author Tomaz Fernandes + * @since 3.0 + */ +@FunctionalInterface +public interface AsyncErrorHandler { + + CompletableFuture handleError(Message message, Throwable t); + +} diff --git a/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/AsyncMessageInterceptor.java b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/AsyncMessageInterceptor.java new file mode 100644 index 000000000..b5750f44d --- /dev/null +++ b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/AsyncMessageInterceptor.java @@ -0,0 +1,29 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.messaging.support.listener; + +import java.util.concurrent.CompletableFuture; +import org.springframework.messaging.Message; + +/** + * @author Tomaz Fernandes + * @since 3.0 + */ +public interface AsyncMessageInterceptor { + + CompletableFuture> intercept(Message message); + +} diff --git a/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/AsyncMessageListener.java b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/AsyncMessageListener.java new file mode 100644 index 000000000..ce7f9903a --- /dev/null +++ b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/AsyncMessageListener.java @@ -0,0 +1,30 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.messaging.support.listener; + +import java.util.concurrent.CompletableFuture; +import org.springframework.messaging.Message; + +/** + * @author Tomaz Fernandes + * @since 3.0 + */ +@FunctionalInterface +public interface AsyncMessageListener { + + CompletableFuture onMessage(Message message); + +} diff --git a/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/AsyncMessageProducer.java b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/AsyncMessageProducer.java new file mode 100644 index 000000000..6d87205d0 --- /dev/null +++ b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/AsyncMessageProducer.java @@ -0,0 +1,32 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.messaging.support.listener; + +import java.time.Duration; +import java.util.Collection; +import java.util.concurrent.CompletableFuture; +import org.springframework.messaging.Message; + +/** + * @author Tomaz Fernandes + * @since 3.0 + */ +@FunctionalInterface +public interface AsyncMessageProducer { + + CompletableFuture>> produce(int numberOfMessages, Duration timeout); + +} diff --git a/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/CallbackMessageListener.java b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/CallbackMessageListener.java new file mode 100644 index 000000000..cf38e3ff6 --- /dev/null +++ b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/CallbackMessageListener.java @@ -0,0 +1,30 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.messaging.support.listener; + +import java.util.concurrent.CompletableFuture; +import java.util.function.BiFunction; +import org.springframework.messaging.Message; + +/** + * @author Tomaz Fernandes + * @since 3.0 + */ +public interface CallbackMessageListener extends AsyncMessageListener { + + void addResultCallback(BiFunction, Throwable, CompletableFuture> callback); + +} diff --git a/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/ContainerOptions.java b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/ContainerOptions.java new file mode 100644 index 000000000..be37a3320 --- /dev/null +++ b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/ContainerOptions.java @@ -0,0 +1,28 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.messaging.support.listener; + +import io.awspring.cloud.messaging.support.endpoint.Endpoint; + +/** + * @author Tomaz Fernandes + * @since 3.0 + */ +public interface ContainerOptions { + + Endpoint getEndpoint(); + +} diff --git a/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/DefaultListenerContainerRegistry.java b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/DefaultListenerContainerRegistry.java new file mode 100644 index 000000000..b4939e68c --- /dev/null +++ b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/DefaultListenerContainerRegistry.java @@ -0,0 +1,79 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.messaging.support.listener; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Tomaz Fernandes + * @since 3.0 + */ +public class DefaultListenerContainerRegistry implements MessageListenerContainerRegistry { + + private static final Logger logger = LoggerFactory.getLogger(DefaultListenerContainerRegistry.class); + + private final List> listenerContainers = new ArrayList<>(); + + private final Object lifecycleMonitor = new Object(); + + private volatile boolean running = false; + + @Override + public void registerListenerContainer(MessageListenerContainer listenerContainer) { + logger.debug("Registering listener container {}", listenerContainer); + this.listenerContainers.add(listenerContainer); + } + + @Override + public Collection> retrieveListenerContainers() { + return Collections.unmodifiableList(this.listenerContainers); + } + + @Override + public void start() { + synchronized (this.lifecycleMonitor) { + logger.debug("Starting registry {}", this); + this.running = true; + this.listenerContainers.forEach(MessageListenerContainer::start); + } + } + + @Override + public void stop() { + synchronized (this.lifecycleMonitor) { + logger.debug("Stopping registry {}", this); + this.running = false; + this.listenerContainers.forEach(MessageListenerContainer::stop); + } + } + + @Override + public void stop(Runnable callback) { + stop(); + callback.run(); + } + + @Override + public boolean isRunning() { + return this.running; + } + +} diff --git a/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/LoggingErrorHandler.java b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/LoggingErrorHandler.java new file mode 100644 index 000000000..43e4aeeda --- /dev/null +++ b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/LoggingErrorHandler.java @@ -0,0 +1,36 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.messaging.support.listener; + +import java.util.concurrent.CompletableFuture; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.messaging.Message; + +/** + * @author Tomaz Fernandes + * @since 3.0 + */ +public class LoggingErrorHandler implements AsyncErrorHandler { + + private static final Logger logger = LoggerFactory.getLogger(LoggingErrorHandler.class); + + @Override + public CompletableFuture handleError(Message message, Throwable t) { + logger.error("Error processing message {}", message, t); + return CompletableFuture.completedFuture(null); + } +} diff --git a/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/MessageHeaders.java b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/MessageHeaders.java new file mode 100644 index 000000000..9b1339f01 --- /dev/null +++ b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/MessageHeaders.java @@ -0,0 +1,26 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.messaging.support.listener; + +/** + * @author Tomaz Fernandes + * @since 3.0 + */ +public class MessageHeaders { + + public static final String ACKNOWLEDGMENT_HEADER = "acknowledgement"; + +} diff --git a/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/MessageListenerContainer.java b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/MessageListenerContainer.java new file mode 100644 index 000000000..2bce47ae7 --- /dev/null +++ b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/MessageListenerContainer.java @@ -0,0 +1,26 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.messaging.support.listener; + +import org.springframework.context.SmartLifecycle; + +/** + * @author Tomaz Fernandes + * @since 3.0 + */ +public interface MessageListenerContainer extends SmartLifecycle { + +} diff --git a/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/MessageListenerContainerRegistry.java b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/MessageListenerContainerRegistry.java new file mode 100644 index 000000000..619d6345d --- /dev/null +++ b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/MessageListenerContainerRegistry.java @@ -0,0 +1,31 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.messaging.support.listener; + +import java.util.Collection; +import org.springframework.context.SmartLifecycle; + +/** + * @author Tomaz Fernandes + * @since 3.0 + */ +public interface MessageListenerContainerRegistry extends SmartLifecycle { + + void registerListenerContainer(MessageListenerContainer listenerContainer); + + Collection> retrieveListenerContainers(); + +} diff --git a/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/acknowledgement/AckFrequencyHandler.java b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/acknowledgement/AckFrequencyHandler.java new file mode 100644 index 000000000..9e02c8d81 --- /dev/null +++ b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/acknowledgement/AckFrequencyHandler.java @@ -0,0 +1,42 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.messaging.support.listener.acknowledgement; + +import java.util.concurrent.CompletableFuture; + +/** + * @author Tomaz Fernandes + * @since 3.0 + */ +// TODO: Implement this +public interface AckFrequencyHandler { + + CompletableFuture registerAck(AsyncAcknowledgement ack); + + enum AckFrequency { + + EACH, + + BATCH, + + TIME, + + COUNT, + + COUNT_OR_TIME + + } +} diff --git a/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/acknowledgement/AsyncAckHandler.java b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/acknowledgement/AsyncAckHandler.java new file mode 100644 index 000000000..183acb586 --- /dev/null +++ b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/acknowledgement/AsyncAckHandler.java @@ -0,0 +1,32 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.messaging.support.listener.acknowledgement; + +import java.util.concurrent.CompletableFuture; +import org.springframework.messaging.Message; + +/** + * @author Tomaz Fernandes + * @since 3.0 + */ +public interface AsyncAckHandler { + + CompletableFuture onSuccess(Message message); + + default CompletableFuture onError(Message message, Throwable t) { + return CompletableFuture.completedFuture(null); + } +} diff --git a/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/acknowledgement/AsyncAcknowledgement.java b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/acknowledgement/AsyncAcknowledgement.java new file mode 100644 index 000000000..9f8d6928c --- /dev/null +++ b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/acknowledgement/AsyncAcknowledgement.java @@ -0,0 +1,28 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.messaging.support.listener.acknowledgement; + +import java.util.concurrent.CompletableFuture; + +/** + * @author Tomaz Fernandes + * @since 3.0 + */ +public interface AsyncAcknowledgement { + + CompletableFuture acknowledge(); + +} diff --git a/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/acknowledgement/OnSuccessAckHandler.java b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/acknowledgement/OnSuccessAckHandler.java new file mode 100644 index 000000000..1d858e9f2 --- /dev/null +++ b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/acknowledgement/OnSuccessAckHandler.java @@ -0,0 +1,42 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.messaging.support.listener.acknowledgement; + +import io.awspring.cloud.messaging.support.listener.MessageHeaders; +import java.util.concurrent.CompletableFuture; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.messaging.Message; +import org.springframework.util.Assert; + +/** + * @author Tomaz Fernandes + * @since 3.0 + */ +public class OnSuccessAckHandler implements AsyncAckHandler { + + private static final Logger logger = LoggerFactory.getLogger(OnSuccessAckHandler.class); + + @SuppressWarnings("unchecked") + @Override + public CompletableFuture onSuccess(Message message) { + logger.trace("Acknowledging message " + message); + Object ackObject = message.getHeaders().get(MessageHeaders.ACKNOWLEDGMENT_HEADER); + Assert.notNull(ackObject, () -> "No acknowledgment found for " + message); + Assert.isInstanceOf(AsyncAcknowledgement.class, ackObject, () -> "Wrong ack type for message: " + ackObject); + return ((AsyncAcknowledgement) ackObject).acknowledge(); + } +} diff --git a/spring-cloud-aws-sqs/pom.xml b/spring-cloud-aws-sqs/pom.xml new file mode 100644 index 000000000..50a86dbaf --- /dev/null +++ b/spring-cloud-aws-sqs/pom.xml @@ -0,0 +1,49 @@ + + + + spring-cloud-aws + io.awspring.cloud + 3.0.0-SNAPSHOT + + 4.0.0 + + spring-cloud-aws-sqs + Spring Cloud AWS SQS + Spring Cloud AWS Simple Queue Service + + + 8 + 8 + + + + + software.amazon.awssdk + sqs + + + spring-cloud-aws-messaging-support + io.awspring.cloud + 3.0.0-SNAPSHOT + + + org.testcontainers + localstack + test + + + org.testcontainers + junit-jupiter + test + + + + com.amazonaws + aws-java-sdk-core + test + + + + diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/annotation/EnableSqs.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/annotation/EnableSqs.java new file mode 100644 index 000000000..e61ce3dca --- /dev/null +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/annotation/EnableSqs.java @@ -0,0 +1,38 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.sqs.annotation; + +import io.awspring.cloud.messaging.support.config.MessagingBootstrapConfiguration; +import io.awspring.cloud.sqs.config.SqsConfigurationSupport; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.springframework.context.annotation.Import; + +/** + * @author Tomaz Fernandes + * @since 3.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Import({ MessagingBootstrapConfiguration.class, SqsConfigurationSupport.class }) +public @interface EnableSqs { +} + +// TODO: Probably add autoconfiguration for this diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/annotation/SqsListener.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/annotation/SqsListener.java new file mode 100644 index 000000000..4452f435f --- /dev/null +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/annotation/SqsListener.java @@ -0,0 +1,85 @@ +/* + * Copyright 2013-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.sqs.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.springframework.core.annotation.AliasFor; + +/** + * Annotation for mapping a {@link org.springframework.messaging.Message} onto listener methods by matching to the + * message destination. The destination can be a logical queue name (CloudFormation), a physical queue name or a queue + * URL. + *

+ * Listener methods which are annotated with this annotation are allowed to have flexible signatures. They may have + * arguments of the following types, in arbitrary order: + *

    + *
  • {@link org.springframework.messaging.Message} to get access to the complete message being processed.
  • + *
  • {@link org.springframework.messaging.handler.annotation.Payload}-annotated method arguments to extract the + * payload of a message and optionally convert it using a + * {@link org.springframework.messaging.converter.MessageConverter}. The presence of the annotation is not required + * since it is assumed by default for method arguments that are not annotated. + *
  • {@link org.springframework.messaging.handler.annotation.Header}-annotated method arguments to extract a specific + * header value along with type conversion with a {@link org.springframework.core.convert.converter.Converter} if + * necessary.
  • + *
  • {@link org.springframework.messaging.handler.annotation.Headers}-annotated argument that must also be assignable + * to {@link java.util.Map} for getting access to all headers.
  • + *
  • {@link org.springframework.messaging.MessageHeaders} arguments for getting access to all headers.
  • + *
  • {@link org.springframework.messaging.support.MessageHeaderAccessor}
  • + *
  • {@link io.awspring.cloud.messaging.listener.Acknowledgment} to be able to acknowledge the reception of a message + * an trigger the deletion of it. This argument is only available when using the deletion policy + * {@link SqsMessageDeletionPolicy#NEVER}.
  • + *
+ *

+ * Additionally a deletion policy can be chosen to define when a message must be deleted once the listener method has + * been called. To get an overview of the available deletion policies read the {@link SqsMessageDeletionPolicy} + * documentation. + *

+ *

+ * By default the return value is wrapped as a message and sent to the destination specified with an + * {@link org.springframework.messaging.handler.annotation.SendTo @SendTo} method-level annotation. + * + * @author Alain Sahli + * @author Matej Nedic + * @author Tomaz Fernandes + * @since 1.1 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface SqsListener { + + /** + * List of queues. Queues can be defined by their logical/physical name or URL. + * @return list of queues + */ + String[] value() default {}; + + @AliasFor("value") + String[] queueNames() default {}; + + String factory() default ""; + + String concurrentPollsPerContainer() default ""; + + String pollTimeoutSeconds() default ""; + + String minSecondsToProcess() default ""; + +} diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/config/SqsConfigUtils.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/config/SqsConfigUtils.java new file mode 100644 index 000000000..82d0ce558 --- /dev/null +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/config/SqsConfigUtils.java @@ -0,0 +1,28 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.sqs.config; + +/** + * @author Tomaz Fernandes + * @since 3.0 + */ +public class SqsConfigUtils { + + public static final String SQS_ASYNC_CLIENT_BEAN_NAME = "io.awspring.cloud.sqs.internalSqsAsyncClient"; + + public static final String SQS_ASYNC_LISTENER_BEAN_NAME = "io.awspring.cloud.sqs.internalSqsAsyncListener"; + +} diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/config/SqsConfigurationSupport.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/config/SqsConfigurationSupport.java new file mode 100644 index 000000000..b96f90056 --- /dev/null +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/config/SqsConfigurationSupport.java @@ -0,0 +1,44 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.sqs.config; + +import io.awspring.cloud.messaging.support.config.MessagingConfigUtils; +import io.awspring.cloud.messaging.support.listener.AsyncMessageListener; +import io.awspring.cloud.sqs.invocation.EndpointMessageHandler; +import io.awspring.cloud.sqs.listener.MessageHandlerMessageListener; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.messaging.MessageHandler; +import software.amazon.awssdk.services.sqs.SqsAsyncClient; + +/** + * @author Tomaz Fernandes + * @since 3.0 + */ +public class SqsConfigurationSupport { + + @Bean(name = MessagingConfigUtils.MESSAGE_LISTENER_BEAN_NAME) + public AsyncMessageListener messageListener(MessageHandler messageHandler) { + return new MessageHandlerMessageListener<>(messageHandler); + } + + @Bean(name = MessagingConfigUtils.ENDPOINT_REGISTRY_BEAN_NAME) + public MessageHandler endpointMessageHandler( + @Qualifier(SqsConfigUtils.SQS_ASYNC_CLIENT_BEAN_NAME) SqsAsyncClient sqsAsyncClient) { + return new EndpointMessageHandler(sqsAsyncClient); + } + +} diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/config/SqsFactoryOptions.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/config/SqsFactoryOptions.java new file mode 100644 index 000000000..1041b2b6a --- /dev/null +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/config/SqsFactoryOptions.java @@ -0,0 +1,77 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.sqs.config; + +import io.awspring.cloud.messaging.support.config.AbstractFactoryOptions; + +/** + * @author Tomaz Fernandes + * @since 3.0 + */ +public class SqsFactoryOptions extends AbstractFactoryOptions { + + private Integer simultaneousPollsPerQueue; + + private Integer pollTimeoutSeconds; + + private Integer messagesPerPoll; + + private Integer minTimeToProcess; + + private SqsFactoryOptions() { + } + + public static SqsFactoryOptions withOptions() { + return new SqsFactoryOptions(); + } + + public SqsFactoryOptions concurrentPollsPerContainer(int maxActivePollingRequestsPerQueue) { + this.simultaneousPollsPerQueue = maxActivePollingRequestsPerQueue; + return this; + } + + public SqsFactoryOptions pollingTimeoutSeconds(int pollingTimeoutSeconds) { + this.pollTimeoutSeconds = pollingTimeoutSeconds; + return this; + } + + public SqsFactoryOptions messagesPerPoll(int messagesPerPoll) { + this.messagesPerPoll = messagesPerPoll; + return this; + } + + public SqsFactoryOptions minTimeToProcess(int minTimeToProcess) { + this.minTimeToProcess = minTimeToProcess; + return this; + } + + Integer getMinTimeToProcess() { + return this.minTimeToProcess; + } + + Integer getSimultaneousPollsPerQueue() { + return this.simultaneousPollsPerQueue; + } + + Integer getPollTimeoutSeconds() { + return this.pollTimeoutSeconds; + } + + Integer getMessagesPerPoll() { + return this.messagesPerPoll; + } + +} diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/config/SqsMessageListenerContainerFactory.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/config/SqsMessageListenerContainerFactory.java new file mode 100644 index 000000000..f065090e5 --- /dev/null +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/config/SqsMessageListenerContainerFactory.java @@ -0,0 +1,109 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.sqs.config; + +import io.awspring.cloud.messaging.support.MessagingUtils; +import io.awspring.cloud.messaging.support.config.AbstractMessageListenerContainerFactory; +import io.awspring.cloud.messaging.support.listener.AsyncMessageListener; +import io.awspring.cloud.sqs.endpoint.SqsEndpoint; +import io.awspring.cloud.sqs.listener.MessageVisibilityExtenderInterceptor; +import io.awspring.cloud.sqs.listener.SqsContainerOptions; +import io.awspring.cloud.sqs.listener.SqsMessageListenerContainer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.Assert; +import software.amazon.awssdk.services.sqs.SqsAsyncClient; + +/** + * @author Tomaz Fernandes + * @since 3.0 + */ +public class SqsMessageListenerContainerFactory + extends AbstractMessageListenerContainerFactory { + + private static final Logger logger = LoggerFactory.getLogger(SqsMessageListenerContainerFactory.class); + + private SqsAsyncClient sqsAsyncClient; + + private final SqsFactoryOptions factoryOptions; + + public SqsMessageListenerContainerFactory() { + this(SqsFactoryOptions.withOptions()); + } + + public SqsMessageListenerContainerFactory(SqsFactoryOptions factoryOptions) { + super(factoryOptions); + this.factoryOptions = factoryOptions; + } + + @Override + protected SqsMessageListenerContainer createContainerInstance(SqsEndpoint endpoint) { + SqsContainerOptions containerOptions = createContainerOptions(endpoint); + SqsMessageListenerContainer container = new SqsMessageListenerContainer(containerOptions, this.sqsAsyncClient, + getMessageListener(endpoint), super.createTaskExecutor()); + MessagingUtils.INSTANCE.acceptBothIfNoneNull(containerOptions.getMinTimeToProcess(), container, + this::addVisibilityExtender); + return container; + } + + @SuppressWarnings("unchecked") + private AsyncMessageListener getMessageListener(SqsEndpoint endpoint) { + return endpoint.isAsync() && super.getBeanFactory().containsBean(SqsConfigUtils.SQS_ASYNC_CLIENT_BEAN_NAME) + ? getBeanFactory().getBean(SqsConfigUtils.SQS_ASYNC_LISTENER_BEAN_NAME, AsyncMessageListener.class) + : super.getMessageListener(); + } + + private void addVisibilityExtender(Integer minTimeToProcess, SqsMessageListenerContainer container) { + MessageVisibilityExtenderInterceptor interceptor = new MessageVisibilityExtenderInterceptor<>( + this.sqsAsyncClient); + interceptor.setMinTimeToProcessMessage(minTimeToProcess); + container.setMessageInterceptor(interceptor); + } + + protected SqsContainerOptions createContainerOptions(SqsEndpoint endpoint) { + SqsContainerOptions options = SqsContainerOptions.optionsFor(endpoint); + MessagingUtils.INSTANCE.acceptFirstNonNull(options::messagesPerProduce, factoryOptions.getMessagesPerPoll()) + .acceptFirstNonNull(options::minTimeToProcess, endpoint.getMinTimeToProcess(), + factoryOptions.getMinTimeToProcess()) + .acceptFirstNonNull(options::simultaneousProduceCalls, endpoint.getSimultaneousPollsPerQueue(), + factoryOptions.getSimultaneousPollsPerQueue()) + .acceptFirstNonNull(options::produceTimeout, endpoint.getPollTimeoutSeconds(), + factoryOptions.getPollTimeoutSeconds()); + return options; + } + + public void setSqsAsyncClient(SqsAsyncClient sqsAsyncClient) { + Assert.notNull(sqsAsyncClient, "sqsAsyncClient cannot be null"); + this.sqsAsyncClient = sqsAsyncClient; + } + + @Override + protected void initializeContainer(SqsMessageListenerContainer container) { + logger.debug("Initializing container: " + container); + container.afterPropertiesSet(); + } + + @Override + protected void initializeFactory() { + if (this.sqsAsyncClient == null) { + Assert.isTrue(getBeanFactory().containsBean(SqsConfigUtils.SQS_ASYNC_CLIENT_BEAN_NAME), + "A SqsAsyncClient must be registered with name " + SqsConfigUtils.SQS_ASYNC_CLIENT_BEAN_NAME); + this.sqsAsyncClient = getBeanFactory().getBean(SqsConfigUtils.SQS_ASYNC_CLIENT_BEAN_NAME, + SqsAsyncClient.class); + } + + } +} diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/endpoint/SqsEndpoint.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/endpoint/SqsEndpoint.java new file mode 100644 index 000000000..e85187d67 --- /dev/null +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/endpoint/SqsEndpoint.java @@ -0,0 +1,151 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.sqs.endpoint; + +import io.awspring.cloud.messaging.support.endpoint.Endpoint; +import io.awspring.cloud.sqs.listener.QueueAttributes; +import java.util.Collection; +import java.util.Map; +import org.springframework.util.Assert; + +/** + * @author Tomaz Fernandes + * @since 3.0 + */ +public class SqsEndpoint implements Endpoint { + + private final Collection logicalEndpointNames; + + private final String listenerContainerFactoryName; + + private final Integer simultaneousPollsPerQueue; + + private final Integer pollTimeoutSeconds; + + private final Integer minTimeToProcess; + + private final Map queueAttributesMap; + + private final boolean isAsync; + + private SqsEndpoint(Collection logicalEndpointNames, String listenerContainerFactoryName, + Integer simultaneousPollsPerQueue, Integer pollTimeoutSeconds, Integer minTimeToProcess, + Map queueAttributesMap, boolean isAsync) { + Assert.notEmpty(logicalEndpointNames, "logicalEndpointNames cannot be null."); + this.queueAttributesMap = queueAttributesMap; + this.logicalEndpointNames = logicalEndpointNames; + this.listenerContainerFactoryName = listenerContainerFactoryName; + this.simultaneousPollsPerQueue = simultaneousPollsPerQueue; + this.pollTimeoutSeconds = pollTimeoutSeconds; + this.minTimeToProcess = minTimeToProcess; + this.isAsync = isAsync; + } + + public static SqsEndpointBuilder from(Collection logicalEndpointNames) { + return new SqsEndpointBuilder(logicalEndpointNames); + } + + @Override + public Collection getLogicalEndpointNames() { + return logicalEndpointNames; + } + + @Override + public String getListenerContainerFactoryName() { + return this.listenerContainerFactoryName; + } + + public Integer getSimultaneousPollsPerQueue() { + return this.simultaneousPollsPerQueue; + } + + public Integer getPollTimeoutSeconds() { + return this.pollTimeoutSeconds; + } + + public Integer getMinTimeToProcess() { + return this.minTimeToProcess; + } + + public QueueAttributes getAttributesFor(String queueName) { + return this.queueAttributesMap.get(queueName); + } + + public Map getQueueAttributes() { + return queueAttributesMap; + } + + public boolean isAsync() { + return isAsync; + } + + public static class SqsEndpointBuilder { + + private final Collection logicalEndpointNames; + + private Integer simultaneousPollsPerQueue; + + private Integer pollTimeoutSeconds; + + private String factoryName; + + private Integer minTimeToProcess; + + private Map queueAttributesMap; + + private boolean async; + + public SqsEndpointBuilder(Collection logicalEndpointNames) { + this.logicalEndpointNames = logicalEndpointNames; + } + + public SqsEndpointBuilder factoryBeanName(String factoryName) { + this.factoryName = factoryName; + return this; + } + + public SqsEndpointBuilder simultaneousPollsPerQueue(Integer simultaneousPollsPerQueue) { + this.simultaneousPollsPerQueue = simultaneousPollsPerQueue; + return this; + } + + public SqsEndpointBuilder pollTimeoutSeconds(Integer pollTimeoutSeconds) { + this.pollTimeoutSeconds = pollTimeoutSeconds; + return this; + } + + public SqsEndpointBuilder minTimeToProcess(Integer minTimeToProcess) { + this.minTimeToProcess = minTimeToProcess; + return this; + } + + public SqsEndpointBuilder queuesAttributes(Map queueAttributesMap) { + this.queueAttributesMap = queueAttributesMap; + return this; + } + + public SqsEndpointBuilder async(boolean async) { + this.async = async; + return this; + } + + public SqsEndpoint build() { + return new SqsEndpoint(this.logicalEndpointNames, this.factoryName, this.simultaneousPollsPerQueue, + this.pollTimeoutSeconds, this.minTimeToProcess, this.queueAttributesMap, this.async); + } + } + +} diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/invocation/EndpointMessageHandler.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/invocation/EndpointMessageHandler.java new file mode 100644 index 000000000..3fffed2ba --- /dev/null +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/invocation/EndpointMessageHandler.java @@ -0,0 +1,316 @@ +/* + * Copyright 2013-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.sqs.invocation; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.awspring.cloud.messaging.support.endpoint.Endpoint; +import io.awspring.cloud.messaging.support.endpoint.EndpointRegistry; +import io.awspring.cloud.messaging.support.listener.MessageHeaders; +import io.awspring.cloud.sqs.annotation.SqsListener; +import io.awspring.cloud.sqs.endpoint.SqsEndpoint; +import io.awspring.cloud.sqs.listener.QueueAttributes; +import io.awspring.cloud.sqs.listener.SqsMessageHeaders; +import io.awspring.cloud.sqs.support.AsyncAcknowledgmentHandlerMethodArgumentResolver; +import io.awspring.cloud.sqs.support.SqsHeadersMethodArgumentResolver; +import io.awspring.cloud.sqs.support.SqsMessageMethodArgumentResolver; +import io.awspring.cloud.sqs.support.VisibilityHandlerMethodArgumentResolver; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletionStage; +import java.util.stream.Collectors; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.config.BeanExpressionContext; +import org.springframework.beans.factory.config.BeanExpressionResolver; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessagingException; +import org.springframework.messaging.converter.CompositeMessageConverter; +import org.springframework.messaging.converter.MappingJackson2MessageConverter; +import org.springframework.messaging.converter.MessageConverter; +import org.springframework.messaging.converter.SimpleMessageConverter; +import org.springframework.messaging.converter.StringMessageConverter; +import org.springframework.messaging.handler.HandlerMethod; +import org.springframework.messaging.handler.annotation.support.AnnotationExceptionHandlerMethodResolver; +import org.springframework.messaging.handler.annotation.support.HeaderMethodArgumentResolver; +import org.springframework.messaging.handler.annotation.support.MessageMethodArgumentResolver; +import org.springframework.messaging.handler.annotation.support.PayloadMethodArgumentResolver; +import org.springframework.messaging.handler.invocation.AbstractExceptionHandlerMethodResolver; +import org.springframework.messaging.handler.invocation.AbstractMethodMessageHandler; +import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; +import org.springframework.messaging.handler.invocation.HandlerMethodReturnValueHandler; +import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; +import org.springframework.util.StringUtils; +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; +import software.amazon.awssdk.services.sqs.SqsAsyncClient; +import software.amazon.awssdk.services.sqs.model.QueueAttributeName; + +/** + * @author Agim Emruli + * @author Alain Sahli + * @author Maciej Walkowiak + * @author Wojciech MÄ…ka + * @author Matej Nedic + * @author Tomaz Fernandes + * @since 1.0 + */ +public class EndpointMessageHandler extends AbstractMethodMessageHandler + implements EndpointRegistry, DisposableBean { + + private final List messageConverters; + + private ObjectMapper objectMapper; + + private final SqsAsyncClient sqsAsyncClient; + + public EndpointMessageHandler(SqsAsyncClient sqsAsyncClient, List messageConverters) { + this.sqsAsyncClient = sqsAsyncClient; + this.messageConverters = messageConverters; + } + + public EndpointMessageHandler(SqsAsyncClient sqsAsyncClient) { + this(sqsAsyncClient, Collections.emptyList()); + } + + private static String[] wrapInStringArray(Object valueToWrap) { + return new String[] { valueToWrap.toString() }; + } + + public void setObjectMapper(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @Override + protected List initArgumentResolvers() { + + List resolvers = new ArrayList<>(getCustomArgumentResolvers()); + + resolvers.add(new SqsHeadersMethodArgumentResolver()); + resolvers.add(new AsyncAcknowledgmentHandlerMethodArgumentResolver(MessageHeaders.ACKNOWLEDGMENT_HEADER)); + resolvers.add(new VisibilityHandlerMethodArgumentResolver(SqsMessageHeaders.VISIBILITY)); + resolvers.add(new SqsMessageMethodArgumentResolver()); + resolvers.add(new HeaderMethodArgumentResolver(new GenericConversionService(), null)); + resolvers.add(new MessageMethodArgumentResolver(this.messageConverters.isEmpty() ? new StringMessageConverter() + : new CompositeMessageConverter(this.messageConverters))); + CompositeMessageConverter compositeMessageConverter = createPayloadArgumentCompositeConverter(); + resolvers.add(new PayloadMethodArgumentResolver(compositeMessageConverter)); + return resolvers; + } + + private CompositeMessageConverter createPayloadArgumentCompositeConverter() { + List payloadArgumentConverters = new ArrayList<>(this.messageConverters); + payloadArgumentConverters.add(getDefaultMappingJackson2MessageConverter()); + payloadArgumentConverters.add(new SimpleMessageConverter()); + return new CompositeMessageConverter(payloadArgumentConverters); + } + + private MappingJackson2MessageConverter getDefaultMappingJackson2MessageConverter() { + MappingJackson2MessageConverter jacksonMessageConverter = new MappingJackson2MessageConverter(); + jacksonMessageConverter.setSerializedPayloadClass(String.class); + jacksonMessageConverter.setStrictContentTypeMatch(false); + + if (this.objectMapper != null) { + jacksonMessageConverter.setObjectMapper(objectMapper); + } + return jacksonMessageConverter; + } + + @Override + protected List initReturnValueHandlers() { + return new ArrayList<>(this.getCustomReturnValueHandlers()); + } + + @Override + protected boolean isHandler(Class beanType) { + return true; + } + + @Override + protected SqsEndpoint getMappingForMethod(Method method, Class handlerType) { + SqsListener sqsListenerAnnotation = AnnotationUtils.findAnnotation(method, SqsListener.class); + if (sqsListenerAnnotation != null && sqsListenerAnnotation.value().length > 0) { + Set logicalEndpointNames = resolveDestinationNames(sqsListenerAnnotation.value()); + return SqsEndpoint.from(logicalEndpointNames) + .factoryBeanName(resolveName(sqsListenerAnnotation.factory())[0]) + .pollTimeoutSeconds(resolveInteger(sqsListenerAnnotation.pollTimeoutSeconds())) + .simultaneousPollsPerQueue(resolveInteger(sqsListenerAnnotation.concurrentPollsPerContainer())) + .minTimeToProcess(resolveInteger(sqsListenerAnnotation.minSecondsToProcess())) + .async(CompletionStage.class.isAssignableFrom(method.getReturnType())) + .queuesAttributes(logicalEndpointNames.stream() + .collect(Collectors.toMap(name -> name, this::queueAttributes))) + .build(); + } + + return null; + } + + private Integer resolveInteger(String pollingTimeoutSeconds) { + String[] pollingStrings = resolveName(pollingTimeoutSeconds); + return pollingStrings == null || pollingStrings.length == 0 || !StringUtils.hasText(pollingStrings[0]) ? null + : Integer.parseInt(pollingStrings[0]); + } + + private Set resolveDestinationNames(String[] destinationNames) { + Set result = new HashSet<>(destinationNames.length); + + for (String destinationName : destinationNames) { + result.addAll(Arrays.asList(resolveName(destinationName))); + } + + return result; + } + + private String[] resolveName(String name) { + + if (!(getApplicationContext() instanceof ConfigurableApplicationContext)) { + return wrapInStringArray(name); + } + + ConfigurableApplicationContext applicationContext = (ConfigurableApplicationContext) getApplicationContext(); + ConfigurableBeanFactory configurableBeanFactory = applicationContext.getBeanFactory(); + + String placeholdersResolved = configurableBeanFactory.resolveEmbeddedValue(name); + BeanExpressionResolver exprResolver = configurableBeanFactory.getBeanExpressionResolver(); + if (exprResolver == null) { + return wrapInStringArray(name); + } + Object result = exprResolver.evaluate(placeholdersResolved, + new BeanExpressionContext(configurableBeanFactory, null)); + if (result instanceof String[]) { + return (String[]) result; + } + else if (result != null) { + return wrapInStringArray(result); + } + else { + return wrapInStringArray(name); + } + } + + private QueueAttributes queueAttributes(String queue) { + return queueAttributes(queue, 10); + } + + private QueueAttributes queueAttributes(String queue, int attemptsLeft) { + try { + logger.debug("Fetching queue attributes for queue " + queue); + + String queueUrl = this.sqsAsyncClient.getQueueUrl(req -> req.queueName(queue)).get().queueUrl(); + Map attributes = this.sqsAsyncClient + .getQueueAttributes(req -> req.queueUrl(queueUrl)).get().attributes(); + + boolean hasRedrivePolicy = attributes.containsKey(QueueAttributeName.REDRIVE_POLICY); + boolean isFifo = queue.endsWith(".fifo"); + return new QueueAttributes(queueUrl, hasRedrivePolicy, getVisibility(attributes), isFifo); + } + catch (Exception e) { + logger.warn( + String.format("Could not retrieve attributes for queue %s. Attempts left: %s", queue, attemptsLeft), + e); + try { + if (attemptsLeft == 0) { + throw new IllegalStateException("Could not retrieve properties for queue " + queue, e); + } + Thread.sleep(8000 / attemptsLeft); + return queueAttributes(queue, attemptsLeft - 1); + } + catch (InterruptedException ex) { + throw new IllegalStateException("Interrupted while retrieving attributes for queue " + queue, ex); + } + catch (Exception ex) { + throw new IllegalStateException("Could not retrieve properties for queue " + queue, ex); + } + } + } + + private Integer getVisibility(Map attributes) { + String visibilityTimeout = attributes.get(QueueAttributeName.VISIBILITY_TIMEOUT); + return visibilityTimeout != null ? Integer.parseInt(visibilityTimeout) : null; + } + + @Override + protected Set getDirectLookupDestinations(Endpoint mapping) { + return new HashSet<>(mapping.getLogicalEndpointNames()); + } + + @Override + protected String getDestination(Message message) { + return message.getHeaders().get(SqsMessageHeaders.SQS_LOGICAL_RESOURCE_ID).toString(); + } + + @Override + protected Endpoint getMatchingMapping(Endpoint endpoint, Message message) { + return endpoint.getLogicalEndpointNames().contains(getDestination(message)) ? endpoint : null; + } + + @Override + protected Comparator getMappingComparator(Message message) { + return (o1, o2) -> 0; + } + + @Override + protected AbstractExceptionHandlerMethodResolver createExceptionHandlerMethodResolverFor(Class beanType) { + return new AnnotationExceptionHandlerMethodResolver(beanType); + } + + @Override + protected void handleNoMatch(Set ts, String lookupDestination, Message message) { + this.logger.warn("No match found"); + } + + @Override + protected void processHandlerMethodException(HandlerMethod handlerMethod, Exception ex, Message message) { + InvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, ex); + if (exceptionHandlerMethod != null) { + super.processHandlerMethodException(handlerMethod, ex, message); + } + throw new MessagingException(message, "An exception occurred while invoking the handler method", ex); + } + + @Override + public Collection retrieveEndpoints() { + return Collections.unmodifiableSet(super.getHandlerMethods().keySet()); + } + + @Override + public void destroy() throws Exception { + logger.error("Destroying"); + } + + private static final class NoOpValidator implements Validator { + + @Override + public boolean supports(Class clazz) { + return false; + } + + @Override + public void validate(Object target, Errors errors) { + } + + } +} diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/AsyncMessageHandlerMessageListener.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/AsyncMessageHandlerMessageListener.java new file mode 100644 index 000000000..a4feeca1e --- /dev/null +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/AsyncMessageHandlerMessageListener.java @@ -0,0 +1,64 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.sqs.listener; + +import io.awspring.cloud.messaging.support.listener.CallbackMessageListener; +import io.awspring.cloud.sqs.support.CallbackFutureReturnValueHandler; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.BiFunction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHandler; +import org.springframework.messaging.handler.invocation.AbstractMethodMessageHandler; +import org.springframework.messaging.handler.invocation.HandlerMethodReturnValueHandler; + +/** + * @author Tomaz Fernandes + * @since 3.0 + */ +public class AsyncMessageHandlerMessageListener implements CallbackMessageListener { + + private static final Logger logger = LoggerFactory.getLogger(AsyncMessageHandlerMessageListener.class); + + private final MessageHandler messageHandler; + + public AsyncMessageHandlerMessageListener(MessageHandler messageHandler) { + this.messageHandler = messageHandler; + } + + @SuppressWarnings("unchecked") + @Override + public void addResultCallback(BiFunction, Throwable, CompletableFuture> callback) { + AbstractMethodMessageHandler messageHandler = (AbstractMethodMessageHandler) this.messageHandler; + List returnValueHandlers = new ArrayList<>( + messageHandler.getReturnValueHandlers()); + returnValueHandlers.add(new CallbackFutureReturnValueHandler<>(callback)); + messageHandler.setReturnValueHandlers(returnValueHandlers); + } + + @Override + public CompletableFuture onMessage(Message message) { + logger.trace("Handling message {} in thread {}", message.getPayload(), Thread.currentThread().getName()); + return CompletableFuture.supplyAsync(() -> { + this.messageHandler.handleMessage(message); + return null; + }); + } + +} diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/MessageHandlerMessageListener.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/MessageHandlerMessageListener.java new file mode 100644 index 000000000..0648694c2 --- /dev/null +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/MessageHandlerMessageListener.java @@ -0,0 +1,60 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.sqs.listener; + +import io.awspring.cloud.messaging.support.listener.AsyncMessageListener; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.task.TaskExecutor; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHandler; + +/** + * @author Tomaz Fernandes + * @since 3.0 + */ +public class MessageHandlerMessageListener implements AsyncMessageListener { + + private static final Logger logger = LoggerFactory.getLogger(MessageHandlerMessageListener.class); + + private final MessageHandler messageHandler; + + private TaskExecutor taskExecutor; + + public MessageHandlerMessageListener(MessageHandler messageHandler) { + this.messageHandler = messageHandler; + } + + @Override + public CompletableFuture onMessage(Message message) { + logger.trace("Handling message {} in thread {}", message.getPayload(), Thread.currentThread().getName()); + return this.taskExecutor != null ? CompletableFuture.supplyAsync(handleMessage(message), this.taskExecutor) + : CompletableFuture.supplyAsync(handleMessage(message)); + } + + private Supplier handleMessage(Message message) { + return () -> { + this.messageHandler.handleMessage(message); + return null; + }; + } + + public void setTaskExecutor(TaskExecutor taskExecutor) { + this.taskExecutor = taskExecutor; + } +} diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/MessageVisibilityExtenderInterceptor.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/MessageVisibilityExtenderInterceptor.java new file mode 100644 index 000000000..b5900d60c --- /dev/null +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/MessageVisibilityExtenderInterceptor.java @@ -0,0 +1,77 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.sqs.listener; + +import io.awspring.cloud.messaging.support.listener.AsyncMessageInterceptor; +import java.time.Instant; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.messaging.Message; +import org.springframework.util.Assert; +import software.amazon.awssdk.services.sqs.SqsAsyncClient; + +/** + * @author Tomaz Fernandes + * @since 3.0 + */ +public class MessageVisibilityExtenderInterceptor implements AsyncMessageInterceptor { + + private static final Logger logger = LoggerFactory.getLogger(MessageVisibilityExtenderInterceptor.class); + + private static final int DEFAULT_MIN_VISIBILITY = 30; + + private static final int DEFAULT_QUEUE_VISIBILITY = 30; + + private final SqsAsyncClient asyncClient; + + private int minTimeToProcessMessage = DEFAULT_MIN_VISIBILITY; + + public MessageVisibilityExtenderInterceptor(SqsAsyncClient asyncClient) { + this.asyncClient = asyncClient; + } + + @Override + public CompletableFuture> intercept(Message message) { + Object receivedAtObject = message.getHeaders().get(SqsMessageHeaders.RECEIVED_AT); + Object queueVisibilityObject = message.getHeaders().get(SqsMessageHeaders.QUEUE_VISIBILITY); + if (receivedAtObject == null) { + logger.warn("Header {} needs to be present to extend visibility. Skipping.", SqsMessageHeaders.RECEIVED_AT); + return forwardMessage(message); + } + int queueVisibility = queueVisibilityObject != null ? (int) queueVisibilityObject : DEFAULT_QUEUE_VISIBILITY; + + return Instant.now().plusSeconds(this.minTimeToProcessMessage) + .isAfter(((Instant) receivedAtObject).plusSeconds(queueVisibility)) ? doChangeVisibility(message) + : forwardMessage(message); + } + + private CompletableFuture> doChangeVisibility(Message message) { + return ((Visibility) Objects.requireNonNull(message.getHeaders().get(SqsMessageHeaders.VISIBILITY), + "No Visibility found in " + message)).changeTo(this.minTimeToProcessMessage).thenApply(res -> message); + } + + private CompletableFuture> forwardMessage(Message message) { + return CompletableFuture.supplyAsync(() -> message); + } + + public void setMinTimeToProcessMessage(int minTimeToProcessMessage) { + Assert.isTrue(minTimeToProcessMessage > 0, "minTimeToProcessMessage cannot be < 0"); + Assert.isTrue(minTimeToProcessMessage < 43200, "minTimeToProcessMessage cannot be > 43200"); + this.minTimeToProcessMessage = minTimeToProcessMessage; + } +} diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/QueueAttributes.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/QueueAttributes.java new file mode 100644 index 000000000..4b6bdd4b9 --- /dev/null +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/QueueAttributes.java @@ -0,0 +1,54 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.sqs.listener; + +/** + * @author Tomaz Fernandes + * @since 3.0 + */ +public class QueueAttributes { + + private final String destinationUrl; + + private final boolean hasRedrivePolicy; + + private final Integer visibilityTimeout; + + private final boolean fifo; + + public QueueAttributes(String destinationUrl, boolean hasRedrivePolicy, Integer visibilityTimeout, boolean fifo) { + this.hasRedrivePolicy = hasRedrivePolicy; + this.destinationUrl = destinationUrl; + this.visibilityTimeout = visibilityTimeout; + this.fifo = fifo; + } + + public boolean hasRedrivePolicy() { + return this.hasRedrivePolicy; + } + + boolean isFifo() { + return fifo; + } + + public String getDestinationUrl() { + return destinationUrl; + } + + public Integer getVisibilityTimeout() { + return visibilityTimeout; + } +} diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/QueueMessageVisibility.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/QueueMessageVisibility.java new file mode 100644 index 000000000..c3f09b6bb --- /dev/null +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/QueueMessageVisibility.java @@ -0,0 +1,51 @@ +/* + * Copyright 2013-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.sqs.listener; + +import java.util.concurrent.CompletableFuture; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import software.amazon.awssdk.services.sqs.SqsAsyncClient; + +/** + * @author Szymon Dembek + * @author Tomaz Fernandes + * @since 1.3 + */ +public class QueueMessageVisibility implements Visibility { + + private static final Logger logger = LoggerFactory.getLogger(QueueMessageVisibility.class); + + private final SqsAsyncClient sqsAsyncClient; + + private final String queueUrl; + + private final String receiptHandle; + + public QueueMessageVisibility(SqsAsyncClient amazonSqsAsync, String queueUrl, String receiptHandle) { + this.sqsAsyncClient = amazonSqsAsync; + this.queueUrl = queueUrl; + this.receiptHandle = receiptHandle; + } + + @Override + public CompletableFuture changeTo(int seconds) { + logger.debug("Changing visibility of message {} to {}", this.receiptHandle, seconds); + return this.sqsAsyncClient.changeMessageVisibility( + req -> req.queueUrl(this.queueUrl).receiptHandle(this.receiptHandle).visibilityTimeout(seconds)); + } + +} diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/SqsAcknowledge.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/SqsAcknowledge.java new file mode 100644 index 000000000..371e4e4b1 --- /dev/null +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/SqsAcknowledge.java @@ -0,0 +1,44 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.sqs.listener; + +import io.awspring.cloud.messaging.support.listener.acknowledgement.AsyncAcknowledgement; +import java.util.concurrent.CompletableFuture; +import software.amazon.awssdk.services.sqs.SqsAsyncClient; + +/** + * @author Tomaz Fernandes + * @since 3.0 + */ +public class SqsAcknowledge implements AsyncAcknowledgement { + + private final SqsAsyncClient sqsAsyncClient; + + private final String queueUrl; + + private final String receiptHandle; + + public SqsAcknowledge(SqsAsyncClient sqsAsyncClient, String queueUrl, String receiptHandle) { + this.sqsAsyncClient = sqsAsyncClient; + this.queueUrl = queueUrl; + this.receiptHandle = receiptHandle; + } + + @Override + public CompletableFuture acknowledge() { + return this.sqsAsyncClient.deleteMessage(req -> req.queueUrl(this.queueUrl).receiptHandle(this.receiptHandle)); + } +} diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/SqsContainerOptions.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/SqsContainerOptions.java new file mode 100644 index 000000000..18987300a --- /dev/null +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/SqsContainerOptions.java @@ -0,0 +1,53 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.sqs.listener; + +import io.awspring.cloud.messaging.support.listener.AbstractContainerOptions; +import io.awspring.cloud.sqs.endpoint.SqsEndpoint; + +/** + * @author Tomaz Fernandes + * @since 3.0 + */ +public class SqsContainerOptions extends AbstractContainerOptions { + + private final SqsEndpoint endpoint; + + private Integer minTimeToProcess; + + private SqsContainerOptions(SqsEndpoint endpoint) { + this.endpoint = endpoint; + } + + public static SqsContainerOptions optionsFor(SqsEndpoint endpoint) { + return new SqsContainerOptions(endpoint); + } + + public SqsContainerOptions minTimeToProcess(Integer minTimeToProcess) { + this.minTimeToProcess = minTimeToProcess; + return this; + } + + @Override + public SqsEndpoint getEndpoint() { + return this.endpoint; + } + + public Integer getMinTimeToProcess() { + return minTimeToProcess; + } + +} diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/SqsMessageHeaders.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/SqsMessageHeaders.java new file mode 100644 index 000000000..188a2d755 --- /dev/null +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/SqsMessageHeaders.java @@ -0,0 +1,108 @@ +/* + * Copyright 2013-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.sqs.listener; + +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import org.springframework.messaging.MessageHeaders; + +/** + * Specialization of the {@link MessageHeaders} class that allows to set an ID. This was done to support cases where the + * ID sent by the producer must be restored on the consumer side for traceability. + * + * @author Alain Sahli + * @author Wojciech MÄ…ka + * @author Tomaz Fernandes + * @since 1.0 + */ +public class SqsMessageHeaders extends MessageHeaders { + + /** + * Delay header in a SQS message. + */ + public static final String SQS_DELAY_HEADER = "delay"; + + /** + * Group id header in a SQS message. + */ + public static final String SQS_GROUP_ID_HEADER = "message-group-id"; + + /** + * Deduplication header in a SQS message. + */ + public static final String SQS_DEDUPLICATION_ID_HEADER = "message-deduplication-id"; + + /** + * ApproximateFirstReceiveTimestamp header in a SQS message. + */ + public static final String SQS_APPROXIMATE_FIRST_RECEIVE_TIMESTAMP = "ApproximateFirstReceiveTimestamp"; + + /** + * ApproximateReceiveCount header in a SQS message. + */ + public static final String SQS_APPROXIMATE_RECEIVE_COUNT = "ApproximateReceiveCount"; + + /** + * SentTimestamp header in a SQS message. + */ + public static final String SQS_SENT_TIMESTAMP = "SentTimestamp"; + + public static final String SQS_LOGICAL_RESOURCE_ID = "LogicalResourceId"; + + public static final String RECEIPT_HANDLE_MESSAGE_ATTRIBUTE_NAME = "ReceiptHandle"; + + public static final String MESSAGE_ID_MESSAGE_ATTRIBUTE_NAME = "MessageId"; + + public static final String SOURCE_DATA_HEADER = "sourceData"; + + public static final String VISIBILITY = "Visibility"; + + public static final String RECEIVED_AT = "ReceivedAt"; + + public static final String QUEUE_VISIBILITY = "QueueVisibility"; + + public SqsMessageHeaders(Map headers) { + super(headers, getId(headers), getTimestamp(headers)); + } + + public Long getApproximateFirstReceiveTimestamp() { + return containsKey(SQS_APPROXIMATE_FIRST_RECEIVE_TIMESTAMP) + ? Long.parseLong(Objects.requireNonNull(get(SQS_APPROXIMATE_FIRST_RECEIVE_TIMESTAMP, String.class))) + : null; + } + + public Long getSentTimestamp() { + return getTimestamp(getRawHeaders()); + } + + public Long getApproximateReceiveCount() { + return containsKey(SQS_APPROXIMATE_RECEIVE_COUNT) + ? Long.parseLong(Objects.requireNonNull(get(SQS_APPROXIMATE_RECEIVE_COUNT, String.class))) + : null; + } + + private static Long getTimestamp(Map headers) { + return headers.containsKey(SQS_SENT_TIMESTAMP) + ? Long.parseLong(Objects.requireNonNull((String) headers.get(SQS_SENT_TIMESTAMP))) + : null; + } + + private static UUID getId(Map headers) { + return headers.containsKey(MessageHeaders.ID) ? (UUID) headers.get(MessageHeaders.ID) : null; + } + +} diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/SqsMessageListenerContainer.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/SqsMessageListenerContainer.java new file mode 100644 index 000000000..823e90a96 --- /dev/null +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/SqsMessageListenerContainer.java @@ -0,0 +1,64 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.sqs.listener; + +import io.awspring.cloud.messaging.support.listener.AbstractMessageListenerContainer; +import io.awspring.cloud.messaging.support.listener.AsyncMessageListener; +import io.awspring.cloud.messaging.support.listener.AsyncMessageProducer; +import java.util.List; +import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.task.TaskExecutor; +import software.amazon.awssdk.services.sqs.SqsAsyncClient; + +/** + * @author Tomaz Fernandes + * @since 3.0 + */ +public class SqsMessageListenerContainer extends AbstractMessageListenerContainer { + + private final static Logger logger = LoggerFactory.getLogger(SqsMessageListenerContainer.class); + + public SqsMessageListenerContainer(SqsContainerOptions options, SqsAsyncClient sqsClient, + AsyncMessageListener messageListener, TaskExecutor taskExecutor) { + super(options, taskExecutor, messageListener, createMessageProducers(options, sqsClient)); + } + + private static List> createMessageProducers(SqsContainerOptions options, + SqsAsyncClient sqsClient) { + return options.getEndpoint().getQueueAttributes().entrySet().stream() + .map(entry -> createMessageProducer(sqsClient, entry.getKey(), entry.getValue())) + .collect(Collectors.toList()); + } + + private static SqsMessageProducer createMessageProducer(SqsAsyncClient sqsClient, String logicalEndpointName, + QueueAttributes queueAttributes) { + return new SqsMessageProducer(logicalEndpointName, queueAttributes, sqsClient); + } + + @Override + protected void doStart() { + logger.debug("Starting SqsMessageListenerContainer: " + this); + super.getMessageProducers().stream().map(SqsMessageProducer.class::cast).forEach(SqsMessageProducer::start); + } + + @Override + protected void doStop() { + logger.debug("Stopping SqsMessageListenerContainer: " + this); + super.getMessageProducers().stream().map(SqsMessageProducer.class::cast).forEach(SqsMessageProducer::stop); + } +} diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/SqsMessageProducer.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/SqsMessageProducer.java new file mode 100644 index 000000000..2705a46fd --- /dev/null +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/SqsMessageProducer.java @@ -0,0 +1,161 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.sqs.listener; + +import io.awspring.cloud.messaging.support.listener.AsyncMessageProducer; +import io.awspring.cloud.messaging.support.listener.MessageHeaders; +import java.time.Duration; +import java.time.Instant; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.SmartLifecycle; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.GenericMessage; +import org.springframework.util.MimeType; +import software.amazon.awssdk.services.sqs.SqsAsyncClient; +import software.amazon.awssdk.services.sqs.model.MessageSystemAttributeName; +import software.amazon.awssdk.services.sqs.model.ReceiveMessageResponse; + +/** + * @author Tomaz Fernandes + * @since 3.0 + */ +public class SqsMessageProducer implements AsyncMessageProducer, SmartLifecycle { + + private static final Logger logger = LoggerFactory.getLogger(SqsMessageProducer.class); + + private final String logicalEndpointName; + + private final QueueAttributes queueAttributes; + + private final SqsAsyncClient sqsAsyncClient; + + private final String queueUrl; + + private volatile boolean running; + + public SqsMessageProducer(String logicalEndpointName, QueueAttributes queueAttributes, SqsAsyncClient sqsClient) { + this.logicalEndpointName = logicalEndpointName; + this.queueUrl = queueAttributes.getDestinationUrl(); + this.queueAttributes = queueAttributes; + this.sqsAsyncClient = sqsClient; + } + + @Override + public CompletableFuture>> produce(int numberOfMessages, Duration timeout) { + logger.trace("Polling for messages at " + this.queueUrl); + return sqsAsyncClient + .receiveMessage(req -> req.queueUrl(this.queueUrl).maxNumberOfMessages(numberOfMessages) + .waitTimeSeconds((int) timeout.getSeconds())) + .thenApply(ReceiveMessageResponse::messages).thenApply(this::getMessagesForExecution) + .exceptionally(handleException()); + } + + protected Function>> handleException() { + return t -> { + logger.error("Error retrieving messages from SQS", t); + return Collections.emptyList(); + }; + } + + private Collection> getMessagesForExecution( + List messages) { + logger.trace("Poll returned {} messages", messages.size()); + return messages.stream().map(this::getMessageForExecution).collect(Collectors.toList()); + } + + protected org.springframework.messaging.Message getMessageForExecution( + final software.amazon.awssdk.services.sqs.model.Message message) { + HashMap additionalHeaders = new HashMap<>(); + additionalHeaders.put(SqsMessageHeaders.SQS_LOGICAL_RESOURCE_ID, this.logicalEndpointName); + additionalHeaders.put(SqsMessageHeaders.RECEIVED_AT, Instant.now()); + additionalHeaders.put(SqsMessageHeaders.QUEUE_VISIBILITY, this.queueAttributes.getVisibilityTimeout()); + additionalHeaders.put(SqsMessageHeaders.VISIBILITY, + new QueueMessageVisibility(this.sqsAsyncClient, this.queueUrl, message.receiptHandle())); + return createMessage(message, additionalHeaders); + } + + protected org.springframework.messaging.Message createMessage( + software.amazon.awssdk.services.sqs.model.Message message, Map additionalHeaders) { + + HashMap messageHeaders = new HashMap<>(); + messageHeaders.put(SqsMessageHeaders.MESSAGE_ID_MESSAGE_ATTRIBUTE_NAME, message.messageId()); + messageHeaders.put(SqsMessageHeaders.RECEIPT_HANDLE_MESSAGE_ATTRIBUTE_NAME, message.receiptHandle()); + messageHeaders.put(SqsMessageHeaders.SOURCE_DATA_HEADER, message); + messageHeaders.put(MessageHeaders.ACKNOWLEDGMENT_HEADER, + new SqsAcknowledge(this.sqsAsyncClient, this.queueUrl, message.receiptHandle())); + messageHeaders.putAll(additionalHeaders); + messageHeaders.putAll(getAttributesAsMessageHeaders(message)); + messageHeaders.putAll(getMessageAttributesAsMessageHeaders(message)); + return new GenericMessage<>(message.body(), new SqsMessageHeaders(messageHeaders)); + } + + private static Map getMessageAttributesAsMessageHeaders( + software.amazon.awssdk.services.sqs.model.Message message) { + + Map messageHeaders = new HashMap<>(); + for (Map.Entry messageAttribute : message.attributes().entrySet()) { + if (org.springframework.messaging.MessageHeaders.CONTENT_TYPE.equals(messageAttribute.getKey().name())) { + messageHeaders.put(org.springframework.messaging.MessageHeaders.CONTENT_TYPE, + MimeType.valueOf(messageAttribute.getValue())); + } + else if (org.springframework.messaging.MessageHeaders.ID.equals(messageAttribute.getKey().name())) { + messageHeaders.put(org.springframework.messaging.MessageHeaders.ID, + UUID.fromString(messageAttribute.getValue())); + } + else { + messageHeaders.put(messageAttribute.getKey().name(), messageAttribute.getValue()); + } + } + return messageHeaders; + } + + private static Map getAttributesAsMessageHeaders( + software.amazon.awssdk.services.sqs.model.Message message) { + Map messageHeaders = new HashMap<>(); + for (Map.Entry attributeKeyValuePair : message.attributes().entrySet()) { + messageHeaders.put(attributeKeyValuePair.getKey().name(), attributeKeyValuePair.getValue()); + } + return messageHeaders; + } + + @Override + public void start() { + logger.debug("Starting SqsMessageProducer for " + this.logicalEndpointName); + // Synchronization not necessary as field is volatile + this.running = true; + } + + @Override + public void stop() { + logger.debug("Stopping SqsMessageProducer for " + this.logicalEndpointName); + this.running = false; + } + + @Override + public boolean isRunning() { + return this.running; + } +} diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/Visibility.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/Visibility.java new file mode 100644 index 000000000..2a249eb5b --- /dev/null +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/Visibility.java @@ -0,0 +1,39 @@ +/* + * Copyright 2013-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.sqs.listener; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; + +/** + * Visibility interface that can be injected as parameter into a listener method. The purpose of this interface is to + * provide a way for the listener methods to extend the visibility timeout of the message being currently processed. + * + * @author Szymon Dembek + * @author Tomaz Fernandes + * @since 1.3 + */ +public interface Visibility { + + /** + * Allows extending the visibility timeout of a message that was already fetched from the queue, in case when the + * configured visibility timeout turns out to be to short. + * @param seconds number of seconds to extend the visibility timeout by + * @return a {@link Future} as the extension can involve some asynchronous request (i.e. request to an AWS API). + */ + CompletableFuture changeTo(int seconds); + +} diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/support/AsyncAcknowledgmentHandlerMethodArgumentResolver.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/support/AsyncAcknowledgmentHandlerMethodArgumentResolver.java new file mode 100644 index 000000000..b8c303f51 --- /dev/null +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/support/AsyncAcknowledgmentHandlerMethodArgumentResolver.java @@ -0,0 +1,52 @@ +/* + * Copyright 2013-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.sqs.support; + +import io.awspring.cloud.messaging.support.listener.acknowledgement.AsyncAcknowledgement; +import org.springframework.core.MethodParameter; +import org.springframework.messaging.Message; +import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; +import org.springframework.util.ClassUtils; + +/** + * @author Alain Sahli + * @author Tomaz Fernandes + * @since 1.1 + */ +public class AsyncAcknowledgmentHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver { + + private final String acknowledgmentHeaderName; + + public AsyncAcknowledgmentHandlerMethodArgumentResolver(String acknowledgmentHeaderName) { + this.acknowledgmentHeaderName = acknowledgmentHeaderName; + } + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return ClassUtils.isAssignable(AsyncAcknowledgement.class, parameter.getParameterType()); + } + + @Override + public Object resolveArgument(MethodParameter parameter, Message message) { + if (!message.getHeaders().containsKey(this.acknowledgmentHeaderName) + || message.getHeaders().get(this.acknowledgmentHeaderName) == null) { + throw new IllegalArgumentException( + "No acknowledgment object found for message header: '" + this.acknowledgmentHeaderName + "'"); + } + return message.getHeaders().get(this.acknowledgmentHeaderName); + } + +} diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/support/CallbackFutureReturnValueHandler.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/support/CallbackFutureReturnValueHandler.java new file mode 100644 index 000000000..df74c8975 --- /dev/null +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/support/CallbackFutureReturnValueHandler.java @@ -0,0 +1,55 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.sqs.support; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.function.BiFunction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.MethodParameter; +import org.springframework.messaging.Message; +import org.springframework.messaging.handler.invocation.HandlerMethodReturnValueHandler; + +/** + * @author Tomaz Fernandes + * @since 3.0 + */ +public class CallbackFutureReturnValueHandler implements HandlerMethodReturnValueHandler { + + private static final Logger logger = LoggerFactory.getLogger(CallbackFutureReturnValueHandler.class); + + private final BiFunction, Throwable, CompletableFuture> callback; + + public CallbackFutureReturnValueHandler(BiFunction, Throwable, CompletableFuture> callback) { + this.callback = callback; + } + + @Override + public boolean supportsReturnType(MethodParameter returnType) { + return CompletionStage.class.isAssignableFrom(returnType.getParameterType()); + } + + @SuppressWarnings("unchecked") + @Override + public void handleReturnValue(Object returnValue, MethodParameter returnType, Message message) { + ((CompletionStage) returnValue).handle((val, t) -> callback.apply(((Message) message), t)) + .exceptionally(t -> { + logger.error("Error executing callback for message {}", message, t); + return null; + }); + } +} diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/support/SqsHeadersMethodArgumentResolver.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/support/SqsHeadersMethodArgumentResolver.java new file mode 100644 index 000000000..d2bce10e3 --- /dev/null +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/support/SqsHeadersMethodArgumentResolver.java @@ -0,0 +1,57 @@ +/* + * Copyright 2013-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.sqs.support; + +import io.awspring.cloud.sqs.listener.SqsMessageHeaders; +import java.util.Map; +import java.util.Objects; +import org.springframework.core.MethodParameter; +import org.springframework.messaging.Message; +import org.springframework.messaging.handler.annotation.support.HeadersMethodArgumentResolver; + +/** + * Argument resolver for SQS message headers. + * + * @author Wojciech MÄ…ka + * @since 2.2.3 + */ +public class SqsHeadersMethodArgumentResolver extends HeadersMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return super.supportsParameter(parameter) || SqsMessageHeaders.class == parameter.getParameterType(); + } + + @SuppressWarnings("unchecked") + @Override + public Object resolveArgument(MethodParameter parameter, Message message) throws Exception { + final Object resolvedParameter = Objects.requireNonNull(super.resolveArgument(parameter, message)); + if (Map.class.isAssignableFrom(resolvedParameter.getClass()) + && SqsMessageHeaders.class != resolvedParameter.getClass()) { + return new SqsMessageHeaders((Map) resolvedParameter); + } + else { + // Here according to source code in HeadersMethodArgumentResolver we can have + // MessageHeadersAccessor. + // Return everything which cannot be wrapped in SqsMessageHeaders due to + // handler method signature, + // or if resolved parameter already is SqsMessageHeader - do not wrap it + // again. + return resolvedParameter; + } + } + +} diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/support/SqsMessageMethodArgumentResolver.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/support/SqsMessageMethodArgumentResolver.java new file mode 100644 index 000000000..3c72eff45 --- /dev/null +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/support/SqsMessageMethodArgumentResolver.java @@ -0,0 +1,40 @@ +/* + * Copyright 2013-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.sqs.support; + +import io.awspring.cloud.sqs.listener.SqsMessageHeaders; +import org.springframework.core.MethodParameter; +import org.springframework.messaging.Message; +import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; + +/** + * Resolves original SQS message object {@link (software.amazon.awssdk.services.sqs.model.Message)} from Spring + * Messaging message object {@link Message}. + * + * @author Maciej Walkowiak + */ +public class SqsMessageMethodArgumentResolver implements HandlerMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return software.amazon.awssdk.services.sqs.model.Message.class.isAssignableFrom(parameter.getParameterType()); + } + + @Override + public Object resolveArgument(MethodParameter parameter, Message message) { + return message.getHeaders().get(SqsMessageHeaders.SOURCE_DATA_HEADER); + } +} diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/support/VisibilityHandlerMethodArgumentResolver.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/support/VisibilityHandlerMethodArgumentResolver.java new file mode 100644 index 000000000..bcbd6767d --- /dev/null +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/support/VisibilityHandlerMethodArgumentResolver.java @@ -0,0 +1,51 @@ +/* + * Copyright 2013-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.sqs.support; + +import io.awspring.cloud.sqs.listener.Visibility; +import org.springframework.core.MethodParameter; +import org.springframework.messaging.Message; +import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; +import org.springframework.util.ClassUtils; + +/** + * @author Szymon Dembek + * @since 1.3 + */ +public class VisibilityHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver { + + private final String visibilityHeaderName; + + public VisibilityHandlerMethodArgumentResolver(String visibilityHeaderName) { + this.visibilityHeaderName = visibilityHeaderName; + } + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return ClassUtils.isAssignable(Visibility.class, parameter.getParameterType()); + } + + @Override + public Object resolveArgument(MethodParameter parameter, Message message) throws Exception { + if (!message.getHeaders().containsKey(this.visibilityHeaderName) + || message.getHeaders().get(this.visibilityHeaderName) == null) { + throw new IllegalArgumentException( + "No visibility object found for message header: '" + this.visibilityHeaderName + "'"); + } + return message.getHeaders().get(this.visibilityHeaderName); + } + +} diff --git a/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/BaseSqsIntegrationTest.java b/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/BaseSqsIntegrationTest.java new file mode 100644 index 000000000..a6b2725d4 --- /dev/null +++ b/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/BaseSqsIntegrationTest.java @@ -0,0 +1,63 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.sqs; + +import static org.testcontainers.containers.localstack.LocalStackContainer.Service.SQS; + +import java.io.IOException; +import org.junit.jupiter.api.BeforeAll; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.containers.localstack.LocalStackContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +@Testcontainers +abstract class BaseSqsIntegrationTest { + + protected static final String RECEIVES_MESSAGE_QUEUE_NAME = "receives.message.test.queue"; + protected static final String DOES_NOT_ACK_ON_ERROR_QUEUE_NAME = "does.not.ack.test.queue"; + protected static final String RESOLVES_PARAMETER_TYPES_QUEUE_NAME = "resolves.parameter.test.queue"; + protected static final String RESOLVES_POJO_TYPES_QUEUE_NAME = "resolves.pojo.test.queue"; + protected static final String RECEIVE_FROM_MANY_1_QUEUE_NAME = "receive.many.test.queue.1"; + protected static final String RECEIVE_FROM_MANY_2_QUEUE_NAME = "receive.many.test.queue.2"; + protected static final String ASYNC_RECEIVE_FROM_MANY_1_QUEUE_NAME = "async.receive.many.test.queue.1"; + protected static final String ASYNC_RECEIVE_FROM_MANY_2_QUEUE_NAME = "async.receive.many.test.queue.2"; + + @Container + static LocalStackContainer localstack = new LocalStackContainer( + DockerImageName.parse("localstack/localstack:0.14.0")).withServices(SQS).withReuse(false); + + @BeforeAll + static void beforeAll() throws IOException, InterruptedException { + // create needed queues in SQS + // TODO: Not working as expected due to some port mapping issue - will look into in the future + localstack.execInContainer("awslocal", "io/awspring/cloud/sqs", "create-queue", "--queue-name", + RECEIVES_MESSAGE_QUEUE_NAME); + localstack.execInContainer("awslocal", "io/awspring/cloud/sqs", "create-queue", "--queue-name", + DOES_NOT_ACK_ON_ERROR_QUEUE_NAME); + localstack.execInContainer("awslocal", "io/awspring/cloud/sqs", "create-queue", "--queue-name", + RECEIVE_FROM_MANY_1_QUEUE_NAME); + } + + @DynamicPropertySource + static void registerSqsProperties(DynamicPropertyRegistry registry) { + // overwrite SQS endpoint with one provided by Localstack + registry.add("cloud.aws.sqs.endpoint", () -> localstack.getEndpointOverride(SQS).toString()); + } + +} diff --git a/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/SqsIntegrationTests.java b/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/SqsIntegrationTests.java new file mode 100644 index 000000000..75fd7f2c9 --- /dev/null +++ b/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/SqsIntegrationTests.java @@ -0,0 +1,519 @@ +/* + * Copyright 2013-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.awspring.cloud.sqs; + +import static io.awspring.cloud.sqs.config.SqsFactoryOptions.withOptions; +import static java.util.Collections.singletonMap; +import static org.assertj.core.api.Assertions.assertThat; +import static org.testcontainers.containers.localstack.LocalStackContainer.Service.SQS; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.awspring.cloud.messaging.support.config.MessageListenerContainerFactory; +import io.awspring.cloud.messaging.support.listener.AsyncErrorHandler; +import io.awspring.cloud.messaging.support.listener.AsyncMessageInterceptor; +import io.awspring.cloud.messaging.support.listener.AsyncMessageListener; +import io.awspring.cloud.messaging.support.listener.MessageHeaders; +import io.awspring.cloud.messaging.support.listener.MessageListenerContainerRegistry; +import io.awspring.cloud.messaging.support.listener.acknowledgement.AsyncAckHandler; +import io.awspring.cloud.messaging.support.listener.acknowledgement.AsyncAcknowledgement; +import io.awspring.cloud.sqs.annotation.EnableSqs; +import io.awspring.cloud.sqs.annotation.SqsListener; +import io.awspring.cloud.sqs.config.SqsConfigUtils; +import io.awspring.cloud.sqs.config.SqsMessageListenerContainerFactory; +import io.awspring.cloud.sqs.listener.AsyncMessageHandlerMessageListener; +import io.awspring.cloud.sqs.listener.SqsMessageHeaders; +import io.awspring.cloud.sqs.listener.Visibility; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.IntStream; +import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.MessageHandler; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestPropertySource; +import org.springframework.util.Assert; +import org.springframework.util.StopWatch; +import software.amazon.awssdk.services.sqs.SqsAsyncClient; +import software.amazon.awssdk.services.sqs.model.QueueAttributeName; +import software.amazon.awssdk.services.sqs.model.ReceiveMessageResponse; +import software.amazon.awssdk.services.sqs.model.SendMessageBatchRequestEntry; + +/** + * @author Tomaz Fernandes + * @since 3.0 + */ +@SpringBootTest +@DirtiesContext +@TestPropertySource(properties = { "cloud.aws.credentials.access-key=noop", "cloud.aws.credentials.secret-key=noop", + "cloud.aws.region.static=us-east-2" }) +class SqsIntegrationTests extends BaseSqsIntegrationTest { + + private static final Logger logger = LoggerFactory.getLogger(SqsIntegrationTests.class); + + private static final String TEST_SQS_ASYNC_CLIENT_BEAN_NAME = "testSqsAsyncClient"; + + @Autowired + LatchContainer latchContainer; + + @Autowired + @Qualifier(TEST_SQS_ASYNC_CLIENT_BEAN_NAME) + SqsAsyncClient sqsAsyncClient; + + @Autowired + MessageListenerContainerRegistry registry; + + @Autowired + ObjectMapper objectMapper; + + private static final String TEST_PAYLOAD = "My test"; + + @Test + void contextLoads() throws Exception { + sqsAsyncClient.createQueue(req -> req.queueName("myQueue").build()).get(); + sendMessageTo("myQueue"); + String queueUrl = fetchQueueUrl("myQueue"); + ReceiveMessageResponse response = sqsAsyncClient + .receiveMessage(rec -> rec.queueUrl(queueUrl).maxNumberOfMessages(10)).get(); + assertThat(response.messages().get(0).body()).isEqualTo(TEST_PAYLOAD); + } + + @Test + void receivesMessage() throws Exception { + sendMessageTo(RECEIVES_MESSAGE_QUEUE_NAME); + assertThat(latchContainer.receivesMessageLatch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(latchContainer.interceptorLatch.await(10, TimeUnit.SECONDS)).isTrue(); + } + + @Test + void doesNotAckOnError() throws Exception { + sendMessageTo(DOES_NOT_ACK_ON_ERROR_QUEUE_NAME); + assertThat(latchContainer.doesNotAckLatch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(latchContainer.errorHandlerLatch.await(10, TimeUnit.SECONDS)).isTrue(); + } + + @Test + void resolvesManyParameterTypes() throws Exception { + sendMessageTo(RESOLVES_PARAMETER_TYPES_QUEUE_NAME); + assertThat(latchContainer.manyParameterTypesLatch.await(10, TimeUnit.SECONDS)).isTrue(); + } + + @Test + void resolvesPojoParameterTypes() throws Exception { + sendMessageTo(RESOLVES_POJO_TYPES_QUEUE_NAME, + objectMapper.writeValueAsString(new MyPojo("firstValue", "secondValue"))); + assertThat(latchContainer.resolvesPojoLatch.await(10, TimeUnit.SECONDS)).isTrue(); + } + + final int outerBatchSize = 20; + final int innerBatchSize = 10; + final int totalMessages = 2 * outerBatchSize * innerBatchSize; + + @Test + void receivesManyFromTwoQueuesWithLoad() throws Exception { + latchContainer.manyMessagesTwoQueuesLatch = new CountDownLatch(totalMessages); + latchContainer.messageAckLatch = new CountDownLatch(totalMessages); + testWithLoad(RECEIVE_FROM_MANY_1_QUEUE_NAME, RECEIVE_FROM_MANY_2_QUEUE_NAME, + latchContainer.manyMessagesTwoQueuesLatch, latchContainer.messageAckLatch); + assertThat(latchContainer.messageAckLatch.await(60, TimeUnit.SECONDS)).isTrue(); + } + + @Test + void asyncReceivesManyFromTwoQueuesWithLoad() throws Exception { + latchContainer.asyncManyMessagesTwoQueuesLatch = new CountDownLatch(totalMessages); + latchContainer.messageAckLatchAsync = new CountDownLatch(totalMessages); + testWithLoad(ASYNC_RECEIVE_FROM_MANY_1_QUEUE_NAME, ASYNC_RECEIVE_FROM_MANY_2_QUEUE_NAME, + latchContainer.asyncManyMessagesTwoQueuesLatch, latchContainer.messageAckLatchAsync); + assertThat(latchContainer.messageAckLatchAsync.await(60, TimeUnit.SECONDS)).isTrue(); + } + + private void testWithLoad(String queue1, String queue2, CountDownLatch countDownLatch, CountDownLatch secondLatch) + throws InterruptedException, ExecutionException { + StopWatch watch = new StopWatch(); + watch.start(); + String queueUrl1 = fetchQueueUrl(queue1); + String queueUrl2 = fetchQueueUrl(queue2); + IntStream.range(0, outerBatchSize).forEach(index -> sendMessageBatch(queueUrl1, index, innerBatchSize)); + IntStream.range(outerBatchSize, outerBatchSize * 2) + .forEach(index -> sendMessageBatch(queueUrl2, index, innerBatchSize)); + assertThat(countDownLatch.await(60, TimeUnit.SECONDS)).isTrue(); + assertThat(secondLatch.await(60, TimeUnit.SECONDS)).isTrue(); + watch.stop(); + double totalTimeSeconds = watch.getTotalTimeSeconds(); + logger.info("{} seconds for sending and consuming {} messages. Messages / second: {}", totalTimeSeconds, + totalMessages, totalMessages / totalTimeSeconds); + } + + private void sendMessageBatch(String queueUrl, int parentIndex, int batchSize) { + try { + sqsAsyncClient + .sendMessageBatch( + req -> req.entries(getBatchEntries(batchSize, parentIndex)).queueUrl(queueUrl).build()) + .get(); + if (parentIndex % 5 == 0) { + logger.debug("Sent " + parentIndex * batchSize + " messages."); + } + } + catch (Exception e) { + logger.error("Error sending messages: ", e); + throw new RuntimeException(e); + } + } + + private SendMessageBatchRequestEntry[] getBatchEntries(int batchSize, int parentIndex) { + return IntStream.range(0, batchSize) + .mapToObj(index -> SendMessageBatchRequestEntry.builder().id(UUID.randomUUID().toString()) + .messageBody(TEST_PAYLOAD + " - " + ((parentIndex * 10) + index)).build()) + .toArray(SendMessageBatchRequestEntry[]::new); + } + + private void sendMessageTo(String receivesMessageQueueName) throws InterruptedException, ExecutionException { + String queueUrl = fetchQueueUrl(receivesMessageQueueName); + sqsAsyncClient.sendMessage(req -> req.messageBody(TEST_PAYLOAD).queueUrl(queueUrl).build()).get(); + } + + private void sendMessageTo(String queueName, String messageBody) throws InterruptedException, ExecutionException { + String queueUrl = fetchQueueUrl(queueName); + sqsAsyncClient.sendMessage(req -> req.messageBody(messageBody).queueUrl(queueUrl).build()).get(); + logger.debug("Sent message to queue {} with messageBody {}", queueName, messageBody); + } + + private String fetchQueueUrl(String receivesMessageQueueName) throws InterruptedException, ExecutionException { + return sqsAsyncClient.getQueueUrl(req -> req.queueName(receivesMessageQueueName)).get().queueUrl(); + } + + static class ReceivesMessageListener { + + @Autowired + LatchContainer latchContainer; + + @SqsListener(queueNames = RECEIVES_MESSAGE_QUEUE_NAME) + void listen(String message) { + logger.debug("Received message in Listener Method: " + message); + latchContainer.receivesMessageLatch.countDown(); + } + } + + static class DoesNotAckOnErrorListener { + + @Autowired + LatchContainer latchContainer; + + @SqsListener(queueNames = DOES_NOT_ACK_ON_ERROR_QUEUE_NAME, factory = "lowResourceFactory") + void listen(String message) { + logger.debug("Received message in Listener Method: " + message); + latchContainer.doesNotAckLatch.countDown(); + throw new RuntimeException("Expected exception"); + } + } + + static class ResolvesParameterTypesListener { + + @Autowired + LatchContainer latchContainer; + + @SqsListener(queueNames = RESOLVES_PARAMETER_TYPES_QUEUE_NAME, minSecondsToProcess = "20") + void listen(String message, SqsMessageHeaders headers, AsyncAcknowledgement ack, Visibility visibility, + software.amazon.awssdk.services.sqs.model.Message originalMessage) { + Assert.notNull(headers, "Received null SqsMessageHeaders"); + Assert.notNull(ack, "Received null AsyncAcknowledgement"); + Assert.notNull(visibility, "Received null Visibility"); + Assert.notNull(originalMessage, "Received null software.amazon.awssdk.services.sqs.model.Message"); + Assert.notNull(message, "Received null message"); + logger.debug("Received message in Listener Method: " + message); + latchContainer.manyParameterTypesLatch.countDown(); + } + } + + static class ResolvesPojoListener { + + @Autowired + LatchContainer latchContainer; + + @SqsListener(queueNames = RESOLVES_POJO_TYPES_QUEUE_NAME, concurrentPollsPerContainer = "1") + void listen(MyPojo pojo) { + Assert.notNull(pojo, "Received null message"); + logger.debug("Received message in Listener Method: " + pojo); + latchContainer.resolvesPojoLatch.countDown(); + } + } + + static class ReceiveManyFromTwoQueuesListener { + + @Autowired + LatchContainer latchContainer; + + AtomicInteger messagesReceived = new AtomicInteger(); + + @SqsListener(queueNames = { RECEIVE_FROM_MANY_1_QUEUE_NAME, + RECEIVE_FROM_MANY_2_QUEUE_NAME }, factory = "highThroughputFactory") + void listen(String message) throws Exception { + logger.debug("Started processing " + message); + Thread.sleep(1000); + int count; + if ((count = messagesReceived.incrementAndGet()) % 50 == 0) { + logger.debug("Listener processed {} messages", count); + } + latchContainer.manyMessagesTwoQueuesLatch.countDown(); + logger.debug("Finished processing " + message); + } + } + + static class AsyncReceiveManyFromTwoQueuesListener { + + @Autowired + LatchContainer latchContainer; + + AtomicInteger messagesReceived = new AtomicInteger(); + + ThreadPoolTaskExecutor taskExecutor = getTaskExecutor(); + + private ThreadPoolTaskExecutor getTaskExecutor() { + ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); + taskExecutor.initialize(); + taskExecutor.setCorePoolSize(25); + taskExecutor.setMaxPoolSize(25); + return taskExecutor; + } + + @SqsListener(queueNames = { ASYNC_RECEIVE_FROM_MANY_1_QUEUE_NAME, + ASYNC_RECEIVE_FROM_MANY_2_QUEUE_NAME }, factory = "lowResourceFactory") + CompletableFuture listenAsync(String message) { + logger.debug("Received message {}", message); + return CompletableFuture.supplyAsync(() -> processMessage(message), this.taskExecutor); + } + + @Nullable + private Void processMessage(String message) { + try { + logger.debug("Started processing " + message); + Thread.sleep(1000); + logEvery50messages(); + latchContainer.asyncManyMessagesTwoQueuesLatch.countDown(); + logger.debug("Finished processing " + message); + return null; + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + logger.error("Interrupted while sleeping", e); + throw new RuntimeException(e); + } + catch (Exception e) { + logger.error("Error in listener", e); + throw new RuntimeException(e); + } + } + + private void logEvery50messages() { + int count; + if ((count = messagesReceived.incrementAndGet()) % 50 == 0) { + logger.debug("Listener processed {} messages", count); + } + } + } + + static class LatchContainer { + + final CountDownLatch receivesMessageLatch = new CountDownLatch(1); + final CountDownLatch doesNotAckLatch = new CountDownLatch(2); + final CountDownLatch errorHandlerLatch = new CountDownLatch(2); + final CountDownLatch interceptorLatch = new CountDownLatch(1); + final CountDownLatch manyParameterTypesLatch = new CountDownLatch(1); + final CountDownLatch resolvesPojoLatch = new CountDownLatch(1); + // Lazily initialized + CountDownLatch manyMessagesTwoQueuesLatch = new CountDownLatch(1); + CountDownLatch asyncManyMessagesTwoQueuesLatch = new CountDownLatch(1); + CountDownLatch messageAckLatch = new CountDownLatch(1); + CountDownLatch messageAckLatchAsync = new CountDownLatch(1); + + } + + @EnableSqs + @Configuration + static class SQSConfiguration { + + // TODO: Probably move some of this to auto configuration with @ConditionalOnMissingBean + @Bean(name = SqsConfigUtils.SQS_ASYNC_CLIENT_BEAN_NAME) + SqsAsyncClient sqsAsyncClientConsumer() throws Exception { + SqsAsyncClient asyncClient = SqsAsyncClient.builder().endpointOverride(localstack.getEndpointOverride(SQS)) + .build(); + createQueues(asyncClient); + return asyncClient; + } + + @Bean + public MessageListenerContainerFactory defaultListenerContainerFactory() { + return new SqsMessageListenerContainerFactory(); + } + + @Bean + public MessageListenerContainerFactory highThroughputFactory() { + return new SqsMessageListenerContainerFactory( + withOptions().concurrentWorkersPerContainer(25).concurrentPollsPerContainer(10).messagesPerPoll(10) + .pollingTimeoutSeconds(1).errorHandler(testErrorHandler()).ackHandler(testAckHandler())); + } + + @Bean + public MessageListenerContainerFactory lowResourceFactory() { + return new SqsMessageListenerContainerFactory(withOptions().concurrentWorkersPerContainer(3) + .concurrentPollsPerContainer(10).interceptor(testInterceptor()).messagesPerPoll(10) + .ackHandler(testAckHandler()).errorHandler(testErrorHandler()).pollingTimeoutSeconds(3)); + } + + @Bean(name = SqsConfigUtils.SQS_ASYNC_LISTENER_BEAN_NAME) + AsyncMessageListener asyncMessageListener(MessageHandler messageHandler) { + return new AsyncMessageHandlerMessageListener<>(messageHandler); + } + + LatchContainer latchContainer = new LatchContainer(); + + @Bean + ReceivesMessageListener receivesMessageListener() { + return new ReceivesMessageListener(); + } + + @Bean + DoesNotAckOnErrorListener doesNotAckOnErrorListener() { + return new DoesNotAckOnErrorListener(); + } + + @Bean + ResolvesParameterTypesListener resolvesParameterTypesListener() { + return new ResolvesParameterTypesListener(); + } + + @Bean + ResolvesPojoListener resolvesPojoListener() { + return new ResolvesPojoListener(); + } + + @Bean + ReceiveManyFromTwoQueuesListener receiveManyFromTwoQueuesListener() { + return new ReceiveManyFromTwoQueuesListener(); + } + + @Bean + AsyncReceiveManyFromTwoQueuesListener asyncReceiveManyFromTwoQueuesListener() { + return new AsyncReceiveManyFromTwoQueuesListener(); + } + + @Bean + LatchContainer latchContainer() { + return this.latchContainer; + } + + @Bean + ObjectMapper objectMapper() { + return new ObjectMapper(); + } + + @Bean(name = TEST_SQS_ASYNC_CLIENT_BEAN_NAME) + SqsAsyncClient sqsAsyncClientProducer() { + return SqsAsyncClient.builder().endpointOverride(localstack.getEndpointOverride(SQS)).build(); + } + + private void createQueues(SqsAsyncClient client) throws InterruptedException, ExecutionException { + CompletableFuture.allOf( + client.createQueue(req -> req.queueName(RECEIVES_MESSAGE_QUEUE_NAME) + .attributes(singletonMap(QueueAttributeName.VISIBILITY_TIMEOUT, "1")).build()), + client.createQueue(req -> req.queueName(DOES_NOT_ACK_ON_ERROR_QUEUE_NAME) + .attributes(singletonMap(QueueAttributeName.VISIBILITY_TIMEOUT, "1")).build()), + client.createQueue(req -> req.queueName(RECEIVE_FROM_MANY_1_QUEUE_NAME) + .attributes(singletonMap(QueueAttributeName.VISIBILITY_TIMEOUT, "1")).build()), + client.createQueue(req -> req.queueName(RECEIVE_FROM_MANY_2_QUEUE_NAME) + .attributes(singletonMap(QueueAttributeName.VISIBILITY_TIMEOUT, "1")).build()), + client.createQueue(req -> req.queueName(RESOLVES_PARAMETER_TYPES_QUEUE_NAME).build()), + client.createQueue(req -> req.queueName(RESOLVES_POJO_TYPES_QUEUE_NAME).build()), + client.createQueue(req -> req.queueName(ASYNC_RECEIVE_FROM_MANY_1_QUEUE_NAME).build()), + client.createQueue(req -> req.queueName(ASYNC_RECEIVE_FROM_MANY_2_QUEUE_NAME).build())).get(); + } + + private AsyncMessageInterceptor testInterceptor() { + return msg -> { + latchContainer.interceptorLatch.countDown(); + return CompletableFuture.completedFuture(msg); + }; + } + + private AsyncErrorHandler testErrorHandler() { + return (msg, t) -> { + logger.error("Error processing msg {}", msg, t); + latchContainer.errorHandlerLatch.countDown(); + // Eventually ack to not interfere with other tests. + if (latchContainer.errorHandlerLatch.getCount() == 0) { + Objects.requireNonNull( + (AsyncAcknowledgement) msg.getHeaders().get(MessageHeaders.ACKNOWLEDGMENT_HEADER), + "No acknowledgement present").acknowledge(); + } + return CompletableFuture.completedFuture(null); + }; + } + + private AsyncAckHandler testAckHandler() { + return msg -> { + latchContainer.messageAckLatch.countDown(); + latchContainer.messageAckLatchAsync.countDown(); + return Objects + .requireNonNull( + (AsyncAcknowledgement) msg.getHeaders().get(MessageHeaders.ACKNOWLEDGMENT_HEADER)) + .acknowledge(); + }; + } + } + + static class MyPojo { + + String firstField; + String secondField; + + MyPojo(String firstField, String secondField) { + this.firstField = firstField; + this.secondField = secondField; + } + + MyPojo() { + } + + public String getFirstField() { + return firstField; + } + + public void setFirstField(String firstField) { + this.firstField = firstField; + } + + public String getSecondField() { + return secondField; + } + + public void setSecondField(String secondField) { + this.secondField = secondField; + } + } + +} diff --git a/spring-cloud-aws-sqs/src/test/resources/logback.xml b/spring-cloud-aws-sqs/src/test/resources/logback.xml new file mode 100644 index 000000000..a8a4dfa16 --- /dev/null +++ b/spring-cloud-aws-sqs/src/test/resources/logback.xml @@ -0,0 +1,32 @@ + + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + From db8147fe2ce3033f3f1d12d7df24e0bf5f803050 Mon Sep 17 00:00:00 2001 From: Tomaz Fernandes Date: Fri, 6 May 2022 12:02:23 -0300 Subject: [PATCH 2/5] Remove retries for fetching queue attributes --- .../invocation/EndpointMessageHandler.java | 36 ++++++------------- 1 file changed, 10 insertions(+), 26 deletions(-) diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/invocation/EndpointMessageHandler.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/invocation/EndpointMessageHandler.java index 3fffed2ba..ece1b13b7 100644 --- a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/invocation/EndpointMessageHandler.java +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/invocation/EndpointMessageHandler.java @@ -38,6 +38,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.CompletionStage; +import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.config.BeanExpressionContext; @@ -161,7 +162,7 @@ protected SqsEndpoint getMappingForMethod(Method method, Class handlerType) { .minTimeToProcess(resolveInteger(sqsListenerAnnotation.minSecondsToProcess())) .async(CompletionStage.class.isAssignableFrom(method.getReturnType())) .queuesAttributes(logicalEndpointNames.stream() - .collect(Collectors.toMap(name -> name, this::queueAttributes))) + .collect(Collectors.toMap(name -> name, this::getQueueAttributes))) .build(); } @@ -211,39 +212,22 @@ else if (result != null) { } } - private QueueAttributes queueAttributes(String queue) { - return queueAttributes(queue, 10); - } - - private QueueAttributes queueAttributes(String queue, int attemptsLeft) { + private QueueAttributes getQueueAttributes(String queue) { try { logger.debug("Fetching queue attributes for queue " + queue); - String queueUrl = this.sqsAsyncClient.getQueueUrl(req -> req.queueName(queue)).get().queueUrl(); Map attributes = this.sqsAsyncClient - .getQueueAttributes(req -> req.queueUrl(queueUrl)).get().attributes(); - + .getQueueAttributes(req -> req.queueUrl(queueUrl)).get().attributes(); boolean hasRedrivePolicy = attributes.containsKey(QueueAttributeName.REDRIVE_POLICY); boolean isFifo = queue.endsWith(".fifo"); return new QueueAttributes(queueUrl, hasRedrivePolicy, getVisibility(attributes), isFifo); } - catch (Exception e) { - logger.warn( - String.format("Could not retrieve attributes for queue %s. Attempts left: %s", queue, attemptsLeft), - e); - try { - if (attemptsLeft == 0) { - throw new IllegalStateException("Could not retrieve properties for queue " + queue, e); - } - Thread.sleep(8000 / attemptsLeft); - return queueAttributes(queue, attemptsLeft - 1); - } - catch (InterruptedException ex) { - throw new IllegalStateException("Interrupted while retrieving attributes for queue " + queue, ex); - } - catch (Exception ex) { - throw new IllegalStateException("Could not retrieve properties for queue " + queue, ex); - } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("Interrupted while fetching attributes for queue " + queue); + } + catch (ExecutionException e) { + throw new IllegalStateException("ExecutionException while fetching attributes for queue " + queue); } } From 9721fbe644db37afd62f92af51f707d00a2155d0 Mon Sep 17 00:00:00 2001 From: Tomaz Fernandes Date: Fri, 6 May 2022 12:32:18 -0300 Subject: [PATCH 3/5] Address Sonar issues --- .../MessageListenerContainerFactory.java | 2 +- .../support/config/MessagingConfigUtils.java | 3 +++ .../endpoint/DefaultEndpointProcessor.java | 8 ++----- .../AbstractMessageListenerContainer.java | 16 ++++++-------- .../DefaultListenerContainerRegistry.java | 7 ++++--- .../support/listener/MessageHeaders.java | 3 +++ .../listener/MessageListenerContainer.java | 2 +- .../MessageListenerContainerRegistry.java | 4 ++-- .../acknowledgement/AsyncAckHandler.java | 4 ++-- .../acknowledgement/AsyncAcknowledgement.java | 2 +- .../acknowledgement/OnSuccessAckHandler.java | 2 +- .../cloud/sqs/config/SqsConfigUtils.java | 3 +++ .../SqsMessageListenerContainerFactory.java | 2 +- .../invocation/EndpointMessageHandler.java | 21 +------------------ .../AsyncMessageHandlerMessageListener.java | 9 +++++--- .../sqs/listener/QueueMessageVisibility.java | 7 +++---- .../cloud/sqs/listener/SqsAcknowledge.java | 10 +++++++-- .../listener/SqsMessageListenerContainer.java | 6 +++--- .../sqs/listener/SqsMessageProducer.java | 7 +++---- .../cloud/sqs/listener/Visibility.java | 2 +- .../cloud/sqs/SqsIntegrationTests.java | 7 +++++-- 21 files changed, 60 insertions(+), 67 deletions(-) diff --git a/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/config/MessageListenerContainerFactory.java b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/config/MessageListenerContainerFactory.java index 1a3f38a32..60913925a 100644 --- a/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/config/MessageListenerContainerFactory.java +++ b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/config/MessageListenerContainerFactory.java @@ -22,7 +22,7 @@ * @author Tomaz Fernandes * @since 3.0 */ -public interface MessageListenerContainerFactory, E extends Endpoint> { +public interface MessageListenerContainerFactory { C create(E endpoint); diff --git a/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/config/MessagingConfigUtils.java b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/config/MessagingConfigUtils.java index fd2fcb5f7..fe2d8d807 100644 --- a/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/config/MessagingConfigUtils.java +++ b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/config/MessagingConfigUtils.java @@ -21,6 +21,9 @@ */ public class MessagingConfigUtils { + private MessagingConfigUtils() { + } + public static final String DEFAULT_LISTENER_CONTAINER_FACTORY_BEAN_NAME = "defaultListenerContainerFactory"; public static final String ENDPOINT_REGISTRY_BEAN_NAME = "io.awspring.cloud.messaging.internalEndpointRegistryBeanName"; diff --git a/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/endpoint/DefaultEndpointProcessor.java b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/endpoint/DefaultEndpointProcessor.java index ff541e366..e9991ccd1 100644 --- a/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/endpoint/DefaultEndpointProcessor.java +++ b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/endpoint/DefaultEndpointProcessor.java @@ -36,10 +36,6 @@ public class DefaultEndpointProcessor implements EndpointProcessor, BeanFactoryA private static final Logger logger = LoggerFactory.getLogger(DefaultEndpointProcessor.class); - public static final String DEFAULT_LISTENER_CONTAINER_FACTORY_BEAN_NAME = "defaultListenerContainerFactory"; - - public static final String ENDPOINT_REGISTRY_BEAN_NAME = "defaultEndpointRegistry"; - private BeanFactory beanFactory; private MessageListenerContainerRegistry listenerContainerRegistry; @@ -58,12 +54,12 @@ public void afterSingletonsInstantiated() { @Override public void process(Endpoint endpoint) { - logger.debug("Processing endpoint: " + endpoint); + logger.debug("Processing endpoint {}", endpoint); this.listenerContainerRegistry.registerListenerContainer(createContainerFor(endpoint)); } @SuppressWarnings("unchecked") - private MessageListenerContainer createContainerFor(Endpoint endpoint) { + private MessageListenerContainer createContainerFor(Endpoint endpoint) { String factoryBeanName = getListenerContainerFactoryName(endpoint); Assert.isTrue(this.beanFactory.containsBean(factoryBeanName), () -> "No bean with name " + factoryBeanName + " found for MessageListenerContainerFactory."); diff --git a/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/AbstractMessageListenerContainer.java b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/AbstractMessageListenerContainer.java index a1ee1a3ae..91c391445 100644 --- a/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/AbstractMessageListenerContainer.java +++ b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/AbstractMessageListenerContainer.java @@ -34,7 +34,7 @@ * @author Tomaz Fernandes * @since 3.0 */ -public abstract class AbstractMessageListenerContainer implements MessageListenerContainer, InitializingBean { +public abstract class AbstractMessageListenerContainer implements MessageListenerContainer, InitializingBean { private static final Logger logger = LoggerFactory.getLogger(AbstractMessageListenerContainer.class); @@ -58,7 +58,7 @@ public abstract class AbstractMessageListenerContainer implements MessageList private AsyncMessageInterceptor messageInterceptor = null; - public AbstractMessageListenerContainer(AbstractContainerOptions options, TaskExecutor taskExecutor, + protected AbstractMessageListenerContainer(AbstractContainerOptions options, TaskExecutor taskExecutor, AsyncMessageListener messageListener, Collection> producers) { this.messageListener = messageListener; handleCallbackListener(messageListener); @@ -128,7 +128,7 @@ private void produceAndProcessMessages() { } protected CompletableFuture splitAndProcessMessages(Collection> messages) { - logger.trace("Received {} messages in Thread {}", messages.size(), threadName()); + logger.trace("Received {} messages in Thread {}", messages.size(), Thread.currentThread().getName()); return CompletableFuture .allOf(messages.stream().map(this::processMessageAsync).toArray(CompletableFuture[]::new)); } @@ -138,7 +138,7 @@ protected CompletableFuture processMessageAsync(Message msg) { } protected CompletableFuture processMessage(Message message) { - logger.debug("Processing message {} in thread {}", message, threadName()); + logger.debug("Processing message {} in thread {}", message, Thread.currentThread().getName()); CompletableFuture messageListenerResult = maybeIntercept(message, this.messageListener::onMessage); return this.messageListener instanceof CallbackMessageListener ? CompletableFuture.completedFuture(null) : messageListenerResult.handle((val, t) -> handleResult(message, t)); @@ -156,19 +156,15 @@ private CompletableFuture maybeIntercept(Message message, : listener.apply(message); } - private String threadName() { - return Thread.currentThread().getName(); - } - private void acquireSemaphore() throws InterruptedException { producersSemaphore.acquire(); - logger.trace("Semaphore acquired for producer {} in thread {} ", this, threadName()); + logger.trace("Semaphore acquired for producer {} in thread {} ", this, Thread.currentThread().getName()); } private Runnable releaseSemaphore() { return () -> { this.producersSemaphore.release(); - logger.trace("Semaphore released for producer {} in thread {} ", this, threadName()); + logger.trace("Semaphore released for producer {} in thread {} ", this, Thread.currentThread().getName()); }; } diff --git a/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/DefaultListenerContainerRegistry.java b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/DefaultListenerContainerRegistry.java index b4939e68c..cddbabb90 100644 --- a/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/DefaultListenerContainerRegistry.java +++ b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/DefaultListenerContainerRegistry.java @@ -19,6 +19,7 @@ import java.util.Collection; import java.util.Collections; import java.util.List; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -30,20 +31,20 @@ public class DefaultListenerContainerRegistry implements MessageListenerContaine private static final Logger logger = LoggerFactory.getLogger(DefaultListenerContainerRegistry.class); - private final List> listenerContainers = new ArrayList<>(); + private final List listenerContainers = new ArrayList<>(); private final Object lifecycleMonitor = new Object(); private volatile boolean running = false; @Override - public void registerListenerContainer(MessageListenerContainer listenerContainer) { + public void registerListenerContainer(MessageListenerContainer listenerContainer) { logger.debug("Registering listener container {}", listenerContainer); this.listenerContainers.add(listenerContainer); } @Override - public Collection> retrieveListenerContainers() { + public Collection retrieveListenerContainers() { return Collections.unmodifiableList(this.listenerContainers); } diff --git a/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/MessageHeaders.java b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/MessageHeaders.java index 9b1339f01..52e138cec 100644 --- a/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/MessageHeaders.java +++ b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/MessageHeaders.java @@ -21,6 +21,9 @@ */ public class MessageHeaders { + private MessageHeaders() { + } + public static final String ACKNOWLEDGMENT_HEADER = "acknowledgement"; } diff --git a/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/MessageListenerContainer.java b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/MessageListenerContainer.java index 2bce47ae7..03c8fb389 100644 --- a/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/MessageListenerContainer.java +++ b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/MessageListenerContainer.java @@ -21,6 +21,6 @@ * @author Tomaz Fernandes * @since 3.0 */ -public interface MessageListenerContainer extends SmartLifecycle { +public interface MessageListenerContainer extends SmartLifecycle { } diff --git a/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/MessageListenerContainerRegistry.java b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/MessageListenerContainerRegistry.java index 619d6345d..229462684 100644 --- a/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/MessageListenerContainerRegistry.java +++ b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/MessageListenerContainerRegistry.java @@ -24,8 +24,8 @@ */ public interface MessageListenerContainerRegistry extends SmartLifecycle { - void registerListenerContainer(MessageListenerContainer listenerContainer); + void registerListenerContainer(MessageListenerContainer listenerContainer); - Collection> retrieveListenerContainers(); + Collection retrieveListenerContainers(); } diff --git a/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/acknowledgement/AsyncAckHandler.java b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/acknowledgement/AsyncAckHandler.java index 183acb586..67925b851 100644 --- a/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/acknowledgement/AsyncAckHandler.java +++ b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/acknowledgement/AsyncAckHandler.java @@ -24,9 +24,9 @@ */ public interface AsyncAckHandler { - CompletableFuture onSuccess(Message message); + CompletableFuture onSuccess(Message message); - default CompletableFuture onError(Message message, Throwable t) { + default CompletableFuture onError(Message message, Throwable t) { return CompletableFuture.completedFuture(null); } } diff --git a/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/acknowledgement/AsyncAcknowledgement.java b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/acknowledgement/AsyncAcknowledgement.java index 9f8d6928c..6c3e4fecd 100644 --- a/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/acknowledgement/AsyncAcknowledgement.java +++ b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/acknowledgement/AsyncAcknowledgement.java @@ -23,6 +23,6 @@ */ public interface AsyncAcknowledgement { - CompletableFuture acknowledge(); + CompletableFuture acknowledge(); } diff --git a/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/acknowledgement/OnSuccessAckHandler.java b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/acknowledgement/OnSuccessAckHandler.java index 1d858e9f2..3ccc6661c 100644 --- a/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/acknowledgement/OnSuccessAckHandler.java +++ b/spring-cloud-aws-messaging-support/src/main/java/io/awspring/cloud/messaging/support/listener/acknowledgement/OnSuccessAckHandler.java @@ -32,7 +32,7 @@ public class OnSuccessAckHandler implements AsyncAckHandler { @SuppressWarnings("unchecked") @Override - public CompletableFuture onSuccess(Message message) { + public CompletableFuture onSuccess(Message message) { logger.trace("Acknowledging message " + message); Object ackObject = message.getHeaders().get(MessageHeaders.ACKNOWLEDGMENT_HEADER); Assert.notNull(ackObject, () -> "No acknowledgment found for " + message); diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/config/SqsConfigUtils.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/config/SqsConfigUtils.java index 82d0ce558..6f2095be4 100644 --- a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/config/SqsConfigUtils.java +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/config/SqsConfigUtils.java @@ -21,6 +21,9 @@ */ public class SqsConfigUtils { + private SqsConfigUtils() { + } + public static final String SQS_ASYNC_CLIENT_BEAN_NAME = "io.awspring.cloud.sqs.internalSqsAsyncClient"; public static final String SQS_ASYNC_LISTENER_BEAN_NAME = "io.awspring.cloud.sqs.internalSqsAsyncListener"; diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/config/SqsMessageListenerContainerFactory.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/config/SqsMessageListenerContainerFactory.java index f065090e5..d85c6a502 100644 --- a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/config/SqsMessageListenerContainerFactory.java +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/config/SqsMessageListenerContainerFactory.java @@ -92,7 +92,7 @@ public void setSqsAsyncClient(SqsAsyncClient sqsAsyncClient) { @Override protected void initializeContainer(SqsMessageListenerContainer container) { - logger.debug("Initializing container: " + container); + logger.debug("Initializing container {}", container); container.afterPropertiesSet(); } diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/invocation/EndpointMessageHandler.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/invocation/EndpointMessageHandler.java index ece1b13b7..66ffab376 100644 --- a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/invocation/EndpointMessageHandler.java +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/invocation/EndpointMessageHandler.java @@ -65,8 +65,6 @@ import org.springframework.messaging.handler.invocation.HandlerMethodReturnValueHandler; import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; import org.springframework.util.StringUtils; -import org.springframework.validation.Errors; -import org.springframework.validation.Validator; import software.amazon.awssdk.services.sqs.SqsAsyncClient; import software.amazon.awssdk.services.sqs.model.QueueAttributeName; @@ -80,7 +78,7 @@ * @since 1.0 */ public class EndpointMessageHandler extends AbstractMethodMessageHandler - implements EndpointRegistry, DisposableBean { + implements EndpointRegistry { private final List messageConverters; @@ -280,21 +278,4 @@ public Collection retrieveEndpoints() { return Collections.unmodifiableSet(super.getHandlerMethods().keySet()); } - @Override - public void destroy() throws Exception { - logger.error("Destroying"); - } - - private static final class NoOpValidator implements Validator { - - @Override - public boolean supports(Class clazz) { - return false; - } - - @Override - public void validate(Object target, Errors errors) { - } - - } } diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/AsyncMessageHandlerMessageListener.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/AsyncMessageHandlerMessageListener.java index a4feeca1e..a915f34d1 100644 --- a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/AsyncMessageHandlerMessageListener.java +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/AsyncMessageHandlerMessageListener.java @@ -27,6 +27,7 @@ import org.springframework.messaging.MessageHandler; import org.springframework.messaging.handler.invocation.AbstractMethodMessageHandler; import org.springframework.messaging.handler.invocation.HandlerMethodReturnValueHandler; +import org.springframework.util.Assert; /** * @author Tomaz Fernandes @@ -45,11 +46,13 @@ public AsyncMessageHandlerMessageListener(MessageHandler messageHandler) { @SuppressWarnings("unchecked") @Override public void addResultCallback(BiFunction, Throwable, CompletableFuture> callback) { - AbstractMethodMessageHandler messageHandler = (AbstractMethodMessageHandler) this.messageHandler; + Assert.isInstanceOf(AbstractMethodMessageHandler.class, this.messageHandler, () -> "MessageHandler must be an instance of" + + "AbstractMethodMessageHandler in order to enable callbacks"); + AbstractMethodMessageHandler abstractHandler = (AbstractMethodMessageHandler) this.messageHandler; List returnValueHandlers = new ArrayList<>( - messageHandler.getReturnValueHandlers()); + abstractHandler.getReturnValueHandlers()); returnValueHandlers.add(new CallbackFutureReturnValueHandler<>(callback)); - messageHandler.setReturnValueHandlers(returnValueHandlers); + abstractHandler.setReturnValueHandlers(returnValueHandlers); } @Override diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/QueueMessageVisibility.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/QueueMessageVisibility.java index c3f09b6bb..e4cee1dce 100644 --- a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/QueueMessageVisibility.java +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/QueueMessageVisibility.java @@ -42,10 +42,9 @@ public QueueMessageVisibility(SqsAsyncClient amazonSqsAsync, String queueUrl, St } @Override - public CompletableFuture changeTo(int seconds) { - logger.debug("Changing visibility of message {} to {}", this.receiptHandle, seconds); + public CompletableFuture changeTo(int seconds) { return this.sqsAsyncClient.changeMessageVisibility( - req -> req.queueUrl(this.queueUrl).receiptHandle(this.receiptHandle).visibilityTimeout(seconds)); + req -> req.queueUrl(this.queueUrl).receiptHandle(this.receiptHandle).visibilityTimeout(seconds)) + .thenRun(() -> logger.trace("Changed the visibility of message {} to {} seconds", this.receiptHandle, seconds)); } - } diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/SqsAcknowledge.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/SqsAcknowledge.java index 371e4e4b1..1cdb2bf7f 100644 --- a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/SqsAcknowledge.java +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/SqsAcknowledge.java @@ -17,6 +17,9 @@ import io.awspring.cloud.messaging.support.listener.acknowledgement.AsyncAcknowledgement; import java.util.concurrent.CompletableFuture; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import software.amazon.awssdk.services.sqs.SqsAsyncClient; /** @@ -25,6 +28,8 @@ */ public class SqsAcknowledge implements AsyncAcknowledgement { + private static final Logger logger = LoggerFactory.getLogger(SqsAcknowledge.class); + private final SqsAsyncClient sqsAsyncClient; private final String queueUrl; @@ -38,7 +43,8 @@ public SqsAcknowledge(SqsAsyncClient sqsAsyncClient, String queueUrl, String rec } @Override - public CompletableFuture acknowledge() { - return this.sqsAsyncClient.deleteMessage(req -> req.queueUrl(this.queueUrl).receiptHandle(this.receiptHandle)); + public CompletableFuture acknowledge() { + return this.sqsAsyncClient.deleteMessage(req -> req.queueUrl(this.queueUrl).receiptHandle(this.receiptHandle)) + .thenRun(() -> logger.trace("Acknowledged message with handle {} from queue {}", this.receiptHandle, this.queueUrl)); } } diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/SqsMessageListenerContainer.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/SqsMessageListenerContainer.java index 823e90a96..8dd9af93f 100644 --- a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/SqsMessageListenerContainer.java +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/SqsMessageListenerContainer.java @@ -31,7 +31,7 @@ */ public class SqsMessageListenerContainer extends AbstractMessageListenerContainer { - private final static Logger logger = LoggerFactory.getLogger(SqsMessageListenerContainer.class); + private static final Logger logger = LoggerFactory.getLogger(SqsMessageListenerContainer.class); public SqsMessageListenerContainer(SqsContainerOptions options, SqsAsyncClient sqsClient, AsyncMessageListener messageListener, TaskExecutor taskExecutor) { @@ -52,13 +52,13 @@ private static SqsMessageProducer createMessageProducer(SqsAsyncClient sqsClient @Override protected void doStart() { - logger.debug("Starting SqsMessageListenerContainer: " + this); + logger.debug("Starting SqsMessageListenerContainer {}", this); super.getMessageProducers().stream().map(SqsMessageProducer.class::cast).forEach(SqsMessageProducer::start); } @Override protected void doStop() { - logger.debug("Stopping SqsMessageListenerContainer: " + this); + logger.debug("Stopping SqsMessageListenerContainer {}", this); super.getMessageProducers().stream().map(SqsMessageProducer.class::cast).forEach(SqsMessageProducer::stop); } } diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/SqsMessageProducer.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/SqsMessageProducer.java index 2705a46fd..53de5d42a 100644 --- a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/SqsMessageProducer.java +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/SqsMessageProducer.java @@ -65,7 +65,7 @@ public SqsMessageProducer(String logicalEndpointName, QueueAttributes queueAttri @Override public CompletableFuture>> produce(int numberOfMessages, Duration timeout) { - logger.trace("Polling for messages at " + this.queueUrl); + logger.trace("Polling for messages at {}", this.queueUrl); return sqsAsyncClient .receiveMessage(req -> req.queueUrl(this.queueUrl).maxNumberOfMessages(numberOfMessages) .waitTimeSeconds((int) timeout.getSeconds())) @@ -143,14 +143,13 @@ private static Map getAttributesAsMessageHeaders( @Override public void start() { - logger.debug("Starting SqsMessageProducer for " + this.logicalEndpointName); - // Synchronization not necessary as field is volatile + logger.debug("Starting SqsMessageProducer for {}", this.logicalEndpointName); this.running = true; } @Override public void stop() { - logger.debug("Stopping SqsMessageProducer for " + this.logicalEndpointName); + logger.debug("Stopping SqsMessageProducer for {}", this.logicalEndpointName); this.running = false; } diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/Visibility.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/Visibility.java index 2a249eb5b..306fc8795 100644 --- a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/Visibility.java +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/Visibility.java @@ -34,6 +34,6 @@ public interface Visibility { * @param seconds number of seconds to extend the visibility timeout by * @return a {@link Future} as the extension can involve some asynchronous request (i.e. request to an AWS API). */ - CompletableFuture changeTo(int seconds); + CompletableFuture changeTo(int seconds); } diff --git a/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/SqsIntegrationTests.java b/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/SqsIntegrationTests.java index 75fd7f2c9..08e10c1af 100644 --- a/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/SqsIntegrationTests.java +++ b/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/SqsIntegrationTests.java @@ -130,10 +130,13 @@ void resolvesPojoParameterTypes() throws Exception { assertThat(latchContainer.resolvesPojoLatch.await(10, TimeUnit.SECONDS)).isTrue(); } - final int outerBatchSize = 20; - final int innerBatchSize = 10; + final int outerBatchSize = 1; //20; + final int innerBatchSize = 1; //10; final int totalMessages = 2 * outerBatchSize * innerBatchSize; + // These tests are really only for us to have some indication on how the system performs under load. + // We can probably remove them later, or adapt to only make sure it handles more than one queue. + @Test void receivesManyFromTwoQueuesWithLoad() throws Exception { latchContainer.manyMessagesTwoQueuesLatch = new CountDownLatch(totalMessages); From 77e1f7d5bb210fb448bf71148eb534ee8da8306f Mon Sep 17 00:00:00 2001 From: Tomaz Fernandes Date: Fri, 6 May 2022 15:57:35 -0300 Subject: [PATCH 4/5] Update test properties --- .../java/io/awspring/cloud/sqs/BaseSqsIntegrationTest.java | 4 ++-- .../test/java/io/awspring/cloud/sqs/SqsIntegrationTests.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/BaseSqsIntegrationTest.java b/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/BaseSqsIntegrationTest.java index a6b2725d4..e93696740 100644 --- a/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/BaseSqsIntegrationTest.java +++ b/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/BaseSqsIntegrationTest.java @@ -40,7 +40,7 @@ abstract class BaseSqsIntegrationTest { @Container static LocalStackContainer localstack = new LocalStackContainer( - DockerImageName.parse("localstack/localstack:0.14.0")).withServices(SQS).withReuse(false); + DockerImageName.parse("localstack/localstack:0.14.0")).withServices(SQS).withReuse(true); @BeforeAll static void beforeAll() throws IOException, InterruptedException { @@ -57,7 +57,7 @@ static void beforeAll() throws IOException, InterruptedException { @DynamicPropertySource static void registerSqsProperties(DynamicPropertyRegistry registry) { // overwrite SQS endpoint with one provided by Localstack - registry.add("cloud.aws.sqs.endpoint", () -> localstack.getEndpointOverride(SQS).toString()); + registry.add("spring.cloud.aws.endpoint", () -> localstack.getEndpointOverride(SQS).toString()); } } diff --git a/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/SqsIntegrationTests.java b/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/SqsIntegrationTests.java index 08e10c1af..8fdf57d5b 100644 --- a/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/SqsIntegrationTests.java +++ b/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/SqsIntegrationTests.java @@ -70,8 +70,8 @@ */ @SpringBootTest @DirtiesContext -@TestPropertySource(properties = { "cloud.aws.credentials.access-key=noop", "cloud.aws.credentials.secret-key=noop", - "cloud.aws.region.static=us-east-2" }) +@TestPropertySource(properties = { "spring.cloud.aws.credentials.access-key=noop", "spring.cloud.aws.credentials.secret-key=noop", + "spring.cloud.aws.region.static=us-east-2" }) class SqsIntegrationTests extends BaseSqsIntegrationTest { private static final Logger logger = LoggerFactory.getLogger(SqsIntegrationTests.class); From f377c2eb3c364f0cc73d179acaf3c89629ae30d6 Mon Sep 17 00:00:00 2001 From: Maciej Walkowiak Date: Fri, 6 May 2022 21:34:50 +0200 Subject: [PATCH 5/5] Fix running SQS tests in environment without AWS CLI configured. --- .../io/awspring/cloud/sqs/BaseSqsIntegrationTest.java | 9 +++++++++ .../java/io/awspring/cloud/sqs/SqsIntegrationTests.java | 8 ++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/BaseSqsIntegrationTest.java b/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/BaseSqsIntegrationTest.java index e93696740..47bd2636d 100644 --- a/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/BaseSqsIntegrationTest.java +++ b/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/BaseSqsIntegrationTest.java @@ -17,6 +17,7 @@ import static org.testcontainers.containers.localstack.LocalStackContainer.Service.SQS; +import com.amazonaws.auth.AWSCredentials; import java.io.IOException; import org.junit.jupiter.api.BeforeAll; import org.springframework.test.context.DynamicPropertyRegistry; @@ -25,6 +26,8 @@ import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import org.testcontainers.utility.DockerImageName; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; @Testcontainers abstract class BaseSqsIntegrationTest { @@ -42,6 +45,8 @@ abstract class BaseSqsIntegrationTest { static LocalStackContainer localstack = new LocalStackContainer( DockerImageName.parse("localstack/localstack:0.14.0")).withServices(SQS).withReuse(true); + static StaticCredentialsProvider credentialsProvider; + @BeforeAll static void beforeAll() throws IOException, InterruptedException { // create needed queues in SQS @@ -52,6 +57,10 @@ static void beforeAll() throws IOException, InterruptedException { DOES_NOT_ACK_ON_ERROR_QUEUE_NAME); localstack.execInContainer("awslocal", "io/awspring/cloud/sqs", "create-queue", "--queue-name", RECEIVE_FROM_MANY_1_QUEUE_NAME); + + AWSCredentials localstackCredentials = localstack.getDefaultCredentialsProvider().getCredentials(); + credentialsProvider = StaticCredentialsProvider.create(AwsBasicCredentials + .create(localstackCredentials.getAWSAccessKeyId(), localstackCredentials.getAWSSecretKey())); } @DynamicPropertySource diff --git a/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/SqsIntegrationTests.java b/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/SqsIntegrationTests.java index 8fdf57d5b..674f1e8d2 100644 --- a/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/SqsIntegrationTests.java +++ b/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/SqsIntegrationTests.java @@ -59,6 +59,7 @@ import org.springframework.test.context.TestPropertySource; import org.springframework.util.Assert; import org.springframework.util.StopWatch; +import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.sqs.SqsAsyncClient; import software.amazon.awssdk.services.sqs.model.QueueAttributeName; import software.amazon.awssdk.services.sqs.model.ReceiveMessageResponse; @@ -363,7 +364,8 @@ static class SQSConfiguration { // TODO: Probably move some of this to auto configuration with @ConditionalOnMissingBean @Bean(name = SqsConfigUtils.SQS_ASYNC_CLIENT_BEAN_NAME) SqsAsyncClient sqsAsyncClientConsumer() throws Exception { - SqsAsyncClient asyncClient = SqsAsyncClient.builder().endpointOverride(localstack.getEndpointOverride(SQS)) + SqsAsyncClient asyncClient = SqsAsyncClient.builder().credentialsProvider(credentialsProvider) + .endpointOverride(localstack.getEndpointOverride(SQS)).region(Region.of(localstack.getRegion())) .build(); createQueues(asyncClient); return asyncClient; @@ -437,7 +439,9 @@ ObjectMapper objectMapper() { @Bean(name = TEST_SQS_ASYNC_CLIENT_BEAN_NAME) SqsAsyncClient sqsAsyncClientProducer() { - return SqsAsyncClient.builder().endpointOverride(localstack.getEndpointOverride(SQS)).build(); + return SqsAsyncClient.builder().credentialsProvider(credentialsProvider) + .endpointOverride(localstack.getEndpointOverride(SQS)).region(Region.of(localstack.getRegion())) + .build(); } private void createQueues(SqsAsyncClient client) throws InterruptedException, ExecutionException {