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

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

Go to download

An Amazon SQS client that supports creating lightweight, automatically-deleted temporary queues, for use in common messaging patterns such as Request/Response. See http://aws.amazon.com/sqs.

The newest version!
/*
 * 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.util;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import software.amazon.awssdk.core.exception.SdkClientException;
import software.amazon.awssdk.services.sqs.SqsClient;
import software.amazon.awssdk.services.sqs.model.ChangeMessageVisibilityBatchRequest;
import software.amazon.awssdk.services.sqs.model.ChangeMessageVisibilityBatchRequestEntry;
import software.amazon.awssdk.services.sqs.model.GetQueueAttributesRequest;
import software.amazon.awssdk.services.sqs.model.Message;
import software.amazon.awssdk.services.sqs.model.QueueAttributeName;
import software.amazon.awssdk.services.sqs.model.ReceiveMessageRequest;
import software.amazon.awssdk.services.sqs.model.ReceiveMessageResponse;

/**
 * The ReceiveQueueBuffer class is responsible for dequeueing of messages from a single SQS queue.
 * 

* Synchronization strategy: * - 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 final Log LOG = LogFactory.getLog(ReceiveQueueBuffer.class); private static final Queue EMPTY_DEQUE = new ArrayDeque<>(); private final ScheduledExecutorService waitTimer; private final SqsClient sqsClient; /** * This buffer's queue visibility timeout. Used to detect expired message that should not be * returned by the {@code receiveMessage} call. */ private final long defaultVisibilityTimeoutNanos; /** * 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. */ private final long defaultWaitTimeNanos; /** shutdown buffer does not retrieve any more messages from sqs */ volatile boolean shutDown = false; // TODO-RS: We could use a SynchronousQueue to manange handing messages // from sources to futures, which would simplify a lot of logic here. // We just have to be willing to create threads to block on Queue#poll // and Queue#offer. /** message delivery futures we gave out */ private final Set futures = new LinkedHashSet<>(); /** finished batches are stored in this list. */ protected LinkedList finishedTasks = new LinkedList<>(); public ReceiveQueueBuffer(SqsClient sqsClient, ScheduledExecutorService waitTimer, String queueUrl) { this.sqsClient = sqsClient; this.waitTimer = waitTimer; if (queueUrl.endsWith(".fifo")) { throw new IllegalArgumentException("FIFO queues are not yet supported: " + queueUrl); } GetQueueAttributesRequest getQueueAttributesRequest = GetQueueAttributesRequest.builder().queueUrl(queueUrl) .attributeNames(QueueAttributeName.RECEIVE_MESSAGE_WAIT_TIME_SECONDS, QueueAttributeName.VISIBILITY_TIMEOUT).build(); Map attributes = sqsClient.getQueueAttributes(getQueueAttributesRequest).attributesAsStrings(); long visibilityTimeoutSeconds = Long.parseLong(attributes.get("VisibilityTimeout")); defaultVisibilityTimeoutNanos = TimeUnit.SECONDS.toNanos(visibilityTimeoutSeconds); long waitTimeSeconds = Long.parseLong(attributes.get("ReceiveMessageWaitTimeSeconds")); defaultWaitTimeNanos = TimeUnit.SECONDS.toNanos(waitTimeSeconds); } public ReceiveQueueBuffer(ReceiveQueueBuffer other) { this.sqsClient = other.sqsClient; this.waitTimer = other.waitTimer; this.defaultWaitTimeNanos = other.defaultWaitTimeNanos; this.defaultVisibilityTimeoutNanos = other.defaultVisibilityTimeoutNanos; } /** * Prevents adding new futures and nacks all inflight messages. */ public void shutdown() { shutDown = true; clear(); } /** * 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 Future receiveMessageAsync(ReceiveMessageRequest rq) { if (shutDown) { throw SdkClientException.create("The buffer has been shut down."); } // issue the future... int numMessages = 10; if (rq.maxNumberOfMessages() != null) { numMessages = rq.maxNumberOfMessages(); } long waitTimeNanos; if (rq.waitTimeSeconds() != null) { waitTimeNanos = TimeUnit.SECONDS.toNanos(rq.waitTimeSeconds()); } else { waitTimeNanos = defaultWaitTimeNanos; } ReceiveMessageFuture toReturn = issueFuture(numMessages, waitTimeNanos); // attempt to satisfy it right away... satisfyFuturesFromBuffer(); 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 waitTimeNanos) { synchronized (futures) { ReceiveMessageFuture theFuture = new ReceiveMessageFuture(size, waitTimeNanos); 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. */ protected void satisfyFuturesFromBuffer() { synchronized (futures) { synchronized (finishedTasks) { pruneExpiredFutures(); // 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()) { if (fulfillFuture(futureIter.next())) { futureIter.remove(); } else { // We couldn't produce enough messages, so break the loop and return. // We may not hit the while loop termination condition because we might // have inflight FIFO messages that are blocking some of the messages. return; } } } } } } /** * 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 boolean fulfillFuture(ReceiveMessageFuture future) { for (Iterator iter = finishedTasks.iterator(); iter.hasNext();) { ReceiveMessageBatchTask task = iter.next(); Exception exception = task.getException(); if (exception != null) { // Only fulfill a future with an exception if it hasn't collected any messages yet! // Otherwise messages will be lost. if (future.messages.isEmpty()) { iter.remove(); future.completeExceptionally(exception); return true; } } else { task.populateResult(future); if (task.isEmpty()) { task.clear(); iter.remove(); } if (future.isFull()) { return true; } } } if (!future.messages.isEmpty() || future.isExpired()) { future.complete(); return true; } else { return false; } } /** * 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(t -> 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(t -> 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()) { ReceiveMessageBatchTask task = finishedTasks.getFirst(); if (pruneCondition.test(task)) { task.clear(); finishedTasks.removeFirst(); numberPruned++; } else { break; } } return numberPruned; } private void pruneExpiredFutures() { for (Iterator iterator = futures.iterator(); iterator.hasNext();) { ReceiveMessageFuture future = iterator.next(); if (future.isExpired()) { future.complete(); iterator.remove(); } } } public void deliverMessages(List messages, String sourceQueueUrl, Integer visibilityTimeoutNanosOverride) { submit(Runnable::run, () -> messages, sourceQueueUrl, visibilityTimeoutNanosOverride); } public void deliverException(Exception exception) { submit(Runnable::run, () -> {throw exception;}, null, 0); } public void submit(Executor executor, Callable> callable, String queueUrl, Integer visibilityTimeoutSecondsOverride) { long visibilityTimeoutNanos; if (visibilityTimeoutSecondsOverride == null) { visibilityTimeoutNanos = defaultVisibilityTimeoutNanos; } else { visibilityTimeoutNanos = TimeUnit.SECONDS.toNanos(visibilityTimeoutSecondsOverride); } ReceiveMessageBatchTask task = new ReceiveMessageBatchTask(callable, queueUrl, visibilityTimeoutNanos); executor.execute(task); } /** * This method is called by the batches after they have finished retrieving the messages. */ void reportBatchFinished(ReceiveMessageBatchTask batch) { if (shutDown) { batch.clear(); return; } synchronized (finishedTasks) { finishedTasks.addLast(batch); } satisfyFuturesFromBuffer(); } /** * Clears and nacks any pre-fetched messages in this buffer. */ public void clear() { boolean done = false; while (!done) { ReceiveMessageBatchTask currentBatch; synchronized (finishedTasks) { currentBatch = finishedTasks.poll(); } if (currentBatch != null) { currentBatch.clear(); } else { // ran out of batches to clear done = true; } } } protected class ReceiveMessageFuture extends CompletableFuture { /* how many messages did the request ask for */ private final int requestedSize; private final List messages; private final Long waitTimeDeadlineNano; private Future timeoutFuture; ReceiveMessageFuture(int paramSize, Long waitTimeNanos) { requestedSize = paramSize; messages = new ArrayList<>(requestedSize); if (waitTimeNanos != null) { this.waitTimeDeadlineNano = System.nanoTime() + waitTimeNanos; } else { this.waitTimeDeadlineNano = null; } whenComplete((result, exception) -> cancelTimeout()); } public synchronized void startWaitTimer() { if (waitTimeDeadlineNano == null || isDone() || timeoutFuture != null) { return; } long remaining = waitTimeDeadlineNano - System.nanoTime(); if (remaining < 0) { timeout(); } else { timeoutFuture = waitTimer.schedule(this::timeout, remaining, TimeUnit.NANOSECONDS); } } public boolean isExpired() { return waitTimeDeadlineNano != null && System.nanoTime() > waitTimeDeadlineNano; } public synchronized void addMessage(Message message) { if (isDone()) { throw new IllegalStateException("Future is already completed"); } if (isFull()) { throw new IllegalStateException("Future already has enough messages"); } messages.add(message); if (isFull()) { complete(); } } public boolean isFull() { return messages.size() >= requestedSize; } public synchronized void timeout() { if (!isDone()) { complete(); } } public synchronized void complete() { if (!isDone()) { ReceiveMessageResponse result = ReceiveMessageResponse.builder().messages(messages).build(); complete(result); } } 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. */ protected class ReceiveMessageBatchTask extends FutureTask> { private Exception exception = null; protected Queue messages; private final String sourceQueueUrl; private final long visibilityTimeoutNanos; private long visibilityDeadlineNano; private Future expiryFuture; /** * Constructs a receive task waiting the specified time before calling SQS. * * @param waitTimeMs * the time to wait before calling SQS */ ReceiveMessageBatchTask(Callable> callable, String sourceQueueUrl, long visibilityTimeoutNanos) { super(callable); this.sourceQueueUrl = sourceQueueUrl; this.visibilityTimeoutNanos = visibilityTimeoutNanos; messages = EMPTY_DEQUE; } synchronized boolean isEmpty() { if (!isDone()) { throw new IllegalStateException(); } return messages.isEmpty(); } /** * @return the exception that was thrown during execution, or null if there was no exception */ synchronized Exception getException() { if (!isDone()) { throw new IllegalStateException(); } return exception; } synchronized void populateResult(ReceiveMessageFuture future) { if (!isDone()) { throw new IllegalStateException("batch is not open"); } // our messages expired. if (isExpired()) { clear(); return; } if (messages.isEmpty()) { return; } for (Iterator iter = messages.iterator(); iter.hasNext() && !future.isFull();) { Message message = iter.next(); iter.remove(); future.addMessage(message); } } public synchronized void startExpiryTimer() { if (isExpired() || expiryFuture != null) { return; } long remaining = visibilityDeadlineNano - System.nanoTime(); if (remaining < 0) { clear(); } else { expiryFuture = waitTimer.schedule(this::clear, remaining, TimeUnit.NANOSECONDS); } } boolean isExpired() { return System.nanoTime() > visibilityDeadlineNano; } /** * Nacks and clears all messages remaining in the batch. */ synchronized void clear() { if (!isDone()) { throw new IllegalStateException("batch is not open"); } if (expiryFuture != null) { expiryFuture.cancel(false); } if (!isExpired()) { nackMessages(messages); } messages.clear(); } protected void nackMessages(Collection messages) { if (messages.isEmpty()) { return; } ChangeMessageVisibilityBatchRequest.Builder batchRequestBuilder = ChangeMessageVisibilityBatchRequest.builder().queueUrl(sourceQueueUrl); // TODO-RS: UserAgent? List entries = new ArrayList(messages.size()); int i = 0; for (Message m : messages) { entries.add(ChangeMessageVisibilityBatchRequestEntry.builder().id(Integer.toString(i)) .receiptHandle(m.receiptHandle()).visibilityTimeout(0).build()); ++i; } try { batchRequestBuilder.entries(entries); sqsClient.changeMessageVisibilityBatch(batchRequestBuilder.build()); } catch (SdkClientException e) { // Log and ignore. LOG.warn("ReceiveMessageBatchTask: changeMessageVisibility failed " + e); } } @Override protected void set(List v) { messages = new ArrayDeque(v); visibilityDeadlineNano = System.nanoTime() + visibilityTimeoutNanos; super.set(v); } @Override protected void done() { reportBatchFinished(this); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy