Kotlin Coroutines in Practice:
writing asynchronous RabbitMQ client
String message = "Hello World!";
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
ch.txSelect();
for (int i = 0; i < MSGCOUNT; ++i) {
ch.basicPublish("",
QUEUENAME,
PERSISTENTBASIC,
"Hello".getBytes());
ch.txCommit();
}
Transactions are blocking: the publisher has to wait for the broker to process each message
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
}
});
ch.confirmSelect();
for (long i = 0; i < MSGCOUNT; ++i) {
unconfirmedSet.add(ch.getNextPublishSeqNo());
ch.basicPublish("",
QUEUENAME,
PERSISTENT_BASIC,
"Hello".getBytes());
}
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)
You do not want threads:
You cannot afford threads:
You cannot do threads:
Continuation-passing style (CPS) is a style of programming in which control is passed explicitly in the form of a continuation. This is contrasted with a direct style, which is the usual style of programming.
ch.setConfirmListener(new ConfirmListener() {
public void handleAck(long seqNo, boolean multiple) {
// handle the acked messages somehow
}
public void handleNack(long seqNo, boolean multiple) {
// handle the nacked messages somehow
}
});
In computing, reactive programming is a declarative programming paradigm concerned with data streams and the propagation of change.
Flux<OutboundMessageResult> confirmations =
sender.sendWithPublishConfirms(Flux.range(1, count)
.map(i -> new OutboundMessage("", queue, ("Message_" + i).getBytes())));
sender.declareQueue(QueueSpecification.queue(queue))
.thenMany(confirmations)
.doOnError(e -> LOGGER.error("Send failed", e))
.subscribe(r -> {
if (r.isAck()) {
// do something
}
});
}
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.
try {
channel.declareQueue(QueueSpecification(QUEUE_NAME))
val message = new OutboundMessage("", queue, ("Message_" + i).getBytes()))
val ack = publisher.publishWithConfirm(message)
if (ack) {
//Do something
}
} catch (e: Exception) {
LOGGER.error("Send failed", e)
}
Reactive:
Coroutines:
fun publishWithConfirm(message: OutboundMessage): Boolean
Function signatures
fun publishWithConfirm(message: OutboundMessage): Observable<Boolean>
* Just a bit
suspend
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
Reactive:
Coroutines:
Debugging
for ((message) in list) {
publishWithConfirm(message)
}
Direct loops
try {
publishWithConfirm(message)
} catch (e: Exception) {
...
}
Direct exception handing
Direct higher-order functions
(1..times).map {
async {
publisher.publishWithConfirm(createMessage("Hello #$it"))
}
}.awaitAll()
//forEach, let, apply, repeat, filter, map, use etc
Easy to manage
withContext(Dispatchers.IO) {
publisher.publishWithConfirm(createMessage("Hello #$it"))
}
Reactive:
Coroutines
...
.subscribeOn(Schedulers.single())
.observeOn(Schedulers.elastic())
...
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
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>)
}
Coroutines: State machine object
Data for each state
Index of current state
/**
* 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
}
suspend fun Channel.declareQueue(queueSpecification: QueueSpecification): AMQP.Queue.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 AMQP.Queue.DeclareOk
}
@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
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
...
}
public fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T> {
...
}
public interface CoroutineScope {
public val coroutineContext: CoroutineContext
}
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
...
}
interface Element : CoroutineContext
interface Key<E : Element>
TODO: add example of passing custom property to context
public object GlobalScope : CoroutineScope {
override val coroutineContext: CoroutineContext
get() = EmptyCoroutineContext
}
Returns immediately, coroutine works in a background thread pool
@Test
fun `bind queue test`() {
factory.newConnection().use { connection ->
connection.createChannel().use { channel ->
runBlocking {
channel.declareExchange(ExchangeSpecification("test_exchange")) //#1
channel.declareQueue(QueueSpecification("test_queue")) //#2
channel.bindQueue(BindQueueSpecification("test_queue", "test_exchange")) //#3
}
}
}
}
TODO:
redraw the picture
TODO:
redraw the picture
@Test
fun `bind queue test`() = runBlocking {
factory.newConnection().use { connection ->
connection.createChannel().use { channel ->
val exchange = async { channel.declareExchange(ExchangeSpecification("test_exchange")) } //#1
val queue = async { channel.declareQueue(QueueSpecification("test_queue")) } //#2
awaitAll(exchange, queue) //#3
channel.bindQueue(BindQueueSpecification("test_queue", "test_exchange")) //#4
}
}
}
...
coroutineScope {
val exchange = async { channel.declareExchange(ExchangeSpecification("test_exchange")) }
val queue = async { channel.declareQueue(QueueSpecification("queue_test")) }
awaitAll(exchange, queue)
channel.bindQueue(BindQueueSpecification("queue_test", "test_exchange"))
}
...
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")
GlobalScope.launch(context = /** default **/ Dispatchers.Default) {}
Coroutine dispatchers direct traffic
(to threads)
val exchangeDeclaration = CompletableFuture.supplyAsync {
channel.exchangeDeclare("exchange", "direct")
}
val queueDeclaration = CompletableFuture.supplyAsync {
channel.queueDeclare("queue", false, false, false, emptyMap())
}
val allOf = CompletableFuture.allOf(exchangeDeclaration, queueDeclaration)
allOf.get()
channel.queueBind("queue", "exchange", "")
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);
val exchangeDeclaration =
Supplier { sender.declareExchange(ExchangeSpecification.exchange()) }
val queueDeclaration =
Supplier { sender.declareQueue(QueueSpecification.queue()) }
Flux.just(exchangeDeclaration, queueDeclaration)
.flatMap { task ->
Mono.just(task)
.subscribeOn(Schedulers.elastic())
.map { supplier -> supplier.get() }
}
.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())
}
...
class ConfirmPublisher internal constructor(private val channel: Channel) {
private val continuations = ConcurrentHashMap<Long, Continuation<Boolean>>()
init {
channel.addConfirmListener(AckListener(continuations))
}
suspend fun publishWithConfirm(message: OutboundMessage): Boolean {
val messageSequenceNumber = channel.nextPublishSeqNo
logger.debug { "The message Sequence Number: $messageSequenceNumber" }
return suspendCancellableCoroutine { continuation ->
continuations[messageSequenceNumber] = continuation
message.run { channel.basicPublish(exchange, routingKey, properties, body) }
}
}
}
class AckListener(
private val continuations: ConcurrentHashMap<Long, Continuation<Boolean>>
) : ConfirmListener {
...
private fun handle(deliveryTag: Long, multiple: Boolean, ack: Boolean) {
val lowerBound = lowerBoundOfMultiple.get()
if (multiple) {
for (tag in lowerBound..deliveryTag) {
continuations.remove(tag)?.resume(ack)
}
lowerBoundOfMultiple.compareAndSet(lowerBound, deliveryTag)
} else {
continuations.remove(deliveryTag)?.resume(ack)
if (deliveryTag == lowerBound + 1) {
lowerBoundOfMultiple.compareAndSet(lowerBound, deliveryTag)
}
}
}
}
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())
val channel = connection.createConfirmChannel()
val publisher = channel.publisher()
channel.declareQueue(QueueSpecification(QUEUE_NAME))
val acks = (1..times).map {
async {
publisher.publishWithConfirm(createMessage("Hello #$it"))
LOGGER.info("Message {} sent successfully",
String(r.getOutboundMessage().getBody()))
}
}.awaitAll()
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()))
}
})
[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")
public interface Channel extends ShutdownNotifier, AutoCloseable {
...
String basicConsume(
String queue,
boolean autoAck,
DeliverCallback deliverCallback,
CancelCallback cancelCallback
);
void basicAck(
long deliveryTag,
boolean multiple
);
}
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)
}
}
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
private val continuations = Channel<Delivery>()
private lateinit var consTag: String
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()
}
}
)
}
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) {
GlobalScope.launch { continuations.send(message) }
}
},
{ consumerTag ->
if (consumerTag == consTag) {
logger.info { "Consumer $consumerTag has been cancelled" }
continuations.cancel()
}
}
)
}
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()
}
}
)
}
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)
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 ?
suspend fun consumeWithConfirm(
handler: suspend (Delivery) -> Unit,
handlerDispatcher: CoroutineDispatcher)
= coroutineScope {
val delivery = continuations.receive()
withContext(handlerDispatcher) { handler(delivery) }
AMQPChannel.basicAck(delivery.envelope.deliveryTag, false)
}
public interface Channel ... {
...
void basicQos(
int prefetchCount,
boolean global
);
}
class ConfirmConsumer internal constructor(
private val AMQPChannel: Channel,
AMQPQueue: String,
prefetchSize: Int = 0) {
private val continuations = Channel<Delivery>(prefetchSize)
private lateinit var consTag: String
...
}
@Test
fun `test one message publishing v2`() {
factory.newConnection().use { connection ->
connection.createChannel().use { channel ->
runBlocking {
channel.declareQueue(QueueSpecification(QUEUE_NAME))
val consumer = channel.consumer(QUEUE_NAME, 3)
consumer.consumeWithConfirm(
parallelism = 3,
handler = { handleDelivery(it) }
)
}
}
}
}