我试图在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}}
答案 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
包含可变状态。