diff --git a/docs/src/main/asciidoc/sqs.adoc b/docs/src/main/asciidoc/sqs.adoc index 9b69146b0..0aee29f15 100644 --- a/docs/src/main/asciidoc/sqs.adoc +++ b/docs/src/main/asciidoc/sqs.adoc @@ -197,6 +197,7 @@ See <> for more information. #CREATE |Set the strategy to use in case a queue is not found. With `QueueNotFoundStrategy#FAIL`, an exception is thrown in case a queue is not found. +With `QueueNotFoundStrategy#IGNORE`, the listener for the missing queue is skipped at startup with a warning log and subsequent polls for it return no messages, so application startup is not blocked. |`queueAttributeNames` |Collection diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/QueueAttributesResolver.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/QueueAttributesResolver.java index 5bf87d805..d83a22e74 100644 --- a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/QueueAttributesResolver.java +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/QueueAttributesResolver.java @@ -86,6 +86,13 @@ public CompletableFuture resolveQueueAttributes() { } private CompletableFuture wrapException(Throwable t) { + Throwable unwrapped = t instanceof CompletionException ? t.getCause() : t; + if (unwrapped instanceof QueueNotFoundException) { + // Already a typed "ignore this queue" signal — preserve the type so callers can distinguish it from + // other resolver failures and skip starting the listener gracefully. + return CompletableFutures.failedFuture(unwrapped); + } + String message = "Error resolving attributes for queue " + this.queueName + " with strategy " + this.queueNotFoundStrategy + " and queueAttributesNames " + this.queueAttributeNames; @@ -94,8 +101,7 @@ private CompletableFuture wrapException(Throwable t) { "Please verify your AWS credentials, network settings, and queue configuration."; } - return CompletableFutures.failedFuture(new QueueAttributesResolvingException(message, - t instanceof CompletionException ? t.getCause() : t)); + return CompletableFutures.failedFuture(new QueueAttributesResolvingException(message, unwrapped)); } private CompletableFuture resolveQueueUrl() { @@ -120,10 +126,17 @@ private CompletableFuture doResolveQueueUrl() { } private CompletableFuture handleException(Throwable t) { - return t.getCause() instanceof QueueDoesNotExistException - && QueueNotFoundStrategy.CREATE.equals(this.queueNotFoundStrategy) - ? createQueue() - : CompletableFutures.failedFuture(t); + if (t.getCause() instanceof QueueDoesNotExistException) { + if (QueueNotFoundStrategy.CREATE.equals(this.queueNotFoundStrategy)) { + return createQueue(); + } + if (QueueNotFoundStrategy.IGNORE.equals(this.queueNotFoundStrategy)) { + logger.warn("Queue {} not found and strategy is IGNORE; the listener for this queue will be skipped", + this.queueName); + return CompletableFutures.failedFuture(new QueueNotFoundException(this.queueName, t.getCause())); + } + } + return CompletableFutures.failedFuture(t); } private CompletableFuture createQueue() { diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/QueueNotFoundException.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/QueueNotFoundException.java new file mode 100644 index 000000000..96f976349 --- /dev/null +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/QueueNotFoundException.java @@ -0,0 +1,52 @@ +/* + * Copyright 2013-2026 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 io.awspring.cloud.sqs.listener.QueueNotFoundStrategy; + +/** + * Signals that a queue was not found and the configured {@link QueueNotFoundStrategy} is + * {@link QueueNotFoundStrategy#IGNORE IGNORE}. The listener container catches this to skip starting the source while + * allowing the application context to come up; other resolver failures continue to surface as + * {@link QueueAttributesResolvingException}. + * + * @author Bill Kim + * @since 4.1 + */ +public class QueueNotFoundException extends QueueAttributesResolvingException { + + private final String queueName; + + /** + * Create an instance for the given queue name. + * @param queueName the name of the queue that was not found. + * @param cause the underlying cause, typically a + * {@code software.amazon.awssdk.services.sqs.model.QueueDoesNotExistException}. + */ + public QueueNotFoundException(String queueName, Throwable cause) { + super("Queue not found: " + queueName, cause); + this.queueName = queueName; + } + + /** + * Return the name of the queue that was not found. + * @return the queue name. + */ + public String getQueueName() { + return this.queueName; + } + +} diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/QueueNotFoundStrategy.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/QueueNotFoundStrategy.java index 8e99d8d9a..b830ae155 100644 --- a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/QueueNotFoundStrategy.java +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/QueueNotFoundStrategy.java @@ -32,6 +32,15 @@ public enum QueueNotFoundStrategy { * Create queues that are not found at startup. Mind that in production environments the application might not have * permissions to create the queue and throw an exception. */ - CREATE + CREATE, + + /** + * Skip starting the listener for a queue that is not found, log a warning, and allow application startup to + * proceed. Subsequent polls for that listener return no messages. Useful when a queue may legitimately be absent in + * some deployments (for example, optional feature queues) and neither {@link #CREATE} nor {@link #FAIL} fits — this + * mirrors the default behavior of Spring Cloud AWS 2.x's {@code spring-cloud-starter-aws-messaging}, which silently + * ignored missing queues at startup. + */ + IGNORE } diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/source/AbstractSqsMessageSource.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/source/AbstractSqsMessageSource.java index 7541262f1..078d996e8 100644 --- a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/source/AbstractSqsMessageSource.java +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/source/AbstractSqsMessageSource.java @@ -17,6 +17,7 @@ import io.awspring.cloud.sqs.ConfigUtils; import io.awspring.cloud.sqs.QueueAttributesResolver; +import io.awspring.cloud.sqs.QueueNotFoundException; import io.awspring.cloud.sqs.listener.ContainerOptions; import io.awspring.cloud.sqs.listener.QueueAttributes; import io.awspring.cloud.sqs.listener.QueueAttributesAware; @@ -34,6 +35,7 @@ import java.util.Collections; import java.util.List; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; import java.util.stream.Collectors; import java.util.stream.IntStream; import org.jspecify.annotations.Nullable; @@ -92,6 +94,8 @@ public abstract class AbstractSqsMessageSource extends AbstractPollingMessage private int pollTimeout; + private boolean skipped; + @Override public void setSqsAsyncClient(SqsAsyncClient sqsAsyncClient) { Assert.notNull(sqsAsyncClient, "sqsAsyncClient cannot be null."); @@ -119,7 +123,19 @@ protected void doConfigure(ContainerOptions containerOptions) { protected void doStart() { Assert.notNull(this.sqsAsyncClient, "sqsAsyncClient not set"); Assert.notNull(this.queueAttributeNames, "queueAttributeNames not set"); - this.queueAttributes = resolveQueueAttributes(); + try { + this.queueAttributes = resolveQueueAttributes(); + } + catch (CompletionException ex) { + if (ex.getCause() instanceof QueueNotFoundException qnfe) { + // QueueNotFoundStrategy.IGNORE — log warning and short-circuit the source so the application + // context can come up. doPollForMessages() will return empty for the lifetime of this source. + logger.warn("Skipping start for queue {}: {}", qnfe.getQueueName(), qnfe.getMessage()); + this.skipped = true; + return; + } + throw ex; + } this.queueUrl = this.queueAttributes.getQueueUrl(); configureConversionContextAndAcknowledgement(); } @@ -170,6 +186,9 @@ protected AcknowledgementExecutor createAcknowledgementExecutorInstance() { @Override protected CompletableFuture> doPollForMessages( int maxNumberOfMessages) { + if (this.skipped) { + return CompletableFuture.completedFuture(Collections.emptyList()); + } logger.debug("Polling queue {} for {} messages.", this.queueUrl, maxNumberOfMessages); return maxNumberOfMessages <= 10 ? executePoll(maxNumberOfMessages) diff --git a/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/integration/QueueAttributesResolverIntegrationTests.java b/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/integration/QueueAttributesResolverIntegrationTests.java index 41f43fbad..117fae255 100644 --- a/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/integration/QueueAttributesResolverIntegrationTests.java +++ b/spring-cloud-aws-sqs/src/test/java/io/awspring/cloud/sqs/integration/QueueAttributesResolverIntegrationTests.java @@ -20,6 +20,7 @@ import io.awspring.cloud.sqs.QueueAttributesResolver; import io.awspring.cloud.sqs.QueueAttributesResolvingException; +import io.awspring.cloud.sqs.QueueNotFoundException; import io.awspring.cloud.sqs.config.SqsBootstrapConfiguration; import io.awspring.cloud.sqs.listener.QueueAttributes; import io.awspring.cloud.sqs.listener.QueueNotFoundStrategy; @@ -113,6 +114,28 @@ void shouldNotCreateQueue() { .isInstanceOf(QueueDoesNotExistException.class); } + @Test + void shouldIgnoreQueueWhenStrategyIsIgnore() { + // GH-1143: With QueueNotFoundStrategy.IGNORE, a missing queue must surface as QueueNotFoundException + // (a subtype of QueueAttributesResolvingException) so the listener container can catch it and skip + // startup for that queue rather than failing the application context. + String queueName = "testQueueName-" + UUID.randomUUID(); + SqsAsyncClient client = createAsyncClient(); + QueueAttributesResolver resolver = QueueAttributesResolver + .builder() + .queueAttributeNames(Collections.emptyList()) + .sqsAsyncClient(client) + .queueName(queueName) + .queueNotFoundStrategy(QueueNotFoundStrategy.IGNORE) + .build(); + assertThatThrownBy(() -> resolver.resolveQueueAttributes().join()) + .isInstanceOf(CompletionException.class) + .extracting(Throwable::getCause) + .isInstanceOf(QueueNotFoundException.class) + .extracting(Throwable::getCause) + .isInstanceOf(QueueDoesNotExistException.class); + } + @Test void shouldGetQueueAttributes() { String queueName = "should-get-queue-attributes";