From d61ae525c21bf960482c32c278d900046cf4881a Mon Sep 17 00:00:00 2001 From: Marcos Tischer Vallim Date: Fri, 29 May 2026 03:35:25 -0300 Subject: [PATCH 1/5] test(integration): improve tests Signed-off-by: Marcos Tischer Vallim --- .github/workflows/ci-maven.yml | 4 ++-- README.md | 2 +- .../amazon/sqs/messaging/lib/core/ListenableFutureTest.java | 2 +- .../sqs/messaging/lib/core/AmazonSqsProducerAsyncTest.java | 1 + 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-maven.yml b/.github/workflows/ci-maven.yml index d1c2ef3..d1572e6 100644 --- a/.github/workflows/ci-maven.yml +++ b/.github/workflows/ci-maven.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java-version: [8, 11, 17, 21] + java-version: [8, 11, 17, 21, 25] steps: - name: Checkout repository uses: actions/checkout@v6 @@ -34,7 +34,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java-version: [8, 11, 17, 21] + java-version: [8, 11, 17, 21, 25] steps: - name: Checkout repository uses: actions/checkout@v6 diff --git a/README.md b/README.md index 64aad36..10bb853 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Combine multiple requests to optimally utilise the network. Article [Martin Fowler](https://martinfowler.com) [Request Batch](https://martinfowler.com/articles/patterns-of-distributed-systems/request-batch.html) -_**Compatible JDK 8, 11 and 17**_ +_**Compatible JDK 8, 11, 17, 21 and 25**_ _**Compatible AWS JDK v1 >= 1.12**_ diff --git a/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/core/ListenableFutureTest.java b/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/core/ListenableFutureTest.java index 6ad4f44..957185d 100644 --- a/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/core/ListenableFutureTest.java +++ b/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/core/ListenableFutureTest.java @@ -114,7 +114,7 @@ void testAddCallbackDefaultFailureCallbackIsNoOp() { final boolean[] called = { false }; final ResponseFailEntry entry = mock(ResponseFailEntry.class); - listenableFuture.addCallback(successCallback -> called[0] = true); + listenableFuture.addCallback(callback -> called[0] = true); listenableFuture.fail(entry); assertThat(called[0], is(false)); diff --git a/amazon-sqs-java-messaging-lib-v1/src/test/java/com/amazon/sqs/messaging/lib/core/AmazonSqsProducerAsyncTest.java b/amazon-sqs-java-messaging-lib-v1/src/test/java/com/amazon/sqs/messaging/lib/core/AmazonSqsProducerAsyncTest.java index 6187c29..775f13b 100644 --- a/amazon-sqs-java-messaging-lib-v1/src/test/java/com/amazon/sqs/messaging/lib/core/AmazonSqsProducerAsyncTest.java +++ b/amazon-sqs-java-messaging-lib-v1/src/test/java/com/amazon/sqs/messaging/lib/core/AmazonSqsProducerAsyncTest.java @@ -71,6 +71,7 @@ void before() { .maximumPoolSize(10) .queueUrl("http://localhost/000000000000/queue") .build(); + sqsTemplate = new AmazonSqsTemplate<>(amazonSQS, queueProperty, new RingBufferBlockingQueue<>(1024)); } From fb2d732586a5dc7aab01d148051bff158d0f481e Mon Sep 17 00:00:00 2001 From: Marcos Tischer Vallim Date: Fri, 29 May 2026 03:37:51 -0300 Subject: [PATCH 2/5] test(integration): improve tests Signed-off-by: Marcos Tischer Vallim --- pom.xml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pom.xml b/pom.xml index 7ef7667..e935ddc 100644 --- a/pom.xml +++ b/pom.xml @@ -449,6 +449,16 @@ + + 25 + + 25 + + + 25 + + + sonar From 9f6c20382ae344c943036899cef32baba7a93a56 Mon Sep 17 00:00:00 2001 From: Marcos Tischer Vallim Date: Fri, 29 May 2026 03:41:41 -0300 Subject: [PATCH 3/5] test(integration): improve tests Signed-off-by: Marcos Tischer Vallim --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index e935ddc..ba52b60 100644 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ true - 1.18.38 + 1.18.42 1.3.14 4.11.0 5.10.2 From 6d5c9e3fac5cc60f00fbfe3c9006d5c7b41803db Mon Sep 17 00:00:00 2001 From: Marcos Tischer Vallim Date: Fri, 29 May 2026 03:45:34 -0300 Subject: [PATCH 4/5] test(integration): improve tests Signed-off-by: Marcos Tischer Vallim --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ba52b60..8bd29fb 100644 --- a/pom.xml +++ b/pom.xml @@ -47,7 +47,7 @@ 2.22.2 3.2.0 3.2.8 - 0.8.12 + 0.8.14 4.0.1.6619 0.8.0 From 5df53ace1d129c881d5b6854caa488161108d119 Mon Sep 17 00:00:00 2001 From: Marcos Tischer Vallim Date: Mon, 8 Jun 2026 23:35:57 -0300 Subject: [PATCH 5/5] test(integration): improve tests Signed-off-by: Marcos Tischer Vallim --- .../AmazonSqsThreadPoolExecutor.java | 80 +-- .../concurrent/BlockingSubmissionPolicy.java | 16 +- .../lib/concurrent/ExecutorsProvider.java | 102 --- .../concurrent/RingBufferBlockingQueue.java | 57 +- .../lib/concurrent/ThreadFactoryProvider.java | 5 +- .../lib/core/AbstractAmazonSqsConsumer.java | 68 +- .../lib/core/AbstractAmazonSqsProducer.java | 53 +- .../lib/core/AbstractAmazonSqsTemplate.java | 190 +++++- .../lib/core/AbstractMessageAttributes.java | 2 +- .../messaging/lib/core/AmazonSqsConsumer.java | 70 ++ .../messaging/lib/core/AmazonSqsProducer.java | 48 ++ .../messaging/lib/core/ListenableFuture.java | 5 +- .../lib/core/ListenableFutureImpl.java | 8 +- .../lib/core/RequestEntryInternalFactory.java | 14 +- .../MaximumAllowedMessageException.java | 18 +- ...ractAmazonSqsConsumerMetricsDecorator.java | 181 ++++++ .../BlockingQueueMetricsDecorator.java | 363 +++++++++++ .../ExecutorServiceMetricsDecorator.java | 241 +++++++ .../lib/model/PublishRequestBuilder.java | 9 +- .../sqs/messaging/lib/model/RequestEntry.java | 4 +- .../AmazonSqsThreadPoolExecutorTest.java | 172 ++--- .../BlockingSubmissionPolicyTest.java | 168 +++++ .../RingBufferBlockingQueueTest.java | 11 +- .../core/AbstractAmazonSqsConsumerTest.java | 112 ++-- .../core/AbstractAmazonSqsProducerTest.java | 122 ++-- .../core/AbstractAmazonSqsTemplateTest.java | 372 +++++++++-- .../core/AbstractMessageAttributesTest.java | 2 +- .../lib/core/ListenableFutureImplTest.java | 2 +- .../lib/core/ListenableFutureTest.java | 2 +- .../core/RequestEntryInternalFactoryTest.java | 610 ++++++++++++++++++ .../messaging/lib/helpers/TryConsumer.java | 16 + ...AmazonSqsConsumerMetricsDecoratorTest.java | 441 +++++++++++++ .../BlockingQueueMetricsDecoratorTest.java | 521 +++++++++++++++ .../ExecutorServiceMetricsDecoratorTest.java | 498 ++++++++++++++ ...nsumer.java => AmazonSqsConsumerImpl.java} | 14 +- ...oducer.java => AmazonSqsProducerImpl.java} | 13 +- .../messaging/lib/core/AmazonSqsTemplate.java | 87 ++- .../messaging/lib/core/MessageAttributes.java | 2 +- .../AmazonSqsConsumerMetricsDecorator.java | 109 ++++ .../lib/core/AmazonSqsProducerAsyncTest.java | 2 +- .../lib/core/AmazonSqsProducerSyncTest.java | 2 +- .../AmazonSqsTemplateIntegrationTest.java | 2 +- .../lib/core/MessageAttributesTest.java | 2 +- .../lib/core/helper/ConsumerHelper.java | 2 +- .../src/test/resources/logback.xml | 2 +- ...nsumer.java => AmazonSqsConsumerImpl.java} | 14 +- ...oducer.java => AmazonSqsProducerImpl.java} | 13 +- .../messaging/lib/core/AmazonSqsTemplate.java | 87 ++- .../messaging/lib/core/MessageAttributes.java | 2 +- .../AmazonSqsConsumerMetricsDecorator.java | 109 ++++ .../lib/core/AmazonSqsProducerAsyncTest.java | 2 +- .../lib/core/AmazonSqsProducerSyncTest.java | 2 +- .../AmazonSqsTemplateIntegrationTest.java | 2 +- .../lib/core/MessageAttributesTest.java | 2 +- .../lib/core/helper/ConsumerHelper.java | 2 +- .../src/test/resources/logback.xml | 2 +- pom.xml | 17 +- 57 files changed, 4413 insertions(+), 661 deletions(-) delete mode 100644 amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/concurrent/ExecutorsProvider.java create mode 100644 amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/core/AmazonSqsConsumer.java create mode 100644 amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/core/AmazonSqsProducer.java create mode 100644 amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/metrics/AbstractAmazonSqsConsumerMetricsDecorator.java create mode 100644 amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/metrics/BlockingQueueMetricsDecorator.java create mode 100644 amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/metrics/ExecutorServiceMetricsDecorator.java create mode 100644 amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/concurrent/BlockingSubmissionPolicyTest.java create mode 100644 amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/core/RequestEntryInternalFactoryTest.java create mode 100644 amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/metrics/AbstractAmazonSqsConsumerMetricsDecoratorTest.java create mode 100644 amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/metrics/BlockingQueueMetricsDecoratorTest.java create mode 100644 amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/metrics/ExecutorServiceMetricsDecoratorTest.java rename amazon-sqs-java-messaging-lib-v1/src/main/java/com/amazon/sqs/messaging/lib/core/{AmazonSqsConsumer.java => AmazonSqsConsumerImpl.java} (91%) rename amazon-sqs-java-messaging-lib-v1/src/main/java/com/amazon/sqs/messaging/lib/core/{AmazonSqsProducer.java => AmazonSqsProducerImpl.java} (74%) create mode 100644 amazon-sqs-java-messaging-lib-v1/src/main/java/com/amazon/sqs/messaging/lib/metrics/AmazonSqsConsumerMetricsDecorator.java rename amazon-sqs-java-messaging-lib-v2/src/main/java/com/amazon/sqs/messaging/lib/core/{AmazonSqsConsumer.java => AmazonSqsConsumerImpl.java} (91%) rename amazon-sqs-java-messaging-lib-v2/src/main/java/com/amazon/sqs/messaging/lib/core/{AmazonSqsProducer.java => AmazonSqsProducerImpl.java} (74%) create mode 100644 amazon-sqs-java-messaging-lib-v2/src/main/java/com/amazon/sqs/messaging/lib/metrics/AmazonSqsConsumerMetricsDecorator.java diff --git a/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/concurrent/AmazonSqsThreadPoolExecutor.java b/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/concurrent/AmazonSqsThreadPoolExecutor.java index 1617507..f2a0608 100644 --- a/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/concurrent/AmazonSqsThreadPoolExecutor.java +++ b/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/concurrent/AmazonSqsThreadPoolExecutor.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,88 +16,26 @@ package com.amazon.sqs.messaging.lib.concurrent; -import java.util.Objects; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; /** - * A {@link ThreadPoolExecutor} for Amazon SQS operations that tracks active, - * failed, and succeeded task counts. + * A {@link ThreadPoolExecutor} configured for Amazon SQS publishing. Uses a + * {@link SynchronousQueue} with zero core threads, allowing threads to be + * created on demand up to the specified maximum pool size. Tasks that cannot be + * accepted immediately by the queue will block up to 30 seconds via + * {@link BlockingSubmissionPolicy}. */ public class AmazonSqsThreadPoolExecutor extends ThreadPoolExecutor { - private final AtomicInteger activeTaskCount = new AtomicInteger(); - - private final AtomicInteger failedTaskCount = new AtomicInteger(); - - private final AtomicInteger succeededTaskCount = new AtomicInteger(); - /** - * Creates a thread pool executor with the given maximum pool size, using a - * synchronous queue, virtual thread factory, and a 30-second blocking submission policy. + * Creates a new thread pool executor with the given maximum pool size. * - * @param maximumPoolSize the maximum number of threads + * @param maximumPoolSize the maximum number of threads allowed in the pool */ public AmazonSqsThreadPoolExecutor(final int maximumPoolSize) { super(0, maximumPoolSize, 60, TimeUnit.SECONDS, new SynchronousQueue<>(), ThreadFactoryProvider.getThreadFactory(), new BlockingSubmissionPolicy(30000)); } - /** - * Returns the number of currently active tasks. - * - * @return the active task count - */ - public int getActiveTaskCount() { - return activeTaskCount.get(); - } - - /** - * Returns the number of tasks that have failed. - * - * @return the failed task count - */ - public int getFailedTaskCount() { - return failedTaskCount.get(); - } - - /** - * Returns the number of tasks that have completed successfully. - * - * @return the succeeded task count - */ - public int getSucceededTaskCount() { - return succeededTaskCount.get(); - } - - /** - * {@inheritDoc} - */ - @Override - protected void beforeExecute(final Thread thread, final Runnable runnable) { - try { - super.beforeExecute(thread, runnable); - } finally { - activeTaskCount.incrementAndGet(); - } - } - - /** - * {@inheritDoc} - */ - @Override - protected void afterExecute(final Runnable runnable, final Throwable throwable) { - try { - super.afterExecute(runnable, throwable); - } finally { - if (Objects.nonNull(throwable)) { - failedTaskCount.incrementAndGet(); - } else { - succeededTaskCount.incrementAndGet(); - } - activeTaskCount.decrementAndGet(); - } - } - -} \ No newline at end of file +} diff --git a/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/concurrent/BlockingSubmissionPolicy.java b/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/concurrent/BlockingSubmissionPolicy.java index b0571d0..8777213 100644 --- a/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/concurrent/BlockingSubmissionPolicy.java +++ b/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/concurrent/BlockingSubmissionPolicy.java @@ -25,28 +25,26 @@ import lombok.SneakyThrows; /** - * A {@link RejectedExecutionHandler} that blocks the submitting thread until - * capacity becomes available in the queue, up to a configurable timeout. + * A {@link RejectedExecutionHandler} that blocks the caller thread until the + * task can be enqueued, up to the specified timeout. If the timeout elapses, a + * {@link RejectedExecutionException} is thrown. */ public class BlockingSubmissionPolicy implements RejectedExecutionHandler { + /** The maximum time to wait for queue insertion, in milliseconds. */ private final long timeout; /** - * Creates a blocking submission policy with the specified timeout. + * Creates a new policy with the given blocking timeout. * - * @param timeout the maximum time to wait for queue capacity, in milliseconds + * @param timeout the maximum time to wait for queue insertion, in milliseconds */ public BlockingSubmissionPolicy(final long timeout) { this.timeout = timeout; } /** - * Attempts to offer the task to the executor's queue, blocking up to the configured timeout. - * - * @param runnable the rejected task - * @param executor the executor that rejected the task - * @throws RejectedExecutionException if the timeout is reached + * {@inheritDoc} */ @Override @SneakyThrows diff --git a/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/concurrent/ExecutorsProvider.java b/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/concurrent/ExecutorsProvider.java deleted file mode 100644 index 79821de..0000000 --- a/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/concurrent/ExecutorsProvider.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright 2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License 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.amazon.sqs.messaging.lib.concurrent; - -import java.lang.reflect.Method; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.function.Supplier; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import lombok.AccessLevel; -import lombok.NoArgsConstructor; -import lombok.SneakyThrows; - -/** - * Provides {@link ExecutorService} instances, selecting between virtual thread executors - * (Java 21+) and default single-thread executors based on the runtime Java version. - */ -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public final class ExecutorsProvider { - - private static final Logger LOGGER = LoggerFactory.getLogger(ExecutorsProvider.class); - - private static Supplier supplierExecutorService; - - static { - if (ExecutorsProvider.getJavaVersion() >= 21) { - ExecutorsProvider.supplierExecutorService = ExecutorsProvider::getVirtualExecutorService; - ExecutorsProvider.LOGGER.info("Java version is {}, using virtual thread executor", ExecutorsProvider.getJavaVersion()); - } else { - ExecutorsProvider.supplierExecutorService = ExecutorsProvider::getDefaultExecutorService; - ExecutorsProvider.LOGGER.info("Java version is {}, using default thread executor", ExecutorsProvider.getJavaVersion()); - } - } - - /** - * Returns an {@link ExecutorService} appropriate for the current Java version. - * - * @return a virtual thread executor (Java 21+) or a single-thread executor - */ - public static ExecutorService getExecutorService() { - return ExecutorsProvider.supplierExecutorService.get(); - } - - /** - * Creates a single-thread executor for Java versions below 21. - * - * @return a single-thread executor - */ - @SneakyThrows - private static ExecutorService getDefaultExecutorService() { - return Executors.newSingleThreadExecutor(); - } - - /** - * Creates a virtual thread executor using reflection (Java 21+). - * - * @return a virtual thread per task executor - */ - @SneakyThrows - private static ExecutorService getVirtualExecutorService() { - final Class clazzThread = Executors.class; - final Method ofVirtualMethod = clazzThread.getMethod("newVirtualThreadPerTaskExecutor"); - return ExecutorService.class.cast(ofVirtualMethod.invoke(null)); - } - - /** - * Parses the Java runtime version. - * - * @return the major Java version number - */ - private static int getJavaVersion() { - String version = System.getProperty("java.version"); - - if (version.startsWith("1.")) { - version = version.substring(2); - } - - final int dotPos = version.indexOf('.'); - final int dashPos = version.indexOf('-'); - final int endIndex = dotPos > -1 ? dotPos : dashPos > -1 ? dashPos : version.length(); - - return Integer.parseInt(version.substring(0, endIndex)); - } - -} diff --git a/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/concurrent/RingBufferBlockingQueue.java b/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/concurrent/RingBufferBlockingQueue.java index b0aed29..7e15edf 100644 --- a/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/concurrent/RingBufferBlockingQueue.java +++ b/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/concurrent/RingBufferBlockingQueue.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 the original author or authors. + * Copyright 2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,36 +34,48 @@ import lombok.SneakyThrows; /** - * A lock-based ring buffer implementation of {@link BlockingQueue} with a fixed - * capacity. Supports blocking {@code put} and {@code take} operations with - * fairness policy. + * A bounded blocking queue backed by a ring buffer (circular array). Supports + * blocking {@link #put(Object)} and {@link #take()} operations. Other + * {@link BlockingQueue} methods throw {@link UnsupportedOperationException}. * - * @param the element type + * @param the type of elements held in this queue */ public class RingBufferBlockingQueue extends AbstractQueue implements BlockingQueue { + /** Default capacity when no explicit capacity is provided. */ private static final int DEFAULT_CAPACITY = 2048; + /** The ring buffer array holding queue entries. */ private final AtomicReferenceArray> buffer; + /** The fixed maximum number of elements the queue can hold. */ private final int capacity; + /** + * Sequence number tracking the next write position (starts at -1 indicating no + * writes). + */ private final AtomicLong writeSequence = new AtomicLong(-1); + /** Sequence number tracking the next read position. */ private final AtomicLong readSequence = new AtomicLong(0); + /** Current number of elements in the queue. */ private final AtomicInteger size = new AtomicInteger(0); + /** Fair reentrant lock for coordinating producer/consumer access. */ private final ReentrantLock reentrantLock; + /** Condition for consumers waiting when the queue is empty. */ private final Condition waitingConsumer; + /** Condition for producers waiting when the queue is full. */ private final Condition waitingProducer; /** - * Creates a ring buffer blocking queue with the specified capacity. + * Creates a ring buffer with the specified capacity. * - * @param capacity the fixed capacity of the ring buffer + * @param capacity the maximum number of elements the queue can hold */ public RingBufferBlockingQueue(final int capacity) { this.capacity = capacity; @@ -75,34 +87,42 @@ public RingBufferBlockingQueue(final int capacity) { } /** - * Creates a ring buffer blocking queue with the default capacity of 2048. + * Creates a ring buffer with the default capacity of 2048. */ public RingBufferBlockingQueue() { this(RingBufferBlockingQueue.DEFAULT_CAPACITY); } /** - * Prevents sequence overflow by wrapping around if the value approaches - * Long.MAX_VALUE. + * Prevents sequence overflow by wrapping around when the maximum long value is + * reached. * * @param sequence the current sequence value - * @return the sequence, or a wrapped value if near overflow + * @return the sequence value, wrapped if necessary */ private long avoidSequenceOverflow(final long sequence) { return (sequence < Long.MAX_VALUE ? sequence : wrap(sequence)); } /** - * Computes the buffer index for a given sequence number using modulo - * arithmetic. + * Wraps a sequence number to a valid buffer index. * - * @param sequence the sequence number + * @param sequence the sequence number to wrap * @return the buffer index */ private int wrap(final long sequence) { return Math.toIntExact(sequence % capacity); } + /** + * Returns the fixed capacity of this ring buffer. + * + * @return the capacity + */ + public int capacity() { + return capacity; + } + /** * {@inheritDoc} */ @@ -120,9 +140,9 @@ public boolean isEmpty() { } /** - * Checks if the buffer has reached its capacity. + * Returns whether the queue is full. * - * @return true if the buffer is full + * @return true if the queue size equals its capacity */ public boolean isFull() { return size.get() >= capacity; @@ -276,6 +296,11 @@ public int drainTo(final Collection collection, final int maxElements throw new UnsupportedOperationException(); } + /** + * Internal entry wrapper that holds a value within the ring buffer. + * + * @param the type of the value + */ @Getter @Setter static class Entry { diff --git a/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/concurrent/ThreadFactoryProvider.java b/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/concurrent/ThreadFactoryProvider.java index 85cde15..d21a60a 100644 --- a/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/concurrent/ThreadFactoryProvider.java +++ b/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/concurrent/ThreadFactoryProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,7 +30,8 @@ /** * Provides {@link ThreadFactory} instances, selecting between virtual thread - * factories (Java 21+) and default thread factories based on the runtime Java version. + * factories (Java 21+) and default thread factories based on the runtime Java + * version. */ @NoArgsConstructor(access = AccessLevel.PRIVATE) public final class ThreadFactoryProvider { diff --git a/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/core/AbstractAmazonSqsConsumer.java b/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/core/AbstractAmazonSqsConsumer.java index 92389f9..0e311d6 100644 --- a/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/core/AbstractAmazonSqsConsumer.java +++ b/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/core/AbstractAmazonSqsConsumer.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,48 +54,64 @@ // @formatter:off /** - * Abstract base class for consuming and batching messages from an Amazon SQS queue. - * Implements {@link Runnable} to periodically flush batched messages. + * Abstract base class for Amazon SNS message consumers. Periodically drains a + * {@link BlockingQueue} of {@link RequestEntry} items, batches them, and publishes + * them to SNS. Subclasses implement the actual publish and response handling logic. * * @param the Amazon SQS client type * @param the publish batch request type - * @param the publish batch response type + * @param the publish batch result type * @param the request entry payload type */ -abstract class AbstractAmazonSqsConsumer implements Runnable { +abstract class AbstractAmazonSqsConsumer implements Runnable, AmazonSqsConsumer { + /** + * Kilobyte constant used for size calculations. + */ private static final Integer KB = 1024; + /** + * Maximum batch size threshold of 1024 KB imposed by Amazon SQS. + */ private static final Integer BATCH_SIZE_BYTES_THRESHOLD = 1024 * KB; + /** Class logger. */ private static final Logger LOGGER = LoggerFactory.getLogger(AbstractAmazonSqsConsumer.class); + /** Single-thread scheduler that periodically triggers batch draining. */ private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(ThreadFactoryProvider.getThreadFactory()); + /** The Amazon SQS client used for publishing batches. */ protected final C amazonSqsClient; + /** The topic configuration properties. */ private final QueueProperty queueProperty; + /** Factory for creating internal request entry representations. */ private final RequestEntryInternalFactory requestEntryInternalFactory; + /** Shared map of pending requests keyed by request ID for async completion. */ protected final ConcurrentMap> pendingRequests; + /** The blocking queue that buffers incoming topic requests. */ private final BlockingQueue> queueRequests; + /** Optional decorator applied to the publish batch request before sending. */ private final UnaryOperator publishDecorator; + /** Executor service for asynchronous (non-FIFO) publishing. */ private final ExecutorService executorService; /** - * Constructs a new consumer with the given dependencies. + * Creates a new abstract consumer. * - * @param amazonSqsClient the Amazon SQS client - * @param queueProperty the queue configuration properties - * @param objectMapper the JSON object mapper for payload serialization - * @param pendingRequests the map of pending requests keyed by request ID - * @param queueRequests the blocking queue of incoming requests - * @param executorService the executor service for async publishing - * @param publishDecorator a decorator function applied to batch publish requests + * @param amazonSqsClient the Amazon SQS client + * @param queueProperty the queue configuration + * @param objectMapper the Jackson ObjectMapper for payload serialization + * @param pendingRequests the shared map of pending requests keyed by request ID + * @param queueRequests the shared blocking queue for queue requests + * @param executorService the executor service for async publishing + * @param publishDecorator a decorator for the publish batch request */ protected AbstractAmazonSqsConsumer( final C amazonSqsClient, @@ -117,30 +133,6 @@ protected AbstractAmazonSqsConsumer( scheduledExecutorService.scheduleAtFixedRate(this, 0, queueProperty.getLinger(), TimeUnit.MILLISECONDS); } - /** - * Publishes a batch of messages to Amazon SQS. - * - * @param publishBatchRequest the batch publish request - * @return the batch publish response - */ - protected abstract O publish(final R publishBatchRequest); - - /** - * Handles errors that occur during batch publishing. - * - * @param publishBatchRequest the batch publish request that failed - * @param throwable the exception that occurred - */ - protected abstract void handleError(final R publishBatchRequest, final Throwable throwable); - - /** - * Handles the successful response from a batch publish operation, notifying - * pending futures of success or failure per entry. - * - * @param publishBatchResult the batch publish result - */ - protected abstract void handleResponse(final O publishBatchResult); - /** * Provides a factory function that creates a batch publish request from a queue URL * and a list of internal request entries. @@ -201,6 +193,7 @@ public void run() { * Shuts down the consumer, waiting for all pending requests to complete * before terminating the scheduled and executor services. */ + @Override @SneakyThrows public void shutdown() { await().thenRun(() -> { @@ -335,6 +328,7 @@ private Optional createBatch(final BlockingQueue> requests) { * * @return a future that completes when all requests are processed */ + @Override @SneakyThrows public CompletableFuture await() { return CompletableFuture.runAsync(() -> { diff --git a/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/core/AbstractAmazonSqsProducer.java b/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/core/AbstractAmazonSqsProducer.java index b0d5ade..c0ac684 100644 --- a/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/core/AbstractAmazonSqsProducer.java +++ b/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/core/AbstractAmazonSqsProducer.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,14 +16,9 @@ package com.amazon.sqs.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.TimeUnit; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import java.util.concurrent.atomic.AtomicReference; import com.amazon.sqs.messaging.lib.model.RequestEntry; import com.amazon.sqs.messaging.lib.model.ResponseFailEntry; @@ -33,6 +28,7 @@ import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; +//@formatter:off /** * Abstract base class for producing messages to an Amazon SQS queue. Enqueues * request entries and tracks their completion via {@link ListenableFuture}. @@ -40,40 +36,46 @@ * @param the request entry payload type */ @RequiredArgsConstructor(access = AccessLevel.PROTECTED) -abstract class AbstractAmazonSqsProducer { +abstract class AbstractAmazonSqsProducer implements AmazonSqsProducer { - private static final Logger LOGGER = LoggerFactory.getLogger(AbstractAmazonSqsProducer.class); + private final AtomicReference state = new AtomicReference<>(State.RUNNIG); private final ConcurrentMap> pendingRequests; private final BlockingQueue> queueRequests; - private final ExecutorService executorService; - /** * Sends a request entry by enqueuing it for batch processing. * * @param requestEntry the request entry to send * @return a {@link ListenableFuture} for tracking the send result */ + @Override @SneakyThrows public ListenableFuture send(final RequestEntry requestEntry) { - return enqueueRequest(requestEntry); + if (State.RUNNIG.equals(state.get())) { + return enqueueRequest(requestEntry); + } else { + final ListenableFutureImpl listenableFutureImpl = new ListenableFutureImpl(); + + listenableFutureImpl.fail(ResponseFailEntry.builder() + .withCode("000") + .withId(requestEntry.getId()) + .withMessage(String.format("Producer is currently in %s mode; no further messages will be accepted.", state.get().name())) + .withSenderFault(true) + .build()); + + return listenableFutureImpl; + } } /** - * Shuts down the producer's executor service, waiting for ongoing tasks to complete. + * Transitions the producer to the shutdown state. No further messages will be + * accepted once shutdown. */ - @SneakyThrows + @Override public void shutdown() { - LOGGER.warn("Shutdown producer {}", getClass().getSimpleName()); - - executorService.shutdown(); - if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) { - LOGGER.warn("Executor service did not terminate in the specified time."); - final List droppedTasks = executorService.shutdownNow(); - LOGGER.warn("Executor service was abruptly shut down. {} tasks will not be executed.", droppedTasks.size()); - } + state.compareAndSet(State.RUNNIG, State.SHUTDOWN); } /** @@ -90,4 +92,11 @@ private ListenableFuture enqueueRequest return trackPendingRequest; } + /** + * Lifecycle states of the producer. + */ + enum State { + RUNNIG, SHUTDOWN + } + } \ No newline at end of file diff --git a/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/core/AbstractAmazonSqsTemplate.java b/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/core/AbstractAmazonSqsTemplate.java index 695e242..4821567 100644 --- a/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/core/AbstractAmazonSqsTemplate.java +++ b/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/core/AbstractAmazonSqsTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,39 +16,55 @@ package com.amazon.sqs.messaging.lib.core; +import java.util.Objects; +import java.util.concurrent.BlockingQueue; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ExecutorService; +import java.util.function.Function; +import java.util.function.UnaryOperator; import com.amazon.sqs.messaging.lib.concurrent.AmazonSqsThreadPoolExecutor; +import com.amazon.sqs.messaging.lib.concurrent.RingBufferBlockingQueue; +import com.amazon.sqs.messaging.lib.metrics.BlockingQueueMetricsDecorator; +import com.amazon.sqs.messaging.lib.metrics.ExecutorServiceMetricsDecorator; import com.amazon.sqs.messaging.lib.model.QueueProperty; import com.amazon.sqs.messaging.lib.model.RequestEntry; import com.amazon.sqs.messaging.lib.model.ResponseFailEntry; import com.amazon.sqs.messaging.lib.model.ResponseSuccessEntry; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import lombok.AccessLevel; +import lombok.Getter; import lombok.RequiredArgsConstructor; +//@formatter:off /** - * Abstract base template for Amazon SQS messaging operations. Provides a high-level - * abstraction for sending messages and managing the lifecycle of the underlying - * producer and consumer components. + * Abstract base template for Amazon SQS messaging operations. Provides a + * high-level abstraction for sending messages and managing the lifecycle of the + * underlying producer and consumer components. * - * @param the Amazon SQS client type * @param the publish batch request type * @param the publish batch response type * @param the request entry payload type */ @RequiredArgsConstructor(access = AccessLevel.PROTECTED) -abstract class AbstractAmazonSqsTemplate { +abstract class AbstractAmazonSqsTemplate { - private final AbstractAmazonSqsProducer amazonSqsProducer; + private final AmazonSqsProducer amazonSqsProducer; - private final AbstractAmazonSqsConsumer amazonSqsConsumer; + private final AmazonSqsConsumer amazonSqsConsumer; /** * Sends a message to the Amazon SQS queue. * - * @param requestEntry the request entry containing the message payload and headers - * @return a {@link ListenableFuture} that can be used to track the success or failure of the send operation + * @param requestEntry the request entry containing the message payload and + * headers + * @return a {@link ListenableFuture} that can be used to track the success or + * failure of the send operation */ public ListenableFuture send(final RequestEntry requestEntry) { return amazonSqsProducer.send(requestEntry); @@ -65,25 +81,163 @@ public void shutdown() { /** * Waits for all pending requests to be processed. * - * @return a {@link CompletableFuture} that completes when all pending requests have been processed + * @return a {@link CompletableFuture} that completes when all pending requests + * have been processed */ public CompletableFuture await() { return amazonSqsConsumer.await(); } /** - * Creates an {@link AmazonSqsThreadPoolExecutor} configured for the given queue property. - * For FIFO queues, a single-thread executor is used to preserve message ordering. + * Creates an {@link AmazonSqsThreadPoolExecutor} configured for the given queue + * property. For FIFO queues, a single-thread executor is used to preserve + * message ordering. * * @param queueProperty the queue configuration properties * @return a configured thread pool executor */ - protected static AmazonSqsThreadPoolExecutor getAmazonSqsThreadPoolExecutor(final QueueProperty queueProperty) { - if (queueProperty.isFifo()) { - return new AmazonSqsThreadPoolExecutor(1); - } else { - return new AmazonSqsThreadPoolExecutor(queueProperty.getMaximumPoolSize()); + protected static ExecutorService getExecutorService(final QueueProperty queueProperty, final MeterRegistry meterRegistry) { + return queueProperty.isFifo() + ? new ExecutorServiceMetricsDecorator( + new AmazonSqsThreadPoolExecutor(1), + meterRegistry, + queueProperty.getQueueUrl() + ) + : new ExecutorServiceMetricsDecorator( + new AmazonSqsThreadPoolExecutor(queueProperty.getMaximumPoolSize()), + meterRegistry, + queueProperty.getQueueUrl() + ); + } + + @Getter + public static final class Builder> { + + /** + * The Amazon SNS client used for publishing. + */ + private final C amazonSqsClient; + + /** + * The queue configuration properties. + */ + private final QueueProperty queueProperty; + + /** + * Map of pending requests tracked by request ID for asynchronous completion. + */ + private ConcurrentMap> pendingRequests = new ConcurrentHashMap<>(); + + /** + * The blocking queue for buffering queue requests before batching. + */ + private BlockingQueue> queueRequests; + + /** + * The Jackson ObjectMapper for serializing payloads. + */ + private ObjectMapper objectMapper = new ObjectMapper(); + + /** + * Decorator function applied to the publish batch request before sending. + */ + private UnaryOperator publishDecorator = UnaryOperator.identity(); + + /** + * The Micrometer meter registry for collecting metrics. + */ + private MeterRegistry meterRegistry = new SimpleMeterRegistry(); + + /** + * Internal constructor reference used to create the template instance. + */ + private final Function, T> constructor; + + /** + * Creates a new builder with the required constructor reference, client, and topic. + * + * @param constructor the constructor function for creating the template instance + * @param amazonSqsClient the Amazon SQS client + * @param queueProperty the queue configuration properties + */ + Builder(final Function, T> constructor, final C amazonSqsClient, final QueueProperty queueProperty) { + this.amazonSqsClient = Objects.requireNonNull(amazonSqsClient, "amazonSqsClient"); + this.queueProperty = Objects.requireNonNull(queueProperty, "queueProperty"); + this.constructor = Objects.requireNonNull(constructor, "constructor"); + } + + /** + * Sets the map of pending requests. + * + * @param pendingRequests the concurrent map keyed by request ID + * @return this builder + */ + public Builder pendingRequests(final ConcurrentMap> pendingRequests) { + this.pendingRequests = Objects.requireNonNull(pendingRequests, "pendingRequests"); + return this; } + + /** + * Sets the blocking queue for topic requests. + * + * @param queueRequests the blocking queue for queue requests + * @return this builder + */ + public Builder queueRequests(final BlockingQueue> queueRequests) { + this.queueRequests = Objects.requireNonNull(queueRequests, "queueRequests"); + return this; + } + + /** + * Sets the Jackson ObjectMapper for serializing payloads. + * + * @param objectMapper the Jackson ObjectMapper + * @return this builder + */ + public Builder objectMapper(final ObjectMapper objectMapper) { + this.objectMapper = Objects.requireNonNull(objectMapper, "objectMapper"); + return this; + } + + /** + * Sets the decorator for the publish batch request. + * + * @param publishDecorator the unary operator to apply before publishing + * @return this builder + */ + public Builder publishDecorator(final UnaryOperator publishDecorator) { + this.publishDecorator = Objects.requireNonNull(publishDecorator, "publishDecorator"); + return this; + } + + /** + * Sets the Micrometer meter registry. + * + * @param meterRegistry the meter registry for metrics + * @return this builder + */ + public Builder meterRegistry(final MeterRegistry meterRegistry) { + this.meterRegistry = Objects.requireNonNull(meterRegistry, "meterRegistry"); + return this; + } + + /** + * Builds the template instance. If no topic requests queue was provided, a default + * {@link RingBufferBlockingQueue} is created. The queue is then decorated with + * {@link BlockingQueueMetricsDecorator}. + * + * @return the constructed template instance + */ + public T build() { + if (Objects.isNull(queueRequests)) { + queueRequests = new RingBufferBlockingQueue<>(queueProperty.getMaximumPoolSize() * queueProperty.getMaxBatchSize()); + } + + queueRequests = new BlockingQueueMetricsDecorator<>(queueRequests, meterRegistry, queueProperty.getQueueUrl()); + + return constructor.apply(this); + } + } } \ No newline at end of file diff --git a/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/core/AbstractMessageAttributes.java b/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/core/AbstractMessageAttributes.java index 6d1b7e5..cae4644 100644 --- a/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/core/AbstractMessageAttributes.java +++ b/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/core/AbstractMessageAttributes.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/core/AmazonSqsConsumer.java b/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/core/AmazonSqsConsumer.java new file mode 100644 index 0000000..ca74d24 --- /dev/null +++ b/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/core/AmazonSqsConsumer.java @@ -0,0 +1,70 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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.amazon.sqs.messaging.lib.core; + +import java.util.concurrent.CompletableFuture; + +/** + * Consumer interface for Amazon SNS messaging. Implementations handle batch + * publishing of requests and dispatching of responses or errors to pending + * request futures. + * + * @param the publish batch request type + * @param the publish batch result type + */ +public interface AmazonSqsConsumer { + + /** + * Publishes a batch of messages to Amazon SQS. + * + * @param publishBatchRequest the batch publish request + * @return the batch publish response + */ + public O publish(final R publishBatchRequest); + + /** + * Handles errors that occur during batch publishing. + * + * @param publishBatchRequest the batch publish request that failed + * @param throwable the exception that occurred + */ + public void handleError(final R publishBatchRequest, final Throwable throwable); + + /** + * Handles the successful response from a batch publish operation, notifying + * pending futures of success or failure per entry. + * + * @param publishBatchResult the batch publish result + */ + public void handleResponse(final O publishBatchResult); + + /** + * Shuts down the consumer, waiting up to 60 seconds for both the scheduled and + * worker executor services to terminate. + */ + public void shutdown(); + + /** + * 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). + * + * @return a future that completes when all requests are drained + */ + public CompletableFuture await(); + +} diff --git a/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/core/AmazonSqsProducer.java b/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/core/AmazonSqsProducer.java new file mode 100644 index 0000000..5f00472 --- /dev/null +++ b/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/core/AmazonSqsProducer.java @@ -0,0 +1,48 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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.amazon.sqs.messaging.lib.core; + +import com.amazon.sqs.messaging.lib.model.RequestEntry; +import com.amazon.sqs.messaging.lib.model.ResponseFailEntry; +import com.amazon.sqs.messaging.lib.model.ResponseSuccessEntry; + +/** + * Producer interface for Amazon SNS messaging. Implementations enqueue request + * entries for batch publishing and track pending requests for asynchronous + * completion. + * + * @param the request entry payload type + */ +public interface AmazonSqsProducer { + + /** + * Sends a request entry for asynchronous publishing to an SNS topic. + * + * @param requestEntry the request entry containing the message payload and + * metadata + * @return a {@link ListenableFuture} that completes when the request is + * processed + */ + public ListenableFuture send(final RequestEntry requestEntry); + + /** + * Shuts down the producer, preventing any further messages from being accepted. + */ + public void shutdown(); + +} +// @formatter:on diff --git a/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/core/ListenableFuture.java b/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/core/ListenableFuture.java index a006f9b..b07acb5 100644 --- a/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/core/ListenableFuture.java +++ b/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/core/ListenableFuture.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,7 +44,8 @@ public interface ListenableFuture { * @param successCallback the callback invoked on success */ default void addCallback(final Consumer successCallback) { - addCallback(successCallback, result -> { }); + addCallback(successCallback, result -> { + }); } /** diff --git a/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/core/ListenableFutureImpl.java b/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/core/ListenableFutureImpl.java index 62b61a2..b10e63e 100644 --- a/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/core/ListenableFutureImpl.java +++ b/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/core/ListenableFutureImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -56,8 +56,10 @@ class ListenableFutureImpl implements ListenableFuture successCallback, final Consumer failureCallback) { synchronized (mutex) { - final Consumer success = Objects.nonNull(successCallback) ? successCallback : result -> { }; - final Consumer failure = Objects.nonNull(failureCallback) ? failureCallback : result -> { }; + final Consumer success = Objects.nonNull(successCallback) ? successCallback : result -> { + }; + final Consumer failure = Objects.nonNull(failureCallback) ? failureCallback : result -> { + }; switch (state) { case NEW: diff --git a/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/core/RequestEntryInternalFactory.java b/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/core/RequestEntryInternalFactory.java index 1662d11..4076301 100644 --- a/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/core/RequestEntryInternalFactory.java +++ b/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/core/RequestEntryInternalFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -60,6 +60,16 @@ public RequestEntryInternal create(final RequestEntry requestEntry, final byt .build(); } + /** + * Creates an internal request entry, serializing the payload automatically. + * + * @param requestEntry the source request entry + * @return a new internal request entry with serialized payload + */ + public RequestEntryInternal create(final RequestEntry requestEntry) { + return create(requestEntry, convertPayload(requestEntry)); + } + /** * Converts a request entry's value to a byte array. Strings are converted using UTF-8, * and other types are serialized using Jackson JSON. @@ -112,8 +122,6 @@ static class RequestEntryInternal { private final Map messageHeaders; - private final String subject; - private final String groupId; private final String deduplicationId; diff --git a/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/exception/MaximumAllowedMessageException.java b/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/exception/MaximumAllowedMessageException.java index adf4bc8..d051166 100644 --- a/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/exception/MaximumAllowedMessageException.java +++ b/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/exception/MaximumAllowedMessageException.java @@ -1,3 +1,19 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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.amazon.sqs.messaging.lib.exception; import com.amazon.sqs.messaging.lib.model.RequestEntry; @@ -9,7 +25,7 @@ * Contains the original request entry for error handling. */ @Getter -@SuppressWarnings({ "rawtypes", "unchecked" }) +@SuppressWarnings({ "rawtypes", "unchecked", "java:S1948" }) public class MaximumAllowedMessageException extends RuntimeException { private static final long serialVersionUID = -529663449633021689L; diff --git a/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/metrics/AbstractAmazonSqsConsumerMetricsDecorator.java b/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/metrics/AbstractAmazonSqsConsumerMetricsDecorator.java new file mode 100644 index 0000000..755f771 --- /dev/null +++ b/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/metrics/AbstractAmazonSqsConsumerMetricsDecorator.java @@ -0,0 +1,181 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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.amazon.sqs.messaging.lib.metrics; + +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicInteger; + +import com.amazon.sqs.messaging.lib.core.AmazonSqsConsumer; +import com.amazon.sqs.messaging.lib.model.QueueProperty; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.DistributionSummary; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.Timer; +import io.micrometer.core.instrument.composite.CompositeMeterRegistry; + +// @formatter:off +/** + * Abstract base class for decorating an {@link AmazonSnsConsumer} with Micrometer metrics. + * Tracks publish attempts, successes, failures, latency, batch size, and inflight counts. + * + * @param the publish batch request type + * @param the publish batch result type + */ +abstract class AbstractAmazonSqsConsumerMetricsDecorator implements AmazonSqsConsumer { + + /** Base tag prefix for all SQS metrics. */ + private static final String TAG_SQS = "sqs"; + + /** Metric name for total publish attempts. */ + protected static final String METRIC_PUBLISH_ATTEMPTS = TAG_SQS.concat(".publish.attempts"); + + /** Metric name for successful publish operations. */ + protected static final String METRIC_PUBLISH_SUCCESS = TAG_SQS.concat(".publish.success"); + + /** Metric name for failed publish operations. */ + protected static final String METRIC_PUBLISH_FAILURE = TAG_SQS.concat(".publish.failure"); + + /** Metric name for publish latency duration. */ + protected static final String METRIC_PUBLISH_DURATION = TAG_SQS.concat(".publish.duration"); + + /** Metric name for publish batch size distribution. */ + protected static final String METRIC_PUBLISH_BATCH_SIZE = TAG_SQS.concat(".publish.batch.size"); + + /** Metric name for inflight publish count. */ + protected static final String METRIC_PUBLISH_INFLIGHT = TAG_SQS.concat(".publish.inflight"); + + /** Tag key for error code dimension. */ + protected static final String TAG_ERROR_CODE = "error_code"; + + /** Tag key for error type dimension. */ + protected static final String TAG_ERROR_TYPE = "error_type"; + + /** Error type value for Amazon service exceptions. */ + protected static final String ERROR_TYPE_AMAZON = "amazon_service_exception"; + + /** Error type value for unknown exceptions. */ + protected static final String ERROR_TYPE_OTHER = "unknown"; + + /** The composite Micrometer meter registry. */ + protected final MeterRegistry registry; + + /** Metric tags identifying the SQS queue. */ + protected final Tags tags; + + /** Counter for total publish attempts. */ + protected final Counter publishAttemptsCounter; + + /** Counter for successful publishes. */ + protected final Counter successCounter; + + /** Timer for publish latency. */ + protected final Timer publishTimer; + + /** Distribution summary for batch sizes. */ + protected final DistributionSummary batchSizeSummary; + + /** Atomic gauge tracking the number of inflight publish operations. */ + protected final AtomicInteger inflightGauge = new AtomicInteger(); + + /** The decorated {@link AmazonSqsConsumer} delegate. */ + protected final AmazonSqsConsumer delegate; + + /** + * Creates a new metrics decorator. + * + * @param delegate the consumer to decorate + * @param queueProperty the queue configuration (used for queue tags) + * @param meterRegistry the Micrometer meter registry (may be null) + */ + AbstractAmazonSqsConsumerMetricsDecorator( + final AmazonSqsConsumer delegate, + final QueueProperty queueProperty, + final MeterRegistry meterRegistry) { + + this.delegate = delegate; + + final CompositeMeterRegistry compositeMeterRegistry = new CompositeMeterRegistry(); + + Optional.ofNullable(meterRegistry).ifPresent(compositeMeterRegistry::add); + + registry = compositeMeterRegistry; + + tags = Tags.of("queue", queueProperty.getQueueUrl()); + + publishAttemptsCounter = Counter.builder(METRIC_PUBLISH_ATTEMPTS) + .tags(tags) + .description("Total number of SQS PublishBatch calls attempted") + .register(compositeMeterRegistry); + + successCounter = Counter.builder(METRIC_PUBLISH_SUCCESS) + .tags(tags) + .description("Individual SQS messages acknowledged as successful") + .register(compositeMeterRegistry); + + publishTimer = Timer.builder(METRIC_PUBLISH_DURATION) + .tags(tags) + .description("End-to-end latency of SQS PublishBatch calls") + .publishPercentiles(0.5, 0.95, 0.99) + .register(compositeMeterRegistry); + + batchSizeSummary = DistributionSummary.builder(METRIC_PUBLISH_BATCH_SIZE) + .tags(tags) + .description("Number of entries per SQS PublishBatch request") + .register(compositeMeterRegistry); + + Gauge.builder(METRIC_PUBLISH_INFLIGHT, inflightGauge, AtomicInteger::get) + .tags(tags) + .description("PublishBatches currently in progress") + .register(compositeMeterRegistry); + } + + /** + * Returns (creating if necessary) a failure counter tagged with the given error code and type. + * + * @param errorCode the error code for the failure tag + * @param errorType the error type for the failure tag + * @return the failure counter + */ + protected Counter failureCounter(final String errorCode, final String errorType) { + return Counter.builder(METRIC_PUBLISH_FAILURE) + .description("Individual SQS messages that failed to be published") + .tags(tags.and(TAG_ERROR_CODE, errorCode) + .and(TAG_ERROR_TYPE, errorType)) + .register(registry); + } + + /** + * {@inheritDoc} + */ + @Override + public void shutdown() { + delegate.shutdown(); + } + + /** + * {@inheritDoc} + */ + @Override + public CompletableFuture await() { + return delegate.await(); + } + +} diff --git a/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/metrics/BlockingQueueMetricsDecorator.java b/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/metrics/BlockingQueueMetricsDecorator.java new file mode 100644 index 0000000..c79760c --- /dev/null +++ b/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/metrics/BlockingQueueMetricsDecorator.java @@ -0,0 +1,363 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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.amazon.sqs.messaging.lib.metrics; + +import java.util.Collection; +import java.util.Iterator; +import java.util.Optional; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.Timer; +import io.micrometer.core.instrument.composite.CompositeMeterRegistry; + +// @formatter:off +/** + * A decorator around {@link BlockingQueue} that collects Micrometer metrics for + * put/take operations, including counters, latency histograms, and queue size gauges. + * + * @param the type of elements held in the queue + */ +public class BlockingQueueMetricsDecorator implements BlockingQueue { + + /** Base tag prefix for blocking queue metrics. */ + private static final String TAG_QUEUE = "blocking.queue"; + + /** Metric name for total put operations. */ + private static final String METRIC_PUTS_TOTAL = TAG_QUEUE.concat(".puts.total"); + + /** Metric name for failed put operations. */ + private static final String METRIC_PUTS_FAILED = TAG_QUEUE.concat(".puts.failed"); + + /** Metric name for put operation latency. */ + private static final String METRIC_PUT_DURATION = TAG_QUEUE.concat(".put.duration"); + + /** Metric name for total take operations. */ + private static final String METRIC_TAKES_TOTAL = TAG_QUEUE.concat(".takes.total"); + + /** Metric name for failed take operations. */ + private static final String METRIC_TAKES_FAILED = TAG_QUEUE.concat(".takes.failed"); + + /** Metric name for take operation latency. */ + private static final String METRIC_TAKE_DURATION = TAG_QUEUE.concat(".take.duration"); + + /** Metric name for queue size gauge. */ + private static final String METRIC_SIZE = TAG_QUEUE.concat(".size"); + + /** The decorated blocking queue. */ + private final BlockingQueue delegate; + + /** Counter for successful put operations. */ + private final Counter putsTotal; + + /** Counter for put operations that threw an exception. */ + private final Counter putsFailed; + + /** Timer for put operation latency. */ + private final Timer putDuration; + + /** Counter for successful take operations. */ + private final Counter takesTotal; + + /** Counter for take operations that threw an exception. */ + private final Counter takesFailed; + + /** Timer for take operation latency. */ + private final Timer takeDuration; + + /** + * Creates a new metrics decorator for the given blocking queue. + * + * @param delegate the blocking queue to decorate + * @param registry the Micrometer meter registry + * @param queueName the name tag for the metrics + */ + public BlockingQueueMetricsDecorator( + final BlockingQueue delegate, + final MeterRegistry registry, + final String queueName) { + + this.delegate = delegate; + + final CompositeMeterRegistry compositeMeterRegistry = new CompositeMeterRegistry(); + + Optional.ofNullable(registry).ifPresent(compositeMeterRegistry::add); + + final Tags tags = Tags.of("name", queueName); + + putsTotal = Counter.builder(METRIC_PUTS_TOTAL) + .description("Total number of successful put operations") + .tags(tags) + .register(compositeMeterRegistry); + + putsFailed = Counter.builder(METRIC_PUTS_FAILED) + .description("Total number of put operations that threw an exception") + .tags(tags) + .register(compositeMeterRegistry); + + takesTotal = Counter.builder(METRIC_TAKES_TOTAL) + .description("Total number of successful take operations") + .tags(tags) + .register(compositeMeterRegistry); + + takesFailed = Counter.builder(METRIC_TAKES_FAILED) + .description("Total number of take operations that threw an exception") + .tags(tags) + .register(compositeMeterRegistry); + + putDuration = Timer.builder(METRIC_PUT_DURATION) + .description("Latency of put operations (including wait time when queue is full)") + .tags(tags) + .publishPercentileHistogram() + .register(compositeMeterRegistry); + + takeDuration = Timer.builder(METRIC_TAKE_DURATION) + .description("Latency of take operations (including wait time when queue is empty)") + .tags(tags) + .publishPercentileHistogram() + .register(compositeMeterRegistry); + + Gauge.builder(METRIC_SIZE, this.delegate, BlockingQueue::size) + .description("Current number of elements in the queue") + .tags(tags) + .register(compositeMeterRegistry); + } + + /** + * {@inheritDoc} + */ + @Override + public void put(final E element) throws InterruptedException { + final Timer.Sample sample = Timer.start(); + try { + delegate.put(element); + putsTotal.increment(); + } catch (final Exception ex) { + putsFailed.increment(); + throw ex; + } finally { + sample.stop(putDuration); + } + } + + /** + * {@inheritDoc} + */ + @Override + public E take() throws InterruptedException { + final Timer.Sample sample = Timer.start(); + try { + final E value = delegate.take(); + takesTotal.increment(); + return value; + } catch (final Exception ex) { + takesFailed.increment(); + throw ex; + } finally { + sample.stop(takeDuration); + } + } + + /** + * {@inheritDoc} + */ + @Override + public int size() { + return delegate.size(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isEmpty() { + return delegate.isEmpty(); + } + + /** + * {@inheritDoc} + */ + @Override + public E peek() { + return delegate.peek(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean offer(final E element) { + return delegate.offer(element); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean offer(final E element, final long timeout, final TimeUnit unit) throws InterruptedException { + return delegate.offer(element, timeout, unit); + } + + /** + * {@inheritDoc} + */ + @Override + public E poll() { + return delegate.poll(); + } + + /** + * {@inheritDoc} + */ + @Override + public E poll(final long timeout, final TimeUnit unit) throws InterruptedException { + return delegate.poll(timeout, unit); + } + + /** + * {@inheritDoc} + */ + @Override + public Iterator iterator() { + return delegate.iterator(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean add(final E element) { + return delegate.add(element); + } + + /** + * {@inheritDoc} + */ + @Override + public int remainingCapacity() { + return delegate.remainingCapacity(); + } + + /** + * {@inheritDoc} + */ + @Override + public int drainTo(final Collection collection) { + return delegate.drainTo(collection); + } + + /** + * {@inheritDoc} + */ + @Override + public int drainTo(final Collection collection, final int maxElements) { + return delegate.drainTo(collection, maxElements); + } + + /** + * {@inheritDoc} + */ + @Override + public E remove() { + return delegate.remove(); + } + + /** + * {@inheritDoc} + */ + @Override + public E element() { + return delegate.element(); + } + + /** + * {@inheritDoc} + */ + @Override + public Object[] toArray() { + return delegate.toArray(); + } + + /** + * {@inheritDoc} + */ + @Override + public T[] toArray(final T[] a) { + return delegate.toArray(a); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean containsAll(final Collection c) { + return delegate.containsAll(c); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean addAll(final Collection c) { + return delegate.addAll(c); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean removeAll(final Collection c) { + return delegate.removeAll(c); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean retainAll(final Collection c) { + return delegate.retainAll(c); + } + + /** + * {@inheritDoc} + */ + @Override + public void clear() { + delegate.clear(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean remove(final Object o) { + return delegate.remove(o); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean contains(final Object o) { + return delegate.contains(o); + } + +} \ No newline at end of file diff --git a/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/metrics/ExecutorServiceMetricsDecorator.java b/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/metrics/ExecutorServiceMetricsDecorator.java new file mode 100644 index 0000000..aa881b2 --- /dev/null +++ b/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/metrics/ExecutorServiceMetricsDecorator.java @@ -0,0 +1,241 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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.amazon.sqs.messaging.lib.metrics; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.Timer; +import io.micrometer.core.instrument.composite.CompositeMeterRegistry; + +// @formatter:off +/** + * A decorator around {@link ExecutorService} that collects Micrometer metrics for + * task execution, including active task count, succeeded/failed counters, and + * task duration histograms. + */ +public class ExecutorServiceMetricsDecorator implements ExecutorService { + + /** Base tag prefix for executor metrics. */ + private static final String TAG_EXECUTOR = "executor"; + + /** Metric name for active task count gauge. */ + private static final String METRIC_ACTIVE = TAG_EXECUTOR.concat(".active"); + + /** Metric name for succeeded task counter. */ + private static final String METRIC_TASKS_SUCCEEDED = TAG_EXECUTOR.concat(".tasks.succeeded"); + + /** Metric name for failed task counter. */ + private static final String METRIC_TASKS_FAILED = TAG_EXECUTOR.concat(".tasks.failed"); + + /** Metric name for task duration timer. */ + private static final String METRIC_TASK_DURATION = TAG_EXECUTOR.concat(".task.duration"); + + /** The decorated executor service. */ + private final ExecutorService delegate; + + /** Atomic gauge tracking the number of active tasks. */ + private final AtomicInteger activeTaskCount = new AtomicInteger(); + + /** Counter for tasks that completed without throwing an exception. */ + private final Counter succeededCounter; + + /** Counter for tasks that completed by throwing an exception. */ + private final Counter failedCounter; + + /** Timer for wall-clock duration of task execution. */ + private final Timer taskTimer; + + /** + * Creates a new metrics decorator for the given executor service. + * + * @param delegate the executor service to decorate + * @param registry the Micrometer meter registry + * @param executorName the name tag for the metrics + */ + public ExecutorServiceMetricsDecorator( + final ExecutorService delegate, + final MeterRegistry registry, + final String executorName) { + + this.delegate = delegate; + + final CompositeMeterRegistry compositeMeterRegistry = new CompositeMeterRegistry(); + + Optional.ofNullable(registry).ifPresent(compositeMeterRegistry::add); + + final Tags tags = Tags.of("name", executorName); + + Gauge.builder(METRIC_ACTIVE, activeTaskCount, AtomicInteger::get) + .tags(tags) + .description("Number of tasks currently being executed by pool threads") + .register(compositeMeterRegistry); + + succeededCounter = Counter.builder(METRIC_TASKS_SUCCEEDED) + .tags(tags) + .description("Total number of tasks that completed without throwing an exception") + .register(compositeMeterRegistry); + + failedCounter = Counter.builder(METRIC_TASKS_FAILED) + .tags(tags) + .description("Total number of tasks that completed by throwing an exception") + .register(compositeMeterRegistry); + + taskTimer = Timer.builder(METRIC_TASK_DURATION) + .tags(tags) + .description("Wall-clock duration of each task execution, from beforeExecute to afterExecute") + .register(compositeMeterRegistry); + } + + /** + * {@inheritDoc} + */ + @Override + public Future submit(final Runnable task) { + return delegate.submit(task); + } + + /** + * {@inheritDoc} + */ + @Override + public Future submit(final Runnable task, final T result) { + return delegate.submit(task, result); + } + + /** + * {@inheritDoc} + */ + @Override + public Future submit(final Callable task) { + return delegate.submit(task); + } + + /** + * {@inheritDoc} + */ + @Override + public T invokeAny(final Collection> tasks) throws InterruptedException, ExecutionException { + return delegate.invokeAny(tasks); + } + + /** + * {@inheritDoc} + */ + @Override + public T invokeAny(final Collection> tasks, final long timeout, final TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + return delegate.invokeAny(tasks, timeout, unit); + } + + /** + * {@inheritDoc} + */ + @Override + public List> invokeAll(final Collection> tasks) throws InterruptedException { + return delegate.invokeAll(tasks); + } + + /** + * {@inheritDoc} + */ + @Override + public List> invokeAll(final Collection> tasks, final long timeout, final TimeUnit unit) throws InterruptedException { + return delegate.invokeAll(tasks, timeout, unit); + } + + /** + * {@inheritDoc} + */ + @Override + public void shutdown() { + delegate.shutdown(); + } + + /** + * {@inheritDoc} + */ + @Override + public List shutdownNow() { + return delegate.shutdownNow(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isShutdown() { + return delegate.isShutdown(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isTerminated() { + return delegate.isTerminated(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean awaitTermination(final long timeout, final TimeUnit unit) throws InterruptedException { + return delegate.awaitTermination(timeout, unit); + } + + /** + * Delegates execution to the decorated executor, wrapping the command with + * metrics tracking for active count, success/failure counters, and duration. + * + * @param command the task to execute + */ + @Override + public void execute(final Runnable command) { + delegate.execute(() -> { + activeTaskCount.incrementAndGet(); + + final Timer.Sample sample = Timer.start(); + + try { + command.run(); + + succeededCounter.increment(); + } + catch (final Exception ex) { + failedCounter.increment(); + throw ex; + } + finally { + sample.stop(taskTimer); + activeTaskCount.decrementAndGet(); + } + }); + } + +} diff --git a/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/model/PublishRequestBuilder.java b/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/model/PublishRequestBuilder.java index 884abdb..941ddcf 100644 --- a/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/model/PublishRequestBuilder.java +++ b/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/model/PublishRequestBuilder.java @@ -25,7 +25,8 @@ import lombok.RequiredArgsConstructor; /** - * Builder for constructing publish batch requests using a supplied factory function. + * Builder for constructing publish batch requests using a supplied factory + * function. */ @Getter @NoArgsConstructor(access = AccessLevel.PRIVATE) @@ -43,7 +44,8 @@ public static Builder builder() { } /** - * Builds a publish request using the configured supplier, queue URL, and entries. + * Builds a publish request using the configured supplier, queue URL, and + * entries. * * @param the publish request type * @param the entry type @@ -91,7 +93,8 @@ public Builder entries(final List entries) { } /** - * Builds the publish request by applying the supplier to the configured URL and entries. + * Builds the publish request by applying the supplier to the configured URL and + * entries. * * @return the constructed publish request */ diff --git a/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/model/RequestEntry.java b/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/model/RequestEntry.java index a48aab3..bcb4d91 100644 --- a/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/model/RequestEntry.java +++ b/amazon-sqs-java-messaging-lib-template/src/main/java/com/amazon/sqs/messaging/lib/model/RequestEntry.java @@ -27,8 +27,8 @@ import lombok.ToString; /** - * Represents a request entry to be sent to an SQS queue, containing the payload, - * message headers, and optional FIFO attributes. + * Represents a request entry to be sent to an SQS queue, containing the + * payload, message headers, and optional FIFO attributes. * * @param the payload type */ diff --git a/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/concurrent/AmazonSqsThreadPoolExecutorTest.java b/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/concurrent/AmazonSqsThreadPoolExecutorTest.java index 7c15577..7a837ca 100644 --- a/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/concurrent/AmazonSqsThreadPoolExecutorTest.java +++ b/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/concurrent/AmazonSqsThreadPoolExecutorTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,110 +16,136 @@ package com.amazon.sqs.messaging.lib.concurrent; -import static org.assertj.core.api.Assertions.catchThrowableOfType; -import static org.awaitility.Awaitility.await; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; -import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.Test; -// @formatter:off class AmazonSqsThreadPoolExecutorTest { @Test - void testSuccessCounters() { - final AmazonSqsThreadPoolExecutor amazonSqsThreadPoolExecutor = new AmazonSqsThreadPoolExecutor(10); - - assertThat(amazonSqsThreadPoolExecutor.getActiveTaskCount(), is(0)); - assertThat(amazonSqsThreadPoolExecutor.getSucceededTaskCount(), is(0)); - assertThat(amazonSqsThreadPoolExecutor.getFailedTaskCount(), is(0)); - assertThat(amazonSqsThreadPoolExecutor.getCorePoolSize(), is(0)); + void testConstructorCreatesInstance() { + final AmazonSqsThreadPoolExecutor executor = new AmazonSqsThreadPoolExecutor(4); + assertThat(executor, is(notNullValue())); + executor.shutdownNow(); } @Test - void testSuccessSucceededTaskCount() throws InterruptedException { - final AmazonSqsThreadPoolExecutor amazonSqsThreadPoolExecutor = new AmazonSqsThreadPoolExecutor(10); - - assertThat(amazonSqsThreadPoolExecutor.getSucceededTaskCount(), is(0)); - - for(int i = 0; i < 300; i++) { - amazonSqsThreadPoolExecutor.execute(() -> { - await().pollDelay(1, TimeUnit.MILLISECONDS).until(() -> true); - }); - } - - amazonSqsThreadPoolExecutor.shutdown(); - - if (!amazonSqsThreadPoolExecutor.awaitTermination(10, TimeUnit.SECONDS)) { - amazonSqsThreadPoolExecutor.shutdownNow(); - } - - assertThat(amazonSqsThreadPoolExecutor.getActiveTaskCount(), is(0)); - assertThat(amazonSqsThreadPoolExecutor.getSucceededTaskCount(), is(300)); - assertThat(amazonSqsThreadPoolExecutor.getFailedTaskCount(), is(0)); + void testExtendsThreadPoolExecutor() { + final AmazonSqsThreadPoolExecutor executor = new AmazonSqsThreadPoolExecutor(4); + assertThat(executor, is(instanceOf(ThreadPoolExecutor.class))); + executor.shutdownNow(); } @Test - void testSuccessFailedTaskCount() throws InterruptedException { - final AmazonSqsThreadPoolExecutor amazonSqsThreadPoolExecutor = new AmazonSqsThreadPoolExecutor(10); - - assertThat(amazonSqsThreadPoolExecutor.getSucceededTaskCount(), is(0)); + void testCorePoolSizeIsZero() { + final AmazonSqsThreadPoolExecutor executor = new AmazonSqsThreadPoolExecutor(4); + assertThat(executor.getCorePoolSize(), is(equalTo(0))); + executor.shutdownNow(); + } - for(int i = 0; i < 300; i++) { - amazonSqsThreadPoolExecutor.execute(() -> { throw new RuntimeException(); }); - } + @Test + void testMaximumPoolSizeMatchesConstructorArgument() { + final AmazonSqsThreadPoolExecutor executor = new AmazonSqsThreadPoolExecutor(8); + assertThat(executor.getMaximumPoolSize(), is(equalTo(8))); + executor.shutdownNow(); + } - amazonSqsThreadPoolExecutor.shutdown(); + @Test + void testMaximumPoolSizeOfOne() { + final AmazonSqsThreadPoolExecutor executor = new AmazonSqsThreadPoolExecutor(1); + assertThat(executor.getMaximumPoolSize(), is(equalTo(1))); + executor.shutdownNow(); + } - if (!amazonSqsThreadPoolExecutor.awaitTermination(10, TimeUnit.SECONDS)) { - amazonSqsThreadPoolExecutor.shutdownNow(); - } + @Test + void testKeepAliveTimeIsSetTo60Seconds() { + final AmazonSqsThreadPoolExecutor executor = new AmazonSqsThreadPoolExecutor(4); + assertThat(executor.getKeepAliveTime(TimeUnit.SECONDS), is(equalTo(60L))); + executor.shutdownNow(); + } - assertThat(amazonSqsThreadPoolExecutor.getActiveTaskCount(), is(0)); - assertThat(amazonSqsThreadPoolExecutor.getSucceededTaskCount(), is(0)); - assertThat(amazonSqsThreadPoolExecutor.getFailedTaskCount(), is(300)); + @Test + void testQueueIsSynchronousQueue() { + final AmazonSqsThreadPoolExecutor executor = new AmazonSqsThreadPoolExecutor(4); + assertThat(executor.getQueue(), is(instanceOf(SynchronousQueue.class))); + executor.shutdownNow(); } @Test - void testSuccessActiveTaskCount() throws InterruptedException { - final AmazonSqsThreadPoolExecutor amazonSqsThreadPoolExecutor = new AmazonSqsThreadPoolExecutor(10); + void testRejectedExecutionHandlerIsBlockingSubmissionPolicy() { + final AmazonSqsThreadPoolExecutor executor = new AmazonSqsThreadPoolExecutor(4); + assertThat(executor.getRejectedExecutionHandler(), is(instanceOf(BlockingSubmissionPolicy.class))); + executor.shutdownNow(); + } - assertThat(amazonSqsThreadPoolExecutor.getSucceededTaskCount(), is(0)); + @Test + void testThreadFactoryIsNotNull() { + final AmazonSqsThreadPoolExecutor executor = new AmazonSqsThreadPoolExecutor(4); + assertThat(executor.getThreadFactory(), is(notNullValue())); + executor.shutdownNow(); + } - for(int i = 0; i < 10; i++) { - amazonSqsThreadPoolExecutor.execute(() -> { - while(true) { - await().pollDelay(1, TimeUnit.MILLISECONDS).until(() -> true); - } - }); - } + @Test + void testIsNotShutdownAfterCreation() { + final AmazonSqsThreadPoolExecutor executor = new AmazonSqsThreadPoolExecutor(4); + assertThat(executor.isShutdown(), is(false)); + executor.shutdownNow(); + } - amazonSqsThreadPoolExecutor.shutdown(); + @Test + void testIsShutdownAfterShutdownNow() { + final AmazonSqsThreadPoolExecutor executor = new AmazonSqsThreadPoolExecutor(4); + executor.shutdownNow(); + assertThat(executor.isShutdown(), is(true)); + } - if (!amazonSqsThreadPoolExecutor.awaitTermination(10, TimeUnit.SECONDS)) { - amazonSqsThreadPoolExecutor.shutdownNow(); - } + @Test + void testActiveCountIsZeroAfterCreation() { + final AmazonSqsThreadPoolExecutor executor = new AmazonSqsThreadPoolExecutor(4); + assertThat(executor.getActiveCount(), is(equalTo(0))); + executor.shutdownNow(); + } - assertThat(amazonSqsThreadPoolExecutor.getActiveTaskCount(), is(10)); - assertThat(amazonSqsThreadPoolExecutor.getSucceededTaskCount(), is(0)); - assertThat(amazonSqsThreadPoolExecutor.getFailedTaskCount(), is(0)); + @Test + void testTaskCountIsZeroAfterCreation() { + final AmazonSqsThreadPoolExecutor executor = new AmazonSqsThreadPoolExecutor(4); + assertThat(executor.getTaskCount(), is(equalTo(0L))); + executor.shutdownNow(); } @Test - void testSuccessBlockingSubmissionPolicy() { - final AmazonSqsThreadPoolExecutor amazonSqsThreadPoolExecutor = new AmazonSqsThreadPoolExecutor(1); + void testCompletedTaskCountIsZeroAfterCreation() { + final AmazonSqsThreadPoolExecutor executor = new AmazonSqsThreadPoolExecutor(4); + assertThat(executor.getCompletedTaskCount(), is(equalTo(0L))); + executor.shutdownNow(); + } - amazonSqsThreadPoolExecutor.execute(() -> { - while(true) { - await().pollDelay(1, TimeUnit.MILLISECONDS).until(() -> true); - } - }); + @Test + void testQueueIsEmptyAfterCreation() { + final AmazonSqsThreadPoolExecutor executor = new AmazonSqsThreadPoolExecutor(4); + assertThat(executor.getQueue().isEmpty(), is(true)); + executor.shutdownNow(); + } - catchThrowableOfType(() -> amazonSqsThreadPoolExecutor.execute(() -> { }), RejectedExecutionException.class); + @Test + void testPoolSizeIsZeroBeforeAnyTask() { + final AmazonSqsThreadPoolExecutor executor = new AmazonSqsThreadPoolExecutor(4); + assertThat(executor.getPoolSize(), is(equalTo(0))); + executor.shutdownNow(); } -} -// @formatter:on + @Test + void testLargestPoolSizeIsZeroBeforeAnyTask() { + final AmazonSqsThreadPoolExecutor executor = new AmazonSqsThreadPoolExecutor(4); + assertThat(executor.getLargestPoolSize(), is(equalTo(0))); + executor.shutdownNow(); + } +} \ No newline at end of file diff --git a/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/concurrent/BlockingSubmissionPolicyTest.java b/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/concurrent/BlockingSubmissionPolicyTest.java new file mode 100644 index 0000000..f21f83b --- /dev/null +++ b/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/concurrent/BlockingSubmissionPolicyTest.java @@ -0,0 +1,168 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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.amazon.sqs.messaging.lib.concurrent; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.instanceOf; +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.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mock.Strictness; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class BlockingSubmissionPolicyTest { + + @Mock(strictness = Strictness.LENIENT) + private ThreadPoolExecutor executorMock; + + @Mock(strictness = Strictness.LENIENT) + private BlockingQueue queueMock; + + private BlockingSubmissionPolicy policy; + + @BeforeEach + void setUp() { + policy = new BlockingSubmissionPolicy(30000); + when(executorMock.getQueue()).thenReturn(queueMock); + } + + @Test + void testConstructorCreatesInstance() { + assertThat(new BlockingSubmissionPolicy(1000), is(notNullValue())); + } + + @Test + void testImplementsRejectedExecutionHandler() { + assertThat(policy, is(instanceOf(RejectedExecutionHandler.class))); + } + + @Test + void testRejectedExecutionOffersRunnableToQueue() throws InterruptedException { + final Runnable task = mock(Runnable.class); + when(queueMock.offer(any(), anyLong(), any())).thenReturn(true); + + policy.rejectedExecution(task, executorMock); + + verify(queueMock).offer(eq(task), eq(30000L), eq(TimeUnit.MILLISECONDS)); + } + + @Test + void testRejectedExecutionUsesConfiguredTimeout() throws InterruptedException { + final long customTimeout = 5000L; + final BlockingSubmissionPolicy customPolicy = new BlockingSubmissionPolicy(customTimeout); + final Runnable task = mock(Runnable.class); + when(queueMock.offer(any(), anyLong(), any())).thenReturn(true); + + customPolicy.rejectedExecution(task, executorMock); + + verify(queueMock).offer(eq(task), eq(customTimeout), eq(TimeUnit.MILLISECONDS)); + } + + @Test + void testRejectedExecutionUsesMillisecondsTimeUnit() throws InterruptedException { + final Runnable task = mock(Runnable.class); + when(queueMock.offer(any(), anyLong(), any())).thenReturn(true); + + policy.rejectedExecution(task, executorMock); + + verify(queueMock).offer(any(), anyLong(), eq(TimeUnit.MILLISECONDS)); + } + + @Test + void testRejectedExecutionSucceedsWhenQueueAcceptsTask() throws InterruptedException { + final Runnable task = mock(Runnable.class); + when(queueMock.offer(any(), anyLong(), any())).thenReturn(true); + + policy.rejectedExecution(task, executorMock); + + verify(executorMock).getQueue(); + } + + @Test + void testRejectedExecutionThrowsRejectedExecutionExceptionWhenQueueReturnsFalse() throws InterruptedException { + final Runnable task = mock(Runnable.class); + when(queueMock.offer(any(), anyLong(), any())).thenReturn(false); + + assertThrows(RejectedExecutionException.class, () -> policy.rejectedExecution(task, executorMock)); + } + + @Test + void testRejectedExecutionExceptionMessageIsTimeout() throws InterruptedException { + final Runnable task = mock(Runnable.class); + when(queueMock.offer(any(), anyLong(), any())).thenReturn(false); + + final RejectedExecutionException thrown = assertThrows(RejectedExecutionException.class, () -> policy.rejectedExecution(task, executorMock)); + + assertThat(thrown.getMessage(), is("Timeout")); + } + + @Test + void testRejectedExecutionPropagatesInterruptedException() throws InterruptedException { + final Runnable task = mock(Runnable.class); + when(queueMock.offer(any(), anyLong(), any())).thenThrow(new InterruptedException()); + + assertThrows(InterruptedException.class, () -> policy.rejectedExecution(task, executorMock)); + } + + @Test + void testRejectedExecutionRetrievesQueueFromExecutor() throws InterruptedException { + final Runnable task = mock(Runnable.class); + when(queueMock.offer(any(), anyLong(), any())).thenReturn(true); + + policy.rejectedExecution(task, executorMock); + + verify(executorMock).getQueue(); + } + + @Test + void testRejectedExecutionWithZeroTimeoutThrowsWhenQueueFull() throws InterruptedException { + final BlockingSubmissionPolicy zeroTimeoutPolicy = new BlockingSubmissionPolicy(0); + final Runnable task = mock(Runnable.class); + when(queueMock.offer(any(), anyLong(), any())).thenReturn(false); + + assertThrows(RejectedExecutionException.class, () -> zeroTimeoutPolicy.rejectedExecution(task, executorMock)); + } + + @Test + void testRejectedExecutionWithZeroTimeoutSucceedsWhenQueueAccepts() throws InterruptedException { + final BlockingSubmissionPolicy zeroTimeoutPolicy = new BlockingSubmissionPolicy(0); + final Runnable task = mock(Runnable.class); + when(queueMock.offer(any(), anyLong(), any())).thenReturn(true); + + zeroTimeoutPolicy.rejectedExecution(task, executorMock); + + verify(queueMock).offer(eq(task), eq(0L), eq(TimeUnit.MILLISECONDS)); + } +} \ No newline at end of file diff --git a/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/concurrent/RingBufferBlockingQueueTest.java b/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/concurrent/RingBufferBlockingQueueTest.java index eeec6df..c45b384 100644 --- a/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/concurrent/RingBufferBlockingQueueTest.java +++ b/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/concurrent/RingBufferBlockingQueueTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 the original author or authors. + * Copyright 2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,7 +44,7 @@ class RingBufferBlockingQueueTest { void testSuccess() { final ExecutorService producer = Executors.newSingleThreadExecutor(); - final ScheduledExecutorService consumer = Executors.newSingleThreadScheduledExecutor(); + final ScheduledExecutorService consumer = Executors.newSingleThreadScheduledExecutor(ThreadFactoryProvider.getThreadFactory()); final List> requestEntriesOut = new LinkedList<>(); @@ -68,10 +68,11 @@ void testSuccess() { } }, 0, 100L, TimeUnit.MILLISECONDS); - await().atMost(1, TimeUnit.MINUTES).until(() -> ringBlockingQueue.writeSequence() == 99_999); - producer.shutdownNow(); + await().pollInterval(5, TimeUnit.SECONDS).pollDelay(200, TimeUnit.MILLISECONDS).until(() -> { + return (ringBlockingQueue.writeSequence() == 99_999) && (ringBlockingQueue.readSequence() == 100_000); + }); - await().atMost(1, TimeUnit.MINUTES).until(() -> ringBlockingQueue.readSequence() == 100_000); + producer.shutdownNow(); consumer.shutdownNow(); assertThat(ringBlockingQueue.isEmpty(), is(true)); diff --git a/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/core/AbstractAmazonSqsConsumerTest.java b/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/core/AbstractAmazonSqsConsumerTest.java index 9247a68..8f2520d 100644 --- a/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/core/AbstractAmazonSqsConsumerTest.java +++ b/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/core/AbstractAmazonSqsConsumerTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,13 +16,13 @@ package com.amazon.sqs.messaging.lib.core; -import static org.hamcrest.CoreMatchers.containsString; -import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.hasKey; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.notNullValue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doAnswer; @@ -64,7 +64,7 @@ @ExtendWith(MockitoExtension.class) class AbstractAmazonSqsConsumerTest { - private static final String QUEUE_URL = "http://localhost/000000000000/test-queue"; + private static final String QUEUE_URL = "http://localhost/000000000000/queue"; private static final long LINGER_MS = 50L; private static final int MAX_BATCH_SIZE = 10; @@ -84,7 +84,7 @@ class AbstractAmazonSqsConsumerTest { private ConcurrentMap> pendingRequests; - private BlockingQueue> queueRequests; + private BlockingQueue> topicRequests; private UnaryOperator publishDecorator; @@ -92,7 +92,7 @@ class AbstractAmazonSqsConsumerTest { void setUp() { objectMapper = new ObjectMapper(); pendingRequests = new ConcurrentHashMap<>(); - queueRequests = new RingBufferBlockingQueue<>(); + topicRequests = new RingBufferBlockingQueue<>(); publishDecorator = UnaryOperator.identity(); when(queueProperty.getQueueUrl()).thenReturn(QUEUE_URL); @@ -108,9 +108,9 @@ void testConstructorInitializesPendingRequests() { } @Test - void testConstructorInitializesqueueRequests() { - assertThat(queueRequests, is(notNullValue())); - assertThat(queueRequests.isEmpty(), is(true)); + void testConstructorInitializesTopicRequests() { + assertThat(topicRequests, is(notNullValue())); + assertThat(topicRequests.isEmpty(), is(true)); } @Test @@ -132,7 +132,7 @@ void testAwaitReturnNonNullFuture() throws Exception { } @Test - void testAwaitCompletesWhenPendingRequestsAndqueueRequestsAreEmpty() throws Exception { + void testAwaitCompletesWhenPendingRequestsAndTopicRequestsAreEmpty() throws Exception { context(consumer -> { final CompletableFuture future = consumer.await(); future.get(2, TimeUnit.SECONDS); @@ -182,13 +182,13 @@ void testShutdownCanBeCalledMultipleTimes() throws Exception { } @Test - void testRunWithFifoqueuePublishesSynchronously() throws Exception { + void testRunWithFifoTopicPublishesSynchronously() throws Exception { when(queueProperty.isFifo()).thenReturn(true); when(executorService.submit(any(Runnable.class))).thenReturn(null); context(consumer -> { final RequestEntry entry = buildRequestEntry("fifo-message"); - queueRequests.put(entry); + topicRequests.put(entry); await() .untilAsserted(() -> { @@ -199,7 +199,7 @@ void testRunWithFifoqueuePublishesSynchronously() throws Exception { } @Test - void testRunWithNonFifoqueuePublishesAsynchronously() throws Exception { + void testRunWithNonFifoTopicPublishesAsynchronously() throws Exception { when(queueProperty.isFifo()).thenReturn(false); doAnswer(inv -> { ((Runnable) inv.getArgument(0)).run(); @@ -208,7 +208,7 @@ void testRunWithNonFifoqueuePublishesAsynchronously() throws Exception { context(consumer -> { final RequestEntry entry = buildRequestEntry("non-fifo-message"); - queueRequests.put(entry); + topicRequests.put(entry); await() .untilAsserted(() -> @@ -225,7 +225,7 @@ void testRunHandlesPublishExceptionWithoutCrashing() throws Exception { consumer.setThrowOnPublish(true); final RequestEntry entry = buildRequestEntry("error-message"); - queueRequests.put(entry); + topicRequests.put(entry); await() .untilAsserted(() -> { @@ -242,7 +242,7 @@ void testRunRecordsCorrectExceptionOnPublishFailure() throws Exception { context(consumer -> { consumer.setThrowOnPublish(true); - queueRequests.put(buildRequestEntry("fail-message")); + topicRequests.put(buildRequestEntry("fail-message")); await() .untilAsserted(() -> { @@ -269,7 +269,7 @@ void testRunPublishesMultipleEntriesInSingleBatch() throws Exception { context(consumer -> { for (int i = 0; i < 5; i++) { - queueRequests.put(buildRequestEntry("message-" + i)); + topicRequests.put(buildRequestEntry("message-" + i)); } await() @@ -286,7 +286,7 @@ void testRunRespectMaxBatchSizeByPublishingInMultipleBatches() throws Exception context(consumer -> { for (int i = 0; i < 6; i++) { - queueRequests.put(buildRequestEntry("msg-" + i)); + topicRequests.put(buildRequestEntry("msg-" + i)); } await() @@ -320,26 +320,26 @@ void testPendingRequestsCanBeRemovedAfterProcessing() { } @Test - void testqueueRequestsIsEmptyOnConstruction() { - assertThat(queueRequests.isEmpty(), is(true)); + void testTopicRequestsIsEmptyOnConstruction() { + assertThat(topicRequests.isEmpty(), is(true)); } @Test - void testqueueRequestsAcceptsRequestEntries() throws InterruptedException { - queueRequests.put(buildRequestEntry("payload-1")); - queueRequests.put(buildRequestEntry("payload-2")); + void testTopicRequestsAcceptsRequestEntries() throws InterruptedException { + topicRequests.put(buildRequestEntry("payload-1")); + topicRequests.put(buildRequestEntry("payload-2")); - assertThat(queueRequests.size(), is(2)); + assertThat(topicRequests.size(), is(2)); } @Test - void testqueueRequestsPollRemovesEntry() throws InterruptedException { - queueRequests.put(buildRequestEntry("payload")); + void testTopicRequestsPollRemovesEntry() throws InterruptedException { + topicRequests.put(buildRequestEntry("payload")); - final RequestEntry polled = queueRequests.take(); + final RequestEntry polled = topicRequests.take(); assertThat(polled, is(notNullValue())); - assertThat(queueRequests.isEmpty(), is(true)); + assertThat(topicRequests.isEmpty(), is(true)); } @Test @@ -350,7 +350,7 @@ void testPublishDecoratorIsAppliedBeforePublish() throws Exception { when(queueProperty.isFifo()).thenReturn(true); context(trackingDecorator, consumer -> { - queueRequests.put(buildRequestEntry("decorated-message")); + topicRequests.put(buildRequestEntry("decorated-message")); await() .untilAsserted(() -> @@ -364,13 +364,13 @@ void testPublishDecoratorIdentityDoesNotAlterRequest() throws Exception { when(queueProperty.isFifo()).thenReturn(true); context(consumer -> { - queueRequests.put(buildRequestEntry("identity-message")); + topicRequests.put(buildRequestEntry("identity-message")); await() - .untilAsserted(() -> { - assertThat(consumer.getPublishCallCount(), greaterThanOrEqualTo(1)); - assertThat(consumer.getHandleErrorCallCount(), is(0)); - }); + .untilAsserted(() -> { + assertThat(consumer.getPublishCallCount(), greaterThanOrEqualTo(1)); + assertThat(consumer.getHandleErrorCallCount(), is(0)); + }); }); } @@ -379,7 +379,7 @@ void testCanAddPayloadAllowsEntryWellBelowSizeThreshold() throws Exception { when(queueProperty.isFifo()).thenReturn(true); context(consumer -> { - queueRequests.put(buildRequestEntry("small-payload")); + topicRequests.put(buildRequestEntry("small-payload")); await() .untilAsserted(() -> { @@ -395,7 +395,7 @@ void testCanAddPayloadAllowsEntryExactlyAtSizeThreshold() throws Exception { context(consumer -> { final String payloadAtThreshold = buildPayloadOfBytes(TestableAmazonSqsConsumer.batchSizeBytesThreshold()); - queueRequests.put(buildRequestEntry(payloadAtThreshold)); + topicRequests.put(buildRequestEntry(payloadAtThreshold)); await() .untilAsserted(() -> { @@ -411,7 +411,7 @@ void testCanAddPayloadRejectsEntryExceedingSizeThreshold() throws Exception { context(consumer -> { final String oversizedPayload = buildPayloadOfBytes(TestableAmazonSqsConsumer.batchSizeBytesThreshold() + 1); - queueRequests.put(buildRequestEntry(oversizedPayload)); + topicRequests.put(buildRequestEntry(oversizedPayload)); await() .untilAsserted(() -> { @@ -430,9 +430,9 @@ void testCanAddPayloadStopsAccumulatingWhenBatchExceedsThreshold() throws Except context(consumer -> { final int halfThreshold = TestableAmazonSqsConsumer.batchSizeBytesThreshold() / 2; - queueRequests.put(buildRequestEntry(buildPayloadOfBytes(halfThreshold))); - queueRequests.put(buildRequestEntry(buildPayloadOfBytes(halfThreshold))); - queueRequests.put(buildRequestEntry("small-overflow")); + topicRequests.put(buildRequestEntry(buildPayloadOfBytes(halfThreshold))); + topicRequests.put(buildRequestEntry(buildPayloadOfBytes(halfThreshold))); + topicRequests.put(buildRequestEntry("small-overflow")); await() .untilAsserted(() -> { @@ -449,8 +449,8 @@ void testCanAddPayloadPublishesFirstEntryAloneWhenItFillsThreshold() throws Exce context(consumer -> { final int fullThreshold = TestableAmazonSqsConsumer.batchSizeBytesThreshold(); - queueRequests.put(buildRequestEntry(buildPayloadOfBytes(fullThreshold))); - queueRequests.put(buildRequestEntry("second-entry")); + topicRequests.put(buildRequestEntry(buildPayloadOfBytes(fullThreshold))); + topicRequests.put(buildRequestEntry("second-entry")); await() .untilAsserted(() -> { @@ -467,7 +467,7 @@ void testCanAddPayloadAllowsMultipleSmallEntriesUpToThreshold() throws Exception context(consumer -> { for (int i = 0; i < 10; i++) { - queueRequests.put(buildRequestEntry("entry-" + i)); + topicRequests.put(buildRequestEntry("entry-" + i)); } await() @@ -485,9 +485,9 @@ void testCanAddPayloadSplitsBatchWhenCumulativeSizeExceedsThreshold() throws Exc context(consumer -> { final int chunkSize = (TestableAmazonSqsConsumer.batchSizeBytesThreshold() / 3) + 1; - queueRequests.put(buildRequestEntry(buildPayloadOfBytes(chunkSize))); - queueRequests.put(buildRequestEntry(buildPayloadOfBytes(chunkSize))); - queueRequests.put(buildRequestEntry(buildPayloadOfBytes(chunkSize))); + topicRequests.put(buildRequestEntry(buildPayloadOfBytes(chunkSize))); + topicRequests.put(buildRequestEntry(buildPayloadOfBytes(chunkSize))); + topicRequests.put(buildRequestEntry(buildPayloadOfBytes(chunkSize))); await() .untilAsserted(() -> { @@ -503,7 +503,7 @@ void testCanAddPayloadDoesNotPublishEmptyBatchWhenAllEntriesExceedThreshold() th context(consumer -> { final String oversizedPayload = buildPayloadOfBytes(TestableAmazonSqsConsumer.batchSizeBytesThreshold() + 100); - queueRequests.put(buildRequestEntry(oversizedPayload)); + topicRequests.put(buildRequestEntry(oversizedPayload)); await() .untilAsserted(() -> { @@ -522,13 +522,13 @@ private String buildPayloadOfBytes(final int targetBytes) { } private void context(final TryConsumer consumer) throws Exception { - try (final TestableAmazonSqsConsumer snsConsumer = new TestableAmazonSqsConsumer(amazonSnsClient, queueProperty, objectMapper, pendingRequests, queueRequests, executorService, publishDecorator)) { + try (final TestableAmazonSqsConsumer snsConsumer = new TestableAmazonSqsConsumer(amazonSnsClient, queueProperty, objectMapper, pendingRequests, topicRequests, executorService, publishDecorator)) { consumer.accept(snsConsumer); } } private void context(final UnaryOperator trackingDecorator, final TryConsumer consumer) throws Exception { - try (final TestableAmazonSqsConsumer snsConsumer = new TestableAmazonSqsConsumer(amazonSnsClient, queueProperty, objectMapper, pendingRequests, queueRequests, executorService, trackingDecorator)) { + try (final TestableAmazonSqsConsumer snsConsumer = new TestableAmazonSqsConsumer(amazonSnsClient, queueProperty, objectMapper, pendingRequests, topicRequests, executorService, trackingDecorator)) { consumer.accept(snsConsumer); } } @@ -550,14 +550,14 @@ static class TestableAmazonSqsConsumer extends AbstractAmazonSqsConsumer> pendingRequests, - final BlockingQueue> queueRequests, + final BlockingQueue> topicRequests, final ExecutorService executorService, final UnaryOperator publishDecorator) { - super(amazonSnsClient, queueProperty, objectMapper, pendingRequests, queueRequests, executorService, publishDecorator); + super(amazonSnsClient, queueProperty, objectMapper, pendingRequests, topicRequests, executorService, publishDecorator); } @Override - protected Object publish(final Object publishBatchRequest) { + public Object publish(final Object publishBatchRequest) { publishCallCount.incrementAndGet(); if (throwOnPublish) { throw publishException; @@ -566,19 +566,19 @@ protected Object publish(final Object publishBatchRequest) { } @Override - protected void handleError(final Object publishBatchRequest, final Throwable throwable) { + public void handleError(final Object publishBatchRequest, final Throwable throwable) { handleErrorCallCount.incrementAndGet(); lastError = throwable; } @Override - protected void handleResponse(final Object publishBatchResult) { + public void handleResponse(final Object publishBatchResult) { handleResponseCallCount.incrementAndGet(); } @Override protected BiFunction, Object> supplierPublishRequest() { - return (queueUrl, entries) -> { + return (topicArn, entries) -> { publishedBatchSizes.add(entries.size()); return new Object(); }; diff --git a/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/core/AbstractAmazonSqsProducerTest.java b/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/core/AbstractAmazonSqsProducerTest.java index bb9d5a5..4f1de36 100644 --- a/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/core/AbstractAmazonSqsProducerTest.java +++ b/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/core/AbstractAmazonSqsProducerTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,31 +16,24 @@ package com.amazon.sqs.messaging.lib.core; +import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.instanceOf; -import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; +import java.util.Objects; import java.util.UUID; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.ExecutorService; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +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; @@ -56,10 +49,7 @@ class AbstractAmazonSqsProducerTest { @Mock - private BlockingQueue> queueRequests; - - @Mock - private ExecutorService executorService; + private BlockingQueue> topicRequests; private ConcurrentMap> pendingRequests; @@ -68,7 +58,37 @@ class AbstractAmazonSqsProducerTest { @BeforeEach void setUp() { pendingRequests = new ConcurrentHashMap<>(); - producer = new AbstractAmazonSqsProducer(pendingRequests, queueRequests, executorService) { }; + producer = new AbstractAmazonSqsProducer(pendingRequests, topicRequests) { }; + } + + @AfterEach + void tearDown() { + if (Objects.nonNull(producer)) { + producer.shutdown(); + } + } + + @Test + void testSendReturnsShutdownState() throws InterruptedException { + final CountDownLatch countDownLatch = new CountDownLatch(1); + + final RequestEntry entry = requestEntry(); + + producer.shutdown(); + + final ListenableFuture future = producer.send(entry); + + assertThat(future, is(notNullValue())); + + future.addCallback(null, fail -> { + assertThat(fail.getId(), is(entry.getId())); + assertThat(fail.getCode(), is("000")); + assertThat(fail.getMessage(), is("Producer is currently in SHUTDOWN mode; no further messages will be accepted.")); + assertThat(fail.getSenderFault(), is(true)); + countDownLatch.countDown(); + }); + + countDownLatch.await(1, TimeUnit.MINUTES); } @Test @@ -99,12 +119,12 @@ void testSendRegistersPendingRequest() { } @Test - void testSendEnqueuesEntryInqueueRequests() throws InterruptedException { + void testSendEnqueuesEntryInTopicRequests() throws InterruptedException { final RequestEntry entry = requestEntry(); producer.send(entry); - verify(queueRequests).put(entry); + verify(topicRequests).put(entry); } @Test @@ -133,81 +153,25 @@ void testSendMultipleEntriesRegistersAllPendingRequests() { } @Test - void testSendMultipleEntriesEnqueuesAllInqueueRequests() throws InterruptedException { + void testSendMultipleEntriesEnqueuesAllInTopicRequests() throws InterruptedException { final RequestEntry entry1 = requestEntry(); final RequestEntry entry2 = requestEntry(); producer.send(entry1); producer.send(entry2); - verify(queueRequests).put(entry1); - verify(queueRequests).put(entry2); + verify(topicRequests).put(entry1); + verify(topicRequests).put(entry2); } @Test void testSendPropagatesInterruptedExceptionFromQueue() throws InterruptedException { final RequestEntry entry = requestEntry(); - doThrow(InterruptedException.class).when(queueRequests).put(any()); + doThrow(InterruptedException.class).when(topicRequests).put(any()); assertThrows(InterruptedException.class, () -> producer.send(entry)); } - @Test - void testShutdownInvokesExecutorServiceShutdown() throws InterruptedException { - when(executorService.awaitTermination(anyLong(), any(TimeUnit.class))).thenReturn(true); - - producer.shutdown(); - - verify(executorService).shutdown(); - } - - @Test - void testShutdownAwaitsTerminationWith60Seconds() throws InterruptedException { - when(executorService.awaitTermination(anyLong(), any(TimeUnit.class))).thenReturn(true); - - producer.shutdown(); - - verify(executorService).awaitTermination(60L, TimeUnit.SECONDS); - } - - @Test - void testShutdownDoesNotCallShutdownNowWhenTerminatesInTime() throws InterruptedException { - when(executorService.awaitTermination(anyLong(), any(TimeUnit.class))).thenReturn(true); - - producer.shutdown(); - - verify(executorService, never()).shutdownNow(); - } - - @Test - void testShutdownCallsShutdownNowWhenTerminationTimeoutExpires() throws InterruptedException { - when(executorService.awaitTermination(anyLong(), any(TimeUnit.class))).thenReturn(false); - doReturn(Collections.emptyList()).when(executorService).shutdownNow(); - - producer.shutdown(); - - verify(executorService).shutdownNow(); - } - - @Test - void testShutdownForcesShutdownWhenPendingTasksRemain() throws InterruptedException { - final List pendingTasks = Arrays.asList(mock(Runnable.class), mock(Runnable.class)); - when(executorService.awaitTermination(anyLong(), any(TimeUnit.class))).thenReturn(false); - doReturn(pendingTasks).when(executorService).shutdownNow(); - - producer.shutdown(); - - verify(executorService).shutdownNow(); - } - - @Test - void testShutdownCompletesGracefullyWhenNoTasksAreDropped() throws InterruptedException { - when(executorService.awaitTermination(anyLong(), any(TimeUnit.class))).thenReturn(false); - doReturn(Collections.emptyList()).when(executorService).shutdownNow(); - - assertDoesNotThrow(() -> producer.shutdown()); - } - private RequestEntry requestEntry() { return RequestEntry.builder().withId(UUID.randomUUID().toString()).withValue("payload-" + UUID.randomUUID()).build(); } diff --git a/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/core/AbstractAmazonSqsTemplateTest.java b/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/core/AbstractAmazonSqsTemplateTest.java index 568998b..e5306ec 100644 --- a/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/core/AbstractAmazonSqsTemplateTest.java +++ b/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/core/AbstractAmazonSqsTemplateTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,15 +16,23 @@ package com.amazon.sqs.messaging.lib.core; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.instanceOf; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.util.concurrent.BlockingQueue; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.function.UnaryOperator; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -32,119 +40,369 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import com.amazon.sqs.messaging.lib.concurrent.AmazonSqsThreadPoolExecutor; +import com.amazon.sqs.messaging.lib.metrics.BlockingQueueMetricsDecorator; +import com.amazon.sqs.messaging.lib.metrics.ExecutorServiceMetricsDecorator; import com.amazon.sqs.messaging.lib.model.QueueProperty; import com.amazon.sqs.messaging.lib.model.RequestEntry; import com.amazon.sqs.messaging.lib.model.ResponseFailEntry; import com.amazon.sqs.messaging.lib.model.ResponseSuccessEntry; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; + +// @formatter:off @ExtendWith(MockitoExtension.class) +@SuppressWarnings({ "rawtypes", "unchecked"}) class AbstractAmazonSqsTemplateTest { @Mock - private AbstractAmazonSqsProducer amazonSnsProducer; - - @Mock - private AbstractAmazonSqsConsumer amazonSnsConsumer; - - @Mock - private RequestEntry entry; + private AbstractAmazonSqsProducer producerMock; @Mock - private ListenableFuture future; + private AbstractAmazonSqsConsumer consumerMock; - private AbstractAmazonSqsTemplate template; + private AbstractAmazonSqsTemplate template; @BeforeEach void setUp() { - template = new AbstractAmazonSqsTemplate(amazonSnsProducer, amazonSnsConsumer) { - }; + template = new AbstractAmazonSqsTemplate(producerMock, consumerMock) { }; } @Test - void testSendDelegatesRequestToProducer() { - when(amazonSnsProducer.send(entry)).thenReturn(future); + void testSendDelegatesToProducer() { + final RequestEntry requestEntry = RequestEntry.builder().build(); + final ListenableFuture expectedFuture = new ListenableFutureImpl(); + when(producerMock.send(requestEntry)).thenReturn(expectedFuture); - template.send(entry); + final ListenableFuture result = template.send(requestEntry); - verify(amazonSnsProducer).send(entry); + assertThat(result, is(equalTo(expectedFuture))); + verify(producerMock).send(requestEntry); } @Test - void testSendReturnsProducerFuture() { - when(amazonSnsProducer.send(entry)).thenReturn(future); + void testShutdownDelegatesToProducer() { + template.shutdown(); + verify(producerMock).shutdown(); + } - final ListenableFuture result = template.send(entry); + @Test + void testShutdownDelegatesToConsumer() { + template.shutdown(); + verify(consumerMock).shutdown(); + } + + @Test + void testAwaitDelegatesToConsumer() { + final CompletableFuture expectedFuture = CompletableFuture.completedFuture(null); + when(consumerMock.await()).thenReturn(expectedFuture); - assertThat(result, is(future)); + final CompletableFuture result = template.await(); + + assertThat(result, is(equalTo(expectedFuture))); + verify(consumerMock).await(); } @Test - void testShutdownDelegatesShutdownToProducer() { - template.shutdown(); + void testGetExecutorServiceReturnsSingleThreadPoolForFifoTopic() { + final QueueProperty queueProperty = QueueProperty.builder() + .fifo(true) + .queueUrl("http://localhost/000000000000/queue.fifo") + .maximumPoolSize(10) + .maxBatchSize(10) + .build(); + + final ExecutorService executorService = AbstractAmazonSqsTemplate.getExecutorService(queueProperty, new SimpleMeterRegistry()); + + assertThat(executorService, is(notNullValue())); + assertThat(executorService, is(instanceOf(ExecutorServiceMetricsDecorator.class))); + executorService.shutdownNow(); + } - verify(amazonSnsProducer).shutdown(); + @Test + void testGetExecutorServiceReturnsMultiThreadPoolForNonFifoTopic() { + final QueueProperty queueProperty = QueueProperty.builder() + .fifo(false) + .queueUrl("http://localhost/000000000000/queue") + .maximumPoolSize(5) + .maxBatchSize(10) + .build(); + + final ExecutorService executorService = AbstractAmazonSqsTemplate.getExecutorService(queueProperty, new SimpleMeterRegistry()); + + assertThat(executorService, is(notNullValue())); + assertThat(executorService, is(instanceOf(ExecutorServiceMetricsDecorator.class))); + executorService.shutdownNow(); } @Test - void testShutdownDelegatesShutdownToConsumer() { - template.shutdown(); + void testGetExecutorServiceWithNullMeterRegistryDoesNotThrow() { + final QueueProperty queueProperty = QueueProperty.builder() + .fifo(false) + .queueUrl("http://localhost/000000000000/queue") + .maximumPoolSize(4) + .maxBatchSize(10) + .build(); + + final ExecutorService executorService = AbstractAmazonSqsTemplate.getExecutorService(queueProperty, null); + + assertThat(executorService, is(notNullValue())); + executorService.shutdownNow(); + } + + @Test + void testBuilderThrowsWhenAmazonSnsClientIsNull() { + final QueueProperty queueProperty = mock(QueueProperty.class); + assertThrows(NullPointerException.class, () -> new AbstractAmazonSqsTemplate.Builder<>(builder -> null, null, queueProperty)); + } - verify(amazonSnsConsumer).shutdown(); + @Test + void testBuilderThrowsWhenQueuePropertyIsNull() { + assertThrows(NullPointerException.class, () -> new AbstractAmazonSqsTemplate.Builder<>(builder -> null, new Object(), null)); } @Test - void testAwaitDelegatesAwaitToConsumer() { - final CompletableFuture expected = new CompletableFuture<>(); - when(amazonSnsConsumer.await()).thenReturn(expected); + void testBuilderThrowsWhenConstructorIsNull() { + final QueueProperty queueProperty = mock(QueueProperty.class); + assertThrows(NullPointerException.class, () -> new AbstractAmazonSqsTemplate.Builder<>(null, new Object(), queueProperty)); + } - template.await(); + @Test + void testBuilderPendingRequestsThrowsWhenNull() { + final QueueProperty queueProperty = mock(QueueProperty.class); + final AbstractAmazonSqsTemplate.Builder builder = new AbstractAmazonSqsTemplate.Builder<>(b -> null, new Object(), queueProperty); - verify(amazonSnsConsumer).await(); + assertThrows(NullPointerException.class, () -> builder.pendingRequests(null)); } @Test - void testAwaitReturnsConsumerCompletableFuture() { - final CompletableFuture expected = new CompletableFuture<>(); - when(amazonSnsConsumer.await()).thenReturn(expected); + void testBuilderTopicRequestsThrowsWhenNull() { + final QueueProperty queueProperty = mock(QueueProperty.class); + final AbstractAmazonSqsTemplate.Builder builder = new AbstractAmazonSqsTemplate.Builder<>(b -> null, new Object(), queueProperty); - final CompletableFuture result = template.await(); + assertThrows(NullPointerException.class, () -> builder.queueRequests(null)); + } + + @Test + void testBuilderObjectMapperThrowsWhenNull() { + final QueueProperty queueProperty = mock(QueueProperty.class); + final AbstractAmazonSqsTemplate.Builder builder = new AbstractAmazonSqsTemplate.Builder<>(b -> null, new Object(), queueProperty); + + assertThrows(NullPointerException.class, () -> builder.objectMapper(null)); + } + + @Test + void testBuilderPublishDecoratorThrowsWhenNull() { + final QueueProperty queueProperty = mock(QueueProperty.class); + final AbstractAmazonSqsTemplate.Builder builder = new AbstractAmazonSqsTemplate.Builder<>(b -> null, new Object(), queueProperty); + + assertThrows(NullPointerException.class, () -> builder.publishDecorator(null)); + } + + @Test + void testBuilderMeterRegistryThrowsWhenNull() { + final QueueProperty queueProperty = mock(QueueProperty.class); + final AbstractAmazonSqsTemplate.Builder builder = new AbstractAmazonSqsTemplate.Builder<>(b -> null, new Object(), queueProperty); + + assertThrows(NullPointerException.class, () -> builder.meterRegistry(null)); + } + + @Test + void testBuilderStoresAmazonSnsClient() { + final Object client = new Object(); + final QueueProperty queueProperty = mock(QueueProperty.class); + final AbstractAmazonSqsTemplate.Builder builder = new AbstractAmazonSqsTemplate.Builder<>(b -> null, client, queueProperty); + + assertThat(builder.getAmazonSqsClient(), is(equalTo(client))); + } + + @Test + void testBuilderStoresQueueProperty() { + final QueueProperty queueProperty = mock(QueueProperty.class); + final AbstractAmazonSqsTemplate.Builder builder = new AbstractAmazonSqsTemplate.Builder<>(b -> null, new Object(), queueProperty); + + assertThat(builder.getQueueProperty(), is(equalTo(queueProperty))); + } + + @Test + void testBuilderDefaultPendingRequestsIsConcurrentHashMap() { + final QueueProperty queueProperty = mock(QueueProperty.class); + final AbstractAmazonSqsTemplate.Builder builder = new AbstractAmazonSqsTemplate.Builder<>(b -> null, new Object(), queueProperty); - assertThat(result, is(expected)); + assertThat(builder.getPendingRequests(), is(instanceOf(ConcurrentHashMap.class))); } @Test - void testGetAmazonSnsThreadPoolExecutorReturnsSingleThreadForFifoqueue() { + void testBuilderDefaultObjectMapperIsNotNull() { final QueueProperty queueProperty = mock(QueueProperty.class); - when(queueProperty.isFifo()).thenReturn(true); + final AbstractAmazonSqsTemplate.Builder builder = new AbstractAmazonSqsTemplate.Builder<>(b -> null, new Object(), queueProperty); + + assertThat(builder.getObjectMapper(), is(notNullValue())); + assertThat(builder.getObjectMapper(), is(instanceOf(ObjectMapper.class))); + } - final AmazonSqsThreadPoolExecutor executor = AbstractAmazonSqsTemplate.getAmazonSqsThreadPoolExecutor(queueProperty); + @Test + void testBuilderDefaultMeterRegistryIsSimpleMeterRegistry() { + final QueueProperty queueProperty = mock(QueueProperty.class); + final AbstractAmazonSqsTemplate.Builder builder = new AbstractAmazonSqsTemplate.Builder<>(b -> null, new Object(), queueProperty); - assertThat(executor, is(notNullValue())); - assertThat(executor.getMaximumPoolSize(), is(1)); + assertThat(builder.getMeterRegistry(), is(instanceOf(SimpleMeterRegistry.class))); } @Test - void testGetAmazonSnsThreadPoolExecutorReturnsConfiguredPoolSizeForStandardqueue() { + void testBuilderPendingRequestsReturnsSelf() { final QueueProperty queueProperty = mock(QueueProperty.class); - when(queueProperty.isFifo()).thenReturn(false); - when(queueProperty.getMaximumPoolSize()).thenReturn(4); + final AbstractAmazonSqsTemplate.Builder builder = new AbstractAmazonSqsTemplate.Builder<>(b -> null, new Object(), queueProperty); + final ConcurrentMap> map = new ConcurrentHashMap<>(); - final AmazonSqsThreadPoolExecutor executor = AbstractAmazonSqsTemplate.getAmazonSqsThreadPoolExecutor(queueProperty); + final AbstractAmazonSqsTemplate.Builder result = builder.pendingRequests(map); - assertThat(executor, is(notNullValue())); - assertThat(executor.getMaximumPoolSize(), is(4)); + assertThat(result, is(equalTo(builder))); } @Test - void testGetAmazonSnsThreadPoolExecutorReturnsCorrectType() { + void testBuilderTopicRequestsReturnsSelf() { final QueueProperty queueProperty = mock(QueueProperty.class); - when(queueProperty.isFifo()).thenReturn(false); - when(queueProperty.getMaximumPoolSize()).thenReturn(2); + final AbstractAmazonSqsTemplate.Builder builder = new AbstractAmazonSqsTemplate.Builder<>(b -> null, new Object(), queueProperty); + final BlockingQueue> queue = new LinkedBlockingDeque<>(); - final AmazonSqsThreadPoolExecutor executor = AbstractAmazonSqsTemplate.getAmazonSqsThreadPoolExecutor(queueProperty); + final AbstractAmazonSqsTemplate.Builder result = builder.queueRequests(queue); - assertThat(executor, instanceOf(AmazonSqsThreadPoolExecutor.class)); + assertThat(result, is(equalTo(builder))); } -} \ No newline at end of file + @Test + void testBuilderObjectMapperReturnsSelf() { + final QueueProperty queueProperty = mock(QueueProperty.class); + final AbstractAmazonSqsTemplate.Builder builder = new AbstractAmazonSqsTemplate.Builder<>(b -> null, new Object(), queueProperty); + + final AbstractAmazonSqsTemplate.Builder result = builder.objectMapper(new ObjectMapper()); + + assertThat(result, is(equalTo(builder))); + } + + @Test + void testBuilderPublishDecoratorReturnsSelf() { + final QueueProperty queueProperty = mock(QueueProperty.class); + final AbstractAmazonSqsTemplate.Builder builder = new AbstractAmazonSqsTemplate.Builder<>(b -> null, new Object(), queueProperty); + + final AbstractAmazonSqsTemplate.Builder result = builder.publishDecorator(UnaryOperator.identity()); + + assertThat(result, is(equalTo(builder))); + } + + @Test + void testBuilderMeterRegistryReturnsSelf() { + final QueueProperty queueProperty = mock(QueueProperty.class); + final AbstractAmazonSqsTemplate.Builder builder = new AbstractAmazonSqsTemplate.Builder<>(b -> null, new Object(), queueProperty); + + final AbstractAmazonSqsTemplate.Builder result = builder.meterRegistry(new SimpleMeterRegistry()); + + assertThat(result, is(equalTo(builder))); + } + + @Test + void testBuilderBuildWrapsTopicRequestsWithMetricsDecorator() { + final QueueProperty queueProperty = QueueProperty.builder() + .fifo(false) + .queueUrl("http://localhost/000000000000/queue") + .maximumPoolSize(4) + .maxBatchSize(10) + .build(); + + final AbstractAmazonSqsTemplate.Builder builder = new AbstractAmazonSqsTemplate.Builder<>(b -> { + assertThat(b.getQueueRequests(), is(instanceOf(BlockingQueueMetricsDecorator.class))); + return null; + }, new Object(), queueProperty); + + builder.build(); + } + + @Test + void testBuilderBuildCreatesDefaultTopicRequestsWhenNotProvided() { + final QueueProperty queueProperty = QueueProperty.builder() + .fifo(false) + .queueUrl("http://localhost/000000000000/queue") + .maximumPoolSize(4) + .maxBatchSize(10) + .build(); + + final AbstractAmazonSqsTemplate.Builder builder = new AbstractAmazonSqsTemplate.Builder<>(b -> { + assertThat(b.getQueueRequests(), is(notNullValue())); + return null; + }, new Object(), queueProperty); + + builder.build(); + } + + @Test + void testBuilderBuildUsesProvidedTopicRequests() { + final QueueProperty queueProperty = QueueProperty.builder() + .fifo(false) + .queueUrl("http://localhost/000000000000/queue") + .maximumPoolSize(4) + .maxBatchSize(10) + .build(); + + final BlockingQueue> customQueue = new LinkedBlockingDeque<>(); + + final AbstractAmazonSqsTemplate.Builder builder = new AbstractAmazonSqsTemplate.Builder<>(b -> { + assertThat(b.getQueueRequests(), is(instanceOf(BlockingQueueMetricsDecorator.class))); + return null; + }, new Object(), queueProperty); + + builder.queueRequests(customQueue).build(); + } + + @Test + void testBuilderBuildInvokesConstructorFunction() { + final QueueProperty queueProperty = QueueProperty.builder() + .fifo(false) + .queueUrl("http://localhost/000000000000/queue") + .maximumPoolSize(4) + .maxBatchSize(10) + .build(); + + final AbstractAmazonSqsTemplate sentinel = new AbstractAmazonSqsTemplate(producerMock, consumerMock) { }; + + final AbstractAmazonSqsTemplate.Builder builder = new AbstractAmazonSqsTemplate.Builder<>(b -> sentinel, new Object(), queueProperty); + + final Object result = builder.build(); + + assertThat(result, is(equalTo(sentinel))); + } + + @Test + void testBuilderSetsPendingRequests() { + final QueueProperty queueProperty = mock(QueueProperty.class); + final ConcurrentMap> customMap = new ConcurrentHashMap<>(); + + final AbstractAmazonSqsTemplate.Builder builder = new AbstractAmazonSqsTemplate.Builder<>(b -> null, new Object(), queueProperty); + builder.pendingRequests(customMap); + + assertThat(builder.getPendingRequests(), is(equalTo(customMap))); + } + + @Test + void testBuilderSetsObjectMapper() { + final QueueProperty queueProperty = mock(QueueProperty.class); + final ObjectMapper customMapper = new ObjectMapper(); + + final AbstractAmazonSqsTemplate.Builder builder = new AbstractAmazonSqsTemplate.Builder<>(b -> null, new Object(), queueProperty); + builder.objectMapper(customMapper); + + assertThat(builder.getObjectMapper(), is(equalTo(customMapper))); + } + + @Test + void testBuilderSetsMeterRegistry() { + final QueueProperty queueProperty = mock(QueueProperty.class); + final MeterRegistry customRegistry = new SimpleMeterRegistry(); + + final AbstractAmazonSqsTemplate.Builder builder = new AbstractAmazonSqsTemplate.Builder<>(b -> null, new Object(), queueProperty); + builder.meterRegistry(customRegistry); + + assertThat(builder.getMeterRegistry(), is(equalTo(customRegistry))); + } +} diff --git a/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/core/AbstractMessageAttributesTest.java b/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/core/AbstractMessageAttributesTest.java index 9b221a9..e2054ea 100644 --- a/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/core/AbstractMessageAttributesTest.java +++ b/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/core/AbstractMessageAttributesTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/core/ListenableFutureImplTest.java b/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/core/ListenableFutureImplTest.java index 2738bf3..b13a6fa 100644 --- a/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/core/ListenableFutureImplTest.java +++ b/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/core/ListenableFutureImplTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 the original author or authors. + * Copyright 2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/core/ListenableFutureTest.java b/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/core/ListenableFutureTest.java index 957185d..3148cd0 100644 --- a/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/core/ListenableFutureTest.java +++ b/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/core/ListenableFutureTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 the original author or authors. + * Copyright 2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/core/RequestEntryInternalFactoryTest.java b/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/core/RequestEntryInternalFactoryTest.java new file mode 100644 index 0000000..761b8bd --- /dev/null +++ b/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/core/RequestEntryInternalFactoryTest.java @@ -0,0 +1,610 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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.amazon.sqs.messaging.lib.core; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import com.amazon.sqs.messaging.lib.core.RequestEntryInternalFactory.MessageAttributesInternal; +import com.amazon.sqs.messaging.lib.core.RequestEntryInternalFactory.RequestEntryInternal; +import com.amazon.sqs.messaging.lib.model.RequestEntry; +import com.fasterxml.jackson.databind.ObjectMapper; + +// @formatter:off +class RequestEntryInternalFactoryTest { + + private ObjectMapper objectMapper; + + private RequestEntryInternalFactory factory; + + @BeforeEach + void setUp() { + objectMapper = new ObjectMapper(); + factory = new RequestEntryInternalFactory(objectMapper); + } + + private RequestEntry buildRequestEntry(final Object payload, final Map headers) { + return RequestEntry.builder() + .withValue(payload) + .withId("test-id") + .withGroupId("group-1") + .withDeduplicationId("dedup-1") + .withMessageHeaders(headers) + .build(); + } + + private RequestEntry buildMinimalRequestEntry(final Object payload) { + return buildRequestEntry(payload, new HashMap<>()); + } + + @Nested + class CreateWithBytes { + + @Test + void testCreateWithBytesReturnsNotNull() { + final RequestEntry entry = buildMinimalRequestEntry("hello"); + final byte[] bytes = "hello".getBytes(StandardCharsets.UTF_8); + + final RequestEntryInternal result = factory.create(entry, bytes); + + assertThat(result, is(notNullValue())); + } + + @Test + void testCreateWithBytesMapsId() { + final RequestEntry entry = buildMinimalRequestEntry("hello"); + final byte[] bytes = "hello".getBytes(StandardCharsets.UTF_8); + + final RequestEntryInternal result = factory.create(entry, bytes); + + assertThat(result.getId(), equalTo("test-id")); + } + + @Test + void testCreateWithBytesMapsGroupId() { + final RequestEntry entry = buildMinimalRequestEntry("hello"); + final byte[] bytes = "hello".getBytes(StandardCharsets.UTF_8); + + final RequestEntryInternal result = factory.create(entry, bytes); + + assertThat(result.getGroupId(), equalTo("group-1")); + } + + @Test + void testCreateWithBytesMapsDeduplicationId() { + final RequestEntry entry = buildMinimalRequestEntry("hello"); + final byte[] bytes = "hello".getBytes(StandardCharsets.UTF_8); + + final RequestEntryInternal result = factory.create(entry, bytes); + + assertThat(result.getDeduplicationId(), equalTo("dedup-1")); + } + + @Test + void testCreateWithBytesMapsMessageHeaders() { + final Map headers = new HashMap<>(); + headers.put("headerKey", "headerValue"); + final RequestEntry entry = buildRequestEntry("hello", headers); + final byte[] bytes = "hello".getBytes(StandardCharsets.UTF_8); + + final RequestEntryInternal result = factory.create(entry, bytes); + + assertThat(result.getMessageHeaders(), equalTo(headers)); + } + + @Test + void testCreateWithBytesPayloadSizeMatchesByteArrayLength() { + final byte[] bytes = "hello world".getBytes(StandardCharsets.UTF_8); + final RequestEntry entry = buildMinimalRequestEntry("hello world"); + + final RequestEntryInternal result = factory.create(entry, bytes); + + assertThat(result.size(), equalTo(bytes.length)); + } + + @Test + void testCreateWithBytesPayloadDecodedCorrectly() { + final String message = "hello world"; + final byte[] bytes = message.getBytes(StandardCharsets.UTF_8); + final RequestEntry entry = buildMinimalRequestEntry(message); + + final RequestEntryInternal result = factory.create(entry, bytes); + + assertThat(result.getMessage(), equalTo(message)); + } + + @Test + void testCreateWithBytesCreateTimeIsSet() { + final RequestEntry entry = buildMinimalRequestEntry("hello"); + final byte[] bytes = "hello".getBytes(StandardCharsets.UTF_8); + + final RequestEntryInternal result = factory.create(entry, bytes); + + assertThat(result.getCreateTime(), greaterThan(0L)); + } + + @Test + void testCreateWithEmptyBytesPayloadSizeIsZero() { + final byte[] bytes = new byte[0]; + final RequestEntry entry = buildMinimalRequestEntry(""); + + final RequestEntryInternal result = factory.create(entry, bytes); + + assertThat(result.size(), equalTo(0)); + } + + @Test + void testCreateWithNullHeadersMapsNullHeaders() { + final RequestEntry entry = buildRequestEntry("hello", null); + final byte[] bytes = "hello".getBytes(StandardCharsets.UTF_8); + + final RequestEntryInternal result = factory.create(entry, bytes); + + assertThat(result.getMessageHeaders(), is(nullValue())); + } + } + + @Nested + class CreateAutoSerialize { + + @Test + void testCreateWithStringPayloadReturnsNotNull() { + final RequestEntry entry = buildMinimalRequestEntry("hello"); + + final RequestEntryInternal result = factory.create(entry); + + assertThat(result, is(notNullValue())); + } + + @Test + void testCreateWithStringPayloadDecodesCorrectly() { + final RequestEntry entry = buildMinimalRequestEntry("hello"); + + final RequestEntryInternal result = factory.create(entry); + + assertThat(result.getMessage(), equalTo("hello")); + } + + @Test + void testCreateWithStringPayloadSizeMatchesUtf8Length() { + final String message = "hello"; + final RequestEntry entry = buildMinimalRequestEntry(message); + + final RequestEntryInternal result = factory.create(entry); + + assertThat(result.size(), equalTo(message.getBytes(StandardCharsets.UTF_8).length)); + } + + @Test + void testCreateWithMultibyteStringPayloadEncodedInUtf8() { + final String message = "こんにちは"; + final RequestEntry entry = buildMinimalRequestEntry(message); + + final RequestEntryInternal result = factory.create(entry); + + assertThat(result.size(), equalTo(message.getBytes(StandardCharsets.UTF_8).length)); + } + + @Test + void testCreateWithObjectPayloadSerializesViaJackson() throws Exception { + final Map payload = new HashMap<>(); + payload.put("key", "value"); + final RequestEntry entry = buildMinimalRequestEntry(payload); + + final RequestEntryInternal result = factory.create(entry); + + final String decoded = result.getMessage(); + final Map parsed = objectMapper.readValue(decoded, Map.class); + assertThat(parsed.get("key"), equalTo("value")); + } + + @Test + void testCreateWithIntegerPayloadSerializesViaJackson() throws Exception { + final RequestEntry entry = buildMinimalRequestEntry(42); + + final RequestEntryInternal result = factory.create(entry); + + assertThat(result.getMessage(), equalTo("42")); + } + + @Test + void testCreateWithEmptyStringPayloadSizeIsZero() { + final RequestEntry entry = buildMinimalRequestEntry(""); + + final RequestEntryInternal result = factory.create(entry); + + assertThat(result.size(), equalTo(0)); + } + + @Test + void testCreateWithStringPayloadMapsId() { + final RequestEntry entry = buildMinimalRequestEntry("payload"); + + final RequestEntryInternal result = factory.create(entry); + + assertThat(result.getId(), equalTo("test-id")); + } + } + + @Nested + class ConvertPayload { + + @Test + void testConvertPayloadStringReturnsUtf8Bytes() { + final String value = "hello"; + final RequestEntry entry = buildMinimalRequestEntry(value); + + final byte[] result = factory.convertPayload(entry); + + assertThat(result, equalTo(value.getBytes(StandardCharsets.UTF_8))); + } + + @Test + void testConvertPayloadStringIsNotSerializedWithJacksonQuotes() { + final String value = "hello"; + final RequestEntry entry = buildMinimalRequestEntry(value); + + final byte[] result = factory.convertPayload(entry); + + assertThat(new String(result, StandardCharsets.UTF_8), equalTo("hello")); + } + + @Test + void testConvertPayloadObjectUsesJackson() throws Exception { + final Map payload = Collections.singletonMap("a", "b"); + final RequestEntry entry = buildMinimalRequestEntry(payload); + + final byte[] result = factory.convertPayload(entry); + + final Map parsed = objectMapper.readValue(result, Map.class); + assertThat(parsed.get("a"), equalTo("b")); + } + + @Test + void testConvertPayloadListUsesJackson() throws Exception { + final List payload = Arrays.asList("x", "y", "z"); + final RequestEntry entry = buildMinimalRequestEntry(payload); + + final byte[] result = factory.convertPayload(entry); + + final List parsed = objectMapper.readValue(result, List.class); + assertThat(parsed.size(), equalTo(3)); + } + + @Test + void testConvertPayloadMultibyteStringEncodedCorrectly() { + final String value = "日本語"; + final RequestEntry entry = buildMinimalRequestEntry(value); + + final byte[] result = factory.convertPayload(entry); + + assertThat(result, equalTo(value.getBytes(StandardCharsets.UTF_8))); + } + + @Test + void testConvertPayloadEmptyStringReturnsEmptyArray() { + final RequestEntry entry = buildMinimalRequestEntry(""); + + final byte[] result = factory.convertPayload(entry); + + assertThat(result.length, equalTo(0)); + } + } + + @Nested + class MessageAttributesSize { + + @Test + void testMessageAttributesSizeWithEmptyHeadersReturnsZero() { + final RequestEntry entry = buildRequestEntry("hello", new HashMap<>()); + + final Integer result = factory.messageAttributesSize(entry); + + assertThat(result, equalTo(0)); + } + + @Test + void testMessageAttributesSizeWithStringAttributeCountsKeyAndValue() { + final Map headers = new HashMap<>(); + headers.put("key", "value"); + + final RequestEntry entry = buildRequestEntry("hello", headers); + + final Integer result = factory.messageAttributesSize(entry); + + assertThat(result, equalTo(8)); + } + + @Test + void testMessageAttributesSizeWithMultipleAttributesSumsAll() { + final Map headers = new HashMap<>(); + headers.put("k1", "val"); + headers.put("k2", "valu"); + + final RequestEntry entry = buildRequestEntry("hello", headers); + + final Integer result = factory.messageAttributesSize(entry); + + assertThat(result, equalTo(11)); + } + + @Test + void testMessageAttributesSizeWithNumberAttribute() { + final Map headers = new HashMap<>(); + headers.put("num", 12345); + + final RequestEntry entry = buildRequestEntry("hello", headers); + + final Integer result = factory.messageAttributesSize(entry); + + assertThat(result, greaterThan(0)); + } + + @Test + void testMessageAttributesSizeWithEnumAttribute() { + final Map headers = new HashMap<>(); + headers.put("e", SampleEnum.VALUE_ONE); + + final RequestEntry entry = buildRequestEntry("hello", headers); + + final Integer result = factory.messageAttributesSize(entry); + + assertThat(result, equalTo(10)); + } + + @Test + void testMessageAttributesSizeWithBinaryAttribute() { + final byte[] bytes = new byte[] { 1, 2, 3, 4 }; + final Map headers = new HashMap<>(); + headers.put("bin", ByteBuffer.wrap(bytes)); + + final RequestEntry entry = buildRequestEntry("hello", headers); + + final Integer result = factory.messageAttributesSize(entry); + + assertThat(result, equalTo(7)); + } + + @Test + void testMessageAttributesSizeWithStringListAttribute() { + final Map headers = new HashMap<>(); + headers.put("arr", Arrays.asList("a", "b", "c")); + + final RequestEntry entry = buildRequestEntry("hello", headers); + + final Integer result = factory.messageAttributesSize(entry); + + assertThat(result, greaterThan(0)); + } + + @Test + void testMessageAttributesSizeIsNonNegative() { + final Map headers = new HashMap<>(); + headers.put("x", "y"); + + final RequestEntry entry = buildRequestEntry("hello", headers); + + final Integer result = factory.messageAttributesSize(entry); + + assertThat(result, greaterThanOrEqualTo(0)); + } + } + + @Nested + class RequestEntryInternalTest { + + @Test + void testSizeReturnsBufferCapacity() { + final byte[] bytes = "test payload".getBytes(StandardCharsets.UTF_8); + final RequestEntryInternal internal = RequestEntryInternal.builder() + .withId("id") + .withValue(ByteBuffer.wrap(bytes)) + .withCreateTime(System.nanoTime()) + .build(); + + assertThat(internal.size(), equalTo(bytes.length)); + } + + @Test + void testGetMessageDecodesUtf8Correctly() { + final String message = "decoded message"; + final RequestEntryInternal internal = RequestEntryInternal.builder() + .withId("id") + .withValue(ByteBuffer.wrap(message.getBytes(StandardCharsets.UTF_8))) + .withCreateTime(System.nanoTime()) + .build(); + + assertThat(internal.getMessage(), equalTo(message)); + } + + @Test + void testGetMessageDecodesMultibyteUtf8Correctly() { + final String message = "日本語テスト"; + final RequestEntryInternal internal = RequestEntryInternal.builder() + .withId("id") + .withValue(ByteBuffer.wrap(message.getBytes(StandardCharsets.UTF_8))) + .withCreateTime(System.nanoTime()) + .build(); + + assertThat(internal.getMessage(), equalTo(message)); + } + + @Test + void testBuilderSetsAllFieldsCorrectly() { + final Map headers = Collections.singletonMap("h", "v"); + final long now = System.nanoTime(); + final RequestEntryInternal internal = RequestEntryInternal.builder() + .withId("my-id") + .withGroupId("my-group") + .withDeduplicationId("my-dedup") + .withMessageHeaders(headers) + .withValue(ByteBuffer.wrap("data".getBytes(StandardCharsets.UTF_8))) + .withCreateTime(now) + .build(); + + assertThat(internal.getId(), equalTo("my-id")); + assertThat(internal.getGroupId(), equalTo("my-group")); + assertThat(internal.getDeduplicationId(), equalTo("my-dedup")); + assertThat(internal.getMessageHeaders(), equalTo(headers)); + assertThat(internal.getCreateTime(), equalTo(now)); + } + + @Test + void testSizeWithEmptyBufferIsZero() { + final RequestEntryInternal internal = RequestEntryInternal.builder() + .withId("id") + .withValue(ByteBuffer.wrap(new byte[0])) + .withCreateTime(System.nanoTime()) + .build(); + + assertThat(internal.size(), equalTo(0)); + } + + @Test + void testToStringIsNotNull() { + final RequestEntryInternal internal = RequestEntryInternal.builder() + .withId("id") + .withValue(ByteBuffer.wrap("x".getBytes(StandardCharsets.UTF_8))) + .withCreateTime(1L) + .build(); + + assertThat(internal.toString(), is(notNullValue())); + } + } + + @Nested + class MessageAttributesInternalTest { + + private final MessageAttributesInternal instance = MessageAttributesInternal.INSTANCE; + + @Test + void testSingletonInstanceIsNotNull() { + assertThat(instance, is(notNullValue())); + } + + @Test + void testSingletonInstanceIsSameObject() { + assertThat(MessageAttributesInternal.INSTANCE, is(instance)); + } + + @Test + void testGetEnumMessageAttributeReturnsNameLength() { + final Integer result = instance.getEnumMessageAttribute(SampleEnum.VALUE_ONE); + + assertThat(result, equalTo("VALUE_ONE".length())); + } + + @Test + void testGetEnumMessageAttributeShortName() { + final Integer result = instance.getEnumMessageAttribute(SampleEnum.A); + + assertThat(result, equalTo(1)); + } + + @Test + void testGetStringMessageAttributeReturnsLength() { + final Integer result = instance.getStringMessageAttribute("hello"); + + assertThat(result, equalTo(5)); + } + + @Test + void testGetStringMessageAttributeEmptyString() { + final Integer result = instance.getStringMessageAttribute(""); + + assertThat(result, equalTo(0)); + } + + @Test + void testGetNumberMessageAttributeInteger() { + final Integer result = instance.getNumberMessageAttribute(12345); + + assertThat(result, equalTo(5)); + } + + @Test + void testGetNumberMessageAttributeFloat() { + final Integer result = instance.getNumberMessageAttribute(3.14f); + + assertThat(result, equalTo(String.valueOf(3.14f).length())); + } + + @Test + void testGetNumberMessageAttributeNegativeNumber() { + final Integer result = instance.getNumberMessageAttribute(-99); + + assertThat(result, equalTo(3)); + } + + @Test + void testGetBinaryMessageAttributeReturnsRemaining() { + final byte[] bytes = new byte[] { 10, 20, 30 }; + final ByteBuffer buffer = ByteBuffer.wrap(bytes); + + final Integer result = instance.getBinaryMessageAttribute(buffer); + + assertThat(result, equalTo(3)); + } + + @Test + void testGetBinaryMessageAttributeEmptyBuffer() { + final ByteBuffer buffer = ByteBuffer.wrap(new byte[0]); + + final Integer result = instance.getBinaryMessageAttribute(buffer); + + assertThat(result, equalTo(0)); + } + + @Test + void testGetStringArrayMessageAttributeReturnsCombinedLength() { + final List values = Arrays.asList("a", "b", "c"); + + final Integer result = instance.getStringArrayMessageAttribute(values); + + assertThat(result, greaterThan(0)); + } + + @Test + void testGetStringArrayMessageAttributeEmptyList() { + final Integer result = instance.getStringArrayMessageAttribute(Collections.emptyList()); + + assertThat(result, greaterThanOrEqualTo(0)); + } + } + + private enum SampleEnum { + A, VALUE_ONE + } + +} \ No newline at end of file diff --git a/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/helpers/TryConsumer.java b/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/helpers/TryConsumer.java index 0a53ae8..e7b0317 100644 --- a/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/helpers/TryConsumer.java +++ b/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/helpers/TryConsumer.java @@ -1,3 +1,19 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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.amazon.sqs.messaging.lib.helpers; @FunctionalInterface diff --git a/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/metrics/AbstractAmazonSqsConsumerMetricsDecoratorTest.java b/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/metrics/AbstractAmazonSqsConsumerMetricsDecoratorTest.java new file mode 100644 index 0000000..04c4299 --- /dev/null +++ b/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/metrics/AbstractAmazonSqsConsumerMetricsDecoratorTest.java @@ -0,0 +1,441 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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.amazon.sqs.messaging.lib.metrics; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.sameInstance; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.concurrent.CompletableFuture; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.amazon.sqs.messaging.lib.core.AmazonSqsConsumer; +import com.amazon.sqs.messaging.lib.model.QueueProperty; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; + +@ExtendWith(MockitoExtension.class) +class AbstractAmazonSqsConsumerMetricsDecoratorTest { + + private static class TestableDecorator extends AbstractAmazonSqsConsumerMetricsDecorator { + + TestableDecorator(final AmazonSqsConsumer delegate, final QueueProperty queueProperty, final MeterRegistry meterRegistry) { + super(delegate, queueProperty, meterRegistry); + } + + @Override + public void handleError(final Object publishBatchRequest, final Throwable throwable) { + // no-op for testing + } + + @Override + public void handleResponse(final Object publishBatchResult) { + // no-op for testing + } + + @Override + public Object publish(final Object publishBatchRequest) { + return null; + } + + } + + private static final String QUEUE_URL = "http://localhost/000000000000/queue"; + + private TestableDecorator decorator; + + @Mock + private AmazonSqsConsumer delegate; + + @Mock + private QueueProperty queueProperty; + + @Spy + private SimpleMeterRegistry meterRegistry; + + @BeforeEach + void setup() { + when(queueProperty.getQueueUrl()).thenReturn(QUEUE_URL); + + decorator = new TestableDecorator(delegate, queueProperty, meterRegistry); + } + + @Nested + class MetricNameConstants { + + @Test + void testMetricPublishAttemptsHasSnsPrefix() { + assertThat(AbstractAmazonSqsConsumerMetricsDecorator.METRIC_PUBLISH_ATTEMPTS, containsString("sqs")); + } + + @Test + void testMetricPublishAttemptsValue() { + assertThat(AbstractAmazonSqsConsumerMetricsDecorator.METRIC_PUBLISH_ATTEMPTS, equalTo("sqs.publish.attempts")); + } + + @Test + void testMetricPublishSuccessValue() { + assertThat(AbstractAmazonSqsConsumerMetricsDecorator.METRIC_PUBLISH_SUCCESS, equalTo("sqs.publish.success")); + } + + @Test + void testMetricPublishFailureValue() { + assertThat(AbstractAmazonSqsConsumerMetricsDecorator.METRIC_PUBLISH_FAILURE, equalTo("sqs.publish.failure")); + } + + @Test + void testMetricPublishDurationValue() { + assertThat(AbstractAmazonSqsConsumerMetricsDecorator.METRIC_PUBLISH_DURATION, equalTo("sqs.publish.duration")); + } + + @Test + void testMetricPublishBatchSizeValue() { + assertThat(AbstractAmazonSqsConsumerMetricsDecorator.METRIC_PUBLISH_BATCH_SIZE, equalTo("sqs.publish.batch.size")); + } + + @Test + void testMetricPublishInflightValue() { + assertThat(AbstractAmazonSqsConsumerMetricsDecorator.METRIC_PUBLISH_INFLIGHT, equalTo("sqs.publish.inflight")); + } + + @Test + void testTagErrorCodeValue() { + assertThat(AbstractAmazonSqsConsumerMetricsDecorator.TAG_ERROR_CODE, equalTo("error_code")); + } + + @Test + void testTagErrorTypeValue() { + assertThat(AbstractAmazonSqsConsumerMetricsDecorator.TAG_ERROR_TYPE, equalTo("error_type")); + } + + @Test + void testErrorTypeAmazonValue() { + assertThat(AbstractAmazonSqsConsumerMetricsDecorator.ERROR_TYPE_AMAZON, equalTo("amazon_service_exception")); + } + + @Test + void testErrorTypeOtherValue() { + assertThat(AbstractAmazonSqsConsumerMetricsDecorator.ERROR_TYPE_OTHER, equalTo("unknown")); + } + } + + @Nested + class ConstructorInitialization { + + @Test + void testDelegateIsSet() { + assertThat(decorator.delegate, is(sameInstance(delegate))); + } + + @Test + void testRegistryIsNotNull() { + assertThat(decorator.registry, is(notNullValue())); + } + + @Test + void testTagsAreNotNull() { + assertThat(decorator.tags, is(notNullValue())); + } + + @Test + void testTagsContainTopicArn() { + assertThat(decorator.tags.stream().anyMatch(t -> "queue".equals(t.getKey()) && QUEUE_URL.equals(t.getValue())), is(true)); + } + + @Test + void testPublishAttemptsCounterIsNotNull() { + assertThat(decorator.publishAttemptsCounter, is(notNullValue())); + } + + @Test + void testSuccessCounterIsNotNull() { + assertThat(decorator.successCounter, is(notNullValue())); + } + + @Test + void testPublishTimerIsNotNull() { + assertThat(decorator.publishTimer, is(notNullValue())); + } + + @Test + void testBatchSizeSummaryIsNotNull() { + assertThat(decorator.batchSizeSummary, is(notNullValue())); + } + + @Test + void testInflightGaugeIsNotNull() { + assertThat(decorator.inflightGauge, is(notNullValue())); + } + + @Test + void testInflightGaugeInitialValueIsZero() { + assertThat(decorator.inflightGauge.get(), equalTo(0)); + } + + @Test + void testConstructorWithNullMeterRegistryDoesNotThrow() { + final TestableDecorator nullRegistryDecorator = new TestableDecorator(delegate, queueProperty, null); + + assertThat(nullRegistryDecorator, is(notNullValue())); + } + + @Test + void testConstructorWithNullMeterRegistryInitializesCounters() { + final TestableDecorator nullRegistryDecorator = new TestableDecorator(delegate, queueProperty, null); + + assertThat(nullRegistryDecorator.publishAttemptsCounter, is(notNullValue())); + assertThat(nullRegistryDecorator.successCounter, is(notNullValue())); + } + } + + @Nested + class MetersRegisteredInRegistry { + + @Test + void testPublishAttemptsCounterRegisteredInRegistry() { + assertThat(meterRegistry.find(AbstractAmazonSqsConsumerMetricsDecorator.METRIC_PUBLISH_ATTEMPTS).counter(), is(notNullValue())); + } + + @Test + void testPublishSuccessCounterRegisteredInRegistry() { + assertThat(meterRegistry.find(AbstractAmazonSqsConsumerMetricsDecorator.METRIC_PUBLISH_SUCCESS).counter(), is(notNullValue())); + } + + @Test + void testPublishTimerRegisteredInRegistry() { + assertThat(meterRegistry.find(AbstractAmazonSqsConsumerMetricsDecorator.METRIC_PUBLISH_DURATION).timer(), is(notNullValue())); + } + + @Test + void testBatchSizeSummaryRegisteredInRegistry() { + assertThat(meterRegistry.find(AbstractAmazonSqsConsumerMetricsDecorator.METRIC_PUBLISH_BATCH_SIZE).summary(), is(notNullValue())); + } + + @Test + void testInflightGaugeRegisteredInRegistry() { + assertThat(meterRegistry.find(AbstractAmazonSqsConsumerMetricsDecorator.METRIC_PUBLISH_INFLIGHT).gauge(), is(notNullValue())); + } + + @Test + void testPublishAttemptsCounterInitialValueIsZero() { + assertThat(decorator.publishAttemptsCounter.count(), equalTo(0.0)); + } + + @Test + void testSuccessCounterInitialValueIsZero() { + assertThat(decorator.successCounter.count(), equalTo(0.0)); + } + + @Test + void testPublishAttemptsCounterIncrementsCorrectly() { + decorator.publishAttemptsCounter.increment(); + + assertThat(decorator.publishAttemptsCounter.count(), equalTo(1.0)); + } + + @Test + void testSuccessCounterIncrementsCorrectly() { + decorator.successCounter.increment(3); + + assertThat(decorator.successCounter.count(), equalTo(3.0)); + } + } + + @Nested + class FailureCounter { + + @Test + void testFailureCounterReturnsNotNull() { + final Counter counter = decorator.failureCounter("400", "amazon_service_exception"); + + assertThat(counter, is(notNullValue())); + } + + @Test + void testFailureCounterInitialValueIsZero() { + final Counter counter = decorator.failureCounter("500", "unknown"); + + assertThat(counter.count(), equalTo(0.0)); + } + + @Test + void testFailureCounterIncrementsCorrectly() { + final Counter counter = decorator.failureCounter("400", "amazon_service_exception"); + counter.increment(); + + assertThat(counter.count(), equalTo(1.0)); + } + + @Test + void testFailureCounterRegisteredInRegistry() { + decorator.failureCounter("InvalidParameter", "amazon_service_exception"); + + assertThat(meterRegistry.find(AbstractAmazonSqsConsumerMetricsDecorator.METRIC_PUBLISH_FAILURE).counter(), is(notNullValue())); + } + + @Test + void testFailureCounterWithSameTagsReturnsSameMeter() { + final Counter first = decorator.failureCounter("400", "amazon_service_exception"); + final Counter second = decorator.failureCounter("400", "amazon_service_exception"); + + assertThat(first, is(sameInstance(second))); + } + + @Test + void testFailureCounterWithDifferentErrorCodesAreDistinct() { + final Counter counter400 = decorator.failureCounter("400", "amazon_service_exception"); + final Counter counter500 = decorator.failureCounter("500", "amazon_service_exception"); + + assertThat(counter400, is(not(sameInstance(counter500)))); + } + + @Test + void testFailureCounterWithDifferentErrorTypesAreDistinct() { + final Counter amazon = decorator.failureCounter("400", "amazon_service_exception"); + final Counter unknown = decorator.failureCounter("400", "unknown"); + + assertThat(amazon, is(not(sameInstance(unknown)))); + } + + @Test + void testFailureCounterTagsIncludeTopicArn() { + decorator.failureCounter("400", "amazon_service_exception"); + + final Counter found = meterRegistry.find(AbstractAmazonSqsConsumerMetricsDecorator.METRIC_PUBLISH_FAILURE).tag("queue", QUEUE_URL).counter(); + + assertThat(found, is(notNullValue())); + } + + @Test + void testFailureCounterTagsIncludeErrorCode() { + decorator.failureCounter("InvalidParam", "amazon_service_exception"); + + final Counter found = meterRegistry.find(AbstractAmazonSqsConsumerMetricsDecorator.METRIC_PUBLISH_FAILURE).tag(AbstractAmazonSqsConsumerMetricsDecorator.TAG_ERROR_CODE, "InvalidParam").counter(); + + assertThat(found, is(notNullValue())); + } + + @Test + void testFailureCounterTagsIncludeErrorType() { + decorator.failureCounter("400", "unknown"); + + final Counter found = meterRegistry.find(AbstractAmazonSqsConsumerMetricsDecorator.METRIC_PUBLISH_FAILURE).tag(AbstractAmazonSqsConsumerMetricsDecorator.TAG_ERROR_TYPE, "unknown").counter(); + + assertThat(found, is(notNullValue())); + } + } + + @Nested + class InflightGauge { + + @Test + void testInflightGaugeReflectsIncrementInRegistry() { + decorator.inflightGauge.incrementAndGet(); + + final double gaugeValue = meterRegistry.find(AbstractAmazonSqsConsumerMetricsDecorator.METRIC_PUBLISH_INFLIGHT).gauge().value(); + + assertThat(gaugeValue, equalTo(1.0)); + } + + @Test + void testInflightGaugeReflectsDecrementInRegistry() { + decorator.inflightGauge.set(3); + decorator.inflightGauge.decrementAndGet(); + + final double gaugeValue = meterRegistry.find(AbstractAmazonSqsConsumerMetricsDecorator.METRIC_PUBLISH_INFLIGHT).gauge().value(); + + assertThat(gaugeValue, equalTo(2.0)); + } + + @Test + void testInflightGaugeReflectsZeroAfterReset() { + decorator.inflightGauge.set(5); + decorator.inflightGauge.set(0); + + final double gaugeValue = meterRegistry.find(AbstractAmazonSqsConsumerMetricsDecorator.METRIC_PUBLISH_INFLIGHT).gauge().value(); + + assertThat(gaugeValue, equalTo(0.0)); + } + } + + @Nested + class Shutdown { + + @Test + void testShutdownDelegatesToDelegate() { + decorator.shutdown(); + + verify(delegate).shutdown(); + } + + @Test + void testShutdownCanBeCalledMultipleTimes() { + decorator.shutdown(); + decorator.shutdown(); + + verify(delegate, org.mockito.Mockito.times(2)).shutdown(); + } + } + + @Nested + class Await { + + @Test + void testAwaitDelegatesToDelegate() { + final CompletableFuture future = CompletableFuture.completedFuture(null); + when(delegate.await()).thenReturn(future); + + final CompletableFuture result = decorator.await(); + + assertThat(result, is(sameInstance(future))); + verify(delegate).await(); + } + + @Test + void testAwaitReturnsNotNull() { + when(delegate.await()).thenReturn(CompletableFuture.completedFuture(null)); + + assertThat(decorator.await(), is(notNullValue())); + } + + @Test + void testAwaitPropagatesDelegateResult() { + final CompletableFuture expected = new CompletableFuture<>(); + when(delegate.await()).thenReturn(expected); + + final CompletableFuture result = decorator.await(); + + assertThat(result, is(sameInstance(expected))); + } + } + +} \ No newline at end of file diff --git a/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/metrics/BlockingQueueMetricsDecoratorTest.java b/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/metrics/BlockingQueueMetricsDecoratorTest.java new file mode 100644 index 0000000..3918f13 --- /dev/null +++ b/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/metrics/BlockingQueueMetricsDecoratorTest.java @@ -0,0 +1,521 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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.amazon.sqs.messaging.lib.metrics; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +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.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; + +@ExtendWith(MockitoExtension.class) +class BlockingQueueMetricsDecoratorTest { + + @Mock + private BlockingQueue delegateMock; + + private MeterRegistry meterRegistry; + + private BlockingQueueMetricsDecorator decorator; + + @BeforeEach + void setUp() { + meterRegistry = new SimpleMeterRegistry(); + decorator = new BlockingQueueMetricsDecorator<>(delegateMock, meterRegistry, "test-queue"); + } + + @Test + void testConstructorWithNullRegistryDoesNotThrow() { + final BlockingQueueMetricsDecorator instance = new BlockingQueueMetricsDecorator<>(delegateMock, null, "test-queue"); + assertThat(instance, is(notNullValue())); + } + + @Test + void testConstructorWithValidRegistryCreatesInstance() { + assertThat(decorator, is(notNullValue())); + } + + @Test + void testPutDelegatesToDelegate() throws InterruptedException { + decorator.put("element"); + verify(delegateMock).put("element"); + } + + @Test + void testPutIncrementsPutsTotalOnSuccess() throws InterruptedException { + decorator.put("element"); + final double count = meterRegistry.counter("blocking.queue.puts.total", "name", "test-queue").count(); + assertThat(count, is(equalTo(1.0))); + } + + @Test + void testPutDoesNotIncrementPutsFailedOnSuccess() throws InterruptedException { + decorator.put("element"); + final double count = meterRegistry.counter("blocking.queue.puts.failed", "name", "test-queue").count(); + assertThat(count, is(equalTo(0.0))); + } + + @Test + void testPutIncrementsPutsFailedOnException() throws InterruptedException { + doThrow(new InterruptedException()).when(delegateMock).put(any()); + assertThrows(InterruptedException.class, () -> decorator.put("element")); + final double count = meterRegistry.counter("blocking.queue.puts.failed", "name", "test-queue").count(); + assertThat(count, is(equalTo(1.0))); + } + + @Test + void testPutDoesNotIncrementPutsTotalOnException() throws InterruptedException { + doThrow(new InterruptedException()).when(delegateMock).put(any()); + assertThrows(InterruptedException.class, () -> decorator.put("element")); + final double count = meterRegistry.counter("blocking.queue.puts.total", "name", "test-queue").count(); + assertThat(count, is(equalTo(0.0))); + } + + @Test + void testPutRethrowsInterruptedException() throws InterruptedException { + doThrow(new InterruptedException()).when(delegateMock).put(any()); + assertThrows(InterruptedException.class, () -> decorator.put("element")); + } + + @Test + void testPutRecordsDuration() throws InterruptedException { + decorator.put("element"); + final long count = meterRegistry.get("blocking.queue.put.duration").tag("name", "test-queue").timer().count(); + assertThat(count, is(equalTo(1L))); + } + + @Test + void testPutRecordsDurationEvenOnException() throws InterruptedException { + doThrow(new InterruptedException()).when(delegateMock).put(any()); + assertThrows(InterruptedException.class, () -> decorator.put("element")); + final long count = meterRegistry.get("blocking.queue.put.duration").tag("name", "test-queue").timer().count(); + assertThat(count, is(equalTo(1L))); + } + + @Test + void testPutMultipleSuccessesAccumulateCount() throws InterruptedException { + decorator.put("a"); + decorator.put("b"); + decorator.put("c"); + final double count = meterRegistry.counter("blocking.queue.puts.total", "name", "test-queue").count(); + assertThat(count, is(equalTo(3.0))); + } + + @Test + void testTakeDelegatesToDelegate() throws InterruptedException { + when(delegateMock.take()).thenReturn("element"); + decorator.take(); + verify(delegateMock).take(); + } + + @Test + void testTakeReturnsValueFromDelegate() throws InterruptedException { + when(delegateMock.take()).thenReturn("element"); + final String result = decorator.take(); + assertThat(result, is(equalTo("element"))); + } + + @Test + void testTakeIncrementsTakesTotalOnSuccess() throws InterruptedException { + when(delegateMock.take()).thenReturn("element"); + decorator.take(); + final double count = meterRegistry.counter("blocking.queue.takes.total", "name", "test-queue").count(); + assertThat(count, is(equalTo(1.0))); + } + + @Test + void testTakeDoesNotIncrementTakesFailedOnSuccess() throws InterruptedException { + when(delegateMock.take()).thenReturn("element"); + decorator.take(); + final double count = meterRegistry.counter("blocking.queue.takes.failed", "name", "test-queue").count(); + assertThat(count, is(equalTo(0.0))); + } + + @Test + void testTakeIncrementsTakesFailedOnException() throws InterruptedException { + when(delegateMock.take()).thenThrow(new InterruptedException()); + assertThrows(InterruptedException.class, () -> decorator.take()); + final double count = meterRegistry.counter("blocking.queue.takes.failed", "name", "test-queue").count(); + assertThat(count, is(equalTo(1.0))); + } + + @Test + void testTakeDoesNotIncrementTakesTotalOnException() throws InterruptedException { + when(delegateMock.take()).thenThrow(new InterruptedException()); + assertThrows(InterruptedException.class, () -> decorator.take()); + final double count = meterRegistry.counter("blocking.queue.takes.total", "name", "test-queue").count(); + assertThat(count, is(equalTo(0.0))); + } + + @Test + void testTakeRethrowsInterruptedException() throws InterruptedException { + when(delegateMock.take()).thenThrow(new InterruptedException()); + assertThrows(InterruptedException.class, () -> decorator.take()); + } + + @Test + void testTakeRecordsDuration() throws InterruptedException { + when(delegateMock.take()).thenReturn("element"); + decorator.take(); + final long count = meterRegistry.get("blocking.queue.take.duration").tag("name", "test-queue").timer().count(); + assertThat(count, is(equalTo(1L))); + } + + @Test + void testTakeRecordsDurationEvenOnException() throws InterruptedException { + when(delegateMock.take()).thenThrow(new InterruptedException()); + assertThrows(InterruptedException.class, () -> decorator.take()); + final long count = meterRegistry.get("blocking.queue.take.duration").tag("name", "test-queue").timer().count(); + assertThat(count, is(equalTo(1L))); + } + + @Test + void testTakeMultipleSuccessesAccumulateCount() throws InterruptedException { + when(delegateMock.take()).thenReturn("a", "b", "c"); + decorator.take(); + decorator.take(); + decorator.take(); + final double count = meterRegistry.counter("blocking.queue.takes.total", "name", "test-queue").count(); + assertThat(count, is(equalTo(3.0))); + } + + @Test + void testSizeDelegatesToDelegate() { + when(delegateMock.size()).thenReturn(5); + assertThat(decorator.size(), is(equalTo(5))); + verify(delegateMock).size(); + } + + @Test + void testIsEmptyReturnsTrueWhenDelegateReturnsTrue() { + when(delegateMock.isEmpty()).thenReturn(true); + assertThat(decorator.isEmpty(), is(true)); + } + + @Test + void testIsEmptyReturnsFalseWhenDelegateReturnsFalse() { + when(delegateMock.isEmpty()).thenReturn(false); + assertThat(decorator.isEmpty(), is(false)); + } + + @Test + void testPeekDelegatesToDelegate() { + when(delegateMock.peek()).thenReturn("element"); + assertThat(decorator.peek(), is(equalTo("element"))); + verify(delegateMock).peek(); + } + + @Test + void testPeekReturnsNullWhenDelegateReturnsNull() { + when(delegateMock.peek()).thenReturn(null); + assertThat(decorator.peek(), is(equalTo(null))); + } + + @Test + void testOfferDelegatesToDelegate() { + when(delegateMock.offer("element")).thenReturn(true); + assertThat(decorator.offer("element"), is(true)); + verify(delegateMock).offer("element"); + } + + @Test + void testOfferReturnsFalseWhenDelegateReturnsFalse() { + when(delegateMock.offer("element")).thenReturn(false); + assertThat(decorator.offer("element"), is(false)); + } + + @Test + void testOfferWithTimeoutDelegatesToDelegate() throws InterruptedException { + when(delegateMock.offer("element", 5L, TimeUnit.SECONDS)).thenReturn(true); + assertThat(decorator.offer("element", 5L, TimeUnit.SECONDS), is(true)); + verify(delegateMock).offer("element", 5L, TimeUnit.SECONDS); + } + + @Test + void testOfferWithTimeoutReturnsFalseWhenDelegateReturnsFalse() throws InterruptedException { + when(delegateMock.offer("element", 5L, TimeUnit.SECONDS)).thenReturn(false); + assertThat(decorator.offer("element", 5L, TimeUnit.SECONDS), is(false)); + } + + @Test + void testOfferWithTimeoutPropagatesInterruptedException() throws InterruptedException { + when(delegateMock.offer(any(), anyLong(), any())).thenThrow(new InterruptedException()); + assertThrows(InterruptedException.class, () -> decorator.offer("element", 5L, TimeUnit.SECONDS)); + } + + @Test + void testPollDelegatesToDelegate() { + when(delegateMock.poll()).thenReturn("element"); + assertThat(decorator.poll(), is(equalTo("element"))); + verify(delegateMock).poll(); + } + + @Test + void testPollReturnsNullWhenDelegateReturnsNull() { + when(delegateMock.poll()).thenReturn(null); + assertThat(decorator.poll(), is(equalTo(null))); + } + + @Test + void testPollWithTimeoutDelegatesToDelegate() throws InterruptedException { + when(delegateMock.poll(5L, TimeUnit.SECONDS)).thenReturn("element"); + assertThat(decorator.poll(5L, TimeUnit.SECONDS), is(equalTo("element"))); + verify(delegateMock).poll(5L, TimeUnit.SECONDS); + } + + @Test + void testPollWithTimeoutPropagatesInterruptedException() throws InterruptedException { + when(delegateMock.poll(anyLong(), any())).thenThrow(new InterruptedException()); + assertThrows(InterruptedException.class, () -> decorator.poll(5L, TimeUnit.SECONDS)); + } + + @Test + void testIteratorDelegatesToDelegate() { + final Iterator expectedIterator = mock(Iterator.class); + when(delegateMock.iterator()).thenReturn(expectedIterator); + assertThat(decorator.iterator(), is(equalTo(expectedIterator))); + verify(delegateMock).iterator(); + } + + @Test + void testAddDelegatesToDelegate() { + when(delegateMock.add("element")).thenReturn(true); + assertThat(decorator.add("element"), is(true)); + verify(delegateMock).add("element"); + } + + @Test + void testAddReturnsFalseWhenDelegateReturnsFalse() { + when(delegateMock.add("element")).thenReturn(false); + assertThat(decorator.add("element"), is(false)); + } + + @Test + void testRemainingCapacityDelegatesToDelegate() { + when(delegateMock.remainingCapacity()).thenReturn(10); + assertThat(decorator.remainingCapacity(), is(equalTo(10))); + verify(delegateMock).remainingCapacity(); + } + + @Test + void testDrainToDelegatesToDelegate() { + final List collection = new java.util.ArrayList<>(); + when(delegateMock.drainTo(collection)).thenReturn(3); + assertThat(decorator.drainTo(collection), is(equalTo(3))); + verify(delegateMock).drainTo(collection); + } + + @Test + void testDrainToWithMaxElementsDelegatesToDelegate() { + final List collection = new java.util.ArrayList<>(); + when(delegateMock.drainTo(collection, 5)).thenReturn(5); + assertThat(decorator.drainTo(collection, 5), is(equalTo(5))); + verify(delegateMock).drainTo(collection, 5); + } + + @Test + void testRemoveDelegatesToDelegate() { + when(delegateMock.remove()).thenReturn("element"); + assertThat(decorator.remove(), is(equalTo("element"))); + verify(delegateMock).remove(); + } + + @Test + void testRemoveObjectDelegatesToDelegate() { + when(delegateMock.remove("element")).thenReturn(true); + assertThat(decorator.remove("element"), is(true)); + verify(delegateMock).remove("element"); + } + + @Test + void testRemoveObjectReturnsFalseWhenDelegateReturnsFalse() { + when(delegateMock.remove("element")).thenReturn(false); + assertThat(decorator.remove("element"), is(false)); + } + + @Test + void testElementDelegatesToDelegate() { + when(delegateMock.element()).thenReturn("element"); + assertThat(decorator.element(), is(equalTo("element"))); + verify(delegateMock).element(); + } + + @Test + void testToArrayDelegatesToDelegate() { + final Object[] expected = new Object[] { "a", "b" }; + when(delegateMock.toArray()).thenReturn(expected); + assertThat(decorator.toArray(), is(equalTo(expected))); + verify(delegateMock).toArray(); + } + + @Test + void testToArrayWithTypeDelegatesToDelegate() { + final String[] input = new String[0]; + final String[] expected = new String[] { "a", "b" }; + when(delegateMock.toArray(input)).thenReturn(expected); + assertThat(decorator.toArray(input), is(equalTo(expected))); + verify(delegateMock).toArray(input); + } + + @Test + void testContainsAllDelegatesToDelegate() { + final Collection c = Arrays.asList("a", "b"); + when(delegateMock.containsAll(c)).thenReturn(true); + assertThat(decorator.containsAll(c), is(true)); + verify(delegateMock).containsAll(c); + } + + @Test + void testContainsAllReturnsFalseWhenDelegateReturnsFalse() { + final Collection c = Arrays.asList("a", "b"); + when(delegateMock.containsAll(c)).thenReturn(false); + assertThat(decorator.containsAll(c), is(false)); + } + + @Test + void testAddAllDelegatesToDelegate() { + final Collection c = Arrays.asList("a", "b"); + when(delegateMock.addAll(c)).thenReturn(true); + assertThat(decorator.addAll(c), is(true)); + verify(delegateMock).addAll(c); + } + + @Test + void testRemoveAllDelegatesToDelegate() { + final Collection c = Arrays.asList("a", "b"); + when(delegateMock.removeAll(c)).thenReturn(true); + assertThat(decorator.removeAll(c), is(true)); + verify(delegateMock).removeAll(c); + } + + @Test + void testRetainAllDelegatesToDelegate() { + final Collection c = Arrays.asList("a", "b"); + when(delegateMock.retainAll(c)).thenReturn(true); + assertThat(decorator.retainAll(c), is(true)); + verify(delegateMock).retainAll(c); + } + + @Test + void testClearDelegatesToDelegate() { + decorator.clear(); + verify(delegateMock).clear(); + } + + @Test + void testContainsDelegatesToDelegate() { + when(delegateMock.contains("element")).thenReturn(true); + assertThat(decorator.contains("element"), is(true)); + verify(delegateMock).contains("element"); + } + + @Test + void testContainsReturnsFalseWhenDelegateReturnsFalse() { + when(delegateMock.contains("element")).thenReturn(false); + assertThat(decorator.contains("element"), is(false)); + } + + @Test + void testPutsTotalCounterIsRegisteredWithCorrectName() { + assertThat(meterRegistry.get("blocking.queue.puts.total").tag("name", "test-queue").counter(), is(notNullValue())); + } + + @Test + void testPutsFailedCounterIsRegisteredWithCorrectName() { + assertThat(meterRegistry.get("blocking.queue.puts.failed").tag("name", "test-queue").counter(), is(notNullValue())); + } + + @Test + void testPutDurationTimerIsRegisteredWithCorrectName() { + assertThat(meterRegistry.get("blocking.queue.put.duration").tag("name", "test-queue").timer(), is(notNullValue())); + } + + @Test + void testTakesTotalCounterIsRegisteredWithCorrectName() { + assertThat(meterRegistry.get("blocking.queue.takes.total").tag("name", "test-queue").counter(), is(notNullValue())); + } + + @Test + void testTakesFailedCounterIsRegisteredWithCorrectName() { + assertThat(meterRegistry.get("blocking.queue.takes.failed").tag("name", "test-queue").counter(), is(notNullValue())); + } + + @Test + void testTakeDurationTimerIsRegisteredWithCorrectName() { + assertThat(meterRegistry.get("blocking.queue.take.duration").tag("name", "test-queue").timer(), is(notNullValue())); + } + + @Test + void testSizeGaugeIsRegisteredWithCorrectName() { + assertThat(meterRegistry.get("blocking.queue.size").tag("name", "test-queue").gauge(), is(notNullValue())); + } + + @Test + void testSizeGaugeReflectsDelegateSize() { + when(delegateMock.size()).thenReturn(7); + final double gaugeValue = meterRegistry.get("blocking.queue.size").tag("name", "test-queue").gauge().value(); + assertThat(gaugeValue, is(equalTo(7.0))); + } + + @Test + void testPutAndTakeCountersAreIndependent() throws InterruptedException { + when(delegateMock.take()).thenReturn("element"); + decorator.put("element"); + decorator.take(); + + final double puts = meterRegistry.counter("blocking.queue.puts.total", "name", "test-queue").count(); + final double takes = meterRegistry.counter("blocking.queue.takes.total", "name", "test-queue").count(); + assertThat(puts, is(equalTo(1.0))); + assertThat(takes, is(equalTo(1.0))); + } + + @Test + void testPutsFailedAndTakesFailedAreIndependent() throws InterruptedException { + doThrow(new InterruptedException()).when(delegateMock).put(any()); + when(delegateMock.take()).thenThrow(new InterruptedException()); + + assertThrows(InterruptedException.class, () -> decorator.put("element")); + assertThrows(InterruptedException.class, () -> decorator.take()); + + final double putsFailed = meterRegistry.counter("blocking.queue.puts.failed", "name", "test-queue").count(); + final double takesFailed = meterRegistry.counter("blocking.queue.takes.failed", "name", "test-queue").count(); + assertThat(putsFailed, is(equalTo(1.0))); + assertThat(takesFailed, is(equalTo(1.0))); + } +} \ No newline at end of file diff --git a/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/metrics/ExecutorServiceMetricsDecoratorTest.java b/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/metrics/ExecutorServiceMetricsDecoratorTest.java new file mode 100644 index 0000000..43a8fe4 --- /dev/null +++ b/amazon-sqs-java-messaging-lib-template/src/test/java/com/amazon/sqs/messaging/lib/metrics/ExecutorServiceMetricsDecoratorTest.java @@ -0,0 +1,498 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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.amazon.sqs.messaging.lib.metrics; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +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.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; + +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; + +@ExtendWith(MockitoExtension.class) +class ExecutorServiceMetricsDecoratorTest { + + private ExecutorServiceMetricsDecorator decorator; + + @Mock + private ExecutorService delegateMock; + + @Spy + private SimpleMeterRegistry meterRegistry; + + @BeforeEach + void setUp() { + decorator = new ExecutorServiceMetricsDecorator(delegateMock, meterRegistry, "test-executor"); + } + + @Test + void testConstructorWithNullRegistryDoesNotThrow() { + final ExecutorServiceMetricsDecorator instance = new ExecutorServiceMetricsDecorator(delegateMock, null, "test-executor"); + assertThat(instance, is(notNullValue())); + } + + @Test + void testConstructorWithValidRegistryCreatesInstance() { + assertThat(decorator, is(notNullValue())); + } + + @Test + void testSubmitRunnableDelegatesToDelegate() { + final Runnable task = mock(Runnable.class); + final Future expectedFuture = mock(Future.class); + when(delegateMock.submit(task)).thenReturn(expectedFuture); + + final Future result = decorator.submit(task); + + assertThat(result, is(equalTo(expectedFuture))); + verify(delegateMock).submit(task); + } + + @Test + void testSubmitRunnableWithResultDelegatesToDelegate() { + final Runnable task = mock(Runnable.class); + final String expectedResult = "result"; + final Future expectedFuture = mock(Future.class); + when(delegateMock.submit(task, expectedResult)).thenReturn(expectedFuture); + + final Future result = decorator.submit(task, expectedResult); + + assertThat(result, is(equalTo(expectedFuture))); + verify(delegateMock).submit(task, expectedResult); + } + + @Test + void testSubmitCallableDelegatesToDelegate() { + final Callable task = mock(Callable.class); + final Future expectedFuture = mock(Future.class); + when(delegateMock.submit(task)).thenReturn(expectedFuture); + + final Future result = decorator.submit(task); + + assertThat(result, is(equalTo(expectedFuture))); + verify(delegateMock).submit(task); + } + + @Test + void testInvokeAnyDelegatesToDelegate() throws InterruptedException, ExecutionException { + final Collection> tasks = Collections.singletonList(mock(Callable.class)); + when(delegateMock.invokeAny(tasks)).thenReturn("done"); + + final String result = decorator.invokeAny(tasks); + + assertThat(result, is(equalTo("done"))); + verify(delegateMock).invokeAny(tasks); + } + + @Test + void testInvokeAnyWithTimeoutDelegatesToDelegate() throws InterruptedException, ExecutionException, TimeoutException { + final Collection> tasks = Collections.singletonList(mock(Callable.class)); + when(delegateMock.invokeAny(tasks, 5L, TimeUnit.SECONDS)).thenReturn("done"); + + final String result = decorator.invokeAny(tasks, 5L, TimeUnit.SECONDS); + + assertThat(result, is(equalTo("done"))); + verify(delegateMock).invokeAny(tasks, 5L, TimeUnit.SECONDS); + } + + @Test + void testInvokeAllDelegatesToDelegate() throws InterruptedException { + final Collection> tasks = Collections.singletonList(mock(Callable.class)); + final List> expectedFutures = Collections.singletonList(mock(Future.class)); + when(delegateMock.invokeAll(tasks)).thenReturn(expectedFutures); + + final List> result = decorator.invokeAll(tasks); + + assertThat(result, is(equalTo(expectedFutures))); + verify(delegateMock).invokeAll(tasks); + } + + @Test + void testInvokeAllWithTimeoutDelegatesToDelegate() throws InterruptedException { + final Collection> tasks = Collections.singletonList(mock(Callable.class)); + final List> expectedFutures = Collections.singletonList(mock(Future.class)); + when(delegateMock.invokeAll(tasks, 5L, TimeUnit.SECONDS)).thenReturn(expectedFutures); + + final List> result = decorator.invokeAll(tasks, 5L, TimeUnit.SECONDS); + + assertThat(result, is(equalTo(expectedFutures))); + verify(delegateMock).invokeAll(tasks, 5L, TimeUnit.SECONDS); + } + + @Test + void testShutdownDelegatesToDelegate() { + decorator.shutdown(); + verify(delegateMock).shutdown(); + } + + @Test + void testShutdownNowDelegatesToDelegate() { + final List pending = Collections.singletonList(mock(Runnable.class)); + when(delegateMock.shutdownNow()).thenReturn(pending); + + final List result = decorator.shutdownNow(); + + assertThat(result, is(equalTo(pending))); + verify(delegateMock).shutdownNow(); + } + + @Test + void testIsShutdownReturnsTrueWhenDelegateReturnsTrue() { + when(delegateMock.isShutdown()).thenReturn(true); + assertThat(decorator.isShutdown(), is(true)); + } + + @Test + void testIsShutdownReturnsFalseWhenDelegateReturnsFalse() { + when(delegateMock.isShutdown()).thenReturn(false); + assertThat(decorator.isShutdown(), is(false)); + } + + @Test + void testIsTerminatedReturnsTrueWhenDelegateReturnsTrue() { + when(delegateMock.isTerminated()).thenReturn(true); + assertThat(decorator.isTerminated(), is(true)); + } + + @Test + void testIsTerminatedReturnsFalseWhenDelegateReturnsFalse() { + when(delegateMock.isTerminated()).thenReturn(false); + assertThat(decorator.isTerminated(), is(false)); + } + + @Test + void testAwaitTerminationReturnsTrueWhenDelegateReturnsTrue() throws InterruptedException { + when(delegateMock.awaitTermination(10L, TimeUnit.SECONDS)).thenReturn(true); + assertThat(decorator.awaitTermination(10L, TimeUnit.SECONDS), is(true)); + } + + @Test + void testAwaitTerminationReturnsFalseWhenDelegateReturnsFalse() throws InterruptedException { + when(delegateMock.awaitTermination(10L, TimeUnit.SECONDS)).thenReturn(false); + assertThat(decorator.awaitTermination(10L, TimeUnit.SECONDS), is(false)); + } + + @Test + void testExecuteWrapsCommandAndDelegatesToDelegate() { + final Runnable command = mock(Runnable.class); + final ArgumentCaptor captor = ArgumentCaptor.forClass(Runnable.class); + + decorator.execute(command); + + verify(delegateMock).execute(captor.capture()); + assertThat(captor.getValue(), is(notNullValue())); + } + + @Test + void testExecuteWrappedRunnableRunsOriginalCommand() { + final Runnable command = mock(Runnable.class); + final ArgumentCaptor captor = ArgumentCaptor.forClass(Runnable.class); + + decorator.execute(command); + verify(delegateMock).execute(captor.capture()); + + captor.getValue().run(); + + verify(command).run(); + } + + @Test + void testExecuteIncrementsSucceededCounterOnSuccess() { + final ArgumentCaptor captor = ArgumentCaptor.forClass(Runnable.class); + + decorator.execute(() -> { + }); + verify(delegateMock).execute(captor.capture()); + captor.getValue().run(); + + final double count = meterRegistry.counter("executor.tasks.succeeded", "name", "test-executor").count(); + assertThat(count, is(equalTo(1.0))); + } + + @Test + void testExecuteDoesNotIncrementFailedCounterOnSuccess() { + final ArgumentCaptor captor = ArgumentCaptor.forClass(Runnable.class); + + decorator.execute(() -> { + }); + verify(delegateMock).execute(captor.capture()); + captor.getValue().run(); + + final double count = meterRegistry.counter("executor.tasks.failed", "name", "test-executor").count(); + assertThat(count, is(equalTo(0.0))); + } + + @Test + void testExecuteIncrementsFailedCounterOnException() { + final ArgumentCaptor captor = ArgumentCaptor.forClass(Runnable.class); + final Runnable failingCommand = () -> { + throw new RuntimeException("boom"); + }; + + decorator.execute(failingCommand); + verify(delegateMock).execute(captor.capture()); + + try { + captor.getValue().run(); + } catch (final RuntimeException ignored) { + } + + final double count = meterRegistry.counter("executor.tasks.failed", "name", "test-executor").count(); + assertThat(count, is(equalTo(1.0))); + } + + @Test + void testExecuteDoesNotIncrementSucceededCounterOnException() { + final ArgumentCaptor captor = ArgumentCaptor.forClass(Runnable.class); + final Runnable failingCommand = () -> { + throw new RuntimeException("boom"); + }; + + decorator.execute(failingCommand); + verify(delegateMock).execute(captor.capture()); + + try { + captor.getValue().run(); + } catch (final RuntimeException ignored) { + } + + final double count = meterRegistry.counter("executor.tasks.succeeded", "name", "test-executor").count(); + assertThat(count, is(equalTo(0.0))); + } + + @Test + void testExecuteRethrowsExceptionFromCommand() { + final ArgumentCaptor captor = ArgumentCaptor.forClass(Runnable.class); + final RuntimeException expectedException = new RuntimeException("test error"); + final Runnable failingCommand = () -> { + throw expectedException; + }; + + decorator.execute(failingCommand); + verify(delegateMock).execute(captor.capture()); + + RuntimeException thrown = null; + try { + captor.getValue().run(); + } catch (final RuntimeException ex) { + thrown = ex; + } + + assertThat(thrown, is(equalTo(expectedException))); + } + + @Test + void testExecuteDecrementsActiveTaskCountAfterSuccessfulRun() { + final ArgumentCaptor captor = ArgumentCaptor.forClass(Runnable.class); + + decorator.execute(() -> { + }); + verify(delegateMock).execute(captor.capture()); + captor.getValue().run(); + + final double gaugeValue = meterRegistry.get("executor.active").tag("name", "test-executor").gauge().value(); + assertThat(gaugeValue, is(equalTo(0.0))); + } + + @Test + void testExecuteDecrementsActiveTaskCountAfterFailedRun() { + final ArgumentCaptor captor = ArgumentCaptor.forClass(Runnable.class); + final Runnable failingCommand = () -> { + throw new RuntimeException("fail"); + }; + + decorator.execute(failingCommand); + verify(delegateMock).execute(captor.capture()); + + try { + captor.getValue().run(); + } catch (final RuntimeException ignored) { + } + + final double gaugeValue = meterRegistry.get("executor.active").tag("name", "test-executor").gauge().value(); + assertThat(gaugeValue, is(equalTo(0.0))); + } + + @Test + void testExecuteRecordsTaskDuration() { + final ArgumentCaptor captor = ArgumentCaptor.forClass(Runnable.class); + + decorator.execute(() -> { + }); + verify(delegateMock).execute(captor.capture()); + captor.getValue().run(); + + final long timerCount = meterRegistry.get("executor.task.duration").tag("name", "test-executor").timer().count(); + assertThat(timerCount, is(equalTo(1L))); + } + + @Test + void testExecuteRecordsTaskDurationEvenOnException() { + final ArgumentCaptor captor = ArgumentCaptor.forClass(Runnable.class); + final Runnable failingCommand = () -> { + throw new RuntimeException("fail"); + }; + + decorator.execute(failingCommand); + verify(delegateMock).execute(captor.capture()); + + try { + captor.getValue().run(); + } catch (final RuntimeException ignored) { + } + + final long timerCount = meterRegistry.get("executor.task.duration").tag("name", "test-executor").timer().count(); + assertThat(timerCount, is(equalTo(1L))); + } + + @Test + void testExecuteMultipleSuccessesAccumulateCount() { + final int runs = 5; + for (int i = 0; i < runs; i++) { + final ArgumentCaptor captor = ArgumentCaptor.forClass(Runnable.class); + decorator.execute(() -> { + }); + verify(delegateMock, atLeast(1)).execute(captor.capture()); + captor.getValue().run(); + } + + final double count = meterRegistry.counter("executor.tasks.succeeded", "name", "test-executor").count(); + assertThat(count, is(equalTo((double) runs))); + } + + @Test + void testExecuteMultipleFailuresAccumulateCount() { + final Runnable failingCommand = () -> { + throw new RuntimeException("fail"); + }; + final int runs = 3; + + for (int i = 0; i < runs; i++) { + final ArgumentCaptor captor = ArgumentCaptor.forClass(Runnable.class); + decorator.execute(failingCommand); + verify(delegateMock, atLeast(1)).execute(captor.capture()); + try { + captor.getValue().run(); + } catch (final RuntimeException ignored) { + } + } + + final double count = meterRegistry.counter("executor.tasks.failed", "name", "test-executor").count(); + assertThat(count, is(equalTo((double) runs))); + } + + @Test + void testActiveGaugeIsRegisteredWithCorrectName() { + assertThat(meterRegistry.get("executor.active").tag("name", "test-executor").gauge(), is(notNullValue())); + } + + @Test + void testSucceededCounterIsRegisteredWithCorrectName() { + assertThat(meterRegistry.get("executor.tasks.succeeded").tag("name", "test-executor").counter(), is(notNullValue())); + } + + @Test + void testFailedCounterIsRegisteredWithCorrectName() { + assertThat(meterRegistry.get("executor.tasks.failed").tag("name", "test-executor").counter(), is(notNullValue())); + } + + @Test + void testTaskTimerIsRegisteredWithCorrectName() { + assertThat(meterRegistry.get("executor.task.duration").tag("name", "test-executor").timer(), is(notNullValue())); + } + + @Test + void testInvokeAnyPropagatesInterruptedException() throws InterruptedException, ExecutionException { + final Collection> tasks = Collections.singletonList(mock(Callable.class)); + when(delegateMock.invokeAny(tasks)).thenThrow(new InterruptedException()); + + assertThrows(InterruptedException.class, () -> decorator.invokeAny(tasks)); + } + + @Test + void testInvokeAnyPropagatesExecutionException() throws InterruptedException, ExecutionException { + final Collection> tasks = Collections.singletonList(mock(Callable.class)); + when(delegateMock.invokeAny(tasks)).thenThrow(new ExecutionException(new RuntimeException())); + + assertThrows(ExecutionException.class, () -> decorator.invokeAny(tasks)); + } + + @Test + void testInvokeAnyWithTimeoutPropagatesTimeoutException() throws InterruptedException, ExecutionException, TimeoutException { + final Collection> tasks = Collections.singletonList(mock(Callable.class)); + when(delegateMock.invokeAny(tasks, 1L, TimeUnit.SECONDS)).thenThrow(new TimeoutException()); + + assertThrows(TimeoutException.class, () -> decorator.invokeAny(tasks, 1L, TimeUnit.SECONDS)); + } + + @Test + void testInvokeAllPropagatesInterruptedException() throws InterruptedException { + final Collection> tasks = Collections.singletonList(mock(Callable.class)); + when(delegateMock.invokeAll(tasks)).thenThrow(new InterruptedException()); + + assertThrows(InterruptedException.class, () -> decorator.invokeAll(tasks)); + } + + @Test + void testInvokeAllWithTimeoutPropagatesInterruptedException() throws InterruptedException { + final Collection> tasks = Collections.singletonList(mock(Callable.class)); + when(delegateMock.invokeAll(tasks, 1L, TimeUnit.SECONDS)).thenThrow(new InterruptedException()); + + assertThrows(InterruptedException.class, () -> decorator.invokeAll(tasks, 1L, TimeUnit.SECONDS)); + } + + @Test + void testAwaitTerminationPropagatesInterruptedException() throws InterruptedException { + when(delegateMock.awaitTermination(anyLong(), any())).thenThrow(new InterruptedException()); + + assertThrows(InterruptedException.class, () -> decorator.awaitTermination(1L, TimeUnit.SECONDS)); + } + + @Test + void testShutdownNowReturnsEmptyListWhenNoPendingTasks() { + when(delegateMock.shutdownNow()).thenReturn(Collections.emptyList()); + + final List result = decorator.shutdownNow(); + + assertThat(result.isEmpty(), is(true)); + } +} \ No newline at end of file diff --git a/amazon-sqs-java-messaging-lib-v1/src/main/java/com/amazon/sqs/messaging/lib/core/AmazonSqsConsumer.java b/amazon-sqs-java-messaging-lib-v1/src/main/java/com/amazon/sqs/messaging/lib/core/AmazonSqsConsumerImpl.java similarity index 91% rename from amazon-sqs-java-messaging-lib-v1/src/main/java/com/amazon/sqs/messaging/lib/core/AmazonSqsConsumer.java rename to amazon-sqs-java-messaging-lib-v1/src/main/java/com/amazon/sqs/messaging/lib/core/AmazonSqsConsumerImpl.java index c93090c..4d64fed 100644 --- a/amazon-sqs-java-messaging-lib-v1/src/main/java/com/amazon/sqs/messaging/lib/core/AmazonSqsConsumer.java +++ b/amazon-sqs-java-messaging-lib-v1/src/main/java/com/amazon/sqs/messaging/lib/core/AmazonSqsConsumerImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -49,9 +49,9 @@ * @param the request entry payload type */ @SuppressWarnings("java:S6204") -class AmazonSqsConsumer extends AbstractAmazonSqsConsumer { +class AmazonSqsConsumerImpl extends AbstractAmazonSqsConsumer { - private static final Logger LOGGER = LoggerFactory.getLogger(AmazonSqsConsumer.class); + private static final Logger LOGGER = LoggerFactory.getLogger(AmazonSqsConsumerImpl.class); private static final MessageAttributes messageAttributes = new MessageAttributes(); @@ -66,7 +66,7 @@ class AmazonSqsConsumer extends AbstractAmazonSqsConsumer, SendMessageBatchRequest * {@inheritDoc} */ @Override - protected void handleError(final SendMessageBatchRequest publishBatchRequest, final Throwable throwable) { + public void handleError(final SendMessageBatchRequest publishBatchRequest, final Throwable throwable) { final String code = throwable instanceof AmazonServiceException ? AmazonServiceException.class.cast(throwable).getErrorCode() : "000"; final String message = throwable instanceof AmazonServiceException ? AmazonServiceException.class.cast(throwable).getErrorMessage() : throwable.getMessage(); @@ -129,7 +129,7 @@ protected void handleError(final SendMessageBatchRequest publishBatchRequest, fi * {@inheritDoc} */ @Override - protected void handleResponse(final SendMessageBatchResult publishBatchResult) { + public void handleResponse(final SendMessageBatchResult publishBatchResult) { publishBatchResult.getSuccessful().forEach(entry -> Optional.ofNullable(pendingRequests.remove(entry.getId())).ifPresent(listenableFuture -> listenableFuture.success(ResponseSuccessEntry.builder() diff --git a/amazon-sqs-java-messaging-lib-v1/src/main/java/com/amazon/sqs/messaging/lib/core/AmazonSqsProducer.java b/amazon-sqs-java-messaging-lib-v1/src/main/java/com/amazon/sqs/messaging/lib/core/AmazonSqsProducerImpl.java similarity index 74% rename from amazon-sqs-java-messaging-lib-v1/src/main/java/com/amazon/sqs/messaging/lib/core/AmazonSqsProducer.java rename to amazon-sqs-java-messaging-lib-v1/src/main/java/com/amazon/sqs/messaging/lib/core/AmazonSqsProducerImpl.java index d0e3d80..18c4c2c 100644 --- a/amazon-sqs-java-messaging-lib-v1/src/main/java/com/amazon/sqs/messaging/lib/core/AmazonSqsProducer.java +++ b/amazon-sqs-java-messaging-lib-v1/src/main/java/com/amazon/sqs/messaging/lib/core/AmazonSqsProducerImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,6 @@ import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.ExecutorService; import com.amazon.sqs.messaging.lib.model.RequestEntry; import com.amazon.sqs.messaging.lib.model.ResponseFailEntry; @@ -30,20 +29,18 @@ * * @param the request entry payload type */ -class AmazonSqsProducer extends AbstractAmazonSqsProducer { +class AmazonSqsProducerImpl extends AbstractAmazonSqsProducer { /** * Constructs a v1 SQS producer. * * @param pendingRequests the map of pending requests * @param queueRequests the blocking queue of incoming requests - * @param executorService the executor service */ - public AmazonSqsProducer( + public AmazonSqsProducerImpl( final ConcurrentMap> pendingRequests, - final BlockingQueue> queueRequests, - final ExecutorService executorService) { - super(pendingRequests, queueRequests, executorService); + final BlockingQueue> queueRequests) { + super(pendingRequests, queueRequests); } } diff --git a/amazon-sqs-java-messaging-lib-v1/src/main/java/com/amazon/sqs/messaging/lib/core/AmazonSqsTemplate.java b/amazon-sqs-java-messaging-lib-v1/src/main/java/com/amazon/sqs/messaging/lib/core/AmazonSqsTemplate.java index 20de46d..2366765 100644 --- a/amazon-sqs-java-messaging-lib-v1/src/main/java/com/amazon/sqs/messaging/lib/core/AmazonSqsTemplate.java +++ b/amazon-sqs-java-messaging-lib-v1/src/main/java/com/amazon/sqs/messaging/lib/core/AmazonSqsTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,16 +17,12 @@ package com.amazon.sqs.messaging.lib.core; import java.util.concurrent.BlockingQueue; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; import java.util.function.UnaryOperator; -import com.amazon.sqs.messaging.lib.concurrent.ExecutorsProvider; import com.amazon.sqs.messaging.lib.concurrent.RingBufferBlockingQueue; +import com.amazon.sqs.messaging.lib.metrics.AmazonSqsConsumerMetricsDecorator; import com.amazon.sqs.messaging.lib.model.QueueProperty; import com.amazon.sqs.messaging.lib.model.RequestEntry; -import com.amazon.sqs.messaging.lib.model.ResponseFailEntry; -import com.amazon.sqs.messaging.lib.model.ResponseSuccessEntry; import com.amazonaws.services.sqs.AmazonSQS; import com.amazonaws.services.sqs.model.SendMessageBatchRequest; import com.amazonaws.services.sqs.model.SendMessageBatchResult; @@ -39,37 +35,53 @@ * * @param the request entry payload type */ -public class AmazonSqsTemplate extends AbstractAmazonSqsTemplate { +public class AmazonSqsTemplate extends AbstractAmazonSqsTemplate { + + private AmazonSqsTemplate(final Builder> builder) { + super( + new AmazonSqsProducerImpl<>( + builder.getPendingRequests(), + builder.getQueueRequests() + ), + new AmazonSqsConsumerMetricsDecorator( + new AmazonSqsConsumerImpl<>( + builder.getAmazonSqsClient(), + builder.getQueueProperty(), + builder.getObjectMapper(), + builder.getPendingRequests(), + builder.getQueueRequests(), + getExecutorService(builder.getQueueProperty(), builder.getMeterRegistry()), + builder.getPublishDecorator() + ), + builder.getQueueProperty(), + builder.getMeterRegistry() + ) + ); + } /** - * Primary constructor that wires the producer and consumer together. + * Creates a new builder for constructing an {@link AmazonSnsTemplate}. * - * @param amazonSqsClient the AWS SDK v1 SQS client - * @param queueProperty the queue configuration properties - * @param pendingRequests the map of pending requests - * @param queueRequests the blocking queue of incoming requests - * @param objectMapper the JSON object mapper - * @param publishDecorator a decorator for batch publish requests + * @param the request entry payload type + * @param amazonSqsClient the v1 {@link AmazonSQS} client + * @param queueProperty the queue configuration + * @return a new builder instance */ - private AmazonSqsTemplate( + public static Builder> builder( final AmazonSQS amazonSqsClient, - final QueueProperty queueProperty, - final ConcurrentMap> pendingRequests, - final BlockingQueue> queueRequests, - final ObjectMapper objectMapper, - final UnaryOperator publishDecorator) { - super( - new AmazonSqsProducer<>(pendingRequests, queueRequests, ExecutorsProvider.getExecutorService()), - new AmazonSqsConsumer<>(amazonSqsClient, queueProperty, objectMapper, pendingRequests, queueRequests, getAmazonSqsThreadPoolExecutor(queueProperty), publishDecorator) - ); + final QueueProperty queueProperty) { + return new Builder<>(AmazonSqsTemplate::new, amazonSqsClient, queueProperty); } /** * Creates a template with default object mapper and identity publish decorator. * + * @deprecated since 1.3.0, use {@link #builder(AmazonSQS, QueueProperty)} instead + * * @param amazonSqsClient the AWS SDK v1 SQS client * @param queueProperty the queue configuration properties */ + @Deprecated public AmazonSqsTemplate(final AmazonSQS amazonSqsClient, final QueueProperty queueProperty) { this(amazonSqsClient, queueProperty, UnaryOperator.identity()); } @@ -77,10 +89,13 @@ public AmazonSqsTemplate(final AmazonSQS amazonSqsClient, final QueueProperty qu /** * Creates a template with a custom publish decorator and default object mapper. * + * @deprecated since 1.3.0, use {@link #builder(AmazonSQS, QueueProperty)} and {@link Builder#publishDecorator(UnaryOperator)} instead + * * @param amazonSqsClient the AWS SDK v1 SQS client * @param queueProperty the queue configuration properties * @param publishDecorator a decorator for batch publish requests */ + @Deprecated public AmazonSqsTemplate(final AmazonSQS amazonSqsClient, final QueueProperty queueProperty, final UnaryOperator publishDecorator) { this(amazonSqsClient, queueProperty, new ObjectMapper(), publishDecorator); } @@ -89,10 +104,13 @@ public AmazonSqsTemplate(final AmazonSQS amazonSqsClient, final QueueProperty qu * Creates a template with a custom request queue, default object mapper, * and identity publish decorator. * + * @deprecated since 1.3.0, use {@link #builder(AmazonSQS, QueueProperty)} and {@link Builder#queueRequests(BlockingQueue)} instead + * * @param amazonSqsClient the AWS SDK v1 SQS client * @param queueProperty the queue configuration properties * @param queueRequests the blocking queue for incoming requests */ + @Deprecated public AmazonSqsTemplate(final AmazonSQS amazonSqsClient, final QueueProperty queueProperty, final BlockingQueue> queueRequests) { this(amazonSqsClient, queueProperty, queueRequests, UnaryOperator.identity()); } @@ -100,11 +118,14 @@ public AmazonSqsTemplate(final AmazonSQS amazonSqsClient, final QueueProperty qu /** * Creates a template with a custom request queue and publish decorator. * + * @deprecated since 1.3.0, use {@link #builder(AmazonSQS, QueueProperty)} with {@link Builder#queueRequests(BlockingQueue)} and {@link Builder#publishDecorator(UnaryOperator)} instead + * * @param amazonSqsClient the AWS SDK v1 SQS client * @param queueProperty the queue configuration properties * @param queueRequests the blocking queue for incoming requests * @param publishDecorator a decorator for batch publish requests */ + @Deprecated public AmazonSqsTemplate(final AmazonSQS amazonSqsClient, final QueueProperty queueProperty, final BlockingQueue> queueRequests, final UnaryOperator publishDecorator) { this(amazonSqsClient, queueProperty, queueRequests, new ObjectMapper(), publishDecorator); } @@ -112,10 +133,13 @@ public AmazonSqsTemplate(final AmazonSQS amazonSqsClient, final QueueProperty qu /** * Creates a template with a custom object mapper and identity publish decorator. * + * @deprecated since 1.3.0, use {@link #builder(AmazonSQS, QueueProperty)} and {@link Builder#objectMapper(ObjectMapper)} instead + * * @param amazonSqsClient the AWS SDK v1 SQS client * @param queueProperty the queue configuration properties * @param objectMapper the JSON object mapper */ + @Deprecated public AmazonSqsTemplate(final AmazonSQS amazonSqsClient, final QueueProperty queueProperty, final ObjectMapper objectMapper) { this(amazonSqsClient, queueProperty, objectMapper, UnaryOperator.identity()); } @@ -123,11 +147,14 @@ public AmazonSqsTemplate(final AmazonSQS amazonSqsClient, final QueueProperty qu /** * Creates a template with a custom object mapper and publish decorator. * + * @deprecated since 1.3.0, use {@link #builder(AmazonSQS, QueueProperty)} with {@link Builder#objectMapper(ObjectMapper)} and {@link Builder#publishDecorator(UnaryOperator)} instead + * * @param amazonSqsClient the AWS SDK v1 SQS client * @param queueProperty the queue configuration properties * @param objectMapper the JSON object mapper * @param publishDecorator a decorator for batch publish requests */ + @Deprecated public AmazonSqsTemplate(final AmazonSQS amazonSqsClient, final QueueProperty queueProperty, final ObjectMapper objectMapper, final UnaryOperator publishDecorator) { this(amazonSqsClient, queueProperty, new RingBufferBlockingQueue<>(queueProperty.getMaximumPoolSize() * queueProperty.getMaxBatchSize()), objectMapper, publishDecorator); } @@ -135,11 +162,14 @@ public AmazonSqsTemplate(final AmazonSQS amazonSqsClient, final QueueProperty qu /** * Creates a template with a custom request queue and object mapper. * + * @deprecated since 1.3.0, use {@link #builder(AmazonSQS, QueueProperty)} with {@link Builder#queueRequests(BlockingQueue)} and {@link Builder#objectMapper(ObjectMapper)} instead + * * @param amazonSqsClient the AWS SDK v1 SQS client * @param queueProperty the queue configuration properties * @param queueRequests the blocking queue for incoming requests * @param objectMapper the JSON object mapper */ + @Deprecated public AmazonSqsTemplate(final AmazonSQS amazonSqsClient, final QueueProperty queueProperty, final BlockingQueue> queueRequests, final ObjectMapper objectMapper) { this(amazonSqsClient, queueProperty, queueRequests, objectMapper, UnaryOperator.identity()); } @@ -147,14 +177,21 @@ public AmazonSqsTemplate(final AmazonSQS amazonSqsClient, final QueueProperty qu /** * Creates a template with full custom configuration. * + * @deprecated since 1.3.0, use {@link #builder(AmazonSQS, QueueProperty)} with builder setters instead + * * @param amazonSqsClient the AWS SDK v1 SQS client * @param queueProperty the queue configuration properties * @param queueRequests the blocking queue for incoming requests * @param objectMapper the JSON object mapper * @param publishDecorator a decorator for batch publish requests */ + @Deprecated public AmazonSqsTemplate(final AmazonSQS amazonSqsClient, final QueueProperty queueProperty, final BlockingQueue> queueRequests, final ObjectMapper objectMapper, final UnaryOperator publishDecorator) { - this(amazonSqsClient, queueProperty, new ConcurrentHashMap<>(), queueRequests, objectMapper, publishDecorator); + this(AmazonSqsTemplate.builder(amazonSqsClient, queueProperty) + .queueRequests(queueRequests) + .objectMapper(objectMapper) + .publishDecorator(publishDecorator) + ); } } diff --git a/amazon-sqs-java-messaging-lib-v1/src/main/java/com/amazon/sqs/messaging/lib/core/MessageAttributes.java b/amazon-sqs-java-messaging-lib-v1/src/main/java/com/amazon/sqs/messaging/lib/core/MessageAttributes.java index 79f90da..02c5d02 100644 --- a/amazon-sqs-java-messaging-lib-v1/src/main/java/com/amazon/sqs/messaging/lib/core/MessageAttributes.java +++ b/amazon-sqs-java-messaging-lib-v1/src/main/java/com/amazon/sqs/messaging/lib/core/MessageAttributes.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/amazon-sqs-java-messaging-lib-v1/src/main/java/com/amazon/sqs/messaging/lib/metrics/AmazonSqsConsumerMetricsDecorator.java b/amazon-sqs-java-messaging-lib-v1/src/main/java/com/amazon/sqs/messaging/lib/metrics/AmazonSqsConsumerMetricsDecorator.java new file mode 100644 index 0000000..53f96fe --- /dev/null +++ b/amazon-sqs-java-messaging-lib-v1/src/main/java/com/amazon/sqs/messaging/lib/metrics/AmazonSqsConsumerMetricsDecorator.java @@ -0,0 +1,109 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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.amazon.sqs.messaging.lib.metrics; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.amazon.sqs.messaging.lib.core.AmazonSqsConsumer; +import com.amazon.sqs.messaging.lib.model.QueueProperty; +import com.amazonaws.AmazonServiceException; +import com.amazonaws.services.sqs.model.SendMessageBatchRequest; +import com.amazonaws.services.sqs.model.SendMessageBatchResult; + +import io.micrometer.core.instrument.MeterRegistry; +import lombok.SneakyThrows; + +// @formatter:off +/** + * AWS SDK v1 metrics decorator for {@link AmazonSqsConsumer}. Records publish attempt/success/failure + * counters, latency, batch size, and inflight gauges using Micrometer. Handles + * {@link AmazonServiceException} error codes for failure tagging. + */ +public class AmazonSqsConsumerMetricsDecorator extends AbstractAmazonSqsConsumerMetricsDecorator { + + private static final Logger LOGGER = LoggerFactory.getLogger(AmazonSqsConsumerMetricsDecorator.class); + + /** + * Creates a new v1 SQS consumer metrics decorator. + * + * @param delegate the consumer to decorate + * @param queueProperty the queue configuration + * @param meterRegistry the Micrometer meter registry + */ + public AmazonSqsConsumerMetricsDecorator( + final AmazonSqsConsumer delegate, + final QueueProperty queueProperty, + final MeterRegistry meterRegistry) { + super(delegate, queueProperty, meterRegistry); + } + + /** + * {@inheritDoc} + */ + @Override + @SneakyThrows + public SendMessageBatchResult publish(final SendMessageBatchRequest publishBatchRequest) { + publishAttemptsCounter.increment(); + batchSizeSummary.record(publishBatchRequest.getEntries().size()); + inflightGauge.incrementAndGet(); + + try { + return publishTimer.recordCallable(() -> delegate.publish(publishBatchRequest)); + } finally { + inflightGauge.decrementAndGet(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void handleResponse(final SendMessageBatchResult publishBatchResult) { + delegate.handleResponse(publishBatchResult); + + final int successCount = publishBatchResult.getSuccessful().size(); + final int failureCount = publishBatchResult.getFailed().size(); + + if (successCount > 0) { + successCounter.increment(successCount); + } + + publishBatchResult.getFailed().forEach(entry -> failureCounter(entry.getCode(), ERROR_TYPE_AMAZON).increment()); + + if (failureCount > 0) { + LOGGER.warn("SNS batch partially failed: {} succeeded, {} failed", successCount, failureCount); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void handleError(final SendMessageBatchRequest publishBatchRequest, final Throwable throwable) { + delegate.handleError(publishBatchRequest, throwable); + + final String errorCode = throwable instanceof AmazonServiceException ? AmazonServiceException.class.cast(throwable).getErrorCode() : "000"; + + final String errorType = throwable instanceof AmazonServiceException ? ERROR_TYPE_AMAZON : ERROR_TYPE_OTHER; + + final int failedEntries = publishBatchRequest.getEntries().size(); + + failureCounter(errorCode, errorType).increment(failedEntries); + } + +} diff --git a/amazon-sqs-java-messaging-lib-v1/src/test/java/com/amazon/sqs/messaging/lib/core/AmazonSqsProducerAsyncTest.java b/amazon-sqs-java-messaging-lib-v1/src/test/java/com/amazon/sqs/messaging/lib/core/AmazonSqsProducerAsyncTest.java index 775f13b..a87cf87 100644 --- a/amazon-sqs-java-messaging-lib-v1/src/test/java/com/amazon/sqs/messaging/lib/core/AmazonSqsProducerAsyncTest.java +++ b/amazon-sqs-java-messaging-lib-v1/src/test/java/com/amazon/sqs/messaging/lib/core/AmazonSqsProducerAsyncTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/amazon-sqs-java-messaging-lib-v1/src/test/java/com/amazon/sqs/messaging/lib/core/AmazonSqsProducerSyncTest.java b/amazon-sqs-java-messaging-lib-v1/src/test/java/com/amazon/sqs/messaging/lib/core/AmazonSqsProducerSyncTest.java index cc46e58..afa657f 100644 --- a/amazon-sqs-java-messaging-lib-v1/src/test/java/com/amazon/sqs/messaging/lib/core/AmazonSqsProducerSyncTest.java +++ b/amazon-sqs-java-messaging-lib-v1/src/test/java/com/amazon/sqs/messaging/lib/core/AmazonSqsProducerSyncTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/amazon-sqs-java-messaging-lib-v1/src/test/java/com/amazon/sqs/messaging/lib/core/AmazonSqsTemplateIntegrationTest.java b/amazon-sqs-java-messaging-lib-v1/src/test/java/com/amazon/sqs/messaging/lib/core/AmazonSqsTemplateIntegrationTest.java index 3ca1e34..fbc3439 100644 --- a/amazon-sqs-java-messaging-lib-v1/src/test/java/com/amazon/sqs/messaging/lib/core/AmazonSqsTemplateIntegrationTest.java +++ b/amazon-sqs-java-messaging-lib-v1/src/test/java/com/amazon/sqs/messaging/lib/core/AmazonSqsTemplateIntegrationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/amazon-sqs-java-messaging-lib-v1/src/test/java/com/amazon/sqs/messaging/lib/core/MessageAttributesTest.java b/amazon-sqs-java-messaging-lib-v1/src/test/java/com/amazon/sqs/messaging/lib/core/MessageAttributesTest.java index 17b1842..e26e59c 100644 --- a/amazon-sqs-java-messaging-lib-v1/src/test/java/com/amazon/sqs/messaging/lib/core/MessageAttributesTest.java +++ b/amazon-sqs-java-messaging-lib-v1/src/test/java/com/amazon/sqs/messaging/lib/core/MessageAttributesTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/amazon-sqs-java-messaging-lib-v1/src/test/java/com/amazon/sqs/messaging/lib/core/helper/ConsumerHelper.java b/amazon-sqs-java-messaging-lib-v1/src/test/java/com/amazon/sqs/messaging/lib/core/helper/ConsumerHelper.java index deef405..5003f99 100644 --- a/amazon-sqs-java-messaging-lib-v1/src/test/java/com/amazon/sqs/messaging/lib/core/helper/ConsumerHelper.java +++ b/amazon-sqs-java-messaging-lib-v1/src/test/java/com/amazon/sqs/messaging/lib/core/helper/ConsumerHelper.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/amazon-sqs-java-messaging-lib-v1/src/test/resources/logback.xml b/amazon-sqs-java-messaging-lib-v1/src/test/resources/logback.xml index 67bc8f2..d7d66b7 100644 --- a/amazon-sqs-java-messaging-lib-v1/src/test/resources/logback.xml +++ b/amazon-sqs-java-messaging-lib-v1/src/test/resources/logback.xml @@ -10,6 +10,6 @@ - + \ No newline at end of file diff --git a/amazon-sqs-java-messaging-lib-v2/src/main/java/com/amazon/sqs/messaging/lib/core/AmazonSqsConsumer.java b/amazon-sqs-java-messaging-lib-v2/src/main/java/com/amazon/sqs/messaging/lib/core/AmazonSqsConsumerImpl.java similarity index 91% rename from amazon-sqs-java-messaging-lib-v2/src/main/java/com/amazon/sqs/messaging/lib/core/AmazonSqsConsumer.java rename to amazon-sqs-java-messaging-lib-v2/src/main/java/com/amazon/sqs/messaging/lib/core/AmazonSqsConsumerImpl.java index 93089f3..0f009dc 100644 --- a/amazon-sqs-java-messaging-lib-v2/src/main/java/com/amazon/sqs/messaging/lib/core/AmazonSqsConsumer.java +++ b/amazon-sqs-java-messaging-lib-v2/src/main/java/com/amazon/sqs/messaging/lib/core/AmazonSqsConsumerImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -50,9 +50,9 @@ * @param the request entry payload type */ @SuppressWarnings("java:S6204") -class AmazonSqsConsumer extends AbstractAmazonSqsConsumer { +class AmazonSqsConsumerImpl extends AbstractAmazonSqsConsumer { - private static final Logger LOGGER = LoggerFactory.getLogger(AmazonSqsConsumer.class); + private static final Logger LOGGER = LoggerFactory.getLogger(AmazonSqsConsumerImpl.class); private static final MessageAttributes messageAttributes = new MessageAttributes(); @@ -67,7 +67,7 @@ class AmazonSqsConsumer extends AbstractAmazonSqsConsumer, SendMessageBatchRequest * {@inheritDoc} */ @Override - protected void handleError(final SendMessageBatchRequest publishBatchRequest, final Throwable throwable) { + public void handleError(final SendMessageBatchRequest publishBatchRequest, final Throwable throwable) { final String code = throwable instanceof AwsServiceException ? AwsServiceException.class.cast(throwable).awsErrorDetails().errorCode() : "000"; final String message = throwable instanceof AwsServiceException ? AwsServiceException.class.cast(throwable).awsErrorDetails().errorMessage() : throwable.getMessage(); @@ -131,7 +131,7 @@ protected void handleError(final SendMessageBatchRequest publishBatchRequest, fi * {@inheritDoc} */ @Override - protected void handleResponse(final SendMessageBatchResponse publishBatchResult) { + public void handleResponse(final SendMessageBatchResponse publishBatchResult) { publishBatchResult.successful().forEach(entry -> Optional.ofNullable(pendingRequests.remove(entry.id())).ifPresent(listenableFuture -> listenableFuture.success(ResponseSuccessEntry.builder() diff --git a/amazon-sqs-java-messaging-lib-v2/src/main/java/com/amazon/sqs/messaging/lib/core/AmazonSqsProducer.java b/amazon-sqs-java-messaging-lib-v2/src/main/java/com/amazon/sqs/messaging/lib/core/AmazonSqsProducerImpl.java similarity index 74% rename from amazon-sqs-java-messaging-lib-v2/src/main/java/com/amazon/sqs/messaging/lib/core/AmazonSqsProducer.java rename to amazon-sqs-java-messaging-lib-v2/src/main/java/com/amazon/sqs/messaging/lib/core/AmazonSqsProducerImpl.java index 06eb0cb..3826556 100644 --- a/amazon-sqs-java-messaging-lib-v2/src/main/java/com/amazon/sqs/messaging/lib/core/AmazonSqsProducer.java +++ b/amazon-sqs-java-messaging-lib-v2/src/main/java/com/amazon/sqs/messaging/lib/core/AmazonSqsProducerImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,6 @@ import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.ExecutorService; import com.amazon.sqs.messaging.lib.model.RequestEntry; import com.amazon.sqs.messaging.lib.model.ResponseFailEntry; @@ -30,20 +29,18 @@ * * @param the request entry payload type */ -class AmazonSqsProducer extends AbstractAmazonSqsProducer { +class AmazonSqsProducerImpl extends AbstractAmazonSqsProducer { /** * Constructs a v2 SQS producer. * * @param pendingRequests the map of pending requests * @param queueRequests the blocking queue of incoming requests - * @param executorService the executor service */ - public AmazonSqsProducer( + public AmazonSqsProducerImpl( final ConcurrentMap> pendingRequests, - final BlockingQueue> queueRequests, - final ExecutorService executorService) { - super(pendingRequests, queueRequests, executorService); + final BlockingQueue> queueRequests) { + super(pendingRequests, queueRequests); } } diff --git a/amazon-sqs-java-messaging-lib-v2/src/main/java/com/amazon/sqs/messaging/lib/core/AmazonSqsTemplate.java b/amazon-sqs-java-messaging-lib-v2/src/main/java/com/amazon/sqs/messaging/lib/core/AmazonSqsTemplate.java index d4a17da..1ce7082 100644 --- a/amazon-sqs-java-messaging-lib-v2/src/main/java/com/amazon/sqs/messaging/lib/core/AmazonSqsTemplate.java +++ b/amazon-sqs-java-messaging-lib-v2/src/main/java/com/amazon/sqs/messaging/lib/core/AmazonSqsTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,16 +17,12 @@ package com.amazon.sqs.messaging.lib.core; import java.util.concurrent.BlockingQueue; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; import java.util.function.UnaryOperator; -import com.amazon.sqs.messaging.lib.concurrent.ExecutorsProvider; import com.amazon.sqs.messaging.lib.concurrent.RingBufferBlockingQueue; +import com.amazon.sqs.messaging.lib.metrics.AmazonSqsConsumerMetricsDecorator; import com.amazon.sqs.messaging.lib.model.QueueProperty; import com.amazon.sqs.messaging.lib.model.RequestEntry; -import com.amazon.sqs.messaging.lib.model.ResponseFailEntry; -import com.amazon.sqs.messaging.lib.model.ResponseSuccessEntry; import com.fasterxml.jackson.databind.ObjectMapper; import software.amazon.awssdk.services.sqs.SqsClient; @@ -40,37 +36,53 @@ * * @param the request entry payload type */ -public class AmazonSqsTemplate extends AbstractAmazonSqsTemplate { +public class AmazonSqsTemplate extends AbstractAmazonSqsTemplate { + + private AmazonSqsTemplate(final Builder> builder) { + super( + new AmazonSqsProducerImpl<>( + builder.getPendingRequests(), + builder.getQueueRequests() + ), + new AmazonSqsConsumerMetricsDecorator( + new AmazonSqsConsumerImpl<>( + builder.getAmazonSqsClient(), + builder.getQueueProperty(), + builder.getObjectMapper(), + builder.getPendingRequests(), + builder.getQueueRequests(), + getExecutorService(builder.getQueueProperty(), builder.getMeterRegistry()), + builder.getPublishDecorator() + ), + builder.getQueueProperty(), + builder.getMeterRegistry() + ) + ); + } /** - * Primary constructor that wires the producer and consumer together. + * Creates a new builder for constructing an {@link AmazonSqsTemplate}. * - * @param amazonSqsClient the AWS SDK v2 SQS client - * @param queueProperty the queue configuration properties - * @param pendingRequests the map of pending requests - * @param queueRequests the blocking queue of incoming requests - * @param objectMapper the JSON object mapper - * @param publishDecorator a decorator for batch publish requests + * @param the request entry payload type + * @param amazonSqsClient the v2 {@link SqsClient} client + * @param queueProperty the queue configuration + * @return a new builder instance */ - private AmazonSqsTemplate( + public static Builder> builder( final SqsClient amazonSqsClient, - final QueueProperty queueProperty, - final ConcurrentMap> pendingRequests, - final BlockingQueue> queueRequests, - final ObjectMapper objectMapper, - final UnaryOperator publishDecorator) { - super( - new AmazonSqsProducer<>(pendingRequests, queueRequests, ExecutorsProvider.getExecutorService()), - new AmazonSqsConsumer<>(amazonSqsClient, queueProperty, objectMapper, pendingRequests, queueRequests, getAmazonSqsThreadPoolExecutor(queueProperty), publishDecorator) - ); + final QueueProperty queueProperty) { + return new Builder<>(AmazonSqsTemplate::new, amazonSqsClient, queueProperty); } /** * Creates a template with default object mapper and identity publish decorator. * + * @deprecated since 1.3.0, use {@link #builder(SqsClient, QueueProperty)} instead + * * @param amazonSqsClient the AWS SDK v2 SQS client * @param queueProperty the queue configuration properties */ + @Deprecated public AmazonSqsTemplate(final SqsClient amazonSqsClient, final QueueProperty queueProperty) { this(amazonSqsClient, queueProperty, UnaryOperator.identity()); } @@ -78,10 +90,13 @@ public AmazonSqsTemplate(final SqsClient amazonSqsClient, final QueueProperty qu /** * Creates a template with a custom publish decorator and default object mapper. * + * @deprecated since 1.3.0, use {@link #builder(SqsClient, QueueProperty)} and {@link Builder#publishDecorator(UnaryOperator)} instead + * * @param amazonSqsClient the AWS SDK v2 SQS client * @param queueProperty the queue configuration properties * @param publishDecorator a decorator for batch publish requests */ + @Deprecated public AmazonSqsTemplate(final SqsClient amazonSqsClient, final QueueProperty queueProperty, final UnaryOperator publishDecorator) { this(amazonSqsClient, queueProperty, new ObjectMapper(), publishDecorator); } @@ -90,10 +105,13 @@ public AmazonSqsTemplate(final SqsClient amazonSqsClient, final QueueProperty qu * Creates a template with a custom request queue, default object mapper, * and identity publish decorator. * + * @deprecated since 1.3.0, use {@link #builder(SqsClient, QueueProperty)} and {@link Builder#queueRequests(BlockingQueue)} instead + * * @param amazonSqsClient the AWS SDK v2 SQS client * @param queueProperty the queue configuration properties * @param queueRequests the blocking queue for incoming requests */ + @Deprecated public AmazonSqsTemplate(final SqsClient amazonSqsClient, final QueueProperty queueProperty, final BlockingQueue> queueRequests) { this(amazonSqsClient, queueProperty, queueRequests, UnaryOperator.identity()); } @@ -101,11 +119,14 @@ public AmazonSqsTemplate(final SqsClient amazonSqsClient, final QueueProperty qu /** * Creates a template with a custom request queue and publish decorator. * + * @deprecated since 1.3.0, use {@link #builder(SqsClient, QueueProperty)} with {@link Builder#queueRequests(BlockingQueue)} and {@link Builder#publishDecorator(UnaryOperator)} instead + * * @param amazonSqsClient the AWS SDK v2 SQS client * @param queueProperty the queue configuration properties * @param queueRequests the blocking queue for incoming requests * @param publishDecorator a decorator for batch publish requests */ + @Deprecated public AmazonSqsTemplate(final SqsClient amazonSqsClient, final QueueProperty queueProperty, final BlockingQueue> queueRequests, final UnaryOperator publishDecorator) { this(amazonSqsClient, queueProperty, queueRequests, new ObjectMapper(), publishDecorator); } @@ -113,10 +134,13 @@ public AmazonSqsTemplate(final SqsClient amazonSqsClient, final QueueProperty qu /** * Creates a template with a custom object mapper and identity publish decorator. * + * @deprecated since 1.3.0, use {@link #builder(SqsClient, QueueProperty)} and {@link Builder#objectMapper(ObjectMapper)} instead + * * @param amazonSqsClient the AWS SDK v2 SQS client * @param queueProperty the queue configuration properties * @param objectMapper the JSON object mapper */ + @Deprecated public AmazonSqsTemplate(final SqsClient amazonSqsClient, final QueueProperty queueProperty, final ObjectMapper objectMapper) { this(amazonSqsClient, queueProperty, objectMapper, UnaryOperator.identity()); } @@ -124,11 +148,14 @@ public AmazonSqsTemplate(final SqsClient amazonSqsClient, final QueueProperty qu /** * Creates a template with a custom object mapper and publish decorator. * + * @deprecated since 1.3.0, use {@link #builder(SqsClient, QueueProperty)} with {@link Builder#objectMapper(ObjectMapper)} and {@link Builder#publishDecorator(UnaryOperator)} instead + * * @param amazonSqsClient the AWS SDK v2 SQS client * @param queueProperty the queue configuration properties * @param objectMapper the JSON object mapper * @param publishDecorator a decorator for batch publish requests */ + @Deprecated public AmazonSqsTemplate(final SqsClient amazonSqsClient, final QueueProperty queueProperty, final ObjectMapper objectMapper, final UnaryOperator publishDecorator) { this(amazonSqsClient, queueProperty, new RingBufferBlockingQueue<>(queueProperty.getMaximumPoolSize() * queueProperty.getMaxBatchSize()), objectMapper, publishDecorator); } @@ -136,11 +163,14 @@ public AmazonSqsTemplate(final SqsClient amazonSqsClient, final QueueProperty qu /** * Creates a template with a custom request queue and object mapper. * + * @deprecated since 1.3.0, use {@link #builder(SqsClient, QueueProperty)} with {@link Builder#queueRequests(BlockingQueue)} and {@link Builder#objectMapper(ObjectMapper)} instead + * * @param amazonSqsClient the AWS SDK v2 SQS client * @param queueProperty the queue configuration properties * @param queueRequests the blocking queue for incoming requests * @param objectMapper the JSON object mapper */ + @Deprecated public AmazonSqsTemplate(final SqsClient amazonSqsClient, final QueueProperty queueProperty, final BlockingQueue> queueRequests, final ObjectMapper objectMapper) { this(amazonSqsClient, queueProperty, queueRequests, objectMapper, UnaryOperator.identity()); } @@ -148,14 +178,21 @@ public AmazonSqsTemplate(final SqsClient amazonSqsClient, final QueueProperty qu /** * Creates a template with full custom configuration. * + * @deprecated since 1.3.0, use {@link #builder(SqsClient, QueueProperty)} with builder setters instead + * * @param amazonSqsClient the AWS SDK v2 SQS client * @param queueProperty the queue configuration properties * @param queueRequests the blocking queue for incoming requests * @param objectMapper the JSON object mapper * @param publishDecorator a decorator for batch publish requests */ + @Deprecated public AmazonSqsTemplate(final SqsClient amazonSqsClient, final QueueProperty queueProperty, final BlockingQueue> queueRequests, final ObjectMapper objectMapper, final UnaryOperator publishDecorator) { - this(amazonSqsClient, queueProperty, new ConcurrentHashMap<>(), queueRequests, objectMapper, publishDecorator); + this(AmazonSqsTemplate.builder(amazonSqsClient, queueProperty) + .queueRequests(queueRequests) + .objectMapper(objectMapper) + .publishDecorator(publishDecorator) + ); } } diff --git a/amazon-sqs-java-messaging-lib-v2/src/main/java/com/amazon/sqs/messaging/lib/core/MessageAttributes.java b/amazon-sqs-java-messaging-lib-v2/src/main/java/com/amazon/sqs/messaging/lib/core/MessageAttributes.java index 97f27ef..0b4ccb7 100644 --- a/amazon-sqs-java-messaging-lib-v2/src/main/java/com/amazon/sqs/messaging/lib/core/MessageAttributes.java +++ b/amazon-sqs-java-messaging-lib-v2/src/main/java/com/amazon/sqs/messaging/lib/core/MessageAttributes.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/amazon-sqs-java-messaging-lib-v2/src/main/java/com/amazon/sqs/messaging/lib/metrics/AmazonSqsConsumerMetricsDecorator.java b/amazon-sqs-java-messaging-lib-v2/src/main/java/com/amazon/sqs/messaging/lib/metrics/AmazonSqsConsumerMetricsDecorator.java new file mode 100644 index 0000000..603d681 --- /dev/null +++ b/amazon-sqs-java-messaging-lib-v2/src/main/java/com/amazon/sqs/messaging/lib/metrics/AmazonSqsConsumerMetricsDecorator.java @@ -0,0 +1,109 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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.amazon.sqs.messaging.lib.metrics; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.amazon.sqs.messaging.lib.core.AmazonSqsConsumer; +import com.amazon.sqs.messaging.lib.model.QueueProperty; + +import io.micrometer.core.instrument.MeterRegistry; +import lombok.SneakyThrows; +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.services.sqs.model.SendMessageBatchRequest; +import software.amazon.awssdk.services.sqs.model.SendMessageBatchResponse; + +// @formatter:off +/** + * AWS SDK v2 metrics decorator for {@link AmazonSqsConsumer}. Records publish attempt/success/failure + * counters, latency, batch size, and inflight gauges using Micrometer. Handles + * {@link AwsServiceException} error codes for failure tagging. + */ +public class AmazonSqsConsumerMetricsDecorator extends AbstractAmazonSqsConsumerMetricsDecorator { + + private static final Logger LOGGER = LoggerFactory.getLogger(AmazonSqsConsumerMetricsDecorator.class); + + /** + * Creates a new v2 SQS consumer metrics decorator. + * + * @param delegate the consumer to decorate + * @param queueProperty the queue configuration + * @param meterRegistry the Micrometer meter registry + */ + public AmazonSqsConsumerMetricsDecorator( + final AmazonSqsConsumer delegate, + final QueueProperty queueProperty, + final MeterRegistry meterRegistry) { + super(delegate, queueProperty, meterRegistry); + } + + /** + * {@inheritDoc} + */ + @Override + @SneakyThrows + public SendMessageBatchResponse publish(final SendMessageBatchRequest publishBatchRequest) { + publishAttemptsCounter.increment(); + batchSizeSummary.record(publishBatchRequest.entries().size()); + inflightGauge.incrementAndGet(); + + try { + return publishTimer.recordCallable(() -> delegate.publish(publishBatchRequest)); + } finally { + inflightGauge.decrementAndGet(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void handleResponse(final SendMessageBatchResponse publishBatchResult) { + delegate.handleResponse(publishBatchResult); + + final int successCount = publishBatchResult.successful().size(); + final int failureCount = publishBatchResult.failed().size(); + + if (successCount > 0) { + successCounter.increment(successCount); + } + + publishBatchResult.failed().forEach(entry -> failureCounter(entry.code(), ERROR_TYPE_AMAZON).increment()); + + if (failureCount > 0) { + LOGGER.warn("SNS batch partially failed: {} succeeded, {} failed", successCount, failureCount); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void handleError(final SendMessageBatchRequest publishBatchRequest, final Throwable throwable) { + delegate.handleError(publishBatchRequest, throwable); + + final String errorCode = throwable instanceof AwsServiceException ? AwsServiceException.class.cast(throwable).awsErrorDetails().errorCode() : "000"; + + final String errorType = throwable instanceof AwsServiceException ? ERROR_TYPE_AMAZON : ERROR_TYPE_OTHER; + + final int failedEntries = publishBatchRequest.entries().size(); + + failureCounter(errorCode, errorType).increment(failedEntries); + } + +} diff --git a/amazon-sqs-java-messaging-lib-v2/src/test/java/com/amazon/sqs/messaging/lib/core/AmazonSqsProducerAsyncTest.java b/amazon-sqs-java-messaging-lib-v2/src/test/java/com/amazon/sqs/messaging/lib/core/AmazonSqsProducerAsyncTest.java index 0f01810..989ad04 100644 --- a/amazon-sqs-java-messaging-lib-v2/src/test/java/com/amazon/sqs/messaging/lib/core/AmazonSqsProducerAsyncTest.java +++ b/amazon-sqs-java-messaging-lib-v2/src/test/java/com/amazon/sqs/messaging/lib/core/AmazonSqsProducerAsyncTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/amazon-sqs-java-messaging-lib-v2/src/test/java/com/amazon/sqs/messaging/lib/core/AmazonSqsProducerSyncTest.java b/amazon-sqs-java-messaging-lib-v2/src/test/java/com/amazon/sqs/messaging/lib/core/AmazonSqsProducerSyncTest.java index ac3fa55..29e02bb 100644 --- a/amazon-sqs-java-messaging-lib-v2/src/test/java/com/amazon/sqs/messaging/lib/core/AmazonSqsProducerSyncTest.java +++ b/amazon-sqs-java-messaging-lib-v2/src/test/java/com/amazon/sqs/messaging/lib/core/AmazonSqsProducerSyncTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/amazon-sqs-java-messaging-lib-v2/src/test/java/com/amazon/sqs/messaging/lib/core/AmazonSqsTemplateIntegrationTest.java b/amazon-sqs-java-messaging-lib-v2/src/test/java/com/amazon/sqs/messaging/lib/core/AmazonSqsTemplateIntegrationTest.java index 883d7dc..80fdcf2 100644 --- a/amazon-sqs-java-messaging-lib-v2/src/test/java/com/amazon/sqs/messaging/lib/core/AmazonSqsTemplateIntegrationTest.java +++ b/amazon-sqs-java-messaging-lib-v2/src/test/java/com/amazon/sqs/messaging/lib/core/AmazonSqsTemplateIntegrationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/amazon-sqs-java-messaging-lib-v2/src/test/java/com/amazon/sqs/messaging/lib/core/MessageAttributesTest.java b/amazon-sqs-java-messaging-lib-v2/src/test/java/com/amazon/sqs/messaging/lib/core/MessageAttributesTest.java index 96bf87c..d2165dd 100644 --- a/amazon-sqs-java-messaging-lib-v2/src/test/java/com/amazon/sqs/messaging/lib/core/MessageAttributesTest.java +++ b/amazon-sqs-java-messaging-lib-v2/src/test/java/com/amazon/sqs/messaging/lib/core/MessageAttributesTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/amazon-sqs-java-messaging-lib-v2/src/test/java/com/amazon/sqs/messaging/lib/core/helper/ConsumerHelper.java b/amazon-sqs-java-messaging-lib-v2/src/test/java/com/amazon/sqs/messaging/lib/core/helper/ConsumerHelper.java index deef405..5003f99 100644 --- a/amazon-sqs-java-messaging-lib-v2/src/test/java/com/amazon/sqs/messaging/lib/core/helper/ConsumerHelper.java +++ b/amazon-sqs-java-messaging-lib-v2/src/test/java/com/amazon/sqs/messaging/lib/core/helper/ConsumerHelper.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/amazon-sqs-java-messaging-lib-v2/src/test/resources/logback.xml b/amazon-sqs-java-messaging-lib-v2/src/test/resources/logback.xml index 67bc8f2..d7d66b7 100644 --- a/amazon-sqs-java-messaging-lib-v2/src/test/resources/logback.xml +++ b/amazon-sqs-java-messaging-lib-v2/src/test/resources/logback.xml @@ -10,6 +10,6 @@ - + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 8bd29fb..eeae6c4 100644 --- a/pom.xml +++ b/pom.xml @@ -28,16 +28,17 @@ 4.11.0 5.10.2 2.16.1 - 4.2.2 + 4.3.0 3.24.2 3.20.0 4.5.0 2.0.6 3.0 + 1.16.3 1.20.4 - 3.2.0 + 3.5.0 3.12.1 3.0.0 3.1.0 @@ -108,6 +109,13 @@ amazon-sqs-java-messaging-lib-v2 ${project.version} + + io.micrometer + micrometer-bom + ${micrometer.version} + pom + import + org.testcontainers testcontainers-bom @@ -143,6 +151,11 @@ ${jackson-databind.version} + + io.micrometer + micrometer-core + + org.projectlombok lombok