diff --git a/README.md b/README.md
index 135f6a9..76d9b3d 100644
--- a/README.md
+++ b/README.md
@@ -105,7 +105,16 @@ repositories {
| **`linger`** | **int** | refers to the time to wait before sending messages out to SNS. |
| **`maxBatchSize`** | **int** | refers to the maximum amount of data to be collected before sending the batch. |
-**NOTICE**: the buffer of message store in memory is calculate using **`maximumPoolSize`** * **`maxBatchSize`** huge values demand huge memory.
+> [!NOTE]
+> The buffer of message store in memory is calculated using **`maximumPoolSize`** * **`maxBatchSize`**; huge values demand huge memory.
+>
+> **Note on effective capacity:** the default queue implementation (`RingBufferBlockingQueue`) internally
+> rounds its capacity up to the next power of two, to allow fast bitwise index calculation. This means the
+> *actual* allocated capacity may be up to ~2x the value computed above — e.g. `maximumPoolSize=10` and
+> `maxBatchSize=10` yields a requested capacity of 100, but the queue actually allocates 128 slots. If you
+> need to budget memory precisely, use `RingBufferBlockingQueue#remainingCapacity()` (or a `LinkedBlockingQueue`
+> via the [Custom `BlockingQueue`](#custom-blockingqueue) option below, which does not round up) rather than
+> relying on the `maximumPoolSize * maxBatchSize` formula as an exact figure.
#### Custom `BlockingQueue`
diff --git a/amazon-sns-java-messaging-lib-template/src/main/java/com/amazon/sns/messaging/lib/concurrent/RingBufferBlockingQueue.java b/amazon-sns-java-messaging-lib-template/src/main/java/com/amazon/sns/messaging/lib/concurrent/RingBufferBlockingQueue.java
index 1125e98..eaa6ea3 100644
--- a/amazon-sns-java-messaging-lib-template/src/main/java/com/amazon/sns/messaging/lib/concurrent/RingBufferBlockingQueue.java
+++ b/amazon-sns-java-messaging-lib-template/src/main/java/com/amazon/sns/messaging/lib/concurrent/RingBufferBlockingQueue.java
@@ -34,6 +34,13 @@
* blocking {@link #put(Object)} and {@link #take()} operations. Other
* {@link BlockingQueue} methods throw {@link UnsupportedOperationException}.
*
+ *
Capacity rounding: the requested capacity is rounded up to the next
+ * power of two (see {@link #RingBufferBlockingQueue(int)}), so that slot indices
+ * can be computed with a bitwise AND ({@code sequence & indexMask}) instead of a
+ * modulo operation. As a result, the effective capacity — reported by
+ * {@link #remainingCapacity()} plus {@link #size()} — may be up to twice the
+ * value that was requested.
+ *
* @param the type of elements held in this queue
*/
@SuppressWarnings({ "unchecked", "java:S3078", "java:S1948" })
@@ -83,8 +90,12 @@ public class RingBufferBlockingQueue extends AbstractQueue implements Bloc
/**
* Creates a ring buffer with the specified capacity.
*
- * @param capacity the maximum number of elements the queue can hold; must be
- * positive
+ * @param capacity the requested capacity; must be positive. This value is
+ * rounded up to the next power of two internally — the queue's
+ * actual capacity (see {@link #remainingCapacity()}) may end up
+ * up to 2x larger than the value passed here. This trade-off
+ * enables the ring buffer to use a bitwise mask for index
+ * calculation instead of a modulo operation on every put/take.
* @throws IllegalArgumentException if {@code capacity <= 0}
*/
public RingBufferBlockingQueue(final int capacity) {
diff --git a/amazon-sns-java-messaging-lib-template/src/main/java/com/amazon/sns/messaging/lib/core/AbstractAmazonSnsConsumer.java b/amazon-sns-java-messaging-lib-template/src/main/java/com/amazon/sns/messaging/lib/core/AbstractAmazonSnsConsumer.java
index 7efec64..a1bc29e 100644
--- a/amazon-sns-java-messaging-lib-template/src/main/java/com/amazon/sns/messaging/lib/core/AbstractAmazonSnsConsumer.java
+++ b/amazon-sns-java-messaging-lib-template/src/main/java/com/amazon/sns/messaging/lib/core/AbstractAmazonSnsConsumer.java
@@ -23,11 +23,14 @@
import java.util.Optional;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiFunction;
import java.util.function.UnaryOperator;
@@ -263,7 +266,7 @@ private boolean maxBatchSizeReached(final BlockingQueue> request
* @return true if the request can be added
*/
private boolean canAddToBatch(final int batchSizeBytes, final int requestEntriesSize, final RequestEntry request) {
- return (batchSizeBytes < AbstractAmazonSnsConsumer.BATCH_SIZE_BYTES_THRESHOLD)
+ return (batchSizeBytes < BATCH_SIZE_BYTES_THRESHOLD)
&& (requestEntriesSize < topicProperty.getMaxBatchSize())
&& Objects.nonNull(request);
}
@@ -275,7 +278,7 @@ private boolean canAddToBatch(final int batchSizeBytes, final int requestEntries
* @return true if the batch is still within the size limit
*/
private boolean canAddPayload(final int batchSizeBytes) {
- return batchSizeBytes <= AbstractAmazonSnsConsumer.BATCH_SIZE_BYTES_THRESHOLD;
+ return batchSizeBytes <= BATCH_SIZE_BYTES_THRESHOLD;
}
/**
@@ -349,5 +352,44 @@ public CompletableFuture await() {
});
}
+ /**
+ * Returns a {@link CompletableFuture} that completes once all pending requests have
+ * been processed (i.e., both the pending requests map and the topic requests queue are empty),
+ * bounded by the given timeout.
+ *
+ * Internally reuses {@link #await()} and waits on it via {@link CompletableFuture#get(long, TimeUnit)}
+ * on a separate thread, so the calling thread is never blocked directly. If the timeout elapses
+ * before all pending requests are drained, the returned future completes exceptionally with a
+ * {@link CompletionException} wrapping a {@link java.util.concurrent.TimeoutException}.
+ *
+ * Note that the underlying drain triggered by {@link #await()} is not cancelled when the timeout
+ * elapses; it keeps running in the background until the pending requests and topic requests queue
+ * are actually empty.
+ *
+ * @param timeout the maximum time to wait for all pending requests to be processed
+ * @return a future that completes when all requests are drained, or completes exceptionally
+ * if {@code timeout} elapses first
+ * @throws NullPointerException if {@code timeout} is {@code null}
+ */
+ @Override
+ public CompletableFuture await(final Duration timeout) {
+ Objects.requireNonNull(timeout, "timeout cannot be null");
+
+ final CompletableFuture pending = await();
+
+ return CompletableFuture.runAsync(() -> {
+ try {
+ pending.get(timeout.toNanos(), TimeUnit.NANOSECONDS);
+ } catch (final InterruptedException ex) {
+ Thread.currentThread().interrupt();
+ throw new CompletionException(ex);
+ } catch (final ExecutionException ex) {
+ throw new CompletionException(ex.getCause());
+ } catch (final TimeoutException ex) {
+ throw new CompletionException(ex);
+ }
+ });
+ }
+
}
// @formatter:on
diff --git a/amazon-sns-java-messaging-lib-template/src/main/java/com/amazon/sns/messaging/lib/core/AbstractAmazonSnsProducer.java b/amazon-sns-java-messaging-lib-template/src/main/java/com/amazon/sns/messaging/lib/core/AbstractAmazonSnsProducer.java
index e7eead1..2d6ed0d 100644
--- a/amazon-sns-java-messaging-lib-template/src/main/java/com/amazon/sns/messaging/lib/core/AbstractAmazonSnsProducer.java
+++ b/amazon-sns-java-messaging-lib-template/src/main/java/com/amazon/sns/messaging/lib/core/AbstractAmazonSnsProducer.java
@@ -16,10 +16,18 @@
package com.amazon.sns.messaging.lib.core;
+import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.amazon.sns.messaging.lib.concurrent.ThreadFactoryProvider;
import com.amazon.sns.messaging.lib.model.RequestEntry;
import com.amazon.sns.messaging.lib.model.ResponseFailEntry;
import com.amazon.sns.messaging.lib.model.ResponseSuccessEntry;
@@ -39,6 +47,9 @@
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
abstract class AbstractAmazonSnsProducer implements AmazonSnsProducer {
+ /** Class logger. */
+ private static final Logger LOGGER = LoggerFactory.getLogger(AbstractAmazonSnsProducer.class);
+
/** The producer lifecycle state, initially {@link State#RUNNING}. */
private final AtomicReference state = new AtomicReference<>(State.RUNNING);
@@ -48,6 +59,8 @@ abstract class AbstractAmazonSnsProducer implements AmazonSnsProducer {
/** The blocking queue for buffering requests before batch processing. */
private final BlockingQueue> topicRequests;
+ private final ExecutorService callbackExecutor = Executors.newCachedThreadPool(ThreadFactoryProvider.getThreadFactory());
+
/**
* Sends a request entry by enqueuing it for batch processing.
*
@@ -59,7 +72,7 @@ public ListenableFuture send(final Requ
if (State.RUNNING.equals(state.get())) {
return enqueueRequest(requestEntry);
} else {
- final ListenableFutureImpl listenableFutureImpl = new ListenableFutureImpl();
+ final ListenableFutureImpl listenableFutureImpl = new ListenableFutureImpl(Runnable::run);
listenableFutureImpl.fail(ResponseFailEntry.builder()
.withCode("000")
@@ -78,8 +91,24 @@ public ListenableFuture send(final Requ
* accepted once shutdown.
*/
@Override
- public void shutdown() {
+ public void shutdown(final Runnable runnable) {
state.compareAndSet(State.RUNNING, State.SHUTDOWN);
+
+ runnable.run();
+
+ try {
+ LOGGER.warn("Shutdown producer {}", getClass().getSimpleName());
+
+ callbackExecutor.shutdown();
+ if (!callbackExecutor.awaitTermination(60, TimeUnit.SECONDS)) {
+ LOGGER.warn("Producer executor service did not terminate in the specified time.");
+ final List droppedTasks = callbackExecutor.shutdownNow();
+ LOGGER.warn("Producer executor service was abruptly shut down. {} tasks will not be executed.", droppedTasks.size());
+ }
+ } catch (final InterruptedException ex) {
+ LOGGER.error(ex.getMessage(), ex);
+ Thread.currentThread().interrupt();
+ }
}
/**
@@ -92,7 +121,7 @@ public void shutdown() {
@SneakyThrows
private ListenableFuture enqueueRequest(final RequestEntry requestEntry) {
try {
- final ListenableFuture trackPendingRequest = new ListenableFutureImpl();
+ final ListenableFuture trackPendingRequest = new ListenableFutureImpl(callbackExecutor);
pendingRequests.put(requestEntry.getId(), trackPendingRequest);
topicRequests.put(requestEntry);
return trackPendingRequest;
diff --git a/amazon-sns-java-messaging-lib-template/src/main/java/com/amazon/sns/messaging/lib/core/AbstractAmazonSnsTemplate.java b/amazon-sns-java-messaging-lib-template/src/main/java/com/amazon/sns/messaging/lib/core/AbstractAmazonSnsTemplate.java
index 880b487..01d957c 100644
--- a/amazon-sns-java-messaging-lib-template/src/main/java/com/amazon/sns/messaging/lib/core/AbstractAmazonSnsTemplate.java
+++ b/amazon-sns-java-messaging-lib-template/src/main/java/com/amazon/sns/messaging/lib/core/AbstractAmazonSnsTemplate.java
@@ -16,6 +16,7 @@
package com.amazon.sns.messaging.lib.core;
+import java.time.Duration;
import java.util.Objects;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CompletableFuture;
@@ -72,8 +73,7 @@ public ListenableFuture send(final Requ
* Shuts down both the producer and consumer gracefully.
*/
public void shutdown() {
- amazonSnsProducer.shutdown();
- amazonSnsConsumer.shutdown();
+ amazonSnsProducer.shutdown(amazonSnsConsumer::shutdown);
}
/**
@@ -85,6 +85,20 @@ public CompletableFuture await() {
return amazonSnsConsumer.await();
}
+ /**
+ * Returns a future that completes once all pending requests are drained and processed,
+ * bounded by the given timeout.
+ *
+ * @param timeout the maximum time to wait for all pending requests to be processed
+ * @return a {@link CompletableFuture} that completes when the consumer has finished, or
+ * completes exceptionally with a {@link java.util.concurrent.TimeoutException}
+ * if {@code timeout} elapses first
+ * @throws NullPointerException if {@code timeout} is {@code null}
+ */
+ public CompletableFuture await(final Duration timeout) {
+ return amazonSnsConsumer.await(timeout);
+ }
+
/**
* Creates an {@link AmazonSnsThreadPoolExecutor} configured for the given topic property.
* For FIFO topics, a single-threaded pool is used to guarantee order.
diff --git a/amazon-sns-java-messaging-lib-template/src/main/java/com/amazon/sns/messaging/lib/core/AmazonSnsConsumer.java b/amazon-sns-java-messaging-lib-template/src/main/java/com/amazon/sns/messaging/lib/core/AmazonSnsConsumer.java
index 1741aa7..b4ddc1a 100644
--- a/amazon-sns-java-messaging-lib-template/src/main/java/com/amazon/sns/messaging/lib/core/AmazonSnsConsumer.java
+++ b/amazon-sns-java-messaging-lib-template/src/main/java/com/amazon/sns/messaging/lib/core/AmazonSnsConsumer.java
@@ -16,6 +16,7 @@
package com.amazon.sns.messaging.lib.core;
+import java.time.Duration;
import java.util.concurrent.CompletableFuture;
/**
@@ -66,4 +67,18 @@ public interface AmazonSnsConsumer {
*/
public CompletableFuture await();
+ /**
+ * Returns a {@link CompletableFuture} that completes once all pending requests
+ * have been processed, bounded by the given timeout. If the timeout elapses
+ * before all pending requests are drained, the returned future completes
+ * exceptionally with a {@link java.util.concurrent.TimeoutException}.
+ *
+ * @param timeout the maximum time to wait for all pending requests to be
+ * processed
+ * @return a future that completes when all requests are drained, or completes
+ * exceptionally if {@code timeout} elapses first
+ * @throws NullPointerException if {@code timeout} is {@code null}
+ */
+ public CompletableFuture await(final Duration timeout);
+
}
diff --git a/amazon-sns-java-messaging-lib-template/src/main/java/com/amazon/sns/messaging/lib/core/AmazonSnsProducer.java b/amazon-sns-java-messaging-lib-template/src/main/java/com/amazon/sns/messaging/lib/core/AmazonSnsProducer.java
index 526584e..592ef76 100644
--- a/amazon-sns-java-messaging-lib-template/src/main/java/com/amazon/sns/messaging/lib/core/AmazonSnsProducer.java
+++ b/amazon-sns-java-messaging-lib-template/src/main/java/com/amazon/sns/messaging/lib/core/AmazonSnsProducer.java
@@ -42,7 +42,7 @@ public interface AmazonSnsProducer {
/**
* Shuts down the producer, preventing any further messages from being accepted.
*/
- public void shutdown();
+ public void shutdown(final Runnable runnable);
}
// @formatter:on
diff --git a/amazon-sns-java-messaging-lib-template/src/main/java/com/amazon/sns/messaging/lib/core/ListenableFuture.java b/amazon-sns-java-messaging-lib-template/src/main/java/com/amazon/sns/messaging/lib/core/ListenableFuture.java
index d4bc6d6..a6fdb9a 100644
--- a/amazon-sns-java-messaging-lib-template/src/main/java/com/amazon/sns/messaging/lib/core/ListenableFuture.java
+++ b/amazon-sns-java-messaging-lib-template/src/main/java/com/amazon/sns/messaging/lib/core/ListenableFuture.java
@@ -18,6 +18,9 @@
import static java.util.function.Function.identity;
+import java.time.Duration;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import com.amazon.sns.messaging.lib.model.ResponseFailEntry;
@@ -64,5 +67,32 @@ default void addCallback(final Consumer super S> successCallback) {
*/
void fail(final ResponseFailEntry entry);
+ /**
+ * Waits, if necessary, for this future to complete, then returns its success result.
+ *
+ * Mirrors the contract of {@link java.util.concurrent.Future#get()}: if the future
+ * completed with a failure, this throws {@link ExecutionException} instead of returning.
+ * The exception's cause is the {@link Throwable} carried by the failure result, if any
+ * (see {@link ResponseFailEntry#getThrowable()}), or a new exception built from the
+ * failure result's message otherwise.
+ *
+ * @return the success result
+ * @throws InterruptedException if the current thread is interrupted while waiting
+ * @throws ExecutionException if the future completed with a failure
+ */
+ S get() throws InterruptedException, ExecutionException;
+
+ /**
+ * Waits, if necessary, for at most the given timeout for this future to complete, then
+ * returns its success result. See {@link #get()} for failure semantics.
+ *
+ * @param timeout the maximum time to wait
+ * @return the success result
+ * @throws InterruptedException if the current thread is interrupted while waiting
+ * @throws ExecutionException if the future completed with a failure
+ * @throws TimeoutException if the timeout elapses before the future completes
+ */
+ S get(final Duration timeout) throws InterruptedException, ExecutionException, TimeoutException;
+
}
// @formatter:on
diff --git a/amazon-sns-java-messaging-lib-template/src/main/java/com/amazon/sns/messaging/lib/core/ListenableFutureImpl.java b/amazon-sns-java-messaging-lib-template/src/main/java/com/amazon/sns/messaging/lib/core/ListenableFutureImpl.java
index 443b138..d0ad7c9 100644
--- a/amazon-sns-java-messaging-lib-template/src/main/java/com/amazon/sns/messaging/lib/core/ListenableFutureImpl.java
+++ b/amazon-sns-java-messaging-lib-template/src/main/java/com/amazon/sns/messaging/lib/core/ListenableFutureImpl.java
@@ -16,111 +16,157 @@
package com.amazon.sns.messaging.lib.core;
+import static br.com.fluentvalidator.predicate.LogicalPredicate.not;
import static java.util.function.Function.identity;
-import java.util.LinkedList;
+import java.time.Duration;
+import java.util.Objects;
import java.util.Optional;
-import java.util.Queue;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
-import org.apache.commons.collections4.CollectionUtils;
-
import com.amazon.sns.messaging.lib.model.ResponseFailEntry;
import com.amazon.sns.messaging.lib.model.ResponseSuccessEntry;
-import lombok.AccessLevel;
import lombok.Getter;
// @formatter:off
/**
- * Default implementation of {@link ListenableFuture}. Supports state tracking (NEW, SUCCESS, FAILURE)
- * and thread-safe callback registration and notification.
+ * Default implementation of {@link ListenableFuture}. Supports state tracking
+ * (NEW, SUCCESS, FAILURE), thread-safe callback registration and notification,
+ * and blocking retrieval of the result via {@link #get()} /
+ * {@link #get(Duration)}.
+ *
+ * All state transitions and reads are guarded by {@link #mutex}. Completion
+ * ({@link #success(ResponseSuccessEntry)} or {@link #fail(ResponseFailEntry)})
+ * both notifies any registered callbacks synchronously and wakes up any thread
+ * blocked in {@link #get()} / {@link #get(Duration)} via
+ * {@link Object#notifyAll()}.
*/
class ListenableFutureImpl implements ListenableFuture {
- private final Object mutex = new Object();
-
- @Getter(value = AccessLevel.PACKAGE)
- private State state = State.NEW;
-
- @Getter(value = AccessLevel.PACKAGE)
- private ResponseSuccessEntry successResult;
-
- @Getter(value = AccessLevel.PACKAGE)
- private ResponseFailEntry failureResult;
+ /**
+ * Runs registered callbacks off of the calling (consumer) thread; see class
+ * Javadoc.
+ */
+ private final Executor callbackExecutor;
- private final Queue> successCallback = new LinkedList<>();
+ /**
+ * Backing future. Completed with a success result, or exceptionally with a
+ * {@link FailureSignal}.
+ */
+ private final CompletableFuture delegate = new CompletableFuture<>();
- private final Queue> failureCallback = new LinkedList<>();
+ /**
+ * Creates a new future whose callbacks are dispatched on the given executor.
+ *
+ * @param callbackExecutor the executor used to run success/failure callbacks;
+ * must not be one of the library's own publish/consumer
+ * executors, to avoid starving them (see class Javadoc)
+ */
+ ListenableFutureImpl(final Executor callbackExecutor) {
+ this.callbackExecutor = Objects.requireNonNull(callbackExecutor, "callbackExecutor cannot be null");
+ }
@Override
public void addCallback(final Consumer super ResponseSuccessEntry> successCallback, final Consumer super ResponseFailEntry> failureCallback) {
- synchronized (mutex) {
- final Consumer super ResponseSuccessEntry> success = Optional.ofNullable(successCallback).orElse(identity()::apply);
- final Consumer super ResponseFailEntry> failure = Optional.ofNullable(failureCallback).orElse(identity()::apply);
-
- switch (state) {
- case NEW:
- this.successCallback.add(success);
- this.failureCallback.add(failure);
- break;
- case SUCCESS:
- notifySuccess(success);
- break;
- case FAILURE:
- notifyFailure(failure);
- break;
+ final Consumer super ResponseSuccessEntry> success = Optional.ofNullable(successCallback).orElse(identity()::apply);
+ final Consumer super ResponseFailEntry> failure = Optional.ofNullable(failureCallback).orElse(identity()::apply);
+
+ delegate.whenCompleteAsync((result, throwable) -> {
+ if (Objects.isNull(throwable)) {
+ success.accept(result);
+ } else {
+ failure.accept(unwrap(throwable));
}
- }
+ }, callbackExecutor);
}
@Override
public void success(final ResponseSuccessEntry entry) {
- synchronized (mutex) {
- state = State.SUCCESS;
- successResult = entry;
-
- while (CollectionUtils.isNotEmpty(successCallback)) {
- notifySuccess(successCallback.poll());
- }
+ if (not(delegate::complete).test(entry)) {
+ throw new IllegalStateException("ListenableFuture already completed.");
}
}
@Override
public void fail(final ResponseFailEntry entry) {
- synchronized (mutex) {
- state = State.FAILURE;
- failureResult = entry;
+ if (not(delegate::completeExceptionally).test(new FailureSignal(entry))) {
+ throw new IllegalStateException("ListenableFuture already completed.");
+ }
+ }
- while (CollectionUtils.isNotEmpty(failureCallback)) {
- notifyFailure(failureCallback.poll());
- }
+ @Override
+ public ResponseSuccessEntry get() throws InterruptedException, ExecutionException {
+ try {
+ return delegate.get();
+ } catch (final ExecutionException ex) {
+ throw new ExecutionException(unwrapCause(ex.getCause()));
+ }
+ }
+
+ @Override
+ public ResponseSuccessEntry get(final Duration timeout) throws InterruptedException, ExecutionException, TimeoutException {
+ Objects.requireNonNull(timeout, "timeout cannot be null");
+
+ try {
+ return delegate.get(timeout.toNanos(), TimeUnit.NANOSECONDS);
+ } catch (final ExecutionException ex) {
+ throw new ExecutionException(unwrapCause(ex.getCause()));
}
}
/**
- * Notifies a single success callback with the stored result.
+ * Unwraps a {@link FailureSignal} into the {@link ResponseFailEntry} it
+ * carries.
*
- * @param callback the callback to invoke
+ * @param throwable the throwable passed to
+ * {@link CompletableFuture#whenCompleteAsync}; either a
+ * {@link FailureSignal} directly, or (in composed/chained
+ * usages) a {@link CompletionException} wrapping one
+ * @return the original failure result
*/
- private void notifySuccess(final Consumer super ResponseSuccessEntry> callback) {
- callback.accept(successResult);
+ private static ResponseFailEntry unwrap(final Throwable throwable) {
+ final Throwable cause = throwable instanceof CompletionException ? throwable.getCause() : throwable;
+ return FailureSignal.class.cast(cause).getEntry();
}
/**
- * Notifies a single failure callback with the stored result.
+ * Returns the cause that {@link #get()} / {@link #get(Duration)} should report:
+ * the original {@link Throwable} carried by the {@link ResponseFailEntry} if
+ * present, or the entry itself (via {@link FailureSignal}) as a fallback.
*
- * @param callback the callback to invoke
+ * @param cause the cause of the {@link ExecutionException} thrown by the
+ * backing future, expected to be a {@link FailureSignal}
+ * @return the throwable to expose as the {@link ExecutionException}'s cause
*/
- private void notifyFailure(final Consumer super ResponseFailEntry> callback) {
- callback.accept(failureResult);
+ private static Throwable unwrapCause(final Throwable cause) {
+ final ResponseFailEntry entry = FailureSignal.class.cast(cause).getEntry();
+ return Optional.ofNullable(entry.getThrowable()).orElseGet(() -> new IllegalStateException(entry.getMessage()));
}
/**
- * The lifecycle states of a {@link ListenableFuture}.
+ * Wraps a {@link ResponseFailEntry} so it can be used to complete the backing
+ * {@link CompletableFuture} exceptionally via
+ * {@link CompletableFuture#completeExceptionally(Throwable)}.
*/
- enum State {
- NEW, SUCCESS, FAILURE
+ private static final class FailureSignal extends RuntimeException {
+
+ private static final long serialVersionUID = -1790175325395257266L;
+
+ @Getter
+ private final transient ResponseFailEntry entry;
+
+ FailureSignal(final ResponseFailEntry entry) {
+ super(entry.getMessage());
+ this.entry = entry;
+ }
+
}
}
diff --git a/amazon-sns-java-messaging-lib-template/src/main/java/com/amazon/sns/messaging/lib/core/RequestEntryInternalFactory.java b/amazon-sns-java-messaging-lib-template/src/main/java/com/amazon/sns/messaging/lib/core/RequestEntryInternalFactory.java
index a2ac06c..ef0aebe 100644
--- a/amazon-sns-java-messaging-lib-template/src/main/java/com/amazon/sns/messaging/lib/core/RequestEntryInternalFactory.java
+++ b/amazon-sns-java-messaging-lib-template/src/main/java/com/amazon/sns/messaging/lib/core/RequestEntryInternalFactory.java
@@ -144,9 +144,9 @@ static class RequestEntryInternal {
private final String deduplicationId;
/**
- * Returns the size of the serialized payload in bytes.
+ * Returns the size of the binary payload in bytes.
*
- * @return the payload size
+ * @return the payload size in bytes
*/
public int size() {
return value.capacity();
@@ -163,6 +163,10 @@ public String getMessage() {
}
+ /**
+ * Internal implementation of {@link AbstractMessageAttributes} that calculates
+ * attribute size values for batching decisions.
+ */
@SuppressWarnings("java:S6548")
@NoArgsConstructor(access = AccessLevel.PRIVATE)
static class MessageAttributesInternal extends AbstractMessageAttributes {
diff --git a/amazon-sns-java-messaging-lib-template/src/main/java/com/amazon/sns/messaging/lib/metrics/AbstractAmazonSnsConsumerMetricsDecorator.java b/amazon-sns-java-messaging-lib-template/src/main/java/com/amazon/sns/messaging/lib/metrics/AbstractAmazonSnsConsumerMetricsDecorator.java
index 836e689..5c7e9de 100644
--- a/amazon-sns-java-messaging-lib-template/src/main/java/com/amazon/sns/messaging/lib/metrics/AbstractAmazonSnsConsumerMetricsDecorator.java
+++ b/amazon-sns-java-messaging-lib-template/src/main/java/com/amazon/sns/messaging/lib/metrics/AbstractAmazonSnsConsumerMetricsDecorator.java
@@ -16,6 +16,7 @@
package com.amazon.sns.messaging.lib.metrics;
+import java.time.Duration;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;
@@ -178,4 +179,11 @@ public CompletableFuture await() {
return delegate.await();
}
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public CompletableFuture await(final Duration timeout) {
+ return delegate.await(timeout);
+ }
}
diff --git a/amazon-sns-java-messaging-lib-template/src/test/java/com/amazon/sns/messaging/lib/core/AbstractAmazonSnsProducerTest.java b/amazon-sns-java-messaging-lib-template/src/test/java/com/amazon/sns/messaging/lib/core/AbstractAmazonSnsProducerTest.java
index eba4111..5d0de1f 100644
--- a/amazon-sns-java-messaging-lib-template/src/test/java/com/amazon/sns/messaging/lib/core/AbstractAmazonSnsProducerTest.java
+++ b/amazon-sns-java-messaging-lib-template/src/test/java/com/amazon/sns/messaging/lib/core/AbstractAmazonSnsProducerTest.java
@@ -64,7 +64,7 @@ void setUp() {
@AfterEach
void tearDown() {
if (Objects.nonNull(producer)) {
- producer.shutdown();
+ producer.shutdown(() -> {});
}
}
@@ -74,7 +74,7 @@ void testSendReturnsShutdownState() throws InterruptedException {
final RequestEntry entry = requestEntry();
- producer.shutdown();
+ producer.shutdown(() -> {});
final ListenableFuture future = producer.send(entry);
diff --git a/amazon-sns-java-messaging-lib-template/src/test/java/com/amazon/sns/messaging/lib/core/AbstractAmazonSnsTemplateTest.java b/amazon-sns-java-messaging-lib-template/src/test/java/com/amazon/sns/messaging/lib/core/AbstractAmazonSnsTemplateTest.java
index fa5c81f..47b0ac3 100644
--- a/amazon-sns-java-messaging-lib-template/src/test/java/com/amazon/sns/messaging/lib/core/AbstractAmazonSnsTemplateTest.java
+++ b/amazon-sns-java-messaging-lib-template/src/test/java/com/amazon/sns/messaging/lib/core/AbstractAmazonSnsTemplateTest.java
@@ -22,6 +22,8 @@
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -31,9 +33,11 @@
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.function.UnaryOperator;
+import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -64,15 +68,23 @@ class AbstractAmazonSnsTemplateTest {
private AbstractAmazonSnsTemplate