
epus-std_3.0.1.0.source-code.RPCChannel.scala Maven / Gradle / Ivy
/*
* Copyright 2021 Hossein Naderi
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package lepus.std
import cats.effect.Concurrent
import cats.syntax.all.*
import fs2.Stream
import lepus.client.*
import lepus.client.apis.NormalMessagingChannel
import lepus.protocol.domains.*
trait RPCServer[F[_], I, O] {
def requests: Stream[F, RequestMethod[I]]
def respond(req: RequestMethod[I], o: O): F[Unit]
def ignore(i: RequestMethod[I]): F[Unit]
def reject(i: RequestMethod[I]): F[Unit]
}
trait RPCClient[F[_], I, O] {
def send(id: ShortString, i: I): F[Unit]
def responses: Stream[F, ResponseMethod[O]]
def processed(i: ResponseMethod[O]): F[Unit]
}
final case class RequestMethod[I](
id: ShortString,
sender: QueueName,
payload: I,
tag: DeliveryTag
)
final case class ResponseMethod[I](
requestId: ShortString,
payload: I,
tag: DeliveryTag
)
/** RPCChannel implements an async RPC communication channel topology.
*
* In this topology, each server has its own endpoint, where clients can send
* methods to, server then can decide to response to sender's address, ignore
* the request, or reject it. Clients can then consume responses, and mark them
* as processed. This topology models an point to point communication, with at
* least one delivery semantics, so your processing MUST be idempotent and
* async, as both responses and requests might be received several times, and
* with any ordering.
*/
object RPCChannel {
/** server peer in [[lepus.std.RPCChannel]] topology */
def server[F[_]: Concurrent, I, O](
endpoint: RPCDefinition[I, O]
)(ch: Channel[F, NormalMessagingChannel[F]]): F[RPCServer[F, I, O]] = for {
_ <- ch.queue.declare(endpoint.name, durable = true)
} yield new {
override def respond(req: RequestMethod[I], o: O): F[Unit] =
endpoint.serverCodec.encode(o) match {
case Left(error) => error.raiseError
case Right(msg) =>
ch.messaging.publishRaw(
ExchangeName.default,
routingKey = req.sender,
msg
) >> ch.messaging.ack(req.tag, false)
}
override def requests: Stream[F, RequestMethod[I]] = ch.messaging
.consumeRaw(endpoint.name, noAck = false)
.flatMap(env =>
endpoint.clientCodec.decode(env.message) match
case Left(error @ _) =>
Stream.exec(reject(env.deliveryTag))
case Right(value) =>
val senderQ =
value.properties.replyTo
.flatMap(QueueName.from(_).toOption)
val msgId = value.properties.messageId
msgId
.zip(senderQ)
.fold(Stream.exec(reject(env.deliveryTag))) { (id, sender) =>
Stream.emit(
RequestMethod(
id,
sender,
value.payload,
env.deliveryTag
)
)
}
)
override def ignore(i: RequestMethod[I]): F[Unit] =
ch.messaging.ack(i.tag)
override def reject(i: RequestMethod[I]): F[Unit] = reject(i.tag)
private def reject(dtag: DeliveryTag) =
ch.messaging.reject(dtag, false)
}
/** client peer in [[lepus.std.RPCChannel]] topology */
def client[F[_], I, O](
endpoint: RPCDefinition[I, O],
persistent: Option[QueueName] = None
)(
ch: Channel[F, NormalMessagingChannel[F]]
)(using F: Concurrent[F]): F[RPCClient[F, I, O]] = for {
q <- persistent match {
case None =>
ch.queue
.declare(QueueName.autoGen, exclusive = true)
.flatMap(
F.fromOption(_, new UnknownError("Must respond with Queue name"))
)
.map(_.queue)
case Some(value) => ch.queue.declare(value, durable = true).as(value)
}
} yield new {
override def send(id: ShortString, i: I): F[Unit] = endpoint.clientCodec
.encode(i)
.fold(
_.raiseError,
msg =>
ch.messaging.publishRaw(
ExchangeName.default,
routingKey = endpoint.name,
msg.withMessageId(id).withReplyTo(q)
)
)
override def responses: Stream[F, ResponseMethod[O]] = ch.messaging
.consumeRaw(q, noAck = false)
.flatMap(env =>
endpoint.serverCodec.decode(env.message) match
case Left(error) => Stream.exec(reject(env.deliveryTag))
case Right(value) =>
value.properties.correlationId
.fold(Stream.exec(reject(env.deliveryTag)))(responseTo =>
Stream.emit(
ResponseMethod(responseTo, value.payload, env.deliveryTag)
)
)
)
override def processed(i: ResponseMethod[O]): F[Unit] =
ch.messaging.ack(i.tag)
private def reject(dtag: DeliveryTag) =
ch.messaging.reject(dtag, requeue = false)
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy