Skip to content

Commit 149879b

Browse files
committed
Ensure response RPC clients always exist
Avoid dynamically creating/destroying clients and topics on every RPC request
1 parent a63deaa commit 149879b

File tree

3 files changed

+41
-40
lines changed

3 files changed

+41
-40
lines changed

latte/src/main/java/gg/beemo/latte/broker/rabbitmq/RabbitConnection.kt

-3
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,6 @@ private class ChannelData(
1818
var consumerTag: String? = null,
1919
)
2020

21-
// TODO Temporary keys/topics created by RPC clients should ideally be cached and re-used,
22-
// instead of being destroyed and recreated every time.
23-
2421
class RabbitConnection(
2522
rabbitHosts: Array<String>,
2623
override val serviceName: String,

latte/src/main/java/gg/beemo/latte/broker/rpc/RpcClient.kt

+40-36
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ import kotlinx.coroutines.CoroutineScope
77
import kotlinx.coroutines.TimeoutCancellationException
88
import kotlinx.coroutines.delay
99
import kotlinx.coroutines.flow.Flow
10+
import kotlinx.coroutines.flow.MutableSharedFlow
1011
import kotlinx.coroutines.flow.callbackFlow
1112
import kotlinx.coroutines.flow.single
13+
import kotlinx.coroutines.launch
1214
import java.util.concurrent.atomic.AtomicInteger
1315
import java.util.concurrent.atomic.AtomicReference
1416
import kotlin.time.Duration
@@ -37,14 +39,6 @@ class RpcClient<RequestT, ResponseT>(
3739

3840
private val requestProducer = client.producer(topic, key, options, requestType, requestIsNullable)
3941
private val requestConsumer = client.consumer(topic, key, options, requestType, requestIsNullable) { msg ->
40-
val responseProducer = client.producer(
41-
client.toResponseTopic(topic),
42-
client.toResponseKey(key),
43-
options,
44-
responseType,
45-
responseIsNullable,
46-
)
47-
4842
suspend fun sendResponse(response: ResponseT?, status: RpcStatus, isException: Boolean, isUpdate: Boolean) {
4943
val responseMsg = RpcResponseMessage(
5044
client.toResponseTopic(topic),
@@ -77,15 +71,30 @@ class RpcClient<RequestT, ResponseT>(
7771
return@consumer
7872
} catch (ex: Exception) {
7973
log.error(
80-
"Uncaught RPC callback error while processing message ${msg.headers.messageId} " +
74+
"Uncaught RPC callbac#k error while processing message ${msg.headers.messageId} " +
8175
"with key '$key' in topic '$topic'",
8276
ex,
8377
)
8478
return@consumer
85-
} finally {
86-
responseProducer.destroy()
8779
}
8880
}
81+
private val responseProducer = client.producer(
82+
client.toResponseTopic(topic),
83+
client.toResponseKey(key),
84+
options,
85+
responseType,
86+
responseIsNullable,
87+
)
88+
private val responseFlow = MutableSharedFlow<BaseBrokerMessage<ResponseT>>()
89+
private val responseConsumer = client.consumer(
90+
client.toResponseTopic(topic),
91+
client.toResponseKey(key),
92+
options,
93+
responseType,
94+
responseIsNullable,
95+
) {
96+
responseFlow.emit(it)
97+
}
8998

9099
suspend fun call(
91100
request: RequestT,
@@ -110,36 +119,29 @@ class RpcClient<RequestT, ResponseT>(
110119
require(maxResponses > 0) { "maxResponses must be at least 1" }
111120
}
112121
return callbackFlow {
122+
val cbFlow = this
113123
val responseCounter = AtomicInteger(0)
114124
val timeoutLatch = maxResponses?.let { SuspendingCountDownLatch(it) }
115125
val messageId = AtomicReference<String?>(null)
116126

117-
val responseConsumer = client.consumer(
118-
client.toResponseTopic(topic),
119-
client.toResponseKey(key),
120-
options,
121-
responseType,
122-
responseIsNullable,
123-
) {
124-
val msg = it.toRpcResponseMessage()
125-
if (msg.headers.inReplyTo != messageId.get()) {
126-
return@consumer
127+
launch { // Asynchronously consume responses; gets cancelled with callbackFlow
128+
responseFlow.collect {
129+
val msg = it.toRpcResponseMessage()
130+
if (msg.headers.inReplyTo != messageId.get()) {
131+
return@collect
132+
}
133+
// Close the callbackFlow if we receive an exception
134+
if (msg.headers.isException) {
135+
cbFlow.close(RpcException(msg.headers.status))
136+
return@collect
137+
}
138+
cbFlow.send(msg)
139+
timeoutLatch?.countDown()
140+
val count = responseCounter.incrementAndGet()
141+
if (maxResponses != null && count >= maxResponses) {
142+
cbFlow.close()
143+
}
127144
}
128-
// Close the flow if we receive an exception
129-
if (msg.headers.isException) {
130-
close(RpcException(msg.headers.status))
131-
return@consumer
132-
}
133-
send(msg)
134-
timeoutLatch?.countDown()
135-
val count = responseCounter.incrementAndGet()
136-
if (maxResponses != null && count >= maxResponses) {
137-
close()
138-
}
139-
}
140-
141-
invokeOnClose {
142-
responseConsumer.destroy()
143145
}
144146

145147
messageId.set(requestProducer.send(request, services, instances))
@@ -161,6 +163,8 @@ class RpcClient<RequestT, ResponseT>(
161163
override fun doDestroy() {
162164
requestProducer.destroy()
163165
requestConsumer.destroy()
166+
responseProducer.destroy()
167+
responseConsumer.destroy()
164168
}
165169

166170
}

latte/src/test/kotlin/gg/beemo/latte/broker/TestBrokerClient.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class TestBrokerClient(
2222

2323
val greetingRpc = rpc<GreetingRequest, GreetingResponse>(
2424
topic = "rpc.greetings",
25-
key = "greeting.requests",
25+
key = "greet",
2626
) {
2727
log.info("greetingRpc received request: ${it.value}")
2828
return@rpc RpcStatus.OK to GreetingResponse("Hello, ${it.value.name}")

0 commit comments

Comments
 (0)