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

internal.LowlevelChannel.scala Maven / Gradle / Ivy

The newest version!
/*
 * 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.client
package internal

import cats.effect.Concurrent
import cats.effect.kernel.Resource
import cats.implicits.*
import fs2.Stream
import fs2.concurrent.Signal
import fs2.concurrent.SignallingRef
import lepus.client.Channel.Status
import lepus.protocol.ChannelClass.Close
import lepus.protocol.*
import lepus.protocol.domains.ConsumerTag

private[client] type ContentMethod = BasicClass.Deliver | BasicClass.Return
private[client] type ContentSyncResponse =
  BasicClass.GetOk | BasicClass.GetEmpty.type
private[client] type ConfirmationResponse =
  BasicClass.Ack | BasicClass.Nack

private[client] trait ChannelReceiver[F[_]] {
  def asyncContent(m: ContentMethod): F[Unit]
  def syncContent(m: ContentSyncResponse): F[Unit]
  def header(h: Frame.Header): F[Unit]
  def body(h: Frame.Body): F[Unit]
  def method(m: Method): F[Unit]
  def onClose: F[Unit]
}

private[client] trait ChannelTransmitter[F[_]] {
  def publish(method: BasicClass.Publish, msg: MessageRaw): F[Unit]

  def sendWait(m: Method): F[Method]
  def sendNoWait(m: Method): F[Unit]

  def get(m: BasicClass.Get): F[Option[SynchronousGetRaw]]

  def delivered(
      ctag: Option[ConsumerTag]
  ): Resource[F, (ConsumerTag, Stream[F, DeliveredMessageRaw])]
  final def delivered
      : Resource[F, (ConsumerTag, Stream[F, DeliveredMessageRaw])] =
    delivered(None)

  def returned: Stream[F, ReturnedMessageRaw]
  def confirmed: Stream[F, Confirmation]

  def status: Signal[F, Status]
}

private[client] trait LowlevelChannel[F[_]]
    extends ChannelReceiver[F],
      ChannelTransmitter[F]

/** Facade over other small components */
private[client] object LowlevelChannel {
  def from[F[_]: Concurrent](config: ChannelConfig): ChannelFactory[F] = in =>
    for {
      disp <- MessageDispatcher[F](
        returnedBufSize = config.returnedBufSize,
        confirmBufSize = config.confirmBufSize,
        deliveryBufSize = config.deliveryBufSize
      )
      out <- ChannelOutput(in.output, maxMethods = config.maxConcurrentPublish)
      wlist <- Waitlist[F, Option[SynchronousGetRaw]](size =
        config.maxConcurrentGet
      )
      content <- ContentChannel(in.number, out, disp, wlist)
      rpc <- RPCChannel(out, in.number, maxMethods = config.maxConcurrentRPC)
      pub = ChannelPublisher(in.number, in.frameMax, out)
      ch <- apply(content, rpc, pub, disp, out)
    } yield ch

  def apply[F[_]](
      content: ContentChannel[F],
      rpc: RPCChannel[F],
      pub: ChannelPublisher[F],
      disp: MessageDispatcher[F],
      out: ChannelOutput[F, Frame]
  )(using F: Concurrent[F]): F[LowlevelChannel[F]] = for {
    state <- SignallingRef[F].of(Status.Active)
  } yield new LowlevelChannel[F] {

    override def onClose: F[Unit] =
      state.set(Status.Closed) >> rpc.sendNoWait(ChannelClass.CloseOk)

    private def handle(f: F[Unit]) =
      f.onError(_ => state.set(Status.Closed)).recoverWith {
        case e: AMQPError if e.replyCode.isChannelError =>
          rpc.sendNoWait(
            ChannelClass.Close(
              e.replyCode,
              e.replyText,
              e.classId,
              e.methodId
            )
          )
      }

    override def status: Signal[F, Status] = state

    private def isClosed = status.map(_ == Status.Closed)

    private def setFlow(e: Boolean) =
      if e
      then out.unblock >> state.set(Status.Active)
      else out.block >> state.set(Status.InActive)

    def asyncContent(m: ContentMethod): F[Unit] =
      handle(content.asyncNotify(m))

    def syncContent(m: ContentSyncResponse): F[Unit] =
      handle(content.syncNotify(m))

    def header(h: Frame.Header): F[Unit] = handle(content.recv(h))

    def body(h: Frame.Body): F[Unit] = handle(content.recv(h))

    def method(m: Method): F[Unit] =
      handle(m match {
        case ChannelClass.Flow(e) =>
          setFlow(e) >> rpc.sendNoWait(ChannelClass.FlowOk(e))
        case ChannelClass.Close(replyCode, replyText, classId, methodId) =>
          onClose
        case m @ ChannelClass.CloseOk => state.set(Status.Closed) >> rpc.recv(m)
        case m: ConfirmationResponse  => disp.confirm(m)
        case BasicClass.Cancel(ctag, _) => disp.cancel(ctag)
        case _                          => content.abort >> rpc.recv(m)
      })

    def publish(method: BasicClass.Publish, msg: MessageRaw): F[Unit] =
      pub.send(method, msg)

    def sendWait(m: Method): F[Method] =
      F.race(rpc.sendWait(m), isClosed.waitUntil(identity)).flatMap {
        case Right(_)    => F.raiseError(ChannelIsClosed)
        case Left(value) => F.pure(value)
      }

    def sendNoWait(m: Method): F[Unit] = rpc.sendNoWait(m)
    def get(m: BasicClass.Get): F[Option[SynchronousGetRaw]] =
      content.get(m).flatMap(_.get)

    def delivered(
        ctag: Option[ConsumerTag]
    ): Resource[F, (ConsumerTag, Stream[F, DeliveredMessageRaw])] =
      disp.deliveryQ(ctag).map { case (ctag, q) =>
        (ctag, Stream.fromQueueNoneTerminated(q).interruptWhen(isClosed))
      }

    def returned: Stream[F, ReturnedMessageRaw] =
      Stream.fromQueueUnterminated(disp.returnQ).interruptWhen(isClosed)

    def confirmed: Stream[F, Confirmation] = Stream
      .fromQueueUnterminated(disp.confirmationQ)
      .map {
        case BasicClass.Ack(tag, multi) =>
          Confirmation(Acknowledgment.Ack, tag, multi)
        case BasicClass.Nack(tag, multi, _) =>
          Confirmation(Acknowledgment.Nack, tag, multi)
      }
      .interruptWhen(isClosed)
  }

  case object ChannelIsClosed
      extends RuntimeException("Channel is already closed!")
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy