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

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

Go to download

The AWS Java SDK for Amazon SQS module holds the client classes that are used for communicating with Amazon Simple Queue Service

There is a newer version: 1.12.778
Show newest version
/*
 * Copyright 2012-2024 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.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import com.amazonaws.services.sqs.model.QueueAttributeName;
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.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 *
      *
    • * If you need to lock both futures and an individual future, lock futures first and the future second *
    • *
    *
  • *
  • * 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 { /** * Simple interface to represent a condition * * @param */ private interface Predicate { /** * @param t * Object being evaluated against the condition * @return True if t meets the condition, false if not */ boolean test(T t); } private static Log log = LogFactory.getLog(ReceiveQueueBuffer.class); private final QueueBufferConfig config; private final String qUrl; private final Executor executor; private final ScheduledExecutorService waitTimer = Executors.newSingleThreadScheduledExecutor(); 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; /** * This buffer's queue default receive wait time. Used to set the timeout on futures so they complete * according to when the synchronous call to SQS would have. Synchronized by {@code futures}. -1 * indicates that the time is uninitialized. */ private volatile int defaultWaitTimeSeconds = -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 Set futures = new LinkedHashSet(); /** 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(); } waitTimer.shutdown(); try { waitTimer.awaitTermination(20, TimeUnit.SECONDS); } 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(); } synchronized (futures) { if (defaultWaitTimeSeconds == -1) { GetQueueAttributesRequest request = new GetQueueAttributesRequest().withQueueUrl(qUrl) .withAttributeNames(QueueAttributeName.ReceiveMessageWaitTimeSeconds); ResultConverter.appendUserAgent(request, AmazonSQSBufferedAsyncClient.USER_AGENT); defaultWaitTimeSeconds = Integer.parseInt(sqsClient.getQueueAttributes(request).getAttributes() .get(QueueAttributeName.ReceiveMessageWaitTimeSeconds.toString())); } } int waitTimeSeconds = defaultWaitTimeSeconds; if (rq.getWaitTimeSeconds() != null) { waitTimeSeconds = rq.getWaitTimeSeconds(); } long waitTimeMs = Math.max(config.getMinReceiveWaitTimeMs(), TimeUnit.MILLISECONDS.convert(waitTimeSeconds, TimeUnit.SECONDS)); ReceiveMessageFuture toReturn = issueFuture(numMessages, waitTimeMs, callback); // attempt to satisfy it right away... satisfyFuturesFromBuffer(); // spawn more receive tasks if we need them... spawnMoreReceiveTasks(); // start the wait timer on the future (which will do nothing if the future is already satisfied) toReturn.startWaitTimer(); 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, long waitTimeMs, QueueBufferCallback callback) { synchronized (futures) { ReceiveMessageFuture theFuture = new ReceiveMessageFuture(callback, size, waitTimeMs); futures.add(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 Iterator futureIter = futures.iterator(); while (futureIter.hasNext() && (!finishedTasks.isEmpty())) { // Remove any expired tasks before attempting to fufill the future pruneExpiredTasks(); // Fufill the future from a non expired task if there is one. There is still a // slight chance that the first task could have expired between the time we // pruned and the time we fufill the future if (!finishedTasks.isEmpty()) { fufillFuture(futureIter.next()); futureIter.remove(); } } } } } /** * 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 fufillFuture(ReceiveMessageFuture future) { ReceiveMessageBatchTask task = finishedTasks.getFirst(); ReceiveMessageResult result = new ReceiveMessageResult(); LinkedList messages = new LinkedList(); result.setMessages(messages); Exception exception = task.getException(); int numRetrieved = 0; boolean batchDone = false; while (numRetrieved < future.getRequestedSize()) { Message msg = task.removeMessage(); // a non-empty batch can still give back a null // message if the message expired. if (msg != null) { messages.add(msg); ++numRetrieved; } else { batchDone = true; break; } } // we may have just drained the batch. batchDone = batchDone || task.isEmpty() || (exception != null); if (batchDone) { finishedTasks.removeFirst(); } result.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) { future.setFailure(exception); } else { future.setSuccess(result); } } /** * Prune any expired tasks that do not have an exception associated with them. This method * assumes that you are holding the finishedTasks lock when invoking it */ private void pruneExpiredTasks() { int numberExpiredTasksPruned = pruneHeadTasks(new Predicate() { @Override public boolean test(ReceiveMessageBatchTask t) { return t.isExpired() && t.getException() == null; } }); // If we pruned any tasks because they are expired we also want to prune any empty tasks // afterwards so we have a chance to receive those expired messages again. if (numberExpiredTasksPruned > 0) { pruneHeadTasks(new Predicate() { @Override public boolean test(ReceiveMessageBatchTask t) { return t.isEmpty() && t.getException() == null; } }); } } /** * Prune all tasks at the beginning of the finishedTasks list that meet the given condition. * Once a task is found that does not meet the given condition the pruning stops. This method * assumes that you are holding the finishedTasks lock when invoking it. * * @param pruneCondition * Condition on whether a task is eligible to be pruned * @return Number of total tasks pruned from finishedTasks */ private int pruneHeadTasks(Predicate pruneCondition) { int numberPruned = 0; while (!finishedTasks.isEmpty()) { if (pruneCondition.test(finishedTasks.getFirst())) { finishedTasks.removeFirst(); numberPruned++; } else { break; } } return numberPruned; } /** * 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; if (config.isAdapativePrefetching()) { synchronized (futures) { int totalRequested = 0; for (Iterator futuresIter = futures.iterator(); futuresIter.hasNext();) { ReceiveMessageFuture future = futuresIter.next(); totalRequested += future.getRequestedSize(); } int batchesNeededToFulfillFutures = (int) Math.ceil((float) totalRequested / config.getMaxBatchSize()); desiredBatches = Math.min(batchesNeededToFulfillFutures, 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.trace("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 { /* how many messages did the request ask for */ private int requestedSize; private final long waitTimeDeadlineNano; private volatile Future timeoutFuture; ReceiveMessageFuture(QueueBufferCallback cb, int paramSize, long waitTimeMs) { super(cb); requestedSize = paramSize; waitTimeDeadlineNano = System.nanoTime() + TimeUnit.NANOSECONDS.convert(waitTimeMs, TimeUnit.MILLISECONDS); } public void startWaitTimer() { if (isDone() || timeoutFuture != null) { return; } long remaining = waitTimeDeadlineNano - System.nanoTime(); if (remaining <= 0) { timeout(); } else { Runnable timeoutRunnable = new Runnable() { @Override public void run() { timeout(); } }; timeoutFuture = waitTimer.schedule(timeoutRunnable, remaining, TimeUnit.NANOSECONDS); } } public boolean isExpired() { return System.nanoTime() > waitTimeDeadlineNano; } @Override public synchronized void setSuccess(ReceiveMessageResult result) { cancelTimeout(); super.setSuccess(result); } @Override public synchronized void setFailure(Exception exception) { cancelTimeout(); super.setFailure(exception); } public void timeout() { // Synchronize on the set of futures before synchronizing on this // object to avoid deadlock. synchronized (futures) { synchronized (this) { if (!isDone()) { setSuccess(new ReceiveMessageResult()); futures.remove(this); } } } } public int getRequestedSize() { return requestedSize; } private synchronized void cancelTimeout() { if (timeoutFuture != null) { timeoutFuture.cancel(false); } } } /** * 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. */ ReceiveMessageBatchTask(ReceiveQueueBuffer paramParentBuffer) { parentBuffer = paramParentBuffer; messages = Collections.emptyList(); } 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 (isExpired()) { messages.clear(); return null; } if (messages.isEmpty()) return null; else return messages.remove(messages.size() - 1); } boolean isExpired() { return System.nanoTime() > visibilityDeadlineNano; } /** * Nacks and clears all messages remaining in the batch. */ synchronized void clear() { if (!open) { throw new IllegalStateException("batch is not open"); } if (!isExpired()) { 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()) .withMessageAttributeNames(config.getReceiveMessageAttributeNames()) .withAttributeNames(config.getReceiveAttributeNames()); 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); } } } }





    © 2015 - 2024 Weber Informatics LLC | Privacy Policy