/* * Copyright 2012 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at * * http://aws.amazon.com/apache2.0 * * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.sqs.buffered; import com.amazonaws.AmazonClientException; import com.amazonaws.AmazonWebServiceRequest; import com.amazonaws.services.sqs.AmazonSQS; import com.amazonaws.services.sqs.model.BatchResultErrorEntry; import com.amazonaws.services.sqs.model.ChangeMessageVisibilityBatchRequest; import com.amazonaws.services.sqs.model.ChangeMessageVisibilityBatchRequestEntry; import com.amazonaws.services.sqs.model.ChangeMessageVisibilityBatchResult; import com.amazonaws.services.sqs.model.ChangeMessageVisibilityBatchResultEntry; import com.amazonaws.services.sqs.model.ChangeMessageVisibilityRequest; import com.amazonaws.services.sqs.model.DeleteMessageBatchRequest; import com.amazonaws.services.sqs.model.DeleteMessageBatchRequestEntry; import com.amazonaws.services.sqs.model.DeleteMessageBatchResult; import com.amazonaws.services.sqs.model.DeleteMessageBatchResultEntry; import com.amazonaws.services.sqs.model.DeleteMessageRequest; import com.amazonaws.services.sqs.model.SendMessageBatchRequest; import com.amazonaws.services.sqs.model.SendMessageBatchRequestEntry; import com.amazonaws.services.sqs.model.SendMessageBatchResult; import com.amazonaws.services.sqs.model.SendMessageBatchResultEntry; import com.amazonaws.services.sqs.model.SendMessageRequest; import com.amazonaws.services.sqs.model.SendMessageResult; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; /** * This class is responsible for buffering outgoing SQS requests, i.e. requests * to send a message, delete a message and change the visibility of the message. <br> * When a request arrives, the buffer adds the message to a message batch of an * appropriate type (creating such a batch if there currently isn't one * outstanding). When the outstanding batch becomes full, or when a configurable * timeout expires, the buffer makes a call to SQS to execute the current batch. <br> * Internally, the batch objects maintain a list of futures corresponding to the * requests added to them. When a batch completes, it loads the results into the * futures and marks the futures as complete. */ public class SendQueueBuffer { private static Log log = LogFactory.getLog(SendQueueBuffer.class); // Interface to support event notifications with a parameter. private interface Listener<T> { void invoke(T o); }; /** Config settings for this buffer */ private final QueueBufferConfig config; /** Url of our queue */ private final String qUrl; /** * The {@code AmazonSQS} client to use for this buffer's operations. */ private final AmazonSQS sqsClient; /** * The executor service for the batching tasks. */ private final Executor executor; /** * Object used to serialize sendMessage calls. */ private final Object sendMessageLock = new Object(); /** * Object used to serialize deleteMessage calls. */ private final Object deleteMessageLock = new Object(); /** * Object used to serialize changeMessageVisibility calls. */ private final Object changeMessageVisibilityLock = new Object(); /** * Current batching task for sendMessage. Using a size 1 array to allow * "passing by reference". Synchronized by {@code sendMessageLock}. */ private final SendMessageBatchTask[] openSendMessageBatchTask = new SendMessageBatchTask[1]; /** * Current batching task for deleteMessage. Using a size 1 array to allow * "passing by reference". Synchronized by {@code deleteMessageLock}. */ private final DeleteMessageBatchTask[] openDeleteMessageBatchTask = new DeleteMessageBatchTask[1]; /** * Current batching task for changeMessageVisibility. Using a size 1 array * to allow "passing by reference". Synchronized by * {@code changeMessageVisibilityLock}. */ private final ChangeMessageVisibilityBatchTask[] openChangeMessageVisibilityBatchTask = new ChangeMessageVisibilityBatchTask[1]; /** * Permits controlling the number of in flight SendMessage batches. */ private final Semaphore inflightSendMessageBatches; /** * Permits controlling the number of in flight DeleteMessage batches. */ private final Semaphore inflightDeleteMessageBatches; /** * Permits controlling the number of in flight ChangeMessageVisibility * batches. */ private final Semaphore inflightChangeMessageVisibilityBatches; SendQueueBuffer(AmazonSQS sqsClient, Executor executor, QueueBufferConfig paramConfig, String url) { this.sqsClient = sqsClient; this.executor = executor; this.config = paramConfig; qUrl = url; int maxBatch = config.getMaxInflightOutboundBatches(); // must allow at least one outbound batch. maxBatch = maxBatch > 0 ? maxBatch : 1; this.inflightSendMessageBatches = new Semaphore(maxBatch); this.inflightDeleteMessageBatches = new Semaphore(maxBatch); this.inflightChangeMessageVisibilityBatches = new Semaphore(maxBatch); } public QueueBufferConfig getConfig() { return config; } /** * @return never null */ public QueueBufferFuture<SendMessageRequest, SendMessageResult> sendMessage( SendMessageRequest request, QueueBufferCallback<SendMessageRequest, SendMessageResult> callback) { QueueBufferFuture<SendMessageRequest, SendMessageResult> result = submitOutboundRequest(sendMessageLock, openSendMessageBatchTask, request, inflightSendMessageBatches, callback); return result; } /** * @return never null */ public QueueBufferFuture<DeleteMessageRequest, Void> deleteMessage( DeleteMessageRequest request, QueueBufferCallback<DeleteMessageRequest, Void> callback) { return submitOutboundRequest(deleteMessageLock, openDeleteMessageBatchTask, request, inflightDeleteMessageBatches, callback); } /** * @return never null */ public QueueBufferFuture<ChangeMessageVisibilityRequest, Void> changeMessageVisibility( ChangeMessageVisibilityRequest request, QueueBufferCallback<ChangeMessageVisibilityRequest, Void> callback) { return submitOutboundRequest( changeMessageVisibilityLock, openChangeMessageVisibilityBatchTask, request, inflightChangeMessageVisibilityBatches, callback); } /** * @return new {@code OutboundBatchTask} of appropriate type, never null */ @SuppressWarnings("unchecked") private <R extends AmazonWebServiceRequest, Result> OutboundBatchTask<R, Result> newOutboundBatchTask( R request) { if (request instanceof SendMessageRequest) return (OutboundBatchTask<R, Result>) new SendMessageBatchTask(); else if (request instanceof DeleteMessageRequest) return (OutboundBatchTask<R, Result>) new DeleteMessageBatchTask(); else if (request instanceof ChangeMessageVisibilityRequest) return (OutboundBatchTask<R, Result>) new ChangeMessageVisibilityBatchTask(); else // this should never happen throw new IllegalArgumentException("Unsupported request type " + request.getClass().getName()); } /** * Flushes all outstanding outbound requests ({@code SendMessage}, * {@code DeleteMessage}, {@code ChangeMessageVisibility}) in this buffer. * <p> * The call returns successfully when all outstanding outbound requests * submitted before the call are completed (i.e. processed by SQS). */ public void flush() { try { synchronized (sendMessageLock) { inflightSendMessageBatches .acquire(config.getMaxInflightOutboundBatches()); inflightSendMessageBatches .release(config.getMaxInflightOutboundBatches()); } synchronized (deleteMessageLock) { inflightDeleteMessageBatches .acquire(config.getMaxInflightOutboundBatches()); inflightDeleteMessageBatches .release(config.getMaxInflightOutboundBatches()); } synchronized (changeMessageVisibilityLock) { inflightChangeMessageVisibilityBatches .acquire(config.getMaxInflightOutboundBatches()); inflightChangeMessageVisibilityBatches .release(config.getMaxInflightOutboundBatches()); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } /** * Submits an outbound request for delivery to the queue associated with * this buffer. * <p> * * @param operationLock the lock synchronizing calls for the call type ( * {@code sendMessage}, {@code deleteMessage}, * {@code changeMessageVisibility} ) * @param openOutboundBatchTask the open batch task for this call type * @param request the request to submit * @param inflightOperationBatches the permits controlling the batches for * this type of request * @return never null * @throws AmazonClientException (see the various outbound calls for * details) */ @SuppressWarnings("unchecked") <OBT extends OutboundBatchTask<R, Result>, R extends AmazonWebServiceRequest, Result> QueueBufferFuture<R, Result> submitOutboundRequest( Object operationLock, OBT[] openOutboundBatchTask, R request, final Semaphore inflightOperationBatches, QueueBufferCallback<R, Result> callback) { /* * Callers add requests to a single batch task (openOutboundBatchTask) * until it is full or maxBatchOpenMs elapses. The total number of batch * task in flight is controlled by the inflightOperationBatch semaphore * capped at maxInflightOutboundBatches. */ QueueBufferFuture<R, Result> theFuture = null; try { synchronized (operationLock) { if (openOutboundBatchTask[0] == null || ((theFuture = openOutboundBatchTask[0].addRequest(request, callback))) == null) { OBT obt = (OBT) newOutboundBatchTask(request); inflightOperationBatches.acquire(); openOutboundBatchTask[0] = obt; // Register a listener for the event signaling that the // batch task has completed (successfully or not). openOutboundBatchTask[0].onCompleted = new Listener<OutboundBatchTask<R, Result>>() { @Override public void invoke(OutboundBatchTask<R, Result> task) { inflightOperationBatches.release(); } }; if (log.isTraceEnabled()) { log.trace("Queue " + qUrl + " created new batch for " + request.getClass().toString() + " " + inflightOperationBatches.availablePermits() + " free slots remain"); } theFuture = openOutboundBatchTask[0].addRequest(request, callback); executor.execute(openOutboundBatchTask[0]); if (null == theFuture) { // this can happen only if the request itself is flawed, // so that it can't be added to any batch, even a brand // new one throw new AmazonClientException("Failed to schedule request " + request + " for execution"); } } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); AmazonClientException toThrow = new AmazonClientException( "Interrupted while waiting for lock."); toThrow.initCause(e); throw toThrow; } return theFuture; } /** * Task to send a batch of outbound requests to SQS. * <p> * The batch task is constructed open and accepts requests until full, or * until {@code maxBatchOpenMs} elapses. At that point, the batch closes and * the collected requests are assembled into a single batch request to SQS. * Specialized for each type of outbound request. * <p> * Instances of this class (and subclasses) are thread-safe. * * @param <R> the type of the SQS request to batch * @param <Result> the type of result he futures issued by this task will * return */ private abstract class OutboundBatchTask<R extends AmazonWebServiceRequest, Result> implements Runnable { final List<R> requests; final ArrayList<QueueBufferFuture<R, Result>> futures; AtomicBoolean open = new AtomicBoolean(true); volatile Listener<OutboundBatchTask<R, Result>> onCompleted = null; OutboundBatchTask() { requests = new ArrayList<R>(config.getMaxBatchSize()); futures = new ArrayList<QueueBufferFuture<R, Result>>(config.getMaxBatchSize()); } /** * Adds a request to the batch if it is still open and has capacity. * * @return the future that can be used to get the results of the * execution, or null if the addition failed. */ synchronized QueueBufferFuture<R, Result> addRequest(R request, QueueBufferCallback<R, Result> callback) { if (!open.get()) return null; QueueBufferFuture<R, Result> theFuture = addIfAllowed(request, callback); // if the addition did not work, or this addition made us full, // we can close the request if ((null == theFuture) || isFull()) { open.set(false); } // the batch request is as full as it will ever be. no need to wait // for the timeout, we can run it now. if (!open.get()) notify(); return theFuture; } /** * Adds the request to the batch if capacity allows it. * * @param request * @return the future that will be signaled when the request is * completed and can be used to retrieve the result. Can be null * if the addition could not be done */ synchronized QueueBufferFuture<R, Result> addIfAllowed(R request, QueueBufferCallback<R, Result> callback) { if (isOkToAdd(request)) { requests.add(request); QueueBufferFuture<R, Result> theFuture = new QueueBufferFuture<R, Result>(callback); futures.add(theFuture); onRequestAdded(request); return theFuture; } else return null; } protected synchronized boolean isOkToAdd(R request) { return requests.size() < config.getMaxBatchSize(); } protected synchronized void onRequestAdded(R request) { // to be overridden by subclasses } /** * @return whether the buffer is filled to capacity */ synchronized boolean isFull() { return requests.size() >= config.getMaxBatchSize(); } /** * Processes the batch once closed. */ abstract void process(); @Override public synchronized void run() { try { long deadlineMs = TimeUnit.MILLISECONDS.convert(System.nanoTime(), TimeUnit.NANOSECONDS) + config.getMaxBatchOpenMs() + 1; long t = TimeUnit.MILLISECONDS.convert(System.nanoTime(), TimeUnit.NANOSECONDS); while (open.get() && (t < deadlineMs)) { t = TimeUnit.MILLISECONDS.convert(System.nanoTime(), TimeUnit.NANOSECONDS); // zero means "wait forever", can't have that. long toWait = Math.max(1, deadlineMs - t); wait(toWait); } open.set(false); process(); } catch (InterruptedException e) { failAll(e); } catch (AmazonClientException e) { failAll(e); } catch (RuntimeException e) { failAll(e); throw e; } catch (Error e) { failAll(new AmazonClientException("Error encountered", e)); throw e; } finally { // make a copy of the listener since it can be modified from // outside Listener<OutboundBatchTask<R, Result>> completionListener = onCompleted; if (completionListener != null) completionListener.invoke(this); } } private void failAll(Exception e) { for (QueueBufferFuture<R, Result> f : futures) { f.setFailure(e); } } } private class SendMessageBatchTask extends OutboundBatchTask<SendMessageRequest, SendMessageResult> { int batchSizeBytes = 0; @Override protected synchronized boolean isOkToAdd(SendMessageRequest request) { return (requests.size() < config.getMaxBatchSize()) && ((request.getMessageBody().getBytes().length + batchSizeBytes) < config .getMaxBatchSizeBytes()); } @Override protected void onRequestAdded(SendMessageRequest request) { batchSizeBytes += request.getMessageBody().getBytes().length; } @Override synchronized boolean isFull() { return (requests.size() >= config.getMaxBatchSize()) || (batchSizeBytes >= config.getMaxBatchSizeBytes()); } @Override void process() { if (requests.isEmpty()) return; SendMessageBatchRequest batchRequest = new SendMessageBatchRequest() .withQueueUrl(qUrl); ResultConverter.appendUserAgent(batchRequest, AmazonSQSBufferedAsyncClient.USER_AGENT); List<SendMessageBatchRequestEntry> entries = new ArrayList<SendMessageBatchRequestEntry>( requests.size()); for (int i = 0, n = requests.size(); i < n; i++) entries.add(new SendMessageBatchRequestEntry() .withId(Integer.toString(i)) .withMessageBody(requests.get(i).getMessageBody()) .withDelaySeconds(requests.get(i).getDelaySeconds()) .withMessageAttributes(requests.get(i).getMessageAttributes())); batchRequest.setEntries(entries); SendMessageBatchResult batchResult = sqsClient .sendMessageBatch(batchRequest); for (SendMessageBatchResultEntry entry : batchResult .getSuccessful()) { int index = Integer.parseInt(entry.getId()); futures.get(index).setSuccess(ResultConverter.convert(entry)); } for (BatchResultErrorEntry errorEntry : batchResult.getFailed()) { int index = Integer.parseInt(errorEntry.getId()); if (errorEntry.isSenderFault()) { futures.get(index).setFailure(ResultConverter.convert(errorEntry)); } else { // retry. try { // this will retry internally up to 3 times. futures.get(index).setSuccess(sqsClient.sendMessage(requests.get(index))); } catch (AmazonClientException ace) { futures.get(index).setFailure(ace); } } } } } private class DeleteMessageBatchTask extends OutboundBatchTask<DeleteMessageRequest, Void> { @Override void process() { if (requests.isEmpty()) return; DeleteMessageBatchRequest batchRequest = new DeleteMessageBatchRequest() .withQueueUrl(qUrl); ResultConverter.appendUserAgent(batchRequest, AmazonSQSBufferedAsyncClient.USER_AGENT); List<DeleteMessageBatchRequestEntry> entries = new ArrayList<DeleteMessageBatchRequestEntry>( requests.size()); for (int i = 0, n = requests.size(); i < n; i++) entries.add(new DeleteMessageBatchRequestEntry().withId( Integer.toString(i)).withReceiptHandle( requests.get(i).getReceiptHandle())); batchRequest.setEntries(entries); DeleteMessageBatchResult batchResult = sqsClient .deleteMessageBatch(batchRequest); for (DeleteMessageBatchResultEntry entry : batchResult .getSuccessful()) { int index = Integer.parseInt(entry.getId()); futures.get(index).setSuccess(null); } for (BatchResultErrorEntry errorEntry : batchResult.getFailed()) { int index = Integer.parseInt(errorEntry.getId()); if (errorEntry.isSenderFault()) { futures.get(index).setFailure(ResultConverter.convert(errorEntry)); } else { try { // retry. sqsClient.deleteMessage(requests.get(index)); futures.get(index).setSuccess(null); } catch (AmazonClientException ace) { futures.get(index).setFailure(ace); } } } } } private class ChangeMessageVisibilityBatchTask extends OutboundBatchTask<ChangeMessageVisibilityRequest, Void> { @Override void process() { if (requests.isEmpty()) return; ChangeMessageVisibilityBatchRequest batchRequest = new ChangeMessageVisibilityBatchRequest() .withQueueUrl(qUrl); ResultConverter.appendUserAgent(batchRequest, AmazonSQSBufferedAsyncClient.USER_AGENT); List<ChangeMessageVisibilityBatchRequestEntry> entries = new ArrayList<ChangeMessageVisibilityBatchRequestEntry>( requests.size()); for (int i = 0, n = requests.size(); i < n; i++) entries.add(new ChangeMessageVisibilityBatchRequestEntry() .withId(Integer.toString(i)) .withReceiptHandle(requests.get(i).getReceiptHandle()) .withVisibilityTimeout( requests.get(i).getVisibilityTimeout())); batchRequest.setEntries(entries); ChangeMessageVisibilityBatchResult batchResult = sqsClient .changeMessageVisibilityBatch(batchRequest); for (ChangeMessageVisibilityBatchResultEntry entry : batchResult .getSuccessful()) { int index = Integer.parseInt(entry.getId()); futures.get(index).setSuccess(null); } for (BatchResultErrorEntry errorEntry : batchResult.getFailed()) { int index = Integer.parseInt(errorEntry.getId()); if (errorEntry.isSenderFault()) { futures.get(index).setFailure(ResultConverter.convert(errorEntry)); } else { try { // retry. sqsClient.changeMessageVisibility(requests.get(index)); futures.get(index).setSuccess(null); } catch (AmazonClientException ace) { futures.get(index).setFailure(ace); } } } } } }