Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/src/main/asciidoc/sqs.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ See <<template-message-conversion>> 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<AttributeNames>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,13 @@ public CompletableFuture<QueueAttributes> resolveQueueAttributes() {
}

private CompletableFuture<QueueAttributes> 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;

Expand All @@ -94,8 +101,7 @@ private CompletableFuture<QueueAttributes> 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<String> resolveQueueUrl() {
Expand All @@ -120,10 +126,17 @@ private CompletableFuture<String> doResolveQueueUrl() {
}

private CompletableFuture<String> 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<String> createQueue() {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -92,6 +94,8 @@ public abstract class AbstractSqsMessageSource<T> extends AbstractPollingMessage

private int pollTimeout;

private boolean skipped;

@Override
public void setSqsAsyncClient(SqsAsyncClient sqsAsyncClient) {
Assert.notNull(sqsAsyncClient, "sqsAsyncClient cannot be null.");
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -170,6 +186,9 @@ protected AcknowledgementExecutor<T> createAcknowledgementExecutorInstance() {
@Override
protected CompletableFuture<Collection<Message>> 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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";
Expand Down
Loading