com.amazonaws.services.sqs.buffered.ReceiveQueueBuffer Maven / Gradle / Ivy
/*
* Copyright 2012-2013 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.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.amazonaws.AmazonClientException;
import com.amazonaws.AmazonWebServiceRequest;
import com.amazonaws.handlers.AsyncHandler;
import com.amazonaws.services.sqs.AmazonSQS;
import com.amazonaws.services.sqs.model.ChangeMessageVisibilityBatchRequest;
import com.amazonaws.services.sqs.model.ChangeMessageVisibilityBatchRequestEntry;
import com.amazonaws.services.sqs.model.GetQueueAttributesRequest;
import com.amazonaws.services.sqs.model.Message;
import com.amazonaws.services.sqs.model.ReceiveMessageRequest;
import com.amazonaws.services.sqs.model.ReceiveMessageResult;
/**
* The ReceiveQueueBuffer class is responsible for dequeueing of messages from
* a single SQS queue. It uses the provided executor to pre-fetch messages from the server
* and keeps them in a buffer which it uses to satisfy incoming requests. The number of requests
* pre-fetched and kept in the buffer, as well as the maximum number of threads used to retrieve
* the messages are configurable.
*
* Synchronization strategy:
* - Threads must hold the TaskSpawnSyncPoint object monitor to spawn a new task or modify
* the number of inflight tasks
* - Threads must hold the monitor of the "futures" list to modify the list
* - Threads must hold the monitor of the "finishedTasks" list to modify the list
* - If you need to lock both futures and finishedTasks, lock futures first and
* finishedTasks second
* */
public class ReceiveQueueBuffer {
private static Log log = LogFactory.getLog(ReceiveQueueBuffer.class);
private final QueueBufferConfig config;
private final String qUrl;
private final Executor executor;
private final AmazonSQS sqsClient;
private long bufferCounter = 0;
/**
* This buffer's queue visibility timeout. Used to detect expired message
* that should not be returned by the {@code receiveMessage} call.
* Synchronized by {@code receiveMessageLock}. -1 indicates that the time is
* uninitialized.
*/
private volatile long visibilityTimeoutNanos = -1;
/**
* Used as permits controlling the number of in flight receive batches.
* Synchronized by {@code taskSpawnSyncPoint}.
*/
private volatile int inflightReceiveMessageBatches;
/**
* synchronize on this object to create new receive batches or modify
* inflight message count
*/
private final Object taskSpawnSyncPoint = new Object();
/** shutdown buffer does not retrieve any more messages from sqs */
volatile boolean shutDown = false;
/** message delivery futures we gave out */
private final LinkedList< ReceiveMessageFuture > futures = new LinkedList();
/** finished batches are stored in this list. */
private LinkedList finishedTasks = new LinkedList();
ReceiveQueueBuffer( AmazonSQS paramSQS, Executor paramExecutor, QueueBufferConfig paramConfig, String url ) {
config = paramConfig;
executor = paramExecutor;
sqsClient = paramSQS;
qUrl = url;
}
/**
* Prevents spawning of new retrieval batches and waits for all in-flight
* retrieval batches to finish
* */
public void shutdown() {
shutDown = true;
try {
while ( inflightReceiveMessageBatches > 0 )
Thread.sleep(100);
} catch( InterruptedException e) {
Thread.currentThread().interrupt();
}
}
/**
* Submits the request for retrieval of messages from the queue and returns
* a future that will be signalled when the request is satisfied. The future
* may already be signalled by the time it is returned.
*
* @return never null
* */
public QueueBufferFuture
receiveMessageAsync( ReceiveMessageRequest rq, QueueBufferCallback callback ) {
if (shutDown) {
throw new AmazonClientException("The client has been shut down.");
}
//issue the future...
int numMessages = 10;
if ( rq.getMaxNumberOfMessages() != null ) {
numMessages = rq.getMaxNumberOfMessages();
}
QueueBufferFuture toReturn = issueFuture(numMessages, callback);
//attempt to satisfy it right away...
satisfyFuturesFromBuffer();
//spawn more receive tasks if we need them...
spawnMoreReceiveTasks();
return toReturn;
}
/**
* Creates and returns a new future object. Sleeps if the list of
* already-issued but as yet unsatisfied futures is over a throttle limit.
*
* @return never null
*/
private ReceiveMessageFuture issueFuture( int size, QueueBufferCallback callback) {
synchronized( futures ) {
ReceiveMessageFuture theFuture = new ReceiveMessageFuture(callback, size);
futures.addLast(theFuture);
return theFuture;
}
}
/**
* Attempts to satisfy some or all of the already-issued futures from the
* local buffer. If the buffer is empty or there are no futures, this method
* won't do anything.
* */
private void satisfyFuturesFromBuffer() {
synchronized( futures ) {
synchronized( finishedTasks ) {
//attempt to satisfy futures until we run out of either futures or
//finished tasks
while ( (!futures.isEmpty()) && (!finishedTasks.isEmpty()) ) {
ReceiveMessageFuture currentFuture = futures.poll();
fillFuture( currentFuture);
}
}
}
}
/**
* Fills the future with whatever results were received by the full batch
* currently at the head of the completed batch queue. Those results may be retrieved
* messages, or an exception.
*
* this method assumes that you are holding the finished tasks lock
* locks when invoking it. violate this assumption at
* your own peril */
private void fillFuture( ReceiveMessageFuture f ){
ReceiveMessageResult r = new ReceiveMessageResult();
LinkedList messages = new LinkedList();
r.setMessages(messages);
Exception exception = null;
if ( !finishedTasks.isEmpty() ) {
ReceiveMessageBatchTask t = finishedTasks.getFirst();
exception = t.getException();
int retrieved = 0;
boolean batchDone = false;
while ( retrieved < f.getRequestedSize() )
{
Message m = t.removeMessage();
// a non-empty batch can still give back a null
// message if the message expired.
if ( null != m) {
messages.add(m);
++retrieved;
}
else {
batchDone = true;
break;
}
}
//we may have just drained the batch.
batchDone = batchDone || t.isEmpty() || ( exception != null );
if ( batchDone) {
finishedTasks.removeFirst();
}
r.setMessages(messages);
}
//if after the above runs the exception is not null,
//the finished batch has encountered an error, and we will
//report that in the Future. Otherwise, we will fill
//the future with the receive result
if ( exception != null )
f.setFailure(exception);
else
f.setSuccess(r);
//now, a bit of maintenance. remove empty non-exception-bearing
//batches so we can get new ones.
while ( !finishedTasks.isEmpty() ) {
ReceiveMessageBatchTask t = finishedTasks.getFirst();
if ( (!t.isEmpty()) || (t.getException() != null) ) {
//if we found a finished task that has useful content,
//our cleanup is done
break;
}
//throw away the empty batch.
finishedTasks.removeFirst();
}
}
/**
* maybe create more receive tasks. extra receive tasks won't be created if
* we are already at the maximum number of receive tasks, or if we are at
* the maximum number of prefetched buffers
*/
private void spawnMoreReceiveTasks() {
if( shutDown )
return;
int desiredBatches = config.getMaxDoneReceiveBatches();
desiredBatches = desiredBatches < 1 ? 1 : desiredBatches;
synchronized( finishedTasks ) {
if ( finishedTasks.size() >= desiredBatches )
return;
//if we have some finished batches already, and
//existing inflight batches will bring us to the limit,
//don't spawn more. if our finished tasks cache is empty, we will
//always spawn a thread.
if ( finishedTasks.size() > 0 && ( finishedTasks.size() + inflightReceiveMessageBatches ) >= desiredBatches )
return;
}
synchronized (taskSpawnSyncPoint) {
if (visibilityTimeoutNanos == -1) {
GetQueueAttributesRequest request = new GetQueueAttributesRequest().
withQueueUrl(qUrl).
withAttributeNames("VisibilityTimeout");
ResultConverter.appendUserAgent(request, AmazonSQSBufferedAsyncClient.USER_AGENT);
long visibilityTimeoutSeconds = Long.parseLong(sqsClient.getQueueAttributes( request ).getAttributes().get("VisibilityTimeout"));
visibilityTimeoutNanos = TimeUnit.NANOSECONDS.convert(visibilityTimeoutSeconds, TimeUnit.SECONDS);
}
int max = config.getMaxInflightReceiveBatches();
//must allow at least one inflight receive task, or receive won't
//work at all.
max = max > 0 ? max : 1;
int toSpawn = max - inflightReceiveMessageBatches;
if (toSpawn > 0) {
ReceiveMessageBatchTask task = new ReceiveMessageBatchTask(this );
++inflightReceiveMessageBatches;
++bufferCounter;
if (log.isTraceEnabled()) {
log.trace("Spawned receive batch #" + bufferCounter + " (" + inflightReceiveMessageBatches
+ " of " + max + " inflight) for queue " + qUrl);
}
executor.execute(task);
}
}
}
/**
* This method is called by the batches after they have finished retrieving
* the messages.
*
* */
void reportBatchFinished( ReceiveMessageBatchTask batch )
{
synchronized( finishedTasks ) {
finishedTasks.addLast( batch );
if ( log.isTraceEnabled() ) {
log.info("Queue " + qUrl + " now has " + finishedTasks.size() + " receive results cached ");
}
}
synchronized( taskSpawnSyncPoint ) {
--inflightReceiveMessageBatches;
}
satisfyFuturesFromBuffer();
spawnMoreReceiveTasks();
}
/**
* Clears and nacks any pre-fetched messages in this buffer.
*/
public void clear() {
boolean done = false;
while ( !done ) {
ReceiveMessageBatchTask currentBatch = null;
synchronized (finishedTasks) {
currentBatch = finishedTasks.poll();
}
if ( currentBatch != null ) {
currentBatch.clear();
} else {
//ran out of batches to clear
done = true;
}
}
}
private class ReceiveMessageFuture extends QueueBufferFuture < ReceiveMessageRequest, ReceiveMessageResult >
{
/* how many messages did the request ask for*/
private int requestedSize;
ReceiveMessageFuture( int paramSize ) {
this(null,paramSize);
}
ReceiveMessageFuture( QueueBufferCallback cb, int paramSize ) {
super(cb);
requestedSize = paramSize;
}
public int getRequestedSize() {
return requestedSize;
}
}
/**
* Task to receive messages from SQS.
*
* The batch task is constructed {@code !open} until the
* {@code ReceiveMessage} completes. At that point, the batch opens and its
* messages (if any) become available to read.
*/
private class ReceiveMessageBatchTask implements Runnable {
private Exception exception = null;
private List messages;
private long visibilityDeadlineNano;
private boolean open = false;
private ReceiveQueueBuffer parentBuffer;
/**
* Constructs a receive task waiting the specified time before calling
* SQS.
*
* @param waitTimeMs
* the time to wait before calling SQS
*/
ReceiveMessageBatchTask(ReceiveQueueBuffer paramParentBuffer) {
parentBuffer = paramParentBuffer;
messages = Collections.emptyList();
}
synchronized int getSize() {
if (!open)
throw new IllegalStateException("batch is not open");
return messages.size();
}
synchronized boolean isEmpty() {
if (!open)
throw new IllegalStateException("batch is not open");
return messages.isEmpty();
}
/** @return the exception that was thrown during execution, or null
* if there was no exception */
synchronized Exception getException() {
if (!open)
throw new IllegalStateException("batch is not open");
return exception;
}
/**
* Returns a message if one is available.
*
* The call adjusts the message count.
*
* @return a message or {@code null} if none is available
*/
synchronized Message removeMessage() {
if (!open)
throw new IllegalStateException("batch is not open");
// our messages expired.
if ( System.nanoTime() > visibilityDeadlineNano ) {
messages.clear();
return null;
}
if (messages.isEmpty())
return null;
else
return messages.remove(messages.size() - 1);
}
/**
* Nacks and clears all messages remaining in the batch.
*/
synchronized void clear() {
if (!open)
throw new IllegalStateException("batch is not open");
if (System.nanoTime() < visibilityDeadlineNano) {
ChangeMessageVisibilityBatchRequest batchRequest = new ChangeMessageVisibilityBatchRequest()
.withQueueUrl(qUrl);
ResultConverter.appendUserAgent(batchRequest, AmazonSQSBufferedAsyncClient.USER_AGENT);
List entries =
new ArrayList(messages.size());
int i = 0;
for (Message m : messages) {
entries.add(new ChangeMessageVisibilityBatchRequestEntry()
.withId(Integer.toString(i))
.withReceiptHandle(m.getReceiptHandle())
.withVisibilityTimeout(0));
++i;
}
try {
batchRequest.setEntries(entries);
sqsClient.changeMessageVisibilityBatch(batchRequest);
} catch (AmazonClientException e) {
// Log and ignore.
log.warn("ReceiveMessageBatchTask: changeMessageVisibility failed " + e);
}
}
messages.clear();
}
/**
* Attempts to retrieve messages from SQS and upon completion (successful or
* unsuccessful) reports the batch as complete and open
* */
public void run() {
try {
visibilityDeadlineNano = System.nanoTime() + visibilityTimeoutNanos;
ReceiveMessageRequest request = new ReceiveMessageRequest(qUrl).withMaxNumberOfMessages(config.getMaxBatchSize());
ResultConverter.appendUserAgent(request, AmazonSQSBufferedAsyncClient.USER_AGENT);
if ( config.getVisibilityTimeoutSeconds() > 0 ) {
request.setVisibilityTimeout(config.getVisibilityTimeoutSeconds());
visibilityDeadlineNano = System.nanoTime() + TimeUnit.NANOSECONDS.convert(config.getVisibilityTimeoutSeconds(), TimeUnit.SECONDS);
}
if ( config.isLongPoll() ) {
request.withWaitTimeSeconds(config.getLongPollWaitTimeoutSeconds());
}
messages = sqsClient.receiveMessage(request).getMessages();
} catch (AmazonClientException e) {
exception = e;
} finally {
//whatever happened, we are done and can be considered open
open = true;
parentBuffer.reportBatchFinished(this);
}
}
}
} //end of ReceiveQueueBuffer