/*
* Copyright 2013-2014 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
*
* http://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 org.springframework.cloud.aws.messaging.listener;
import com.amazonaws.services.sqs.AmazonSQS;
import com.amazonaws.services.sqs.AmazonSQSAsync;
import com.amazonaws.services.sqs.model.GetQueueAttributesRequest;
import com.amazonaws.services.sqs.model.GetQueueAttributesResult;
import com.amazonaws.services.sqs.model.QueueAttributeName;
import com.amazonaws.services.sqs.model.ReceiveMessageRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.cloud.aws.core.env.ResourceIdResolver;
import org.springframework.cloud.aws.core.support.documentation.RuntimeUse;
import org.springframework.cloud.aws.messaging.support.destination.DynamicQueueUrlDestinationResolver;
import org.springframework.context.SmartLifecycle;
import org.springframework.messaging.core.CachingDestinationResolverProxy;
import org.springframework.messaging.core.DestinationResolutionException;
import org.springframework.messaging.core.DestinationResolver;
import org.springframework.util.Assert;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* Abstract base class for message listener containers providing basic lifecycle capabilities and collaborator for the
* concrete sub classes. This class implements all lifecycle and configuration specific interface used by the Spring
* container to create, initialize and start the container.
*
* @author Agim Emruli
* @author Alain Sahli
* @since 1.0
*/
abstract class AbstractMessageListenerContainer implements InitializingBean, DisposableBean, SmartLifecycle, BeanNameAware {
private static final String RECEIVING_ATTRIBUTES = "All";
private static final String RECEIVING_MESSAGE_ATTRIBUTES = "All";
private static final int DEFAULT_MAX_NUMBER_OF_MESSAGES = 10;
private final Logger logger = LoggerFactory.getLogger(getClass());
private final Object lifecycleMonitor = new Object();
@SuppressWarnings("FieldAccessedSynchronizedAndUnsynchronized")
private final Map<String, QueueAttributes> registeredQueues = new HashMap<>();
//Mandatory settings, the container synchronizes this fields after calling the setters hence there is no further synchronization
@SuppressWarnings("FieldAccessedSynchronizedAndUnsynchronized")
private AmazonSQSAsync amazonSqs;
@SuppressWarnings("FieldAccessedSynchronizedAndUnsynchronized")
private DestinationResolver<String> destinationResolver;
private String beanName;
@SuppressWarnings("FieldAccessedSynchronizedAndUnsynchronized")
private QueueMessageHandler messageHandler;
//Optional settings with no defaults
private Integer maxNumberOfMessages;
private Integer visibilityTimeout;
@SuppressWarnings("FieldAccessedSynchronizedAndUnsynchronized")
private ResourceIdResolver resourceIdResolver;
private Integer waitTimeOut;
//Optional settings with defaults
private boolean autoStartup = true;
private int phase = Integer.MAX_VALUE;
//Settings that are changed at runtime
private boolean active;
private boolean running;
protected Map<String, QueueAttributes> getRegisteredQueues() {
return Collections.unmodifiableMap(this.registeredQueues);
}
protected QueueMessageHandler getMessageHandler() {
return this.messageHandler;
}
public void setMessageHandler(QueueMessageHandler messageHandler) {
this.messageHandler = messageHandler;
}
protected Object getLifecycleMonitor() {
return this.lifecycleMonitor;
}
protected Logger getLogger() {
return this.logger;
}
protected AmazonSQSAsync getAmazonSqs() {
return this.amazonSqs;
}
/**
* Configures the mandatory {@link AmazonSQS} client for this instance.
* <b>Note:</b>The configured instance should have a buffering amazon SQS instance (see subclasses) functionality
* to
* improve the performance during message reception and deletion on the queueing system.
*
* @param amazonSqs
* the amazon sqs instance. Must not be null
*/
public void setAmazonSqs(AmazonSQSAsync amazonSqs) {
this.amazonSqs = amazonSqs;
}
protected DestinationResolver<String> getDestinationResolver() {
return this.destinationResolver;
}
/**
* Configures the destination resolver used to retrieve the queue url based on the destination name configured for
* this instance. <br/>
* This setter can be used when a custom configured {@link DestinationResolver}
* must be provided. (For example if one want to have the {@link DynamicQueueUrlDestinationResolver}
* with the auto creation of queues set to {@code true}.
*
* @param destinationResolver
* - the destination resolver. Must not be null
*/
public void setDestinationResolver(DestinationResolver<String> destinationResolver) {
this.destinationResolver = destinationResolver;
}
protected String getBeanName() {
return this.beanName;
}
@Override
public void setBeanName(String name) {
this.beanName = name;
}
protected Integer getMaxNumberOfMessages() {
return this.maxNumberOfMessages;
}
/**
* Configure the maximum number of messages that should be retrieved during one poll to the Amazon SQS system. This
* number must be a positive, non-zero number that has a maximum number of 10. Values higher then 10 are currently
* not supported by the queueing system.
*
* @param maxNumberOfMessages
* the maximum number of messages (between 1-10)
*/
public void setMaxNumberOfMessages(Integer maxNumberOfMessages) {
this.maxNumberOfMessages = maxNumberOfMessages;
}
protected Integer getVisibilityTimeout() {
return this.visibilityTimeout;
}
/**
* Configures the duration (in seconds) that the received messages are hidden from
* subsequent poll requests after being retrieved from the system.
*
* @param visibilityTimeout
* the visibility timeout in seconds
*/
public void setVisibilityTimeout(Integer visibilityTimeout) {
this.visibilityTimeout = visibilityTimeout;
}
/**
* This value must be set if no destination resolver has been set.
*
* @param resourceIdResolver
* the resourceIdResolver to use for resolving logical to physical ids in a CloudFormation environment.
* Must not be null.
*/
@RuntimeUse
public void setResourceIdResolver(ResourceIdResolver resourceIdResolver) {
this.resourceIdResolver = resourceIdResolver;
}
protected Integer getWaitTimeOut() {
return this.waitTimeOut;
}
/**
* Configures the wait timeout that the poll request will wait for new message to arrive if the are currently no
* messages on the queue. Higher values will reduce poll request to the system significantly.
*
* @param waitTimeOut
* - the wait time out in seconds
*/
public void setWaitTimeOut(Integer waitTimeOut) {
this.waitTimeOut = waitTimeOut;
}
@Override
public boolean isAutoStartup() {
return this.autoStartup;
}
/**
* Configures if this container should be automatically started. The default value is true
*
* @param autoStartup
* - false if the container will be manually started
*/
public void setAutoStartup(boolean autoStartup) {
this.autoStartup = autoStartup;
}
@Override
public void stop(Runnable callback) {
this.stop();
callback.run();
}
@Override
public int getPhase() {
return this.phase;
}
/**
* Configure a custom phase for the container to start. This allows to start other beans that also implements the
* {@link SmartLifecycle} interface.
*
* @param phase
* - the phase that defines the phase respecting the {@link org.springframework.core.Ordered} semantics
*/
public void setPhase(int phase) {
this.phase = phase;
}
public boolean isActive() {
synchronized (this.getLifecycleMonitor()) {
return this.active;
}
}
@Override
public void afterPropertiesSet() throws Exception {
validateConfiguration();
initialize();
}
private void validateConfiguration() {
Assert.state(this.amazonSqs != null, "amazonSqs must not be null");
Assert.state(this.messageHandler != null, "messageHandler must not be null");
}
protected void initialize() {
synchronized (this.getLifecycleMonitor()) {
if (this.destinationResolver == null) {
if (this.resourceIdResolver == null) {
this.destinationResolver = new CachingDestinationResolverProxy<>(new DynamicQueueUrlDestinationResolver(this.amazonSqs));
} else {
this.destinationResolver = new CachingDestinationResolverProxy<>(new DynamicQueueUrlDestinationResolver(this.amazonSqs, this.resourceIdResolver));
}
}
for (QueueMessageHandler.MappingInformation mappingInformation : this.messageHandler.getHandlerMethods().keySet()) {
for (String queue : mappingInformation.getLogicalResourceIds()) {
QueueAttributes queueAttributes = queueAttributes(queue, mappingInformation.getDeletionPolicy());
if (queueAttributes != null) {
this.registeredQueues.put(queue, queueAttributes);
}
}
}
this.active = true;
this.getLifecycleMonitor().notifyAll();
}
}
@Override
public void start() {
getLogger().debug("Starting container with name {}", getBeanName());
synchronized (this.getLifecycleMonitor()) {
this.running = true;
this.getLifecycleMonitor().notifyAll();
}
doStart();
}
private QueueAttributes queueAttributes(String queue, SqsMessageDeletionPolicy deletionPolicy) {
String destinationUrl;
try {
destinationUrl = getDestinationResolver().resolveDestination(queue);
} catch (DestinationResolutionException e) {
if (getLogger().isDebugEnabled()) {
getLogger().debug("Ignoring queue with name '" + queue + "' as it does not exist.", e);
} else {
getLogger().warn("Ignoring queue with name '" + queue + "' as it does not exist.");
}
return null;
}
GetQueueAttributesResult queueAttributes = getAmazonSqs().getQueueAttributes(new GetQueueAttributesRequest(destinationUrl)
.withAttributeNames(QueueAttributeName.RedrivePolicy));
boolean hasRedrivePolicy = queueAttributes.getAttributes().containsKey(QueueAttributeName.RedrivePolicy.toString());
return new QueueAttributes(hasRedrivePolicy, deletionPolicy, destinationUrl, getMaxNumberOfMessages(), getVisibilityTimeout(), getWaitTimeOut());
}
@Override
public void stop() {
getLogger().debug("Stopping container with name {}", getBeanName());
synchronized (this.getLifecycleMonitor()) {
this.running = false;
this.getLifecycleMonitor().notifyAll();
}
doStop();
}
@Override
public boolean isRunning() {
synchronized (this.getLifecycleMonitor()) {
return this.running;
}
}
@Override
public void destroy() {
synchronized (this.lifecycleMonitor) {
stop();
this.active = false;
doDestroy();
}
}
protected abstract void doStart();
protected abstract void doStop();
protected void doDestroy() {
}
protected static class QueueAttributes {
private final boolean hasRedrivePolicy;
private final SqsMessageDeletionPolicy deletionPolicy;
private final String destinationUrl;
private final Integer maxNumberOfMessages;
private final Integer visibilityTimeout;
private final Integer waitTimeOut;
public QueueAttributes(boolean hasRedrivePolicy, SqsMessageDeletionPolicy deletionPolicy, String destinationUrl,
Integer maxNumberOfMessages, Integer visibilityTimeout, Integer waitTimeOut) {
this.hasRedrivePolicy = hasRedrivePolicy;
this.deletionPolicy = deletionPolicy;
this.destinationUrl = destinationUrl;
this.maxNumberOfMessages = maxNumberOfMessages;
this.visibilityTimeout = visibilityTimeout;
this.waitTimeOut = waitTimeOut;
}
public boolean hasRedrivePolicy() {
return this.hasRedrivePolicy;
}
public ReceiveMessageRequest getReceiveMessageRequest() {
ReceiveMessageRequest receiveMessageRequest = new ReceiveMessageRequest(this.destinationUrl).
withAttributeNames(RECEIVING_ATTRIBUTES).
withMessageAttributeNames(RECEIVING_MESSAGE_ATTRIBUTES);
if (this.maxNumberOfMessages != null) {
receiveMessageRequest.withMaxNumberOfMessages(this.maxNumberOfMessages);
} else {
receiveMessageRequest.withMaxNumberOfMessages(DEFAULT_MAX_NUMBER_OF_MESSAGES);
}
if (this.visibilityTimeout != null) {
receiveMessageRequest.withVisibilityTimeout(this.visibilityTimeout);
}
if (this.waitTimeOut != null) {
receiveMessageRequest.setWaitTimeSeconds(this.waitTimeOut);
}
return receiveMessageRequest;
}
public SqsMessageDeletionPolicy getDeletionPolicy() {
return this.deletionPolicy;
}
}
}