From db1a90e7d56cf7ad7b4a2fe59ac635ef1a0bb96f Mon Sep 17 00:00:00 2001 From: BK202503 <199436087+BK202503@users.noreply.github.com> Date: Thu, 25 Jun 2026 00:08:05 +0900 Subject: [PATCH 1/4] GH-1143 Add IGNORE QueueNotFoundStrategy option for SQS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restore the Spring Cloud AWS 2.x behavior where a missing SQS queue at listener startup did not fail the application context. In 3.x today the two strategies are FAIL (throws on missing queue, blocks startup) and CREATE (calls CreateQueue on missing, requires sqs:CreateQueue IAM and can interfere with externally-managed queue configuration); neither fits the common case where a queue may legitimately be absent in some deployments — for example an optional feature queue, or a multi-region rollout that hasn't created the queue yet. Add a third strategy, IGNORE, that logs a warning and skips starting the listener container's message source for that queue. Subsequent polls return empty so the application keeps running cleanly. Wiring: - QueueNotFoundStrategy gets a new IGNORE constant with Javadoc that cross-references the 2.x default behavior. - QueueAttributesResolver.handleException, on QueueDoesNotExistException + IGNORE, logs a warning and fails the future with a new QueueNotFoundException (subtype of QueueAttributesResolvingException so existing catch sites keep working; the dedicated subtype is what the source uses to recognize the IGNORE signal). The CREATE path is unchanged. - QueueAttributesResolver.wrapException passes QueueNotFoundException through unwrapped instead of re-wrapping in QueueAttributesResolvingException, so the source sees the typed signal rather than a generic resolver failure. - AbstractSqsMessageSource.doStart catches CompletionException with QueueNotFoundException as its cause, flips a `skipped` flag, logs a warning, and returns without setting queueAttributes/queueUrl or configuring the conversion context. - AbstractSqsMessageSource.doPollForMessages short-circuits to an empty future when `skipped`, so the polling loop is a no-op for the lifetime of the source. Tests: - QueueAttributesResolverIntegrationTests#shouldIgnoreQueueWhenStrategyIsIgnore mirrors the existing #shouldNotCreateQueue pattern against LocalStack, asserting that resolving a non-existent queue with IGNORE surfaces a QueueNotFoundException whose cause is QueueDoesNotExistException (rather than the generic QueueAttributesResolvingException that FAIL produces). - The full QueueAttributesResolverIntegrationTests class (7 tests) passes locally on LocalStack. Docs: - sqs.adoc table entry for queueNotFoundStrategy gets a sentence on the IGNORE behavior next to the existing FAIL note. The property binding for `spring.cloud.aws.sqs.queue-not-found-strategy` picks up IGNORE automatically because SqsProperties already exposes the enum directly. Closes #1143. Signed-off-by: BK202503 <199436087+BK202503@users.noreply.github.com> --- docs/src/main/asciidoc/sqs.adoc | 1 + .../cloud/sqs/QueueAttributesResolver.java | 25 ++++++--- .../cloud/sqs/QueueNotFoundException.java | 52 +++++++++++++++++++ .../sqs/listener/QueueNotFoundStrategy.java | 11 +++- .../source/AbstractSqsMessageSource.java | 21 +++++++- ...eueAttributesResolverIntegrationTests.java | 23 ++++++++ 6 files changed, 125 insertions(+), 8 deletions(-) create mode 100644 spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/QueueNotFoundException.java diff --git a/docs/src/main/asciidoc/sqs.adoc b/docs/src/main/asciidoc/sqs.adoc index d9d91c011a..451a061e4f 100644 --- a/docs/src/main/asciidoc/sqs.adoc +++ b/docs/src/main/asciidoc/sqs.adoc @@ -255,6 +255,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 5bf87d8055..d83a22e746 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 0000000000..96f9763495 --- /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 8e99d8d9ad..b830ae155c 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 7541262f19..078d996e8a 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 41f43fbadc..117fae2558 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"; From cd9f43a314aa62e534fb43157c05eab596352c21 Mon Sep 17 00:00:00 2001 From: BK202503 <199436087+BK202503@users.noreply.github.com> Date: Sun, 26 Jul 2026 11:42:56 +0900 Subject: [PATCH 2/4] Lift IGNORE guard to AbstractPollingMessageSource Address review from @tomazfernandes on #1640. Handling the missing queue inside AbstractSqsMessageSource.doStart alone left the container in a half-started state: the acknowledgement processor still initialised and threw IllegalArgumentException: acknowledgementExecutor not set, and the polling thread kept spinning on a source that would only ever return empty batches. Move the guard up to AbstractPollingMessageSource.start(). doStart() now propagates QueueNotFoundException; the base start() catches it, logs a warning, sets running=false, and returns before wiring the ack processor or starting the polling thread. The source-level skipped flag and the short-circuit in doPollForMessages go away with it. Signed-off-by: BK202503 <199436087+BK202503@users.noreply.github.com> --- .../listener/source/AbstractPollingMessageSource.java | 10 +++++++++- .../sqs/listener/source/AbstractSqsMessageSource.java | 11 +---------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/source/AbstractPollingMessageSource.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/source/AbstractPollingMessageSource.java index 91b7141207..a1a749201e 100644 --- a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/source/AbstractPollingMessageSource.java +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/source/AbstractPollingMessageSource.java @@ -16,6 +16,7 @@ package io.awspring.cloud.sqs.listener.source; import io.awspring.cloud.sqs.ConfigUtils; +import io.awspring.cloud.sqs.QueueNotFoundException; import io.awspring.cloud.sqs.listener.ContainerOptions; import io.awspring.cloud.sqs.listener.IdentifiableContainerComponent; import io.awspring.cloud.sqs.listener.MessageProcessingContext; @@ -185,7 +186,14 @@ public void start() { eap -> eap.setAcknowledgementResultCallback(this.acknowledgementResultCallback)) .acceptIfInstance(this.acknowledgmentProcessor, TaskExecutorAware.class, ea -> ea.setTaskExecutor(this.taskExecutor)); - doStart(); + try { + doStart(); + } + catch (QueueNotFoundException qnfe) { + logger.warn("Skipping start for queue {}: {}", qnfe.getQueueName(), qnfe.getMessage()); + this.running = false; + return; + } setupAcknowledgementForConversion(this.acknowledgmentProcessor.getAcknowledgementCallback()); this.acknowledgmentProcessor.start(); startPollingThread(); 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 078d996e8a..c5e2435638 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 @@ -94,8 +94,6 @@ 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."); @@ -128,11 +126,7 @@ protected void doStart() { } 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 qnfe; } throw ex; } @@ -186,9 +180,6 @@ 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) From 7e8a3c54f2a340398ee966ddefa186fe71656c0a Mon Sep 17 00:00:00 2001 From: BK202503 <199436087+BK202503@users.noreply.github.com> Date: Sun, 2 Aug 2026 00:42:47 +0900 Subject: [PATCH 3/4] Signal IGNORE via QueueAttributesResolvingException flag Fold the "queue was ignored" signal into a boolean on QueueAttributesResolvingException instead of a dedicated QueueNotFoundException subtype, per review feedback. QueueAttributesResolver sets the flag on the IGNORE branch; AbstractPollingMessageSource inspects the wrapped QueueAttributesResolvingException at start() and skips the source when the flag is set. Restores original AbstractSqsMessageSource.doStart() and keeps SqsTemplate exception semantics identical between IGNORE and FAIL. --- .../cloud/sqs/QueueAttributesResolver.java | 7 ++- .../QueueAttributesResolvingException.java | 26 +++++++++- .../cloud/sqs/QueueNotFoundException.java | 52 ------------------- .../source/AbstractPollingMessageSource.java | 13 +++-- .../source/AbstractSqsMessageSource.java | 12 +---- ...eueAttributesResolverIntegrationTests.java | 11 ++-- 6 files changed, 41 insertions(+), 80 deletions(-) delete mode 100644 spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/QueueNotFoundException.java 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 d83a22e746..68e9fee8e1 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 @@ -87,9 +87,7 @@ 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. + if (unwrapped instanceof QueueAttributesResolvingException) { return CompletableFutures.failedFuture(unwrapped); } @@ -133,7 +131,8 @@ private CompletableFuture handleException(Throwable t) { 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(new QueueAttributesResolvingException( + "Queue not found: " + this.queueName, t.getCause(), true)); } } return CompletableFutures.failedFuture(t); diff --git a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/QueueAttributesResolvingException.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/QueueAttributesResolvingException.java index b45a4a5875..c09ea43583 100644 --- a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/QueueAttributesResolvingException.java +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/QueueAttributesResolvingException.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2022 the original author or authors. + * 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. @@ -24,12 +24,36 @@ */ public class QueueAttributesResolvingException extends RuntimeException { + private final boolean queueIgnored; + /** * Create an instance with the message and throwable cause. * @param message the error message. * @param cause the cause. */ public QueueAttributesResolvingException(String message, Throwable cause) { + this(message, cause, false); + } + + /** + * Create an instance with the message, cause, and a flag indicating that the resolver treated the missing queue as + * ignored per {@link io.awspring.cloud.sqs.listener.QueueNotFoundStrategy#IGNORE}. + * @param message the error message. + * @param cause the cause. + * @param queueIgnored whether the resolver signalled that the queue should be ignored. + */ + public QueueAttributesResolvingException(String message, Throwable cause, boolean queueIgnored) { super(message, cause); + this.queueIgnored = queueIgnored; + } + + /** + * Whether the resolver signalled that the missing queue should be ignored under + * {@link io.awspring.cloud.sqs.listener.QueueNotFoundStrategy#IGNORE}, so the listener can skip startup rather than + * fail the application context. + * @return {@code true} if the queue should be ignored. + */ + public boolean isQueueIgnored() { + return this.queueIgnored; } } 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 deleted file mode 100644 index 96f9763495..0000000000 --- a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/QueueNotFoundException.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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/source/AbstractPollingMessageSource.java b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/source/AbstractPollingMessageSource.java index a1a749201e..c3c948f5df 100644 --- a/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/source/AbstractPollingMessageSource.java +++ b/spring-cloud-aws-sqs/src/main/java/io/awspring/cloud/sqs/listener/source/AbstractPollingMessageSource.java @@ -16,7 +16,7 @@ package io.awspring.cloud.sqs.listener.source; import io.awspring.cloud.sqs.ConfigUtils; -import io.awspring.cloud.sqs.QueueNotFoundException; +import io.awspring.cloud.sqs.QueueAttributesResolvingException; import io.awspring.cloud.sqs.listener.ContainerOptions; import io.awspring.cloud.sqs.listener.IdentifiableContainerComponent; import io.awspring.cloud.sqs.listener.MessageProcessingContext; @@ -189,10 +189,13 @@ public void start() { try { doStart(); } - catch (QueueNotFoundException qnfe) { - logger.warn("Skipping start for queue {}: {}", qnfe.getQueueName(), qnfe.getMessage()); - this.running = false; - return; + catch (CompletionException ce) { + if (ce.getCause() instanceof QueueAttributesResolvingException qare && qare.isQueueIgnored()) { + logger.warn("Skipping start for queue {}: {}", this.pollingEndpointName, qare.getMessage()); + this.running = false; + return; + } + throw ce; } setupAcknowledgementForConversion(this.acknowledgmentProcessor.getAcknowledgementCallback()); this.acknowledgmentProcessor.start(); 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 c5e2435638..7541262f19 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,7 +17,6 @@ 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; @@ -35,7 +34,6 @@ 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; @@ -121,15 +119,7 @@ protected void doConfigure(ContainerOptions containerOptions) { protected void doStart() { Assert.notNull(this.sqsAsyncClient, "sqsAsyncClient not set"); Assert.notNull(this.queueAttributeNames, "queueAttributeNames not set"); - try { - this.queueAttributes = resolveQueueAttributes(); - } - catch (CompletionException ex) { - if (ex.getCause() instanceof QueueNotFoundException qnfe) { - throw qnfe; - } - throw ex; - } + this.queueAttributes = resolveQueueAttributes(); this.queueUrl = this.queueAttributes.getQueueUrl(); configureConversionContextAndAcknowledgement(); } 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 117fae2558..1511607a23 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,7 +20,6 @@ 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; @@ -116,9 +115,6 @@ void shouldNotCreateQueue() { @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 @@ -130,9 +126,10 @@ void shouldIgnoreQueueWhenStrategyIsIgnore() { .build(); assertThatThrownBy(() -> resolver.resolveQueueAttributes().join()) .isInstanceOf(CompletionException.class) - .extracting(Throwable::getCause) - .isInstanceOf(QueueNotFoundException.class) - .extracting(Throwable::getCause) + .cause() + .isInstanceOfSatisfying(QueueAttributesResolvingException.class, + qare -> assertThat(qare.isQueueIgnored()).isTrue()) + .cause() .isInstanceOf(QueueDoesNotExistException.class); } From 0a21379d4cfe7fce10cd78e3e55e16b027989edc Mon Sep 17 00:00:00 2001 From: BK202503 <199436087+BK202503@users.noreply.github.com> Date: Sun, 9 Aug 2026 22:06:39 +0900 Subject: [PATCH 4/4] Move IGNORE exception creation to wrapException --- .../cloud/sqs/QueueAttributesResolver.java | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) 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 68e9fee8e1..a97ecd6678 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 @@ -87,14 +87,16 @@ public CompletableFuture resolveQueueAttributes() { private CompletableFuture wrapException(Throwable t) { Throwable unwrapped = t instanceof CompletionException ? t.getCause() : t; - if (unwrapped instanceof QueueAttributesResolvingException) { - return CompletableFutures.failedFuture(unwrapped); + + if (unwrapped instanceof QueueDoesNotExistException && QueueNotFoundStrategy.IGNORE.equals(this.queueNotFoundStrategy)) { + return CompletableFutures.failedFuture(new QueueAttributesResolvingException( + "Queue not found: " + this.queueName, unwrapped, true)); } String message = "Error resolving attributes for queue " + this.queueName + " with strategy " + this.queueNotFoundStrategy + " and queueAttributesNames " + this.queueAttributeNames; - if (t.getCause() instanceof SqsException) { + if (unwrapped instanceof SqsException) { message += "\n This might be due to connectivity issues or incorrect configuration. " + "Please verify your AWS credentials, network settings, and queue configuration."; } @@ -124,16 +126,8 @@ private CompletableFuture doResolveQueueUrl() { } private CompletableFuture handleException(Throwable 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 QueueAttributesResolvingException( - "Queue not found: " + this.queueName, t.getCause(), true)); - } + if (t.getCause() instanceof QueueDoesNotExistException && QueueNotFoundStrategy.CREATE.equals(this.queueNotFoundStrategy)) { + return createQueue(); } return CompletableFutures.failedFuture(t); }