Reactive streams for RabbitMQ with Monix

In Why you should know Monix
I’ve taken a brief look at some of Monix’s abstractions and utilities,
but I haven’t dived into implementing reactive streams elements.
This time I’m going to build Consumer and Observer for RabbitMQ message broker.

What I aim for are: Monix Consumer that takes messages from Observable and produces messages to broker
and Monix Observable that consumes messages from broker and gives them to Consumer.
For both elements I will provide tests for “reactiveness”.

What I’m not going to implement is a complete library of production quality or part of it.
I’m trying to keep things very simple.

messages are consumed from Queues and their consumption (delivery) is acknowledged by client,
delivered messages will not be delivered to other consumer, unacknowledged messages re-appear in Queue and will be delivered again to one of consumers

there are transactions and means to confirm published messages or use transactional channels,
I abstract these things

physical Connection is scarce resource but one can create multiple Channels inside it

violating a protocol when using a Channel can cause closing Connection

server blocks Connection if it produces too much, so it is good idea to have separate Connection for consumption

documentation says that “Channel instances must not be shared between threads”, I understand this as “don’t access Channel concurrently from multiple threads”

R is type of element emitted when Subscriber completes -
for example, a consumer that sums length of all Strings passed to it has type Consumer[String, Long].

ExchangeConsumer I’m implementing will expect an Observable[OutboundMessage] and it will produce observed messages to RabbitMQ server.
There is no value I want to return upon completion of upstream (data producer),
thus I need to return Consumer[OutboundMessage, Unit].
If I was going to count messages sent and signal this number when upstream completes, then type would be Consumer[OutboundMessage, Long].

There is more than one way to skin a cat, my way is to have one Connection per Consumer,
this Connection is used to create one Channel per Subscriber.
This is cheap, but Connection - thus all Subscribers using it’s Channels -
can be blocked by server because of one Subscriber that produces too much.
Let’s live with this design decision and dive into the code:

That’s it.
I can’t tell it was hard but Monix can make it even easier,
this time I’ll create equivalent Consumer using Consumer.fromObserver

Consumer.fromObserver takes care of Callback[Unit] seen in previous example,
by calling it’s onSuccess and onError after calling Observer’s onComplete and onError methods,
thus Callback is absent in this code.

It’s time for short example of how to start streaming from observable to Rabbit,
please just note types that are used:

Finally I’m ready to explain AssignableCancelable.dummy present in the first example.

Subscriber can cancel this AssignableCancelable to cancel subscription to data source.
Monix needs Assignable part of it, because it assigns subscription to Observer to it.
In other words it allows canceling subscription to data source from data sink.

Let’s consider scenario in which RabbitMQ will close ExchangeConsumers Connection,
and therefore all Subscribers Channels.
onNext subsequent to channel shutdown has to fail (Stop should be returned and onError of Callback should be invoked).
But what if Observable uses some scarce resource and supports canceling subscriptions properly?
Then we can save this resource, by canceling subscription, when Channels shutdown is observed.

Thanks to Channel.addShutdownListener improvement is very simple.
Final version of ExchangeConsumer in repo: ExchangeConsumer.scala

Implemented ExchangeSubscriber is now polite to Observable,
because it reacts to connection closed with canceling it’s subscription.
Unfortunately, in following scenario no one is so kind to ExchangeSubscriber,
it will await for subsequent onNext calls keeping Channel open. Forever.

Observer contract
states that “The data-source can get canceled without the observer receiving any notification about it”.
But there is one weird trick that can help us, it is: Observable.onCancelTriggerError.

After little change produce ends with java.util.concurrent.CancellationException but I don’t care,
I just told it to cancel and my resource is free!

Just kidding, tests are missing!
I’m conservative about tests but I’m also motivated to end this section with little effort.

I’m going to omit round trip tests where messages are produced by ExchangeConsumer,
because this post is more about reactive streams than about AMQP.

For tests about being reactive streams, things look good for me,
because someone has written pretty nice Technology Compatibility Kit for Reactive Streams.
I’ll use Monix goodness to obtain org.reactivestreams.Subscriber from my Consumer and TCK to perform tests!

It’s turn to implement Observable that will consume from RabbitMQ queue.
API gives me a choice between callback and pull.
By callback API I understand using Channel.basicConsume(queue, autoAck, consumer),
where consumer has handful of callback methods.
reactive-rabbit uses this approach.

Pulling is using Channel.basicGet(queue, autoAck) which returns GetResponse.
basicGet is synchronous and blocking, but if there is no message ready for consumption,
it returns null instantly - it doesn’t perform blocking await for next message.
I’ll use pull.

The second design decision to make is to choose how to acknowledge messages in case of
basicGet(queue, false) is used - that is when client side decides when messages processing is done and it is OK to remove it from queue.
I’m going to make an QueueObservable that is an Observable[AckableGetResponse],
which closes on Channel reference and allows to ack message to the broker.
Similar approach is used by Akka Streams connector for Apache Kafka, which returns CommittableMessage to commit offsets.

Same type is returned regardless auto-ack is used, that is only to keep code blog-post-example short.

To implement Observable I need to provide a function from Subscriber to Cancelable.
Very side-effecting function in fact.
It should start process of feeding given Subscriber with elements that are observable (messages from queue in our case).
Returned Cancelable ought to allow aborting that process.

feeding is a process that feeds subscriber with messages consumed from queue,
with added cancel handler (sets continue flag to false)
and error handler that signals to subscriber.
I justify so trivial cancellation with synchronous nature of basicGet
and quick spinning in case of no messages ready for being consumed.

oneGet performs one Channel.basicGet and
if there was some message consumed subscriber.onNext is called.
If subscriber wants to Continue, feedSubscriber is trampolined.
In opposite case channel is aborted and processing is terminated by abort task.
When exception occurs, perhaps during basicGet, Task.raiseError is returned.
It will cause onErrorRecover of feeding - thus subscriber.onError.

I ought to justify myself for skipping test for Specification Rule 1.09
Textual specification states:
Original test fails because it expects onSubscribe and given implementation doesn’t call it.
Although it doesn’t break specification because it doesn’t signal anything at all in this test case -
doesn’t violate “MUST call onSubscribe (…) prior to any other signals (…)”.

Of course, more rules of specification could be checked with extra effort although I’m going to stop here.

In my opinion it went pretty easy.
For sure there are features missing,
like Publisher Confirms (for ExchangeConsumer)
or using Channel auto-recovery (for both Consumer and Observable),
also my take on acknowledgments is very simple,
so don’t I dare comparing implementation with Monix to reactive-rabbit
or any other production grade library.
I hope I demonstrated potential of creating one with Monix
and provided nice example of creating Consumers and Observers.