jvmMain.no.dossier.libraries.amqpconnector.rpc.AmqpRpcClient.kt Maven / Gradle / Ivy
package no.dossier.libraries.amqpconnector.rpc
import com.rabbitmq.client.Connection
import kotlinx.coroutines.*
import kotlinx.serialization.KSerializer
import kotlinx.serialization.serializer
import mu.KotlinLogging
import no.dossier.libraries.amqpconnector.error.AmqpConsumingError
import no.dossier.libraries.amqpconnector.error.AmqpRpcError
import no.dossier.libraries.amqpconnector.consumer.AmqpConsumer
import no.dossier.libraries.amqpconnector.consumer.AmqpReplyingMode
import no.dossier.libraries.amqpconnector.primitives.*
import no.dossier.libraries.functional.*
import no.dossier.libraries.amqpconnector.publisher.AmqpPublisher
import no.dossier.libraries.amqpconnector.utils.getValidatedUUID
import no.dossier.libraries.amqpconnector.utils.suspendCancellableCoroutineWithTimeout
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentMap
import java.util.concurrent.Executors
import kotlin.coroutines.resume
class AmqpRpcClient(
private val responsePayloadSerializer: KSerializer,
@PublishedApi
internal val messageProcessingCoroutineScope: CoroutineScope,
private val publishingExchangeSpec: AmqpExchangeSpec,
@PublishedApi
internal val replyToExchangeSpec: AmqpExchangeSpec,
@PublishedApi
internal val replyQueueBindingKey: AmqpBindingKey,
private val routingKey: String,
private val publishingConnection: Connection,
private val consumingConnection: Connection,
private val consumerThreadPoolDispatcher: ExecutorCoroutineDispatcher,
private val onReplyConsumed: (message: AmqpInboundMessage) -> Unit,
private val onReplyRejected: (message: AmqpInboundMessage) -> Unit,
private val onRequestPublished: (message: AmqpOutboundMessage<*>, actualRoutingKey: String) -> Unit,
private val consumerPrefetchCount: Int
) {
@PublishedApi
internal val logger = KotlinLogging.logger { }
private val consumer: AmqpConsumer
@PublishedApi
internal val consumerQueueName: String
@PublishedApi
internal val publisherThreadPoolDispatcher: ExecutorCoroutineDispatcher =
Executors.newFixedThreadPool(1).asCoroutineDispatcher()
@PublishedApi
internal val publisher: AmqpPublisher
@PublishedApi
internal val pendingRequestsMap: ConcurrentMap>> = ConcurrentHashMap()
private val responseMessageHandler: (message: AmqpInboundMessage) -> Outcome = { message ->
logger.debug { "AMQP RPC Client - Received reply message with correlation ID: [${message.correlationId}]" }
val correlationIdResult = message.correlationId
?.let(::getValidatedUUID)
?.mapError { AmqpConsumingError("AMQP RPC Client - Invalid correlation ID") }
?: Failure(AmqpConsumingError("AMQP RPC Client - Missing correlationId"))
when(correlationIdResult) {
is Success -> {
val correlationId = correlationIdResult.value
val continuation = pendingRequestsMap[correlationId]
if(continuation == null) {
logger.error { "AMQP RPC Client - Received message with unknown correlation ID: [$correlationId]" }
} else {
if (continuation.isCancelled) {
logger.warn {
"AMQP RPC Client - Received response with correlation ID: [$correlationId], " +
"but the related request has been cancelled"
}
}
else {
continuation.resume(message.payload.mapError {
AmqpRpcError(it.message, mapOf("Payload" to it))
})
}
}
}
is Failure -> {
logger.error { correlationIdResult.error }
}
}
Success(Unit) // We need to send ACK, regardless the processing status
}
init {
val queueSpec = AmqpQueueSpec(
name = "", // Empty string means that the name will be assigned by the brokerExecutorCoroutineDispatcher
durable = false,
exclusive = true,
autoDelete = true
)
val deadLetterSpec = AmqpDeadLetterSpec(
enabled = false, // Since the dead letter forwarding is disabled, the below arguments are not relevant
exchangeSpec = replyToExchangeSpec,
routingKey = DeadLetterRoutingKey.SameAsOriginalMessage,
implicitQueueEnabled = false
)
consumer = AmqpConsumer(
replyToExchangeSpec,
replyQueueBindingKey, // Will be used only if consumingExchangeSpec.type is DIRECT or TOPIC
responseMessageHandler,
responsePayloadSerializer,
serializer(),
queueSpec,
deadLetterSpec,
AmqpReplyingMode.Never,
messageProcessingCoroutineScope,
onReplyConsumed,
onReplyRejected,
onMessageReplyPublished = { _, _ ->},
false,
consumerPrefetchCount
)
publisher = AmqpPublisher(
publishingExchangeSpec,
routingKey,
false,
publishingConnection,
publisherThreadPoolDispatcher,
onRequestPublished
)
consumerQueueName = consumer.startConsuming(consumingConnection, consumerThreadPoolDispatcher)
}
//TODO: extract internal body into a separate non-inline function and remove the @PublishedApi annotations
suspend inline operator fun invoke(
payload: T,
headers: Map = mapOf(),
timeoutMillis: Long = 10_000,
routingKey: AmqpRoutingKey = AmqpRoutingKey.PublisherDefault,
replyToRoutingKey: String = consumerQueueName
): Outcome {
val correlationId = UUID.randomUUID()
val refinedHeaders =
if (replyToExchangeSpec.name != "")
headers + (AmqpMessageProperty.REPLY_TO_EXCHANGE.name to replyToExchangeSpec.name)
else
headers
val message = AmqpOutboundMessage(
payload,
refinedHeaders,
replyToRoutingKey,
correlationId.toString(),
routingKey
)
return withContext(publisherThreadPoolDispatcher) {
suspendCancellableCoroutineWithTimeout(timeoutMillis, {
pendingRequestsMap.remove(correlationId)
AmqpRpcError("AMQP RPC Client - Request timed out")
}, { continuation ->
pendingRequestsMap[correlationId] = continuation
when (val result = publisher.invokeBlocking(message)) {
is Failure -> {
/* If the submission fails we want to resume right away */
pendingRequestsMap.remove(correlationId)
continuation.resume(
Failure(
AmqpRpcError(
"AMQP RPC Client - Failed to send RPC request",
mapOf("cause" to result.error)
)
)
)
}
is Success -> {
logger.debug { "AMQP RPC Client - Request sent, correlation ID: [${correlationId}]" }
}
}
})
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy