Skip to content
This repository was archived by the owner on Sep 26, 2025. It is now read-only.

Commit 1f62660

Browse files
authored
Merge pull request #42 from pmackowski/channel-mono
Add Mono<Channel> property in Sender.
2 parents 698d1f5 + 69025e1 commit 1f62660

File tree

5 files changed

+206
-31
lines changed

5 files changed

+206
-31
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package reactor.rabbitmq;
2+
3+
import com.rabbitmq.client.Channel;
4+
import org.slf4j.Logger;
5+
import org.slf4j.LoggerFactory;
6+
import reactor.core.publisher.SignalType;
7+
8+
import java.util.function.BiConsumer;
9+
10+
public class ChannelCloseHandlers {
11+
12+
public static class SenderChannelCloseHandler implements BiConsumer<SignalType, Channel> {
13+
14+
private static final Logger LOGGER = LoggerFactory.getLogger(SenderChannelCloseHandler.class);
15+
16+
@Override
17+
public void accept(SignalType signalType, Channel channel) {
18+
int channelNumber = channel.getChannelNumber();
19+
LOGGER.debug("closing channel {} by signal {}", channelNumber, signalType);
20+
try {
21+
if (channel.isOpen() && channel.getConnection().isOpen()) {
22+
channel.close();
23+
}
24+
} catch (Exception e) {
25+
LOGGER.warn("Channel {} didn't close normally: {}", channelNumber, e.getMessage());
26+
}
27+
}
28+
}
29+
30+
}

src/main/java/reactor/rabbitmq/SendOptions.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,5 @@ public SendOptions exceptionHandler(BiConsumer<Sender.SendContext, Exception> ex
3636
this.exceptionHandler = exceptionHandler;
3737
return this;
3838
}
39+
3940
}

src/main/java/reactor/rabbitmq/Sender.java

Lines changed: 33 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -30,20 +30,23 @@
3030
import reactor.core.publisher.Flux;
3131
import reactor.core.publisher.FluxOperator;
3232
import reactor.core.publisher.Mono;
33+
import reactor.core.publisher.SignalType;
3334
import reactor.core.publisher.Operators;
3435
import reactor.core.scheduler.Scheduler;
3536
import reactor.core.scheduler.Schedulers;
3637

3738
import java.io.IOException;
3839
import java.util.Iterator;
3940
import java.util.Map;
41+
import java.util.Objects;
4042
import java.util.concurrent.ConcurrentNavigableMap;
4143
import java.util.concurrent.ConcurrentSkipListMap;
4244
import java.util.concurrent.atomic.AtomicBoolean;
4345
import java.util.concurrent.atomic.AtomicReference;
4446
import java.util.function.BiConsumer;
4547
import java.util.function.Function;
4648
import java.util.function.Supplier;
49+
import java.util.stream.Stream;
4750

4851
/**
4952
* Reactive abstraction to create resources and send messages.
@@ -58,6 +61,10 @@ public class Sender implements AutoCloseable {
5861

5962
private final Mono<? extends Connection> connectionMono;
6063

64+
private final Mono<? extends Channel> channelMono;
65+
66+
private final BiConsumer<SignalType, Channel> channelCloseHandler;
67+
6168
private final AtomicBoolean hasConnection = new AtomicBoolean(false);
6269

6370
private final Mono<? extends Channel> resourceManagementChannelMono;
@@ -83,6 +90,10 @@ public Sender(SenderOptions options) {
8390
.doOnSubscribe(c -> hasConnection.set(true))
8491
.subscribeOn(this.connectionSubscriptionScheduler)
8592
.cache();
93+
this.channelMono = options.getChannelMono();
94+
this.channelCloseHandler = options.getChannelCloseHandler() == null ?
95+
new ChannelCloseHandlers.SenderChannelCloseHandler() :
96+
options.getChannelCloseHandler();
8697
this.privateResourceManagementScheduler = options.getResourceManagementScheduler() == null;
8798
this.resourceManagementScheduler = options.getResourceManagementScheduler() == null ?
8899
createScheduler("rabbitmq-sender-resource-creation") : options.getResourceManagementScheduler();
@@ -102,8 +113,9 @@ public Mono<Void> send(Publisher<OutboundMessage> messages, SendOptions options)
102113
// TODO using a pool of channels?
103114
// would be much more efficient if send is called very often
104115
// less useful if seldom called, only for long or infinite message flux
105-
final Mono<Channel> currentChannelMono = connectionMono.map(CHANNEL_CREATION_FUNCTION).cache();
116+
final Mono<? extends Channel> currentChannelMono = getChannelMono();
106117
final BiConsumer<SendContext, Exception> exceptionHandler = options.getExceptionHandler();
118+
107119
return currentChannelMono.flatMapMany(channel ->
108120
Flux.from(messages)
109121
.doOnNext(message -> {
@@ -119,17 +131,7 @@ public Mono<Void> send(Publisher<OutboundMessage> messages, SendOptions options)
119131
}
120132
})
121133
.doOnError(e -> LOGGER.warn("Send failed with exception {}", e))
122-
.doFinally(st -> {
123-
int channelNumber = channel.getChannelNumber();
124-
LOGGER.info("closing channel {} by signal {}", channelNumber, st);
125-
try {
126-
if (channel.isOpen() && channel.getConnection().isOpen()) {
127-
channel.close();
128-
}
129-
} catch (Exception e) {
130-
LOGGER.warn("Channel {} didn't close normally: {}", channelNumber, e.getMessage());
131-
}
132-
})
134+
.doFinally(st -> channelCloseHandler.accept(st, channel))
133135
).then();
134136
}
135137

@@ -141,17 +143,21 @@ public Flux<OutboundMessageResult> sendWithPublishConfirms(Publisher<OutboundMes
141143
// TODO using a pool of channels?
142144
// would be much more efficient if send is called very often
143145
// less useful if seldom called, only for long or infinite message flux
144-
final Mono<Channel> channelMono = connectionMono.map(CHANNEL_CREATION_FUNCTION)
145-
.map(channel -> {
146+
final Mono<? extends Channel> currentChannelMono = getChannelMono();
147+
148+
return currentChannelMono.map(channel -> {
146149
try {
147150
channel.confirmSelect();
148151
} catch (IOException e) {
149152
throw new RabbitFluxException("Error while setting publisher confirms on channel", e);
150153
}
151154
return channel;
152-
});
155+
})
156+
.flatMapMany(channel -> new PublishConfirmOperator(messages, channel, channelCloseHandler, options));
157+
}
153158

154-
return channelMono.flatMapMany(channel -> new PublishConfirmOperator(messages, channel, options));
159+
private Mono<? extends Channel> getChannelMono() {
160+
return channelMono != null ? channelMono : connectionMono.map(CHANNEL_CREATION_FUNCTION);
155161
}
156162

157163
public RpcClient rpcClient(String exchange, String routingKey) {
@@ -452,15 +458,18 @@ private static class PublishConfirmOperator
452458

453459
private final SendOptions options;
454460

455-
public PublishConfirmOperator(Publisher<OutboundMessage> source, Channel channel, SendOptions options) {
461+
private final BiConsumer<SignalType, Channel> channelCloseHandler;
462+
463+
public PublishConfirmOperator(Publisher<OutboundMessage> source, Channel channel, BiConsumer<SignalType, Channel> channelCloseHandler, SendOptions options) {
456464
super(Flux.from(source));
457465
this.channel = channel;
466+
this.channelCloseHandler = channelCloseHandler;
458467
this.options = options;
459468
}
460469

461470
@Override
462471
public void subscribe(CoreSubscriber<? super OutboundMessageResult> actual) {
463-
source.subscribe(new PublishConfirmSubscriber(channel, actual, options));
472+
source.subscribe(new PublishConfirmSubscriber(channel, channelCloseHandler, actual, options));
464473
}
465474
}
466475

@@ -479,8 +488,11 @@ private static class PublishConfirmSubscriber implements
479488

480489
private final BiConsumer<SendContext, Exception> exceptionHandler;
481490

482-
private PublishConfirmSubscriber(Channel channel, Subscriber<? super OutboundMessageResult> subscriber, SendOptions options) {
491+
private final BiConsumer<SignalType, Channel> channelCloseHandler;
492+
493+
private PublishConfirmSubscriber(Channel channel, BiConsumer<SignalType, Channel> channelCloseHandler, Subscriber<? super OutboundMessageResult> subscriber, SendOptions options) {
483494
this.channel = channel;
495+
this.channelCloseHandler = channelCloseHandler;
484496
this.subscriber = subscriber;
485497
this.exceptionHandler = options.getExceptionHandler();
486498
}
@@ -563,7 +575,7 @@ public void onNext(OutboundMessage message) {
563575
public void onError(Throwable throwable) {
564576
if (state.compareAndSet(SubscriberState.ACTIVE, SubscriberState.COMPLETE) ||
565577
state.compareAndSet(SubscriberState.OUTBOUND_DONE, SubscriberState.COMPLETE)) {
566-
closeResources();
578+
channelCloseHandler.accept(SignalType.ON_ERROR, channel);
567579
// complete the flux state
568580
subscriber.onError(throwable);
569581
} else if (firstException.compareAndSet(null, throwable) && state.get() == SubscriberState.COMPLETE) {
@@ -594,21 +606,11 @@ private void handleError(Exception e, OutboundMessageResult result) {
594606
private void maybeComplete() {
595607
boolean done = state.compareAndSet(SubscriberState.OUTBOUND_DONE, SubscriberState.COMPLETE);
596608
if (done) {
597-
closeResources();
609+
channelCloseHandler.accept(SignalType.ON_COMPLETE, channel);
598610
subscriber.onComplete();
599611
}
600612
}
601613

602-
private void closeResources() {
603-
try {
604-
if (channel.isOpen()) {
605-
channel.close();
606-
}
607-
} catch (Exception e) {
608-
// not much we can do here
609-
}
610-
}
611-
612614
public <T> boolean checkComplete(T t) {
613615
boolean complete = state.get() == SubscriberState.COMPLETE;
614616
if (complete && firstException.get() == null) {

src/main/java/reactor/rabbitmq/SenderOptions.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@
2020
import com.rabbitmq.client.Connection;
2121
import com.rabbitmq.client.ConnectionFactory;
2222
import reactor.core.publisher.Mono;
23+
import reactor.core.publisher.SignalType;
2324
import reactor.core.scheduler.Scheduler;
2425

26+
import java.util.function.BiConsumer;
2527
import java.util.function.Supplier;
2628

2729
/**
@@ -37,6 +39,10 @@ public class SenderOptions {
3739

3840
private Mono<? extends Connection> connectionMono;
3941

42+
private Mono<? extends Channel> channelMono;
43+
44+
private BiConsumer<SignalType, Channel> channelCloseHandler;
45+
4046
private Scheduler resourceManagementScheduler;
4147

4248
private Scheduler connectionSubscriptionScheduler;
@@ -100,6 +106,24 @@ public Mono<? extends Connection> getConnectionMono() {
100106
return connectionMono;
101107
}
102108

109+
public SenderOptions channelMono(Mono<? extends Channel> channelMono) {
110+
this.channelMono = channelMono;
111+
return this;
112+
}
113+
114+
public Mono<? extends Channel> getChannelMono() {
115+
return channelMono;
116+
}
117+
118+
public BiConsumer<SignalType, Channel> getChannelCloseHandler() {
119+
return channelCloseHandler;
120+
}
121+
122+
public SenderOptions channelCloseHandler(BiConsumer<SignalType, Channel> channelCloseHandler) {
123+
this.channelCloseHandler = channelCloseHandler;
124+
return this;
125+
}
126+
103127
public SenderOptions resourceManagementChannelMono(Mono<? extends Channel> resourceManagementChannelMono) {
104128
this.resourceManagementChannelMono = resourceManagementChannelMono;
105129
return this;

src/test/java/reactor/rabbitmq/RabbitFluxTests.java

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@
3131
import reactor.core.publisher.Flux;
3232
import reactor.core.publisher.FluxSink;
3333
import reactor.core.publisher.Mono;
34+
import reactor.core.publisher.SignalType;
3435
import reactor.core.scheduler.Schedulers;
36+
import reactor.rabbitmq.ChannelCloseHandlers.SenderChannelCloseHandler;
37+
import reactor.test.StepVerifier;
3538
import reactor.util.function.Tuple2;
3639
import reactor.util.function.Tuples;
3740

@@ -465,6 +468,87 @@ public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProp
465468
assertEquals(nbMessages, counter.get());
466469
}
467470

471+
@Test
472+
public void senderRetryCreateChannel() throws Exception {
473+
ConnectionFactory mockConnectionFactory = mock(ConnectionFactory.class);
474+
Connection mockConnection = mock(Connection.class);
475+
when(mockConnectionFactory.newConnection()).thenReturn(mockConnection);
476+
when(mockConnection.createChannel())
477+
.thenThrow(new IOException("already closed exception"))
478+
.thenThrow(new IOException("already closed exception"))
479+
.thenReturn(connection.createChannel());
480+
481+
int nbMessages = 10;
482+
483+
Flux<OutboundMessage> msgFlux = Flux.range(0, nbMessages).map(i -> new OutboundMessage("", queue, "".getBytes()));
484+
485+
sender = createSender(new SenderOptions().connectionFactory(mockConnectionFactory));
486+
487+
StepVerifier.create(sender.send(msgFlux).retry(2))
488+
.verifyComplete();
489+
verify(mockConnection, times(3)).createChannel();
490+
491+
StepVerifier.create(consume(queue, nbMessages))
492+
.expectNextCount(nbMessages)
493+
.verifyComplete();
494+
}
495+
496+
@Test
497+
public void senderRetryNotWorkingWhenCreateChannelIsCached() throws Exception {
498+
int nbMessages = 10;
499+
500+
Connection mockConnection = mock(Connection.class);
501+
Channel mockChannel = mock(Channel.class);
502+
when(mockConnection.createChannel())
503+
.thenThrow(new RuntimeException("already closed exception"))
504+
.thenThrow(new RuntimeException("already closed exception"))
505+
.thenReturn(mockChannel);
506+
507+
Flux<OutboundMessage> msgFlux = Flux.range(0, nbMessages).map(i -> new OutboundMessage("", queue, "".getBytes()));
508+
509+
SenderOptions senderOptions = new SenderOptions()
510+
.channelMono(Mono.just(mockConnection).map(this::createChannel).cache());
511+
512+
sender = createSender(senderOptions);
513+
514+
StepVerifier.create(sender.send(msgFlux).retry(2))
515+
.expectError(RabbitFluxException.class)
516+
.verify();
517+
518+
verify(mockChannel, never()).basicPublish(anyString(), anyString(), any(AMQP.BasicProperties.class), any(byte[].class));
519+
verify(mockChannel, never()).close();
520+
}
521+
522+
@Test
523+
public void senderWithCustomChannelCloseHandler() throws Exception {
524+
int nbMessages = 10;
525+
Flux<OutboundMessage> msgFlux = Flux.range(0, nbMessages).map(i -> new OutboundMessage("", queue, "".getBytes()));
526+
527+
SenderChannelCloseHandler channelCloseHandler = mock(SenderChannelCloseHandler.class);
528+
doNothing().when(channelCloseHandler).accept(any(SignalType.class), any(Channel.class));
529+
Mono<Channel> monoChannel = Mono.fromCallable(() -> connection.createChannel()).cache();
530+
SenderOptions senderOptions = new SenderOptions().channelCloseHandler(channelCloseHandler).channelMono(monoChannel);
531+
532+
sender = createSender(senderOptions);
533+
534+
Mono<Void> sendTwice = Mono.when(sender.send(msgFlux), sender.send(msgFlux))
535+
.doFinally(signalType -> {
536+
try {
537+
monoChannel.block().close();
538+
} catch (Exception e) {
539+
throw new RabbitFluxException(e);
540+
}
541+
});
542+
543+
StepVerifier.create(sendTwice)
544+
.verifyComplete();
545+
verify(channelCloseHandler, times(2)).accept(SignalType.ON_COMPLETE, monoChannel.block());
546+
547+
StepVerifier.create(consume(queue, nbMessages * 2))
548+
.expectNextCount(nbMessages * 2)
549+
.verifyComplete();
550+
}
551+
468552
@Test
469553
public void publishConfirms() throws Exception {
470554
int nbMessages = 10;
@@ -499,6 +583,8 @@ public void publishConfirmsErrorWhilePublishing() throws Exception {
499583
Channel mockChannel = mock(Channel.class);
500584
when(mockConnectionFactory.newConnection()).thenReturn(mockConnection);
501585
when(mockConnection.createChannel()).thenReturn(mockChannel);
586+
when(mockConnection.isOpen()).thenReturn(true);
587+
when(mockChannel.getConnection()).thenReturn(mockConnection);
502588

503589
AtomicLong publishSequence = new AtomicLong();
504590
when(mockChannel.getNextPublishSeqNo()).thenAnswer(invocation -> publishSequence.incrementAndGet());
@@ -824,4 +910,36 @@ private void sendAndReceiveMessages(String queue) throws Exception {
824910
assertTrue(latch.await(5, TimeUnit.SECONDS));
825911
subscriber.dispose();
826912
}
913+
914+
private Flux<Delivery> consume(final String queue, int nbMessages) throws Exception {
915+
return consume(queue, nbMessages, Duration.ofSeconds(1L));
916+
}
917+
918+
private Flux<Delivery> consume(final String queue, int nbMessages, Duration timeout) throws Exception {
919+
Channel channel = connection.createChannel();
920+
Flux<Delivery> consumeFlux = Flux.create(emitter -> Mono.just(nbMessages).map(AtomicInteger::new).subscribe(countdown -> {
921+
DeliverCallback deliverCallback = (consumerTag, message) -> {
922+
emitter.next(message);
923+
if (countdown.decrementAndGet() <= 0) {
924+
emitter.complete();
925+
}
926+
};
927+
CancelCallback cancelCallback = consumerTag -> {
928+
};
929+
try {
930+
channel.basicConsume(queue, true, deliverCallback, cancelCallback);
931+
} catch (IOException e) {
932+
e.printStackTrace();
933+
}
934+
}));
935+
return consumeFlux.timeout(timeout);
936+
}
937+
938+
private Channel createChannel(Connection connection) {
939+
try {
940+
return connection.createChannel();
941+
} catch (Exception e) {
942+
throw new RabbitFluxException(e);
943+
}
944+
}
827945
}

0 commit comments

Comments
 (0)