Kotlin Coroutines in Practice:
writing asynchronous RabbitMQ client
Vyacheslav Artemyev


Agenda
- RabbitMQ introduction
- Declare exchange and queue in different manners
- Reactive vs Coroutines
- Coroutines base concepts
- Publisher implementation
- Consumer implementation
- Conclusion
RabbitMQ architecture

RabbitMQ architecture



The problem #1
channel.exchangeDeclare("exchange", "direct")
channel.queueDeclare("queue", true, true, false, emptyMap())
channel.queueBind("queue", "exchange", "")
/** Some useful actions **/
Declare exchange & queue



Solution v1
val exchangeDeclaration = CompletableFuture.supplyAsync {
channel.exchangeDeclare("exchange", "direct")
}
val queueDeclaration = CompletableFuture.supplyAsync {
channel.queueDeclare("queue", false, false, false, emptyMap())
}
CompletableFuture
.allOf(exchangeDeclaration, queueDeclaration)
.thenApply { channel.queueBind("queue", "exchange", "") }
Solution v2
val exchangeDeclaration = CompletableFuture.supplyAsync {
channel.exchangeDeclare("exchange", "direct")
}
val queueDeclaration = CompletableFuture.supplyAsync {
channel.queueDeclare("queue", false, false, false, emptyMap())
}
val tasks = listOf(exchangeDeclaration, queueDeclaration)
val allFutures = CompletableFuture.allOf(*tasks.toTypedArray())
val results = allFutures.thenApply { v -> tasks.map { it.join() } }.get()
Solution v3
val exchangeDeclaration: CompletableFuture<Command> = channel
.asyncCompletableRpc(
AMQP.Exchange.Declare.Builder()
.exchange("exchange")
.type("direct")
.build()
)
val queueDeclaration: CompletableFuture<Command> = channel
.asyncCompletableRpc(
AMQP.Queue.Declare.Builder()
.queue("queue")
.durable(true)
.exclusive(true)
.autoDelete(false)
.build()
)
Reactive solution
val exchangeDeclaration = sender.declareExchange(ExchangeSpecification.exchange())
val queueDeclaration = sender.declareQueue(QueueSpecification.queue())
Flux.concat(exchangeDeclaration, queueDeclaration)
.then(sender.bind(BindingSpecification.binding()))
.doOnError { e -> LOGGER.error("Boom", e) }
.subscribe { r -> LOGGER.info("Done") }
Reactive under the hood
AMQP.Queue.Declare declare = new AMQImpl.Queue.Declare.Builder()
.queue(specification.getName())
.durable(specification.isDurable())
.exclusive(specification.isExclusive())
.autoDelete(specification.isAutoDelete())
.arguments(specification.getArguments())
.build();
return channelMono.map(channel -> {
try {
return channel.asyncCompletableRpc(declare);
} catch (IOException e) {
throw new RabbitFluxException("Error during RPC call", e);
}
})
.flatMap(future -> Mono.fromCompletionStage(future))
.flatMap(command -> Mono.just((AMQP.Queue.DeclareOk) command.getMethod()))
.publishOn(resourceManagementScheduler);
Asynchronous programming
Asynchrony, in computer programming, refers to the occurrence of events independent of the main program flow and ways to deal with such events. These may be "outside" events such as the arrival of signals, or actions instigated by a program that take place concurrently with program execution, without the program blocking to wait for results.
https://en.wikipedia.org/wiki/Asynchrony_(computer_programming)
Coroutines
Coroutines are computer-program components that generalize subroutines for non-preemptive multitasking, by allowing multiple entry points for suspending and resuming execution at certain locations.

Use cases
You do not want threads:
- You have a lot of mutable states
- Mobile/Desktop UI apps, etc
You cannot afford threads:
- They are expensive to keep and to switch
- High-load server-side, micro-tasks, etc
You cannot do threads:
- Your code is a single thread only
- JS/web target, μC, etc
Why Coroutines?
Reactive:
- You need to change return type and this type should be propagated
Coroutines:
- You don't need to change signature*
fun publishWithConfirm(message: OutboundMessage): Boolean
Function signatures
fun publishWithConfirm(message: OutboundMessage): Observable<Boolean>
* Just a bit
suspend
Why Coroutines?
Functions stay "the same"
Localize the changes to a single entry point
(instead of changing how you do thing everywhere)
ONE LESS THING TO LEARN
Why Coroutines?
Reactive:
- Not easy to debug
Coroutines:
- Easier to debug
Debugging
Why Coroutines?
for ((message) in list) {
publishWithConfirm(message)
}
Direct loops
Why Coroutines?
try {
publishWithConfirm(message)
} catch (e: Exception) {
...
}
Direct exception handing
Why Coroutines?
Direct higher-order functions
(1..times).map {
async {
publisher.publishWithConfirm(createMessage("Hello #$it"))
}
}.awaitAll()
//forEach, let, apply, repeat, filter, map, use etc
Why Coroutines?
Easy to manage
withContext(Dispatchers.IO) {
publisher.publishWithConfirm(createMessage("Hello #$it"))
}
Reactive:
Coroutines
...
.subscribeOn(Schedulers.single())
.observeOn(Schedulers.elastic())
...
[main @coroutine#2] DEBUG c.v.t.p.PublisherTest - Coroutine #1 started
[main @coroutine#3] DEBUG c.v.t.p.PublisherTest - Coroutine #2 started
[main @coroutine#4] DEBUG c.v.t.p.PublisherTest - Coroutine #3 started
[main @coroutine#5] DEBUG c.v.t.p.PublisherTest - Coroutine #4 started
[main @coroutine#6] DEBUG c.v.t.p.PublisherTest - Coroutine #5 started
[main @coroutine#2] DEBUG c.v.t.p.ConfirmPublisher - The message Sequence Number: 1
[rabbitmq-nio] DEBUG c.v.t.p.AckListener - deliveryTag = [1], multiple = [false], positive = [true]
[main @coroutine#2] INFO c.v.t.p.ConfirmPublisher - Message Hello #1 has sent
[main @coroutine#3] DEBUG c.v.t.p.ConfirmPublisher - The message Sequence Number: 2
[main @coroutine#3] INFO c.v.t.p.ConfirmPublisher - Message Hello #2 has sent
[main @coroutine#4] DEBUG c.v.t.p.ConfirmPublisher - The message Sequence Number: 3
[rabbitmq-nio] DEBUG c.v.t.p.AckListener - deliveryTag = [2], multiple = [false], positive = [true]
-Dkotlinx.coroutines.debug
CoroutineName("my_coroutine")
Why Coroutines?
Easy to debug
Coroutine basics

Suspending function
A suspending function — a function that is marked with suspend modifier. It may suspend execution of the code without blocking the current thread of execution by invoking other suspending functions.
suspend fun publishWithConfirm(message: OutboundMessage): Boolean {
// some logic is here
}
fun publishWithConfirm(message: OutboundMessage, callback: Continuation<Boolean>): Any? {
// some logic is here
}
Continuation
Being passed
Continuation
Coroutines (like futures!) use callbacks at their low level, but allow asynchronous programming in direct style.
interface Continuation<in T> {
val context: CoroutineContext
fun resumeWith(result: Result<T>)
}
Continuation
Coroutines (like futures!) use callbacks at their low level, but allow asynchronous programming in direct style.
suspend fun method(): Boolean {
return suspendCoroutine { continuation ->
//some actions here
continuation.resume(true)
//another logic here
continuation.resumeWithException(RuntimeException("Boom"))
}
}
Let's try it
/**
* Asynchronously send a method over this channel.
* ...
*/
CompletableFuture<Command> asyncCompletableRpc(Method method) throws IOException;
/**
* Awaits for completion of the completion stage without blocking a thread.
*
* This suspending function is cancellable.
*/
public suspend fun <T> CompletionStage<T>.await(): T {
// fast path when CompletableFuture is already done (does not suspend)
...
// slow path -- suspend
return //The magic is here
}
Creating a queue
suspend fun Channel.declareQueue(queueSpecification: QueueSpecification): DeclareOk {
val queueDeclaration = AMQP.Queue.Declare.Builder()
.queue(queueSpecification.name)
.durable(queueSpecification.durable)
.exclusive(queueSpecification.exclusive)
.autoDelete(queueSpecification.autoDelete)
.arguments(queueSpecification.arguments)
.build()
return this.asyncCompletableRpc(queueDeclaration).await().method as DeclareOk
}
Let's try it!
@Test
fun `declare queue test`() {
val queueName = "declare_queue_test"
factory.newConnection().use { connection ->
connection.createChannel().use { channel ->
channel.declareQueue(QueueSpecification(queueName))
assertTrue { getQueues().find { it.name == queueName } != null }
}
}
}
Doesn't compile: suspend function should be called only from a coroutine or another suspend function

GlobalScope
public object GlobalScope : CoroutineScope {
override val coroutineContext: CoroutineContext
get() = EmptyCoroutineContext
}

Returns immediately, coroutine works in a background thread pool
Combine it!
@Test
fun `bind queue test`() {
factory.newConnection().use { connection ->
connection.createChannel().use { channel ->
runBlocking {
channel.declareExchange(ExchangeSpecification("exchange")) //#1
channel.declareQueue(QueueSpecification("queue")) //#2
channel.bindQueue(BindQueueSpecification("queue", "exchange")) //#3
}
}
}
}
Concurrency in Coroutines

Concurrency in Coroutines
Expectations
Coroutine code
"Normal" code
Coroutine builder
Multi async
Proceeding linearly
Concurrency in Coroutines
Reality
Coroutine code
"Normal" code
Coroutine builder
Proceeding linearly
Proceeding
linearly
Under the hood
Coroutines: State machine object
suspend fun initialize(channel: Channel) {
channel.declareExchange(ExchangeSpecification("exchange"))
channel.declareQueue(QueueSpecification("queue"))
channel.bindQueue(BindQueueSpecification("queue", "exchange"))
}
Under the hood
Coroutines: State machine object
Data for each state
Index of current state
suspend fun initialize(channel: Channel, continuation: Continuation) {
val sm = object : CoroutineImpl { ... }
switch(sm.lable) {
case 0:
channel.declareExchange(ExchangeSpecification("exchange"), sm)
case 1:
channel.declareQueue(QueueSpecification("queue"), sm)
case 2:
channel.bindQueue(BindQueueSpecification("queue", "exchange"), sm)
}
}
Call stack
Coroutine builder
Action 1
Action 2
Suspend coroutine
unwind
Continuation in heap
Coroutine builders

Synchronous code
Suspendable code
Coroutine builders
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
...
}
Launch
public fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T> {
...
}
Async
Runnable??
Callable??
Inside Coroutine
public interface CoroutineScope {
public val coroutineContext: CoroutineContext
}
CoroutineScope
interface CoroutineContext {
abstract operator fun <E : Element> get(key: Key<E>): E?
abstract fun minusKey(key: Key<*>): CoroutineContext
open operator fun plus(context: CoroutineContext): CoroutineContext
...
}
CoroutineContext
interface Element : CoroutineContext
interface Key<E : Element>
Improve it!
@Test
fun `bind queue test`() {
factory.newConnection().use { connection ->
connection.createChannel().use { channel ->
runBlocking {
val exchange: Deferred = async { channel.declareExchange(ExchangeSpecification("exchange")) }
val queue: Deferred = async { channel.declareQueue(QueueSpecification("queue")) }
awaitAll(exchange, queue)
channel.bindQueue(BindQueueSpecification("queue", "exchange"))
}
}
}
}
Structured concurrency
...
coroutineScope {
val exchange = async { channel.declareExchange(ExchangeSpecification("exchange")) }
val queue = async { channel.declareQueue(QueueSpecification("queue")) }
awaitAll(exchange, queue)
channel.bindQueue(BindQueueSpecification("queue", "exchange"))
}
...
Improve it again!
suspend fun Channel.declareQueue(queueSpecification: QueueSpecification): AMQP.Queue.DeclareOk {
val channel = this
val queueDeclaration = AMQP.Queue.Declare.Builder()
.queue(queueSpecification.name)
.durable(queueSpecification.durable)
.exclusive(queueSpecification.exclusive)
.autoDelete(queueSpecification.autoDelete)
.arguments(queueSpecification.arguments)
.build()
return withContext(resourceManagementDispatcher) {
channel.asyncCompletableRpc(queueDeclaration).await().method as AMQP.Queue.DeclareOk
}
}
val resourceManagementDispatcher = newSingleThreadContext("ResourceManagementDispatcher")
Dispatcher
GlobalScope.launch(context = /** default **/ Dispatchers.Default) {}
- Dispatchers.Default - CPU bound work
- Dispatchers.IO - intensive IO blocking operations
- Dispatchers.Unconfined - Risky business
- Private thread pools can be created with newSingleThreadContext and newFixedThreadPoolContext.
- An arbitrary Executor can be converted to a dispatcher with asCoroutineDispatcher extension function.

Coroutine dispatchers direct traffic
(to threads)
Dispatcher
SubscribeOn
Initial Context
ObserveOn
withContext
Recap: CompletableFuture
val exchangeDeclaration = CompletableFuture.supplyAsync {
channel.exchangeDeclare("exchange", "direct")
}.exceptionally(ex -> {
LOGGER.error("Boom", e)
return null;
});
val queueDeclaration = CompletableFuture.supplyAsync {
channel.queueDeclare("queue", false, false, false, emptyMap())
}.exceptionally(ex -> {
LOGGER.error("Boom", e)
return null;
});
val allOf = CompletableFuture.allOf(exchangeDeclaration, queueDeclaration)
allOf.get()
channel.queueBind("queue", "exchange", "")
Recap: Reactive
val exchangeDeclaration = sender.declareExchange(ExchangeSpecification.exchange())
val queueDeclaration = sender.declareQueue(QueueSpecification.queue())
Flux.concat(exchangeDeclaration, queueDeclaration)
.then(sender.bind(BindingSpecification.binding()))
.doOnError { e -> LOGGER.error("Boom", e) }
.subscribe { r -> LOGGER.info("Done") }
coroutineScope {
val exchange = async { channel.declareExchange(ExchangeSpecification()) }
val queue = async { channel.declareQueue(QueueSpecification()) }
awaitAll(exchange, queue)
channel.bindQueue(BindQueueSpecification())
}
Recap: Coroutines
The problem #2
String message = "Hello World!";
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
Publish a message *

* You must not lose messages

Transactions
ch.txSelect();
for (int i = 0; i < MSGCOUNT; ++i) {
ch.basicPublish("",
QUEUENAME,
PERSISTENTBASIC,
"Hello".getBytes());
ch.txCommit();
}
- Transactions are needlessly heavy: every commit requires a fsync(), which takes a lot of time to complete
-
Transactions are blocking: the publisher has to wait for the broker to process each message
Problems:
Enter Confirms
volatile SortedSet<Long> unconfirmedSet =
Collections.synchronizedSortedSet(new TreeSet());
ch.setConfirmListener(new ConfirmListener() {
public void handleAck(long seqNo, boolean multiple) {
if (multiple) {
unconfirmedSet.headSet(seqNo+1).clear();
} else {
unconfirmedSet.remove(seqNo);
}
}
public void handleNack(long seqNo, boolean multiple) {
// handle the lost messages somehow
}
});
Enter Confirms
ch.confirmSelect();
for (long i = 0; i < MSGCOUNT; ++i) {
unconfirmedSet.add(ch.getNextPublishSeqNo());
ch.basicPublish("",
QUEUENAME,
PERSISTENT_BASIC,
"Hello".getBytes());
}
Confirm channel
class ConfirmChannel internal constructor(private val channel: Channel) : Channel by channel {
init {
channel.confirmSelect()
}
fun publisher() = ConfirmPublisher(this)
}
fun Connection.createConfirmChannel(): ConfirmChannel = ConfirmChannel(this.createChannel())


Publisher
class ConfirmPublisher internal constructor(private val channel: Channel) {
private val continuations = ConcurrentHashMap<Long, Continuation<Boolean>>()
init {
channel.addConfirmListener(AckListener(continuations))
}
...
}
Listener
class AckListener(
private val continuations: ConcurrentHashMap<Long, Continuation<Boolean>>
) : ConfirmListener {
...
private fun handle(deliveryTag: Long, multiple: Boolean, ack: Boolean) {
if (multiple) {
...
continuations.remove(tag)?.resume(ack)
...
} else {
continuations.remove(deliveryTag)?.resume(ack)
...
}
}
}
Publisher
class ConfirmPublisher internal constructor(private val channel: Channel) {
...
suspend fun publishWithConfirm(message: OutboundMessage): Boolean {
val messageSequenceNumber = channel.nextPublishSeqNo
logger.debug { "The message Sequence Number: $messageSequenceNumber" }
return suspendCancellableCoroutine { continuation ->
continuations[messageSequenceNumber] = continuation
continuation.invokeOnCancellation { continuations.remove(messageSequenceNumber) }
cancelOnIOException(continuation) {
message.run {
channel.basicPublish(exchange, routingKey, properties, msg.toByteArray())
}
}
}
}
}
Coroutine
val channel = connection.createConfirmChannel()
val publisher = channel.publisher()
channel.declareQueue(QueueSpecification("queue"))
val acks = (1..times).map {
async {
publisher.publishWithConfirm(createMessage("Hello #$it"))
LOGGER.info("Message {} sent successfully",
String(r.getOutboundMessage().getBody()))
}
}.awaitAll()
JavaRx
val confirmations = sender.sendWithPublishConfirms(Flux.range(1, count)
.map({ i -> OutboundMessage("", queue, "Message_$i".toByteArray()) }))
sender.declareQueue(QueueSpecification.queue(queue))
.thenMany(confirmations)
.doOnError({ e -> LOGGER.error("Send failed", e) })
.subscribe({ r ->
if (r.isAck()) {
LOGGER.info("Message {} sent successfully",
String(r.getOutboundMessage().getBody()))
}
})
Results
The White Rabbit
Benchmark (numberOfMessages) Mode Cnt Score Error Units
ConfirmPublisherBenchmark.sendWithPublishConfirm 1 avgt 2 104.172 us/op
ConfirmPublisherBenchmark.sendWithPublishConfirm 10 avgt 2 598.625 us/op
ConfirmPublisherBenchmark.sendWithPublishConfirm 100 avgt 2 3845.833 us/op
ConfirmPublisherBenchmark.sendWithPublishConfirm 1000 avgt 2 36108.709 us/op
ConfirmPublisherBenchmark.sendWithPublishConfirm 10000 avgt 2 392132.353 us/op
ConfirmPublisherBenchmark.sendWithPublishConfirm 100000 avgt 2 4098567.349 us/op
Reactor Rabbitmq
Benchmark (nbMessages) Mode Cnt Score Error Units
SenderBenchmark.sendWithPublishConfirms 1 avgt 2 697.424 us/op
SenderBenchmark.sendWithPublishConfirms 10 avgt 2 1306.490 us/op
SenderBenchmark.sendWithPublishConfirms 100 avgt 2 4819.441 us/op
SenderBenchmark.sendWithPublishConfirms 1000 avgt 2 39597.671 us/op
SenderBenchmark.sendWithPublishConfirms 10000 avgt 2 373226.865 us/op
SenderBenchmark.sendWithPublishConfirms 100000 avgt 2 3900685.520 us/op
Consumer
public interface Channel extends ShutdownNotifier, AutoCloseable {
...
String basicConsume(
String queue,
boolean autoAck,
DeliverCallback deliverCallback,
CancelCallback cancelCallback
);
void basicAck(
long deliveryTag,
boolean multiple
);
}
Consumer v1
class ConfirmConsumer
internal constructor(private val amqpChannel: Channel, amqpQueue: String) {
private val continuations = LinkedBlockingQueue<Continuation<Delivery>>()
init {
amqpChannel.basicConsume(amqpQueue, false,
DeliverCallback { consumerTag, message -> continuations.take().resume(message) },
CancelCallback { logger.info { "Cancelled" } }
)
}
suspend fun consumeWithConfirm(
handler: (Delivery) -> Unit) =
coroutineScope {
val delivery = suspendCancellableCoroutine<Delivery> { continuations.put(it) }
handler(delivery)
amqpChannel.basicAck(delivery.envelope.deliveryTag, false)
}
}
Channel "quality of service"
public interface Channel ... {
...
void basicQos(
int prefetchCount,
boolean global
);
}
Consumer v1
class ConfirmConsumer
constructor(private val amqpChannel: Channel, amqpQueue: String, prefetchSize: Int) {
private val continuations = LinkedBlockingQueue<Continuation<Delivery>>(prefetchSize)
init {
amqpChannel.basicConsume(amqpQueue, false,
DeliverCallback { consumerTag, message -> continuations.take().resume(message) },
CancelCallback { logger.info { "Cancelled" } }
)
}
suspend fun consumeWithConfirm(
handler: (Delivery) -> Unit) =
coroutineScope {
val delivery = suspendCancellableCoroutine<Delivery> { continuations.put(it) }
handler(delivery)
amqpChannel.basicAck(delivery.envelope.deliveryTag, false)
}
}
Channels
Deferred values provide a convenient way to transfer a single value between coroutines. Channels provide a way to transfer a stream of values

https://github.com/Kotlin/kotlinx.coroutines/blob/master/docs/channels.md
suspending
suspending
Channels
private val continuations = Channel<Delivery>()
private lateinit var consTag: String
Compilation error
suspend fun consumeWithConfirm(
handler: (Delivery) -> Unit)
= coroutineScope {
val delivery = continuations.receive()
handler(delivery)
AMQPChannel.basicAck(delivery.envelope.deliveryTag, false)
}
init {
consTag = amqpChannel.basicConsume(amqpQueue, false,
{ consumerTag, message ->
if (consumerTag == consTag) {
continuations.send(message)
}
},
{ consumerTag ->
if (consumerTag == consTag) {
logger.info { "Consumer $consumerTag has been cancelled" }
continuations.cancel()
}
}
)
}
Consumer v2
init {
consTag = AMQPChannel.basicConsume(AMQPQueue, false,
{ consumerTag, message ->
if (consumerTag == consTag) {
GlobalScope.launch { continuations.send(message) }
}
},
{ consumerTag ->
if (consumerTag == consTag) {
logger.info { "Consumer $consumerTag has been cancelled" }
continuations.cancel()
}
}
)
}
Consumer v3
init {
consTag = AMQPChannel.basicConsume(AMQPQueue, false,
{ consumerTag, message ->
if (consumerTag == consTag) {
continuations.sendBlocking(message)
}
},
{ consumerTag ->
if (consumerTag == consTag) {
logger.info { "Consumer $consumerTag has been cancelled" }
continuations.cancel()
}
}
)
}
Recap: Reactive
val queueDeclaration = sender.declareQueue(QueueSpecification.queue(queue))
val messages = receiver.consumeAutoAck(queue)
queueDeclaration
.thenMany(messages)
.take(3)
.subscribe({ m ->
LOGGER.info("Received message {}", String(m.getBody()))
})
subscribeOn ?
observeOn ?
Reacp: Coroutine
val handler = { m -> LOGGER.info("Received message {}", String(m.getBody())) }
channel.declareQueue(QueueSpecification())
val consumer = channel.consumer()
for (i in 1..3) consumer.consumeWithConfirm(handler)
Conclusion
- The code is more readable and maintainable
- An ecosystem is big and is getting bigger
- Easy to integrate to your codebase
- "Free optimisation"
- Upcoming integrations with Spring and so on

Questions?

The White Rabbit

Telegram

Meetup: Kotlin Coroutines in Practice: writing asynchronous RabbitMQ client
By Vyacheslav Artemyev
Meetup: Kotlin Coroutines in Practice: writing asynchronous RabbitMQ client
- 131