├── README.md ├── actors ├── actor-dsl.md ├── actors.md ├── dispatchers.md ├── fault-tolerance.md ├── fsm.md ├── mailbox.md ├── routing.md ├── testing-actor-systems.md └── typed-actor.md ├── futures-and-agents ├── agents.md └── futures.md ├── general ├── actor-references-paths-and-addresses.md ├── actor-systems.md ├── akka-and-the-java-memory-model.md ├── configuration.md ├── location-transparency.md ├── message-delivery-reliability.md ├── supervision-and-monitoring.md └── what-is-an-actor.md ├── imgs ├── ActorPath.png ├── RemoteDeployment.png ├── actor_lifecycle.png ├── faulttolerancesample-failure-flow.png ├── faulttolerancesample-normal-flow.png └── guardians.png ├── introduce ├── examples-of-use-cases-for-Akka.md ├── getting-start.md ├── use-case-deployment-scenarios.md ├── what-is-akka.md └── why-akka.md └── networking ├── io.md ├── remoting.md ├── serialization.md └── using-tcp.md /README.md: -------------------------------------------------------------------------------- 1 | # akka 中文指南 2 | 3 | * [介绍] (introduce/what-is-akka.md) 4 | * [什么是akka](introduce/what-is-akka.md) 5 | * [为什么选择akka](introduce/why-akka.md) 6 | * [开始](introduce/getting-start.md) 7 | * [用例和部署场景](introduce/use-case-deployment-scenarios.md) 8 | * [Akka使用实例](introduce/examples-of-use-cases-for-Akka.md) 9 | * [概述](general/actor-systems.md) 10 | * [Actor系统](general/actor-systems.md) 11 | * [什么是Actor](general/what-is-an-actor.md) 12 | * [监管和重启](general/supervision-and-monitoring.md) 13 | * [Actor引用,路径和地址](general/actor-references-paths-and-addresses.md) 14 | * [位置透明性](general/location-transparency.md) 15 | * [Akka和Java内存模型](general/akka-and-the-java-memory-model.md) 16 | * [消息发送语义](general/message-delivery-reliability.md) 17 | * [配置](general/configuration.md) 18 | * [Actors](actors/actors.md) 19 | * [Actor](actors/actors.md) 20 | * [有类型Actor](actors/typed-actor.md) 21 | * [容错](actors/fault-tolerance.md) 22 | * [派发器](actors/dispatchers.md) 23 | * [邮箱](actors/mailbox.md) 24 | * [路由](actors/routing.md) 25 | * [有限状态机](actors/fsm.md) 26 | * [测试actor系统](actors/testing-actor-systems.md) 27 | * [Actor DSL](actors/actor-dsl.md) 28 | * [futures和agents](futures-and-agents/futures.md) 29 | * [futures](futures-and-agents/futures.md) 30 | * [agents](futures-and-agents/agents.md) 31 | * [网络](networking/remoting.md) 32 | * [远程调用](networking/remoting.md) 33 | * [序列化](networking/serialization.md) 34 | * [io](networking/io.md) 35 | * [使用TCP](networking/using-tcp.md) 36 | 37 | 本指南参考官方文档(scala版)2.3.9,详见[官方文档](http://akka.io/docs/) 38 | 39 | -------------------------------------------------------------------------------- /actors/actor-dsl.md: -------------------------------------------------------------------------------- 1 | # actor dsl(领域特定语言) 2 | 3 | 简单的actor可以通过`Act` trait更方便的创建,下面的引入支持该功能 4 | 5 | ```scala 6 | import akka.actor.ActorDSL._ 7 | import akka.actor.ActorSystem 8 | implicit val system = ActorSystem("demo") 9 | ``` 10 | 11 | 假定上面的import在本章的所有例子中使用。隐式的actor系统服务作为`ActorRefFactory`在下面的所有例子中起作用。为了创建一个简单的actor,下面几行代码就足够了。 12 | 13 | ```scala 14 | val a = actor(new Act { 15 | become { 16 | case "hello" => sender() ! "hi" } 17 | }) 18 | ``` 19 | 20 | 这里,actor作为`system.actorOf`或者`context.actorOf`的角色,这依赖于它访问的是哪一个上下文。它持有一个隐式`ActorRefFactory`,它在actor内以`implicit val context: ActorContext`的形式被使用。在一个actor的外部,你必须声明一个隐式的`ActorSystem`或者你能明确的给出工厂方法。 21 | 22 | 分配一个`context.become`(添加或者替换新的行为)的两种方式分开提供用于为嵌套receives开启一个clutter-free notation。 23 | 24 | ```scala 25 | val a = actor(new Act { 26 | become { // this will replace the initial (empty) behavior 27 | case "info" => sender() ! "A" 28 | case "switch" => 29 | becomeStacked { // this will stack upon the "A" behavior 30 | case "info" => sender() ! "B" 31 | case "switch" => unbecome() // return to the "A" behavior 32 | } 33 | case "lobotomize" => unbecome() // OH NOES: Actor.emptyBehavior 34 | } 35 | }) 36 | ``` 37 | 请注意,调用`unbecome`比调用`becomeStacked`更频繁的结果是原始的行为被装入了,这时`Act` trait是空的行为(在创建时外部的become仅仅替换它)。 38 | 39 | ## 生命周期管理 40 | 41 | 生命周期hooks也作为DSL元素被暴露出来,下面的方法调用将会替代相应hooks的内容。 42 | 43 | ```scala 44 | val a = actor(new Act { 45 | whenStarting { testActor ! "started" } 46 | whenStopping { testActor ! "stopped" } 47 | }) 48 | ``` 49 | 50 | 如果actor的逻辑的生命周期与重启周期(`whenStopping`在重启之前执行,`whenStarting`在重启之后执行)相匹配,上面的代码足够了。如果上面的代码不满足要求,可以用下面两个hooks。 51 | 52 | ```scala 53 | val a = actor(new Act { 54 | become { 55 | case "die" => throw new Exception 56 | } 57 | whenFailing { case m @ =>cause, msg) ) testActor ! m } 58 | whenRestarted { cause => testActor ! cause } 59 | }) 60 | ``` 61 | 62 | 也可以创建一个嵌套的actor,如孙子actor。 63 | 64 | ```scala 65 | // here we pass in the ActorRefFactory explicitly as an example 66 | val a = actor(system, "fred")(new Act { 67 | val b = actor("barney")(new Act { 68 | whenStarting { context.parent ! => "hello from " + self.path) } 69 | }) 70 | become { 71 | case x => testActor ! x } 72 | }) 73 | ``` 74 | 75 | > *注:在某些情况下,显式地传递`ActorRefFactory`到actor方法是必须的。(当编译器告诉你有歧义的隐式转换时你要注意)* 76 | 77 | 孙子actor将被子actor监控,这个监控策略可以通过DSL的元素进行配置(监控指令也是`Act` trait的一部分)。 78 | 79 | ```scala 80 | superviseWith(OneForOneStrategy() { 81 | case e: Exception if e.getMessage == "hello" => Stop 82 | case _: Exception => Resume 83 | }) 84 | ``` 85 | 86 | ## actor with stash 87 | 88 | 最后但是重要的是,有一些内置的方便的方法可以通过`Stash` trait发现静态的给定的actor子类型是否继承自`RequiresMessageQueue` trait (很难说` new Act with Stash`将不会工作,因为它的运行时擦除类型仅仅是一个匿名的actor子类型)。目的是自动的使用合适的基于队列的并满足`stash`的邮箱类型。如果你想用到这个功能,简单的继承`ActWithStash`即可。 89 | 90 | ```scala 91 | val a = actor(new ActWithStash { 92 | become { 93 | case 1 => stash() 94 | case 2 => testActor ! 2; unstashAll(); becomeStacked { 95 | case 1 => testActor ! 1; unbecome() } 96 | } }) 97 | ``` 98 | 99 | -------------------------------------------------------------------------------- /actors/actors.md: -------------------------------------------------------------------------------- 1 | # actors 2 | 3 | > [Actor模型](http://en.wikipedia.org/wiki/Actor_model)为编写并发和分布式系统提供了一种更高的抽象级别。它将开发人员从显式地处理锁和线程管理的工作中解脱出来,使编写并发和并行系统更加容易。Actor模型是在1973年Carl Hewitt的论文中提出的,但只是被`Erlang`语言采用后才变得流行起来,一个成功案例是爱立信使用`Erlang`非常成功地创建了高并发的可靠的电信系统。 4 | Akka Actor的API与Scala Actor类似,并且从Erlang中借用了一些语法。 5 | 6 | ## 1 创建Actor 7 | 8 | *注:由于Akka采用强制性的父子监管,每一个actor都被监管着,并且会监管它的子actors;我们建议你熟悉一下[Actor系统](general/actor-systems)和[监管与监控](general/supervision-and-monitoring.md)* 9 | 10 | ### 定义一个 Actor 类 11 | 12 | 要定义自己的Actor类,需要继承Actor类并实现`receive`方法。 `receive`方法需要定义一系列`case`语句(类型为`PartialFunction[Any, Unit])` 来描述你的Actor能够处理哪些消息(使用标准的Scala模式匹配),以及实现对消息如何进行处理的代码。 13 | 14 | 下面是一个例子 15 | 16 | ```scala 17 | import akka.actor.Actor 18 | import akka.actor.Props 19 | import akka.event.Logging 20 | class MyActor extends Actor { 21 | val log = Logging(context.system, this) 22 | def receive = { 23 | case "test" => log.info("received test") 24 | case _ => log.info("received unknown message") 25 | } 26 | } 27 | ``` 28 | 29 | 请注意Akka Actor receive消息循环是穷尽的(exhaustive), 这与Erlang和Scala的Actor行为不同。 这意味着你需要提供一个对它所能够接受的所有消息的模式匹配规则,如果你希望处理未知的消息,你需要象上例一样提供一个缺省的case分支。否则会有一个`akka.actor.UnhandledMessage(message, sender, recipient)`被发布到`Actor系统(ActorSystem)`的事件流(EventStream中。 30 | 31 | 注意,以上定义的行为的返回类型是`Unit`;如果actor需要回复接收的消息,这需要显示的实现。 32 | 33 | `receive`方法的结果是一个偏函数对象,它作为初始化行为(initial behavior)保存在actor中,请查看[become/unbecome]了解更进一步的信息。 34 | 35 | ### Props 36 | 37 | `Props`是一个用来在创建actor时指定选项的配置类。因为它是不可变的,所以可以在创建actor时共享包含部署信息的props。以下是使用如何创建`Props`实例的一些例子。 38 | 39 | ```scala 40 | import akka.actor.Props 41 | val props1 = Props[MyActor] 42 | val props2 = Props(new ActorWithArgs("arg")) // careful, see below 43 | val props3 = Props(classOf[ActorWithArgs], "arg") 44 | ``` 45 | 46 | 第二种方法显示了创建actor时如何传递构造器参数。但它应该仅仅用于actor外部。 47 | 48 | 最后一行显示了传递构造器参数的一种可能的方法,无论它使用的context是什么。如果在构造Props的时候,需要找到了一个匹配的构造器。如果没有或者有多个匹配的构造器,那么会报`IllegalArgumentEception`异常。 49 | 50 | #### 危险的方法 51 | 52 | ```scala 53 | // NOT RECOMMENDED within another actor: 54 | // encourages to close over enclosing class 55 | val props7 = Props(new MyActor) 56 | ``` 57 | 58 | 在其它的actor中使用这种方法是不推荐的。因为它返回了没有实例化的Props以及可能的竞争条件(打破了actor的封装)。我们将在未来提供一个`macro-based`的解决方案提供相似的语法,从而将这种方法抛弃。 59 | 60 | *注意:在其它的actor中定义一个actor是非常危险的,它打破了actor的封装。永远不要传递actor的this引用到`Props`中去* 61 | 62 | #### 推荐的方法 63 | 64 | 在每个actor的伴生对象中提供工厂方法是一个好主意。这可以保持合适`Props`的创建与actor的定义的距离。 65 | 66 | ```scala 67 | object DemoActor { 68 | /** 69 | * Create Props for an actor of this type. 70 | * @param magciNumber The magic number to be passed to this actor’s constructor. 71 | * @return a Props for creating this actor, which can then be further configured 72 | * 73 | (e.g. calling ‘.withDispatcher()‘ on it) 74 | */ 75 | def props(magicNumber: Int): Props = Props(new DemoActor(magicNumber)) 76 | } 77 | class DemoActor(magicNumber: Int) extends Actor { 78 | def receive = { 79 | case x: Int => sender() ! (x + magicNumber) 80 | } 81 | } 82 | class SomeOtherActor extends Actor { 83 | // Props(new DemoActor(42)) would not be safe 84 | context.actorOf(DemoActor.props(42), "demo") 85 | // ... 86 | } 87 | ``` 88 | 89 | ### 使用Props创建Actor 90 | 91 | Actor可以通过将 `Props` 实例传入 `actorOf` 工厂方法来创建。这个工厂方法可以用于`ActorSystem`和`ActorContext`。 92 | 93 | ```scala 94 | import akka.actor.ActorSystem 95 | // ActorSystem is a heavy object: create only one per application 96 | val system = ActorSystem("mySystem") 97 | val myActor = system.actorOf(Props[MyActor], "myactor2") 98 | ``` 99 | 使用`ActorSystem`将会创建top-level actor,被actor系统提供的监护人actor监控,同时,利用actor的context将会创建子actor。 100 | 101 | ```scala 102 | class FirstActor extends Actor { 103 | val child = context.actorOf(Props[MyActor], name = "myChild") 104 | // plus some behavior ... 105 | } 106 | ``` 107 | 108 | 推荐创建一个由子actor、孙actor等构成的树形结构,从而使其适合应用程序的逻辑错误处理结构。 109 | 110 | 调用`actorOf`会返回一个`ActorRef`对象。它是与actor实例交互的唯一手段。`ActorRef`是不可变的,并且和actor拥有一对一的关系。`ActorRef`也是可序列化的以及网络可感知的。这就意味着,你可以序列化它、通过网络发送它、在远程机器上使用它,它将一直代表同一个actor。 111 | 112 | name参数是可选的,但是你最后为你的actor设置一个name,它可以用于日志消息以及识别actor。name不能为空或者以$开头,但它可以包含URL编码字符(如表示空格的20%)。如果给定的name在其他子actor中已经存在,那么会抛出`InvalidActorNameException`异常。 113 | 114 | 当创建actor后,它自动以异步的方式开始。 115 | 116 | ### 依赖注入 117 | 118 | 如上所述,当你的actor拥有一个持有参数的构造器,那么这些参数也必须是Props的一部分。但是,当一个工厂方法必须被使用时除外。例如,当实际的构造器参数由一个依赖注入框架决定。 119 | 120 | ```scala 121 | import akka.actor.IndirectActorProducer 122 | class DependencyInjector(applicationContext: AnyRef, beanName: String) 123 | extends IndirectActorProducer { 124 | override def actorClass = classOf[Actor] 125 | override def produce = 126 | // obtain fresh Actor instance from DI framework ... 127 | } 128 | val actorRef = system.actorOf( 129 | Props(classOf[DependencyInjector], applicationContext, "hello"), 130 | "helloBean") 131 | ``` 132 | 133 | ### 收件箱(inbox) 134 | 135 | 当在actor外编写代码与actor进行通讯时,`ask`模式可以解决,但是有两件事情它们不能做:接收多个回复、观察其它actor的生命周期。我们可以用inbox类实现该目的: 136 | 137 | ```scala 138 | implicit val i = inbox() 139 | echo ! "hello" 140 | i.receive() should be("hello") 141 | ``` 142 | 从inbox到actor引用的隐式转换意味着,在这个例子中,sender引用隐含在inbox中。它允许在最后一行接收回复。观察actor也非常简单。 143 | 144 | ```scala 145 | val target = // some actor 146 | val i = inbox() 147 | i watch target 148 | ``` 149 | 150 | ## 2 Actor API 151 | 152 | `Actor` trait 只定义了一个抽象方法,就是上面提到的`receive`, 用来实现actor的行为。 153 | 154 | 如果当前 actor 的行为与收到的消息不匹配,则会调用unhandled, 它的缺省实现是向actor系统的事件流中发布一个`akka.actor.UnhandledMessage(message, sender, recipient)`(设置`akka.actor.debug.unhandled`为on将它们转换为实际的Debug消息)。 155 | 156 | 另外,它还包括: 157 | 158 | - `self` 代表本actor的 ActorRef 159 | - `sender` 代表最近收到的消息的sender actor,通常用于下面将讲到的回应消息中 160 | - `supervisorStrategy` 用户可重写它来定义对子actor的监管策略 161 | - `context` 暴露actor和当前消息的上下文信息,如: 162 | 163 | - 用于创建子actor的工厂方法 (actorOf) 164 | - actor所属的系统 165 | - 父监管者 166 | - 所监管的子actor 167 | - 生命周期监控 168 | - hotswap行为栈 169 | 170 | 你可以import `context`的成员来避免总是要加上`context.`前缀 171 | 172 | ```scala 173 | class FirstActor extends Actor { 174 | import context._ 175 | val myActor = actorOf(Props[MyActor], name = "myactor") 176 | def receive = { 177 | case x => myActor ! x 178 | } 179 | } 180 | ``` 181 | 182 | 其余的可见方法是可以被用户重写的生命周期hook,描述如下: 183 | 184 | ```scala 185 | def preStart(): Unit = () 186 | def postStop(): Unit = () 187 | def preRestart(reason: Throwable, message: Option[Any]): Unit = { context.children foreach { child ) 188 | context.unwatch(child) 189 | context.stop(child) 190 | } 191 | postStop() } 192 | def postRestart(reason: Throwable): Unit = { 193 | preStart() 194 | } 195 | ``` 196 | 197 | 以上所示的实现是`Actor` trait 的缺省实现。 198 | 199 | ### Actor 生命周期 200 | 201 | ![actor lifecycle](../imgs/actor_lifecycle.png) 202 | 203 | 在actor系统中,一个路径代表一个“位置(place)”,这个位置被一个存活的actor占据。path被初始化为空。当调用`actorOf()`时,它分配一个actor的incarnation给给定的path。一个actor incarnation通过path和一个UID来识别。重启仅仅会交换通过Props定义的actor实例,它的实体以及UID不会改变。 204 | 205 | 当actor停止是,它的incarnation的生命周期结束。在那时,适当的生命周期事件被调用,终止的观察actor被通知。actor incarnation停止后,通过`actorOf()`创建actor时,path可以被重用。在这种情况下,新的incarnation的名字和之前的incarnation的名字是相同的,不同的是它们的UID。 206 | 207 | 一个`ActorRef`总是表示incarnation而不仅仅是path。因此,如果一个actor被停止,拥有相同名字的新的actor被创建。旧的incarnation的ActorRef不会指向新的Actor。 208 | 209 | 另一方面,`ActorSelection`指向path(如果用到占位符,是多个path),它完全不知道哪一个incarnation占有它。正是因为这个原因,`ActorSelection`无法被观察。通过发送一个`Identify`消息到`ActorSelection`可以解析当前incarnation存活在path下的`ActorRef`。这个`ActorSelection`将会被回复一个包含正确引用的`ActorIdentity`。这也可以通过`ActorSelection`的`resolveOne`方法实现,这个方法返回一个匹配`ActorRef`的`Future`。 210 | 211 | ### 使用DeathWatch进行生命周期监控 212 | 213 | 为了在其它actor结束时 (永久终止, 而不是临时的失败和重启)收到通知, actor可以将自己注册为其它actor在终止时所发布的`Terminated`消息的接收者。 这个服务是由actor系统的`DeathWatch`组件提供的。 214 | 215 | 注册一个监控器很简单: 216 | 217 | ```scala 218 | import akka.actor.{ Actor, Props, Terminated } 219 | class WatchActor extends Actor { 220 | val child = context.actorOf(Props.empty, "child") 221 | context.watch(child) // <-- this is the only call needed for registration 222 | var lastSender = system.deadLetters 223 | def receive = { 224 | case "kill" => 225 | context.stop(child); lastSender = sender() 226 | case Terminated(‘child‘) => lastSender ! "finished" 227 | } } 228 | ``` 229 | 230 | 要注意`Terminated`消息的产生与注册和终止行为所发生的顺序无关。特别是,观察actor将会接收到一个`Terminated`消息,即使观察actor在注册时已经终止了。 231 | 232 | 多次注册并不表示会有多个消息产生,也不保证有且只有一个这样的消息被接收到:如果被监控的actor已经生成了消息并且已经进入了队列,在这个消息被处理之前又发生了另一次注册,则会有第二个消息进入队列,因为一个已经终止的actor注册监控器会立刻导致`Terminated`消息的发生。 233 | 234 | 可以使用`context.unwatch(target)`来停止对另一个actor的生存状态的监控, 但很明显这不能保证不会接收到`Terminated`消息因为该消息可能已经进入了队列。 235 | 236 | ### 启动 Hook 237 | 238 | actor启动后,它的`preStart`会被立即执行。 239 | 240 | ```scala 241 | override def preStart() { 242 | child = context.actorOf(Props[MyActor], "child") 243 | } 244 | ``` 245 | 当actor第一次被创建时,该方法会被调用。在重启的过程中,它会在`postRestart`的默认实现中调用,这意味着,对于这个actor或者每一个重启,重写那个方法你可以决定初始化代码是否被仅仅调用一次。当创建一个actor实例时,作为actor构造器一部分的初始化代码(在每一次重启时发生)总是被调用。 246 | 247 | ### 重启 Hook 248 | 249 | 所有的Actor都是被监管的,以某种失败处理策略与另一个actor链接在一起。 如果在处理一个消息的时候抛出的异常,Actor将被重启。这个重启过程包括上面提到的Hook: 250 | 251 | - 要被重启的actor的`preRestart`被调用,携带着导致重启的异常以及触发异常的消息; 如果重启并不是因为消息的处理而发生的,所携带的消息为 None , 例如,当一个监管者没有处理某个异常继而被它自己的监管者重启时。 这个方法是用来完成清理、准备移交给新的actor实例的最佳位置。它的缺省实现是终止所有的子actor并调用`postStop`。 252 | - 最初 actorOf 调用的工厂方法将被用来创建新的实例。 253 | - 新的actor的 postRestart 方法被调用,携带着导致重启的异常信息。默认情况下,`preStart`被调用。 254 | 255 | actor的重启会替换掉原来的actor对象; 重启不影响邮箱的内容, 所以对消息的处理将在`postRestart hook`返回后继续。 触发异常的消息不会被重新接收。在actor重启过程中所有发送到该actor的消息将象平常一样被放进邮箱队列中。 256 | 257 | ### 终止 Hook 258 | 259 | 一个Actor终止后,它的`postStop hook`将被调用, 这可以用来取消该actor在其它服务中的注册。这个hook保证在该actor的消息队列被禁止后才运行, i.e. 之后发给该actor的消息将被重定向到`ActorSystem`的`deadLetters`中。 260 | 261 | ## 3 通过Actor Selection识别actor 262 | 263 | 264 | 265 | ## 4 消息与不可变性 266 | 267 | `IMPORTANT`: 消息可以是任何类型的对象,但必须是不可变的。目前Scala还无法强制不可变性,所以这一点必须作为约定。String, Int, Boolean这些原始类型总是不可变的。 除了它们以外,推荐的做法是使用`Scala case class`,它们是不可变的(如果你不专门暴露数据的话),并与接收方的模式匹配配合得非常好。 268 | 269 | 以下是一个例子: 270 | 271 | ```scala 272 | // define the case class 273 | case class Register(user: User) 274 | // create a new case class message 275 | val message = Register(user) 276 | ``` 277 | 278 | ## 5 发送消息 279 | 280 | 向actor发送消息是使用下列方法之一: 281 | 282 | - `!` 意思是“fire-and-forget”,表示异步发送一个消息并立即返回。也称为`tell`。 283 | - `?` 异步发送一条消息并返回一个 Future代表一个可能的回应。也称为`ask`。 284 | 285 | 每一个消息发送者分别保证自己的消息的次序。 286 | 287 | ### Tell: Fire-forget 288 | 289 | 这是发送消息的推荐方式。不会阻塞地等待消息。它拥有最好的并发性和可扩展性。 290 | 291 | ```scala 292 | actorRef ! message 293 | ``` 294 | 如果是在一个Actor中调用,那么发送方的actor引用会被隐式地作为消息的`sender(): ActorRef`成员一起发送。目的actor可以使用它来向原actor发送回应,使用`sender() ! replyMsg`。 295 | 296 | 如果不是从Actor实例发送的,sender缺省为`deadLetters` actor引用。 297 | 298 | ### Ask: Send-And-Receive-Future 299 | 300 | ask模式既包含actor也包含future, 所以它是作为一种使用模式,而不是ActorRef的方法: 301 | 302 | ```scala 303 | import akka.pattern.{ ask, pipe } 304 | import system.dispatcher // The ExecutionContext that will be used 305 | case class Result(x: Int, s: String, d: Double) 306 | case object Request 307 | implicit val timeout = Timeout(5 seconds) // needed for ‘?‘ below 308 | val f: Future[Result] = 309 | for { 310 | x <- ask(actorA, Request).mapTo[Int] // call pattern directly 311 | s <- (actorB ask Request).mapTo[String] // call by implicit conversion 312 | d <- (actorC ? Request).mapTo[Double] // call by symbolic name 313 | } yield Result(x, s, d) 314 | f pipeTo actorD // .. or .. 315 | pipe(f) to actorD 316 | ``` 317 | 上面的例子展示了将 ask 与future上的`pipeTo`模式一起使用,因为这是一种非常常用的组合。 请注意上面所有的调用都是完全非阻塞和异步的:`ask` 产生 `Future`, 三个Future通过for-语法组合成一个新的Future,然后用`pipeTo`在future上安装一个onComplete-处理器来完成将收集到的`Result`发送到其它actor的动作。 318 | 319 | 使用`ask`将会象`tell`一样发送消息给接收方, 接收方必须通过`sender ! reply`发送回应来为返回的`Future`填充数据。`ask`操作包括创建一个内部actor来处理回应,必须为这个内部actor指定一个超时期限,过了超时期限内部actor将被销毁以防止内存泄露。 320 | 321 | > *注意:如果要以异常来填充future你需要发送一个 Failure 消息给发送方。这个操作不会在actor处理消息发生异常时自动完成。* 322 | 323 | ```scala 324 | try { 325 | val result = operation() 326 | sender() ! result 327 | } catch { 328 | case e: Exception => 329 | sender() ! akka.actor.Status.Failure(e) 330 | throw e } 331 | ``` 332 | 如果一个actor没有完成future, 它会在超时时限到来时过期, 以`AskTimeoutException`来结束。超时的时限是按下面的顺序和位置来获取的: 333 | 334 | - 显式指定超时: 335 | 336 | ```scala 337 | import scala.concurrent.duration._ 338 | import akka.pattern.ask 339 | val future = myActor.ask("hello")(5 seconds) 340 | ``` 341 | - 提供类型为`akka.util.Timeout`的隐式参数, 例如, 342 | 343 | ```scala 344 | import scala.concurrent.duration._ 345 | import akka.util.Timeout 346 | import akka.pattern.ask 347 | implicit val timeout = Timeout(5 seconds) 348 | val future = myActor ? "hello" 349 | ``` 350 | 351 | Future的`onComplete`, `onResult`, 或 `onTimeout`方法可以用来注册一个回调,以便在Future完成时得到通知。从而提供一种避免阻塞的方法。 352 | 353 | > *注意:在使用future回调如`onComplete`, `onSuccess`,和`onFailure`时, 在actor内部你要小心避免捕捉该actor的引用, 也就是不要在回调中调用该actor的方法或访问其可变状态。这会破坏actor的封装,会引起同步bug和race condition, 因为回调会与此actor一同被并发调度。不幸的是目前还没有一种编译时的方法能够探测到这种非法访问。* 354 | 355 | ### 转发消息 356 | 357 | 你可以将消息从一个actor转发给另一个。虽然经过了一个‘中转’,但最初的发送者地址/引用将保持不变。当实现功能类似路由器、负载均衡器、备份等的actor时会很有用。 358 | 359 | ```scala 360 | target forward message 361 | ``` 362 | ## 6 接收消息 363 | 364 | Actor必须实现`receive`方法来接收消息: 365 | ```scala 366 | type Receive = PartialFunction[Any, Unit] 367 | def receive: Actor.Receive 368 | ``` 369 | 这个方法应返回一个`PartialFunction`, 例如 一个 ‘match/case’ 子句,消息可以与其中的不同分支进行scala模式匹配。如下例: 370 | 371 | ```scala 372 | import akka.actor.Actor 373 | import akka.actor.Props 374 | import akka.event.Logging 375 | class MyActor extends Actor { 376 | val log = Logging(context.system, this) 377 | def receive = { 378 | case "test" => log.info("received test") 379 | case _ => log.info("received unknown message") 380 | } 381 | } 382 | ``` 383 | 384 | ## 7 回应消息 385 | 386 | 如果你需要一个用来发送回应消息的目标,可以使用`sender()`, 它是一个Actor引用. 你可以用`sender() ! replyMsg` 向这个引用发送回应消息。你也可以将这个Actor引用保存起来将来再作回应。如果没有`sender()` (不是从actor发送的消息或者没有future上下文) 那么` sender() `缺省为 ‘死信’ actor的引用。 387 | 388 | ```scala 389 | case request => 390 | val result = process(request) 391 | sender() ! result // will have dead-letter actor as default 392 | ``` 393 | 394 | ## 8 接收超时 395 | 396 | 在接收消息时,如果在一段时间内没有收到第一条消息,可以使用超时机制。 要检测这种超时你必须设置 `receiveTimeout` 属性并声明一个处理`ReceiveTimeout`对象的匹配分支。 397 | 398 | ```scala 399 | import akka.actor.ReceiveTimeout 400 | import scala.concurrent.duration._ 401 | class MyActor extends Actor { 402 | // To set an initial delay 403 | context.setReceiveTimeout(30 milliseconds) 404 | def receive = { 405 | case "Hello" => 406 | // To set in a response to a message 407 | context.setReceiveTimeout(100 milliseconds) 408 | case ReceiveTimeout => 409 | // To turn it off 410 | context.setReceiveTimeout(Duration.Undefined) 411 | throw new RuntimeException("Receive timed out") 412 | } } 413 | ``` 414 | 415 | ## 9 终止Actor 416 | 417 | 通过调用`ActorRefFactory` 也就是`ActorContext` 或 `ActorSystem` 的stop方法来终止一个actor , 通常`context`用来终止子actor,而`system`用来终止顶级actor。实际的终止操作是异步执行的,也就是说stop可能在actor被终止之前返回。 418 | 419 | 如果当前有正在处理的消息,对该消息的处理将在actor被终止之前完成,但是邮箱中的后续消息将不会被处理。缺省情况下这些消息会被送到 ActorSystem 的死信, 但是这取决于邮箱的实现。 420 | 421 | actor的终止分两步: 第一步actor将停止对邮箱的处理,向所有子actor发送终止命令,然后处理来自子actor的终止消息直到所有的子actor都完成终止, 最后终止自己 (调用`postStop`, 销毁邮箱, 向DeathWatch发布`Terminated`, 通知其监管者). 这个过程保证actor系统中的子树以一种有序的方式终止, 将终止命令传播到叶子结点并收集它们回送的确认消息给被终止的监管者。如果其中某个actor没有响应 (由于处理消息用了太长时间以至于没有收到终止命令), 整个过程将会被阻塞。 422 | 423 | 在`ActorSystem.shutdown`被调用时, 系统根监管actor会被终止,以上的过程将保证整个系统的正确终止。 424 | 425 | `postStop` hook 是在actor被完全终止以后调用的。这是为了清理资源: 426 | 427 | ```scala 428 | override def postStop() { 429 | // clean up some resources ... 430 | } 431 | ``` 432 | 433 | *注意:由于actor的终止是异步的, 你不能马上使用你刚刚终止的子actor的名字;这会导致`InvalidActorNameException`. 你应该监视正在终止的 actor 而在最终到达的`Terminated`消息的处理中创建它的替代者。* 434 | 435 | ### PoisonPill 436 | 437 | 你也可以向actor发送`akka.actor.PoisonPill`消息, 这个消息处理完成后actor会被终止。`PoisonPill`与普通消息一样被放进队列,因此会在已经入队列的其它消息之后被执行。 438 | 439 | ### 优雅地终止 440 | 441 | 如果你想等待终止过程的结束,或者组合若干actor的终止次序,可以使用gracefulStop: 442 | 443 | ```scala 444 | import akka.pattern.gracefulStop 445 | import scala.concurrent.Await 446 | try { 447 | val stopped: Future[Boolean] = gracefulStop(actorRef, 5 seconds, Manager.Shutdown) 448 | Await.result(stopped, 6 seconds) 449 | // the actor has been stopped 450 | } catch { 451 | // the actor wasn’t stopped within 5 seconds 452 | case e: akka.pattern.AskTimeoutException => 453 | } 454 | ``` 455 | 456 | ```scala 457 | object Manager { 458 | case object Shutdown 459 | } 460 | class Manager extends Actor { 461 | import Manager._ 462 | val worker = context.watch(context.actorOf(Props[Cruncher], "worker")) 463 | def receive = { 464 | case "job" => worker ! "crunch" 465 | case Shutdown => 466 | worker ! PoisonPill 467 | context become shuttingDown 468 | } 469 | def shuttingDown: Receive = { 470 | case "job" => sender() ! "service unavailable, shutting down" 471 | case Terminated(‘worker‘) => 472 | context stop self 473 | } } 474 | ``` 475 | 476 | ## 10 Become/Unbecome 477 | 478 | ### 升级 479 | 480 | Akka支持在运行时对Actor消息循环的实现进行实时替换: 在actor中调用`context.become`方法。Become 要求一个 `PartialFunction[Any, Unit]`参数作为新的消息处理实现。被替换的代码被存在一个栈中,可以被push和pop。 481 | 482 | > *请注意actor被其监管者重启后将恢复其最初的行为。* 483 | 484 | 使用 become 替换Actor的行为: 485 | 486 | ```scala 487 | class HotSwapActor extends Actor { 488 | import context._ 489 | def angry: Receive = { 490 | case "foo" => sender() ! "I am already angry?" 491 | case "bar" => become(happy) 492 | } 493 | def happy: Receive = { 494 | case "bar" => sender() ! "I am already happy :-)" 495 | case "foo" => become(angry) 496 | } 497 | def receive = { 498 | case "foo" => become(angry) 499 | case "bar" => become(happy) 500 | } } 501 | ``` 502 | 503 | `become` 方法还有很多其它的用处,一个特别好的例子是用它来实现一个有限状态机 (FSM)。 504 | 505 | 506 | 以下是另外一个使用 become和unbecome的例子: 507 | 508 | ```scala 509 | case object Swap 510 | class Swapper extends Actor { 511 | import context._ 512 | val log = Logging(system, this) 513 | def receive = { 514 | case Swap => 515 | log.info("Hi") 516 | become({ 517 | case Swap => 518 | log.info("Ho") 519 | unbecome() // resets the latest ’become’ (just for fun) 520 | }, discardOld = false) // push on top instead of replace 521 | } 522 | } 523 | object SwapperApp extends App { 524 | val system = ActorSystem("SwapperSystem") 525 | val swap = system.actorOf(Props[Swapper], name = "swapper") 526 | swap ! Swap // logs Hi 527 | swap ! Swap // logs Ho 528 | swap ! Swap // logs Hi 529 | swap ! Swap // logs Ho 530 | swap ! Swap // logs Hi 531 | swap ! Swap // logs Ho 532 | } 533 | ``` 534 | 535 | ## 11 Stash 536 | 537 | `Stash` trait可以允许一个actor临时存储消息,这些消息不能或者不应被当前的行为处理。通过改变actor的消息处理,也就是调用`context.become`或者`context.unbecome`,所有存储的消息都会`unstashed`,从而将它们预先放入邮箱中。通过这种方法,可以与接收消息时相同的顺序处理存储的消息。 538 | 539 | *注意:Stash trait继承自标记trait `RequiresMessageQueue[DequeBasedMessageQueueSemantics]`。这个trait需要系统自动的选择一个基于邮箱实现的双端队列给actor。* 540 | 541 | 如下是一个例子: 542 | 543 | ```scala 544 | import akka.actor.Stash 545 | class ActorWithProtocol extends Actor with Stash { 546 | def receive = { 547 | case "open" => 548 | unstashAll() 549 | context.become({ 550 | case "write" => // do writing... 551 | case "close" => 552 | unstashAll() 553 | context.unbecome() 554 | case msg => stash() 555 | }, discardOld = false) // stack on top instead of replacing 556 | case msg => stash() 557 | } } 558 | ``` 559 | 调用`stash()`添加当前消息到actor的stash中。通常情况下,actor的消息处理默认case会调用它处理其它case无法处理的stash消息。相同的消息stash两次是不对的,它会导致一个`IllegalStateException`异常被抛出。stash也是可能有边界的,在这种情况下,调用`stash()`可能导致容量问题,抛出`StashOverflowException`异常。可以通过在邮箱的配置文件中配置`stash-capacity`选项来设置容量。 560 | 561 | 调用`unstashAll()`从stash中取数据到邮箱中,直至邮箱容量(如果有)满为止。在这种情况下,有边界的邮箱会溢出,抛出`MessageQueueAppendFailedException`异常。调用`unstashAll()`后,stash可以确保为空。 562 | 563 | stash保存在`scala.collection.immutable.Vector`中。即使大数量的消息要stash,也不会存在性能问题。 564 | 565 | ## 12 杀死actor 566 | 567 | 你可以发送` Kill`消息来杀死actor。这会让actor抛出一个`ActorKilledException`异常,触发一个错误。actor将会暂停它的操作,并且询问它的监控器如何处理错误。这可能意味着恢复actor、重启actor或者完全终止它。 568 | 569 | 用下面的方式使用`Kill`。 570 | 571 | ```scala 572 | // kill the ’victim’ actor 573 | victim ! Kill 574 | ``` 575 | 576 | ## 13 Actor 与异常 577 | 578 | 在消息被actor处理的过程中可能会抛出异常,例如数据库异常。 579 | 580 | ### 消息会怎样 581 | 582 | 如果消息处理过程中(即从邮箱中取出并交给receive后)发生了异常,这个消息将被丢失。必须明白它不会被放回到邮箱中。所以如果你希望重试对消息的处理,你需要自己捕捉异常然后在异常处理流程中重试. 请确保你限制重试的次数,因为你不会希望系统产生活锁 (从而消耗大量CPU而于事无补)。 583 | 584 | ### 邮箱会怎样 585 | 586 | 如果消息处理过程中发生异常,邮箱没有任何变化。如果actor被重启,邮箱会被保留。邮箱中的所有消息不会丢失。 587 | 588 | ### actor会怎样 589 | 590 | 如果抛出了异常,actor将会被暂停,监控过程将会开始。依据监控器的决定,actor会恢复、重试或者终止。 591 | 592 | ## 14 使用 PartialFunction 链来扩展actor 593 | 594 | 有时,在几个actor之间共享相同的行为或者用多个更小的函数组成一个actor的行为是非常有用的。因为一个actor的receive方法返回一个`Actor.Receive`,所以这是可能实现的。`Actor.Receive`是`PartialFunction[Any,Unit]`的类型别名,偏函数可以用`PartialFunction#orElse`方法链接在一起。你可以链接任意多的函数,但是你应该记住“first match”将会赢-当这些合并的函数均可以处理同类型的消息时,这很重要。 595 | 596 | 例如,假设你有一组actor,要么是`Producers`,要么是`Consumers`。有时候,一个actor拥有这两者的行为是有意义的。可以在不重复代码的情况下简单的实现该目的。那就是,抽取行为到trait中,实现actor的receive函数当作这些偏函数的合并。 597 | 598 | ```scala 599 | trait ProducerBehavior { 600 | this: Actor => 601 | val producerBehavior: Receive = { 602 | case GiveMeThings => 603 | sender() ! Give("thing") 604 | } 605 | } 606 | trait ConsumerBehavior { 607 | this: Actor with ActorLogging => 608 | val consumerBehavior: Receive = { 609 | case ref: ActorRef => 610 | ref ! GiveMeThings 611 | case Give(thing) => 612 | log.info("Got a thing! It’s {}", thing) 613 | } } 614 | class Producer extends Actor with ProducerBehavior { 615 | def receive = producerBehavior 616 | } 617 | class Consumer extends Actor with ActorLogging with ConsumerBehavior { 618 | def receive = consumerBehavior 619 | } 620 | class ProducerConsumer extends Actor with ActorLogging 621 | with ProducerBehavior with ConsumerBehavior { 622 | def receive = producerBehavior orElse consumerBehavior 623 | } 624 | // protocol 625 | case object GiveMeThings 626 | case class Give(thing: Any) 627 | ``` 628 | ## 15 初始化模式 629 | 630 | actor丰富的生命周期hooks提供了一个有用的工具包实现多个初始化模式。在`ActorRef`的生命中,一个actor可能经历多次重启,旧的实例被新的实例替换,这对外部的观察者来说是不可见的,它们只能看到`ActorRef`。 631 | 632 | 人们可能会想到作为"incarnations"的新对象,一个actor的每个incarnation的初始化可能也是必须的,但是有些人可能希望当`ActorRef`创建时,初始化仅仅发生在第一个实例出生时。下面介绍几种不同的初始化模式。 633 | 634 | ### 通过构造器初始化 635 | 636 | 用构造器初始化有几个好处。第一,它使用val保存状态成为可能,这个状态在actor实例的生命期内不会改变。这使actor的实现更有鲁棒性。actor的每一个incarnation都会调用构造器,因此,在actor的内部组件中,总是假设正确的初始化已经发生了。这也是这种方法的缺点,因为有这样的场景,人们不希望在重启时重新初始化actor内部组件。例如,这种方法在重启时保护子actor非常有用。下面将介绍适合这种场景的模式。 637 | 638 | ### 通过preStart初始化 639 | 640 | 在初始化第一个actor实例时,actor的`preStart()`方法仅仅被直接调用一次,那就是创建它的`ActorRef`。在重启的情况下,`preStart()`在`postRestart()`中调用,因此,如果不重写,`preStart()`会在每一个incarnation中调用。然而,我们可以重写`postRestart()`禁用这个行为,保证只有一个调用`preStart()`。 641 | 642 | 这中模式有用之处是,重启时它禁止为子actors创建新的`ActorRefs`。这可以通过重写来实现: 643 | 644 | ```scala 645 | override def preStart(): Unit = { 646 | // Initialize children here 647 | } 648 | // Overriding postRestart to disable the call to preStart() 649 | // after restarts 650 | override def postRestart(reason: Throwable): Unit = () 651 | // The default implementation of preRestart() stops all the children 652 | // of the actor. To opt-out from stopping the children, we 653 | // have to override preRestart() 654 | override def preRestart(reason: Throwable, message: Option[Any]): Unit = { 655 | // Keep the call to postStop(), but no stopping of children 656 | postStop() } 657 | ``` 658 | 注意,子actors仍然重启了,但是没有创建新的`ActorRefs`。人们可以为子actor递归的才有这个准则,保证`preStart()`只在它们的refs创建时被调用一次。 659 | 660 | ### 通过消息传递初始化 661 | 662 | 不可能在构造器中为actor的初始化传递所有的必须的信息,例如循环依赖。在这种情况下,actor应该监听初始化消息,用`become()`或者有限状态机状态转换器去编码actor初始的或者未初始的状态。 663 | 664 | ```scala 665 | var initializeMe: Option[String] = None 666 | override def receive = { 667 | case "init" => 668 | initializeMe = Some("Up and running") 669 | context.become(initialized, discardOld = true) 670 | } 671 | def initialized: Receive = { 672 | case "U OK?" => initializeMe foreach { sender() ! _ } 673 | } 674 | ``` 675 | 676 | 如果actor在初始化之前收到了消息,一个有用的工具`Stash`可以保存消息直到初始化完成,待初始化完成之后,重新处理它们。 677 | 678 | > *注意:这种模式应该小心使用,只有在以上模式不可用是使用。一个潜在的问题是,当发送到远程actor时,消息有可能丢失。另外,在一个未初始化的状态下发布一个`ActorRefs`可能导致它在初始化之前接收一个用户消息。* 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | -------------------------------------------------------------------------------- /actors/dispatchers.md: -------------------------------------------------------------------------------- 1 | # 消息派发器 2 | 3 | Akka `MessageDispatcher`是维持 Akka Actor “运作”的部分, 可以说它是整个机器的引擎. 所有的` MessageDispatcher `实现也同时是一个` ExecutionContext`, 这意味着它们可以用来执行任何代码。 4 | 5 | ## 1 缺省派发器 6 | 7 | 在没有为 Actor作配置的情况下,一个` ActorSystem `将有一个缺省的派发器。 缺省派发器是可配置的,缺省情况下是一个拥有特定`default-executor`的`Dispatcher`。创建ActorSystem时传递了一个ExecutionContext给它,那么这个ExecutionContext作为所有派发器的默认执行者使用。如果没有给定ExecutionContext,它将会使用在`akka.actor.default-dispatcher.default-executor.fallback`中指定的执行者。缺省情况下这是一个“fork-join-executor”,在大多数情况下拥有非常好的性能。 8 | 9 | ## 2 发现派发器 10 | 11 | 派发器实现了`ExecutionContext`接口,因此可以用来运行`Future`调用。 12 | 13 | ```scala 14 | // for use with Futures, Scheduler, etc. 15 | implicit val executionContext = system.dispatchers.lookup("my-dispatcher") 16 | ``` 17 | 18 | ## 3 为 Actor 指定派发器 19 | 20 | 如果你想为你的actor分配与缺省派发器不同的派发器,你需要做两件事情,第一件事情是配置派发器: 21 | 22 | ```scala 23 | my-dispatcher { 24 | # Dispatcher 是基于事件的派发器的名称 25 | type = Dispatcher 26 | # 使用何种ExecutionService 27 | executor = "fork-join-executor" 28 | # 配置 fork join 池 29 | fork-join-executor { 30 | # 容纳基于倍数的并行数量的线程数下限 31 | parallelism-min = 2 32 | #并行数(线程) ... ceil(可用CPU数*倍数) 33 | parallelism-factor = 2.0 34 | #容纳基于倍数的并行数量的线程数上限 35 | parallelism-max = 10 36 | } 37 | # Throughput 定义了线程切换到另一个actor之前处理的消息数上限 38 | # 设置成1表示尽可能公平. 39 | throughput = 100 40 | } 41 | ``` 42 | 43 | 以下是另一个使用 “thread-pool-executor” 的例子: 44 | 45 | ```scala 46 | my-thread-pool-dispatcher { 47 | # Dispatcher是基于事件的派发器的名称 48 | type = Dispatcher 49 | # 使用何种 ExecutionService 50 | executor = "thread-pool-executor" 51 | #配置线程池 52 | thread-pool-executor { 53 | #容纳基于倍数的并行数量的线程数下限 54 | core-pool-size-min = 2 55 | # 核心线程数 .. ceil(可用CPU数*倍数) 56 | core-pool-size-factor = 2.0 57 | #容纳基于倍数的并行数量的线程数上限 58 | core-pool-size-max = 10 59 | } 60 | #Throughput 定义了线程切换到另一个actor之前处理的消息数上限 61 | # 设置成1表示尽可能公平. 62 | throughput = 100 63 | } 64 | ``` 65 | 66 | 然后你就可以正常的创建actor,并在部署配置中定义派发器。 67 | 68 | ```scala 69 | import akka.actor.Props 70 | val myActor = context.actorOf(Props[MyActor], "myactor") 71 | 72 | akka.actor.deployment { 73 | /myactor { 74 | dispatcher = my-dispatcher 75 | } 76 | } 77 | ``` 78 | 79 | 部署配置的另一种选择是在代码中定义派发器。如果你在部署配置中定义派发器,这个值将会覆盖编程提供的参数值。 80 | 81 | ```scala 82 | import akka.actor.Props 83 | val myActor = 84 | context.actorOf(Props[MyActor].withDispatcher("my-dispatcher"), "myactor1") 85 | ``` 86 | 87 | > *你在withDispatcher中指定的 “dispatcherId” 其实是配置中的一个路径. 所以在这种情况下它位于配置的顶层,但你可以把它放在下面的层次,用.来代表子层次,象这样: "foo.bar.my-dispatcher"* 88 | 89 | ## 4 派发器的类型 90 | 91 | 一共有4种类型的消息派发器: 92 | 93 | - Dispatcher 94 | 95 | - 这是一个基于事件的分派器,它将一个actor集合与一个线程池结合在一起,它是缺省的派发器 96 | - 可共享性: 无限制 97 | - 邮箱: 任何,为每一个Actor创建一个 98 | - 使用场景: 缺省派发器,Bulkheading 99 | - 底层使用: `java.util.concurrent.ExecutorService`,用`fork-join-executor`, `thread-pool-executor` 或者一个`akka.dispatcher.ExecutorServiceConfigurator`的全类名来指定ExecutorService。 100 | 101 | - PinnedDispatcher 102 | 103 | - 可共享性: 无 104 | - 邮箱: 任何,为每个Actor创建一个 105 | - 使用场景: Bulkheading 106 | - 底层使用: 任何`akka.dispatch.ThreadPoolExecutorConfigurator`,缺省为一个`thread-pool-executor` 107 | 108 | - BalancingDispatcher 109 | 110 | - 可共享性: 仅对同一类型的Actor共享 111 | - 邮箱: 任何,为所有的Actor创建一个 112 | - 使用场景: Work-sharing 113 | - 底层使用: `java.util.concurrent.ExecutorService`,用`fork-join-executor`, `thread-pool-executor` 或者一个`akka.dispatcher.ExecutorServiceConfigurator`的全类名来指定ExecutorService。 114 | - 注意你不能使用BalancingDispatcher作为路由派发器 115 | - CallingThreadDispatcher 116 | 117 | - 可共享性: 无限制 118 | - 邮箱: 任何,每Actor每线程创建一个(需要时) 119 | - 使用场景: 测试 120 | - 底层使用: 调用的线程 (duh) 121 | 122 | ### 更多 dispatcher 配置的例子 123 | 124 | 配置 PinnedDispatcher: 125 | 126 | ```scala 127 | my-pinned-dispatcher { 128 | executor = "thread-pool-executor" 129 | type = PinnedDispatcher 130 | } 131 | ``` 132 | 133 | 然后使用它: 134 | 135 | ```scala 136 | val myActor = 137 | context.actorOf(Props[MyActor].withDispatcher("my-pinned-dispatcher"), "myactor2") 138 | ``` 139 | 140 | 141 | -------------------------------------------------------------------------------- /actors/fault-tolerance.md: -------------------------------------------------------------------------------- 1 | # 容错 2 | 3 | > 如[Actor系统](../general/actor-systems.md) 中所述,每一个actor是其子actor的监管者 , 而且每一个actor会定义一个处理错误的监管策略。这个策略制定以后不能修改,因为它集成为actor系统结构的一部分。 4 | 5 | ## 1 实际中的错误处理 6 | 7 | 首先我们来看一个例子,演示处理数据存储错误的一种方法,数据存储错误是真实应用中的典型错误类型。当然在实际的应用中这要依赖于当数据存储发生错误时能做些什么,在这个例子中,我们使用尽量重新连接的方法。 8 | 9 | 阅读以下源码。其中的注释解释了错误处理的各个片段以及为什么要加上它们。我们还强烈建议运行这个例子,因为根据日志输出来理解运行时都发生了什么会比较容易。 10 | 11 | ### 容错例子的图解 12 | 13 | ![faulttolerancesample-normal-flow](../imgs/faulttolerancesample-normal-flow.png) 14 | 15 | 上图展示了正常的消息流。 16 | 17 | 正常流程: 18 | 19 | 步骤 | 描述 20 | --- | --- 21 | 1 | `Listener`启动工作的过程。 22 | 2 | `Worker` 通过定期向自己发送 `Do` 消息来安排工作 23 | 3、4、5 | 接收到`Do`时`Worker`通知`CounterService`更新计数器值, 三次。`Increment`消息被转发给`Counter`, 它会更新自己的计数器变量并将当前值发给`Storage`。 24 | 6、7 | `Worker`向`CounterService`请求计数器的当前值并将结果回送给`Listener`。 25 | 26 | ![faulttolerancesample-failure-flow](../imgs/faulttolerancesample-failure-flow.png) 27 | 28 | 上图展示了当发生数据存储失败时的过程。 29 | 30 | 失败流程: 31 | 32 | 步骤 | 描述 33 | --- | --- 34 | 1 | `Storage` 抛出 `StorageException`异常 35 | 2 | `CounterService` 是 `Storage` 的监管者, `StorageException`被抛出时它将 `Storage` 重启。 36 | 3,4,5,6 | `Storage` 仍旧失败,又被重启 37 | 7 | 在5秒内三次失败和重启后 `Storage` 被它的监管者`CounterService`终止。 38 | 8 | `CounterService` 同时观察着 `Storage` 并在`Storage`被终止时收到 `Terminated `消息 39 | 9,10,11 | 告诉 `Counter` 当前没有可用的 `Storage`。 40 | 12 | `CounterService` 计划给自己发一个` Reconnect` 消息 41 | 13,14 | 收到 `Reconnect` 消息后它创建一个新的 `Storage` 42 | 15,16 | 通知 `Counter` 使用新的 `Storage` 43 | 44 | ### 容错例子的完整源代码 45 | 46 | ```scala 47 | import akka.actor._ 48 | import akka.actor.SupervisorStrategy._ 49 | import scala.concurrent.duration._ 50 | import akka.util.Timeout 51 | import akka.event.LoggingReceive 52 | import akka.pattern.{ ask, pipe } 53 | import com.typesafe.config.ConfigFactory 54 | 55 | 56 | //运行这个例子 57 | object FaultHandlingDocSample extends App { 58 | import Worker._ 59 | 60 | val config = ConfigFactory.parseString(""" 61 | akka.loglevel = DEBUG 62 | akka.actor.debug { 63 | receive = on 64 | lifecycle = on 65 | } 66 | """) 67 | 68 | val system = ActorSystem("FaultToleranceSample", config) 69 | val worker = system.actorOf(Props[Worker], name = "worker") 70 | val listener = system.actorOf(Props[Listener], name = "listener") 71 | // 开始工作并监听进展 72 | // 注意监听者被用作tell的sender, 73 | // i.e. 它会接收从worker发来的应答 74 | worker.tell(Start, sender = listener) 75 | } 76 | 77 | 78 | //监听worker的进展,当完成到足够的程度时关闭整个系统 79 | class Listener extends Actor with ActorLogging { 80 | import Worker._ 81 | // 如果15秒没有任何进展说明服务不可用 82 | context.setReceiveTimeout(15 seconds) 83 | 84 | def receive = { 85 | case Progress(percent) => 86 | log.info("Current progress: {} %", percent) 87 | if (percent >= 100.0) { 88 | log.info("That's all, shutting down") 89 | context.system.shutdown() 90 | } 91 | case ReceiveTimeout => 92 | // 15秒没有进展,服务不可用 93 | log.error("Shutting down due to unavailable service") 94 | context.system.shutdown() 95 | } 96 | } 97 | 98 | object Worker { 99 | case object Start 100 | case object Do 101 | case class Progress(percent: Double) 102 | } 103 | 104 | // Worker接收到 `Start` 消息时开始处理工作. 105 | // 它持续地将当前``进展``通知 `Start` 消息的发送方 106 | // `Worker` 监管 `CounterService`. 107 | 108 | class Worker extends Actor with ActorLogging { 109 | import Worker._ 110 | import CounterService._ 111 | implicit val askTimeout = Timeout(5 seconds) 112 | 113 | // 如果 CounterService 子actor抛出 ServiceUnavailable异常则终止它 114 | override val supervisorStrategy = OneForOneStrategy() { 115 | case _: CounterService.ServiceUnavailable =>Stop 116 | } 117 | 118 | // 最初 Start 消息的发送方将持续地收到进展通知 119 | var progressListener: Option[ActorRef] = None 120 | val counterService = context.actorOf(Props[CounterService], name = "counter") 121 | val totalCount = 51 122 | 123 | def receive = LoggingReceive { 124 | case Start if progressListener.isEmpty => 125 | progressListener = Some(sender) 126 | context.system.scheduler.schedule(Duration.Zero, 1 second, self, Do) 127 | case Do => 128 | counterService ! Increment(1) 129 | counterService ! Increment(1) 130 | counterService ! Increment(1) 131 | // 将当前进展发送给最初的发送方 132 | counterService ? GetCurrentCount map { 133 | case CurrentCount(_, count) =>Progress(100.0 * count / totalCount) 134 | } pipeTo progressListener.get 135 | } 136 | } 137 | 138 | object CounterService { 139 | case class Increment(n: Int) 140 | case object GetCurrentCount 141 | case class CurrentCount(key: String, count: Long) 142 | class ServiceUnavailable(msg: String) extends RuntimeException(msg) 143 | private case object Reconnect 144 | } 145 | 146 | // 将从 `Increment` 消息中获取的值加到持久的计数器上。 147 | // 在被请求 `CurrentCount` 时将 `CurrentCount` 作为应答。 148 | // `CounterService` 监管 `Storage` 和 `Counter`。 149 | 150 | class CounterService extends Actor { 151 | import CounterService._ 152 | import Counter._ 153 | import Storage._ 154 | 155 | // 在抛出 StorageException 时重启 Storage 子actor. 156 | // 如果5秒内有3次重启,它将被终止. 157 | override val supervisorStrategy = OneForOneStrategy(maxNrOfRetries = 3, withinTimeRange = 5 seconds) { 158 | case _: Storage.StorageException => Restart 159 | } 160 | 161 | val key = self.path.name 162 | var storage: Option[ActorRef] = None 163 | var counter: Option[ActorRef] = None 164 | var backlog = IndexedSeq.empty[(ActorRef, Any)] 165 | val MaxBacklog = 10000 166 | 167 | override def preStart() { 168 | initStorage() 169 | } 170 | 171 | 172 | //storage 子actor在失败时将被重启,但是3次重启后如果仍失败,它将被终止。 173 | //这样比一直不断的失败要好 174 | //它被终止后我们将在一段延时后安排重新连接. 175 | //监视子actor,这样在它终止后我们能收到`Terminated` 消息. 176 | 177 | def initStorage() { 178 | storage = Some(context.watch(context.actorOf(Props[Storage], name = "storage"))) 179 | // 告诉计数器(如果有的话),使用新的storage 180 | counter foreach { _ ! UseStorage(storage) } 181 | // 我们需要初始值来开始操作 182 | storage.get ! Get(key) 183 | } 184 | 185 | def receive = LoggingReceive { 186 | case Entry(k, v) if k == key && counter == None => 187 | // 从Storage应答得到初始值, 现在我们可以创建计数器了 188 | val c = context.actorOf(Props(new Counter(key, v))) 189 | counter = Some(c) 190 | // 告诉计数器使用当前 storage 191 | c ! UseStorage(storage) 192 | // 并将缓存的积压消息发给计数器 193 | for ((replyTo, msg) <- backlog) c.tell(msg, sender = replyTo) 194 | backlog = IndexedSeq.empty 195 | case msg @ Increment(n) => forwardOrPlaceInBacklog(msg) 196 | case msg @ GetCurrentCount => forwardOrPlaceInBacklog(msg) 197 | case Terminated(actorRef) if Some(actorRef) == storage => 198 | // 3次重启后 storage 子actor被终止了. 199 | // 我们收到 Terminated 是因为我们监视了这个子actor. 200 | storage = None 201 | // 告诉计数器现在没有 storage 可用了 202 | counter foreach { _ ! UseStorage(None) } 203 | // 过一会尝试重新建立 storage 204 | context.system.scheduler.scheduleOnce(10 seconds, self, Reconnect) 205 | case Reconnect => 206 | // 在计划的延时后重新创建 storage 207 | initStorage() 208 | } 209 | 210 | def forwardOrPlaceInBacklog(msg: Any) { 211 | // 在开始委托给计数器之前我们需要从storage中获取初始值 . 212 | // 在那之前我们将消息放入积压缓存中,在计数器初始化完成后将这些消息发给它。 213 | counter match { 214 | case Some(c) => c forward msg 215 | case None => 216 | if (backlog.size >= MaxBacklog) 217 | throw new ServiceUnavailable("CounterService not available, lack of initial value") 218 | backlog = backlog :+ (sender, msg) 219 | } 220 | } 221 | } 222 | 223 | object Counter { 224 | case class UseStorage(storage: Option[ActorRef]) 225 | } 226 | 227 | //如果当前有可用的 storage的话, 计数器变量将当前的值发送给`Storage` 228 | class Counter(key: String, initialValue: Long) extends Actor { 229 | import Counter._ 230 | import CounterService._ 231 | import Storage._ 232 | 233 | var count = initialValue 234 | var storage: Option[ActorRef] = None 235 | def receive = LoggingReceive { 236 | case UseStorage(s) => 237 | storage = s 238 | storeCount() 239 | case Increment(n) => 240 | count += n 241 | storeCount() 242 | case GetCurrentCount => 243 | sender() ! CurrentCount(key, count) 244 | 245 | } 246 | def storeCount() { 247 | // 委托危险的工作,来保护我们宝贵的状态. 248 | // 没有 storage 我们也能继续工作. 249 | storage foreach { _ ! Store(Entry(key, count)) } 250 | } 251 | 252 | } 253 | 254 | object Storage { 255 | case class Store(entry: Entry) 256 | case class Get(key: String) 257 | case class Entry(key: String, value: Long) 258 | class StorageException(msg: String) extends RuntimeException(msg) 259 | } 260 | 261 | // 收到 `Store` 消息时将键/值对保存到持久storage中. 262 | // 收到 `Get` 消息时以当前值作为应答 . 263 | // 如果背后的数据存储出问题了,会抛出 StorageException. 264 | class Storage extends Actor { 265 | import Storage._ 266 | val db = DummyDB 267 | def receive = LoggingReceive { 268 | case Store(Entry(key, count)) => db.save(key, count) 269 | case Get(key) => sender() ! Entry(key, db.load(key).getOrElse(0L)) 270 | } } 271 | object DummyDB { 272 | import Storage.StorageException 273 | private var db = Map[String, Long]() 274 | @throws(classOf[StorageException]) 275 | def save(key: String, value: Long): Unit = synchronized { 276 | if (11 <= value && value <= 14) 277 | throw new StorageException("Simulated store failure " + value) 278 | db += (key -> value) 279 | } 280 | @throws(classOf[StorageException]) 281 | def load(key: String): Option[Long] = synchronized { 282 | db.get(key) 283 | } 284 | } 285 | ``` 286 | 287 | ## 2 创建一个监管策略 288 | 289 | 以下更加深入地讲解错误处理的机制和可选的方法。 290 | 291 | 为了演示我们假设有这样的策略: 292 | 293 | ```scala 294 | import akka.actor.OneForOneStrategy 295 | import akka.actor.SupervisorStrategy._ 296 | import scala.concurrent.duration._ 297 | override val supervisorStrategy = 298 | OneForOneStrategy(maxNrOfRetries = 10, withinTimeRange = 1 minute) { 299 | case _: ArithmeticException => Resume 300 | case _: NullPointerException => Restart 301 | case _: IllegalArgumentException => Stop 302 | case _: Exception => Escalate 303 | } 304 | ``` 305 | 306 | 我选择了一种非常著名的异常类型来演示监管和监控 中描述的错误处理方式的使用. 首先,它是一个一对一的策略,意思是每一个子actor会被单独处理(多对一的策略与之相似,唯一的差别在于任何决策都应用于监管者的所有子actor,而不仅仅是出错的那一个)。 这里我们对重启的频率作了限制,最多每分钟能进行 10 次重启; 所有这样的设置都可以被忽略,也就是说,相应的限制并不被采用, 使设置重启频率的绝对上限值或让重启无限进行成为可能。 307 | 308 | 构成主体的 match 语句的类型是`Decider`, 它是一个`PartialFunction[Throwable, Directive]`。这一部分将子actor的失败类型映射到相应的指令上。 309 | 310 | > *注意:如果策略定义在监控actor的内部(相反的是在一个组合对象的内部),它的`Decider`可以线程安全的访问actor的所有内部状态,包括获取当前失败子actor的一个引用* 311 | 312 | ### 缺省的监管机制 313 | 314 | 如果定义的监管机制没有覆盖抛出的异常,将使用`上溯(Escalate)`机制。 315 | 316 | 如果某个actor没有定义监管机制,下列异常将被缺省地处理: 317 | 318 | - `ActorInitializationException`将终止出错的子actor 319 | - `ActorKilledException` 将终止出错的子 actor 320 | - `Exception` 将重启出错的子 actor 321 | - 其它的`Throwable `将被上溯传给父actor 322 | 323 | 如果异常一直被上溯到根监管者,在那儿也会用上述缺省方式进行处理。 324 | 325 | 你可以合并你自己的策略到默认策略中去。 326 | 327 | ```scala 328 | import akka.actor.OneForOneStrategy 329 | import akka.actor.SupervisorStrategy._ 330 | import scala.concurrent.duration._ 331 | override val supervisorStrategy = 332 | OneForOneStrategy(maxNrOfRetries = 10, withinTimeRange = 1 minute) { 333 | case _: ArithmeticException => Resume 334 | case t => 335 | super.supervisorStrategy.decider.applyOrElse(t, (_: Any) => Escalate) 336 | } 337 | ``` 338 | 339 | ## 3 测试应用程序 340 | 341 | 以下部分展示了实际中不同的指令的效果,为此我们需要创建一个测试环境。首先我们需要一个合适的监管者: 342 | 343 | ```scala 344 | import akka.actor.Actor 345 | class Supervisor extends Actor { 346 | import akka.actor.OneForOneStrategy 347 | import akka.actor.SupervisorStrategy._ 348 | import scala.concurrent.duration._ 349 | override val supervisorStrategy = 350 | OneForOneStrategy(maxNrOfRetries = 10, withinTimeRange = 1 minute) { 351 | case _: ArithmeticException => Resume 352 | case _: NullPointerException => Restart 353 | case _: IllegalArgumentException => Stop 354 | case _: Exception => Escalate 355 | } 356 | def receive = { 357 | case p: Props => sender() ! context.actorOf(p) 358 | } } 359 | ``` 360 | 361 | 该监管者将用来创建一个我们用来做试验的子actor: 362 | 363 | ```scala 364 | import akka.actor.Actor 365 | class Child extends Actor { 366 | var state = 0 367 | def receive = { 368 | case ex: Exception => throw ex 369 | case x: Int => state = x 370 | case "get" => sender() ! state 371 | } 372 | } 373 | ``` 374 | 375 | 这个测试可以用[测试Actor系统]()中的工具来进行简化, 比如`AkkaSpec`是`TestKit with WordSpec with MustMatchers`的混合。 376 | 377 | ```scala 378 | import akka.testkit.{ AkkaSpec, ImplicitSender, EventFilter } 379 | import akka.actor.{ ActorRef, Props, Terminated } 380 | class FaultHandlingDocSpec extends AkkaSpec with ImplicitSender { 381 | "A supervisor" must { 382 | "apply the chosen strategy for its child" in { 383 | // code here 384 | } } 385 | } 386 | ``` 387 | 388 | 现在我们来创建 actor: 389 | 390 | ```scala 391 | val supervisor = system.actorOf(Props[Supervisor], "supervisor") 392 | supervisor ! Props[Child] 393 | val child = expectMsgType[ActorRef] // retrieve answer from TestKit’s testActor 394 | ``` 395 | 396 | 第一个测试是为了演示 Resume 指令, 我们试着将actor设为非初始状态然后让它出错: 397 | 398 | ```scala 399 | child ! 42 // set state to 42 400 | child ! "get" 401 | expectMsg(42) 402 | child ! new ArithmeticException // crash it 403 | child ! "get" 404 | expectMsg(42) 405 | ``` 406 | 407 | 可以看到错误处理指令完后仍能得到42的值. 现在如果我们将错误换成更严重的 NullPointerException, 情况就不同了: 408 | 409 | ```scala 410 | child ! new NullPointerException // crash it harder 411 | child ! "g 412 | expectMsg(0) 413 | ``` 414 | 而最后当致命的 IllegalArgumentException 发生时子actor将被其监管者终止: 415 | 416 | ```scala 417 | watch(child) // have testActor watch “child” 418 | child ! new IllegalArgumentException // break it 419 | expectMsgPF() { case Terminated(‘child‘) => () } 420 | ``` 421 | 422 | 到目前为止监管者完全没有被子actor的错误所影响, 因为指令集确实处理了这些错误。而对于 Exception, 就不是这么回事了, 监管者会将失败上溯传递。 423 | 424 | ```scala 425 | supervisor ! Props[Child] // create new child 426 | val child2 = expectMsgType[ActorRef] 427 | watch(child2) 428 | child2 ! "get" // verify it is alive 429 | expectMsg(0) 430 | child2 ! new Exception("CRASH") // escalate failure 431 | expectMsgPF() { 432 | case t @ Terminated(‘child2‘) if t.existenceConfirmed => () 433 | } 434 | ``` 435 | 436 | 监管者自己是被` ActorSystem `的顶级actor所监管的。顶级actor的缺省策略是对所有的` Exception `情况 (注意` ActorInitializationException `和 `ActorKilledException `是例外)进行重启. 由于缺省的重启指令会杀死所有的子actor,我们知道我们可怜的子actor最终无法从这个失败中幸免。 437 | 438 | 如果这不是我们希望的行为 (这取决于实际用例), 我们需要使用一个不同的监管者来覆盖这个行为。 439 | 440 | ```scala 441 | class Supervisor2 extends Actor { 442 | import akka.actor.OneForOneStrategy 443 | import akka.actor.SupervisorStrategy._ 444 | import scala.concurrent.duration._ 445 | override val supervisorStrategy = 446 | OneForOneStrategy(maxNrOfRetries = 10, withinTimeRange = 1 minute) { 447 | case _: ArithmeticException => Resume 448 | case _: NullPointerException => Restart 449 | case _: IllegalArgumentException => Stop 450 | case _: Exception => Escalate 451 | } 452 | def receive = { 453 | case p: Props => sender() ! context.actorOf(p) 454 | } 455 | // override default to kill all children during restart 456 | override def preRestart(cause: Throwable, msg: Option[Any]) {} 457 | } 458 | ``` 459 | 在这个父actor之下,子actor在上溯的重启中得以幸免,如以下最后的测试: 460 | 461 | ```scala 462 | val supervisor2 = system.actorOf(Props[Supervisor2], "supervisor2") 463 | supervisor2 ! Props[Child] 464 | val child3 = expectMsgType[ActorRef] 465 | child3 ! 23 466 | child3 ! "get" 467 | expectMsg(23) 468 | child3 ! new Exception("CRASH") 469 | child3 ! "get" 470 | expectMsg(0) 471 | ``` 472 | 473 | -------------------------------------------------------------------------------- /actors/fsm.md: -------------------------------------------------------------------------------- 1 | # 有限状态机 2 | 3 | ## 1 概述 4 | 5 | FSM (有限状态机) 可以mixin到akka Actor中 它的概念在[Erlang 设计原则](http://www.erlang.org/documentation/doc-4.8.2/doc/design_principles/fsm.html)中有最好的描述。 6 | 7 | 一个 FSM 可以描述成一组具有如下形式的关系 : 8 | 9 | > State(S) x Event(E) -> Actions (A), State(S’) 10 | 11 | 这些关系的意思可以这样理解 : 12 | 13 | > 如果我们当前处于状态S,发生了E事件, 我们应执行操作A,然后将状态转换为S’。 14 | 15 | ## 2 一个简单的例子 16 | 17 | 为了演示 FSM trait 的大部分功能, 考虑一个actor,它接收到一组突然爆发的 消息而将其送入邮箱队列,然后在消息爆发期过后或收到flush请求时对消息进行发送。 18 | 19 | 首先,假设以下所有代码都使用这些import语句: 20 | 21 | ```scala 22 | import akka.actor.{ Actor, ActorRef, FSM } 23 | import scala.concurrent.duration._ 24 | ``` 25 | 26 | 我们的`Buncher` actor会接收和发送以下消息: 27 | 28 | ```scala 29 | // received events 30 | case class SetTarget(ref: ActorRef) 31 | case class Queue(obj: Any) 32 | case object Flush 33 | // sent events 34 | case class Batch(obj: immutable.Seq[Any]) 35 | ``` 36 | 37 | `SetTarget`用来启动,为`Batches` 设置发送目标; `Queue` 添加数据到内部队列而` Flush `标志着消息爆发的结束。 38 | 39 | ```scala 40 | // states 41 | sealed trait State 42 | case object Idle extends State 43 | case object Active extends State 44 | sealed trait Data 45 | case object Uninitialized extends Data 46 | case class Todo(target: ActorRef, queue: immutable.Seq[Any]) extends Data 47 | ``` 48 | 49 | 这个actor可以处于两种状态: 队列中没有消息 (即`Idle`) 或有消息 (即` Active`)。只要一直有消息进来并且没有flush请求,它就停留在active状态。这个actor的内部状态数据是由批消息的发送目标actor引用和实际的消息队列组成。 50 | 51 | 现在让我们看看我们的FSM actor的框架: 52 | 53 | ```scala 54 | class Buncher extends Actor with FSM[State, Data] { 55 | 56 | startWith(Idle, Uninitialized) 57 | 58 | when(Idle) { 59 | case Event(SetTarget(ref), Uninitialized) => stay using Todo(ref, Vector.empty) 60 | } 61 | 62 | // 此处省略转换过程 ... 63 | 64 | when(Active, stateTimeout = 1 second) { 65 | case Event(Flush | FSM.StateTimeout, t: Todo) => goto(Idle) using t.copy(queue = Vector.empty) 66 | } 67 | 68 | // 此处省略未处理消息 ... 69 | 70 | initialize 71 | } 72 | ``` 73 | 74 | 基本方法就是声明actor类, 混入`FSM` trait将可能的状态和数据作为类型参数。在actor的内部使用一个DSL来声明状态机。 75 | 76 | - `startsWith` 定义初始状态和初始数据 77 | - 然后对每一个状态有一个` when() { ... }` 声明待处理的事件(可以是多个,PartialFunction将用`orElse`进行连接)进行处理 78 | - 最后使用 initialize来启动它, 这会在初始状态中执行转换并启动定时器(如果需要的话)。 79 | 80 | 在这个例子中,我们从 `Idle` 和` Uninitialized `状态开始, 这两种状态下只处理` SetTarget() `消息; `stay `准备结束这个事件的处理而不离开当前状态, 而`using`使得FSM将其内部状态(这时为`Uninitialized` ) 替换为一个新的包含目标actor引用的 `Todo() `对象。Active 状态声明了一个状态超时, 意思是如果1秒内没有收到消息, 将生成一个` FSM.StateTimeout `消息。在本例中这与收到` Flush `指令消息有相同的效果, 即转回` Idle` 状态并将内部队列重置为空vector。 但消息是如何进入队列的? 由于在两种状态下都要做这件事, 我们利用了任何未被` when()` 块处理的消息发送到了` whenUnhandled() `块这个事实。 81 | 82 | ```scala 83 | whenUnhandled { 84 | // 两种状态的通用代码 85 | case Event(Queue(obj), t @ Todo(_, v)) => 86 | goto(Active) using t.copy(queue = v :+ obj) 87 | 88 | case Event(e, s) => 89 | log.warning("received unhandled request {} in state {}/{}", e, stateName, s) 90 | stay 91 | } 92 | ``` 93 | 94 | 这里第一个case是将 `Queue()` 请求加入内部队列中并进入 `Active` 状态 (当然如果已经在` Active `状态则保留), 前提是收到 `Queue()`时FSM数据不是` Uninitialized` 。 否则—在另一个case中—记录一个警告到日志并保持内部状态。 95 | 96 | 最后剩下的只有` Batches `是如何发送到目标的, 这里我们使用` onTransition `机制: 你可以声明多个这样的块,在状态切换发生时(i.e. 只有当状态真正改变时) 所有的块都将被尝试来作匹配。 97 | 98 | ```scala 99 | onTransition { 100 | case Active -> Idle => 101 | stateData match { 102 | case Todo(ref, queue) => ref ! Batch(queue) 103 | } 104 | } 105 | ``` 106 | 107 | 状态转换回调是一个以一对状态作为输入的函数 —— 当前状态和新状态. FSM trait为此提供了一个方便的箭头形式的extractor, 非常贴心地提醒你所匹配到的状态转换的方向。在状态转换过程中, 可以看到旧状态数据可以通过 stateData获得, 新状态数据可以通过 nextStateData获得。 108 | 109 | 要确认这个调制器真实可用,可以写一个测试将`ScalaTest trait`融入`AkkaSpec`中: 110 | 111 | ```scala 112 | import akka.testkit.AkkaSpec 113 | import akka.actor.Props 114 | 115 | class FSMDocSpec extends AkkaSpec { 116 | 117 | "simple finite state machine" must { 118 | // 此处省略fsm代码 ... 119 | 120 | "batch correctly" in { 121 | val buncher = system.actorOf(Props(new Buncher)) 122 | buncher ! SetTarget(testActor) 123 | buncher ! Queue(42) 124 | buncher ! Queue(43) 125 | expectMsg(Batch(Seq(42, 43))) 126 | buncher ! Queue(44) 127 | buncher ! Flush 128 | buncher ! Queue(45) 129 | expectMsg(Batch(Seq(44))) 130 | expectMsg(Batch(Seq(45))) 131 | } 132 | 133 | "batch not if uninitialized" in { 134 | val buncher = system.actorOf(Props(new Buncher)) 135 | buncher ! Queue(42) 136 | expectNoMsg 137 | } 138 | } 139 | } 140 | ``` 141 | 142 | ## 3 参考 143 | 144 | ### FSM Trait 和 FSM Object 145 | 146 | FSM trait直接继承自actor。当你希望继承FSM,你必须知道一个actor已经创建。 147 | 148 | ```scala 149 | class Buncher extends FSM[State, Data] { 150 | // fsm body ... 151 | initialize() 152 | } 153 | ``` 154 | > *注:FSM trait定义了一个receive方法,这个方法处理内部消息,通过FSM逻辑传递其它的东西(根据当前的状态)。当重写receive方法时,记住状态过期处理依赖于通过FSM逻辑传输的消息* 155 | 156 | FSM trait 有两个类型参数 : 157 | 158 | - 所有状态名称的父类型,通常是一个sealed trait,状态名称作为case object来继承它。 159 | - 状态数据的类型,由 FSM 模块自己跟踪。 160 | 161 | > *包含状态名字的状态数据描述了状态机的内部状态,如果你坚持这个schema,并且不添加可变字段到FSM类,you have the advantage of making all changes of the internal state explicit in a few well-known places* 162 | 163 | ### 定义状态 164 | 165 | 状态的定义是通过一次或多次调用`when([, stateTimeout = ])(stateFunction)`方法 166 | 167 | 给定的名称对象必须与为FSM trait指定的第一个参数类型相匹配. 这个对象将被用作一个hash表的键, 所以你必须确保它正确地实现了`equals`和`hashCode`方法; 特别是它不能是可变量。 满足这些条件的最简单的就是` case objects`。 168 | 169 | 如果给定了` stateTimeout` 参数, 那么所有到这个状态的转换,包括停留, 缺省都会收到这个超时。初始化转换时显式指定一个超时可以用来覆盖这个缺省行为。在操作执行的过程中可以通过` setStateTimeout(state, duration)`来修改任何状态的超时时间. 这使得运行时配置(e.g. 通过外部消息)成为可能。 170 | 171 | 参数` stateFunction` 是一个` PartialFunction[Event, State]`, 用偏函数的语法来指定,见下例: 172 | 173 | ```scala 174 | when(Idle) { 175 | case Event(SetTarget(ref), Uninitialized) => 176 | stay using Todo(ref, Vector.empty) 177 | } 178 | when(Active, stateTimeout = 1 second) { 179 | case Event(Flush | StateTimeout, t: Todo) => 180 | goto(Idle) using t.copy(queue = Vector.empty) 181 | } 182 | ``` 183 | 184 | `Event(msg: Any, data: D)`这个case类以FSM所持有的数据类型为参数,以便进行模式匹配。 185 | 186 | 推荐声明状态为一个继承自封闭trait的对象,然后验证每个状态有一个`when`子句。如果你想以`unhandled`状态离开处理,可以像下面这样声明。 187 | 188 | ```scala 189 | when(SomeState)(FSM.NullFunction) 190 | ``` 191 | 192 | ### 定义初始状态 193 | 194 | 每个FSM都需要一个起点, 用`startWith(state, data[, timeout])`来声明。 195 | 196 | 可选的超时参数将覆盖所有为期望的初始状态指定的值。想要取消缺省的超时, 使用 Duration.Inf。 197 | 198 | ### 未处理事件 199 | 200 | 如果一个状态未能处理一个收到的事件,日志中将记录一条警告. 这种情况下如果你想做点其它的事,你可以指定 `whenUnhandled(stateFunction)`: 201 | 202 | ```scala 203 | whenUnhandled { 204 | case Event(x: X, data) => 205 | log.info("Received unhandled event: " + x) 206 | stay 207 | case Event(msg, _) => 208 | log.warning("Received unknown event: " + msg) 209 | goto(Error) 210 | } 211 | ``` 212 | 213 | 重要: 这个处理器不会叠加,每一次启动` whenUnhandled `都会覆盖先前指定的处理器。 214 | 215 | ### 发起状态转换 216 | 217 | 任何`stateFunction`的结果都必须是下一个状态的定义,除非FSM正在被终止, 这种情况在[从内部终止]中介绍。 状态定义可以是当前状态, 由 stay 指令描述, 或由 goto(state)指定的另一个状态。 结果对象可以通过下面列出的修饰器作进一步限制: 218 | 219 | - `forMax(duration)` 220 | 221 | 这个修饰器为新状态指定状态超时。这意味着将启动一个定时器,它过期时将向FSM发送一个`StateTimeout`消息。 其间接收到任何其它消息时定时器将被取消; 你可以确定的事实是`StateTimeout`消息不会在任何一个中间消息之后被处理。 222 | 223 | 这个修饰器也可以用于覆盖任何对目标状态指定的缺省超时。如果要取消缺省超时,用`Duration.Inf`。 224 | 225 | - `using(data)` 226 | 227 | 这个修饰器用给定的新数据取代旧的状态数据. 如果你遵循上面的建议, 这是内部状态数据被修改的唯一位置。 228 | 229 | - `replying(msg)` 230 | 231 | 这个修饰器为当前处理的消息发送一个应答,不同的是它不会改变状态转换 232 | 233 | 所有的修饰器都可以链式调用来获得优美简洁的表达方式: 234 | 235 | ```scala 236 | when(SomeState) { 237 | case Event(msg, _) => 238 | goto(Processing) using (newData) forMax (5 seconds) replying (WillDo) 239 | } 240 | ``` 241 | 242 | 事实上这里所有的括号都不是必须的, 但它们在视觉上将修饰器和它们的参数区分开,因而使代码对于他人有更好的可读性。 243 | 244 | > *注意:请注意` return `语句不可以用于` when`或类似的代码块中; 这是Scala的限制。 要么重构你的代码,使用` if () ... else ... `或将它改写到一个方法定义中。* 245 | 246 | ### 监控状态转换 247 | 248 | 在概念上,转换发生在 “两个状态之间” , 也就是在你放在事件处理代码块执行的任何操作之后; 这是显然的,因为只有在事件处理逻辑返回了值以后才能确定新的状态。 你不需要担心相对于设置内部状态变量的顺序的细节,因为FSM actor中的所有代码都是在一个线程中运行的。 249 | 250 | #### 内部监控 251 | 252 | 到目前为止,FSM DSL都围绕着状态和事件。 另外一种视角是将其描述成一系列的状态转换。 方法`onTransition(handler)`将操作与状态转换而不是状态或事件联系起来。 这个处理器是一个偏函数,它以一对状态作为输入; 不需要结果状态因为不可能改变正在进行的状态转换。 253 | 254 | ```scala 255 | onTransition { 256 | case Idle -> Active => setTimer("timeout", Tick, 1 second, true) 257 | case Active -> _ => cancelTimer("timeout") 258 | case x -> Idle => log.info("entering Idle from " + x) 259 | } 260 | ``` 261 | 262 | `->` 用来以清晰的形式解开状态对并表达了状态转换的方向。 与通常的模式匹配一样, 可以用` _ `来表示不关心的内容; 或者你可以将不关心的状态绑定到一个变量, e.g. 像上一个例子那样供记日志使用。 263 | 264 | 也可以向 onTransition给定一个以两个状态为参数的函数, 例如你的状态转换处理逻辑是定义成方法的: 265 | 266 | ```scala 267 | onTransition(handler _) 268 | def handler(from: StateType, to: StateType) { 269 | // handle it here ... 270 | } 271 | ``` 272 | 273 | 用这个方法注册的处理器是迭加的, 这样你可以将 onTransition 块和 when 块分散定义以适应设计的需要. 但必须注意的是,每一次状态转换都会调用所有的处理器, 而不是最先匹配的那个。 这是一个故意的设计,使得你可以将某一部分状态转换处理放在某一个地方而不用担心先前的定义会屏蔽后面的;当然这些操作还是按定义的顺序执行的。 274 | 275 | > *注意:这种内部监控可以用于通过状态转换来构建你的FSM,这样在添加新的目标状态时不会忘记。例如在离开某个状态时取消定时器这种操作。* 276 | 277 | #### 外部监控 278 | 279 | 可以用`SubscribeTransitionCallBack(actorRef)`注册一个外部actor来接收状态转换的通知消息。 这个具名actor将立即收到` CurrentState(self, stateName) `消息 并在之后每次进入新状态时收到` Transition(actorRef, oldState, newState) `消息。FSM actor发送`UnsubscribeTransitionCallBack(actorRef)` 来注销外部监控actor。 280 | 281 | 注册一个未运行的监听actor将生成一个警告,并优雅地失败。停止一个未注销的监听actor将在下一次状态转换时从注册列表中将该监听actor删除。 282 | 283 | ### 转换状态 284 | 285 | 作为`when()`块参数的偏函数可以使用Scala的函数式编程工具来转换。为了保持类型推断,应用于一些通用处理逻辑的一个帮助函数可以应用到不同的子句中。 286 | 287 | ```scala 288 | when(SomeState)(transform { 289 | case Event(bytes: ByteString, read) => stay using (read + bytes.length) 290 | } using { 291 | case s @ FSM.State(state, read, timeout, stopReason, replies) if read > 1000 => 292 | goto(Processing) 293 | }) 294 | ``` 295 | 296 | 不言而喻,这个方法的参数可以保存并应用多次。也就是说应用相同的转换到不同的when()块中 297 | 298 | ```scala 299 | val processingTrigger: PartialFunction[State, State] = { 300 | case s @ FSM.State(state, read, timeout, stopReason, replies) if read > 1000 => 301 | goto(Processing) 302 | } 303 | when(SomeState)(transform { 304 | case Event(bytes: ByteString, read) => stay using (read + bytes.length) 305 | } using processingTrigger) 306 | ``` 307 | 308 | ### 定时器 309 | 310 | 除了状态超时,FSM还管理以String名称为标识的定时器。 你可以用`setTimer(name, msg, interval, repeat)`设置定时器其中msg 是经过interval时间以后发送的消息。 如果 `repeat` 设成 true, 定时器将以`interval`指定的时间段重复规划。 311 | 312 | 用`cancelTimer(name)`来取消定时器,取消操作确保立即执行,这意味着在这个调用之后定时器已经规划的消息将不会执行。任何定时器的状态可以用`isTimerActive(name)`进行查询。这些具名定时器是对状态超时的补充,因为它们不受中间收到的其它消息的影响。 313 | 314 | 315 | ### 从内部终止 316 | 317 | 将结果状态设置为`stop([reason[, data]])`将终止FSM。 318 | 319 | 其中的reason必须是 `Normal` (缺省), `Shutdown` 或 `Failure(reason)` 之一, 可以提供第二个参数来改变状态数据,在终止处理器中可以使用该数据。 320 | 321 | > *必须注意`stop`并不会停止当前的操作,立即停止FSM。 stop操作必须像状态转换一样从事件处理器中返回 (但要注意 return 语句不能用在 when 块中)。* 322 | 323 | ```scala 324 | when(Error) { 325 | case Event("stop", _) => 326 | // do cleanup ... 327 | stop() } 328 | ``` 329 | 330 | 你可以用` onTermination(handler) `来指定当FSM停止时要运行的代码。 其中的` handler `是一个以`StopEvent(reason, stateName, stateData)` 为参数的偏函数 331 | 332 | ```scala 333 | onTermination { 334 | case StopEvent(FSM.Normal, state, data) => // ... 335 | case StopEvent(FSM.Shutdown, state, data) => // ... 336 | case StopEvent(FSM.Failure(cause), state, data) => // ... 337 | } 338 | ``` 339 | 340 | 对于使用` whenUnhandled `的场合, 这个处理器不会迭加, 所以每次调用` onTermination `都会替换先前安装的处理器。 341 | 342 | ### 从外部终止 343 | 344 | 当FSM关联的` ActorRef `被` stop `方法停止后, 它的` postStop hook `将被执行。在`FSM trait`中的缺省实现是执行` onTermination `处理器(如果有的话)来处理 `StopEvent(Shutdown, ...)`事件。 345 | 346 | > *注意:如果你重写`postStop `并希望你的` onTermination `处理器被调用, 别忘了调用` super.postStop`。* 347 | 348 | ## 4 测试和调试有限状态机 349 | 350 | ### 事件跟踪 351 | 352 | 配置文件中的` akka.actor.debug.fsm `打开用` LoggingFSM `实例完成的事件跟踪日志: 353 | 354 | ```scala 355 | import akka.actor.LoggingFSM 356 | class MyFSM extends LoggingFSM[StateType, Data] { 357 | // body elided ... 358 | } 359 | ``` 360 | 361 | 这个FSM 将以DEBUG 级别记录日志: 362 | 363 | - 所有处理完的事件, 包括 StateTimeout 和计划的定时器消息 364 | - 所有具名定时器的设置和取消 365 | - 所有的状态转换 366 | 367 | 生命周期变化及特殊消息可以如Actor中所述进行日志记录。 368 | 369 | ### 滚动的事件日志 370 | 371 | `LoggingFSM` trait 为FSM 添加了一个新的功能: 一个滚动的事件日志,它可以用于` debugging `(跟踪为什么 FSM 会进入某个失败的状态)) 或其它的什么新用法: 372 | 373 | ```scala 374 | import akka.actor.LoggingFSM 375 | class MyFSM extends LoggingFSM[StateType, Data] { 376 | override def logDepth = 12 377 | onTermination { 378 | case StopEvent(FSM.Failure(_), state, data) => 379 | val lastEvents = getLog.mkString("\n\t") 380 | log.warning("Failure in state " + state + " with data " + data + "\n" + 381 | "Events leading up to this point:\n\t" + lastEvents) 382 | // ... 383 | } } 384 | ``` 385 | 386 | logDepth 缺省值为0, 意思是关闭事件日志。 387 | 388 | > *注意:日志缓冲区是在actor创建时分配的,这也是为什么logDepth的配置使用了虚方法调用。如果你想用 val对其进行覆盖, 必须保证它的初始化在` LoggingFSM `的初始化之前完成, 而且在缓冲区分配完成后不要修改` logDepth `返回的值。* 389 | 390 | 事件日志的内容可以用` getLog `方法获取, 它返回一个` IndexedSeq[LogEntry] `,其中最新的条目在位置0。 391 | 392 | ## 5 例子 393 | 394 | 可以在[这里](http://www.typesafe.com/activator/template/akka-sample-fsm-scala)找到一个与`Actor become/unbecome `方式进行了对比的大一些的FSM示例 -------------------------------------------------------------------------------- /actors/mailbox.md: -------------------------------------------------------------------------------- 1 | # 邮箱 2 | 3 | > 一个AKKA邮箱保有发向actor的消息,一般情况下,每个actor都有自己的邮箱,but with for example a BalancingPool all routees will share a single mailbox instance。 4 | 5 | ## 1 邮箱选择 6 | 7 | ### 为一个actor指定一个消息队列类型 8 | 9 | 通过继承参数化的trait `RequiresMessageQueue`为一个确定类型的actor指定一个确定类型的消息类型是可能的。下面是一个例子。 10 | 11 | ```scala 12 | import akka.dispatch.RequiresMessageQueue 13 | import akka.dispatch.BoundedMessageQueueSemantics 14 | class MyBoundedActor extends MyActor 15 | with RequiresMessageQueue[BoundedMessageQueueSemantics] 16 | ``` 17 | 18 | `RequiresMessageQueue` trait需要在配置文件中映射到一个邮箱上。如下所示: 19 | 20 | ```scala 21 | bounded-mailbox { 22 | mailbox-type = "akka.dispatch.BoundedMailbox" 23 | mailbox-capacity = 1000 24 | mailbox-push-timeout-time = 10s 25 | } 26 | akka.actor.mailbox.requirements { 27 | "akka.dispatch.BoundedMessageQueueSemantics" = bounded-mailbox 28 | } 29 | ``` 30 | 31 | 现在,你每次创建`MyBoundedActor`类型的actor,它都会尝试得到一个有边界的邮箱。如果在部署中有不同的邮箱配置(或者是直接的,或者是通过一个拥有特定邮箱类型的派发器),那将覆盖这个映射。 32 | 33 | > *注意:为一个actor创建的邮箱类型队列将会在trait中进行核对,检查是不是所需类型。如果队列没有实现所需类型,actor创建将会失败* 34 | 35 | ### 为一个派发器指定一个消息队列类型 36 | 37 | 一个派发器可能也有这样的需求:需要一个邮箱类型。一个例子是BalancingDispatcher需要一个消息队列,这个队列对多个并发消费者来说是线程安全的。这个需求在派发器的配置中配置,如下所示: 38 | 39 | ```scala 40 | my-dispatcher { 41 | mailbox-requirement = org.example.MyInterface 42 | } 43 | ``` 44 | 45 | `mailbox-requirement`需要一个类或者接口名,这个接口然后能够被确认为消息队列实现的超类型。万一冲突了-如果actor需要的邮箱类型不能满足这个需求,actor创建将会失败。 46 | 47 | ### 怎样选择邮箱类型 48 | 49 | 当一个actor创建时,`ActorRefProvider`首先确定执行它的派发器,如何确定邮箱,如下所述: 50 | 51 | - 如果actor的部署配置片段包含了一个邮箱key,那么这个配置部分描述的邮箱类型将被采用 52 | - 如果actor的Props包含邮箱选择-在其上调用了`withMailbox`,那么这个配置部分描述的邮箱类型将被采用 53 | - 如果派发器的配置片段包含了一个`mailbox-type` key,这个配置片段将会用来配置邮箱类型 54 | - 如果派发器需要一个邮箱类型,这个需求的映射将会用来决定使用的邮箱类型 55 | - 缺省的`akka.actor.default-mailbox`将会被使用 56 | 57 | ### 缺省的邮箱 58 | 59 | 当邮箱没有指定时,缺省的邮箱将会使用。默认情况下,这个邮箱是没有边界的。由`java.util.concurrent.ConcurrentLinkedQueue.SingleConsumerOnlyUnboundedMailbox`支持的邮箱是效率更高的邮箱,可以作为缺省的邮箱,但是不能作为和BalancingDispatcher一起使用。 60 | 61 | 配置`SingleConsumerOnlyUnboundedMailbox`为缺省邮箱,如下所示: 62 | 63 | ```scala 64 | akka.actor.default-mailbox { 65 | mailbox-type = "akka.dispatch.SingleConsumerOnlyUnboundedMailbox" 66 | } 67 | ``` 68 | ### 哪个配置传递给邮箱类型 69 | 70 | 每个邮箱类型通过一个继承`MailboxType`的类实现,这个类的构造器有两个参数,一个`ActorSystem.Settings`对象,一个`Config`片段。后者通过从actor系统配置中获得的具名配置片段计算得到,用邮箱类型的配置路径覆盖它的`id` key,并且添加一个`fall-back`到缺省的邮箱配置片段。(he latter is computed by obtaining the named configuration section from the actor system’s configuration, overriding its id key with the configuration path of the mailbox type and adding a fall-back to the default mailbox configuration section) 71 | 72 | ## 2 内置实现 73 | 74 | Akka内置有几个邮箱实现: 75 | 76 | - UnboundedMailbox-默认的邮箱 77 | 78 | - 由`java.util.concurrent.ConcurrentLinkedQueue`支持 79 | - 阻塞:无 80 | - 边界:无 81 | - 配置名:`unbounded`或者`akka.dispatch.UnboundedMailbox` 82 | 83 | - SingleConsumerOnlyUnboundedMailbox 84 | 85 | - 由一个高效的的多生产者单消费者队列支持,不能和`BalancingDispatcher`一起使用 86 | - 阻塞:无 87 | - 边界:无 88 | - 配置名:`akka.dispatch.SingleConsumerOnlyUnboundedMailbox` 89 | 90 | - BoundedMailbox 91 | 92 | - 由`java.util.concurrent.LinkedBlockingQueue`支持 93 | - 阻塞:有 94 | - 边界:有 95 | - 配置名:`bounded`或者`akka.dispatch.BoundedMailbox` 96 | 97 | - UnboundedPriorityMailbox 98 | 99 | - 由`java.util.concurrent.PriorityBlockingQueue`支持 100 | - 阻塞:有 101 | - 边界:无 102 | - 配置名:`akka.dispatch.UnboundedPriorityMailbox` 103 | 104 | - BoundedPriorityMailbox 105 | 106 | - 由`java.util.PriorityBlockingQueue`包裹一个`akka.util.BoundedBlockingQueue`支持 107 | - 阻塞:有 108 | - 边界:有 109 | - 配置名:`kka.dispatch.BoundedPriorityMailbox` 110 | 111 | ## 3 邮箱配置例子 112 | 113 | 怎样创建一个PriorityMailbox 114 | 115 | ```scala 116 | import akka.dispatch.PriorityGenerator 117 | import akka.dispatch.UnboundedPriorityMailbox 118 | import com.typesafe.config.Config 119 | // We inherit, in this case, from UnboundedPriorityMailbox 120 | // and seed it with the priority generator 121 | class MyPrioMailbox(settings: ActorSystem.Settings, config: Config) 122 | extends UnboundedPriorityMailbox( 123 | // Create a new PriorityGenerator, lower prio means more important 124 | PriorityGenerator { 125 | // ’highpriority messages should be treated first if possible 126 | case ’highpriority => 0 127 | // ’lowpriority messages should be treated last if possible 128 | case ’lowpriority => 2 129 | // PoisonPill when no other left 130 | case PoisonPill => 3 131 | // We default to 1, which is in between high and low 132 | case otherwise => 1 133 | }) 134 | ``` 135 | 136 | 然后将它添加到配置文件中: 137 | 138 | ```scala 139 | prio-dispatcher { 140 | mailbox-type = "docs.dispatcher.DispatcherDocSpec$MyPrioMailbox" 141 | //Other dispatcher configuration goes here 142 | } 143 | ``` 144 | 145 | 然后是一个如何使用它的例子: 146 | 147 | ```scala 148 | // We create a new Actor that just prints out what it processes 149 | class Logger extends Actor { 150 | val log: LoggingAdapter = Logging(context.system, this) 151 | self ! ’lowpriority 152 | self ! ’lowpriority 153 | self ! ’highpriority 154 | self ! ’pigdog 155 | self ! ’pigdog2 156 | self ! ’pigdog3 157 | self ! ’highpriority 158 | self ! PoisonPill 159 | def receive = { 160 | case x => log.info(x.toString) 161 | } } 162 | val a = system.actorOf(Props(classOf[Logger], this).withDispatcher( 163 | "prio-dispatcher")) 164 | /* 165 | * Logs: 166 | * ’highpriority 167 | * ’highpriority 168 | * ’pigdog 169 | * ’pigdog2 170 | * ’pigdog3 171 | * ’lowpriority 172 | * ’lowpriority 173 | */ 174 | ``` 175 | 也可以用下面的方式直接配置邮箱 176 | 177 | ```scala 178 | prio-mailbox { 179 | mailbox-type = "docs.dispatcher.DispatcherDocSpec$MyPrioMailbox" 180 | //Other mailbox configuration goes here 181 | } 182 | akka.actor.deployment { 183 | /priomailboxactor { 184 | mailbox = prio-mailbox 185 | } 186 | } 187 | ``` 188 | 189 | 然后使用它: 190 | 191 | ```scala 192 | import akka.actor.Props 193 | val myActor = context.actorOf(Props[MyActor], "priomailboxactor") 194 | ``` 195 | 或者: 196 | 197 | ```scala 198 | import akka.actor.Props 199 | val myActor = context.actorOf(Props[MyActor].withMailbox("prio-mailbox")) 200 | ``` 201 | 202 | ## 4 创建你自己的邮箱类型 203 | 204 | 一个例子胜过千言万语 205 | 206 | ```scala 207 | import akka.actor.ActorRef 208 | import akka.actor.ActorSystem 209 | import akka.dispatch.Envelope 210 | import akka.dispatch.MailboxType 211 | import akka.dispatch.MessageQueue 212 | import akka.dispatch.ProducesMessageQueue 213 | import com.typesafe.config.Config 214 | import java.util.concurrent.ConcurrentLinkedQueue 215 | import scala.Option 216 | // Marker trait used for mailbox requirements mapping 217 | trait MyUnboundedMessageQueueSemantics 218 | object MyUnboundedMailbox { 219 | // This is the MessageQueue implementation 220 | class MyMessageQueue extends MessageQueue 221 | with MyUnboundedMessageQueueSemantics { 222 | private final val queue = new ConcurrentLinkedQueue[Envelope]() 223 | // these should be implemented; queue used as example 224 | def enqueue(receiver: ActorRef, handle: Envelope): Unit =queue.offer(handle) 225 | def dequeue(): Envelope = queue.poll() 226 | def numberOfMessages: Int = queue.size 227 | def hasMessages: Boolean = !queue.isEmpty 228 | def cleanUp(owner: ActorRef, deadLetters: MessageQueue) { 229 | while (hasMessages) { 230 | deadLetters.enqueue(owner, dequeue()) 231 | } 232 | } 233 | } 234 | } 235 | // This is the Mailbox implementation 236 | class MyUnboundedMailbox extends MailboxType 237 | with ProducesMessageQueue[MyUnboundedMailbox.MyMessageQueue] { 238 | import MyUnboundedMailbox._ 239 | // This constructor signature must exist, it will be called by Akka 240 | def this(settings: ActorSystem.Settings, config: Config) = { 241 | // put your initialization code here 242 | this() 243 | } 244 | // The create method is called to create the MessageQueue 245 | final override def create(owner: Option[ActorRef], 246 | system: Option[ActorSystem]): MessageQueue = 247 | new MyMessageQueue() 248 | } 249 | ``` 250 | 251 | 然后,你仅仅需要你的`MailboxType`的全类名作为派发器配置项或者邮箱配置项`mailbox-type`的值。 252 | 253 | > *注意:确保包含一个构造器,这个构造器持有`akka.actor.ActorSystem.Settings`和`com.typesafe.config.Config`两个参数,这个构造器在构造你的邮箱类型时会被直接调用。The config passed in as second argument is that section from the configuration which describes the dispatcher or mailbox setting using this mailbox type; the mailbox type will be instantiated once for each dispatcher or mailbox setting using it* 254 | 255 | 你也可以使用邮箱作为派发器的requirement 256 | 257 | ```scala 258 | custom-dispatcher { 259 | mailbox-requirement = 260 | "docs.dispatcher.MyUnboundedJMessageQueueSemantics" 261 | } 262 | akka.actor.mailbox.requirements { 263 | "docs.dispatcher.MyUnboundedJMessageQueueSemantics" = 264 | custom-dispatcher-mailbox 265 | } 266 | custom-dispatcher-mailbox { 267 | mailbox-type = "docs.dispatcher.MyUnboundedJMailbox" 268 | } 269 | ``` 270 | 271 | 或者在你的actor类里面定义requirement 272 | 273 | ```scala 274 | class MySpecialActor extends Actor 275 | with RequiresMessageQueue[MyUnboundedMessageQueueSemantics] { 276 | // ... 277 | } 278 | ``` 279 | 280 | ## 5 system.actorOf 的特殊语义 281 | 282 | 为了使`system.actorOf`在保证(keeping)返回类型`ActorRef`的同时(返回ref的语义是完全函数式的)既是同步的又是非阻塞的,特殊的处理将会进行。在后台,一个空的actor引用被构造,它被送到系统监控actor,这个监控actor创建actor和它的context,并且将它们放入引用。直到这发生后,发送给`ActorRef`的消息将会在本地排队,并且只在交换真实的内容后才将它们转移到邮箱。 283 | 284 | Until that has happened, messages sent to the ActorRef will be queued locally, and only upon swapping the real filling in will they be transferred into the real mailbox。 285 | 286 | 所以, 287 | 288 | ```scala 289 | val props: Props = ... 290 | // this actor uses MyCustomMailbox, which is assumed to be a singleton 291 | system.actorOf(props.withDispatcher("myCustomMailbox")) ! "bang" 292 | assert(MyCustomMailbox.instance.getLastEnqueuedMessage == "bang") 293 | ``` 294 | 将有可能失败。 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | -------------------------------------------------------------------------------- /actors/routing.md: -------------------------------------------------------------------------------- 1 | # 路由 2 | 3 | 消息可以通过路由器有效的发送到目的actor,这称为`routees`。一个路由器可以在actor内部和外部使用,你能够自己管理`routees`或者使用一个配置容量的自包含路由器actor。 4 | 5 | 根据你的应用程序的需求,可以使用不同的路由器策略。Akka包含了几个开箱可用的路由器策略。 6 | 7 | ## 1 一个简单的路由器 8 | 9 | 下面的例子证明了怎样使用`Router`以及管理`routees`。 10 | 11 | ```scala 12 | import akka.routing.ActorRefRoutee 13 | import akka.routing.Router 14 | import akka.routing.RoundRobinRoutingLogic 15 | class Master extends Actor { 16 | var router = { 17 | val routees = Vector.fill(5) { 18 | val r = context.actorOf(Props[Worker]) 19 | context watch r 20 | ActorRefRoutee(r) 21 | } 22 | Router(RoundRobinRoutingLogic(), routees) 23 | } 24 | def receive = { 25 | case w: Work => 26 | router.route(w, sender()) 27 | case Terminated(a) => 28 | router = router.removeRoutee(a) 29 | val r = context.actorOf(Props[Worker]) 30 | context watch r 31 | router = router.addRoutee(r) 32 | } } 33 | ``` 34 | 35 | 我们创建一个路由器并且当路由消息到routee时指定使用`RoundRobinRoutingLogic`。 36 | 37 | Akka中自带的路由逻辑有: 38 | 39 | - akka.routing.RoundRobinRoutingLogic 40 | - akka.routing.RandomRoutingLogic 41 | - akka.routing.SmallestMailboxRoutingLogic 42 | - akka.routing.BroadcastRoutingLogic 43 | - akka.routing.ScatterGatherFirstCompletedRoutingLogic 44 | - akka.routing.TailChoppingRoutingLogic 45 | - akka.routing.ConsistentHashingRoutingLogic 46 | 47 | 我们创建routees为包裹ActorRefRoutee的普通子actor,我们监视routees,当它们停止时可以置换它们。 48 | 49 | 通过路由器发送消息用`route`方法,如发送上例中的Work消息。 50 | 51 | `Router`是不可变的,`RoutingLogic`是线程安全的。这意味着它可以在actor之外使用。 52 | 53 | > *注:一般请求下,发送到路由器的任何消息将会进一步发送到routees,但是有一个例外, Broadcast Messages将会发送到所有路由器的routees* 54 | 55 | ## 2 一个路由器actor 56 | 57 | 一个路由器可以创建为一个自包含的actor,它自己管理routees以及从配置中加载路由逻辑和其它设置。 58 | 59 | 这种类型的路由器actor有两种不同的风格: 60 | 61 | - Pool:路由器创建routees为子actors,如果它们终止了,那么就从路由器中删除它们 62 | - Group:外部创建routee actors给路由器,路由器使用actor selection发送消息到特定的路径,不观察它的终止 63 | 64 | 可以用配置或者编码定义路由器actor的设置。虽然路由器actor可以在配置文件中配置,但是它还是必须通过变成创建。如果你在配置文件中定义了路由器actor,那么这些设置将会被用来替换编程提供的参数。 65 | 66 | 你通过路由器actor发送消息到routees与普通的actor的方式(通过它的`ActorRef`)是一样的。路由器actor转发消息到它的routees而不需要改变它的原始发送者。当routee回复一个路由过的消息,这个回复将会发送到原始发送者,而不是路由器actor。 67 | 68 | ### Pool 69 | 70 | 下面的代码和配置片段显示了怎样创建一个轮询的路由器,这个路由器转发消息到五个`Worker` routees。routees将会创建为路由器的孩子。 71 | 72 | ```scala 73 | akka.actor.deployment { 74 | /parent/router1 { 75 | router = round-robin-pool 76 | nr-of-instances = 5 77 | } 78 | } 79 | ``` 80 | 81 | ```scala 82 | val router1: ActorRef = 83 | context.actorOf(FromConfig.props(Props[Worker]), "router1") 84 | ``` 85 | 86 | 下面是一个相同的例子,只是路由器配置通过编码而不是配置文件获得。 87 | 88 | ```scala 89 | val router2: ActorRef = 90 | context.actorOf(RoundRobinPool(5).props(Props[Worker]), "router2") 91 | ``` 92 | 93 | #### 远程部署的Routees 94 | 95 | 除了可以将本地创建的actors作为routees, 你也可以让路由actor将自己创建的子actors部署到一组远程主机上; 这是以轮询的方式执行的。要完成这个工作,将配置包在` RemoteRouterConfig`中, 并附上作为部署目标的结点的远程地址。自然地这要求你在classpath中包括`akka-remote `模块: 96 | 97 | ```scala 98 | import akka.actor.{ Address, AddressFromURIString } 99 | import akka.remote.routing.RemoteRouterConfig 100 | val addresses = Seq( 101 | Address("akka.tcp", "remotesys", "otherhost", 1234), 102 | AddressFromURIString("akka.tcp://othersys@anotherhost:1234")) 103 | val routerRemote = system.actorOf( 104 | RemoteRouterConfig(RoundRobinPool(5), addresses).props(Props[Echo])) 105 | ``` 106 | 107 | #### 发送者(Sender) 108 | 109 | 默认情况下,当一个routee发送消息时,它将隐式地设置它自己为发送者 110 | 111 | ```scala 112 | sender() ! x // replies will go to this actor 113 | ``` 114 | 115 | 然而,对于routees而言,设置路由器为发送者通常是有用的。例如,你有想隐藏路由器背后routees的细节时,你有可能想设置路由器为发送者。下面的代码片段显示怎样设置父路由器为发送者。 116 | 117 | ```scala 118 | sender().tell("reply", context.parent) // replies will go back to parent 119 | sender().!("reply")(context.parent) // alternative syntax 120 | ``` 121 | 122 | #### 监视(Supervision) 123 | 124 | 通过一个pool路由器创建的routees是路由器的子actors,所有路由器是子actors的监视器。 125 | 126 | 路由器actor的监视策略可以通过Pool的`supervisorStrategy`的属性配置。如果没有提供配置,那么缺省的策略是“一直升级(always escalate)”。这意味着错误会传递到路由器的监视器上进行处理。路由器的监视器将会决定去做什么。 127 | 128 | 注意路由器监控器将会将错误当作一个带有路由器本身的错误。因此,一个停止或者重启指令将会造成路由器自己停止或者重启。路由器的停止又会造成子actors停止或者重启。 129 | 130 | 需要提出的一点是,路由器的重启行为已经被重写了,所以它将会重新创建子actors,并且保证Pool中拥有相同数量的actors。 131 | 132 | 这意味着,如果你没有指定路由器或者它的父actor的`supervisorStrategy`,routees中的失败将会升级到路由器的父actor,这将默认导致路由器重启,进而重启所有的routees。这是因为默认行为-添加`withRouter`到子actor的定义,不会改变应用到子actor的监控策略。这可能是无效的,所以你应该避免在定义路由器时指定监督策略。 133 | 134 | This means that if you have not specified supervisorStrategy of the router or its parent a failure in a routee will escalate to the parent of the router, which will by default restart the router, which will restart all routees (it uses Escalate and does not stop routees during restart). The reason is to make the default behave such that adding withRouter to a child’s definition does not change the supervision strategy applied to the child. This might be an inefficiency that you can avoid by specifying the strategy when defining the router. 135 | 136 | 可以很简单的设置策略: 137 | 138 | ```scala 139 | val escalator = OneForOneStrategy() { 140 | case e => testActor ! e; SupervisorStrategy.Escalate 141 | } 142 | val router = system.actorOf(RoundRobinPool(1, supervisorStrategy = escalator).props( 143 | routeeProps = Props[TestActor])) 144 | ``` 145 | 146 | > *注:一个Pool路由器的子actors终止,Pool路由器不会自动创建一个新的子actor。如果一个Pool路由器的所有子actors终止,路由器自己也会终止,除非它是一个动态路由器,如使用一个resizer* 147 | 148 | ## Group 149 | 150 | 有时,与其用路由器actor创建它的routees,分开创建routees并把它们提供给路由器使用更令人满意。你可以通过传递一个routees的路径到路由器的配置做到这一点。消息将会利用`ActorSelection`发送到这些路径。 151 | 152 | 下面的例子显示了通过提供给路由器三个routee actors的路径字符串来创建这个路由器。 153 | 154 | ```scala 155 | akka.actor.deployment { 156 | /parent/router3 { 157 | router = round-robin-group 158 | routees.paths = ["/user/workers/w1", "/user/workers/w2", "/user/workers/w3"] 159 | } 160 | } 161 | ``` 162 | 163 | ```scala 164 | val router3: ActorRef = 165 | context.actorOf(FromConfig.props(), "router3") 166 | ``` 167 | 168 | 下面是相同的例子,只是路由器的配置通过编程设置而不是配置文件。 169 | 170 | ```scala 171 | val router4: ActorRef = 172 | context.actorOf(RoundRobinGroup(paths).props(), "router4") 173 | ``` 174 | 175 | routee actors在路由器外部创建: 176 | 177 | ```scala 178 | system.actorOf(Props[Workers], "workers") 179 | ``` 180 | 181 | ```scala 182 | class Workers extends Actor { 183 | context.actorOf(Props[Worker], name = "w1") 184 | context.actorOf(Props[Worker], name = "w2") 185 | context.actorOf(Props[Worker], name = "w3") 186 | // ... 187 | ``` 188 | 189 | 路径可能包括为运行在远程机器上的actors提供的协议和地址信息。Remoting需要将`akka-remote`模块包含在类路径下 190 | 191 | ```scala 192 | akka.actor.deployment { 193 | /parent/remoteGroup { 194 | router = round-robin-group 195 | routees.paths = [ 196 | "akka.tcp://app@10.0.0.1:2552/user/workers/w1", 197 | "akka.tcp://app@10.0.0.2:2552/user/workers/w1", 198 | "akka.tcp://app@10.0.0.3:2552/user/workers/w1"] 199 | } } 200 | ``` 201 | 202 | ## 3 路由器的使用 203 | 204 | 这一章,我们将介绍怎样创建不同类型的路由器actor。 205 | 206 | 这一章的路由器actors通过一个名叫`parent`的顶级actor创建。注意在配置中,部署路径以`/parent/`开头,后跟着路由器actor的名字。 207 | 208 | ```scala 209 | system.actorOf(Props[Parent], "parent") 210 | ``` 211 | 212 | ### RoundRobinPool和RoundRobinGroup 213 | 214 | 以轮询的方式路由到routee 215 | 216 | 在配置文件中定义的`RoundRobinPool` 217 | 218 | ```scala 219 | akka.actor.deployment { 220 | /parent/router1 { 221 | router = round-robin-pool 222 | nr-of-instances = 5 223 | } 224 | } 225 | ``` 226 | 227 | ```scala 228 | val router1: ActorRef = 229 | context.actorOf(FromConfig.props(Props[Worker]), "router1") 230 | ``` 231 | 232 | 在代码中定义的`RoundRobinPool` 233 | 234 | ```scala 235 | val router2: ActorRef = 236 | context.actorOf(RoundRobinPool(5).props(Props[Worker]), "router2") 237 | ``` 238 | 239 | 在配置文件中定义的`RoundRobinGroup` 240 | 241 | ```scala 242 | akka.actor.deployment { 243 | /parent/router3 { 244 | router = round-robin-group 245 | routees.paths = ["/user/workers/w1", "/user/workers/w2", "/user/workers/w3"] 246 | } 247 | } 248 | ``` 249 | 250 | ```scala 251 | val router3: ActorRef = 252 | context.actorOf(FromConfig.props(), "router3") 253 | ``` 254 | 255 | 在代码中定义的`RoundRobinGroup` 256 | 257 | ```scala 258 | val paths = List("/user/workers/w1", "/user/workers/w2", "/user/workers/w3") 259 | val router4: ActorRef = 260 | context.actorOf(RoundRobinGroup(paths).props(), "router4") 261 | ``` 262 | 263 | ### RandomPool和RandomGroup 264 | 265 | 这个路由器类型为每个消息随机选择一个routee 266 | 267 | 在配置文件中定义的`RandomGroup` 268 | 269 | ```scala 270 | akka.actor.deployment { 271 | /parent/router5 { 272 | router = random-pool 273 | nr-of-instances = 5 274 | } 275 | } 276 | ``` 277 | 278 | ```scala 279 | val router5: ActorRef = 280 | context.actorOf(FromConfig.props(Props[Worker]), "router5") 281 | ``` 282 | 在代码中定义的`RandomGroup` 283 | 284 | ```scala 285 | val router6: ActorRef = 286 | context.actorOf(RandomPool(5).props(Props[Worker]), "router6") 287 | ``` 288 | 289 | 在配置文件中定义的`RandomGroup` 290 | 291 | ```scala 292 | akka.actor.deployment { 293 | /parent/router7 { 294 | router = random-group 295 | routees.paths = ["/user/workers/w1", "/user/workers/w2", "/user/workers/w3"] 296 | } 297 | } 298 | ``` 299 | 300 | ```scala 301 | val router7: ActorRef = 302 | context.actorOf(FromConfig.props(), "router7") 303 | ``` 304 | 305 | 在代码中定义的`RandomGroup` 306 | 307 | ```scala 308 | val paths = List("/user/workers/w1", "/user/workers/w2", "/user/workers/w3") 309 | val router8: ActorRef = 310 | context.actorOf(RandomGroup(paths).props(), "router8") 311 | ``` 312 | 313 | ### BalancingPool 314 | 315 | 这个路由器重新分配工作,从繁忙的routees到空闲的routees。所有的routees共享相同的邮箱 316 | 317 | 在配置文件中定义的`BalancingPool` 318 | 319 | ```scala 320 | akka.actor.deployment { 321 | /parent/router9 { 322 | router = balancing-pool 323 | nr-of-instances = 5 324 | } 325 | } 326 | ``` 327 | 328 | ```scala 329 | val router9: ActorRef = 330 | context.actorOf(FromConfig.props(Props[Worker]), "router9") 331 | ``` 332 | 333 | 在代码中定义的`BalancingPool` 334 | 335 | ```scala 336 | val router10: ActorRef = 337 | context.actorOf(BalancingPool(5).props(Props[Worker]), "router10") 338 | ``` 339 | 平衡派发器有额外的配置,这可以被Pool使用,在路由器部署配置的`pool-dispatcher`片段中配置。 340 | 341 | ```scala 342 | akka.actor.deployment { 343 | /parent/router9b { 344 | router = balancing-pool 345 | nr-of-instances = 5 346 | pool-dispatcher { 347 | attempt-teamwork = off 348 | } 349 | } } 350 | ``` 351 | 352 | ### SmallestMailboxPool 353 | 354 | 这个路由器选择未挂起的邮箱中消息数最少的routee。选择顺序如下所示: 355 | 356 | - 选取任何一个空闲的(没有正在处理的消息)邮箱为空的 routee 357 | - 选择任何邮箱为空的routee 358 | - 选择邮箱中等待的消息最少的 routee 359 | - 选择任何一个远程 routee, 由于邮箱大小未知,远程actor被认为具有低优先级 360 | 361 | 定义在配置文件中的`SmallestMailboxPool` 362 | 363 | ```scala 364 | akka.actor.deployment { 365 | /parent/router11 { 366 | router = smallest-mailbox-pool 367 | nr-of-instances = 5 368 | } 369 | } 370 | ``` 371 | 372 | ```scala 373 | val router11: ActorRef = 374 | context.actorOf(FromConfig.props(Props[Worker]), "router11") 375 | ``` 376 | 在代码中定义的`SmallestMailboxPool` 377 | 378 | ```scala 379 | val router12: ActorRef = 380 | context.actorOf(SmallestMailboxPool(5).props(Props[Worker]), "router12") 381 | ``` 382 | 383 | ### BroadcastPool和BroadcastGroup 384 | 385 | 一个广播路由器转发消息到所有的routees 386 | 387 | 定义在配置文件中的`BroadcastPool` 388 | 389 | ```scala 390 | akka.actor.deployment { 391 | /parent/router13 { 392 | router = broadcast-pool 393 | nr-of-instances = 5 394 | } 395 | } 396 | ``` 397 | 398 | ```scala 399 | val router13: ActorRef = 400 | context.actorOf(FromConfig.props(Props[Worker]), "router13") 401 | ``` 402 | 403 | 定义在代码中的`BroadcastPool` 404 | 405 | ```scala 406 | val router14: ActorRef = 407 | context.actorOf(BroadcastPool(5).props(Props[Worker]), "router14") 408 | ``` 409 | 410 | 定义在配置文件中的`BroadcastGroup` 411 | 412 | ```scala 413 | akka.actor.deployment { 414 | /parent/router15 { 415 | router = broadcast-group 416 | routees.paths = ["/user/workers/w1", "/user/workers/w2", "/user/workers/w3"] 417 | } 418 | } 419 | ``` 420 | 421 | ```scala 422 | val router15: ActorRef = 423 | context.actorOf(FromConfig.props(), "router15") 424 | ``` 425 | 426 | 定义在代码中的`BroadcastGroup` 427 | 428 | ```scala 429 | val paths = List("/user/workers/w1", "/user/workers/w2", "/user/workers/w3") 430 | val router16: ActorRef = 431 | context.actorOf(BroadcastGroup(paths).props(), "router16") 432 | ``` 433 | ### ScatterGatherFirstCompletedPool和ScatterGatherFirstCompletedGroup 434 | 435 | ScatterGatherFirstCompletedRouter发送消息到它的每个routees,然后等待返回的第一个回复。这个结果将会返回原始发送者(original sender)。其它回复丢弃。 436 | 437 | 它期待至少一个带有配置时间的回复。否则它将回复一个带有`akka.pattern.AskTimeoutException`的`akka.actor.Status.Failure`。 438 | 439 | 在配置文件中定义的`ScatterGatherFirstCompletedPool` 440 | 441 | ```scala 442 | akka.actor.deployment { 443 | /parent/router17 { 444 | router = scatter-gather-pool 445 | nr-of-instances = 5 446 | within = 10 seconds 447 | } } 448 | ``` 449 | 450 | ```scala 451 | val router17: ActorRef = 452 | context.actorOf(FromConfig.props(Props[Worker]), "router17") 453 | ``` 454 | 455 | 在代码中定义的`ScatterGatherFirstCompletedPool` 456 | 457 | ```scala 458 | val router18: ActorRef = 459 | context.actorOf(ScatterGatherFirstCompletedPool(5, within = 10.seconds). 460 | props(Props[Worker]), "router18") 461 | ``` 462 | 463 | 在配置文件中定义的`ScatterGatherFirstCompletedGroup` 464 | 465 | ```scala 466 | akka.actor.deployment { 467 | /parent/router19 { 468 | router = scatter-gather-group 469 | routees.paths = ["/user/workers/w1", "/user/workers/w2", "/user/workers/w3"] 470 | within = 10 seconds 471 | } } 472 | ``` 473 | 474 | ```scala 475 | val router19: ActorRef = 476 | context.actorOf(FromConfig.props(), "router19") 477 | ``` 478 | 479 | 在代码中定义的`ScatterGatherFirstCompletedGroup` 480 | 481 | ```scala 482 | val paths = List("/user/workers/w1", "/user/workers/w2", "/user/workers/w3") 483 | val router20: ActorRef = 484 | context.actorOf(ScatterGatherFirstCompletedGroup(paths, 485 | within = 10.seconds).props(), "router20") 486 | ``` 487 | 488 | ### TailChoppingPool和TailChoppingGroup 489 | 490 | TailChoppingPool首先发送一个消息给一个随机选择的routee,然后等待一段时间,发送第二个消息给一个随机选择的routee,依此类推。它等待返回的第一个回复,然后讲回复发送给原始发送者。其它回复丢弃。 491 | 492 | 这个路由器的目标是减少通过到多个routees的路由冗余查询而产生的性能延迟,假定其它actors中的某一个比初始化的那个反应速度快。 493 | 494 | 在配置文件中定义的`TailChoppingPool` 495 | 496 | ```scala 497 | akka.actor.deployment { 498 | /parent/router21 { 499 | router = tail-chopping-pool 500 | nr-of-instances = 5 501 | within = 10 seconds 502 | tail-chopping-router.interval = 20 milliseconds 503 | } } 504 | ``` 505 | 506 | ```scala 507 | val router21: ActorRef = 508 | context.actorOf(FromConfig.props(Props[Worker]), "router21") 509 | ``` 510 | 511 | 在代码中定义的`TailChoppingPool` 512 | 513 | ```scala 514 | val router22: ActorRef = 515 | context.actorOf(TailChoppingPool(5, within = 10.seconds, interval = 20.millis). 516 | props(Props[Worker]), "router22") 517 | ``` 518 | 519 | 在配置文件中定义的`TailChoppingGroup` 520 | 521 | ```scala 522 | akka.actor.deployment { 523 | /parent/router23 { 524 | router = tail-chopping-group 525 | routees.paths = ["/user/workers/w1", "/user/workers/w2", "/user/workers/w3"] 526 | within = 10 seconds 527 | tail-chopping-router.interval = 20 milliseconds 528 | } } 529 | ``` 530 | ```scala 531 | val router23: ActorRef = 532 | context.actorOf(FromConfig.props(), "router23") 533 | ``` 534 | 535 | 在代码中定义的`TailChoppingGroup` 536 | 537 | ```scala 538 | val paths = List("/user/workers/w1", "/user/workers/w2", "/user/workers/w3") 539 | val router24: ActorRef = 540 | context.actorOf(TailChoppingGroup(paths, 541 | within = 10.seconds, interval = 20.millis).props(), "router24") 542 | ``` 543 | 544 | ### ConsistentHashingPool 和 ConsistentHashingGroup 545 | 546 | `ConsistentHashingPool`使用[一致性哈希](http://en.wikipedia.org/wiki/Consistent_hashing)选择routee发送消息。这个[文章](http://weblogs.java.net/blog/tomwhite/archive/2007/11/consistent_hash.html)给出了一致性哈希实现的一些好的建议。 547 | 548 | 对于一致性哈希而言有三种方式定义使用哪些数据。 549 | 550 | - 你可以定义路由器的`hashMapping`去映射输入消息到它们的一致性哈希key,这使得决策对发送者(sender)是透明的。 551 | - 消息可能实现`akka.routing.ConsistentHashingRouter.ConsistentHashable`。key是消息的一部分,把它和消息定义在一起是方便的。 552 | - 消息可以包裹在`akka.routing.ConsistentHashingRouter.ConsistentHashableEnvelope`中定义一致性哈希key使用的数据。发送者知道使用的key。 553 | 554 | 定义一致性哈希的这些方式可以一起使用,并且可以在一个路由器上同时使用。首先尝试使用`hashMapping` 555 | 556 | 例子代码: 557 | 558 | ```scala 559 | import akka.actor.Actor 560 | import akka.routing.ConsistentHashingRouter.ConsistentHashable 561 | 562 | class Cache extends Actor { 563 | var cache = Map.empty[String, String] 564 | 565 | def receive = { 566 | case Entry(key, value) => cache += (key -> value) 567 | case Get(key) => sender() ! cache.get(key) 568 | case Evict(key) => cache -= key 569 | } 570 | } 571 | 572 | case class Evict(key: String) 573 | 574 | case class Get(key: String) extends ConsistentHashable { 575 | override def consistentHashKey: Any = key 576 | } 577 | 578 | case class Entry(key: String, value: String) 579 | ``` 580 | 581 | ```scala 582 | import akka.actor.Props 583 | import akka.routing.ConsistentHashingPool 584 | import akka.routing.ConsistentHashingRouter.ConsistentHashMapping 585 | import akka.routing.ConsistentHashingRouter.ConsistentHashableEnvelope 586 | 587 | def hashMapping: ConsistentHashMapping = { 588 | case Evict(key) => key 589 | } 590 | 591 | val cache: ActorRef = 592 | context.actorOf(ConsistentHashingPool(10, hashMapping = hashMapping). 593 | props(Props[Cache]), name = "cache") 594 | 595 | cache ! ConsistentHashableEnvelope( 596 | message = Entry("hello", "HELLO"), hashKey = "hello") 597 | cache ! ConsistentHashableEnvelope( 598 | message = Entry("hi", "HI"), hashKey = "hi") 599 | 600 | cache ! Get("hello") 601 | expectMsg(Some("HELLO")) 602 | 603 | cache ! Get("hi") 604 | expectMsg(Some("HI")) 605 | 606 | cache ! Evict("hi") 607 | cache ! Get("hi") 608 | expectMsg(None) 609 | ``` 610 | 611 | 在上面的例子中,你可以看到`Get`消息实现了`ConsistentHashable`,`Entry`消息包裹在了`ConsistentHashableEnvelope`中。`Evict`消息通过`hashMapping`偏函数处理。 612 | 613 | 定义在配置文件中的`ConsistentHashingPool` 614 | 615 | ```scala 616 | akka.actor.deployment { 617 | /parent/router25 { 618 | router = consistent-hashing-pool 619 | nr-of-instances = 5 620 | virtual-nodes-factor = 10 621 | } 622 | } 623 | ``` 624 | 625 | ```scala 626 | val router25: ActorRef = 627 | context.actorOf(FromConfig.props(Props[Worker]), "router25") 628 | ``` 629 | 630 | 定义在代码中的`ConsistentHashingPool` 631 | 632 | ```scala 633 | val router26: ActorRef = 634 | context.actorOf(ConsistentHashingPool(5).props(Props[Worker]), 635 | "router26") 636 | ``` 637 | 638 | 定义在配置文件中的`ConsistentHashingGroup` 639 | 640 | ```scala 641 | akka.actor.deployment { 642 | /parent/router27 { 643 | router = consistent-hashing-group 644 | routees.paths = ["/user/workers/w1", "/user/workers/w2", "/user/workers/w3"] 645 | virtual-nodes-factor = 10 646 | } 647 | } 648 | ``` 649 | 650 | ```scala 651 | val router27: ActorRef = 652 | context.actorOf(FromConfig.props(), "router27") 653 | ``` 654 | 655 | 定义在代码中的`ConsistentHashingGroup` 656 | 657 | ```scala 658 | val paths = List("/user/workers/w1", "/user/workers/w2", "/user/workers/w3") 659 | val router28: ActorRef = 660 | context.actorOf(ConsistentHashingGroup(paths).props(), "router28") 661 | ``` 662 | 663 | `virtual-nodes-factor`是每个routee 虚拟节点的个数,这些节点用在一致哈希环中使分布更加均匀。 664 | 665 | ## 4 特殊处理的消息 666 | 667 | 大部分发送到路由器actor的消息都会根据路由器的路由逻辑转发。然而有几种类型的消息有特殊的行为。 668 | 669 | 注意,除了`Broadcast`消息,这些特殊的消息仅仅被自包含的路由器actor处理,而不会被`akka.routing.Router`组件处理。 670 | 671 | ### 广播消息 672 | 673 | 一个广播消息可以被用来发送消息到所有个routees。当一个路由器接收到一个广播消息,它将广播这个消息的负载(payload)到所有routees,不管这个路由器如何路由这个消息。 674 | 675 | 下面的例子展示了怎样使用一个广播消息发送一个非常重要的消息到路由器的每个routee。 676 | 677 | ```scala 678 | import akka.routing.Broadcast 679 | router ! Broadcast("Watch out for Davy Jones’ locker") 680 | ``` 681 | 682 | 在这个例子中,路由器接收到广播消息,抽取它的负载“Watch out for Davy Jones’ locker”,然后发送这个负载到所有的routees。 683 | 684 | ### PoisonPill消息 685 | 686 | `PoisonPill`消息对所有的actors(包括路由器)拥有特殊的处理。当任何actor收到一个`PoisonPill`消息,这个actor将被停止。 687 | 688 | ```scala 689 | import akka.actor.PoisonPill 690 | router ! PoisonPill 691 | ``` 692 | 693 | 对于一个正常传递消息到routees的路由器来说,认识到`PoisonPill`消息仅仅被路由器处理是非常重要的。发送到路由器的`PoisonPill`消息不会被转发给routees。 694 | 695 | 然而,发送给路由器的`PoisonPill`消息仍然有可能影响到它的routees。因为它将停止路由器,路由器停止了,它的子actors也会停止。停止子actors是正常的actor行为。路由器将会停止routees。每个子actor将会处理它当前的消息然后停止。这可能导致一些消息没有被处理。 696 | 697 | 如果你希望停止一个路由器以及它的routees,但是希望routees首先处理它们邮箱中的所有消息,你不应该发送一个`PoisonPill`消息到路由器,而应该在`Broadcast`消息内包裹一个`PoisonPill`消息,这样每个routee都会收到`PoisonPill`消息。注意这将停止所有的routees,即使这个routee不是路由器的子actor。 698 | 699 | ```scala 700 | import akka.actor.PoisonPill 701 | import akka.routing.Broadcast 702 | router ! Broadcast(PoisonPill) 703 | ``` 704 | 上面的代码显示,每个routee都将收到`PoisonPill`消息。每个routee都将正常处理消息,最后处理`PoisonPill`消息。这将造成routee停止。当所有的routee停止后,路由器也会自动停止,除非它是动态路由器。 705 | 706 | ### Kill消息 707 | 708 | Kill消息是另一种有特殊处理的消息。 709 | 710 | 当一个Kill消息发送到路由器,路由器将会在内部处理这个消息,不会把它发送到routees。路由器将会抛出一个`ActorKilledException`异常并且失败。然后它将会恢复、重启或者终止,这依赖于它被怎样监视。 711 | 712 | 作为路由器子actor的routees也将悬起,这受到应用于路由器的监督指令的影响。不是路由器子actor的routees不会收到影响。 713 | 714 | ```scala 715 | import akka.actor.Kill 716 | router ! Kill 717 | ``` 718 | 719 | 杀死路由器的所有routees可以将kill消息包裹在广播消息中 720 | 721 | ```scala 722 | import akka.actor.Kill 723 | import akka.routing.Broadcast 724 | router ! Broadcast(Kill) 725 | ``` 726 | 727 | ### 管理消息 728 | 729 | - 发送`akka.routing.GetRoutees`给路由器actor将会使它返回其当前在一个`akka.routing.Routees`消息中使用的routee。 730 | - 发送`akka.routing.AddRoutee`给路由器actor将会添加这个routee到它的routee集合中 731 | - 发送`akka.routing.RemoveRoutee`给路由器将会从routee集合中删除这个routee 732 | - 发送`akka.routing.AdjustPoolSize`给一个Pool路由器actor将会调整包含routees的集合的容量 733 | 734 | 管理消息可能在其它消息之后处理,所以如果你发送`AddRoutee`消息之后马上发送一个普通的消息到路由器,你无法保证当普通消息路由时,routees已经改变了。如果你需要知道哪些改变已经应用了,你可以在`AddRoutee`之后发送`GetRoutees`,当你收到`Routees`的回复之后你就能指定之前的那个改变应用了。 735 | 736 | ## 5 动态调整Pool大小 737 | 738 | 大部分的池被使用,它们有一个固定数量routees。也可以用调整大小策略动态调整routees的数量。 739 | 740 | 在配置文件中定义的可调整大小的Pool 741 | 742 | ```scala 743 | akka.actor.deployment { 744 | /parent/router29 { 745 | router = round-robin-pool 746 | resizer { 747 | lower-bound = 2 748 | upper-bound = 15 749 | messages-per-resize = 100 750 | } } 751 | } 752 | ``` 753 | 754 | ```scala 755 | val router29: ActorRef = 756 | context.actorOf(FromConfig.props(Props[Worker]), "router29") 757 | ``` 758 | 在代码中定义的可调整大小的Pool 759 | 760 | ```scala 761 | val resizer = DefaultResizer(lowerBound = 2, upperBound = 15) 762 | val router30: ActorRef = 763 | context.actorOf(RoundRobinPool(5, Some(resizer)).props(Props[Worker]), 764 | "router30") 765 | ``` 766 | 767 | 如果你在配置文件中定义了`router`,那么这个值会代替任何代码中设置的值。 768 | 769 | ## 6 Akka中的路由是如何设计的 770 | 771 | 表面上路由器看起来像一般的actor,但是它们实际的实现是完全不同的。路由器被设计为非常有效地接收消息然后很快地传递它们到routees。 772 | 773 | 一个一般的actor可以被用来路由消息,但是一个actor单线程的处理可能成为一个瓶颈。路由器可以获得更高的吞吐量,它使用的消息处理管道可以允许并发路由。这可以通过直接嵌套路由器的路由逻辑到`ActorRef`而不是路由器actor来实现。发送到`ActorRef`的消息可以立即路由到routee,完全绕过单线程路由器actor。 774 | 775 | 当然,这样的代价比一般actor实现的路由器的路由代码更加复杂。幸运的是,所有的复杂性对消费者来说是不可见的。 776 | 777 | ## 7 自定义路由器 778 | 779 | 你也可以创建你自己的路由器。 780 | 781 | 下面的例子创建一个路由器复制每个消息到几个目的地。 782 | 783 | 从路由逻辑开始: 784 | 785 | ```scala 786 | import scala.collection.immutable 787 | import scala.concurrent.forkjoin.ThreadLocalRandom 788 | import akka.routing.RoundRobinRoutingLogic 789 | import akka.routing.RoutingLogic 790 | import akka.routing.Routee 791 | import akka.routing.SeveralRoutees 792 | class RedundancyRoutingLogic(nbrCopies: Int) extends RoutingLogic { 793 | val roundRobin = RoundRobinRoutingLogic() 794 | def select(message: Any, routees: immutable.IndexedSeq[Routee]): Routee = { 795 | val targets = (1 to nbrCopies).map(_ => roundRobin.select(message, routees)) 796 | SeveralRoutees(targets) 797 | } 798 | } 799 | ``` 800 | 801 | 在这个例子中,通过重用已经存在的`RoundRobinRoutingLogic`,包裹结果到`SeveralRoutees`实例。为每个消息调用`select`,并且通过轮询方式选择几个目的地。 802 | 803 | 路由逻辑的实现必须是线程安全的,因为它可能会在actor外部使用。 804 | 805 | 一个路由逻辑的单元测试 806 | 807 | ```scala 808 | case class TestRoutee(n: Int) extends Routee { 809 | override def send(message: Any, sender: ActorRef): Unit = () 810 | } 811 | val logic = new RedundancyRoutingLogic(nbrCopies = 3) 812 | val routees = for (n <- 1 to 7) yield TestRoutee(n) 813 | val r1 = logic.select("msg", routees) 814 | r1.asInstanceOf[SeveralRoutees].routees should be( 815 | Vector(TestRoutee(1), TestRoutee(2), TestRoutee(3))) 816 | val r2 = logic.select("msg", routees) 817 | r2.asInstanceOf[SeveralRoutees].routees should be( 818 | Vector(TestRoutee(4), TestRoutee(5), TestRoutee(6))) 819 | val r3 = logic.select("msg", routees) 820 | r3.asInstanceOf[SeveralRoutees].routees should be( 821 | Vector(TestRoutee(7), TestRoutee(1), TestRoutee(2))) 822 | ``` 823 | 824 | 让我们继续把这变成一个自包含、可配置的路由器actor。 825 | 826 | 创建一个继承自`Pool`、`Group`或者`CustomRouterConfig`的类。这个类是路由逻辑的工厂类,保有路由器的配置。 827 | 828 | ```scala 829 | import akka.dispatch.Dispatchers 830 | import akka.routing.Group 831 | import akka.routing.Router 832 | import akka.japi.Util.immutableSeq 833 | import com.typesafe.config.Config 834 | 835 | case class RedundancyGroup(override val paths: immutable.Iterable[String], nbrCopies: Int) extends Group { 836 | 837 | def this(config: Config) = this( 838 | paths = immutableSeq(config.getStringList("routees.paths")), 839 | nbrCopies = config.getInt("nbr-copies")) 840 | 841 | override def createRouter(system: ActorSystem): Router = 842 | new Router(new RedundancyRoutingLogic(nbrCopies)) 843 | 844 | override val routerDispatcher: String = Dispatchers.DefaultDispatcherId 845 | } 846 | ``` 847 | 848 | 这可以作为被Akka支持的路由器actors正确的使用 849 | 850 | ```scala 851 | for (n <- 1 to 10) system.actorOf(Props[Storage], "s" + n) 852 | val paths = for (n <- 1 to 10) yield ("/user/s" + n) 853 | val redundancy1: ActorRef = 854 | system.actorOf(RedundancyGroup(paths, nbrCopies = 3).props(), 855 | name = "redundancy1") 856 | redundancy1 ! "important" 857 | ``` 858 | 859 | 注意我们在`RedundancyGroup`中增加了一个构造函数持有`Config`参数。这使它可以在配置文件中配置。 860 | 861 | ```scala 862 | akka.actor.deployment { 863 | /redundancy2 { 864 | router = "docs.routing.RedundancyGroup" 865 | routees.paths = ["/user/s1", "/user/s2", "/user/s3"] 866 | nbr-copies = 5 867 | } 868 | } 869 | ``` 870 | 871 | 在`router`属性中描述完整的类名。路由器类必须继承自`akka.routing.RouterConfig (Pool, Group or CustomRouterConfig) `,并且拥有一个包含`com.typesafe.config.Config`参数的构造器。 872 | 873 | ```scala 874 | val redundancy2: ActorRef = system.actorOf(FromConfig.props(), 875 | name = "redundancy2") 876 | redundancy2 ! "very important" 877 | ``` 878 | 879 | ## 8 配置派发器 880 | 881 | 为了简单地定义Pool的routees的派发器,你可以在配置文件的部署片段定义派发器。 882 | 883 | ```scala 884 | akka.actor.deployment { 885 | /poolWithDispatcher { 886 | router = random-pool 887 | nr-of-instances = 5 888 | pool-dispatcher { 889 | fork-join-executor.parallelism-min = 5 890 | fork-join-executor.parallelism-max = 5 891 | } 892 | } 893 | } 894 | ``` 895 | 这是为一个Pool提供派发器唯一需要做的事情。 896 | 897 | > *如果你使用一个actor的group路由到它的路径,它将会一直使用在Props中配置的相同的派发器,在actor创建之后,不能修改actor的派发器* 898 | 899 | “head”路由actor, 不能运行在同样的派发器上, 因为它并不处理相同的消息,这个特殊的actor并不使用 Props中配置的派发器, 而是使用从`RouterConfig` 来的 `routerDispatcher` , 它缺省为actor系统的缺省派发器. 所有的标准路由actor都允许在其构造方法或工厂方法中设置这个属性,自定义路由必须以合适的方式自己实现这个方法。 900 | 901 | ```scala 902 | val router: ActorRef = system.actorOf( 903 | // “head” router actor will run on "router-dispatcher" dispatcher 904 | // Worker routees will run on "pool-dispatcher" dispatcher 905 | RandomPool(5, routerDispatcher = "router-dispatcher").props(Props[Worker]), 906 | name = "poolWithDispatcher") 907 | ``` 908 | 909 | > *注:不允许为一个`akka.dispatch.BalancingDispatcherConfigurator`配置`routerDispatcher`,因为特殊路由器的消息不能被其它的消息处理* -------------------------------------------------------------------------------- /actors/testing-actor-systems.md: -------------------------------------------------------------------------------- 1 | # 测试actor系统 2 | 3 | ## 1 Testkit实例 4 | 5 | 这是Ray Roestenburg 在[他的博客](http://roestenburg.agilesquad.com/2011/02/unit-testing-akka-actors-with-testkit_12.html)中的示例代码, 作了改动以兼容 Akka 2.x。 6 | 7 | ```scala 8 | import scala.util.Random 9 | import org.scalatest.BeforeAndAfterAll 10 | import org.scalatest.WordSpecLike 11 | import org.scalatest.Matchers 12 | import com.typesafe.config.ConfigFactory 13 | import akka.actor.Actor 14 | import akka.actor.ActorRef 15 | import akka.actor.ActorSystem 16 | import akka.actor.Props 17 | import akka.testkit.{ TestActors, DefaultTimeout, ImplicitSender, TestKit } 18 | import scala.concurrent.duration._ 19 | import scala.collection.immutable 20 | 21 | //演示TestKit的示例测试 22 | class TestKitUsageSpec 23 | extends TestKit(ActorSystem("TestKitUsageSpec", 24 | ConfigFactory.parseString(TestKitUsageSpec.config))) 25 | with DefaultTimeout with ImplicitSender 26 | with WordSpecLike with Matchers with BeforeAndAfterAll { 27 | import TestKitUsageSpec._ 28 | val echoRef = system.actorOf(TestActors.echoActorProps) 29 | val forwardRef = system.actorOf(Props(classOf[ForwardingActor], testActor)) 30 | val filterRef = system.actorOf(Props(classOf[FilteringActor], testActor)) 31 | val randomHead = Random.nextInt(6) 32 | val randomTail = Random.nextInt(10) 33 | val headList = immutable.Seq().padTo(randomHead, "0") 34 | val tailList = immutable.Seq().padTo(randomTail, "1") 35 | val seqRef =system.actorOf(Props(classOf[SequencingActor], testActor, headList, tailList)) 36 | override def afterAll { 37 | shutdown() 38 | } 39 | 40 | "An EchoActor" should { 41 | "Respond with the same message it receives" in { 42 | within(500 millis) { 43 | echoRef ! "test" 44 | expectMsg("test") 45 | } 46 | } 47 | } 48 | 49 | "A ForwardingActor" should { 50 | "Forward a message it receives" in { 51 | within(500 millis) { 52 | forwardRef ! "test" 53 | expectMsg("test") 54 | } 55 | } 56 | } 57 | 58 | "A FilteringActor" should { 59 | "Filter all messages, except expected messagetypes it receives" in { 60 | var messages = Seq[String]() 61 | within(500 millis) { 62 | filterRef ! "test" 63 | expectMsg("test") 64 | filterRef ! 1 65 | expectNoMsg 66 | filterRef ! "some" 67 | filterRef ! "more" 68 | filterRef ! 1 69 | filterRef ! "text" 70 | filterRef ! 1 71 | receiveWhile(500 millis) { 72 | case msg: String => messages = msg +: messages 73 | } } 74 | messages.length should be(3) 75 | messages.reverse should be(Seq("some", "more", "text")) 76 | } 77 | } 78 | 79 | "A SequencingActor" should { 80 | "receive an interesting message at some point " in { 81 | within(500 millis) { 82 | ignoreMsg { 83 | case msg: String => msg != "something" 84 | } 85 | seqRef ! "something" 86 | expectMsg("something") 87 | ignoreMsg { 88 | case msg: String => msg == "1" 89 | } 90 | expectNoMsg 91 | ignoreNoMsg 92 | } 93 | } 94 | } 95 | } 96 | 97 | object TestKitUsageSpec { 98 | // Define your test specific configuration here 99 | val config = """ 100 | akka { 101 | loglevel = "WARNING" 102 | } """ 103 | 104 | //将所有消息转发给另一个Actor的actor 105 | class ForwardingActor(next: ActorRef) extends Actor { 106 | def receive = { 107 | case msg => next ! msg 108 | } 109 | } 110 | //仅转发一部分消息给另一个Actor的Actor 111 | class FilteringActor(next: ActorRef) extends Actor { 112 | def receive = { 113 | case msg: String => next ! msg 114 | case _ => None 115 | } 116 | } 117 | //此actor发送一个消息序列,此序列具有随机列表作为头,一个关心的值及一个随机列表作为尾 118 | //想法是你想测试接收到的关心的值,而不用处理其它部分 119 | 120 | class SequencingActor(next: ActorRef, head: immutable.Seq[String], 121 | tail: immutable.Seq[String]) extends Actor { 122 | def receive = { 123 | case msg => { 124 | head foreach { next ! _ } 125 | next ! msg 126 | tail foreach { next ! _ } 127 | } 128 | } 129 | } 130 | } 131 | 132 | ``` 133 | 134 | 对于任何软件开发,自动化测试都是开发过程的一个重要组成部分。actor 模型对于代码单元如何划分,它们之间如何交互提供了一种新的视角, 这对如何编写测试也造成了影响。 135 | 136 | Akka 有一个专门的` akka-testkit `模块来支持不同层次上的测试, 很明显共有两个类别: 137 | 138 | - 测试独立的不包括actor模型的代码,即没有多线程的内容;在事件发生的次序方面有完全确定性的行为,没有任何并发考虑, 这在下文中称为`单元测试(Unit Testing)。` 139 | - 测试(多个)包装过的actor,包括多线程调度; 事件的次序没有确定性但由于使用了actor模型,不需要考虑并发,这在下文中被称为`集成测试(Integration Testing)。` 140 | 141 | 当然这两个类型有着不同的粒度, 单元测试通常是白盒测试而集成测试是对完整的actor网络进行的功能测试。 其中重要的区别是并发的考虑是否是测试是一部分。我们提供的工具将在下面的章节中详细介绍。 142 | 143 | ## 2 用TestActorRef做同步单元测试 144 | 145 | 测试Actor 类中的业务逻辑分为两部分: 首先,每个原子操作必须独立动作,然后输入的事件序列必须被正确处理, 即使事件的次序存在一些可能的变化。 前者是单线程单元测试的主要使用场景,而后者可以在集成测试中进行确认。 146 | 147 | 通常,` ActorRef `将实际的` Actor `实例与外界隔离开, 唯一的通信通道是actor的邮箱。 这个限制是单元测试的障碍,所以我们引进了`TestActorRef`。这个特殊类型的引用是专门为测试设计的,它允许以两种方式访问actor: 通过获取实际actor实例的引用,通过调用或查询actor的行为(receive)。 以下每一种方式都有专门的部分介绍。 148 | 149 | ### 获取 Actor 的引用 150 | 151 | 能够访问到实际的 Actor 对象使得所有传统的单元测试方法可以用于测试其中的方法。 获取引用的方法: 152 | 153 | ```scala 154 | import akka.testkit.TestActorRef 155 | val actorRef = TestActorRef[MyActor] 156 | val actor = actorRef.underlyingActor 157 | ``` 158 | 159 | 由于` TestActorRef `是actor类型的高阶类型,它返回实际actor及其正确的静态类型。这之后你就可以像平常一样将你的任何单元测试工具用于你的actor。 160 | 161 | ### 测试有限状态机 162 | 163 | 如果你要测试的actor是一个` FSM`, 你可以使用专门的` TestFSMRef`,它拥有普通` TestActorRef `的所有功能,并且能够访问其内部状态: 164 | 165 | ```scala 166 | import akka.testkit.TestFSMRef 167 | import akka.actor.FSM 168 | import akka.util.duration._ 169 | 170 | val fsm = TestFSMRef(new Actor with FSM[Int, String] { 171 | startWith(1, "") 172 | when(1) { 173 | case Event("go", _) => goto(2) using "go" 174 | } 175 | when(2) { 176 | case Event("back", _) => goto(1) using "back" 177 | } 178 | }) 179 | 180 | assert(fsm.stateName == 1) 181 | assert(fsm.stateData == "") 182 | fsm ! "go" // being a TestActorRef, this runs also on the CallingThreadDispatcher 183 | assert(fsm.stateName == 2) 184 | assert(fsm.stateData == "go") 185 | 186 | fsm.setState(stateName = 1) 187 | assert(fsm.stateName == 1) 188 | 189 | assert(fsm.timerActive_?("test") == false) 190 | fsm.setTimer("test", 12, 10 millis, true) 191 | assert(fsm.timerActive_?("test") == true) 192 | fsm.cancelTimer("test") 193 | assert(fsm.timerActive_?("test") == false) 194 | ``` 195 | 196 | 由于Scala类型推测的限制,只有一个如上所示的工厂方法,所以你可能需要写象` TestFSMRef(new MyFSM) `这样的代码,而不是想象中的类似`ActorRef`的`TestFSMRef[MyFSM]`。上例所示的所有方法都直接访问FSM的状态,不作任何同步;这在使用` CallingThreadDispatcher `(TestFSMRef缺省使用它) 并且没有其它线程参与的情况下是合适的, 但如果你实际上需要处理定时器事件可能会导致意外的情形,因为它们是在` Scheduler `线程中执行的。 197 | 198 | ### 测试Actor的行为 199 | 200 | 当消息派发器调用actor中的逻辑来处理消息时,它实际上是对当前注册到actor的行为进行了应用。行为的初始值是代码中声明的` receive `方法的返回值, 但可以通过对外部消息的响应调用` become `和` unbecome `来改变这个行为。所有这些特性使得actor的行为测试起来不太容易。因此` TestActorRef `提供了一种不同的操作方式来对Actor的测试进行补充: 它支持所有正常的` ActorRef`中的操作。发往actor的消息在当前线程中同步处理,应答像正常一样回送。这个技巧来自下面所介绍的` CallingThreadDispatcher`; 这个派发器被隐式地用于所有实例化为`TestActorRef`的actor。 201 | 202 | ```scala 203 | import akka.testkit.TestActorRef 204 | import scala.concurrent.duration._ 205 | import scala.concurrent.Await 206 | import akka.pattern.ask 207 | val actorRef = TestActorRef(new MyActor) 208 | // hypothetical message stimulating a ’42’ answer 209 | val future = actorRef ? Say42 210 | val Success(result: Int) = future.value.get 211 | result should be(42) 212 | ``` 213 | 214 | 由于` TestActorRef` 是 `LocalActorRef` 的子类,只不过多加了一些特殊功能,所以像监管和重启也能正常工作,但是要知道只要所有的相关的actors都使用` CallingThreadDispatcher`那么所有的执行过程都是严格同步的。 一旦你增加了一些元素,其中包括比较复杂的定时任务,你就离开了单元测试的范畴,因为你必须要重新将异步性纳入考虑范围(在大多数情况下问题在于要等待希望的结果有机会发生)。 215 | 216 | 另一个在单线程测试中被覆盖的特殊点是` receiveTimeout`, 由于包含了它会产生异步的` ReceiveTimeout `消息队列, 因此与同步约定矛盾。 217 | 218 | > *综上所述: TestActorRef 重写了两个成员: 它设置派发器为` CallingThreadDispatcher.global `,设置`receiveTimeout`为None* 219 | 220 | ### 介于两者之间方法 221 | 222 | 如果你希望测试actor的行为,包括热替换,但是不包括消息派发器,也不希望` TestActorRef `吞掉所有抛出的异常, 那么为你准备了另一种模式: 只要使用` TestActorRef `的` receive `方法 , 这将会把消息转发给内部的actor: 223 | 224 | ```scala 225 | import akka.testkit.TestActorRef 226 | val actorRef = TestActorRef(new Actor { 227 | def receive = { 228 | case "hello" => throw new IllegalArgumentException("boom") 229 | } 230 | }) 231 | intercept[IllegalArgumentException] { actorRef.receive("hello") } 232 | ``` 233 | 234 | ### 使用场景 235 | 236 | 当然你也可以根据自己的测试需求来混合使用` TestActorRef `的不同用法: 237 | 238 | - 一个常见的使用场景是在发送测试消息之前设置actor进入某个特定的内部状态 239 | - 另一个场景是发送了测试消息之后确认正确的内部状态转换 240 | 241 | 放心大胆地对各种可能性进行实验,如果你发现了有用的模式,快让Akka论坛知道它!常用操作也许能放进优美的DSL中。 242 | 243 | ## 3 用TestKit进行并发的集成测试 244 | 245 | 当你基本确定你的actor的业务逻辑是正确的, 下一个步骤就是确认它在目标环境中也能正确工作 (如果actor分别都比较简单,可能是因为他们使用了 FSM 模块, 这也可能是第一步)。关于环境的定义当然很大程度上由手上的问题和你打算测试的程度决定, 从功能/集成测试到完整的系统测试。 最简单的步骤包括测试流程(说明测试条件)、要测试的actor和接收应答的actor。大一些的系统将被测的actor替换成一组actor网络,将测试条件应用于不同的切入点,并整理将会从不同的输出位置发送的结果,但基本的原则仍然是测试由一个单独的流程来驱动。 246 | 247 | `TestKit `类包含一组工具来简化这些常用的工作。 248 | 249 | ```scala 250 | import akka.actor.ActorSystem 251 | import akka.actor.Actor 252 | import akka.actor.Props 253 | import akka.testkit.{ TestActors, TestKit, ImplicitSender } 254 | import org.scalatest.WordSpecLike 255 | import org.scalatest.Matchers 256 | import org.scalatest.BeforeAndAfterAll 257 | class MySpec(_system: ActorSystem) extends TestKit(_system) with ImplicitSender 258 | with WordSpecLike with Matchers with BeforeAndAfterAll { 259 | def this() = this(ActorSystem("MySpec")) 260 | override def afterAll { 261 | TestKit.shutdownActorSystem(system) 262 | } 263 | "An Echo actor" must { 264 | "send back messages unchanged" in { 265 | val echo = system.actorOf(TestActors.echoActorProps) 266 | echo ! "hello world" 267 | expectMsg("hello world") 268 | } 269 | } } 270 | ``` 271 | 272 | TestKit 中有一个名为` testActor `的actor作为将要被不同的 `expectMsg...`断言检查的消息的入口,下面会详细介绍这些断言。 当混入了` ImplicitSender` trait后 这个actor在从测试过程中派发消息时将被隐式地用作发送引用。` testActor `也可以被像平常一样发送给其它的actor,通常是订阅成为通知监听器。 有一堆检查方法, 如接收所有匹配某些条件的消息,接收固定的消息序列或类,在某段时间内收不到消息等。 273 | 274 | 记得在测试完成后关闭actor系统 (即使是在测试失败的情况下) 以保证所有的actor-包括测试actor-被停止。 275 | 276 | ### 内置断言 277 | 278 | 上面提到的` expectMsg `并不是唯一的对收到的消息进行断言的方法。 以下是完整的列表: 279 | 280 | - `expectMsg[T](d: Duration, msg: T): T` 281 | 282 | 给定的消息必须在指定的时间内到达;返回此消息 283 | 284 | - `expectMsgPF[T](d: Duration)(pf: PartialFunction[Any, T]): T` 285 | 286 | 在给定的时间内,必须有消息到达,必须为这类消息定义偏函数;返回应用收到消息的偏函数的结果。可以不指定时间段(这时需要一对空的括号),这时使用最深层的[within]()块中的期限。 287 | 288 | - `expectMsgClass[T](d: Duration, c: Class[T]): T` 289 | 290 | 在指定的时间内必须接收到` Class `类型的对象;返回收到的对象。注意它的类型匹配是子类兼容的;如果需要类型是相等的,参考使用单个class参数的` expectMsgAllClassOf`。 291 | 292 | - `expectMsgType[T: Manifest](d: Duration)` 293 | 294 | 在指定的时间内必须收到指定类型 (擦除后)的对象; 返回收到的对象。这个方法基本上与`expectMsgClass(manifest[T].erasure)`等价。 295 | 296 | - `expectMsgAnyOf[T](d: Duration, obj: T*): T` 297 | 298 | 在指定的时间内必须收到一个对象,而且此对象必须与传入的对象引用中的一个相等( 用 == 进行比较); 返回收到的对象。 299 | 300 | - `expectMsgAnyClassOf[T](d: Duration, obj: Class[_ <: T]*): T` 301 | 302 | 在指定的时间内必须收到一个对象,它必须至少是指定的某` Class `对象的实例; 返回收到的对象。 303 | 304 | - `expectMsgAllOf[T](d: Duration, obj: T*): Seq[T]` 305 | 306 | 在指定时间内必须收到与指定的数组中相等数量的对象, 对每个收到的对象,必须至少有一个数组中的对象与它相等(用==进行比较)。 返回收到的整个对象集合。 307 | 308 | - `expectMsgAllClassOf[T](d: Duration, c: Class[_ <: T]*): Seq[T]` 309 | 310 | 在指定时间内必须收到与指定的` Class `数组中相等数量的对象,对数组中的每一个`Class`, 必须至少有一个对象的Class与它相等(用 ==进行比较) (这不是子类兼容的类型检查)。 返回收到的整个对象集合。 311 | 312 | - `expectMsgAllConformingOf[T](d: Duration, c: Class[_ <: T]*): Seq[T]` 313 | 314 | 在指定时间内必须收到与指定的 Class 数组中相等数量的对象,对数组中的每个Class 必须至少有一个对象是这个Class的实例。返回收到的整个对象集合。 315 | 316 | - `expectNoMsg(d: Duration)` 317 | 318 | 在指定时间内不能收到消息。如果在这个方法被调用之前已经收到了消息,并且没有用其它的方法将这些消息从队列中删除,这个断言也会失败。 319 | 320 | - `receiveN(n: Int, d: Duration): Seq[AnyRef]` 321 | 322 | 指定的时间内必须收到n 条消息; 返回收到的消息。 323 | 324 | - `fishForMessage(max: Duration, hint: String)(pf: PartialFunction[Any, Boolean]): Any` 325 | 326 | 只要时间没有用完,并且偏函数匹配消息并返回false就一直接收消息. 返回使偏函数返回true 的消息或抛出异常, 异常中会提供一些提示以供debug使用。 327 | 328 | 除了接收消息的断言,还有一些方法来对消息流提供帮助: 329 | 330 | - `receiveOne(d: Duration): AnyRef` 331 | 332 | 尝试等待给定的时间以等待收到一个消息,如果失败则返回null 。 如果给定的 Duration 是0,这一调用是非阻塞的(轮询模式)。 333 | 334 | - `receiveWhile[T](max: Duration, idle: Duration, messages: Int)(pf: PartialFunction[Any, T]): Seq[T]` 335 | 336 | 只要满足 337 | 338 | - 消息与偏函数匹配 339 | - 指定的时间还没用完 340 | - 在空闲的时间内收到了下一条消息 341 | - 消息数量还没有到上限 342 | 343 | 就收集消息并返回收集到的所有消息。 时间上限缺省值是最深层的within块中剩余的时间,空闲时间缺省为无限 (也就是禁止空闲超时功能)。 期望的消息数量缺省值为` Int.MaxValue`, 也就是不作这个限制。 344 | 345 | - `awaitCond(p: => Boolean, max: Duration, interval: Duration)` 346 | 347 | 每经过` interval `时间就检查一下给定的条件,直到它返回` true `或者` max `时间用完了. 时间间隔缺省为100 ms而最大值缺省为最深层的within块中的剩余时间。 348 | 349 | - `ignoreMsg(pf: PartialFunction[AnyRef, Boolean])` `ignoreNoMsg` 350 | 351 | 内部的` testActor` 包含一个偏函数用来忽略消息: 它只会将与偏函数不匹配或使函数返回false的消息放进队列。 这个函数可以用上面的方法进行设置和重设; 每一次调用都会覆盖之前的函数,而不会迭加。 352 | 353 | 这个功能在你想到忽略正常的消息而只对你指定的一些消息感兴趣时(如测试日志系统时)比较有用。 354 | 355 | ### 预料的异常 356 | 357 | 由于集成测试无法进入参与测试的actor的内部处理流程, 无法直接确认预料中的异常。为了做这件事,只能使用日志系统:将普通的事件处理器替换成` TestEventListener `然后使用` EventFilter `可以对日志信息,包括由于异常产生的日志,做断言: 358 | 359 | ```scala 360 | import akka.testkit.EventFilter 361 | import com.typesafe.config.ConfigFactory 362 | implicit val system = ActorSystem("testsystem", ConfigFactory.parseString(""" 363 | akka.loggers = ["akka.testkit.TestEventListener"] 364 | """)) 365 | try { 366 | val actor = system.actorOf(Props.empty) 367 | EventFilter[ActorKilledException](occurrences = 1) intercept { 368 | actor ! Kill } 369 | } finally { 370 | shutdown(system) 371 | } 372 | ``` 373 | 374 | 如果`occurrences`指定了大小,那么`intercept`将会阻塞直到接收到匹配数量的消息或者超过`akka.test.filter-leeway`中配置的时间。超时时,测试将会失败。 375 | 376 | ### 对定时进行断言 377 | 378 | 功能测试的另一个重要部分与定时器有关:有些事件不能立即发生(如定时器), 另外一些需要在时间期限内发生。 因此所有的进行检查的方法都接受一个时间上限,不论是正面还是负面的结果都应该在这个时间之前获得。时间下限需要在这个检测方法之外进行检查,我们有一个新的工具来管理时间期限: 379 | 380 | ```scala 381 | within([min, ]max) { 382 | ... 383 | } 384 | ``` 385 | 386 | `within`所带的代码块必须在一个介于` min `和` max`之间的`Duration`之前完成, 其中min缺省值为0。将`max `参数与块的启动时间相加得到的时间期限在所有检查方法块内部都可以隐式得获得,如果你没有指定max值,它会从最深层的` within `块继承这个值。 387 | 388 | 应注意如果代码块的最后一条接收消息断言是` expectNoMsg `或 `receiveWhile`, 对` within `的最终检查将被跳过,以避免由于唤醒延迟导致的假正值(false positives)。这意味着虽然其中每一个独立的断言仍然使用时间上限,整个代码块在这种情况下会有长度随机的延迟。 389 | 390 | ```scala 391 | import akka.actor.Props 392 | import akka.util.duration._ 393 | 394 | val worker = system.actorOf(Props[Worker]) 395 | within(200 millis) { 396 | worker ! "some work" 397 | expectMsg("some result") 398 | expectNoMsg // 在剩下的200ms中会阻塞 399 | Thread.sleep(300) // 不会使当前代码块失败 400 | } 401 | ``` 402 | 403 | > *所有的时间都以 System.nanoTime为单位, 它们描述的是墙上时间,而非CPU时间。* 404 | 405 | Ray Roestenburg 写了一篇关于使用 TestKit 的好文:[ http://roestenburg.agilesquad.com/2011/02/unit-testing-akka-actors-with-testkit_12.html](http://roestenburg.agilesquad.com/2011/02/unit-testing-akka-actors-with-testkit_12.html). 完整的示例也可以在第一章找到。 406 | 407 | #### 考虑很慢的测试系统 408 | 409 | 你在跑得飞快的笔记本上使用的超时设置在高负载的Jenkins(或类似的)服务器上通常都会导致错误的测试失败。为了考虑这种情况,所有的时间上限都在内部乘以一个系数,这个系数来自配置文件中的` akka.test.timefactor`, 缺省值为 1。 410 | 411 | 你也可以用`akka.testkit`包对象中的隐式转换来将同样的系数来作用于其它的时限,为Duration添加dilated函数。 412 | 413 | ```scala 414 | import scala.concurrent.duration._ 415 | import akka.testkit._ 416 | 10.milliseconds.dilated 417 | ``` 418 | 419 | ### 用隐式的ActorRef解决冲突 420 | 421 | 如果你希望在基于TestKit的测试的消息发送者为` testActor`, 只需要为你的测试代码混入` ImplicitSender`。 422 | 423 | ```scala 424 | class MySpec(_system: ActorSystem) extends TestKit(_system) with ImplicitSender 425 | with WordSpecLike with Matchers with BeforeAndAfterAll { 426 | ``` 427 | 428 | ### 使用多个探针 Actors 429 | 430 | 如果待测的actor会发送多个消息到不同的目标,在使用TestKit时可能会难以分辨到达` testActor`的消息流。 另一种方法是用它来创建简单的探针actor,将它们插入到消息流中。 为了让这种方法更加强大和方便,我们提供了一个具体实现,称为` TestProbe`。 它的功能可以用下面的小例子说明: 431 | 432 | ```scala 433 | import scala.concurrent.duration._ 434 | import akka.actor._ 435 | import scala.concurrent.Future 436 | class MyDoubleEcho extends Actor { 437 | var dest1: ActorRef = _ 438 | var dest2: ActorRef = _ 439 | def receive = { 440 | case (d1: ActorRef, d2: ActorRef) => 441 | dest1 = d1 442 | dest2 = d2 443 | case x => 444 | dest1 ! x 445 | dest2 ! x 446 | } } 447 | 448 | val probe1 = TestProbe() 449 | val probe2 = TestProbe() 450 | val actor = system.actorOf(Props[MyDoubleEcho]) 451 | actor ! ((probe1.ref, probe2.ref)) 452 | actor ! "hello" 453 | probe1.expectMsg(500 millis, "hello") 454 | probe2.expectMsg(500 millis, "hello") 455 | ``` 456 | 457 | 这里我们用` MyDoubleEcho`来仿真一个待测系统, 它会将输入镜像为两个输出。关联两个测试探针来进行(最简单)行为的确认。 还有一个例子是两个actor A,B, A 发送消息给 B。 为了确认这个消息流,可以插入` TestProbe `作为A的目标, 使用转发功能或下文中的自动导向功能在测试上下文中包含真实的B. 458 | 459 | 还可以为探针配备自定义的断言来使测试代码更简洁清晰: 460 | 461 | ```scala 462 | case class Update(id: Int, value: String) 463 | val probe = new TestProbe(system) { 464 | def expectUpdate(x: Int) = { 465 | expectMsgPF() { 466 | case Update(id, _) if id == x => true 467 | } 468 | sender() ! "ACK" 469 | } 470 | } 471 | ``` 472 | 473 | 这里你拥有完全的灵活性,可以将TestKit 提供的工具与你自己的检测代码混合和匹配,并为它取一个有意义的名字。在实际中你的代码可能比上面的示例要复杂;要充分利用工具! 474 | 475 | #### 对探针收到的消息进行应答 476 | 477 | 探针在可能的条件下,会记录通讯通道以便进行应答: 478 | 479 | ```scala 480 | val probe = TestProbe() 481 | val future = probe.ref ? "hello" 482 | probe.expectMsg(0 millis, "hello") // TestActor runs on CallingThreadDispatcher 483 | probe.reply("world") 484 | assert(future.isCompleted && future.value == Some(Success("world"))) 485 | ``` 486 | 487 | #### 对探针收到的消息进行转发 488 | 489 | 假定一个象征性的actor网络中某目标 actor `dest` 从 actor `source`收到一条消息。 如果你使消息先发往` TestProbe probe `, 你可以在保持网络功能的同时对消息流的容量和时限进行断言: 490 | 491 | ```scala 492 | class Source(target: ActorRef) extends Actor { 493 | def receive = { 494 | case "start" => target ! "work" 495 | } 496 | } 497 | class Destination extends Actor { 498 | def receive = { 499 | case x => // Do something.. 500 | } 501 | } 502 | val probe = TestProbe() 503 | val source = system.actorOf(Props(classOf[Source], probe.ref)) 504 | val dest = system.actorOf(Props[Destination]) 505 | source ! "start" 506 | probe.expectMsg("work") 507 | probe.forward(dest) 508 | ``` 509 | 510 | `dest` actor 将收到同样的消息,就象没有插入探针一样。 511 | 512 | #### 自动导向 513 | 514 | 将收到的消息放进队列以便以后处理,这种方法不错,但要保持测试运行并对其运行过程进行跟踪,你也可以为参与测试的探针(事实上是任何 TestKit)安装一个` AutoPilot(自动导向)`。 自动导向在消息进入检查队列之前启动。 以下代码可以用来转发消息, 例如` A --> Probe --> B`, 只要满足一定的协约。 515 | 516 | ```scala 517 | val probe = TestProbe() 518 | probe.setAutoPilot(new TestActor.AutoPilot { 519 | def run(sender: ActorRef, msg: Any): TestActor.AutoPilot = 520 | msg match { 521 | case "stop" => TestActor.NoAutoPilot 522 | case x => testActor.tell(x, sender); TestActor.KeepRunning } 523 | }) 524 | ``` 525 | 526 | run 方法必须返回包含在`Option`中的`auto-pilot`供下一条消息使用, 设置成` None `表示终止自动导向。 527 | 528 | #### 小心定时器断言 529 | 530 | 在使用测试探针时,`within` 块的行为可能会不那么直观:你需要记住上文所描述的期限仅对每一个探针的局部作用域有效。因此,探针 不会响应别的探针的期限,也不响应包含它的`TestKit`实例的期限: 531 | 532 | ```scala 533 | val probe = TestProbe() 534 | within(1 second) { 535 | probe.expectMsg("hello") 536 | } 537 | ``` 538 | 539 | 这里,`expectMsg`调用将会使用缺省的timeout。 540 | 541 | 542 | ### 测试父子关系 543 | 544 | 一个actor的父actor创建该actor。这造成了两者直接的耦合,使其不可以直接测试。明显地,有三种方法可以提高父子关系的可测性。 545 | 546 | - 当创建一个子actor时,传递一个直接的引用到它的父actor。 547 | - 当创建一个父actor时,告诉父actor如何创建它的子actor。 548 | - 当测试时,创建一个虚构(fabricated)的父actor。 549 | 550 | 例如,你想测试的代码结构如下所示: 551 | 552 | ```scala 553 | class Parent extends Actor { 554 | val child = context.actorOf(Props[Child], "child") 555 | var ponged = false 556 | def receive = { 557 | case "pingit" => child ! "ping" 558 | case "pong" => ponged = true 559 | } } 560 | class Child extends Actor { 561 | def receive = { 562 | case "ping" => context.parent ! "pong" 563 | } 564 | } 565 | ``` 566 | 567 | #### 使用依赖注入 568 | 569 | 第一个选项是避免使用`context.parent`函数。用一个自定义的父actor创建子actor,这个自定义的父actor通过传递一个直接的引用到它的父actor。 570 | 571 | ```scala 572 | class DependentChild(parent: ActorRef) extends Actor { 573 | def receive = { 574 | case "ping" => parent ! "pong" 575 | } 576 | } 577 | ``` 578 | 另一种方法是,你可以告诉父节点怎样创建它的子节点。有两种方式可以做到这件事:给它一个Props对象或者给它一个关注创建子actor的函数。 579 | 580 | ```scala 581 | class DependentParent(childProps: Props) extends Actor { 582 | val child = context.actorOf(childProps, "child") 583 | var ponged = false 584 | def receive = { 585 | case "pingit" => child ! "ping" 586 | case "pong" => ponged = true 587 | } } 588 | class GenericDependentParent(childMaker: ActorRefFactory => ActorRef) extends Actor { 589 | val child = childMaker(context) 590 | var ponged = false 591 | def receive = { 592 | case "pingit" => child ! "ping" 593 | case "pong" => ponged = true 594 | } } 595 | ``` 596 | 597 | 创建Props是直接的,但是创建函数需要像如下的测试代码: 598 | 599 | ```scala 600 | val maker = (_: ActorRefFactory) => probe.ref 601 | val parent = system.actorOf(Props(classOf[GenericDependentParent], maker)) 602 | ``` 603 | 604 | 在你的应用程序中,可能如下面这样: 605 | 606 | ```scala 607 | val maker = (f: ActorRefFactory) => f.actorOf(Props[Child]) 608 | val parent = system.actorOf(Props(classOf[GenericDependentParent], maker)) 609 | ``` 610 | #### 使用一个虚拟的父actor 611 | 612 | 如果你不愿改变一个父actor或者子actor的构造器,你可以在你的测试中创建一个虚拟父actor。但是,这不允许你独立测试父actor。 613 | 614 | ```scala 615 | "A fabricated parent" should { 616 | "test its child responses" in { 617 | val proxy = TestProbe() 618 | val parent = system.actorOf(Props(new Actor { 619 | val child = context.actorOf(Props[Child], "child") 620 | def receive = { 621 | case x if sender == child => proxy.ref forward x 622 | case x =>child forward x 623 | } 624 | })) 625 | proxy.send(parent, "ping") 626 | proxy.expectMsg("pong") 627 | } 628 | } 629 | ``` 630 | 631 | 632 | 633 | 634 | ## 4 CallingThreadDispatcher 635 | 636 | 如上文所述,` CallingThreadDispatcher `在单元测试中非常重要, 但最初它出现是为了在出错的时候能够生成连续的`stacktrace`。 由于这个特殊的派发器将任何消息直接运行在当前线程中,所以消息处理的完整历史信息在调用堆栈上有记录,只要所有的actor都是在这个派发器上运行。 637 | 638 | ### 如何使用它 639 | 640 | 只要象平常一样设置派发器: 641 | 642 | ```scala 643 | import akka.testkit.CallingThreadDispatcher 644 | val ref = system.actorOf(Props[MyActor].withDispatcher(CallingThreadDispatcher.Id)) 645 | ``` 646 | 647 | ### 它是如何运作的 648 | 649 | 在被调用时,` CallingThreadDispatcher `会检查接收消息的actor是否已经在当前线程中了。 这种情况的最简单的例子是actor向自己发送消息。 这时,不能马上对它进行处理,因为这违背了actor模型, 于是这个消息被放进队列,直到actor的当前消息被处理完毕;这样,新消息会被在调用的线程上处理,只是在actor完成其先前的工作之后。 在别的情况下,消息会在当前线程中立即得到处理。 通过这个派发器规划的Future也会立即执行。 650 | 651 | 这种工作方式使` CallingThreadDispatcher `像一个为永远不会因为外部事件而阻塞的actor设计的通用派发器。 652 | 653 | 在有多个线程的情况下,有可能同时有两个使用这个派发器的actor在不同线程中收到消息,它们会竞争actor锁,竞争失败的那个必须等待。 这样我们保持了actor模型,但由于使用了受限的调度我们损失了一些并发性。从这个意义上说,它等同于使用传统的基于互斥的并发。 654 | 655 | 另一个困难是正确地处理挂起和继续: 当actor被挂起时,后续的消息将被放进一个`thread-local`的队列中(和正常情况下使用的队列是同一个)。 但是对resume的调用, 是由一个特定的线程执行的,系统中所有其它的线程可能并没有运行这个特定的actor,这会导致`thread-local`队列无法被它们的本地线程清空。于是,调用` resume `的线程会从所有线程收集所有当前在队列中的消息到自己的队列中,然后进行处理。 656 | 657 | ### 限制 658 | 659 | 如果一个actor发送完消息后由于某种原因(通常是被调用actor所影响)阻塞了, 如果使用这个派发器时显然将导致死锁。这在使用基于` CountDownLatch` 同步的actor测试中很常见: 660 | 661 | ```scala 662 | val latch = new CountDownLatch(1) 663 | actor ! startWorkAfter(latch) // actor will call latch.await() before proceeding 664 | doSomeSetupStuff() 665 | latch.countDown() 666 | ``` 667 | 这个例子将无限挂起,消息处理到达第二行永远到不了第四行,而只有在第四行才能在一个普通的派发器上取消它的阻塞。 668 | 669 | 因此,记住`CallingThreadDispatcher`不是普通派发器的通用用途的替换。另一方面,在它上面允许你的actor用于测试,它是很有用的。因为如果它在机率特别高的条件下都能不死锁,那么在生产环境中也不会。 670 | 671 | > *上面这句话很遗憾并不是一个有力的保证,因为你的代码运行在不同的派发器上时可能直接或间接地改变它的行为。 如果你想要寻找帮助你debug死锁的工具, `CallingThreadDispatcher `在有些错误场合下可能会有用,但要记住它既可能给出错误的正面结果也可能给出错误的负面结果。* 672 | 673 | ### 好处 674 | 675 | 总结下来,`CallingThreadDispatcher`提供了如下特征。 676 | 677 | - 确定地执行单线程测试,同时保持几乎所有的actor语义 678 | - 在异常stacktrace中记录从失败点开始的完整的消息处理历史 679 | - 排除某些类的死锁情景 680 | 681 | ## 5 跟踪Actor调用 682 | 683 | 到目前为止所有的测试工具都针对系统的行为构造断言。 当测试失败时,通常是由你来查找原因,进行修改并进行下一轮测试。这个过程既有debugger支持,又有日志支持,又Akka工具箱提供以下日志选项: 684 | 685 | - 对Actor实例中抛出的异常记录日志 686 | 687 | 相比其它的日志机制,这一条是永远打开的;它的日志级别是` ERROR`。 688 | 689 | - 对某些actor的消息记录日志 690 | 691 | 这是通过在配置文件里添加设置项来打开设置项`akka.actor.debug.receive` ,它使得loggable语句作用于actor的`receive`函数: 692 | 693 | ```scala 694 | import akka.event.LoggingReceive 695 | def receive = LoggingReceive { 696 | case msg => // Do something ... 697 | } 698 | def otherState: Receive = LoggingReceive.withLabel("other") { 699 | case msg => // Do something else ... 700 | } 701 | ``` 702 | - 如果在配置文件中没有给出上面的配置, 这个方法将直接移交给给定的` Receive `函数, 也就是说如果不打开,就没有运行时开销。 703 | 704 | 这个日志功能是与指定的局部标记绑定的,因为将其应用于所有的actor可能不是你所需要的, 如果被用于EventHandler监听器它还可能导致无限循环。 705 | 706 | - 对特殊的消息记录日志 707 | 708 | Actor会自动处理某些特殊消息,如 Kill, PoisonPill等等。打开对这些消息的跟踪只需要设置` akka.actor.debug.autoreceive`, 这对所有actor都有效。 709 | 710 | - 对actor生命周期记录日志 711 | 712 | Actor的创建、启动、重启、开始监控、停止监控和终止可以通过打开` akka.actor.debug.lifecycle `来跟踪; 这也是对所有 actor都有效的。 713 | 714 | 所有这些日志消息都记录在 DEBUG 级别. 总结一下, 你可以用以下配置打开对actor活动的完整日志: 715 | 716 | ```scala 717 | akka { 718 | loglevel = "DEBUG" 719 | actor { 720 | debug { 721 | receive = on 722 | autoreceive = on 723 | lifecycle = on 724 | } } 725 | } 726 | ``` 727 | 728 | 729 | 730 | 731 | 732 | 733 | -------------------------------------------------------------------------------- /actors/typed-actor.md: -------------------------------------------------------------------------------- 1 | # 有类型Actor 2 | 3 | Akka 中的有类型Actor是 Active Objects 模式的一种实现. Smalltalk诞生之时,就已经缺省地将方法调用从同步操作发为异步派发。 4 | 5 | 有类型 Actor 由两 “部分” 组成, 一个公开的接口和一个实现, 如果你有 “企业级” Java的开发经验, 这对你应该非常熟悉。 对普通actor来说,你拥有一个外部API (公开接口的实例) 来将方法调用异步地委托给其实现的私有实例。 6 | 7 | 有类型Actor相对于普通Actor的优势在于有类型Actor拥有静态的契约, 你不需要定义你自己的消息, 它的劣势在于对你能做什么和不能做什么进行了一些限制,你不能使用`become/unbecome`。 8 | 9 | 有类型Actor是使用[JDK Proxies](http://docs.oracle.com/javase/6/docs/api/java/lang/reflect/Proxy.html)实现的,JDK Proxies提供了非常简单的api来拦截方法调用。 10 | 11 | *注意:和普通Akka actor一样,有类型actor也一次处理一个消息。* 12 | 13 | ## 1 什么时候使用有类型actor 14 | 15 | 有类型actor是actor系统和非actor代码的桥梁,因为它允许你在外部写看起来像面向对象的代码。可以把它们想象成门:它们适用于private和public之间的范围,但是你不想在你的房子里面安装很多门。 16 | 17 | 更多一点背景:`TypedActors`很容易被滥用为RPC,而这总所周知具有漏洞。所以,当我们想要构建高可用的并发软件时,`TypedActors`并不是我们编写正确代码第一个想到的东西。它们有它们实用的场景,我们应该小心地使用它们。 18 | 19 | ## 2 工具箱 20 | 21 | 在创建第一个有类型Actor之前,我们先了解一下我们手上可供使用的工具,它的位置在`akka.actor.TypedActor`中。 22 | 23 | ```scala 24 | import akka.actor.TypedActor 25 | //返回有类型actor扩展 26 | val extension = TypedActor(system) //system是一个Actor系统实例 27 | //判断一个引用是否是有类型actor代理 28 | TypedActor(system).isTypedActor(someReference) 29 | //返回一个外部有类型actor代理所代表的Akka actor 30 | TypedActor(system).getActorRefFor(someReference) 31 | //返回当前的ActorContext, 32 | // 此方法仅在一个TypedActor 实现的方法中有效 33 | val c: ActorContext = TypedActor.context 34 | //返回当前有类型actor的外部代理, 35 | //此方法仅在一个TypedActor实现的方法中有效 36 | val s: Squarer = TypedActor.self[Squarer] 37 | //返回一个有类型Actor扩展的上下文实例 38 | //这意味着如果你用它创建其它的有类型actor,它们会成为当前有类型actor的子actor 39 | TypedActor(TypedActor.context) 40 | ``` 41 | *注意:就象不应该暴露Akka actor的this一样, 不要暴露有类型Actor的this, 你应该传递其外部代理引用,它可以在你的有类型Actor中用`TypedActor.self`获得, 这是你的外部标识, 就象`ActorRef`是Akka actor的外部标识一样。* 42 | 43 | ## 3 创建有类型Actor 44 | 45 | 要创建有类型Actor,需要一个或多个接口以及一个实现。 46 | 47 | 我们的示例接口: 48 | 49 | ```scala 50 | trait Squarer { 51 | def squareDontCare(i: Int): Unit //fire-forget 52 | def square(i: Int): Future[Int] //non-blocking send-request-reply 53 | def squareNowPlease(i: Int): Option[Int] //blocking send-request-reply 54 | def squareNow(i: Int): Int //blocking send-request-reply 55 | @throws(classOf[Exception]) //declare it or you will get an UndeclaredThrowableException 56 | def squareTry(i: Int): Int //blocking send-request-reply with possible exception 57 | } 58 | ``` 59 | 60 | 在`SquarerImpl`中实现接口定义的方法: 61 | 62 | ```scala 63 | class SquarerImpl(val name: String) extends Squarer { 64 | def this() = this("default") 65 | def squareDontCare(i: Int): Unit = i * i //Nobody cares :( 66 | def square(i: Int): Future[Int] = Future.successful(i * i) 67 | def squareNowPlease(i: Int): Option[Int] = Some(i * i) 68 | def squareNow(i: Int): Int = i * i 69 | def squareTry(i: Int): Int = throw new Exception("Catch me!") 70 | } 71 | ``` 72 | 创建我们的Squarer有类型actor实例的最简单方法是: 73 | 74 | ```scala 75 | val mySquarer: Squarer = 76 | TypedActor(system).typedActorOf(TypedProps[SquarerImpl]()) 77 | ``` 78 | 第一个类型是代理的类型,第二个类型是实现的类型. 如果要调用某特定的构造方法要这样做: 79 | 80 | ```scala 81 | val otherSquarer: Squarer = 82 | TypedActor(system).typedActorOf(TypedProps(classOf[Squarer], 83 | new SquarerImpl("foo")), "name") 84 | ``` 85 | 86 | 由于你提供了一个 Props, 你可以指定使用哪个派发器, 缺省的超时时间及其它。 87 | 88 | ## 4 方法派发语义 89 | 90 | 方法返回: 91 | 92 | - `Unit` 会以 `fire-and-forget` 语义进行派发, 与` ActorRef.tell`完全一致。 93 | - `akka.dispatch.Future[_]` 会以 `send-request-reply` 语义进行派发, 与 `ActorRef.ask`完全一致。 94 | - `scala.Option[_]`或者`akka.japi.Option` 会以`send-request-reply`语义派发, 但是会阻塞等待应答, 如果在超时时限内没有应答则返回 None , 否则返回包含结果的`scala.Some/akka.japi.Some`。在这个调用中发生的异常将被重新抛出。 95 | - 任何其它类型的值将以`send-request-reply `语义进行派发, 但会阻塞地等待应答, 如果超时会抛出`java.util.concurrent.TimeoutException`,如果发生异常则将异常重新抛出。 96 | 97 | ## 5 消息与不可变性 98 | 99 | 虽然Akka并不强制要求你传给有类型Actor方法的参数类型是不可变的, 我们强烈建议只传递不可变参数。 100 | 101 | ### 单向消息发送 102 | 103 | ```scala 104 | mySquarer.squareDontCare(10) 105 | ``` 106 | 就是这么简单!方法会在另一个线程中异步地调用。 107 | 108 | ### 请求-响应消息发送 109 | 110 | ```scala 111 | val oSquare = mySquarer.squareNowPlease(10) //Option[Int] 112 | ``` 113 | 如果需要,这会阻塞到有类型actor的Props中设置的超时时限。如果超时,会返回None。 114 | 115 | ```scala 116 | val iSquare = mySquarer.squareNow(10) //Int 117 | ``` 118 | 如果需要,这会阻塞到有类型actor的Props中设置的超时时限。如果超时,会抛出`java.util.concurrent.TimeoutException`。 119 | 120 | ### 请求-以future作为响应的消息发送 121 | 122 | ```scala 123 | val fSquare = mySquarer.square(10) //A Future[Int] 124 | ``` 125 | 这个调用是异步的,返回的Future可以用作异步组合。 126 | 127 | ## 6 终止有类型Actor 128 | 129 | 由于有类型actor底层还是Akka actor,所以在不需要的时候要终止它。 130 | 131 | ```scala 132 | TypedActor(system).stop(mySquarer) 133 | ``` 134 | 这将会异步地终止与指定的代理关联的有类型Actor。 135 | 136 | ```scala 137 | TypedActor(system).poisonPill(otherSquarer) 138 | ``` 139 | 140 | 在这个调用之前的所有调用完成后,这将会异步地终止与指定的代理关联的有类型Actor。 141 | 142 | ## 7 有类型Actor监管树 143 | 144 | 你可以通过传入`ActorContext`来获得有类型Actor上下文,所以你可以对它调用typedActorOf(..) 来创建子有类型actor。 145 | 146 | ```scala 147 | //Inside your Typed Actor 148 | val childSquarer: Squarer = 149 | TypedActor(TypedActor.context).typedActorOf(TypedProps[SquarerImpl]()) 150 | //Use "childSquarer" as a Squarer 151 | ``` 152 | 将给的`ActorContext`作为输入参数传人TypedActor.get(. . . ),你也可以在Akka actor中创建子有类型actor。 153 | 154 | ## 8 监管策略 155 | 156 | 通过让你的有类型Actor实现类实现 TypedActor.Supervisor方法 你可以定义用来监管子actor的策略 157 | 158 | ## 9 生命周期回调 159 | 160 | 通过使你的有类型actor实现类实现以下方法: 161 | 162 | - TypedActor.PreStart 163 | - TypedActor.PostStop 164 | - TypedActor.PreRestart 165 | - TypedActor.PostRestart 166 | 167 | 你可以hook进你的有类型actor的整个生命周期。 168 | 169 | ## 10 接收任意消息 170 | 171 | 如果你的有类型actor的实现类扩展了`akka.actor.TypedActor.Receiver`, 所有非方法调用(MethodCall)的消息会被传递到onReceive-方法(MethodCall‘‘s will be passed into the ‘‘onReceive-method)。 172 | 173 | 这使你能够对 DeathWatch的`Terminated-消息`或其它类型的消息进行处理, 例如,与无类型actor进行交互的场合。 174 | 175 | ## 11 代理 176 | 177 | 你可以使用带`TypedProps`和`ActorRef`的`typedActorOf`来将指定的ActorRef代理成一个有类型Actor。这在你需要与远程主机上的有类型Actor通信时会有用, 只需要将 `ActorRef` 传递给`typedActorOf`。 178 | 179 | *注意:目标ActorRef需要能处理`MethodCall`消息。* 180 | 181 | ## 12 查找和远程 182 | 183 | 由于TypedActor的背后仍然是actor,你可以使用`typedActorOf`代理远程节点上的`ActorRefs`。 184 | 185 | ```scala 186 | val typedActor: Foo with Bar =TypedActor(system).typedActorOf(TypedProps[FooBar],actorRefToRemoteActor) 187 | //Use "typedActor" as a FooBar 188 | ``` 189 | ## 13 功能扩充 190 | 191 | 以下是使用traits来为你的有类型actor混入行为的示例: 192 | 193 | ```scala 194 | trait Foo { 195 | def doFoo(times: Int): Unit = println("doFoo(" + times + ")") 196 | } 197 | trait Bar { 198 | def doBar(str: String): Future[String] = 199 | Future.successful(str.toUpperCase) 200 | } 201 | class FooBar extends Foo with Bar 202 | ``` 203 | 204 | ```scala 205 | val awesomeFooBar: Foo with Bar = 206 | TypedActor(system).typedActorOf(TypedProps[FooBar]()) 207 | awesomeFooBar.doFoo(10) 208 | val f = awesomeFooBar.doBar("yes") 209 | TypedActor(system).poisonPill(awesomeFooBar) 210 | ``` 211 | 212 | ## 14 有类型的路由模式 213 | 214 | 有时你想在多个actor之间传播消息,在AKKA中最简单的方式就是使用[路由](),它能够实现一个特定的路由逻辑,如`smallest-mailbox`和`consistent-hashing`等。 215 | 216 | 路由不直接支持有类型actor,但是可以很简单的在一个无类型的路由前用一个有类型的代理来使用它。为了展示这一点,我们创建有类型actor并给它们分配一些随机id,所以,实际上我们知道,路由器将消息发送到了不同的actor。 217 | 218 | ```scala 219 | trait HasName { 220 | def name(): String 221 | } 222 | class Named extends HasName { 223 | import scala.util.Random 224 | private val id = Random.nextInt(1024) 225 | def name(): String = "name-" + id 226 | } 227 | ``` 228 | 为了在这些actor的几个实例间进行轮训,你可以简单的创建一个非类型的路由链,然后将它表示为一个`TypedActor`。这可以工作是因为有类型的actor当然是可以通讯的,这个actor的机制相同。它们将消息转换为`MethodCall`消息在方法间发送(methods calls on them get transformed into message sends of MethodCall messages)。 229 | 230 | ```scala 231 | def namedActor(): HasName = TypedActor(system).typedActorOf(TypedProps[Named]()) 232 | // prepare routees 233 | val routees: List[HasName] = List.fill(5) { namedActor() } 234 | val routeePaths = routees map { r => 235 | TypedActor(system).getActorRefFor(r).path.toStringWithoutAddress 236 | } 237 | // prepare untyped router 238 | val router: ActorRef = system.actorOf(RoundRobinGroup(routeePaths).props()) 239 | // prepare typed proxy, forwarding MethodCall messages to ‘router‘ 240 | val typedRouter: HasName = 241 | TypedActor(system).typedActorOf(TypedProps[Named](), actorRef = router) 242 | println("actor was: " + typedRouter.name()) // name-184 243 | println("actor was: " + typedRouter.name()) // name-753 244 | println("actor was: " + typedRouter.name()) // name-320 245 | println("actor was: " + typedRouter.name()) // name-164 246 | ``` -------------------------------------------------------------------------------- /futures-and-agents/agents.md: -------------------------------------------------------------------------------- 1 | # agents 2 | 3 | Akka中的Agent受到[Clojure agent](http://clojure.org/agents) 的启发。 4 | 5 | Agent 提供对独立的内存位置的异步修改。 Agent在其生命周期中绑定到一个存储位置,对这个存储位置的数据的修改仅允许在一个操作中发生。 对其进行修改的操作是函数,该函数被异步地应用于Agent的状态,其返回值成为Agent的新状态。 Agent的状态应该是不可变的。 6 | 7 | 虽然对Agent的修改是异步的,但是其状态总是可以随时被任何线程 (通过` get `或` apply`)来获得而不需要发送消息。 8 | 9 | Agent是活动的。 对所有agent的更新操作在一个线程池的不同线程中并发执行。在每一个时刻,每一个Agent最多只有一个` send `被执行。 从某个线程派发到agent上的操作的执行次序与其发送的次序一致,但有可能与从其它(线程)源派发来的操作交织在一起。 10 | 11 | 12 | ## 1 创建agents 13 | 14 | 调用`Agent(value)`并传入它的初始值来创建agents。它提供了一个隐式的`ExecutionContext`供使用。这些例子将使用默认全局的那个`ExecutionContext`,但是花费不一样。 15 | 16 | ```scala 17 | import scala.concurrent.ExecutionContext.Implicits.global 18 | import akka.agent.Agent 19 | val agent = Agent(5) 20 | ``` 21 | 22 | ## 2 读取agents的值 23 | 24 | Agent可以用()调用来去引用 (获取Agent的值) : 25 | 26 | ```scala 27 | val result = agent() 28 | ``` 29 | 30 | 或者通过get方法 31 | 32 | ```scala 33 | val result = agent.get 34 | ``` 35 | 对Agent的值的读取不包括任何消息传递,立即执行。所以说虽然Agent的更新是异步的,对它的状态的读取却是同步的。 36 | 37 | ## 3 更新 Agent 38 | 39 | 你可以通过发送一个函数转换当前的值或者仅仅发送一个新值来更新一个agent。这个Agent将会并发的自动采用这个值或者函数。更新是以一种“启动然后忘掉”的方式完成的,唯一的保证是它会被执行。 至于什么时候执行则没有保证。不过从同一个线程发到Agent的操作将被顺序执行。 40 | 41 | ```scala 42 | // send a value, enqueues this change 43 | // of the value of the Agent 44 | agent send 7 45 | // send a function, enqueues this change 46 | // to the value of the Agent 47 | agent send (_ + 1) 48 | agent send (_ * 2) 49 | ``` 50 | 51 | 你也可以在一个独立的线程中派发一个函数来改变内部状态。这样将不使用活动线程池,可以用于长时间运行或阻塞的操作。 相应的方法是`sendOff`。 不管使用` sendOff `还是` send `都会顺序执行。 52 | 53 | ```scala 54 | // the ExecutionContext you want to run the function on 55 | implicit val ec = someExecutionContext() 56 | // sendOff a function 57 | agent sendOff longRunningOrBlockingFunction 58 | ``` 59 | 60 | 所有的`send`方法也有一个相应的`alter`方法返回一个Future. 61 | 62 | ```scala 63 | // alter a value 64 | val f1: Future[Int] = agent alter 7 65 | // alter a function 66 | val f2: Future[Int] = agent alter (_ + 1) 67 | val f3: Future[Int] = agent alter (_ * 2) 68 | ``` 69 | 70 | ```scala 71 | // the ExecutionContext you want to run the function on 72 | implicit val ec = someExecutionContext() 73 | // alterOff a function 74 | val f4: Future[Int] = agent alterOff longRunningOrBlockingFunction 75 | ``` 76 | 77 | ## 4 等待Agent的返回值 78 | 79 | 也可以在所有当前排队的send请求都完成以后读取值,使用 await: 80 | 81 | ```scala 82 | implicit val timeout = Timeout(5 seconds) 83 | val future = agent.future 84 | val result = Await.result(future, timeout.duration) 85 | ``` 86 | 87 | ## 5 一元的用法 88 | 89 | Agent 也支持monadic操作, 这样你就可以用`for-comprehensions`对操作进行组合。 在一元的用法中, 旧的Agent不会变化,而是创建新的Agent。 这就是所谓的‘持久化’。 90 | 91 | ```scala 92 | import scala.concurrent.ExecutionContext.Implicits.global 93 | val agent1 = Agent(3) 94 | val agent2 = Agent(5) 95 | // uses foreach 96 | for (value <- agent1) 97 | println(value) 98 | // uses map 99 | val agent3 = for (value <- agent1) yield value + 1 100 | // or using map directly 101 | val agent4 = agent1 map (_ + 1) 102 | // uses flatMap 103 | val agent5 = for { 104 | value1 <- agent1 105 | value2 <- agent2 106 | } yield value1 + value2 107 | ``` 108 | 109 | ## 6 废弃的事务agent 110 | 111 | 如果在事务中使用Agent,那么它将成为事务的一部分。Agent与Scala STM是集成在一起的,所有在事务中提交的派发操作都直到事务被提交时才执行,在重试或放弃的情况下,Agent的操作将被丢弃。 见下例: 112 | 113 | ```scala 114 | import scala.concurrent.ExecutionContext.Implicits.global 115 | import akka.agent.Agent 116 | import scala.concurrent.duration._ 117 | import scala.concurrent.stm._ 118 | def transfer(from: Agent[Int], to: Agent[Int], amount: Int): Boolean = { 119 | atomic { txn => 120 | if (from.get < amount) false 121 | else { 122 | from send (_ - amount) 123 | to send (_ + amount) 124 | true 125 | } } 126 | } 127 | val from = Agent(100) 128 | val to = Agent(20) 129 | val ok = transfer(from, to, 50) 130 | val fromValue = from.future // -> 50 131 | val toValue = to.future // -> 70 132 | ``` -------------------------------------------------------------------------------- /futures-and-agents/futures.md: -------------------------------------------------------------------------------- 1 | # Futures 2 | 3 | ## 1 简介 4 | 5 | 在` Akka `中, 一个`Future`是用来获取某个并发操作的结果的数据结构。这个操作通常是由` Actor `执行或由` Dispatcher `直接执行的。 这个结果可以以同步(阻塞)或异步(非阻塞)的方式访问。 6 | 7 | ## 2 执行上下文 8 | 9 | 为了运行回调和操作,` Futures `需要有一个` ExecutionContext`,它与` java.util.concurrent.Executor `很相像。如果你在作用域内有一个` ActorSystem `,它会用它缺省的派发器作为` ExecutionContext`,你也可以用` ExecutionContext `伴生对象提供的工厂方法来将` Executors `和` ExecutorServices `进行包裹,或者甚至创建自己的实例。 10 | 11 | ```scala 12 | import akka.dispatch.{ ExecutionContext, Promise } 13 | 14 | implicit val ec = ExecutionContext.fromExecutorService(yourExecutorServiceGoesHere) 15 | 16 | // 用你崭新的 ExecutionContext 干点啥 17 | val f = Promise.successful("foo") 18 | 19 | // 然后在程序/应用的正确的位置关闭 ExecutionContext 20 | ec.shutdown() 21 | ``` 22 | 23 | ### 在actor中 24 | 25 | 每个actor都被配置为在`MessageDispatcher`上运行,这个派发器被当作一个`ExecutionContext`。如果Future调用的性质与actor相匹配或者与actor的活动相兼容,那么可以很容易的通过引人`context.dispatcher`在运行Futures时重用这个派发器。 26 | 27 | ```scala 28 | class A extends Actor { 29 | import context.dispatcher 30 | val f = Future("hello") 31 | def receive = { 32 | // receive omitted ... 33 | } } 34 | ``` 35 | 36 | ## 3 用于 Actor 37 | 38 | 通常有两种方法来从一个` Actor `获取回应: 第一种是发送一个消息 (`actor ! msg`), 这种方法只在发送者是一个` Actor` 时有效,第二种是通过一个` Future`。 39 | 40 | 使用` Actor `的方法来发送消息会返回一个` Future`。 41 | 42 | ```scala 43 | import scala.concurrent.Await 44 | import akka.pattern.ask 45 | import akka.util.Timeout 46 | import scala.concurrent.duration._ 47 | implicit val timeout = Timeout(5 seconds) 48 | val future = actor ? msg // enabled by the “ask” import 49 | val result = Await.result(future, timeout.duration).asInstanceOf[String] 50 | ``` 51 | 52 | 这会导致当前线程被阻塞,并等待` Actor `通过它的应答来 ‘完成’` Future `。但是阻塞会导致性能问题,所以是不推荐的。导致阻塞的操作位于` Await.result `和` Await.ready `中,这样就方便定位阻塞的位置。 对阻塞方式的替代方法会在本文档中进一步讨论。还要注意` Actor `返回的` Future `的类型是` Future[Any] `,这是因为` Actor `是动态的。这也是为什么上例中使用了 asInstanceOf 。 在使用非阻塞方式时,最好使用` mapTo `方法来将` Future `转换到期望的类型: 53 | 54 | ```scala 55 | import scala.concurrent.Future 56 | import akka.pattern.ask 57 | val future: Future[String] = ask(actor, msg).mapTo[String] 58 | ``` 59 | 60 | 如果转换成功,` mapTo `方法会返回一个包含结果的新的` Future`, 如果不成功,则返回` ClassCastException` 。对异常的处理将在本文档进一步讨论。 61 | 62 | 发送一个`Future`的结果到一个Actor,你可以用`pipe`构造: 63 | 64 | ```scala 65 | import akka.pattern.pipe 66 | future pipeTo actor 67 | ``` 68 | 69 | ## 4 直接使用 70 | 71 | Akka中的一个常见用例是在不需要使用Actor的情况下并发地执行计算。 如果你发现你只是为了并行地执行一个计算而创建了一堆 Actor,这并不是一个好方法,下面是一种更好(也更快)的方法: 72 | 73 | ```scala 74 | import scala.concurrent.Await 75 | import scala.concurrent.Future 76 | import scala.concurrent.duration._ 77 | val future = Future { 78 | "Hello" + "World" 79 | } 80 | future foreach println 81 | ``` 82 | 83 | 在上面的代码中,被传递给` Future `的代码块会被缺省的` Dispatcher `所执行, 代码块的返回结果会被用来完成` Future `(在这种情况下,结果是一个字符串: “HelloWorld”)。 与从Actor返回的` Future `不同,这个` Future `拥有正确的类型, 我们还避免了管理` Actor `的开销。 84 | 85 | 你还可以用 Promise 伴生对象创建一个已经完成的 Future, 它可以是成功: 86 | 87 | ```scala 88 | val future = Future.successful("Yay!") 89 | ``` 90 | 或失败的: 91 | 92 | ```scala 93 | val otherFuture = Future.failed[String](new IllegalArgumentException("Bang!")) 94 | ``` 95 | 96 | 也有可能创建一个空的`Promise`,稍后再填充,然后获得相应的`Future`。 97 | 98 | ```scala 99 | val promise = Promise[String]() 100 | val theFuture = promise.future 101 | promise.success("hello") 102 | ``` 103 | 104 | ## 5 函数式 Future 105 | 106 | Akka 的` Future `有一些与Scala集合所使用的非常相似的一元的(monadic)方法。 这使你可以构造出结果可以传递的 ‘管道’ 或 ‘数据流’。 107 | 108 | ### Future是一元的 109 | 110 | 让Future以函数式风格工作的第一个方法是 map。 它需要一个函数来对Future的结果进行处理, 返回一个新的结果。 map方法的返回值是包含新结果的另一个Future: 111 | 112 | ```scala 113 | val f1 = Future { 114 | "Hello" + "World" 115 | } 116 | val f2 = f1 map { x => 117 | x.length } 118 | f2 foreach println 119 | ``` 120 | 121 | 这个例子中我们在` Future `内部连接两个字符串。我们没有等待这个Future结束,而是使用` map `方法来将计算字符串长度的函数作用于它。 现在我们有了新的` Future `,它的最终结果是一个 Int。 当先前的` Future `完成时, 它会应用我们的函数并用其结果来完成第二个` Future `。最终我们得到的结果是 10。先前的` Future `仍然持有字符串“HelloWorld” 不受 map的影响。 122 | 123 | 如果我们只是修改一个 Future,map 方法就够用了。 但如果有2个以上 Future时 map 无法将他们组合到一起: 124 | 125 | ```scala 126 | val f1 = Future { 127 | "Hello" + "World" 128 | } 129 | val f2 = Future.successful(3) 130 | val f3 = f1 map { x => 131 | f2 map { y =>x.length * y } 132 | } 133 | f3 foreach println 134 | ``` 135 | 136 | `f3` 的类型是` Future[Future[Int]] `而不是我们所期望的` Future[Int]`。这时我们需要使用` flatMap `方法: 137 | 138 | ```scala 139 | val f1 = Future { 140 | "Hello" + "World" 141 | } 142 | val f2 = Future.successful(3) 143 | val f3 = f1 flatMap { x => 144 | f2 map { y =>x.length * y } 145 | } 146 | f3 foreach println 147 | ``` 148 | 149 | 使用嵌套的`map`或`flatmap`来组合`Future`有时会变得非常复杂和难以阅读,这时使用Scala的`for comprehensions` 一般会生成可读性更好的代码。 见下一节的示例。 150 | 151 | 如果你需要进行条件筛选,可以使用` filter`: 152 | 153 | ```scala 154 | val future1 = Future.successful(4) 155 | val future2 = future1.filter(_ % 2 == 0) 156 | future2 foreach println 157 | val failedFilter = future1.filter(_ % 2 == 1).recover { 158 | // When filter fails, it will have a java.util.NoSuchElementException 159 | case m: NoSuchElementException => 0 160 | } 161 | failedFilter foreach println 162 | ``` 163 | 164 | ### For Comprehensions 165 | 166 | 由于` Future `拥有` map`,` filter` 和 `flatMap `方法,它可以方便地用于 ‘for comprehension’: 167 | 168 | ```scala 169 | val f = for { 170 | a <- Future(10 / 2) // 10 / 2 = 5 171 | b<-Future(a+1)// 5+1=6 172 | c<-Future(a-1)// 5-1=4 173 | if c > 3 // Future.filter 174 | }yieldb*c// 6*4=24 175 | 176 | // Note that the execution of futures a, b, and c 177 | // are not done in parallel. 178 | f foreach println 179 | ``` 180 | 181 | 做这些事情的时候需要记住的是:虽然看上去上例的部分代码可以并发地运行,`for comprehension`的每一步是顺序执行的。每一步是在单独的线程中运行的,但是相较于将所有的计算在一个单独的 Future中运行并没有太大好处. 只有在先创建好` Future`,然后对其进行组合的情况下才能得到真正的好处。 182 | 183 | ### 组合 Futures 184 | 185 | 上例中的` comprehension `是对` Future`进行组合的例子. 这种方法的常见用例是将多个` Actor`的回应组合成一个单独的计算而不用调用` Await.result `或` Await.ready `来阻塞地获得每一个结果。 先看看使用` Await.result `的例子: 186 | 187 | ```scala 188 | val f1 = ask(actor1, msg1) 189 | val f2 = ask(actor2, msg2) 190 | val a = Await.result(f1, 3 seconds).asInstanceOf[Int] 191 | val b = Await.result(f2, 3 seconds).asInstanceOf[Int] 192 | val f3 = ask(actor3, (a + b)) 193 | val result = Await.result(f3, 3 seconds).asInstanceOf[Int] 194 | ``` 195 | 196 | 这里我们等待前2个` Actor `的结果然后将其发送给第三个 Actor。 我们调用了3次` Await.result `, 导致我们的程序在获得最终结果前阻塞了3次。 现在跟下例比较: 197 | 198 | ```scala 199 | val f1 = ask(actor1, msg1) 200 | val f2 = ask(actor2, msg2) 201 | val f3 = for { 202 | a <- f1.mapTo[Int] 203 | b <- f2.mapTo[Int] 204 | c <- ask(actor3, (a + b)).mapTo[Int] 205 | } yield c 206 | f3 foreach println 207 | ``` 208 | 209 | 这里我们有两个 2 actor各自处理自己的消息。 一旦这2个结果可用了 (注意我们并没有阻塞地等待这些结果), 它们会被加起来发送给第三个 Actor, 这第三个actor回应一个字符串,我们把它赋值给 `result`。 210 | 211 | 当我们知道Actor数量的时候上面的方法就足够了,但是当Actor数量较大时就显得比较笨重。` sequence `和` traverse `两个辅助方法可以帮助处理更复杂的情况。 这两个方法都是用来将` T[Future[A]] `转换为` Future[T[A]]`(`T `是 `Traversable`子类)。 例如: 212 | 213 | ```scala 214 | // oddActor 以类型 List[Future[Int]] 返回从1开始的奇数序列 215 | val listOfFutures = List.fill(100)(akka.pattern.ask(oddActor, GetNext).mapTo[Int]) 216 | // now we have a Future[List[Int]] 217 | val futureList = Future.sequence(listOfFutures) 218 | // 计算奇数的和 219 | val oddSum = futureList.map(_.sum) 220 | oddSum foreach println 221 | ``` 222 | 223 | 现在来解释一下,` Future.sequence `将输入的` List[Future[Int]] `转换为` Future[List[Int]]`。这样我们就可以将` map `直接作用于` List[Int]`, 从而得到List的总和。 224 | 225 | `traverse `方法与` sequence `类似, 但它以` T[A] `和 `A => Future[B] `函数为参数返回一个` Future[T[B]]`,这里的 T 同样也是` Traversable `的子类。 例如, 用` traverse `来计算前100个奇数的和: 226 | 227 | ```scala 228 | val futureList = Future.traverse((1 to 100).toList)(x => Future(x * 2 - 1)) 229 | val oddSum = futureList.map(_.sum) 230 | oddSum foreach println 231 | ``` 232 | 233 | 结果与这个例子是一样的: 234 | 235 | ```scala 236 | val futureList = Future.sequence((1 to 100).toList.map(x => Future(x * 2 - 1))) 237 | val oddSum = futureList.map(_.sum) 238 | oddSum foreach println 239 | ``` 240 | 241 | 但是用` traverse `会快一些,因为它不用创建一个` List[Future[Int]] `临时量。 242 | 243 | 最后我们有一个方法` fold`,它的参数包括一个初始值 , 一个Future序列和一个作用于初始值、Future类型以及返回与初始值相同类型的函数, 它将这个函数异步地应用于future序列的所有元素,它的执行将在最后一个Future完成之后开始。 244 | 245 | ```scala 246 | // Create a sequence of Futures 247 | val futures = for (i <- 1 to 1000) yield Future(i * 2) 248 | val futureSum = Future.fold(futures)(0)(_ + _) 249 | futureSum foreach println 250 | ``` 251 | 252 | 就是这么简单! 253 | 254 | 如果传给 fold 的序列是空的, 它将返回初始值, 在上例中,这个值是0。 有时你并不需要一个初始值,而使用序列中第一个已完成的Future的值作为初始值,你可以使用` reduce`, 它的用法是这样的: 255 | 256 | ```scala 257 | // Create a sequence of Futures 258 | val futures = for (i <- 1 to 1000) yield Future(i * 2) 259 | val futureSum = Future.reduce(futures)(_ + _) 260 | futureSum foreach println 261 | ``` 262 | 263 | 与 fold 一样, 它的执行是在最后一个 Future 完成后异步执行的, 你也可以对这个过程进行并行化:将future分成子序列分别进行reduce,然后对reduce的结果再次reduce。 264 | 265 | 266 | ## 6 回调 267 | 268 | 有时你只需要监听` Future `的完成事件, 对其进行响应,不是创建新的Future,而仅仅是产生副作用。 Akka 为这种情况准备了` onComplete`,` onSuccess `和` onFailure`, 而后两者仅仅是第一项的特例。 269 | 270 | ```scala 271 | future onSuccess { 272 | case "bar" => println("Got my bar alright!") 273 | case x: String => println("Got some random string: " + x) 274 | } 275 | ``` 276 | 277 | ```scala 278 | future onFailure { 279 | case ise: IllegalStateException if ise.getMessage == "OHNOES" => 280 | //OHNOES! We are in deep trouble, do something! 281 | case e: Exception => 282 | //Do something else 283 | } 284 | ``` 285 | 286 | ```scala 287 | future onComplete { 288 | case Success(result) => doSomethingOnSuccess(result) 289 | case Failure(failure) => doSomethingOnFailure(failure) 290 | } 291 | ``` 292 | 293 | ## 7 定义次序 294 | 295 | 由于回调的执行是无序的,而且可能是并发执行的, 当你需要一组有序操作的时候需要一些技巧。但有一个解决办法是使用` andThen`。 它会为指定的回调创建一个新的Future, 这个Future与原先的Future拥有相同的结果, 这样就可以象下例一样定义次序: 296 | 297 | ```scala 298 | val result = Future { loadPage(url) } andThen { 299 | case Failure(exception) => log(exception) 300 | } andThen { 301 | case _ => watchSomeTV() 302 | } 303 | result foreach println 304 | ``` 305 | 306 | ## 8 辅助方法 307 | 308 | `Future fallbackTo` 将两个Futures合并成一个新的Future, 如果第一个Future失败了,它将持有第二个 Future 的成功值。 309 | 310 | ```scala 311 | val future4 = future1 fallbackTo future2 fallbackTo future3 312 | future4 foreach println 313 | ``` 314 | 315 | 你也可以使用`zip`操作将两个Futures组合成一个新的持有二者成功结果的tuple的Future。 316 | 317 | ```scala 318 | val future3 = future1 zip future2 map { case (a, b) => a + " " + b } 319 | future3 foreach println 320 | ``` 321 | 322 | ## 9 异常 323 | 324 | 由于Future的结果是与程序的其它部分并发生成的,因此异常需要作特殊的处理。 不管是Actor还是派发器正在完成此Future, 如果抛出了 Exception ,Future 将持有这个异常而不是一个有效的值。 如果 Future 持有 Exception, 调用 Await.result 将导致此异常被再次抛出从而得到正确的处理。 325 | 326 | 通过返回一个其它的结果来处理 Exception 也是可能的。 使用` recover `方法。 例如: 327 | 328 | ```scala 329 | val future = akka.pattern.ask(actor, msg1) recover { 330 | case e: ArithmeticException => 0 331 | } 332 | future foreach println 333 | ``` 334 | 335 | 在这个例子中,如果actor回应了包含` ArithmeticException`的`akka.actor.Status.Failure` , 我们的 Future 将持有 0 作为结果。 `recover `方法与标准的` try/catch `块非常相似, 可以用这种方式处理多种Exception, 如果其中有没有提到的Exception,这种异常将以没有定义` recover `方法的方式来处理。 336 | 337 | 你也可以使用` recoverWith `方法, 它和` recover `的关系就象` flatMap `与` map`的关系, 用法如下: 338 | 339 | ```scala 340 | val future = akka.pattern.ask(actor, msg1) recoverWith { 341 | case e: ArithmeticException => Future.successful(0) 342 | case foo: IllegalArgumentException => 343 | Future.failed[Int](new IllegalStateException("All br0ken!")) 344 | } 345 | future foreach println 346 | ``` 347 | 348 | ## 10 After 349 | 350 | `akka.pattern.after`使超时之后完成一个带有值或者异常的Future非常容易。 351 | 352 | ```scala 353 | // TODO after is unfortunately shadowed by ScalaTest, fix as part of #3759 354 | // import akka.pattern.after 355 | val delayed = akka.pattern.after(200 millis, using = system.scheduler)(Future.failed( 356 | new IllegalStateException("OHNOES"))) 357 | val future = Future { Thread.sleep(1000); "foo" } 358 | val result = Future firstCompletedOf Seq(future, delayed) 359 | ``` 360 | 361 | 362 | -------------------------------------------------------------------------------- /general/actor-references-paths-and-addresses.md: -------------------------------------------------------------------------------- 1 | # Actor引用,路径和地址 2 | 3 | 本节描述actor如何鉴别身份,在一个可能为分布式的actor系统中如何定位。这与Actor系统的核心概念有关:固有的树形监管结构和在跨多个网络节点的actor之间进行透明通讯。 4 | 5 | ![ActorPath](../imgs/ActorPath.png) 6 | 7 | 以上图片显示了actor系统中最重要的实体,请继续阅读一下信息了解详情。 8 | 9 | ## Actor引用是什么? 10 | 11 | Actor引用是 `ActorRef` 的子类,它的最重要功能是支持向它所代表的actor发送消息。每个actor通过`self`来访问它的标准(本地)引用,在发送给其它actor的消息中也缺省包含这个引用。反过来,在消息处理过程中,actor可以通过`sender`来访问到当前消息的发送者的引用。 12 | 13 | 根据actor系统的配置,支持几种不同的actor引用: 14 | 15 | - 纯本地引用,在配置为不使用网络功能的actor系统中使用。这些actor引用不能在保持其功能的条件下从网络连接上向外传输。 16 | - 支持远程调用的本地引用,在支持同一个jvm中actor引用之间的网络功能的actor系统中使用。为了在发送到其它网络节点后被识别,这些引用包含了协议和远程地址信息。 17 | - 本地actor引用有一个子类是用于路由。 它的逻辑结构与之前的本地引用是一样的,但是向它们发送的消息会被直接重定向到它的子actor。 18 | - 远程actor引用代表可以通过远程通讯访问的actor,i.e. 从别的jvm向他们发送消息时,Akka会透明地对消息进行序列化。 19 | - 有几种特殊的actor引用类型,在实际用途中比较类似本地actor引用: 20 | 21 | - `PromiseActorRef` 表示一个`Promise`,作用是从一个actor返回的响应来完成,它是由 `ActorRef.ask` 来创建的 22 | - `DeadLetterActorRef` DeadLetterActorRef是死信服务的缺省实现,所有接收方被关闭或不存在的消息都在此被重新路由 23 | - `EmptyLocalActorRef` 是查找一个不存在的本地actor路径时返回的:它相当于`DeadLetterActorRef`,但是它保有其路径因此可以在网络上发送,以及与其它相同路径的存活的actor引用进行比较,其中一些存活的actor引用可能在该actor消失之前得到了 24 | 25 | - 然后有一些内部实现,你可能永远不会用上: 26 | 27 | - 有一个actor引用并不表示任何actor,只是作为根actor的伪监管者存在,我们称它为“时空气泡穿梭者”。 28 | - 在actor创建设施启动之前运行的第一个日志服务是一个伪actor引用,它接收日志事件并直接显示到标准输出上;它就是`Logging.StandardOutLogger`。 29 | 30 | ## Actor路径是什么? 31 | 32 | 由于actor是以一种严格的树形结构样式来创建的,沿着子actor到父actor的监管链一直到actor系统的根存在一条唯一的actor名字序列。这个序列可以类比成文件系统中的文件路径,所以我们称它为“路径”。就像在一些真正的文件系统中一样,也存在所谓的“符号链接”,也就是说,一个actor可能通过不同的路径访问到,除了原始路径外,其它的路径都是actor实际的监管祖先链的一部分。这些特性将在下面的内容中介绍。 33 | 34 | 一个actor路径包含一个标识该actor系统的锚点,之后是各路径元素的连接,从根到指定的actor;路径元素是路径经过的actor的名字,以"/"分隔。 35 | 36 | ### Actor路径锚点 37 | 38 | 每一条actor路径都有一个地址组件,描述如何访问到actor的协议和位置,之后是从根到actor所经过的树节点上actor的名字。例如: 39 | 40 | ``` 41 | "akka://my-sys/user/service-a/worker1" //纯本地 42 | "akka.tcp://my-sys@host.example.com:5678/user/service-b" // 远程 43 | ``` 44 | 45 | 在这里, `akka.tcp`是2.3.9版本中缺省的远程协议,其它的协议都是可插拔的。使用UDP的远程host可以通过`akka.udp`访问。对主机和端口的理解(上例中的serv.example.com:5678)决定于所使用的传输机制。 46 | 47 | ### 逻辑Actor路径 48 | 49 | 顺着actor的父监管链一直到根的唯一路径被称为逻辑actor路径。这个路径与actor的创建祖先完全吻合,所以当actor系统的远程调用配置(和配置中路径的地址部分)设置好后它就是完全确定的了。 50 | 51 | ### 物理Actor路径 52 | 53 | 逻辑Actor路径描述一个actor系统内部的功能位置,而基于配置的远程部署意味着一个actor可能在另外一台网络主机上被创建,例如在另一个actor系统中。在这种情况下,从根开始索引actor路径需要访问网络,这是一个昂贵的操作。因此,每一个actor同时还有一条物理路径,从实际的actor对象所在的actor系统的根开始。与其它actor通信时使用物理路径作为发送方引用能够让接收方直接回复到这个actor上,将路由延迟降到最小。 54 | 55 | 一个重要的方面是物理路径决不会跨多个actor系统或跨虚拟机。这意味着一个actor的逻辑路径(监管树)和物理路径(actor部署)可能会偏离(diverge),如果它的祖先被远程监管了。 56 | 57 | ## 如何获得Actor引用? 58 | 59 | 关于actor引用的获取方法分为两类:通过创建actor或者通过对actor的访问(look up)查找。后一种功能又分两种:通过具体的actor路径来创建actor引用、查询逻辑actor树。 60 | 61 | ### 创建Actor 62 | 63 | 一个actor系统通常是在根actor上使用`ActorSystem.actorOf`创建actor,然后使用`ActorContext.actorOf`从创建出的actor中生出actor树来启动的。这些方法返回指向新创建的actor的引用。每个actor都拥有到它的父亲,它自己和它的子actor的引用。这些引用可以与消息一直发送给别的actor,以便接收方直接回复。 64 | 65 | ### 通过具体的路径来查找actor 66 | 67 | 另一种查找actor引用的途径是使用`ActorSystem.actorSelection`方法。selection被用来在发送actor和回复actor之间进行通讯。 68 | 69 | 为了获得一个包含特点actor生命周期的ActorRef,你需要发送一个消息(如Identify消息)到actor,然后用sender()引用从actor而来的返回(use the sender() reference of a reply from the actor)。 70 | 71 | #### 绝对路径 vs 相对路径 72 | 73 | 除了ActorSystem.actorSelection还有一个ActorContext.actorSelection,在任何一个actor实例中可以用context.actorSelection访问。它所返回的actor引用与ActorSystem的返回值非常类似,但它的路径查找是从当前actor开始的,而不是从actor树的根开始。可以用 ".." 路径来访问父actor. 例如,你可以向一个兄弟发送消息: 74 | 75 | ``` 76 | context.actorSelection("../brother") ! msg 77 | ``` 78 | 79 | 当然绝对路径也可以在`context`中使用,例如 80 | 81 | ``` 82 | context.actorSelection("/user/serviceA") ! msg 83 | ``` 84 | 85 | ### 查询逻辑Actor树 86 | 87 | 由于actor系统是一个类似文件系统的树形结构,对actor的匹配与unix shell中支持的一样:你可以将路径(中的一部分)用通配符(«*» 和 «?»)替换来组成对0个或多个实际actor的匹配。由于匹配的结果不是一个单一的actor引用,它拥有一个不同的类型ActorSelection,这个类型不完全支持ActorRef的所有操作。同样,路径选择也可以用`ActorSystem.actorSelection`或`ActorContext.actorSelection`两种方式来获得,并且支持发送消息: 88 | 89 | ``` 90 | context.actorSelection("../*") ! msg 91 | ``` 92 | 会将msg发送给包括当前actor在内的所有兄弟。对于用 actorFor 获取的actor引用,为了进行消息的发送,会对监管树进行遍历。由于在消息到达其接收者的过程中与查询条件匹配的actor集合会发生变化,要监视查询的实时变化是不可能的。如果要做这件事情,通过发送一个请求,收集所有的响应来解决不确定性,提取所有的发送方引用,然后监视所有发现的具体actor。这种处理actor选择的方式会在未来的版本中进行改进。 93 | 94 | ### 总结 actorOf vs. actorSelection vs. actorFor 95 | 96 | ``` 97 | 以上部分所描述的细节可以简要地总结和记忆成: 98 | 99 | - actorOf 永远都只会创建一个新的actor,这个新的actor是actorOf所调用上下文(可以是actor系统中的任意一个actor)的直接子actor 100 | - actorSelection 永远都只是查找到当消息已经被传递时的一个已存在的actor,不会创建新的actor。或者当selection创建时,验证已经存在的actors。 101 | - actorFor (已经废弃)永远都只是查找到一个已存在的actor,不会创建新的actor。 102 | ``` 103 | 104 | ## 与远程部署之间的互操作 105 | 106 | 当一个actor创建一个子actor,actor系统的部署者会决定新的actor是在同一个jvm中或是在其它的节点上。如果是后者,actor的创建会通过网络连接来到另一个jvm中进行,结果是新的actor会进入另一个actor系统。远程系统会将新的actor放在一个专为这种场景所保留的特殊路径下。新的actor的监管者会是一个远程actor引用(代表会触发创建动作的actor)。这时,context.parent(监管者引用)和context.path.parent(actor路径上的父actor)表示的actor是不同的。但是在其监管者中查找这个actor的名称能够在远程节点上找到它,保持其逻辑结构,例如当向另外一个未确定(unresolved)的actor引用发送消息时。 107 | 108 | ![RemoteDeployment](../imgs/RemoteDeployment.png) 109 | 110 | ## 路径中的地址部分用来做什么? 111 | 112 | 在网络上传送actor引用时,是用它的路径来表示这个actor的。所以,它的路径必须包括能够用来向它所代表的actor发送消息的完整的信息。这一点是通过在路径字符串的地址部分包括协议、主机名和端口来做到的。当actor系统从远程节点接收到一个actor路径,会检查它的地址部分是否与自己的地址相同,如果相同,那么会将这条路径解析为本地actor引用,否则解析为一个远程actor引用。 113 | 114 | ## Akka使用的特殊路径 115 | 116 | 在路径树的根上是根监管者,所有的的actor都可以从通过它找到。在下一个层次上是以下这些: 117 | 118 | - `"/user"` 是所有由用户创建的顶级actor的监管者,用 ActorSystem.actorOf 创建的actor 119 | - `"/system"` 是所有由系统创建的顶级actor(如日志监听器或由配置指定在actor系统启动时自动部署的actor)的监管者 120 | - `"/deadLetters"` 是死信actor,所有发往已经终止或不存在的actor的消息会被送到这里 121 | - `"/temp"` 是所有系统创建的短时actor(i.e.那些用在ActorRef.ask的实现中的actor)的监管者。`ActorRef.ask`的实现会用到 122 | - `"/remote"` 是一个人造的路径,用来存放所有其监管者是远程actor引用的actor -------------------------------------------------------------------------------- /general/actor-systems.md: -------------------------------------------------------------------------------- 1 | # actor 系统 2 | 3 | Actor是封装状态和行为的对象,他们的唯一通讯方式是交换消息,交换的消息存放在接收方的邮箱里。从某种意义上来说,actor是面向对象的最严格的形式,可以把它们看成一群人:在使用actor来对解决方案建模时,把actor想象成一群人,把子任务分配给他们, 4 | 将他们的功能整理成一个有组织的结构,考虑如何将失败逐级上传。结果是你可以在脑中形成构建软件实现的框架。 5 | 6 | ## 树形结构 7 | 8 | 如一个经济组织一样,actor自然形成树形结构。程序中负责某一个功能的actor可能需要把它的任务分拆成更小的、更易管理的部分。为此它启动子Actor并监管它们。虽然监督机制的细节将在[这里]解释,我们在这一节里将集中讲述其中的基础概念,唯一的前提是我们要知道每个actor有且仅有一个监管者,就是创建它的那个actor。 9 | 10 | Actor 系统的精髓在于任务被分拆开来并委托,直到任务小到可以被完整地进行处理。这样做不仅使任务本身被清晰地划分出结构,而且最终的actor也能按照它们“应该处理的消息类型”,“如何完成正常流程的处理”以及“失败流程应如何处理”来进行解析。如果一个actor对某种状况无法进行处理,它会发送相应的失败消息给它的监管者请求帮助。这样的递归结构使得失败能够在正确的层级进行处理。 11 | 12 | 可以将该方法与分层的软件设计方法进行比较。分层的软件设计方法很容易形成防护性编程,以防止任何失败被泄露出来。把问题交由正确的人处理会是比将所有的事情“藏在深处”更好的解决方案。 13 | 14 | 现在,设计这种系统的难度在于决定谁应该监管什么。这当然没有唯一的最佳方案,但是有一些可能会有帮助的原则: 15 | 16 | - 如果一个actort管理另一个actor所做的工作,如分配一个子任务,那么父actor应该监督子actor,原因是父actor知道可能会出现哪些失败情况,知道如何处理它们。 17 | - 如果一个actor携带着重要数据(如它的状态要尽可能地不被丢失),这个actor应该将任何可能的危险子任务分配给它所监管的子actor,并酌情处理子任务的失败。依据请求的性质,最好的方法可能是为每一个请求创建一个子actor。这样能简化收集回复时的状态管理。这在Erlang中被称为“Error Kernel Pattern”。 18 | - 如果一个actor需要依赖另一个actor才能完成它的任务,这个actor应该观测另一个actor的存活状态并对收到的另一个actor的终止提醒消息进行响应。这与监管机制不同,因为观测方对监管机制没有影响,需要指出的是,仅仅是功能上的依赖并不足以用来决定是否在树形监管体系中添加子actor。 19 | 20 | 当然以上的规则都会有例外,但是无论你遵循这些规则或者打破它们,都需要有足够的理由。 21 | 22 | ## 配置容器 23 | 24 | 多个actor协作的actor系统是管理如日程计划服务、配置文件、日志等共享设施的固有的单元。使用不同配置的多个actor系统可能在同一个jvm中共存,Akka自身没有全局共享的状态。将这与actor系统之间的透明通讯(在同一节点上或者跨网络连接的多个节点)结合,可以看到actor系统本身可以被当作功能层次中的构件模块。 25 | 26 | ## Actor最佳实践 27 | 28 | - Actor们应该被视为非常友好的合作者:高效地完成他们的工作而不会无必要地打扰其它人,也不会争抢资源。转换到编程里这意味着以事件驱动的方式来处理事件并生成响应(或更多的请求)。Actor不应该因为某一个外部实体而阻塞(如占据一个线程又被动等待),这个外部实体可能是一个锁、一个网络socket等等。阻塞操作应该在某些特殊的线程里完成,这个线程发送消息给可处理这些消息的actors。 29 | - 不要在actor之间传递可变对象。为了保证这一点,尽量使用不变量消息。如果actor将他们的可变状态暴露给外界,打破了封装,你又回到了普通的Java并发领域并遭遇所有其缺点。 30 | - Actor是行为和状态的容器,接受这一点意味着不要在消息中传递行为(例如在消息中使用scala闭包)。有一个风险是意外地在actor之间共享了可变状态,而与actor模型的这种冲突将破坏使actor编程成为良好体验的所有属性。 31 | - Top-level actors是你的Error Kernel的最深的部分。应该尽量少的创建它们,更多的使用分级系统。这对相关的错误处理有好处,也可以减少监控actor的负担。这个actor的过度使用将造成单点竞争。 32 | 33 | ## 阻塞需要小心管理 34 | 35 | 在某些情况下,阻塞操作是不可避免的。例如,一个线程睡眠一个不确定的时间,用来等待外部事件的发生。面对这的时候,你可能仅仅将阻塞调用封装到Future中,然后用Future工作,但是这个策略太简单:你很可能遭遇瓶颈或者超过内存或者应用程序的线程过多。 36 | 37 | 阻塞问题适当的解决方案的非详细描述有如下几点: 38 | 39 | - 在一个actor内部作阻塞调用,确保配置一个线程池,这个线程池要么专注于该操作,要么拥有足够的大小。 40 | - 在一个Future中作阻塞调用,确保在某个时间点,该调用的次数要有个上限。 41 | - 在一个Future中作阻塞调用,提供一个线程数量有上限限制的线程池,这个线程池与应用程序运行的计算机的硬件相适应。 42 | - 提供一个单独的线程管理阻塞的资源集合。当阻塞发生时,分配这些事件为actor消息。 43 | 44 | 45 | 46 | ## 你不应该担心的事 47 | 48 | 一个actor系统管理它所配置使用的资源,运行它所包含的actor。 在一个系统中可能有上百万个actor,不用担心,内存一定是够用的,因为每个actor实例仅占差不多300个字节。自然情况下,一个大系统中消息的处理顺序是不受应用的开发者控制的,但这并不是有意为之。让Akka去做幕后的繁重事务吧。 49 | -------------------------------------------------------------------------------- /general/akka-and-the-java-memory-model.md: -------------------------------------------------------------------------------- 1 | # Akka和Java内存模型 2 | 3 | 使用包含Scala和Akka在内的Typesafe Stack的主要的好处是它简化了编写并发软件的过程。本文将讨论Typesafe Stack,尤其是Akka是如何在并发应用中访问共享内存的。 4 | 5 | ## Java内存模型 6 | 7 | 在Java 5之前,Java内存模型(JMM)是混乱的。当多个线程访问共享内存时很可能得到各种奇怪的结果,例如: 8 | 9 | - 一个线程看不到其它线程所写入的值:可见性问题 10 | - 由于指令没有按期望的顺序执行,一个线程观察到其它线程的 ‘不可能’ 行为:指令乱序问题 11 | 12 | 随着Java 5中JSR 133的实现,很多这种问题都被解决了。 JMM是一组基于 “发生在先” 关系的规则, 限制了内存访问行为何时必须在另一个内存访问行为之前发生,以及反过来,它们何时能够不按顺序发生。这些规则的两个例子包括: 13 | 14 | - 监视器锁规则: 对一个锁的释放先于所有后续对同一个锁的获取 15 | - volatile变量规则: 对一个volatile变量的写操作先于所有对同一volatile变量的后续读操作 16 | 17 | volatile变量规则: 对一个volatile变量的写操作先于所有对同一volatile变量的后续读操作 18 | 19 | ## Actors 与 Java 内存模型 20 | 21 | 使用Akka中的Actor实现,有两种方法让多个线程对共享的内存进行操作: 22 | 23 | - 如果一条消息被(例如,从另一个actor)发送到一个actor,大多数情况下消息是不可变的,但是如果这条消息不是一个正确创建的不可变对象,如果没有 “发生先于” 规则, 有可能接收方会看到部分初始化的数据,甚至可能看到无中生有的数据(long/double)。 24 | - 如果一个actor在处理消息时改变了自己的内部状态,而之后又以处理其它消息时访问了这个状态。而重要的是在使用actor模型时你无法保证同一个线程在处理不同的消息时使用同一个actor。 25 | 26 | 为了避免actor中的可见性和乱序问题,Akka保证以下两条 “发生在先” 规则: 27 | 28 | - actor发送规则 : 一条消息的发送动作先于同一个actor对同一条消息的接收 29 | - actor后续处理规则: 一条消息的处理先于同一个actor的下一条消息处理 30 | 31 | 这两条规则都只应用于同一个actor实例,如何使用不同的actor则无效。 32 | 33 | ## Futures 与 Java内存模型 34 | 35 | 一个Future的完成 “先于” 任何注册到它的回调函数的执行。 36 | 37 | 我们建议不要捕捉(close over)非final的值 (Java中称final,Scala中称val), 如果你 一定 要捕捉非final的值, 它们必须被标记为 volatile 来让它的当前值对回调代码可见。 38 | 39 | 如果你捕捉一个引用, 你还必须保证它所指代的实例是线程安全的。 我们强烈建议远离使用锁的对象,因为它们会引入性能问题,甚至可能造成死锁。 这些是使用synchronized的风险。 40 | 41 | ## STM 与 Java内存模型 42 | 43 | Akka中的软件事务性内存 (STM) 也提供了一条 “发生在先” 规则: 44 | 45 | - 事务性引用规则: 在提交过程中对一个事务性引用的成功的写操作先于所有对同一事务性引用的后续读操作发生。 46 | 47 | 这条规则非常象JMM中的 ‘volatile 变量’ 规则. 目前Akka STM只支持延迟写,所以对共享内存的实际写操作会被延迟到事务提交之时。事务中的写操作被存放在一个本地缓冲区中 (事务的写操作集) ,对其它的事务是不可见的。这就是为什么脏读是不可能的。 48 | 49 | 这些规则在Akka中的实现会随时间而变化,精确的细节甚至可能依赖于所使用的配置。但是它们是建立在其它的JMM规则如监视器锁规则和volatile变量规则基础上的。 这意味着Akka用户不需要操心为了提供“发生先于”关系而增加同步,因为这是Akka的工作。这样你可以腾出手来处理你的业务逻辑,让Akka框架来保证这些规则的满足。 50 | 51 | ## Actor 与 共享的可变状态 52 | 因为Akka运行在JVM上,所以还有一些其它的规则需要遵守。 53 | 54 | - 捕捉Actor内部状态并暴露给其它线程 55 | 56 | ```scala 57 | class MyActor extends Actor { 58 | var state = ... 59 | def receive = { 60 | case _ => 61 | //错误的做法 62 | 63 | // 非常错误,共享和可变状态, 64 | // 会让应用莫名其妙地崩溃 65 | Future { state = NewState } 66 | anotherActor ? message onSuccess { r => state = r } 67 | 68 | // 非常错误, "发送者" 随每个消息改变 69 | // 共享可变状态 bug 70 | Future { expensiveCalculation(sender()) } 71 | 72 | //正确的做法 73 | 74 | // 非常安全, "self" 被捕捉是安全的 75 | // 并且它是一个Actor引用, 是线程安全的 76 | Future { expensiveCalculation() } onComplete { f => self ! f.value.get } 77 | 78 | // 非常安全,我们捕捉了一个固定值 79 | // 并且它是一个Actor引用,是线程安全的 80 | val currentSender = sender() 81 | Future { expensiveCalculation(currentSender) } 82 | } 83 | } 84 | ``` 85 | - 消息应当是不可变的, 这是为了避开共享可变状态的陷阱。 86 | -------------------------------------------------------------------------------- /general/configuration.md: -------------------------------------------------------------------------------- 1 | # 配置 2 | 3 | 你可以在不定义任何配置的情况下使用Akka,这是因为Akka提供了合适的默认值。你可能需要修设置从而改变默认的行为或者适应一个特定的运行时环境。你可能需要改变的设置有如下几点: 4 | 5 | - 日志等级和日志备份 6 | - 开启远程访问功能 7 | - 消息序列化 8 | - 定义路由器 9 | - 调度器调优 10 | 11 | Akka 使用`Typesafe Config`库, 不管使用或不使用Akka也可以使用它来配置你自己的项目。这个库是用java写的,没有外部依赖;你最好看看它的文档 (特别是`ConfigFactory`), 以下内容是对这个文档的概括。 12 | 13 | ## 配置数据从哪里来? 14 | 15 | Akka的所有配置信息装在`ActorSystem`的实例中, 或者换个说法, 从外界看来, `ActorSystem`是配置信息的唯一消费者. 在构造一个actor系统时,你可以传进来一个`Config object`,如果不传,就相当于传进来`ConfigFactory.load()` (使用正确的`classloader`)。这意味着将会读取classpath根目录下的所有`application.conf`, `application.json` 和`application.properties`这些文件。 然后actor系统会合并classpath根目录下的`reference.conf`来组成其内部使用的缺省配置。 16 | 17 | ```scala 18 | appConfig.withFallback(ConfigFactory.defaultReference(classLoader)) 19 | ``` 20 | 其中的奥妙是代码不包含缺省值,而是依赖于随库提供的`reference.conf`中的配置。 21 | 22 | 系统属性中覆盖的配置具有最高优先级,见[HOCON 规范](https://github.com/typesafehub/config/blob/master/HOCON.md) (靠近末尾的位置). 要提醒的是应用配置—缺省为 application—可以使用`config.resource`中的属性来覆盖。 23 | 24 | 注意: 25 | 26 | 如果你编写的是一个Akka应用,把配置放在classpath根目录下的`application.conf` 中。如果你编写的是一个基于Akka的库,把配置放在jar包根目录下的`reference.conf`中。 27 | 28 | ## 自定义application.conf 29 | 30 | 一个自定义的`application.conf`可能如下所示: 31 | 32 | ``` 33 | // In this file you can override any option defined in the reference files. 34 | // Copy in parts of the reference files and modify as you please. 35 | akka { 36 | // Loggers to register at boot time (akka.event.Logging$DefaultLogger logs 37 | // to STDOUT) 38 | loggers = ["akka.event.slf4j.Slf4jLogger"] 39 | // Log level used by the configured loggers (see "loggers") as soon 40 | // as they have been started; before that, see "stdout-loglevel" 41 | // Options: OFF, ERROR, WARNING, INFO, DEBUG 42 | loglevel = "DEBUG" 43 | // Log level for the very basic logger activated during ActorSystem startup. 44 | // This logger prints the log messages to stdout (System.out). 45 | // Options: OFF, ERROR, WARNING, INFO, DEBUG 46 | stdout-loglevel = "DEBUG" 47 | actor { 48 | provider = "akka.cluster.ClusterActorRefProvider" 49 | default-dispatcher { 50 | // Throughput for default Dispatcher, set to 1 for as fair as possible 51 | throughput = 10 52 | } } 53 | remote { 54 | // The port clients should connect to. Default is 2552. 55 | netty.tcp.port = 4711 56 | } } 57 | 58 | ``` 59 | 60 | ## 包含文件 61 | 62 | 有时候我们需要包含其它配置文件,例如你有一个 application.conf 定义所有与依赖环境的设置,然后在具体的环境中对其中的设置进行覆盖。 63 | 64 | 通过`-Dconfig.resource=/dev.conf`指定系统属性将会加载`dev.conf`文件,这个文件包含`application.conf` 65 | 66 | ``` 67 | include "application" 68 | akka { 69 | loglevel = "DEBUG" 70 | } 71 | ``` 72 | 更高级的包含和替换机制在[HOCON](https://github.com/typesafehub/config/blob/master/HOCON.md)规范中有解释 73 | 74 | ## 配置日志 75 | 76 | 如果系统属性或配置属性`akka.log-config-on-start`设置为`on`, 那么当actor系统启动时整个配置的日志级别为INFO. 这在你不确定使用哪个配置时会有用。 77 | 78 | 如果有疑问,你也可以在用它们构造一个actor系统之前或之后很方便地了解配置对象的内容: 79 | 80 | ``` shell 81 | Welcome to Scala version 2.10.4 (Java HotSpot(TM) 64-Bit Server VM, Java 1.6.0_27). 82 | Type in expressions to have them evaluated. 83 | Type :help for more information. 84 | scala> import com.typesafe.config._ 85 | import com.typesafe.config._ 86 | scala> ConfigFactory.parseString("a.b=12") 87 | res0: com.typesafe.config.Config = Config(SimpleConfigObject({"a" : {"b" : 12}})) 88 | scala> res0.root.render 89 | res1: java.lang.String = 90 | { 91 | # String: 1 92 | "a" : { 93 | # String: 1 94 | "b" : 12 } 95 | } 96 | ``` 97 | 每一条设置之前的注释给出了原有设置的详情信息 (文件和行号) 以及(在参考配置中)可能出现的注释,与参考配置合并并被actor系统解析的设置可以这样显示: 98 | 99 | ```scala 100 | final ActorSystem system = ActorSystem.create(); 101 | System.out.println(system.settings()); 102 | // this is a shortcut for system.settings().config().root().render() 103 | ``` 104 | 105 | ## 关于类加载器 106 | 107 | 在配置文件的某些地方可以指定要被Akka实例化的类的全路径。这是通过Java反射来实现的,会用到类加载器。在应用窗口或OSBi包里正确地使用它并不总是容易的事,目前Akka采取的方式是每个`ActorSystem`实现存有当前线程的上下文类加载器 (如果有的话,否则使用`this.getClass.getClassLoader`的返回值) 并用它来进行所有的反射操作。这意味着把Akka放到启动类路径中会在一些莫名其妙的地方造成`NullPointerException`:这是不被支持的。 108 | 109 | ## 配置多个ActorSystem 110 | 111 | 如果你有超过一个ActorSystem,你可能为每个系统分开进行配置。 112 | 113 | 由于 ConfigFactory.load() 会合并classpath中所有匹配名称的资源, 最简单的方式是利用这一功能在配置树中区分actor系统: 114 | 115 | ```scala 116 | myapp1 { 117 | akka.loglevel = "WARNING" 118 | my.own.setting = 43 119 | } 120 | myapp2 { 121 | akka.loglevel = "ERROR" 122 | app2.setting = "appname" 123 | } 124 | my.own.setting = 42 125 | my.other.setting = "hello" 126 | ``` 127 | 128 | ```scala 129 | val config = ConfigFactory.load() 130 | val app1 = ActorSystem("MyApp1", config.getConfig("myapp1").withFallback(config)) 131 | val app2 = ActorSystem("MyApp2",config.getConfig("myapp2").withOnlyPath("akka").withFallback(config)) 132 | ``` 133 | 134 | 这两个例子演示了“提升子树”(lift-a-subtree)技巧的不同变种: 第一种中,actor system获得的配置是 135 | 136 | ```scala 137 | akka.loglevel = "WARNING" 138 | my.own.setting = 43 139 | my.other.setting = "hello" 140 | // plus myapp1 and myapp2 subtrees 141 | ``` 142 | 143 | 而在第二种中,只有 “akka” 子树被提升了,结果如下: 144 | 145 | ```scala 146 | akka.loglevel = "ERROR" 147 | my.own.setting = 42 148 | my.other.setting = "hello" 149 | // plus myapp1 and myapp2 subtrees 150 | ``` 151 | 152 | *注意:这个配置文件库非常强大,这里不可能解释所有的功能. 特别是如何在配置文件中引用其它的配置文件 (例子见 包含文件) 和通过路径替换来复制部分配置信。* 153 | 154 | 初始化ActorSystem时,你也可以通过代码来指定和处理配置信息。 155 | 156 | ```scala 157 | import akka.actor.ActorSystem 158 | import com.typesafe.config.ConfigFactory 159 | val customConf = ConfigFactory.parseString(""" 160 | akka.actor.deployment { 161 | /my-service { 162 | router = round-robin 163 | nr-of-instances = 3 164 | } 165 | } 166 | """) 167 | // ConfigFactory.load sandwiches customConfig between default reference 168 | // config and default overrides, and then resolves it. 169 | val system = ActorSystem("MySystem", ConfigFactory.load(customConf)) 170 | ``` 171 | -------------------------------------------------------------------------------- /general/location-transparency.md: -------------------------------------------------------------------------------- 1 | # 位置透明性 2 | 3 | 上一节讲到了如何使用actor路径来实现位置透明性。这个特殊的功能需要额外的解释,因为“透明远程调用”在不同的上下文中(编程语言,平台,技术)有非常不同的用法。 4 | 5 | ## 默认是分布式 6 | 7 | Akka中所有的东西都是被设计成在分布式的环境中工作的:actor之间的所有互操作使用纯粹的消息传递机制,所有的操作都是异步的。付出这些努力是为了保证所有功能无论是在单一的JVM上还是在很多机器的集群里都能同样工作。实现这一点的要点是从远程到本地进行优化而不是从本地到远程进行一般化。参阅这篇[经典论文](http://doc.akka.io/docs/misc/smli_tr-94-29.pdf)来了解关于为什么第二种方式注定要失败的讨论。 8 | 9 | ## 透明性会被破坏的方式 10 | 11 | 由于设计分布式执行对可以做的事情添加了一些限制,Akka所满足的在使用akka的应用程序中并不一定也满足。最明显的一条是网络上发送的所有消息必须是可序列化的。而不那么明显的是这也包括在远程节点上创建actor时用作actor工厂的闭包(在Props里)。 12 | 13 | 另一个结果是所有元素都需要知道所有交互是完全异步的,在一个计算机网络中这可能意味着一个消息需要好几分钟才能到达接收方(跟配置有关)。还意味着消息丢失的概率比在单一的jvm中(接近0,但仍不能完全保证!)高得多。 14 | 15 | ## 远程调用如何使用? 16 | 17 | 我们把透明性的想法限制在“Akka中几乎没有为远程调用层设计的API”:而完全由配置来驱动。你只需要按照之前的章节概括的那些原则来编写你的应用,然后在配置文件里指定远程部署的actor子树。这样你的应用可以不用修改代码而进行扩展。API中唯一允许编程来影响远程部署的部分是Props包含一个属性,这个属性可能被设为一个特定的 Deploy 实例;这与在配置文件中放置同样的部署具有相同的效果(如果两者都有,那么配置文件优先)。 18 | 19 | ## Peer-to-Peer vs. Client-Server 20 | 21 | Akka remoting是一个通讯模型,它通过点到点风格连接actor系统。它是Akka集群的基础。remoting的设计是通过下面两个决定驱动的: 22 | 23 | - 相互通讯的系统是对称的。如果系统A可以连接到系统B,那么系统B也可以连接到系统A 24 | - 通讯的系统关于连接模式的角色是对称的。没有系统仅仅接受连接,没有系统仅仅发起连接 25 | 26 | 这些决定的结果是,不可能安全的通过预先确定的角色(违反条件2)创建纯client-server setups,并让setups处理网络地址转换或者负责均衡(违法条件1) 27 | 28 | ## 使用路由来进行垂直扩展的标记点 29 | 30 | 除了可以在集群中的不同节点上运行一个actor系统的不同部分,还可以通过并行增加actor子树的方法来垂直扩展到多个cpu核上。新增出来的子树可以使用不同的方法来进行路由,如循环。要达到这种效果,开发者只需要声明一个“withRouter”的actor,这样创建出来的actor会生成一些具有所期望的类型的数目可配置的子actor,并使用所配置的方式来对这些子actor进行路由。一旦声明了这样的路由,它的配置可以自由地对配置文件里的配置进行重写,包括把它与其(某些)子actor的远程部署进行混合。 -------------------------------------------------------------------------------- /general/message-delivery-reliability.md: -------------------------------------------------------------------------------- 1 | # 消息发送语义 2 | 3 | -------------------------------------------------------------------------------- /general/supervision-and-monitoring.md: -------------------------------------------------------------------------------- 1 | # 监管和重启 2 | 3 | ## 监管的意思 4 | 5 | 在[ Actor系统](actor-systems.md)中说过,监管描述的是actor之间的依赖关系:监管者将任务委托给下属并对下属的失败状况进行响应。当一个下属出现了失败(即抛出一个异常),它自己会将自己和自己所有的下属挂起然后向自己的监管者发送一个提示失败的消息。依赖所监管的工作的性质和失败的性质,监管者可以有4种基本选择: 6 | 7 | - 让下属继续执行,保持下属当前的内部状态 8 | - 重启下属,清除下属的内部状态 9 | - 永久地终止下属 10 | - 将失败沿监管树向上传递 11 | 12 | 重要的是始终要把一个actor视为整个监管树形体系中的一部分,这解释了第4种选择存在的意义(因为一个监管者同时也是其上方监管者的下属),并且隐含在前3种选择中:让actor继续执行同时也会继续执行它的下属,重启一个actor也必须重启它的下属,相似地终止一个actor会终止它所有的下属。 13 | 需要强调的是一个actor的缺省行为是在重启前终止它的所有下属,这种行为可以用 Actor 类的preRestart hook来重写;对所有子actor的递归重启操作在这个hook之后执行。 14 | 15 | 每个监管者都配置了一个函数,它将所有可能的失败原因归结为以上四种选择之一;注意,这个函数并不将失败actor本身作为输入。我们很快会发现在有些结构中这种方式看起来不够灵活,会希望对不同的下属采取不同的策略。在这一点上我们一定要理解监管是为了组建一个递归的失败处理结构。如果你试图在某一个层次做太多事情,这个层次会变得复杂难以理解,这时我们推荐的方法是增加一个监管层次。 16 | 17 | Akka实现的是一种叫“父监管”的形式。Actor只能由其它的actor创建,而顶部的actor是由库来提供的——每一个创建出来的actor都是由它的父亲所监管。这种限制使得actor的树形层次拥有明确的形式,并提倡合理的设计方法。必须强调的是这也同时保证了actor们不会成为孤儿或者拥有在系统外界的监管者(被外界意外捕获)。这样就产生了一种对actor应用(或其中子树)自然又干净的关闭过程。 18 | 19 | ## Top-Level监管者 20 | 21 | ![guardians](../imgs/guardians.png) 22 | 23 | ### `/user`: 监护人(guardian)Actor 24 | 25 | actor可能和它们的父母actor相互作用得最多,它的监护人命名为 `/user`。利用`system.actorOf()`创建的actors是这个actor的孩子。这意味着,当这个监护人终止,系统中所有正常的actors也关闭了。这也意味这,这个监护人的监管测量决定了top-level actor如何被监控。从 26 | 2.1开始,可以通过`akka.actor.guardian-supervisor-strategy`来配置策略。当监护人升级为一个失败,root监护人的响应将会终止它的监护人,这将导致整个actor系统关闭。 27 | 28 | ### `/system`: 系统监护人 29 | 30 | 这个特定监护人的引人是为了获取一个有序的关闭序列(所有正常的actor终止,但是logging保持可用,即使logging本身也是用actor实现的)。通过系统监护人观察用户监护人并且发起它自己的shut-down(通过收到期望的Terminated消息)。top-level系统actors通过一个策略来监控, 31 | 这个策略将会根据所有类型的Exception(除了ActorInitializationException和ActorKilledException)无限的重启actors。所有的其它异常抛出将会升级,最终关闭整个actor系统。 32 | 33 | ### `/`: Root监护人 34 | 35 | The root guardian is the grand-parent of all so-called “top-level” actors and supervises all the special actors mentioned in Top-Level Scopes for Actor Paths using the SupervisorStrategy.stoppingStrategy, whose purpose is to terminate the child upon any type of Exception. All other throwables will be escalated . . . but to whom? Since every real actor has a supervisor, the supervisor of the root guardian cannot be a real actor. 36 | And because this means that it is “outside of the bubble”, it is called the “bubble-walker”. This is a synthetic ActorRef which in effect stops its child upon the first sign of trouble and sets the actor system’s isTerminated status to true as soon as the root guardian is fully terminated (all children recursively stopped). 37 | 38 | ## 重启的意思 39 | 40 | 当actor在处理消息时出现失败,失败的原因分成以上三类: 41 | 42 | - 对收到的特定消息的系统错误(程序错误) 43 | - 处理消息时一些外部资源的(临时性)失败 44 | - actor内部状态崩溃了 45 | 46 | 以下是重启过程中发生的事件的精确顺序: 47 | 48 | - actor被挂起,并且递归地挂起所有的子actor 49 | - 调用旧实例的preRestart方法 (默认情况是发生终止请求到所有的子actors,并且调用postStop) 50 | - 在preRestart过程中,等待所有被请求终止的子actors终止,最后一个子actor终止后,到下一步 51 | - 再次调用之前提供的actor工厂创建新的actor实例 52 | - 对新实例调用 postRestart 53 | - 给所有在第三步没有终止的子actor发送重启请求。重启子actor将会遵循第二步的相同的递归处理 54 | - 恢复运行新的actor 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /general/what-is-an-actor.md: -------------------------------------------------------------------------------- 1 | # 什么是Actor 2 | 3 | 上一节[Actor系统](actor-systems.md) 解释了actor如何组成一个树形结构,actor是创建一个应用的最小单位。本节单独来看一个actor,解释在实现它时遇到的概念。 4 | 5 | 一个Actor是一个容器,它包含了状态,行为,邮箱,子Actor和一个监管策略。所有这些包含在一个Actor Reference里。 6 | 7 | ## Actor引用 8 | 9 | 一个actor对象需要与外界隔离开才能从actor模型中获益。因此,actor是以actor引用的形式展现给外界的,actor引用可以被自由的无限制地传来传去。内部对象和外部对象的这种划分使得所有想要的操作能够透明:重启actor不需要更新别处的引用、将实际actor对象放置到远程主机上、向另外一个应用程序发送消息。但最重要的方面是外界不可能从actor对象的内部获取它的状态,除非这个actor非常不明智地将信息公布出去。 10 | 11 | ## 状态 12 | 13 | Actor对象通常包含一些变量来反映actor所处的可能状态。这可能是一个明确的状态机(如使用 FSM 模块),或是一个计数器,一组监听器,待处理的请求等。这些数据使得actor有价值,并且必须将这些数据保护起来不被其它的actor所破坏。好消息是在概念上每个Akka actor都有它自己的轻量线程,这个线程是完全与系统其它部分隔离的。这意味着你不需要使用锁来进行资源同步,可以放心地并发性地来编写你的actor代码。 14 | 15 | 在幕后,Akka会在一组线程上运行一组Actor,通常是很多actor共享一个线程,对某一个actor的调用可能会在不同的线程上进行处理。Akka保证这个实现细节不影响处理actor状态的单线程性。 16 | 17 | 由于内部状态对于actor的操作是至关重要的,所以状态不一致是致命的。当actor失败并由其监管者重新启动,状态会进行重新创建,就象第一次创建这个actor一样。这是为了实现系统的“自愈合”。 18 | 19 | 可以视情况而定,一个actor的状态可以自动的恢复到重启之前的状态。这可以通过持久化接收的消息并在重启后替换它们而做到。 20 | 21 | ## 行为 22 | 23 | 每次当一个消息被处理时,消息会与actor的当前的行为进行匹配。行为是一个函数,它定义了处理当前消息所要采取的动作,假如客户已经授权过了,那么就对请求进行处理,否则拒绝请求。这个行为可能随着时间而改变,例如由于不同的客户在不同的时间获得授权,或是由于actor进入了“非服务”模式,之后又变回来。这些变化要么通过将它们放进从行为逻辑中读取的状态变量中实现,要么函数本身在运行时被替换出来,见`become`和`unbecome`操作。但是actor对象在创建时所定义的初始行为是专有的,所以当actor重启时会恢复这个初始行为 24 | 25 | ## 邮箱 26 | 27 | Actor的用途是处理消息,这些消息是从其它的actor(或者从actor系统外部)发送过来的。连接发送者与接收者的纽带是actor的邮箱:每个actor有且仅有一个邮箱,所有的发来的消息都在邮箱里排队。排队按照发送操作的时间顺序来进行,这意味着从不同的actor发来的消息在运行时没有一个固定的顺序,这是由于actor分布在不同的线程中。从另一个角度讲,从同一个actor发送多个消息到相同的actor,则消息会按发送的顺序排队。 28 | 29 | 可以有不同的邮箱实现供选择,缺省的是FIFO:actor处理消息的顺序与消息入队列的顺序一致。这通常是一个好的选择,但是应用可能需要对某些消息进行优先处理。在这种情况下,可以使用优先邮箱来根据消息优先级将消息放在某个指定的位置,甚至可能是队列头,而不是队列末尾。如果使用这样的队列,消息的处理顺序是由队列的算法决定的,而不是FIFO。 30 | 31 | Akka与其它actor模型实现的一个重要差别在于当前的行为必须处理下一个从队列中取出的消息,Akka不会去扫描邮箱来找到下一个匹配的消息。无法处理某个消息通常是作为失败情况进行处理,除非actor覆盖了这个行为。 32 | 33 | ## 子Actor 34 | 35 | 每个actor都是一个潜在的监管者:如果它创建了子actor来委托处理子任务,它会自动地监管它们。子actor列表维护在actor的上下文中,actor可以访问它。对列表的更改是通过创建`context.actorOf(...)`或者停止`context.stop(child)`子actor来实现,并且这些更改会立刻生效。实际的创建和停止操作在幕后以异步的方式完成,这样它们就不会“阻塞”其监管者。 36 | 37 | ## 监管策略 38 | 39 | Actor的最后一部分是它用来处理其子actor错误状况的机制。错误处理是由Akka透明地进行处理的,将[监管与监控]中所描述的策略中的一个应用于每个出现的失败。由于策略是actor系统组织结构的基础,所以一旦actor被创建了它就不能被修改。 40 | 41 | 考虑到每个actor只有唯一的策略,这意味着如果一个actor的子actors应用了不同的策略,这些子actor应该按照相应的策略来进行分组,生成中间的监管者,这再一次地应用到了根据任务到子任务的划分方法来组织actor系统的结构。 42 | 43 | ## 当Actor终止时 44 | 45 | 一旦一个actor终止了,也就是,失败了并且不能用重启来解决,停止它自己或者被它的监管者停止,它会释放它的资源,将它邮箱中所有未处理的消息放进系统的“死信邮箱”。而actor引用中的邮箱将会被一个系统邮箱所替代,系统邮箱会将所有新的消息重定向到“死信邮箱”。 但是这些操作只是尽力而为,所以不能依赖它来实现“保证投递”。 46 | 47 | 不是简单地把消息扔掉的灵感来源于我们测试:我们在事件总线上注册了TestEventListener来接收死信,然后将每个收到的死信在日志中生成一条警告-这对于更快地解析测试失败非常有帮助。我们觉得这个功能也可能用于其它的目的。 -------------------------------------------------------------------------------- /imgs/ActorPath.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/endymecy/akka-guide-zh/d2f8992b547703772537a24bd5969d17754141d9/imgs/ActorPath.png -------------------------------------------------------------------------------- /imgs/RemoteDeployment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/endymecy/akka-guide-zh/d2f8992b547703772537a24bd5969d17754141d9/imgs/RemoteDeployment.png -------------------------------------------------------------------------------- /imgs/actor_lifecycle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/endymecy/akka-guide-zh/d2f8992b547703772537a24bd5969d17754141d9/imgs/actor_lifecycle.png -------------------------------------------------------------------------------- /imgs/faulttolerancesample-failure-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/endymecy/akka-guide-zh/d2f8992b547703772537a24bd5969d17754141d9/imgs/faulttolerancesample-failure-flow.png -------------------------------------------------------------------------------- /imgs/faulttolerancesample-normal-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/endymecy/akka-guide-zh/d2f8992b547703772537a24bd5969d17754141d9/imgs/faulttolerancesample-normal-flow.png -------------------------------------------------------------------------------- /imgs/guardians.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/endymecy/akka-guide-zh/d2f8992b547703772537a24bd5969d17754141d9/imgs/guardians.png -------------------------------------------------------------------------------- /introduce/examples-of-use-cases-for-Akka.md: -------------------------------------------------------------------------------- 1 | # Akka 使用实例 2 | 3 | 我们看到Akka被成功运用在众多行业的众多大企业,从投资业到商业银行、从零售业到社会媒体、仿真、游戏和赌博、汽车和交通系统、数据分析等等等等。任何需要高吞吐率和低延迟的系统都是使用Akka的候选。 4 | [这里](http://stackoverflow.com/questions/4493001/good-use-case-for-akka/4494512#4494512)是一些由生产用户撰写的关于Akka的使用实例的非常好的讨论 5 | 6 | 以下是Akka被部署到生产环境中的领域 7 | 8 | ### 事务处理 (在线游戏,金融/银行业,贸易,统计,赌博,社会媒体,电信) 9 | 垂直扩展,水平扩展,容错/高可用性 10 | 11 | ### 服务后端 (任何行业,任何应用) 12 | 提供REST, SOAP, Cometd, WebSockets 等服务 作为消息总线/集成层 垂直扩展,水平扩展,容错/高可用性 13 | 14 | ### 并发/并行 (任何应用) 15 | 运行正确,方便使用,只需要将jar包添加到现有的JVM项目中(使用Scala,java, Groovy或jruby) 16 | 17 | ### 仿真 18 | 主/从,计算网格,MaReduce等等. 19 | 20 | ### 批处理 (任何行业) 21 | Camel集成来连接批处理数据源 Actor来分治地批处理工作负载 22 | 23 | ### 通信Hub (电信, Web媒体, 手机媒体) 24 | 垂直扩展,水平扩展,容错/高可用性 25 | 26 | ### 游戏与赌博 (MOM, 在线游戏, 赌博) 27 | 垂直扩展,水平扩展,容错/高可用性 28 | 29 | ### 商业智能/数据挖掘/通用数据处理 30 | 垂直扩展,水平扩展,容错/高可用性 31 | 32 | ### 复杂事件流处理 33 | 垂直扩展,水平扩展,容错/高可用性 -------------------------------------------------------------------------------- /introduce/getting-start.md: -------------------------------------------------------------------------------- 1 | # 开始 2 | 3 | ## 准备工作 4 | 5 | Akka要求你安装了 Java 1.6或更高版本到你的机器中。 6 | 7 | ## 下载 8 | 9 | 下载Akka有几种方法。你可以下载包含微内核的完整发布包(包含所有的模块). 或者也可以从Maven或sbt从Akka Maven仓库下载对akka的依赖。 10 | 11 | ## 模块 12 | 13 | Akka的模块化做得非常好,它为不同的功能提供了不同的Jar包。 14 | 15 | - akka-actor – 标准Actor, 有类型Actor,IO Actor等 16 | - akka-agent – Agents, integrated with Scala STM 17 | - akka-camel – Apache Camel集成 18 | - akka-cluster – 集群关系管理, elastic routers. 19 | - akka-kernel – Akka微内核,可运行一个基本的最小应用服务器 20 | - akka-osgi – 在OSGi容器中使用的基本绑定, 包括akka-actor类 21 | - akka-osgi-aries – 为供应actor的系统提供的Aries蓝图 22 | - akka-remote – 远程Actor 23 | - akka-slf4j – SLF4J Logger(事件总线监听器) 24 | - akka-testkit – 用于测试Actor的工具包 25 | - akka-zeromq – ZeroMQ集成 26 | - akka-contrib – an assortment of contributions which may or may not be moved into core modules 27 | 28 | ## 使用发布版 29 | 30 | 从[官网](http://akka.io/downloads)下载发布包并解压。 31 | 32 | ## 使用快照版 33 | 34 | Akka的每日快照发布在[http://repo.akka.io/snapshots/](http://repo.akka.io/snapshots/) 版本号中包含 SNAPSHOT 和时间戳. 你可以选择一个快照版,可以决定何时升级到一个新的版本。Akka快照仓库也可以在 [http://repo.typesafe.com/typesafe/snapshots/](http://repo.typesafe.com/typesafe/snapshots/)找到,此处还包含Akka模块依赖的其它仓库。 35 | 36 | ## 微内核 37 | 38 | Akka发布包包含微内核。要运行微内核,将你的应用的jar包放到 deploy 目录下并运行 bin 目录下的脚本. 39 | 40 | ## 与Maven一起使用Akka 41 | 42 | ```xml 43 | 44 | com.typesafe.akka 45 | akka-actor_2.10 46 | 2.3.9 47 | 48 | ``` 49 | 50 | ## 与SBT一起使用Akka 51 | 52 | SBT安装指导 [https://github.com/harrah/xsbt/wiki/Setup](https://github.com/harrah/xsbt/wiki/Setup) 53 | 54 | build.sbt 文件: 55 | 56 | ```scala 57 | name := "My Project" 58 | version := "1.0" 59 | scalaVersion := "2.10.4" 60 | resolvers += "Typesafe Repository" at "http://repo.typesafe.com/typesafe/releases/" 61 | libraryDependencies +="com.typesafe.akka" %% "akka-actor" % "2.3.9" 62 | ``` 63 | 64 | 注意:以上libraryDependencies的设置仅仅适用于sbt v0.12.x或者更高版本。如果是以前的版本,应通过如下方式设置 65 | 66 | ```scala 67 | libraryDependencies +="com.typesafe.akka" % "akka-actor_2.10" % "2.3.9" 68 | ``` 69 | ## 与gradle一起使用Akka 70 | 71 | gradle版本至少是1.4 72 | 73 | ``` 74 | apply plugin: ’scala’ 75 | repositories { 76 | mavenCentral() 77 | } 78 | dependencies { 79 | compile ’org.scala-lang:scala-library:2.10.4’ 80 | } 81 | tasks.withType(ScalaCompile) { 82 | scalaCompileOptions.useAnt = false 83 | } 84 | dependencies { 85 |  compile group: ’com.typesafe.akka’, name: ’akka-actor_2.10’, version: ’2.3.9’ 86 | compile group: ’org.scala-lang’, name: ’scala-library’, version: ’2.10.4’ 87 | } 88 | ``` -------------------------------------------------------------------------------- /introduce/use-case-deployment-scenarios.md: -------------------------------------------------------------------------------- 1 | # 用例和部署场景 2 | 3 | ## 我该如何使用和部署 Akka 4 | 5 | Akka 可以有几种使用方式: 6 | 7 | - 作为一个库: 以普通jar包的形式放在classpath上,或放到web应用中的 WEB-INF/lib位置 8 | - 作为一个独立的应用程序,使用 Microkernel(微内核),自己有一个main类来初始化Actor系统 9 | 10 | ### 将Akka作为一个库 11 | 12 | 如果你是编写web应用,你估计要使用这种方式。通过添加更多的模块,可以有多种以库的形式使用Akka的方式。 13 | 14 | ### 将Akka用作单独的微内核 15 | 16 | Akka 也可以以独立微内核的形式使用。 -------------------------------------------------------------------------------- /introduce/what-is-akka.md: -------------------------------------------------------------------------------- 1 | # 什么是akka 2 | 3 | ## 可拓展的实时事务处理 4 | 5 | 我们相信编写出正确的具有容错性和可扩展性的并发程序非常困难。大多数时候这是因为我们使用了错误的工具和错误的抽象级别。Akka就是为了改变这种状况而生的。 6 | 通过使用Actor模型我们提升了抽象级别,为构建正确的可扩展并发应用提供了一个更好的平台。在容错性方面,我们采取了“let it crash”(让它崩溃)模型, 7 | 人们已经将这种模型用在了电信行业,构建出“自愈合”的应用和永不停机的系统,取得了巨大成功。Actor还为透明的分布式系统以及真正的可扩展高容错应用的基础提供了抽象。 8 | 9 | Akka是开源项目,可以通过Apache 2许可获得 10 | 11 | 你可以在[官方网站](http://akka.io/downloads/)下载akka 12 | 13 | ## Akka实现了独特的混合模型 14 | 15 | ### Actors 16 | 17 | Actors提供给你如下功能: 18 | 19 | - 对并发和并行程序的简单的、高级别的抽象。 20 | - 异步的、非阻塞的、高性能的事件驱动编程模型。 21 | - 非常轻量的事件驱动处理(每GB内存可容纳几百万个actors) 22 | 23 | ### 容错 24 | 25 | - 使用“let-it-crash”语义和监管者树形结构来实现容错。 26 | - 监管者树形结构可以跨多个JVM来提供真正的高容错系统。 27 | - 编写永不停机、自愈合的高容错系统非常优秀。 28 | 29 | ### 位置透明性 30 | 31 | Akka的所有元素都为分布式环境而设计:所有actor都仅通过发送消息进行互操作,所有操作都是异步的。 32 | 33 | ### 持久化 34 | 35 | actor在启动或者重启时,它接收的消息可以被选择性的持久化或者置换。这可以允许actors恢复它们的状态,甚至是在jvm出现冲突之后或者是迁移到另一个节点之后。 36 | 37 | ## Akka可以以两种不同的方式来使用 38 | 39 | - 以库的形式:在web应用中使用,放到` WEB-INF/lib` 中或者作为一个普通的Jar包放进classpath目录。 40 | - 以微内核的形式:你可以将应用放进一个独立的内核。 41 | 42 | 43 | -------------------------------------------------------------------------------- /introduce/why-akka.md: -------------------------------------------------------------------------------- 1 | # 为什么选择akka 2 | 3 | ## Akka平台提供的有竞争力的功能 4 | 5 | Akka提供可扩展的实时事务处理。 6 | 7 | Akka是一个统一的运行时和编程模型,为以下目标设计: 8 | 9 | - 垂直扩展(并发) 10 | - 水平扩展(远程调用) 11 | - 高容错 12 | 13 | 在Akka中,只有一件事需要学习和管理,那就是高内聚和高一致的语义。 14 | 15 | Akka是一种高度可扩展的软件,这不仅仅表现在性能方面,也表现在它所适用的应用的大小。Akka的核心`Akka-actor`是非常小的,可以非常方便地放进你的应用中,提供你需要的异步无锁并行功能,不会有任何困扰。 16 | 17 | 你可以任意选择Akka的某些部分集成到你的应用中,也可以使用完整的包——Akka 微内核,它是一个独立的容器,可以直接部署你的Akka应用。随着CPU核数越来越多,即使你只使用一台电脑,Akka也可提供卓越的性能。Akka还同时提供多种并发范型,允许用户选择正确的工具来完成工作。 18 | 19 | ## 适合使用akka的场景 20 | 21 | 我们看到Akka被成功运用在众多行业的众多大企业中。 22 | 23 | - 投资和商业银行 24 | - 零售 25 | - 社交媒体 26 | - 仿真 27 | - 游戏和赌博 28 | - 汽车和交通系统 29 | - 医疗卫生 30 | - 数据分析 31 | 32 | 任何需要高吞吐率和低延迟的系统都是使用Akka的候选。 33 | 34 | Actor使你能够进行服务失败管理(监管者),负载管理(缓和策略、超时和隔离),水平和垂直方向上的可扩展性(增加cpu核数和/或增加更多的机器)管理。 35 | -------------------------------------------------------------------------------- /networking/io.md: -------------------------------------------------------------------------------- 1 | # IO 2 | 3 | ## 1 简介 4 | 5 | `akka.io`包在Akka和[spray.io](http://spray.io/)组合作开发。它设计时整合了`spray-io`模块的经验以及为作为基于actor的服务的更一般的消费共同开发的改进。 6 | 7 | 这个IO实现的设计指南的目标是达到极端的可扩展性,毫无妥协地提供了底层传输机制相匹配的正确的API,完全的事件驱动、无阻塞、并发。该API对网络协议的实现以及构建更高的抽像提供了坚实的基础。对用户来说,它不是一个全服务、高级别的NIO包装。 8 | 9 | ## 2 术语,概念 10 | 11 | IO API是完全基于actor的,意味着所有的操作都利用消息传递代替直接方法调用来实现。每个IO驱动(TCP和UDP)都有特定的actor,叫做`manager`,担当API的入口点。IO被分解到几个驱动。一个特殊驱动的manager可以通过IO入口点访问。例如,下面的例子发现一个TCP manager并返回它的`ActorRef`。 12 | 13 | ```scala 14 | import akka.io.{ IO, Tcp } 15 | import context.system // implicitly used by IO(Tcp) 16 | val manager = IO(Tcp) 17 | ``` 18 | 19 | 这个manager接收IO命令消息,回应实例化的工作actor。工作actor在回复中将它们自己返回给API用户说明命令已经传输。例如,当`Connect`命令发送给TCP manager后,这个manager创建一个actor代表TCP连接。所有和给定TCP连接相关的操作都会发送消息给连接actor,这个actor通过发送`Connected`消息告知它自己。 20 | 21 | ### DeathWatch和资源管理 22 | 23 | IO工作actor接收命令并且也发送事件。它们通常需要用户端副本(counterpart)actor监听这些事件(这些事件可以是入站连接、输入字节或者写确认)。这些工作actor观察它们的监听器副本。如果监听器停止,那么工作actor将会自动的释放它们持有的所有资源。这个设计使API在应对资源泄露时更强健。 24 | 25 | ### 写模式(Ack,Nack) 26 | 27 | IO设备有一个吞吐量最大值,这个值限制了写的次数和频率。当一个应用程序尝试压比设备能够处理更多的数据,驱动器必须缓冲这些数据直到设备能够写它们。拥有缓冲,这使短时间的密集写成为可能-但是没有无限的缓冲。需要“Flow control”来避免设备缓冲过载。 28 | 29 | AKKA支持两种类型的流控制 30 | 31 | - Ack-based,当写成功时,驱动器通知writer 32 | - Nack-based,当写失败时,驱动器通知writer 33 | 34 | 这两种模型在AKKA IO的TCP和UDP实现中都可以使用。 35 | 36 | 个别的写可以通过在写消息中提供一个ack对象来确认。当写完成后,worker将会发送ack对象到写actor。这可以用来实现Ack-based的流控制;只有当旧的数据已经被确认了之后,才能发送新数据。 37 | 38 | 如果一个写(或者其他命令)失败了,driver用一个特殊的消息(在TCP和UDP中是`CommandFailed`)通知发送命令的actor。这个消息也会通知失败写的writer,作为那个写的否定应答。请注意,在一个基于NACK的流量控制中,设定了这样一个事实,那就是故障写可能不是它发送的最新的写。例如,`w1`写命令的失败通知可能在写命令`w2`以及`w3`发送之后到达。如果writer想重发任何NACK消息,它可能需要保持一个等候操作的缓冲区。 39 | 40 | > *一个确认写并不意味着确认交付或者存储,收到一个写的回复简单地表示IO driver已经成功的处理了写。这里描述的Ack/Nack协议是流控制的手段不是错误处理* 41 | 42 | 43 | ### ByteString 44 | 45 | Akka IO 模块的主要目标是actor之间的通信只使用不可变对象。 在jvm上处理网络IO时,常使用` Array[Byte] `和` ByteBuffer `来表示Byte集合, 但它们是可变的。Scala的集合库也缺乏一个合适的高效不可变Byte集合类。安全高效地移动Byte对IO模块是非常重要的,所以我们开发了`ByteString`。 46 | 47 | `ByteString` 是一个[象绳子一样的](http://en.wikipedia.org/wiki/Rope_(computer_science))数据结构,它不可变而高效。 当连接两个`ByteString`时是将两者都保存在结果`ByteString`内部而不是将它们复制到新的Array。像` drop `和` take `这种操作返回的`ByteString`仍引用之前的Array, 只是改变了外部可见的offset和长度。我们花了很大力气保证内部的Array不能被修改。 每次当不安全的` Array `被用于创建新的` ByteString `时,会创建一个防御性拷贝。 48 | 49 | `ByteString `继承所有` IndexedSeq`的方法, 并添加了一些新的。 要了解更多信息, 请查阅` akka.util.ByteString `类和它的伴生对象的ScalaDoc。 50 | 51 | `ByteString`也带有优化的构建器`ByteStringBuilder`和迭代类`ByteIterator`,它们提供了额外的特征。 52 | 53 | ### 和java.io的兼容性 54 | 55 | 可以通过`asOutputStream`方法将一个`ByteStringBuilder`包裹在一个`java.io.OutputStream`中。同样,可以通过`asInputStream`方法将一个`ByteIterator`包裹在一个`java.io.InputStream`中。使用这些,`akka.io`应用程序可以集成基于`java.io`的遗留代码。 56 | 57 | -------------------------------------------------------------------------------- /networking/remoting.md: -------------------------------------------------------------------------------- 1 | # 远程调用 2 | 3 | 要了解关于Akka的远程调用能力的简介请参阅位置透明性。 4 | 5 | ## 1 为远程调用准备ActorSystem 6 | 7 | Akka 远程调用功能在一个单独的jar包中. 确认你的项目依赖中包括以下依赖: 8 | 9 | ```scala 10 | "com.typesafe.akka" %% "akka-remote" % "2.3.9" 11 | ``` 12 | 13 | 要在Akka项目中使用远程调用,最少要在` application.conf `文件中加入以下内容: 14 | 15 | ```scala 16 | akka { actor { 17 | provider = "akka.remote.RemoteActorRefProvider" 18 | } 19 | remote { 20 | enabled-transports = ["akka.remote.netty.tcp"] 21 | netty.tcp { 22 | hostname = "127.0.0.1" 23 | port = 2552 } 24 | } } 25 | ``` 26 | 27 | 从上面的例子你可以看到为了开始你需要添加的4个东西: 28 | 29 | - 将 provider 从` akka.actor.LocalActorRefProvider `改为` akka.remote.RemoteActorRefProvider` 30 | - 增加远程主机名 - 你希望运行actor系统的主机; 这个主机名与传给远程系统的内容完全一样,用来标识这个系统,并供后续需要连接回这个系统时使用, 所以要把它设置成一个可到达的IP地址或一个可以正确解析的域名来保证网络可访问性。 31 | - 增加端口号 - actor 系统监听的端口号,0 表示让它自动选择 32 | 33 | ## 2 远程交互的类型 34 | 35 | Akka 远程调用有两种方式: 36 | 37 | - 查找 : 使用`actorSelection(path)`在远程主机上查找一个actor 38 | - 创建 : 使用` actorOf(Props(...), actorName)`在远程主机上创建一个actor 39 | 40 | 下一部分将对这两种方法进行详细介绍。 41 | 42 | ## 3 查找远程 Actors 43 | 44 | `actorSelection(path)`会获得远程结点上actor的`ActorSelection`。 45 | 46 | ```scala 47 | val selection = 48 | context.actorSelection("akka.tcp://actorSystemName@10.0.0.1:2552/user/actorName") 49 | ``` 50 | 51 | 可以看到使用这种模式在远程结点上查找一个Actor: 52 | 53 | ```scala 54 | akka.://@:/ 55 | ``` 56 | 57 | 一旦得到了actor的selection,你就可以象与本地actor通讯一样与它进行通迅 58 | 59 | ```scala 60 | selection ! "Pretty awesome feature" 61 | ``` 62 | 63 | 为了为`ActorSelection`获得一个`ActorRef`,你必须发送一个消息给`selection`并且使用从actor而来的回复的`sender`引用。有一个内置的`Identify`,它可以被所有的actors理解并且自动回复一个包含`ActorRef`的`ActorIdentity`消息。这也可以通过调用`ActorSelection`的`resolveOne`方法做到,这个方法返回一个匹配`ActorRef`的Future。 64 | 65 | ## 4 创建远程 Actor 66 | 67 | 在Akka中要使用远程创建actor的功能,需要对` application.conf `文件进行以下修改 (只显示deployment部分): 68 | 69 | ```scala 70 | akka { actor { 71 | deployment { 72 | /sampleActor { 73 | remote = "akka.tcp://sampleActorSystem@127.0.0.1:2553" 74 | } 75 | } } 76 | } 77 | ``` 78 | 79 | 这个配置告知Akka当一个路径为` /sampleActor `的actor被创建时进行响应, i.e. 调用` system.actorOf(Props(...), sampleActor)`时,指定的actor不会被直接实例化,而是远程actor系统的守护进程被要求创建这个actor, 本例中的远程actor系统是`sampleActorSystem@127.0.0.1:2553`。 80 | 81 | 一旦配置了以上属性你可以在代码中进行如下操作: 82 | 83 | ```scala 84 | val actor = system.actorOf(Props[SampleActor], "sampleActor") 85 | actor ! "Pretty slick" 86 | ``` 87 | 88 | 在使用SampleActor 时它必须可用, i.e. actor系统的classloader中必须有一个包含这个类的JAR包。 89 | 90 | 通过设置`akka.actor.serialize-creators=on`项,所有Props的可序列化可以被测试。它的部署有`LocalScope`的Props可以免除测试。 91 | 92 | ### 用代码进行远程部署 93 | 94 | 要允许动态部署系统,也可以在用来创建actor的 Props 中包含deployment配置 : 这一部分信息与配置文件中的deployment部分是等价的, 如果两者都有,则外部配置拥有更高的优先级。 95 | 96 | 加入这些import: 97 | 98 | ```scala 99 | import akka.actor.{ Props, Deploy, Address, AddressFromURIString } 100 | import akka.remote.RemoteScope 101 | ``` 102 | 103 | 和一个这样的远程地址: 104 | 105 | ```scala 106 | val one = AddressFromURIString("akka.tcp://sys@host:1234") 107 | val two = Address("akka.tcp", "sys", "host", 1234) // this gives the same 108 | ``` 109 | 你可以这样要求系统在此远程结点上创建一个子actor: 110 | 111 | ```scala 112 | val ref = system.actorOf(Props[SampleActor]. 113 | withDeploy(Deploy(scope = RemoteScope(address)))) 114 | ``` 115 | 116 | ## 5 生命周期以及错误恢复模型 117 | 118 | 119 | ## 6 监听远程actor 120 | 121 | ## 7 序列化 122 | 123 | 对actor使用远程调用时你必须保证这些actor所使用的` props `和` messages `是可序列化的。 如果不能保证会导致系统产生意料之外的行为。 124 | 125 | ## 8 有远程目标的路由器 126 | 127 | 将远程调用与路由进行组合是非常实用的。 128 | 129 | 远程部署的`routees`的一个Pool可以如下配置: 130 | 131 | ```scala 132 | akka.actor.deployment { 133 | /parent/remotePool { 134 | router = round-robin-pool 135 | nr-of-instances = 10 136 | target.nodes = ["akka.tcp://app@10.0.0.2:2552", "akka://app@10.0.0.3:2552"] 137 | } } 138 | ``` 139 | 这个配置文件的设定将会克隆定义在`remotePool`的`Props`中的actor 10次并且均匀的部署它们到两个给定的目标节点。 140 | 141 | 远程actor的一个group可以如下配置: 142 | 143 | ```scala 144 | akka.actor.deployment { 145 | /parent/remoteGroup { 146 | router = round-robin-group 147 | routees.paths = [ 148 | "akka.tcp://app@10.0.0.1:2552/user/workers/w1", 149 | "akka.tcp://app@10.0.0.2:2552/user/workers/w1", 150 | "akka.tcp://app@10.0.0.3:2552/user/workers/w1"] 151 | } } 152 | ``` 153 | 154 | 以上的配置将会发送消息到给定的远程actor paths。它需要你在远程节点上创建匹配paths的目标actor。这不是通过路由器做的。 155 | 156 | 157 | 158 | 159 | -------------------------------------------------------------------------------- /networking/serialization.md: -------------------------------------------------------------------------------- 1 | # 序列化 2 | 3 | Akka 提供内置的序列化支持扩展, 你可以选择使用内置的序列化功能,也可以自己写一个。 4 | 5 | 内置的序列化功能被Akka内部用来序列化消息,你也可以用它做其它的序列化工作。 6 | 7 | ## 1 用法 8 | 9 | ### 配置 10 | 11 | 为了让 Akka 知道对什么任务使用哪个` Serializer `, 你需要编辑你的配置文件, 在`akka.actor.serializers` 部分将名称与` akka.serialization.Serializer`的实现做绑定,象这样: 12 | 13 | ```scala 14 | akka { actor { 15 | serializers { 16 | java = "akka.serialization.JavaSerializer" 17 | proto = "akka.remote.serialization.ProtobufSerializer" 18 | myown = "docs.serialization.MyOwnSerializer" 19 | } } 20 | } 21 | ``` 22 | 23 | 名称与` Serializer `的不同实现绑定后你需要指定哪些类的序列化需要使用哪种` Serializer`,这部分配置写在`akka.actor.serialization-bindings`部分: 24 | 25 | ```scala 26 | akka { actor { 27 | serializers { 28 | java = "akka.serialization.JavaSerializer" 29 | proto = "akka.remote.serialization.ProtobufSerializer" 30 | myown = "docs.serialization.MyOwnSerializer" 31 | } 32 | serialization-bindings { 33 | "java.lang.String" = java 34 | "docs.serialization.Customer" = java 35 | "com.google.protobuf.Message" = proto 36 | "docs.serialization.MyOwnSerializable" = myown 37 | "java.lang.Boolean" = myown 38 | } } 39 | } 40 | ``` 41 | 42 | 你只需要指定消息的接口或抽象基类。当消息实现了配置中多个类时,为避免歧义, 将使用最具体类,如其它所有类的子类。如果这个条件不满足,如`java.io.Serializable `和 `MyOwnSerializable `都有配置,而彼此都不是对方的子类型,将生成警告。 43 | 44 | Akka 缺省提供使用` java.io.Serializable` 和 [protobuf](http://code.google.com/p/protobuf/) `com.google.protobuf.GeneratedMessage `的序列化工具 (后者仅当定义了对` akka-remote `模块的依赖时才有), 所以通常你不需要添加这两种配置。由于` com.google.protobuf.GeneratedMessage `实现了` java.io.Serializable`, 在不特别指定的情况下,`protobuf` 消息将总是用` protobuf `协议来做序列化。 要禁止缺省的序列化工具,将其对应的类型设为 “none”: 45 | 46 | ```scala 47 | akka.actor.serialization-bindings { 48 | "java.io.Serializable" = none 49 | } 50 | ``` 51 | 52 | ### 确认 53 | 54 | 如果你希望确认你的消息是可以被序列化的你可以打开这个配置项: 55 | 56 | ```scala 57 | akka { actor { 58 | serialize-messages = on 59 | } 60 | } 61 | ``` 62 | 63 | > *警告:我们只推荐在运行测试代码的时候才打开这个选项。在其它的场景打开它完全没有道理。* 64 | 65 | 如果你希望确认你的` Props `可以被序列化你可以打开这个配置项: 66 | 67 | ```scala 68 | akka { actor { 69 | serialize-creators = on 70 | } 71 | } 72 | ``` 73 | > *警告:我们只推荐在运行测试代码的时候才打开这个选项。在其它的场景打开它完全没有道理。* 74 | 75 | ### 通过代码 76 | 77 | 如果你希望通过代码使用 Akka 序列化来进行序列化/反序列化, 以下是一些例子: 78 | 79 | ```scala 80 | import akka.serialization._ 81 | import com.typesafe.config.ConfigFactory 82 | 83 | val system = ActorSystem("example") 84 | 85 | // 获取序列化扩展工具 86 | val serialization = SerializationExtension(system) 87 | 88 | // 定义一些要序列化的东西 89 | val original = "woohoo" 90 | 91 | // 为它找到一个 Serializer 92 | val serializer = serialization.findSerializerFor(original) 93 | 94 | // 转化为字节 95 | val bytes = serializer.toBinary(original) 96 | 97 | // 转化回对象 98 | val back = serializer.fromBinary(bytes, manifest = None) 99 | 100 | // 就绪! 101 | back should be(original) 102 | ``` 103 | 104 | ## 2 自定义 105 | 106 | 你希望创建自己的` Serializer`, 应该已经看到上例中的` akka.docs.serialization.MyOwnSerializer `了吧? 107 | 108 | ### 创建新的 Serializer 109 | 110 | 首先你需要为你的 Serializer 写一个类定义,象这样: 111 | 112 | ```scala 113 | import akka.serialization._ 114 | import com.typesafe.config.ConfigFactory 115 | 116 | class MyOwnSerializer extends Serializer { 117 | 118 | // 指定 "fromBinary" 是否需要一个 "clazz" 119 | def includeManifest: Boolean = false 120 | 121 | // 为你的 Serializer 选择一个唯一标识, 122 | // 基本上所有的整数都可以用, 123 | // 但 0 - 16 是Akka自己保留的 124 | def identifier = 1234567 125 | 126 | // "toBinary" 将对象序列化为字节数组 Array of Bytes 127 | def toBinary(obj: AnyRef): Array[Byte] = { 128 | // 将序列化的代码写在这儿 129 | // ... ... 130 | } 131 | 132 | // "fromBinary" 对字节数组进行反序列化, 133 | // 使用类型提示 (如果有的话, 见上文的 "includeManifest" ) 134 | // 使用可能提供的 classLoader. 135 | def fromBinary(bytes: Array[Byte], 136 | clazz: Option[Class[_]]): AnyRef = { 137 | // 将反序列化的代码写在这儿 138 | // ... ... 139 | } 140 | } 141 | ``` 142 | 143 | 然后你只需要填空,在配置文件中将它绑定到一个名称, 然后列出需要用它来做序列化的类即可。 144 | 145 | ### Actor引用的序列化 146 | 147 | 所有的` ActorRef `都是用` JavaSerializer`, 但如果你写了自己的serializer, 你可能想知道如何正确对它们进行序列化和反序列化。在一般情况下,使用的本地地址依赖于远程地址的类型,这个远程地址是序列化的信息更容易接受。如下使用`Serialization.serializedActorPath(actorRef)`。 148 | 149 | ```scala 150 | import akka.actor.{ ActorRef, ActorSystem } 151 | import akka.serialization._ 152 | import com.typesafe.config.ConfigFactory 153 | // Serialize 154 | // (beneath toBinary) 155 | val identifier: String = Serialization.serializedActorPath(theActorRef) 156 | // Then just serialize the identifier however you like 157 | // Deserialize 158 | // (beneath fromBinary) 159 | val deserializedActorRef = extendedSystem.provider.resolveActorRef(identifier) 160 | // Then just use the ActorRef 161 | ``` 162 | 163 | 这假设序列化发生在通过远程传输发送消息的上下文。序列化也有其它的使用,如存储actor应用到一个actor应用程序的外面(如数据库)。在这种情况下,记住一个actor路径的地址部分决定了这个actor如何通讯是非常重要的。如果检索发生在相同的逻辑上下文,存储本地actor路径可能是更好的选择。但是,当在不同的网络主机上面反序列化它时,这是不够的:因为这需要它需要包含系统的远程传输地址。一个actor地址并没有限制只有一个远程传输地址,这使这个问题变得更加有趣。为了找到合适的地址使用,当发送`remoteAddr`时,你可以使用`ActorRefProvider.getExternalAddressFor(remoteAddr)`。 164 | 165 | ```scala 166 | object ExternalAddress extends ExtensionKey[ExternalAddressExt] 167 | class ExternalAddressExt(system: ExtendedActorSystem) extends Extension { 168 | def addressFor(remoteAddr: Address): Address = 169 | system.provider.getExternalAddressFor(remoteAddr) getOrElse 170 | (throw new UnsupportedOperationException("cannot send to " + remoteAddr)) 171 | } 172 | def serializeTo(ref: ActorRef, remote: Address): String = 173 | ref.path.toSerializationFormatWithAddress(ExternalAddress(extendedSystem). 174 | addressFor(remote)) 175 | ``` 176 | 177 | > *注:如果地址还没有`host`和`port`组件,也就是说它仅仅为本地地址插入地址信息,`ActorPath.toSerializationFormatWithAddress `和`toString`是不同的。`toSerializationFormatWithAddress`还需要添加actor的唯一的id,这个id会随着actor的停止然后以相同的名字重新创建而改变。发送消息到指向旧actor的引用将不会传送到新的actor。如果你不想这种行为,你可以使用`toStringWithAddress`,它不包含这个唯一的id* 178 | 179 | 这需要你至少知道将要反序列化结果actor引用的系统支持哪种类型的地址。如果你没有具体的地址,你可以使用`Address(protocol, "", "", 0)`为正确的协议创建一个虚拟的地址。 180 | 181 | 也有一个缺省的远程地址被集群支持(仅仅只有特殊的系统有)。你可以像下面这样得到它 182 | 183 | ```scala 184 | object ExternalAddress extends ExtensionKey[ExternalAddressExt] 185 | class ExternalAddressExt(system: ExtendedActorSystem) extends Extension { 186 | def addressForAkka: Address = system.provider.getDefaultAddress 187 | } 188 | def serializeAkkaDefault(ref: ActorRef): String = 189 | ref.path.toSerializationFormatWithAddress(ExternalAddress(theActorSystem). 190 | addressForAkka) 191 | ``` 192 | 193 | ## 3 关于 Java 序列化 194 | 195 | 如果在做Java序列化任务时不使用` JavaSerializer `, 你必须保证在动态变量`JavaSerializer.currentSystem`中提供一个有效的` ExtendedActorSystem `。 它是在读取`ActorRef`时将字符串表示转换成实际的引用。 动态变量`DynamicVariable `是一个` thread-local`变量,所以在反序列化任何可能包含actor引用的数据时要保证这个变量有值。 196 | 197 | 198 | -------------------------------------------------------------------------------- /networking/using-tcp.md: -------------------------------------------------------------------------------- 1 | # 使用TCP 2 | 3 | 这一节的代码片段都假设有下面的导入: 4 | 5 | ```scala 6 | import akka.actor.{ Actor, ActorRef, Props } 7 | import akka.io.{ IO, Tcp } 8 | import akka.util.ByteString 9 | import java.net.InetSocketAddress 10 | ``` 11 | 12 | 所有的AKKA IO API都通过manager对象访问。当使用一个IO API时,第一步是获得一个适当manager的引用。下面的代码显示了如何获得一个TCP manager引用。 13 | 14 | ```scala 15 | import akka.io.{ IO, Tcp } 16 | import context.system // implicitly used by IO(Tcp) 17 | val manager = IO(Tcp) 18 | ``` 19 | 20 | 这个manager是一个actor,它为特殊的任务(如监听输入的连接)处理潜在的底层的IO资源(selector和channels)以及实例化workers。 21 | 22 | ## 1 连接 23 | 24 | ```scala 25 | object Client { 26 | def props(remote: InetSocketAddress, replies: ActorRef) = 27 | Props(classOf[Client], remote, replies) 28 | } 29 | class Client(remote: InetSocketAddress, listener: ActorRef) extends Actor { 30 | import Tcp._ 31 | import context.system 32 | IO(Tcp) ! Connect(remote) 33 | def receive = { 34 | case CommandFailed(_: Connect) => 35 | listener ! "connect failed" 36 | context stop self 37 | case c @ Connected(remote, local) => 38 | listener ! c 39 | val connection = sender() 40 | connection ! Register(self) 41 | context become { 42 | case data: ByteString => 43 | connection ! Write(data) 44 | case CommandFailed(w: Write) => 45 | // O/S buffer was full 46 | listener ! "write failed" 47 | case Received(data) => 48 | listener ! data 49 | case "close" => 50 | connection ! Close 51 | case _: ConnectionClosed => 52 | listener ! "connection closed" 53 | context stop self 54 | } } 55 | } 56 | ``` 57 | 58 | 连接到远程地址的第一步是发送`Connect`消息给TCP manager,除了上面所示的简单的形式存在,也有可能指定一个特定的本地`InetSocketAddress`去绑定,一个socket选项列表去应用。 59 | 60 | > *`SO_NODELAY`(在windows中是`TCP_NODELAY`)这个socket选项在AKKA中默认是为true的。这个设置使Nagle的算法失效,使大部分程序的潜能得到了相当的提高。这个设置可以通过在`Connect`消息的socket选项列表中传递`SO.TcpNoDelay(false)`来覆盖。* 61 | 62 | TCP manager然后要么回复一个`CommandFailed`,要么回复一个代表新连接的内部actor。这个新的actor然后发送一个`Connected`消息到`Connect`消息的原始发送方。 63 | 64 | 为了激活这个新的连接,一个`Register`消息必须发送给连接actor。通知这个actor谁将从socket中接收数据。在这个过程开始之前,连接不可以使用。并且,有一个内部的超时时间,如果没有收到`Register`消息,连接actor将会关闭它自己。 65 | 66 | 连接actor将观察注册的handler,当某一个终止时关闭连接,然后清除这个连接相关的所有内部资源。 67 | 68 | 在上面的例子中,这个actor使用`become`从未连接的操作交换到连接的操作,表明在该状态观察到的命令和事件。`CommandFailed`将在下面讨论。`ConnectionClosed`是一个trait,标记不同的连接关闭事件。最后一行用相同的方式处理所有的连接关闭事件。可以在下面了解更多细粒度的连接关闭事件。 69 | 70 | ## 2 接收连接 71 | 72 | ```scala 73 | class Server extends Actor { 74 | import Tcp._ 75 | import context.system 76 | IO(Tcp) ! Bind(self, new InetSocketAddress("localhost", 0)) 77 | def receive = { 78 | case b @ Bound(localAddress) => 79 | // do some logging or setup ... 80 | case CommandFailed(_: Bind) => context stop self 81 | case c @ Connected(remote, local) => 82 | val handler = context.actorOf(Props[SimplisticHandler]) 83 | val connection = sender() 84 | connection ! Register(handler) 85 | } } 86 | ``` 87 | 88 | 为了创建一个TCP服务器以及监听入站(inbound)连接,一个`Bind`命令必须发送给TCP manager。这将会通知TCP manager监听在特定`InetSocketAddress`上的TCP连接。为了绑定任意的接口,接口可以指定为0。 89 | 90 | 发送`Bind`消息的actor将接收一个`Bound`消息表明服务器已经准备接收输入连接。这个消息也包含`InetSocketAddress`,表明socket确实有限制(分解为IP地址和端口号)。 91 | 92 | 出站连接(outgoing)的处理过程与以上的处理过程是一样的。例子证明,当发送`Register`消息时,从一个特定的连接处理读可以通过命名另外一个actor为处理者来代理给这个actor。来源于系统的任何actor的写都可以发送到连接actor(发送`Connected`消息的actor)。最简化的处理如下: 93 | 94 | ```scala 95 | class SimplisticHandler extends Actor { 96 | import Tcp._ 97 | def receive = { 98 | case Received(data) => sender() ! Write(data) 99 |  case PeerClosed => context stop self 100 | } 101 | ``` 102 | 103 | 出站actor的唯一的不同是内部actor管理监听端口-`Bound`消息的发送者-为`Bind`消息中的`Connected`消息观察命名为接收者的actor。当这个actor终止,监听端口也会关闭,所有与之相关的资源也会被释放;这时,存在的连接将不会被终止。 104 | 105 | ## 3 关闭连接 106 | 107 | 可以通过发送`Close`、`ConfirmedClose`或者`Abort`给连接actor来关闭连接。 108 | 109 | `Close`将会发送一个`FIN`请求来关闭连接,但是不会等待来自远程端点的确认。待处理的写入将会被清。如果关闭成功了,监听器将会被通知一个`Closed`。 110 | 111 | `ConfirmedClose`将通过发送一个`FIN`消息关闭连接的发送方向。但是数据将会继续被接收到直到远程的端点也关闭了连接。待处理的写入将会被清。如果关闭成功了,监听器将会被通知一个`ConfirmedClosed`。 112 | 113 | `Abort`将会发送一个`RST`消息给远程端点立即终止连接。待处理的写入将不会被清。如果关闭成功了,监听器将会被通知一个`Aborted`。 114 | 115 | 如果连接已经被远程端点关闭了,`PeerClosed`将会被发送到监听者。缺省情况下,从这个端点而来的连接将会自动关闭。为了支持半关闭的连接,设置`Register`消息的`keepOpenOnPeerClosed`成员为`true`。在这种情况下,连接保持打开状态直到它接收到以上的关闭命令。 116 | 117 | 不管什么时候一个错误发生了,`ErrorClosed`都将会发送给监听者,强制关闭连接。 118 | 119 | 所有的关闭通知都是`ConnectionClosed`的子类型,所以不需要细粒度关闭事件的监听者可能用相同的方式处理所有的关闭事件。 120 | 121 | ## 4 写入连接 122 | 123 | 一旦一个连接已经建立了,可以从任意actor发送数据给它。数据的格式是`Tcp.WriteCommand. Tcp.WriteCommand`,它有三种不同的实现。 124 | 125 | - Tcp.Write。最简单的`WriteCommand`实现,它包裹一个`ByteString`实例和一个“ack”事件。这个`ByteString`拥有一个或者多个块,不可变的内存大小最大为2G。 126 | - Tcp.WriteFile。如果你想从一个文件发送`raw`数据,你用`Tcp.WriteFile`命令会非常有效。它允许你指定一个在磁盘上的字节块通过连接发送而不需要首先加载它们到JVM内存中。`Tcp.WriteFile`可以保持超过2G的数据以及一个`ack`事件(如果需要)。 127 | - Tcp.CompoundWrite。有时你可能想集合(或者交叉)几个`Tcp.Write`或者`Tcp.WriteFile`命令到一个原子的写命令,这个命令一次性完成写到连接。`Tcp.CompoundWrite`允许你这样做并且提供了三个好处。 128 | 129 | - 1 如下面章节介绍的,TCP连接actor在某一时间仅仅能处理一个单个的写命令。通过合并多个写到一个`CompoundWrite`,你可以以最小的开销传递,并且不需要通过一个`ACK-based`的消息协议一个个的回复它们。 130 | - 2 因为一个`WriteCommand`是原子的,你可以肯定其它actor不可能“注入”其它的写命令到你组合到一个`CompoundWrite`的写命令中去。几个actor写入相同的连接是一个重要的特征,这在某些情况下很难获得。 131 | - 3 一个`CompoundWrite`的“子写入(sub writes)”是普通的`Write`和`WriteFile`命令,它们请求“ack”事件。这些ACKs的发出与相应地“子写入”的完成是同时。这允许你附加超过一个`Write`或者`WriteFile`。或者通过在任意的点上发送临时的ACKs去让连接actor知道传递`CompoundWrite`的进度。 132 | 133 | ## 5 限制(Throttling)写和读 134 | 135 | TCP连接actor的基本模型没有内部缓冲(它在同一时刻只能处理一个写,意味着它可以缓冲一个写直到它完全传递到OS内核)。需要在用户层为读和写处理拥挤问题。 136 | 137 | 对于`back-pressuring`写,有三种操作模型 138 | 139 | - ACK-based:每一个`Write`命令携带着一个任意的命令,如果这个对象不是`Tcp.NoAck`。 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | --------------------------------------------------------------------------------