From 228a2d89c47bb14eeb69c46a2aa2be814db30488 Mon Sep 17 00:00:00 2001 From: Ozan Gunalp Date: Thu, 3 Aug 2023 16:16:22 +0200 Subject: [PATCH 1/5] Message metadata propagation with ack and nack --- api/pom.xml | 25 + api/revapi.json | 116 ++++- .../smallrye/reactive/messaging/Messages.java | 50 +- .../reactive/messaging/Message.java | 476 +++++++++++------- .../messaging/MessagesWithMetadataTest.java | 284 +++++++++++ .../reactive/messaging/AckNackChainTest.java | 168 +++++++ .../CustomMessageAckNackWithMetadataTest.java | 464 +++++++++++++++++ .../reactive/messaging/CustomMessageTest.java | 404 +++++++++++++++ .../MessageSpecCompatibilityTest.java | 53 ++ .../reactive/messaging/MessageTest.java | 4 +- .../reactive/messaging/amqp/AmqpMessage.java | 8 +- .../messaging/amqp/OutgoingAmqpMessage.java | 8 +- .../amqp/ConcurrentProcessorTest.java | 4 +- .../messaging/amqp/ConsumptionBean.java | 2 +- .../messaging/amqp/NullProducingBean.java | 2 +- .../messaging/amqp/ProducingBean.java | 2 +- .../ProducingBeanUsingOutboundMetadata.java | 2 +- .../messaging/camel/CamelMessage.java | 4 +- .../messaging/gcp/pubsub/PubSubMessage.java | 7 +- .../messaging/jms/IncomingJmsMessage.java | 6 +- smallrye-reactive-messaging-kafka/revapi.json | 83 ++- .../messaging/kafka/IncomingKafkaRecord.java | 8 +- .../kafka/IncomingKafkaRecordBatch.java | 10 +- .../messaging/kafka/KafkaConsumer.java | 11 + .../messaging/kafka/OutgoingKafkaRecord.java | 45 +- .../kafka/impl/ReactiveKafkaConsumer.java | 25 + .../kafka/ConcurrentProcessorTest.java | 4 +- .../kafka/ConsumptionBeanUsingRawMessage.java | 2 +- .../messaging/kafka/ProducingBean.java | 2 +- .../kafka/ProducingMessageWithHeaderBean.java | 2 +- .../kafka/health/SinkHealthCheckTest.java | 2 +- smallrye-reactive-messaging-mqtt/revapi.json | 8 +- .../messaging/mqtt/ReceivingMqttMessage.java | 4 +- .../messaging/mqtt/SendingMqttMessage.java | 5 +- .../mqtt/ConcurrentProcessorTest.java | 4 +- .../messaging/mqtt/NullProducingBean.java | 2 +- .../messaging/mqtt/ProducingBean.java | 2 +- .../OutgoingInterceptorDecorator.java | 6 +- .../decorator/AppendingDecorator.java | 2 +- .../AppendingDeprecatedDecorator.java | 2 +- .../messaging/metadata/MessageTest.java | 12 +- .../pulsar/PulsarIncomingBatchMessage.java | 10 +- .../pulsar/PulsarIncomingMessage.java | 14 +- .../pulsar/PulsarOutgoingMessage.java | 21 +- .../pulsar/ConcurrentProcessorTest.java | 4 +- .../pulsar/health/HealthCheckTest.java | 2 +- .../rabbitmq/IncomingRabbitMQMessage.java | 9 +- .../rabbitmq/ConcurrentProcessorTest.java | 4 +- .../messaging/rabbitmq/ConsumptionBean.java | 2 +- .../messaging/rabbitmq/NullProducingBean.java | 2 +- .../messaging/rabbitmq/ProducingBean.java | 2 +- 51 files changed, 2082 insertions(+), 318 deletions(-) create mode 100644 api/src/test/java/io/smallrye/reactive/messaging/MessagesWithMetadataTest.java create mode 100644 api/src/test/java/org/eclipse/microprofile/reactive/messaging/CustomMessageAckNackWithMetadataTest.java create mode 100644 api/src/test/java/org/eclipse/microprofile/reactive/messaging/CustomMessageTest.java create mode 100644 api/src/test/java/org/eclipse/microprofile/reactive/messaging/MessageSpecCompatibilityTest.java diff --git a/api/pom.xml b/api/pom.xml index cec248ab2e..953d4ca660 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -49,6 +49,31 @@ + + + + maven-dependency-plugin + + + process-test-resources + + copy + + + + + org.eclipse.microprofile.reactive.messaging + microprofile-reactive-messaging-api + + + true + ${project.build.directory}/lib + + + + + + coverage diff --git a/api/revapi.json b/api/revapi.json index 7bc3bce7fb..2e311d6478 100644 --- a/api/revapi.json +++ b/api/revapi.json @@ -27,7 +27,119 @@ "criticality" : "highlight", "minSeverity" : "POTENTIALLY_BREAKING", "minCriticality" : "documented", - "differences" : [ ] + "differences" : [ + { + "code": "java.annotation.removed", + "old": "method java.util.function.Function> org.eclipse.microprofile.reactive.messaging.Message::getNack()", + "new": "method java.util.function.Function> org.eclipse.microprofile.reactive.messaging.Message::getNack()", + "annotation": "@io.smallrye.common.annotation.Experimental(\"nack support is a SmallRye-only feature\")", + "justification": "Added Message metadata propagation, nack support no longer a SmallRye-only feature" + + }, + { + "code": "java.annotation.removed", + "old": "method java.util.concurrent.CompletionStage org.eclipse.microprofile.reactive.messaging.Message::nack(java.lang.Throwable)", + "new": "method java.util.concurrent.CompletionStage org.eclipse.microprofile.reactive.messaging.Message::nack(java.lang.Throwable)", + "annotation": "@io.smallrye.common.annotation.Experimental(\"nack support is a SmallRye-only feature\")", + "justification": "Added Message metadata propagation, nack support no longer a SmallRye-only feature" + }, + { + "code": "java.annotation.attributeValueChanged", + "old": "method org.eclipse.microprofile.reactive.messaging.Message org.eclipse.microprofile.reactive.messaging.Message::of(T, org.eclipse.microprofile.reactive.messaging.Metadata, java.util.function.Supplier>, java.util.function.Function>)", + "new": "method org.eclipse.microprofile.reactive.messaging.Message org.eclipse.microprofile.reactive.messaging.Message::of(T, org.eclipse.microprofile.reactive.messaging.Metadata, java.util.function.Supplier>, java.util.function.Function>)", + "annotationType": "io.smallrye.common.annotation.Experimental", + "attribute": "value", + "oldValue": "\"nack support is a SmallRye-only feature\"", + "newValue": "\"metadata propagation is a SmallRye-specific feature\"", + "justification": "Added Message metadata propagation, nack support no longer a SmallRye-only feature" + }, + { + "code": "java.annotation.attributeValueChanged", + "old": "method org.eclipse.microprofile.reactive.messaging.Message org.eclipse.microprofile.reactive.messaging.Message::withNack(java.util.function.Function>)", + "new": "method org.eclipse.microprofile.reactive.messaging.Message org.eclipse.microprofile.reactive.messaging.Message::withNack(java.util.function.Function>)", + "annotationType": "io.smallrye.common.annotation.Experimental", + "attribute": "value", + "oldValue": "\"nack support is a SmallRye-only feature\"", + "newValue": "\"metadata propagation is a SmallRye-specific feature\"", + "justification": "Added Message metadata propagation, nack support no longer a SmallRye-only feature" + }, + { + "ignore": true, + "code": "java.annotation.removed", + "old": "method java.util.function.Function> org.eclipse.microprofile.reactive.messaging.Message::getNack() @ io.smallrye.reactive.messaging.TargetedMessages.Default", + "new": "method java.util.function.Function> org.eclipse.microprofile.reactive.messaging.Message::getNack() @ io.smallrye.reactive.messaging.TargetedMessages.Default", + "annotation": "@io.smallrye.common.annotation.Experimental(\"nack support is a SmallRye-only feature\")", + "justification": "Added Message metadata propagation, nack support no longer a SmallRye-only feature" + }, + { + "ignore": true, + "code": "java.annotation.removed", + "old": "method java.util.concurrent.CompletionStage org.eclipse.microprofile.reactive.messaging.Message::nack(java.lang.Throwable) @ io.smallrye.reactive.messaging.TargetedMessages.Default", + "new": "method java.util.concurrent.CompletionStage org.eclipse.microprofile.reactive.messaging.Message::nack(java.lang.Throwable) @ io.smallrye.reactive.messaging.TargetedMessages.Default", + "annotation": "@io.smallrye.common.annotation.Experimental(\"nack support is a SmallRye-only feature\")", + "justification": "Added Message metadata propagation, nack support no longer a SmallRye-only feature" + }, + { + "ignore": true, + "code": "java.annotation.attributeValueChanged", + "old": "method org.eclipse.microprofile.reactive.messaging.Message org.eclipse.microprofile.reactive.messaging.Message::of(T, org.eclipse.microprofile.reactive.messaging.Metadata, java.util.function.Supplier>, java.util.function.Function>) @ io.smallrye.reactive.messaging.TargetedMessages.Default", + "new": "method org.eclipse.microprofile.reactive.messaging.Message org.eclipse.microprofile.reactive.messaging.Message::of(T, org.eclipse.microprofile.reactive.messaging.Metadata, java.util.function.Supplier>, java.util.function.Function>) @ io.smallrye.reactive.messaging.TargetedMessages.Default", + "annotationType": "io.smallrye.common.annotation.Experimental", + "attribute": "value", + "oldValue": "\"nack support is a SmallRye-only feature\"", + "newValue": "\"metadata propagation is a SmallRye-specific feature\"", + "justification": "Added Message metadata propagation, nack support no longer a SmallRye-only feature" + }, + { + "ignore": true, + "code": "java.annotation.attributeValueChanged", + "old": "method org.eclipse.microprofile.reactive.messaging.Message org.eclipse.microprofile.reactive.messaging.Message::withNack(java.util.function.Function>) @ io.smallrye.reactive.messaging.TargetedMessages.Default", + "new": "method org.eclipse.microprofile.reactive.messaging.Message org.eclipse.microprofile.reactive.messaging.Message::withNack(java.util.function.Function>) @ io.smallrye.reactive.messaging.TargetedMessages.Default", + "annotationType": "io.smallrye.common.annotation.Experimental", + "attribute": "value", + "oldValue": "\"nack support is a SmallRye-only feature\"", + "newValue": "\"metadata propagation is a SmallRye-specific feature\"", + "justification": "Added Message metadata propagation, nack support no longer a SmallRye-only feature" + }, + { + "ignore": true, + "code": "java.annotation.removed", + "old": "method java.util.function.Function> org.eclipse.microprofile.reactive.messaging.Message::getNack() @ io.smallrye.reactive.messaging.TargetedMessages", + "new": "method java.util.function.Function> org.eclipse.microprofile.reactive.messaging.Message::getNack() @ io.smallrye.reactive.messaging.TargetedMessages", + "annotation": "@io.smallrye.common.annotation.Experimental(\"nack support is a SmallRye-only feature\")", + "justification": "Added Message metadata propagation, nack support no longer a SmallRye-only feature" + }, + { + "ignore": true, + "code": "java.annotation.removed", + "old": "method java.util.concurrent.CompletionStage org.eclipse.microprofile.reactive.messaging.Message::nack(java.lang.Throwable) @ io.smallrye.reactive.messaging.TargetedMessages", + "new": "method java.util.concurrent.CompletionStage org.eclipse.microprofile.reactive.messaging.Message::nack(java.lang.Throwable) @ io.smallrye.reactive.messaging.TargetedMessages", + "annotation": "@io.smallrye.common.annotation.Experimental(\"nack support is a SmallRye-only feature\")", + "justification": "Added Message metadata propagation, nack support no longer a SmallRye-only feature" + }, + { + "ignore": true, + "code": "java.annotation.attributeValueChanged", + "old": "method org.eclipse.microprofile.reactive.messaging.Message org.eclipse.microprofile.reactive.messaging.Message::of(T, org.eclipse.microprofile.reactive.messaging.Metadata, java.util.function.Supplier>, java.util.function.Function>) @ io.smallrye.reactive.messaging.TargetedMessages", + "new": "method org.eclipse.microprofile.reactive.messaging.Message org.eclipse.microprofile.reactive.messaging.Message::of(T, org.eclipse.microprofile.reactive.messaging.Metadata, java.util.function.Supplier>, java.util.function.Function>) @ io.smallrye.reactive.messaging.TargetedMessages", + "annotationType": "io.smallrye.common.annotation.Experimental", + "attribute": "value", + "oldValue": "\"nack support is a SmallRye-only feature\"", + "newValue": "\"metadata propagation is a SmallRye-specific feature\"", + "justification": "Added Message metadata propagation, nack support no longer a SmallRye-only feature" + }, + { + "ignore": true, + "code": "java.annotation.attributeValueChanged", + "old": "method org.eclipse.microprofile.reactive.messaging.Message org.eclipse.microprofile.reactive.messaging.Message::withNack(java.util.function.Function>) @ io.smallrye.reactive.messaging.TargetedMessages", + "new": "method org.eclipse.microprofile.reactive.messaging.Message org.eclipse.microprofile.reactive.messaging.Message::withNack(java.util.function.Function>) @ io.smallrye.reactive.messaging.TargetedMessages", + "annotationType": "io.smallrye.common.annotation.Experimental", + "attribute": "value", + "oldValue": "\"nack support is a SmallRye-only feature\"", + "newValue": "\"metadata propagation is a SmallRye-specific feature\"", + "justification": "Added Message metadata propagation, nack support no longer a SmallRye-only feature" + } + ] } }, { "extension" : "revapi.reporter.json", @@ -46,4 +158,4 @@ "minCriticality" : "documented", "output" : "out" } -} ] \ No newline at end of file +} ] diff --git a/api/src/main/java/io/smallrye/reactive/messaging/Messages.java b/api/src/main/java/io/smallrye/reactive/messaging/Messages.java index 4c68141672..aef5d64eed 100644 --- a/api/src/main/java/io/smallrye/reactive/messaging/Messages.java +++ b/api/src/main/java/io/smallrye/reactive/messaging/Messages.java @@ -11,8 +11,8 @@ import java.util.concurrent.CompletionStage; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiFunction; import java.util.function.Function; -import java.util.function.Supplier; import java.util.stream.Collectors; import org.eclipse.microprofile.reactive.messaging.Message; @@ -70,17 +70,17 @@ static Message merge(List> list, Function, T> combinat throw e; } - Supplier> ack = () -> { + Function> ack = metadata -> { List> acks = new ArrayList<>(); for (Message message : list) { - acks.add(message.ack().toCompletableFuture()); + acks.add(message.ack(metadata).toCompletableFuture()); } return CompletableFuture.allOf(acks.toArray(new CompletableFuture[0])); }; - Function> nack = (e) -> { + BiFunction> nack = (metadata, throwable) -> { List> nacks = new ArrayList<>(); for (Message message : list) { - nacks.add(message.nack(e).toCompletableFuture()); + nacks.add(message.nack(metadata, throwable).toCompletableFuture()); } return CompletableFuture.allOf(nacks.toArray(new CompletableFuture[0])); }; @@ -91,8 +91,8 @@ static Message merge(List> list, Function, T> combinat } return Message.of(payload) - .withAck(ack) - .withNack(nack) + .withAckWithMetadata(ack) + .withNackWithMetadata(nack) .withMetadata(metadata); } @@ -115,17 +115,17 @@ static Message> merge(List> list) { return Message.of(Collections.emptyList()); } List payload = list.stream().map(Message::getPayload).collect(Collectors.toList()); - Supplier> ack = () -> { + Function> ack = metadata -> { List> acks = new ArrayList<>(); for (Message message : list) { - acks.add(message.ack().toCompletableFuture()); + acks.add(message.ack(metadata).toCompletableFuture()); } return CompletableFuture.allOf(acks.toArray(new CompletableFuture[0])); }; - Function> nack = (e) -> { + BiFunction> nack = (metadata, throwable) -> { List> nacks = new ArrayList<>(); for (Message message : list) { - nacks.add(message.nack(e).toCompletableFuture()); + nacks.add(message.nack(metadata, throwable).toCompletableFuture()); } return CompletableFuture.allOf(nacks.toArray(new CompletableFuture[0])); }; @@ -136,8 +136,8 @@ static Message> merge(List> list) { } return Message.of(payload) - .withAck(ack) - .withNack(nack) + .withAckWithMetadata(ack) + .withNackWithMetadata(nack) .withMetadata(metadata); } @@ -249,20 +249,20 @@ public List> with(Message... messages) { tmp = tmp.addMetadata(metadatum); } outcomes.add(tmp - .withAck(() -> { - CompletionStage acked = message.ack(); + .withAckWithMetadata((metadata) -> { + CompletionStage acked = message.ack(metadata); if (trackers.remove(message)) { if (trackers.isEmpty() && done.compareAndSet(false, true)) { - return acked.thenCompose(x -> input.ack()); + return acked.thenCompose(x -> input.ack(metadata)); } } return acked; }) - .withNack((reason) -> { - CompletionStage nacked = message.nack(reason); + .withNackWithMetadata((reason, metadata) -> { + CompletionStage nacked = message.nack(reason, metadata); if (trackers.remove(message)) { if (done.compareAndSet(false, true)) { - return nacked.thenCompose(x -> input.nack(reason)); + return nacked.thenCompose(x -> input.nack(reason, metadata)); } } return nacked; @@ -295,20 +295,20 @@ public TargetedMessages with(Map> messages) { tmp = tmp.addMetadata(metadatum); } outcomes.put(key, tmp - .withAck(() -> { - CompletionStage acked = message.ack(); + .withAckWithMetadata(metadata -> { + CompletionStage acked = message.ack(metadata); if (trackers.remove(message)) { if (trackers.isEmpty() && done.compareAndSet(false, true)) { - return acked.thenCompose(x -> input.ack()); + return acked.thenCompose(x -> input.ack(metadata)); } } return acked; }) - .withNack((reason) -> { - CompletionStage nacked = message.nack(reason); + .withNackWithMetadata((reason, metadata) -> { + CompletionStage nacked = message.nack(reason, metadata); if (trackers.remove(message)) { if (done.compareAndSet(false, true)) { - return nacked.thenCompose(x -> input.nack(reason)); + return nacked.thenCompose(x -> input.nack(reason, metadata)); } } return nacked; diff --git a/api/src/main/java/org/eclipse/microprofile/reactive/messaging/Message.java b/api/src/main/java/org/eclipse/microprofile/reactive/messaging/Message.java index f6ab71aabb..17f8c42bce 100644 --- a/api/src/main/java/org/eclipse/microprofile/reactive/messaging/Message.java +++ b/api/src/main/java/org/eclipse/microprofile/reactive/messaging/Message.java @@ -21,6 +21,7 @@ import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; +import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Supplier; import java.util.logging.Logger; @@ -40,6 +41,137 @@ public interface Message { Logger LOGGER = Logger.getLogger(Message.class.getName()); + Function> EMPTY_ACK = m -> CompletableFuture.completedFuture(null); + BiFunction> EMPTY_NACK = (t, m) -> CompletableFuture.completedFuture(null); + + private static Function> validateAck(Function> ackM) { + return (ackM != null) ? ackM : EMPTY_ACK; + } + + private static Function> validateAck(Supplier> ack) { + return (ack != null) ? m -> ack.get() : EMPTY_ACK; + } + + private static BiFunction> validateNack( + BiFunction> nackM) { + return (nackM != null) ? nackM : EMPTY_NACK; + } + + private static BiFunction> validateNack( + Function> nack) { + return (nack != null) ? (t, m) -> nack.apply(t) : EMPTY_NACK; + } + + private static Function> wrapAck(Message message) { + var ackM = message.getAckWithMetadata(); + return ackM != EMPTY_ACK ? ackM : validateAck(message.getAck()); + } + + private static BiFunction> wrapNack(Message message) { + var nackM = message.getNackWithMetadata(); + return nackM != EMPTY_NACK ? nackM : validateNack(message.getNack()); + } + + private static Message newMessage(T payload, Metadata metadata) { + return new Message<>() { + @Override + public T getPayload() { + return payload; + } + + @Override + public Metadata getMetadata() { + return metadata; + } + }; + } + + private static Message newMessage(T payload, + Function> actualAck) { + return new Message<>() { + @Override + public T getPayload() { + return payload; + } + + @Override + public Function> getAckWithMetadata() { + return actualAck; + } + + }; + } + + private static Message newMessage(T payload, + Metadata metadata, + Function> actualAck) { + return new Message<>() { + @Override + public T getPayload() { + return payload; + } + + @Override + public Metadata getMetadata() { + return metadata; + } + + @Override + public Function> getAckWithMetadata() { + return actualAck; + } + + }; + } + + private static Message newMessage(T payload, + Function> actualAck, + BiFunction> actualNack) { + return new Message<>() { + @Override + public T getPayload() { + return payload; + } + + @Override + public Function> getAckWithMetadata() { + return actualAck; + } + + @Override + public BiFunction> getNackWithMetadata() { + return actualNack; + } + }; + } + + private static Message newMessage(T payload, + Metadata metadata, + Function> actualAck, + BiFunction> actualNack) { + return new Message<>() { + @Override + public T getPayload() { + return payload; + } + + @Override + public Metadata getMetadata() { + return metadata; + } + + @Override + public Function> getAckWithMetadata() { + return actualAck; + } + + @Override + public BiFunction> getNackWithMetadata() { + return actualNack; + } + }; + } + /** * Create a message with the given payload. * No metadata are associated with the message, the acknowledgement and negative acknowledgement are immediate. @@ -62,21 +194,7 @@ static Message of(T payload) { * @return A message with the given payload, metadata and no-op ack and nack functions. */ static Message of(T payload, Metadata metadata) { - if (metadata == null) { - metadata = Metadata.empty(); - } - Metadata actual = metadata; - return new Message() { - @Override - public T getPayload() { - return payload; - } - - @Override - public Metadata getMetadata() { - return actual; - } - }; + return newMessage(payload, metadata == null ? Metadata.empty() : metadata); } /** @@ -89,18 +207,7 @@ public Metadata getMetadata() { * @return A message with the given payload, metadata and no-op ack and nack functions. */ static Message of(T payload, Iterable metadata) { - Metadata validated = Metadata.from(metadata); - return new Message() { - @Override - public T getPayload() { - return payload; - } - - @Override - public Metadata getMetadata() { - return validated; - } - }; + return newMessage(payload, Metadata.from(metadata)); } /** @@ -114,22 +221,21 @@ public Metadata getMetadata() { * @return A message with the given payload, no metadata and ack function. */ static Message of(T payload, Supplier> ack) { - return new Message() { - @Override - public T getPayload() { - return payload; - } - - @Override - public Metadata getMetadata() { - return Metadata.empty(); - } + return newMessage(payload, validateAck(ack)); + } - @Override - public Supplier> getAck() { - return ack; - } - }; + /** + * Create a message with the given payload and ack function. + * No metadata are associated with the message. + * Negative-acknowledgement is immediate. + * + * @param payload The payload. + * @param ackM The ack function, this will be invoked when the returned messages {@link #ack(Metadata)} method is invoked. + * @param the type of payload + * @return A message with the given payload, no metadata and ack function. + */ + static Message of(T payload, Function> ackM) { + return newMessage(payload, validateAck(ackM)); } /** @@ -144,26 +250,7 @@ public Supplier> getAck() { */ static Message of(T payload, Metadata metadata, Supplier> ack) { - if (metadata == null) { - metadata = Metadata.empty(); - } - Metadata actual = metadata; - return new Message() { - @Override - public T getPayload() { - return payload; - } - - @Override - public Metadata getMetadata() { - return actual; - } - - @Override - public Supplier> getAck() { - return ack; - } - }; + return newMessage(payload, metadata == null ? Metadata.empty() : metadata, validateAck(ack)); } /** @@ -176,25 +263,22 @@ public Supplier> getAck() { * @param the type of payload * @return A message with the given payload and ack function. */ - static Message of(T payload, Iterable metadata, - Supplier> ack) { - Metadata validated = Metadata.from(metadata); - return new Message() { - @Override - public T getPayload() { - return payload; - } - - @Override - public Metadata getMetadata() { - return validated; - } + static Message of(T payload, Iterable metadata, Supplier> ack) { + return newMessage(payload, Metadata.from(metadata), validateAck(ack)); + } - @Override - public Supplier> getAck() { - return ack; - } - }; + /** + * Create a message with the given payload, metadata and ack function. + * Negative-acknowledgement is immediate. + * + * @param payload The payload. + * @param metadata the metadata, must not be {@code null}, must not contain {@code null} values. + * @param ackM The ack function, this will be invoked when the returned messages {@link #ack(Metadata)} method is invoked. + * @param the type of payload + * @return A message with the given payload and ack function. + */ + static Message of(T payload, Iterable metadata, Function> ackM) { + return newMessage(payload, Metadata.from(metadata), validateAck(ackM)); } /** @@ -208,29 +292,15 @@ public Supplier> getAck() { * @return A message with the given payload, metadata, ack and nack functions. */ @Experimental("nack support is a SmallRye-only feature") - static Message of(T payload, - Supplier> ack, Function> nack) { - return new Message() { - @Override - public T getPayload() { - return payload; - } - - @Override - public Metadata getMetadata() { - return Metadata.empty(); - } - - @Override - public Supplier> getAck() { - return ack; - } + static Message of(T payload, Supplier> ack, + Function> nack) { + return newMessage(payload, validateAck(ack), validateNack(nack)); + } - @Override - public Function> getNack() { - return nack; - } - }; + @Experimental("nack support is a SmallRye-only feature") + static Message of(T payload, Function> ack, + BiFunction> nack) { + return newMessage(payload, validateAck(ack), validateNack(nack)); } /** @@ -247,28 +317,25 @@ public Function> getNack() { @Experimental("nack support is a SmallRye-only feature") static Message of(T payload, Iterable metadata, Supplier> ack, Function> nack) { - Metadata validated = Metadata.from(metadata); - return new Message() { - @Override - public T getPayload() { - return payload; - } - - @Override - public Metadata getMetadata() { - return validated; - } - - @Override - public Supplier> getAck() { - return ack; - } + return newMessage(payload, Metadata.from(metadata), validateAck(ack), validateNack(nack)); + } - @Override - public Function> getNack() { - return nack; - } - }; + /** + * Create a message with the given payload, metadata and ack and nack functions. + * + * @param payload The payload. + * @param metadata the metadata, must not be {@code null}, must not contain {@code null} values. + * @param ackM The ack function, this will be invoked when the returned messages {@link #ack(Metadata)} method is invoked. + * @param nackM The negative-ack function, this will be invoked when the returned messages + * {@link #nack(Throwable, Metadata)} + * method is invoked. + * @param the type of payload + * @return A message with the given payload, metadata, ack and nack functions. + */ + @Experimental("nack support is a SmallRye-only feature") + static Message of(T payload, Iterable metadata, Function> ackM, + BiFunction> nackM) { + return newMessage(payload, Metadata.from(metadata), validateAck(ackM), validateNack(nackM)); } /** @@ -282,34 +349,27 @@ public Function> getNack() { * @param the type of payload * @return A message with the given payload, metadata, ack and nack functions. */ - @Experimental("nack support is a SmallRye-only feature") + @Experimental("metadata propagation is a SmallRye-specific feature") static Message of(T payload, Metadata metadata, Supplier> ack, Function> nack) { - if (metadata == null) { - metadata = Metadata.empty(); - } - Metadata actual = metadata; - return new Message() { - @Override - public T getPayload() { - return payload; - } - - @Override - public Metadata getMetadata() { - return actual; - } - - @Override - public Supplier> getAck() { - return ack; - } + return newMessage(payload, metadata == null ? Metadata.empty() : metadata, validateAck(ack), validateNack(nack)); + } - @Override - public Function> getNack() { - return nack; - } - }; + /** + * Create a message with the given payload, metadata and ack and nack functions. + * + * @param payload The payload. + * @param metadata the metadata, must not be {@code null}, must not contain {@code null} values. + * @param ackM The ack function, this will be invoked when the returned messages {@link #ack()} method is invoked. + * @param nackM The negative-ack function, this will be invoked when the returned messages {@link #nack(Throwable)} + * method is invoked. + * @param the type of payload + * @return A message with the given payload, metadata, ack and nack functions. + */ + @Experimental("metadata propagation is a SmallRye-specific feature") + static Message of(T payload, Metadata metadata, + Function> ackM, BiFunction> nackM) { + return newMessage(payload, metadata == null ? Metadata.empty() : metadata, validateAck(ackM), validateNack(nackM)); } /** @@ -321,18 +381,18 @@ public Function> getNack() { * @return the new instance of {@link Message} */ default

Message

withPayload(P payload) { - return Message.of(payload, Metadata.from(getMetadata()), getAck(), getNack()); + return Message.of(payload, Metadata.from(getMetadata()), wrapAck(this), wrapNack(this)); } /** * Creates a new instance of {@link Message} with the specified metadata. * The payload and ack/nack functions are taken from the current {@link Message}. * - * @param metadata the metadata, must not be {@code null}, must not contains {@code null}. + * @param metadata the metadata, must not be {@code null}, must not contain {@code null}. * @return the new instance of {@link Message} */ default Message withMetadata(Iterable metadata) { - return Message.of(getPayload(), Metadata.from(metadata), getAck(), getNack()); + return Message.of(getPayload(), Metadata.from(metadata), wrapAck(this), wrapNack(this)); } /** @@ -343,7 +403,7 @@ default Message withMetadata(Iterable metadata) { * @return the new instance of {@link Message} */ default Message withMetadata(Metadata metadata) { - return Message.of(getPayload(), Metadata.from(metadata), getAck(), getNack()); + return Message.of(getPayload(), Metadata.from(metadata), wrapAck(this), wrapNack(this)); } /** @@ -354,7 +414,19 @@ default Message withMetadata(Metadata metadata) { * @return the new instance of {@link Message} */ default Message withAck(Supplier> supplier) { - return Message.of(getPayload(), getMetadata(), supplier, getNack()); + return Message.of(getPayload(), getMetadata(), validateAck(supplier), wrapNack(this)); + } + + /** + * Creates a new instance of {@link Message} with the given acknowledgement supplier. + * The payload, metadata and nack function are taken from the current {@link Message}. + * + * @param supplier the acknowledgement supplier + * @return the new instance of {@link Message} + */ + @Experimental("metadata propagation is a SmallRye-specific feature") + default Message withAckWithMetadata(Function> supplier) { + return Message.of(getPayload(), getMetadata(), supplier, wrapNack(this)); } /** @@ -364,9 +436,21 @@ default Message withAck(Supplier> supplier) { * @param nack the negative-acknowledgement function * @return the new instance of {@link Message} */ - @Experimental("nack support is a SmallRye-only feature") + @Experimental("metadata propagation is a SmallRye-specific feature") default Message withNack(Function> nack) { - return Message.of(getPayload(), getMetadata(), getAck(), nack); + return Message.of(getPayload(), getMetadata(), wrapAck(this), validateNack(nack)); + } + + /** + * Creates a new instance of {@link Message} with the given negative-acknowledgement function. + * The payload, metadata and acknowledgment are taken from the current {@link Message}. + * + * @param nack the negative-acknowledgement function + * @return the new instance of {@link Message} + */ + @Experimental("metadata propagation is a SmallRye-specific feature") + default Message withNackWithMetadata(BiFunction> nack) { + return Message.of(getPayload(), getMetadata(), wrapAck(this), nack); } /** @@ -408,15 +492,30 @@ default Optional getMetadata(Class clazz) { * @return the supplier used to retrieve the acknowledgement {@link CompletionStage}. */ default Supplier> getAck() { - return () -> CompletableFuture.completedFuture(null); + return () -> getAckWithMetadata().apply(this.getMetadata()); + } + + /** + * @return the supplier used to retrieve the acknowledgement {@link CompletionStage}. + */ + @Experimental("metadata propagation is a SmallRye-specific feature") + default Function> getAckWithMetadata() { + return EMPTY_ACK; } /** * @return the function used to retrieve the negative-acknowledgement asynchronous function. */ - @Experimental("nack support is a SmallRye-only feature") default Function> getNack() { - return reason -> CompletableFuture.completedFuture(null); + return reason -> getNackWithMetadata().apply(reason, this.getMetadata()); + } + + /** + * @return the function used to retrieve the negative-acknowledgement asynchronous function. + */ + @Experimental("metadata propagation is a SmallRye-specific feature") + default BiFunction> getNackWithMetadata() { + return EMPTY_NACK; } /** @@ -426,11 +525,27 @@ default Function> getNack() { * completion stage propagates the failure. */ default CompletionStage ack() { - Supplier> ack = getAck(); - if (ack == null) { - return CompletableFuture.completedFuture(null); + return ack(this.getMetadata()); + } + + /** + * Acknowledge this message. + * + * @return a completion stage completed when the message is acknowledged. If the acknowledgement fails, the + * completion stage propagates the failure. + */ + @Experimental("metadata propagation is a SmallRye-specific feature") + default CompletionStage ack(Metadata metadata) { + Function> ackM = getAckWithMetadata(); + if (ackM != null) { + return ackM.apply(metadata); } else { - return ack.get(); + Supplier> ack = getAck(); + if (ack != null) { + return ack.get(); + } else { + return CompletableFuture.completedFuture(null); + } } } @@ -442,9 +557,8 @@ default CompletionStage ack() { * @return a completion stage completed when the message is negative-acknowledgement has completed. If the * negative acknowledgement fails, the completion stage propagates the failure. */ - @Experimental("nack support is a SmallRye-only feature") default CompletionStage nack(Throwable reason) { - return nack(reason, null); + return nack(reason, this.getMetadata()); } /** @@ -463,15 +577,20 @@ default CompletionStage nack(Throwable reason, Metadata metadata) { if (reason == null) { throw new IllegalArgumentException("The reason must not be `null`"); } - Function> nack = getNack(); - if (nack == null) { - LOGGER.warning( - String.format("A message has been nacked, but no nack function has been provided. The reason was: %s", - reason.getMessage())); - LOGGER.finer(String.format("The full failure is: %s", reason)); - return CompletableFuture.completedFuture(null); + BiFunction> nackM = getNackWithMetadata(); + if (nackM != null) { + return nackM.apply(reason, metadata); } else { - return nack.apply(reason); + Function> nack = getNack(); + if (nack == null) { + LOGGER.warning( + String.format("A message has been nacked, but no nack function has been provided. The reason was: %s", + reason.getMessage())); + LOGGER.finer(String.format("The full failure is: %s", reason)); + return CompletableFuture.completedFuture(null); + } else { + return nack.apply(reason); + } } } @@ -511,6 +630,17 @@ default C unwrap(Class unwrapType) { * @return the new instance of {@link Message} */ default Message addMetadata(Object metadata) { - return Message.of(getPayload(), getMetadata().with(metadata), getAck(), getNack()); + return Message.of(getPayload(), getMetadata().with(metadata), wrapAck(this), wrapNack(this)); + } + + /** + * Apply the given modifier function to the current message returning the result for further composition + * + * @param modifier the function to modify the current message + * @return the modified message + * @param the payload type of the modified message + */ + default Message thenApply(Function, Message> modifier) { + return modifier.apply(this); } } diff --git a/api/src/test/java/io/smallrye/reactive/messaging/MessagesWithMetadataTest.java b/api/src/test/java/io/smallrye/reactive/messaging/MessagesWithMetadataTest.java new file mode 100644 index 0000000000..1309f8224e --- /dev/null +++ b/api/src/test/java/io/smallrye/reactive/messaging/MessagesWithMetadataTest.java @@ -0,0 +1,284 @@ +package io.smallrye.reactive.messaging; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Metadata; +import org.junit.jupiter.api.Test; + +public class MessagesWithMetadataTest { + + @Test + public void testMergeOfTwoMessagesAndAckWithMetadata() { + AtomicBoolean ackM1 = new AtomicBoolean(); + AtomicBoolean ackM2 = new AtomicBoolean(); + Class metaType = Integer.class; + Message m1 = Message.of("A").withAckWithMetadata(metadata -> { + if (metadata.get(metaType).isPresent()) { + ackM1.set(true); + } + return CompletableFuture.completedFuture(null); + }); + Message m2 = Message.of(1).withAckWithMetadata(metadata -> { + if (metadata.get(metaType).isPresent()) { + ackM2.set(true); + } + return CompletableFuture.completedFuture(null); + }); + + Message merged = Messages.merge(List.of(m1, m2), l -> l.get(0) + "-" + l.get(1)); + + assertThat(merged.getPayload()).isEqualTo("A-1"); + + assertThat(merged.ack(Metadata.of(1))).isCompleted(); + assertThat(ackM1).isTrue(); + assertThat(ackM2).isTrue(); + } + + @Test + public void testMergeOfTwoMessagesOfSameTypeAndAckWithMetadata() { + AtomicBoolean ackM1 = new AtomicBoolean(); + AtomicBoolean ackM2 = new AtomicBoolean(); + Message m1 = Message.of("A").withAckWithMetadata(metadata -> { + if (metadata.get(Integer.class).isPresent()) { + ackM1.set(true); + } + return CompletableFuture.completedFuture(null); + }); + Message m2 = Message.of("B").withAckWithMetadata(metadata -> { + if (metadata.get(Integer.class).isPresent()) { + ackM2.set(true); + } + return CompletableFuture.completedFuture(null); + }); + + Message merged = Messages.merge(List.of(m1, m2), l -> l.get(0) + "-" + l.get(1)); + + assertThat(merged.getPayload()).isEqualTo("A-B"); + + assertThat(merged.addMetadata(1).ack()).isCompleted(); + assertThat(ackM1).isTrue(); + assertThat(ackM2).isTrue(); + } + + @Test + public void testMergeOfTwoMessagesAndNackWithMetadata() { + AtomicBoolean nackM1 = new AtomicBoolean(); + AtomicBoolean nackM2 = new AtomicBoolean(); + Message m1 = Message.of("A").withNackWithMetadata((throwable, metadata) -> { + if (metadata.get(Integer.class).isPresent()) { + nackM1.set(true); + } + return CompletableFuture.completedFuture(null); + }); + Message m2 = Message.of(1).withNackWithMetadata((throwable, metadata) -> { + if (metadata.get(Integer.class).isPresent()) { + nackM2.set(true); + } + return CompletableFuture.completedFuture(null); + }); + + Message merged = Messages.merge(List.of(m1, m2), l -> l.get(0) + "-" + l.get(1)); + + assertThat(merged.getPayload()).isEqualTo("A-1"); + + assertThat(merged.nack(new IOException("boom"), Metadata.of(1))).isCompleted(); + assertThat(nackM1).isTrue(); + assertThat(nackM2).isTrue(); + } + + @Test + void testMergeAsListWithMetadata() { + AtomicBoolean ackM1 = new AtomicBoolean(); + AtomicBoolean ackM2 = new AtomicBoolean(); + AtomicBoolean ackM3 = new AtomicBoolean(); + Message m1 = Message.of("A").withAckWithMetadata(metadata -> { + if (metadata.get(Integer.class).isPresent()) { + ackM1.set(true); + } + return CompletableFuture.completedFuture(null); + }); + Message m2 = Message.of("B").withAckWithMetadata(metadata -> { + if (metadata.get(Integer.class).isPresent()) { + ackM2.set(true); + } + return CompletableFuture.completedFuture(null); + }); + Message m3 = Message.of("C").withAckWithMetadata(metadata -> { + if (metadata.get(Integer.class).isPresent()) { + ackM3.set(true); + } + return CompletableFuture.completedFuture(null); + }); + + Message> result = Messages.merge(List.of(m1, m2, m3)); + assertThat(result.getPayload()).containsExactly("A", "B", "C"); + + assertThat(result.ack(Metadata.of(1))).isCompleted(); + assertThat(ackM1).isTrue(); + assertThat(ackM2).isTrue(); + assertThat(ackM3).isTrue(); + } + + @Test + void testMergeAsListAndNackWithMetadata() { + AtomicBoolean nackM1 = new AtomicBoolean(); + AtomicBoolean nackM2 = new AtomicBoolean(); + AtomicBoolean nackM3 = new AtomicBoolean(); + Message m1 = Message.of("A").withNackWithMetadata((throwable, metadata) -> { + if (metadata.get(Integer.class).isPresent()) { + nackM1.set(true); + } + return CompletableFuture.completedFuture(null); + }); + Message m2 = Message.of("B").withNackWithMetadata((throwable, metadata) -> { + if (metadata.get(Integer.class).isPresent()) { + nackM2.set(true); + } + return CompletableFuture.completedFuture(null); + }); + Message m3 = Message.of("C").withNackWithMetadata((throwable, metadata) -> { + if (metadata.get(Integer.class).isPresent()) { + nackM3.set(true); + } + return CompletableFuture.completedFuture(null); + }); + + Message> result = Messages.merge(List.of(m1, m2, m3)); + assertThat(result.getPayload()).containsExactly("A", "B", "C"); + + assertThat(result.nack(new IOException("boom"), Metadata.of(1))).isCompleted(); + assertThat(nackM1).isTrue(); + assertThat(nackM2).isTrue(); + assertThat(nackM3).isTrue(); + } + + @Test + void checkSimpleChainAcknowledgementWithMetadata() { + AtomicBoolean o1Ack = new AtomicBoolean(); + AtomicBoolean o2Ack = new AtomicBoolean(); + AtomicInteger i1Ack = new AtomicInteger(); + Message o1 = Message.of("foo", metadata -> { + if (metadata.get(Integer.class).isPresent()) { + o1Ack.set(true); + } + return CompletableFuture.completedFuture(null); + }); + Message o2 = Message.of("bar", metadata -> { + if (metadata.get(Integer.class).isPresent()) { + o2Ack.set(true); + } + return CompletableFuture.completedFuture(null); + }); + + Message i = Message.of(1, metadata -> { + if (metadata.get(Integer.class).isPresent()) { + i1Ack.incrementAndGet(); + } + return CompletableFuture.completedFuture(null); + }); + + List> outcomes = Messages.chain(i).with(o1, o2); + assertThat(i1Ack).hasValue(0); + assertThat(o1Ack).isFalse(); + assertThat(o2Ack).isFalse(); + + outcomes.get(0).ack(Metadata.of(1)); + assertThat(i1Ack).hasValue(0); + assertThat(o1Ack).isTrue(); + assertThat(o2Ack).isFalse(); + + outcomes.get(1).ack(Metadata.of(1)); + assertThat(i1Ack).hasValue(1); + assertThat(o1Ack).isTrue(); + assertThat(o1Ack).isTrue(); + + outcomes.get(1).ack(Metadata.of(2)); + outcomes.get(0).ack(Metadata.of(2)); + assertThat(i1Ack).hasValue(1); + + outcomes.get(1).nack(new Exception("boom")); + outcomes.get(0).nack(new Exception("boom")); + assertThat(i1Ack).hasValue(1); + } + + @Test + void checkSimpleChainNegativeAcknowledgementWithMetadata() { + AtomicBoolean o1Ack = new AtomicBoolean(); + AtomicBoolean o2Ack = new AtomicBoolean(); + AtomicBoolean o1Nack = new AtomicBoolean(); + AtomicBoolean o2Nack = new AtomicBoolean(); + AtomicInteger i1Ack = new AtomicInteger(); + AtomicInteger i1Nack = new AtomicInteger(); + Message o1 = Message.of("foo", metadata -> { + if (metadata.get(Integer.class).isPresent()) { + o1Ack.set(true); + } + return CompletableFuture.completedFuture(null); + }, (throwable, metadata) -> { + if (metadata.get(Integer.class).isPresent()) { + o1Nack.set(true); + } + return CompletableFuture.completedFuture(null); + }); + Message o2 = Message.of("bar", metadata -> { + if (metadata.get(Integer.class).isPresent()) { + o2Ack.set(true); + } + return CompletableFuture.completedFuture(null); + }, (throwable, metadata) -> { + if (metadata.get(Integer.class).isPresent()) { + o2Nack.set(true); + } + return CompletableFuture.completedFuture(null); + }); + + Message i = Message.of(1, metadata -> { + if (metadata.get(Integer.class).isPresent()) { + i1Ack.incrementAndGet(); + } + return CompletableFuture.completedFuture(null); + }, (throwable, metadata) -> { + if (metadata.get(Integer.class).isPresent()) { + i1Nack.incrementAndGet(); + } + return CompletableFuture.completedFuture(null); + }); + + List> outcomes = Messages.chain(i).with(o1, o2); + assertThat(i1Ack).hasValue(0); + assertThat(o1Ack).isFalse(); + assertThat(o2Ack).isFalse(); + assertThat(i1Nack).hasValue(0); + assertThat(o1Nack).isFalse(); + assertThat(o2Nack).isFalse(); + + outcomes.get(0).ack(Metadata.of(1)); + assertThat(i1Ack).hasValue(0); + assertThat(o1Ack).isTrue(); + assertThat(o2Ack).isFalse(); + assertThat(i1Nack).hasValue(0); + assertThat(o1Nack).isFalse(); + assertThat(o2Nack).isFalse(); + + outcomes.get(0).nack(new Exception("boom"), Metadata.of(1)); + assertThat(i1Ack).hasValue(0); + assertThat(i1Nack).hasValue(0); + + outcomes.get(1).nack(new Exception("boom"), Metadata.of(1)); + assertThat(i1Nack).hasValue(1); + assertThat(i1Ack).hasValue(0); + assertThat(o2Nack).isTrue(); + + outcomes.get(1).ack(Metadata.of(1)); + assertThat(i1Nack).hasValue(1); + assertThat(i1Ack).hasValue(0); + } + +} diff --git a/api/src/test/java/org/eclipse/microprofile/reactive/messaging/AckNackChainTest.java b/api/src/test/java/org/eclipse/microprofile/reactive/messaging/AckNackChainTest.java index 32a3df8681..fcaed784af 100644 --- a/api/src/test/java/org/eclipse/microprofile/reactive/messaging/AckNackChainTest.java +++ b/api/src/test/java/org/eclipse/microprofile/reactive/messaging/AckNackChainTest.java @@ -5,6 +5,9 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; import org.junit.jupiter.api.Test; @@ -32,6 +35,74 @@ public void testAckChain() { assertThat(m1Ack).hasValue(1); } + @Test + public void testAckChainWithCustomMessageImplementingAck() { + AtomicInteger m1Ack = new AtomicInteger(); + AtomicInteger m2Ack = new AtomicInteger(); + AtomicInteger m3Ack = new AtomicInteger(); + Message m1 = new Message<>() { + @Override + public String getPayload() { + return "1"; + } + + @Override + public Supplier> getAck() { + return this::ack; + } + + @Override + public CompletionStage ack() { + m1Ack.incrementAndGet(); + return CompletableFuture.completedFuture(null); + } + }; + + Message m2 = Message.of("2", () -> m1.getAck().get().thenAccept(x -> m2Ack.incrementAndGet())); + + Message m3 = Message.of("3", () -> m2.ack().thenAccept(x -> m3Ack.incrementAndGet())); + + CompletionStage acked = m3.ack(); + acked.toCompletableFuture().join(); + + assertThat(m3Ack).hasValue(1); + assertThat(m2Ack).hasValue(1); + assertThat(m1Ack).hasValue(1); + } + + @Test + public void testAckChainWithCustomMessageImplementingAckWithMetadata() { + AtomicInteger m1Ack = new AtomicInteger(); + AtomicInteger m2Ack = new AtomicInteger(); + AtomicInteger m3Ack = new AtomicInteger(); + Message m1 = new Message<>() { + @Override + public String getPayload() { + return "1"; + } + + @Override + public Function> getAckWithMetadata() { + return metadata -> { + m1Ack.incrementAndGet(); + return CompletableFuture.completedFuture(null); + }; + } + + }; + + Message m2 = Message.of("2", () -> m1.getAck().get().thenAccept(x -> m2Ack.incrementAndGet())); + + Message m3 = Message.of("3", () -> m2.ack().thenAccept(x -> m3Ack.incrementAndGet())); + + CompletionStage acked = m3.ack(); + acked.toCompletableFuture().join(); + + assertThat(m3Ack).hasValue(1); + assertThat(m2Ack).hasValue(1); + assertThat(m1Ack).hasValue(1); + } + @Test public void testNackChain() { AtomicInteger m1Nack = new AtomicInteger(); @@ -67,4 +138,101 @@ public void testNackChain() { assertThat(m1Nack).hasValue(1); } + @Test + public void testNackChainWithCustomMessage() { + AtomicInteger m1Nack = new AtomicInteger(); + AtomicInteger m2Nack = new AtomicInteger(); + AtomicInteger m3Nack = new AtomicInteger(); + + Message m1 = new Message<>() { + @Override + public String getPayload() { + return "1"; + } + + @Override + public Supplier> getAck() { + return () -> CompletableFuture.completedFuture(null); + } + + @Override + public Function> getNack() { + return cause -> { + assertThat(cause).isNotNull(); + m1Nack.incrementAndGet(); + return CompletableFuture.completedFuture(null); + }; + } + }; + + Message m2 = Message.of("2", Metadata.empty(), + () -> CompletableFuture.completedFuture(null), + cause -> { + assertThat(cause).isNotNull(); + return m1.getNack().apply(cause).thenAccept(x -> m2Nack.incrementAndGet()); + }); + + Message m3 = Message.of("3", Metadata.empty(), + () -> CompletableFuture.completedFuture(null), + cause -> { + assertThat(cause).isNotNull(); + return m2.nack(cause).thenAccept(x -> m3Nack.incrementAndGet()); + }); + + CompletionStage nacked = m3.nack(new Exception("boom")); + nacked.toCompletableFuture().join(); + + assertThat(m3Nack).hasValue(1); + assertThat(m2Nack).hasValue(1); + assertThat(m1Nack).hasValue(1); + } + + @Test + public void testNackChainWithCustomMessageImplementingAckWithMetadata() { + AtomicInteger m1Nack = new AtomicInteger(); + AtomicInteger m2Nack = new AtomicInteger(); + AtomicInteger m3Nack = new AtomicInteger(); + Message m1 = new Message<>() { + @Override + public String getPayload() { + return "1"; + } + + @Override + public Function> getAckWithMetadata() { + return metadata -> CompletableFuture.completedFuture(null); + } + + @Override + public BiFunction> getNackWithMetadata() { + return (cause, metadata) -> { + assertThat(cause).isNotNull(); + m1Nack.incrementAndGet(); + return CompletableFuture.completedFuture(null); + }; + } + }; + + Message m2 = Message.of("2", Metadata.empty(), + () -> CompletableFuture.completedFuture(null), + cause -> { + assertThat(cause).isNotNull(); + return m1.getNack().apply(cause).thenAccept(x -> m2Nack.incrementAndGet()); + }); + + Message m3 = Message.of("3", Metadata.empty(), + () -> CompletableFuture.completedFuture(null), + cause -> { + assertThat(cause).isNotNull(); + return m2.nack(cause).thenAccept(x -> m3Nack.incrementAndGet()); + }); + + CompletionStage nacked = m3.nack(new Exception("boom")); + nacked.toCompletableFuture().join(); + + assertThat(m3Nack).hasValue(1); + assertThat(m2Nack).hasValue(1); + assertThat(m1Nack).hasValue(1); + } + } diff --git a/api/src/test/java/org/eclipse/microprofile/reactive/messaging/CustomMessageAckNackWithMetadataTest.java b/api/src/test/java/org/eclipse/microprofile/reactive/messaging/CustomMessageAckNackWithMetadataTest.java new file mode 100644 index 0000000000..cfa49ebbcc --- /dev/null +++ b/api/src/test/java/org/eclipse/microprofile/reactive/messaging/CustomMessageAckNackWithMetadataTest.java @@ -0,0 +1,464 @@ +package org.eclipse.microprofile.reactive.messaging; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiFunction; +import java.util.function.Function; + +import org.junit.jupiter.api.Test; + +public class CustomMessageAckNackWithMetadataTest { + + private final MyMetadata myMetadata = new MyMetadata("bar"); + + @Test + public void testCreationFromPayloadOnly() { + Message message = () -> "foo"; + assertThat(message.getPayload()).isEqualTo("foo"); + assertThat(message.getMetadata()).isEmpty(); + assertThat(message.getAck()).isNotNull(); + assertThat(message.getNack()).isNotNull(); + assertThat(message.getAckWithMetadata()).isNotNull(); + assertThat(message.getNackWithMetadata()).isNotNull(); + + assertThat(message.ack().toCompletableFuture().join()).isNull(); + assertThat(message.ack(Metadata.of(myMetadata)).toCompletableFuture().join()).isNull(); + assertThat(message.nack(new Exception("cause")).toCompletableFuture().join()).isNull(); + assertThat(message.nack(new Exception("cause"), Metadata.of(myMetadata)).toCompletableFuture().join()).isNull(); + } + + @Test + public void testCreationFromPayloadAndMetadataOnly() { + Message message = new Message<>() { + @Override + public String getPayload() { + return "foo"; + } + + @Override + public Metadata getMetadata() { + return Metadata.of(myMetadata); + } + }; + assertThat(message.getPayload()).isEqualTo("foo"); + assertThat(message.getMetadata()).hasSize(1).containsExactly(myMetadata); + assertThat(message.getAck()).isNotNull(); + assertThat(message.getNack()).isNotNull(); + assertThat(message.getAckWithMetadata()).isNotNull(); + assertThat(message.getNackWithMetadata()).isNotNull(); + + assertThat(message.ack().toCompletableFuture().join()).isNull(); + assertThat(message.ack(Metadata.of(myMetadata)).toCompletableFuture().join()).isNull(); + assertThat(message.nack(new Exception("cause")).toCompletableFuture().join()).isNull(); + assertThat(message.nack(new Exception("cause"), Metadata.of(myMetadata)).toCompletableFuture().join()).isNull(); + + message = new Message<>() { + @Override + public String getPayload() { + return "foo"; + } + + @Override + public Metadata getMetadata() { + return Metadata.empty(); + } + }; + assertThat(message.getPayload()).isEqualTo("foo"); + assertThat(message.getMetadata()).isEmpty(); + assertThat(message.getAck()).isNotNull(); + assertThat(message.getNack()).isNotNull(); + + } + + @Test + public void testCreationFromPayloadAndMetadataAsIterable() { + List metadata = Arrays.asList(myMetadata, new AtomicInteger(2)); + Message message = new Message<>() { + @Override + public String getPayload() { + return "foo"; + } + + @Override + public Metadata getMetadata() { + return Metadata.from(metadata); + } + }; + assertThat(message.getPayload()).isEqualTo("foo"); + assertThat(message.getMetadata()).hasSize(2); + assertThat(message.getAck()).isNotNull(); + assertThat(message.getNack()).isNotNull(); + assertThat(message.getAckWithMetadata()).isNotNull(); + assertThat(message.getNackWithMetadata()).isNotNull(); + + assertThat(message.ack().toCompletableFuture().join()).isNull(); + assertThat(message.ack(Metadata.from(metadata)).toCompletableFuture().join()).isNull(); + assertThat(message.nack(new Exception("cause")).toCompletableFuture().join()).isNull(); + assertThat(message.nack(new Exception("cause"), Metadata.from(metadata)).toCompletableFuture().join()).isNull(); + + assertThatThrownBy(() -> Message.of("x", (Iterable) null)).isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void testCreationFromPayloadAndAck() { + AtomicInteger count = new AtomicInteger(0); + Message message = new Message<>() { + @Override + public String getPayload() { + return "foo"; + } + + @Override + public Function> getAckWithMetadata() { + return this::ack; + } + + @Override + public CompletionStage ack(Metadata metadata) { + count.incrementAndGet(); + return CompletableFuture.completedFuture(null); + } + + }; + assertThat(message.getPayload()).isEqualTo("foo"); + assertThat(message.getMetadata()).hasSize(0); + assertThat(message.getAck()).isNotNull(); + assertThat(message.getNack()).isNotNull(); + assertThat(message.getAckWithMetadata()).isNotNull(); + assertThat(message.getNackWithMetadata()).isNotNull(); + + assertThat(message.ack().toCompletableFuture().join()).isNull(); + assertThat(message.ack(Metadata.empty()).toCompletableFuture().join()).isNull(); + assertThat(message.nack(new Exception("cause")).toCompletableFuture().join()).isNull(); + assertThat(message.nack(new Exception("cause"), Metadata.empty()).toCompletableFuture().join()).isNull(); + + assertThat(count).hasValue(2); + + } + + @Test + public void testCreationFromPayloadMetadataAndAck() { + AtomicInteger count = new AtomicInteger(0); + Message message = new Message<>() { + @Override + public String getPayload() { + return "foo"; + } + + @Override + public Metadata getMetadata() { + return Metadata.of(myMetadata); + } + + @Override + public Function> getAckWithMetadata() { + return this::ack; + } + + @Override + public CompletionStage ack(Metadata metadata) { + count.incrementAndGet(); + return CompletableFuture.completedFuture(null); + } + + }; + assertThat(message.getPayload()).isEqualTo("foo"); + assertThat(message.getMetadata()).hasSize(1).containsExactly(myMetadata); + assertThat(message.getAck()).isNotNull(); + assertThat(message.getNack()).isNotNull(); + assertThat(message.getAckWithMetadata()).isNotNull(); + assertThat(message.getNackWithMetadata()).isNotNull(); + + assertThat(message.ack().toCompletableFuture().join()).isNull(); + assertThat(message.ack(Metadata.empty()).toCompletableFuture().join()).isNull(); + assertThat(message.nack(new Exception("cause")).toCompletableFuture().join()).isNull(); + assertThat(count).hasValue(2); + + assertThat(Message.of("foo", null, () -> CompletableFuture.completedFuture(null)).getMetadata()) + .isEmpty(); + } + + @Test + public void testCreationFromPayloadMetadataAsIterableAndAck() { + List metadata = Arrays.asList(myMetadata, new AtomicInteger(2)); + AtomicInteger count = new AtomicInteger(0); + Message message = new Message<>() { + @Override + public String getPayload() { + return "foo"; + } + + @Override + public Metadata getMetadata() { + return Metadata.from(metadata); + } + + @Override + public Function> getAckWithMetadata() { + return this::ack; + } + + @Override + public CompletionStage ack(Metadata metadata) { + count.incrementAndGet(); + return CompletableFuture.completedFuture(null); + } + + }; + + assertThat(message.getPayload()).isEqualTo("foo"); + assertThat(message.getMetadata()).hasSize(2).contains(myMetadata); + assertThat(message.getAck()).isNotNull(); + assertThat(message.getNack()).isNotNull(); + assertThat(message.getAckWithMetadata()).isNotNull(); + assertThat(message.getNackWithMetadata()).isNotNull(); + + assertThat(message.ack().toCompletableFuture().join()).isNull(); + assertThat(message.ack(Metadata.from(metadata)).toCompletableFuture().join()).isNull(); + assertThat(message.nack(new Exception("cause")).toCompletableFuture().join()).isNull(); + assertThat(message.nack(new Exception("cause"), Metadata.from(metadata)).toCompletableFuture().join()).isNull(); + assertThat(count).hasValue(2); + + assertThatThrownBy(() -> Message.of("foo", (Iterable) null, () -> CompletableFuture.completedFuture(null))) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void testCreationFromPayloadMetadataAckAndNack() { + AtomicInteger ack = new AtomicInteger(0); + AtomicInteger nack = new AtomicInteger(0); + Message message = new CustomMessage<>("foo", Metadata.of(myMetadata), ack, nack); + + assertThat(message.getPayload()).isEqualTo("foo"); + assertThat(message.getMetadata()).hasSize(1).containsExactly(myMetadata); + assertThat(message.getAck()).isNotNull(); + assertThat(message.getNack()).isNotNull(); + + assertThat(message.ack().toCompletableFuture().join()).isNull(); + assertThat(message.ack(Metadata.of(myMetadata)).toCompletableFuture().join()).isNull(); + assertThat(message.nack(new Exception("cause")).toCompletableFuture().join()).isNull(); + assertThat(message.nack(new Exception("cause"), Metadata.of(myMetadata)).toCompletableFuture().join()).isNull(); + assertThat(ack).hasValue(2); + assertThat(nack).hasValue(2); + } + + @Test + public void testWithPayload() { + AtomicInteger ack = new AtomicInteger(0); + AtomicInteger nack = new AtomicInteger(0); + Message message = new CustomMessage<>("foo", Metadata.of(myMetadata), ack, nack); + + Message created = message.withPayload("bar"); + assertThat(created.getPayload()).isEqualTo("bar"); + assertThat(created.getMetadata()).hasSize(1).containsExactly(myMetadata); + assertThat(created.getAck()).isNotNull(); + assertThat(created.getNack()).isNotNull(); + assertThat(message.getAckWithMetadata()).isNotNull(); + assertThat(message.getNackWithMetadata()).isNotNull(); + + assertThat(created.ack().toCompletableFuture().join()).isNull(); + assertThat(created.ack(Metadata.of(myMetadata)).toCompletableFuture().join()).isNull(); + assertThat(created.nack(new Exception("cause")).toCompletableFuture().join()).isNull(); + assertThat(created.nack(new Exception("cause"), Metadata.of(myMetadata)).toCompletableFuture().join()).isNull(); + assertThat(ack).hasValue(2); + assertThat(nack).hasValue(2); + + } + + @Test + public void testWithMetadata() { + AtomicInteger ack = new AtomicInteger(0); + AtomicInteger nack = new AtomicInteger(0); + Message message = new CustomMessage<>("foo", Metadata.of(myMetadata), ack, nack); + MyMetadata mm = new MyMetadata("hello"); + Message created = message.withMetadata(Metadata.of(mm)); + assertThat(created.getPayload()).isEqualTo("foo"); + assertThat(created.getMetadata()).hasSize(1).containsExactly(mm); + assertThat(created.getAck()).isNotNull(); + assertThat(created.getNack()).isNotNull(); + assertThat(message.getAckWithMetadata()).isNotNull(); + assertThat(message.getNackWithMetadata()).isNotNull(); + + assertThat(created.ack().toCompletableFuture().join()).isNull(); + assertThat(created.ack(Metadata.of(myMetadata)).toCompletableFuture().join()).isNull(); + assertThat(created.nack(new Exception("cause")).toCompletableFuture().join()).isNull(); + assertThat(created.nack(new Exception("cause"), Metadata.of(myMetadata)).toCompletableFuture().join()).isNull(); + assertThat(ack).hasValue(2); + assertThat(nack).hasValue(2); + + } + + @Test + public void testWithAck() { + AtomicInteger ack = new AtomicInteger(0); + AtomicInteger ack2 = new AtomicInteger(0); + AtomicInteger nack = new AtomicInteger(0); + Message message = new CustomMessage<>("foo", Metadata.of(myMetadata), ack, nack); + Message created = message.withAck(() -> { + ack2.incrementAndGet(); + return CompletableFuture.completedFuture(null); + }); + assertThat(created.getPayload()).isEqualTo("foo"); + assertThat(created.getMetadata()).hasSize(1).containsExactly(myMetadata); + assertThat(created.getAck()).isNotNull(); + assertThat(created.getNack()).isNotNull(); + assertThat(message.getAckWithMetadata()).isNotNull(); + assertThat(message.getNackWithMetadata()).isNotNull(); + + assertThat(created.ack().toCompletableFuture().join()).isNull(); + assertThat(created.ack(Metadata.of(myMetadata)).toCompletableFuture().join()).isNull(); + assertThat(created.nack(new Exception("cause")).toCompletableFuture().join()).isNull(); + assertThat(created.nack(new Exception("cause"), Metadata.of(myMetadata)).toCompletableFuture().join()).isNull(); + assertThat(ack2).hasValue(2); + assertThat(ack).hasValue(0); + assertThat(nack).hasValue(2); + + } + + @Test + public void testWithNack() { + AtomicInteger ack = new AtomicInteger(0); + AtomicInteger nack = new AtomicInteger(0); + AtomicInteger nack2 = new AtomicInteger(0); + Message message = new CustomMessage<>("foo", Metadata.of(myMetadata), ack, nack); + + Message created = message.withNack(t -> { + assertThat(t).hasMessage("cause"); + nack2.incrementAndGet(); + return CompletableFuture.completedFuture(null); + }); + assertThat(created.getPayload()).isEqualTo("foo"); + assertThat(created.getMetadata()).hasSize(1).containsExactly(myMetadata); + assertThat(created.getAck()).isNotNull(); + assertThat(created.getNack()).isNotNull(); + assertThat(message.getAckWithMetadata()).isNotNull(); + assertThat(message.getNackWithMetadata()).isNotNull(); + + assertThat(created.ack().toCompletableFuture().join()).isNull(); + assertThat(created.ack(Metadata.of(myMetadata)).toCompletableFuture().join()).isNull(); + assertThat(created.nack(new Exception("cause")).toCompletableFuture().join()).isNull(); + assertThat(created.nack(new Exception("cause"), Metadata.of(myMetadata)).toCompletableFuture().join()).isNull(); + assertThat(ack).hasValue(2); + assertThat(nack2).hasValue(2); + assertThat(nack).hasValue(0); + } + + @Test + public void testAddMetadata() { + AtomicInteger ack = new AtomicInteger(0); + AtomicInteger nack = new AtomicInteger(0); + Message message = new CustomMessage<>("foo", Metadata.of(myMetadata), ack, nack); + Message created = message.addMetadata(new AtomicInteger(2)); + assertThat(created.getPayload()).isEqualTo("foo"); + assertThat(created.getMetadata()).hasSize(2).contains(myMetadata); + assertThat(created.getAck()).isNotNull(); + assertThat(created.getNack()).isNotNull(); + assertThat(message.getAckWithMetadata()).isNotNull(); + assertThat(message.getNackWithMetadata()).isNotNull(); + + assertThat(created.ack().toCompletableFuture().join()).isNull(); + assertThat(created.ack(Metadata.of(myMetadata)).toCompletableFuture().join()).isNull(); + assertThat(created.nack(new Exception("cause")).toCompletableFuture().join()).isNull(); + assertThat(created.nack(new Exception("cause"), Metadata.of(myMetadata)).toCompletableFuture().join()).isNull(); + assertThat(ack).hasValue(2); + assertThat(nack).hasValue(2); + } + + @Test + public void testAckAndNackNull() { + AtomicInteger ack = new AtomicInteger(0); + AtomicInteger nack = new AtomicInteger(0); + Message message = new CustomMessage<>("foo", Metadata.of(myMetadata), ack, nack); + Message created = message.withAck(null).withNack(null); + assertThat(created.getPayload()).isEqualTo("foo"); + assertThat(created.getMetadata()).hasSize(1).contains(myMetadata); + assertThat(created.getAck()).isNotNull(); + assertThat(created.getNack()).isNotNull(); + assertThat(message.getAckWithMetadata()).isNotNull(); + assertThat(message.getNackWithMetadata()).isNotNull(); + + assertThat(created.ack().toCompletableFuture().join()).isNull(); + assertThat(created.ack(Metadata.of(myMetadata)).toCompletableFuture().join()).isNull(); + assertThat(created.nack(new Exception("cause")).toCompletableFuture().join()).isNull(); + assertThat(created.nack(new Exception("cause"), Metadata.of(myMetadata)).toCompletableFuture().join()).isNull(); + assertThat(ack).hasValue(0); + assertThat(nack).hasValue(0); + } + + @Test + public void testAccessingMetadata() { + Message message = Message.of("hello", Metadata.of(myMetadata)).addMetadata(new AtomicInteger(2)); + + assertThat(message.getMetadata(MyMetadata.class)) + .hasValueSatisfying(m -> assertThat(m.getValue()).isEqualTo("bar")); + assertThat(message.getMetadata(AtomicInteger.class)).hasValueSatisfying(m -> assertThat(m.get()).isEqualTo(2)); + assertThat(message.getMetadata(String.class)).isEmpty(); + assertThatThrownBy(() -> message.getMetadata(null)).isInstanceOf(IllegalArgumentException.class); + } + + private static class MyMetadata { + private final String value; + + public MyMetadata(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } + + private static class CustomMessage implements Message { + + T payload; + Metadata metadata; + AtomicInteger ack; + AtomicInteger nack; + + public CustomMessage(T payload, Metadata metadata, AtomicInteger ack, AtomicInteger nack) { + this.payload = payload; + this.metadata = metadata; + this.ack = ack; + this.nack = nack; + } + + @Override + public T getPayload() { + return payload; + } + + @Override + public Metadata getMetadata() { + return metadata; + } + + @Override + public Function> getAckWithMetadata() { + return this::ack; + } + + @Override + public BiFunction> getNackWithMetadata() { + return this::nack; + } + + @Override + public CompletionStage ack(Metadata metadata) { + ack.incrementAndGet(); + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletionStage nack(Throwable reason, Metadata metadata) { + assertThat(reason).hasMessage("cause"); + nack.incrementAndGet(); + return CompletableFuture.completedFuture(null); + } + } +} diff --git a/api/src/test/java/org/eclipse/microprofile/reactive/messaging/CustomMessageTest.java b/api/src/test/java/org/eclipse/microprofile/reactive/messaging/CustomMessageTest.java new file mode 100644 index 0000000000..9be217d4b7 --- /dev/null +++ b/api/src/test/java/org/eclipse/microprofile/reactive/messaging/CustomMessageTest.java @@ -0,0 +1,404 @@ +package org.eclipse.microprofile.reactive.messaging; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.junit.jupiter.api.Test; + +public class CustomMessageTest { + + private final MyMetadata myMetadata = new MyMetadata("bar"); + + @Test + public void testCreationFromPayloadOnly() { + Message message = () -> "foo"; + assertThat(message.getPayload()).isEqualTo("foo"); + assertThat(message.getMetadata()).isEmpty(); + assertThat(message.getAck()).isNotNull(); + assertThat(message.getNack()).isNotNull(); + + assertThat(message.ack().toCompletableFuture().join()).isNull(); + assertThat(message.nack(new Exception("cause")).toCompletableFuture().join()).isNull(); + } + + @Test + public void testCreationFromPayloadAndMetadataOnly() { + + Message message = new Message() { + @Override + public String getPayload() { + return "foo"; + } + + @Override + public Metadata getMetadata() { + return Metadata.of(myMetadata); + } + }; + assertThat(message.getPayload()).isEqualTo("foo"); + assertThat(message.getMetadata()).hasSize(1).containsExactly(myMetadata); + assertThat(message.getAck()).isNotNull(); + assertThat(message.getNack()).isNotNull(); + + assertThat(message.ack().toCompletableFuture().join()).isNull(); + assertThat(message.nack(new Exception("cause")).toCompletableFuture().join()).isNull(); + + message = Message.of("foo", (Metadata) null); + assertThat(message.getPayload()).isEqualTo("foo"); + assertThat(message.getMetadata()).isEmpty(); + assertThat(message.getAck()).isNotNull(); + assertThat(message.getNack()).isNotNull(); + + } + + @Test + public void testCreationFromPayloadAndMetadataAsIterable() { + List metadata = Arrays.asList(myMetadata, new AtomicInteger(2)); + Message message = new Message<>() { + @Override + public String getPayload() { + return "foo"; + } + + @Override + public Metadata getMetadata() { + return Metadata.from(metadata); + } + }; + assertThat(message.getPayload()).isEqualTo("foo"); + assertThat(message.getMetadata()).hasSize(2); + assertThat(message.getAck()).isNotNull(); + assertThat(message.getNack()).isNotNull(); + + assertThat(message.ack().toCompletableFuture().join()).isNull(); + assertThat(message.nack(new Exception("cause")).toCompletableFuture().join()).isNull(); + + assertThatThrownBy(() -> Message.of("x", (Iterable) null)).isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void testCreationFromPayloadAndAck() { + AtomicInteger count = new AtomicInteger(0); + Message message = new Message<>() { + @Override + public String getPayload() { + return "foo"; + } + + @Override + public Supplier> getAck() { + return this::ack; + } + + @Override + public CompletionStage ack() { + count.incrementAndGet(); + return CompletableFuture.completedFuture(null); + } + + }; + assertThat(message.getPayload()).isEqualTo("foo"); + assertThat(message.getMetadata()).hasSize(0); + assertThat(message.getAck()).isNotNull(); + assertThat(message.getNack()).isNotNull(); + + assertThat(message.ack().toCompletableFuture().join()).isNull(); + assertThat(message.nack(new Exception("cause")).toCompletableFuture().join()).isNull(); + assertThat(count).hasValue(1); + + } + + @Test + public void testCreationFromPayloadMetadataAndAck() { + AtomicInteger count = new AtomicInteger(0); + Message message = new Message<>() { + @Override + public String getPayload() { + return "foo"; + } + + @Override + public Metadata getMetadata() { + return Metadata.of(myMetadata); + } + + @Override + public Supplier> getAck() { + return this::ack; + } + + @Override + public CompletionStage ack() { + count.incrementAndGet(); + return CompletableFuture.completedFuture(null); + } + + }; + assertThat(message.getPayload()).isEqualTo("foo"); + assertThat(message.getMetadata()).hasSize(1).containsExactly(myMetadata); + assertThat(message.getAck()).isNotNull(); + assertThat(message.getNack()).isNotNull(); + + assertThat(message.ack().toCompletableFuture().join()).isNull(); + assertThat(message.nack(new Exception("cause")).toCompletableFuture().join()).isNull(); + assertThat(count).hasValue(1); + + assertThat(Message.of("foo", null, () -> CompletableFuture.completedFuture(null)).getMetadata()) + .isEmpty(); + } + + @Test + public void testCreationFromPayloadMetadataAsIterableAndAck() { + List metadata = Arrays.asList(myMetadata, new AtomicInteger(2)); + AtomicInteger count = new AtomicInteger(0); + Message message = new Message<>() { + @Override + public String getPayload() { + return "foo"; + } + + @Override + public Metadata getMetadata() { + return Metadata.from(metadata); + } + + @Override + public Supplier> getAck() { + return this::ack; + } + + @Override + public CompletionStage ack() { + count.incrementAndGet(); + return CompletableFuture.completedFuture(null); + } + + }; + + assertThat(message.getPayload()).isEqualTo("foo"); + assertThat(message.getMetadata()).hasSize(2).contains(myMetadata); + assertThat(message.getAck()).isNotNull(); + assertThat(message.getNack()).isNotNull(); + + assertThat(message.ack().toCompletableFuture().join()).isNull(); + assertThat(message.nack(new Exception("cause")).toCompletableFuture().join()).isNull(); + assertThat(count).hasValue(1); + + assertThatThrownBy(() -> Message.of("foo", (Iterable) null, () -> CompletableFuture.completedFuture(null))) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void testCreationFromPayloadMetadataAckAndNack() { + AtomicInteger ack = new AtomicInteger(0); + AtomicInteger nack = new AtomicInteger(0); + Message message = new CustomMessage<>("foo", Metadata.of(myMetadata), ack, nack); + + assertThat(message.getPayload()).isEqualTo("foo"); + assertThat(message.getMetadata()).hasSize(1).containsExactly(myMetadata); + assertThat(message.getAck()).isNotNull(); + assertThat(message.getNack()).isNotNull(); + + assertThat(message.ack().toCompletableFuture().join()).isNull(); + assertThat(message.nack(new Exception("cause")).toCompletableFuture().join()).isNull(); + assertThat(ack).hasValue(1); + assertThat(nack).hasValue(1); + } + + @Test + public void testWithPayload() { + AtomicInteger ack = new AtomicInteger(0); + AtomicInteger nack = new AtomicInteger(0); + Message message = new CustomMessage<>("foo", Metadata.of(myMetadata), ack, nack); + + Message created = message.withPayload("bar"); + assertThat(created.getPayload()).isEqualTo("bar"); + assertThat(created.getMetadata()).hasSize(1).containsExactly(myMetadata); + assertThat(created.getAck()).isNotNull(); + assertThat(created.getNack()).isNotNull(); + + assertThat(created.ack().toCompletableFuture().join()).isNull(); + assertThat(created.nack(new Exception("cause")).toCompletableFuture().join()).isNull(); + assertThat(ack).hasValue(1); + assertThat(nack).hasValue(1); + + } + + @Test + public void testWithMetadata() { + AtomicInteger ack = new AtomicInteger(0); + AtomicInteger nack = new AtomicInteger(0); + Message message = new CustomMessage<>("foo", Metadata.of(myMetadata), ack, nack); + MyMetadata mm = new MyMetadata("hello"); + Message created = message.withMetadata(Metadata.of(mm)); + assertThat(created.getPayload()).isEqualTo("foo"); + assertThat(created.getMetadata()).hasSize(1).containsExactly(mm); + assertThat(created.getAck()).isNotNull(); + assertThat(created.getNack()).isNotNull(); + + assertThat(created.ack().toCompletableFuture().join()).isNull(); + assertThat(created.nack(new Exception("cause")).toCompletableFuture().join()).isNull(); + assertThat(ack).hasValue(1); + assertThat(nack).hasValue(1); + + } + + @Test + public void testWithAck() { + AtomicInteger ack = new AtomicInteger(0); + AtomicInteger ack2 = new AtomicInteger(0); + AtomicInteger nack = new AtomicInteger(0); + Message message = new CustomMessage<>("foo", Metadata.of(myMetadata), ack, nack); + Message created = message.withAck(() -> { + ack2.incrementAndGet(); + return CompletableFuture.completedFuture(null); + }); + assertThat(created.getPayload()).isEqualTo("foo"); + assertThat(created.getMetadata()).hasSize(1).containsExactly(myMetadata); + assertThat(created.getAck()).isNotNull(); + assertThat(created.getNack()).isNotNull(); + + assertThat(created.ack().toCompletableFuture().join()).isNull(); + assertThat(created.nack(new Exception("cause")).toCompletableFuture().join()).isNull(); + assertThat(ack2).hasValue(1); + assertThat(ack).hasValue(0); + assertThat(nack).hasValue(1); + + } + + @Test + public void testWithNack() { + AtomicInteger ack = new AtomicInteger(0); + AtomicInteger nack = new AtomicInteger(0); + AtomicInteger nack2 = new AtomicInteger(0); + Message message = new CustomMessage<>("foo", Metadata.of(myMetadata), ack, nack); + + Message created = message.withNack(t -> { + assertThat(t).hasMessage("cause"); + nack2.incrementAndGet(); + return CompletableFuture.completedFuture(null); + }); + assertThat(created.getPayload()).isEqualTo("foo"); + assertThat(created.getMetadata()).hasSize(1).containsExactly(myMetadata); + assertThat(created.getAck()).isNotNull(); + assertThat(created.getNack()).isNotNull(); + + assertThat(created.ack().toCompletableFuture().join()).isNull(); + assertThat(created.nack(new Exception("cause")).toCompletableFuture().join()).isNull(); + assertThat(ack).hasValue(1); + assertThat(nack2).hasValue(1); + assertThat(nack).hasValue(0); + } + + @Test + public void testAddMetadata() { + AtomicInteger ack = new AtomicInteger(0); + AtomicInteger nack = new AtomicInteger(0); + Message message = new CustomMessage<>("foo", Metadata.of(myMetadata), ack, nack); + Message created = message.addMetadata(new AtomicInteger(2)); + assertThat(created.getPayload()).isEqualTo("foo"); + assertThat(created.getMetadata()).hasSize(2).contains(myMetadata); + assertThat(created.getAck()).isNotNull(); + assertThat(created.getNack()).isNotNull(); + + assertThat(created.ack().toCompletableFuture().join()).isNull(); + assertThat(created.nack(new Exception("cause")).toCompletableFuture().join()).isNull(); + assertThat(ack).hasValue(1); + assertThat(nack).hasValue(1); + } + + @Test + public void testAckAndNackNull() { + AtomicInteger ack = new AtomicInteger(0); + AtomicInteger nack = new AtomicInteger(0); + Message message = new CustomMessage<>("foo", Metadata.of(myMetadata), ack, nack); + Message created = message.withAck(null).withNack(null); + assertThat(created.getPayload()).isEqualTo("foo"); + assertThat(created.getMetadata()).hasSize(1).contains(myMetadata); + assertThat(created.getAck()).isNotNull(); + assertThat(created.getNack()).isNotNull(); + assertThat(created.ack().toCompletableFuture().join()).isNull(); + assertThat(created.nack(new Exception("cause")).toCompletableFuture().join()).isNull(); + assertThat(ack).hasValue(0); + assertThat(nack).hasValue(0); + } + + @Test + public void testAccessingMetadata() { + Message message = Message.of("hello", Metadata.of(myMetadata)).addMetadata(new AtomicInteger(2)); + + assertThat(message.getMetadata(MyMetadata.class)) + .hasValueSatisfying(m -> assertThat(m.getValue()).isEqualTo("bar")); + assertThat(message.getMetadata(AtomicInteger.class)).hasValueSatisfying(m -> assertThat(m.get()).isEqualTo(2)); + assertThat(message.getMetadata(String.class)).isEmpty(); + assertThatThrownBy(() -> message.getMetadata(null)).isInstanceOf(IllegalArgumentException.class); + } + + private static class MyMetadata { + private final String value; + + public MyMetadata(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + } + + private static class CustomMessage implements Message { + + T payload; + Metadata metadata; + AtomicInteger ack; + AtomicInteger nack; + + public CustomMessage(T payload, Metadata metadata, AtomicInteger ack, AtomicInteger nack) { + this.payload = payload; + this.metadata = metadata; + this.ack = ack; + this.nack = nack; + } + + @Override + public T getPayload() { + return payload; + } + + @Override + public Metadata getMetadata() { + return metadata; + } + + @Override + public Supplier> getAck() { + return this::ack; + } + + @Override + public Function> getNack() { + return this::nack; + } + + @Override + public CompletionStage ack() { + ack.incrementAndGet(); + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletionStage nack(Throwable reason, Metadata metadata) { + assertThat(reason).hasMessage("cause"); + nack.incrementAndGet(); + return CompletableFuture.completedFuture(null); + } + } +} diff --git a/api/src/test/java/org/eclipse/microprofile/reactive/messaging/MessageSpecCompatibilityTest.java b/api/src/test/java/org/eclipse/microprofile/reactive/messaging/MessageSpecCompatibilityTest.java new file mode 100644 index 0000000000..b533386608 --- /dev/null +++ b/api/src/test/java/org/eclipse/microprofile/reactive/messaging/MessageSpecCompatibilityTest.java @@ -0,0 +1,53 @@ +package org.eclipse.microprofile.reactive.messaging; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; + +public class MessageSpecCompatibilityTest { + + public static Class getClassesFromJarFile(File jarFile) throws IOException, ClassNotFoundException { + // URL classloader without parent + try (URLClassLoader cl = URLClassLoader.newInstance( + new URL[] { new URL("jar:file:" + jarFile + "!/") }, null)) { + return cl.loadClass(Message.class.getName()); + } + } + + @Test + void loadClassFromJar() throws IOException, ClassNotFoundException { + File jarFile = new File("./target/lib/microprofile-reactive-messaging-api.jar"); + Class clazz = getClassesFromJarFile(jarFile); + + List specMethods = Arrays.stream(clazz.getDeclaredMethods()) + .filter(m -> Modifier.isPublic(m.getModifiers())) + .map(Method::toGenericString) + .collect(Collectors.toList()); + + List rmMethods = Arrays.stream(Message.class.getDeclaredMethods()) + .filter(m -> Modifier.isPublic(m.getModifiers())) + .map(Method::toGenericString) + .collect(Collectors.toList()); + + System.out.println(Message.class.getName() + " public methods from MicroProfile specification:"); + for (String signature : specMethods) { + System.out.println(signature); + } + assertThat(rmMethods).containsAll(specMethods); + rmMethods.removeAll(specMethods); + System.out.println("Remaining methods in Smallrye implementation:"); + for (String signature : rmMethods) { + System.out.println(signature); + } + } +} diff --git a/api/src/test/java/org/eclipse/microprofile/reactive/messaging/MessageTest.java b/api/src/test/java/org/eclipse/microprofile/reactive/messaging/MessageTest.java index 7858034190..5e61f3a3ec 100644 --- a/api/src/test/java/org/eclipse/microprofile/reactive/messaging/MessageTest.java +++ b/api/src/test/java/org/eclipse/microprofile/reactive/messaging/MessageTest.java @@ -386,8 +386,8 @@ public void testAckAndNackNull() { Message created = message.withAck(null).withNack(null); assertThat(created.getPayload()).isEqualTo("foo"); assertThat(created.getMetadata()).hasSize(1).contains(myMetadata); - assertThat(created.getAck()).isNull(); - assertThat(created.getNack()).isNull(); + assertThat(created.getAck()).isNotNull(); + assertThat(created.getNack()).isNotNull(); assertThat(created.ack().toCompletableFuture().join()).isNull(); assertThat(created.nack(new Exception("cause")).toCompletableFuture().join()).isNull(); assertThat(ack).hasValue(0); diff --git a/smallrye-reactive-messaging-amqp/src/main/java/io/smallrye/reactive/messaging/amqp/AmqpMessage.java b/smallrye-reactive-messaging-amqp/src/main/java/io/smallrye/reactive/messaging/amqp/AmqpMessage.java index a006275533..aea01b48c4 100644 --- a/smallrye-reactive-messaging-amqp/src/main/java/io/smallrye/reactive/messaging/amqp/AmqpMessage.java +++ b/smallrye-reactive-messaging-amqp/src/main/java/io/smallrye/reactive/messaging/amqp/AmqpMessage.java @@ -4,8 +4,8 @@ import java.util.ArrayList; import java.util.concurrent.CompletionStage; +import java.util.function.BiFunction; import java.util.function.Function; -import java.util.function.Supplier; import org.apache.qpid.proton.amqp.Binary; import org.apache.qpid.proton.amqp.messaging.AmqpSequence; @@ -96,7 +96,7 @@ public AmqpMessage(io.vertx.amqp.AmqpMessage msg, Context context, AmqpFailureHa } @Override - public CompletionStage ack() { + public CompletionStage ack(Metadata metadata) { // We must switch to the context having created the message. // This context is passed when this instance of message is created. // It's more a Vert.x AMQP client issue which should ensure calling `accepted` on the right context. @@ -229,12 +229,12 @@ public io.vertx.mutiny.amqp.AmqpMessage getAmqpMessage() { } @Override - public Supplier> getAck() { + public Function> getAckWithMetadata() { return this::ack; } @Override - public Function> getNack() { + public BiFunction> getNackWithMetadata() { return this::nack; } diff --git a/smallrye-reactive-messaging-amqp/src/main/java/io/smallrye/reactive/messaging/amqp/OutgoingAmqpMessage.java b/smallrye-reactive-messaging-amqp/src/main/java/io/smallrye/reactive/messaging/amqp/OutgoingAmqpMessage.java index 894b154e33..4c69c3c862 100644 --- a/smallrye-reactive-messaging-amqp/src/main/java/io/smallrye/reactive/messaging/amqp/OutgoingAmqpMessage.java +++ b/smallrye-reactive-messaging-amqp/src/main/java/io/smallrye/reactive/messaging/amqp/OutgoingAmqpMessage.java @@ -2,8 +2,8 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; +import java.util.function.BiFunction; import java.util.function.Function; -import java.util.function.Supplier; import org.eclipse.microprofile.reactive.messaging.Metadata; @@ -95,7 +95,7 @@ public String getSubject() { } @Override - public CompletionStage ack() { + public CompletionStage ack(Metadata metadata) { return CompletableFuture.completedFuture(null); } @@ -105,12 +105,12 @@ public CompletionStage nack(Throwable reason, Metadata metadata) { } @Override - public Supplier> getAck() { + public Function> getAckWithMetadata() { return this::ack; } @Override - public Function> getNack() { + public BiFunction> getNackWithMetadata() { return this::nack; } diff --git a/smallrye-reactive-messaging-amqp/src/test/java/io/smallrye/reactive/messaging/amqp/ConcurrentProcessorTest.java b/smallrye-reactive-messaging-amqp/src/test/java/io/smallrye/reactive/messaging/amqp/ConcurrentProcessorTest.java index 6671097662..52d2d5c26a 100644 --- a/smallrye-reactive-messaging-amqp/src/test/java/io/smallrye/reactive/messaging/amqp/ConcurrentProcessorTest.java +++ b/smallrye-reactive-messaging-amqp/src/test/java/io/smallrye/reactive/messaging/amqp/ConcurrentProcessorTest.java @@ -214,7 +214,7 @@ public Uni> process(AmqpMessage input) { int value = input.getPayload(); int next = value + 1; perThread.computeIfAbsent(Thread.currentThread(), t -> new CopyOnWriteArrayList<>()).add(next); - return Uni.createFrom().item(Message.of(next, input::ack)) + return Uni.createFrom().item(input.withPayload(next)) .onItem().delayIt().by(Duration.ofMillis(100)); } @@ -246,7 +246,7 @@ public Multi> process(Multi> multi) { int value = input.getPayload(); int next = value + 1; perThread.computeIfAbsent(Thread.currentThread(), t -> new CopyOnWriteArrayList<>()).add(next); - return Uni.createFrom().item(Message.of(next, input::ack)) + return Uni.createFrom().item(input.withPayload(next)) .onItem().delayIt().by(Duration.ofMillis(100)); }); } diff --git a/smallrye-reactive-messaging-amqp/src/test/java/io/smallrye/reactive/messaging/amqp/ConsumptionBean.java b/smallrye-reactive-messaging-amqp/src/test/java/io/smallrye/reactive/messaging/amqp/ConsumptionBean.java index 0ba3d11a3e..1d5becd912 100644 --- a/smallrye-reactive-messaging-amqp/src/test/java/io/smallrye/reactive/messaging/amqp/ConsumptionBean.java +++ b/smallrye-reactive-messaging-amqp/src/test/java/io/smallrye/reactive/messaging/amqp/ConsumptionBean.java @@ -20,7 +20,7 @@ public class ConsumptionBean { @Acknowledgment(Acknowledgment.Strategy.MANUAL) public Message process(AmqpMessage input) { int value = input.getPayload(); - return Message.of(value + 1, input::ack); + return input.withPayload(value + 1); } @Incoming("sink") diff --git a/smallrye-reactive-messaging-amqp/src/test/java/io/smallrye/reactive/messaging/amqp/NullProducingBean.java b/smallrye-reactive-messaging-amqp/src/test/java/io/smallrye/reactive/messaging/amqp/NullProducingBean.java index 6552e15774..991ef135db 100644 --- a/smallrye-reactive-messaging-amqp/src/test/java/io/smallrye/reactive/messaging/amqp/NullProducingBean.java +++ b/smallrye-reactive-messaging-amqp/src/test/java/io/smallrye/reactive/messaging/amqp/NullProducingBean.java @@ -18,7 +18,7 @@ public class NullProducingBean { @Outgoing("sink") @Acknowledgment(Acknowledgment.Strategy.MANUAL) public Message process(Message input) { - return Message.of(null, input::ack); + return input.withPayload(null); } @Outgoing("data") diff --git a/smallrye-reactive-messaging-amqp/src/test/java/io/smallrye/reactive/messaging/amqp/ProducingBean.java b/smallrye-reactive-messaging-amqp/src/test/java/io/smallrye/reactive/messaging/amqp/ProducingBean.java index 0702866e2f..7fc77e624f 100644 --- a/smallrye-reactive-messaging-amqp/src/test/java/io/smallrye/reactive/messaging/amqp/ProducingBean.java +++ b/smallrye-reactive-messaging-amqp/src/test/java/io/smallrye/reactive/messaging/amqp/ProducingBean.java @@ -18,7 +18,7 @@ public class ProducingBean { @Outgoing("sink") @Acknowledgment(Acknowledgment.Strategy.MANUAL) public Message process(Message input) { - return Message.of(input.getPayload() + 1, input::ack); + return input.withPayload(input.getPayload() + 1); } @Outgoing("data") diff --git a/smallrye-reactive-messaging-amqp/src/test/java/io/smallrye/reactive/messaging/amqp/ProducingBeanUsingOutboundMetadata.java b/smallrye-reactive-messaging-amqp/src/test/java/io/smallrye/reactive/messaging/amqp/ProducingBeanUsingOutboundMetadata.java index 966fd24675..a956454d8d 100644 --- a/smallrye-reactive-messaging-amqp/src/test/java/io/smallrye/reactive/messaging/amqp/ProducingBeanUsingOutboundMetadata.java +++ b/smallrye-reactive-messaging-amqp/src/test/java/io/smallrye/reactive/messaging/amqp/ProducingBeanUsingOutboundMetadata.java @@ -23,7 +23,7 @@ public Message process(Message input) { .withAddress("metadata-address") .build(); - return Message.of(input.getPayload() + 1, input::ack).addMetadata(metadata); + return input.withPayload(input.getPayload() + 1).addMetadata(metadata); } @Outgoing("data") diff --git a/smallrye-reactive-messaging-camel/src/main/java/io/smallrye/reactive/messaging/camel/CamelMessage.java b/smallrye-reactive-messaging-camel/src/main/java/io/smallrye/reactive/messaging/camel/CamelMessage.java index e07e9c6eb3..505b27fa74 100644 --- a/smallrye-reactive-messaging-camel/src/main/java/io/smallrye/reactive/messaging/camel/CamelMessage.java +++ b/smallrye-reactive-messaging-camel/src/main/java/io/smallrye/reactive/messaging/camel/CamelMessage.java @@ -1,7 +1,7 @@ package io.smallrye.reactive.messaging.camel; import java.util.concurrent.CompletionStage; -import java.util.function.Function; +import java.util.function.BiFunction; import org.apache.camel.Exchange; import org.eclipse.microprofile.reactive.messaging.Message; @@ -43,7 +43,7 @@ public CompletionStage nack(Throwable reason, Metadata metadata) { } @Override - public Function> getNack() { + public BiFunction> getNackWithMetadata() { return this::nack; } } diff --git a/smallrye-reactive-messaging-gcp-pubsub/src/main/java/io/smallrye/reactive/messaging/gcp/pubsub/PubSubMessage.java b/smallrye-reactive-messaging-gcp-pubsub/src/main/java/io/smallrye/reactive/messaging/gcp/pubsub/PubSubMessage.java index 838a6e847f..0f54fb57a0 100644 --- a/smallrye-reactive-messaging-gcp-pubsub/src/main/java/io/smallrye/reactive/messaging/gcp/pubsub/PubSubMessage.java +++ b/smallrye-reactive-messaging-gcp-pubsub/src/main/java/io/smallrye/reactive/messaging/gcp/pubsub/PubSubMessage.java @@ -5,9 +5,10 @@ import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; -import java.util.function.Supplier; +import java.util.function.Function; import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Metadata; import com.google.cloud.pubsub.v1.AckReplyConsumer; import com.google.pubsub.v1.PubsubMessage; @@ -38,7 +39,7 @@ public String getPayload() { } @Override - public CompletionStage ack() { + public CompletionStage ack(Metadata metadata) { if (ackReplyConsumer != null) { ackReplyConsumer.ack(); } @@ -46,7 +47,7 @@ public CompletionStage ack() { } @Override - public Supplier> getAck() { + public Function> getAckWithMetadata() { return this::ack; } diff --git a/smallrye-reactive-messaging-jms/src/main/java/io/smallrye/reactive/messaging/jms/IncomingJmsMessage.java b/smallrye-reactive-messaging-jms/src/main/java/io/smallrye/reactive/messaging/jms/IncomingJmsMessage.java index d116bb9f2b..d2c2c71971 100644 --- a/smallrye-reactive-messaging-jms/src/main/java/io/smallrye/reactive/messaging/jms/IncomingJmsMessage.java +++ b/smallrye-reactive-messaging-jms/src/main/java/io/smallrye/reactive/messaging/jms/IncomingJmsMessage.java @@ -4,7 +4,7 @@ import java.util.concurrent.CompletionStage; import java.util.concurrent.Executor; -import java.util.function.Supplier; +import java.util.function.Function; import jakarta.jms.JMSException; import jakarta.jms.Message; @@ -104,12 +104,12 @@ private T convert(String value) { } @Override - public Supplier> getAck() { + public Function> getAckWithMetadata() { return this::ack; } @Override - public CompletionStage ack() { + public CompletionStage ack(Metadata metadata) { return Uni.createFrom().voidItem() .onItem().invoke(m -> { try { diff --git a/smallrye-reactive-messaging-kafka/revapi.json b/smallrye-reactive-messaging-kafka/revapi.json index 87c9580120..d651f0be6f 100644 --- a/smallrye-reactive-messaging-kafka/revapi.json +++ b/smallrye-reactive-messaging-kafka/revapi.json @@ -36,7 +36,86 @@ "criticality" : "highlight", "minSeverity" : "POTENTIALLY_BREAKING", "minCriticality" : "documented", - "differences" : [ ] + "differences" : [ + { + "code": "java.method.numberOfParametersChanged", + "old": "method java.util.concurrent.CompletionStage io.smallrye.reactive.messaging.kafka.IncomingKafkaRecord::ack()", + "new": "method java.util.concurrent.CompletionStage io.smallrye.reactive.messaging.kafka.IncomingKafkaRecord::ack(org.eclipse.microprofile.reactive.messaging.Metadata)", + "justification": "Added Message metadata propagation" + }, + { + "code": "java.method.removed", + "old": "method java.util.function.Supplier> io.smallrye.reactive.messaging.kafka.IncomingKafkaRecord::getAck()", + "justification": "Added Message metadata propagation" + }, + { + "code": "java.method.removed", + "old": "method java.util.function.Function> io.smallrye.reactive.messaging.kafka.IncomingKafkaRecord::getNack()", + "justification": "Added Message metadata propagation" + }, + { + "code": "java.method.numberOfParametersChanged", + "old": "method java.util.concurrent.CompletionStage io.smallrye.reactive.messaging.kafka.IncomingKafkaRecordBatch::ack()", + "new": "method java.util.concurrent.CompletionStage io.smallrye.reactive.messaging.kafka.IncomingKafkaRecordBatch::ack(org.eclipse.microprofile.reactive.messaging.Metadata)", + "justification": "Added Message metadata propagation" + }, + { + "code": "java.method.removed", + "old": "method java.util.function.Supplier> io.smallrye.reactive.messaging.kafka.IncomingKafkaRecordBatch::getAck()", + "justification": "Added Message metadata propagation" + }, + { + "code": "java.method.removed", + "old": "method java.util.function.Function> io.smallrye.reactive.messaging.kafka.IncomingKafkaRecordBatch::getNack()", + "justification": "Added Message metadata propagation" + }, + { + "code": "java.method.addedToInterface", + "new": "method io.smallrye.mutiny.Uni>> io.smallrye.reactive.messaging.kafka.KafkaConsumer::lisTopics()", + "justification": "Added Message metadata propagation" + }, + { + "code": "java.method.addedToInterface", + "new": "method io.smallrye.mutiny.Uni>> io.smallrye.reactive.messaging.kafka.KafkaConsumer::lisTopics(java.time.Duration)", + "justification": "Added Message metadata propagation" + }, + { + "code": "java.method.addedToInterface", + "new": "method io.smallrye.mutiny.Uni> io.smallrye.reactive.messaging.kafka.KafkaConsumer::partitionsFor(java.lang.String)", + "justification": "Added Message metadata propagation" + }, + { + "code": "java.method.parameterTypeChanged", + "old": "parameter void io.smallrye.reactive.messaging.kafka.OutgoingKafkaRecord::(java.lang.String, K, T, java.time.Instant, int, org.apache.kafka.common.header.Headers, ===java.util.function.Supplier>===, java.util.function.Function>, org.eclipse.microprofile.reactive.messaging.Metadata)", + "new": "parameter void io.smallrye.reactive.messaging.kafka.OutgoingKafkaRecord::(java.lang.String, K, T, java.time.Instant, int, org.apache.kafka.common.header.Headers, ===java.util.function.Function>===, java.util.function.BiFunction>, org.eclipse.microprofile.reactive.messaging.Metadata)", + "parameterIndex": "6", + "justification": "Added Message metadata propagation" + }, + { + "code": "java.method.parameterTypeChanged", + "old": "parameter void io.smallrye.reactive.messaging.kafka.OutgoingKafkaRecord::(java.lang.String, K, T, java.time.Instant, int, org.apache.kafka.common.header.Headers, java.util.function.Supplier>, ===java.util.function.Function>===, org.eclipse.microprofile.reactive.messaging.Metadata)", + "new": "parameter void io.smallrye.reactive.messaging.kafka.OutgoingKafkaRecord::(java.lang.String, K, T, java.time.Instant, int, org.apache.kafka.common.header.Headers, java.util.function.Function>, ===java.util.function.BiFunction>===, org.eclipse.microprofile.reactive.messaging.Metadata)", + "parameterIndex": "7", + "justification": "Added Message metadata propagation" + }, + { + "code": "java.method.numberOfParametersChanged", + "old": "method java.util.concurrent.CompletionStage io.smallrye.reactive.messaging.kafka.OutgoingKafkaRecord::ack()", + "new": "method java.util.concurrent.CompletionStage io.smallrye.reactive.messaging.kafka.OutgoingKafkaRecord::ack(org.eclipse.microprofile.reactive.messaging.Metadata)", + "justification": "Added Message metadata propagation" + + }, + { + "code": "java.method.removed", + "old": "method java.util.function.Supplier> io.smallrye.reactive.messaging.kafka.OutgoingKafkaRecord::getAck()", + "justification": "Added Message metadata propagation" + }, + { + "code": "java.method.removed", + "old": "method java.util.function.Function> io.smallrye.reactive.messaging.kafka.OutgoingKafkaRecord::getNack()", + "justification": "Added Message metadata propagation" + } + ] } }, { "extension" : "revapi.reporter.json", @@ -55,4 +134,4 @@ "minCriticality" : "documented", "output" : "out" } -} ] \ No newline at end of file +} ] diff --git a/smallrye-reactive-messaging-kafka/src/main/java/io/smallrye/reactive/messaging/kafka/IncomingKafkaRecord.java b/smallrye-reactive-messaging-kafka/src/main/java/io/smallrye/reactive/messaging/kafka/IncomingKafkaRecord.java index e9bace8928..90353f10f7 100644 --- a/smallrye-reactive-messaging-kafka/src/main/java/io/smallrye/reactive/messaging/kafka/IncomingKafkaRecord.java +++ b/smallrye-reactive-messaging-kafka/src/main/java/io/smallrye/reactive/messaging/kafka/IncomingKafkaRecord.java @@ -3,8 +3,8 @@ import java.time.Instant; import java.util.ArrayList; import java.util.concurrent.CompletionStage; +import java.util.function.BiFunction; import java.util.function.Function; -import java.util.function.Supplier; import org.apache.kafka.clients.consumer.ConsumerRecord; import org.apache.kafka.common.header.Headers; @@ -113,17 +113,17 @@ public Metadata getMetadata() { } @Override - public Supplier> getAck() { + public Function> getAckWithMetadata() { return this::ack; } @Override - public Function> getNack() { + public BiFunction> getNackWithMetadata() { return this::nack; } @Override - public CompletionStage ack() { + public CompletionStage ack(Metadata metadata) { return commitHandler.handle(this).subscribeAsCompletionStage(); } diff --git a/smallrye-reactive-messaging-kafka/src/main/java/io/smallrye/reactive/messaging/kafka/IncomingKafkaRecordBatch.java b/smallrye-reactive-messaging-kafka/src/main/java/io/smallrye/reactive/messaging/kafka/IncomingKafkaRecordBatch.java index 97f8a87361..26dba7c095 100644 --- a/smallrye-reactive-messaging-kafka/src/main/java/io/smallrye/reactive/messaging/kafka/IncomingKafkaRecordBatch.java +++ b/smallrye-reactive-messaging-kafka/src/main/java/io/smallrye/reactive/messaging/kafka/IncomingKafkaRecordBatch.java @@ -9,8 +9,8 @@ import java.util.List; import java.util.Map; import java.util.concurrent.CompletionStage; +import java.util.function.BiFunction; import java.util.function.Function; -import java.util.function.Supplier; import java.util.stream.Collectors; import org.apache.kafka.clients.consumer.ConsumerRecord; @@ -75,20 +75,20 @@ public Metadata getMetadata() { } @Override - public Supplier> getAck() { + public Function> getAckWithMetadata() { return this::ack; } @Override - public Function> getNack() { + public BiFunction> getNackWithMetadata() { return this::nack; } @Override - public CompletionStage ack() { + public CompletionStage ack(Metadata metadata) { return Multi.createBy().concatenating().collectFailures() .streams(this.latestOffsetRecords.values().stream() - .map(record -> Multi.createFrom().completionStage(record.getAck())) + .map(record -> Multi.createFrom().completionStage(record.ack(metadata))) .collect(Collectors.toList())) .toUni().subscribeAsCompletionStage(); } diff --git a/smallrye-reactive-messaging-kafka/src/main/java/io/smallrye/reactive/messaging/kafka/KafkaConsumer.java b/smallrye-reactive-messaging-kafka/src/main/java/io/smallrye/reactive/messaging/kafka/KafkaConsumer.java index d561d60b10..a8c9850970 100644 --- a/smallrye-reactive-messaging-kafka/src/main/java/io/smallrye/reactive/messaging/kafka/KafkaConsumer.java +++ b/smallrye-reactive-messaging-kafka/src/main/java/io/smallrye/reactive/messaging/kafka/KafkaConsumer.java @@ -1,6 +1,8 @@ package io.smallrye.reactive.messaging.kafka; +import java.time.Duration; import java.util.Collection; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Function; @@ -8,6 +10,7 @@ import org.apache.kafka.clients.consumer.Consumer; import org.apache.kafka.clients.consumer.ConsumerGroupMetadata; import org.apache.kafka.clients.consumer.OffsetAndMetadata; +import org.apache.kafka.common.PartitionInfo; import org.apache.kafka.common.TopicPartition; import io.smallrye.common.annotation.CheckReturnValue; @@ -209,4 +212,12 @@ Uni commit( @CheckReturnValue Uni resetToLastCommittedPositions(); + @CheckReturnValue + Uni>> lisTopics(); + + @CheckReturnValue + Uni>> lisTopics(Duration timeout); + + @CheckReturnValue + Uni> partitionsFor(String topic); } diff --git a/smallrye-reactive-messaging-kafka/src/main/java/io/smallrye/reactive/messaging/kafka/OutgoingKafkaRecord.java b/smallrye-reactive-messaging-kafka/src/main/java/io/smallrye/reactive/messaging/kafka/OutgoingKafkaRecord.java index 3711200462..5dae9a6ca1 100644 --- a/smallrye-reactive-messaging-kafka/src/main/java/io/smallrye/reactive/messaging/kafka/OutgoingKafkaRecord.java +++ b/smallrye-reactive-messaging-kafka/src/main/java/io/smallrye/reactive/messaging/kafka/OutgoingKafkaRecord.java @@ -5,6 +5,7 @@ import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; +import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Supplier; @@ -17,15 +18,16 @@ public class OutgoingKafkaRecord implements KafkaRecord { private final T value; - private final Supplier> ack; - private final Function> nack; + private final Function> ack; + private final BiFunction> nack; private final Metadata metadata; // TODO Use a normal import once OutgoingKafkaRecordMetadata in this package has been removed private final io.smallrye.reactive.messaging.kafka.api.OutgoingKafkaRecordMetadata kafkaMetadata; @SuppressWarnings("deprecation") public OutgoingKafkaRecord(String topic, K key, T value, Instant timestamp, int partition, Headers headers, - Supplier> ack, Function> nack, Metadata existingMetadata) { + Function> ack, + BiFunction> nack, Metadata existingMetadata) { kafkaMetadata = io.smallrye.reactive.messaging.kafka.api.OutgoingKafkaRecordMetadata. builder() .withTopic(topic) .withKey(key) @@ -69,15 +71,15 @@ public static OutgoingKafkaRecord from(Message message) { return new OutgoingKafkaRecord<>(kafkaMetadata.getTopic(), kafkaMetadata.getKey(), message.getPayload(), kafkaMetadata.getTimestamp(), kafkaMetadata.getPartition(), - kafkaMetadata.getHeaders(), message.getAck(), message.getNack(), message.getMetadata()); + kafkaMetadata.getHeaders(), message.getAckWithMetadata(), message.getNackWithMetadata(), message.getMetadata()); } @Override - public CompletionStage ack() { + public CompletionStage ack(Metadata metadata) { if (ack == null) { return CompletableFuture.completedFuture(null); } else { - return ack.get(); + return ack.apply(metadata); } } @@ -107,12 +109,12 @@ public Headers getHeaders() { } @Override - public Supplier> getAck() { + public Function> getAckWithMetadata() { return ack; } @Override - public Function> getNack() { + public BiFunction> getNackWithMetadata() { return nack; } @@ -148,7 +150,7 @@ public byte[] value() { } }); return new OutgoingKafkaRecord<>(getTopic(), getKey(), getPayload(), getTimestamp(), getPartition(), - copy, getAck(), getNack(), getMetadata()); + copy, getAckWithMetadata(), getNackWithMetadata(), getMetadata()); } /** @@ -173,7 +175,7 @@ public byte[] value() { } }); return new OutgoingKafkaRecord<>(getTopic(), getKey(), getPayload(), getTimestamp(), getPartition(), - copy, getAck(), getNack(), getMetadata()); + copy, getAckWithMetadata(), getNackWithMetadata(), getMetadata()); } /** @@ -199,41 +201,42 @@ public byte[] value() { } }); return new OutgoingKafkaRecord<>(getTopic(), getKey(), getPayload(), getTimestamp(), getPartition(), - copy, getAck(), getNack(), getMetadata()); + copy, getAckWithMetadata(), getNackWithMetadata(), getMetadata()); } public OutgoingKafkaRecord with(String topic, K key, T value) { - return new OutgoingKafkaRecord<>(topic, key, value, getTimestamp(), getPartition(), getHeaders(), getAck(), getNack(), - getMetadata()); + return new OutgoingKafkaRecord<>(topic, key, value, getTimestamp(), getPartition(), getHeaders(), getAckWithMetadata(), + getNackWithMetadata(), getMetadata()); } public OutgoingKafkaRecord with(String topic, T value) { - return new OutgoingKafkaRecord<>(topic, getKey(), value, getTimestamp(), getPartition(), getHeaders(), getAck(), - getNack(), getMetadata()); + return new OutgoingKafkaRecord<>(topic, getKey(), value, getTimestamp(), getPartition(), getHeaders(), + getAckWithMetadata(), + getNackWithMetadata(), getMetadata()); } public OutgoingKafkaRecord with(String topic, K key, T value, Instant timestamp, int partition) { - return new OutgoingKafkaRecord<>(topic, key, value, timestamp, partition, getHeaders(), getAck(), getNack(), - getMetadata()); + return new OutgoingKafkaRecord<>(topic, key, value, timestamp, partition, getHeaders(), getAckWithMetadata(), + getNackWithMetadata(), getMetadata()); } @Override public

OutgoingKafkaRecord withPayload(P payload) { - return OutgoingKafkaRecord.from(Message.of(payload, getMetadata(), getAck(), getNack())); + return OutgoingKafkaRecord.from(Message.of(payload, getMetadata(), getAckWithMetadata(), getNackWithMetadata())); } @Override public OutgoingKafkaRecord withMetadata(Iterable metadata) { // TODO this adds the entire provided Iterable as a single datum in the existing Metadata Metadata newMetadata = getMetadata().with(metadata); - return OutgoingKafkaRecord.from(Message.of(getPayload(), newMetadata, getAck(), getNack())); + return OutgoingKafkaRecord.from(Message.of(getPayload(), newMetadata, getAckWithMetadata(), getNackWithMetadata())); } @Override public OutgoingKafkaRecord withMetadata(Metadata metadata) { // TODO this adds the entire provided Metadata as a single datum in the existing Metadata Metadata newMetadata = getMetadata().with(metadata); - return OutgoingKafkaRecord.from(Message.of(getPayload(), newMetadata, getAck(), getNack())); + return OutgoingKafkaRecord.from(Message.of(getPayload(), newMetadata, getAckWithMetadata(), getNackWithMetadata())); } @Override @@ -243,6 +246,6 @@ public OutgoingKafkaRecord withAck(Supplier> supplie @Override public OutgoingKafkaRecord withNack(Function> nack) { - return OutgoingKafkaRecord.from(Message.of(getPayload(), getMetadata(), getAck(), nack)); + return OutgoingKafkaRecord.from(Message.of(getPayload(), getMetadata(), getAckWithMetadata(), (t, m) -> nack.apply(t))); } } diff --git a/smallrye-reactive-messaging-kafka/src/main/java/io/smallrye/reactive/messaging/kafka/impl/ReactiveKafkaConsumer.java b/smallrye-reactive-messaging-kafka/src/main/java/io/smallrye/reactive/messaging/kafka/impl/ReactiveKafkaConsumer.java index d6aabf3508..ff7682bc84 100644 --- a/smallrye-reactive-messaging-kafka/src/main/java/io/smallrye/reactive/messaging/kafka/impl/ReactiveKafkaConsumer.java +++ b/smallrye-reactive-messaging-kafka/src/main/java/io/smallrye/reactive/messaging/kafka/impl/ReactiveKafkaConsumer.java @@ -20,6 +20,7 @@ import jakarta.enterprise.inject.literal.NamedLiteral; import org.apache.kafka.clients.consumer.*; +import org.apache.kafka.common.PartitionInfo; import org.apache.kafka.common.TopicPartition; import org.apache.kafka.common.errors.WakeupException; import org.apache.kafka.common.serialization.Deserializer; @@ -510,6 +511,30 @@ public Uni seekToEnd(Collection partitions) { }); } + @Override + @CheckReturnValue + public Uni>> lisTopics() { + return runOnPollingThread(consumer -> { + return consumer.listTopics(); + }); + } + + @Override + @CheckReturnValue + public Uni>> lisTopics(Duration timeout) { + return runOnPollingThread(consumer -> { + return consumer.listTopics(timeout); + }); + } + + @Override + @CheckReturnValue + public Uni> partitionsFor(String topic) { + return runOnPollingThread(consumer -> { + return consumer.partitionsFor(topic); + }); + } + boolean isClosed() { return closed.get(); } diff --git a/smallrye-reactive-messaging-kafka/src/test/java/io/smallrye/reactive/messaging/kafka/ConcurrentProcessorTest.java b/smallrye-reactive-messaging-kafka/src/test/java/io/smallrye/reactive/messaging/kafka/ConcurrentProcessorTest.java index e9a81e61a1..1a6c5ca452 100644 --- a/smallrye-reactive-messaging-kafka/src/test/java/io/smallrye/reactive/messaging/kafka/ConcurrentProcessorTest.java +++ b/smallrye-reactive-messaging-kafka/src/test/java/io/smallrye/reactive/messaging/kafka/ConcurrentProcessorTest.java @@ -157,7 +157,7 @@ public Uni> process(Message input) { int value = input.getPayload(); int next = value + 1; perThread.computeIfAbsent(Thread.currentThread(), t -> new CopyOnWriteArrayList<>()).add(next); - return Uni.createFrom().item(Message.of(next, input::ack)) + return Uni.createFrom().item(input.withPayload(next)) .onItem().delayIt().by(Duration.ofMillis(100)); } @@ -189,7 +189,7 @@ public Multi> process(Multi> multi) { int value = input.getPayload(); int next = value + 1; perThread.computeIfAbsent(Thread.currentThread(), t -> new CopyOnWriteArrayList<>()).add(next); - return Uni.createFrom().item(Message.of(next, input::ack)) + return Uni.createFrom().item(input.withPayload(next)) .onItem().delayIt().by(Duration.ofMillis(100)); }); } diff --git a/smallrye-reactive-messaging-kafka/src/test/java/io/smallrye/reactive/messaging/kafka/ConsumptionBeanUsingRawMessage.java b/smallrye-reactive-messaging-kafka/src/test/java/io/smallrye/reactive/messaging/kafka/ConsumptionBeanUsingRawMessage.java index 866576288d..65f47d487c 100644 --- a/smallrye-reactive-messaging-kafka/src/test/java/io/smallrye/reactive/messaging/kafka/ConsumptionBeanUsingRawMessage.java +++ b/smallrye-reactive-messaging-kafka/src/test/java/io/smallrye/reactive/messaging/kafka/ConsumptionBeanUsingRawMessage.java @@ -21,7 +21,7 @@ public class ConsumptionBeanUsingRawMessage { @Acknowledgment(Acknowledgment.Strategy.MANUAL) public Message process(Message input) { kafka.add(input); - return Message.of(input.getPayload() + 1, input::ack); + return input.withPayload(input.getPayload() + 1); } @Incoming("sink") diff --git a/smallrye-reactive-messaging-kafka/src/test/java/io/smallrye/reactive/messaging/kafka/ProducingBean.java b/smallrye-reactive-messaging-kafka/src/test/java/io/smallrye/reactive/messaging/kafka/ProducingBean.java index 7da9900be1..a1e51b1f87 100644 --- a/smallrye-reactive-messaging-kafka/src/test/java/io/smallrye/reactive/messaging/kafka/ProducingBean.java +++ b/smallrye-reactive-messaging-kafka/src/test/java/io/smallrye/reactive/messaging/kafka/ProducingBean.java @@ -18,7 +18,7 @@ public class ProducingBean { @Outgoing("output") @Acknowledgment(Acknowledgment.Strategy.MANUAL) public Message process(Message input) { - return Message.of(input.getPayload() + 1, input::ack); + return input.withPayload(input.getPayload() + 1); } @Outgoing("data") diff --git a/smallrye-reactive-messaging-kafka/src/test/java/io/smallrye/reactive/messaging/kafka/ProducingMessageWithHeaderBean.java b/smallrye-reactive-messaging-kafka/src/test/java/io/smallrye/reactive/messaging/kafka/ProducingMessageWithHeaderBean.java index 9802c6a4fa..5d4e9c26e5 100644 --- a/smallrye-reactive-messaging-kafka/src/test/java/io/smallrye/reactive/messaging/kafka/ProducingMessageWithHeaderBean.java +++ b/smallrye-reactive-messaging-kafka/src/test/java/io/smallrye/reactive/messaging/kafka/ProducingMessageWithHeaderBean.java @@ -30,7 +30,7 @@ public Message process(Message input) { Metadata.of( OutgoingKafkaRecordMetadata.builder().withKey(Integer.toString(input.getPayload())) .withHeaders(list).build()), - input::ack); + m -> input.ack(m)); } @Outgoing("data") diff --git a/smallrye-reactive-messaging-kafka/src/test/java/io/smallrye/reactive/messaging/kafka/health/SinkHealthCheckTest.java b/smallrye-reactive-messaging-kafka/src/test/java/io/smallrye/reactive/messaging/kafka/health/SinkHealthCheckTest.java index 10d496b938..045a03836b 100644 --- a/smallrye-reactive-messaging-kafka/src/test/java/io/smallrye/reactive/messaging/kafka/health/SinkHealthCheckTest.java +++ b/smallrye-reactive-messaging-kafka/src/test/java/io/smallrye/reactive/messaging/kafka/health/SinkHealthCheckTest.java @@ -181,7 +181,7 @@ public static class ProducingBean { @Outgoing("output") @Acknowledgment(Acknowledgment.Strategy.MANUAL) public Message process(Message input) { - return Message.of(input.getPayload() + 1, input::ack) + return input.withPayload(input.getPayload() + 1) .addMetadata(OutgoingKafkaRecordMetadata.builder().withTopic(topic).build()); } diff --git a/smallrye-reactive-messaging-mqtt/revapi.json b/smallrye-reactive-messaging-mqtt/revapi.json index ce9b008e35..e82ee18250 100644 --- a/smallrye-reactive-messaging-mqtt/revapi.json +++ b/smallrye-reactive-messaging-mqtt/revapi.json @@ -21,7 +21,11 @@ "criticality" : "highlight", "minSeverity" : "POTENTIALLY_BREAKING", "minCriticality" : "documented", - "differences" : [ ] + "differences" : [ { + "code": "java.method.removed", + "old": "method java.util.function.Function> io.smallrye.reactive.messaging.mqtt.ReceivingMqttMessage::getNack()", + "justification": "Added Message metadata propagation" + }] } }, { "extension" : "revapi.reporter.json", @@ -40,4 +44,4 @@ "minCriticality" : "documented", "output" : "out" } -} ] \ No newline at end of file +} ] diff --git a/smallrye-reactive-messaging-mqtt/src/main/java/io/smallrye/reactive/messaging/mqtt/ReceivingMqttMessage.java b/smallrye-reactive-messaging-mqtt/src/main/java/io/smallrye/reactive/messaging/mqtt/ReceivingMqttMessage.java index 4f8601e858..0084391c9f 100644 --- a/smallrye-reactive-messaging-mqtt/src/main/java/io/smallrye/reactive/messaging/mqtt/ReceivingMqttMessage.java +++ b/smallrye-reactive-messaging-mqtt/src/main/java/io/smallrye/reactive/messaging/mqtt/ReceivingMqttMessage.java @@ -3,7 +3,7 @@ import static io.smallrye.reactive.messaging.providers.locals.ContextAwareMessage.captureContextMetadata; import java.util.concurrent.CompletionStage; -import java.util.function.Function; +import java.util.function.BiFunction; import org.eclipse.microprofile.reactive.messaging.Metadata; @@ -64,7 +64,7 @@ public CompletionStage nack(Throwable reason, Metadata metadata) { } @Override - public Function> getNack() { + public BiFunction> getNackWithMetadata() { return this::nack; } } diff --git a/smallrye-reactive-messaging-mqtt/src/main/java/io/smallrye/reactive/messaging/mqtt/SendingMqttMessage.java b/smallrye-reactive-messaging-mqtt/src/main/java/io/smallrye/reactive/messaging/mqtt/SendingMqttMessage.java index 65ae73f9b7..e1673cd4aa 100644 --- a/smallrye-reactive-messaging-mqtt/src/main/java/io/smallrye/reactive/messaging/mqtt/SendingMqttMessage.java +++ b/smallrye-reactive-messaging-mqtt/src/main/java/io/smallrye/reactive/messaging/mqtt/SendingMqttMessage.java @@ -2,6 +2,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; +import java.util.function.Function; import java.util.function.Supplier; import org.eclipse.microprofile.reactive.messaging.Metadata; @@ -32,7 +33,7 @@ public Metadata getMetadata() { } @Override - public CompletionStage ack() { + public CompletionStage ack(Metadata metadata) { if (ack != null) { return ack.get(); } @@ -40,7 +41,7 @@ public CompletionStage ack() { } @Override - public Supplier> getAck() { + public Function> getAckWithMetadata() { return this::ack; } diff --git a/smallrye-reactive-messaging-mqtt/src/test/java/io/smallrye/reactive/messaging/mqtt/ConcurrentProcessorTest.java b/smallrye-reactive-messaging-mqtt/src/test/java/io/smallrye/reactive/messaging/mqtt/ConcurrentProcessorTest.java index 0d0f8f918f..f282dbee23 100644 --- a/smallrye-reactive-messaging-mqtt/src/test/java/io/smallrye/reactive/messaging/mqtt/ConcurrentProcessorTest.java +++ b/smallrye-reactive-messaging-mqtt/src/test/java/io/smallrye/reactive/messaging/mqtt/ConcurrentProcessorTest.java @@ -197,7 +197,7 @@ public Uni> process(Message input) { int value = Integer.parseInt(input.getPayload()); int next = value + 1; perThread.computeIfAbsent(Thread.currentThread(), t -> new CopyOnWriteArrayList<>()).add(next); - return Uni.createFrom().item(Message.of(next, input::ack)) + return Uni.createFrom().item(input.withPayload(next)) .onItem().delayIt().by(Duration.ofMillis(100)); } @@ -229,7 +229,7 @@ public Multi> process(Multi> multi) { int value = Integer.parseInt(input.getPayload()); int next = value + 1; perThread.computeIfAbsent(Thread.currentThread(), t -> new CopyOnWriteArrayList<>()).add(next); - return Uni.createFrom().item(Message.of(next, input::ack)) + return Uni.createFrom().item(input.withPayload(next)) .onItem().delayIt().by(Duration.ofMillis(100)); }); } diff --git a/smallrye-reactive-messaging-mqtt/src/test/java/io/smallrye/reactive/messaging/mqtt/NullProducingBean.java b/smallrye-reactive-messaging-mqtt/src/test/java/io/smallrye/reactive/messaging/mqtt/NullProducingBean.java index 8e1874019b..9fc0d6f04c 100644 --- a/smallrye-reactive-messaging-mqtt/src/test/java/io/smallrye/reactive/messaging/mqtt/NullProducingBean.java +++ b/smallrye-reactive-messaging-mqtt/src/test/java/io/smallrye/reactive/messaging/mqtt/NullProducingBean.java @@ -18,7 +18,7 @@ public class NullProducingBean { @Outgoing("sink") @Acknowledgment(Acknowledgment.Strategy.MANUAL) public Message process(Message input) { - return Message.of(null, input::ack); + return input.withPayload(null); } @Outgoing("data") diff --git a/smallrye-reactive-messaging-mqtt/src/test/java/io/smallrye/reactive/messaging/mqtt/ProducingBean.java b/smallrye-reactive-messaging-mqtt/src/test/java/io/smallrye/reactive/messaging/mqtt/ProducingBean.java index 1aa65295ed..76d38b998e 100644 --- a/smallrye-reactive-messaging-mqtt/src/test/java/io/smallrye/reactive/messaging/mqtt/ProducingBean.java +++ b/smallrye-reactive-messaging-mqtt/src/test/java/io/smallrye/reactive/messaging/mqtt/ProducingBean.java @@ -18,7 +18,7 @@ public class ProducingBean { @Outgoing("sink") @Acknowledgment(Acknowledgment.Strategy.MANUAL) public Message process(Message input) { - return Message.of(input.getPayload() + 1, input::ack); + return input.withPayload(input.getPayload() + 1); } @Outgoing("data") diff --git a/smallrye-reactive-messaging-provider/src/main/java/io/smallrye/reactive/messaging/providers/OutgoingInterceptorDecorator.java b/smallrye-reactive-messaging-provider/src/main/java/io/smallrye/reactive/messaging/providers/OutgoingInterceptorDecorator.java index ef6c586ed3..0c3cfcb29d 100644 --- a/smallrye-reactive-messaging-provider/src/main/java/io/smallrye/reactive/messaging/providers/OutgoingInterceptorDecorator.java +++ b/smallrye-reactive-messaging-provider/src/main/java/io/smallrye/reactive/messaging/providers/OutgoingInterceptorDecorator.java @@ -49,10 +49,10 @@ public Multi> decorate(Multi> toBeSubs OutgoingInterceptor interceptor = matching.get(0); multi = multi.map(m -> { Message before = interceptor.onMessage(m.addMetadata(new OutgoingMessageMetadata<>())); - Message withAck = before.withAck(() -> before.ack() + Message withAck = before.withAckWithMetadata((metadata) -> before.ack(before.getMetadata()) .thenAccept(Unchecked.consumer(x -> interceptor.onMessageAck(before)))); - return withAck.withNack(t -> withAck.nack(t) - .thenAccept(Unchecked.consumer(x -> interceptor.onMessageNack(withAck, t)))); + return withAck.withNackWithMetadata((throwable, metadata) -> withAck.nack(throwable, metadata) + .thenAccept(Unchecked.consumer(x -> interceptor.onMessageNack(withAck, throwable)))); }); } } diff --git a/smallrye-reactive-messaging-provider/src/test/java/io/smallrye/reactive/messaging/decorator/AppendingDecorator.java b/smallrye-reactive-messaging-provider/src/test/java/io/smallrye/reactive/messaging/decorator/AppendingDecorator.java index cf4cd6775c..698c2bdfa7 100644 --- a/smallrye-reactive-messaging-provider/src/test/java/io/smallrye/reactive/messaging/decorator/AppendingDecorator.java +++ b/smallrye-reactive-messaging-provider/src/test/java/io/smallrye/reactive/messaging/decorator/AppendingDecorator.java @@ -19,7 +19,7 @@ public Multi> decorate(Multi> publishe private Message appendString(Message message, String string) { if (message.getPayload() instanceof String) { String payload = (String) message.getPayload(); - return Message.of(payload + "-" + string, message::ack); + return Message.of(payload + "-" + string, () -> message.ack()); } else { return message; } diff --git a/smallrye-reactive-messaging-provider/src/test/java/io/smallrye/reactive/messaging/decorator/AppendingDeprecatedDecorator.java b/smallrye-reactive-messaging-provider/src/test/java/io/smallrye/reactive/messaging/decorator/AppendingDeprecatedDecorator.java index d5edb22dd7..6730356da6 100644 --- a/smallrye-reactive-messaging-provider/src/test/java/io/smallrye/reactive/messaging/decorator/AppendingDeprecatedDecorator.java +++ b/smallrye-reactive-messaging-provider/src/test/java/io/smallrye/reactive/messaging/decorator/AppendingDeprecatedDecorator.java @@ -17,7 +17,7 @@ public Multi> decorate(Multi> publishe private Message appendString(Message message, String string) { if (message.getPayload() instanceof String) { String payload = (String) message.getPayload(); - return Message.of(payload + "-" + string, message::ack); + return Message.of(payload + "-" + string, metadata -> message.ack(metadata)); } else { return message; } diff --git a/smallrye-reactive-messaging-provider/src/test/java/io/smallrye/reactive/messaging/metadata/MessageTest.java b/smallrye-reactive-messaging-provider/src/test/java/io/smallrye/reactive/messaging/metadata/MessageTest.java index 97c01c876b..3dfea3206d 100644 --- a/smallrye-reactive-messaging-provider/src/test/java/io/smallrye/reactive/messaging/metadata/MessageTest.java +++ b/smallrye-reactive-messaging-provider/src/test/java/io/smallrye/reactive/messaging/metadata/MessageTest.java @@ -7,7 +7,7 @@ import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; -import java.util.function.Supplier; +import java.util.function.Function; import org.eclipse.microprofile.reactive.messaging.Message; import org.eclipse.microprofile.reactive.messaging.Metadata; @@ -40,11 +40,11 @@ public void testMessageCreation() { assertThat(message.getMetadata()).isEmpty(); assertThat(message.ack()).isCompleted(); - Supplier> supplier = () -> CompletableFuture.completedFuture(null); + Function> supplier = metadata -> CompletableFuture.completedFuture(null); message = Message.of("hello", supplier); assertThat(message.getPayload()).isEqualTo("hello"); assertThat(message.getMetadata()).isEmpty(); - assertThat(message.getAck()).isEqualTo(supplier); + assertThat(message.getAckWithMetadata()).isEqualTo(supplier); assertThat(message.ack()).isCompleted(); message = Message.of("hello", Metadata.of(new MyMetadata<>("v"))); @@ -57,14 +57,14 @@ public void testMessageCreation() { assertThat(message.getPayload()).isEqualTo("hello"); assertThat(message.getMetadata()).hasSize(1); assertThat(message.getMetadata(MyMetadata.class).map(m -> m.v)).hasValue("v"); - assertThat(message.getAck()).isEqualTo(supplier); + assertThat(message.getAckWithMetadata()).isEqualTo(supplier); assertThat(message.ack()).isCompleted(); - message = Message.of("hello", Metadata.of(new MyMetadata<>("v")), null); + message = Message.of("hello", Metadata.of(new MyMetadata<>("v"))); assertThat(message.getPayload()).isEqualTo("hello"); assertThat(message.getMetadata()).hasSize(1); assertThat(message.getMetadata(MyMetadata.class).map(m -> m.v)).hasValue("v"); - assertThat(message.getAck()).isNotEqualTo(supplier); + assertThat(message.getAckWithMetadata()).isNotEqualTo(supplier); assertThat(message.ack()).isCompleted(); } diff --git a/smallrye-reactive-messaging-pulsar/src/main/java/io/smallrye/reactive/messaging/pulsar/PulsarIncomingBatchMessage.java b/smallrye-reactive-messaging-pulsar/src/main/java/io/smallrye/reactive/messaging/pulsar/PulsarIncomingBatchMessage.java index 6fd3cc4ec8..55278c42cc 100644 --- a/smallrye-reactive-messaging-pulsar/src/main/java/io/smallrye/reactive/messaging/pulsar/PulsarIncomingBatchMessage.java +++ b/smallrye-reactive-messaging-pulsar/src/main/java/io/smallrye/reactive/messaging/pulsar/PulsarIncomingBatchMessage.java @@ -5,8 +5,8 @@ import java.util.*; import java.util.concurrent.CompletionStage; +import java.util.function.BiFunction; import java.util.function.Function; -import java.util.function.Supplier; import org.apache.pulsar.client.api.Message; import org.apache.pulsar.client.api.Messages; @@ -63,7 +63,7 @@ public Metadata getMetadata() { } @Override - public CompletionStage ack() { + public CompletionStage ack(Metadata metadata) { return Multi.createFrom().iterable(incomingMessages) .plug(stream -> { var txnMetadata = getMetadata(PulsarTransactionMetadata.class); @@ -73,12 +73,12 @@ public CompletionStage ack() { return stream; } }) - .onItem().transformToUniAndMerge(m -> Uni.createFrom().completionStage(m.getAck())) + .onItem().transformToUniAndMerge(m -> Uni.createFrom().completionStage(m.getAckWithMetadata().apply(metadata))) .toUni().subscribeAsCompletionStage(); } @Override - public Supplier> getAck() { + public Function> getAckWithMetadata() { return this::ack; } @@ -90,7 +90,7 @@ public CompletionStage nack(Throwable reason, Metadata metadata) { } @Override - public Function> getNack() { + public BiFunction> getNackWithMetadata() { return this::nack; } diff --git a/smallrye-reactive-messaging-pulsar/src/main/java/io/smallrye/reactive/messaging/pulsar/PulsarIncomingMessage.java b/smallrye-reactive-messaging-pulsar/src/main/java/io/smallrye/reactive/messaging/pulsar/PulsarIncomingMessage.java index a75f8910ef..f09a6b0828 100644 --- a/smallrye-reactive-messaging-pulsar/src/main/java/io/smallrye/reactive/messaging/pulsar/PulsarIncomingMessage.java +++ b/smallrye-reactive-messaging-pulsar/src/main/java/io/smallrye/reactive/messaging/pulsar/PulsarIncomingMessage.java @@ -6,8 +6,8 @@ import java.util.Map; import java.util.Objects; import java.util.concurrent.CompletionStage; +import java.util.function.BiFunction; import java.util.function.Function; -import java.util.function.Supplier; import org.apache.pulsar.client.api.Message; import org.apache.pulsar.client.api.MessageId; @@ -89,23 +89,23 @@ public Metadata getMetadata() { } @Override - public CompletionStage ack() { + public CompletionStage ack(Metadata metadata) { return ackHandler.handle(this).subscribeAsCompletionStage(); } @Override - public Supplier> getAck() { + public Function> getAckWithMetadata() { return this::ack; } @Override - public CompletionStage nack(Throwable reason, Metadata metadata) { - return nackHandler.handle(this, reason, metadata).subscribeAsCompletionStage(); + public BiFunction> getNackWithMetadata() { + return this::nack; } @Override - public Function> getNack() { - return this::nack; + public CompletionStage nack(Throwable reason, Metadata metadata) { + return nackHandler.handle(this, reason, metadata).subscribeAsCompletionStage(); } @Override diff --git a/smallrye-reactive-messaging-pulsar/src/main/java/io/smallrye/reactive/messaging/pulsar/PulsarOutgoingMessage.java b/smallrye-reactive-messaging-pulsar/src/main/java/io/smallrye/reactive/messaging/pulsar/PulsarOutgoingMessage.java index d550577503..32c1c8f379 100644 --- a/smallrye-reactive-messaging-pulsar/src/main/java/io/smallrye/reactive/messaging/pulsar/PulsarOutgoingMessage.java +++ b/smallrye-reactive-messaging-pulsar/src/main/java/io/smallrye/reactive/messaging/pulsar/PulsarOutgoingMessage.java @@ -4,8 +4,8 @@ import java.util.Map; import java.util.concurrent.CompletionStage; +import java.util.function.BiFunction; import java.util.function.Function; -import java.util.function.Supplier; import org.eclipse.microprofile.reactive.messaging.Message; import org.eclipse.microprofile.reactive.messaging.Metadata; @@ -17,18 +17,18 @@ public class PulsarOutgoingMessage implements PulsarMessage, ContextAwareM private final T payload; private final PulsarOutgoingMessageMetadata outgoingMessageMetadata; private final Metadata metadata; - private final Supplier> ack; - private final Function> nack; + private final Function> ack; + private final BiFunction> nack; public PulsarOutgoingMessage(T payload, - Supplier> ack, - Function> nack) { + Function> ack, + BiFunction> nack) { this(payload, ack, nack, PulsarOutgoingMessageMetadata.builder().build()); } public PulsarOutgoingMessage(T payload, - Supplier> ack, - Function> nack, + Function> ack, + BiFunction> nack, PulsarOutgoingMessageMetadata outgoingMessageMetadata) { this.payload = payload; this.ack = ack; @@ -38,7 +38,7 @@ public PulsarOutgoingMessage(T payload, } public static PulsarOutgoingMessage from(Message message) { - return new PulsarOutgoingMessage<>(message.getPayload(), message.getAck(), message.getNack(), + return new PulsarOutgoingMessage<>(message.getPayload(), message.getAckWithMetadata(), message.getNackWithMetadata(), message.getMetadata(PulsarOutgoingMessageMetadata.class) .orElseGet(() -> PulsarOutgoingMessageMetadata.builder().build())); } @@ -89,12 +89,13 @@ public Metadata getMetadata() { } @Override - public Supplier> getAck() { + public Function> getAckWithMetadata() { return this.ack; } @Override - public Function> getNack() { + public BiFunction> getNackWithMetadata() { return this.nack; } + } diff --git a/smallrye-reactive-messaging-pulsar/src/test/java/io/smallrye/reactive/messaging/pulsar/ConcurrentProcessorTest.java b/smallrye-reactive-messaging-pulsar/src/test/java/io/smallrye/reactive/messaging/pulsar/ConcurrentProcessorTest.java index fd3c016221..62b94e4da1 100644 --- a/smallrye-reactive-messaging-pulsar/src/test/java/io/smallrye/reactive/messaging/pulsar/ConcurrentProcessorTest.java +++ b/smallrye-reactive-messaging-pulsar/src/test/java/io/smallrye/reactive/messaging/pulsar/ConcurrentProcessorTest.java @@ -164,7 +164,7 @@ public Uni> process(PulsarMessage input) { int value = input.getPayload(); int next = value + 1; perThread.computeIfAbsent(Thread.currentThread(), t -> new CopyOnWriteArrayList<>()).add(next); - return Uni.createFrom().item(Message.of(next, input::ack)) + return Uni.createFrom().item(input.withPayload(next)) .onItem().delayIt().by(Duration.ofMillis(100)); } @@ -196,7 +196,7 @@ public Multi> process(Multi> multi) { int value = input.getPayload(); int next = value + 1; perThread.computeIfAbsent(Thread.currentThread(), t -> new CopyOnWriteArrayList<>()).add(next); - return Uni.createFrom().item(Message.of(next, input::ack)) + return Uni.createFrom().item(input.withPayload(next)) .onItem().delayIt().by(Duration.ofMillis(100)); }); } diff --git a/smallrye-reactive-messaging-pulsar/src/test/java/io/smallrye/reactive/messaging/pulsar/health/HealthCheckTest.java b/smallrye-reactive-messaging-pulsar/src/test/java/io/smallrye/reactive/messaging/pulsar/health/HealthCheckTest.java index 5d5213755d..4062e5cb95 100644 --- a/smallrye-reactive-messaging-pulsar/src/test/java/io/smallrye/reactive/messaging/pulsar/health/HealthCheckTest.java +++ b/smallrye-reactive-messaging-pulsar/src/test/java/io/smallrye/reactive/messaging/pulsar/health/HealthCheckTest.java @@ -151,7 +151,7 @@ public static class ProducingBean { @Outgoing("output") @Acknowledgment(Acknowledgment.Strategy.MANUAL) public Message process(Message input) { - return Message.of(input.getPayload() + 1, input::ack); + return input.withPayload(input.getPayload() + 1); } @Outgoing("data") diff --git a/smallrye-reactive-messaging-rabbitmq/src/main/java/io/smallrye/reactive/messaging/rabbitmq/IncomingRabbitMQMessage.java b/smallrye-reactive-messaging-rabbitmq/src/main/java/io/smallrye/reactive/messaging/rabbitmq/IncomingRabbitMQMessage.java index 9b5b8ccb1a..0667bccc7a 100644 --- a/smallrye-reactive-messaging-rabbitmq/src/main/java/io/smallrye/reactive/messaging/rabbitmq/IncomingRabbitMQMessage.java +++ b/smallrye-reactive-messaging-rabbitmq/src/main/java/io/smallrye/reactive/messaging/rabbitmq/IncomingRabbitMQMessage.java @@ -9,8 +9,8 @@ import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; +import java.util.function.BiFunction; import java.util.function.Function; -import java.util.function.Supplier; import org.eclipse.microprofile.reactive.messaging.Message; import org.eclipse.microprofile.reactive.messaging.Metadata; @@ -83,18 +83,17 @@ public CompletionStage handle(IncomingRabbitMQMessage message, Meta } @Override - public Supplier> getAck() { + public Function> getAckWithMetadata() { return this::ack; - //return () -> onAck.handle(this, context); } @Override - public Function> getNack() { + public BiFunction> getNackWithMetadata() { return this::nack; } @Override - public CompletionStage ack() { + public CompletionStage ack(Metadata metadata) { try { // We must switch to the context having created the message. // This context is passed when this instance of message is created. diff --git a/smallrye-reactive-messaging-rabbitmq/src/test/java/io/smallrye/reactive/messaging/rabbitmq/ConcurrentProcessorTest.java b/smallrye-reactive-messaging-rabbitmq/src/test/java/io/smallrye/reactive/messaging/rabbitmq/ConcurrentProcessorTest.java index 484679e1dc..51ce689ea2 100644 --- a/smallrye-reactive-messaging-rabbitmq/src/test/java/io/smallrye/reactive/messaging/rabbitmq/ConcurrentProcessorTest.java +++ b/smallrye-reactive-messaging-rabbitmq/src/test/java/io/smallrye/reactive/messaging/rabbitmq/ConcurrentProcessorTest.java @@ -155,7 +155,7 @@ public Uni> process(IncomingRabbitMQMessage input) { int value = Integer.parseInt(input.getPayload()); int next = value + 1; perThread.computeIfAbsent(Thread.currentThread(), t -> new CopyOnWriteArrayList<>()).add(next); - return Uni.createFrom().item(Message.of(next, input::ack)) + return Uni.createFrom().item(input.withPayload(next)) .onItem().delayIt().by(Duration.ofMillis(100)); } @@ -187,7 +187,7 @@ public Multi> process(Multi> mu int value = Integer.parseInt(input.getPayload()); int next = value + 1; perThread.computeIfAbsent(Thread.currentThread(), t -> new CopyOnWriteArrayList<>()).add(next); - return Uni.createFrom().item(Message.of(next, input::ack)) + return Uni.createFrom().item(input.withPayload(next)) .onItem().delayIt().by(Duration.ofMillis(100)); }); } diff --git a/smallrye-reactive-messaging-rabbitmq/src/test/java/io/smallrye/reactive/messaging/rabbitmq/ConsumptionBean.java b/smallrye-reactive-messaging-rabbitmq/src/test/java/io/smallrye/reactive/messaging/rabbitmq/ConsumptionBean.java index 71b8084b55..2bd8838971 100644 --- a/smallrye-reactive-messaging-rabbitmq/src/test/java/io/smallrye/reactive/messaging/rabbitmq/ConsumptionBean.java +++ b/smallrye-reactive-messaging-rabbitmq/src/test/java/io/smallrye/reactive/messaging/rabbitmq/ConsumptionBean.java @@ -32,7 +32,7 @@ public Message process(Message input) { } catch (ClassCastException e) { typeCastCounter.incrementAndGet(); } - return Message.of(value + 1, input::ack); + return input.withPayload(value + 1); } @Incoming("sink") diff --git a/smallrye-reactive-messaging-rabbitmq/src/test/java/io/smallrye/reactive/messaging/rabbitmq/NullProducingBean.java b/smallrye-reactive-messaging-rabbitmq/src/test/java/io/smallrye/reactive/messaging/rabbitmq/NullProducingBean.java index 79779f3d68..ebb2f3f3be 100644 --- a/smallrye-reactive-messaging-rabbitmq/src/test/java/io/smallrye/reactive/messaging/rabbitmq/NullProducingBean.java +++ b/smallrye-reactive-messaging-rabbitmq/src/test/java/io/smallrye/reactive/messaging/rabbitmq/NullProducingBean.java @@ -23,7 +23,7 @@ public class NullProducingBean { @Outgoing("sink") @Acknowledgment(Acknowledgment.Strategy.MANUAL) public Message process(Message input) { - return Message.of(null, input::ack); + return input.withPayload(null); } @Outgoing("data") diff --git a/smallrye-reactive-messaging-rabbitmq/src/test/java/io/smallrye/reactive/messaging/rabbitmq/ProducingBean.java b/smallrye-reactive-messaging-rabbitmq/src/test/java/io/smallrye/reactive/messaging/rabbitmq/ProducingBean.java index 729c6da2d9..90342987a2 100644 --- a/smallrye-reactive-messaging-rabbitmq/src/test/java/io/smallrye/reactive/messaging/rabbitmq/ProducingBean.java +++ b/smallrye-reactive-messaging-rabbitmq/src/test/java/io/smallrye/reactive/messaging/rabbitmq/ProducingBean.java @@ -23,7 +23,7 @@ public class ProducingBean { @Outgoing("sink") @Acknowledgment(Acknowledgment.Strategy.MANUAL) public Message process(Message input) { - return Message.of(input.getPayload() + 1, input::ack); + return input.withPayload(input.getPayload() + 1); } @Outgoing("data") From 1a7da6ddc90d59063a79d3493957a68c00ac6337 Mon Sep 17 00:00:00 2001 From: Ozan Gunalp Date: Wed, 19 Jul 2023 10:49:21 +0200 Subject: [PATCH 2/5] Added IncomingInterceptor to allow intercepting inbound messages --- .../messaging/IncomingInterceptor.java | 50 +++++++ documentation/mkdocs.yml | 2 +- .../src/main/docs/concepts/decorators.md | 40 +++-- .../interceptors/MyIncomingInterceptor.java | 26 ++++ ...ceptor.java => MyOutgoingInterceptor.java} | 2 +- .../IncomingInterceptorDecorator.java | 61 ++++++++ .../messaging/IncomingInterceptorTest.java | 140 ++++++++++++++++++ .../messaging/WeldTestBaseWithoutTails.java | 2 + .../resources/config/interceptor.properties | 1 + 9 files changed, 312 insertions(+), 12 deletions(-) create mode 100644 api/src/main/java/io/smallrye/reactive/messaging/IncomingInterceptor.java create mode 100644 documentation/src/main/java/interceptors/MyIncomingInterceptor.java rename documentation/src/main/java/interceptors/{MyInterceptor.java => MyOutgoingInterceptor.java} (91%) create mode 100644 smallrye-reactive-messaging-provider/src/main/java/io/smallrye/reactive/messaging/providers/IncomingInterceptorDecorator.java create mode 100644 smallrye-reactive-messaging-provider/src/test/java/io/smallrye/reactive/messaging/IncomingInterceptorTest.java diff --git a/api/src/main/java/io/smallrye/reactive/messaging/IncomingInterceptor.java b/api/src/main/java/io/smallrye/reactive/messaging/IncomingInterceptor.java new file mode 100644 index 0000000000..c0bbc33063 --- /dev/null +++ b/api/src/main/java/io/smallrye/reactive/messaging/IncomingInterceptor.java @@ -0,0 +1,50 @@ +package io.smallrye.reactive.messaging; + +import jakarta.enterprise.inject.spi.Prioritized; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.common.annotation.Experimental; + +/** + * Interceptor for incoming messages on connector channels. + *

+ * To register an outgoing interceptor, expose a managed bean, implementing this interface, + * and qualified with {@code @Identifier} with the targeted channel name. + *

+ * Only one interceptor is allowed to be bound for interception per incoming channel. + * When multiple interceptors are available, implementation should override the {@link #getPriority()} method. + */ +@Experimental("Smallrye-only feature") +public interface IncomingInterceptor extends Prioritized { + + @Override + default int getPriority() { + return -1; + } + + /** + * Called after message received + * + * @param message received message + * @return the message to dispatch for consumer methods, possibly mutated + */ + default Message onMessage(Message message) { + return message; + } + + /** + * Called after message acknowledgment + * + * @param message acknowledged message + */ + void onMessageAck(Message message); + + /** + * Called after message negative-acknowledgement + * + * @param message message to negative-acknowledge + * @param failure failure + */ + void onMessageNack(Message message, Throwable failure); +} diff --git a/documentation/mkdocs.yml b/documentation/mkdocs.yml index 0ff498ca4c..c4472d6b1d 100644 --- a/documentation/mkdocs.yml +++ b/documentation/mkdocs.yml @@ -21,7 +21,7 @@ nav: - 'Skipping Messages': concepts/skipping.md - 'Message Converters': concepts/converters.md - 'Keyed Streams': concepts/keyed-multi.md - - 'Channel Decorators': concepts/decorators.md + - 'Channel Decorators and Interceptors': concepts/decorators.md - 'Broadcast' : concepts/broadcast.md - 'Merge channels' : concepts/merge.md - 'Incoming Channel Concurrency' : concepts/incoming-concurrency.md diff --git a/documentation/src/main/docs/concepts/decorators.md b/documentation/src/main/docs/concepts/decorators.md index 2e22d7fcb1..1e03e8cda8 100644 --- a/documentation/src/main/docs/concepts/decorators.md +++ b/documentation/src/main/docs/concepts/decorators.md @@ -32,18 +32,42 @@ obtained using the `jakarta.enterprise.inject.spi.Prioritized#getPriority` metho The `SubscriberDecorator` receive a list of channel names because `@Incoming` annotation is repeatable and consuming methods can be linked to multiple channels. -## Intercepting Outgoing Messages +## Intercepting Incoming and Outgoing Messages -Decorators can be used to intercept and alter messages, both on incoming and outgoing channels. -Smallrye Reactive Messaging provides a `SubscriberDecorator` by default to allow intercepting outgoing messages for a specific channel. +Decorators (`PublisherDecorator` and `SubscriberDecorator`) can be used to intercept and alter messages, both on incoming and outgoing channels. + +Smallrye Reactive Messaging allows defining intercepting incoming and outgoing messages for a specific channel, using +`IncomingInterceptor` and `OutgoingInterceptor` respectively. -To provide an outgoing interceptor implement a bean exposing the interface {{ javadoc('io.smallrye.reactive.messaging.OutgoingInterceptor') }}, qualified with a `@Identifier` with the channel name to intercept. Only one interceptor is allowed to be bound for interception per outgoing channel. If no interceptors are found with a `@Identifier` but a `@Default` one is available, it is used. When multiple interceptors are available, the bean with the highest priority is used. +### `IncomingInterceptor` + +To provide an incoming interceptor implement a bean exposing the interface {{ javadoc('io.smallrye.reactive.messaging.IncomingInterceptor') }}, qualified with a `@Identifier` with the channel name to intercept. + ``` java -{{ insert('interceptors/MyInterceptor.java') }} +{{ insert('interceptors/MyIncomingInterceptor.java') }} +``` + +An `IncomingInterceptor` can implement these three methods: + +- `Message onMessage(Message message)` : Called after receiving the message from an incoming connector. + The message can be altered by returning a new message from this method. The modified message will be consumed in incoming channels. +- `void onMessageAck(Message message)` : Called after message acknowledgment. +- `void onMessageNack(Message message, Throwable failure)` : Called after message negative-acknowledgment. + +!!!Note + If you are willing to adapt an incoming message payload to fit a consuming method receiving type, + you can use [`MessageConverter`](./converters)s. + +### `OutgoingInterceptor` + +To provide an outgoing interceptor implement a bean exposing the interface {{ javadoc('io.smallrye.reactive.messaging.OutgoingInterceptor') }}, qualified with a `@Identifier` with the channel name to intercept. + +``` java +{{ insert('interceptors/MyOutgoingInterceptor.java') }} ``` An `OutgoingInterceptor` can implement these three methods: @@ -52,8 +76,4 @@ An `OutgoingInterceptor` can implement these three methods: The message can be altered by returning a new message from this method. - `void onMessageAck(Message message)` : Called after message acknowledgment. This callback can access `OutgoingMessageMetadata` which will hold the result of the message transmission to the broker, if supported by the connector. This is only supported by MQTT and Kafka connectors. -- `void onMessageNack(Message message, Throwable failure)` : Called before message negative-acknowledgment. - -!!!Note - If you are willing to adapt an incoming message payload to fit a consuming method receiving type, - you can use `MessageConverter`s. +- `void onMessageNack(Message message, Throwable failure)` : Called after message negative-acknowledgment. diff --git a/documentation/src/main/java/interceptors/MyIncomingInterceptor.java b/documentation/src/main/java/interceptors/MyIncomingInterceptor.java new file mode 100644 index 0000000000..4498263759 --- /dev/null +++ b/documentation/src/main/java/interceptors/MyIncomingInterceptor.java @@ -0,0 +1,26 @@ +package interceptors; + +import io.smallrye.common.annotation.Identifier; +import io.smallrye.reactive.messaging.IncomingInterceptor; +import jakarta.enterprise.context.ApplicationScoped; +import org.eclipse.microprofile.reactive.messaging.Message; + +@Identifier("channel-a") +@ApplicationScoped +public class MyIncomingInterceptor implements IncomingInterceptor { + + @Override + public Message onMessage(Message message) { + return message.withPayload("changed " + message.getPayload()); + } + + @Override + public void onMessageAck(Message message) { + // Called after message ack + } + + @Override + public void onMessageNack(Message message, Throwable failure) { + // Called after message nack + } +} diff --git a/documentation/src/main/java/interceptors/MyInterceptor.java b/documentation/src/main/java/interceptors/MyOutgoingInterceptor.java similarity index 91% rename from documentation/src/main/java/interceptors/MyInterceptor.java rename to documentation/src/main/java/interceptors/MyOutgoingInterceptor.java index 8712531d9a..6efdc88358 100644 --- a/documentation/src/main/java/interceptors/MyInterceptor.java +++ b/documentation/src/main/java/interceptors/MyOutgoingInterceptor.java @@ -10,7 +10,7 @@ @Identifier("channel-a") @ApplicationScoped -public class MyInterceptor implements OutgoingInterceptor { +public class MyOutgoingInterceptor implements OutgoingInterceptor { @Override public Message onMessage(Message message) { diff --git a/smallrye-reactive-messaging-provider/src/main/java/io/smallrye/reactive/messaging/providers/IncomingInterceptorDecorator.java b/smallrye-reactive-messaging-provider/src/main/java/io/smallrye/reactive/messaging/providers/IncomingInterceptorDecorator.java new file mode 100644 index 0000000000..24b20a2631 --- /dev/null +++ b/smallrye-reactive-messaging-provider/src/main/java/io/smallrye/reactive/messaging/providers/IncomingInterceptorDecorator.java @@ -0,0 +1,61 @@ +package io.smallrye.reactive.messaging.providers; + +import static io.smallrye.reactive.messaging.providers.helpers.CDIUtils.getInstanceById; +import static io.smallrye.reactive.messaging.providers.helpers.CDIUtils.getSortedInstances; + +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Any; +import jakarta.enterprise.inject.Default; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.unchecked.Unchecked; +import io.smallrye.reactive.messaging.IncomingInterceptor; +import io.smallrye.reactive.messaging.PublisherDecorator; + +/** + * Decorator to support {@link IncomingInterceptor}s. + * High priority value to be called after other decorators. + */ +@ApplicationScoped +public class IncomingInterceptorDecorator implements PublisherDecorator { + + @Any + @Inject + Instance interceptors; + + @Override + public int getPriority() { + return 500; + } + + @Override + public Multi> decorate(Multi> publisher, String channelName, + boolean isConnector) { + Multi> multi = publisher; + if (isConnector) { + Instance instances = getInstanceById(interceptors, channelName); + if (instances.isUnsatisfied()) { + instances = interceptors.select().select(Default.Literal.INSTANCE); + } + List matching = getSortedInstances(instances); + if (!matching.isEmpty()) { + IncomingInterceptor interceptor = matching.get(0); + multi = multi.map(m -> { + Message before = interceptor.onMessage(m); + Message withAck = before.withAck(() -> before.ack() + .thenAccept(Unchecked.consumer(x -> interceptor.onMessageAck(before)))); + return withAck.withNack(t -> withAck.nack(t) + .thenAccept(Unchecked.consumer(x -> interceptor.onMessageNack(withAck, t)))); + }); + } + } + return multi; + } + +} diff --git a/smallrye-reactive-messaging-provider/src/test/java/io/smallrye/reactive/messaging/IncomingInterceptorTest.java b/smallrye-reactive-messaging-provider/src/test/java/io/smallrye/reactive/messaging/IncomingInterceptorTest.java new file mode 100644 index 0000000000..9104bc59e4 --- /dev/null +++ b/smallrye-reactive-messaging-provider/src/test/java/io/smallrye/reactive/messaging/IncomingInterceptorTest.java @@ -0,0 +1,140 @@ +package io.smallrye.reactive.messaging; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import java.util.List; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.smallrye.common.annotation.Identifier; + +public class IncomingInterceptorTest extends WeldTestBaseWithoutTails { + + @BeforeEach + void setupConfig() { + installConfig("src/test/resources/config/interceptor.properties"); + } + + @Test + public void testIncomingInterceptorWithIdentifier() { + addBeanClass(DefaultInterceptor.class); + addBeanClass(InterceptorBean.class); + addBeanClass(MyMessageConsumer.class); + + initialize(); + + InterceptorBean interceptor = container.getBeanManager().createInstance() + .select(InterceptorBean.class, Identifier.Literal.of("B")).get(); + MyMessageConsumer consumerBean = get(MyMessageConsumer.class); + await().until(() -> interceptor.acks() == 2); + await().until(() -> interceptor.nacks() == 1); + assertThat(interceptor.interceptedMessages()).isEqualTo(3); + assertThat(consumerBean.received()) + .isNotEmpty() + .allSatisfy(o -> assertThat(o).isInstanceOf(InterceptorBean.class)); + } + + @Test + public void testIncomingInterceptorWithDefault() { + addBeanClass(DefaultInterceptor.class); + addBeanClass(MyMessageConsumer.class); + + initialize(); + + DefaultInterceptor interceptor = get(DefaultInterceptor.class); + await().until(() -> interceptor.acks() == 2); + await().until(() -> interceptor.nacks() == 1); + } + + @ApplicationScoped + public static class MyMessageConsumer { + + List receivedMetadata = new CopyOnWriteArrayList<>(); + + @Incoming("B") + public CompletionStage consume(Message msg) { + msg.getMetadata(InterceptorBean.class).ifPresent(receivedMetadata::add); + if (msg.getPayload() == 3) { + return msg.nack(new RuntimeException("boom!")); + } + return msg.ack(); + } + + public List received() { + return receivedMetadata; + } + } + + @Identifier("B") + @ApplicationScoped + static class InterceptorBean implements IncomingInterceptor { + + final AtomicInteger acks = new AtomicInteger(); + final AtomicInteger nacks = new AtomicInteger(); + final AtomicInteger interceptedMessages = new AtomicInteger(); + + @Override + public Message onMessage(Message message) { + interceptedMessages.incrementAndGet(); + return message.addMetadata(this); + } + + @Override + public void onMessageAck(Message message) { + acks.incrementAndGet(); + } + + @Override + public void onMessageNack(Message message, Throwable failure) { + nacks.incrementAndGet(); + } + + public int interceptedMessages() { + return interceptedMessages.get(); + } + + public int acks() { + return acks.get(); + } + + public int nacks() { + return nacks.get(); + } + + } + + @ApplicationScoped + static class DefaultInterceptor implements IncomingInterceptor { + + final AtomicInteger acks = new AtomicInteger(); + final AtomicInteger nacks = new AtomicInteger(); + + @Override + public void onMessageAck(Message message) { + acks.incrementAndGet(); + } + + @Override + public void onMessageNack(Message message, Throwable failure) { + nacks.incrementAndGet(); + } + + public int acks() { + return acks.get(); + } + + public int nacks() { + return nacks.get(); + } + } + +} diff --git a/smallrye-reactive-messaging-provider/src/test/java/io/smallrye/reactive/messaging/WeldTestBaseWithoutTails.java b/smallrye-reactive-messaging-provider/src/test/java/io/smallrye/reactive/messaging/WeldTestBaseWithoutTails.java index c59744c955..dfc8116380 100644 --- a/smallrye-reactive-messaging-provider/src/test/java/io/smallrye/reactive/messaging/WeldTestBaseWithoutTails.java +++ b/smallrye-reactive-messaging-provider/src/test/java/io/smallrye/reactive/messaging/WeldTestBaseWithoutTails.java @@ -9,6 +9,7 @@ import java.util.Collections; import java.util.List; +import io.smallrye.reactive.messaging.providers.IncomingInterceptorDecorator; import jakarta.enterprise.inject.se.SeContainer; import jakarta.enterprise.inject.se.SeContainerInitializer; import jakarta.enterprise.inject.spi.Extension; @@ -119,6 +120,7 @@ public void setUp() { MutinyEmitterFactoryImpl.class, LegacyEmitterFactoryImpl.class, OutgoingInterceptorDecorator.class, + IncomingInterceptorDecorator.class, // SmallRye config io.smallrye.config.inject.ConfigProducer.class); diff --git a/smallrye-reactive-messaging-provider/src/test/resources/config/interceptor.properties b/smallrye-reactive-messaging-provider/src/test/resources/config/interceptor.properties index afed95f27e..d87e0c415c 100644 --- a/smallrye-reactive-messaging-provider/src/test/resources/config/interceptor.properties +++ b/smallrye-reactive-messaging-provider/src/test/resources/config/interceptor.properties @@ -1,3 +1,4 @@ # You should not be able to use the same channel name in an outgoing and incoming configuration mp.messaging.outgoing.A.connector=dummy +mp.messaging.incoming.B.connector=dummy From 040558c3e5f9a5ce3aef6f52c74d3c07f162b880 Mon Sep 17 00:00:00 2001 From: Ozan Gunalp Date: Wed, 19 Jul 2023 10:53:06 +0200 Subject: [PATCH 3/5] Change message interceptor method names OutgoingInterceptor#onMessage deprecated and replaced by OutgoingInterceptor#beforeMessageSend --- .../messaging/IncomingInterceptor.java | 2 +- .../messaging/OutgoingInterceptor.java | 12 +++++++ .../src/main/docs/concepts/decorators.md | 4 +-- .../interceptors/MyIncomingInterceptor.java | 8 +++-- .../IncomingInterceptorDecorator.java | 8 ++--- .../OutgoingInterceptorDecorator.java | 2 +- .../messaging/IncomingInterceptorTest.java | 35 ++++++++++++++++--- .../messaging/WeldTestBaseWithoutTails.java | 2 +- 8 files changed, 57 insertions(+), 16 deletions(-) diff --git a/api/src/main/java/io/smallrye/reactive/messaging/IncomingInterceptor.java b/api/src/main/java/io/smallrye/reactive/messaging/IncomingInterceptor.java index c0bbc33063..288257835e 100644 --- a/api/src/main/java/io/smallrye/reactive/messaging/IncomingInterceptor.java +++ b/api/src/main/java/io/smallrye/reactive/messaging/IncomingInterceptor.java @@ -29,7 +29,7 @@ default int getPriority() { * @param message received message * @return the message to dispatch for consumer methods, possibly mutated */ - default Message onMessage(Message message) { + default Message afterMessageReceive(Message message) { return message; } diff --git a/api/src/main/java/io/smallrye/reactive/messaging/OutgoingInterceptor.java b/api/src/main/java/io/smallrye/reactive/messaging/OutgoingInterceptor.java index cd8b61b15f..f39ffd7937 100644 --- a/api/src/main/java/io/smallrye/reactive/messaging/OutgoingInterceptor.java +++ b/api/src/main/java/io/smallrye/reactive/messaging/OutgoingInterceptor.java @@ -28,11 +28,23 @@ default int getPriority() { * * @param message message to send * @return the message to send, possibly mutated + * @deprecated use {@link #beforeMessageSend(Message)} */ + @Deprecated(since = "4.12.0") default Message onMessage(Message message) { return message; } + /** + * Called before message transmission + * + * @param message message to send + * @return the message to send, possibly mutated + */ + default Message beforeMessageSend(Message message) { + return onMessage(message); + } + /** * Called after message acknowledgment * diff --git a/documentation/src/main/docs/concepts/decorators.md b/documentation/src/main/docs/concepts/decorators.md index 1e03e8cda8..499478c8d5 100644 --- a/documentation/src/main/docs/concepts/decorators.md +++ b/documentation/src/main/docs/concepts/decorators.md @@ -53,7 +53,7 @@ To provide an incoming interceptor implement a bean exposing the interface {{ ja An `IncomingInterceptor` can implement these three methods: -- `Message onMessage(Message message)` : Called after receiving the message from an incoming connector. +- `Message afterMessageReceive(Message message)` : Called after receiving the message from an incoming connector. The message can be altered by returning a new message from this method. The modified message will be consumed in incoming channels. - `void onMessageAck(Message message)` : Called after message acknowledgment. - `void onMessageNack(Message message, Throwable failure)` : Called after message negative-acknowledgment. @@ -72,7 +72,7 @@ To provide an outgoing interceptor implement a bean exposing the interface {{ ja An `OutgoingInterceptor` can implement these three methods: -- `Message onMessage(Message message)` : Called before passing the message to the outgoing connector for transmission. +- `Message beforeMessageSend(Message message)` : Called before passing the message to the outgoing connector for transmission. The message can be altered by returning a new message from this method. - `void onMessageAck(Message message)` : Called after message acknowledgment. This callback can access `OutgoingMessageMetadata` which will hold the result of the message transmission to the broker, if supported by the connector. This is only supported by MQTT and Kafka connectors. diff --git a/documentation/src/main/java/interceptors/MyIncomingInterceptor.java b/documentation/src/main/java/interceptors/MyIncomingInterceptor.java index 4498263759..c322a75532 100644 --- a/documentation/src/main/java/interceptors/MyIncomingInterceptor.java +++ b/documentation/src/main/java/interceptors/MyIncomingInterceptor.java @@ -1,16 +1,18 @@ package interceptors; -import io.smallrye.common.annotation.Identifier; -import io.smallrye.reactive.messaging.IncomingInterceptor; import jakarta.enterprise.context.ApplicationScoped; + import org.eclipse.microprofile.reactive.messaging.Message; +import io.smallrye.common.annotation.Identifier; +import io.smallrye.reactive.messaging.IncomingInterceptor; + @Identifier("channel-a") @ApplicationScoped public class MyIncomingInterceptor implements IncomingInterceptor { @Override - public Message onMessage(Message message) { + public Message afterMessageReceive(Message message) { return message.withPayload("changed " + message.getPayload()); } diff --git a/smallrye-reactive-messaging-provider/src/main/java/io/smallrye/reactive/messaging/providers/IncomingInterceptorDecorator.java b/smallrye-reactive-messaging-provider/src/main/java/io/smallrye/reactive/messaging/providers/IncomingInterceptorDecorator.java index 24b20a2631..6049d51c9b 100644 --- a/smallrye-reactive-messaging-provider/src/main/java/io/smallrye/reactive/messaging/providers/IncomingInterceptorDecorator.java +++ b/smallrye-reactive-messaging-provider/src/main/java/io/smallrye/reactive/messaging/providers/IncomingInterceptorDecorator.java @@ -47,11 +47,11 @@ public Multi> decorate(Multi> publishe if (!matching.isEmpty()) { IncomingInterceptor interceptor = matching.get(0); multi = multi.map(m -> { - Message before = interceptor.onMessage(m); - Message withAck = before.withAck(() -> before.ack() + Message before = interceptor.afterMessageReceive(m); + Message withAck = before.withAckWithMetadata(metadata -> before.ack(metadata) .thenAccept(Unchecked.consumer(x -> interceptor.onMessageAck(before)))); - return withAck.withNack(t -> withAck.nack(t) - .thenAccept(Unchecked.consumer(x -> interceptor.onMessageNack(withAck, t)))); + return withAck.withNackWithMetadata((reason, metadata) -> withAck.nack(reason, metadata) + .thenAccept(Unchecked.consumer(x -> interceptor.onMessageNack(withAck, reason)))); }); } } diff --git a/smallrye-reactive-messaging-provider/src/main/java/io/smallrye/reactive/messaging/providers/OutgoingInterceptorDecorator.java b/smallrye-reactive-messaging-provider/src/main/java/io/smallrye/reactive/messaging/providers/OutgoingInterceptorDecorator.java index 0c3cfcb29d..7b2d267d91 100644 --- a/smallrye-reactive-messaging-provider/src/main/java/io/smallrye/reactive/messaging/providers/OutgoingInterceptorDecorator.java +++ b/smallrye-reactive-messaging-provider/src/main/java/io/smallrye/reactive/messaging/providers/OutgoingInterceptorDecorator.java @@ -48,7 +48,7 @@ public Multi> decorate(Multi> toBeSubs if (!matching.isEmpty()) { OutgoingInterceptor interceptor = matching.get(0); multi = multi.map(m -> { - Message before = interceptor.onMessage(m.addMetadata(new OutgoingMessageMetadata<>())); + Message before = interceptor.beforeMessageSend(m.addMetadata(new OutgoingMessageMetadata<>())); Message withAck = before.withAckWithMetadata((metadata) -> before.ack(before.getMetadata()) .thenAccept(Unchecked.consumer(x -> interceptor.onMessageAck(before)))); return withAck.withNackWithMetadata((throwable, metadata) -> withAck.nack(throwable, metadata) diff --git a/smallrye-reactive-messaging-provider/src/test/java/io/smallrye/reactive/messaging/IncomingInterceptorTest.java b/smallrye-reactive-messaging-provider/src/test/java/io/smallrye/reactive/messaging/IncomingInterceptorTest.java index 9104bc59e4..76bee41c74 100644 --- a/smallrye-reactive-messaging-provider/src/test/java/io/smallrye/reactive/messaging/IncomingInterceptorTest.java +++ b/smallrye-reactive-messaging-provider/src/test/java/io/smallrye/reactive/messaging/IncomingInterceptorTest.java @@ -12,6 +12,7 @@ import org.eclipse.microprofile.reactive.messaging.Incoming; import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Metadata; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -41,6 +42,9 @@ public void testIncomingInterceptorWithIdentifier() { assertThat(consumerBean.received()) .isNotEmpty() .allSatisfy(o -> assertThat(o).isInstanceOf(InterceptorBean.class)); + assertThat(interceptor.receivedMetadata()) + .isNotEmpty() + .allSatisfy(o -> assertThat(o).isInstanceOf(MyMessageConsumer.class)); } @Test @@ -64,9 +68,9 @@ public static class MyMessageConsumer { public CompletionStage consume(Message msg) { msg.getMetadata(InterceptorBean.class).ifPresent(receivedMetadata::add); if (msg.getPayload() == 3) { - return msg.nack(new RuntimeException("boom!")); + return msg.nack(new RuntimeException("boom!"), Metadata.of(this)); } - return msg.ack(); + return msg.ack(Metadata.of(this)); } public List received() { @@ -82,10 +86,29 @@ static class InterceptorBean implements IncomingInterceptor { final AtomicInteger nacks = new AtomicInteger(); final AtomicInteger interceptedMessages = new AtomicInteger(); + final List receivedMetadata = new CopyOnWriteArrayList<>(); + @Override - public Message onMessage(Message message) { + public Message afterMessageReceive(Message message) { interceptedMessages.incrementAndGet(); - return message.addMetadata(this); + return message.addMetadata(this) + .withNack(throwable -> { + System.out.println("nack1"); + return message.nack(throwable); + }).thenApply(msg -> msg.withNackWithMetadata((throwable, metadata) -> msg.nack(throwable, metadata) + .thenAccept(unused -> { + System.out.println(throwable.getMessage()); + for (Object metadatum : metadata) { + System.out.println("nack2 " + metadatum.getClass()); + receivedMetadata.add(metadatum); + } + }))) + .thenApply(msg -> msg.withAckWithMetadata(metadata -> { + for (Object metadatum : metadata) { + System.out.println("ack " + metadatum); + } + return message.ack(metadata); + })); } @Override @@ -110,6 +133,10 @@ public int nacks() { return nacks.get(); } + public List receivedMetadata() { + return receivedMetadata; + } + } @ApplicationScoped diff --git a/smallrye-reactive-messaging-provider/src/test/java/io/smallrye/reactive/messaging/WeldTestBaseWithoutTails.java b/smallrye-reactive-messaging-provider/src/test/java/io/smallrye/reactive/messaging/WeldTestBaseWithoutTails.java index dfc8116380..dcd1257547 100644 --- a/smallrye-reactive-messaging-provider/src/test/java/io/smallrye/reactive/messaging/WeldTestBaseWithoutTails.java +++ b/smallrye-reactive-messaging-provider/src/test/java/io/smallrye/reactive/messaging/WeldTestBaseWithoutTails.java @@ -9,7 +9,6 @@ import java.util.Collections; import java.util.List; -import io.smallrye.reactive.messaging.providers.IncomingInterceptorDecorator; import jakarta.enterprise.inject.se.SeContainer; import jakarta.enterprise.inject.se.SeContainerInitializer; import jakarta.enterprise.inject.spi.Extension; @@ -21,6 +20,7 @@ import io.smallrye.config.SmallRyeConfigProviderResolver; import io.smallrye.mutiny.Multi; +import io.smallrye.reactive.messaging.providers.IncomingInterceptorDecorator; import io.smallrye.reactive.messaging.providers.MediatorFactory; import io.smallrye.reactive.messaging.providers.OutgoingInterceptorDecorator; import io.smallrye.reactive.messaging.providers.connectors.ExecutionHolder; From 463392b2b49161daa6ee9cddac459291584f9afd Mon Sep 17 00:00:00 2001 From: Ozan Gunalp Date: Thu, 12 Oct 2023 18:02:05 +0200 Subject: [PATCH 4/5] Deprecate PublisherDecorator#decorate(String, String, boolean) --- .../smallrye/reactive/messaging/PublisherDecorator.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/api/src/main/java/io/smallrye/reactive/messaging/PublisherDecorator.java b/api/src/main/java/io/smallrye/reactive/messaging/PublisherDecorator.java index 5ae80a13ac..5e79fef5b5 100644 --- a/api/src/main/java/io/smallrye/reactive/messaging/PublisherDecorator.java +++ b/api/src/main/java/io/smallrye/reactive/messaging/PublisherDecorator.java @@ -32,9 +32,13 @@ public interface PublisherDecorator extends Prioritized { * @param channelName the name of the channel to which this publisher publishes * @param isConnector {@code true} if decorated channel is connector * @return the extended multi + * @deprecated replaced with {@link #decorate(Multi, List, boolean)} */ - Multi> decorate(Multi> publisher, String channelName, - boolean isConnector); + @Deprecated(since = "4.10.1") + default Multi> decorate(Multi> publisher, String channelName, + boolean isConnector) { + return publisher; + } /** * Decorate a Multi From 86664d4384d03e99e05a4b7dff4c9d02951e7f3a Mon Sep 17 00:00:00 2001 From: Ozan Gunalp Date: Thu, 12 Oct 2023 18:40:29 +0200 Subject: [PATCH 5/5] Message Observation API Decorator to allow defining an observation collector to observe message acks, nacks and completion duration from inbound and outbound channels --- .../messaging/PublisherDecorator.java | 2 +- .../DefaultMessageObservation.java | 77 +++++ .../observation/MessageObservation.java | 56 ++++ .../MessageObservationCollector.java | 44 +++ .../observation/ObservationContext.java | 22 ++ documentation/mkdocs.yml | 1 + .../src/main/docs/concepts/observability.md | 30 ++ .../ContextMessageObservationCollector.java | 57 ++++ .../SimpleMessageObservationCollector.java | 21 ++ .../messaging/kafka/base/WeldTestBase.java | 4 + .../kafka/metrics/ObservationTest.java | 251 +++++++++++++++ .../extension/ObservationDecorator.java | 104 ++++++ .../OutgoingObservationDecorator.java | 43 +++ .../messaging/WeldTestBaseWithoutTails.java | 6 +- .../inject/EmitterObservationTest.java | 131 ++++++++ .../providers/connectors/ObservationTest.java | 301 ++++++++++++++++++ .../resources/config/observation.properties | 4 + 17 files changed, 1152 insertions(+), 2 deletions(-) create mode 100644 api/src/main/java/io/smallrye/reactive/messaging/observation/DefaultMessageObservation.java create mode 100644 api/src/main/java/io/smallrye/reactive/messaging/observation/MessageObservation.java create mode 100644 api/src/main/java/io/smallrye/reactive/messaging/observation/MessageObservationCollector.java create mode 100644 api/src/main/java/io/smallrye/reactive/messaging/observation/ObservationContext.java create mode 100644 documentation/src/main/docs/concepts/observability.md create mode 100644 documentation/src/main/java/observability/ContextMessageObservationCollector.java create mode 100644 documentation/src/main/java/observability/SimpleMessageObservationCollector.java create mode 100644 smallrye-reactive-messaging-kafka/src/test/java/io/smallrye/reactive/messaging/kafka/metrics/ObservationTest.java create mode 100644 smallrye-reactive-messaging-provider/src/main/java/io/smallrye/reactive/messaging/providers/extension/ObservationDecorator.java create mode 100644 smallrye-reactive-messaging-provider/src/main/java/io/smallrye/reactive/messaging/providers/extension/OutgoingObservationDecorator.java create mode 100644 smallrye-reactive-messaging-provider/src/test/java/io/smallrye/reactive/messaging/inject/EmitterObservationTest.java create mode 100644 smallrye-reactive-messaging-provider/src/test/java/io/smallrye/reactive/messaging/providers/connectors/ObservationTest.java create mode 100644 smallrye-reactive-messaging-provider/src/test/resources/config/observation.properties diff --git a/api/src/main/java/io/smallrye/reactive/messaging/PublisherDecorator.java b/api/src/main/java/io/smallrye/reactive/messaging/PublisherDecorator.java index 5e79fef5b5..41cef8bb44 100644 --- a/api/src/main/java/io/smallrye/reactive/messaging/PublisherDecorator.java +++ b/api/src/main/java/io/smallrye/reactive/messaging/PublisherDecorator.java @@ -34,7 +34,7 @@ public interface PublisherDecorator extends Prioritized { * @return the extended multi * @deprecated replaced with {@link #decorate(Multi, List, boolean)} */ - @Deprecated(since = "4.10.1") + @Deprecated(since = "4.12.0") default Multi> decorate(Multi> publisher, String channelName, boolean isConnector) { return publisher; diff --git a/api/src/main/java/io/smallrye/reactive/messaging/observation/DefaultMessageObservation.java b/api/src/main/java/io/smallrye/reactive/messaging/observation/DefaultMessageObservation.java new file mode 100644 index 0000000000..d8878fc08f --- /dev/null +++ b/api/src/main/java/io/smallrye/reactive/messaging/observation/DefaultMessageObservation.java @@ -0,0 +1,77 @@ +package io.smallrye.reactive.messaging.observation; + +import java.time.Duration; + +import org.eclipse.microprofile.reactive.messaging.Message; + +/** + * The default implementation based on system nano time. + */ +public class DefaultMessageObservation implements MessageObservation { + + // metadata + private final String channelName; + + // time + private final long creation; + protected volatile long completion; + + // status + protected volatile boolean done; + protected volatile Throwable nackReason; + + public DefaultMessageObservation(String channelName) { + this(channelName, System.nanoTime()); + } + + public DefaultMessageObservation(String channelName, long creationTime) { + this.channelName = channelName; + this.creation = creationTime; + } + + @Override + public String getChannel() { + return channelName; + } + + @Override + public long getCreationTime() { + return creation; + } + + @Override + public long getCompletionTime() { + return completion; + } + + @Override + public boolean isDone() { + return done || nackReason != null; + } + + @Override + public Throwable getReason() { + return nackReason; + } + + @Override + public Duration getCompletionDuration() { + if (isDone()) { + return Duration.ofNanos(completion - creation); + } + return null; + } + + @Override + public void onMessageAck(Message message) { + completion = System.nanoTime(); + done = true; + } + + @Override + public void onMessageNack(Message message, Throwable reason) { + completion = System.nanoTime(); + nackReason = reason; + } + +} diff --git a/api/src/main/java/io/smallrye/reactive/messaging/observation/MessageObservation.java b/api/src/main/java/io/smallrye/reactive/messaging/observation/MessageObservation.java new file mode 100644 index 0000000000..8045d7c79c --- /dev/null +++ b/api/src/main/java/io/smallrye/reactive/messaging/observation/MessageObservation.java @@ -0,0 +1,56 @@ +package io.smallrye.reactive.messaging.observation; + +import java.time.Duration; + +import org.eclipse.microprofile.reactive.messaging.Message; + +/** + * The message observation contract + */ +public interface MessageObservation { + + /** + * @return the channel name of the message + */ + String getChannel(); + + /** + * @return the creation time of the message in system nanos + */ + long getCreationTime(); + + /** + * @return the completion time of the message in system nanos + */ + long getCompletionTime(); + + /** + * + * @return the duration between creation and the completion time, null if message processing is not completed + */ + Duration getCompletionDuration(); + + /** + * + * @return {@code true} if the message processing is completed with acknowledgement or negative acknowledgement + */ + boolean isDone(); + + /** + * @return the negative acknowledgement reason + */ + Throwable getReason(); + + /** + * Notify the observation of acknowledgement event + * + */ + void onMessageAck(Message message); + + /** + * Notify the observation of negative acknowledgement event + * + * @param reason the reason of the negative acknowledgement + */ + void onMessageNack(Message message, Throwable reason); +} diff --git a/api/src/main/java/io/smallrye/reactive/messaging/observation/MessageObservationCollector.java b/api/src/main/java/io/smallrye/reactive/messaging/observation/MessageObservationCollector.java new file mode 100644 index 0000000000..2abd059d85 --- /dev/null +++ b/api/src/main/java/io/smallrye/reactive/messaging/observation/MessageObservationCollector.java @@ -0,0 +1,44 @@ +package io.smallrye.reactive.messaging.observation; + +import org.eclipse.microprofile.reactive.messaging.Message; + +/** + * The observation collector is called with the new message and returns the message observation that will be used + * to observe messages from their creation until the ack or the nack event + *

+ * + *

+ * The implementation of this interface must be a CDI managed bean in order to be discovered + * + * @param the type of the observation context + */ +public interface MessageObservationCollector { + + /** + * Initialize observation for the given channel + * If {@code null} is returned the observation for the given channel is disabled + * + * @param channel the channel of the message + * @param incoming whether the channel is incoming or outgoing + * @param emitter whether the channel is an emitter + * @return the observation context + */ + default T initObservation(String channel, boolean incoming, boolean emitter) { + // enabled by default + return (T) ObservationContext.DEFAULT; + } + + /** + * Returns a new {@link MessageObservation} object on which to collect the message processing events. + * If {@link #initObservation(String, boolean, boolean)} is implemented, + * the {@link ObservationContext} object returned from that method will be passed to this method. + * If not it is called with {@link ObservationContext#DEFAULT} and should be ignored. + * + * @param channel the channel of the message + * @param message the message + * @param observationContext the observation context + * @return the message observation + */ + MessageObservation onNewMessage(String channel, Message message, T observationContext); + +} diff --git a/api/src/main/java/io/smallrye/reactive/messaging/observation/ObservationContext.java b/api/src/main/java/io/smallrye/reactive/messaging/observation/ObservationContext.java new file mode 100644 index 0000000000..874baa4cff --- /dev/null +++ b/api/src/main/java/io/smallrye/reactive/messaging/observation/ObservationContext.java @@ -0,0 +1,22 @@ +package io.smallrye.reactive.messaging.observation; + +/** + * The per-channel context of the Message observation. + * It is created at the observation initialization by-channel and included at each message observation calls. + */ +public interface ObservationContext { + + /** + * Default no-op observation context + */ + ObservationContext DEFAULT = observation -> { + + }; + + /** + * Called after observation is completed. + * + * @param observation the completed message observation + */ + void complete(MessageObservation observation); +} diff --git a/documentation/mkdocs.yml b/documentation/mkdocs.yml index c4472d6b1d..dfc6d4c5db 100644 --- a/documentation/mkdocs.yml +++ b/documentation/mkdocs.yml @@ -29,6 +29,7 @@ nav: - '@Outgoings' : concepts/outgoings.md - 'Testing' : concepts/testing.md - 'Logging' : concepts/logging.md + - 'Observability API' : concepts/observability.md - 'Advanced Configuration' : concepts/advanced-config.md - 'Message Context' : concepts/message-context.md - 'Metadata Injection': concepts/incoming-metadata-injection.md diff --git a/documentation/src/main/docs/concepts/observability.md b/documentation/src/main/docs/concepts/observability.md new file mode 100644 index 0000000000..a69302ed68 --- /dev/null +++ b/documentation/src/main/docs/concepts/observability.md @@ -0,0 +1,30 @@ +# Observability API + +!!!important + Observability API is experimental and SmallRye only feature. + +Smallrye Reactive Messaging proposes an observability API that allows to observe messages received and send through inbound and outbound channels. + +For any observation to happen, you need to provide an implementation of the `MessageObservationCollector`, discovered as a CDI-managed bean. + +At wiring time the discovered `MessageObservationCollector` implementation `initObservation` method is called once per channel to initialize the `ObservationContext`. +The default `initObservation` implementation returns a default `ObservationContext` object, +but the collector implementation can provide a custom per-channel `ObservationContext` object that'll hold information necessary for the observation. +The `ObservationContext#complete` method is called each time a message observation is completed – message being acked or nacked. +The collector implementation can decide at initialization time to disable the observation per channel by returning a `null` observation context. + +For each new message, the collector is on `onNewMessage` method with the channel name, the `Message` and the `ObservationContext` object initialized beforehand. +This method can react to the creation of a new message but also is responsible for instantiating and returning a `MessageObservation`. +While custom implementations can augment the observability capability, SmallRye Reactive Messaging provides a default implementation `DefaultMessageObservation`. + +So a simple observability collector can be implemented as such: + +``` java +{{ insert('observability/SimpleMessageObservationCollector.java', ) }} +``` + +A collector with a custom `ObservationContext` can be implemented as such : + +``` java +{{ insert('observability/ContextMessageObservationCollector.java', ) }} +``` diff --git a/documentation/src/main/java/observability/ContextMessageObservationCollector.java b/documentation/src/main/java/observability/ContextMessageObservationCollector.java new file mode 100644 index 0000000000..608e368dc1 --- /dev/null +++ b/documentation/src/main/java/observability/ContextMessageObservationCollector.java @@ -0,0 +1,57 @@ +package observability; + +import java.time.Duration; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.observation.DefaultMessageObservation; +import io.smallrye.reactive.messaging.observation.MessageObservation; +import io.smallrye.reactive.messaging.observation.MessageObservationCollector; +import io.smallrye.reactive.messaging.observation.ObservationContext; + +@ApplicationScoped +public class ContextMessageObservationCollector + implements MessageObservationCollector { + + @Override + public MyContext initObservation(String channel, boolean incoming, boolean emitter) { + // Called on observation setup, per channel + // if returned null the observation for that channel is disabled + return new MyContext(channel, incoming, emitter); + } + + @Override + public MessageObservation onNewMessage(String channel, Message message, MyContext ctx) { + // Called after message has been created + return new DefaultMessageObservation(channel); + } + + public static class MyContext implements ObservationContext { + + private final String channel; + private final boolean incoming; + private final boolean emitter; + + public MyContext(String channel, boolean incoming, boolean emitter) { + this.channel = channel; + this.incoming = incoming; + this.emitter = emitter; + } + + @Override + public void complete(MessageObservation observation) { + // called after message processing has completed and observation is done + // register duration + Duration duration = observation.getCompletionDuration(); + Throwable reason = observation.getReason(); + if (reason != null) { + // message was nacked + } else { + // message was acked successfully + } + } + } + +} diff --git a/documentation/src/main/java/observability/SimpleMessageObservationCollector.java b/documentation/src/main/java/observability/SimpleMessageObservationCollector.java new file mode 100644 index 0000000000..838bceb5fa --- /dev/null +++ b/documentation/src/main/java/observability/SimpleMessageObservationCollector.java @@ -0,0 +1,21 @@ +package observability; + +import jakarta.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.reactive.messaging.observation.DefaultMessageObservation; +import io.smallrye.reactive.messaging.observation.MessageObservation; +import io.smallrye.reactive.messaging.observation.MessageObservationCollector; +import io.smallrye.reactive.messaging.observation.ObservationContext; + +@ApplicationScoped +public class SimpleMessageObservationCollector implements MessageObservationCollector { + + @Override + public MessageObservation onNewMessage(String channel, Message message, ObservationContext ctx) { + // Called after message has been created + return new DefaultMessageObservation(channel); + } + +} diff --git a/smallrye-reactive-messaging-kafka/src/test/java/io/smallrye/reactive/messaging/kafka/base/WeldTestBase.java b/smallrye-reactive-messaging-kafka/src/test/java/io/smallrye/reactive/messaging/kafka/base/WeldTestBase.java index 0886d8abef..a8d6ce83a5 100644 --- a/smallrye-reactive-messaging-kafka/src/test/java/io/smallrye/reactive/messaging/kafka/base/WeldTestBase.java +++ b/smallrye-reactive-messaging-kafka/src/test/java/io/smallrye/reactive/messaging/kafka/base/WeldTestBase.java @@ -36,6 +36,8 @@ import io.smallrye.reactive.messaging.providers.extension.LegacyEmitterFactoryImpl; import io.smallrye.reactive.messaging.providers.extension.MediatorManager; import io.smallrye.reactive.messaging.providers.extension.MutinyEmitterFactoryImpl; +import io.smallrye.reactive.messaging.providers.extension.ObservationDecorator; +import io.smallrye.reactive.messaging.providers.extension.OutgoingObservationDecorator; import io.smallrye.reactive.messaging.providers.extension.ReactiveMessagingExtension; import io.smallrye.reactive.messaging.providers.impl.ConfiguredChannelFactory; import io.smallrye.reactive.messaging.providers.impl.ConnectorFactories; @@ -102,6 +104,8 @@ public void initWeld() { weld.addBeanClass(MetricDecorator.class); weld.addBeanClass(MicrometerDecorator.class); weld.addBeanClass(ContextDecorator.class); + weld.addBeanClass(ObservationDecorator.class); + weld.addBeanClass(OutgoingObservationDecorator.class); weld.disableDiscovery(); } diff --git a/smallrye-reactive-messaging-kafka/src/test/java/io/smallrye/reactive/messaging/kafka/metrics/ObservationTest.java b/smallrye-reactive-messaging-kafka/src/test/java/io/smallrye/reactive/messaging/kafka/metrics/ObservationTest.java new file mode 100644 index 0000000000..636dc00b69 --- /dev/null +++ b/smallrye-reactive-messaging-kafka/src/test/java/io/smallrye/reactive/messaging/kafka/metrics/ObservationTest.java @@ -0,0 +1,251 @@ +package io.smallrye.reactive.messaging.kafka.metrics; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.apache.kafka.common.serialization.StringDeserializer; +import org.apache.kafka.common.serialization.StringSerializer; +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Emitter; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; +import org.junit.jupiter.api.Test; + +import io.smallrye.mutiny.Multi; +import io.smallrye.reactive.messaging.kafka.api.IncomingKafkaRecordMetadata; +import io.smallrye.reactive.messaging.kafka.base.KafkaCompanionTestBase; +import io.smallrye.reactive.messaging.kafka.metrics.ObservationTest.MyReactiveMessagingMessageObservationCollector.KafkaMessageObservation; +import io.smallrye.reactive.messaging.observation.DefaultMessageObservation; +import io.smallrye.reactive.messaging.observation.MessageObservation; +import io.smallrye.reactive.messaging.observation.MessageObservationCollector; +import io.smallrye.reactive.messaging.observation.ObservationContext; + +public class ObservationTest extends KafkaCompanionTestBase { + + @Test + void testConsumeIndividualMessages() { + addBeans(MyReactiveMessagingMessageObservationCollector.class); + addBeans(MyConsumingApp.class); + + runApplication(kafkaConfig("mp.messaging.incoming.kafka", false) + .with("topic", topic) + .with(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest") + .with(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()) + .with(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName())); + + companion.produceStrings() + .fromMulti(Multi.createFrom().range(0, 5).map(i -> new ProducerRecord<>(topic, null, Integer.toString(i)))); + + MyReactiveMessagingMessageObservationCollector reporter = get(MyReactiveMessagingMessageObservationCollector.class); + await().untilAsserted(() -> { + assertThat(reporter.getObservations()).hasSize(5); + assertThat(reporter.getObservations()).allSatisfy(obs -> { + assertThat(obs.getCreationTime()).isNotEqualTo(-1); + assertThat(obs.getCompletionTime()).isNotEqualTo(-1).isGreaterThan(obs.getCreationTime()); + assertThat(obs.getReason()).isNull(); + }); + }); + } + + @Test + void testConsumeBatchMessages() { + addBeans(MyReactiveMessagingMessageObservationCollector.class); + addBeans(MyBatchConsumingApp.class); + + runApplication(kafkaConfig("mp.messaging.incoming.kafka", false) + .with("topic", topic) + .with("batch", true) + .with(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest") + .with(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName()) + .with(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName())); + + companion.produceStrings() + .fromMulti(Multi.createFrom().range(0, 1000).map(i -> new ProducerRecord<>(topic, null, Integer.toString(i)))); + + MyReactiveMessagingMessageObservationCollector reporter = get(MyReactiveMessagingMessageObservationCollector.class); + MyBatchConsumingApp batches = get(MyBatchConsumingApp.class); + await().untilAsserted(() -> { + assertThat(batches.count()).isEqualTo(1000); + assertThat(reporter.getObservations()).allSatisfy(obs -> { + assertThat(obs.getCreationTime()).isNotEqualTo(-1); + assertThat(obs.getCreationTime()).isNotEqualTo(-1); + assertThat(obs.getCompletionTime()).isNotEqualTo(-1).isGreaterThan(obs.getCreationTime()); + assertThat(obs.getReason()).isNull(); + }); + }); + } + + @Test + void testProducer() { + addBeans(MyReactiveMessagingMessageObservationCollector.class); + addBeans(MyProducerApp.class); + + runApplication(kafkaConfig("mp.messaging.outgoing.kafka", false) + .with("topic", topic) + .with(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()) + .with(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName())); + + MyReactiveMessagingMessageObservationCollector reporter = get(MyReactiveMessagingMessageObservationCollector.class); + MyProducerApp producer = get(MyProducerApp.class); + await().untilAsserted(() -> { + assertThat(producer.count()).isEqualTo(5); + assertThat(reporter.getObservations()).hasSize(5); + assertThat(reporter.getObservations()).allSatisfy(obs -> { + assertThat(obs.getCreationTime()).isNotEqualTo(-1); + assertThat(obs.getCreationTime()).isNotEqualTo(-1); + assertThat(obs.getCompletionTime()).isNotEqualTo(-1).isGreaterThan(obs.getCreationTime()); + assertThat(obs.getReason()).isNull(); + }); + }); + } + + @Test + void testEmitterProducer() { + addBeans(MyReactiveMessagingMessageObservationCollector.class); + addBeans(MyEmitterProducerApp.class); + + runApplication(kafkaConfig("mp.messaging.outgoing.kafka", false) + .with("topic", topic) + .with(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()) + .with(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName())); + + MyReactiveMessagingMessageObservationCollector reporter = get(MyReactiveMessagingMessageObservationCollector.class); + MyEmitterProducerApp producer = get(MyEmitterProducerApp.class); + producer.produce(); + await().untilAsserted(() -> { + assertThat(producer.count()).isEqualTo(5); + assertThat(reporter.getObservations()).hasSize(5); + assertThat(reporter.getObservations()).allSatisfy(obs -> { + assertThat(obs.getCreationTime()).isNotEqualTo(-1); + assertThat(obs.getCreationTime()).isNotEqualTo(-1); + assertThat(obs.getCompletionTime()).isNotEqualTo(-1).isGreaterThan(obs.getCreationTime()); + assertThat(obs.getReason()).isNull(); + }); + }); + } + + @ApplicationScoped + public static class MyConsumingApp { + @Incoming("kafka") + public void consume(String ignored, KafkaMessageObservation metadata) { + assertThat(metadata).isNotNull(); + } + } + + @ApplicationScoped + public static class MyBatchConsumingApp { + + AtomicInteger count = new AtomicInteger(); + + @Incoming("kafka") + public void consume(List s, KafkaMessageObservation metadata) { + assertThat(metadata).isNotNull(); + count.addAndGet(s.size()); + } + + public int count() { + return count.get(); + } + } + + @ApplicationScoped + public static class MyProducerApp { + + AtomicInteger acked = new AtomicInteger(); + + @Outgoing("kafka") + public Multi> produce() { + return Multi.createFrom().items("1", "2", "3", "4", "5") + .map(s -> Message.of(s, () -> { + acked.incrementAndGet(); + return CompletableFuture.completedFuture(null); + })); + } + + public int count() { + return acked.get(); + } + } + + @ApplicationScoped + public static class MyEmitterProducerApp { + AtomicInteger acked = new AtomicInteger(); + + @Inject + @Channel("kafka") + Emitter emitter; + + public void produce() { + for (int i = 0; i < 5; i++) { + emitter.send(Message.of(String.valueOf(i + 1), () -> { + acked.incrementAndGet(); + return CompletableFuture.completedFuture(null); + })); + } + } + + public int count() { + return acked.get(); + } + } + + @ApplicationScoped + public static class MyReactiveMessagingMessageObservationCollector + implements MessageObservationCollector { + + private final List observations = new CopyOnWriteArrayList<>(); + + @Override + public MessageObservation onNewMessage(String channel, Message message, ObservationContext ctx) { + KafkaMessageObservation observation = new KafkaMessageObservation(channel, message); + observations.add(observation); + return observation; + } + + public List getObservations() { + return observations; + } + + public static class KafkaMessageObservation extends DefaultMessageObservation { + final long recordTs; + final long createdMs; + volatile long completedMs; + + public KafkaMessageObservation(String channel, Message message) { + super(channel); + this.createdMs = System.currentTimeMillis(); + Optional metadata = message.getMetadata(IncomingKafkaRecordMetadata.class); + if (metadata.isPresent()) { + Instant inst = metadata.get().getTimestamp(); + recordTs = inst.toEpochMilli(); + System.out.println("record " + recordTs); + } else { + recordTs = 0L; + } + } + + @Override + public void onMessageAck(Message message) { + super.onMessageAck(message); + completedMs = System.currentTimeMillis(); + System.out.println("completed in " + completedMs); + done = true; + } + } + } + +} diff --git a/smallrye-reactive-messaging-provider/src/main/java/io/smallrye/reactive/messaging/providers/extension/ObservationDecorator.java b/smallrye-reactive-messaging-provider/src/main/java/io/smallrye/reactive/messaging/providers/extension/ObservationDecorator.java new file mode 100644 index 0000000000..410db59a1d --- /dev/null +++ b/smallrye-reactive-messaging-provider/src/main/java/io/smallrye/reactive/messaging/providers/extension/ObservationDecorator.java @@ -0,0 +1,104 @@ +package io.smallrye.reactive.messaging.providers.extension; + +import static io.smallrye.mutiny.unchecked.Unchecked.consumer; + +import java.lang.reflect.InvocationTargetException; +import java.util.List; +import java.util.Optional; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Metadata; + +import io.smallrye.mutiny.Multi; +import io.smallrye.reactive.messaging.ChannelRegistry; +import io.smallrye.reactive.messaging.PublisherDecorator; +import io.smallrye.reactive.messaging.observation.MessageObservation; +import io.smallrye.reactive.messaging.observation.MessageObservationCollector; +import io.smallrye.reactive.messaging.observation.ObservationContext; +import io.smallrye.reactive.messaging.providers.ProcessingException; + +@ApplicationScoped +public class ObservationDecorator implements PublisherDecorator { + + @Inject + @ConfigProperty(name = "smallrye.messaging.observation.enabled", defaultValue = "true") + boolean enabled; + + @Inject + ChannelRegistry registry; + + @Inject + Instance> observationCollector; + + @Override + public Multi> decorate(Multi> multi, List channelName, + boolean isConnector) { + String channel = channelName.isEmpty() ? null : channelName.get(0); + boolean isEmitter = registry.getEmitterNames().contains(channel); + if (observationCollector.isResolvable() && enabled && (isConnector || isEmitter)) { + // if this is an emitter channel than it is an outgoing channel => incoming=false + return decorateObservation(observationCollector.get(), multi, channel, !isEmitter, isEmitter); + } + return multi; + } + + static Multi> decorateObservation( + MessageObservationCollector obsCollector, + Multi> multi, + String channel, + boolean incoming, + boolean emitter) { + MessageObservationCollector collector = (MessageObservationCollector) obsCollector; + ObservationContext context = collector.initObservation(channel, incoming, emitter); + if (context == null) { + return multi; + } + return multi.map(message -> { + MessageObservation observation = collector.onNewMessage(channel, message, context); + if (observation != null) { + return message.addMetadata(observation) + .thenApply(msg -> msg.withAckWithMetadata(metadata -> msg.ack(metadata) + .thenAccept(consumer(x -> getObservationMetadata(metadata) + .ifPresent(obs -> { + obs.onMessageAck(msg); + context.complete(obs); + }))))) + .thenApply(msg -> msg.withNackWithMetadata((reason, metadata) -> { + getObservationMetadata(metadata).ifPresent(consumer(obs -> { + obs.onMessageNack(msg, extractReason(reason)); + context.complete(obs); + })); + return msg.nack(reason, metadata); + })); + } else { + return message; + } + }); + } + + static Optional getObservationMetadata(Metadata metadata) { + for (Object item : metadata) { + if (item instanceof MessageObservation) { + return Optional.of((MessageObservation) item); + } + } + return Optional.empty(); + } + + static Throwable extractReason(Throwable reason) { + if (reason instanceof ProcessingException) { + Throwable cause = reason.getCause(); + if (cause instanceof InvocationTargetException) { + cause = ((InvocationTargetException) cause).getTargetException(); + } + return cause; + } + return reason; + } + +} diff --git a/smallrye-reactive-messaging-provider/src/main/java/io/smallrye/reactive/messaging/providers/extension/OutgoingObservationDecorator.java b/smallrye-reactive-messaging-provider/src/main/java/io/smallrye/reactive/messaging/providers/extension/OutgoingObservationDecorator.java new file mode 100644 index 0000000000..fe95894c06 --- /dev/null +++ b/smallrye-reactive-messaging-provider/src/main/java/io/smallrye/reactive/messaging/providers/extension/OutgoingObservationDecorator.java @@ -0,0 +1,43 @@ +package io.smallrye.reactive.messaging.providers.extension; + +import static io.smallrye.reactive.messaging.providers.extension.ObservationDecorator.decorateObservation; + +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.reactive.messaging.Message; + +import io.smallrye.mutiny.Multi; +import io.smallrye.reactive.messaging.ChannelRegistry; +import io.smallrye.reactive.messaging.SubscriberDecorator; +import io.smallrye.reactive.messaging.observation.MessageObservationCollector; + +@ApplicationScoped +public class OutgoingObservationDecorator implements SubscriberDecorator { + + @Inject + @ConfigProperty(name = "smallrye.messaging.observation.enabled", defaultValue = "true") + boolean enabled; + + @Inject + ChannelRegistry registry; + + @Inject + Instance> observationCollector; + + @Override + public Multi> decorate(Multi> multi, List channelName, + boolean isConnector) { + String channel = channelName.isEmpty() ? null : channelName.get(0); + boolean isEmitter = registry.getEmitterNames().contains(channel); + if (observationCollector.isResolvable() && enabled && !isEmitter && isConnector) { + return decorateObservation(observationCollector.get(), multi, channel, false, false); + } + return multi; + } + +} diff --git a/smallrye-reactive-messaging-provider/src/test/java/io/smallrye/reactive/messaging/WeldTestBaseWithoutTails.java b/smallrye-reactive-messaging-provider/src/test/java/io/smallrye/reactive/messaging/WeldTestBaseWithoutTails.java index dcd1257547..6dec4d27f9 100644 --- a/smallrye-reactive-messaging-provider/src/test/java/io/smallrye/reactive/messaging/WeldTestBaseWithoutTails.java +++ b/smallrye-reactive-messaging-provider/src/test/java/io/smallrye/reactive/messaging/WeldTestBaseWithoutTails.java @@ -32,6 +32,8 @@ import io.smallrye.reactive.messaging.providers.extension.LegacyEmitterFactoryImpl; import io.smallrye.reactive.messaging.providers.extension.MediatorManager; import io.smallrye.reactive.messaging.providers.extension.MutinyEmitterFactoryImpl; +import io.smallrye.reactive.messaging.providers.extension.ObservationDecorator; +import io.smallrye.reactive.messaging.providers.extension.OutgoingObservationDecorator; import io.smallrye.reactive.messaging.providers.extension.ReactiveMessagingExtension; import io.smallrye.reactive.messaging.providers.impl.ConfiguredChannelFactory; import io.smallrye.reactive.messaging.providers.impl.ConnectorFactories; @@ -121,7 +123,9 @@ public void setUp() { LegacyEmitterFactoryImpl.class, OutgoingInterceptorDecorator.class, IncomingInterceptorDecorator.class, - + // Observation Decorator + ObservationDecorator.class, + OutgoingObservationDecorator.class, // SmallRye config io.smallrye.config.inject.ConfigProducer.class); diff --git a/smallrye-reactive-messaging-provider/src/test/java/io/smallrye/reactive/messaging/inject/EmitterObservationTest.java b/smallrye-reactive-messaging-provider/src/test/java/io/smallrye/reactive/messaging/inject/EmitterObservationTest.java new file mode 100644 index 0000000000..0b628306d9 --- /dev/null +++ b/smallrye-reactive-messaging-provider/src/test/java/io/smallrye/reactive/messaging/inject/EmitterObservationTest.java @@ -0,0 +1,131 @@ +package io.smallrye.reactive.messaging.inject; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Emitter; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.junit.jupiter.api.Test; + +import io.smallrye.reactive.messaging.WeldTestBaseWithoutTails; +import io.smallrye.reactive.messaging.observation.DefaultMessageObservation; +import io.smallrye.reactive.messaging.observation.MessageObservation; +import io.smallrye.reactive.messaging.observation.MessageObservationCollector; +import io.smallrye.reactive.messaging.observation.ObservationContext; + +public class EmitterObservationTest extends WeldTestBaseWithoutTails { + + @Test + void testObservationPointsWhenEmittingPayloads() { + addBeanClass(MyMessageObservationCollector.class); + addBeanClass(MyComponentWithAnEmitterOfPayload.class); + + initialize(); + + MyMessageObservationCollector observation = container.select(MyMessageObservationCollector.class).get(); + MyComponentWithAnEmitterOfPayload component = get(MyComponentWithAnEmitterOfPayload.class); + + component.emit(1); + component.emit(2); + component.emit(3); + + await().untilAsserted(() -> assertThat(observation.getObservations()).hasSize(3)); + assertThat(observation.getObservations()).allSatisfy(obs -> { + assertThat(obs.isDone()).isTrue(); + assertThat(obs.getReason()).isNull(); + assertThat(obs.getCreationTime()).isNotEqualTo(-1); + assertThat(obs.getCompletionTime()).isNotEqualTo(-1); + }); + + } + + @Test + void testObservationPointsWhenEmittingMessages() { + addBeanClass(MyComponentWithAnEmitterOfMessage.class); + addBeanClass(MyMessageObservationCollector.class); + + initialize(); + + MyMessageObservationCollector observation = container.select(MyMessageObservationCollector.class).get(); + MyComponentWithAnEmitterOfMessage component = get(MyComponentWithAnEmitterOfMessage.class); + + component.emit(Message.of(1)); + component.emit(Message.of(2)); + component.emit(Message.of(3)); + + await().untilAsserted(() -> assertThat(observation.getObservations()).hasSize(3)); + assertThat(observation.getObservations()).allSatisfy(obs -> { + assertThat(obs.isDone()).isTrue(); + assertThat(obs.getCreationTime()).isNotEqualTo(-1); + assertThat(obs.getCreationTime()).isNotEqualTo(-1); + assertThat(obs.getCompletionTime()).isNotEqualTo(-1); + }); + + assertThat(observation.getObservations().get(2).getReason()).isInstanceOf(IllegalArgumentException.class); + + } + + @ApplicationScoped + public static class MyComponentWithAnEmitterOfPayload { + + @Inject + @Channel("output") + Emitter emitter; + + public void emit(int i) { + emitter.send(i); + } + + @Incoming("output") + public void consume(int i) { + // do nothing. + } + + } + + @ApplicationScoped + public static class MyComponentWithAnEmitterOfMessage { + + @Inject + @Channel("output") + Emitter emitter; + + public void emit(Message i) { + emitter.send(i); + } + + @Incoming("output") + public void consume(int i, MessageObservation mo) { + assertThat(mo).isNotNull(); + if (i == 3) { + throw new IllegalArgumentException("boom"); + } + } + + } + + @ApplicationScoped + public static class MyMessageObservationCollector implements MessageObservationCollector { + + private final List observations = new CopyOnWriteArrayList<>(); + + public List getObservations() { + return observations; + } + + @Override + public MessageObservation onNewMessage(String channel, Message message, ObservationContext ctx) { + MessageObservation observation = new DefaultMessageObservation(channel); + observations.add(observation); + return observation; + } + } +} diff --git a/smallrye-reactive-messaging-provider/src/test/java/io/smallrye/reactive/messaging/providers/connectors/ObservationTest.java b/smallrye-reactive-messaging-provider/src/test/java/io/smallrye/reactive/messaging/providers/connectors/ObservationTest.java new file mode 100644 index 0000000000..e707a75076 --- /dev/null +++ b/smallrye-reactive-messaging-provider/src/test/java/io/smallrye/reactive/messaging/providers/connectors/ObservationTest.java @@ -0,0 +1,301 @@ +package io.smallrye.reactive.messaging.providers.connectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import java.io.IOException; +import java.net.MalformedURLException; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.CopyOnWriteArrayList; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.eclipse.microprofile.reactive.messaging.Channel; +import org.eclipse.microprofile.reactive.messaging.Emitter; +import org.eclipse.microprofile.reactive.messaging.Incoming; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Outgoing; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.smallrye.mutiny.Multi; +import io.smallrye.reactive.messaging.WeldTestBaseWithoutTails; +import io.smallrye.reactive.messaging.observation.DefaultMessageObservation; +import io.smallrye.reactive.messaging.observation.MessageObservation; +import io.smallrye.reactive.messaging.observation.MessageObservationCollector; +import io.smallrye.reactive.messaging.observation.ObservationContext; + +public class ObservationTest extends WeldTestBaseWithoutTails { + + @BeforeEach + void setupConfig() { + installConfig("src/test/resources/config/observation.properties"); + } + + @Test + void testMessageObservationPointsFromPayloadConsumer() { + addBeanClass(MyMessageObservationCollector.class); + addBeanClass(MyPayloadConsumer.class); + + initialize(); + + MyMessageObservationCollector observation = container.select(MyMessageObservationCollector.class).get(); + MyPayloadConsumer consumer = container.select(MyPayloadConsumer.class).get(); + + await().until(() -> observation.getObservations().size() == 3); + await().until(() -> consumer.received().size() == 3); + + assertThat(observation.getObservations()).allSatisfy(obs -> { + assertThat(obs.getCreationTime()).isNotEqualTo(-1); + assertThat(obs.getCompletionTime()).isNotEqualTo(-1).isGreaterThan(obs.getCreationTime()); + }); + + assertThat(observation.getObservations().get(0).getReason()).isNull(); + assertThat(observation.getObservations().get(1).getReason()).isInstanceOf(IOException.class); + assertThat(observation.getObservations().get(2).getReason()).isInstanceOf(MalformedURLException.class); + } + + @Test + void testMessageObservationPointsFromMessageConsumer() { + addBeanClass(MyMessageObservationCollector.class); + addBeanClass(MyMessageConsumer.class); + + initialize(); + + MyMessageObservationCollector observation = container.select(MyMessageObservationCollector.class).get(); + MyMessageConsumer consumer = container.select(MyMessageConsumer.class).get(); + + await().until(() -> observation.getObservations().size() == 3); + await().until(() -> consumer.received().size() == 3); + + assertThat(observation.getObservations()).allSatisfy(obs -> { + assertThat(obs.getCreationTime()).isNotEqualTo(-1); + assertThat(obs.getCompletionTime()).isNotEqualTo(-1).isGreaterThan(obs.getCreationTime()); + }); + + assertThat(observation.getObservations().get(0).getReason()).isNull(); + assertThat(observation.getObservations().get(1).getReason()).isInstanceOf(IOException.class); + assertThat(observation.getObservations().get(2).getReason()).isInstanceOf(MalformedURLException.class); + } + + @Test + void testMessageObservationPointsFromMessageProducer() { + addBeanClass(MyMessageObservationCollector.class); + addBeanClass(MyMessageProducer.class); + + initialize(); + + MyMessageObservationCollector observation = container.select(MyMessageObservationCollector.class).get(); + MyMessageProducer producer = container.select(MyMessageProducer.class).get(); + + await().until(() -> observation.getObservations().size() == 3); + await().until(() -> producer.acked().size() == 3); + + assertThat(observation.getObservations()).allSatisfy(obs -> { + assertThat(obs.getCreationTime()).isNotEqualTo(-1); + assertThat(obs.getReason()).isNull(); + assertThat(obs.getCompletionTime()).isNotEqualTo(-1).isGreaterThan(obs.getCreationTime()); + }); + } + + @Test + void testMessageObservationPointsFromPayloadProducer() { + addBeanClass(MyMessageObservationCollector.class); + addBeanClass(MyPayloadProducer.class); + + initialize(); + + MyMessageObservationCollector observation = container.select(MyMessageObservationCollector.class).get(); + MyPayloadProducer producer = container.select(MyPayloadProducer.class).get(); + + await().until(() -> observation.getObservations().size() == 3); + + assertThat(observation.getObservations()).allSatisfy(obs -> { + assertThat(obs.getCreationTime()).isNotEqualTo(-1); + assertThat(obs.getReason()).isNull(); + assertThat(obs.getCompletionTime()).isNotEqualTo(-1).isGreaterThan(obs.getCreationTime()); + }); + } + + @Test + void testMessageObservationPointsFromPayloadEmitter() { + addBeanClass(MyMessageObservationCollector.class); + addBeanClass(MyEmitterProducer.class); + + initialize(); + + MyMessageObservationCollector observation = container.select(MyMessageObservationCollector.class).get(); + MyEmitterProducer producer = container.select(MyEmitterProducer.class).get(); + + producer.produce(); + + await().until(() -> observation.getObservations().size() == 3); + + assertThat(observation.getObservations()).allSatisfy(obs -> { + assertThat(obs.getCreationTime()).isNotEqualTo(-1); + assertThat(obs.getReason()).isNull(); + assertThat(obs.getCompletionTime()).isNotEqualTo(-1).isGreaterThan(obs.getCreationTime()); + }); + } + + @Test + void testMessageObservationPointsFromMessageEmitter() { + addBeanClass(MyMessageObservationCollector.class); + addBeanClass(MyEmitterMessageProducer.class); + + initialize(); + + MyMessageObservationCollector observation = container.select(MyMessageObservationCollector.class).get(); + MyEmitterMessageProducer producer = container.select(MyEmitterMessageProducer.class).get(); + + producer.produceMessages(); + + await().until(() -> observation.getObservations().size() == 3); + await().until(() -> producer.acked().size() == 3); + + assertThat(observation.getObservations()).allSatisfy(obs -> { + assertThat(obs.getCreationTime()).isNotEqualTo(-1); + assertThat(obs.getReason()).isNull(); + assertThat(obs.getCompletionTime()).isNotEqualTo(-1).isGreaterThan(obs.getCreationTime()); + }); + } + + @ApplicationScoped + public static class MyPayloadConsumer { + private final List received = new CopyOnWriteArrayList<>(); + + @Incoming("A") + void consume(int payload, MessageObservation tracking) throws IOException { + received.add(payload); + assertThat(tracking).isNotNull(); + if (payload == 3) { + throw new IOException(); + } + if (payload == 4) { + throw new MalformedURLException(); + } + } + + public List received() { + return received; + } + } + + @ApplicationScoped + public static class MyMessageConsumer { + + private final List received = new CopyOnWriteArrayList<>(); + + @Incoming("A") + CompletionStage consume(Message msg) { + int payload = msg.getPayload(); + received.add(payload); + assertThat(msg.getMetadata(MessageObservation.class)).isNotEmpty(); + if (payload == 3) { + return msg.nack(new IOException()); + } + if (payload == 4) { + return msg.nack(new MalformedURLException()); + } + return msg.ack(); + } + + public List received() { + return received; + } + } + + @ApplicationScoped + public static class MyPayloadProducer { + + private final List acked = new CopyOnWriteArrayList<>(); + + @Outgoing("B") + Multi produce() { + return Multi.createFrom().items(1, 2, 3); + } + + public List acked() { + return acked; + } + } + + @ApplicationScoped + public static class MyMessageProducer { + + private final List acked = new CopyOnWriteArrayList<>(); + + @Outgoing("B") + Multi> produce() { + return Multi.createFrom().items(1, 2, 3) + .map(i -> Message.of(i, () -> { + acked.add(i); + return CompletableFuture.completedFuture(null); + })); + } + + public List acked() { + return acked; + } + } + + @ApplicationScoped + public static class MyEmitterProducer { + + @Inject + @Channel("B") + Emitter emitter; + + void produce() { + emitter.send(1); + emitter.send(2); + emitter.send(3); + } + + } + + @ApplicationScoped + public static class MyEmitterMessageProducer { + + private final List acked = new CopyOnWriteArrayList<>(); + + @Inject + @Channel("B") + Emitter emitter; + + void produceMessages() { + for (int i = 0; i < 3; i++) { + int j = i; + emitter.send(Message.of(j, () -> { + acked.add(j); + return CompletableFuture.completedFuture(null); + })); + } + } + + public List acked() { + return acked; + } + } + + @ApplicationScoped + public static class MyMessageObservationCollector implements MessageObservationCollector { + + private final List observations = new CopyOnWriteArrayList<>(); + + @Override + public MessageObservation onNewMessage(String channel, Message message, ObservationContext ctx) { + MessageObservation observation = new DefaultMessageObservation(channel); + observations.add(observation); + return observation; + } + + public List getObservations() { + return observations; + } + + } +} diff --git a/smallrye-reactive-messaging-provider/src/test/resources/config/observation.properties b/smallrye-reactive-messaging-provider/src/test/resources/config/observation.properties new file mode 100644 index 0000000000..202e52bab8 --- /dev/null +++ b/smallrye-reactive-messaging-provider/src/test/resources/config/observation.properties @@ -0,0 +1,4 @@ +# You should not be able to use the same channel name in an outgoing and incoming configuration +mp.messaging.incoming.A.connector=dummy +mp.messaging.outgoing.B.connector=dummy +