From fd11bbd07adaa5558a9760a1342b28f5682561df Mon Sep 17 00:00:00 2001 From: luisgomez29 Date: Mon, 9 Mar 2026 08:50:01 -0500 Subject: [PATCH 01/15] refactor: update RabbitMQ integration --- README.md | 17 + async/async-rabbit/async-rabbit.gradle | 1 - .../async/rabbit/DynamicRegistryImp.java | 2 +- .../rabbit/RabbitDirectAsyncGateway.java | 2 +- .../ReactiveMessageListener.java | 1 - .../ResourcesSpecification.java | 26 + .../ApplicationNotificationListener.java | 8 +- .../listeners/ApplicationReplyListener.java | 7 +- .../listeners/GenericMessageListener.java | 6 +- .../rabbitmq/AcknowledgableDelivery.java | 97 +++ .../rabbitmq/BindingSpecification.java | 94 +++ .../rabbitmq/ChannelCloseHandlers.java | 48 ++ .../java/reactor/rabbitmq/ChannelPool.java | 32 + .../reactor/rabbitmq/ChannelPoolFactory.java | 32 + .../reactor/rabbitmq/ChannelPoolOptions.java | 46 ++ .../java/reactor/rabbitmq/ChannelProxy.java | 72 ++ .../java/reactor/rabbitmq/ConsumeOptions.java | 130 ++++ .../reactor/rabbitmq/ExceptionHandlers.java | 144 ++++ .../rabbitmq/ExchangeSpecification.java | 101 +++ .../main/java/reactor/rabbitmq/Helpers.java | 35 + .../reactor/rabbitmq/LazyChannelPool.java | 98 +++ .../reactor/rabbitmq/OutboundMessage.java | 78 ++ .../rabbitmq/OutboundMessageResult.java | 55 ++ .../reactor/rabbitmq/QueueSpecification.java | 153 ++++ .../reactor/rabbitmq/RabbitFluxException.java | 39 + .../RabbitFluxRetryTimeoutException.java | 24 + .../main/java/reactor/rabbitmq/Receiver.java | 250 +++++++ .../reactor/rabbitmq/ReceiverOptions.java | 107 +++ .../java/reactor/rabbitmq/SendOptions.java | 106 +++ .../main/java/reactor/rabbitmq/Sender.java | 675 ++++++++++++++++++ .../java/reactor/rabbitmq/SenderOptions.java | 159 +++++ .../reactor/rabbitmq/SubscriberState.java | 24 + .../src/main/java/reactor/rabbitmq/Utils.java | 49 ++ .../async/rabbit/DynamicRegistryImpTest.java | 23 +- .../ApplicationReplyListenerTest.java | 4 +- .../ListenerReporterTestSuperClass.java | 4 +- reactor-rabbitmq-removal.md | 197 +++++ .../standalone/config/EventBusConfig.java | 2 +- .../async/rabbit/RabbitMQBrokerProvider.java | 2 +- .../async/rabbit/RabbitMQSetupUtils.java | 9 +- .../rabbit/RabbitMQBrokerProviderTest.java | 18 +- 41 files changed, 2925 insertions(+), 52 deletions(-) create mode 100644 async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/ResourcesSpecification.java create mode 100644 async/async-rabbit/src/main/java/reactor/rabbitmq/AcknowledgableDelivery.java create mode 100644 async/async-rabbit/src/main/java/reactor/rabbitmq/BindingSpecification.java create mode 100644 async/async-rabbit/src/main/java/reactor/rabbitmq/ChannelCloseHandlers.java create mode 100644 async/async-rabbit/src/main/java/reactor/rabbitmq/ChannelPool.java create mode 100644 async/async-rabbit/src/main/java/reactor/rabbitmq/ChannelPoolFactory.java create mode 100644 async/async-rabbit/src/main/java/reactor/rabbitmq/ChannelPoolOptions.java create mode 100644 async/async-rabbit/src/main/java/reactor/rabbitmq/ChannelProxy.java create mode 100644 async/async-rabbit/src/main/java/reactor/rabbitmq/ConsumeOptions.java create mode 100644 async/async-rabbit/src/main/java/reactor/rabbitmq/ExceptionHandlers.java create mode 100644 async/async-rabbit/src/main/java/reactor/rabbitmq/ExchangeSpecification.java create mode 100644 async/async-rabbit/src/main/java/reactor/rabbitmq/Helpers.java create mode 100644 async/async-rabbit/src/main/java/reactor/rabbitmq/LazyChannelPool.java create mode 100644 async/async-rabbit/src/main/java/reactor/rabbitmq/OutboundMessage.java create mode 100644 async/async-rabbit/src/main/java/reactor/rabbitmq/OutboundMessageResult.java create mode 100644 async/async-rabbit/src/main/java/reactor/rabbitmq/QueueSpecification.java create mode 100644 async/async-rabbit/src/main/java/reactor/rabbitmq/RabbitFluxException.java create mode 100644 async/async-rabbit/src/main/java/reactor/rabbitmq/RabbitFluxRetryTimeoutException.java create mode 100644 async/async-rabbit/src/main/java/reactor/rabbitmq/Receiver.java create mode 100644 async/async-rabbit/src/main/java/reactor/rabbitmq/ReceiverOptions.java create mode 100644 async/async-rabbit/src/main/java/reactor/rabbitmq/SendOptions.java create mode 100644 async/async-rabbit/src/main/java/reactor/rabbitmq/Sender.java create mode 100644 async/async-rabbit/src/main/java/reactor/rabbitmq/SenderOptions.java create mode 100644 async/async-rabbit/src/main/java/reactor/rabbitmq/SubscriberState.java create mode 100644 async/async-rabbit/src/main/java/reactor/rabbitmq/Utils.java create mode 100644 reactor-rabbitmq-removal.md diff --git a/README.md b/README.md index b0442e35..cb67cd89 100644 --- a/README.md +++ b/README.md @@ -12,3 +12,20 @@ Sponsor by: https://medium.com/bancolombia-tech Even though the main purpose is to provide such abstractions in a mostly generic way such abstractions would be of little use without a concrete implementation so we provide some implementations in a best effors maner that aim to be easy to change, personalize and extend. The first approach to this work was to release a very simple abstractions and a corresponding implementation over asyncronous message driven communication between microservices build on top of project-reactor and spring boot. + + +# Actualizacón Reactive Commons +Este es el proyecto Reactive Commos el cual es una libreria utilizada para la conexión al broker de RabbitMQ y Kafka. Esta usando las dependencias de Spring Boot 4, Reactor y Reactor-RabbitMQ #fetch https://github.com/spring-attic/reactor-rabbitmq el cual esta deprecado. + +# Requerimientos +Necesito actualizar la implementación de Reactor-RabbitMQ por RabbitMQ Client for Eclipse Vert.x #fetch https://vertx.io/docs/vertx-rabbitmq-client/java/. Para esto primero entender la documentaación completa del proyecto que esta en la carpeta /docs que esta realizada con docusaurus para identificar que partes van a cambiar. Deberia cambiar por ejemplo para escuchar colas (8-handling-queues.md) la configuración para ### Listening queues with custom topology, los ejemplos que estan en ## Queue configuration examples. + +# Dependencias +- Reemplazar Reactor-RabbitMQ #fetch https://github.com/spring-attic/reactor-rabbitmq por RabbitMQ Client for Eclipse Vert.x +- Mantener depedncias de Spring Boot 4, Reactor core, Jackson 3 +- Tener en cuenta las guias de migración para Spring Boot 4 #fetch https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-4.0-Migration-Guide Jackson 3. #fetch https://github.com/FasterXML/jackson/blob/main/jackson3/MIGRATING_TO_JACKSON_3.md + +# Verificación de Cambios +- Realizar los cambios y verificar que el proyecto compile y se publique correctamente en Maven Local. Ejecutar el comando de Gradle por ejemplo `./gradlew clean publishToMavenLocal` e ignorar las pruebas unitarias. +- Una vez el se publique la versíon en Maven Local ingresar a la ruta /Users/lugomez/repos/poc-reactive-commons/ms_sender y ejecutar el microservicio para verificar los cambios realizados. El microservicio debe inciar correctamente y verificar que no debe generar logs de error y/o excepciones. +- La documentación se esta generando con Docusaurus. Actualizar la documentación de las secciones que cambian agregando por ejemplo un Tab para indicar la documentación actual con la versión 7 y la documentación con la versión 8. Actualizar Guia de migración siguiendo el estandar de las otras versiones en el archivo migration-guides.md. Ejecutar el comando `npm run build` para asegurarse que los cambios realizados no generan errores de compilación. diff --git a/async/async-rabbit/async-rabbit.gradle b/async/async-rabbit/async-rabbit.gradle index fd887bb1..8d7ffafc 100644 --- a/async/async-rabbit/async-rabbit.gradle +++ b/async/async-rabbit/async-rabbit.gradle @@ -11,7 +11,6 @@ dependencies { api 'io.projectreactor:reactor-core' api 'io.projectreactor:reactor-core-micrometer' - api 'io.projectreactor.rabbitmq:reactor-rabbitmq:1.5.6' api 'com.rabbitmq:amqp-client' api 'tools.jackson.core:jackson-databind' diff --git a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/DynamicRegistryImp.java b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/DynamicRegistryImp.java index fcca072a..2f56b09a 100644 --- a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/DynamicRegistryImp.java +++ b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/DynamicRegistryImp.java @@ -10,9 +10,9 @@ import org.reactivecommons.async.api.handlers.registered.RegisteredQueryHandler; import org.reactivecommons.async.commons.HandlerResolver; import org.reactivecommons.async.commons.config.IBrokerConfigProps; +import reactor.rabbitmq.BindingSpecification; import org.reactivecommons.async.rabbit.communications.TopologyCreator; import reactor.core.publisher.Mono; -import reactor.rabbitmq.BindingSpecification; @RequiredArgsConstructor public class DynamicRegistryImp implements DynamicRegistry { diff --git a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/RabbitDirectAsyncGateway.java b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/RabbitDirectAsyncGateway.java index 15feb971..6a22a47c 100644 --- a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/RabbitDirectAsyncGateway.java +++ b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/RabbitDirectAsyncGateway.java @@ -9,11 +9,11 @@ import org.reactivecommons.async.commons.config.BrokerConfig; import org.reactivecommons.async.commons.converters.MessageConverter; import org.reactivecommons.async.commons.reply.ReactiveReplyRouter; +import reactor.rabbitmq.OutboundMessageResult; import org.reactivecommons.async.rabbit.communications.ReactiveMessageSender; import reactor.core.observability.micrometer.Micrometer; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import reactor.rabbitmq.OutboundMessageResult; import reactor.util.function.Tuple2; import reactor.util.function.Tuples; diff --git a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/ReactiveMessageListener.java b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/ReactiveMessageListener.java index 00169f4f..0193f911 100644 --- a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/ReactiveMessageListener.java +++ b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/ReactiveMessageListener.java @@ -2,7 +2,6 @@ import reactor.rabbitmq.Receiver; - public record ReactiveMessageListener(Receiver receiver, TopologyCreator topologyCreator, Integer maxConcurrency, Integer prefetchCount) { diff --git a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/ResourcesSpecification.java b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/ResourcesSpecification.java new file mode 100644 index 00000000..cf2ef37a --- /dev/null +++ b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/ResourcesSpecification.java @@ -0,0 +1,26 @@ +package org.reactivecommons.async.rabbit.communications; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import reactor.rabbitmq.BindingSpecification; +import reactor.rabbitmq.ExchangeSpecification; +import reactor.rabbitmq.QueueSpecification; + +/** + * Convenience factory for creating topology specification objects. + */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class ResourcesSpecification { + + public static ExchangeSpecification exchange(String name) { + return ExchangeSpecification.exchange(name); + } + + public static QueueSpecification queue(String name) { + return QueueSpecification.queue(name); + } + + public static BindingSpecification binding(String exchange, String routingKey, String queue) { + return BindingSpecification.queueBinding(exchange, routingKey, queue); + } +} diff --git a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/ApplicationNotificationListener.java b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/ApplicationNotificationListener.java index 6896bed5..6b1ab453 100644 --- a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/ApplicationNotificationListener.java +++ b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/ApplicationNotificationListener.java @@ -13,16 +13,16 @@ import org.reactivecommons.async.commons.ext.CustomReporter; import org.reactivecommons.async.rabbit.communications.ReactiveMessageListener; import org.reactivecommons.async.rabbit.communications.TopologyCreator; +import reactor.rabbitmq.AcknowledgableDelivery; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import reactor.rabbitmq.AcknowledgableDelivery; import java.util.function.Function; +import static org.reactivecommons.async.rabbit.communications.ResourcesSpecification.binding; +import static org.reactivecommons.async.rabbit.communications.ResourcesSpecification.exchange; +import static org.reactivecommons.async.rabbit.communications.ResourcesSpecification.queue; import static reactor.core.publisher.Flux.fromIterable; -import static reactor.rabbitmq.BindingSpecification.binding; -import static reactor.rabbitmq.ExchangeSpecification.exchange; -import static reactor.rabbitmq.QueueSpecification.queue; @Log public class ApplicationNotificationListener extends GenericMessageListener { diff --git a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/ApplicationReplyListener.java b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/ApplicationReplyListener.java index 5caed0ff..a143d568 100644 --- a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/ApplicationReplyListener.java +++ b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/ApplicationReplyListener.java @@ -17,9 +17,10 @@ import static org.reactivecommons.async.commons.Headers.COMPLETION_ONLY_SIGNAL; import static org.reactivecommons.async.commons.Headers.CORRELATION_ID; -import static reactor.rabbitmq.ResourcesSpecification.binding; -import static reactor.rabbitmq.ResourcesSpecification.exchange; -import static reactor.rabbitmq.ResourcesSpecification.queue; +import static org.reactivecommons.async.rabbit.communications.ResourcesSpecification.binding; +import static org.reactivecommons.async.rabbit.communications.ResourcesSpecification.exchange; +import static org.reactivecommons.async.rabbit.communications.ResourcesSpecification.queue; + @Log public class ApplicationReplyListener { diff --git a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/GenericMessageListener.java b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/GenericMessageListener.java index 02c35968..dac167ff 100644 --- a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/GenericMessageListener.java +++ b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/GenericMessageListener.java @@ -9,15 +9,15 @@ import org.reactivecommons.async.commons.utils.LoggerSubscriber; import org.reactivecommons.async.rabbit.InstanceIdentifier; import org.reactivecommons.async.rabbit.RabbitMessage; +import reactor.rabbitmq.AcknowledgableDelivery; +import reactor.rabbitmq.ConsumeOptions; import org.reactivecommons.async.rabbit.communications.ReactiveMessageListener; import org.reactivecommons.async.rabbit.communications.TopologyCreator; +import reactor.rabbitmq.Receiver; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Schedulers; -import reactor.rabbitmq.AcknowledgableDelivery; -import reactor.rabbitmq.ConsumeOptions; -import reactor.rabbitmq.Receiver; import reactor.util.retry.Retry; import java.time.Duration; diff --git a/async/async-rabbit/src/main/java/reactor/rabbitmq/AcknowledgableDelivery.java b/async/async-rabbit/src/main/java/reactor/rabbitmq/AcknowledgableDelivery.java new file mode 100644 index 00000000..0ab05610 --- /dev/null +++ b/async/async-rabbit/src/main/java/reactor/rabbitmq/AcknowledgableDelivery.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.rabbitmq; + +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Delivery; + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +public class AcknowledgableDelivery extends Delivery { + + private final Channel channel; + private final BiConsumer exceptionHandler; + + private final AtomicBoolean notAckedOrNacked = new AtomicBoolean(true); + + public AcknowledgableDelivery(Delivery delivery, Channel channel, + BiConsumer exceptionHandler) { + super(delivery.getEnvelope(), delivery.getProperties(), delivery.getBody()); + this.channel = channel; + this.exceptionHandler = exceptionHandler; + } + + public void ack(boolean multiple) { + if (notAckedOrNacked.getAndSet(false)) { + try { + basicAck(multiple); + } catch (Exception e) { + retry(e, (delivery) -> delivery.basicAck(multiple)); + } + } + } + + public void ack() { + ack(false); + } + + public void nack(boolean multiple, boolean requeue) { + if (notAckedOrNacked.getAndSet(false)) { + try { + basicNack(multiple, requeue); + } catch (Exception e) { + retry(e, (delivery) -> delivery.basicNack(multiple, requeue)); + } + } + } + + public void nack(boolean requeue) { + nack(false, requeue); + } + + private void basicAck(boolean multiple) { + try { + channel.basicAck(getEnvelope().getDeliveryTag(), multiple); + } catch (RuntimeException e) { + throw e; + } catch (IOException e) { + throw new RabbitFluxException(e); + } + } + + private void basicNack(boolean multiple, boolean requeue) { + try { + channel.basicNack(getEnvelope().getDeliveryTag(), multiple, requeue); + } catch (RuntimeException e) { + throw e; + } catch (IOException e) { + throw new RabbitFluxException(e); + } + } + + private void retry(Exception e, Consumer consumer) { + try { + exceptionHandler.accept(new Receiver.AcknowledgmentContext(this, consumer), e); + } catch (Exception e2) { + notAckedOrNacked.set(true); + throw e2; + } + } +} diff --git a/async/async-rabbit/src/main/java/reactor/rabbitmq/BindingSpecification.java b/async/async-rabbit/src/main/java/reactor/rabbitmq/BindingSpecification.java new file mode 100644 index 00000000..338f9d4b --- /dev/null +++ b/async/async-rabbit/src/main/java/reactor/rabbitmq/BindingSpecification.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.rabbitmq; + +import reactor.util.annotation.Nullable; + +import java.util.Map; + +public class BindingSpecification { + + private String queue, exchange, exchangeTo, routingKey; + private Map arguments; + + public static BindingSpecification binding() { + return new BindingSpecification(); + } + + public static BindingSpecification binding(String exchange, String routingKey, String queue) { + return new BindingSpecification().exchange(exchange).routingKey(routingKey).queue(queue); + } + + public static BindingSpecification queueBinding(String exchange, String routingKey, String queue) { + return binding(exchange, routingKey, queue); + } + + public static BindingSpecification exchangeBinding(String exchangeFrom, String routingKey, String exchangeTo) { + return new BindingSpecification().exchangeFrom(exchangeFrom).routingKey(routingKey).exchangeTo(exchangeTo); + } + + public BindingSpecification queue(String queue) { + this.queue = queue; + return this; + } + + public BindingSpecification exchange(String exchange) { + this.exchange = exchange; + return this; + } + + public BindingSpecification exchangeFrom(String exchangeFrom) { + this.exchange = exchangeFrom; + return this; + } + + public BindingSpecification exchangeTo(String exchangeTo) { + this.exchangeTo = exchangeTo; + return this; + } + + public BindingSpecification routingKey(String routingKey) { + this.routingKey = routingKey; + return this; + } + + public BindingSpecification arguments(@Nullable Map arguments) { + this.arguments = arguments; + return this; + } + + public String getQueue() { + return queue; + } + + public String getExchange() { + return exchange; + } + + public String getRoutingKey() { + return routingKey; + } + + public String getExchangeTo() { + return exchangeTo; + } + + @Nullable + public Map getArguments() { + return arguments; + } +} diff --git a/async/async-rabbit/src/main/java/reactor/rabbitmq/ChannelCloseHandlers.java b/async/async-rabbit/src/main/java/reactor/rabbitmq/ChannelCloseHandlers.java new file mode 100644 index 00000000..0091b061 --- /dev/null +++ b/async/async-rabbit/src/main/java/reactor/rabbitmq/ChannelCloseHandlers.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2018-2021 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.rabbitmq; + +import com.rabbitmq.client.Channel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.SignalType; + +import java.util.function.BiConsumer; + +public class ChannelCloseHandlers { + + public static final BiConsumer SENDER_CHANNEL_CLOSE_HANDLER_INSTANCE = + new SenderChannelCloseHandler(); + + public static class SenderChannelCloseHandler implements BiConsumer { + + private static final Logger LOGGER = LoggerFactory.getLogger(SenderChannelCloseHandler.class); + + @Override + public void accept(SignalType signalType, Channel channel) { + int channelNumber = channel.getChannelNumber(); + LOGGER.debug("closing channel {} by signal {}", channelNumber, signalType); + try { + if (channel.isOpen() && channel.getConnection().isOpen()) { + channel.close(); + } + } catch (Exception e) { + LOGGER.warn("Channel {} didn't close normally: {}", channelNumber, e.getMessage()); + } + } + } +} diff --git a/async/async-rabbit/src/main/java/reactor/rabbitmq/ChannelPool.java b/async/async-rabbit/src/main/java/reactor/rabbitmq/ChannelPool.java new file mode 100644 index 00000000..0218a720 --- /dev/null +++ b/async/async-rabbit/src/main/java/reactor/rabbitmq/ChannelPool.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2019-2021 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.rabbitmq; + +import com.rabbitmq.client.Channel; +import reactor.core.publisher.Mono; +import reactor.core.publisher.SignalType; + +import java.util.function.BiConsumer; + +public interface ChannelPool extends AutoCloseable { + + Mono getChannelMono(); + + BiConsumer getChannelCloseHandler(); + + void close(); +} diff --git a/async/async-rabbit/src/main/java/reactor/rabbitmq/ChannelPoolFactory.java b/async/async-rabbit/src/main/java/reactor/rabbitmq/ChannelPoolFactory.java new file mode 100644 index 00000000..c9aefba5 --- /dev/null +++ b/async/async-rabbit/src/main/java/reactor/rabbitmq/ChannelPoolFactory.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2019-2021 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.rabbitmq; + +import com.rabbitmq.client.Connection; +import reactor.core.publisher.Mono; + +public class ChannelPoolFactory { + + public static ChannelPool createChannelPool(Mono connectionMono) { + return createChannelPool(connectionMono, new ChannelPoolOptions()); + } + + public static ChannelPool createChannelPool(Mono connectionMono, + ChannelPoolOptions channelPoolOptions) { + return new LazyChannelPool(connectionMono, channelPoolOptions); + } +} diff --git a/async/async-rabbit/src/main/java/reactor/rabbitmq/ChannelPoolOptions.java b/async/async-rabbit/src/main/java/reactor/rabbitmq/ChannelPoolOptions.java new file mode 100644 index 00000000..ab1686e1 --- /dev/null +++ b/async/async-rabbit/src/main/java/reactor/rabbitmq/ChannelPoolOptions.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2019-2021 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.rabbitmq; + +import reactor.core.scheduler.Scheduler; +import reactor.util.annotation.Nullable; + +public class ChannelPoolOptions { + + private Integer maxCacheSize; + private Scheduler subscriptionScheduler; + + public ChannelPoolOptions maxCacheSize(int maxCacheSize) { + this.maxCacheSize = maxCacheSize; + return this; + } + + @Nullable + public Integer getMaxCacheSize() { + return maxCacheSize; + } + + public ChannelPoolOptions subscriptionScheduler(@Nullable Scheduler subscriptionScheduler) { + this.subscriptionScheduler = subscriptionScheduler; + return this; + } + + @Nullable + public Scheduler getSubscriptionScheduler() { + return subscriptionScheduler; + } +} diff --git a/async/async-rabbit/src/main/java/reactor/rabbitmq/ChannelProxy.java b/async/async-rabbit/src/main/java/reactor/rabbitmq/ChannelProxy.java new file mode 100644 index 00000000..02842ecf --- /dev/null +++ b/async/async-rabbit/src/main/java/reactor/rabbitmq/ChannelProxy.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2018-2021 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.rabbitmq; + +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.Method; + +import java.io.IOException; +import java.lang.reflect.Proxy; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Factory for creating a {@link Channel} proxy that only supports + * {@link Channel#asyncCompletableRpc(Method)}, re-opening the underlying channel if necessary. + * Used for resource management operations (declare, bind, unbind) in {@link Sender}. + */ +public final class ChannelProxy { + + private ChannelProxy() { + } + + /** + * Creates a dynamic proxy implementing {@link Channel} where only + * {@code asyncCompletableRpc} is functional. All other methods throw + * {@link UnsupportedOperationException}. + */ + public static Channel create(Connection connection) throws IOException { + var delegate = new AtomicReference<>(connection.createChannel()); + var lock = new ReentrantLock(); + + return (Channel) Proxy.newProxyInstance( + Channel.class.getClassLoader(), + new Class[]{Channel.class}, + (proxy, method, args) -> switch (method.getName()) { + case "asyncCompletableRpc" -> { + if (!delegate.get().isOpen()) { + lock.lock(); + try { + if (!delegate.get().isOpen()) { + delegate.set(connection.createChannel()); + } + } finally { + lock.unlock(); + } + } + yield delegate.get().asyncCompletableRpc((Method) args[0]); + } + case "toString" -> + "ChannelProxy[delegate=" + delegate.get() + "]"; + default -> throw new UnsupportedOperationException( + "ChannelProxy only supports asyncCompletableRpc, not " + method.getName()); + } + ); + } +} diff --git a/async/async-rabbit/src/main/java/reactor/rabbitmq/ConsumeOptions.java b/async/async-rabbit/src/main/java/reactor/rabbitmq/ConsumeOptions.java new file mode 100644 index 00000000..9a63ce3e --- /dev/null +++ b/async/async-rabbit/src/main/java/reactor/rabbitmq/ConsumeOptions.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.rabbitmq; + +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Delivery; +import java.util.Collections; +import java.util.Map; +import reactor.core.publisher.FluxSink; + +import java.time.Duration; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Consumer; + +public class ConsumeOptions { + + private int qos = 250; + private String consumerTag = ""; + private FluxSink.OverflowStrategy overflowStrategy = FluxSink.OverflowStrategy.BUFFER; + + private BiFunction hookBeforeEmitBiFunction = + (requestedFromDownstream, message) -> true; + + private BiFunction stopConsumingBiFunction = + (requestedFromDownstream, message) -> false; + + private BiConsumer exceptionHandler = + new ExceptionHandlers.RetryAcknowledgmentExceptionHandler( + Duration.ofSeconds(10), Duration.ofMillis(200), + ExceptionHandlers.CONNECTION_RECOVERY_PREDICATE + ); + + private Consumer channelCallback = ch -> { + }; + + private Map arguments = Collections.emptyMap(); + + public int getQos() { + return qos; + } + + public ConsumeOptions qos(int qos) { + if (qos < 0) { + throw new IllegalArgumentException("QoS must be greater or equal to 0"); + } + this.qos = qos; + return this; + } + + public String getConsumerTag() { + return consumerTag; + } + + public ConsumeOptions consumerTag(String consumerTag) { + if (consumerTag == null) { + throw new IllegalArgumentException("consumerTag must be non-null"); + } + this.consumerTag = consumerTag; + return this; + } + + public FluxSink.OverflowStrategy getOverflowStrategy() { + return overflowStrategy; + } + + public ConsumeOptions overflowStrategy(FluxSink.OverflowStrategy overflowStrategy) { + this.overflowStrategy = overflowStrategy; + return this; + } + + public BiFunction getHookBeforeEmitBiFunction() { + return hookBeforeEmitBiFunction; + } + + public ConsumeOptions hookBeforeEmitBiFunction(BiFunction hookBeforeEmit) { + this.hookBeforeEmitBiFunction = hookBeforeEmit; + return this; + } + + public BiFunction getStopConsumingBiFunction() { + return stopConsumingBiFunction; + } + + public ConsumeOptions stopConsumingBiFunction(BiFunction stopConsumingBiFunction) { + this.stopConsumingBiFunction = stopConsumingBiFunction; + return this; + } + + public ConsumeOptions exceptionHandler(BiConsumer exceptionHandler) { + this.exceptionHandler = exceptionHandler; + return this; + } + + public BiConsumer getExceptionHandler() { + return exceptionHandler; + } + + public ConsumeOptions channelCallback(Consumer channelCallback) { + this.channelCallback = channelCallback; + return this; + } + + public Consumer getChannelCallback() { + return channelCallback; + } + + public ConsumeOptions arguments(Map arguments) { + this.arguments = arguments; + return this; + } + + public Map getArguments() { + return arguments; + } +} diff --git a/async/async-rabbit/src/main/java/reactor/rabbitmq/ExceptionHandlers.java b/async/async-rabbit/src/main/java/reactor/rabbitmq/ExceptionHandlers.java new file mode 100644 index 00000000..ca96b138 --- /dev/null +++ b/async/async-rabbit/src/main/java/reactor/rabbitmq/ExceptionHandlers.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 2018-2021 VMware Inc. or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.rabbitmq; + +import com.rabbitmq.client.ShutdownSignalException; +import com.rabbitmq.client.impl.recovery.AutorecoveringConnection; + +import java.time.Duration; +import java.util.concurrent.Callable; +import java.util.function.BiConsumer; +import java.util.function.Predicate; + +public class ExceptionHandlers { + + public static Predicate CONNECTION_RECOVERY_PREDICATE = new ConnectionRecoveryTriggeringPredicate(); + + public static class ConnectionRecoveryTriggeringPredicate implements Predicate { + @Override + public boolean test(Throwable throwable) { + if (throwable instanceof ShutdownSignalException sse) { + return AutorecoveringConnection.DEFAULT_CONNECTION_RECOVERY_TRIGGERING_CONDITION.test(sse); + } + return false; + } + } + + public static class SimpleRetryTemplate { + + private final long timeout; + private final long waitingTime; + private final Predicate predicate; + private boolean failOnTimeout; + + public SimpleRetryTemplate(Duration timeout, Duration waitingTime, Predicate predicate) { + this(timeout, waitingTime, predicate, true); + } + + public SimpleRetryTemplate(Duration timeout, Duration waitingTime, Predicate predicate, + boolean failOnTimeout) { + if (timeout == null || timeout.isNegative() || timeout.isZero()) { + throw new IllegalArgumentException("Timeout must be greater than 0"); + } + if (waitingTime == null || waitingTime.isNegative() || waitingTime.isZero()) { + throw new IllegalArgumentException("Waiting time must be greater than 0"); + } + if (timeout.compareTo(waitingTime) <= 0) { + throw new IllegalArgumentException("Timeout must be greater than waiting time"); + } + if (predicate == null) { + throw new NullPointerException("Predicate cannot be null"); + } + this.timeout = timeout.toMillis(); + this.waitingTime = waitingTime.toMillis(); + this.predicate = predicate; + this.failOnTimeout = failOnTimeout; + } + + public void retry(Callable operation, Exception e) { + if (predicate.test(e)) { + int elapsedTime = 0; + boolean callSucceeded = false; + while (elapsedTime < timeout) { + try { + Thread.sleep(waitingTime); + } catch (InterruptedException ie) { + throw new RabbitFluxException("Thread interrupted while retry on sending", e); + } + elapsedTime += waitingTime; + try { + operation.call(); + callSucceeded = true; + break; + } catch (Throwable sendingException) { + if (!predicate.test(sendingException)) { + throw new RabbitFluxException("Not retryable exception thrown during retry", + sendingException); + } + } + } + if (!callSucceeded && failOnTimeout) { + throw new RabbitFluxRetryTimeoutException("Retry timed out after " + this.timeout + " ms", e); + } + } else { + throw new RabbitFluxException("Not retryable exception, cannot retry", e); + } + } + } + + public static class RetryAcknowledgmentExceptionHandler + implements BiConsumer { + + private final SimpleRetryTemplate retryTemplate; + + public RetryAcknowledgmentExceptionHandler(Duration timeout, Duration waitingTime, + Predicate predicate) { + this.retryTemplate = new SimpleRetryTemplate(timeout, waitingTime, predicate, false); + } + + @Override + public void accept(Receiver.AcknowledgmentContext acknowledgmentContext, Exception e) { + retryTemplate.retry(() -> { + acknowledgmentContext.ackOrNack(); + return null; + }, e); + } + } + + public static class RetrySendingExceptionHandler implements BiConsumer { + + private final SimpleRetryTemplate retryTemplate; + + public RetrySendingExceptionHandler(Duration timeout, Duration waitingTime, + Predicate predicate) { + this(timeout, waitingTime, predicate, true); + } + + public RetrySendingExceptionHandler(Duration timeout, Duration waitingTime, + Predicate predicate, boolean failOnTimeout) { + this.retryTemplate = new SimpleRetryTemplate(timeout, waitingTime, predicate, failOnTimeout); + } + + @Override + public void accept(Sender.SendContext sendContext, Exception e) { + retryTemplate.retry(() -> { + sendContext.publish(); + return null; + }, e); + } + } +} diff --git a/async/async-rabbit/src/main/java/reactor/rabbitmq/ExchangeSpecification.java b/async/async-rabbit/src/main/java/reactor/rabbitmq/ExchangeSpecification.java new file mode 100644 index 00000000..9632a401 --- /dev/null +++ b/async/async-rabbit/src/main/java/reactor/rabbitmq/ExchangeSpecification.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.rabbitmq; + +import reactor.util.annotation.Nullable; + +import java.util.Map; + +public class ExchangeSpecification { + + private String name; + private String type = "direct"; + private boolean durable = false, autoDelete = false, internal = false, passive = false; + private Map arguments; + + public static ExchangeSpecification exchange() { + return new ExchangeSpecification(); + } + + public static ExchangeSpecification exchange(String name) { + return new ExchangeSpecification().name(name); + } + + public ExchangeSpecification name(String name) { + this.name = name; + return this; + } + + public ExchangeSpecification type(String type) { + this.type = type; + return this; + } + + public ExchangeSpecification durable(boolean durable) { + this.durable = durable; + return this; + } + + public ExchangeSpecification autoDelete(boolean autoDelete) { + this.autoDelete = autoDelete; + return this; + } + + public ExchangeSpecification internal(boolean internal) { + this.internal = internal; + return this; + } + + public ExchangeSpecification passive(boolean passive) { + this.passive = passive; + return this; + } + + public ExchangeSpecification arguments(@Nullable Map arguments) { + this.arguments = arguments; + return this; + } + + public String getName() { + return name; + } + + public String getType() { + return type; + } + + public boolean isDurable() { + return durable; + } + + public boolean isAutoDelete() { + return autoDelete; + } + + public boolean isInternal() { + return internal; + } + + public boolean isPassive() { + return passive; + } + + @Nullable + public Map getArguments() { + return arguments; + } +} diff --git a/async/async-rabbit/src/main/java/reactor/rabbitmq/Helpers.java b/async/async-rabbit/src/main/java/reactor/rabbitmq/Helpers.java new file mode 100644 index 00000000..f313491c --- /dev/null +++ b/async/async-rabbit/src/main/java/reactor/rabbitmq/Helpers.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2019-2021 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.rabbitmq; + +import org.slf4j.Logger; + +class Helpers { + + static void safelyExecute(Logger logger, ExceptionRunnable action, String message) { + try { + action.run(); + } catch (Exception e) { + logger.warn(message + ": {}", e.getMessage()); + } + } + + @FunctionalInterface + interface ExceptionRunnable { + void run() throws Exception; + } +} diff --git a/async/async-rabbit/src/main/java/reactor/rabbitmq/LazyChannelPool.java b/async/async-rabbit/src/main/java/reactor/rabbitmq/LazyChannelPool.java new file mode 100644 index 00000000..2ec5e027 --- /dev/null +++ b/async/async-rabbit/src/main/java/reactor/rabbitmq/LazyChannelPool.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2019-2021 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.rabbitmq; + +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import reactor.core.publisher.Mono; +import reactor.core.publisher.SignalType; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.function.BiConsumer; + +import static reactor.rabbitmq.ChannelCloseHandlers.SENDER_CHANNEL_CLOSE_HANDLER_INSTANCE; + +class LazyChannelPool implements ChannelPool { + + private static final int DEFAULT_CHANNEL_POOL_SIZE = 5; + + private final Mono connectionMono; + private final BlockingQueue channelsQueue; + private final Scheduler subscriptionScheduler; + + LazyChannelPool(Mono connectionMono, ChannelPoolOptions channelPoolOptions) { + int channelsQueueCapacity = channelPoolOptions.getMaxCacheSize() == null ? + DEFAULT_CHANNEL_POOL_SIZE : channelPoolOptions.getMaxCacheSize(); + this.channelsQueue = new LinkedBlockingQueue<>(channelsQueueCapacity); + this.connectionMono = connectionMono; + this.subscriptionScheduler = channelPoolOptions.getSubscriptionScheduler() == null ? + Schedulers.newBoundedElastic( + Schedulers.DEFAULT_BOUNDED_ELASTIC_SIZE, + Schedulers.DEFAULT_BOUNDED_ELASTIC_QUEUESIZE, + "sender-channel-pool") : + channelPoolOptions.getSubscriptionScheduler(); + } + + public Mono getChannelMono() { + return connectionMono.map(connection -> { + Channel channel = channelsQueue.poll(); + if (channel == null) { + channel = createChannel(connection); + } else { + channel.clearConfirmListeners(); + channel.clearReturnListeners(); + } + return channel; + }).subscribeOn(subscriptionScheduler); + } + + @Override + public BiConsumer getChannelCloseHandler() { + return (signalType, channel) -> { + if (!channel.isOpen()) { + return; + } + boolean offer = signalType == SignalType.ON_COMPLETE && channelsQueue.offer(channel); + if (!offer) { + SENDER_CHANNEL_CLOSE_HANDLER_INSTANCE.accept(signalType, channel); + } + }; + } + + @Override + public void close() { + List channels = new ArrayList<>(); + channelsQueue.drainTo(channels); + channels.forEach(channel -> { + SENDER_CHANNEL_CLOSE_HANDLER_INSTANCE.accept(SignalType.ON_COMPLETE, channel); + }); + } + + private Channel createChannel(Connection connection) { + try { + return connection.createChannel(); + } catch (IOException e) { + throw new RabbitFluxException("Error while creating channel", e); + } + } +} diff --git a/async/async-rabbit/src/main/java/reactor/rabbitmq/OutboundMessage.java b/async/async-rabbit/src/main/java/reactor/rabbitmq/OutboundMessage.java new file mode 100644 index 00000000..291fda82 --- /dev/null +++ b/async/async-rabbit/src/main/java/reactor/rabbitmq/OutboundMessage.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.rabbitmq; + +import com.rabbitmq.client.AMQP.BasicProperties; +import reactor.util.annotation.Nullable; + +import java.util.Arrays; + +public class OutboundMessage { + + private final String exchange; + private final String routingKey; + private final BasicProperties properties; + private final byte[] body; + + private volatile boolean published = false; + + public OutboundMessage(String exchange, String routingKey, byte[] body) { + this(exchange, routingKey, null, body); + } + + public OutboundMessage(String exchange, String routingKey, @Nullable BasicProperties properties, byte[] body) { + this.exchange = exchange; + this.routingKey = routingKey; + this.properties = properties; + this.body = body; + } + + public String getExchange() { + return exchange; + } + + public String getRoutingKey() { + return routingKey; + } + + @Nullable + public BasicProperties getProperties() { + return properties; + } + + public byte[] getBody() { + return body; + } + + @Override + public String toString() { + return "OutboundMessage{" + + "exchange='" + exchange + '\'' + + ", routingKey='" + routingKey + '\'' + + ", properties=" + properties + + ", body=" + Arrays.toString(body) + + '}'; + } + + void published() { + this.published = true; + } + + boolean isPublished() { + return published; + } +} diff --git a/async/async-rabbit/src/main/java/reactor/rabbitmq/OutboundMessageResult.java b/async/async-rabbit/src/main/java/reactor/rabbitmq/OutboundMessageResult.java new file mode 100644 index 00000000..670ac8ec --- /dev/null +++ b/async/async-rabbit/src/main/java/reactor/rabbitmq/OutboundMessageResult.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.rabbitmq; + +public class OutboundMessageResult { + + private final OMSG outboundMessage; + private final boolean ack; + private final boolean returned; + + public OutboundMessageResult(OMSG outboundMessage, boolean ack) { + this(outboundMessage, ack, false); + } + + public OutboundMessageResult(OMSG outboundMessage, boolean ack, boolean returned) { + this.outboundMessage = outboundMessage; + this.ack = ack; + this.returned = returned; + } + + public OMSG getOutboundMessage() { + return outboundMessage; + } + + public boolean isAck() { + return ack; + } + + public boolean isReturned() { + return returned; + } + + @Override + public String toString() { + return "OutboundMessageResult{" + + "outboundMessage=" + outboundMessage + + ", ack=" + ack + + ", returned=" + returned + + '}'; + } +} diff --git a/async/async-rabbit/src/main/java/reactor/rabbitmq/QueueSpecification.java b/async/async-rabbit/src/main/java/reactor/rabbitmq/QueueSpecification.java new file mode 100644 index 00000000..fa5409cc --- /dev/null +++ b/async/async-rabbit/src/main/java/reactor/rabbitmq/QueueSpecification.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.rabbitmq; + +import reactor.util.annotation.Nullable; + +import java.util.Map; + +public class QueueSpecification { + + protected String name; + protected boolean durable = false; + protected boolean exclusive = false; + protected boolean autoDelete = false; + protected boolean passive = false; + protected Map arguments; + + public static QueueSpecification queue() { + return new NullNameQueueSpecification(); + } + + public static QueueSpecification queue(@Nullable String name) { + return new QueueSpecification().name(name); + } + + public QueueSpecification name(@Nullable String queue) { + if (queue == null) { + return new NullNameQueueSpecification().arguments(this.arguments); + } + this.name = queue; + return this; + } + + public QueueSpecification durable(boolean durable) { + this.durable = durable; + return this; + } + + public QueueSpecification exclusive(boolean exclusive) { + this.exclusive = exclusive; + return this; + } + + public QueueSpecification autoDelete(boolean autoDelete) { + this.autoDelete = autoDelete; + return this; + } + + public QueueSpecification arguments(@Nullable Map arguments) { + this.arguments = arguments; + return this; + } + + public QueueSpecification passive(boolean passive) { + this.passive = passive; + return this; + } + + @Nullable + public String getName() { + return name; + } + + public boolean isDurable() { + return durable; + } + + public boolean isExclusive() { + return exclusive; + } + + public boolean isAutoDelete() { + return autoDelete; + } + + public boolean isPassive() { + return passive; + } + + @Nullable + public Map getArguments() { + return arguments; + } + + private static class NullNameQueueSpecification extends QueueSpecification { + + NullNameQueueSpecification() { + this.name = null; + this.durable = false; + this.exclusive = true; + this.autoDelete = true; + this.passive = false; + } + + @Override + public QueueSpecification name(@Nullable String name) { + if (name == null) { + return this; + } + return QueueSpecification.queue(name) + .durable(durable) + .exclusive(exclusive) + .autoDelete(autoDelete) + .passive(passive); + } + + @Override + public QueueSpecification durable(boolean durable) { + if (this.durable != durable) { + throw new IllegalArgumentException("Once a queue has a null name, durable is always false"); + } + return this; + } + + @Override + public QueueSpecification exclusive(boolean exclusive) { + if (this.exclusive != exclusive) { + throw new IllegalArgumentException("Once a queue has a null name, exclusive is always true"); + } + return this; + } + + @Override + public QueueSpecification autoDelete(boolean autoDelete) { + if (this.autoDelete != autoDelete) { + throw new IllegalArgumentException("Once a queue has a null name, autoDelete is always true"); + } + return this; + } + + @Override + public QueueSpecification passive(boolean passive) { + if (this.passive != passive) { + throw new IllegalArgumentException("Once a queue has a null name, passive is always false"); + } + return this; + } + } +} diff --git a/async/async-rabbit/src/main/java/reactor/rabbitmq/RabbitFluxException.java b/async/async-rabbit/src/main/java/reactor/rabbitmq/RabbitFluxException.java new file mode 100644 index 00000000..7620621f --- /dev/null +++ b/async/async-rabbit/src/main/java/reactor/rabbitmq/RabbitFluxException.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.rabbitmq; + +public class RabbitFluxException extends RuntimeException { + + public RabbitFluxException() { + } + + public RabbitFluxException(String message) { + super(message); + } + + public RabbitFluxException(String message, Throwable cause) { + super(message, cause); + } + + public RabbitFluxException(Throwable cause) { + super(cause); + } + + public RabbitFluxException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/async/async-rabbit/src/main/java/reactor/rabbitmq/RabbitFluxRetryTimeoutException.java b/async/async-rabbit/src/main/java/reactor/rabbitmq/RabbitFluxRetryTimeoutException.java new file mode 100644 index 00000000..0486af59 --- /dev/null +++ b/async/async-rabbit/src/main/java/reactor/rabbitmq/RabbitFluxRetryTimeoutException.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2021 VMware, Inc. or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.rabbitmq; + +public class RabbitFluxRetryTimeoutException extends RabbitFluxException { + + public RabbitFluxRetryTimeoutException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/async/async-rabbit/src/main/java/reactor/rabbitmq/Receiver.java b/async/async-rabbit/src/main/java/reactor/rabbitmq/Receiver.java new file mode 100644 index 00000000..1926dbac --- /dev/null +++ b/async/async-rabbit/src/main/java/reactor/rabbitmq/Receiver.java @@ -0,0 +1,250 @@ +/* + * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.rabbitmq; + +import com.rabbitmq.client.*; +import com.rabbitmq.client.impl.recovery.AutorecoveringConnection; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.FluxSink; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; + +import java.io.Closeable; +import java.io.IOException; +import java.time.Duration; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.function.Function; + +import static reactor.rabbitmq.Helpers.safelyExecute; + +/** + * Reactive abstraction to consume messages as a {@link Flux}. + */ +public class Receiver implements Closeable { + + private static final Logger LOGGER = LoggerFactory.getLogger(Receiver.class); + + private static final Function CHANNEL_CREATION_FUNCTION = new Receiver.ChannelCreationFunction(); + + private final Mono connectionMono; + + private final AtomicReference connection = new AtomicReference<>(); + + private final Scheduler connectionSubscriptionScheduler; + + private final boolean privateConnectionSubscriptionScheduler; + + private final int connectionClosingTimeout; + + private final AtomicBoolean closingOrClosed = new AtomicBoolean(false); + + public Receiver() { + this(new ReceiverOptions()); + } + + public Receiver(ReceiverOptions options) { + this.privateConnectionSubscriptionScheduler = options.getConnectionSubscriptionScheduler() == null; + this.connectionSubscriptionScheduler = options.getConnectionSubscriptionScheduler() == null ? + createScheduler("rabbitmq-receiver-connection-subscription") : + options.getConnectionSubscriptionScheduler(); + + Mono cm; + if (options.getConnectionMono() == null) { + cm = Mono.fromCallable(() -> { + if (options.getConnectionSupplier() == null) { + return options.getConnectionFactory().newConnection(); + } else { + return options.getConnectionSupplier().apply(null); + } + }); + cm = options.getConnectionMonoConfigurator().apply(cm); + cm = cm.doOnNext(conn -> connection.set(conn)) + .subscribeOn(this.connectionSubscriptionScheduler) + .transform(this::cache); + } else { + cm = options.getConnectionMono(); + } + this.connectionMono = cm; + if (options.getConnectionClosingTimeout() != null && !Duration.ZERO.equals(options.getConnectionClosingTimeout())) { + this.connectionClosingTimeout = (int) options.getConnectionClosingTimeout().toMillis(); + } else { + this.connectionClosingTimeout = -1; + } + } + + protected Mono cache(Mono mono) { + return Utils.cache(mono); + } + + protected Scheduler createScheduler(String name) { + return Schedulers.newBoundedElastic( + Schedulers.DEFAULT_BOUNDED_ELASTIC_SIZE, + Schedulers.DEFAULT_BOUNDED_ELASTIC_QUEUESIZE, + name + ); + } + + protected void completeOnChannelShutdown(Channel channel, FluxSink emitter) { + channel.addShutdownListener(reason -> { + if (isRecoverable(channel)) { + // we complete only on "normal" (channels closed by application) for recoverable channels + // other cases includes disconnection, so the channel should recover and resume consuming + if (!AutorecoveringConnection.DEFAULT_CONNECTION_RECOVERY_TRIGGERING_CONDITION.test(reason)) { + emitter.complete(); + } + } else { + // we always complete for non-recoverable channels, because they won't recover by themselves + emitter.complete(); + } + }); + } + + public Flux consumeAutoAck(final String queue) { + return consumeAutoAck(queue, new ConsumeOptions()); + } + + public Flux consumeAutoAck(final String queue, ConsumeOptions options) { + return consumeManualAck(queue, options) + .doOnNext(AcknowledgableDelivery::ack) + .map(ackableMsg -> ackableMsg); + } + + public Flux consumeManualAck(final String queue) { + return consumeManualAck(queue, new ConsumeOptions()); + } + + public Flux consumeManualAck(final String queue, ConsumeOptions options) { + return Flux.create(emitter -> connectionMono.map(CHANNEL_CREATION_FUNCTION).subscribe(channel -> { + try { + if (options.getChannelCallback() != null) { + options.getChannelCallback().accept(channel); + } + if (options.getQos() != 0) { + channel.basicQos(options.getQos()); + } + + DeliverCallback deliverCallback = (consumerTag, message) -> { + AcknowledgableDelivery delivery = new AcknowledgableDelivery(message, channel, options.getExceptionHandler()); + if (options.getHookBeforeEmitBiFunction().apply(emitter.requestedFromDownstream(), delivery)) { + emitter.next(delivery); + } + if (options.getStopConsumingBiFunction().apply(emitter.requestedFromDownstream(), message)) { + emitter.complete(); + } + }; + + AtomicBoolean basicCancel = new AtomicBoolean(true); + CancelCallback cancelCallback = consumerTag -> { + LOGGER.info("Flux consumer {} has been cancelled", consumerTag); + basicCancel.set(false); + emitter.complete(); + }; + + completeOnChannelShutdown(channel, emitter); + + final String consumerTag = channel.basicConsume( + queue, + false, // no auto-ack + options.getConsumerTag(), + false, // noLocal (not supported by RabbitMQ) + false, // not exclusive + options.getArguments(), + deliverCallback, + cancelCallback); + AtomicBoolean cancelled = new AtomicBoolean(false); + LOGGER.info("Consumer {} consuming from {} has been registered", consumerTag, queue); + emitter.onDispose(() -> { + LOGGER.info("Cancelling consumer {} consuming from {}", consumerTag, queue); + if (cancelled.compareAndSet(false, true)) { + try { + if (channel.isOpen() && channel.getConnection().isOpen()) { + if (basicCancel.compareAndSet(true, false)) { + channel.basicCancel(consumerTag); + } + channel.close(); + } + } catch (TimeoutException | IOException e) { + LOGGER.warn("Error while closing channel: " + e.getMessage()); + } + } + }); + } catch (Exception e) { + emitter.error(new RabbitFluxException(e)); + } + }, emitter::error), options.getOverflowStrategy()); + } + + protected boolean isRecoverable(Connection connection) { + return Utils.isRecoverable(connection); + } + + protected boolean isRecoverable(Channel channel) { + return Utils.isRecoverable(channel); + } + + public void close() { + if (closingOrClosed.compareAndSet(false, true)) { + if (connection.get() != null) { + safelyExecute( + LOGGER, + () -> connection.get().close(this.connectionClosingTimeout), + "Error while closing receiver connection" + ); + } + if (privateConnectionSubscriptionScheduler) { + safelyExecute( + LOGGER, + () -> this.connectionSubscriptionScheduler.dispose(), + "Error while disposing connection subscriber scheduler" + ); + } + } + } + + public static class AcknowledgmentContext { + + private final AcknowledgableDelivery delivery; + private final Consumer consumer; + + public AcknowledgmentContext(AcknowledgableDelivery delivery, Consumer consumer) { + this.delivery = delivery; + this.consumer = consumer; + } + + public void ackOrNack() { + consumer.accept(delivery); + } + } + + private static class ChannelCreationFunction implements Function { + + @Override + public Channel apply(Connection connection) { + try { + return connection.createChannel(); + } catch (IOException e) { + throw new RabbitFluxException("Error while creating channel", e); + } + } + } +} diff --git a/async/async-rabbit/src/main/java/reactor/rabbitmq/ReceiverOptions.java b/async/async-rabbit/src/main/java/reactor/rabbitmq/ReceiverOptions.java new file mode 100644 index 00000000..3826184b --- /dev/null +++ b/async/async-rabbit/src/main/java/reactor/rabbitmq/ReceiverOptions.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.rabbitmq; + +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Scheduler; +import reactor.util.annotation.Nullable; + +import java.time.Duration; +import java.util.function.Function; +import java.util.function.Supplier; + +public class ReceiverOptions { + + private ConnectionFactory connectionFactory = ((Supplier) () -> { + ConnectionFactory connectionFactory = new ConnectionFactory(); + connectionFactory.useNio(); + return connectionFactory; + }).get(); + + private Mono connectionMono; + private Scheduler connectionSubscriptionScheduler; + private Utils.ExceptionFunction connectionSupplier; + private Function, Mono> connectionMonoConfigurator = cm -> cm; + private Duration connectionClosingTimeout = Duration.ofSeconds(30); + + public ConnectionFactory getConnectionFactory() { + return connectionFactory; + } + + public ReceiverOptions connectionFactory(ConnectionFactory connectionFactory) { + this.connectionFactory = connectionFactory; + return this; + } + + @Nullable + public Scheduler getConnectionSubscriptionScheduler() { + return connectionSubscriptionScheduler; + } + + public ReceiverOptions connectionSubscriptionScheduler(@Nullable Scheduler connectionSubscriptionScheduler) { + this.connectionSubscriptionScheduler = connectionSubscriptionScheduler; + return this; + } + + public ReceiverOptions connectionSupplier( + Utils.ExceptionFunction function) { + return this.connectionSupplier(this.connectionFactory, function); + } + + public ReceiverOptions connectionSupplier(ConnectionFactory connectionFactory, + Utils.ExceptionFunction function) { + this.connectionSupplier = ignored -> function.apply(connectionFactory); + return this; + } + + public ReceiverOptions connectionMono(@Nullable Mono connectionMono) { + this.connectionMono = connectionMono; + return this; + } + + public ReceiverOptions connectionMonoConfigurator( + Function, Mono> connectionMonoConfigurator) { + this.connectionMonoConfigurator = connectionMonoConfigurator; + return this; + } + + @Nullable + public Mono getConnectionMono() { + return connectionMono; + } + + @Nullable + public Utils.ExceptionFunction getConnectionSupplier() { + return connectionSupplier; + } + + public Function, Mono> getConnectionMonoConfigurator() { + return connectionMonoConfigurator; + } + + public ReceiverOptions connectionClosingTimeout(@Nullable Duration connectionClosingTimeout) { + this.connectionClosingTimeout = connectionClosingTimeout; + return this; + } + + @Nullable + public Duration getConnectionClosingTimeout() { + return connectionClosingTimeout; + } +} diff --git a/async/async-rabbit/src/main/java/reactor/rabbitmq/SendOptions.java b/async/async-rabbit/src/main/java/reactor/rabbitmq/SendOptions.java new file mode 100644 index 00000000..9796fcdd --- /dev/null +++ b/async/async-rabbit/src/main/java/reactor/rabbitmq/SendOptions.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2018-2021 VMware Inc. or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.rabbitmq; + +import com.rabbitmq.client.Channel; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Mono; +import reactor.core.publisher.SignalType; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; +import reactor.util.annotation.Nullable; + +import java.time.Duration; +import java.util.function.BiConsumer; + +public class SendOptions { + + private BiConsumer exceptionHandler = new ExceptionHandlers.RetrySendingExceptionHandler( + Duration.ofSeconds(10), Duration.ofMillis(200), + ExceptionHandlers.CONNECTION_RECOVERY_PREDICATE + ); + + private Integer maxInFlight; + private boolean trackReturned; + private Scheduler scheduler = Schedulers.immediate(); + private Mono channelMono; + private BiConsumer channelCloseHandler; + + @Nullable + public Integer getMaxInFlight() { + return maxInFlight; + } + + public boolean isTrackReturned() { + return trackReturned; + } + + public SendOptions trackReturned(boolean trackReturned) { + this.trackReturned = trackReturned; + return this; + } + + public SendOptions maxInFlight(int maxInFlight) { + this.maxInFlight = maxInFlight; + return this; + } + + public SendOptions maxInFlight(int maxInFlight, Scheduler scheduler) { + this.maxInFlight = maxInFlight; + this.scheduler = scheduler; + return this; + } + + public Scheduler getScheduler() { + return scheduler; + } + + public BiConsumer getExceptionHandler() { + return exceptionHandler; + } + + public SendOptions exceptionHandler(BiConsumer exceptionHandler) { + this.exceptionHandler = exceptionHandler; + return this; + } + + @Nullable + public Mono getChannelMono() { + return channelMono; + } + + public SendOptions channelMono(@Nullable Mono channelMono) { + this.channelMono = channelMono; + return this; + } + + @Nullable + public BiConsumer getChannelCloseHandler() { + return channelCloseHandler; + } + + public SendOptions channelCloseHandler(@Nullable BiConsumer channelCloseHandler) { + this.channelCloseHandler = channelCloseHandler; + return this; + } + + public SendOptions channelPool(ChannelPool channelPool) { + this.channelMono = channelPool.getChannelMono(); + this.channelCloseHandler = channelPool.getChannelCloseHandler(); + return this; + } +} diff --git a/async/async-rabbit/src/main/java/reactor/rabbitmq/Sender.java b/async/async-rabbit/src/main/java/reactor/rabbitmq/Sender.java new file mode 100644 index 00000000..7f290aca --- /dev/null +++ b/async/async-rabbit/src/main/java/reactor/rabbitmq/Sender.java @@ -0,0 +1,675 @@ +/* + * Copyright (c) 2017-2021 VMware Inc. or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.rabbitmq; + +import com.rabbitmq.client.*; +import com.rabbitmq.client.impl.AMQImpl; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.CoreSubscriber; +import reactor.core.publisher.*; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; +import reactor.util.annotation.Nullable; + +import java.io.IOException; +import java.time.Duration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentNavigableMap; +import java.util.concurrent.ConcurrentSkipListMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Stream; + +import static reactor.rabbitmq.Helpers.safelyExecute; + +/** + * Reactive abstraction to create resources and send messages. + */ +public class Sender implements AutoCloseable { + + private static final Logger LOGGER = LoggerFactory.getLogger(Sender.class); + + private static final Function CHANNEL_CREATION_FUNCTION = new ChannelCreationFunction(); + + private static final Function CHANNEL_PROXY_CREATION_FUNCTION = new ChannelProxyCreationFunction(); + + private final Mono connectionMono; + + private final Mono channelMono; + + private final BiConsumer channelCloseHandler; + + private final AtomicReference connection = new AtomicReference<>(); + + private final Mono resourceManagementChannelMono; + + private final Scheduler resourceManagementScheduler; + + private final boolean privateResourceManagementScheduler; + + private final Scheduler connectionSubscriptionScheduler; + + private final boolean privateConnectionSubscriptionScheduler; + + private final ExecutorService channelCloseThreadPool = Executors.newCachedThreadPool(); + + private final int connectionClosingTimeout; + + private final AtomicBoolean closingOrClosed = new AtomicBoolean(false); + + private static final String REACTOR_RABBITMQ_DELIVERY_TAG_HEADER = "reactor_rabbitmq_delivery_tag"; + + public Sender() { + this(new SenderOptions()); + } + + public Sender(SenderOptions options) { + this.privateConnectionSubscriptionScheduler = options.getConnectionSubscriptionScheduler() == null; + this.connectionSubscriptionScheduler = options.getConnectionSubscriptionScheduler() == null + ? createScheduler("rabbitmq-sender-connection-subscription") + : options.getConnectionSubscriptionScheduler(); + + Mono cm; + if (options.getConnectionMono() == null) { + cm = Mono.fromCallable(() -> { + if (options.getConnectionSupplier() == null) { + return options.getConnectionFactory().newConnection(); + } else { + return options.getConnectionSupplier().apply(null); + } + }); + cm = options.getConnectionMonoConfigurator().apply(cm); + cm = cm.doOnNext(conn -> connection.set(conn)) + .subscribeOn(this.connectionSubscriptionScheduler) + .transform(this::cache); + } else { + cm = options.getConnectionMono(); + } + + this.connectionMono = cm; + this.channelMono = options.getChannelMono(); + this.channelCloseHandler = options.getChannelCloseHandler() == null + ? ChannelCloseHandlers.SENDER_CHANNEL_CLOSE_HANDLER_INSTANCE + : options.getChannelCloseHandler(); + this.privateResourceManagementScheduler = options.getResourceManagementScheduler() == null; + this.resourceManagementScheduler = options.getResourceManagementScheduler() == null + ? createScheduler("rabbitmq-sender-resource-creation") + : options.getResourceManagementScheduler(); + this.resourceManagementChannelMono = options.getResourceManagementChannelMono() == null + ? connectionMono.map(CHANNEL_PROXY_CREATION_FUNCTION).transform(this::cache) + : options.getResourceManagementChannelMono(); + if (options.getConnectionClosingTimeout() != null && !Duration.ZERO.equals(options.getConnectionClosingTimeout())) { + this.connectionClosingTimeout = (int) options.getConnectionClosingTimeout().toMillis(); + } else { + this.connectionClosingTimeout = -1; + } + } + + protected Scheduler createScheduler(String name) { + return Schedulers.newBoundedElastic( + Schedulers.DEFAULT_BOUNDED_ELASTIC_SIZE, + Schedulers.DEFAULT_BOUNDED_ELASTIC_QUEUESIZE, + name + ); + } + + protected Mono cache(Mono mono) { + return Utils.cache(mono); + } + + // --- Send --- + + public Mono send(Publisher messages) { + return send(messages, new SendOptions()); + } + + public Mono send(Publisher messages, @Nullable SendOptions options) { + var opts = options == null ? new SendOptions() : options; + final Mono currentChannelMono = getChannelMono(opts); + final BiConsumer exceptionHandler = opts.getExceptionHandler(); + final BiConsumer closeHandler = getChannelCloseHandler(opts); + + return currentChannelMono.flatMapMany(channel -> + Flux.from(messages) + .doOnNext(message -> { + try { + channel.basicPublish( + message.getExchange(), + message.getRoutingKey(), + message.getProperties(), + message.getBody() + ); + } catch (Exception e) { + exceptionHandler.accept(new SendContext<>(channel, message), e); + } + }) + .doOnError(e -> LOGGER.warn("Send failed with exception {}", e)) + .doFinally(st -> closeHandler.accept(st, channel)) + ).then(); + } + + // --- Publish Confirms --- + + public Flux sendWithPublishConfirms(Publisher messages) { + return sendWithPublishConfirms(messages, new SendOptions()); + } + + public Flux sendWithPublishConfirms(Publisher messages, SendOptions options) { + return Flux.from(sendWithTypedPublishConfirms(messages, options)); + } + + public Flux> sendWithTypedPublishConfirms(Publisher messages) { + return sendWithTypedPublishConfirms(messages, new SendOptions()); + } + + public Flux> sendWithTypedPublishConfirms( + Publisher messages, @Nullable SendOptions options) { + var sendOptions = options == null ? new SendOptions() : options; + final Mono currentChannelMono = getChannelMono(sendOptions); + final BiConsumer closeHandler = getChannelCloseHandler(sendOptions); + + Flux> result = currentChannelMono.map(channel -> { + try { + channel.confirmSelect(); + } catch (IOException e) { + throw new RabbitFluxException("Error while setting publisher confirms on channel", e); + } + return channel; + }).flatMapMany(channel -> new PublishConfirmOperator<>(messages, channel, sendOptions).doFinally(signalType -> { + if (signalType == SignalType.ON_ERROR) { + closeHandler.accept(signalType, channel); + } else { + channelCloseThreadPool.execute(() -> closeHandler.accept(signalType, channel)); + } + })); + + if (sendOptions.getMaxInFlight() != null) { + result = result.publishOn(sendOptions.getScheduler(), sendOptions.getMaxInFlight()); + } + return result; + } + + // package-protected for testing + Mono getChannelMono(SendOptions options) { + return Stream.of(options.getChannelMono(), channelMono) + .filter(Objects::nonNull) + .findFirst().orElse(connectionMono.map(CHANNEL_CREATION_FUNCTION)); + } + + private BiConsumer getChannelCloseHandler(SendOptions options) { + return options.getChannelCloseHandler() != null + ? options.getChannelCloseHandler() : this.channelCloseHandler; + } + + // --- Resource Management (declare / bind / unbind) --- + + public Mono declare(QueueSpecification specification) { + AMQP.Queue.Declare declare; + if (specification.getName() == null) { + declare = new AMQImpl.Queue.Declare.Builder() + .queue("") + .durable(false) + .exclusive(true) + .autoDelete(true) + .arguments(specification.getArguments()) + .build(); + } else { + declare = new AMQImpl.Queue.Declare.Builder() + .queue(specification.getName()) + .durable(specification.isDurable()) + .exclusive(specification.isExclusive()) + .autoDelete(specification.isAutoDelete()) + .passive(specification.isPassive()) + .arguments(specification.getArguments()) + .build(); + } + + return resourceManagementChannelMono.map(channel -> { + try { + return channel.asyncCompletableRpc(declare); + } catch (IOException e) { + throw new RabbitFluxException("Error during RPC call", e); + } + }).flatMap(Mono::fromCompletionStage) + .map(command -> (AMQP.Queue.DeclareOk) command.getMethod()) + .publishOn(resourceManagementScheduler); + } + + public Mono declare(ExchangeSpecification specification) { + AMQP.Exchange.Declare declare = new AMQImpl.Exchange.Declare.Builder() + .exchange(specification.getName()) + .type(specification.getType()) + .durable(specification.isDurable()) + .autoDelete(specification.isAutoDelete()) + .internal(specification.isInternal()) + .passive(specification.isPassive()) + .arguments(specification.getArguments()) + .build(); + + return resourceManagementChannelMono.map(channel -> { + try { + return channel.asyncCompletableRpc(declare); + } catch (IOException e) { + throw new RabbitFluxException("Error during RPC call", e); + } + }).flatMap(Mono::fromCompletionStage) + .map(command -> (AMQP.Exchange.DeclareOk) command.getMethod()) + .publishOn(resourceManagementScheduler); + } + + public Mono bind(BindingSpecification specification) { + AMQP.Queue.Bind binding = new AMQImpl.Queue.Bind.Builder() + .exchange(specification.getExchange()) + .queue(specification.getQueue()) + .routingKey(specification.getRoutingKey()) + .arguments(specification.getArguments()) + .build(); + + return resourceManagementChannelMono.map(channel -> { + try { + return channel.asyncCompletableRpc(binding); + } catch (IOException e) { + throw new RabbitFluxException("Error during RPC call", e); + } + }).flatMap(Mono::fromCompletionStage) + .map(command -> (AMQP.Queue.BindOk) command.getMethod()) + .publishOn(resourceManagementScheduler); + } + + public Mono unbind(BindingSpecification specification) { + AMQP.Queue.Unbind unbinding = new AMQImpl.Queue.Unbind.Builder() + .exchange(specification.getExchange()) + .queue(specification.getQueue()) + .routingKey(specification.getRoutingKey()) + .arguments(specification.getArguments()) + .build(); + + return resourceManagementChannelMono.map(channel -> { + try { + return channel.asyncCompletableRpc(unbinding); + } catch (IOException e) { + throw new RabbitFluxException("Error during RPC call", e); + } + }).flatMap(Mono::fromCompletionStage) + .map(command -> (AMQP.Queue.UnbindOk) command.getMethod()) + .publishOn(resourceManagementScheduler); + } + + // --- Lifecycle --- + + @Override + public void close() { + if (closingOrClosed.compareAndSet(false, true)) { + if (connection.get() != null) { + safelyExecute(LOGGER, () -> connection.get().close(this.connectionClosingTimeout), + "Error while closing sender connection"); + } + if (this.privateConnectionSubscriptionScheduler) { + safelyExecute(LOGGER, () -> this.connectionSubscriptionScheduler.dispose(), + "Error while disposing connection subscription scheduler"); + } + if (this.privateResourceManagementScheduler) { + safelyExecute(LOGGER, () -> this.resourceManagementScheduler.dispose(), + "Error while disposing resource management scheduler"); + } + safelyExecute(LOGGER, () -> channelCloseThreadPool.shutdown(), + "Error while closing channel closing thread pool"); + } + } + + // --- Inner Classes --- + + public static class SendContext { + + protected final Channel channel; + protected final OMSG message; + + protected SendContext(Channel channel, OMSG message) { + this.channel = channel; + this.message = message; + } + + public OMSG getMessage() { + return message; + } + + public Channel getChannel() { + return channel; + } + + public void publish(OutboundMessage outboundMessage) throws Exception { + this.channel.basicPublish( + outboundMessage.getExchange(), + outboundMessage.getRoutingKey(), + outboundMessage.getProperties(), + outboundMessage.getBody() + ); + } + + public void publish() throws Exception { + this.publish(getMessage()); + } + } + + public static class ConfirmSendContext extends SendContext { + + private final PublishConfirmSubscriber subscriber; + + protected ConfirmSendContext(Channel channel, OMSG message, PublishConfirmSubscriber subscriber) { + super(channel, message); + this.subscriber = subscriber; + } + + @Override + public void publish(OutboundMessage outboundMessage) throws Exception { + long nextPublishSeqNo = channel.getNextPublishSeqNo(); + try { + subscriber.unconfirmed.putIfAbsent(nextPublishSeqNo, this.message); + this.channel.basicPublish( + outboundMessage.getExchange(), + outboundMessage.getRoutingKey(), + this.subscriber.trackReturned, + this.subscriber.propertiesProcessor.apply(message.getProperties(), nextPublishSeqNo), + outboundMessage.getBody() + ); + } catch (Exception e) { + subscriber.unconfirmed.remove(nextPublishSeqNo); + throw e; + } + } + + @Override + public void publish() throws Exception { + this.publish(getMessage()); + } + } + + private static class PublishConfirmOperator + extends FluxOperator> { + + private final Channel channel; + private final SendOptions options; + + PublishConfirmOperator(Publisher source, Channel channel, SendOptions options) { + super(Flux.from(source)); + this.channel = channel; + this.options = options; + } + + @Override + public void subscribe(CoreSubscriber> actual) { + source.subscribe(new PublishConfirmSubscriber<>(channel, actual, options)); + } + } + + static class PublishConfirmSubscriber implements + CoreSubscriber, Subscription { + + private final AtomicReference state = new AtomicReference<>(SubscriberState.INIT); + private final AtomicReference firstException = new AtomicReference<>(); + final ConcurrentNavigableMap unconfirmed = new ConcurrentSkipListMap<>(); + + private final Channel channel; + private final Subscriber> subscriber; + private final BiConsumer exceptionHandler; + + private Subscription subscription; + private ConfirmListener confirmListener; + private ReturnListener returnListener; + private ShutdownListener shutdownListener; + + final boolean trackReturned; + final BiFunction propertiesProcessor; + + PublishConfirmSubscriber(Channel channel, Subscriber> subscriber, + SendOptions options) { + this.channel = channel; + this.subscriber = subscriber; + this.exceptionHandler = options.getExceptionHandler(); + this.trackReturned = options.isTrackReturned(); + this.propertiesProcessor = this.trackReturned + ? PublishConfirmSubscriber::addReactorRabbitMQDeliveryTag + : (properties, deliveryTag) -> properties; + } + + @Override + public void request(long n) { + subscription.request(n); + } + + @Override + public void cancel() { + subscription.cancel(); + } + + @Override + public void onSubscribe(Subscription subscription) { + if (Operators.validate(this.subscription, subscription)) { + + if (this.trackReturned) { + this.returnListener = (replyCode, replyText, exchange, routingKey, properties, body) -> { + try { + Object deliveryTagObj = properties.getHeaders().get(REACTOR_RABBITMQ_DELIVERY_TAG_HEADER); + if (deliveryTagObj instanceof Long deliveryTag) { + OMSG outboundMessage = unconfirmed.get(deliveryTag); + subscriber.onNext(new OutboundMessageResult<>(outboundMessage, true, true)); + unconfirmed.remove(deliveryTag); + } else { + handleError(new IllegalArgumentException( + "Missing header " + REACTOR_RABBITMQ_DELIVERY_TAG_HEADER), null); + } + } catch (Exception e) { + handleError(e, null); + } + }; + channel.addReturnListener(this.returnListener); + } + + this.confirmListener = new ConfirmListener() { + + @Override + public void handleAck(long deliveryTag, boolean multiple) { + handleAckNack(deliveryTag, multiple, true); + } + + @Override + public void handleNack(long deliveryTag, boolean multiple) { + handleAckNack(deliveryTag, multiple, false); + } + + private void handleAckNack(long deliveryTag, boolean multiple, boolean ack) { + if (multiple) { + try { + var unconfirmedToSend = unconfirmed.headMap(deliveryTag, true); + var iterator = unconfirmedToSend.entrySet().iterator(); + while (iterator.hasNext()) { + subscriber.onNext(new OutboundMessageResult<>( + iterator.next().getValue(), ack, false)); + iterator.remove(); + } + } catch (Exception e) { + handleError(e, null); + } + } else { + OMSG outboundMessage = unconfirmed.get(deliveryTag); + if (outboundMessage != null) { + try { + unconfirmed.remove(deliveryTag); + subscriber.onNext(new OutboundMessageResult<>(outboundMessage, ack, false)); + } catch (Exception e) { + handleError(e, new OutboundMessageResult<>(outboundMessage, ack, false)); + } + } + } + if (unconfirmed.isEmpty()) { + maybeComplete(); + } + } + }; + channel.addConfirmListener(confirmListener); + + this.shutdownListener = sse -> { + var iterator = this.unconfirmed.entrySet().iterator(); + while (iterator.hasNext()) { + OMSG message = iterator.next().getValue(); + if (!message.isPublished()) { + try { + subscriber.onNext(new OutboundMessageResult<>(message, false, false)); + iterator.remove(); + } catch (Exception e) { + LOGGER.info("Error while nacking messages after channel failure"); + } + } + } + if (!sse.isHardError() && !sse.isInitiatedByApplication()) { + subscriber.onError(sse); + } + }; + channel.addShutdownListener(shutdownListener); + + state.set(SubscriberState.ACTIVE); + this.subscription = subscription; + subscriber.onSubscribe(this); + } + } + + @Override + public void onNext(OMSG message) { + if (checkComplete(message)) { + return; + } + long nextPublishSeqNo = channel.getNextPublishSeqNo(); + try { + unconfirmed.putIfAbsent(nextPublishSeqNo, message); + channel.basicPublish( + message.getExchange(), + message.getRoutingKey(), + this.trackReturned, + this.propertiesProcessor.apply(message.getProperties(), nextPublishSeqNo), + message.getBody() + ); + message.published(); + } catch (Exception e) { + unconfirmed.remove(nextPublishSeqNo); + try { + this.exceptionHandler.accept(new ConfirmSendContext<>(channel, message, this), e); + } catch (RabbitFluxRetryTimeoutException timeoutException) { + subscriber.onNext(new OutboundMessageResult<>(message, false, false)); + } catch (Exception innerException) { + handleError(innerException, new OutboundMessageResult<>(message, false, false)); + } + } + } + + private static AMQP.BasicProperties addReactorRabbitMQDeliveryTag( + AMQP.BasicProperties properties, long deliveryTag) { + var baseProperties = properties != null ? properties : new AMQP.BasicProperties(); + var headers = baseProperties.getHeaders() != null + ? new HashMap<>(baseProperties.getHeaders()) : new HashMap(); + headers.putIfAbsent(REACTOR_RABBITMQ_DELIVERY_TAG_HEADER, deliveryTag); + return baseProperties.builder().headers(headers).build(); + } + + @Override + public void onError(Throwable throwable) { + if (state.compareAndSet(SubscriberState.ACTIVE, SubscriberState.COMPLETE) + || state.compareAndSet(SubscriberState.OUTBOUND_DONE, SubscriberState.COMPLETE)) { + channel.removeConfirmListener(confirmListener); + channel.removeShutdownListener(shutdownListener); + if (returnListener != null) { + channel.removeReturnListener(returnListener); + } + subscriber.onError(throwable); + } else if (firstException.compareAndSet(null, throwable) && state.get() == SubscriberState.COMPLETE) { + Operators.onErrorDropped(throwable, currentContext()); + } + } + + @Override + public void onComplete() { + if (state.compareAndSet(SubscriberState.ACTIVE, SubscriberState.OUTBOUND_DONE) + && unconfirmed.isEmpty()) { + maybeComplete(); + } + } + + private void handleError(Exception e, @Nullable OutboundMessageResult result) { + LOGGER.error("error in publish confirm sending", e); + boolean complete = checkComplete(e); + firstException.compareAndSet(null, e); + if (!complete) { + if (result != null) { + subscriber.onNext(result); + } + onError(e); + } + } + + private void maybeComplete() { + if (state.compareAndSet(SubscriberState.OUTBOUND_DONE, SubscriberState.COMPLETE)) { + channel.removeConfirmListener(confirmListener); + channel.removeShutdownListener(shutdownListener); + if (returnListener != null) { + channel.removeReturnListener(returnListener); + } + subscriber.onComplete(); + } + } + + boolean checkComplete(T t) { + boolean complete = state.get() == SubscriberState.COMPLETE; + if (complete && firstException.get() == null) { + Operators.onNextDropped(t, currentContext()); + } + return complete; + } + } + + private static class ChannelCreationFunction implements Function { + @Override + public Channel apply(Connection connection) { + try { + return connection.createChannel(); + } catch (IOException e) { + throw new RabbitFluxException("Error while creating channel", e); + } + } + } + + private static class ChannelProxyCreationFunction implements Function { + @Override + public Channel apply(Connection connection) { + try { + return ChannelProxy.create(connection); + } catch (IOException e) { + throw new RabbitFluxException("Error while creating channel", e); + } + } + } +} diff --git a/async/async-rabbit/src/main/java/reactor/rabbitmq/SenderOptions.java b/async/async-rabbit/src/main/java/reactor/rabbitmq/SenderOptions.java new file mode 100644 index 00000000..576126e4 --- /dev/null +++ b/async/async-rabbit/src/main/java/reactor/rabbitmq/SenderOptions.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.rabbitmq; + +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; +import reactor.core.publisher.Mono; +import reactor.core.publisher.SignalType; +import reactor.core.scheduler.Scheduler; +import reactor.util.annotation.Nullable; + +import java.time.Duration; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.Supplier; + +public class SenderOptions { + + private ConnectionFactory connectionFactory = ((Supplier) () -> { + ConnectionFactory connectionFactory = new ConnectionFactory(); + connectionFactory.useNio(); + return connectionFactory; + }).get(); + + private Mono connectionMono; + private Mono channelMono; + private BiConsumer channelCloseHandler; + private Scheduler resourceManagementScheduler; + private Scheduler connectionSubscriptionScheduler; + private Mono resourceManagementChannelMono; + private Utils.ExceptionFunction connectionSupplier; + private Function, Mono> connectionMonoConfigurator = cm -> cm; + private Duration connectionClosingTimeout = Duration.ofSeconds(30); + + public ConnectionFactory getConnectionFactory() { + return connectionFactory; + } + + public SenderOptions connectionFactory(ConnectionFactory connectionFactory) { + this.connectionFactory = connectionFactory; + return this; + } + + @Nullable + public Scheduler getResourceManagementScheduler() { + return resourceManagementScheduler; + } + + public SenderOptions resourceManagementScheduler(@Nullable Scheduler resourceManagementScheduler) { + this.resourceManagementScheduler = resourceManagementScheduler; + return this; + } + + @Nullable + public Scheduler getConnectionSubscriptionScheduler() { + return connectionSubscriptionScheduler; + } + + public SenderOptions connectionSubscriptionScheduler(@Nullable Scheduler connectionSubscriptionScheduler) { + this.connectionSubscriptionScheduler = connectionSubscriptionScheduler; + return this; + } + + public SenderOptions connectionSupplier(Utils.ExceptionFunction function) { + return this.connectionSupplier(this.connectionFactory, function); + } + + public SenderOptions connectionSupplier(ConnectionFactory connectionFactory, + Utils.ExceptionFunction function) { + this.connectionSupplier = ignored -> function.apply(connectionFactory); + return this; + } + + public SenderOptions connectionMono(@Nullable Mono connectionMono) { + this.connectionMono = connectionMono; + return this; + } + + @Nullable + public Mono getConnectionMono() { + return connectionMono; + } + + public SenderOptions channelMono(@Nullable Mono channelMono) { + this.channelMono = channelMono; + return this; + } + + @Nullable + public Mono getChannelMono() { + return channelMono; + } + + @Nullable + public BiConsumer getChannelCloseHandler() { + return channelCloseHandler; + } + + public SenderOptions channelCloseHandler(@Nullable BiConsumer channelCloseHandler) { + this.channelCloseHandler = channelCloseHandler; + return this; + } + + public SenderOptions channelPool(ChannelPool channelPool) { + this.channelMono = channelPool.getChannelMono(); + this.channelCloseHandler = channelPool.getChannelCloseHandler(); + return this; + } + + public SenderOptions connectionMonoConfigurator( + Function, Mono> connectionMonoConfigurator) { + this.connectionMonoConfigurator = connectionMonoConfigurator; + return this; + } + + public SenderOptions resourceManagementChannelMono(@Nullable Mono resourceManagementChannelMono) { + this.resourceManagementChannelMono = resourceManagementChannelMono; + return this; + } + + @Nullable + public Mono getResourceManagementChannelMono() { + return resourceManagementChannelMono; + } + + @Nullable + public Utils.ExceptionFunction getConnectionSupplier() { + return connectionSupplier; + } + + public Function, Mono> getConnectionMonoConfigurator() { + return connectionMonoConfigurator; + } + + public SenderOptions connectionClosingTimeout(@Nullable Duration connectionClosingTimeout) { + this.connectionClosingTimeout = connectionClosingTimeout; + return this; + } + + @Nullable + public Duration getConnectionClosingTimeout() { + return connectionClosingTimeout; + } +} diff --git a/async/async-rabbit/src/main/java/reactor/rabbitmq/SubscriberState.java b/async/async-rabbit/src/main/java/reactor/rabbitmq/SubscriberState.java new file mode 100644 index 00000000..4dc64096 --- /dev/null +++ b/async/async-rabbit/src/main/java/reactor/rabbitmq/SubscriberState.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2018-2021 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.rabbitmq; + +enum SubscriberState { + INIT, + ACTIVE, + OUTBOUND_DONE, + COMPLETE +} diff --git a/async/async-rabbit/src/main/java/reactor/rabbitmq/Utils.java b/async/async-rabbit/src/main/java/reactor/rabbitmq/Utils.java new file mode 100644 index 00000000..58d3e16c --- /dev/null +++ b/async/async-rabbit/src/main/java/reactor/rabbitmq/Utils.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2018-2021 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.rabbitmq; + +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; +import com.rabbitmq.client.RecoverableChannel; +import com.rabbitmq.client.RecoverableConnection; +import reactor.core.publisher.Mono; + +import java.time.Duration; + +public abstract class Utils { + + public static Mono cache(Mono mono) { + return mono.cache( + element -> Duration.ofMillis(Long.MAX_VALUE), + throwable -> Duration.ZERO, + () -> Duration.ZERO); + } + + static boolean isRecoverable(Connection connection) { + return connection instanceof RecoverableConnection; + } + + static boolean isRecoverable(Channel channel) { + return channel instanceof RecoverableChannel; + } + + @FunctionalInterface + public interface ExceptionFunction { + R apply(T t) throws Exception; + } +} diff --git a/async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/DynamicRegistryImpTest.java b/async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/DynamicRegistryImpTest.java index 2f3ea505..7da7db9a 100644 --- a/async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/DynamicRegistryImpTest.java +++ b/async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/DynamicRegistryImpTest.java @@ -1,7 +1,5 @@ package org.reactivecommons.async.rabbit; -import com.rabbitmq.client.AMQP; -import com.rabbitmq.client.AMQP.Queue.BindOk; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -18,7 +16,6 @@ import reactor.core.publisher.Mono; import reactor.rabbitmq.BindingSpecification; import reactor.test.StepVerifier; -import reactor.test.publisher.PublisherProbe; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -62,7 +59,7 @@ void setUp() { @Test void registerEventListener() { setupMock(); - when(topologyCreator.bind(any())).thenReturn(just(mock(BindOk.class))); + when(topologyCreator.bind(any())).thenReturn(Mono.empty()); dynamicRegistry.listenEvent("event1", message -> Mono.empty(), Long.class); final RegisteredEventListener listener = resolver.getEventListener("event1"); @@ -73,7 +70,7 @@ void registerEventListener() { void declareBindingWhenRegisterEventListener() { setupMock(); ArgumentCaptor captor = ArgumentCaptor.forClass(BindingSpecification.class); - when(topologyCreator.bind(any())).thenReturn(just(mock(BindOk.class))); + when(topologyCreator.bind(any())).thenReturn(Mono.empty()); dynamicRegistry.listenEvent("event1", message -> Mono.empty(), Long.class); @@ -87,13 +84,11 @@ void declareBindingWhenRegisterEventListener() { @Test void subscribeToResultWhenRegisterEventListener() { setupMock(); - PublisherProbe probe = PublisherProbe.of(just(mock(BindOk.class))); - when(topologyCreator.bind(any())).thenReturn(probe.mono()); + when(topologyCreator.bind(any())).thenReturn(Mono.empty()); Mono result = dynamicRegistry.listenEvent("event1", message -> Mono.empty(), Long.class); StepVerifier.create(result).verifyComplete(); - probe.assertWasSubscribed(); } @@ -103,10 +98,8 @@ void shouldBindDomainEventsToEventsQueueUsingEventName() { ArgumentCaptor bindingSpecificationCaptor = ArgumentCaptor.forClass(BindingSpecification.class); - PublisherProbe topologyCreatorProbe = PublisherProbe.empty(); - when(topologyCreator.bind(bindingSpecificationCaptor.capture())) - .thenReturn(topologyCreatorProbe.mono()); + .thenReturn(Mono.empty()); String eventName = "a.b.c"; BindingSpecification bindingSpecification = @@ -119,8 +112,6 @@ void shouldBindDomainEventsToEventsQueueUsingEventName() { assertThat(bindingSpecificationCaptor.getValue()) .usingRecursiveComparison() .isEqualTo(bindingSpecification); - - topologyCreatorProbe.assertWasSubscribed(); } @Test @@ -129,10 +120,8 @@ void shouldUnbindDomainEventsToEventsQueueUsingEventName() { ArgumentCaptor bindingSpecificationCaptor = ArgumentCaptor.forClass(BindingSpecification.class); - PublisherProbe topologyCreatorProbe = PublisherProbe.empty(); - when(topologyCreator.unbind(bindingSpecificationCaptor.capture())) - .thenReturn(topologyCreatorProbe.mono()); + .thenReturn(Mono.empty()); String eventName = "a.b.c"; BindingSpecification bindingSpecification = @@ -145,8 +134,6 @@ void shouldUnbindDomainEventsToEventsQueueUsingEventName() { assertThat(bindingSpecificationCaptor.getValue()) .usingRecursiveComparison() .isEqualTo(bindingSpecification); - - topologyCreatorProbe.assertWasSubscribed(); } @Test diff --git a/async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/listeners/ApplicationReplyListenerTest.java b/async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/listeners/ApplicationReplyListenerTest.java index 4d41d84d..b6fc33cb 100644 --- a/async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/listeners/ApplicationReplyListenerTest.java +++ b/async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/listeners/ApplicationReplyListenerTest.java @@ -79,10 +79,10 @@ void setUp() { .thenReturn(Mono.just(new AMQImpl.Queue.DeclareOk(REPLY_QUEUE, 0, 0))); when(topologyCreator.declare(exchangeCaptor.capture())) - .thenReturn(Mono.just(new AMQImpl.Exchange.DeclareOk())); + .thenReturn(Mono.empty()); when(topologyCreator.bind(bindingCaptor.capture())) - .thenReturn(Mono.just(new AMQImpl.Queue.BindOk())); + .thenReturn(Mono.empty()); } @Test diff --git a/async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/listeners/ListenerReporterTestSuperClass.java b/async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/listeners/ListenerReporterTestSuperClass.java index 3d810949..12aac7ef 100644 --- a/async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/listeners/ListenerReporterTestSuperClass.java +++ b/async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/listeners/ListenerReporterTestSuperClass.java @@ -75,7 +75,7 @@ public abstract class ListenerReporterTestSuperClass { @BeforeEach void init() { Mockito.when(topologyCreator.declare(any(ExchangeSpecification.class))) - .thenReturn(just(mock(AMQP.Exchange.DeclareOk.class))); + .thenReturn(empty()); Mockito.when(topologyCreator.declareDLQ (any(String.class), any(String.class), any(Integer.class), any(Optional.class)) ) @@ -83,7 +83,7 @@ void init() { Mockito.when(topologyCreator.declareQueue(any(String.class), any(String.class), any(Optional.class))) .thenReturn(just(mock(AMQP.Queue.DeclareOk.class))); Mockito.when(topologyCreator.bind(any(BindingSpecification.class))) - .thenReturn(just(mock(AMQP.Queue.BindOk.class))); + .thenReturn(empty()); } protected void assertContinueAfterSendErrorToCustomReporter(HandlerRegistry handlerRegistry, diff --git a/reactor-rabbitmq-removal.md b/reactor-rabbitmq-removal.md new file mode 100644 index 00000000..720d9a23 --- /dev/null +++ b/reactor-rabbitmq-removal.md @@ -0,0 +1,197 @@ +# Eliminación de reactor-rabbitmq: Justificación y Diseño + +## Contexto + +A partir de la versión **8.0.0** de Reactive Commons, se eliminó la dependencia `io.projectreactor.rabbitmq:reactor-rabbitmq:1.5.6` y se reemplazó con clases bridge propias que envuelven directamente `com.rabbitmq:amqp-client`. + +Este documento explica las razones técnicas, las alternativas evaluadas y las consideraciones de diseño. + +--- + +## ¿Por qué se eliminó reactor-rabbitmq? + +### 1. Proyecto archivado y sin mantenimiento + +El repositorio [`reactor/reactor-rabbitmq`](https://github.com/reactor/reactor-rabbitmq) fue **archivado en 2023**. Esto significa: + +- No recibe correcciones de seguridad +- No recibe actualizaciones de compatibilidad +- No se aceptan pull requests ni issues +- No hay roadmap de futuras versiones + +### 2. Incompatibilidad con el stack actual + +| Dependencia | Versión en reactor-rabbitmq 1.5.6 | Versión en Reactive Commons 8.x | +|---|---|---| +| Reactor Core | 3.4.x | 3.7.x+ (Spring Boot 4.0.2) | +| amqp-client | 5.16.x | 5.28.0 | +| Java | 8-17 | 17-21 | + +Estas diferencias generan riesgo de: + +- Incompatibilidades binarias silenciosas +- Comportamientos inesperados en runtime +- Bugs que no serán corregidos upstream + +### 3. Complejidad innecesaria + +reactor-rabbitmq es una librería de ~15,000 líneas que incluye: + +- `ChannelPool` y `ChannelPoolFactory` con lógica de cache compleja +- `FluxSink`-based publishing con `ExecutorService` dedicados +- Manejo de `ResourceManagementChannel` con proxies dinámicos +- Soporte para features que Reactive Commons no utiliza + +El proyecto solo usa ~10% de la API expuesta. + +--- + +## Alternativas evaluadas + +### Opción 1: Mantener reactor-rabbitmq (descartada) + +**Pros:** +- Cero esfuerzo de migración + +**Contras:** +- Dependencia muerta sin fixes de seguridad +- Riesgo creciente de incompatibilidad con cada actualización de Spring Boot / Reactor +- No hay soporte oficial para Java 21+ + +### Opción 2: Usar Vert.x RabbitMQ Client (descartada) + +Se evaluó `io.vertx:vertx-rabbitmq-client:5.0.8` como reemplazo. + +**Pros:** +- Proyecto activo con releases frecuentes +- API reactiva nativa (basada en `Future`) + +**Contras:** +- **SSL/TLS problemático**: Vert.x no carga automáticamente el trust store de la JVM (`cacerts`). Requiere configuración explícita de `JksOptions`/`PfxOptions`, lo que causa fallos de conexión con brokers como **AWS Amazon MQ** que usan certificados de CAs públicas (Amazon Trust Services) +- **Overhead innecesario**: Agrega Vert.x Core (~1.5MB) + vertx-rabbitmq-client al classpath +- **Bridging costoso**: Requiere convertir `io.vertx.core.Future` → `CompletionStage` → `Mono`, agregando overhead y complejidad en stack traces +- **Modelo de concurrencia diferente**: El event loop de Vert.x es un paradigma ajeno al modelo Spring Boot + Reactor del proyecto +- **Canal único**: `RabbitMQClient` de Vert.x maneja un solo canal interno, limitando la concurrencia comparado con el modelo multi-canal de amqp-client + +### Opción 3: Clases bridge sobre amqp-client + Netty (seleccionada) + +**Pros:** +- **SSL funciona out-of-the-box**: `SslContextBuilder.forClient()` de Netty + `TrustManagerFactory.init(null)` carga automáticamente los cacerts de la JVM +- **Cero dependencias adicionales**: `com.rabbitmq:amqp-client` ya era dependencia transitiva +- **Control total**: Código propio, corregible en minutos +- **Alineación con el ecosistema**: `ConnectionFactory` es el estándar de Spring AMQP y RabbitMQ +- **Transporte eficiente**: Netty NIO compartido via `EventLoopGroup`, [documentado oficialmente por RabbitMQ](https://www.rabbitmq.com/client-libraries/java-api-guide#netty) + +**Contras:** +- ~410 líneas de código propio a mantener (riesgo bajo dado que la API de amqp-client es estable) + +--- + +## Diseño de las clases bridge + +### Arquitectura + +``` +┌─────────────────────────────────────────────────────┐ +│ Reactive Commons │ +│ (ReactiveMessageSender, Listeners, etc.) │ +├─────────────────────────────────────────────────────┤ +│ Clases Bridge │ +│ Sender, Receiver, AcknowledgableDelivery, etc. │ +│ (~410 líneas, API reactiva Mono/Flux) │ +├─────────────────────────────────────────────────────┤ +│ com.rabbitmq:amqp-client │ +│ ConnectionFactory, Channel, Connection │ +│ + Netty NIO Transport (SSL) │ +└─────────────────────────────────────────────────────┘ +``` + +### Clases creadas + +| Clase | Líneas | Reemplaza | Responsabilidad | +|---|---|---|---| +| `Sender` | ~130 | `reactor.rabbitmq.Sender` | Declarar topología (exchanges, queues, bindings) y publicar mensajes con/sin publisher confirms | +| `Receiver` | ~120 | `reactor.rabbitmq.Receiver` | Consumir mensajes con manual ack o auto ack, cada consumidor obtiene su propio `Channel` | +| `AcknowledgableDelivery` | ~40 | `reactor.rabbitmq.AcknowledgableDelivery` | Extiende `Delivery` con `ack()` y `nack(boolean)` sobre el `Channel` del consumidor | +| `OutboundMessage` | ~10 | `reactor.rabbitmq.OutboundMessage` | POJO: exchange + routingKey + properties + body | +| `OutboundMessageResult` | ~10 | `reactor.rabbitmq.OutboundMessageResult` | POJO: outboundMessage + ack + returned | +| `ExchangeSpecification` | ~25 | `reactor.rabbitmq.ExchangeSpecification` | Builder para declarar exchanges | +| `QueueSpecification` | ~30 | `reactor.rabbitmq.QueueSpecification` | Builder para declarar queues | +| `BindingSpecification` | ~15 | `reactor.rabbitmq.BindingSpecification` | Factory para crear bindings | +| `ResourcesSpecification` | ~15 | `reactor.rabbitmq.ResourcesSpecification` | Factory estática: `exchange()`, `queue()`, `binding()` | +| `ConsumeOptions` | ~15 | `reactor.rabbitmq.ConsumeOptions` | Configuración de QoS y consumer tag | + +### Principios de diseño + +1. **Mínima superficie**: Solo se implementa lo que Reactive Commons realmente usa +2. **Channel-per-operation**: Cada operación de topología o publish abre un `Channel`, ejecuta, y cierra. Los consumidores mantienen su canal abierto +3. **Publisher Confirms atómicos**: `confirmSelect()` + `basicPublish()` + `waitForConfirmsOrDie()` en un solo canal dedicado +4. **Scheduling explícito**: Operaciones bloqueantes de amqp-client se ejecutan en `Schedulers.boundedElastic()` para no bloquear el event loop de Reactor +5. **Conexión compartida**: Una sola `Mono.cache()` con retry exponencial por `ConnectionFactory` + +--- + +## Consideraciones adicionales + +### Gestión de conexiones + +- Se usa un `ConcurrentMap>` como cache para reutilizar la conexión +- La conexión utiliza retry con backoff exponencial (`300ms` inicial, `3000ms` máximo) para reconexión automática +- El `Connection Name` incluye el `appName` + `InstanceIdentifier` para trazabilidad en el management UI de RabbitMQ + +### Transporte Netty + +- Se comparte un único `EventLoopGroup` (NIO) entre todas las conexiones via `SHARED_EVENT_LOOP_GROUP` +- El lifecycle del `EventLoopGroup` se gestiona con `EventLoopGroupLifecycleManager` (Spring `SmartLifecycle`) para shutdown limpio +- Referencia: [RabbitMQ Java API Guide - Use of Netty for Network I/O](https://www.rabbitmq.com/client-libraries/java-api-guide#netty) + +### SSL/TLS + +- Se usa `SslContextBuilder.forClient()` de Netty (no el `SSLContext` de JDK) +- `TrustManagerFactory.init(null)` carga automáticamente los certificados del trust store de la JVM +- Compatible con AWS Amazon MQ, CloudAMQP, y cualquier broker con certificados de CAs públicas sin configuración adicional +- Soporta trust store y key store explícitos vía propiedades `ssl.trustStore`, `ssl.keyStore` +- Hostname verification habilitada por defecto vía `factory.enableHostnameVerification()` + +### Testing + +- Las clases bridge no rompen la interfaz pública de Reactive Commons — `ReactiveMessageSender`, `ReactiveMessageListener`, `TopologyCreator` mantienen sus firmas +- Los tests de integración existentes (`acceptance/async-tests`) siguen funcionando sin cambios +- Los consumidores de la librería solo necesitan actualizar imports si referenciaban directamente tipos de `reactor.rabbitmq` (ver [Guía de Migración](migration-guides.md)) + +### Monitoreo y observabilidad + +- `reactor-core-micrometer` sigue instrumentando las operaciones reactivas (Mono/Flux) +- Las métricas de amqp-client (`MicrometerMetricsCollector`) están disponibles si se necesitan métricas a nivel de conexión/canal +- Los nombres de conexión incluyen el `appName` para identificación en RabbitMQ Management + +### Riesgos y mitigaciones + +| Riesgo | Probabilidad | Mitigación | +|---|---|---| +| Cambio en API de amqp-client Channel | Muy baja — API estable desde hace 10+ años | Las clases bridge son simples de actualizar | +| Leak de canales | Baja — cada operación usa try/finally | Monitorear canal count en RabbitMQ Management | +| Bloqueo en `waitForConfirmsOrDie` | Baja — timeout de 5s configurado | Schedulers.boundedElastic() evita bloquear event loop | +| Cambio en Netty SslContext API | Muy baja — API estable | Spring Boot BOM gestiona versiones de Netty | + +### Futuras mejoras posibles + +1. **Channel pooling**: Para escenarios de alto throughput, se podría implementar un pool de canales en `Sender` en lugar de crear uno por operación +2. **Batch confirms**: Agrupar múltiples publishes en un solo canal con confirm para reducir round-trips +3. **Métricas propias**: Instrumentar `Sender` y `Receiver` con Micrometer para métricas específicas de publish/consume +4. **Connection recovery**: Evaluar si `ConnectionFactory.setAutomaticRecoveryEnabled(true)` complementa o reemplaza el retry de `Mono` + +--- + +## Resumen de cambios en dependencias + +```diff +# async-rabbit.gradle + +- api 'io.projectreactor.rabbitmq:reactor-rabbitmq:1.5.6' ++ # Eliminada: reactor-rabbitmq (deprecated/archived) ++ # Se usan clases bridge propias sobre com.rabbitmq:amqp-client + api 'com.rabbitmq:amqp-client' +``` + +No se agregan nuevas dependencias. Se elimina una dependencia deprecada. diff --git a/starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/standalone/config/EventBusConfig.java b/starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/standalone/config/EventBusConfig.java index 959e19d9..601b43b6 100644 --- a/starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/standalone/config/EventBusConfig.java +++ b/starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/standalone/config/EventBusConfig.java @@ -5,7 +5,7 @@ import org.reactivecommons.async.rabbit.RabbitDomainEventBus; import org.reactivecommons.async.rabbit.communications.ReactiveMessageSender; -import static reactor.rabbitmq.ExchangeSpecification.exchange; +import static org.reactivecommons.async.rabbit.communications.ResourcesSpecification.exchange; @RequiredArgsConstructor diff --git a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/RabbitMQBrokerProvider.java b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/RabbitMQBrokerProvider.java index a833183e..96848890 100644 --- a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/RabbitMQBrokerProvider.java +++ b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/RabbitMQBrokerProvider.java @@ -24,7 +24,7 @@ import org.reactivecommons.async.starter.config.health.RCHealth; import reactor.core.publisher.Mono; -import static reactor.rabbitmq.ExchangeSpecification.exchange; +import static org.reactivecommons.async.rabbit.communications.ResourcesSpecification.exchange; @Log public record RabbitMQBrokerProvider(String domain, diff --git a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/RabbitMQSetupUtils.java b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/RabbitMQSetupUtils.java index 49edb7e9..b4f98420 100644 --- a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/RabbitMQSetupUtils.java +++ b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/RabbitMQSetupUtils.java @@ -29,7 +29,6 @@ import reactor.rabbitmq.ChannelPool; import reactor.rabbitmq.ChannelPoolFactory; import reactor.rabbitmq.ChannelPoolOptions; -import reactor.rabbitmq.RabbitFlux; import reactor.rabbitmq.Receiver; import reactor.rabbitmq.ReceiverOptions; import reactor.rabbitmq.Sender; @@ -95,7 +94,7 @@ public static ReactiveMessageSender createMessageSender(ConnectionFactoryProvide AsyncProps props, MessageConverter converter, UnroutableMessageNotifier unroutableMessageNotifier) { - final Sender sender = RabbitFlux.createSender(reactiveCommonsSenderOptions(props.getAppName(), provider, + final Sender sender = new Sender(reactiveCommonsSenderOptions(props.getAppName(), provider, props.getConnectionProperties())); return new ReactiveMessageSender(sender, props.getAppName(), converter, new TopologyCreator(sender, props.getQueueType()), @@ -106,8 +105,8 @@ public static ReactiveMessageSender createMessageSender(ConnectionFactoryProvide public static ReactiveMessageListener createMessageListener(ConnectionFactoryProvider provider, AsyncProps props) { final Mono connection = createConnectionMono(provider.getConnectionFactory(), props.getAppName()); - final Receiver receiver = RabbitFlux.createReceiver(new ReceiverOptions().connectionMono(connection)); - final Sender sender = RabbitFlux.createSender(new SenderOptions().connectionMono(connection)); + final Receiver receiver = new Receiver(new ReceiverOptions().connectionMono(connection)); + final Sender sender = new Sender(new SenderOptions().connectionMono(connection)); return new ReactiveMessageListener(receiver, new TopologyCreator(sender, props.getQueueType()), @@ -118,7 +117,7 @@ public static ReactiveMessageListener createMessageListener(ConnectionFactoryPro public static TopologyCreator createTopologyCreator(AsyncProps props, ConnectionFactoryCustomizer cfCustomizer) { ConnectionFactoryProvider provider = connectionFactoryProvider(props, cfCustomizer); final Mono connection = createConnectionMono(provider.getConnectionFactory(), props.getAppName()); - final Sender sender = RabbitFlux.createSender(new SenderOptions().connectionMono(connection)); + final Sender sender = new Sender(new SenderOptions().connectionMono(connection)); return new TopologyCreator(sender, props.getQueueType()); } diff --git a/starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/rabbit/RabbitMQBrokerProviderTest.java b/starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/rabbit/RabbitMQBrokerProviderTest.java index 14b96ee9..cb5ba4d6 100644 --- a/starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/rabbit/RabbitMQBrokerProviderTest.java +++ b/starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/rabbit/RabbitMQBrokerProviderTest.java @@ -100,7 +100,7 @@ void init() { void shouldCreateDomainEventBus() { when(sender.getTopologyCreator()).thenReturn(creator); when(creator.declare(any(ExchangeSpecification.class))) - .thenReturn(Mono.just(mock(AMQP.Exchange.DeclareOk.class))); + .thenReturn(Mono.empty()); // Act DomainEventBus domainBus = brokerProvider.getDomainBus(); // Assert @@ -112,9 +112,9 @@ void shouldCreateDirectAsyncGateway() { when(sender.getTopologyCreator()).thenReturn(creator); when(listener.topologyCreator()).thenReturn(creator); when(creator.declare(any(ExchangeSpecification.class))) - .thenReturn(Mono.just(mock(AMQP.Exchange.DeclareOk.class))); + .thenReturn(Mono.empty()); when(creator.bind(any(BindingSpecification.class))) - .thenReturn(Mono.just(mock(AMQP.Queue.BindOk.class))); + .thenReturn(Mono.empty()); when(creator.declare(any(QueueSpecification.class))) .thenReturn(Mono.just(mock(AMQP.Queue.DeclareOk.class))); when(listener.receiver()).thenReturn(receiver); @@ -129,7 +129,7 @@ void shouldCreateDirectAsyncGateway() { void shouldListenDomainEvents() { when(listener.topologyCreator()).thenReturn(creator); when(creator.declare(any(ExchangeSpecification.class))) - .thenReturn(Mono.just(mock(AMQP.Exchange.DeclareOk.class))); + .thenReturn(Mono.empty()); when(creator.declareQueue(any(String.class), any())).thenReturn(Mono.just(mock(AMQP.Queue.DeclareOk.class))); when(listener.receiver()).thenReturn(receiver); when(listener.maxConcurrency()).thenReturn(1); @@ -146,7 +146,7 @@ void shouldListenNotificationEvents() { when(handlerResolver.hasNotificationListeners()).thenReturn(true); when(listener.topologyCreator()).thenReturn(creator); when(creator.declare(any(ExchangeSpecification.class))) - .thenReturn(Mono.just(mock(AMQP.Exchange.DeclareOk.class))); + .thenReturn(Mono.empty()); when(creator.declare(any(QueueSpecification.class))) .thenReturn(Mono.just(mock(AMQP.Queue.DeclareOk.class))); when(listener.receiver()).thenReturn(receiver); @@ -165,11 +165,11 @@ void shouldListenCommands() { when(handlerResolver.hasCommandHandlers()).thenReturn(true); when(listener.topologyCreator()).thenReturn(creator); when(creator.declare(any(ExchangeSpecification.class))) - .thenReturn(Mono.just(mock(AMQP.Exchange.DeclareOk.class))); + .thenReturn(Mono.empty()); when(creator.declareQueue(any(String.class), any())) .thenReturn(Mono.just(mock(AMQP.Queue.DeclareOk.class))); when(creator.bind(any(BindingSpecification.class))) - .thenReturn(Mono.just(mock(AMQP.Queue.BindOk.class))); + .thenReturn(Mono.empty()); when(listener.receiver()).thenReturn(receiver); when(listener.maxConcurrency()).thenReturn(1); when(receiver.consumeManualAck(any(String.class), any())).thenReturn(Flux.never()); @@ -184,11 +184,11 @@ void shouldListenQueries() { when(handlerResolver.hasQueryHandlers()).thenReturn(true); when(listener.topologyCreator()).thenReturn(creator); when(creator.declare(any(ExchangeSpecification.class))) - .thenReturn(Mono.just(mock(AMQP.Exchange.DeclareOk.class))); + .thenReturn(Mono.empty()); when(creator.declareQueue(any(String.class), any())) .thenReturn(Mono.just(mock(AMQP.Queue.DeclareOk.class))); when(creator.bind(any(BindingSpecification.class))) - .thenReturn(Mono.just(mock(AMQP.Queue.BindOk.class))); + .thenReturn(Mono.empty()); when(listener.receiver()).thenReturn(receiver); when(listener.maxConcurrency()).thenReturn(1); when(receiver.consumeManualAck(any(String.class), any())).thenReturn(Flux.never()); From 591002d7aeb5bec43b05e60c87d2896522d02825 Mon Sep 17 00:00:00 2001 From: luisgomez29 Date: Tue, 24 Mar 2026 09:38:33 -0500 Subject: [PATCH 02/15] feat: add Reactor RabbitMQ integration and related tests --- .../async/api/HandlerRegistryTest.java | 104 ++++ .../async/commons/CommandExecutorTest.java | 47 ++ .../async/commons/DLQDiscardNotifierTest.java | 167 ++++++ .../async/commons/EventExecutorTest.java | 47 ++ .../async/commons/FallbackStrategyTest.java | 33 ++ .../async/commons/HandlerResolverTest.java | 189 +++++++ .../async/commons/QueryExecutorTest.java | 51 ++ .../commons/config/BrokerConfigTest.java | 36 ++ .../json/CloudEventBuilderExtTest.java | 56 ++ .../json/JacksonMessageConverterTest.java | 132 +++++ .../MessageConversionExceptionTest.java | 29 + .../SendFailureNoAckExceptionTest.java | 14 + .../ext/DefaultCustomReporterTest.java | 93 ++++ .../resolver/HandlerResolverBuilderTest.java | 147 ++++++ async/async-rabbit/async-rabbit.gradle | 1 + .../communications/ReactiveMessageSender.java | 8 +- .../UnroutableMessageProcessor.java | 2 +- .../UnroutableMessageProcessorTest.java | 4 +- .../reactor-rabbitmq/reactor-rabbitmq.gradle | 9 + .../rabbitmq/AcknowledgableDelivery.java | 6 +- .../rabbitmq/BindingSpecification.java | 31 +- .../rabbitmq/ChannelCloseHandlers.java | 3 + .../java/reactor/rabbitmq/ChannelPool.java | 0 .../reactor/rabbitmq/ChannelPoolFactory.java | 3 + .../reactor/rabbitmq/ChannelPoolOptions.java | 14 +- .../java/reactor/rabbitmq/ChannelProxy.java | 7 +- .../java/reactor/rabbitmq/ConsumeOptions.java | 47 +- .../reactor/rabbitmq/ExceptionHandlers.java | 14 +- .../rabbitmq/ExchangeSpecification.java | 40 +- .../main/java/reactor/rabbitmq/Helpers.java | 2 +- .../reactor/rabbitmq/LazyChannelPool.java | 6 +- .../reactor/rabbitmq/OutboundMessage.java | 27 +- .../rabbitmq/OutboundMessageResult.java | 24 +- .../reactor/rabbitmq/QueueSpecification.java | 36 +- .../reactor/rabbitmq/RabbitFluxException.java | 7 +- .../RabbitFluxRetryTimeoutException.java | 0 .../main/java/reactor/rabbitmq/Receiver.java | 29 +- .../reactor/rabbitmq/ReceiverOptions.java | 49 +- .../java/reactor/rabbitmq/SendOptions.java | 35 +- .../main/java/reactor/rabbitmq/Sender.java | 119 ++--- .../java/reactor/rabbitmq/SenderOptions.java | 79 +-- .../reactor/rabbitmq/SubscriberState.java | 0 .../src/main/java/reactor/rabbitmq/Utils.java | 1 - .../rabbitmq/AcknowledgableDeliveryTest.java | 131 +++++ .../rabbitmq/BindingSpecificationTest.java | 68 +++ .../rabbitmq/ChannelCloseHandlersTest.java | 75 +++ .../rabbitmq/ChannelPoolFactoryTest.java | 26 + .../rabbitmq/ChannelPoolOptionsTest.java | 29 + .../reactor/rabbitmq/ChannelProxyTest.java | 79 +++ .../reactor/rabbitmq/ConsumeOptionsTest.java | 72 +++ .../rabbitmq/ExceptionHandlersTest.java | 130 +++++ .../rabbitmq/ExchangeSpecificationTest.java | 50 ++ .../java/reactor/rabbitmq/HelpersTest.java | 33 ++ .../reactor/rabbitmq/LazyChannelPoolTest.java | 172 ++++++ .../rabbitmq/OutboundMessageResultTest.java | 34 ++ .../reactor/rabbitmq/OutboundMessageTest.java | 44 ++ .../rabbitmq/QueueSpecificationTest.java | 143 +++++ .../rabbitmq/RabbitFluxExceptionTest.java | 45 ++ .../reactor/rabbitmq/ReceiverOptionsTest.java | 84 +++ .../java/reactor/rabbitmq/ReceiverTest.java | 191 +++++++ .../reactor/rabbitmq/SendOptionsTest.java | 73 +++ .../reactor/rabbitmq/SenderOptionsTest.java | 132 +++++ .../java/reactor/rabbitmq/SenderTest.java | 496 ++++++++++++++++++ .../test/java/reactor/rabbitmq/UtilsTest.java | 61 +++ gradle.properties | 4 +- .../spring/RabbitPropertiesBaseTest.java | 206 ++++++++ .../common/rabbit/RabbitMQConfigTest.java | 4 +- 67 files changed, 3703 insertions(+), 427 deletions(-) create mode 100644 async/async-commons/src/test/java/org/reactivecommons/async/commons/CommandExecutorTest.java create mode 100644 async/async-commons/src/test/java/org/reactivecommons/async/commons/DLQDiscardNotifierTest.java create mode 100644 async/async-commons/src/test/java/org/reactivecommons/async/commons/EventExecutorTest.java create mode 100644 async/async-commons/src/test/java/org/reactivecommons/async/commons/FallbackStrategyTest.java create mode 100644 async/async-commons/src/test/java/org/reactivecommons/async/commons/HandlerResolverTest.java create mode 100644 async/async-commons/src/test/java/org/reactivecommons/async/commons/QueryExecutorTest.java create mode 100644 async/async-commons/src/test/java/org/reactivecommons/async/commons/config/BrokerConfigTest.java create mode 100644 async/async-commons/src/test/java/org/reactivecommons/async/commons/converters/json/CloudEventBuilderExtTest.java create mode 100644 async/async-commons/src/test/java/org/reactivecommons/async/commons/converters/json/JacksonMessageConverterTest.java create mode 100644 async/async-commons/src/test/java/org/reactivecommons/async/commons/exceptions/MessageConversionExceptionTest.java create mode 100644 async/async-commons/src/test/java/org/reactivecommons/async/commons/exceptions/SendFailureNoAckExceptionTest.java create mode 100644 async/async-commons/src/test/java/org/reactivecommons/async/commons/ext/DefaultCustomReporterTest.java create mode 100644 async/async-commons/src/test/java/org/reactivecommons/async/commons/utils/resolver/HandlerResolverBuilderTest.java create mode 100644 async/reactor-rabbitmq/reactor-rabbitmq.gradle rename async/{async-rabbit => reactor-rabbitmq}/src/main/java/reactor/rabbitmq/AcknowledgableDelivery.java (91%) rename async/{async-rabbit => reactor-rabbitmq}/src/main/java/reactor/rabbitmq/BindingSpecification.java (79%) rename async/{async-rabbit => reactor-rabbitmq}/src/main/java/reactor/rabbitmq/ChannelCloseHandlers.java (94%) rename async/{async-rabbit => reactor-rabbitmq}/src/main/java/reactor/rabbitmq/ChannelPool.java (100%) rename async/{async-rabbit => reactor-rabbitmq}/src/main/java/reactor/rabbitmq/ChannelPoolFactory.java (91%) rename async/{async-rabbit => reactor-rabbitmq}/src/main/java/reactor/rabbitmq/ChannelPoolOptions.java (75%) rename async/{async-rabbit => reactor-rabbitmq}/src/main/java/reactor/rabbitmq/ChannelProxy.java (96%) rename async/{async-rabbit => reactor-rabbitmq}/src/main/java/reactor/rabbitmq/ConsumeOptions.java (70%) rename async/{async-rabbit => reactor-rabbitmq}/src/main/java/reactor/rabbitmq/ExceptionHandlers.java (92%) rename async/{async-rabbit => reactor-rabbitmq}/src/main/java/reactor/rabbitmq/ExchangeSpecification.java (72%) rename async/{async-rabbit => reactor-rabbitmq}/src/main/java/reactor/rabbitmq/Helpers.java (94%) rename async/{async-rabbit => reactor-rabbitmq}/src/main/java/reactor/rabbitmq/LazyChannelPool.java (97%) rename async/{async-rabbit => reactor-rabbitmq}/src/main/java/reactor/rabbitmq/OutboundMessage.java (76%) rename async/{async-rabbit => reactor-rabbitmq}/src/main/java/reactor/rabbitmq/OutboundMessageResult.java (64%) rename async/{async-rabbit => reactor-rabbitmq}/src/main/java/reactor/rabbitmq/QueueSpecification.java (82%) rename async/{async-rabbit => reactor-rabbitmq}/src/main/java/reactor/rabbitmq/RabbitFluxException.java (95%) rename async/{async-rabbit => reactor-rabbitmq}/src/main/java/reactor/rabbitmq/RabbitFluxRetryTimeoutException.java (100%) rename async/{async-rabbit => reactor-rabbitmq}/src/main/java/reactor/rabbitmq/Receiver.java (91%) rename async/{async-rabbit => reactor-rabbitmq}/src/main/java/reactor/rabbitmq/ReceiverOptions.java (57%) rename async/{async-rabbit => reactor-rabbitmq}/src/main/java/reactor/rabbitmq/SendOptions.java (73%) rename async/{async-rabbit => reactor-rabbitmq}/src/main/java/reactor/rabbitmq/Sender.java (90%) rename async/{async-rabbit => reactor-rabbitmq}/src/main/java/reactor/rabbitmq/SenderOptions.java (53%) rename async/{async-rabbit => reactor-rabbitmq}/src/main/java/reactor/rabbitmq/SubscriberState.java (100%) rename async/{async-rabbit => reactor-rabbitmq}/src/main/java/reactor/rabbitmq/Utils.java (97%) create mode 100644 async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/AcknowledgableDeliveryTest.java create mode 100644 async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/BindingSpecificationTest.java create mode 100644 async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/ChannelCloseHandlersTest.java create mode 100644 async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/ChannelPoolFactoryTest.java create mode 100644 async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/ChannelPoolOptionsTest.java create mode 100644 async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/ChannelProxyTest.java create mode 100644 async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/ConsumeOptionsTest.java create mode 100644 async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/ExceptionHandlersTest.java create mode 100644 async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/ExchangeSpecificationTest.java create mode 100644 async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/HelpersTest.java create mode 100644 async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/LazyChannelPoolTest.java create mode 100644 async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/OutboundMessageResultTest.java create mode 100644 async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/OutboundMessageTest.java create mode 100644 async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/QueueSpecificationTest.java create mode 100644 async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/RabbitFluxExceptionTest.java create mode 100644 async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/ReceiverOptionsTest.java create mode 100644 async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/ReceiverTest.java create mode 100644 async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/SendOptionsTest.java create mode 100644 async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/SenderOptionsTest.java create mode 100644 async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/SenderTest.java create mode 100644 async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/UtilsTest.java create mode 100644 starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/rabbit/config/spring/RabbitPropertiesBaseTest.java diff --git a/async/async-commons-api/src/test/java/org/reactivecommons/async/api/HandlerRegistryTest.java b/async/async-commons-api/src/test/java/org/reactivecommons/async/api/HandlerRegistryTest.java index eb761c49..9bacee8a 100644 --- a/async/async-commons-api/src/test/java/org/reactivecommons/async/api/HandlerRegistryTest.java +++ b/async/async-commons-api/src/test/java/org/reactivecommons/async/api/HandlerRegistryTest.java @@ -296,6 +296,110 @@ void serveQueryDelegateWithLambda() { .hasSize(1); } + @Test + void handleCommandWithDomain() { + SomeDomainCommandHandler handler = new SomeDomainCommandHandler<>(); + registry.handleCommand(domain, name, handler, SomeDataClass.class); + assertThat(registry.getCommandHandlers().get(domain)) + .anySatisfy(registered -> assertThat(registered) + .extracting(RegisteredCommandHandler::path, RegisteredCommandHandler::inputClass) + .containsExactly(name, SomeDataClass.class)).hasSize(1); + } + + @Test + void handleCloudEventCommandWithDomain() { + SomeCloudCommandHandler handler = new SomeCloudCommandHandler(); + registry.handleCloudEventCommand(domain, name, handler); + assertThat(registry.getCommandHandlers().get(domain)) + .anySatisfy(registered -> assertThat(registered) + .extracting(RegisteredCommandHandler::path, RegisteredCommandHandler::inputClass) + .containsExactly(name, CloudEvent.class)).hasSize(1); + } + + @Test + void handleRawCommandWithDomain() { + SomeRawCommandEventHandler handler = new SomeRawCommandEventHandler(); + registry.handleRawCommand(domain, handler); + assertThat(registry.getCommandHandlers().get(domain)) + .anySatisfy(registered -> assertThat(registered) + .extracting(RegisteredCommandHandler::path, RegisteredCommandHandler::inputClass) + .containsExactly("", RawMessage.class)).hasSize(1); + } + + @Test + void listenDomainRawEvent() { + SomeRawEventHandler handler = new SomeRawEventHandler(); + registry.listenDomainRawEvent(domain, name, handler); + assertThat(registry.getDomainEventListeners().get(domain)) + .anySatisfy(registered -> assertThat(registered) + .extracting(RegisteredEventListener::path, RegisteredEventListener::inputClass) + .containsExactly(name, RawMessage.class)).hasSize(1); + } + + @Test + void listenNotificationEventWithDomain() { + registry.listenNotificationEvent(domain, name, evt -> Mono.empty(), SomeDataClass.class); + assertThat(registry.getEventNotificationListener().get(domain)) + .anySatisfy(listener -> assertThat(listener.path()).isEqualTo(name)).hasSize(1); + } + + @Test + void listenNotificationCloudEventWithDomain() { + registry.listenNotificationCloudEvent(domain, name, ce -> Mono.empty()); + assertThat(registry.getEventNotificationListener().get(domain)) + .anySatisfy(listener -> assertThat(listener) + .extracting(RegisteredEventListener::path, RegisteredEventListener::inputClass) + .containsExactly(name, CloudEvent.class)).hasSize(1); + } + + @Test + void listenNotificationRawEventWithDomain() { + SomeRawEventHandler handler = new SomeRawEventHandler(); + registry.listenNotificationRawEvent(domain, name, handler); + assertThat(registry.getEventNotificationListener().get(domain)) + .anySatisfy(registered -> assertThat(registered) + .extracting(RegisteredEventListener::path, RegisteredEventListener::inputClass) + .containsExactly(name, RawMessage.class)).hasSize(1); + } + + @Test + void serveCloudEventQueryDelegate() { + QueryHandlerDelegate delegate = (from, ce) -> Mono.empty(); + registry.serveCloudEventQuery(name, delegate); + assertThat(registry.getHandlers().get(DEFAULT_DOMAIN)) + .anySatisfy(registered -> assertThat(registered) + .extracting(RegisteredQueryHandler::path, RegisteredQueryHandler::queryClass) + .containsExactly(name, CloudEvent.class)).hasSize(1); + } + + @Test + void listenQueueSimple() { + registry.listenQueue("myQueue", msg -> Mono.empty()); + assertThat(registry.getQueueHandlers().get(DEFAULT_DOMAIN)) + .anySatisfy(q -> assertThat(q.queueName()).isEqualTo("myQueue")).hasSize(1); + } + + @Test + void listenQueueWithDomain() { + registry.listenQueue(domain, "myQueue", msg -> Mono.empty()); + assertThat(registry.getQueueHandlers().get(domain)) + .anySatisfy(q -> assertThat(q.queueName()).isEqualTo("myQueue")).hasSize(1); + } + + @Test + void listenQueueWithTopology() { + registry.listenQueue("myQueue", msg -> Mono.empty(), creator -> Mono.empty()); + assertThat(registry.getQueueHandlers().get(DEFAULT_DOMAIN)) + .anySatisfy(q -> assertThat(q.queueName()).isEqualTo("myQueue")).hasSize(1); + } + + @Test + void listenQueueWithDomainAndTopology() { + registry.listenQueue(domain, "myQueue", msg -> Mono.empty(), creator -> Mono.empty()); + assertThat(registry.getQueueHandlers().get(domain)) + .anySatisfy(q -> assertThat(q.queueName()).isEqualTo("myQueue")).hasSize(1); + } + private static class SomeQueryHandlerDelegate implements QueryHandlerDelegate { @Override public Mono handle(From from, SomeDataClass message) { diff --git a/async/async-commons/src/test/java/org/reactivecommons/async/commons/CommandExecutorTest.java b/async/async-commons/src/test/java/org/reactivecommons/async/commons/CommandExecutorTest.java new file mode 100644 index 00000000..e5e0c495 --- /dev/null +++ b/async/async-commons/src/test/java/org/reactivecommons/async/commons/CommandExecutorTest.java @@ -0,0 +1,47 @@ +package org.reactivecommons.async.commons; + +import org.junit.jupiter.api.Test; +import org.reactivecommons.async.api.handlers.CommandHandler; +import org.reactivecommons.async.commons.communications.Message; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.util.Map; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class CommandExecutorTest { + + @SuppressWarnings("unchecked") + @Test + void executeCallsHandlerWithConvertedMessage() { + CommandHandler handler = mock(CommandHandler.class); + when(handler.handle(any())).thenReturn(Mono.empty()); + + CommandExecutor executor = new CommandExecutor<>(handler, msg -> "converted"); + StepVerifier.create(executor.execute(createMessage())) + .verifyComplete(); + } + + private Message createMessage() { + return new Message() { + @Override + public String getType() { return "test"; } + @Override + public byte[] getBody() { return new byte[0]; } + @Override + public Properties getProperties() { + return new Properties() { + @Override + public String getContentType() { return "application/json"; } + @Override + public long getContentLength() { return 0; } + @Override + public Map getHeaders() { return Map.of(); } + }; + } + }; + } +} diff --git a/async/async-commons/src/test/java/org/reactivecommons/async/commons/DLQDiscardNotifierTest.java b/async/async-commons/src/test/java/org/reactivecommons/async/commons/DLQDiscardNotifierTest.java new file mode 100644 index 00000000..88aa54c2 --- /dev/null +++ b/async/async-commons/src/test/java/org/reactivecommons/async/commons/DLQDiscardNotifierTest.java @@ -0,0 +1,167 @@ +package org.reactivecommons.async.commons; + +import io.cloudevents.CloudEvent; +import io.cloudevents.core.builder.CloudEventBuilder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.reactivecommons.api.domain.DomainEvent; +import org.reactivecommons.api.domain.DomainEventBus; +import org.reactivecommons.async.commons.communications.Message; +import org.reactivecommons.async.commons.converters.MessageConverter; +import org.reactivecommons.async.commons.exceptions.MessageConversionException; +import reactor.core.publisher.Mono; + +import java.net.URI; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@SuppressWarnings("unchecked") +class DLQDiscardNotifierTest { + + DomainEventBus eventBus; + MessageConverter messageConverter; + DLQDiscardNotifier notifier; + + @BeforeEach + void setUp() { + eventBus = mock(DomainEventBus.class); + messageConverter = mock(MessageConverter.class); + notifier = new DLQDiscardNotifier(eventBus, messageConverter); + when(eventBus.emit(any(DomainEvent.class))).thenReturn(Mono.empty()); + when(eventBus.emit(any(CloudEvent.class))).thenReturn(Mono.empty()); + } + + @Test + void notifyDiscardWithCloudEvent() { + var message = createMessage("body".getBytes(), "application/cloudevents+json"); + var cloudEvent = CloudEventBuilder.v1() + .withId("123") + .withType("test.type") + .withSource(URI.create("/test")) + .build(); + when(messageConverter.readCloudEvent(message)).thenReturn(cloudEvent); + + notifier.notifyDiscard(message).block(); + + verify(eventBus).emit(argThat((CloudEvent ce) -> ce.getType().equals("test.type.dlq"))); + } + + @Test + void notifyDiscardWithCommandJson() { + var message = createMessage( + "{\"name\":\"myCmd\",\"commandId\":\"cid\",\"data\":{}}".getBytes(), + "application/json"); + + // The notifier reads the message as JsonSkeleton via messageConverter.readValue + // We mock readValue to throw for CloudEvent check (not cloud event), then succeed for skeleton + when(messageConverter.readCloudEvent(message)).thenThrow(new RuntimeException("not a CE")); + + // Since message content-type is not cloud event, it reads JsonSkeleton + // We need to mock readValue for the private JsonSkeleton class + // Instead, use doAnswer to return a command-like object + doAnswer(inv -> { + Class cls = inv.getArgument(1); + var mapper = new tools.jackson.databind.json.JsonMapper(); + return mapper.readValue(message.getBody(), cls); + }).when(messageConverter).readValue(eq(message), any(Class.class)); + + notifier.notifyDiscard(message).block(); + + verify(eventBus).emit(argThat((DomainEvent ev) -> ev.getName().equals("myCmd.dlq"))); + } + + @Test + void notifyDiscardWithEventJson() { + var message = createMessage( + "{\"name\":\"myEvt\",\"eventId\":\"eid\",\"data\":{}}".getBytes(), + "application/json"); + + doAnswer(inv -> { + Class cls = inv.getArgument(1); + var mapper = new tools.jackson.databind.json.JsonMapper(); + return mapper.readValue(message.getBody(), cls); + }).when(messageConverter).readValue(eq(message), any(Class.class)); + + notifier.notifyDiscard(message).block(); + + verify(eventBus).emit(argThat((DomainEvent ev) -> ev.getName().equals("myEvt.dlq"))); + } + + @Test + void notifyDiscardWithQueryJson() { + var message = createMessage( + "{\"resource\":\"query.test\",\"queryData\":{}}".getBytes(), + "application/json"); + + doAnswer(inv -> { + Class cls = inv.getArgument(1); + var mapper = new tools.jackson.databind.json.JsonMapper(); + return mapper.readValue(message.getBody(), cls); + }).when(messageConverter).readValue(eq(message), any(Class.class)); + + notifier.notifyDiscard(message).block(); + + verify(eventBus).emit(argThat((DomainEvent ev) -> ev.getName().equals("query.test.dlq"))); + } + + @Test + void notifyDiscardWithUnreadableMessage() { + var message = createMessage("garbage".getBytes(), "application/json"); + + when(messageConverter.readValue(eq(message), any(Class.class))) + .thenThrow(new MessageConversionException("cannot read")); + + notifier.notifyDiscard(message).block(); + + verify(eventBus).emit(argThat((DomainEvent ev) -> ev.getName().equals("corruptData.dlq"))); + } + + @Test + void notifyDiscardHandlesEmitError() { + var message = createMessage("body".getBytes(), "application/json"); + when(messageConverter.readValue(eq(message), any(Class.class))) + .thenThrow(new MessageConversionException("bad")); + when(eventBus.emit(any(DomainEvent.class))).thenReturn(Mono.error(new RuntimeException("bus error"))); + + // Should not throw, returns empty + var result = notifier.notifyDiscard(message).block(); + assertThat(result).isNull(); + } + + private Message createMessage(byte[] body, String contentType) { + return new Message() { + @Override + public String getType() { + return "test"; + } + + @Override + public byte[] getBody() { + return body; + } + + @Override + public Properties getProperties() { + return new Properties() { + @Override + public String getContentType() { + return contentType; + } + + @Override + public long getContentLength() { + return body.length; + } + + @Override + public Map getHeaders() { + return Map.of(); + } + }; + } + }; + } +} diff --git a/async/async-commons/src/test/java/org/reactivecommons/async/commons/EventExecutorTest.java b/async/async-commons/src/test/java/org/reactivecommons/async/commons/EventExecutorTest.java new file mode 100644 index 00000000..b30dbe56 --- /dev/null +++ b/async/async-commons/src/test/java/org/reactivecommons/async/commons/EventExecutorTest.java @@ -0,0 +1,47 @@ +package org.reactivecommons.async.commons; + +import org.junit.jupiter.api.Test; +import org.reactivecommons.async.api.handlers.EventHandler; +import org.reactivecommons.async.commons.communications.Message; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.util.Map; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class EventExecutorTest { + + @SuppressWarnings("unchecked") + @Test + void executeCallsHandlerWithConvertedMessage() { + EventHandler handler = mock(EventHandler.class); + when(handler.handle(any())).thenReturn(Mono.empty()); + + EventExecutor executor = new EventExecutor<>(handler, msg -> "converted"); + StepVerifier.create(executor.execute(createMessage())) + .verifyComplete(); + } + + private Message createMessage() { + return new Message() { + @Override + public String getType() { return "test"; } + @Override + public byte[] getBody() { return new byte[0]; } + @Override + public Properties getProperties() { + return new Properties() { + @Override + public String getContentType() { return "application/json"; } + @Override + public long getContentLength() { return 0; } + @Override + public Map getHeaders() { return Map.of(); } + }; + } + }; + } +} diff --git a/async/async-commons/src/test/java/org/reactivecommons/async/commons/FallbackStrategyTest.java b/async/async-commons/src/test/java/org/reactivecommons/async/commons/FallbackStrategyTest.java new file mode 100644 index 00000000..e7446d09 --- /dev/null +++ b/async/async-commons/src/test/java/org/reactivecommons/async/commons/FallbackStrategyTest.java @@ -0,0 +1,33 @@ +package org.reactivecommons.async.commons; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class FallbackStrategyTest { + + @Test + void fastRetryHasMessage() { + assertThat(FallbackStrategy.FAST_RETRY.message).contains("Fast retry"); + } + + @Test + void definitiveDiscardHasMessage() { + assertThat(FallbackStrategy.DEFINITIVE_DISCARD.message).contains("DEFINITIVE DISCARD"); + } + + @Test + void retryDlqHasMessage() { + assertThat(FallbackStrategy.RETRY_DLQ.message).contains("Retry DLQ"); + } + + @Test + void valuesReturnsAllStrategies() { + assertThat(FallbackStrategy.values()).hasSize(3); + } + + @Test + void valueOfReturnsCorrect() { + assertThat(FallbackStrategy.valueOf("FAST_RETRY")).isEqualTo(FallbackStrategy.FAST_RETRY); + } +} diff --git a/async/async-commons/src/test/java/org/reactivecommons/async/commons/HandlerResolverTest.java b/async/async-commons/src/test/java/org/reactivecommons/async/commons/HandlerResolverTest.java new file mode 100644 index 00000000..3bf3b067 --- /dev/null +++ b/async/async-commons/src/test/java/org/reactivecommons/async/commons/HandlerResolverTest.java @@ -0,0 +1,189 @@ +package org.reactivecommons.async.commons; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.reactivecommons.async.api.handlers.registered.RegisteredCommandHandler; +import org.reactivecommons.async.api.handlers.registered.RegisteredEventListener; +import org.reactivecommons.async.api.handlers.registered.RegisteredQueryHandler; +import org.reactivecommons.async.api.handlers.registered.RegisteredQueueListener; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; + +class HandlerResolverTest { + + Map> queryHandlers; + Map> eventListeners; + Map> eventsToBind; + Map> notificationListeners; + Map> commandHandlers; + Map queueListeners; + HandlerResolver resolver; + + @BeforeEach + void setUp() { + queryHandlers = new ConcurrentHashMap<>(); + eventListeners = new ConcurrentHashMap<>(); + eventsToBind = new ConcurrentHashMap<>(); + notificationListeners = new ConcurrentHashMap<>(); + commandHandlers = new ConcurrentHashMap<>(); + queueListeners = new ConcurrentHashMap<>(); + resolver = new HandlerResolver(queryHandlers, eventListeners, eventsToBind, + notificationListeners, commandHandlers, queueListeners); + } + + @Test + void hasNotificationListenersWhenEmpty() { + assertThat(resolver.hasNotificationListeners()).isFalse(); + } + + @Test + void hasNotificationListenersWhenPresent() { + notificationListeners.put("event.test", new RegisteredEventListener<>("event.test", mock(), Object.class)); + resolver = new HandlerResolver(queryHandlers, eventListeners, eventsToBind, + notificationListeners, commandHandlers, queueListeners); + assertThat(resolver.hasNotificationListeners()).isTrue(); + } + + @Test + void hasCommandHandlersWhenEmpty() { + assertThat(resolver.hasCommandHandlers()).isFalse(); + } + + @Test + void hasCommandHandlersWhenPresent() { + commandHandlers.put("cmd.test", new RegisteredCommandHandler<>("cmd.test", mock(), Object.class)); + resolver = new HandlerResolver(queryHandlers, eventListeners, eventsToBind, + notificationListeners, commandHandlers, queueListeners); + assertThat(resolver.hasCommandHandlers()).isTrue(); + } + + @Test + void hasQueryHandlersWhenEmpty() { + assertThat(resolver.hasQueryHandlers()).isFalse(); + } + + @Test + void hasQueryHandlersWhenPresent() { + queryHandlers.put("query.test", new RegisteredQueryHandler<>("query.test", mock(), Object.class)); + resolver = new HandlerResolver(queryHandlers, eventListeners, eventsToBind, + notificationListeners, commandHandlers, queueListeners); + assertThat(resolver.hasQueryHandlers()).isTrue(); + } + + @Test + void getQueryHandlerReturnsRegistered() { + var handler = new RegisteredQueryHandler<>("query.test", mock(), Object.class); + queryHandlers.put("query.test", handler); + assertThat(resolver.getQueryHandler("query.test")).isSameAs(handler); + } + + @Test + void getCommandHandlerReturnsRegistered() { + var handler = new RegisteredCommandHandler<>("cmd.test", mock(), Object.class); + commandHandlers.put("cmd.test", handler); + assertThat(resolver.getCommandHandler("cmd.test")).isSameAs(handler); + } + + @Test + void getEventListenerReturnsRegistered() { + var handler = new RegisteredEventListener<>("event.test", mock(), Object.class); + eventListeners.put("event.test", handler); + assertThat(resolver.getEventListener("event.test")).isSameAs(handler); + } + + @Test + void getEventListenerUsesMatcherForUnknownPath() { + var handler = new RegisteredEventListener<>("event.*", mock(), Object.class); + eventListeners.put("event.*", handler); + var result = resolver.getEventListener("event.foo"); + assertThat(result).isSameAs(handler); + } + + @Test + void getNotificationListeners() { + var handler = new RegisteredEventListener<>("notif.test", mock(), Object.class); + notificationListeners.put("notif.test", handler); + resolver = new HandlerResolver(queryHandlers, eventListeners, eventsToBind, + notificationListeners, commandHandlers, queueListeners); + Collection> listeners = resolver.getNotificationListeners(); + assertThat(listeners).hasSize(1); + } + + @Test + void getNotificationListener() { + var handler = new RegisteredEventListener<>("notif.test", mock(), Object.class); + notificationListeners.put("notif.test", handler); + resolver = new HandlerResolver(queryHandlers, eventListeners, eventsToBind, + notificationListeners, commandHandlers, queueListeners); + assertThat(resolver.getNotificationListener("notif.test")).isSameAs(handler); + } + + @Test + void getEventListenersReturnsEventsToBindValues() { + var handler = new RegisteredEventListener<>("bind.test", mock(), Object.class); + eventsToBind.put("bind.test", handler); + assertThat(resolver.getEventListeners()).containsExactly(handler); + } + + @Test + void getEventNames() { + eventListeners.put("event.a", new RegisteredEventListener<>("event.a", mock(), Object.class)); + eventListeners.put("event.b", new RegisteredEventListener<>("event.b", mock(), Object.class)); + List names = resolver.getEventNames(); + assertThat(names).containsExactlyInAnyOrder("event.a", "event.b"); + } + + @Test + void getNotificationNames() { + notificationListeners.put("n.a", new RegisteredEventListener<>("n.a", mock(), Object.class)); + resolver = new HandlerResolver(queryHandlers, eventListeners, eventsToBind, + notificationListeners, commandHandlers, queueListeners); + assertThat(resolver.getNotificationNames()).containsExactly("n.a"); + } + + @Test + void addEventListener() { + var handler = new RegisteredEventListener<>("dynamic.event", mock(), Object.class); + resolver.addEventListener(handler); + assertThat(resolver.getEventListener("dynamic.event")).isSameAs(handler); + } + + @Test + void addQueryHandler() { + var handler = new RegisteredQueryHandler<>("query.new", mock(), Object.class); + resolver.addQueryHandler(handler); + assertThat(resolver.getQueryHandler("query.new")).isSameAs(handler); + } + + @Test + void addQueryHandlerRejectsWildcard() { + var handler = new RegisteredQueryHandler<>("query.*", mock(), Object.class); + assertThatThrownBy(() -> resolver.addQueryHandler(handler)) + .isInstanceOf(RuntimeException.class) + .hasMessageContaining("avoid * or #"); + } + + @Test + void addQueryHandlerRejectsHash() { + var handler = new RegisteredQueryHandler<>("query.#", mock(), Object.class); + assertThatThrownBy(() -> resolver.addQueryHandler(handler)) + .isInstanceOf(RuntimeException.class) + .hasMessageContaining("avoid * or #"); + } + + @Test + void getQueueListeners() { + var ql = new RegisteredQueueListener("queue", mock(), mock()); + queueListeners.put("queue", ql); + resolver = new HandlerResolver(queryHandlers, eventListeners, eventsToBind, + notificationListeners, commandHandlers, queueListeners); + assertThat(resolver.getQueueListeners()).containsEntry("queue", ql); + } +} diff --git a/async/async-commons/src/test/java/org/reactivecommons/async/commons/QueryExecutorTest.java b/async/async-commons/src/test/java/org/reactivecommons/async/commons/QueryExecutorTest.java new file mode 100644 index 00000000..c1444ced --- /dev/null +++ b/async/async-commons/src/test/java/org/reactivecommons/async/commons/QueryExecutorTest.java @@ -0,0 +1,51 @@ +package org.reactivecommons.async.commons; + +import org.junit.jupiter.api.Test; +import org.reactivecommons.async.api.From; +import org.reactivecommons.async.api.handlers.QueryHandlerDelegate; +import org.reactivecommons.async.commons.communications.Message; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.util.Map; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class QueryExecutorTest { + + @SuppressWarnings("unchecked") + @Test + void executeCallsHandlerWithConvertedMessage() { + QueryHandlerDelegate handler = mock(QueryHandlerDelegate.class); + when(handler.handle(any(From.class), any())).thenReturn(Mono.just("result")); + + QueryExecutor executor = new QueryExecutor<>(handler, msg -> "query"); + StepVerifier.create(executor.execute(createMessage())) + .expectNext("result") + .verifyComplete(); + } + + private Message createMessage() { + return new Message() { + @Override + public String getType() { return "test"; } + @Override + public byte[] getBody() { return new byte[0]; } + @Override + public Properties getProperties() { + return new Properties() { + @Override + public String getContentType() { return "application/json"; } + @Override + public long getContentLength() { return 0; } + @Override + public Map getHeaders() { + return Map.of("correlationID", "corr-123", "replyID", "reply-456"); + } + }; + } + }; + } +} diff --git a/async/async-commons/src/test/java/org/reactivecommons/async/commons/config/BrokerConfigTest.java b/async/async-commons/src/test/java/org/reactivecommons/async/commons/config/BrokerConfigTest.java new file mode 100644 index 00000000..8f2f6bd6 --- /dev/null +++ b/async/async-commons/src/test/java/org/reactivecommons/async/commons/config/BrokerConfigTest.java @@ -0,0 +1,36 @@ +package org.reactivecommons.async.commons.config; + +import org.junit.jupiter.api.Test; + +import java.time.Duration; + +import static org.assertj.core.api.Assertions.assertThat; + +class BrokerConfigTest { + + @Test + void defaultConstructor() { + var config = new BrokerConfig(); + assertThat(config.isPersistentQueries()).isFalse(); + assertThat(config.isPersistentCommands()).isTrue(); + assertThat(config.isPersistentEvents()).isTrue(); + assertThat(config.getReplyTimeout()).isEqualTo(Duration.ofSeconds(15)); + assertThat(config.getRoutingKey()).isNotBlank(); + } + + @Test + void allArgsConstructor() { + var config = new BrokerConfig(true, false, true, Duration.ofSeconds(30)); + assertThat(config.isPersistentQueries()).isTrue(); + assertThat(config.isPersistentCommands()).isFalse(); + assertThat(config.isPersistentEvents()).isTrue(); + assertThat(config.getReplyTimeout()).isEqualTo(Duration.ofSeconds(30)); + } + + @Test + void routingKeyIsGenerated() { + var c1 = new BrokerConfig(); + var c2 = new BrokerConfig(); + assertThat(c1.getRoutingKey()).isNotEqualTo(c2.getRoutingKey()); + } +} diff --git a/async/async-commons/src/test/java/org/reactivecommons/async/commons/converters/json/CloudEventBuilderExtTest.java b/async/async-commons/src/test/java/org/reactivecommons/async/commons/converters/json/CloudEventBuilderExtTest.java new file mode 100644 index 00000000..e9a2c03a --- /dev/null +++ b/async/async-commons/src/test/java/org/reactivecommons/async/commons/converters/json/CloudEventBuilderExtTest.java @@ -0,0 +1,56 @@ +package org.reactivecommons.async.commons.converters.json; + +import io.cloudevents.CloudEvent; +import io.cloudevents.core.builder.CloudEventBuilder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.junit.jupiter.api.Test; + +import java.net.URI; +import java.nio.charset.StandardCharsets; + +import static org.assertj.core.api.Assertions.assertThat; + +class CloudEventBuilderExtTest { + + @Test + void asBytesSerializesObject() { + byte[] bytes = CloudEventBuilderExt.asBytes(new TestData("hello")); + assertThat(bytes).isNotEmpty(); + var json = new String(bytes, StandardCharsets.UTF_8); + assertThat(json).contains("hello"); + } + + @Test + void asCloudEventDataWrapsObject() { + var data = CloudEventBuilderExt.asCloudEventData(new TestData("test")); + assertThat(data.toBytes()).isNotEmpty(); + } + + @Test + void fromCloudEventDataDeserializes() { + byte[] json = "{\"value\":\"hello\"}".getBytes(StandardCharsets.UTF_8); + CloudEvent event = CloudEventBuilder.v1() + .withId("1") + .withType("test") + .withSource(URI.create("/test")) + .withData("application/json", json) + .build(); + + TestData result = CloudEventBuilderExt.fromCloudEventData(event, TestData.class); + assertThat(result.getValue()).isEqualTo("hello"); + } + + @Setter + @Getter + @NoArgsConstructor + public static class TestData { + private String value; + + public TestData(String value) { + this.value = value; + } + + } +} diff --git a/async/async-commons/src/test/java/org/reactivecommons/async/commons/converters/json/JacksonMessageConverterTest.java b/async/async-commons/src/test/java/org/reactivecommons/async/commons/converters/json/JacksonMessageConverterTest.java new file mode 100644 index 00000000..f0abf051 --- /dev/null +++ b/async/async-commons/src/test/java/org/reactivecommons/async/commons/converters/json/JacksonMessageConverterTest.java @@ -0,0 +1,132 @@ +package org.reactivecommons.async.commons.converters.json; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.reactivecommons.async.commons.communications.Message; +import org.reactivecommons.async.commons.exceptions.MessageConversionException; +import tools.jackson.databind.json.JsonMapper; + +import java.nio.charset.StandardCharsets; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class JacksonMessageConverterTest { + + private static final JsonMapper MAPPER = new JsonMapper(); + + private JacksonMessageConverter converter; + + @BeforeEach + void setUp() { + converter = new JacksonMessageConverter(MAPPER) { + @Override + public Message toMessage(Object object) { + byte[] bytes = jsonMapper.writeValueAsBytes(object); + return createMessage(bytes, "application/json"); + } + }; + } + + @Test + void readAsyncQuery() { + String json = "{\"resource\":\"myQuery\",\"queryData\":{\"id\":\"1\",\"name\":\"n\",\"date\":null}}"; + var msg = createMessage(json.getBytes(StandardCharsets.UTF_8), "application/json"); + var result = converter.readAsyncQuery(msg, SampleClass.class); + assertThat(result.getResource()).isEqualTo("myQuery"); + assertThat(result.getQueryData()).isNotNull(); + assertThat(result.getQueryData().getName()).isEqualTo("n"); + } + + @Test + void readDomainEvent() { + String json = "{\"name\":\"myEvent\",\"eventId\":\"eid123\",\"data\":{\"id\":\"1\",\"name\":\"val\",\"date\":null}}"; + var msg = createMessage(json.getBytes(StandardCharsets.UTF_8), "application/json"); + var result = converter.readDomainEvent(msg, SampleClass.class); + assertThat(result.getName()).isEqualTo("myEvent"); + assertThat(result.getEventId()).isEqualTo("eid123"); + assertThat(result.getData()).isNotNull(); + assertThat(result.getData().getName()).isEqualTo("val"); + } + + @Test + void readCommand() { + String json = "{\"name\":\"myCmd\",\"commandId\":\"cid456\",\"data\":{\"id\":\"x\",\"name\":\"v\",\"date\":null}}"; + var msg = createMessage(json.getBytes(StandardCharsets.UTF_8), "application/json"); + var result = converter.readCommand(msg, SampleClass.class); + assertThat(result.getName()).isEqualTo("myCmd"); + assertThat(result.getCommandId()).isEqualTo("cid456"); + assertThat(result.getData()).isNotNull(); + } + + @Test + void readValueThrowsOnInvalidJson() { + var msg = createMessage("not-json".getBytes(StandardCharsets.UTF_8), "application/json"); + assertThatThrownBy(() -> converter.readValue(msg, SampleClass.class)) + .isInstanceOf(MessageConversionException.class) + .hasMessageContaining("Failed to convert Message content"); + } + + @Test + void readCommandStructure() { + String json = "{\"name\":\"cmd\",\"commandId\":\"id\",\"data\":{\"field\":\"x\"}}"; + var msg = createMessage(json.getBytes(StandardCharsets.UTF_8), "application/json"); + var result = converter.readCommandStructure(msg); + assertThat(result.getName()).isEqualTo("cmd"); + assertThat(result.getCommandId()).isEqualTo("id"); + assertThat(result.getData()).isNotNull(); + } + + @Test + void readDomainEventStructure() { + String json = "{\"name\":\"ev\",\"eventId\":\"eid\",\"data\":{\"field\":\"y\"}}"; + var msg = createMessage(json.getBytes(StandardCharsets.UTF_8), "application/json"); + var result = converter.readDomainEventStructure(msg); + assertThat(result.getName()).isEqualTo("ev"); + assertThat(result.getEventId()).isEqualTo("eid"); + } + + @Test + void readAsyncQueryStructure() { + String json = "{\"resource\":\"q\",\"queryData\":{\"field\":\"z\"}}"; + var msg = createMessage(json.getBytes(StandardCharsets.UTF_8), "application/json"); + var result = converter.readAsyncQueryStructure(msg); + assertThat(result.getResource()).isEqualTo("q"); + assertThat(result.getQueryData()).isNotNull(); + } + + private Message createMessage(byte[] body, String contentType) { + return new Message() { + @Override + public String getType() { + return "test"; + } + + @Override + public byte[] getBody() { + return body; + } + + @Override + public Properties getProperties() { + return new Properties() { + @Override + public String getContentType() { + return contentType; + } + + @Override + public long getContentLength() { + return body.length; + } + + @Override + public Map getHeaders() { + return Map.of(); + } + }; + } + }; + } +} diff --git a/async/async-commons/src/test/java/org/reactivecommons/async/commons/exceptions/MessageConversionExceptionTest.java b/async/async-commons/src/test/java/org/reactivecommons/async/commons/exceptions/MessageConversionExceptionTest.java new file mode 100644 index 00000000..3de00d54 --- /dev/null +++ b/async/async-commons/src/test/java/org/reactivecommons/async/commons/exceptions/MessageConversionExceptionTest.java @@ -0,0 +1,29 @@ +package org.reactivecommons.async.commons.exceptions; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class MessageConversionExceptionTest { + + @Test + void messageAndCause() { + var cause = new RuntimeException("root"); + var ex = new MessageConversionException("fail", cause); + assertThat(ex.getMessage()).isEqualTo("fail"); + assertThat(ex.getCause()).isSameAs(cause); + } + + @Test + void messageOnly() { + var ex = new MessageConversionException("fail"); + assertThat(ex.getMessage()).isEqualTo("fail"); + } + + @Test + void wrapsException() { + var cause = new Exception("root"); + var ex = new MessageConversionException(cause); + assertThat(ex.getCause()).isSameAs(cause); + } +} diff --git a/async/async-commons/src/test/java/org/reactivecommons/async/commons/exceptions/SendFailureNoAckExceptionTest.java b/async/async-commons/src/test/java/org/reactivecommons/async/commons/exceptions/SendFailureNoAckExceptionTest.java new file mode 100644 index 00000000..ad1bbfbf --- /dev/null +++ b/async/async-commons/src/test/java/org/reactivecommons/async/commons/exceptions/SendFailureNoAckExceptionTest.java @@ -0,0 +1,14 @@ +package org.reactivecommons.async.commons.exceptions; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class SendFailureNoAckExceptionTest { + + @Test + void messageIsSet() { + var ex = new SendFailureNoAckException("no ack"); + assertThat(ex.getMessage()).isEqualTo("no ack"); + } +} diff --git a/async/async-commons/src/test/java/org/reactivecommons/async/commons/ext/DefaultCustomReporterTest.java b/async/async-commons/src/test/java/org/reactivecommons/async/commons/ext/DefaultCustomReporterTest.java new file mode 100644 index 00000000..64e3f1f4 --- /dev/null +++ b/async/async-commons/src/test/java/org/reactivecommons/async/commons/ext/DefaultCustomReporterTest.java @@ -0,0 +1,93 @@ +package org.reactivecommons.async.commons.ext; + +import org.junit.jupiter.api.Test; +import org.reactivecommons.api.domain.Command; +import org.reactivecommons.api.domain.DomainEvent; +import org.reactivecommons.async.api.AsyncQuery; +import org.reactivecommons.async.commons.communications.Message; +import reactor.test.StepVerifier; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThatNoException; +class DefaultCustomReporterTest { + + DefaultCustomReporter reporter = new DefaultCustomReporter(); + Message msg = createMessage(); + + @Test + void reportErrorCommandReturnsEmpty() { + reporter.reportError(new RuntimeException(), msg, new Command<>("cmd", "id", "data"), false) + .as(StepVerifier::create) + .verifyComplete(); + } + + @Test + void reportErrorDomainEventReturnsEmpty() { + reporter.reportError(new RuntimeException(), msg, new DomainEvent<>("ev", "id", "data"), false) + .as(StepVerifier::create) + .verifyComplete(); + } + + @Test + void reportErrorAsyncQueryReturnsEmpty() { + reporter.reportError(new RuntimeException(), msg, new AsyncQuery<>("res", "data"), false) + .as(StepVerifier::create) + .verifyComplete(); + } + + @Test + void reportErrorRoutesCommand() { + var command = new Command<>("cmd", "id", "data"); + reporter.reportError(new RuntimeException(), msg, (Object) command, false) + .as(StepVerifier::create) + .verifyComplete(); + } + + @Test + void reportErrorRoutesDomainEvent() { + var event = new DomainEvent<>("ev", "id", "data"); + reporter.reportError(new RuntimeException(), msg, (Object) event, false) + .as(StepVerifier::create) + .verifyComplete(); + } + + @Test + void reportErrorRoutesAsyncQuery() { + var query = new AsyncQuery<>("res", "data"); + reporter.reportError(new RuntimeException(), msg, (Object) query, false) + .as(StepVerifier::create) + .verifyComplete(); + } + + @Test + void reportErrorUnknownTypeReturnsEmpty() { + var result = reporter.reportError(new RuntimeException(), msg, "unknown", false); + assertThatNoException().isThrownBy(() -> result.as(StepVerifier::create).verifyComplete()); + } + + @Test + void reportMetricDoesNothing() { + assertThatNoException().isThrownBy(() -> reporter.reportMetric("type", "path", 100L, true)); + } + + private Message createMessage() { + return new Message() { + @Override + public String getType() { return "test"; } + @Override + public byte[] getBody() { return new byte[0]; } + @Override + public Properties getProperties() { + return new Properties() { + @Override + public String getContentType() { return "application/json"; } + @Override + public long getContentLength() { return 0; } + @Override + public Map getHeaders() { return Map.of(); } + }; + } + }; + } +} diff --git a/async/async-commons/src/test/java/org/reactivecommons/async/commons/utils/resolver/HandlerResolverBuilderTest.java b/async/async-commons/src/test/java/org/reactivecommons/async/commons/utils/resolver/HandlerResolverBuilderTest.java new file mode 100644 index 00000000..e9062ba6 --- /dev/null +++ b/async/async-commons/src/test/java/org/reactivecommons/async/commons/utils/resolver/HandlerResolverBuilderTest.java @@ -0,0 +1,147 @@ +package org.reactivecommons.async.commons.utils.resolver; + +import org.junit.jupiter.api.Test; +import org.reactivecommons.async.api.DefaultCommandHandler; +import org.reactivecommons.async.api.HandlerRegistry; +import reactor.core.publisher.Mono; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +class HandlerResolverBuilderTest { + + private final DefaultCommandHandler defaultHandler = command -> Mono.empty(); + + @Test + void buildResolverWithEmptyRegistries() { + var registry = HandlerRegistry.register(); + var resolver = HandlerResolverBuilder.buildResolver("app", Map.of("r1", registry), defaultHandler); + + assertThat(resolver.hasQueryHandlers()).isFalse(); + assertThat(resolver.hasCommandHandlers()).isFalse(); + assertThat(resolver.hasNotificationListeners()).isFalse(); + } + + @Test + void buildResolverWithQueryHandlers() { + var registry = HandlerRegistry.register() + .serveQuery("my.query", msg -> Mono.just("result"), String.class); + + var resolver = HandlerResolverBuilder.buildResolver("app", Map.of("r1", registry), defaultHandler); + + assertThat(resolver.hasQueryHandlers()).isTrue(); + assertThat(resolver.getQueryHandler("my.query")).isNotNull(); + assertThat(resolver.getQueryHandler("my.query").path()).isEqualTo("my.query"); + } + + @Test + void buildResolverWithCommandHandlers() { + var registry = HandlerRegistry.register() + .handleCommand("my.cmd", cmd -> Mono.empty(), String.class); + + var resolver = HandlerResolverBuilder.buildResolver("app", Map.of("r1", registry), defaultHandler); + + assertThat(resolver.hasCommandHandlers()).isTrue(); + assertThat(resolver.getCommandHandler("my.cmd")).isNotNull(); + assertThat(resolver.getCommandHandler("my.cmd").path()).isEqualTo("my.cmd"); + } + + @Test + void buildResolverWithEventListeners() { + var registry = HandlerRegistry.register() + .listenEvent("my.event", evt -> Mono.empty(), String.class); + + var resolver = HandlerResolverBuilder.buildResolver("app", Map.of("r1", registry), defaultHandler); + + assertThat(resolver.getEventListener("my.event")).isNotNull(); + } + + @Test + void buildResolverWithNotificationListeners() { + var registry = HandlerRegistry.register() + .listenNotificationEvent("notif.event", evt -> Mono.empty(), String.class); + + var resolver = HandlerResolverBuilder.buildResolver("app", Map.of("r1", registry), defaultHandler); + + assertThat(resolver.hasNotificationListeners()).isTrue(); + assertThat(resolver.getNotificationListener("notif.event")).isNotNull(); + } + + @Test + void buildResolverWithQueueListeners() { + var registry = HandlerRegistry.register() + .listenQueue("myQueue", msg -> Mono.empty()); + + var resolver = HandlerResolverBuilder.buildResolver("app", Map.of("r1", registry), defaultHandler); + + assertThat(resolver.getQueueListeners()).containsKey("myQueue"); + } + + @Test + void buildResolverWithDynamicEventHandlers() { + var registry = HandlerRegistry.register() + .handleDynamicEvents("event.*", evt -> Mono.empty(), String.class); + + var resolver = HandlerResolverBuilder.buildResolver("app", Map.of("r1", registry), defaultHandler); + + assertThat(resolver.getEventListener("event.foo")).isNotNull(); + } + + @Test + void defaultCommandHandlerReturnedForUnknownPath() { + var registry = HandlerRegistry.register(); + var resolver = HandlerResolverBuilder.buildResolver("app", Map.of("r1", registry), defaultHandler); + + var handler = resolver.getCommandHandler("unknown.command"); + assertThat(handler).isNotNull(); + assertThat(handler.path()).isEmpty(); + } + + @Test + void buildResolverWithMultipleRegistries() { + var registry1 = HandlerRegistry.register() + .serveQuery("q1", msg -> Mono.just("r1"), String.class); + var registry2 = HandlerRegistry.register() + .handleCommand("c1", cmd -> Mono.empty(), String.class); + + var resolver = HandlerResolverBuilder.buildResolver("app", + Map.of("r1", registry1, "r2", registry2), defaultHandler); + + assertThat(resolver.hasQueryHandlers()).isTrue(); + assertThat(resolver.hasCommandHandlers()).isTrue(); + } + + @Test + void buildResolverForCustomDomain() { + var registry = HandlerRegistry.register() + .handleCommand("custom", "domain.cmd", cmd -> Mono.empty(), String.class); + + var resolver = HandlerResolverBuilder.buildResolver("custom", Map.of("r1", registry), defaultHandler); + + assertThat(resolver.hasCommandHandlers()).isTrue(); + assertThat(resolver.getCommandHandler("domain.cmd")).isNotNull(); + } + + @Test + void buildResolverForDomainIgnoresOtherDomainEntries() { + var registry = HandlerRegistry.register() + .handleCommand("other", "other.cmd", cmd -> Mono.empty(), String.class); + + var resolver = HandlerResolverBuilder.buildResolver("app", Map.of("r1", registry), defaultHandler); + + assertThat(resolver.hasCommandHandlers()).isFalse(); + } + + @Test + void buildResolverEventListenersIncludesDynamics() { + var registry = HandlerRegistry.register() + .listenEvent("static.event", evt -> Mono.empty(), String.class) + .handleDynamicEvents("dynamic.*", evt -> Mono.empty(), String.class); + + var resolver = HandlerResolverBuilder.buildResolver("app", Map.of("r1", registry), defaultHandler); + + assertThat(resolver.getEventListener("static.event")).isNotNull(); + assertThat(resolver.getEventListener("dynamic.foo")).isNotNull(); + } +} diff --git a/async/async-rabbit/async-rabbit.gradle b/async/async-rabbit/async-rabbit.gradle index 8d7ffafc..dde69636 100644 --- a/async/async-rabbit/async-rabbit.gradle +++ b/async/async-rabbit/async-rabbit.gradle @@ -8,6 +8,7 @@ dependencies { api project(':domain-events-api') api project(':async-commons') api project(':cloudevents-json-jackson') + api project(':reactor-rabbitmq') api 'io.projectreactor:reactor-core' api 'io.projectreactor:reactor-core-micrometer' diff --git a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/ReactiveMessageSender.java b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/ReactiveMessageSender.java index 954b2148..a34149ec 100644 --- a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/ReactiveMessageSender.java +++ b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/ReactiveMessageSender.java @@ -69,12 +69,12 @@ private void initializeSenders() { final Flux messageSource = Flux.create(fluxSinkConfirm::add); sender.sendWithTypedPublishConfirms(messageSource, new SendOptions().trackReturned(isMandatory)) .doOnNext((OutboundMessageResult outboundMessageResult) -> { - if (outboundMessageResult.isReturned()) { + if (outboundMessageResult.returned()) { this.unroutableMessageNotifier.notifyUnroutableMessage(outboundMessageResult); } final Consumer ackNotifier = - outboundMessageResult.getOutboundMessage().getAckNotifier(); - executorService.submit(() -> ackNotifier.accept(outboundMessageResult.isAck())); + outboundMessageResult.outboundMessage().getAckNotifier(); + executorService.submit(() -> ackNotifier.accept(outboundMessageResult.ack())); }).subscribe(); } @@ -108,7 +108,7 @@ public Flux sendWithConfirmBatch(Flux messages, St Map headers, boolean persistent) { return messages.map(message -> toOutboundMessage(message, exchange, routingKey, headers, persistent)) .as(sender::sendWithPublishConfirms) - .flatMap(result -> result.isAck() ? + .flatMap(result -> result.ack() ? Mono.empty() : Mono.error(new SendFailureNoAckException("Event no ACK in communications")) ); diff --git a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/UnroutableMessageProcessor.java b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/UnroutableMessageProcessor.java index f2ce39b7..2acd6e5e 100644 --- a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/UnroutableMessageProcessor.java +++ b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/UnroutableMessageProcessor.java @@ -14,7 +14,7 @@ public class UnroutableMessageProcessor implements UnroutableMessageHandler { @Override public Mono processMessage(OutboundMessageResult result) { - var outboundMessage = result.getOutboundMessage(); + var outboundMessage = result.outboundMessage(); log.severe("Unroutable message: exchange=" + outboundMessage.getExchange() + ", routingKey=" + outboundMessage.getRoutingKey() + ", body=" + new String(outboundMessage.getBody(), StandardCharsets.UTF_8) diff --git a/async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/communications/UnroutableMessageProcessorTest.java b/async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/communications/UnroutableMessageProcessorTest.java index c9fabe91..645a6c48 100644 --- a/async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/communications/UnroutableMessageProcessorTest.java +++ b/async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/communications/UnroutableMessageProcessorTest.java @@ -28,7 +28,7 @@ class UnroutableMessageProcessorTest { @Test void logsUnroutableMessageDetails() { - when(messageResult.getOutboundMessage()).thenReturn(myOutboundMessage); + when(messageResult.outboundMessage()).thenReturn(myOutboundMessage); when(myOutboundMessage.getExchange()).thenReturn("test-exchange"); when(myOutboundMessage.getRoutingKey()).thenReturn("test-routingKey"); when(myOutboundMessage.getBody()).thenReturn("test-body".getBytes(StandardCharsets.UTF_8)); @@ -37,7 +37,7 @@ void logsUnroutableMessageDetails() { StepVerifier.create(unroutableMessageProcessor.processMessage(messageResult)) .verifyComplete(); - verify(messageResult).getOutboundMessage(); + verify(messageResult).outboundMessage(); verify(myOutboundMessage).getExchange(); verify(myOutboundMessage).getRoutingKey(); verify(myOutboundMessage).getBody(); diff --git a/async/reactor-rabbitmq/reactor-rabbitmq.gradle b/async/reactor-rabbitmq/reactor-rabbitmq.gradle new file mode 100644 index 00000000..3e48635f --- /dev/null +++ b/async/reactor-rabbitmq/reactor-rabbitmq.gradle @@ -0,0 +1,9 @@ +ext { + artifactId = 'reactor-rabbitmq' + artifactDescription = 'Reactor RabbitMQ' +} + +dependencies { + api 'io.projectreactor:reactor-core' + api 'com.rabbitmq:amqp-client' +} diff --git a/async/async-rabbit/src/main/java/reactor/rabbitmq/AcknowledgableDelivery.java b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/AcknowledgableDelivery.java similarity index 91% rename from async/async-rabbit/src/main/java/reactor/rabbitmq/AcknowledgableDelivery.java rename to async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/AcknowledgableDelivery.java index 0ab05610..a55695dc 100644 --- a/async/async-rabbit/src/main/java/reactor/rabbitmq/AcknowledgableDelivery.java +++ b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/AcknowledgableDelivery.java @@ -32,7 +32,7 @@ public class AcknowledgableDelivery extends Delivery { private final AtomicBoolean notAckedOrNacked = new AtomicBoolean(true); public AcknowledgableDelivery(Delivery delivery, Channel channel, - BiConsumer exceptionHandler) { + BiConsumer exceptionHandler) { super(delivery.getEnvelope(), delivery.getProperties(), delivery.getBody()); this.channel = channel; this.exceptionHandler = exceptionHandler; @@ -43,7 +43,7 @@ public void ack(boolean multiple) { try { basicAck(multiple); } catch (Exception e) { - retry(e, (delivery) -> delivery.basicAck(multiple)); + retry(e, delivery -> delivery.basicAck(multiple)); } } } @@ -57,7 +57,7 @@ public void nack(boolean multiple, boolean requeue) { try { basicNack(multiple, requeue); } catch (Exception e) { - retry(e, (delivery) -> delivery.basicNack(multiple, requeue)); + retry(e, delivery -> delivery.basicNack(multiple, requeue)); } } } diff --git a/async/async-rabbit/src/main/java/reactor/rabbitmq/BindingSpecification.java b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/BindingSpecification.java similarity index 79% rename from async/async-rabbit/src/main/java/reactor/rabbitmq/BindingSpecification.java rename to async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/BindingSpecification.java index 338f9d4b..6d54fefd 100644 --- a/async/async-rabbit/src/main/java/reactor/rabbitmq/BindingSpecification.java +++ b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/BindingSpecification.java @@ -16,13 +16,18 @@ package reactor.rabbitmq; -import reactor.util.annotation.Nullable; + +import lombok.Getter; import java.util.Map; +@Getter public class BindingSpecification { - private String queue, exchange, exchangeTo, routingKey; + private String queue; + private String exchange; + private String exchangeTo; + private String routingKey; private Map arguments; public static BindingSpecification binding() { @@ -66,29 +71,9 @@ public BindingSpecification routingKey(String routingKey) { return this; } - public BindingSpecification arguments(@Nullable Map arguments) { + public BindingSpecification arguments(Map arguments) { this.arguments = arguments; return this; } - public String getQueue() { - return queue; - } - - public String getExchange() { - return exchange; - } - - public String getRoutingKey() { - return routingKey; - } - - public String getExchangeTo() { - return exchangeTo; - } - - @Nullable - public Map getArguments() { - return arguments; - } } diff --git a/async/async-rabbit/src/main/java/reactor/rabbitmq/ChannelCloseHandlers.java b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/ChannelCloseHandlers.java similarity index 94% rename from async/async-rabbit/src/main/java/reactor/rabbitmq/ChannelCloseHandlers.java rename to async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/ChannelCloseHandlers.java index 0091b061..8e672e59 100644 --- a/async/async-rabbit/src/main/java/reactor/rabbitmq/ChannelCloseHandlers.java +++ b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/ChannelCloseHandlers.java @@ -17,12 +17,15 @@ package reactor.rabbitmq; import com.rabbitmq.client.Channel; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.SignalType; import java.util.function.BiConsumer; +@NoArgsConstructor(access = AccessLevel.PRIVATE) public class ChannelCloseHandlers { public static final BiConsumer SENDER_CHANNEL_CLOSE_HANDLER_INSTANCE = diff --git a/async/async-rabbit/src/main/java/reactor/rabbitmq/ChannelPool.java b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/ChannelPool.java similarity index 100% rename from async/async-rabbit/src/main/java/reactor/rabbitmq/ChannelPool.java rename to async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/ChannelPool.java diff --git a/async/async-rabbit/src/main/java/reactor/rabbitmq/ChannelPoolFactory.java b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/ChannelPoolFactory.java similarity index 91% rename from async/async-rabbit/src/main/java/reactor/rabbitmq/ChannelPoolFactory.java rename to async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/ChannelPoolFactory.java index c9aefba5..1e39d3e4 100644 --- a/async/async-rabbit/src/main/java/reactor/rabbitmq/ChannelPoolFactory.java +++ b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/ChannelPoolFactory.java @@ -17,8 +17,11 @@ package reactor.rabbitmq; import com.rabbitmq.client.Connection; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; import reactor.core.publisher.Mono; +@NoArgsConstructor(access = AccessLevel.PRIVATE) public class ChannelPoolFactory { public static ChannelPool createChannelPool(Mono connectionMono) { diff --git a/async/async-rabbit/src/main/java/reactor/rabbitmq/ChannelPoolOptions.java b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/ChannelPoolOptions.java similarity index 75% rename from async/async-rabbit/src/main/java/reactor/rabbitmq/ChannelPoolOptions.java rename to async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/ChannelPoolOptions.java index ab1686e1..1d7d5c45 100644 --- a/async/async-rabbit/src/main/java/reactor/rabbitmq/ChannelPoolOptions.java +++ b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/ChannelPoolOptions.java @@ -16,9 +16,10 @@ package reactor.rabbitmq; +import lombok.Getter; import reactor.core.scheduler.Scheduler; -import reactor.util.annotation.Nullable; +@Getter public class ChannelPoolOptions { private Integer maxCacheSize; @@ -29,18 +30,9 @@ public ChannelPoolOptions maxCacheSize(int maxCacheSize) { return this; } - @Nullable - public Integer getMaxCacheSize() { - return maxCacheSize; - } - - public ChannelPoolOptions subscriptionScheduler(@Nullable Scheduler subscriptionScheduler) { + public ChannelPoolOptions subscriptionScheduler(Scheduler subscriptionScheduler) { this.subscriptionScheduler = subscriptionScheduler; return this; } - @Nullable - public Scheduler getSubscriptionScheduler() { - return subscriptionScheduler; - } } diff --git a/async/async-rabbit/src/main/java/reactor/rabbitmq/ChannelProxy.java b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/ChannelProxy.java similarity index 96% rename from async/async-rabbit/src/main/java/reactor/rabbitmq/ChannelProxy.java rename to async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/ChannelProxy.java index 02842ecf..5f7af155 100644 --- a/async/async-rabbit/src/main/java/reactor/rabbitmq/ChannelProxy.java +++ b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/ChannelProxy.java @@ -19,11 +19,12 @@ import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.Method; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; import java.io.IOException; import java.lang.reflect.Proxy; import java.util.concurrent.atomic.AtomicReference; -import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** @@ -31,11 +32,9 @@ * {@link Channel#asyncCompletableRpc(Method)}, re-opening the underlying channel if necessary. * Used for resource management operations (declare, bind, unbind) in {@link Sender}. */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) public final class ChannelProxy { - private ChannelProxy() { - } - /** * Creates a dynamic proxy implementing {@link Channel} where only * {@code asyncCompletableRpc} is functional. All other methods throw diff --git a/async/async-rabbit/src/main/java/reactor/rabbitmq/ConsumeOptions.java b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/ConsumeOptions.java similarity index 70% rename from async/async-rabbit/src/main/java/reactor/rabbitmq/ConsumeOptions.java rename to async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/ConsumeOptions.java index 9a63ce3e..303d80b7 100644 --- a/async/async-rabbit/src/main/java/reactor/rabbitmq/ConsumeOptions.java +++ b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/ConsumeOptions.java @@ -18,25 +18,27 @@ import com.rabbitmq.client.Channel; import com.rabbitmq.client.Delivery; -import java.util.Collections; -import java.util.Map; +import lombok.Getter; import reactor.core.publisher.FluxSink; import java.time.Duration; +import java.util.Collections; +import java.util.Map; import java.util.function.BiConsumer; -import java.util.function.BiFunction; +import java.util.function.BiPredicate; import java.util.function.Consumer; +@Getter public class ConsumeOptions { private int qos = 250; private String consumerTag = ""; private FluxSink.OverflowStrategy overflowStrategy = FluxSink.OverflowStrategy.BUFFER; - private BiFunction hookBeforeEmitBiFunction = + private BiPredicate hookBeforeEmitBiFunction = (requestedFromDownstream, message) -> true; - private BiFunction stopConsumingBiFunction = + private BiPredicate stopConsumingBiFunction = (requestedFromDownstream, message) -> false; private BiConsumer exceptionHandler = @@ -50,10 +52,6 @@ public class ConsumeOptions { private Map arguments = Collections.emptyMap(); - public int getQos() { - return qos; - } - public ConsumeOptions qos(int qos) { if (qos < 0) { throw new IllegalArgumentException("QoS must be greater or equal to 0"); @@ -62,10 +60,6 @@ public ConsumeOptions qos(int qos) { return this; } - public String getConsumerTag() { - return consumerTag; - } - public ConsumeOptions consumerTag(String consumerTag) { if (consumerTag == null) { throw new IllegalArgumentException("consumerTag must be non-null"); @@ -74,29 +68,17 @@ public ConsumeOptions consumerTag(String consumerTag) { return this; } - public FluxSink.OverflowStrategy getOverflowStrategy() { - return overflowStrategy; - } - public ConsumeOptions overflowStrategy(FluxSink.OverflowStrategy overflowStrategy) { this.overflowStrategy = overflowStrategy; return this; } - public BiFunction getHookBeforeEmitBiFunction() { - return hookBeforeEmitBiFunction; - } - - public ConsumeOptions hookBeforeEmitBiFunction(BiFunction hookBeforeEmit) { + public ConsumeOptions hookBeforeEmitBiFunction(BiPredicate hookBeforeEmit) { this.hookBeforeEmitBiFunction = hookBeforeEmit; return this; } - public BiFunction getStopConsumingBiFunction() { - return stopConsumingBiFunction; - } - - public ConsumeOptions stopConsumingBiFunction(BiFunction stopConsumingBiFunction) { + public ConsumeOptions stopConsumingBiFunction(BiPredicate stopConsumingBiFunction) { this.stopConsumingBiFunction = stopConsumingBiFunction; return this; } @@ -106,25 +88,14 @@ public ConsumeOptions exceptionHandler(BiConsumer getExceptionHandler() { - return exceptionHandler; - } - public ConsumeOptions channelCallback(Consumer channelCallback) { this.channelCallback = channelCallback; return this; } - public Consumer getChannelCallback() { - return channelCallback; - } - public ConsumeOptions arguments(Map arguments) { this.arguments = arguments; return this; } - public Map getArguments() { - return arguments; - } } diff --git a/async/async-rabbit/src/main/java/reactor/rabbitmq/ExceptionHandlers.java b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/ExceptionHandlers.java similarity index 92% rename from async/async-rabbit/src/main/java/reactor/rabbitmq/ExceptionHandlers.java rename to async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/ExceptionHandlers.java index ca96b138..20a4c7dd 100644 --- a/async/async-rabbit/src/main/java/reactor/rabbitmq/ExceptionHandlers.java +++ b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/ExceptionHandlers.java @@ -18,15 +18,18 @@ import com.rabbitmq.client.ShutdownSignalException; import com.rabbitmq.client.impl.recovery.AutorecoveringConnection; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; import java.time.Duration; import java.util.concurrent.Callable; import java.util.function.BiConsumer; import java.util.function.Predicate; +@NoArgsConstructor(access = AccessLevel.PRIVATE) public class ExceptionHandlers { - public static Predicate CONNECTION_RECOVERY_PREDICATE = new ConnectionRecoveryTriggeringPredicate(); + public static final Predicate CONNECTION_RECOVERY_PREDICATE = new ConnectionRecoveryTriggeringPredicate(); public static class ConnectionRecoveryTriggeringPredicate implements Predicate { @Override @@ -43,7 +46,7 @@ public static class SimpleRetryTemplate { private final long timeout; private final long waitingTime; private final Predicate predicate; - private boolean failOnTimeout; + private final boolean failOnTimeout; public SimpleRetryTemplate(Duration timeout, Duration waitingTime, Predicate predicate) { this(timeout, waitingTime, predicate, true); @@ -71,20 +74,21 @@ public SimpleRetryTemplate(Duration timeout, Duration waitingTime, Predicate operation, Exception e) { if (predicate.test(e)) { - int elapsedTime = 0; + long elapsedTime = 0; boolean callSucceeded = false; while (elapsedTime < timeout) { try { Thread.sleep(waitingTime); } catch (InterruptedException ie) { - throw new RabbitFluxException("Thread interrupted while retry on sending", e); + Thread.currentThread().interrupt(); + throw new RabbitFluxException("Thread interrupted while retry on sending", ie); } elapsedTime += waitingTime; try { operation.call(); callSucceeded = true; break; - } catch (Throwable sendingException) { + } catch (Exception sendingException) { if (!predicate.test(sendingException)) { throw new RabbitFluxException("Not retryable exception thrown during retry", sendingException); diff --git a/async/async-rabbit/src/main/java/reactor/rabbitmq/ExchangeSpecification.java b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/ExchangeSpecification.java similarity index 72% rename from async/async-rabbit/src/main/java/reactor/rabbitmq/ExchangeSpecification.java rename to async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/ExchangeSpecification.java index 9632a401..5cedcf36 100644 --- a/async/async-rabbit/src/main/java/reactor/rabbitmq/ExchangeSpecification.java +++ b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/ExchangeSpecification.java @@ -16,15 +16,18 @@ package reactor.rabbitmq; -import reactor.util.annotation.Nullable; - +import lombok.Getter; import java.util.Map; +@Getter public class ExchangeSpecification { private String name; private String type = "direct"; - private boolean durable = false, autoDelete = false, internal = false, passive = false; + private boolean durable = false; + private boolean autoDelete = false; + private boolean internal = false; + private boolean passive = false; private Map arguments; public static ExchangeSpecification exchange() { @@ -65,37 +68,8 @@ public ExchangeSpecification passive(boolean passive) { return this; } - public ExchangeSpecification arguments(@Nullable Map arguments) { + public ExchangeSpecification arguments(Map arguments) { this.arguments = arguments; return this; } - - public String getName() { - return name; - } - - public String getType() { - return type; - } - - public boolean isDurable() { - return durable; - } - - public boolean isAutoDelete() { - return autoDelete; - } - - public boolean isInternal() { - return internal; - } - - public boolean isPassive() { - return passive; - } - - @Nullable - public Map getArguments() { - return arguments; - } } diff --git a/async/async-rabbit/src/main/java/reactor/rabbitmq/Helpers.java b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/Helpers.java similarity index 94% rename from async/async-rabbit/src/main/java/reactor/rabbitmq/Helpers.java rename to async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/Helpers.java index f313491c..eeb138a4 100644 --- a/async/async-rabbit/src/main/java/reactor/rabbitmq/Helpers.java +++ b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/Helpers.java @@ -24,7 +24,7 @@ static void safelyExecute(Logger logger, ExceptionRunnable action, String messag try { action.run(); } catch (Exception e) { - logger.warn(message + ": {}", e.getMessage()); + logger.warn("{}: {}", message, e.getMessage()); } } diff --git a/async/async-rabbit/src/main/java/reactor/rabbitmq/LazyChannelPool.java b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/LazyChannelPool.java similarity index 97% rename from async/async-rabbit/src/main/java/reactor/rabbitmq/LazyChannelPool.java rename to async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/LazyChannelPool.java index 2ec5e027..547e8d11 100644 --- a/async/async-rabbit/src/main/java/reactor/rabbitmq/LazyChannelPool.java +++ b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/LazyChannelPool.java @@ -83,9 +83,9 @@ public BiConsumer getChannelCloseHandler() { public void close() { List channels = new ArrayList<>(); channelsQueue.drainTo(channels); - channels.forEach(channel -> { - SENDER_CHANNEL_CLOSE_HANDLER_INSTANCE.accept(SignalType.ON_COMPLETE, channel); - }); + channels.forEach(channel -> + SENDER_CHANNEL_CLOSE_HANDLER_INSTANCE.accept(SignalType.ON_COMPLETE, channel) + ); } private Channel createChannel(Connection connection) { diff --git a/async/async-rabbit/src/main/java/reactor/rabbitmq/OutboundMessage.java b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/OutboundMessage.java similarity index 76% rename from async/async-rabbit/src/main/java/reactor/rabbitmq/OutboundMessage.java rename to async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/OutboundMessage.java index 291fda82..3ccacf55 100644 --- a/async/async-rabbit/src/main/java/reactor/rabbitmq/OutboundMessage.java +++ b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/OutboundMessage.java @@ -17,10 +17,10 @@ package reactor.rabbitmq; import com.rabbitmq.client.AMQP.BasicProperties; -import reactor.util.annotation.Nullable; - +import lombok.Getter; import java.util.Arrays; +@Getter public class OutboundMessage { private final String exchange; @@ -34,30 +34,13 @@ public OutboundMessage(String exchange, String routingKey, byte[] body) { this(exchange, routingKey, null, body); } - public OutboundMessage(String exchange, String routingKey, @Nullable BasicProperties properties, byte[] body) { + public OutboundMessage(String exchange, String routingKey, BasicProperties properties, byte[] body) { this.exchange = exchange; this.routingKey = routingKey; this.properties = properties; this.body = body; } - public String getExchange() { - return exchange; - } - - public String getRoutingKey() { - return routingKey; - } - - @Nullable - public BasicProperties getProperties() { - return properties; - } - - public byte[] getBody() { - return body; - } - @Override public String toString() { return "OutboundMessage{" + @@ -71,8 +54,4 @@ public String toString() { void published() { this.published = true; } - - boolean isPublished() { - return published; - } } diff --git a/async/async-rabbit/src/main/java/reactor/rabbitmq/OutboundMessageResult.java b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/OutboundMessageResult.java similarity index 64% rename from async/async-rabbit/src/main/java/reactor/rabbitmq/OutboundMessageResult.java rename to async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/OutboundMessageResult.java index 670ac8ec..7c971f8a 100644 --- a/async/async-rabbit/src/main/java/reactor/rabbitmq/OutboundMessageResult.java +++ b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/OutboundMessageResult.java @@ -16,34 +16,12 @@ package reactor.rabbitmq; -public class OutboundMessageResult { - - private final OMSG outboundMessage; - private final boolean ack; - private final boolean returned; +public record OutboundMessageResult(OMSG outboundMessage, boolean ack, boolean returned) { public OutboundMessageResult(OMSG outboundMessage, boolean ack) { this(outboundMessage, ack, false); } - public OutboundMessageResult(OMSG outboundMessage, boolean ack, boolean returned) { - this.outboundMessage = outboundMessage; - this.ack = ack; - this.returned = returned; - } - - public OMSG getOutboundMessage() { - return outboundMessage; - } - - public boolean isAck() { - return ack; - } - - public boolean isReturned() { - return returned; - } - @Override public String toString() { return "OutboundMessageResult{" + diff --git a/async/async-rabbit/src/main/java/reactor/rabbitmq/QueueSpecification.java b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/QueueSpecification.java similarity index 82% rename from async/async-rabbit/src/main/java/reactor/rabbitmq/QueueSpecification.java rename to async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/QueueSpecification.java index fa5409cc..4aac2aa1 100644 --- a/async/async-rabbit/src/main/java/reactor/rabbitmq/QueueSpecification.java +++ b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/QueueSpecification.java @@ -16,10 +16,11 @@ package reactor.rabbitmq; -import reactor.util.annotation.Nullable; +import lombok.Getter; import java.util.Map; +@Getter public class QueueSpecification { protected String name; @@ -33,11 +34,11 @@ public static QueueSpecification queue() { return new NullNameQueueSpecification(); } - public static QueueSpecification queue(@Nullable String name) { + public static QueueSpecification queue(String name) { return new QueueSpecification().name(name); } - public QueueSpecification name(@Nullable String queue) { + public QueueSpecification name(String queue) { if (queue == null) { return new NullNameQueueSpecification().arguments(this.arguments); } @@ -60,7 +61,7 @@ public QueueSpecification autoDelete(boolean autoDelete) { return this; } - public QueueSpecification arguments(@Nullable Map arguments) { + public QueueSpecification arguments(Map arguments) { this.arguments = arguments; return this; } @@ -70,31 +71,6 @@ public QueueSpecification passive(boolean passive) { return this; } - @Nullable - public String getName() { - return name; - } - - public boolean isDurable() { - return durable; - } - - public boolean isExclusive() { - return exclusive; - } - - public boolean isAutoDelete() { - return autoDelete; - } - - public boolean isPassive() { - return passive; - } - - @Nullable - public Map getArguments() { - return arguments; - } private static class NullNameQueueSpecification extends QueueSpecification { @@ -107,7 +83,7 @@ private static class NullNameQueueSpecification extends QueueSpecification { } @Override - public QueueSpecification name(@Nullable String name) { + public QueueSpecification name(String name) { if (name == null) { return this; } diff --git a/async/async-rabbit/src/main/java/reactor/rabbitmq/RabbitFluxException.java b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/RabbitFluxException.java similarity index 95% rename from async/async-rabbit/src/main/java/reactor/rabbitmq/RabbitFluxException.java rename to async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/RabbitFluxException.java index 7620621f..d69c387e 100644 --- a/async/async-rabbit/src/main/java/reactor/rabbitmq/RabbitFluxException.java +++ b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/RabbitFluxException.java @@ -16,10 +16,11 @@ package reactor.rabbitmq; -public class RabbitFluxException extends RuntimeException { - public RabbitFluxException() { - } +import lombok.NoArgsConstructor; + +@NoArgsConstructor +public class RabbitFluxException extends RuntimeException { public RabbitFluxException(String message) { super(message); diff --git a/async/async-rabbit/src/main/java/reactor/rabbitmq/RabbitFluxRetryTimeoutException.java b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/RabbitFluxRetryTimeoutException.java similarity index 100% rename from async/async-rabbit/src/main/java/reactor/rabbitmq/RabbitFluxRetryTimeoutException.java rename to async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/RabbitFluxRetryTimeoutException.java diff --git a/async/async-rabbit/src/main/java/reactor/rabbitmq/Receiver.java b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/Receiver.java similarity index 91% rename from async/async-rabbit/src/main/java/reactor/rabbitmq/Receiver.java rename to async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/Receiver.java index 1926dbac..025b9624 100644 --- a/async/async-rabbit/src/main/java/reactor/rabbitmq/Receiver.java +++ b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/Receiver.java @@ -44,7 +44,13 @@ public class Receiver implements Closeable { private static final Logger LOGGER = LoggerFactory.getLogger(Receiver.class); - private static final Function CHANNEL_CREATION_FUNCTION = new Receiver.ChannelCreationFunction(); + private static final Function CHANNEL_CREATION_FUNCTION = conn -> { + try { + return conn.createChannel(); + } catch (IOException e) { + throw new RabbitFluxException("Error while creating channel", e); + } + }; private final Mono connectionMono; @@ -78,7 +84,7 @@ public Receiver(ReceiverOptions options) { } }); cm = options.getConnectionMonoConfigurator().apply(cm); - cm = cm.doOnNext(conn -> connection.set(conn)) + cm = cm.doOnNext(connection::set) .subscribeOn(this.connectionSubscriptionScheduler) .transform(this::cache); } else { @@ -145,10 +151,10 @@ public Flux consumeManualAck(final String queue, Consume DeliverCallback deliverCallback = (consumerTag, message) -> { AcknowledgableDelivery delivery = new AcknowledgableDelivery(message, channel, options.getExceptionHandler()); - if (options.getHookBeforeEmitBiFunction().apply(emitter.requestedFromDownstream(), delivery)) { + if (options.getHookBeforeEmitBiFunction().test(emitter.requestedFromDownstream(), delivery)) { emitter.next(delivery); } - if (options.getStopConsumingBiFunction().apply(emitter.requestedFromDownstream(), message)) { + if (options.getStopConsumingBiFunction().test(emitter.requestedFromDownstream(), message)) { emitter.complete(); } }; @@ -184,7 +190,7 @@ public Flux consumeManualAck(final String queue, Consume channel.close(); } } catch (TimeoutException | IOException e) { - LOGGER.warn("Error while closing channel: " + e.getMessage()); + LOGGER.warn("Error while closing channel: {}", e.getMessage()); } } }); @@ -214,7 +220,7 @@ public void close() { if (privateConnectionSubscriptionScheduler) { safelyExecute( LOGGER, - () -> this.connectionSubscriptionScheduler.dispose(), + this.connectionSubscriptionScheduler::dispose, "Error while disposing connection subscriber scheduler" ); } @@ -236,15 +242,4 @@ public void ackOrNack() { } } - private static class ChannelCreationFunction implements Function { - - @Override - public Channel apply(Connection connection) { - try { - return connection.createChannel(); - } catch (IOException e) { - throw new RabbitFluxException("Error while creating channel", e); - } - } - } } diff --git a/async/async-rabbit/src/main/java/reactor/rabbitmq/ReceiverOptions.java b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/ReceiverOptions.java similarity index 57% rename from async/async-rabbit/src/main/java/reactor/rabbitmq/ReceiverOptions.java rename to async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/ReceiverOptions.java index 3826184b..d198d5a2 100644 --- a/async/async-rabbit/src/main/java/reactor/rabbitmq/ReceiverOptions.java +++ b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/ReceiverOptions.java @@ -18,43 +18,30 @@ import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; +import lombok.Getter; import reactor.core.publisher.Mono; import reactor.core.scheduler.Scheduler; -import reactor.util.annotation.Nullable; import java.time.Duration; -import java.util.function.Function; -import java.util.function.Supplier; +import java.util.function.UnaryOperator; +@Getter public class ReceiverOptions { - private ConnectionFactory connectionFactory = ((Supplier) () -> { - ConnectionFactory connectionFactory = new ConnectionFactory(); - connectionFactory.useNio(); - return connectionFactory; - }).get(); + private ConnectionFactory connectionFactory = new ConnectionFactory(); private Mono connectionMono; private Scheduler connectionSubscriptionScheduler; private Utils.ExceptionFunction connectionSupplier; - private Function, Mono> connectionMonoConfigurator = cm -> cm; + private UnaryOperator> connectionMonoConfigurator = cm -> cm; private Duration connectionClosingTimeout = Duration.ofSeconds(30); - public ConnectionFactory getConnectionFactory() { - return connectionFactory; - } - public ReceiverOptions connectionFactory(ConnectionFactory connectionFactory) { this.connectionFactory = connectionFactory; return this; } - @Nullable - public Scheduler getConnectionSubscriptionScheduler() { - return connectionSubscriptionScheduler; - } - - public ReceiverOptions connectionSubscriptionScheduler(@Nullable Scheduler connectionSubscriptionScheduler) { + public ReceiverOptions connectionSubscriptionScheduler(Scheduler connectionSubscriptionScheduler) { this.connectionSubscriptionScheduler = connectionSubscriptionScheduler; return this; } @@ -70,38 +57,20 @@ public ReceiverOptions connectionSupplier(ConnectionFactory connectionFactory, return this; } - public ReceiverOptions connectionMono(@Nullable Mono connectionMono) { + public ReceiverOptions connectionMono(Mono connectionMono) { this.connectionMono = connectionMono; return this; } public ReceiverOptions connectionMonoConfigurator( - Function, Mono> connectionMonoConfigurator) { + UnaryOperator> connectionMonoConfigurator) { this.connectionMonoConfigurator = connectionMonoConfigurator; return this; } - @Nullable - public Mono getConnectionMono() { - return connectionMono; - } - - @Nullable - public Utils.ExceptionFunction getConnectionSupplier() { - return connectionSupplier; - } - - public Function, Mono> getConnectionMonoConfigurator() { - return connectionMonoConfigurator; - } - - public ReceiverOptions connectionClosingTimeout(@Nullable Duration connectionClosingTimeout) { + public ReceiverOptions connectionClosingTimeout(Duration connectionClosingTimeout) { this.connectionClosingTimeout = connectionClosingTimeout; return this; } - @Nullable - public Duration getConnectionClosingTimeout() { - return connectionClosingTimeout; - } } diff --git a/async/async-rabbit/src/main/java/reactor/rabbitmq/SendOptions.java b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/SendOptions.java similarity index 73% rename from async/async-rabbit/src/main/java/reactor/rabbitmq/SendOptions.java rename to async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/SendOptions.java index 9796fcdd..84fde5f2 100644 --- a/async/async-rabbit/src/main/java/reactor/rabbitmq/SendOptions.java +++ b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/SendOptions.java @@ -17,16 +17,16 @@ package reactor.rabbitmq; import com.rabbitmq.client.Channel; -import org.reactivestreams.Publisher; +import lombok.Getter; import reactor.core.publisher.Mono; import reactor.core.publisher.SignalType; import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Schedulers; -import reactor.util.annotation.Nullable; import java.time.Duration; import java.util.function.BiConsumer; +@Getter public class SendOptions { private BiConsumer exceptionHandler = new ExceptionHandlers.RetrySendingExceptionHandler( @@ -40,15 +40,6 @@ public class SendOptions { private Mono channelMono; private BiConsumer channelCloseHandler; - @Nullable - public Integer getMaxInFlight() { - return maxInFlight; - } - - public boolean isTrackReturned() { - return trackReturned; - } - public SendOptions trackReturned(boolean trackReturned) { this.trackReturned = trackReturned; return this; @@ -65,35 +56,17 @@ public SendOptions maxInFlight(int maxInFlight, Scheduler scheduler) { return this; } - public Scheduler getScheduler() { - return scheduler; - } - - public BiConsumer getExceptionHandler() { - return exceptionHandler; - } - public SendOptions exceptionHandler(BiConsumer exceptionHandler) { this.exceptionHandler = exceptionHandler; return this; } - @Nullable - public Mono getChannelMono() { - return channelMono; - } - - public SendOptions channelMono(@Nullable Mono channelMono) { + public SendOptions channelMono(Mono channelMono) { this.channelMono = channelMono; return this; } - @Nullable - public BiConsumer getChannelCloseHandler() { - return channelCloseHandler; - } - - public SendOptions channelCloseHandler(@Nullable BiConsumer channelCloseHandler) { + public SendOptions channelCloseHandler(BiConsumer channelCloseHandler) { this.channelCloseHandler = channelCloseHandler; return this; } diff --git a/async/async-rabbit/src/main/java/reactor/rabbitmq/Sender.java b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/Sender.java similarity index 90% rename from async/async-rabbit/src/main/java/reactor/rabbitmq/Sender.java rename to async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/Sender.java index 7f290aca..62931bc6 100644 --- a/async/async-rabbit/src/main/java/reactor/rabbitmq/Sender.java +++ b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/Sender.java @@ -16,7 +16,12 @@ package reactor.rabbitmq; -import com.rabbitmq.client.*; +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.ConfirmListener; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ReturnListener; +import com.rabbitmq.client.ShutdownListener; import com.rabbitmq.client.impl.AMQImpl; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; @@ -24,16 +29,17 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.CoreSubscriber; -import reactor.core.publisher.*; +import reactor.core.publisher.Flux; +import reactor.core.publisher.FluxOperator; +import reactor.core.publisher.Mono; +import reactor.core.publisher.Operators; +import reactor.core.publisher.SignalType; import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Schedulers; -import reactor.util.annotation.Nullable; import java.io.IOException; import java.time.Duration; import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentNavigableMap; import java.util.concurrent.ConcurrentSkipListMap; @@ -55,9 +61,21 @@ public class Sender implements AutoCloseable { private static final Logger LOGGER = LoggerFactory.getLogger(Sender.class); - private static final Function CHANNEL_CREATION_FUNCTION = new ChannelCreationFunction(); + private static final Function CHANNEL_CREATION_FUNCTION = conn -> { + try { + return conn.createChannel(); + } catch (IOException e) { + throw new RabbitFluxException("Error while creating channel", e); + } + }; - private static final Function CHANNEL_PROXY_CREATION_FUNCTION = new ChannelProxyCreationFunction(); + private static final Function CHANNEL_PROXY_CREATION_FUNCTION = conn -> { + try { + return ChannelProxy.create(conn); + } catch (IOException e) { + throw new RabbitFluxException("Error while creating channel", e); + } + }; private final Mono connectionMono; @@ -105,7 +123,7 @@ public Sender(SenderOptions options) { } }); cm = options.getConnectionMonoConfigurator().apply(cm); - cm = cm.doOnNext(conn -> connection.set(conn)) + cm = cm.doOnNext(connection::set) .subscribeOn(this.connectionSubscriptionScheduler) .transform(this::cache); } else { @@ -149,7 +167,7 @@ public Mono send(Publisher messages) { return send(messages, new SendOptions()); } - public Mono send(Publisher messages, @Nullable SendOptions options) { + public Mono send(Publisher messages, SendOptions options) { var opts = options == null ? new SendOptions() : options; final Mono currentChannelMono = getChannelMono(opts); final BiConsumer exceptionHandler = opts.getExceptionHandler(); @@ -169,7 +187,7 @@ public Mono send(Publisher messages, @Nullable SendOption exceptionHandler.accept(new SendContext<>(channel, message), e); } }) - .doOnError(e -> LOGGER.warn("Send failed with exception {}", e)) + .doOnError(e -> LOGGER.warn("Send failed with exception", e)) .doFinally(st -> closeHandler.accept(st, channel)) ).then(); } @@ -189,7 +207,7 @@ public Flux> sendWith } public Flux> sendWithTypedPublishConfirms( - Publisher messages, @Nullable SendOptions options) { + Publisher messages, SendOptions options) { var sendOptions = options == null ? new SendOptions() : options; final Mono currentChannelMono = getChannelMono(sendOptions); final BiConsumer closeHandler = getChannelCloseHandler(sendOptions); @@ -251,12 +269,12 @@ public Mono declare(QueueSpecification specification) { } return resourceManagementChannelMono.map(channel -> { - try { - return channel.asyncCompletableRpc(declare); - } catch (IOException e) { - throw new RabbitFluxException("Error during RPC call", e); - } - }).flatMap(Mono::fromCompletionStage) + try { + return channel.asyncCompletableRpc(declare); + } catch (IOException e) { + throw new RabbitFluxException("Error during RPC call", e); + } + }).flatMap(Mono::fromCompletionStage) .map(command -> (AMQP.Queue.DeclareOk) command.getMethod()) .publishOn(resourceManagementScheduler); } @@ -273,12 +291,12 @@ public Mono declare(ExchangeSpecification specification .build(); return resourceManagementChannelMono.map(channel -> { - try { - return channel.asyncCompletableRpc(declare); - } catch (IOException e) { - throw new RabbitFluxException("Error during RPC call", e); - } - }).flatMap(Mono::fromCompletionStage) + try { + return channel.asyncCompletableRpc(declare); + } catch (IOException e) { + throw new RabbitFluxException("Error during RPC call", e); + } + }).flatMap(Mono::fromCompletionStage) .map(command -> (AMQP.Exchange.DeclareOk) command.getMethod()) .publishOn(resourceManagementScheduler); } @@ -292,12 +310,12 @@ public Mono bind(BindingSpecification specification) { .build(); return resourceManagementChannelMono.map(channel -> { - try { - return channel.asyncCompletableRpc(binding); - } catch (IOException e) { - throw new RabbitFluxException("Error during RPC call", e); - } - }).flatMap(Mono::fromCompletionStage) + try { + return channel.asyncCompletableRpc(binding); + } catch (IOException e) { + throw new RabbitFluxException("Error during RPC call", e); + } + }).flatMap(Mono::fromCompletionStage) .map(command -> (AMQP.Queue.BindOk) command.getMethod()) .publishOn(resourceManagementScheduler); } @@ -311,12 +329,12 @@ public Mono unbind(BindingSpecification specification) { .build(); return resourceManagementChannelMono.map(channel -> { - try { - return channel.asyncCompletableRpc(unbinding); - } catch (IOException e) { - throw new RabbitFluxException("Error during RPC call", e); - } - }).flatMap(Mono::fromCompletionStage) + try { + return channel.asyncCompletableRpc(unbinding); + } catch (IOException e) { + throw new RabbitFluxException("Error during RPC call", e); + } + }).flatMap(Mono::fromCompletionStage) .map(command -> (AMQP.Queue.UnbindOk) command.getMethod()) .publishOn(resourceManagementScheduler); } @@ -331,14 +349,14 @@ public void close() { "Error while closing sender connection"); } if (this.privateConnectionSubscriptionScheduler) { - safelyExecute(LOGGER, () -> this.connectionSubscriptionScheduler.dispose(), + safelyExecute(LOGGER, this.connectionSubscriptionScheduler::dispose, "Error while disposing connection subscription scheduler"); } if (this.privateResourceManagementScheduler) { - safelyExecute(LOGGER, () -> this.resourceManagementScheduler.dispose(), + safelyExecute(LOGGER, this.resourceManagementScheduler::dispose, "Error while disposing resource management scheduler"); } - safelyExecute(LOGGER, () -> channelCloseThreadPool.shutdown(), + safelyExecute(LOGGER, channelCloseThreadPool::shutdown, "Error while closing channel closing thread pool"); } } @@ -448,7 +466,7 @@ static class PublishConfirmSubscriber implements final BiFunction propertiesProcessor; PublishConfirmSubscriber(Channel channel, Subscriber> subscriber, - SendOptions options) { + SendOptions options) { this.channel = channel; this.subscriber = subscriber; this.exceptionHandler = options.getExceptionHandler(); @@ -619,7 +637,7 @@ public void onComplete() { } } - private void handleError(Exception e, @Nullable OutboundMessageResult result) { + private void handleError(Exception e, OutboundMessageResult result) { LOGGER.error("error in publish confirm sending", e); boolean complete = checkComplete(e); firstException.compareAndSet(null, e); @@ -651,25 +669,4 @@ boolean checkComplete(T t) { } } - private static class ChannelCreationFunction implements Function { - @Override - public Channel apply(Connection connection) { - try { - return connection.createChannel(); - } catch (IOException e) { - throw new RabbitFluxException("Error while creating channel", e); - } - } - } - - private static class ChannelProxyCreationFunction implements Function { - @Override - public Channel apply(Connection connection) { - try { - return ChannelProxy.create(connection); - } catch (IOException e) { - throw new RabbitFluxException("Error while creating channel", e); - } - } - } } diff --git a/async/async-rabbit/src/main/java/reactor/rabbitmq/SenderOptions.java b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/SenderOptions.java similarity index 53% rename from async/async-rabbit/src/main/java/reactor/rabbitmq/SenderOptions.java rename to async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/SenderOptions.java index 576126e4..dda4ce14 100644 --- a/async/async-rabbit/src/main/java/reactor/rabbitmq/SenderOptions.java +++ b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/SenderOptions.java @@ -19,23 +19,19 @@ import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; +import lombok.Getter; import reactor.core.publisher.Mono; import reactor.core.publisher.SignalType; import reactor.core.scheduler.Scheduler; -import reactor.util.annotation.Nullable; import java.time.Duration; import java.util.function.BiConsumer; -import java.util.function.Function; -import java.util.function.Supplier; +import java.util.function.UnaryOperator; +@Getter public class SenderOptions { - private ConnectionFactory connectionFactory = ((Supplier) () -> { - ConnectionFactory connectionFactory = new ConnectionFactory(); - connectionFactory.useNio(); - return connectionFactory; - }).get(); + private ConnectionFactory connectionFactory = new ConnectionFactory(); private Mono connectionMono; private Mono channelMono; @@ -44,34 +40,20 @@ public class SenderOptions { private Scheduler connectionSubscriptionScheduler; private Mono resourceManagementChannelMono; private Utils.ExceptionFunction connectionSupplier; - private Function, Mono> connectionMonoConfigurator = cm -> cm; + private UnaryOperator> connectionMonoConfigurator = cm -> cm; private Duration connectionClosingTimeout = Duration.ofSeconds(30); - public ConnectionFactory getConnectionFactory() { - return connectionFactory; - } - public SenderOptions connectionFactory(ConnectionFactory connectionFactory) { this.connectionFactory = connectionFactory; return this; } - @Nullable - public Scheduler getResourceManagementScheduler() { - return resourceManagementScheduler; - } - - public SenderOptions resourceManagementScheduler(@Nullable Scheduler resourceManagementScheduler) { + public SenderOptions resourceManagementScheduler(Scheduler resourceManagementScheduler) { this.resourceManagementScheduler = resourceManagementScheduler; return this; } - @Nullable - public Scheduler getConnectionSubscriptionScheduler() { - return connectionSubscriptionScheduler; - } - - public SenderOptions connectionSubscriptionScheduler(@Nullable Scheduler connectionSubscriptionScheduler) { + public SenderOptions connectionSubscriptionScheduler(Scheduler connectionSubscriptionScheduler) { this.connectionSubscriptionScheduler = connectionSubscriptionScheduler; return this; } @@ -81,37 +63,22 @@ public SenderOptions connectionSupplier(Utils.ExceptionFunction function) { + Utils.ExceptionFunction function) { this.connectionSupplier = ignored -> function.apply(connectionFactory); return this; } - public SenderOptions connectionMono(@Nullable Mono connectionMono) { + public SenderOptions connectionMono(Mono connectionMono) { this.connectionMono = connectionMono; return this; } - @Nullable - public Mono getConnectionMono() { - return connectionMono; - } - - public SenderOptions channelMono(@Nullable Mono channelMono) { + public SenderOptions channelMono(Mono channelMono) { this.channelMono = channelMono; return this; } - @Nullable - public Mono getChannelMono() { - return channelMono; - } - - @Nullable - public BiConsumer getChannelCloseHandler() { - return channelCloseHandler; - } - - public SenderOptions channelCloseHandler(@Nullable BiConsumer channelCloseHandler) { + public SenderOptions channelCloseHandler(BiConsumer channelCloseHandler) { this.channelCloseHandler = channelCloseHandler; return this; } @@ -123,37 +90,19 @@ public SenderOptions channelPool(ChannelPool channelPool) { } public SenderOptions connectionMonoConfigurator( - Function, Mono> connectionMonoConfigurator) { + UnaryOperator> connectionMonoConfigurator) { this.connectionMonoConfigurator = connectionMonoConfigurator; return this; } - public SenderOptions resourceManagementChannelMono(@Nullable Mono resourceManagementChannelMono) { + public SenderOptions resourceManagementChannelMono(Mono resourceManagementChannelMono) { this.resourceManagementChannelMono = resourceManagementChannelMono; return this; } - @Nullable - public Mono getResourceManagementChannelMono() { - return resourceManagementChannelMono; - } - - @Nullable - public Utils.ExceptionFunction getConnectionSupplier() { - return connectionSupplier; - } - - public Function, Mono> getConnectionMonoConfigurator() { - return connectionMonoConfigurator; - } - - public SenderOptions connectionClosingTimeout(@Nullable Duration connectionClosingTimeout) { + public SenderOptions connectionClosingTimeout(Duration connectionClosingTimeout) { this.connectionClosingTimeout = connectionClosingTimeout; return this; } - @Nullable - public Duration getConnectionClosingTimeout() { - return connectionClosingTimeout; - } } diff --git a/async/async-rabbit/src/main/java/reactor/rabbitmq/SubscriberState.java b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/SubscriberState.java similarity index 100% rename from async/async-rabbit/src/main/java/reactor/rabbitmq/SubscriberState.java rename to async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/SubscriberState.java diff --git a/async/async-rabbit/src/main/java/reactor/rabbitmq/Utils.java b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/Utils.java similarity index 97% rename from async/async-rabbit/src/main/java/reactor/rabbitmq/Utils.java rename to async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/Utils.java index 58d3e16c..9e9ef7e6 100644 --- a/async/async-rabbit/src/main/java/reactor/rabbitmq/Utils.java +++ b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/Utils.java @@ -18,7 +18,6 @@ import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; -import com.rabbitmq.client.ConnectionFactory; import com.rabbitmq.client.RecoverableChannel; import com.rabbitmq.client.RecoverableConnection; import reactor.core.publisher.Mono; diff --git a/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/AcknowledgableDeliveryTest.java b/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/AcknowledgableDeliveryTest.java new file mode 100644 index 00000000..7224d412 --- /dev/null +++ b/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/AcknowledgableDeliveryTest.java @@ -0,0 +1,131 @@ +package reactor.rabbitmq; + +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Delivery; +import com.rabbitmq.client.Envelope; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiConsumer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.*; + +class AcknowledgableDeliveryTest { + + Channel channel; + Envelope envelope; + AMQP.BasicProperties props; + BiConsumer noopHandler; + + @BeforeEach + void setUp() { + channel = mock(Channel.class); + envelope = new Envelope(42L, false, "exch", "rk"); + props = new AMQP.BasicProperties(); + noopHandler = (ctx, ex) -> {}; + } + + @Test + void ackCallsBasicAck() throws Exception { + var delivery = new AcknowledgableDelivery( + new Delivery(envelope, props, "body".getBytes()), channel, noopHandler); + delivery.ack(); + verify(channel).basicAck(42L, false); + } + + @Test + void ackWithMultiple() throws Exception { + var delivery = new AcknowledgableDelivery( + new Delivery(envelope, props, "body".getBytes()), channel, noopHandler); + delivery.ack(true); + verify(channel).basicAck(42L, true); + } + + @Test + void ackIsIdempotent() throws Exception { + var delivery = new AcknowledgableDelivery( + new Delivery(envelope, props, "body".getBytes()), channel, noopHandler); + delivery.ack(); + delivery.ack(); + verify(channel, times(1)).basicAck(42L, false); + } + + @Test + void nackCallsBasicNack() throws Exception { + var delivery = new AcknowledgableDelivery( + new Delivery(envelope, props, "body".getBytes()), channel, noopHandler); + delivery.nack(true); + verify(channel).basicNack(42L, false, true); + } + + @Test + void nackWithMultipleAndRequeue() throws Exception { + var delivery = new AcknowledgableDelivery( + new Delivery(envelope, props, "body".getBytes()), channel, noopHandler); + delivery.nack(true, false); + verify(channel).basicNack(42L, true, false); + } + + @Test + void nackIsIdempotent() throws Exception { + var delivery = new AcknowledgableDelivery( + new Delivery(envelope, props, "body".getBytes()), channel, noopHandler); + delivery.nack(false); + delivery.nack(false); + verify(channel, times(1)).basicNack(42L, false, false); + } + + @Test + void ackThenNackIgnoresNack() throws Exception { + var delivery = new AcknowledgableDelivery( + new Delivery(envelope, props, "body".getBytes()), channel, noopHandler); + delivery.ack(); + delivery.nack(true); + verify(channel).basicAck(42L, false); + verify(channel, never()).basicNack(anyLong(), anyBoolean(), anyBoolean()); + } + + @Test + void ackFailureInvokesExceptionHandler() throws Exception { + doThrow(new IOException("channel closed")).when(channel).basicAck(anyLong(), anyBoolean()); + var handlerCalled = new AtomicBoolean(false); + BiConsumer handler = (ctx, ex) + -> handlerCalled.set(true); + + var delivery = new AcknowledgableDelivery( + new Delivery(envelope, props, "body".getBytes()), channel, handler); + delivery.ack(); + assertThat(handlerCalled).isTrue(); + } + + @Test + void nackFailureInvokesExceptionHandler() throws Exception { + doThrow(new IOException("channel closed")).when(channel).basicNack(anyLong(), anyBoolean(), anyBoolean()); + var handlerCalled = new AtomicBoolean(false); + BiConsumer handler = (ctx, ex) + -> handlerCalled.set(true); + + var delivery = new AcknowledgableDelivery( + new Delivery(envelope, props, "body".getBytes()), channel, handler); + delivery.nack(false); + assertThat(handlerCalled).isTrue(); + } + + @Test + void exceptionHandlerRethrowResetsAckFlag() throws Exception { + doThrow(new IOException("channel closed")).when(channel).basicAck(anyLong(), anyBoolean()); + BiConsumer handler = (ctx, ex) -> { + throw new RabbitFluxException(ex); + }; + + var delivery = new AcknowledgableDelivery( + new Delivery(envelope, props, "body".getBytes()), channel, handler); + assertThatThrownBy(delivery::ack).isInstanceOf(RabbitFluxException.class); + // After the handler rethrew, flag should be reset so ack can be retried + } +} diff --git a/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/BindingSpecificationTest.java b/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/BindingSpecificationTest.java new file mode 100644 index 00000000..ca9ea092 --- /dev/null +++ b/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/BindingSpecificationTest.java @@ -0,0 +1,68 @@ +package reactor.rabbitmq; + +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +class BindingSpecificationTest { + + @Test + void defaultValues() { + var spec = BindingSpecification.binding(); + assertThat(spec.getQueue()).isNull(); + assertThat(spec.getExchange()).isNull(); + assertThat(spec.getExchangeTo()).isNull(); + assertThat(spec.getRoutingKey()).isNull(); + assertThat(spec.getArguments()).isNull(); + } + + @Test + void queueBindingFactory() { + var spec = BindingSpecification.binding("my-exchange", "my-rk", "my-queue"); + assertThat(spec.getExchange()).isEqualTo("my-exchange"); + assertThat(spec.getRoutingKey()).isEqualTo("my-rk"); + assertThat(spec.getQueue()).isEqualTo("my-queue"); + } + + @Test + void queueBindingStaticAlias() { + var spec = BindingSpecification.queueBinding("exch", "rk", "q"); + assertThat(spec.getExchange()).isEqualTo("exch"); + assertThat(spec.getRoutingKey()).isEqualTo("rk"); + assertThat(spec.getQueue()).isEqualTo("q"); + } + + @Test + void exchangeBindingFactory() { + var spec = BindingSpecification.exchangeBinding("from-exch", "rk", "to-exch"); + assertThat(spec.getExchange()).isEqualTo("from-exch"); + assertThat(spec.getRoutingKey()).isEqualTo("rk"); + assertThat(spec.getExchangeTo()).isEqualTo("to-exch"); + assertThat(spec.getQueue()).isNull(); + } + + @Test + void fluentSetters() { + Map args = Map.of("x-match", "all"); + var spec = BindingSpecification.binding() + .queue("q") + .exchange("e") + .exchangeTo("e2") + .routingKey("rk") + .arguments(args); + + assertThat(spec.getQueue()).isEqualTo("q"); + assertThat(spec.getExchange()).isEqualTo("e"); + assertThat(spec.getExchangeTo()).isEqualTo("e2"); + assertThat(spec.getRoutingKey()).isEqualTo("rk"); + assertThat(spec.getArguments()).isEqualTo(args); + } + + @Test + void exchangeFromSetsExchange() { + var spec = BindingSpecification.binding().exchangeFrom("src-exch"); + assertThat(spec.getExchange()).isEqualTo("src-exch"); + } +} diff --git a/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/ChannelCloseHandlersTest.java b/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/ChannelCloseHandlersTest.java new file mode 100644 index 00000000..05d42c10 --- /dev/null +++ b/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/ChannelCloseHandlersTest.java @@ -0,0 +1,75 @@ +package reactor.rabbitmq; + +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.SignalType; + +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + + +class ChannelCloseHandlersTest { + + @Test + void closesOpenChannelOnOpenConnection() throws Exception { + var channel = mock(Channel.class); + var connection = mock(Connection.class); + when(channel.isOpen()).thenReturn(true); + when(channel.getConnection()).thenReturn(connection); + when(connection.isOpen()).thenReturn(true); + when(channel.getChannelNumber()).thenReturn(42); + + ChannelCloseHandlers.SENDER_CHANNEL_CLOSE_HANDLER_INSTANCE + .accept(SignalType.ON_COMPLETE, channel); + + verify(channel).close(); + } + + @Test + void doesNotCloseChannelWhenChannelAlreadyClosed() throws Exception { + var channel = mock(Channel.class); + when(channel.isOpen()).thenReturn(false); + when(channel.getChannelNumber()).thenReturn(1); + + ChannelCloseHandlers.SENDER_CHANNEL_CLOSE_HANDLER_INSTANCE + .accept(SignalType.ON_COMPLETE, channel); + + verify(channel, never()).close(); + } + + @Test + void doesNotCloseChannelWhenConnectionClosed() throws Exception { + var channel = mock(Channel.class); + var connection = mock(Connection.class); + when(channel.isOpen()).thenReturn(true); + when(channel.getConnection()).thenReturn(connection); + when(connection.isOpen()).thenReturn(false); + when(channel.getChannelNumber()).thenReturn(2); + + ChannelCloseHandlers.SENDER_CHANNEL_CLOSE_HANDLER_INSTANCE + .accept(SignalType.ON_COMPLETE, channel); + + verify(channel, never()).close(); + } + + @Test + void handlesCloseExceptionGracefully() throws Exception { + var channel = mock(Channel.class); + var connection = mock(Connection.class); + when(channel.isOpen()).thenReturn(true); + when(channel.getConnection()).thenReturn(connection); + when(connection.isOpen()).thenReturn(true); + when(channel.getChannelNumber()).thenReturn(3); + doThrow(new RuntimeException("close failed")).when(channel).close(); + + // Should not throw + Assertions.assertDoesNotThrow(() -> ChannelCloseHandlers.SENDER_CHANNEL_CLOSE_HANDLER_INSTANCE + .accept(SignalType.ON_ERROR, channel) + ); + } +} diff --git a/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/ChannelPoolFactoryTest.java b/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/ChannelPoolFactoryTest.java new file mode 100644 index 00000000..e311286f --- /dev/null +++ b/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/ChannelPoolFactoryTest.java @@ -0,0 +1,26 @@ +package reactor.rabbitmq; + +import com.rabbitmq.client.Connection; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +class ChannelPoolFactoryTest { + + @Test + void createChannelPoolWithDefaultOptions() { + var connection = mock(Connection.class); + var pool = ChannelPoolFactory.createChannelPool(Mono.just(connection)); + assertThat(pool).isInstanceOf(LazyChannelPool.class); + } + + @Test + void createChannelPoolWithCustomOptions() { + var connection = mock(Connection.class); + var options = new ChannelPoolOptions().maxCacheSize(10); + var pool = ChannelPoolFactory.createChannelPool(Mono.just(connection), options); + assertThat(pool).isInstanceOf(LazyChannelPool.class); + } +} diff --git a/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/ChannelPoolOptionsTest.java b/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/ChannelPoolOptionsTest.java new file mode 100644 index 00000000..173ecfe2 --- /dev/null +++ b/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/ChannelPoolOptionsTest.java @@ -0,0 +1,29 @@ +package reactor.rabbitmq; + +import org.junit.jupiter.api.Test; +import reactor.core.scheduler.Schedulers; + +import static org.assertj.core.api.Assertions.assertThat; + +class ChannelPoolOptionsTest { + + @Test + void defaultValues() { + var opts = new ChannelPoolOptions(); + assertThat(opts.getMaxCacheSize()).isNull(); + assertThat(opts.getSubscriptionScheduler()).isNull(); + } + + @Test + void maxCacheSize() { + var opts = new ChannelPoolOptions().maxCacheSize(10); + assertThat(opts.getMaxCacheSize()).isEqualTo(10); + } + + @Test + void subscriptionScheduler() { + var scheduler = Schedulers.boundedElastic(); + var opts = new ChannelPoolOptions().subscriptionScheduler(scheduler); + assertThat(opts.getSubscriptionScheduler()).isSameAs(scheduler); + } +} diff --git a/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/ChannelProxyTest.java b/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/ChannelProxyTest.java new file mode 100644 index 00000000..167babf4 --- /dev/null +++ b/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/ChannelProxyTest.java @@ -0,0 +1,79 @@ +package reactor.rabbitmq; + +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.Method; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.CompletableFuture; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +class ChannelProxyTest { + + @Test + void asyncCompletableRpcDelegatesToRealChannel() throws Exception { + var connection = mock(Connection.class); + var realChannel = mock(Channel.class); + when(connection.createChannel()).thenReturn(realChannel); + when(realChannel.isOpen()).thenReturn(true); + + var command = mock(com.rabbitmq.client.Command.class); + var future = CompletableFuture.completedFuture(command); + when(realChannel.asyncCompletableRpc(any(Method.class))).thenReturn(future); + + Channel proxy = ChannelProxy.create(connection); + var method = mock(Method.class); + var result = proxy.asyncCompletableRpc(method); + + assertThat(result.get()).isSameAs(command); + verify(realChannel).asyncCompletableRpc(method); + } + + @Test + void asyncCompletableRpcRecreatesChannelWhenClosed() throws Exception { + var connection = mock(Connection.class); + var closedChannel = mock(Channel.class); + var newChannel = mock(Channel.class); + when(connection.createChannel()).thenReturn(closedChannel, newChannel); + when(closedChannel.isOpen()).thenReturn(false); + when(newChannel.isOpen()).thenReturn(true); + + var command = mock(com.rabbitmq.client.Command.class); + var future = CompletableFuture.completedFuture(command); + when(newChannel.asyncCompletableRpc(any(Method.class))).thenReturn(future); + + Channel proxy = ChannelProxy.create(connection); + var method = mock(Method.class); + proxy.asyncCompletableRpc(method); + + // closedChannel was detected as not open, newChannel was created + verify(connection, times(2)).createChannel(); + verify(newChannel).asyncCompletableRpc(method); + } + + @Test + void toStringReturnsReadableRepresentation() throws Exception { + var connection = mock(Connection.class); + var realChannel = mock(Channel.class); + when(connection.createChannel()).thenReturn(realChannel); + + Channel proxy = ChannelProxy.create(connection); + assertThat(proxy.toString()).startsWith("ChannelProxy[delegate="); + } + + @Test + void unsupportedMethodThrowsException() throws Exception { + var connection = mock(Connection.class); + var realChannel = mock(Channel.class); + when(connection.createChannel()).thenReturn(realChannel); + + Channel proxy = ChannelProxy.create(connection); + assertThatThrownBy(() -> proxy.basicPublish("", "", null, new byte[0])) + .isInstanceOf(UnsupportedOperationException.class) + .hasMessageContaining("basicPublish"); + } +} diff --git a/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/ConsumeOptionsTest.java b/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/ConsumeOptionsTest.java new file mode 100644 index 00000000..e9e9541d --- /dev/null +++ b/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/ConsumeOptionsTest.java @@ -0,0 +1,72 @@ +package reactor.rabbitmq; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.FluxSink; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class ConsumeOptionsTest { + + @Test + void defaultValues() { + var opts = new ConsumeOptions(); + assertThat(opts.getQos()).isEqualTo(250); + assertThat(opts.getConsumerTag()).isEmpty(); + assertThat(opts.getOverflowStrategy()).isEqualTo(FluxSink.OverflowStrategy.BUFFER); + assertThat(opts.getHookBeforeEmitBiFunction()).isNotNull(); + assertThat(opts.getStopConsumingBiFunction()).isNotNull(); + assertThat(opts.getExceptionHandler()).isNotNull(); + assertThat(opts.getChannelCallback()).isNotNull(); + assertThat(opts.getArguments()).isEmpty(); + } + + @Test + void hookBeforeEmitDefaultReturnsTrue() { + var opts = new ConsumeOptions(); + assertThat(opts.getHookBeforeEmitBiFunction().test(1L, null)).isTrue(); + } + + @Test + void stopConsumingDefaultReturnsFalse() { + var opts = new ConsumeOptions(); + assertThat(opts.getStopConsumingBiFunction().test(1L, null)).isFalse(); + } + + @Test + void qosRejectsNegative() { + var opts = new ConsumeOptions(); + assertThatThrownBy(() -> opts.qos(-1)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void qosAcceptsZero() { + var opts = new ConsumeOptions().qos(0); + assertThat(opts.getQos()).isZero(); + } + + @Test + void consumerTagRejectsNull() { + var opts = new ConsumeOptions(); + assertThatThrownBy(() -> opts.consumerTag(null)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void fluentSetters() { + Map args = Map.of("key", "value"); + var opts = new ConsumeOptions() + .qos(100) + .consumerTag("tag") + .overflowStrategy(FluxSink.OverflowStrategy.DROP) + .arguments(args); + + assertThat(opts.getQos()).isEqualTo(100); + assertThat(opts.getConsumerTag()).isEqualTo("tag"); + assertThat(opts.getOverflowStrategy()).isEqualTo(FluxSink.OverflowStrategy.DROP); + assertThat(opts.getArguments()).isEqualTo(args); + } +} diff --git a/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/ExceptionHandlersTest.java b/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/ExceptionHandlersTest.java new file mode 100644 index 00000000..f4b0e671 --- /dev/null +++ b/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/ExceptionHandlersTest.java @@ -0,0 +1,130 @@ +package reactor.rabbitmq; + +import com.rabbitmq.client.ShutdownSignalException; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Predicate; + +import static java.time.Duration.ofMillis; +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ExceptionHandlersTest { + + @Test + void connectionRecoveryTriggeringHardErrorNotByApplication() { + Predicate predicate = new ExceptionHandlers.ConnectionRecoveryTriggeringPredicate(); + assertTrue(predicate.test(new ShutdownSignalException(true, false, null, null)), + "hard error, not triggered by application"); + } + + @Test + void connectionRecoveryTriggeringSoftErrorNotByApplication() { + Predicate predicate = new ExceptionHandlers.ConnectionRecoveryTriggeringPredicate(); + assertTrue(predicate.test(new ShutdownSignalException(false, false, null, null)), + "soft error, not triggered by application"); + } + + @Test + void connectionRecoveryNotTriggeringWhenInitiatedByApplication() { + Predicate predicate = new ExceptionHandlers.ConnectionRecoveryTriggeringPredicate(); + assertFalse(predicate.test(new ShutdownSignalException(false, true, null, null)), + "soft error, triggered by application"); + } + + @Test + void connectionRecoveryFalseForNonShutdownSignal() { + Predicate predicate = new ExceptionHandlers.ConnectionRecoveryTriggeringPredicate(); + assertFalse(predicate.test(new RuntimeException("not a shutdown signal"))); + } + + @Test + void connectionRecoveryPredicateConstantShouldWork() { + assertNotNull(ExceptionHandlers.CONNECTION_RECOVERY_PREDICATE); + assertFalse(ExceptionHandlers.CONNECTION_RECOVERY_PREDICATE.test(new RuntimeException())); + } + + @Test + void simpleRetryTemplateShouldThrowIfNotRetryable() { + var retryTemplate = new ExceptionHandlers.SimpleRetryTemplate( + ofMillis(100), ofMillis(10), ExceptionHandlers.CONNECTION_RECOVERY_PREDICATE); + var illegalArgEx = new IllegalArgumentException(); + assertThatThrownBy(() -> retryTemplate.retry(() -> null, illegalArgEx)) + .isInstanceOf(RabbitFluxException.class); + } + + @Test + void simpleRetryTemplateInvalidArguments() { + var timeout100 = ofMillis(100); + var timeout10 = ofMillis(10); + assertThatThrownBy(() -> new ExceptionHandlers.SimpleRetryTemplate(null, timeout10, e -> true)) + .isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> new ExceptionHandlers.SimpleRetryTemplate(timeout100, null, e -> true)) + .isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> new ExceptionHandlers.SimpleRetryTemplate(timeout10, timeout100, e -> true)) + .isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> new ExceptionHandlers.SimpleRetryTemplate(timeout100, timeout10, null)) + .isInstanceOf(NullPointerException.class); + } + + @Test + void retrySucceeds() { + var handler = new ExceptionHandlers.RetrySendingExceptionHandler( + ofMillis(200), ofMillis(10), e -> true); + AtomicLong counter = new AtomicLong(0); + handler.accept(sendContext(() -> { + if (counter.incrementAndGet() < 3) { + throw new Exception(); + } + return null; + }), new Exception()); + assertEquals(3, counter.get()); + } + + @Test + void retryTimeoutIsReachedAndDoesNotThrowWhenNotConfigured() { + var handler = new ExceptionHandlers.RetrySendingExceptionHandler( + ofMillis(50), ofMillis(10), e -> true, false); + var ctx = sendContext(() -> { throw new Exception(); }); + assertThatNoException().isThrownBy(() -> handler.accept(ctx, new Exception())); + } + + @Test + void retryTimeoutThrowsWhenConfigured() { + var handler = new ExceptionHandlers.RetrySendingExceptionHandler( + ofMillis(50), ofMillis(10), e -> true, true); + var ctx = sendContext(() -> { throw new Exception(); }); + var triggerEx = new Exception(); + assertThatThrownBy(() -> handler.accept(ctx, triggerEx)) + .isInstanceOf(RabbitFluxRetryTimeoutException.class); + } + + @Test + void retryAcknowledgmentHandlerCallsAckOrNack() { + var handler = new ExceptionHandlers.RetryAcknowledgmentExceptionHandler( + ofMillis(200), ofMillis(10), e -> true); + AtomicLong counter = new AtomicLong(0); + var context = new Receiver.AcknowledgmentContext(null, delivery -> { + if (counter.incrementAndGet() < 2) { + throw new RuntimeException("transient"); + } + }); + handler.accept(context, new RuntimeException("initial")); + assertEquals(2, counter.get()); + } + + private Sender.SendContext sendContext(Callable callable) { + return new Sender.SendContext<>(null, null) { + @Override + public void publish() throws Exception { + callable.call(); + } + }; + } +} diff --git a/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/ExchangeSpecificationTest.java b/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/ExchangeSpecificationTest.java new file mode 100644 index 00000000..302ab5d5 --- /dev/null +++ b/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/ExchangeSpecificationTest.java @@ -0,0 +1,50 @@ +package reactor.rabbitmq; + +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +class ExchangeSpecificationTest { + + @Test + void defaultValues() { + var spec = ExchangeSpecification.exchange(); + assertThat(spec.getName()).isNull(); + assertThat(spec.getType()).isEqualTo("direct"); + assertThat(spec.isDurable()).isFalse(); + assertThat(spec.isAutoDelete()).isFalse(); + assertThat(spec.isInternal()).isFalse(); + assertThat(spec.isPassive()).isFalse(); + assertThat(spec.getArguments()).isNull(); + } + + @Test + void factoryWithName() { + var spec = ExchangeSpecification.exchange("my-exchange"); + assertThat(spec.getName()).isEqualTo("my-exchange"); + assertThat(spec.getType()).isEqualTo("direct"); + } + + @Test + void fluentSetters() { + Map args = Map.of("x-expires", 60000); + var spec = ExchangeSpecification.exchange() + .name("test") + .type("fanout") + .durable(true) + .autoDelete(true) + .internal(true) + .passive(true) + .arguments(args); + + assertThat(spec.getName()).isEqualTo("test"); + assertThat(spec.getType()).isEqualTo("fanout"); + assertThat(spec.isDurable()).isTrue(); + assertThat(spec.isAutoDelete()).isTrue(); + assertThat(spec.isInternal()).isTrue(); + assertThat(spec.isPassive()).isTrue(); + assertThat(spec.getArguments()).isEqualTo(args); + } +} diff --git a/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/HelpersTest.java b/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/HelpersTest.java new file mode 100644 index 00000000..19b44633 --- /dev/null +++ b/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/HelpersTest.java @@ -0,0 +1,33 @@ +package reactor.rabbitmq; + +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; + +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +class HelpersTest { + + @Test + void safelyExecuteRunsAction() { + var logger = mock(Logger.class); + var ran = new AtomicBoolean(false); + Helpers.safelyExecute(logger, () -> ran.set(true), "test"); + assertThat(ran).isTrue(); + verify(logger, never()).warn(anyString(), anyString(), anyString()); + } + + @Test + void safelyExecuteLogsExceptionAndDoesNotThrow() { + var logger = mock(Logger.class); + Helpers.safelyExecute(logger, () -> { + throw new RuntimeException("boom"); + }, "action failed"); + verify(logger).warn("{}: {}", "action failed", "boom"); + } +} diff --git a/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/LazyChannelPoolTest.java b/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/LazyChannelPoolTest.java new file mode 100644 index 00000000..3142991e --- /dev/null +++ b/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/LazyChannelPoolTest.java @@ -0,0 +1,172 @@ +package reactor.rabbitmq; + +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; +import reactor.test.scheduler.VirtualTimeScheduler; + +import java.io.IOException; +import java.time.Duration; + +import static java.time.Duration.ofSeconds; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.nullable; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class LazyChannelPoolTest { + + LazyChannelPool lazyChannelPool; + Connection connection; + Channel channel1, channel2, channel3; + + @BeforeEach + void setUp() throws IOException { + connection = mock(Connection.class); + when(connection.isOpen()).thenReturn(true); + channel1 = channel(1); + channel2 = channel(2); + channel3 = channel(3); + when(connection.createChannel()).thenReturn(channel1, channel2, channel3); + } + + @Test + void testChannelPoolLazyInitialization() throws Exception { + var options = new ChannelPoolOptions() + .maxCacheSize(2) + .subscriptionScheduler(VirtualTimeScheduler.create()); + lazyChannelPool = new LazyChannelPool(Mono.just(connection), options); + + StepVerifier.withVirtualTime(() -> + Mono.when( + useChannelFromStartUntil(ofSeconds(1)), + useChannelBetween(ofSeconds(2), ofSeconds(3)) + )) + .expectSubscription() + .thenAwait(ofSeconds(3)) + .verifyComplete(); + + // channel1 used twice (created at 0, pooled at 1, reused at 2) + verifyBasicPublish(channel1, 2); + verifyBasicPublishNever(channel2); + verify(channel1, never()).close(); + + lazyChannelPool.close(); + + verify(channel1).close(); + verify(channel2, never()).close(); + verify(channel1).clearConfirmListeners(); + verify(channel2, never()).clearConfirmListeners(); + } + + @Test + void testChannelPoolExceedsMaxPoolSize() throws Exception { + var options = new ChannelPoolOptions() + .maxCacheSize(2) + .subscriptionScheduler(VirtualTimeScheduler.create()); + lazyChannelPool = new LazyChannelPool(Mono.just(connection), options); + + StepVerifier.withVirtualTime(() -> + Mono.when( + useChannelBetween(ofSeconds(1), ofSeconds(4)), + useChannelBetween(ofSeconds(2), ofSeconds(5)), + useChannelBetween(ofSeconds(3), ofSeconds(6)) + )) + .expectSubscription() + .thenAwait(ofSeconds(6)) + .verifyComplete(); + + verifyBasicPublishOnce(channel1); + verifyBasicPublishOnce(channel2); + verifyBasicPublishOnce(channel3); + verify(channel1, never()).close(); + verify(channel2, never()).close(); + verify(channel3).close(); // exceeded pool capacity + + lazyChannelPool.close(); + + verify(channel1).close(); + verify(channel2).close(); + } + + @Test + void testChannelPool() throws Exception { + var options = new ChannelPoolOptions() + .maxCacheSize(1) + .subscriptionScheduler(VirtualTimeScheduler.create()); + lazyChannelPool = new LazyChannelPool(Mono.just(connection), options); + + StepVerifier.withVirtualTime(() -> + Mono.when( + useChannelFromStartUntil(ofSeconds(3)), + useChannelBetween(ofSeconds(1), ofSeconds(2)), + useChannelBetween(ofSeconds(4), ofSeconds(5)) + )) + .expectSubscription() + .thenAwait(ofSeconds(5)) + .verifyComplete(); + + verifyBasicPublishOnce(channel1); + verifyBasicPublish(channel2, 2); + verify(channel1).close(); // exceeded pool at second 3 + verify(channel2, never()).close(); + verify(channel1, never()).clearConfirmListeners(); + verify(channel2).clearConfirmListeners(); + + lazyChannelPool.close(); + verify(channel2).close(); + } + + private Mono useChannelFromStartUntil(Duration until) { + return useChannelBetween(Duration.ZERO, until); + } + + private Mono useChannelBetween(Duration from, Duration to) { + return Mono.delay(from) + .then(lazyChannelPool.getChannelMono()) + .flatMap(channel -> + Mono.just(1) + .doOnNext(i -> { + try { + channel.basicPublish("", "", null, "".getBytes()); + } catch (IOException e) { + throw new RuntimeException(e); + } + }) + .delayElement(to.minus(from)) + .doFinally(signalType -> + lazyChannelPool.getChannelCloseHandler().accept(signalType, channel) + ) + ) + .then(); + } + + private void verifyBasicPublishNever(Channel channel) throws Exception { + verifyBasicPublish(channel, 0); + } + + private void verifyBasicPublishOnce(Channel channel) throws Exception { + verifyBasicPublish(channel, 1); + } + + private void verifyBasicPublish(Channel channel, int times) throws Exception { + verify(channel, times(times)).basicPublish(anyString(), anyString(), + nullable(AMQP.BasicProperties.class), any(byte[].class)); + } + + private Channel channel(int channelNumber) { + Channel channel = mock(Channel.class); + when(channel.getChannelNumber()).thenReturn(channelNumber); + when(channel.isOpen()).thenReturn(true); + when(channel.getConnection()).thenReturn(connection); + return channel; + } +} diff --git a/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/OutboundMessageResultTest.java b/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/OutboundMessageResultTest.java new file mode 100644 index 00000000..e3440b4b --- /dev/null +++ b/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/OutboundMessageResultTest.java @@ -0,0 +1,34 @@ +package reactor.rabbitmq; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class OutboundMessageResultTest { + + @Test + void twoArgConstructorDefaultsReturnedToFalse() { + var msg = new OutboundMessage("exch", "rk", "body".getBytes()); + var result = new OutboundMessageResult<>(msg, true); + assertThat(result.outboundMessage()).isSameAs(msg); + assertThat(result.ack()).isTrue(); + assertThat(result.returned()).isFalse(); + } + + @Test + void threeArgConstructor() { + var msg = new OutboundMessage("exch", "rk", "body".getBytes()); + var result = new OutboundMessageResult<>(msg, false, true); + assertThat(result.outboundMessage()).isSameAs(msg); + assertThat(result.ack()).isFalse(); + assertThat(result.returned()).isTrue(); + } + + @Test + void toStringContainsAckAndReturned() { + var msg = new OutboundMessage("exch", "rk", "body".getBytes()); + var result = new OutboundMessageResult<>(msg, true, true); + var str = result.toString(); + assertThat(str).contains("ack=true").contains("returned=true"); + } +} diff --git a/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/OutboundMessageTest.java b/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/OutboundMessageTest.java new file mode 100644 index 00000000..9b6a728b --- /dev/null +++ b/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/OutboundMessageTest.java @@ -0,0 +1,44 @@ +package reactor.rabbitmq; + +import com.rabbitmq.client.AMQP; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class OutboundMessageTest { + + @Test + void constructorWithoutProperties() { + var msg = new OutboundMessage("exch", "rk", "body".getBytes()); + assertThat(msg.getExchange()).isEqualTo("exch"); + assertThat(msg.getRoutingKey()).isEqualTo("rk"); + assertThat(msg.getProperties()).isNull(); + assertThat(msg.getBody()).isEqualTo("body".getBytes()); + assertThat(msg.isPublished()).isFalse(); + } + + @Test + void constructorWithProperties() { + var props = new AMQP.BasicProperties.Builder().contentType("text/plain").build(); + var msg = new OutboundMessage("exch", "rk", props, "body".getBytes()); + assertThat(msg.getExchange()).isEqualTo("exch"); + assertThat(msg.getRoutingKey()).isEqualTo("rk"); + assertThat(msg.getProperties()).isSameAs(props); + assertThat(msg.getBody()).isEqualTo("body".getBytes()); + } + + @Test + void publishedFlagIsSetByPublished() { + var msg = new OutboundMessage("exch", "rk", new byte[0]); + assertThat(msg.isPublished()).isFalse(); + msg.published(); + assertThat(msg.isPublished()).isTrue(); + } + + @Test + void toStringContainsFieldValues() { + var msg = new OutboundMessage("exch", "rk", "hi".getBytes()); + var str = msg.toString(); + assertThat(str).contains("exch").contains("rk"); + } +} diff --git a/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/QueueSpecificationTest.java b/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/QueueSpecificationTest.java new file mode 100644 index 00000000..b4448ed5 --- /dev/null +++ b/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/QueueSpecificationTest.java @@ -0,0 +1,143 @@ +package reactor.rabbitmq; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.*; + +class QueueSpecificationTest { + + @Nested + class NullName { + @Test + void nullNameShouldReturnANonDurableQueue() { + assertFalse(QueueSpecification.queue().isDurable()); + } + + @Test + void passingNullNameShouldReturnANonDurableQueue() { + assertFalse(QueueSpecification.queue(null).isDurable()); + } + + @Test + void nullNameShouldNotAbleToConfigureDurableToTrue() { + var spec = QueueSpecification.queue(); + assertThatThrownBy(() -> spec.durable(true)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void nullNameShouldKeepDurableWhenConfigureDurableToFalse() { + assertFalse(QueueSpecification.queue().durable(false).isDurable()); + } + + @Test + void passingNullNameShouldReturnAnExclusiveQueue() { + assertTrue(QueueSpecification.queue().isExclusive()); + } + + @Test + void nullNameShouldNotAbleToConfigureExclusiveToFalse() { + var spec = QueueSpecification.queue(); + assertThatThrownBy(() -> spec.exclusive(false)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void nullNameShouldKeepExclusiveWhenConfigureExclusiveToTrue() { + assertTrue(QueueSpecification.queue().exclusive(true).isExclusive()); + } + + @Test + void passingNullNameShouldReturnAnAutoDeleteQueue() { + assertTrue(QueueSpecification.queue().isAutoDelete()); + } + + @Test + void nullNameShouldNotAbleToConfigureAutoDeleteToFalse() { + var spec = QueueSpecification.queue(); + assertThatThrownBy(() -> spec.autoDelete(false)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void nullNameShouldKeepAutoDeleteWhenConfigureAutoDeleteToTrue() { + assertTrue(QueueSpecification.queue().autoDelete(true).isAutoDelete()); + } + + @Test + void nullNameShouldNotAbleToConfigurePassiveToTrue() { + var spec = QueueSpecification.queue(); + assertThatThrownBy(() -> spec.passive(true)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void passingANonNullNameAfterShouldReturnAConfigurableQueueSpecification() { + var spec = QueueSpecification.queue() + .name("not-null-anymore") + .durable(true) + .exclusive(false) + .autoDelete(false) + .passive(false); + + assertEquals("not-null-anymore", spec.getName()); + assertTrue(spec.isDurable()); + assertFalse(spec.isAutoDelete()); + assertFalse(spec.isExclusive()); + assertFalse(spec.isPassive()); + } + } + + @Nested + class NotNullName { + @Test + void queueSpecificationShouldReturnCorrespondingProperties() { + var spec = QueueSpecification.queue("my-queue") + .durable(false) + .autoDelete(false) + .exclusive(false) + .passive(true); + + assertEquals("my-queue", spec.getName()); + assertFalse(spec.isDurable()); + assertFalse(spec.isAutoDelete()); + assertFalse(spec.isExclusive()); + assertTrue(spec.isPassive()); + assertNull(spec.getArguments()); + } + + @Test + void queueSpecificationShouldReturnCorrespondingPropertiesWhenEmptyName() { + var spec = QueueSpecification.queue("") + .durable(false) + .autoDelete(false) + .exclusive(false) + .passive(false); + + assertEquals("", spec.getName()); + assertFalse(spec.isDurable()); + assertFalse(spec.isAutoDelete()); + assertFalse(spec.isExclusive()); + assertFalse(spec.isPassive()); + assertNull(spec.getArguments()); + } + + @Test + void queueSpecificationShouldKeepArgumentsWhenNullName() { + var spec = QueueSpecification.queue("") + .arguments(Collections.singletonMap("x-max-length", 1000)) + .name(null); + assertNull(spec.getName()); + assertFalse(spec.isDurable()); + assertTrue(spec.isAutoDelete()); + assertTrue(spec.isExclusive()); + assertFalse(spec.isPassive()); + assertThat(spec.getArguments()).isNotNull().hasSize(1).containsKey("x-max-length"); + } + } +} diff --git a/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/RabbitFluxExceptionTest.java b/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/RabbitFluxExceptionTest.java new file mode 100644 index 00000000..b13586a0 --- /dev/null +++ b/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/RabbitFluxExceptionTest.java @@ -0,0 +1,45 @@ +package reactor.rabbitmq; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class RabbitFluxExceptionTest { + + @Test + void noArgConstructor() { + var ex = new RabbitFluxException(); + assertThat(ex.getMessage()).isNull(); + assertThat(ex.getCause()).isNull(); + } + + @Test + void messageConstructor() { + var ex = new RabbitFluxException("error"); + assertThat(ex.getMessage()).isEqualTo("error"); + } + + @Test + void messageCauseConstructor() { + var cause = new RuntimeException("root"); + var ex = new RabbitFluxException("error", cause); + assertThat(ex.getMessage()).isEqualTo("error"); + assertThat(ex.getCause()).isSameAs(cause); + } + + @Test + void causeConstructor() { + var cause = new RuntimeException("root"); + var ex = new RabbitFluxException(cause); + assertThat(ex.getCause()).isSameAs(cause); + } + + @Test + void retryTimeoutExceptionExtends() { + var cause = new RuntimeException("timeout"); + var ex = new RabbitFluxRetryTimeoutException("timed out", cause); + assertThat(ex).isInstanceOf(RabbitFluxException.class); + assertThat(ex.getMessage()).isEqualTo("timed out"); + assertThat(ex.getCause()).isSameAs(cause); + } +} diff --git a/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/ReceiverOptionsTest.java b/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/ReceiverOptionsTest.java new file mode 100644 index 00000000..0e758063 --- /dev/null +++ b/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/ReceiverOptionsTest.java @@ -0,0 +1,84 @@ +package reactor.rabbitmq; + +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +import java.time.Duration; +import java.util.function.UnaryOperator; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +class ReceiverOptionsTest { + + @Test + void defaultValues() { + var opts = new ReceiverOptions(); + assertThat(opts.getConnectionFactory()).isNotNull(); + assertThat(opts.getConnectionMono()).isNull(); + assertThat(opts.getConnectionSubscriptionScheduler()).isNull(); + assertThat(opts.getConnectionSupplier()).isNull(); + assertThat(opts.getConnectionMonoConfigurator()).isNotNull(); + assertThat(opts.getConnectionClosingTimeout()).isEqualTo(Duration.ofSeconds(30)); + } + + @Test + void connectionFactory() { + var factory = new ConnectionFactory(); + var opts = new ReceiverOptions().connectionFactory(factory); + assertThat(opts.getConnectionFactory()).isSameAs(factory); + } + + @Test + void connectionMono() { + Mono mono = Mono.just(mock(Connection.class)); + var opts = new ReceiverOptions().connectionMono(mono); + assertThat(opts.getConnectionMono()).isSameAs(mono); + } + + @Test + void connectionSubscriptionScheduler() { + var scheduler = Schedulers.boundedElastic(); + var opts = new ReceiverOptions().connectionSubscriptionScheduler(scheduler); + assertThat(opts.getConnectionSubscriptionScheduler()).isSameAs(scheduler); + } + + @Test + void connectionSupplier() throws Exception { + var connection = mock(Connection.class); + var opts = new ReceiverOptions().connectionSupplier(cf -> connection); + assertThat(opts.getConnectionSupplier()).isNotNull(); + assertThat(opts.getConnectionSupplier().apply(null)).isSameAs(connection); + } + + @Test + void connectionSupplierWithExplicitFactory() throws Exception { + var connection = mock(Connection.class); + var factory = new ConnectionFactory(); + var opts = new ReceiverOptions().connectionSupplier(factory, cf -> connection); + assertThat(opts.getConnectionSupplier().apply(null)).isSameAs(connection); + } + + @Test + void connectionMonoConfigurator() { + UnaryOperator> configurator = Mono::cache; + var opts = new ReceiverOptions().connectionMonoConfigurator(configurator); + assertThat(opts.getConnectionMonoConfigurator()).isSameAs(configurator); + } + + @Test + void connectionMonoConfiguratorDefaultIsIdentity() { + var opts = new ReceiverOptions(); + Mono mono = Mono.just(mock(Connection.class)); + assertThat(opts.getConnectionMonoConfigurator().apply(mono)).isSameAs(mono); + } + + @Test + void connectionClosingTimeout() { + var opts = new ReceiverOptions().connectionClosingTimeout(Duration.ofMillis(500)); + assertThat(opts.getConnectionClosingTimeout()).isEqualTo(Duration.ofMillis(500)); + } +} diff --git a/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/ReceiverTest.java b/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/ReceiverTest.java new file mode 100644 index 00000000..a46cde44 --- /dev/null +++ b/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/ReceiverTest.java @@ -0,0 +1,191 @@ +package reactor.rabbitmq; + +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.DeliverCallback; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; + +import java.io.IOException; +import java.time.Duration; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyMap; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class ReceiverTest { + + Connection connection; + Channel channel; + Receiver receiver; + + @BeforeEach + void setUp() throws Exception { + connection = mock(Connection.class); + channel = mock(Channel.class); + when(connection.createChannel()).thenReturn(channel); + when(channel.isOpen()).thenReturn(true); + when(channel.getConnection()).thenReturn(connection); + } + + @AfterEach + void tearDown() { + if (receiver != null) { + receiver.close(); + } + } + + @Test + void consumeManualAckRegistersConsumer() throws Exception { + AtomicReference deliverRef = new AtomicReference<>(); + when(channel.basicConsume(anyString(), eq(false), anyString(), eq(false), eq(false), + anyMap(), any(DeliverCallback.class), any(com.rabbitmq.client.CancelCallback.class))) + .thenAnswer(invocation -> { + deliverRef.set(invocation.getArgument(6)); + return "consumer-tag-1"; + }); + + receiver = new Receiver(new ReceiverOptions().connectionMono(Mono.just(connection))); + + var latch = new CountDownLatch(1); + receiver.consumeManualAck("test-queue") + .subscribe(delivery -> { + delivery.ack(); + latch.countDown(); + }); + + // Simulate delivery + if (deliverRef.get() != null) { + var envelope = new com.rabbitmq.client.Envelope(1L, false, "exch", "rk"); + var props = new com.rabbitmq.client.AMQP.BasicProperties(); + deliverRef.get().handle("consumer-tag-1", + new com.rabbitmq.client.Delivery(envelope, props, "hello".getBytes())); + } + + assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue(); + verify(channel).basicAck(1L, false); + } + + @Test + void consumeAutoAckAutoAcksMessages() throws Exception { + AtomicReference deliverRef = new AtomicReference<>(); + when(channel.basicConsume(anyString(), eq(false), anyString(), eq(false), eq(false), + anyMap(), any(DeliverCallback.class), any(com.rabbitmq.client.CancelCallback.class))) + .thenAnswer(invocation -> { + deliverRef.set(invocation.getArgument(6)); + return "consumer-tag-2"; + }); + + receiver = new Receiver(new ReceiverOptions().connectionMono(Mono.just(connection))); + + var latch = new CountDownLatch(1); + receiver.consumeAutoAck("test-queue") + .subscribe(delivery -> latch.countDown()); + + if (deliverRef.get() != null) { + var envelope = new com.rabbitmq.client.Envelope(1L, false, "exch", "rk"); + var props = new com.rabbitmq.client.AMQP.BasicProperties(); + deliverRef.get().handle("consumer-tag-2", + new com.rabbitmq.client.Delivery(envelope, props, "hello".getBytes())); + } + + assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue(); + verify(channel).basicAck(1L, false); + } + + @Test + void consumeManualAckEmitsErrorOnChannelCreationFailure() throws Exception { + when(connection.createChannel()).thenThrow(new IOException("channel creation failed")); + receiver = new Receiver(new ReceiverOptions().connectionMono(Mono.just(connection))); + var publisher = receiver.consumeManualAck("test-queue"); + var timeout = Duration.ofSeconds(5); + assertThatThrownBy(() -> publisher.blockLast(timeout)) + .isInstanceOf(RabbitFluxException.class); + } + + @Test + void closeIsIdempotent() { + receiver = new Receiver(new ReceiverOptions().connectionMono(Mono.just(connection))); + assertThatNoException().isThrownBy(() -> { + receiver.close(); + receiver.close(); + }); + receiver = null; // prevent double close in tearDown + } + + @Test + void connectionClosedWithTimeout() throws Exception { + Connection c = mock(Connection.class); + Channel ch = mock(Channel.class); + when(c.createChannel()).thenReturn(ch); + when(ch.isOpen()).thenReturn(true); + when(ch.getConnection()).thenReturn(c); + var connectionLatch = new CountDownLatch(1); + when(ch.basicConsume(anyString(), eq(false), anyString(), eq(false), eq(false), + anyMap(), any(com.rabbitmq.client.DeliverCallback.class), any(com.rabbitmq.client.CancelCallback.class) + )) + .thenAnswer(inv -> { + connectionLatch.countDown(); + return "tag"; + }); + + var options = new ReceiverOptions() + .connectionSupplier(cf -> c) + .connectionClosingTimeout(Duration.ofMillis(5000)); + + receiver = new Receiver(options); + // Trigger connection establishment by subscribing + receiver.consumeManualAck("test-queue").subscribe().dispose(); + assertThat(connectionLatch.await(5, TimeUnit.SECONDS)).isTrue(); + receiver.close(); + + verify(c, times(1)).close(5000); + receiver = null; + } + + @Test + void channelCallbackIsCalled() throws Exception { + AtomicReference deliverRef = new AtomicReference<>(); + when(channel.basicConsume(anyString(), eq(false), anyString(), eq(false), eq(false), + anyMap(), any(DeliverCallback.class), any(com.rabbitmq.client.CancelCallback.class))) + .thenAnswer(invocation -> { + deliverRef.set(invocation.getArgument(6)); + return "consumer-tag-3"; + }); + + receiver = new Receiver(new ReceiverOptions().connectionMono(Mono.just(connection))); + + var callbackCalled = new CountDownLatch(1); + var consumeOptions = new ConsumeOptions().channelCallback(ch -> callbackCalled.countDown()); + + var latch = new CountDownLatch(1); + receiver.consumeManualAck("test-queue", consumeOptions) + .subscribe(delivery -> { + delivery.ack(); + latch.countDown(); + }); + + if (deliverRef.get() != null) { + var envelope = new com.rabbitmq.client.Envelope(1L, false, "exch", "rk"); + var props = new com.rabbitmq.client.AMQP.BasicProperties(); + deliverRef.get().handle("consumer-tag-3", + new com.rabbitmq.client.Delivery(envelope, props, "hello".getBytes())); + } + + assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(callbackCalled.await(5, TimeUnit.SECONDS)).isTrue(); + } +} diff --git a/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/SendOptionsTest.java b/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/SendOptionsTest.java new file mode 100644 index 00000000..295ef6af --- /dev/null +++ b/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/SendOptionsTest.java @@ -0,0 +1,73 @@ +package reactor.rabbitmq; + +import com.rabbitmq.client.Channel; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import reactor.core.publisher.SignalType; +import reactor.core.scheduler.Schedulers; + +import java.util.function.BiConsumer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +class SendOptionsTest { + + @Test + void defaultValues() { + var opts = new SendOptions(); + assertThat(opts.getExceptionHandler()).isNotNull(); + assertThat(opts.getMaxInFlight()).isNull(); + assertThat(opts.isTrackReturned()).isFalse(); + assertThat(opts.getScheduler()).isEqualTo(Schedulers.immediate()); + assertThat(opts.getChannelMono()).isNull(); + assertThat(opts.getChannelCloseHandler()).isNull(); + } + + @Test + void trackReturned() { + var opts = new SendOptions().trackReturned(true); + assertThat(opts.isTrackReturned()).isTrue(); + } + + @Test + void maxInFlight() { + var opts = new SendOptions().maxInFlight(256); + assertThat(opts.getMaxInFlight()).isEqualTo(256); + } + + @Test + void maxInFlightWithScheduler() { + var scheduler = Schedulers.boundedElastic(); + var opts = new SendOptions().maxInFlight(128, scheduler); + assertThat(opts.getMaxInFlight()).isEqualTo(128); + assertThat(opts.getScheduler()).isSameAs(scheduler); + } + + @Test + void channelMono() { + Mono mono = Mono.just(mock(Channel.class)); + var opts = new SendOptions().channelMono(mono); + assertThat(opts.getChannelMono()).isSameAs(mono); + } + + @Test + void channelCloseHandler() { + BiConsumer handler = (s, c) -> {}; + var opts = new SendOptions().channelCloseHandler(handler); + assertThat(opts.getChannelCloseHandler()).isSameAs(handler); + } + + @Test + void channelPoolSetsMonoAndHandler() { + var pool = mock(ChannelPool.class); + Mono mono = Mono.just(mock(Channel.class)); + BiConsumer handler = (s, c) -> {}; + doReturn(mono).when(pool).getChannelMono(); + doReturn(handler).when(pool).getChannelCloseHandler(); + + var opts = new SendOptions().channelPool(pool); + assertThat(opts.getChannelMono()).isSameAs(mono); + assertThat(opts.getChannelCloseHandler()).isSameAs(handler); + } +} diff --git a/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/SenderOptionsTest.java b/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/SenderOptionsTest.java new file mode 100644 index 00000000..54d3cdfa --- /dev/null +++ b/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/SenderOptionsTest.java @@ -0,0 +1,132 @@ +package reactor.rabbitmq; + +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ConnectionFactory; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import reactor.core.publisher.SignalType; +import reactor.core.scheduler.Schedulers; + +import java.time.Duration; +import java.util.function.BiConsumer; +import java.util.function.UnaryOperator; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +class SenderOptionsTest { + + @Test + void defaultValues() { + var opts = new SenderOptions(); + assertThat(opts.getConnectionFactory()).isNotNull(); + assertThat(opts.getConnectionMono()).isNull(); + assertThat(opts.getChannelMono()).isNull(); + assertThat(opts.getChannelCloseHandler()).isNull(); + assertThat(opts.getResourceManagementScheduler()).isNull(); + assertThat(opts.getConnectionSubscriptionScheduler()).isNull(); + assertThat(opts.getResourceManagementChannelMono()).isNull(); + assertThat(opts.getConnectionSupplier()).isNull(); + assertThat(opts.getConnectionMonoConfigurator()).isNotNull(); + assertThat(opts.getConnectionClosingTimeout()).isEqualTo(Duration.ofSeconds(30)); + } + + @Test + void connectionFactory() { + var factory = new ConnectionFactory(); + var opts = new SenderOptions().connectionFactory(factory); + assertThat(opts.getConnectionFactory()).isSameAs(factory); + } + + @Test + void connectionMono() { + Mono mono = Mono.just(mock(Connection.class)); + var opts = new SenderOptions().connectionMono(mono); + assertThat(opts.getConnectionMono()).isSameAs(mono); + } + + @Test + void channelMono() { + Mono mono = Mono.just(mock(Channel.class)); + var opts = new SenderOptions().channelMono(mono); + assertThat(opts.getChannelMono()).isSameAs(mono); + } + + @Test + void channelCloseHandler() { + BiConsumer handler = (s, c) -> {}; + var opts = new SenderOptions().channelCloseHandler(handler); + assertThat(opts.getChannelCloseHandler()).isSameAs(handler); + } + + @Test + void resourceManagementScheduler() { + var scheduler = Schedulers.boundedElastic(); + var opts = new SenderOptions().resourceManagementScheduler(scheduler); + assertThat(opts.getResourceManagementScheduler()).isSameAs(scheduler); + } + + @Test + void connectionSubscriptionScheduler() { + var scheduler = Schedulers.boundedElastic(); + var opts = new SenderOptions().connectionSubscriptionScheduler(scheduler); + assertThat(opts.getConnectionSubscriptionScheduler()).isSameAs(scheduler); + } + + @Test + void connectionSupplierWithFactory() throws Exception { + var connection = mock(Connection.class); + var opts = new SenderOptions().connectionSupplier(cf -> connection); + assertThat(opts.getConnectionSupplier()).isNotNull(); + assertThat(opts.getConnectionSupplier().apply(null)).isSameAs(connection); + } + + @Test + void connectionSupplierWithExplicitFactory() throws Exception { + var connection = mock(Connection.class); + var factory = new ConnectionFactory(); + var opts = new SenderOptions().connectionSupplier(factory, cf -> connection); + assertThat(opts.getConnectionSupplier().apply(null)).isSameAs(connection); + } + + @Test + void connectionMonoConfigurator() { + UnaryOperator> configurator = Mono::cache; + var opts = new SenderOptions().connectionMonoConfigurator(configurator); + assertThat(opts.getConnectionMonoConfigurator()).isSameAs(configurator); + } + + @Test + void connectionMonoConfiguratorDefaultIsIdentity() { + var opts = new SenderOptions(); + Mono mono = Mono.just(mock(Connection.class)); + assertThat(opts.getConnectionMonoConfigurator().apply(mono)).isSameAs(mono); + } + + @Test + void resourceManagementChannelMono() { + Mono mono = Mono.just(mock(Channel.class)); + var opts = new SenderOptions().resourceManagementChannelMono(mono); + assertThat(opts.getResourceManagementChannelMono()).isSameAs(mono); + } + + @Test + void connectionClosingTimeout() { + var opts = new SenderOptions().connectionClosingTimeout(Duration.ofMillis(500)); + assertThat(opts.getConnectionClosingTimeout()).isEqualTo(Duration.ofMillis(500)); + } + + @Test + void channelPool() { + var pool = mock(ChannelPool.class); + Mono mono = Mono.just(mock(Channel.class)); + BiConsumer handler = (s, c) -> {}; + doReturn(mono).when(pool).getChannelMono(); + doReturn(handler).when(pool).getChannelCloseHandler(); + + var opts = new SenderOptions().channelPool(pool); + assertThat(opts.getChannelMono()).isSameAs(mono); + assertThat(opts.getChannelCloseHandler()).isSameAs(handler); + } +} diff --git a/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/SenderTest.java b/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/SenderTest.java new file mode 100644 index 00000000..f6e0a647 --- /dev/null +++ b/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/SenderTest.java @@ -0,0 +1,496 @@ +package reactor.rabbitmq; + +import com.rabbitmq.client.AMQP; +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.ConfirmListener; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.ReturnListener; +import com.rabbitmq.client.ShutdownSignalException; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.io.IOException; +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class SenderTest { + + private Connection connection; + private Channel channel; + private Sender sender; + + @BeforeEach + void setUp() throws Exception { + connection = mock(Connection.class); + channel = mock(Channel.class); + when(connection.createChannel()).thenReturn(channel); + when(channel.isOpen()).thenReturn(true); + when(channel.getConnection()).thenReturn(connection); + } + + @AfterEach + void tearDown() { + if (sender != null) { + sender.close(); + } + } + + @Test + void sendPublishesMessages() throws Exception { + sender = new Sender(new SenderOptions().connectionMono(Mono.just(connection))); + Flux messages = Flux.range(0, 5) + .map(i -> new OutboundMessage("exch", "rk", ("msg" + i).getBytes())); + + sender.send(messages).block(Duration.ofSeconds(5)); + + verify(channel, times(5)) + .basicPublish(eq("exch"), eq("rk"), isNull(), any(byte[].class)); + } + + @Test + void sendWithPublishConfirmsAcksMessages() throws Exception { + when(channel.getNextPublishSeqNo()).thenReturn(1L, 2L, 3L); + doAnswer(invocation -> { + channel.getConnection(); // just to make it compile + return null; + }).when(channel).confirmSelect(); + + // Simulate confirm ack for all messages upon confirmSelect + doAnswer(invocation -> null) + .when(channel).addConfirmListener(any(com.rabbitmq.client.ConfirmListener.class)); + + sender = new Sender(new SenderOptions().connectionMono(Mono.just(connection))); + + // Since we can't easily test async confirms in a unit test, + // verify that confirmSelect is called and publish happens + Flux messages = Flux.just( + new OutboundMessage("exch", "rk", "msg".getBytes())); + + // Just verify the pipeline doesn't error out with setup + assertNotNull(sender.sendWithPublishConfirms(messages)); + } + + @Test + void declareQueueExecutesRpc() throws Exception { + var rpcFuture = new CompletableFuture(); + var command = mock(com.rabbitmq.client.Command.class); + var declareOk = mock(AMQP.Queue.DeclareOk.class); + when(command.getMethod()).thenReturn(declareOk); + rpcFuture.complete(command); + when(channel.asyncCompletableRpc(any())).thenReturn(rpcFuture); + + sender = new Sender(new SenderOptions().connectionMono(Mono.just(connection))); + + StepVerifier.create(sender.declare(QueueSpecification.queue("test-queue").durable(true))) + .expectNext(declareOk) + .verifyComplete(); + } + + @Test + void declareExchangeExecutesRpc() throws Exception { + var rpcFuture = new CompletableFuture(); + var command = mock(com.rabbitmq.client.Command.class); + var declareOk = mock(AMQP.Exchange.DeclareOk.class); + when(command.getMethod()).thenReturn(declareOk); + rpcFuture.complete(command); + when(channel.asyncCompletableRpc(any())).thenReturn(rpcFuture); + + sender = new Sender(new SenderOptions().connectionMono(Mono.just(connection))); + + StepVerifier.create(sender.declare(ExchangeSpecification.exchange("test-exchange").type("direct"))) + .expectNext(declareOk) + .verifyComplete(); + } + + @Test + void bindExecutesRpc() throws Exception { + var rpcFuture = new CompletableFuture(); + var command = mock(com.rabbitmq.client.Command.class); + var bindOk = mock(AMQP.Queue.BindOk.class); + when(command.getMethod()).thenReturn(bindOk); + rpcFuture.complete(command); + when(channel.asyncCompletableRpc(any())).thenReturn(rpcFuture); + + sender = new Sender(new SenderOptions().connectionMono(Mono.just(connection))); + + StepVerifier.create(sender.bind(BindingSpecification.binding("exch", "rk", "queue"))) + .expectNext(bindOk) + .verifyComplete(); + } + + @Test + void unbindExecutesRpc() throws Exception { + var rpcFuture = new CompletableFuture(); + var command = mock(com.rabbitmq.client.Command.class); + var unbindOk = mock(AMQP.Queue.UnbindOk.class); + when(command.getMethod()).thenReturn(unbindOk); + rpcFuture.complete(command); + when(channel.asyncCompletableRpc(any())).thenReturn(rpcFuture); + + sender = new Sender(new SenderOptions().connectionMono(Mono.just(connection))); + + StepVerifier.create( + sender.unbind(BindingSpecification.binding("exch", "rk", "queue")) + ) + .expectNext(unbindOk) + .verifyComplete(); + } + + @Test + void declareQueueWithNullName() throws Exception { + var rpcFuture = new CompletableFuture(); + var command = mock(com.rabbitmq.client.Command.class); + var declareOk = mock(AMQP.Queue.DeclareOk.class); + when(command.getMethod()).thenReturn(declareOk); + rpcFuture.complete(command); + when(channel.asyncCompletableRpc(any())).thenReturn(rpcFuture); + + sender = new Sender(new SenderOptions().connectionMono(Mono.just(connection))); + + StepVerifier.create(sender.declare(QueueSpecification.queue())) + .expectNext(declareOk) + .verifyComplete(); + } + + @Test + void channelMonoPriority() { + Mono senderChannelMono = Mono.just(mock(Channel.class)); + Mono sendChannelMono = Mono.just(mock(Channel.class)); + + sender = new Sender(new SenderOptions().connectionMono(Mono.just(connection))); + assertNotNull(sender.getChannelMono(new SendOptions())); + assertSame(sendChannelMono, sender.getChannelMono(new SendOptions().channelMono(sendChannelMono))); + + sender.close(); + sender = new Sender(new SenderOptions().connectionMono(Mono.just(connection)) + .channelMono(senderChannelMono)); + assertSame(senderChannelMono, sender.getChannelMono(new SendOptions())); + assertSame(sendChannelMono, sender.getChannelMono(new SendOptions().channelMono(sendChannelMono))); + } + + @Test + void closeIsIdempotent() { + sender = new Sender(new SenderOptions().connectionMono(Mono.just(connection))); + sender.close(); + sender.close(); + sender = null; // prevent double close in tearDown + } + + @Test + void connectionIsClosedWithTimeout() throws Exception { + Connection c = mock(Connection.class); + Channel ch = mock(Channel.class); + when(c.createChannel()).thenReturn(ch); + when(ch.isOpen()).thenReturn(true); + when(ch.getConnection()).thenReturn(c); + + var options = new SenderOptions() + .connectionSupplier(cf -> c) + .connectionClosingTimeout(Duration.ofMillis(5000)); + + sender = new Sender(options); + sender.send(Flux.range(1, 1) + .map(i -> new OutboundMessage("", "", new byte[0]))) + .block(Duration.ofSeconds(5)); + sender.close(); + + verify(c, times(1)).close(5000); + sender = null; + } + + @Test + void connectionClosedWithZeroTimeoutMeansNoTimeout() throws Exception { + Connection c = mock(Connection.class); + Channel ch = mock(Channel.class); + when(c.createChannel()).thenReturn(ch); + when(ch.isOpen()).thenReturn(true); + when(ch.getConnection()).thenReturn(c); + + var options = new SenderOptions() + .connectionSupplier(cf -> c) + .connectionClosingTimeout(Duration.ZERO); + + sender = new Sender(options); + sender.send(Flux.range(1, 1) + .map(i -> new OutboundMessage("", "", new byte[0]))) + .block(Duration.ofSeconds(5)); + sender.close(); + + verify(c, times(1)).close(-1); + sender = null; + } + + @Test + void sendWithExceptionHandlerCallsHandler() throws Exception { + doThrow(new IOException("publish failed")).when(channel) + .basicPublish(anyString(), anyString(), any(), any(byte[].class)); + sender = new Sender(new SenderOptions().connectionMono(Mono.just(connection))); + + var handlerCalled = new java.util.concurrent.atomic.AtomicBoolean(false); + var sendOptions = new SendOptions().exceptionHandler((ctx, ex) -> handlerCalled.set(true)); + + sender.send(Flux.just(new OutboundMessage("exch", "rk", "body".getBytes())), sendOptions) + .block(Duration.ofSeconds(5)); + + assertThat(handlerCalled.get()).isTrue(); + } + + @Test + void sendContextPublishDelegates() throws Exception { + var ctx = new Sender.SendContext<>(channel, new OutboundMessage("e", "r", "b".getBytes())); + assertThat(ctx.getChannel()).isSameAs(channel); + assertThat(ctx.getMessage().getExchange()).isEqualTo("e"); + + ctx.publish(); + verify(channel).basicPublish(eq("e"), eq("r"), isNull(), any(byte[].class)); + } + + @Test + void sendContextPublishWithDifferentMessage() throws Exception { + var ctx = new Sender.SendContext<>(channel, new OutboundMessage("e1", "r1", "b1".getBytes())); + var other = new OutboundMessage("e2", "r2", "b2".getBytes()); + ctx.publish(other); + verify(channel).basicPublish(eq("e2"), eq("r2"), isNull(), eq("b2".getBytes())); + } + + @Nested + class PublishConfirmTests { + + @BeforeEach + void setUpConfirm() { + when(channel.getNextPublishSeqNo()).thenReturn(1L, 2L, 3L, 4L, 5L); + } + + @Test + void publishConfirmSingleAck() throws Exception { + AtomicReference listenerRef = new AtomicReference<>(); + doAnswer(inv -> { + listenerRef.set(inv.getArgument(0)); + return null; + }).when(channel).addConfirmListener(any(ConfirmListener.class)); + + sender = new Sender(new SenderOptions().connectionMono(Mono.just(connection))); + + Flux messages = Flux.just( + new OutboundMessage("exch", "rk", "msg1".getBytes())); + + List> results = new ArrayList<>(); + var flux = sender.sendWithPublishConfirms(messages); + + flux.subscribe(results::add); + + // Simulate broker ack for delivery tag 1 + if (listenerRef.get() != null) { + listenerRef.get().handleAck(1, false); + } + + assertThat(results) + .hasSize(1) + .first() + .satisfies(r -> { + assertThat(r.ack()).isTrue(); + assertThat(r.returned()).isFalse(); + }); + } + + @Test + void publishConfirmMultipleAck() throws Exception { + AtomicReference listenerRef = new AtomicReference<>(); + doAnswer(inv -> { + listenerRef.set(inv.getArgument(0)); + return null; + }).when(channel).addConfirmListener(any(ConfirmListener.class)); + + sender = new Sender(new SenderOptions().connectionMono(Mono.just(connection))); + + Flux messages = Flux.just( + new OutboundMessage("exch", "rk", "m1".getBytes()), + new OutboundMessage("exch", "rk", "m2".getBytes())); + + List> results = new ArrayList<>(); + sender.sendWithPublishConfirms(messages).subscribe(results::add); + + // Simulate broker ack multiple for delivery tag 2 (confirms 1 and 2) + if (listenerRef.get() != null) { + listenerRef.get().handleAck(2, true); + } + + assertThat(results).hasSize(2).allMatch(OutboundMessageResult::ack); + } + + @Test + void publishConfirmNack() throws Exception { + AtomicReference listenerRef = new AtomicReference<>(); + doAnswer(inv -> { + listenerRef.set(inv.getArgument(0)); + return null; + }).when(channel).addConfirmListener(any(ConfirmListener.class)); + + sender = new Sender(new SenderOptions().connectionMono(Mono.just(connection))); + + Flux messages = Flux.just( + new OutboundMessage("exch", "rk", "m1".getBytes())); + + List> results = new ArrayList<>(); + sender.sendWithPublishConfirms(messages).subscribe(results::add); + + if (listenerRef.get() != null) { + listenerRef.get().handleNack(1, false); + } + + assertThat(results) + .hasSize(1) + .first() + .satisfies(r -> assertThat(r.ack()).isFalse()); + } + + @Test + void publishConfirmWithTrackReturned() throws Exception { + when(channel.getNextPublishSeqNo()).thenReturn(1L); + AtomicReference returnRef = new AtomicReference<>(); + doAnswer(inv -> null).when(channel).addConfirmListener(any(ConfirmListener.class)); + doAnswer(inv -> { + returnRef.set(inv.getArgument(0)); + return null; + }).when(channel).addReturnListener(any(ReturnListener.class)); + + sender = new Sender(new SenderOptions().connectionMono(Mono.just(connection))); + + var sendOptions = new SendOptions().trackReturned(true); + Flux messages = Flux.just( + new OutboundMessage("exch", "rk", "m1".getBytes())); + + List> results = new ArrayList<>(); + sender.sendWithPublishConfirms(messages, sendOptions).subscribe(results::add); + + // Simulate a return (message was returned by broker) + if (returnRef.get() != null) { + var headers = new HashMap(); + headers.put("reactor_rabbitmq_delivery_tag", 1L); + var props = new AMQP.BasicProperties.Builder().headers(headers).build(); + returnRef.get().handleReturn( + 312, "NO_ROUTE", "exch", "rk", props, "m1".getBytes() + ); + } + + assertThat(results) + .hasSize(1) + .first() + .satisfies(r -> { + assertThat(r.returned()).isTrue(); + assertThat(r.ack()).isTrue(); + }); + } + + @Test + void publishConfirmShutdownNacksUnpublished() { + AtomicReference shutdownRef = new AtomicReference<>(); + doAnswer(inv -> { + shutdownRef.set(inv.getArgument(0)); + return null; + }).when(channel).addShutdownListener(any(com.rabbitmq.client.ShutdownListener.class)); + doAnswer(inv -> null).when(channel).addConfirmListener(any(ConfirmListener.class)); + + sender = new Sender(new SenderOptions().connectionMono(Mono.just(connection))); + + Flux messages = Flux.just( + new OutboundMessage("exch", "rk", "m1".getBytes())); + + AtomicReference errorRef = new AtomicReference<>(); + sender.sendWithPublishConfirms(messages).subscribe(r -> {}, errorRef::set); + + // Simulate channel shutdown (soft error, not by application) + if (shutdownRef.get() != null) { + shutdownRef.get().shutdownCompleted( + new ShutdownSignalException(false, false, null, channel)); + } + + // Shutdown on a soft non-application error should cause an onError + assertThat(errorRef.get()).isInstanceOf(ShutdownSignalException.class); + } + + @Test + void publishConfirmOnPublishExceptionInvokesExceptionHandler() throws Exception { + when(channel.getNextPublishSeqNo()).thenReturn(1L); + doThrow(new IOException("publish failed")).when(channel) + .basicPublish(anyString(), anyString(), anyBoolean(), any(), any(byte[].class)); + doAnswer(inv -> null).when(channel).addConfirmListener(any(ConfirmListener.class)); + + sender = new Sender(new SenderOptions().connectionMono(Mono.just(connection))); + + // Default exception handler is RetrySendingExceptionHandler which will retry and eventually timeout + // Use a noop handler that just throws RabbitFluxRetryTimeoutException + var sendOptions = new SendOptions().exceptionHandler((ctx, ex) -> { + throw new RabbitFluxRetryTimeoutException("timed out", ex); + }); + + List> results = new ArrayList<>(); + sender.sendWithPublishConfirms( + Flux.just(new OutboundMessage("exch", "rk", "m".getBytes())), sendOptions + ).subscribe(results::add); + + // On RabbitFluxRetryTimeoutException, the subscriber emits a nack result + assertThat(results) + .hasSize(1) + .first() + .satisfies(r -> assertThat(r.ack()).isFalse()); + } + + @Test + void confirmSendContextPublishTracksDeliveryTag() throws Exception { + when(channel.getNextPublishSeqNo()).thenReturn(42L); + AtomicReference confirmRef = new AtomicReference<>(); + doAnswer(inv -> { + confirmRef.set(inv.getArgument(0)); + return null; + }).when(channel).addConfirmListener(any(ConfirmListener.class)); + + sender = new Sender(new SenderOptions().connectionMono(Mono.just(connection))); + + var sendOptions = new SendOptions().trackReturned(true); + Flux messages = Flux.just( + new OutboundMessage("exch", "rk", "m".getBytes())); + + List> results = new ArrayList<>(); + sender.sendWithPublishConfirms(messages, sendOptions).subscribe(results::add); + + // Verify that basicPublish was called with mandatory=true (trackReturned) + // and that properties contain the delivery tag header + ArgumentCaptor propsCaptor = ArgumentCaptor.forClass(AMQP.BasicProperties.class); + verify(channel).basicPublish(eq("exch"), eq("rk"), eq(true), propsCaptor.capture(), any(byte[].class)); + + var props = propsCaptor.getValue(); + assertThat(props.getHeaders()).containsEntry("reactor_rabbitmq_delivery_tag", 42L); + + // Confirm it to complete + if (confirmRef.get() != null) { + confirmRef.get().handleAck(42, false); + } + } + } +} diff --git a/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/UtilsTest.java b/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/UtilsTest.java new file mode 100644 index 00000000..06fa37e0 --- /dev/null +++ b/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/UtilsTest.java @@ -0,0 +1,61 @@ +package reactor.rabbitmq; + +import com.rabbitmq.client.Channel; +import com.rabbitmq.client.Connection; +import com.rabbitmq.client.RecoverableChannel; +import com.rabbitmq.client.RecoverableConnection; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; + +import java.time.Duration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +class UtilsTest { + + @Test + void cacheReturnsElementIndefinitely() { + var cached = Utils.cache(Mono.just("value")); + assertThat(cached.block(Duration.ofSeconds(1))).isEqualTo("value"); + assertThat(cached.block(Duration.ofSeconds(1))).isEqualTo("value"); + } + + @Test + void cacheDoesNotCacheErrors() { + var counter = new java.util.concurrent.atomic.AtomicInteger(0); + var mono = Utils.cache(Mono.defer(() -> { + if (counter.incrementAndGet() == 1) { + return Mono.error(new RuntimeException("first call")); + } + return Mono.just("ok"); + })); + + assertThat(mono.onErrorResume(e -> Mono.empty()).block(Duration.ofSeconds(1))).isNull(); + assertThat(mono.block(Duration.ofSeconds(1))).isEqualTo("ok"); + } + + @Test + void isRecoverableConnectionReturnsTrueForRecoverable() { + var recoverable = mock(RecoverableConnection.class); + assertThat(Utils.isRecoverable((Connection) recoverable)).isTrue(); + } + + @Test + void isRecoverableConnectionReturnsFalseForNonRecoverable() { + var connection = mock(Connection.class); + assertThat(Utils.isRecoverable(connection)).isFalse(); + } + + @Test + void isRecoverableChannelReturnsTrueForRecoverable() { + var recoverable = mock(RecoverableChannel.class); + assertThat(Utils.isRecoverable((Channel) recoverable)).isTrue(); + } + + @Test + void isRecoverableChannelReturnsFalseForNonRecoverable() { + var channel = mock(Channel.class); + assertThat(Utils.isRecoverable(channel)).isFalse(); + } +} diff --git a/gradle.properties b/gradle.properties index a07044c4..560ae34a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=7.0.6 -toPublish=domain-events-api,async-commons-api,async-commons,cloudevents-json-jackson,async-rabbit,async-commons-rabbit-standalone,async-commons-rabbit-starter,async-kafka,async-kafka-starter,async-commons-starter +version=7.0.7 +toPublish=domain-events-api,async-commons-api,async-commons,cloudevents-json-jackson,reactor-rabbitmq,async-rabbit,async-commons-rabbit-standalone,async-commons-rabbit-starter,async-kafka,async-kafka-starter,async-commons-starter onlyUpdater=true \ No newline at end of file diff --git a/starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/rabbit/config/spring/RabbitPropertiesBaseTest.java b/starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/rabbit/config/spring/RabbitPropertiesBaseTest.java new file mode 100644 index 00000000..1a5fdd56 --- /dev/null +++ b/starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/rabbit/config/spring/RabbitPropertiesBaseTest.java @@ -0,0 +1,206 @@ +package org.reactivecommons.async.rabbit.config.spring; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class RabbitPropertiesBaseTest { + + private final RabbitPropertiesBase props = new RabbitPropertiesBase(); + + @Test + void defaultHost() { + assertThat(props.getHost()).isEqualTo("localhost"); + } + + @Test + void defaultPort() { + assertThat(props.getPort()).isEqualTo(5672); + } + + @Test + void defaultCredentials() { + assertThat(props.getUsername()).isEqualTo("guest"); + assertThat(props.getPassword()).isEqualTo("guest"); + } + + @Test + void determineHostWithoutAddresses() { + assertThat(props.determineHost()).isEqualTo("localhost"); + } + + @Test + void determinePortWithoutAddresses() { + assertThat(props.determinePort()).isEqualTo(5672); + } + + @Test + void determineAddressesWithoutParsed() { + assertThat(props.determineAddresses()).isEqualTo("localhost:5672"); + } + + @Test + void determineUsernameWithoutAddresses() { + assertThat(props.determineUsername()).isEqualTo("guest"); + } + + @Test + void determinePasswordWithoutAddresses() { + assertThat(props.determinePassword()).isEqualTo("guest"); + } + + @Test + void determineVirtualHostWithoutAddresses() { + assertThat(props.determineVirtualHost()).isNull(); + } + + @Test + void setVirtualHostEmpty() { + props.setVirtualHost(""); + assertThat(props.getVirtualHost()).isEqualTo("/"); + } + + @Test + void setVirtualHostNonEmpty() { + props.setVirtualHost("myVhost"); + assertThat(props.getVirtualHost()).isEqualTo("myVhost"); + } + + @Nested + class WithAddresses { + + @Test + void parseSimpleAddress() { + props.setAddresses("myhost:5673"); + assertThat(props.determineHost()).isEqualTo("myhost"); + assertThat(props.determinePort()).isEqualTo(5673); + assertThat(props.determineAddresses()).isEqualTo("myhost:5673"); + } + + @Test + void parseAddressWithDefaultPort() { + props.setAddresses("myhost"); + assertThat(props.determineHost()).isEqualTo("myhost"); + assertThat(props.determinePort()).isEqualTo(5672); + } + + @Test + void parseMultipleAddresses() { + props.setAddresses("host1:5672,host2:5673"); + assertThat(props.determineAddresses()).isEqualTo("host1:5672,host2:5673"); + assertThat(props.determineHost()).isEqualTo("host1"); + assertThat(props.determinePort()).isEqualTo(5672); + } + + @Test + void parseAmqpPrefixed() { + props.setAddresses("amqp://myhost:5674"); + assertThat(props.determineHost()).isEqualTo("myhost"); + assertThat(props.determinePort()).isEqualTo(5674); + } + + @Test + void parseWithCredentials() { + props.setAddresses("user:pass@myhost:5675"); + assertThat(props.determineUsername()).isEqualTo("user"); + assertThat(props.determinePassword()).isEqualTo("pass"); + assertThat(props.determineHost()).isEqualTo("myhost"); + } + + @Test + void parseWithUsernameOnly() { + props.setAddresses("user@myhost:5675"); + assertThat(props.determineHost()).isEqualTo("myhost"); + } + + @Test + void parseWithVirtualHost() { + props.setAddresses("myhost:5672/myvhost"); + assertThat(props.determineVirtualHost()).isEqualTo("myvhost"); + } + + @Test + void parseWithEmptyVirtualHost() { + props.setAddresses("myhost:5672/"); + assertThat(props.determineVirtualHost()).isEqualTo("/"); + } + + @Test + void usernameFromAddressFallsBackToDefault() { + props.setAddresses("myhost:5672"); + assertThat(props.determineUsername()).isEqualTo("guest"); + } + + @Test + void passwordFromAddressFallsBackToDefault() { + props.setAddresses("myhost:5672"); + assertThat(props.determinePassword()).isEqualTo("guest"); + } + + @Test + void virtualHostFallsBackToDefault() { + props.setAddresses("myhost:5672"); + props.setVirtualHost("fallback"); + assertThat(props.determineVirtualHost()).isEqualTo("fallback"); + } + + @Test + void fullAmqpUri() { + props.setAddresses("amqp://admin:secret@broker:5676/production"); + assertThat(props.determineHost()).isEqualTo("broker"); + assertThat(props.determinePort()).isEqualTo(5676); + assertThat(props.determineUsername()).isEqualTo("admin"); + assertThat(props.determinePassword()).isEqualTo("secret"); + assertThat(props.determineVirtualHost()).isEqualTo("production"); + } + } + + @Test + void sslDefaults() { + var ssl = props.getSsl(); + assertThat(ssl.isEnabled()).isFalse(); + assertThat(ssl.getKeyStoreType()).isEqualTo("PKCS12"); + assertThat(ssl.getTrustStoreType()).isEqualTo("JKS"); + assertThat(ssl.isValidateServerCertificate()).isTrue(); + assertThat(ssl.isVerifyHostname()).isTrue(); + } + + @Test + void cacheDefaults() { + var cache = props.getCache(); + assertThat(cache.getChannel()).isNotNull(); + assertThat(cache.getConnection()).isNotNull(); + assertThat(cache.getConnection().getMode()).isEqualTo("CHANNEL"); + } + + @Test + void listenerDefaults() { + var listener = props.getListener(); + assertThat(listener.getType()).isEqualTo(RabbitPropertiesBase.ContainerType.SIMPLE); + assertThat(listener.getSimple()).isNotNull(); + assertThat(listener.getDirect()).isNotNull(); + assertThat(listener.getSimple().isMissingQueuesFatal()).isTrue(); + assertThat(listener.getDirect().isMissingQueuesFatal()).isFalse(); + } + + @Test + void templateDefaults() { + var template = props.getTemplate(); + assertThat(template.getExchange()).isEmpty(); + assertThat(template.getRoutingKey()).isEmpty(); + } + + @Test + void retryDefaults() { + var retry = props.getTemplate().getRetry(); + assertThat(retry.isEnabled()).isFalse(); + assertThat(retry.getMaxAttempts()).isEqualTo(3); + } + + @Test + void listenerRetryDefaults() { + var listenerRetry = props.getListener().getSimple().getRetry(); + assertThat(listenerRetry.isStateless()).isTrue(); + } +} diff --git a/starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/starter/impl/common/rabbit/RabbitMQConfigTest.java b/starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/starter/impl/common/rabbit/RabbitMQConfigTest.java index 4a85aee1..07a10230 100644 --- a/starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/starter/impl/common/rabbit/RabbitMQConfigTest.java +++ b/starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/starter/impl/common/rabbit/RabbitMQConfigTest.java @@ -79,7 +79,7 @@ void shouldHasManager() { @Test void shouldProcessAndLogWhenMessageIsReturned() { UnroutableMessageHandler handler = rabbitMQConfig.defaultUnroutableMessageProcessor(unroutableMessageNotifier); - when(resultMock.getOutboundMessage()).thenReturn(outboundMessageMock); + when(resultMock.outboundMessage()).thenReturn(outboundMessageMock); when(outboundMessageMock.getExchange()).thenReturn("test.exchange"); when(outboundMessageMock.getRoutingKey()).thenReturn("test.key"); when(outboundMessageMock.getBody()).thenReturn("test message".getBytes(StandardCharsets.UTF_8)); @@ -87,7 +87,7 @@ void shouldProcessAndLogWhenMessageIsReturned() { StepVerifier.create(handler.processMessage(resultMock)).verifyComplete(); - verify(resultMock).getOutboundMessage(); + verify(resultMock).outboundMessage(); verify(outboundMessageMock).getExchange(); verify(outboundMessageMock).getRoutingKey(); verify(outboundMessageMock).getBody(); From e69f58c5da2c717203157f7b5a8caa5159a52f8a Mon Sep 17 00:00:00 2001 From: luisgomez29 Date: Tue, 24 Mar 2026 09:47:45 -0500 Subject: [PATCH 03/15] refactor: replace volatile FluxSink with AtomicReference in ReactiveMessageSender --- .../rabbit/communications/ReactiveMessageSender.java | 9 ++++----- .../communications/ReactiveMessageSenderTest.java | 11 ++++++++--- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/ReactiveMessageSender.java b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/ReactiveMessageSender.java index a34149ec..f69c5241 100644 --- a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/ReactiveMessageSender.java +++ b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/ReactiveMessageSender.java @@ -23,6 +23,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import static org.reactivecommons.async.api.DirectAsyncGateway.DELAYED; @@ -42,7 +43,7 @@ public class ReactiveMessageSender { private static final int NUMBER_OF_SENDER_SUBSCRIPTIONS = 4; private final CopyOnWriteArrayList> fluxSinkConfirm = new CopyOnWriteArrayList<>(); - private volatile FluxSink fluxSinkNoConfirm; + private final AtomicReference> fluxSinkNoConfirm = new AtomicReference<>(); private final AtomicLong counter = new AtomicLong(); private final ExecutorService executorService = Executors.newFixedThreadPool( @@ -78,9 +79,7 @@ private void initializeSenders() { }).subscribe(); } - final Flux messageSourceNoConfirm = Flux.create(fluxSink -> - this.fluxSinkNoConfirm = fluxSink - ); + final Flux messageSourceNoConfirm = Flux.create(this.fluxSinkNoConfirm::set); sender.send(messageSourceNoConfirm).subscribe(); } @@ -100,7 +99,7 @@ public Mono sendWithConfirm(T message, String exchange, String routing public Mono sendNoConfirm(T message, String exchange, String routingKey, Map headers, boolean persistent) { - fluxSinkNoConfirm.next(toOutboundMessage(message, exchange, routingKey, headers, persistent)); + fluxSinkNoConfirm.get().next(toOutboundMessage(message, exchange, routingKey, headers, persistent)); return Mono.empty(); } diff --git a/async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/communications/ReactiveMessageSenderTest.java b/async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/communications/ReactiveMessageSenderTest.java index 7745d471..d3f2856e 100644 --- a/async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/communications/ReactiveMessageSenderTest.java +++ b/async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/communications/ReactiveMessageSenderTest.java @@ -41,9 +41,6 @@ class ReactiveMessageSenderTest { @Spy private final MessageConverter messageConverter = new RabbitJacksonMessageConverter(jsonMapper); - @Mock - private final SendOptions sendOptions = new SendOptions(); - @Mock private UnroutableMessageNotifier unroutableMessageNotifier; @@ -122,6 +119,14 @@ void sendWithConfirmSomeMessage() { StepVerifier.create(voidMono).verifyComplete(); } + @Test + void sendNoConfirmSomeMessage() { + SomeClass some = new SomeClass("42", "Daniel", new Date()); + final Mono voidMono = messageSender.sendNoConfirm(some, "exchange", "rkey", new HashMap<>(), true); + + StepVerifier.create(voidMono).verifyComplete(); + } + private record SomeClass(String id, String name, Date date) { } From 2be7014a2b7a85bfcb8ced9646fe8a4643a12208 Mon Sep 17 00:00:00 2001 From: luisgomez29 Date: Tue, 24 Mar 2026 10:00:17 -0500 Subject: [PATCH 04/15] refactor: replace volatile fields with AtomicReference in message listeners --- .../kafka/listeners/GenericMessageListener.java | 13 +++++++------ .../communications/UnroutableMessageNotifier.java | 14 ++++++++------ .../listeners/ApplicationReplyListener.java | 9 +++++---- .../rabbit/listeners/GenericMessageListener.java | 15 ++++++++------- .../UnroutableMessageNotifierTest.java | 4 +++- 5 files changed, 31 insertions(+), 24 deletions(-) diff --git a/async/async-kafka/src/main/java/org/reactivecommons/async/kafka/listeners/GenericMessageListener.java b/async/async-kafka/src/main/java/org/reactivecommons/async/kafka/listeners/GenericMessageListener.java index 7c14e631..7634d17e 100644 --- a/async/async-kafka/src/main/java/org/reactivecommons/async/kafka/listeners/GenericMessageListener.java +++ b/async/async-kafka/src/main/java/org/reactivecommons/async/kafka/listeners/GenericMessageListener.java @@ -20,6 +20,7 @@ import java.time.Instant; import java.util.List; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; import java.util.logging.Level; @@ -43,7 +44,7 @@ public abstract class GenericMessageListener { private final DiscardNotifier discardNotifier; private final String objectType; private final CustomReporter customReporter; - private volatile Flux> messageFlux; + private final AtomicReference>> messageFlux = new AtomicReference<>(); protected GenericMessageListener(ReactiveMessageListener listener, boolean useDLQ, boolean createTopology, long maxRetries, long retryDelay, DiscardNotifier discardNotifier, @@ -81,21 +82,21 @@ public void startListener(TopologyCreator creator) { } if (createTopology) { - this.messageFlux = setUpBindings(creator) + this.messageFlux.set(setUpBindings(creator) .thenMany(this.messageListener.listen(groupId, topics) .doOnError(err -> log.log(Level.SEVERE, "Error listening queue", err)) - .transform(this::consumeFaultTolerant)); + .transform(this::consumeFaultTolerant))); } else { - this.messageFlux = this.messageListener.listen(groupId, topics) + this.messageFlux.set(this.messageListener.listen(groupId, topics) .doOnError(err -> log.log(Level.SEVERE, "Error listening queue", err)) - .transform(this::consumeFaultTolerant); + .transform(this::consumeFaultTolerant)); } onTerminate(); } private void onTerminate() { - messageFlux.doOnTerminate(this::onTerminate) + messageFlux.get().doOnTerminate(this::onTerminate) .subscribe(new LoggerSubscriber<>(getClass().getName())); } diff --git a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/UnroutableMessageNotifier.java b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/UnroutableMessageNotifier.java index 0c807655..9aa90fe5 100644 --- a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/UnroutableMessageNotifier.java +++ b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/UnroutableMessageNotifier.java @@ -6,10 +6,12 @@ import reactor.core.scheduler.Schedulers; import reactor.rabbitmq.OutboundMessageResult; +import java.util.concurrent.atomic.AtomicReference; + @Log public class UnroutableMessageNotifier { private final Sinks.Many> sink; - private volatile Disposable currentSubscription; + private final AtomicReference currentSubscription = new AtomicReference<>(); public UnroutableMessageNotifier() { this.sink = Sinks.many().multicast().onBackpressureBuffer(); @@ -22,15 +24,15 @@ public void notifyUnroutableMessage(OutboundMessageResult mes } public void listenToUnroutableMessages(UnroutableMessageHandler handler) { - if (currentSubscription != null && !currentSubscription.isDisposed()) { - currentSubscription.dispose(); - } - currentSubscription = sink.asFlux() + Disposable previous = currentSubscription.getAndSet(sink.asFlux() .subscribeOn(Schedulers.boundedElastic()) .flatMap(handler::processMessage) .onErrorContinue((throwable, o) -> log.severe("Error processing unroutable message: " + throwable.getMessage()) ) - .subscribe(); + .subscribe()); + if (previous != null && !previous.isDisposed()) { + previous.dispose(); + } } } diff --git a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/ApplicationReplyListener.java b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/ApplicationReplyListener.java index a143d568..670e9e7e 100644 --- a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/ApplicationReplyListener.java +++ b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/ApplicationReplyListener.java @@ -13,6 +13,7 @@ import reactor.rabbitmq.ConsumeOptions; import reactor.rabbitmq.Receiver; +import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Level; import static org.reactivecommons.async.commons.Headers.COMPLETION_ONLY_SIGNAL; @@ -32,7 +33,7 @@ public class ApplicationReplyListener { private final String exchangeName; private final boolean createTopology; - private volatile Flux deliveryFlux; + private final AtomicReference> deliveryFlux = new AtomicReference<>(); public ApplicationReplyListener(ReactiveReplyRouter router, ReactiveMessageListener listener, String queueName, String exchangeName, boolean createTopology) { @@ -51,7 +52,7 @@ public void startListening(String routeKey) { } ConsumeOptions consumeOptions = new ConsumeOptions(); consumeOptions.consumerTag(InstanceIdentifier.getInstanceId("replies")); - deliveryFlux = flow + deliveryFlux.set(flow .then(creator.declare(queue(queueName).durable(false).autoDelete(true).exclusive(true))) .then(creator.bind(binding(exchangeName, routeKey, queueName))) .thenMany(receiver.consumeAutoAck(queueName, consumeOptions).doOnNext(delivery -> { @@ -66,13 +67,13 @@ public void startListening(String routeKey) { } catch (Exception e) { log.log(Level.SEVERE, "Error in reply reception", e); } - })); + }))); onTerminate(); } private void onTerminate() { - deliveryFlux.doOnTerminate(this::onTerminate) + deliveryFlux.get().doOnTerminate(this::onTerminate) .subscribe(new LoggerSubscriber<>(getClass().getName())); } diff --git a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/GenericMessageListener.java b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/GenericMessageListener.java index dac167ff..890b0dba 100644 --- a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/GenericMessageListener.java +++ b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/GenericMessageListener.java @@ -26,6 +26,7 @@ import java.util.List; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; import java.util.logging.Level; @@ -51,7 +52,7 @@ public abstract class GenericMessageListener { private final DiscardNotifier discardNotifier; private final String objectType; private final CustomReporter customReporter; - private volatile Flux messageFlux; + private final AtomicReference> messageFlux = new AtomicReference<>(); protected GenericMessageListener(String queueName, ReactiveMessageListener listener, boolean useDLQRetries, boolean createTopology, long maxRetries, long retryDelay, @@ -98,14 +99,14 @@ public void startListener() { consumeOptions.consumerTag(InstanceIdentifier.getInstanceId(getKind())); if (createTopology) { - this.messageFlux = setUpBindings(messageListener.topologyCreator()) + this.messageFlux.set(setUpBindings(messageListener.topologyCreator()) .thenMany(receiver.consumeManualAck(queueName, consumeOptions) .doOnError(err -> log.log(Level.SEVERE, "Error listening queue " + getRootCauseMessage(err), err)) - .transform(this::consumeFaultTolerant)); + .transform(this::consumeFaultTolerant))); } else { - this.messageFlux = receiver.consumeManualAck(queueName, consumeOptions) + this.messageFlux.set(receiver.consumeManualAck(queueName, consumeOptions) .doOnError(err -> log.log(Level.SEVERE, "Error listening queue " + getRootCauseMessage(err), err)) - .transform(this::consumeFaultTolerant); + .transform(this::consumeFaultTolerant)); } onTerminate(); @@ -156,7 +157,7 @@ protected Mono handle(AcknowledgableDelivery msj, Instan } private void onTerminate() { - messageFlux + messageFlux.get() .doOnTerminate(this::onTerminate) .subscribe(new LoggerSubscriber<>(getClass().getName())); } @@ -225,7 +226,7 @@ private Mono requeueOrAck(AcknowledgableDelivery msj, Th logError(err, msj, FallbackStrategy.DEFINITIVE_DISCARD); return discardNotifier .notifyDiscard(rabbitMessage) - .doOnSuccess(_a -> msj.ack()).thenReturn(msj); + .doOnSuccess(a -> msj.ack()).thenReturn(msj); } else if (useDLQRetries) { // DLQ retries logError(err, msj, FallbackStrategy.RETRY_DLQ); msj.nack(false); diff --git a/async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/communications/UnroutableMessageNotifierTest.java b/async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/communications/UnroutableMessageNotifierTest.java index 9600529b..19eca19f 100644 --- a/async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/communications/UnroutableMessageNotifierTest.java +++ b/async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/communications/UnroutableMessageNotifierTest.java @@ -15,6 +15,7 @@ import reactor.rabbitmq.OutboundMessageResult; import java.lang.reflect.Field; +import java.util.concurrent.atomic.AtomicReference; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.any; @@ -93,7 +94,8 @@ void shouldDisposePreviousSubscriptionWhenNewHandlerIsSubscribed() throws Except Field field = UnroutableMessageNotifier.class.getDeclaredField("currentSubscription"); field.setAccessible(true); - field.set(unroutableMessageNotifier, firstSubscription); + AtomicReference ref = (AtomicReference) field.get(unroutableMessageNotifier); + ref.set(firstSubscription); when(sink.asFlux()).thenReturn(Flux.never()); From b50ea6c2d91270debb783b5b13544f110a8ea4fd Mon Sep 17 00:00:00 2001 From: luisgomez29 Date: Tue, 24 Mar 2026 11:10:59 -0500 Subject: [PATCH 05/15] refactor: replace MyOutboundMessage with OutboundMessage in message handling --- .../communications/MyOutboundMessage.java | 21 --------- .../communications/ReactiveMessageSender.java | 12 ++--- .../UnroutableMessageHandler.java | 3 +- .../UnroutableMessageNotifier.java | 5 +- .../UnroutableMessageProcessor.java | 3 +- .../communications/MyOutboundMessageTest.java | 45 ------------------ .../ReactiveMessageSenderTest.java | 6 +-- .../UnroutableMessageNotifierTest.java | 11 +++-- .../UnroutableMessageProcessorTest.java | 21 +++++---- .../reactor/rabbitmq/OutboundMessage.java | 19 ++++++-- .../reactor/rabbitmq/OutboundMessageTest.java | 46 +++++++++++++++++++ .../configuration_properties/1-rabbitmq.md | 13 +++--- .../common/rabbit/RabbitMQConfigTest.java | 6 +-- 13 files changed, 105 insertions(+), 106 deletions(-) delete mode 100644 async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/MyOutboundMessage.java delete mode 100644 async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/communications/MyOutboundMessageTest.java diff --git a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/MyOutboundMessage.java b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/MyOutboundMessage.java deleted file mode 100644 index 5d8e1ffc..00000000 --- a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/MyOutboundMessage.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.reactivecommons.async.rabbit.communications; - -import com.rabbitmq.client.AMQP; -import lombok.Getter; -import reactor.rabbitmq.OutboundMessage; - -import java.util.function.Consumer; - -@Getter -public class MyOutboundMessage extends OutboundMessage { - - private final Consumer ackNotifier; - - public MyOutboundMessage( - String exchange, String routingKey, AMQP.BasicProperties properties, - byte[] body, Consumer ackNotifier - ) { - super(exchange, routingKey, properties, body); - this.ackNotifier = ackNotifier; - } -} diff --git a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/ReactiveMessageSender.java b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/ReactiveMessageSender.java index f69c5241..de2db3cb 100644 --- a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/ReactiveMessageSender.java +++ b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/ReactiveMessageSender.java @@ -41,7 +41,7 @@ public class ReactiveMessageSender { private final UnroutableMessageNotifier unroutableMessageNotifier; private static final int NUMBER_OF_SENDER_SUBSCRIPTIONS = 4; - private final CopyOnWriteArrayList> fluxSinkConfirm = new CopyOnWriteArrayList<>(); + private final CopyOnWriteArrayList> fluxSinkConfirm = new CopyOnWriteArrayList<>(); private final AtomicReference> fluxSinkNoConfirm = new AtomicReference<>(); private final AtomicLong counter = new AtomicLong(); @@ -67,9 +67,9 @@ public ReactiveMessageSender(Sender sender, String sourceApplication, private void initializeSenders() { for (int i = 0; i < NUMBER_OF_SENDER_SUBSCRIPTIONS; ++i) { - final Flux messageSource = Flux.create(fluxSinkConfirm::add); + final Flux messageSource = Flux.create(fluxSinkConfirm::add); sender.sendWithTypedPublishConfirms(messageSource, new SendOptions().trackReturned(isMandatory)) - .doOnNext((OutboundMessageResult outboundMessageResult) -> { + .doOnNext((OutboundMessageResult outboundMessageResult) -> { if (outboundMessageResult.returned()) { this.unroutableMessageNotifier.notifyUnroutableMessage(outboundMessageResult); } @@ -87,7 +87,7 @@ public Mono sendWithConfirm(T message, String exchange, String routing Map headers, boolean persistent) { return Mono.create(monoSink -> { Consumer notifier = new AckNotifier(monoSink); - final MyOutboundMessage outboundMessage = toOutboundMessage( + final OutboundMessage outboundMessage = toOutboundMessage( message, exchange, routingKey, headers, notifier, persistent ); executorService2.submit(() -> fluxSinkConfirm.get( @@ -125,12 +125,12 @@ public void accept(Boolean ack) { } } - private MyOutboundMessage toOutboundMessage(T object, String exchange, + private OutboundMessage toOutboundMessage(T object, String exchange, String routingKey, Map headers, Consumer ackNotifier, boolean persistent) { final Message message = messageConverter.toMessage(object); final AMQP.BasicProperties props = buildMessageProperties(message, headers, persistent); - return new MyOutboundMessage(exchange, routingKey, props, message.getBody(), ackNotifier); + return new OutboundMessage(exchange, routingKey, props, message.getBody(), ackNotifier); } private OutboundMessage toOutboundMessage(T object, String exchange, String routingKey, diff --git a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/UnroutableMessageHandler.java b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/UnroutableMessageHandler.java index e5ea3e5e..fe184abd 100644 --- a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/UnroutableMessageHandler.java +++ b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/UnroutableMessageHandler.java @@ -1,6 +1,7 @@ package org.reactivecommons.async.rabbit.communications; import reactor.core.publisher.Mono; +import reactor.rabbitmq.OutboundMessage; import reactor.rabbitmq.OutboundMessageResult; @FunctionalInterface @@ -10,5 +11,5 @@ public interface UnroutableMessageHandler { * * @param result The result of the outbound message, containing the original message and return details. */ - Mono processMessage(OutboundMessageResult result); + Mono processMessage(OutboundMessageResult result); } \ No newline at end of file diff --git a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/UnroutableMessageNotifier.java b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/UnroutableMessageNotifier.java index 9aa90fe5..8d135e32 100644 --- a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/UnroutableMessageNotifier.java +++ b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/UnroutableMessageNotifier.java @@ -4,20 +4,21 @@ import reactor.core.Disposable; import reactor.core.publisher.Sinks; import reactor.core.scheduler.Schedulers; +import reactor.rabbitmq.OutboundMessage; import reactor.rabbitmq.OutboundMessageResult; import java.util.concurrent.atomic.AtomicReference; @Log public class UnroutableMessageNotifier { - private final Sinks.Many> sink; + private final Sinks.Many> sink; private final AtomicReference currentSubscription = new AtomicReference<>(); public UnroutableMessageNotifier() { this.sink = Sinks.many().multicast().onBackpressureBuffer(); } - public void notifyUnroutableMessage(OutboundMessageResult message) { + public void notifyUnroutableMessage(OutboundMessageResult message) { if (sink.tryEmitNext(message).isFailure()) { log.warning("Failed to emit unroutable message: " + message); } diff --git a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/UnroutableMessageProcessor.java b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/UnroutableMessageProcessor.java index 2acd6e5e..6e8a6e4a 100644 --- a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/UnroutableMessageProcessor.java +++ b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/UnroutableMessageProcessor.java @@ -3,6 +3,7 @@ import lombok.NoArgsConstructor; import lombok.extern.java.Log; import reactor.core.publisher.Mono; +import reactor.rabbitmq.OutboundMessage; import reactor.rabbitmq.OutboundMessageResult; import java.nio.charset.StandardCharsets; @@ -13,7 +14,7 @@ public class UnroutableMessageProcessor implements UnroutableMessageHandler { @Override - public Mono processMessage(OutboundMessageResult result) { + public Mono processMessage(OutboundMessageResult result) { var outboundMessage = result.outboundMessage(); log.severe("Unroutable message: exchange=" + outboundMessage.getExchange() + ", routingKey=" + outboundMessage.getRoutingKey() diff --git a/async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/communications/MyOutboundMessageTest.java b/async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/communications/MyOutboundMessageTest.java deleted file mode 100644 index 73fad1a2..00000000 --- a/async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/communications/MyOutboundMessageTest.java +++ /dev/null @@ -1,45 +0,0 @@ -package org.reactivecommons.async.rabbit.communications; - -import com.rabbitmq.client.AMQP; -import org.junit.jupiter.api.Test; - -import java.util.concurrent.atomic.AtomicBoolean; - -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -class MyOutboundMessageTest { - - @Test - void shouldCreateMessageWithCorrectProperties() { - AMQP.BasicProperties properties = new AMQP.BasicProperties(); - byte[] body = "test message".getBytes(); - AtomicBoolean ackCalled = new AtomicBoolean(false); - - MyOutboundMessage message = new MyOutboundMessage( - "test.exchange", "test.routingKey", properties, body, ackCalled::set - ); - - assertEquals("test.exchange", message.getExchange()); - assertEquals("test.routingKey", message.getRoutingKey()); - assertEquals(properties, message.getProperties()); - assertArrayEquals(body, message.getBody()); - assertNotNull(message.getAckNotifier()); - } - - @Test - void shouldInvokeAckNotifierWhenCalled() { - AtomicBoolean ackCalled = new AtomicBoolean(false); - - MyOutboundMessage message = new MyOutboundMessage( - "test.exchange", "test.routingKey", new AMQP.BasicProperties(), - "test message".getBytes(), ackCalled::set - ); - - message.getAckNotifier().accept(true); - - assertTrue(ackCalled.get()); - } -} \ No newline at end of file diff --git a/async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/communications/ReactiveMessageSenderTest.java b/async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/communications/ReactiveMessageSenderTest.java index d3f2856e..874840ec 100644 --- a/async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/communications/ReactiveMessageSenderTest.java +++ b/async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/communications/ReactiveMessageSenderTest.java @@ -47,9 +47,9 @@ class ReactiveMessageSenderTest { @BeforeEach void init() { when(sender.sendWithTypedPublishConfirms(any(Publisher.class), any(SendOptions.class))).then(invocation -> { - final Flux argument = invocation.getArgument(0); + final Flux argument = invocation.getArgument(0); return argument - .map(myOutboundMessage -> new OutboundMessageResult<>(myOutboundMessage, true)); + .map(outboundMessage -> new OutboundMessageResult<>(outboundMessage, true)); }); when(sender.send(any(Publisher.class))).then(invocation -> { final Flux argument = invocation.getArgument(0); @@ -66,7 +66,7 @@ void init() { void shouldCallUnroutableMessageHandlerWhenMessageIsReturned() { String sourceApplication = "TestApp"; when(sender.sendWithTypedPublishConfirms(any(Publisher.class), any(SendOptions.class))).then(invocation -> { - Flux argument = invocation.getArgument(0); + Flux argument = invocation.getArgument(0); return argument.map(msg -> new OutboundMessageResult<>(msg, false, true)); }); diff --git a/async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/communications/UnroutableMessageNotifierTest.java b/async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/communications/UnroutableMessageNotifierTest.java index 19eca19f..8e82260f 100644 --- a/async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/communications/UnroutableMessageNotifierTest.java +++ b/async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/communications/UnroutableMessageNotifierTest.java @@ -12,6 +12,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.Sinks; +import reactor.rabbitmq.OutboundMessage; import reactor.rabbitmq.OutboundMessageResult; import java.lang.reflect.Field; @@ -32,16 +33,16 @@ class UnroutableMessageNotifierTest { private UnroutableMessageNotifier unroutableMessageNotifier; @Mock - private Sinks.Many> sink; + private Sinks.Many> sink; @Mock - private OutboundMessageResult messageResult; + private OutboundMessageResult messageResult; @Mock private UnroutableMessageHandler handler; @Captor - private ArgumentCaptor> messageCaptor; + private ArgumentCaptor> messageCaptor; @BeforeEach void setUp() { @@ -77,7 +78,7 @@ void shouldNotThrowWhenEmissionFails() { @Test void shouldSubscribeHandlerToFluxOfMessages() { - final Flux> messageResultFlux = Flux.just(messageResult); + final Flux> messageResultFlux = Flux.just(messageResult); when(sink.asFlux()).thenReturn(messageResultFlux); when(handler.processMessage(any(OutboundMessageResult.class))).thenReturn(Mono.empty()); @@ -106,7 +107,7 @@ void shouldDisposePreviousSubscriptionWhenNewHandlerIsSubscribed() throws Except @Test void shouldContinueProcessingWhenHandlerFails() { - OutboundMessageResult messageResult2 = mock(OutboundMessageResult.class); + OutboundMessageResult messageResult2 = mock(OutboundMessageResult.class); when(sink.asFlux()).thenReturn(Flux.just(messageResult, messageResult2)); when(handler.processMessage(messageResult)).thenReturn(Mono.error(new RuntimeException("Processing Error"))); when(handler.processMessage(messageResult2)).thenReturn(Mono.empty()); diff --git a/async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/communications/UnroutableMessageProcessorTest.java b/async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/communications/UnroutableMessageProcessorTest.java index 645a6c48..fa8d4bba 100644 --- a/async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/communications/UnroutableMessageProcessorTest.java +++ b/async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/communications/UnroutableMessageProcessorTest.java @@ -5,6 +5,7 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import reactor.rabbitmq.OutboundMessage; import reactor.rabbitmq.OutboundMessageResult; import reactor.test.StepVerifier; @@ -17,10 +18,10 @@ class UnroutableMessageProcessorTest { @Mock - private OutboundMessageResult messageResult; + private OutboundMessageResult messageResult; @Mock - private MyOutboundMessage myOutboundMessage; + private OutboundMessage outboundMessage; @InjectMocks private UnroutableMessageProcessor unroutableMessageProcessor; @@ -28,18 +29,18 @@ class UnroutableMessageProcessorTest { @Test void logsUnroutableMessageDetails() { - when(messageResult.outboundMessage()).thenReturn(myOutboundMessage); - when(myOutboundMessage.getExchange()).thenReturn("test-exchange"); - when(myOutboundMessage.getRoutingKey()).thenReturn("test-routingKey"); - when(myOutboundMessage.getBody()).thenReturn("test-body".getBytes(StandardCharsets.UTF_8)); - when(myOutboundMessage.getProperties()).thenReturn(null); + when(messageResult.outboundMessage()).thenReturn(outboundMessage); + when(outboundMessage.getExchange()).thenReturn("test-exchange"); + when(outboundMessage.getRoutingKey()).thenReturn("test-routingKey"); + when(outboundMessage.getBody()).thenReturn("test-body".getBytes(StandardCharsets.UTF_8)); + when(outboundMessage.getProperties()).thenReturn(null); StepVerifier.create(unroutableMessageProcessor.processMessage(messageResult)) .verifyComplete(); verify(messageResult).outboundMessage(); - verify(myOutboundMessage).getExchange(); - verify(myOutboundMessage).getRoutingKey(); - verify(myOutboundMessage).getBody(); + verify(outboundMessage).getExchange(); + verify(outboundMessage).getRoutingKey(); + verify(outboundMessage).getBody(); } } \ No newline at end of file diff --git a/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/OutboundMessage.java b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/OutboundMessage.java index 3ccacf55..8e11e540 100644 --- a/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/OutboundMessage.java +++ b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/OutboundMessage.java @@ -19,6 +19,8 @@ import com.rabbitmq.client.AMQP.BasicProperties; import lombok.Getter; import java.util.Arrays; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; @Getter public class OutboundMessage { @@ -27,18 +29,29 @@ public class OutboundMessage { private final String routingKey; private final BasicProperties properties; private final byte[] body; + private final Consumer ackNotifier; - private volatile boolean published = false; + private final AtomicBoolean published = new AtomicBoolean(false); public OutboundMessage(String exchange, String routingKey, byte[] body) { - this(exchange, routingKey, null, body); + this(exchange, routingKey, null, body, null); } public OutboundMessage(String exchange, String routingKey, BasicProperties properties, byte[] body) { + this(exchange, routingKey, properties, body, null); + } + + public OutboundMessage(String exchange, String routingKey, BasicProperties properties, byte[] body, + Consumer ackNotifier) { this.exchange = exchange; this.routingKey = routingKey; this.properties = properties; this.body = body; + this.ackNotifier = ackNotifier; + } + + public boolean isPublished() { + return published.get(); } @Override @@ -52,6 +65,6 @@ public String toString() { } void published() { - this.published = true; + this.published.set(true); } } diff --git a/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/OutboundMessageTest.java b/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/OutboundMessageTest.java index 9b6a728b..25737214 100644 --- a/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/OutboundMessageTest.java +++ b/async/reactor-rabbitmq/src/test/java/reactor/rabbitmq/OutboundMessageTest.java @@ -3,7 +3,14 @@ import com.rabbitmq.client.AMQP; import org.junit.jupiter.api.Test; +import java.util.concurrent.atomic.AtomicBoolean; + import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; class OutboundMessageTest { @@ -41,4 +48,43 @@ void toStringContainsFieldValues() { var str = msg.toString(); assertThat(str).contains("exch").contains("rk"); } + + @Test + void shouldCreateMessageWithAckNotifier() { + AMQP.BasicProperties properties = new AMQP.BasicProperties(); + byte[] body = "test message".getBytes(); + AtomicBoolean ackCalled = new AtomicBoolean(false); + + OutboundMessage message = new OutboundMessage( + "test.exchange", "test.routingKey", properties, body, ackCalled::set + ); + + assertEquals("test.exchange", message.getExchange()); + assertEquals("test.routingKey", message.getRoutingKey()); + assertEquals(properties, message.getProperties()); + assertArrayEquals(body, message.getBody()); + assertNotNull(message.getAckNotifier()); + } + + @Test + void shouldInvokeAckNotifierWhenCalled() { + AtomicBoolean ackCalled = new AtomicBoolean(false); + + OutboundMessage message = new OutboundMessage( + "test.exchange", "test.routingKey", new AMQP.BasicProperties(), + "test message".getBytes(), ackCalled::set + ); + + message.getAckNotifier().accept(true); + + assertTrue(ackCalled.get()); + } + + @Test + void shouldCreateMessageWithoutAckNotifier() { + OutboundMessage message = new OutboundMessage( + "test.exchange", "test.routingKey", new AMQP.BasicProperties(), "body".getBytes() + ); + assertNull(message.getAckNotifier()); + } } diff --git a/docs/docs/reactive-commons/configuration_properties/1-rabbitmq.md b/docs/docs/reactive-commons/configuration_properties/1-rabbitmq.md index b6d553fc..665f8069 100644 --- a/docs/docs/reactive-commons/configuration_properties/1-rabbitmq.md +++ b/docs/docs/reactive-commons/configuration_properties/1-rabbitmq.md @@ -400,10 +400,10 @@ package sample; import co.com.mypackage.usecase.MyUseCase; import lombok.RequiredArgsConstructor; import lombok.extern.java.Log; -import org.reactivecommons.async.rabbit.communications.MyOutboundMessage; import org.reactivecommons.async.rabbit.communications.UnroutableMessageHandler; import org.springframework.stereotype.Component; import reactor.core.publisher.Mono; +import reactor.rabbitmq.OutboundMessage; import reactor.rabbitmq.OutboundMessageResult; import java.nio.charset.StandardCharsets; @@ -416,8 +416,8 @@ public class ResendUnroutableMessageHandler implements UnroutableMessageHandler private final MyUseCase useCase; @Override - public Mono processMessage(OutboundMessageResult result) { - var returned = result.getOutboundMessage(); + public Mono processMessage(OutboundMessageResult result) { + var returned = result.outboundMessage(); log.severe("Unroutable message: exchange=" + returned.getExchange() + ", routingKey=" + returned.getRoutingKey() + ", body=" + new String(returned.getBody(), StandardCharsets.UTF_8) @@ -443,10 +443,10 @@ Therefore, it is recommended to verify or create the queue beforehand to ensure ```java package sample; +import lombok.extern.java.Log; import org.reactivecommons.api.domain.Command; import org.reactivecommons.async.api.DirectAsyncGateway; import org.reactivecommons.async.impl.config.annotations.EnableDirectAsyncGateway; -import org.reactivecommons.async.rabbit.communications.MyOutboundMessage; import org.reactivecommons.async.rabbit.communications.UnroutableMessageHandler; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -457,6 +457,7 @@ import tools.jackson.core.type.TypeReference; import tools.jackson.databind.JsonNode; import tools.jackson.databind.json.JsonMapper; +@Log @Component @EnableDirectAsyncGateway public class ResendUnroutableMessageHandler implements UnroutableMessageHandler { @@ -483,8 +484,8 @@ public class ResendUnroutableMessageHandler implements UnroutableMessageHandler } @Override - public Mono processMessage(OutboundMessageResult result) { - OutboundMessage returned = result.getOutboundMessage(); + public Mono processMessage(OutboundMessageResult result) { + OutboundMessage returned = result.outboundMessage(); try { // The unroutable message is a command, so the message body is deserialized to the Command class. // Use the DomainEvent class for domain events and the AsyncQuery class for asynchronous queries. diff --git a/starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/starter/impl/common/rabbit/RabbitMQConfigTest.java b/starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/starter/impl/common/rabbit/RabbitMQConfigTest.java index 07a10230..4f9524b9 100644 --- a/starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/starter/impl/common/rabbit/RabbitMQConfigTest.java +++ b/starters/async-rabbit-starter/src/test/java/org/reactivecommons/async/starter/impl/common/rabbit/RabbitMQConfigTest.java @@ -9,7 +9,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.reactivecommons.async.rabbit.ConnectionFactoryCustomizer; import org.reactivecommons.async.rabbit.RabbitMQBrokerProviderFactory; -import org.reactivecommons.async.rabbit.communications.MyOutboundMessage; import org.reactivecommons.async.rabbit.communications.UnroutableMessageHandler; import org.reactivecommons.async.rabbit.communications.UnroutableMessageNotifier; import org.reactivecommons.async.rabbit.communications.UnroutableMessageProcessor; @@ -22,6 +21,7 @@ import org.reactivecommons.async.starter.config.ReactiveCommonsListenersConfig; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import reactor.rabbitmq.OutboundMessage; import reactor.rabbitmq.OutboundMessageResult; import reactor.test.StepVerifier; @@ -50,10 +50,10 @@ class RabbitMQConfigTest { private ConnectionManager manager; @Mock - private OutboundMessageResult resultMock; + private OutboundMessageResult resultMock; @Mock - private MyOutboundMessage outboundMessageMock; + private OutboundMessage outboundMessageMock; @Mock private UnroutableMessageNotifier unroutableMessageNotifier; From cc1a098950ee6d80cf6802dfb0672ef5c9a8bd59 Mon Sep 17 00:00:00 2001 From: luisgomez29 Date: Tue, 24 Mar 2026 11:47:03 -0500 Subject: [PATCH 06/15] refactor: update CloudEventContextWriter to handle numeric values according to spec --- .../java/io/cloudevents/jackson/CloudEventSerializer.java | 8 ++++---- .../test/java/io/cloudevents/jackson/JsonFormatTest.java | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/async/cloudevents-json-jackson/src/main/java/io/cloudevents/jackson/CloudEventSerializer.java b/async/cloudevents-json-jackson/src/main/java/io/cloudevents/jackson/CloudEventSerializer.java index a5109bd3..50795e83 100644 --- a/async/cloudevents-json-jackson/src/main/java/io/cloudevents/jackson/CloudEventSerializer.java +++ b/async/cloudevents-json-jackson/src/main/java/io/cloudevents/jackson/CloudEventSerializer.java @@ -54,12 +54,12 @@ public CloudEventContextWriter withContextAttribute(String name, String value) t @Override @Deprecated(forRemoval = true) public CloudEventContextWriter withContextAttribute(String name, Number value) throws CloudEventRWException { - // Only Integer types are supported by the specification if (value instanceof Integer integer) { - this.withContextAttribute(name, integer); + gen.writeNumberProperty(name, integer); } else { - // Default to string representation for other numeric values - this.withContextAttribute(name, value.toString()); + // Non-integer numeric types (Long, BigDecimal, Float…) are not supported by the + // CloudEvents spec; fall back to their string representation. + gen.writeStringProperty(name, value.toString()); } return this; } diff --git a/async/cloudevents-json-jackson/src/test/java/io/cloudevents/jackson/JsonFormatTest.java b/async/cloudevents-json-jackson/src/test/java/io/cloudevents/jackson/JsonFormatTest.java index 35e925b3..8d6a1bc0 100644 --- a/async/cloudevents-json-jackson/src/test/java/io/cloudevents/jackson/JsonFormatTest.java +++ b/async/cloudevents-json-jackson/src/test/java/io/cloudevents/jackson/JsonFormatTest.java @@ -161,13 +161,13 @@ void throwExpectedOnInvalidSpecversion() { .hasMessageContaining(CloudEventRWException.newInvalidSpecVersion("9000.1").getMessage()); } - @ParameterizedTest - @MethodSource("badJsonContent") /** * JSON content that should fail deserialization * as it represents content that is not CE * specification compliant. */ + @ParameterizedTest + @MethodSource("badJsonContent") void verifyDeserializeError(String inputFile) { byte[] input = loadFile(inputFile); From dae45885ad27284bdafe548edb80a3fb2237ea52 Mon Sep 17 00:00:00 2001 From: luisgomez29 Date: Tue, 24 Mar 2026 12:09:23 -0500 Subject: [PATCH 07/15] refactor: simplify CloudEventDeserializer by consolidating data reading and extension handling --- .../jackson/CloudEventDeserializer.java | 145 +++++++++--------- 1 file changed, 72 insertions(+), 73 deletions(-) diff --git a/async/cloudevents-json-jackson/src/main/java/io/cloudevents/jackson/CloudEventDeserializer.java b/async/cloudevents-json-jackson/src/main/java/io/cloudevents/jackson/CloudEventDeserializer.java index fb88d10f..86088956 100644 --- a/async/cloudevents-json-jackson/src/main/java/io/cloudevents/jackson/CloudEventDeserializer.java +++ b/async/cloudevents-json-jackson/src/main/java/io/cloudevents/jackson/CloudEventDeserializer.java @@ -21,6 +21,7 @@ import io.cloudevents.SpecVersion; import io.cloudevents.core.builder.CloudEventBuilder; import io.cloudevents.core.data.BytesCloudEventData; +import io.cloudevents.rw.CloudEventContextWriter; import io.cloudevents.rw.CloudEventDataMapper; import io.cloudevents.rw.CloudEventRWException; import io.cloudevents.rw.CloudEventReader; @@ -60,14 +61,14 @@ private record JsonMessage(JsonParser p, ObjectNode node, boolean forceExtension boolean forceIgnoreInvalidExtensionNameDeserialization, boolean disableDataContentTypeDefaulting) implements CloudEventReader { + private static final String DATA_BASE64 = "data_base64"; + @Override public , V> V read(CloudEventWriterFactory writerFactory, CloudEventDataMapper mapper) throws CloudEventRWException, IllegalStateException { try { SpecVersion specVersion = SpecVersion.parse(getStringNode(this.node, this.p, "specversion")); CloudEventWriter writer = writerFactory.create(specVersion); - // TODO remove all the unnecessary code specversion aware - // Read mandatory attributes for (String attr : specVersion.getMandatoryAttributes()) { if (!"specversion".equals(attr)) { @@ -76,7 +77,7 @@ public , V> V read(CloudEventWriterFactory w } // Parse datacontenttype if any - String contentType = getOptionalStringNode(this.node, this.p, "datacontenttype"); + String contentType = getOptionalStringNode(this.node, "datacontenttype"); if (!this.disableDataContentTypeDefaulting && contentType == null && this.node.has("data")) { contentType = "application/json"; } @@ -86,105 +87,103 @@ public , V> V read(CloudEventWriterFactory w // Read optional attributes for (String attr : specVersion.getOptionalAttributes()) { - if (!"datacontentencoding".equals(attr)) { // Skip datacontentencoding, we need it later - String val = getOptionalStringNode(this.node, this.p, attr); - if (val != null) { - writer.withContextAttribute(attr, val); - } + String val = getOptionalStringNode(this.node, attr); + if (val != null) { + writer.withContextAttribute(attr, val); } } - CloudEventData data = null; - - // Now let's handle the data (V1 only) - if (node.has("data_base64") && node.has("data")) { - throw MismatchedInputException.from(p, CloudEvent.class, "CloudEvent cannot have both 'data' and 'data_base64' fields"); - } - if (node.has("data_base64")) { - data = BytesCloudEventData.wrap(node.remove("data_base64").binaryValue()); - } else if (node.has("data")) { - if (JsonFormat.dataIsJsonContentType(contentType)) { - // This solution is quite bad, but i see no alternatives now. - // Hopefully in future we can improve it - data = JsonCloudEventData.wrap(node.remove("data")); - } else { - JsonNode dataNode = node.remove("data"); - assertNodeType(dataNode, JsonNodeType.STRING, "data", "Because content type is not a json, only a string is accepted as data"); - data = BytesCloudEventData.wrap(dataNode.asString().getBytes(StandardCharsets.UTF_8)); - } - } - - // Now let's process the extensions - node.properties().forEach(entry -> { - String extensionName = entry.getKey(); - if (this.forceExtensionNameLowerCaseDeserialization) { - extensionName = extensionName.toLowerCase(); - } - - if (this.shouldSkipExtensionName(extensionName)) { - return; - } - - JsonNode extensionValue = entry.getValue(); - - switch (extensionValue.getNodeType()) { - case BOOLEAN: - writer.withContextAttribute(extensionName, extensionValue.booleanValue()); - break; - case NUMBER: - - final Number numericValue = extensionValue.numberValue(); - - // Only 'Int' values are supported by the specification - - if (numericValue instanceof Integer integer) { - writer.withContextAttribute(extensionName, integer); - } else { - throw CloudEventRWException.newInvalidAttributeType(extensionName, numericValue); - } - - break; - case STRING: - writer.withContextAttribute(extensionName, extensionValue.asString()); - break; - default: - writer.withContextAttribute(extensionName, extensionValue.toString()); - } - - }); + CloudEventData data = readData(contentType); + readExtensions(writer); if (data != null) { return writer.end(mapper.map(data)); } return writer.end(); } catch (IllegalArgumentException e) { - throw new RuntimeException(MismatchedInputException.from(this.p, CloudEvent.class, e.getMessage())); + throw MismatchedInputException.from(this.p, CloudEvent.class, e.getMessage()); + } + } + + private CloudEventData readData(String contentType) { + if (node.has(DATA_BASE64) && node.has("data")) { + throw MismatchedInputException.from(p, CloudEvent.class, + "CloudEvent cannot have both 'data' and '" + DATA_BASE64 + "' fields"); + } + if (node.has(DATA_BASE64)) { + return BytesCloudEventData.wrap(node.remove(DATA_BASE64).binaryValue()); + } + if (node.has("data")) { + if (JsonFormat.dataIsJsonContentType(contentType)) { + // This solution is quite bad, but i see no alternatives now. + // Hopefully in future we can improve it + return JsonCloudEventData.wrap(node.remove("data")); + } + JsonNode dataNode = node.remove("data"); + assertNodeIsString(dataNode, "data", "Because content type is not a json, only a string is accepted as data"); + return BytesCloudEventData.wrap(dataNode.asString().getBytes(StandardCharsets.UTF_8)); + } + return null; + } + + private void readExtensions(CloudEventContextWriter writer) { + node.properties().forEach(entry -> { + String extensionName = entry.getKey(); + if (this.forceExtensionNameLowerCaseDeserialization) { + extensionName = extensionName.toLowerCase(); + } + if (this.shouldSkipExtensionName(extensionName)) { + return; + } + writeExtensionAttribute(writer, extensionName, entry.getValue()); + }); + } + + private void writeExtensionAttribute(CloudEventContextWriter writer, String extensionName, JsonNode extensionValue) { + switch (extensionValue.getNodeType()) { + case BOOLEAN: + writer.withContextAttribute(extensionName, extensionValue.booleanValue()); + break; + case NUMBER: + final Number numericValue = extensionValue.numberValue(); + if (numericValue instanceof Integer integer) { + writer.withContextAttribute(extensionName, integer); + } else { + throw CloudEventRWException.newInvalidAttributeType(extensionName, numericValue); + } + break; + case STRING: + writer.withContextAttribute(extensionName, extensionValue.asString()); + break; + default: + writer.withContextAttribute(extensionName, extensionValue.toString()); } } private String getStringNode(ObjectNode objNode, JsonParser p, String attributeName) { - String val = getOptionalStringNode(objNode, p, attributeName); + String val = getOptionalStringNode(objNode, attributeName); if (val == null) { throw MismatchedInputException.from(p, CloudEvent.class, "Missing mandatory " + attributeName + " attribute"); } return val; } - private String getOptionalStringNode(ObjectNode objNode, JsonParser p, String attributeName) { + private String getOptionalStringNode(ObjectNode objNode, String attributeName) { JsonNode unparsedAttribute = objNode.remove(attributeName); if (unparsedAttribute == null || unparsedAttribute instanceof NullNode) { return null; } - assertNodeType(unparsedAttribute, JsonNodeType.STRING, attributeName, null); + assertNodeIsString(unparsedAttribute, attributeName, null); return unparsedAttribute.asString(); } - private void assertNodeType(JsonNode node, JsonNodeType type, String attributeName, String desc) { - if (node.getNodeType() != type) { + private void assertNodeIsString(JsonNode node, String attributeName, String desc) { + if (node.getNodeType() != JsonNodeType.STRING) { throw MismatchedInputException.from( p, CloudEvent.class, - "Wrong type " + node.getNodeType() + " for attribute " + attributeName + ", expecting " + type + (desc != null ? ". " + desc : "") + "Wrong type " + node.getNodeType() + " for attribute " + attributeName + + ", expecting " + JsonNodeType.STRING + (desc != null ? ". " + desc : "") ); } } From 5ce58fb2d904232c03bacdc33a569327b097b3e0 Mon Sep 17 00:00:00 2001 From: luisgomez29 Date: Tue, 24 Mar 2026 14:30:22 -0500 Subject: [PATCH 08/15] docs: update README to include Reactor RabbitMQ module and original library details --- README.md | 46 +++--- async/cloudevents-json-jackson/README.md | 7 +- async/reactor-rabbitmq/README.md | 20 +++ reactor-rabbitmq-removal.md | 197 ----------------------- 4 files changed, 53 insertions(+), 217 deletions(-) create mode 100644 async/reactor-rabbitmq/README.md delete mode 100644 reactor-rabbitmq-removal.md diff --git a/README.md b/README.md index cb67cd89..5e00604c 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,39 @@ -![](https://github.com/reactive-commons/reactive-commons-java/workflows/reactive-commons-ci-cd/badge.svg) -[![Reactor RabbitMQ](https://maven-badges.herokuapp.com/maven-central/org.reactivecommons/async-commons-rabbit-starter/badge.svg)](https://mvnrepository.com/artifact/org.reactivecommons/async-commons-rabbit-starter) +![CI/CD](https://github.com/reactive-commons/reactive-commons-java/workflows/reactive-commons-ci-cd/badge.svg) +[![Maven Central](https://img.shields.io/maven-central/v/org.reactivecommons/async-commons-rabbit-starter)](https://central.sonatype.com/artifact/org.reactivecommons/async-commons-rabbit-starter) + # reactive-commons-java + The purpose of reactive-commons is to provide a set of abstractions and implementations over different patterns and practices that make the foundation of a reactive microservices architecture. -Docs: [https://bancolombia.github.io/reactive-commons-java/](https://bancolombia.github.io/reactive-commons-java) +Even though the main purpose is to provide such abstractions in a mostly generic way, they would be of little use without a concrete implementation. So we provide implementations in a best-effort manner that aim to be easy to change, personalize, and extend. + +The first approach to this work was to release simple abstractions and a corresponding implementation over asynchronous message-driven communication between microservices, built on top of Project Reactor and Spring Boot. + +--- + +## Documentation + +**Full documentation is available at:** -Other projects: https://github.com/bancolombia +> ### 👉 [https://bancolombia.github.io/reactive-commons-java/](https://bancolombia.github.io/reactive-commons-java) -Sponsor by: https://medium.com/bancolombia-tech +--- -Even though the main purpose is to provide such abstractions in a mostly generic way such abstractions would be of little use without a concrete implementation so we provide some implementations in a best effors maner that aim to be easy to change, personalize and extend. +## Related -The first approach to this work was to release a very simple abstractions and a corresponding implementation over asyncronous message driven communication between microservices build on top of project-reactor and spring boot. +> - **Other projects:** [https://github.com/bancolombia](https://github.com/bancolombia) +> - **Sponsored by:** [Bancolombia Tech](https://medium.com/bancolombia-tech) +--- -# Actualizacón Reactive Commons -Este es el proyecto Reactive Commos el cual es una libreria utilizada para la conexión al broker de RabbitMQ y Kafka. Esta usando las dependencias de Spring Boot 4, Reactor y Reactor-RabbitMQ #fetch https://github.com/spring-attic/reactor-rabbitmq el cual esta deprecado. +## Third-Party Code Credits -# Requerimientos -Necesito actualizar la implementación de Reactor-RabbitMQ por RabbitMQ Client for Eclipse Vert.x #fetch https://vertx.io/docs/vertx-rabbitmq-client/java/. Para esto primero entender la documentaación completa del proyecto que esta en la carpeta /docs que esta realizada con docusaurus para identificar que partes van a cambiar. Deberia cambiar por ejemplo para escuchar colas (8-handling-queues.md) la configuración para ### Listening queues with custom topology, los ejemplos que estan en ## Queue configuration examples. +This project includes source code internalized from the following open-source libraries: -# Dependencias -- Reemplazar Reactor-RabbitMQ #fetch https://github.com/spring-attic/reactor-rabbitmq por RabbitMQ Client for Eclipse Vert.x -- Mantener depedncias de Spring Boot 4, Reactor core, Jackson 3 -- Tener en cuenta las guias de migración para Spring Boot 4 #fetch https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-4.0-Migration-Guide Jackson 3. #fetch https://github.com/FasterXML/jackson/blob/main/jackson3/MIGRATING_TO_JACKSON_3.md +### reactor-rabbitmq +- **Repository:** [https://github.com/spring-attic/reactor-rabbitmq](https://github.com/spring-attic/reactor-rabbitmq) +- **License:** [Apache License 2.0](https://github.com/spring-attic/reactor-rabbitmq/blob/main/LICENSE) -# Verificación de Cambios -- Realizar los cambios y verificar que el proyecto compile y se publique correctamente en Maven Local. Ejecutar el comando de Gradle por ejemplo `./gradlew clean publishToMavenLocal` e ignorar las pruebas unitarias. -- Una vez el se publique la versíon en Maven Local ingresar a la ruta /Users/lugomez/repos/poc-reactive-commons/ms_sender y ejecutar el microservicio para verificar los cambios realizados. El microservicio debe inciar correctamente y verificar que no debe generar logs de error y/o excepciones. -- La documentación se esta generando con Docusaurus. Actualizar la documentación de las secciones que cambian agregando por ejemplo un Tab para indicar la documentación actual con la versión 7 y la documentación con la versión 8. Actualizar Guia de migración siguiendo el estandar de las otras versiones en el archivo migration-guides.md. Ejecutar el comando `npm run build` para asegurarse que los cambios realizados no generan errores de compilación. +### CloudEvents JSON Jackson +- **Repository:** [https://github.com/cloudevents/sdk-java](https://github.com/cloudevents/sdk-java) +- **License:** [Apache License 2.0](https://github.com/cloudevents/sdk-java/blob/main/LICENSE) diff --git a/async/cloudevents-json-jackson/README.md b/async/cloudevents-json-jackson/README.md index fd978020..483eba9d 100644 --- a/async/cloudevents-json-jackson/README.md +++ b/async/cloudevents-json-jackson/README.md @@ -1,6 +1,11 @@ # CloudEvents JSON Jackson Module -This module was obtained from the official CloudEvents SDK Java repository at [https://github.com/cloudevents/sdk-java/blob/main/formats/json-jackson/](https://github.com/cloudevents/sdk-java/blob/main/formats/json-jackson/). +This module is based on the source code from the official CloudEvents SDK Java repository. + +## Original Library + +- **Repository:** [https://github.com/cloudevents/sdk-java](https://github.com/cloudevents/sdk-java) +- **License:** [Apache License 2.0](https://github.com/cloudevents/sdk-java/blob/main/LICENSE) ## Changes from Original diff --git a/async/reactor-rabbitmq/README.md b/async/reactor-rabbitmq/README.md new file mode 100644 index 00000000..9250d679 --- /dev/null +++ b/async/reactor-rabbitmq/README.md @@ -0,0 +1,20 @@ +# Reactor RabbitMQ Module + +This module is based on the source code from the archived official [reactor-rabbitmq](https://github.com/spring-attic/reactor-rabbitmq) library by VMware Inc. / Spring Attic. + +## Original Library + +- **Repository:** [https://github.com/spring-attic/reactor-rabbitmq](https://github.com/spring-attic/reactor-rabbitmq) +- **License:** [Apache License 2.0](https://github.com/spring-attic/reactor-rabbitmq/blob/main/LICENSE) + +The original library was archived by its maintainers on September 26, 2025 and is no longer actively maintained. The source code was internalized into this project to allow continued use and maintenance as a dependency. + +## Changes from Original + +- Upgraded from Java 8/11 to Java 17 +- Removed dead code and unused APIs +- Replaced `volatile` fields with thread-safe types (`AtomicReference`, `AtomicBoolean`) +- `OutboundMessage` extended with optional `ackNotifier` callback, eliminating the need for a separate subclass +- Java 17 modernizations (records, sealed classes patterns, text blocks) +- SLF4J and SonarQube warning fixes +- Adapted for compatibility with the reactive-commons-java project diff --git a/reactor-rabbitmq-removal.md b/reactor-rabbitmq-removal.md deleted file mode 100644 index 720d9a23..00000000 --- a/reactor-rabbitmq-removal.md +++ /dev/null @@ -1,197 +0,0 @@ -# Eliminación de reactor-rabbitmq: Justificación y Diseño - -## Contexto - -A partir de la versión **8.0.0** de Reactive Commons, se eliminó la dependencia `io.projectreactor.rabbitmq:reactor-rabbitmq:1.5.6` y se reemplazó con clases bridge propias que envuelven directamente `com.rabbitmq:amqp-client`. - -Este documento explica las razones técnicas, las alternativas evaluadas y las consideraciones de diseño. - ---- - -## ¿Por qué se eliminó reactor-rabbitmq? - -### 1. Proyecto archivado y sin mantenimiento - -El repositorio [`reactor/reactor-rabbitmq`](https://github.com/reactor/reactor-rabbitmq) fue **archivado en 2023**. Esto significa: - -- No recibe correcciones de seguridad -- No recibe actualizaciones de compatibilidad -- No se aceptan pull requests ni issues -- No hay roadmap de futuras versiones - -### 2. Incompatibilidad con el stack actual - -| Dependencia | Versión en reactor-rabbitmq 1.5.6 | Versión en Reactive Commons 8.x | -|---|---|---| -| Reactor Core | 3.4.x | 3.7.x+ (Spring Boot 4.0.2) | -| amqp-client | 5.16.x | 5.28.0 | -| Java | 8-17 | 17-21 | - -Estas diferencias generan riesgo de: - -- Incompatibilidades binarias silenciosas -- Comportamientos inesperados en runtime -- Bugs que no serán corregidos upstream - -### 3. Complejidad innecesaria - -reactor-rabbitmq es una librería de ~15,000 líneas que incluye: - -- `ChannelPool` y `ChannelPoolFactory` con lógica de cache compleja -- `FluxSink`-based publishing con `ExecutorService` dedicados -- Manejo de `ResourceManagementChannel` con proxies dinámicos -- Soporte para features que Reactive Commons no utiliza - -El proyecto solo usa ~10% de la API expuesta. - ---- - -## Alternativas evaluadas - -### Opción 1: Mantener reactor-rabbitmq (descartada) - -**Pros:** -- Cero esfuerzo de migración - -**Contras:** -- Dependencia muerta sin fixes de seguridad -- Riesgo creciente de incompatibilidad con cada actualización de Spring Boot / Reactor -- No hay soporte oficial para Java 21+ - -### Opción 2: Usar Vert.x RabbitMQ Client (descartada) - -Se evaluó `io.vertx:vertx-rabbitmq-client:5.0.8` como reemplazo. - -**Pros:** -- Proyecto activo con releases frecuentes -- API reactiva nativa (basada en `Future`) - -**Contras:** -- **SSL/TLS problemático**: Vert.x no carga automáticamente el trust store de la JVM (`cacerts`). Requiere configuración explícita de `JksOptions`/`PfxOptions`, lo que causa fallos de conexión con brokers como **AWS Amazon MQ** que usan certificados de CAs públicas (Amazon Trust Services) -- **Overhead innecesario**: Agrega Vert.x Core (~1.5MB) + vertx-rabbitmq-client al classpath -- **Bridging costoso**: Requiere convertir `io.vertx.core.Future` → `CompletionStage` → `Mono`, agregando overhead y complejidad en stack traces -- **Modelo de concurrencia diferente**: El event loop de Vert.x es un paradigma ajeno al modelo Spring Boot + Reactor del proyecto -- **Canal único**: `RabbitMQClient` de Vert.x maneja un solo canal interno, limitando la concurrencia comparado con el modelo multi-canal de amqp-client - -### Opción 3: Clases bridge sobre amqp-client + Netty (seleccionada) - -**Pros:** -- **SSL funciona out-of-the-box**: `SslContextBuilder.forClient()` de Netty + `TrustManagerFactory.init(null)` carga automáticamente los cacerts de la JVM -- **Cero dependencias adicionales**: `com.rabbitmq:amqp-client` ya era dependencia transitiva -- **Control total**: Código propio, corregible en minutos -- **Alineación con el ecosistema**: `ConnectionFactory` es el estándar de Spring AMQP y RabbitMQ -- **Transporte eficiente**: Netty NIO compartido via `EventLoopGroup`, [documentado oficialmente por RabbitMQ](https://www.rabbitmq.com/client-libraries/java-api-guide#netty) - -**Contras:** -- ~410 líneas de código propio a mantener (riesgo bajo dado que la API de amqp-client es estable) - ---- - -## Diseño de las clases bridge - -### Arquitectura - -``` -┌─────────────────────────────────────────────────────┐ -│ Reactive Commons │ -│ (ReactiveMessageSender, Listeners, etc.) │ -├─────────────────────────────────────────────────────┤ -│ Clases Bridge │ -│ Sender, Receiver, AcknowledgableDelivery, etc. │ -│ (~410 líneas, API reactiva Mono/Flux) │ -├─────────────────────────────────────────────────────┤ -│ com.rabbitmq:amqp-client │ -│ ConnectionFactory, Channel, Connection │ -│ + Netty NIO Transport (SSL) │ -└─────────────────────────────────────────────────────┘ -``` - -### Clases creadas - -| Clase | Líneas | Reemplaza | Responsabilidad | -|---|---|---|---| -| `Sender` | ~130 | `reactor.rabbitmq.Sender` | Declarar topología (exchanges, queues, bindings) y publicar mensajes con/sin publisher confirms | -| `Receiver` | ~120 | `reactor.rabbitmq.Receiver` | Consumir mensajes con manual ack o auto ack, cada consumidor obtiene su propio `Channel` | -| `AcknowledgableDelivery` | ~40 | `reactor.rabbitmq.AcknowledgableDelivery` | Extiende `Delivery` con `ack()` y `nack(boolean)` sobre el `Channel` del consumidor | -| `OutboundMessage` | ~10 | `reactor.rabbitmq.OutboundMessage` | POJO: exchange + routingKey + properties + body | -| `OutboundMessageResult` | ~10 | `reactor.rabbitmq.OutboundMessageResult` | POJO: outboundMessage + ack + returned | -| `ExchangeSpecification` | ~25 | `reactor.rabbitmq.ExchangeSpecification` | Builder para declarar exchanges | -| `QueueSpecification` | ~30 | `reactor.rabbitmq.QueueSpecification` | Builder para declarar queues | -| `BindingSpecification` | ~15 | `reactor.rabbitmq.BindingSpecification` | Factory para crear bindings | -| `ResourcesSpecification` | ~15 | `reactor.rabbitmq.ResourcesSpecification` | Factory estática: `exchange()`, `queue()`, `binding()` | -| `ConsumeOptions` | ~15 | `reactor.rabbitmq.ConsumeOptions` | Configuración de QoS y consumer tag | - -### Principios de diseño - -1. **Mínima superficie**: Solo se implementa lo que Reactive Commons realmente usa -2. **Channel-per-operation**: Cada operación de topología o publish abre un `Channel`, ejecuta, y cierra. Los consumidores mantienen su canal abierto -3. **Publisher Confirms atómicos**: `confirmSelect()` + `basicPublish()` + `waitForConfirmsOrDie()` en un solo canal dedicado -4. **Scheduling explícito**: Operaciones bloqueantes de amqp-client se ejecutan en `Schedulers.boundedElastic()` para no bloquear el event loop de Reactor -5. **Conexión compartida**: Una sola `Mono.cache()` con retry exponencial por `ConnectionFactory` - ---- - -## Consideraciones adicionales - -### Gestión de conexiones - -- Se usa un `ConcurrentMap>` como cache para reutilizar la conexión -- La conexión utiliza retry con backoff exponencial (`300ms` inicial, `3000ms` máximo) para reconexión automática -- El `Connection Name` incluye el `appName` + `InstanceIdentifier` para trazabilidad en el management UI de RabbitMQ - -### Transporte Netty - -- Se comparte un único `EventLoopGroup` (NIO) entre todas las conexiones via `SHARED_EVENT_LOOP_GROUP` -- El lifecycle del `EventLoopGroup` se gestiona con `EventLoopGroupLifecycleManager` (Spring `SmartLifecycle`) para shutdown limpio -- Referencia: [RabbitMQ Java API Guide - Use of Netty for Network I/O](https://www.rabbitmq.com/client-libraries/java-api-guide#netty) - -### SSL/TLS - -- Se usa `SslContextBuilder.forClient()` de Netty (no el `SSLContext` de JDK) -- `TrustManagerFactory.init(null)` carga automáticamente los certificados del trust store de la JVM -- Compatible con AWS Amazon MQ, CloudAMQP, y cualquier broker con certificados de CAs públicas sin configuración adicional -- Soporta trust store y key store explícitos vía propiedades `ssl.trustStore`, `ssl.keyStore` -- Hostname verification habilitada por defecto vía `factory.enableHostnameVerification()` - -### Testing - -- Las clases bridge no rompen la interfaz pública de Reactive Commons — `ReactiveMessageSender`, `ReactiveMessageListener`, `TopologyCreator` mantienen sus firmas -- Los tests de integración existentes (`acceptance/async-tests`) siguen funcionando sin cambios -- Los consumidores de la librería solo necesitan actualizar imports si referenciaban directamente tipos de `reactor.rabbitmq` (ver [Guía de Migración](migration-guides.md)) - -### Monitoreo y observabilidad - -- `reactor-core-micrometer` sigue instrumentando las operaciones reactivas (Mono/Flux) -- Las métricas de amqp-client (`MicrometerMetricsCollector`) están disponibles si se necesitan métricas a nivel de conexión/canal -- Los nombres de conexión incluyen el `appName` para identificación en RabbitMQ Management - -### Riesgos y mitigaciones - -| Riesgo | Probabilidad | Mitigación | -|---|---|---| -| Cambio en API de amqp-client Channel | Muy baja — API estable desde hace 10+ años | Las clases bridge son simples de actualizar | -| Leak de canales | Baja — cada operación usa try/finally | Monitorear canal count en RabbitMQ Management | -| Bloqueo en `waitForConfirmsOrDie` | Baja — timeout de 5s configurado | Schedulers.boundedElastic() evita bloquear event loop | -| Cambio en Netty SslContext API | Muy baja — API estable | Spring Boot BOM gestiona versiones de Netty | - -### Futuras mejoras posibles - -1. **Channel pooling**: Para escenarios de alto throughput, se podría implementar un pool de canales en `Sender` en lugar de crear uno por operación -2. **Batch confirms**: Agrupar múltiples publishes en un solo canal con confirm para reducir round-trips -3. **Métricas propias**: Instrumentar `Sender` y `Receiver` con Micrometer para métricas específicas de publish/consume -4. **Connection recovery**: Evaluar si `ConnectionFactory.setAutomaticRecoveryEnabled(true)` complementa o reemplaza el retry de `Mono` - ---- - -## Resumen de cambios en dependencias - -```diff -# async-rabbit.gradle - -- api 'io.projectreactor.rabbitmq:reactor-rabbitmq:1.5.6' -+ # Eliminada: reactor-rabbitmq (deprecated/archived) -+ # Se usan clases bridge propias sobre com.rabbitmq:amqp-client - api 'com.rabbitmq:amqp-client' -``` - -No se agregan nuevas dependencias. Se elimina una dependencia deprecada. From 016eee8f368f34f4e28e8eb56952150edc085c41 Mon Sep 17 00:00:00 2001 From: luisgomez29 Date: Wed, 25 Mar 2026 16:20:13 -0500 Subject: [PATCH 09/15] chore: delete starter async-rabbit-standalone --- gradle.properties | 2 +- .../async-commons-rabbit-standalone.gradle | 8 --- .../config/DirectAsyncGatewayConfig.java | 70 ------------------- .../standalone/config/EventBusConfig.java | 20 ------ .../standalone/config/RabbitProperties.java | 13 ---- .../src/test/java/.gitkeep | 0 6 files changed, 1 insertion(+), 112 deletions(-) delete mode 100644 starters/async-rabbit-standalone/async-commons-rabbit-standalone.gradle delete mode 100644 starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/standalone/config/DirectAsyncGatewayConfig.java delete mode 100644 starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/standalone/config/EventBusConfig.java delete mode 100644 starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/standalone/config/RabbitProperties.java delete mode 100644 starters/async-rabbit-standalone/src/test/java/.gitkeep diff --git a/gradle.properties b/gradle.properties index 560ae34a..fb84a793 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ version=7.0.7 -toPublish=domain-events-api,async-commons-api,async-commons,cloudevents-json-jackson,reactor-rabbitmq,async-rabbit,async-commons-rabbit-standalone,async-commons-rabbit-starter,async-kafka,async-kafka-starter,async-commons-starter +toPublish=domain-events-api,async-commons-api,async-commons,cloudevents-json-jackson,reactor-rabbitmq,async-rabbit,async-commons-rabbit-starter,async-kafka,async-kafka-starter,async-commons-starter onlyUpdater=true \ No newline at end of file diff --git a/starters/async-rabbit-standalone/async-commons-rabbit-standalone.gradle b/starters/async-rabbit-standalone/async-commons-rabbit-standalone.gradle deleted file mode 100644 index 1306f86e..00000000 --- a/starters/async-rabbit-standalone/async-commons-rabbit-standalone.gradle +++ /dev/null @@ -1,8 +0,0 @@ -ext { - artifactId = 'async-commons-rabbit-standalone' - artifactDescription = 'Async Commons Standalone Config' -} - -dependencies { - api project(':async-rabbit') -} \ No newline at end of file diff --git a/starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/standalone/config/DirectAsyncGatewayConfig.java b/starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/standalone/config/DirectAsyncGatewayConfig.java deleted file mode 100644 index 0beea22f..00000000 --- a/starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/standalone/config/DirectAsyncGatewayConfig.java +++ /dev/null @@ -1,70 +0,0 @@ -package org.reactivecommons.async.rabbit.standalone.config; - -import io.micrometer.core.instrument.MeterRegistry; -import lombok.RequiredArgsConstructor; -import org.reactivecommons.async.commons.config.BrokerConfig; -import org.reactivecommons.async.commons.converters.MessageConverter; -import org.reactivecommons.async.commons.reply.ReactiveReplyRouter; -import org.reactivecommons.async.rabbit.RabbitDirectAsyncGateway; -import org.reactivecommons.async.rabbit.communications.ReactiveMessageListener; -import org.reactivecommons.async.rabbit.communications.ReactiveMessageSender; -import org.reactivecommons.async.rabbit.listeners.ApplicationReplyListener; - -import java.nio.ByteBuffer; -import java.util.Base64; -import java.util.UUID; - -@RequiredArgsConstructor -public class DirectAsyncGatewayConfig { - - private String directMessagesExchangeName; - private String globalReplyExchangeName; - private String appName; - - - public RabbitDirectAsyncGateway rabbitDirectAsyncGateway(BrokerConfig config, ReactiveReplyRouter router, - ReactiveMessageSender rSender, MessageConverter converter, - MeterRegistry meterRegistry) { - return new RabbitDirectAsyncGateway(config, router, rSender, directMessagesExchangeName, converter, - meterRegistry); - } - - public ApplicationReplyListener msgListener(ReactiveReplyRouter router, BrokerConfig config, - ReactiveMessageListener listener, boolean createTopology) { - final ApplicationReplyListener replyListener = new ApplicationReplyListener(router, listener, generateName(), - globalReplyExchangeName, createTopology); - replyListener.startListening(config.getRoutingKey()); - return replyListener; - } - - - public BrokerConfig brokerConfig() { - return new BrokerConfig(); - } - - - public ReactiveReplyRouter router() { - return new ReactiveReplyRouter(); - } - - public String generateName() { - UUID uuid = UUID.randomUUID(); - ByteBuffer bb = ByteBuffer.wrap(new byte[16]); - bb.putLong(uuid.getMostSignificantBits()) - .putLong(uuid.getLeastSignificantBits()); - // Convert to base64 and remove trailing = - return this.appName + encodeToUrlSafeString(bb.array()) - .replace("=", ""); - } - - public static String encodeToUrlSafeString(byte[] src) { - return new String(encodeUrlSafe(src)); - } - - public static byte[] encodeUrlSafe(byte[] src) { - if (src.length == 0) { - return src; - } - return Base64.getUrlEncoder().encode(src); - } -} diff --git a/starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/standalone/config/EventBusConfig.java b/starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/standalone/config/EventBusConfig.java deleted file mode 100644 index 601b43b6..00000000 --- a/starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/standalone/config/EventBusConfig.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.reactivecommons.async.rabbit.standalone.config; - -import lombok.RequiredArgsConstructor; -import org.reactivecommons.api.domain.DomainEventBus; -import org.reactivecommons.async.rabbit.RabbitDomainEventBus; -import org.reactivecommons.async.rabbit.communications.ReactiveMessageSender; - -import static org.reactivecommons.async.rabbit.communications.ResourcesSpecification.exchange; - - -@RequiredArgsConstructor -public class EventBusConfig { - - private final String domainEventsExchangeName; - - public DomainEventBus domainEventBus(ReactiveMessageSender sender) { - sender.getTopologyCreator().declare(exchange(domainEventsExchangeName).durable(true).type("topic")).subscribe(); - return new RabbitDomainEventBus(sender, domainEventsExchangeName); - } -} diff --git a/starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/standalone/config/RabbitProperties.java b/starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/standalone/config/RabbitProperties.java deleted file mode 100644 index 93c9a1f3..00000000 --- a/starters/async-rabbit-standalone/src/main/java/org/reactivecommons/async/rabbit/standalone/config/RabbitProperties.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.reactivecommons.async.rabbit.standalone.config; - -import lombok.Data; - -@Data -public class RabbitProperties { - private String host = "localhost"; - private int port = 5672; - private String username = "guest"; - private String password = "guest"; //NOSONAR - private String virtualHost; - private Integer channelPoolMaxCacheSize; -} diff --git a/starters/async-rabbit-standalone/src/test/java/.gitkeep b/starters/async-rabbit-standalone/src/test/java/.gitkeep deleted file mode 100644 index e69de29b..00000000 From a95581d52d897dc5a2f496a3de7d4b77528ab9f1 Mon Sep 17 00:00:00 2001 From: luisgomez29 Date: Wed, 25 Mar 2026 16:41:25 -0500 Subject: [PATCH 10/15] refactor: update resource specifications to use reactor.rabbitmq package --- .../ResourcesSpecification.java | 26 ------------- .../ApplicationNotificationListener.java | 6 +-- .../listeners/ApplicationReplyListener.java | 6 +-- .../rabbitmq/ResourcesSpecification.java | 37 +++++++++++++++++++ .../async/rabbit/RabbitMQBrokerProvider.java | 2 +- 5 files changed, 44 insertions(+), 33 deletions(-) delete mode 100644 async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/ResourcesSpecification.java create mode 100644 async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/ResourcesSpecification.java diff --git a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/ResourcesSpecification.java b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/ResourcesSpecification.java deleted file mode 100644 index cf2ef37a..00000000 --- a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/ResourcesSpecification.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.reactivecommons.async.rabbit.communications; - -import lombok.AccessLevel; -import lombok.NoArgsConstructor; -import reactor.rabbitmq.BindingSpecification; -import reactor.rabbitmq.ExchangeSpecification; -import reactor.rabbitmq.QueueSpecification; - -/** - * Convenience factory for creating topology specification objects. - */ -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public final class ResourcesSpecification { - - public static ExchangeSpecification exchange(String name) { - return ExchangeSpecification.exchange(name); - } - - public static QueueSpecification queue(String name) { - return QueueSpecification.queue(name); - } - - public static BindingSpecification binding(String exchange, String routingKey, String queue) { - return BindingSpecification.queueBinding(exchange, routingKey, queue); - } -} diff --git a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/ApplicationNotificationListener.java b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/ApplicationNotificationListener.java index 6b1ab453..690df453 100644 --- a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/ApplicationNotificationListener.java +++ b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/ApplicationNotificationListener.java @@ -19,9 +19,9 @@ import java.util.function.Function; -import static org.reactivecommons.async.rabbit.communications.ResourcesSpecification.binding; -import static org.reactivecommons.async.rabbit.communications.ResourcesSpecification.exchange; -import static org.reactivecommons.async.rabbit.communications.ResourcesSpecification.queue; +import static reactor.rabbitmq.ResourcesSpecification.binding; +import static reactor.rabbitmq.ResourcesSpecification.exchange; +import static reactor.rabbitmq.ResourcesSpecification.queue; import static reactor.core.publisher.Flux.fromIterable; @Log diff --git a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/ApplicationReplyListener.java b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/ApplicationReplyListener.java index 670e9e7e..42ca98a7 100644 --- a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/ApplicationReplyListener.java +++ b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/ApplicationReplyListener.java @@ -18,9 +18,9 @@ import static org.reactivecommons.async.commons.Headers.COMPLETION_ONLY_SIGNAL; import static org.reactivecommons.async.commons.Headers.CORRELATION_ID; -import static org.reactivecommons.async.rabbit.communications.ResourcesSpecification.binding; -import static org.reactivecommons.async.rabbit.communications.ResourcesSpecification.exchange; -import static org.reactivecommons.async.rabbit.communications.ResourcesSpecification.queue; +import static reactor.rabbitmq.ResourcesSpecification.binding; +import static reactor.rabbitmq.ResourcesSpecification.exchange; +import static reactor.rabbitmq.ResourcesSpecification.queue; @Log diff --git a/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/ResourcesSpecification.java b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/ResourcesSpecification.java new file mode 100644 index 00000000..d7ed829b --- /dev/null +++ b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/ResourcesSpecification.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.rabbitmq; + + +/** + * API that combines {@link ExchangeSpecification}, {@link QueueSpecification}, and {@link BindingSpecification}. + */ +public class ResourcesSpecification { + + public static ExchangeSpecification exchange(String name) { + return ExchangeSpecification.exchange(name); + } + + public static QueueSpecification queue(String name) { + return QueueSpecification.queue(name); + } + + public static BindingSpecification binding(String exchange, String routingKey, String queue) { + return BindingSpecification.binding(exchange, routingKey, queue); + } + +} \ No newline at end of file diff --git a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/RabbitMQBrokerProvider.java b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/RabbitMQBrokerProvider.java index 96848890..f9702b9d 100644 --- a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/RabbitMQBrokerProvider.java +++ b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/RabbitMQBrokerProvider.java @@ -24,7 +24,7 @@ import org.reactivecommons.async.starter.config.health.RCHealth; import reactor.core.publisher.Mono; -import static org.reactivecommons.async.rabbit.communications.ResourcesSpecification.exchange; +import static reactor.rabbitmq.ResourcesSpecification.exchange; @Log public record RabbitMQBrokerProvider(String domain, From 8ff147fbb00eae1adc6ea4e71d9f9d93232e1509 Mon Sep 17 00:00:00 2001 From: luisgomez29 Date: Wed, 25 Mar 2026 17:16:20 -0500 Subject: [PATCH 11/15] Revert "refactor: replace volatile FluxSink with AtomicReference in ReactiveMessageSender" This reverts commit e69f58c5da2c717203157f7b5a8caa5159a52f8a. --- .../rabbit/communications/ReactiveMessageSender.java | 9 +++++---- .../communications/ReactiveMessageSenderTest.java | 11 +++-------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/ReactiveMessageSender.java b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/ReactiveMessageSender.java index de2db3cb..70d5cfcf 100644 --- a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/ReactiveMessageSender.java +++ b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/ReactiveMessageSender.java @@ -23,7 +23,6 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import static org.reactivecommons.async.api.DirectAsyncGateway.DELAYED; @@ -43,7 +42,7 @@ public class ReactiveMessageSender { private static final int NUMBER_OF_SENDER_SUBSCRIPTIONS = 4; private final CopyOnWriteArrayList> fluxSinkConfirm = new CopyOnWriteArrayList<>(); - private final AtomicReference> fluxSinkNoConfirm = new AtomicReference<>(); + private volatile FluxSink fluxSinkNoConfirm; private final AtomicLong counter = new AtomicLong(); private final ExecutorService executorService = Executors.newFixedThreadPool( @@ -79,7 +78,9 @@ private void initializeSenders() { }).subscribe(); } - final Flux messageSourceNoConfirm = Flux.create(this.fluxSinkNoConfirm::set); + final Flux messageSourceNoConfirm = Flux.create(fluxSink -> + this.fluxSinkNoConfirm = fluxSink + ); sender.send(messageSourceNoConfirm).subscribe(); } @@ -99,7 +100,7 @@ public Mono sendWithConfirm(T message, String exchange, String routing public Mono sendNoConfirm(T message, String exchange, String routingKey, Map headers, boolean persistent) { - fluxSinkNoConfirm.get().next(toOutboundMessage(message, exchange, routingKey, headers, persistent)); + fluxSinkNoConfirm.next(toOutboundMessage(message, exchange, routingKey, headers, persistent)); return Mono.empty(); } diff --git a/async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/communications/ReactiveMessageSenderTest.java b/async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/communications/ReactiveMessageSenderTest.java index 874840ec..d0f62822 100644 --- a/async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/communications/ReactiveMessageSenderTest.java +++ b/async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/communications/ReactiveMessageSenderTest.java @@ -41,6 +41,9 @@ class ReactiveMessageSenderTest { @Spy private final MessageConverter messageConverter = new RabbitJacksonMessageConverter(jsonMapper); + @Mock + private final SendOptions sendOptions = new SendOptions(); + @Mock private UnroutableMessageNotifier unroutableMessageNotifier; @@ -119,14 +122,6 @@ void sendWithConfirmSomeMessage() { StepVerifier.create(voidMono).verifyComplete(); } - @Test - void sendNoConfirmSomeMessage() { - SomeClass some = new SomeClass("42", "Daniel", new Date()); - final Mono voidMono = messageSender.sendNoConfirm(some, "exchange", "rkey", new HashMap<>(), true); - - StepVerifier.create(voidMono).verifyComplete(); - } - private record SomeClass(String id, String name, Date date) { } From e177c3de97fa1dbc39b7f4fb2264c1ae92c8d079 Mon Sep 17 00:00:00 2001 From: luisgomez29 Date: Wed, 25 Mar 2026 17:22:06 -0500 Subject: [PATCH 12/15] Revert "refactor: replace volatile fields with AtomicReference in message listeners" This reverts commit 2be7014a --- .../kafka/listeners/GenericMessageListener.java | 13 ++++++------- .../communications/UnroutableMessageNotifier.java | 14 ++++++-------- .../listeners/ApplicationReplyListener.java | 9 ++++----- .../rabbit/listeners/GenericMessageListener.java | 15 +++++++-------- .../UnroutableMessageNotifierTest.java | 4 +--- 5 files changed, 24 insertions(+), 31 deletions(-) diff --git a/async/async-kafka/src/main/java/org/reactivecommons/async/kafka/listeners/GenericMessageListener.java b/async/async-kafka/src/main/java/org/reactivecommons/async/kafka/listeners/GenericMessageListener.java index 7634d17e..7c14e631 100644 --- a/async/async-kafka/src/main/java/org/reactivecommons/async/kafka/listeners/GenericMessageListener.java +++ b/async/async-kafka/src/main/java/org/reactivecommons/async/kafka/listeners/GenericMessageListener.java @@ -20,7 +20,6 @@ import java.time.Instant; import java.util.List; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; import java.util.logging.Level; @@ -44,7 +43,7 @@ public abstract class GenericMessageListener { private final DiscardNotifier discardNotifier; private final String objectType; private final CustomReporter customReporter; - private final AtomicReference>> messageFlux = new AtomicReference<>(); + private volatile Flux> messageFlux; protected GenericMessageListener(ReactiveMessageListener listener, boolean useDLQ, boolean createTopology, long maxRetries, long retryDelay, DiscardNotifier discardNotifier, @@ -82,21 +81,21 @@ public void startListener(TopologyCreator creator) { } if (createTopology) { - this.messageFlux.set(setUpBindings(creator) + this.messageFlux = setUpBindings(creator) .thenMany(this.messageListener.listen(groupId, topics) .doOnError(err -> log.log(Level.SEVERE, "Error listening queue", err)) - .transform(this::consumeFaultTolerant))); + .transform(this::consumeFaultTolerant)); } else { - this.messageFlux.set(this.messageListener.listen(groupId, topics) + this.messageFlux = this.messageListener.listen(groupId, topics) .doOnError(err -> log.log(Level.SEVERE, "Error listening queue", err)) - .transform(this::consumeFaultTolerant)); + .transform(this::consumeFaultTolerant); } onTerminate(); } private void onTerminate() { - messageFlux.get().doOnTerminate(this::onTerminate) + messageFlux.doOnTerminate(this::onTerminate) .subscribe(new LoggerSubscriber<>(getClass().getName())); } diff --git a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/UnroutableMessageNotifier.java b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/UnroutableMessageNotifier.java index 8d135e32..5e2eeaaf 100644 --- a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/UnroutableMessageNotifier.java +++ b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/communications/UnroutableMessageNotifier.java @@ -7,12 +7,10 @@ import reactor.rabbitmq.OutboundMessage; import reactor.rabbitmq.OutboundMessageResult; -import java.util.concurrent.atomic.AtomicReference; - @Log public class UnroutableMessageNotifier { private final Sinks.Many> sink; - private final AtomicReference currentSubscription = new AtomicReference<>(); + private volatile Disposable currentSubscription; public UnroutableMessageNotifier() { this.sink = Sinks.many().multicast().onBackpressureBuffer(); @@ -25,15 +23,15 @@ public void notifyUnroutableMessage(OutboundMessageResult messa } public void listenToUnroutableMessages(UnroutableMessageHandler handler) { - Disposable previous = currentSubscription.getAndSet(sink.asFlux() + if (currentSubscription != null && !currentSubscription.isDisposed()) { + currentSubscription.dispose(); + } + currentSubscription = sink.asFlux() .subscribeOn(Schedulers.boundedElastic()) .flatMap(handler::processMessage) .onErrorContinue((throwable, o) -> log.severe("Error processing unroutable message: " + throwable.getMessage()) ) - .subscribe()); - if (previous != null && !previous.isDisposed()) { - previous.dispose(); - } + .subscribe(); } } diff --git a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/ApplicationReplyListener.java b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/ApplicationReplyListener.java index 42ca98a7..220d31d1 100644 --- a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/ApplicationReplyListener.java +++ b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/ApplicationReplyListener.java @@ -13,7 +13,6 @@ import reactor.rabbitmq.ConsumeOptions; import reactor.rabbitmq.Receiver; -import java.util.concurrent.atomic.AtomicReference; import java.util.logging.Level; import static org.reactivecommons.async.commons.Headers.COMPLETION_ONLY_SIGNAL; @@ -33,7 +32,7 @@ public class ApplicationReplyListener { private final String exchangeName; private final boolean createTopology; - private final AtomicReference> deliveryFlux = new AtomicReference<>(); + private volatile Flux deliveryFlux; public ApplicationReplyListener(ReactiveReplyRouter router, ReactiveMessageListener listener, String queueName, String exchangeName, boolean createTopology) { @@ -52,7 +51,7 @@ public void startListening(String routeKey) { } ConsumeOptions consumeOptions = new ConsumeOptions(); consumeOptions.consumerTag(InstanceIdentifier.getInstanceId("replies")); - deliveryFlux.set(flow + deliveryFlux = flow .then(creator.declare(queue(queueName).durable(false).autoDelete(true).exclusive(true))) .then(creator.bind(binding(exchangeName, routeKey, queueName))) .thenMany(receiver.consumeAutoAck(queueName, consumeOptions).doOnNext(delivery -> { @@ -67,13 +66,13 @@ public void startListening(String routeKey) { } catch (Exception e) { log.log(Level.SEVERE, "Error in reply reception", e); } - }))); + })); onTerminate(); } private void onTerminate() { - deliveryFlux.get().doOnTerminate(this::onTerminate) + deliveryFlux.doOnTerminate(this::onTerminate) .subscribe(new LoggerSubscriber<>(getClass().getName())); } diff --git a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/GenericMessageListener.java b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/GenericMessageListener.java index 890b0dba..dac167ff 100644 --- a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/GenericMessageListener.java +++ b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/GenericMessageListener.java @@ -26,7 +26,6 @@ import java.util.List; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; import java.util.logging.Level; @@ -52,7 +51,7 @@ public abstract class GenericMessageListener { private final DiscardNotifier discardNotifier; private final String objectType; private final CustomReporter customReporter; - private final AtomicReference> messageFlux = new AtomicReference<>(); + private volatile Flux messageFlux; protected GenericMessageListener(String queueName, ReactiveMessageListener listener, boolean useDLQRetries, boolean createTopology, long maxRetries, long retryDelay, @@ -99,14 +98,14 @@ public void startListener() { consumeOptions.consumerTag(InstanceIdentifier.getInstanceId(getKind())); if (createTopology) { - this.messageFlux.set(setUpBindings(messageListener.topologyCreator()) + this.messageFlux = setUpBindings(messageListener.topologyCreator()) .thenMany(receiver.consumeManualAck(queueName, consumeOptions) .doOnError(err -> log.log(Level.SEVERE, "Error listening queue " + getRootCauseMessage(err), err)) - .transform(this::consumeFaultTolerant))); + .transform(this::consumeFaultTolerant)); } else { - this.messageFlux.set(receiver.consumeManualAck(queueName, consumeOptions) + this.messageFlux = receiver.consumeManualAck(queueName, consumeOptions) .doOnError(err -> log.log(Level.SEVERE, "Error listening queue " + getRootCauseMessage(err), err)) - .transform(this::consumeFaultTolerant)); + .transform(this::consumeFaultTolerant); } onTerminate(); @@ -157,7 +156,7 @@ protected Mono handle(AcknowledgableDelivery msj, Instan } private void onTerminate() { - messageFlux.get() + messageFlux .doOnTerminate(this::onTerminate) .subscribe(new LoggerSubscriber<>(getClass().getName())); } @@ -226,7 +225,7 @@ private Mono requeueOrAck(AcknowledgableDelivery msj, Th logError(err, msj, FallbackStrategy.DEFINITIVE_DISCARD); return discardNotifier .notifyDiscard(rabbitMessage) - .doOnSuccess(a -> msj.ack()).thenReturn(msj); + .doOnSuccess(_a -> msj.ack()).thenReturn(msj); } else if (useDLQRetries) { // DLQ retries logError(err, msj, FallbackStrategy.RETRY_DLQ); msj.nack(false); diff --git a/async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/communications/UnroutableMessageNotifierTest.java b/async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/communications/UnroutableMessageNotifierTest.java index 8e82260f..9319cbb9 100644 --- a/async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/communications/UnroutableMessageNotifierTest.java +++ b/async/async-rabbit/src/test/java/org/reactivecommons/async/rabbit/communications/UnroutableMessageNotifierTest.java @@ -16,7 +16,6 @@ import reactor.rabbitmq.OutboundMessageResult; import java.lang.reflect.Field; -import java.util.concurrent.atomic.AtomicReference; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.any; @@ -95,8 +94,7 @@ void shouldDisposePreviousSubscriptionWhenNewHandlerIsSubscribed() throws Except Field field = UnroutableMessageNotifier.class.getDeclaredField("currentSubscription"); field.setAccessible(true); - AtomicReference ref = (AtomicReference) field.get(unroutableMessageNotifier); - ref.set(firstSubscription); + field.set(unroutableMessageNotifier, firstSubscription); when(sink.asFlux()).thenReturn(Flux.never()); From 76b870912abc78d5f83d0f73c525e4003d45a09f Mon Sep 17 00:00:00 2001 From: luisgomez29 Date: Wed, 25 Mar 2026 17:28:25 -0500 Subject: [PATCH 13/15] refactor: replace AtomicBoolean with volatile boolean for published state --- .../src/main/java/reactor/rabbitmq/OutboundMessage.java | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/OutboundMessage.java b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/OutboundMessage.java index 8e11e540..6836b651 100644 --- a/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/OutboundMessage.java +++ b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/OutboundMessage.java @@ -19,7 +19,6 @@ import com.rabbitmq.client.AMQP.BasicProperties; import lombok.Getter; import java.util.Arrays; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; @Getter @@ -31,7 +30,7 @@ public class OutboundMessage { private final byte[] body; private final Consumer ackNotifier; - private final AtomicBoolean published = new AtomicBoolean(false); + private volatile boolean published = false; public OutboundMessage(String exchange, String routingKey, byte[] body) { this(exchange, routingKey, null, body, null); @@ -50,10 +49,6 @@ public OutboundMessage(String exchange, String routingKey, BasicProperties prope this.ackNotifier = ackNotifier; } - public boolean isPublished() { - return published.get(); - } - @Override public String toString() { return "OutboundMessage{" + @@ -65,6 +60,6 @@ public String toString() { } void published() { - this.published.set(true); + this.published = true; } } From b0a1d58985a34f077cff916017a11e4f71ff389e Mon Sep 17 00:00:00 2001 From: luisgomez29 Date: Wed, 25 Mar 2026 17:37:56 -0500 Subject: [PATCH 14/15] docs: update README to reflect removal of volatile fields and other enhancements --- async/reactor-rabbitmq/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/async/reactor-rabbitmq/README.md b/async/reactor-rabbitmq/README.md index 9250d679..22ecd766 100644 --- a/async/reactor-rabbitmq/README.md +++ b/async/reactor-rabbitmq/README.md @@ -13,7 +13,6 @@ The original library was archived by its maintainers on September 26, 2025 and i - Upgraded from Java 8/11 to Java 17 - Removed dead code and unused APIs -- Replaced `volatile` fields with thread-safe types (`AtomicReference`, `AtomicBoolean`) - `OutboundMessage` extended with optional `ackNotifier` callback, eliminating the need for a separate subclass - Java 17 modernizations (records, sealed classes patterns, text blocks) - SLF4J and SonarQube warning fixes From fccaac5a42300cfca6762f63f0b5e91ade1c178a Mon Sep 17 00:00:00 2001 From: luisgomez29 Date: Wed, 25 Mar 2026 18:11:40 -0500 Subject: [PATCH 15/15] refactor: update imports to use reactor.rabbitmq specifications --- .../ApplicationNotificationListener.java | 6 +-- .../listeners/ApplicationReplyListener.java | 6 +-- .../rabbitmq/ResourcesSpecification.java | 37 ------------------- gradle.properties | 2 +- .../async/rabbit/RabbitMQBrokerProvider.java | 2 +- 5 files changed, 8 insertions(+), 45 deletions(-) delete mode 100644 async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/ResourcesSpecification.java diff --git a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/ApplicationNotificationListener.java b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/ApplicationNotificationListener.java index 690df453..7e28282c 100644 --- a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/ApplicationNotificationListener.java +++ b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/ApplicationNotificationListener.java @@ -19,9 +19,9 @@ import java.util.function.Function; -import static reactor.rabbitmq.ResourcesSpecification.binding; -import static reactor.rabbitmq.ResourcesSpecification.exchange; -import static reactor.rabbitmq.ResourcesSpecification.queue; +import static reactor.rabbitmq.BindingSpecification.binding; +import static reactor.rabbitmq.ExchangeSpecification.exchange; +import static reactor.rabbitmq.QueueSpecification.queue; import static reactor.core.publisher.Flux.fromIterable; @Log diff --git a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/ApplicationReplyListener.java b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/ApplicationReplyListener.java index 220d31d1..5d560a84 100644 --- a/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/ApplicationReplyListener.java +++ b/async/async-rabbit/src/main/java/org/reactivecommons/async/rabbit/listeners/ApplicationReplyListener.java @@ -17,9 +17,9 @@ import static org.reactivecommons.async.commons.Headers.COMPLETION_ONLY_SIGNAL; import static org.reactivecommons.async.commons.Headers.CORRELATION_ID; -import static reactor.rabbitmq.ResourcesSpecification.binding; -import static reactor.rabbitmq.ResourcesSpecification.exchange; -import static reactor.rabbitmq.ResourcesSpecification.queue; +import static reactor.rabbitmq.BindingSpecification.binding; +import static reactor.rabbitmq.ExchangeSpecification.exchange; +import static reactor.rabbitmq.QueueSpecification.queue; @Log diff --git a/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/ResourcesSpecification.java b/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/ResourcesSpecification.java deleted file mode 100644 index d7ed829b..00000000 --- a/async/reactor-rabbitmq/src/main/java/reactor/rabbitmq/ResourcesSpecification.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2017-2021 VMware Inc. or its affiliates, All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package reactor.rabbitmq; - - -/** - * API that combines {@link ExchangeSpecification}, {@link QueueSpecification}, and {@link BindingSpecification}. - */ -public class ResourcesSpecification { - - public static ExchangeSpecification exchange(String name) { - return ExchangeSpecification.exchange(name); - } - - public static QueueSpecification queue(String name) { - return QueueSpecification.queue(name); - } - - public static BindingSpecification binding(String exchange, String routingKey, String queue) { - return BindingSpecification.binding(exchange, routingKey, queue); - } - -} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index fb84a793..1f600a9b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=7.0.7 +version=7.0.6 toPublish=domain-events-api,async-commons-api,async-commons,cloudevents-json-jackson,reactor-rabbitmq,async-rabbit,async-commons-rabbit-starter,async-kafka,async-kafka-starter,async-commons-starter onlyUpdater=true \ No newline at end of file diff --git a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/RabbitMQBrokerProvider.java b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/RabbitMQBrokerProvider.java index f9702b9d..a833183e 100644 --- a/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/RabbitMQBrokerProvider.java +++ b/starters/async-rabbit-starter/src/main/java/org/reactivecommons/async/rabbit/RabbitMQBrokerProvider.java @@ -24,7 +24,7 @@ import org.reactivecommons.async.starter.config.health.RCHealth; import reactor.core.publisher.Mono; -import static reactor.rabbitmq.ResourcesSpecification.exchange; +import static reactor.rabbitmq.ExchangeSpecification.exchange; @Log public record RabbitMQBrokerProvider(String domain,