no.kodeworks.kvarg.util.AtLeastOnceDelivery.scala Maven / Gradle / Ivy
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