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

com.newmotion.akka.rabbitmq.ChannelActor.scala Maven / Gradle / Ivy

The newest version!
package com.newmotion.akka.rabbitmq

import akka.actor.{ Props, ActorRef, FSM }
import collection.immutable.Queue
import ConnectionActor.ProvideChannel
import scala.annotation.tailrec
import scala.util.control.NonFatal

/**
 * @author Yaroslav Klymko
 */
object ChannelActor {
  sealed trait State
  case object Disconnected extends State
  case object Connected extends State

  case object GetState

  private[rabbitmq] sealed trait Data
  private[rabbitmq] case class InMemory(queue: Queue[OnChannel] = Queue()) extends Data
  private[rabbitmq] case class Connected(channel: Channel) extends Data

  def props(setupChannel: (Channel, ActorRef) => Any = (_, _) => ()): Props = {
    // Props(classOf[ChannelActor], setupChannel)
    Props(new ChannelActor(setupChannel))
  }

  private[rabbitmq] case class Retrying(retries: Int, onChannel: OnChannel) extends OnChannel {
    def apply(channel: Channel): Any = onChannel(channel)
  }
}

class ChannelActor(setupChannel: (Channel, ActorRef) => Any)
  extends RabbitMqActor
  with FSM[ChannelActor.State, ChannelActor.Data] {

  import ChannelActor._

  startWith(Disconnected, InMemory())

  private sealed trait ProcessingResult {}
  private case class ProcessSuccess(m: Any) extends ProcessingResult
  private case class ProcessFailureRetry(onChannel: Retrying) extends ProcessingResult
  private case object ProcessFailureDrop extends ProcessingResult

  private def header(state: ChannelActor.State, msg: Any) = s"in $state received $msg:"

  private def safeWithRetry(channel: Channel, fn: OnChannel): ProcessingResult = {
    safe(fn(channel)) match {
      case Some(r) =>
        ProcessSuccess(r)

      case None if channel.isOpen =>
        // If the function failed but the channel is still open, we know that the
        // problem was with f, and not the channel state. Therefore we do *not* retry f
        // in this case because its failure might be due to some inherent problem with f
        // itself, and in that case a whole application might get stuck in a retry loop.
        ProcessFailureDrop

      case None =>
        // The channel closed but the actor state is Connected: There is a small window
        // between a disconnect, sending an ShutdownSignal, and processing that signal.
        // Just because our ChannelMessage was processed in this window does not mean we
        // should ignore the intent of dropChannelAndRequestNewChannel (because there was,
        // in fact, no channel)
        fn match {
          case Retrying(retries, _) if retries == 0 =>
            ProcessFailureDrop
          case Retrying(retries, onChannel) =>
            ProcessFailureRetry(Retrying(retries - 1, onChannel))
          case _ =>
            ProcessFailureRetry(Retrying(3, fn))
        }
    }
  }

  when(Disconnected) {
    case Event(channel: Channel, InMemory(queue)) =>
      @tailrec
      def loop(qs: Queue[OnChannel]): State = qs.headOption match {
        case None => goto(Connected) using Connected(channel)
        case Some(onChannel) =>
          val res = safeWithRetry(channel, onChannel)
          log.debug("{} queued message {} resulted in {}", header(Disconnected, channel), onChannel, res)
          res match {
            case ProcessSuccess(_) => loop(qs.tail)
            case ProcessFailureRetry(retry) =>
              dropChannelAndRequestNewChannel(channel)
              stay() using InMemory(retry +: qs.tail)
            case ProcessFailureDrop =>
              log.warning("{} stopped retrying message {}", header(Disconnected, channel), onChannel)
              dropChannelAndRequestNewChannel(channel)
              stay() using InMemory(qs.tail)
          }
      }

      if (setup(channel)) {
        if (queue.nonEmpty) log.debug(
          "{} processing {} queued messages {}",
          header(Disconnected, channel), queue.size, queue.mkString("\n", "\n", ""))
        loop(queue)
      } else {
        dropChannelAndRequestNewChannel(channel)
        stay()
      }

    case Event(msg @ ChannelMessage(onChannel, dropIfNoChannel), InMemory(queue)) =>
      if (dropIfNoChannel) {
        log.debug("{} dropping message {}", header(Disconnected, msg), onChannel)
        stay()
      } else {
        log.debug("{} queueing message {}", header(Disconnected, msg), onChannel)
        stay() using InMemory(queue enqueue onChannel)
      }

    case Event(_: ShutdownSignal, _) => stay()
  }

  when(Connected) {
    case Event(channel: Channel, Connected(_)) =>
      log.debug("{} closing unexpected channel {}", header(Connected, channel), channel)
      close(channel)
      stay()

    case Event(msg: ShutdownSignal, Connected(channel)) =>
      (msg match {
        case ParentShutdownSignal =>
          Some(dropChannel _) // The parent is responsible for providing the new channel.
        case amqpSignal: AmqpShutdownSignal if amqpSignal.appliesTo(channel) =>
          Some(dropChannelAndRequestNewChannel _)
        case _ => None
      }).fold(stay()) { shutdownAction =>
        log.debug("{} shutdown", header(Connected, msg))
        shutdownAction(channel)
        goto(Disconnected) using InMemory()
      }

    case Event(msg @ ChannelMessage(onChannel, _), Connected(channel)) =>
      val res = safeWithRetry(channel, onChannel)
      log.debug("{} received channel message resulted in {}", header(Connected, msg), res)
      res match {
        case ProcessSuccess(_) => stay()
        case ProcessFailureRetry(retry) if !msg.dropIfNoChannel =>
          dropChannelAndRequestNewChannel(channel)
          goto(Disconnected) using InMemory(Queue(retry))
        case _ =>
          if (!msg.dropIfNoChannel) {
            log.warning("{} not retrying message {}", header(Connected, msg), onChannel)
          }
          dropChannelAndRequestNewChannel(channel)
          goto(Disconnected) using InMemory()
      }
  }

  whenUnhandled {
    case Event(GetState, _) =>
      sender() ! stateName
      stay()
  }

  onTransition {
    case Disconnected -> Connected => log.info("{} connected", self.path)
    case Connected -> Disconnected => log.warning("{} disconnected", self.path)
  }

  onTermination {
    case StopEvent(_, Connected, Connected(channel)) =>
      log.debug("{} closing channel {}", self.path, channel)
      close(channel)
  }

  initialize()

  private def setup(channel: Channel): Boolean = {
    channel.addShutdownListener(this)
    log.debug("{} setting up new channel {}", self.path, channel)
    try {
      safe(setupChannel(channel, self)).isDefined
    } catch {
      case NonFatal(throwable) =>
        log.debug("{} setup channel callback error {}", self.path, channel)
        close(channel)
        throw throwable
    }
  }

  private def dropChannelAndRequestNewChannel(broken: Channel): Unit = {
    dropChannel(broken)
    askForChannel()
  }

  private def dropChannel(brokenChannel: Channel): Unit = {
    log.debug("{} closing broken channel {}", self.path, brokenChannel)
    close(brokenChannel)
  }

  private def askForChannel(): Unit = {
    log.debug("{} asking for new channel", self.path)
    connectionActor ! ProvideChannel
  }

  private[rabbitmq] def connectionActor = context.parent

  @scala.throws[Exception](classOf[Exception])
  override def postRestart(reason: Throwable): Unit = {
    log.debug(s"{} child restarted with exception {}, reason: {}", self.path, reason, reason.getMessage)
    super.postRestart(reason)
    askForChannel()
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy