All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.amazonaws.services.sqs.buffered.SendQueueBuffer Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2012-2015 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 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;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

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;


/**
 * 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. 
* * 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.
* * 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 { 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 callback) { QueueBufferFuture result = submitOutboundRequest(sendMessageLock, openSendMessageBatchTask, request, inflightSendMessageBatches, callback); return result; } /** * @return never null */ public QueueBufferFuture deleteMessage(DeleteMessageRequest request, QueueBufferCallback callback) { return submitOutboundRequest(deleteMessageLock, openDeleteMessageBatchTask, request, inflightDeleteMessageBatches, callback); } /** * @return never null */ public QueueBufferFuture changeMessageVisibility(ChangeMessageVisibilityRequest request, QueueBufferCallback callback) { return submitOutboundRequest( changeMessageVisibilityLock, openChangeMessageVisibilityBatchTask, request, inflightChangeMessageVisibilityBatches, callback); } /** * @return new {@code OutboundBatchTask} of appropriate type, never null */ @SuppressWarnings("unchecked") private OutboundBatchTask newOutboundBatchTask( R request) { if (request instanceof SendMessageRequest) return (OutboundBatchTask) new SendMessageBatchTask(); else if (request instanceof DeleteMessageRequest) return (OutboundBatchTask) new DeleteMessageBatchTask(); else if (request instanceof ChangeMessageVisibilityRequest) return (OutboundBatchTask) 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. *

* 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. *

* * @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") , R extends AmazonWebServiceRequest, Result> QueueBufferFuture submitOutboundRequest( Object operationLock, OBT[] openOutboundBatchTask, R request, final Semaphore inflightOperationBatches, QueueBufferCallback 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 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>() { public void invoke(OutboundBatchTask 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. *

* 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. *

* Instances of this class (and subclasses) are thread-safe. * * @param * the type of the SQS request to batch * @param * the type of result he futures issued by this task will return */ private abstract class OutboundBatchTask implements Runnable { final List requests; final ArrayList> futures; AtomicBoolean open = new AtomicBoolean(true); volatile Listener> onCompleted = null; OutboundBatchTask() { requests = new ArrayList(config.getMaxBatchSize()); futures = new ArrayList>(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 addRequest(R request, QueueBufferCallback callback) { if (!open.get()) return null; QueueBufferFuture 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 addIfAllowed(R request, QueueBufferCallback callback) { if (isOkToAdd(request)) { requests.add(request); QueueBufferFuture theFuture = new QueueBufferFuture(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> completionListener = onCompleted; if (completionListener != null) completionListener.invoke(this); } } private void failAll( Exception e) { for( QueueBufferFuture f : futures ) { f.setFailure(e); } } } private class SendMessageBatchTask extends OutboundBatchTask { 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 entries = new ArrayList( 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 { @Override void process() { if (requests.isEmpty()) return; DeleteMessageBatchRequest batchRequest = new DeleteMessageBatchRequest() .withQueueUrl(qUrl); ResultConverter.appendUserAgent(batchRequest, AmazonSQSBufferedAsyncClient.USER_AGENT); List entries = new ArrayList( 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 { @Override void process() { if (requests.isEmpty()) return; ChangeMessageVisibilityBatchRequest batchRequest = new ChangeMessageVisibilityBatchRequest() .withQueueUrl(qUrl); ResultConverter.appendUserAgent(batchRequest, AmazonSQSBufferedAsyncClient.USER_AGENT); List entries = new ArrayList( 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); } } } } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy