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

no.kodeworks.kvarg.util.AtLeastOnceDelivery.scala Maven / Gradle / Ivy

There is a newer version: 0.7
Show newest version
package no.kodeworks.kvarg.util

import akka.actor.{Actor, ActorLogging, ActorPath, ActorRef, ActorRefFactory, Props, ReceiveTimeout}
import akka.dispatch.Dispatcher
import akka.util.Timeout
import no.kodeworks.kvarg.util.AtLeastOnceDelivery._

import scala.collection.mutable
import scala.concurrent.Future
import scala.concurrent.duration._
import scala.language.postfixOps
import scala.language.implicitConversions
//TODO need support for sending from outside an actor, like AskSupport

/**
  * Adds !! and ?? support on implementing actor
  *
  * NB implementers _must_ call super.receive in their receive method
  * !! sends a message to target actor with at-least-once semantics, meaning the target has to ack the message for it to be considered delivered.
  * While awaiting ack, other messages might be enqueued for sending by other calls to !!. These are delivered in same order as calls to !!, although
  * some of the messages may be delivered multiple times.
  * Support is also added for processing incoming !! calls. The target need not implement this trait in order to process incoming messages sent with !!,
  * it can interact directly with the sealed case classes defined here.
  */
trait AtLeastOnceDelivery extends Actor with ActorLogging {
  log.debug("AtLeastOnceDelivery init")
  val resendInterval = defaultResendInterval
  val maxNumRetries = defaultMaxNumRetries

  // Must be overriden by implementers like so: override def receive = (super.receive orElse {case ....})
  abstract override def receive = (super.receive orElse {
    case a: AtLeastOnceSend =>
      log.debug("{}", a)
      a.sends.foreach(s => self forward s.msg)
      sender ! AtLeastOnceAck(a.id)
    case a: AtLeastOnceAck =>
      log.debug("{}", a)
      manager forward a
  })

  //manager is here for when !! is called within actor but outside of normal receive processing i.e in a future callback
  val manager = context.actorOf(Props(new Actor with ActorLogging {
    log.debug("Manager init")
    val awaiting = mutable.Map[String, ActorRef]()

    override def receive = {
      case _send: Send =>
        val path = pathToString(_send.ref.path)
        (awaiting.get(path) match {
          case Some(awaiter) =>
            log.debug(s"Send to '$path', existing awaiter")
            awaiter
          case _ =>
            log.debug(s"Send to '$path', new awaiter created")
//            val origin = sender
            val awaiter = context.actorOf(Props(new Awaiter(false, resendInterval, maxNumRetries)).
              withDispatcher(context.dispatcher.asInstanceOf[Dispatcher].id), s"alod_awaiter_${pathToString(_send.ref.path)}")
            awaiting += pathToString(_send.ref.path) -> awaiter
            awaiter
        }) forward _send
      case a: AtLeastOnceAck =>
        awaiting.get(pathToString(sender.path)).foreach(_ forward a)
      case x => log.error(pathToString(self.path) + " " + x)
    }
  }).withDispatcher(context.dispatcher.asInstanceOf[Dispatcher].id), "alod_manager")

  implicit def !!(ref: ActorRef): AtLeastOnceTell = new AtLeastOnceTell(ref, manager)

  //  implicit def ??(ref: ActorRef): AtLeastOnceRef = new AtLeastOnceRef(ref)
}

object AtLeastOnceDelivery {

  //When sending, contains the target, when awaiting recieve, contains the sender
  sealed trait Send {
    def msg: Any

    def ref: ActorRef
  }

  case class Tell(override val msg: Any, override val ref: ActorRef) extends Send

  case class Ask(override val msg: Any, override val ref: ActorRef, timeout: Timeout, sent: Long = System.currentTimeMillis) extends Send {
    def timeLeft(from: Long): Duration = {
      val tl = sent - from + timeout.duration.toMillis
      if (0 >= tl) Duration.Zero
      else tl millis
    }
  }

  sealed trait AtLeastOnceMessage

  case class AtLeastOnceSend(id: Int, sends: Send*) extends AtLeastOnceMessage

  case class AtLeastOnceAck(id: Int) extends AtLeastOnceMessage

  val defaultTimeout: Timeout = Timeout(5 seconds)

  val defaultResendInterval: FiniteDuration = 5 seconds

  var defaultMaxNumRetries: Int = 5

  //awaiter is responsible for all !! interaction with a certain target
  /**
    *
    * @param oneShot
    * @param resendInterval
    * @param maxNumRetries TODO handle this
    */
  class Awaiter(oneShot: Boolean = false, resendInterval: FiniteDuration = defaultResendInterval, maxNumRetries: Int = defaultMaxNumRetries) extends Actor with ActorLogging {
    var _id: Int = 0
    var aloRef: Option[ActorRef] = None
    var target: ActorRef = null
    val unsent = mutable.ListBuffer[Send]()
    var unacked: Option[AtLeastOnceSend] = None
    var unreplied = mutable.ListBuffer[Ask]()
    var numRetries = 0

    log.debug("Awaiter init")

    /**
      * @param sends must be nonempty
      */
    def send(sends: Send*) {
      log.debug(s"Send ${sends.mkString(", ")} to $target")
      if (maxNumRetries == numRetries) {
        numRetries = 0
        log.warning(s"Max number of retries reached, discarding ${sends.size} sends.")
        doUnsent
      } else {
        val aloSend = AtLeastOnceSend(id, sends: _*)
        target.!(aloSend)(self)
        unacked = Some(aloSend)
        context.setReceiveTimeout(resendInterval)
        numRetries += 1
      }
    }

    def isTell: Boolean = aloRef.nonEmpty

    def id = {
      val i = _id
      _id = _id + 1
      i
    }

    //TODO provide exactly once guarantees?
    override def receive = {
      case a: AtLeastOnceAck if unacked.nonEmpty && a.id == unacked.get.id =>
        numRetries = 0
        log.debug(s"Message ${a.id} acked")
        unreplied ++= unacked.get.sends.collect {
          case a: Ask => a
        }
        unacked = None
        if (unreplied.nonEmpty) {
          val now = System.currentTimeMillis
          val maxAsk = unreplied.maxBy(_.timeLeft(now).toMillis).timeLeft(now)
          log.debug(s"Still awaiting ${
            unreplied.size
          } asks for another ${
            maxAsk.toSeconds
          } seconds")
          context.setReceiveTimeout(maxAsk)
        }
        else if (oneShot && isTell) context.stop(self)
        else doUnsent
      case a: AtLeastOnceAck =>
        log.warning(s"Superfluous ack: $a")
      case _send: Send =>
        target = _send.ref
        val s = _send match {
          case a: Ask => a.copy(ref = sender);
          case t: Tell => {
            //if Tell, we know it was sent by ALO implementer (if within trait) or dead letter (if called via pattern)
            aloRef = Some(sender)
            t.copy(ref = sender)
          }
        }
        if (unacked.nonEmpty || unreplied.nonEmpty) {
          log.debug(s"Still awaiting ${
            if (unacked.nonEmpty) s"unacked id ${
              unacked.get.id
            }"
            else s"${
              unreplied.size
            } unreplied messages"
          }" +
            s", postponing send of ${
              s.msg
            } to ${
              pathToString(_send.ref.path)
            }")
          unsent += s
        } else {
          if (unsent.nonEmpty) log.error("Bug in awaiter, unsent nonEmpty")
          send(s)
        }
      case ReceiveTimeout if unacked.nonEmpty =>
        log.debug(s"Message ${
          unacked.get.id
        } not acked in time, resending")
        send(unacked.get.sends: _*)
      case ReceiveTimeout if unreplied.nonEmpty =>
        log.debug(s"Unreplied messages were not replied to")
        unreplied.clear
        doUnsent
      case ReceiveTimeout =>
        log.warning("Superfluous timeout")
      case a: AtLeastOnceSend =>
        // if we get this, it's the response of an ask or tell. If it's the response of an ask, we tell the waiting actor. If response to tell we forward to last known manager
        // otherwise it would've been passed to the AtLeastOnce implementer
        log.debug("Response to ask by at least once message")
        val (replies, tells) = a.sends.map(_.msg).splitAt(unreplied.size)
        val (_, remainingUnreplied) = unreplied.splitAt(replies.size)
        replies.zip(unreplied).foreach {
          case (msg, ask) =>
            ask.ref ! msg
        }
        unreplied = remainingUnreplied
        for {
          a <- aloRef;
          t <- tells
        } a forward t
        sender ! AtLeastOnceAck(a.id)
        if (oneShot && !isTell) context.stop(self)
        else doUnsent
      case msg =>
        //TODO what if its a tell from an actor? need to forward that as well
        log.debug("Response by generic message")
        if (unreplied.nonEmpty) {
          unreplied.remove(0).ref ! msg
        } else {
          log.warning("Got unexpected response, no unreplieds")
        }
        if (oneShot) context.stop(self)
        else doUnsent
    }

    def doUnsent {
      if (unsent.nonEmpty) {
        send(unsent.toArray: _*)
        unsent.clear
      } else context.setReceiveTimeout(Duration.Undefined)
    }

    override def postStop {
      log.debug("Awaiter died")
    }
  }

  final class AtLeastOnceTell(target: ActorRef, manager: ActorRef) {
    def !!(msg: Any)
          (implicit sender: ActorRef = Actor.noSender) {
      manager.!(Tell(msg, target))(sender)
    }

    def ??(msg: Any)
          (implicit sender: ActorRef = Actor.noSender,
           timeout: Timeout = defaultTimeout): Future[Any] = {
      import akka.pattern.ask
      manager ? Ask(msg, target, timeout)
    }
  }

  final class Pattern(target: ActorRef) {
    def !!(msg: Any, resendInterval: FiniteDuration = defaultResendInterval, maxNumRetries: Int = defaultMaxNumRetries)
          (implicit sender: ActorRef = Actor.noSender, context: ActorRefFactory) {
      context.actorOf(Props(new Awaiter(true, resendInterval, maxNumRetries))) ! Tell(msg, target)
    }

    def ??(msg: Any, resendInterval: FiniteDuration = defaultResendInterval, maxNumRetries: Int = defaultMaxNumRetries)
          (implicit sender: ActorRef = Actor.noSender, context: ActorRefFactory, timeout: Timeout = defaultTimeout): Future[Any] = {
      import akka.pattern.ask
      //TODO hack up a 'same thread dispatcher' like in akka test and internally in asksupport
      context.actorOf(Props(new Awaiter(true, resendInterval, maxNumRetries))) ? Ask(msg, target, timeout)
    }
  }

  def pathToString(path: ActorPath) = path.elements.mkString("_")

  //TODO return awaiter so anon user can ensure sequenced messaging?
  implicit def pattern(target: ActorRef): Pattern = new Pattern(target)
}

abstract class AtLeastOnceDeliveryDefault extends IgnoreActor with AtLeastOnceDelivery




© 2015 - 2025 Weber Informatics LLC | Privacy Policy