在Actor的receive方法中调用Multiple Future

时间:2014-08-07 05:02:28

标签: scala redis akka actor future

我试图在Actor的receive方法中进行两次外部调用(到Redis数据库)。两个调用都返回Future,我需要第二个调用中第一个Future的结果。 我在Redis事务中包含两个调用,以避免其他人在读取数据库时修改数据库中的值。

根据第二个Future的值更新actor的内部状态。

以下是我当前的代码看起来不正确,因为我在Future.onComplete回调中更新了actor的内部状态。

我不能使用PipeTo模式,因为我需要两个Future都必须在一个事务中。 如果我使用Await作为第一个Future,那么我的接收方法将阻止。 知道如何解决这个问题吗?

我的第二个问题与我使用Future的方式有关。以下Future的使用情况是否正确?是否有更好的方式来处理多个期货?想象一下,如果每个都有3个或4个Future,具体取决于前一个。

import akka.actor.{Props, ActorLogging, Actor}
import akka.util.ByteString
import redis.RedisClient

import scala.concurrent.Future
import scala.util.{Failure, Success}


object GetSubscriptionsDemo extends App {
  val akkaSystem = akka.actor.ActorSystem("redis-demo")
  val actor = akkaSystem.actorOf(Props(new SimpleRedisActor("localhost", "dummyzset")), name = "simpleactor")
  actor ! UpdateState
}

case object UpdateState

class SimpleRedisActor(ip: String, key: String) extends Actor with ActorLogging {

  //mutable state that is updated on a periodic basis
  var mutableState: Set[String] = Set.empty

  //required by Future
  implicit val ctx = context dispatcher

  var rClient = RedisClient(ip)(context.system)

  def receive = {
    case UpdateState => {
      log.info("Start of UpdateState ...")

      val tran = rClient.transaction()

      val zf: Future[Long] = tran.zcard(key)  //FIRST Future 
      zf.onComplete {

        case Success(z) => {
          //SECOND Future, depends on result of FIRST Future 
          val rf: Future[Seq[ByteString]] = tran.zrange(key, z - 1, z) 
          rf.onComplete {
            case Success(x) => {
              //convert ByteString to UTF8 String
              val v = x.map(_.utf8String)
              log.info(s"Updating state with $v ")
              //update actor's internal state inside callback for a Future
              //IS THIS CORRECT ?
              mutableState ++ v
            }
            case Failure(e) => {
              log.warning("ZRANGE future failed ...", e)
            }
          }
        }
        case Failure(f) => log.warning("ZCARD future failed ...", f)
      }
      tran.exec()

    }
  }

}

编译但是当我运行它时会被击中。

2014-08-07  INFO [redis-demo-akka.actor.default-dispatcher-3] a.e.s.Slf4jLogger - Slf4jLogger started
2014-08-07 04:38:35.106UTC INFO [redis-demo-akka.actor.default-dispatcher-3] e.c.s.e.a.g.SimpleRedisActor - Start of UpdateState ...
2014-08-07 04:38:35.134UTC INFO [redis-demo-akka.actor.default-dispatcher-8] r.a.RedisClientActor - Connect to localhost/127.0.0.1:6379
2014-08-07 04:38:35.172UTC INFO [redis-demo-akka.actor.default-dispatcher-4] r.a.RedisClientActor - Connected to localhost/127.0.0.1:6379

更新1

为了使用pipeTo模式,我需要访问演员的tran和第一个未来(zf),我将Future Future { {1}}因为SECOND z取决于FIRST的值( //SECOND Future, depends on result of FIRST Future val rf: Future[Seq[ByteString]] = tran.zrange(key, z - 1, z) )。

{{1}}

2 个答案:

答案 0 :(得分:1)

在不了解您正在使用的redis客户端的情况下,我可以提供一个更干净的备用解决方案,并且在关闭可变状态时不会遇到问题。这个想法是使用主/工作类情况,其中主(SimpleRedisActor)接收执行工作的请求,然后委托给执行工作的工作者并响应状态进行更新。该解决方案看起来像这样:

object SimpleRedisActor{
  case object UpdateState
  def props(ip:String, key:String) = Props(classOf[SimpleRedisActor], ip, key)
}

class SimpleRedisActor(ip: String, key: String) extends Actor with ActorLogging {
  import SimpleRedisActor._
  import SimpleRedisWorker._

  //mutable state that is updated on a periodic basis
  var mutableState: Set[String] = Set.empty

  val rClient = RedisClient(ip)(context.system)

  def receive = {
    case UpdateState => 
      log.info("Start of UpdateState ...")      
      val worker = context.actorOf(SimpleRedisWorker.props)
      worker ! DoWork(rClient, key)

    case WorkResult(result) =>
      mutableState ++ result

    case FailedWorkResult(ex) =>
      log.error("Worker got failed work result", ex)
  }
}

object SimpleRedisWorker{
  case class DoWork(client:RedisClient, key:String)
  case class WorkResult(result:Seq[String])
  case class FailedWorkResult(ex:Throwable)
  def props = Props[SimpleRedisWorker]
}

class SimpleRedisWorker extends Actor{
  import SimpleRedisWorker._
  import akka.pattern.pipe
  import context._

  def receive = {
    case DoWork(client, key) =>
      val trans = client.transaction()
      trans.zcard(key) pipeTo self
      become(waitingForZCard(sender, trans, key) orElse failureHandler(sender, trans))
  }

  def waitingForZCard(orig:ActorRef, trans:RedisTransaction, key:String):Receive = {      
    case l:Long =>
      trans.zrange(key, l -1, l) pipeTo self
      become(waitingForZRange(orig, trans) orElse failureHandler(orig, trans))
  }

  def waitingForZRange(orig:ActorRef, trans:RedisTransaction):Receive = {
    case s:Seq[ByteString] =>
      orig ! WorkResult(s.map(_.utf8String))
      finishAndStop(trans)
  }

  def failureHandler(orig:ActorRef, trans:RedisTransaction):Receive = {
    case Status.Failure(ex) => 
      orig ! FailedWorkResult(ex)
      finishAndStop(trans)   
  }

  def finishAndStop(trans:RedisTransaction) {
    trans.exec()
    context stop self
  }
}

工作人员启动事务,然后调用redis并最终在停止之前完成事务。当它调用redis时,它会获得未来并管道返回自身以继续处理,将接收方法改变为显示其状态进展的机制。在这样的模型中(我认为它有点类似于错误核心模式),主人拥有并保护状态,委托"冒险"给一个可以弄清楚国家变化应该是什么的孩子,但是变化仍归主人所有。

现在再一次,我不知道你正在使用的redis客户端的功能,以及它是否足够安全甚至可以做这种事情,但这并不是真的重点。关键在于展示一种更安全的结构,这种结构涉及需要安全更改的期货和状态。

答案 1 :(得分:0)

使用回调来改变内部状态不是一个好主意,摘自akka docs

  

当使用未来的回调,例如onComplete,onSuccess和onFailure时,你需要小心避免关闭包含actor的引用,即不要在回调中调用封闭actor上的方法或访问可变状态。

为什么你担心pipeTo和交易? 不确定redis事务是如何工作的,但我猜这个事务并不包含第二个未来的onComplete回调。

我会把状态放到一个单独的演员中,你也管道未来。这样,您就有了一个单独的邮箱,其中的顺序与修改状态所引入的邮件的顺序相同。此外,如果有任何读取请求进入,它们也将按正确的顺序排列。

编辑以回复已修改的问题:好的,所以你不想管道第一个未来,这是有道理的,并且应该没问题,因为第一个回调是无害的。第二个未来的回调就是问题,因为它操纵了国家。但是这个未来可以管道而不需要访问交易。

基本上我的建议是:

val firstFuture = tran.zcard
firstFuture.onComplete {
   val secondFuture = tran.zrange
   secondFuture pipeTo stateActor
}

stateActor包含可变状态。