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

akka.actor.dsl.Inbox.scala Maven / Gradle / Ivy

There is a newer version: 2.2.6.3
Show newest version
/*
 * Copyright (C) 2009-2019 Lightbend Inc. 
 */

package akka.actor.dsl

import scala.concurrent.Await
import akka.actor.ActorLogging

import scala.collection.immutable.TreeSet
import scala.concurrent.duration._
import akka.actor.Cancellable
import akka.actor.Actor

import scala.collection.mutable.Queue
import akka.actor.ActorSystem
import akka.actor.ActorRef
import akka.util.Timeout
import akka.actor.Status
import java.util.concurrent.TimeoutException
import java.util.concurrent.atomic.AtomicInteger

import akka.pattern.ask
import akka.actor.ActorDSL
import akka.actor.Props
import com.github.ghik.silencer.silent

/**
 * INTERNAL API
 */
private[akka] object Inbox {

  private sealed trait Query {
    def deadline: Deadline
    def withClient(c: ActorRef): Query
    def client: ActorRef
  }
  private final case class Get(deadline: Deadline, client: ActorRef = null) extends Query {
    def withClient(c: ActorRef) = copy(client = c)
  }
  private final case class Select(deadline: Deadline, predicate: PartialFunction[Any, Any], client: ActorRef = null)
      extends Query {
    def withClient(c: ActorRef) = copy(client = c)
  }
  private final case class StartWatch(target: ActorRef)
  private case object Kick

}

@silent
trait Inbox { this: ActorDSL.type =>

  import Inbox._

  protected trait InboxExtension { this: Extension =>
    val DSLInboxQueueSize = config.getInt("inbox-size")

    val inboxNr = new AtomicInteger
    val inboxProps = Props(classOf[InboxActor], ActorDSL, DSLInboxQueueSize)

    def newReceiver: ActorRef = mkChild(inboxProps, "inbox-" + inboxNr.incrementAndGet)
  }

  private implicit val deadlineOrder: Ordering[Query] = new Ordering[Query] {
    def compare(left: Query, right: Query): Int = left.deadline.time.compare(right.deadline.time)
  }

  private class InboxActor(size: Int) extends Actor with ActorLogging {
    var clients = Queue.empty[Query]
    val messages = Queue.empty[Any]
    var clientsByTimeout = TreeSet.empty[Query]
    var printedWarning = false

    def enqueueQuery(q: Query): Unit = {
      val query = q.withClient(sender())
      clients.enqueue(query)
      clientsByTimeout += query
    }

    def enqueueMessage(msg: Any): Unit = {
      if (messages.size < size) messages.enqueue(msg)
      else {
        if (!printedWarning) {
          log.warning(
            "dropping message: either your program is buggy or you might want to increase akka.actor.dsl.inbox-size, current value is " + size)
          printedWarning = true
        }
      }
    }

    var currentMsg: Any = _
    val clientPredicate: (Query) => Boolean = {
      case _: Get          => true
      case Select(_, p, _) => p.isDefinedAt(currentMsg)
      case _               => false
    }

    var currentSelect: Select = _
    val messagePredicate: (Any => Boolean) = (msg) => currentSelect.predicate.isDefinedAt(msg)

    var currentDeadline: Option[(Deadline, Cancellable)] = None

    def receive =
      ({
        case g: Get =>
          if (messages.isEmpty) enqueueQuery(g)
          else sender() ! messages.dequeue()
        case s: Select =>
          if (messages.isEmpty) enqueueQuery(s)
          else {
            currentSelect = s
            messages.dequeueFirst(messagePredicate) match {
              case Some(msg) => sender() ! msg
              case None      => enqueueQuery(s)
            }
            currentSelect = null
          }
        case StartWatch(target) => context.watch(target)
        case Kick =>
          val now = Deadline.now
          val pred = (q: Query) => q.deadline.time < now.time
          val overdue = clientsByTimeout.iterator.takeWhile(pred)
          while (overdue.hasNext) {
            val toKick = overdue.next()
            toKick.client ! Status.Failure(new TimeoutException("deadline passed"))
          }
          clients = clients.filterNot(pred)
          clientsByTimeout = clientsByTimeout.from(Get(now))
        case msg =>
          if (clients.isEmpty) enqueueMessage(msg)
          else {
            currentMsg = msg
            clients.dequeueFirst(clientPredicate) match {
              case Some(q) => { clientsByTimeout -= q; q.client ! msg }
              case None    => enqueueMessage(msg)
            }
            currentMsg = null
          }
      }: Receive).andThen { _ =>
        if (clients.isEmpty) {
          if (currentDeadline.isDefined) {
            currentDeadline.get._2.cancel()
            currentDeadline = None
          }
        } else {
          val next = clientsByTimeout.head.deadline
          import context.dispatcher
          if (currentDeadline.isEmpty) {
            currentDeadline = Some((next, context.system.scheduler.scheduleOnce(next.timeLeft, self, Kick)))
          } else {
            // must not rely on the Scheduler to not fire early (for robustness)
            currentDeadline.get._2.cancel()
            currentDeadline = Some((next, context.system.scheduler.scheduleOnce(next.timeLeft, self, Kick)))
          }
        }
      }
  }

  /*
   * make sure that AskTimeout does not accidentally mess up message reception
   * by adding this extra time to the real timeout
   */
  private val extraTime = 1.minute

  /**
   * Create a new actor which will internally queue up messages it gets so that
   * they can be interrogated with the `akka.actor.dsl.Inbox!.Inbox!.receive`
   * and `akka.actor.dsl.Inbox!.Inbox!.select` methods. It will be created as
   * a system actor in the ActorSystem which is implicitly (or explicitly)
   * supplied.
   */
  def inbox()(implicit system: ActorSystem): Inbox = new Inbox(system)

  class Inbox(system: ActorSystem) extends akka.actor.Inbox {

    val receiver: ActorRef = Extension(system).newReceiver

    // Java API
    def getRef: ActorRef = receiver
    def send(target: ActorRef, msg: AnyRef): Unit = target.tell(msg, receiver)

    private val defaultTimeout: FiniteDuration = Extension(system).DSLDefaultTimeout

    /**
     * Receive a single message from the internal `receiver` actor. The supplied
     * timeout is used for cleanup purposes and its precision is subject to the
     * resolution of the system’s scheduler (usually 100ms, but configurable).
     *
     * Warning: This method blocks the current thread until a message is
     * received, thus it can introduce dead-locks (directly as well as
     * indirectly by causing starvation of the thread pool). Do not use
     * this method within an actor!
     */
    def receive(timeout: FiniteDuration = defaultTimeout): Any = {
      implicit val t = Timeout(timeout + extraTime)
      Await.result(receiver ? Get(Deadline.now + timeout), Duration.Inf)
    }

    /**
     * Receive a single message for which the given partial function is defined
     * and return the transformed result, using the internal `receiver` actor.
     * The supplied timeout is used for cleanup purposes and its precision is
     * subject to the resolution of the system’s scheduler (usually 100ms, but
     * configurable).
     *
     * Warning: This method blocks the current thread until a message is
     * received, thus it can introduce dead-locks (directly as well as
     * indirectly by causing starvation of the thread pool). Do not use
     * this method within an actor!
     */
    def select[T](timeout: FiniteDuration = defaultTimeout)(predicate: PartialFunction[Any, T]): T = {
      implicit val t = Timeout(timeout + extraTime)
      predicate(Await.result(receiver ? Select(Deadline.now + timeout, predicate), Duration.Inf))
    }

    /**
     * Make the inbox’s actor watch the target actor such that reception of the
     * Terminated message can then be awaited.
     */
    def watch(target: ActorRef): Unit = receiver ! StartWatch(target)

    /**
     * Overridden finalizer which will try to stop the actor once this Inbox
     * is no longer referenced.
     */
    override def finalize(): Unit = {
      system.stop(receiver)
    }
  }

  implicit def senderFromInbox(implicit inbox: Inbox): ActorRef = inbox.receiver
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy