package com.amazonaws.services.sqs.util; import static com.amazonaws.services.sqs.executors.ExecutorUtils.acceptIntOn; import static com.amazonaws.services.sqs.executors.ExecutorUtils.acceptOn; import static com.amazonaws.services.sqs.executors.ExecutorUtils.applyIntOn; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import com.amazonaws.AmazonClientException; import com.amazonaws.services.sqs.AmazonSQS; import com.amazonaws.services.sqs.model.CreateQueueRequest; import com.amazonaws.services.sqs.model.GetQueueAttributesRequest; import com.amazonaws.services.sqs.model.GetQueueAttributesResult; import com.amazonaws.services.sqs.model.MessageAttributeValue; import com.amazonaws.services.sqs.model.QueueAttributeName; import com.amazonaws.services.sqs.model.QueueDoesNotExistException; import com.amazonaws.services.sqs.model.SendMessageRequest; public class SQSQueueUtils { private static final Log LOG = LogFactory.getLog(SQSQueueUtils.class); public static final String ATTRIBUTE_NAMES_ALL = "All"; public static final String MESSAGE_ATTRIBUTE_TYPE_STRING = "String"; public static final String MESSAGE_ATTRIBUTE_TYPE_BOOLEAN = "String.boolean"; public static final String MESSAGE_ATTRIBUTE_TYPE_LONG = "Number.long"; public static final int SQS_LIST_QUEUES_LIMIT = 1000; public static final Consumer<Exception> DEFAULT_EXCEPTION_HANDLER = e -> { LOG.error("Unexpected exception", e); }; private SQSQueueUtils() { // Never instantiated } public static MessageAttributeValue stringMessageAttributeValue(String value) { return new MessageAttributeValue().withDataType(MESSAGE_ATTRIBUTE_TYPE_STRING) .withStringValue(value); } public static MessageAttributeValue longMessageAttributeValue(long value) { return new MessageAttributeValue().withDataType(MESSAGE_ATTRIBUTE_TYPE_LONG) .withStringValue(Long.toString(value)); } public static MessageAttributeValue booleanMessageAttributeValue(boolean value) { return new MessageAttributeValue().withDataType(MESSAGE_ATTRIBUTE_TYPE_BOOLEAN) .withStringValue(Boolean.toString(value)); } public static Optional<String> getStringMessageAttributeValue(Map<String, MessageAttributeValue> messageAttributes, String key) { return Optional.ofNullable(messageAttributes.get(key)) .filter(value -> MESSAGE_ATTRIBUTE_TYPE_STRING.equals(value.getDataType())) .map(MessageAttributeValue::getStringValue); } public static Optional<Long> getLongMessageAttributeValue(Map<String, MessageAttributeValue> messageAttributes, String key) { return Optional.ofNullable(messageAttributes.get(key)) .filter(value -> MESSAGE_ATTRIBUTE_TYPE_LONG.equals(value.getDataType())) .map(MessageAttributeValue::getStringValue) .map(Long::parseLong); } public static boolean getBooleanMessageAttributeValue(Map<String, MessageAttributeValue> messageAttributes, String key) { return Optional.ofNullable(messageAttributes.get(key)) .filter(value -> MESSAGE_ATTRIBUTE_TYPE_BOOLEAN.equals(value.getDataType())) .map(MessageAttributeValue::getStringValue) .map(Boolean::parseBoolean).orElse(false); } // https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_CreateQueue.html private static final String VALID_QUEUE_NAME_CHARACTERS; static { StringBuilder builder = new StringBuilder(); IntStream.rangeClosed('a', 'z').forEach(i -> builder.append((char)i)); IntStream.rangeClosed('A', 'Z').forEach(i -> builder.append((char)i)); IntStream.rangeClosed('0', '9').forEach(i -> builder.append((char)i)); builder.append('-'); builder.append('_'); VALID_QUEUE_NAME_CHARACTERS = builder.toString(); } public static boolean isQueueEmpty(AmazonSQS sqs, String queueUrl) { QueueAttributeName[] messageCountAttrs = { QueueAttributeName.ApproximateNumberOfMessages, QueueAttributeName.ApproximateNumberOfMessagesDelayed, QueueAttributeName.ApproximateNumberOfMessagesNotVisible }; GetQueueAttributesRequest getQueueAttributesRequest = new GetQueueAttributesRequest() .withQueueUrl(queueUrl) .withAttributeNames(messageCountAttrs); GetQueueAttributesResult result = sqs.getQueueAttributes(getQueueAttributesRequest); Map<String, String> attrValues = result.getAttributes(); return Stream.of(messageCountAttrs).allMatch(attr -> Long.parseLong(attrValues.get(attr.name())) == 0); } public static boolean awaitWithPolling(long period, long timeout, TimeUnit unit, Supplier<Boolean> test) throws InterruptedException { long deadlineNanos = System.nanoTime() + unit.toNanos(timeout); while (!test.get()) { if (deadlineNanos < System.nanoTime()) { return false; } // TODO-RS: Use a ScheduledExecutorService instead of sleeping? unit.sleep(period); } return true; } public static boolean awaitEmptyQueue(AmazonSQS sqs, String queueUrl, long timeout, TimeUnit unit) throws InterruptedException { // There's no way to be directly notified unfortunately. return awaitWithPolling(unit.convert(2, TimeUnit.SECONDS), timeout, unit, () -> isQueueEmpty(sqs, queueUrl)); } public static boolean doesQueueExist(AmazonSQS sqs, String queueUrl) { try { sqs.listQueueTags(queueUrl); return true; } catch (QueueDoesNotExistException e) { return false; } } public static boolean awaitQueueCreated(AmazonSQS sqs, String queueUrl, long timeout, TimeUnit unit) throws InterruptedException { return awaitWithPolling(unit.convert(2, TimeUnit.SECONDS), timeout, unit, () -> doesQueueExist(sqs, queueUrl)); } public static boolean awaitQueueDeleted(AmazonSQS sqs, String queueUrl, long timeout, TimeUnit unit) throws InterruptedException { return awaitWithPolling(unit.convert(2, TimeUnit.SECONDS), timeout, unit, () -> !doesQueueExist(sqs, queueUrl)); } public static void forEachQueue(ExecutorService executor, Function<String, List<String>> lister, String prefix, int limit, Consumer<String> action) { List<String> queueUrls = lister.apply(prefix); if (queueUrls.size() >= limit) { // Manually work around the 1000 queue limit by forking for each // possible next character. Yes this is exponential with a factor of // 64, but we only fork when the results are more than 1000. VALID_QUEUE_NAME_CHARACTERS .chars() .parallel() .forEach(acceptIntOn(executor, c -> forEachQueue(executor, lister, prefix + (char)c, limit, action))); } else { queueUrls.forEach(acceptOn(executor, action)); } } public static List<String> listQueues(ExecutorService executor, Function<String, List<String>> lister, String prefix, int limit) { List<String> queueUrls = lister.apply(prefix); if (queueUrls.size() >= limit) { // Manually work around the 1000 queue limit by forking for each // possible next character. Yes this is exponential with a factor of // 64, but we only fork when the results are more than 1000. return VALID_QUEUE_NAME_CHARACTERS .chars() .parallel() .mapToObj(applyIntOn(executor, c -> listQueues(executor, lister, prefix + (char)c, limit))) .map(List::stream) .flatMap(Function.identity()) .collect(Collectors.toList()); } else { return queueUrls; } } public static CreateQueueRequest copyWithExtraAttributes(CreateQueueRequest request, Map<String, String> extraAttrs) { Map<String, String> newAttributes = new HashMap<>(request.getAttributes()); newAttributes.putAll(extraAttrs); // Clone to create a shallow copy that includes the superclass properties. return request.clone() .withQueueName(request.getQueueName()) .withAttributes(newAttributes); } public static SendMessageRequest copyWithExtraAttributes(SendMessageRequest request, Map<String, MessageAttributeValue> extraAttrs) { Map<String, MessageAttributeValue> newAttributes = new HashMap<>(request.getMessageAttributes()); newAttributes.putAll(extraAttrs); // Clone to create a shallow copy that includes the superclass properties. return request.clone() .withQueueUrl(request.getQueueUrl()) .withMessageBody(request.getMessageBody()) .withMessageAttributes(newAttributes) .withDelaySeconds(request.getDelaySeconds()); } /** * this method carefully waits for futures. If waiting throws, it converts the exceptions to the * exceptions that SQS clients expect. This is what we use to turn asynchronous calls into * synchronous ones. */ // TODO-RS: Copied from QueueBuffer in the buffered asynchronous client public static <V> V waitForFuture(Future<V> future) { V toReturn = null; try { toReturn = future.get(); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); throw new AmazonClientException( "Thread interrupted while waiting for execution result", ie); } catch (ExecutionException ee) { // if the cause of the execution exception is an SQS exception, extract it // and throw the extracted exception to the clients // otherwise, wrap ee in an SQS exception and throw that. Throwable cause = ee.getCause(); if (cause instanceof AmazonClientException) { throw (AmazonClientException) cause; } throw new AmazonClientException( "Caught an exception while waiting for request to complete...", ee); } return toReturn; } }