package com.softwaremill.common.sqs;
import com.amazonaws.AmazonServiceException;
import com.amazonaws.services.sqs.AmazonSQS;
import com.amazonaws.services.sqs.model.*;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableMap;
import org.joda.time.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.softwaremill.common.sqs.exception.SQSRuntimeException;
import java.io.IOException;
import java.io.Serializable;
import static com.google.common.base.Preconditions.*;
import static java.lang.String.format;
import static com.softwaremill.common.sqs.Util.deserializeFromBase64;
/**
* @author Maciej Bilas
* @since 11/10/12 12:43
*/
public class Queue {
private static final Logger LOG = LoggerFactory.getLogger(Queue.class);
private static final int DELIVERY_RETRY_LIMIT = 10;
private static final int DELIVERY_RETRY_SLEEP_TIME_IN_MILLIS = 1000;
private final String name;
private final String url;
private final AmazonSQS sqsClient;
@SuppressWarnings("UnusedDeclaration")
public Queue() {
LOG.error("Don't use this constructor. It's here only to make CDI happy.");
this.name = null;
this.url = null;
this.sqsClient = null;
}
public Queue(String name, String url, AmazonSQS sqsClient) {
this.name = name;
this.url = checkNotNull(url);
this.sqsClient = checkNotNull(sqsClient);
}
public String getURL() {
return url;
}
private MessageId sendSerializableInternal(Serializable object, Optional<Duration> duration) {
String encodedMessage;
try {
encodedMessage = Util.serializeToBase64(object);
} catch (IOException e) {
throw new SQSRuntimeException("Could not serialize message.", e);
}
checkState(encodedMessage.length() <= 64 * 1024);
LOG.debug("Serialized Message: " + encodedMessage);
SendMessageRequest request = new SendMessageRequest(url, encodedMessage);
if (duration.isPresent()) {
request.withDelaySeconds((int) duration.get().getStandardSeconds());
}
for (int i = 0; i < DELIVERY_RETRY_LIMIT; ++i) {
try {
/* No verification of message checksum is done at this point. It might get added in the future, though. */
return new MessageId(sqsClient.sendMessage(request).getMessageId());
} catch (AmazonServiceException e) {
LOG.warn(format("Could not sent message to SQS queue: %s. Retrying.", url), e);
try {
Thread.sleep(DELIVERY_RETRY_SLEEP_TIME_IN_MILLIS);
} catch (InterruptedException e1) {
// Ignore
}
}
}
throw new SQSRuntimeException("Exceeded redelivery value: " + DELIVERY_RETRY_LIMIT + "; message not sent!");
}
public MessageId sendSerializable(Serializable object) {
checkNotNull(object);
return sendSerializableInternal(object, Optional.<Duration>absent());
}
public MessageId sendSerializableDelayed(Serializable object, Duration duration) {
checkNotNull(object);
checkNotNull(duration);
checkArgument(duration.getStandardSeconds() < 15 * 60,
"SQS messages can only be delayed for a maximum of 15 minutes.");
long droppedMillis = duration.getMillis() % 1000;
if (droppedMillis != 0)
LOG.warn(format("SQS delayed messages have a precision of 1s, milliseconds will be stripped. " +
"Object to send: %s Millis: %s", object.toString(), duration.getMillis()));
if (duration.getMillis() < 1000) {
return sendSerializable(object);
} else {
return sendSerializableInternal(object, Optional.of(duration));
}
}
public Optional<ReceivedMessage> receiveSingleMessage() {
LOG.debug(format("Polling queue %s", url));
ReceiveMessageResult response = sqsClient.receiveMessage(new ReceiveMessageRequest(url)
.withMaxNumberOfMessages(1).withAttributeNames("SentTimestamp"));
switch (response.getMessages().size()) {
case 0:
return Optional.absent();
case 1:
return Optional.fromNullable(decodeMessage(response.getMessages().get(0)));
default:
/* This seems very unlikely.
* The API specified the following: SQS never returns more messages than this value but might return fewer.
* http://docs.amazonwebservices.com/AWSSimpleQueueService/latest/APIReference/Query_QueryReceiveMessage.html
*/
throw new IllegalStateException();
}
}
private ReceivedMessage decodeMessage(Message message) {
String receiptHandle = message.getReceiptHandle();
try {
/* Again no MD5 verification, yet */
return new ReceivedMessage(deserializeFromBase64(message.getBody()),
new ReceiptHandle(receiptHandle),
new MessageId(message.getMessageId()));
} catch (IOException e) {
LOG.warn(format("Could not deserialize message from the queue %s.", name));
LOG.debug(format("Message body of unrecognized message %s", message.getBody()));
delete(receiptHandle);
return null;
} catch (ClassNotFoundException e) {
LOG.warn(format("Could not deserialize message from the queue %s.", name));
LOG.debug(format("Message body of unrecognized message %s", message.getBody()));
delete(receiptHandle);
return null;
}
}
private void delete(String receiptHandle) {
sqsClient.deleteMessage(new DeleteMessageRequest(url, receiptHandle));
}
public void deleteMessage(ReceivedMessage message) {
checkNotNull(message);
delete(message.getReceiptHandle().get());
}
/**
* @param timeout timeout in seconds for the whole queue (default is 30) - value is limited to 43200 seconds (12 hours)
*/
public void setQueueVisibilityTimeout(int timeout) {
checkArgument(timeout >= 0);
checkArgument(timeout <= 12 * 60 * 60);
sqsClient.setQueueAttributes(new SetQueueAttributesRequest(url,
ImmutableMap.of("VisibilityTimeout", Integer.toString(timeout))));
}
public void setMessageVisibilityTimeout(ReceivedMessage message, int timeout) {
checkNotNull(message);
checkArgument(timeout >= 0);
checkArgument(timeout <= 12 * 60 * 60);
sqsClient.changeMessageVisibility(new ChangeMessageVisibilityRequest(url,
message.getReceiptHandle().get(), timeout));
}
/**
* @param waitTimeInSeconds wait time for message in seconds, allowed values 0-20. Values > 0 allows longer message pooling
*/
public void setReceiveMessageWaitTime(int waitTimeInSeconds) {
checkArgument(waitTimeInSeconds >= 0);
checkArgument(waitTimeInSeconds <= 20);
sqsClient.setQueueAttributes(new SetQueueAttributesRequest(url,
ImmutableMap.of("ReceiveMessageWaitTimeSeconds", Integer.toString(waitTimeInSeconds))));
}
@SuppressWarnings("UnusedDeclaration")
public String getName() {
return name;
}
}