All Downloads are FREE. Search and download functionalities are using the official Maven repository.

liewhite.rpc.RpcServer.scala Maven / Gradle / Ivy

The newest version!
package liewhite.rpc

import zio.*
import zio.concurrent.*
import com.rabbitmq.client.AMQP
import com.rabbitmq.client.ConnectionFactory
import scala.jdk.CollectionConverters.*
import com.rabbitmq.client.AMQP.Basic.Deliver
import com.rabbitmq.client.Delivery
import java.util.concurrent.ConcurrentMap
import com.rabbitmq.utility.BlockingCell
import java.util.concurrent.ConcurrentHashMap
import com.rabbitmq.client.UnroutableRpcRequestException
import zio.stream.ZStream
import com.rabbitmq.client.AMQP.Confirm
import com.rabbitmq.client.Return
import com.rabbitmq.client.Consumer
import com.rabbitmq.client.DefaultConsumer
import com.rabbitmq.client.AMQP.BasicProperties
import com.rabbitmq.client.Envelope
import java.{util => ju}
import com.rabbitmq.client.Channel
import scala.util.Try
import liewhite.json.*

import liewhite.rpc.Transport

/*
 * Server端只需要将上层业务的返回序列化, 以及恢复异常
 *
 * Client只关心是否消息到达了, 以及对方是否回复了, 不关心回复内容
 */

// 协议层面的返回, 业务层判断code后自行处理data
case class RpcResponse(code: Int, msg: String = "", data: String) derives Schema

class RpcServer(transport: Transport) {

  val channels = new ConcurrentHashMap[Int, Channel]

  def declareQueue(channel: Channel, queueName: String, route: String) =
    ZIO.attemptBlocking {
      channel.queueDeclare(
        queueName,
        true,
        false,
        true,
        new ju.HashMap[String, Object]
      )
      channel.queueBind(queueName, "amq.topic", route)
    }

  def returnListener(channel: Channel) =
    ZStream.asyncScoped[Any, Nothing, Return] { cb =>
      val listener = channel.addReturnListener(msg => cb(ZIO.succeed(Chunk(msg))))

      ZIO.acquireRelease(
        ZIO.logInfo("[rpc-server] create server return listener") *>
          ZIO.succeed(listener)
      )(ln =>
        ZIO.logInfo("[rpc-server] remove return listener") *>
          ZIO
            .succeed(Try(channel.removeReturnListener(ln)))
      )
    }

  def consumer(channel: Channel, queueName: String) =
    ZStream.asyncScoped[Any, Nothing, Delivery] { cb =>
      val consumer = channel.basicConsume(
        queueName,
        false,
        new DefaultConsumer(channel) {
          override def handleDelivery(
            consumerTag: String,
            envelope: Envelope,
            properties: BasicProperties,
            body: Array[Byte]
          ): Unit =
            cb(
              ZIO.succeed(Chunk(Delivery(envelope, properties, body)))
            )
        }
      )

      ZIO.acquireRelease(
        ZIO.logInfo(s"[rpc-server] create consumer $queueName") *>
          ZIO.succeed(consumer)
      )(ln =>
        ZIO.logInfo(s"[rpc-server] remove consumer $queueName") *>
          ZIO
            .succeed(Try(channel.basicCancel(consumer)))
      )
    }

  def listen(
    route: String,
    callback: Delivery => Task[String],
    queue: Option[String] = None
  ): ZIO[Scope, Throwable, Fiber.Runtime[Throwable, Unit]] = {
    val queueName = queue.getOrElse(route)
    (for {
      channel <- transport.scopedChannel()
      _       <- declareQueue(channel, queueName, route)
      _ <- returnListener(channel)
             .runForeach(item =>
               ZIO
                 .logWarning("[rpc-server] response returned, may be client is dead")
             )
             .fork
      f <- consumer(channel, queueName).runForeach { msg =>
             val process =
               ZIO.logInfo(s"serve request: [route: $route queue: $queue body: ${String(msg.getBody())}]") *> ZIO
                 .attempt(callback(msg)) // 捕获业务层面抛出来的异常, 业务层面也应该捕获异常,返回结构化数据
                 .flatten
                 .tapErrorCause(err => ZIO.logWarning(s"failed process handler: $err"))
                 .map(data => RpcResponse(0, "ok", data))
                 .catchAllCause { e =>
                   ZIO.succeed(
                     RpcResponse(
                       500,
                       "failed process message",
                       e.toString()
                     )
                   )
                 }

             process.flatMap { result =>
               val replyTo = Option(msg.getProperties().getReplyTo())
               val deliveryTag = Option(msg.getProperties().getHeaders()).flatMap(i =>
                 Try(i.get("deliveryTag").asInstanceOf[Long]).toOption
               )
               val z = ZIO.succeed(replyTo.zip(deliveryTag))

               z.flatMap { rede =>
                 rede match
                   case None => {
                     ZIO.unit
                   }
                   case Some((reply, tag)) => {
                     publish(
                       channel,
                       "amq.direct",
                       reply,
                       result.toJson.toArray,
                       false,
                       AMQP.BasicProperties
                         .Builder()
                         .headers(
                           Map(
                             "deliveryTag" -> tag
                           ).asJava
                         )
                         .build()
                     )
                   }

               } *> ZIO.attemptBlocking {
                 channel.basicAck(msg.getEnvelope().getDeliveryTag(), false)
               }
             }
           }.tapErrorCause(e => ZIO.logError(s"server exit with: $e")).fork
    } yield f)
  }

  def publish(
    channel: Channel,
    exchange: String,
    route: String,
    message: Array[Byte],
    mandatory: Boolean = false,
    props: AMQP.BasicProperties = null
  ): Task[Unit] =
    ZIO.attemptBlocking {
      val r = channel.basicPublish(exchange, route, mandatory, props, message);
      r
    }

}

object RpcServer {
  def layer: ZLayer[Transport & Scope, Nothing, RpcServer] =
    ZLayer(for {
      transport <- ZIO.service[Transport]
      server <-
        ZIO.acquireRelease(ZIO.succeed(RpcServer(transport)))(s => ZIO.logInfo("server exit: "))
    } yield server)
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy