├── project ├── build.properties └── build.sbt ├── sonatype.sbt ├── src ├── test │ └── scala │ │ └── org │ │ └── querki │ │ └── requester │ │ ├── FailureTests.scala │ │ ├── RequesterTests.scala │ │ ├── FutureTests.scala │ │ ├── StackTests.scala │ │ ├── RetryTests.scala │ │ └── ComprehensionTests.scala └── main │ └── scala │ └── org │ └── querki │ └── requester │ └── Requester.scala ├── .gitignore └── README.md /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.2.8 -------------------------------------------------------------------------------- /project/build.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "2.0.1") 2 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.8.1") 3 | -------------------------------------------------------------------------------- /sonatype.sbt: -------------------------------------------------------------------------------- 1 | 2 | sonatypeProfileName := "org.querki" 3 | 4 | publishMavenStyle := true 5 | 6 | licenses += ("MIT License", url("http://www.opensource.org/licenses/mit-license.php")) 7 | 8 | import xerial.sbt.Sonatype._ 9 | sonatypeProjectHosting := Some(GitHubHosting("jducoeur", "jsext", "justin@querki.net")) 10 | 11 | homepage := Some(url("http://www.querki.net/")) 12 | 13 | scmInfo := Some(ScmInfo( 14 | url("https://github.com/jducoeur/Requester"), 15 | "scm:git:git@github.com/jducoeur/Requester.git", 16 | Some("scm:git:git@github.com/jducoeur/Requester.git"))) 17 | 18 | developers := List( 19 | Developer(id = "jducoeur", name = "Mark Waks", email = "justin@querki.net", url = new URL("https://github.com/jducoeur/")) 20 | ) 21 | -------------------------------------------------------------------------------- /src/test/scala/org/querki/requester/FailureTests.scala: -------------------------------------------------------------------------------- 1 | package org.querki.requester 2 | 3 | import scala.concurrent.{Future, Promise} 4 | 5 | import akka.actor._ 6 | 7 | /** 8 | * @author jducoeur 9 | */ 10 | 11 | object FailureTests { 12 | case class TestThrowable(th:Throwable) 13 | case class StartPromise(p:Promise[String]) 14 | 15 | class PromiseActor extends QTestActor { 16 | def doReceive = { 17 | case StartPromise(p) => { 18 | 19 | } 20 | } 21 | } 22 | } 23 | 24 | class FailureTests extends RequesterTests { 25 | import FailureTests._ 26 | 27 | "A Requester" should { 28 | "be able to catch an Exception through a Future" in { 29 | val myPromise = Promise[String] 30 | val pActor = system.actorOf(Props(classOf[PromiseActor])) 31 | pActor ! StartPromise(myPromise) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/test/scala/org/querki/requester/RequesterTests.scala: -------------------------------------------------------------------------------- 1 | package org.querki.requester 2 | 3 | /** 4 | * @author jducoeur 5 | */ 6 | import scala.concurrent.duration._ 7 | 8 | import akka.actor.ActorSystem 9 | import akka.actor.Actor 10 | import akka.actor.Props 11 | import akka.testkit.{ TestActors, TestKit, ImplicitSender } 12 | 13 | import org.scalatest.WordSpecLike 14 | import org.scalatest.Matchers 15 | import org.scalatest.BeforeAndAfterAll 16 | 17 | class RequesterTests extends TestKit(ActorSystem("RequesterTests")) 18 | with ImplicitSender with WordSpecLike with Matchers with BeforeAndAfterAll 19 | { 20 | override def afterAll { 21 | TestKit.shutdownActorSystem(system) 22 | } 23 | 24 | implicit val dur = 1.second 25 | } 26 | 27 | class Doubler extends Actor { 28 | def receive = { 29 | case n:Int => sender ! n*2 30 | } 31 | } 32 | 33 | abstract class QTestActor extends Actor with Requester { 34 | lazy val doubler = context.actorOf(Props(classOf[Doubler])) 35 | 36 | def doReceive:Receive 37 | 38 | def receive = doReceive 39 | } 40 | -------------------------------------------------------------------------------- /src/test/scala/org/querki/requester/FutureTests.scala: -------------------------------------------------------------------------------- 1 | package org.querki.requester 2 | 3 | import scala.concurrent.{Await, Future} 4 | 5 | import akka.actor._ 6 | 7 | import org.scalatest.concurrent._ 8 | 9 | /** 10 | * @author jducoeur 11 | */ 12 | object FutureTests { 13 | case object Start 14 | case class TestFuture(f:Future[Int]) 15 | 16 | class NewFutureActor(error:Boolean) extends QTestActor { 17 | def doReceive = { 18 | case Start => { 19 | val f:Future[Int] = 20 | for { 21 | four <- doubler.requestFor[Int](2) 22 | eight <- doubler.requestFor[Int](four) 23 | dummy = if (error) throw new Exception("BOOM") 24 | sixteen <- doubler.requestFor[Int](eight) 25 | } 26 | yield sixteen 27 | 28 | sender ! TestFuture(f) 29 | } 30 | } 31 | } 32 | } 33 | 34 | class FutureTests extends RequesterTests with Futures with ScalaFutures { 35 | import FutureTests._ 36 | 37 | "Requester" should { 38 | "be able to work through a Future, new-style" in { 39 | val actor = system.actorOf(Props(classOf[NewFutureActor], false)) 40 | actor ! Start 41 | val TestFuture(fut) = receiveOne(dur) 42 | whenReady(fut) { n => 43 | assert(n == 16) 44 | } 45 | } 46 | 47 | "be able to catch an Exception through a Future, new-style" in { 48 | val actor = system.actorOf(Props(classOf[NewFutureActor], true)) 49 | actor ! Start 50 | val TestFuture(fut) = receiveOne(dur) 51 | whenReady(fut.failed) { ex => 52 | assert(ex.getMessage == "BOOM") 53 | } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /src/test/scala/org/querki/requester/StackTests.scala: -------------------------------------------------------------------------------- 1 | package org.querki.requester 2 | 3 | import akka.actor._ 4 | 5 | /** 6 | * @author jducoeur 7 | */ 8 | object StackTests { 9 | case class Accumulate(nTerms:Int) 10 | 11 | /** 12 | * This works similarly to the doubling tests, but grows more slowly, so that we can 13 | * pressure the stack without hitting Int overflow first. 14 | */ 15 | class Accumulator extends QTestActor { 16 | 17 | lazy val adder = context.actorOf(Props(classOf[Adder])) 18 | 19 | def doReceive = { 20 | case Accumulate(nTerms) => askAdder(nTerms, 0) foreach { result => sender ! result } 21 | } 22 | 23 | def askAdder(nTerms:Int, total:Int):RequestM[Int] = { 24 | if (nTerms == 0) { 25 | RequestM.successful(total) 26 | } else { 27 | adder.requestFor[Int](Add(nTerms, total)) flatMap { newTotal => 28 | askAdder(nTerms - 1, newTotal) 29 | } 30 | } 31 | } 32 | } 33 | 34 | case class Add(x:Int, y:Int) 35 | 36 | class Adder extends QTestActor { 37 | def doReceive = { 38 | case Add(x, y) => sender ! x + y 39 | } 40 | } 41 | } 42 | 43 | class StackTests extends RequesterTests { 44 | import StackTests._ 45 | 46 | // Test very deeply nested flatMaps. Note that this test takes a relatively long time (~1 second) to run. 47 | "A deep stack of flatMaps" should { 48 | "not blow out the stack" in { 49 | val accum = system.actorOf(Props(classOf[Accumulator])) 50 | // The heart of the test: this had been causing a StackOverflow when set to ~2000 until we 51 | // added the non-stack-based unwinding mechanism: 52 | accum ! Accumulate(2000) 53 | expectMsg(2001000) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/test/scala/org/querki/requester/RetryTests.scala: -------------------------------------------------------------------------------- 1 | package org.querki.requester 2 | 3 | import scala.util.{Failure, Success} 4 | 5 | import akka.actor._ 6 | import akka.util.Timeout 7 | import scala.concurrent.duration._ 8 | import akka.pattern.AskTimeoutException 9 | 10 | object RetryTests { 11 | case object Start 12 | case class DropFirstN(n:Int) 13 | case object GaveUp 14 | 15 | /** 16 | * A variant of Doubler that simply drops the first N requests on the floor. 17 | */ 18 | class FinickyDoubler extends Actor { 19 | var nToDrop:Int = 0 20 | 21 | def receive = { 22 | case DropFirstN(n) => nToDrop = n 23 | 24 | case n:Int => { 25 | if (nToDrop > 0) 26 | nToDrop -= 1 27 | else 28 | sender ! n * 2 29 | } 30 | } 31 | } 32 | 33 | class Tester(n:Int, drops:Int, retries:Int) extends Actor with Requester { 34 | 35 | lazy val doubler = context.actorOf(Props(classOf[FinickyDoubler])) 36 | 37 | override implicit val requestTimeout = Timeout(1.millis) 38 | 39 | def receive = { 40 | case Start => { 41 | doubler ! DropFirstN(drops) 42 | val req = for { 43 | result <- doubler.request(n, retries) 44 | } 45 | yield result 46 | 47 | req.onComplete { 48 | case Success(result:Int) => { 49 | sender ! result 50 | context.stop(doubler) 51 | context.stop(self) 52 | } 53 | 54 | case Failure(ex:AskTimeoutException) => { 55 | sender ! GaveUp 56 | context.stop(doubler) 57 | context.stop(self) 58 | } 59 | 60 | case other => throw new Exception(s"Got unexpected response $other") 61 | } 62 | } 63 | } 64 | } 65 | } 66 | 67 | /** 68 | * These tests exercise request's retry mechanism. The recipient Actor drops some messages on the 69 | * floor, so that they have to be retried; we test retrying, as well as retry failures. 70 | */ 71 | class RetryTests extends RequesterTests { 72 | import RetryTests._ 73 | 74 | def startTest(n:Int, drops:Int, retries:Int) = { 75 | val tester = system.actorOf(Props(classOf[Tester], n, drops, retries)) 76 | tester ! Start 77 | } 78 | 79 | "Requester with retries" should { 80 | "work normally if nothing goes wrong" in { 81 | startTest(3, 0, 0) 82 | expectMsg(6) 83 | } 84 | 85 | "retry successfully once" in { 86 | startTest(3, 1, 3) 87 | expectMsg(6) 88 | } 89 | 90 | "retry successfully n times" in { 91 | startTest(3, 3, 3) 92 | expectMsg(6) 93 | } 94 | 95 | "fail after n+1 tries" in { 96 | startTest(3, 4, 3) 97 | expectMsg(GaveUp) 98 | } 99 | 100 | "fail if retry isn't attempted" in { 101 | startTest(3, 1, 0) 102 | expectMsg(GaveUp) 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Querki 3 | ################# 4 | 5 | .target 6 | testdb/ 7 | *.db 8 | application.conf 9 | target/ 10 | project/target/ 11 | 12 | ################# 13 | ## Eclipse 14 | ################# 15 | 16 | *.pydevproject 17 | .project 18 | .metadata 19 | bin/ 20 | tmp/ 21 | *.tmp 22 | *.bak 23 | *.swp 24 | *~.nib 25 | local.properties 26 | .classpath 27 | .settings/ 28 | .loadpath 29 | 30 | # External tool builders 31 | .externalToolBuilders/ 32 | 33 | # Locally stored "Eclipse launch configurations" 34 | *.launch 35 | 36 | # CDT-specific 37 | .cproject 38 | 39 | # PDT-specific 40 | .buildpath 41 | 42 | ################# 43 | ## gEdit 44 | ################# 45 | 46 | *~ 47 | 48 | ################# 49 | ## IntelliJ IDEA 50 | ################# 51 | 52 | .idea 53 | 54 | ################# 55 | ## Visual Studio 56 | ################# 57 | 58 | ## Ignore Visual Studio temporary files, build results, and 59 | ## files generated by popular Visual Studio add-ons. 60 | 61 | # User-specific files 62 | *.suo 63 | *.user 64 | *.sln.docstates 65 | 66 | # Build results 67 | [Dd]ebug/ 68 | [Rr]elease/ 69 | *_i.c 70 | *_p.c 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.pch 75 | *.pdb 76 | *.pgc 77 | *.pgd 78 | *.rsp 79 | *.sbr 80 | *.tlb 81 | *.tli 82 | *.tlh 83 | *.vspscc 84 | .builds 85 | *.dotCover 86 | 87 | ## TODO: If you have NuGet Package Restore enabled, uncomment this 88 | #packages/ 89 | 90 | # Visual C++ cache files 91 | ipch/ 92 | *.aps 93 | *.ncb 94 | *.opensdf 95 | *.sdf 96 | 97 | # Visual Studio profiler 98 | *.psess 99 | *.vsp 100 | 101 | # ReSharper is a .NET coding add-in 102 | _ReSharper* 103 | 104 | # Installshield output folder 105 | [Ee]xpress 106 | 107 | # DocProject is a documentation generator add-in 108 | DocProject/buildhelp/ 109 | DocProject/Help/*.HxT 110 | DocProject/Help/*.HxC 111 | DocProject/Help/*.hhc 112 | DocProject/Help/*.hhk 113 | DocProject/Help/*.hhp 114 | DocProject/Help/Html2 115 | DocProject/Help/html 116 | 117 | # Click-Once directory 118 | publish 119 | 120 | # Others 121 | [Bb]in 122 | [Oo]bj 123 | sql 124 | TestResults 125 | *.Cache 126 | ClientBin 127 | stylecop.* 128 | ~$* 129 | *.dbmdl 130 | Generated_Code #added for RIA/Silverlight projects 131 | 132 | # Backup & report files from converting an old project file to a newer 133 | # Visual Studio version. Backup files are not needed, because we have git ;-) 134 | _UpgradeReport_Files/ 135 | Backup*/ 136 | UpgradeLog*.XML 137 | 138 | 139 | 140 | ############ 141 | ## Windows 142 | ############ 143 | 144 | # Windows image file caches 145 | Thumbs.db 146 | 147 | # Folder config file 148 | Desktop.ini 149 | 150 | 151 | ############# 152 | ## Python 153 | ############# 154 | 155 | *.py[co] 156 | 157 | # Packages 158 | *.egg 159 | *.egg-info 160 | dist 161 | build 162 | eggs 163 | parts 164 | bin 165 | var 166 | sdist 167 | develop-eggs 168 | .installed.cfg 169 | 170 | # Installer logs 171 | pip-log.txt 172 | 173 | # Unit test / coverage reports 174 | .coverage 175 | .tox 176 | 177 | #Translations 178 | *.mo 179 | 180 | #Mr Developer 181 | .mr.developer.cfg 182 | 183 | # Mac crap 184 | .DS_Store 185 | /.cache-main 186 | /.cache-tests 187 | -------------------------------------------------------------------------------- /src/test/scala/org/querki/requester/ComprehensionTests.scala: -------------------------------------------------------------------------------- 1 | package org.querki.requester 2 | 3 | import scala.util.{Try,Success,Failure} 4 | 5 | import akka.actor._ 6 | 7 | /** 8 | * @author jducoeur 9 | */ 10 | 11 | case class BoomException(msg:String) extends Exception(msg) 12 | 13 | object ComprehensionTests { 14 | case object Start 15 | case class Response(msg:String) 16 | case object Hello 17 | case object There 18 | case object World 19 | 20 | class Answering extends QTestActor { 21 | def doReceive = { 22 | case Hello => sender ! Response("Hello") 23 | case There => sender ! Response(" there,") 24 | case World => sender ! Response(" world!") 25 | } 26 | } 27 | 28 | class Asking extends QTestActor { 29 | 30 | lazy val answers = context.actorOf(Props(classOf[Answering])) 31 | 32 | def doReceive = { 33 | case Start => { 34 | for { 35 | // Note that this tests ask-style syntax: 36 | Response(hello) <- answers ? Hello 37 | Response(there) <- answers ? There 38 | Response(world) <- answers ? World 39 | } 40 | sender ! hello + there + world 41 | } 42 | } 43 | } 44 | 45 | class RawExponent extends QTestActor { 46 | def doReceive = { 47 | case Start => { 48 | for { 49 | four <- doubler.requestFor[Int](2) 50 | eight <- doubler.requestFor[Int](four) 51 | sixteen <- doubler.requestFor[Int](eight) 52 | } 53 | sender ! sixteen 54 | } 55 | } 56 | } 57 | 58 | class MapExponent extends QTestActor { 59 | def doReceive = { 60 | case Start => { 61 | val rm = for { 62 | four <- doubler.requestFor[Int](2) 63 | eight <- doubler.requestFor[Int](four) 64 | sixteen <- doubler.requestFor[Int](eight) 65 | } 66 | yield sixteen 67 | 68 | rm foreach { sixteen => sender ! sixteen } 69 | } 70 | } 71 | } 72 | 73 | case class StartWith(errorP:Boolean) 74 | 75 | class CompleteExponent extends QTestActor { 76 | def doReceive = { 77 | case StartWith(errorP) => { 78 | val rm = for { 79 | four <- doubler.requestFor[Int](2) 80 | eight <- doubler.requestFor[Int](four) 81 | sixteen <- doubler.requestFor[Int](eight) 82 | dummy = if (errorP) throw new BoomException("BOOM") 83 | } 84 | yield sixteen 85 | 86 | rm onComplete { 87 | case Success(sixteen) => sender ! sixteen 88 | case Failure(ex) => sender ! ex 89 | } 90 | } 91 | } 92 | } 93 | 94 | class RecoverExponent extends QTestActor { 95 | def doReceive = { 96 | case Start => { 97 | val rm = for { 98 | four <- doubler.requestFor[Int](2) 99 | eight <- doubler.requestFor[Int](four) 100 | boom <- RequestM.failed[Int](new BoomException("Boom")) 101 | sixteen <- doubler.requestFor[Int](eight) 102 | } 103 | yield sixteen 104 | 105 | val recovered = rm recover { 106 | case BoomException(msg) => -1 107 | } 108 | 109 | recovered onComplete { 110 | case Success(negOne) => sender ! negOne 111 | case Failure(ex) => sender ! ex 112 | } 113 | } 114 | } 115 | } 116 | 117 | class RecoverWithExponent extends QTestActor { 118 | def doReceive = { 119 | case Start => { 120 | val rm = for { 121 | four <- doubler.requestFor[Int](2) 122 | eight <- doubler.requestFor[Int](four) 123 | boom <- RequestM.failed[Int](new BoomException("Boom")) 124 | sixteen <- doubler.requestFor[Int](eight) 125 | } 126 | yield sixteen 127 | 128 | val recovered = rm recoverWith { 129 | case BoomException(msg) => RequestM.successful(-1) 130 | } 131 | 132 | recovered onComplete { 133 | case Success(negOne) => sender ! negOne 134 | case Failure(ex) => sender ! ex 135 | } 136 | } 137 | } 138 | } 139 | 140 | case class Terms(seed:Int, exp:Int) 141 | 142 | class Exponent extends QTestActor { 143 | def doReceive = { 144 | case Terms(seed, exp) => askDoubler(seed, exp) foreach { result => sender ! result } 145 | } 146 | 147 | def askDoubler(seed:Int, exp:Int):RequestM[Int] = { 148 | if (exp == 1) { 149 | RequestM.successful(seed) 150 | } else { 151 | doubler.requestFor[Int](seed) flatMap { doubled => 152 | askDoubler(doubled, exp - 1) 153 | } 154 | } 155 | } 156 | } 157 | } 158 | 159 | class ComprehensionTests extends RequesterTests { 160 | 161 | import ComprehensionTests._ 162 | 163 | "Asker" should { 164 | "be able to use a for comprehension" in { 165 | val asker = system.actorOf(Props(classOf[Asking])) 166 | asker ! Start 167 | expectMsg("Hello there, world!") 168 | } 169 | } 170 | 171 | "Requester" should { 172 | "be able to flatMap manually" in { 173 | val exp = system.actorOf(Props(classOf[RawExponent])) 174 | exp ! Start 175 | expectMsg(16) 176 | } 177 | 178 | "be able to map using yield" in { 179 | val exp = system.actorOf(Props(classOf[MapExponent])) 180 | exp ! Start 181 | expectMsg(16) 182 | } 183 | 184 | "be able to use onComplete for success" in { 185 | val exp = system.actorOf(Props(classOf[CompleteExponent])) 186 | exp ! StartWith(false) 187 | expectMsg(16) 188 | } 189 | 190 | "be able to use onComplete for failure" in { 191 | val exp = system.actorOf(Props(classOf[CompleteExponent])) 192 | exp ! StartWith(true) 193 | val ex = expectMsgClass(dur, classOf[BoomException]) 194 | assert(ex.getMessage == "BOOM") 195 | } 196 | 197 | "be able to recursively flatMap" in { 198 | val exp = system.actorOf(Props(classOf[Exponent])) 199 | exp ! Terms(2,4) 200 | expectMsg(16) 201 | } 202 | 203 | "be able to use recover for failure" in { 204 | val exp = system.actorOf(Props(classOf[RecoverExponent])) 205 | exp ! Start 206 | expectMsg(-1) 207 | } 208 | 209 | "be able to use recoverWith for failure" in { 210 | val exp = system.actorOf(Props(classOf[RecoverWithExponent])) 211 | exp ! Start 212 | expectMsg(-1) 213 | } 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Requester 2 | 3 | One of the most useful, and most fraught, functions in Akka is `ask`. On the one hand, it is invaluable for writing clear, understandable code -- after all, the pattern of "Send this message, and then do that with the result" makes loads of sense, and it's usually easy to see what the code is trying to do. 4 | 5 | On the other hand, `ask` is something of a landmine in practice, because it violates the most important invariant of Actors: there should never be code lexically inside of the Actor that doesn't run within the Actor's receive loop. `ask` returns a Future, and that Future will be run out-of-band, at some random time in the future. It can happen in parallel with running receive code, or not. There is lots of danger there, and it is easy to cause erratic, hard-to-reproduce bugs. 6 | 7 | (Not to mention the fact that `sender` probably won't be set to the value you expect during the response handler. One of the easiest Akka traps to fall into is using `sender` during a Future, which almost never works.) 8 | 9 | This library introduces `request`, which you can think of as the better-behaved big brother of `ask`. The look and feel is similar, but there is one crucial difference: the response handler from `request` is *not* Future, and it runs inside the normal receive loop. Also, unlike `ask`, `request` preserves the value of `sender`. The result is that you can safely write straightforward, intuitive, composable code for complex multi-Actor operations, like this: 10 | ```scala 11 | case GetSpacesStatus(requester) => { 12 | for { 13 | ActiveThings(nConvs) <- conversations ? GetActiveThings 14 | ActiveSessions(nSessions) <- sessions ? GetActiveSessions 15 | } 16 | sender ! SpaceStatus(spaceId, state.displayName, nConvs, nSessions) 17 | } 18 | ``` 19 | and have it work just as you expect. 20 | 21 | ### Installing Requester 22 | 23 | To use Requester, add this to your libraryDependencies in sbt: 24 | ``` 25 | "org.querki" %% "requester" % "2.7" 26 | ``` 27 | 28 | This is for Akka 2.5. If you are still on Akka 2.4, use Requester release 2.6 instead. 29 | 30 | (We don't have a 2.6 branch yet; we'll get there eventually. But Requester 31 | as currently designed is aimed at Classic Akka, not Akka Typed, so I expect 32 | this library to have a limited lifespan at this point.) 33 | 34 | ### Using Requester 35 | 36 | The most common and straightforward use case for Requester is when you have one Actor that wants to make requests of others. You enhance the *requesting* Actor (not the target!) with the Requester trait: 37 | ```scala 38 | import org.querki.requester._ 39 | 40 | class MyActor extends Actor with Requester { 41 | ... your usual code ... 42 | } 43 | ``` 44 | Once you've done that, you can write code like the example: 45 | ```scala 46 | case GetSpacesStatus(requester) => { 47 | for { 48 | ActiveThings(nConvs) <- conversations ? GetActiveThings 49 | ActiveSessions(nSessions) <- sessions ? GetActiveSessions 50 | } 51 | sender ! SpaceStatus(spaceId, state.displayName, nConvs, nSessions) 52 | } 53 | ``` 54 | (All examples are real code from [Querki](https://www.querki.net/), sometimes a bit simplified.) 55 | 56 | Or if you don't need to compose, just use `foreach`: 57 | ```scala 58 | persister.request(LoadCommentsFor(thingId, state)) foreach { 59 | case AllCommentsFor(_, comments) => { 60 | val convs = { 61 | val cs = buildConversations(comments) 62 | loadedConversations += (thingId -> cs) 63 | cs 64 | } 65 | f(convs) 66 | } 67 | } 68 | ``` 69 | `request` returns a RequestM, which is a monad that cheats a little -- it's actually slightly mutable, so don't assume perfectly-monadic behavior, but it works as expected inside a for comprehension, and deliberately mimics the core behavior of Future. The functions `map`, `flatMap`, `foreach`, `withFilter` and `onComplete` all work pretty much the same as they do in Future, and other methods of Future will likely be added over time. 70 | 71 | **Important:** since `request` is intended to replace `ask`, you should *not* be importing `akka.pattern._` (which is where `ask` comes from). If you have both imported, you will get a name conflict on `?`, and will have to use the wordier `request()` call instead. 72 | 73 | ### How it works 74 | 75 | `request` actually uses `ask` under the hood, but in a very precise and constrained way. It sends your message to the target Actor and gets the response via `ask`. However, it then loops that response (plus the handler) back as a message to this Actor, preserving the original value of `sender`. This way, the response is relatively safe to use (since it is being processed within the Actor's main receive function), and you still have the sender you expect. 76 | 77 | Note that the native function for Requester is `request()`. This is aliased to `?`, to mimic `akka.pattern.ask`. The name conflict is entirely intentional: using ask inside of a Requester is *usually* a bug, since it invites all sorts of accidental dangers. If you really want to use `ask()` inside of a Requester, do it with the full name. 78 | 79 | ### handleRequestResponse 80 | 81 | Normally, Requester deals with this loopback automatically, by overriding `unhandled()`. However, in some exceptional cases this doesn't work -- in particular, if your receive function handles *all* messages, the loopback will never get to unhandled, so it will never get resolved. This can happen, for example, when using `stash()` aggressively during setup, stashing all messages until the Actor is fully initialized. 82 | 83 | In cases like this, you should put `handleRequestResponse` at the front of your receive function, like this: 84 | ```scala 85 | def receive = handleRequestResponse orElse { 86 | case Start => { 87 | persister.requestFor[LoadedState](LoadMe(myId)) foreach { currentState => 88 | setState(currentState) 89 | unstashAll() 90 | become(normalReceive) 91 | } 92 | } 93 | 94 | case _ => stash() 95 | } 96 | ``` 97 | In this example, if we didn't have handleRequestResponse there, the response to `LoadMe` would get stashed along with everything else, never processed, and the Actor would simply hang in its Start state. But putting handleRequestResponse at the front deals with the loopbacks before that stash, so everything works. 98 | 99 | ### `request` and Futures 100 | 101 | In ordinary Akka, the above is usually enough: you usually use the results of your request-based computation to set local state in the Requester, or to send a response, typically back to `sender`. Occasionally, though, you may want to wrap the whole thing up into a Future -- this is particularly common when you are writing client/server RPC code, using Scala.js on the front end, [Autowire](https://github.com/lihaoyi/autowire) for the API communication, and Akka Actors implementing the back-end server implementation. 102 | 103 | For a case like this, there is an implicit conversion from RequestM[T] to Future[T], so you can write code like this: 104 | ```scala 105 | def doChangeProps(thing:Thing, props:PropMap):Future[PropertyChangeResponse] = { 106 | self.request(createSelfRequest(ChangeProps2(thing.toThingId, props))) map { 107 | case ThingFound(_, _) => PropertyChanged 108 | case ThingError(ex, _) => throw new querki.api.GeneralChangeFailure("Error during save") 109 | } 110 | } 111 | ``` 112 | Note that the bulk of the code is doing a `request`, so it is returning a `RequestM[PropertyChangeResponse]`. Since we are in a context that expects a `Future[PropertyChangeResponse]`, the system implicitly does the conversion, and it works as you want it to. 113 | 114 | ### `requestFor` 115 | 116 | The ordinary `request` call returns Any, as is usual in Akka. Sometimes, it is clearer to be able to state upfront what type you expect to receive, especially if there is only one "right" answer. `requestFor` is a variant of `request` that allows you to state this: 117 | ```scala 118 | notePersister.requestFor[CurrentNotifications](Load) foreach { notes => 119 | currentNotes = notes.notes.sortBy(_.id).reverse 120 | 121 | // Okay, we're ready to roll: 122 | self ! InitComplete 123 | } 124 | ``` 125 | This makes the whole thing more strongly-typed upfront -- in the above code, the compiler knows that `notes` should be a `CurrentNotifications` message. If anything other than `CurrentNotifications` is received, it will throw a `ClassCastException`. 126 | 127 | ### Retries 128 | 129 | `request` and `requestFor` actually have a second parameter -- the number of times to retry sending this request. Akka is explicitly unreliable: that's not as obvious when running on a single-node system (where message delivery usually succeeds), but when running multi-node that can be quite important. So it is *sometimes* appropriate to retry message sends once or twice when they fail. 130 | 131 | You can specify a retry simply by adding the parameter, like this: 132 | ```scala 133 | myTarget.requestFor[Loaded](Load, 2) foreach { loaded => ... 134 | ``` 135 | This says that, if the request times out, Requester should retry up to 2 times. If it continues to fail, after the last retry the RequestM will fail with an `AskTimeoutException`, as usual. 136 | 137 | **Important:** retries aren't a panacea, and you can get yourself into real trouble with them if you're not careful. All this is doing is retrying if we don't get a *response*. That doesn't necessarily mean the request didn't get through. It could be that the response failed to get through, or that the request somehow caused an exception, or that that code pathway is broken, or that it's simply taking a long time. This easy retry mechanism can be helpful, but is not by any means the same thing as reliable message delivery. Think through the whole request/response cycle before using it -- in particular, whether getting a duplicate request is going to confuse the recipient. 138 | 139 | ### loopback() 140 | 141 | Requester is primarily focused on the common case where you are trying to send a request to another Actor within receive. But what if you want to use a plain old Future instead? That is where the loopback() function comes in. 142 | 143 | loopback() takes one parameter, a Future, and treats it like a Request -- it wraps the Future inside of a RequestM, so that when the Future completes, it will execute the subsequent functions (foreach and map) looped back in the main loop of the Actor. As with an ordinary Request, sender is preserved across this operation. 144 | 145 | loopback() is implicit, so if the code already expects a RequestM (for instance, inside of a for comprehension that is led off by a request()), you can just use a Future and it will auto-convert to RequestM. For example: 146 | ```scala 147 | for { 148 | // This returns a RequestM 149 | ThingConversations(convs) <- spaceRouter.requestFor[ThingConversations](ConversationRequest(rc.requesterOrAnon, rc.state.get.id, GetConversations(rc.thing.get.id))) 150 | // This returns a Future 151 | identities <- IdentityAccess.getIdentities(getIds(convs).toSeq) 152 | } 153 | ... 154 | ``` 155 | Since this starts off with requestFor, the for comprehension is expecting RequestM; the Future returned from getIdentities automatically gets looped back, keeping everything running safely inside the Actor's receive loop. 156 | 157 | Note however, this can be a bit fragile -- it is easy to forget to put a request at the top of the comprehension, so everything winds up using dangerous raw Futures. So use this implicit with some care. 158 | 159 | ### RequesterImplicits 160 | 161 | The above is all fine so long as you are sending your requests from directly inside the Requester. But what if your structure is more complex -- say, you have your handlers broken up into a number of classes instantiated by and running under the Requester? (For example, in Querki, we have a single primary UserSpaceSession Actor that mediates all normal client/server requests. Each request then causes an appropriate handler object to be created, running under that Actor's receive loop, which actually deals with the functions in question.) 162 | 163 | If you want one of these outside classes to be able to use `request`, then you should have it inherit from the RequesterImplicits trait, and override that trait's `requester` member to point to the controlling Requester Actor. 164 | 165 | RequesterImplicits actually defines `request`, `requestFor` and `requestFuture`; it delegates the actual processing to the associated Requester. (Requester itself implements RequesterImplicits, so you can just ignore this for the ordinary case.) 166 | 167 | ### `Promise`-like Constructions 168 | 169 | The above covers *most* situations where you want to use RequestM. But what about the down-and-dirty situations, where you have to want to compose complex but relatively primitive interactions safely? These are the sorts of situations where, if you were using `Future`s, you would need to bring `Promise` into play. The corresponding concepts in Requester are the `prep()` and `resolve()` methods. 170 | 171 | `RequestM.prep[T]()` creates a new, raw `RequestM[T]`, containing no value and not in any conventional workflow -- it's just a `RequestM`. Sometime after you have that, you call `.resolve(v:Try[T])` on that `RequestM`, to set its value and (more importantly) fire off any subsequent requests that have been mapped onto it. 172 | 173 | You would typically use this in situations where need to pass a `RequestM` around externally-defined walls. For example, Akka's `PersistentActor` centers on the method `persist[T](evt:T)(handler:T => Unit):Unit`. Note that this returns a Unit. So how do you compose this operation? How do I return something that allows other functions to do something *after* my handler? You would do that like this: 174 | ```scala 175 | def myPersist[T, U](evtIn:T)(handler:T => U):RequestM[U] = { 176 | val rm = RequestM.prep[U] 177 | persist(evtIn) { evt => 178 | val result = handler(evt) 179 | rm.resolve(Success(result)) 180 | } 181 | rm 182 | } 183 | ``` 184 | This persists the event as usual, but returns a `RequestM` that can be mapped and flatMapped, and those mappings will execute immediately after the handler executes. That way, you can build composable flows. (This is especially useful in Akka Persistence when you have multiple commands that can result in the same event, but want somewhat different responses afterwards.) 185 | 186 | It's reasonable to ask, "So why not just use `Promise` and `Future` here?" That's entirely an option. Using `prep`/`resolve` has two advantages: 187 | * Since it is using a RequestM, it combines well with other RequestM-based code, and doesn't require polluting your Actor with potentially dangerous Futures. 188 | * Any additional functions that are mapped onto the returned `RequestM` are guaranteed to be executed synchronously in the same thread where `resolve` was called, so the threading is safe and predictable. 189 | 190 | It is your responsibility to only call `resolve` in an "Actor-safe" thread -- during a `receive`, a `persist` handler, or some other safely single-threaded point. 191 | 192 | Note that `resolve` takes a `Try[T]` -- you can use it to resolve either `Success` or `Failure`. 193 | 194 | `resolve` is the heart of Requester, and is used internally to complete requests -- the conventional Requester workflow is to send an `ask`, loop the response back into the requester's `receive` function, and pass that response into `resolve`. Similarly, `RequestM.successful()` and `.failure()` simply create a `RequestM` and immediately call `resolve` on it. Thus, you can use `resolve` to build other, high-level constructs on top of `RequestM`. 195 | 196 | ### Exceptions and Stack Traces 197 | 198 | As of 2.5, Requester incorporates ideas inspired by [this blog post](http://www.schibsted.pl/blog/tracing-back-scala-future-chains/), to do a quick-and-dirty recording of the call sites. It's by no means comprehensive, but it's cheap enough to be reasonable to do automatically. 199 | 200 | If the Request throws an exception, this information is then appended to the end of the call stack -- essentially, you can see the RequestM traces all the way up to the top. It's not nearly as complete as a true JVM stack trace, but it can make it a bit easier to track down what the heck is going on when you get an Exception six Request levels down. This is especially valuable when you are composing complex constructions using RequestM. 201 | 202 | ### Caveats 203 | 204 | Because of the loopback, request necessarily increases the latency of processing a request. This increase is typically slight (since it sends a message locally to the same Actor), but in a heavily-loaded Actor it could become non-trivial. 205 | 206 | Requester is powerful, and brings you back into the land of Akka sanity, but it isn't a panacea. In particular, remember that your `request` response handler will *always* be run asynchronously, in a later run through receive. The state of your Actor may well have changed since you sent your message -- be sure to keep that in mind when you are writing your response handler. 207 | 208 | Also, for the same reasons, using Requester with frequent `become` operations or with FSM is pretty fraught. While it isn't necessarily incompatible, I recommend using caution if you are trying to combine these concepts. (This is no different from usual, though: FSM always requires care and thought about what you want to have happen when an obsolete request comes back.) 209 | 210 | While Requester is being used heavily in production at Querki, nobody else has used it as of this writing. Please give a yell if you come across bugs, and pull requests are welcomed. 211 | 212 | ### To Do 213 | 214 | Requester clearly ought to pair well with [Typed Actors](http://doc.akka.io/docs/akka/2.3.9/scala/typed-actors.html), but some surgery will be needed. (Unless Typed Actors do this loopback automatically under the hood, in which case Requester isn't necessary.) Basically, we need to extend Requester to have a straightforward way to interpret any Future-producing function (not just ask) as a RequestM, automatically sussing the type that is implicit in the Future, and looping it back as normal. In principle this isn't difficult, but we need to think about how to minimize the boilerplate. 215 | 216 | One possibility for the above: create a new implicit ExecutionContext, available on any Requester, which executes *all* Futures as loopbacks. In principle this seems like it would work in the general case, and would be an enormous win -- if we can do that, then RequestM might be able to go away, and you could simply do ordinary Future-based programming that would work properly. This is the ideal case, but needs more research to figure out if it is actually possible. 217 | 218 | At the moment, the timeout for requests is built into Requester as a member, instead of being an implicit to functions the way Futures usually work. This is very convenient, but I worry that it's too coarse-grained. We should think about whether it needs to be changed. 219 | 220 | I am pretty sure that withFilter() doesn't do the right thing yet. It needs to be adjusted so that its behavior matches that of Future. 221 | 222 | More unit tests are needed, especially around failure management. 223 | 224 | ### Change log 225 | 226 | * **2.7** -- Upgraded to Akka 2.5. Added Scala 2.12 and 2.13 support; dropped 2.10 support. 227 | 228 | * **2.6** -- Added `recover()` and `recoverWith()` to RequestM. These work as you would expect them to from Future. 229 | 230 | * **2.5** -- Added the pseudo-stack-trace support described above under "Exceptions and Stack Traces". This adds a dependency to @lihaoyi's sourcecode library, but that should be cheap enough to not matter too much. 231 | 232 | * **2.4** -- Added support for `prep()` and `resolve()`. `resolve` has always been there -- it's central to the system -- but had previously been private. It is now opened up, to allow `Promise`-like code. 233 | 234 | * **2.1** -- If a Request is being auto-converted to a Future, Exceptions now propagate from the Request to the Future. request() and requestFor() now work with ActorSelection as well as ActorRef. Fixed the unwinding of nested flatMaps to work tail-recursively. (Previously, if you nested a *lot* of flatMaps together, they could throw a StackOverflow while unwinding at the end.) 235 | 236 | * **2.0** -- Improved RequestM to make it compose properly, so you can mostly treat it as you expect from Futures. Added onComplete, so you can handle failures. Added an implicit to convert RequestM[T] to Future[T], which makes interoperability with Futures much easier, and removed the clunky requestFuture mechanism. unhandled() now deals with loopbacks, so you can usually just mix Requester in with no other changes and have it work. Added ? as a syntax for request, specifically to help prevent accidentally mixing the unsafe ask into a Requester. 237 | 238 | ### License 239 | 240 | Copyright (c) 2017 Querki Inc. (justin at querki dot net) 241 | 242 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 243 | 244 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 245 | 246 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 247 | -------------------------------------------------------------------------------- /src/main/scala/org/querki/requester/Requester.scala: -------------------------------------------------------------------------------- 1 | package org.querki.requester 2 | 3 | import scala.annotation.tailrec 4 | import scala.concurrent.duration._ 5 | import scala.concurrent.{Future, Promise} 6 | import scala.util.{Try,Success,Failure} 7 | import scala.reflect.ClassTag 8 | 9 | import akka.actor._ 10 | import akka.pattern.{ask, AskTimeoutException} 11 | import akka.util.Timeout 12 | 13 | /** 14 | * The request "monad". It's actually a bit nasty, in that it's mutable, but by and 15 | * large this behaves the way you expect a monad to work. In particular, it works with for 16 | * comprehensions, allowing you to compose requests much the way you normally do Futures. 17 | * But since it is mutable, it should never be used outside the context of an Actor. 18 | */ 19 | class RequestM[T](val enclosing:sourcecode.FullName, val method:sourcecode.Name, val file:sourcecode.File, val line:sourcecode.Line) 20 | { 21 | /** 22 | * The actions to take after this Request resolves. 23 | * 24 | * Yes, this is mutable. That's arguably sad and evil, but remember that this is only intended 25 | * for use inside the pseudo-single-threaded world of an Actor. 26 | */ 27 | private var callbacks = List.empty[Function[Try[T], _]] 28 | 29 | /** 30 | * Store the result, so that if someone hooks a callback into me after I'm resolved, I 31 | * can handle that immediately. (This can happen if, for example, RequestM.successful() 32 | * comes into play.) 33 | * 34 | * Yes, also mutable. Same argument. Deal. 35 | */ 36 | private var result:Option[Try[T]] = None 37 | 38 | /** 39 | * Build up a chain of RequestM's, from lower levels to higher, so that we can unwind results 40 | * iteratively instead of through the callbacks. 41 | * 42 | * This is rather ugly, but necessary. The original design did the unwinding completely cleanly, 43 | * with each inner RequestM propagating its result by calling resolve() on the level above it. 44 | * But that turned out to cause stack overflow crashes when you got to ~2000 levels of flatMap(). 45 | * So we need a mechanism that can be handled iteratively (well, tail-recursively) instead. 46 | */ 47 | protected var higherLevel:Option[RequestM[T]] = None 48 | protected def setHigherLevel(higher:RequestM[T]) = { 49 | // Note that sometimes this gets called *after* it has already resolved. That will typically 50 | // happen on the innermost flatMap(), which contains a synchronous map() that produces the 51 | // end result. So we need to detect that and trigger unwinding when it happens. 52 | result match { 53 | case Some(res) => { 54 | setFailureStack(res) 55 | unwindHigherLevels(res, Some(higher)) 56 | } 57 | case None => higherLevel = Some(higher) 58 | } 59 | } 60 | @tailrec 61 | private def unwindHigherLevels(v:Try[T], currentLevel:Option[RequestM[T]]):Unit = { 62 | currentLevel match { 63 | case Some(higher) => { 64 | // Resolve the outer layers, but do *not* start more unwinding. This is key: 65 | higher.resolve(v, false) 66 | unwindHigherLevels(v, higher.higherLevel) 67 | } 68 | case None => // We're done unwinding, or didn't need to do it at all 69 | } 70 | } 71 | 72 | @tailrec 73 | private def buildFailureStackRec(rm:RequestM[_], stack:List[StackTraceElement]):List[StackTraceElement] = { 74 | val withMe = new StackTraceElement(rm.enclosing.value, rm.method.value, rm.file.value, rm.line.value) :: stack 75 | rm.higherLevel match { 76 | case Some(higher) => buildFailureStackRec(higher, withMe) 77 | case None => withMe 78 | } 79 | } 80 | private def setFailureStack(t:Try[T]):Unit = { 81 | t match { 82 | case Success(_) => 83 | case Failure(ex) => { 84 | val failStack = buildFailureStackRec(this, List.empty).reverse.toVector 85 | val originalStack = ex.getStackTrace.toVector 86 | val fullStack = originalStack ++ failStack 87 | ex.setStackTrace(fullStack.toArray) 88 | } 89 | } 90 | } 91 | 92 | private [requester] def intercept(test:PartialFunction[Try[T], Boolean]) = _blocker = Some(test) 93 | private var _blocker:Option[PartialFunction[Try[T], Boolean]] = None 94 | 95 | /** 96 | * Set the value for this RequestM, and run through its dependency chain. 97 | * 98 | * You should *not* normally call this yourself -- it is mainly for internal use. Call this only 99 | * when you have obtained a RequestM through prep(), and need to finish it up, usually because 100 | * you have to work around some other concurrency construct. (Such as PersistentActor's persist() 101 | * call.) 102 | * 103 | * You should ignore the startUnwind flag, which is for internal use only. 104 | */ 105 | def resolve(v:Try[T], startUnwind:Boolean = true):Unit = { 106 | // We give the outside world a chance to prevent this from going through, in order to let the 107 | // retry system work. 108 | // TODO: this is ugly, and shouldn't be necessary if the functional interface was sufficiently 109 | // rich. Think about how to make retries work differently. 110 | val blocked = _blocker match { 111 | case Some(blocker) => blocker(v) 112 | case None => false 113 | } 114 | if (!blocked) { 115 | result = Some(v) 116 | try { 117 | if (startUnwind) { 118 | setFailureStack(v) 119 | } 120 | callbacks foreach { cb => cb(v) } 121 | if (startUnwind) { 122 | unwindHigherLevels(v, higherLevel) 123 | } 124 | } catch { 125 | case th:Throwable => { 126 | // TODO: Is there anything useful we can/should do to propagate this error? 127 | println(s"Exception while resolving Request: ${th.getMessage}") 128 | th.printStackTrace() 129 | } 130 | } 131 | } 132 | } 133 | 134 | def onComplete[U](handler: (Try[T]) => U):Unit = { 135 | result match { 136 | case Some(v) => handler(v) 137 | case None => callbacks :+= handler 138 | } 139 | } 140 | 141 | // TODO: This should probably become onSuccess 142 | def handleSucc(handler:T => _):Unit = { 143 | onComplete { t:Try[T] => 144 | t match { 145 | case Success(v) => handler(v) 146 | case Failure(ex) => 147 | } 148 | } 149 | } 150 | 151 | def foreach(handler:T => Unit):Unit = { 152 | handleSucc(handler) 153 | } 154 | 155 | def map[U](handler:T => U)(implicit enclosing:sourcecode.FullName, file:sourcecode.File, line:sourcecode.Line):RequestM[U] = { 156 | // What's going on here? I need to synchronously return a new RequestM, but I won't 157 | // actually complete until sometime later. So when I *do* complete, pipe that result 158 | // into the given handler function, and use that to resolve the returned child. 159 | val child:RequestM[U] = new RequestM(enclosing, "map", file, line) 160 | onComplete { 161 | case Success(v) => { 162 | try { 163 | val result = handler(v) 164 | child.resolve(Success(result)) 165 | result 166 | } catch { 167 | case th:Throwable => child.resolve(Failure(th)) 168 | } 169 | } 170 | case Failure(ex) => child.resolve(Failure(ex)) 171 | } 172 | child 173 | } 174 | 175 | /** 176 | * The central flatMap() operation, which as in any Monad is key to composing these 177 | * things together. 178 | * 179 | * flatMap() is a bit tricky. The problem we have is that we need to return a RequestM 180 | * *synchronously* from flatMap, so that higher-level code can compose on it. But 181 | * the *real* RequestM being returned from handler won't come into existence until 182 | * some indefinite time in the future. So we need to create a new one right now, 183 | * and when the real one comes into existence, link its success to that of the one 184 | * we're returning here. 185 | * 186 | * The initial version of this was beautiful, elegant, and caused stack overflows if 187 | * you nested flatMaps more than a couple thousand levels deep. (Which, yes, we occasionally do at 188 | * Querki.) The issue comes during "unwinding" time, when the innermost RequestM finally gets 189 | * set to a value. The original version had it then call resolve() on the one that contained it, 190 | * which called resolve() on its parent, and so on, until we finally blew the stack. 191 | * 192 | * So instead, flatMap builds an ugly but practical linked list of RequestM's, with each one 193 | * essentially pointing to the one above it. We still call resolve() at each level, but those 194 | * are *not* recursive; instead, we walk up the flatMap chain *tail*-recursively, resolving each node 195 | * along the way. It's a bit less elegant, but doesn't cause the JVM to have conniptions. 196 | */ 197 | def flatMap[U](handler:T => RequestM[U])(implicit enclosing:sourcecode.FullName, file:sourcecode.File, line:sourcecode.Line):RequestM[U] = { 198 | val child:RequestM[U] = new RequestM(enclosing, "flatMap", file, line) 199 | onComplete { 200 | case Success(v) => { 201 | try { 202 | val subHandler = handler(v) 203 | // Note that flatMap specifically does *not* resolve this through onComplete any more. 204 | // The commented-out line worked well, and was nicely elegant, but resulted in stack 205 | // overflows. So instead we build an explicit chain, and unwind that way. 206 | subHandler.setHigherLevel(child) 207 | // subHandler.onComplete { u:Try[U] => child.resolve(u) } 208 | subHandler 209 | } catch { 210 | case th:Throwable => { child.resolve(Failure(th)) } 211 | } 212 | } 213 | case Failure(ex) => child.resolve(Failure(ex)) 214 | } 215 | child 216 | } 217 | 218 | def filter(p:T => Boolean)(implicit enclosing:sourcecode.FullName, file:sourcecode.File, line:sourcecode.Line):RequestM[T] = { 219 | val filtered = new RequestM[T](enclosing, "filter", file, line) 220 | val filteringCb:Function[T,_] = { v:T => 221 | if (p(v)) { 222 | filtered.resolve(Success(v)) 223 | } 224 | } 225 | handleSucc(filteringCb) 226 | filtered 227 | } 228 | 229 | def withFilter(p:T => Boolean):RequestM[T] = filter(p) 230 | 231 | def recover[U >: T](pf: PartialFunction[Throwable, U]): RequestM[U] = { 232 | val child:RequestM[U] = new RequestM(enclosing, "recover", file, line) 233 | onComplete { 234 | case Success(v) => { 235 | try { 236 | child.resolve(Success(v)) 237 | } catch { 238 | case th:Throwable => child.resolve(Success(pf(th))) 239 | } 240 | } 241 | case Failure(ex) => child.resolve(Success(pf(ex))) 242 | } 243 | child 244 | } 245 | 246 | def recoverWith[U >: T](pf: PartialFunction[Throwable, RequestM[U]]): RequestM[U] = { 247 | val child:RequestM[U] = new RequestM(enclosing, "recoverWith", file, line) 248 | onComplete { 249 | case Success(v) => { 250 | try { 251 | child.resolve(Success(v)) 252 | } catch { 253 | case th:Throwable => { 254 | pf(th) onComplete { 255 | case any => child.resolve(any) 256 | } 257 | } 258 | } 259 | } 260 | case Failure(th) => { 261 | pf(th) onComplete { 262 | case any => child.resolve(any) 263 | } 264 | } 265 | } 266 | child 267 | } 268 | } 269 | 270 | object RequestM { 271 | def successful[T](result:T)(implicit enclosing:sourcecode.FullName, file:sourcecode.File, line:sourcecode.Line):RequestM[T] = { 272 | val r = new RequestM[T](enclosing, "successful", file, line) 273 | r.resolve(Success(result)) 274 | r 275 | } 276 | 277 | def failed[T](ex:Throwable)(implicit enclosing:sourcecode.FullName, file:sourcecode.File, line:sourcecode.Line):RequestM[T] = { 278 | val r = new RequestM[T](enclosing, "failed", file, line) 279 | r.resolve(Failure(ex)) 280 | r 281 | } 282 | 283 | /** 284 | * This returns a newly-created RequestM, outside of the usual Requester pathways. This 285 | * is slightly dangerous, but useful -- think of it as the counterpart to creating a Promise 286 | * and returning its .future. The difference here is that we're using the same object for 287 | * both sides -- you can pass the returned RequestM around, map over it, and so on, and 288 | * resolve it at the appropriate time. 289 | * 290 | * If you use prep/resolve, it is your responsibility to call resolve only at a safe time, 291 | * inside the Actor's receive loop. (Or inside persist() in a PersistentActor, or some such.) 292 | * Do this only when necessary; normally, you should work through .request(). 293 | */ 294 | def prep[T]()(implicit enclosing:sourcecode.FullName, file:sourcecode.File, line:sourcecode.Line):RequestM[T] = { 295 | new RequestM[T](enclosing, "prep", file, line) 296 | } 297 | } 298 | 299 | /** 300 | * Implicit that hooks into *other* Actors, to provide the nice request() syntax to send 301 | * messages to them. These implicits are available to any Actor that mixes in Requester, but 302 | * RequesterImplicits should also be mixed into any other class that wants access to this 303 | * capability. Those other classes must have access to a Requester -- usually, they should be 304 | * functional classes owned *by* a Requester. 305 | * 306 | * This trait gives you the functions that you actually call directly -- request() and requestFor(). 307 | * But those calls mostly create RequestM objects, and the actual work gets 308 | * done by the associated Requester. 309 | */ 310 | trait RequesterImplicits { 311 | 312 | /** 313 | * The actual Requester that is going to send the requests and process the responses. If 314 | * you mix RequesterImplicits into a non-Requester, this must point to that Actor, which 315 | * does all the real work. (If you are using this from within Requester, it's already set.) 316 | */ 317 | def requester:Requester 318 | 319 | /** 320 | * Hook to add the request() methods to a third-party Actor. 321 | */ 322 | implicit class RequestableActorRef(target:ActorRef) { 323 | /** 324 | * The basic, simple version of request() -- sends a message, process the response. 325 | * 326 | * You can think of request as a better-behaved version of ask. Where ask sends a message to the 327 | * target actor, and gives you a Future that will execute when the response is received, request 328 | * does the same thing but will process the resulting RequestM '''in the Actor's receive loop'''. 329 | * While this doesn't save you from every possible problem, it makes it much easier to write clear 330 | * and complex operations, involving coordinating several different Actors, without violating the 331 | * central invariants of the Actor model. 332 | * 333 | * The current sender will be preserved and will be active during the processing of the results, 334 | * so you can use it as normal. 335 | * 336 | * This version of the call does not impose any expectations on the results. You can use a 337 | * destructuring case class in a for comprehension if you want just a single return type, or you 338 | * can map the RequestM to a PartialFunction in order to handle several possible returns. 339 | * 340 | * @param msg The message to send to the target actor. 341 | * @param retries The number of times to retry this request, if it times out. 342 | */ 343 | def request(msg:Any, retries:Int = 0):RequestM[Any] = { 344 | requestFor[Any](msg, retries) 345 | } 346 | 347 | /** 348 | * A more strongly-typed version of request(). 349 | * 350 | * This works pretty much exactly like request, but expects that the response will be of type T. It will 351 | * throw a ClassCastException if anything else is received. Otherwise, it is identical to request(). 352 | */ 353 | def requestFor[T](msg:Any, retriesInit:Int = 0)(implicit tag: ClassTag[T], enclosing:sourcecode.FullName, file:sourcecode.File, line:sourcecode.Line):RequestM[T] = { 354 | val req = new RequestM[T](enclosing, "requestFor", file, line) 355 | requester.doRequest[T](target, msg, req) 356 | var retries = retriesInit 357 | req.intercept { 358 | // We replace AskTimeoutException with retries, and maybe with RequestRetriesExhausted. 359 | // TODO: there is probably a better way to do this, but it likely involves inventing something 360 | // like RequestM.transform(). 361 | case Failure(ex:AskTimeoutException) => { 362 | if (retries > 0) { 363 | retries -= 1 364 | requester.doRequest[T](target, msg, req) 365 | true 366 | } else { 367 | // No, really -- we're giving up and letting the timeout happen: 368 | false 369 | } 370 | } 371 | case _ => false 372 | } 373 | req 374 | } 375 | 376 | /** 377 | * ask-style syntax for ordinary requests. 378 | * 379 | * This *intentionally* conflicts with akka.pattern.ask, on the grounds that if you're in an askable situation, 380 | * it is generally a bug to be using raw asks. So if you wind up with ambiguity, that's a warning sign. 381 | * 382 | * (I might be willing to break this out, to make it possible to work around it, but somebody's going to have 383 | * to convince me it's a good idea to do so. If you really want to use ask, then spell it out explicitly.) 384 | */ 385 | def ?(msg:Any):RequestM[Any] = request(msg) 386 | } 387 | 388 | /** 389 | * Similar to RequestableActorRef, but works with an ActorSelection. 390 | */ 391 | implicit class RequestableActorSelection(target:ActorSelection) { 392 | def request(msg:Any):RequestM[Any] = { 393 | requestFor[Any](msg) 394 | } 395 | 396 | def requestFor[T](msg:Any)(implicit tag: ClassTag[T], enclosing:sourcecode.FullName, file:sourcecode.File, line:sourcecode.Line):RequestM[T] = { 397 | val req = new RequestM[T](enclosing, "requestFor", file, line) 398 | requester.doRequestGuts[T](target.ask(msg)(requester.requestTimeout), req) 399 | req 400 | } 401 | 402 | def ?(msg:Any):RequestM[Any] = request(msg) 403 | } 404 | 405 | /** 406 | * Convert a Future into a Request. 407 | * 408 | * This takes the specified Future, and runs it in the Requester's main loop, to make it properly safe. As usual, 409 | * sender will be preserved. 410 | * 411 | * This is implicit, so if you are in a context that already expects a Request (such as a for comprehension with a Request 412 | * at the top), it will quietly turn the Future into a Request. If Request isn't already expected, though, you'll have 413 | * to specify loopback explicitly. 414 | */ 415 | implicit def loopback[T](f:Future[T])(implicit tag:ClassTag[T], enclosing:sourcecode.FullName, file:sourcecode.File, line:sourcecode.Line):RequestM[T] = { 416 | val req = new RequestM[T](enclosing, "loopback", file, line) 417 | requester.doRequestGuts[T](f, req) 418 | req 419 | } 420 | 421 | /** 422 | * Convert a Request into a Future. 423 | * 424 | * Sometimes, at the edges of the API, you need to think in terms of Futures. When this is necessary, 425 | * this implicit will take your RequestM and turn it into a Future of the matching type. 426 | */ 427 | implicit def request2Future[T](req:RequestM[T]):Future[T] = { 428 | val promise = Promise[T] 429 | req onComplete { 430 | case Success(v) => promise.success(v) 431 | case Failure(ex) => promise.failure(ex) 432 | } 433 | promise.future 434 | } 435 | } 436 | 437 | /** 438 | * Easy and relatively safe variant of "ask". 439 | * 440 | * The idea here is that it would be lovely to have a replacement for the "ask" pattern. Ask 441 | * is powerful, but quite dangerous -- in particular, handling the response in the most obvious 442 | * way, using the Future's completion callbacks, is a fine way to break your Actor's threading 443 | * and cause strange timing bugs. 444 | * 445 | * So the idea here is to build something with similar semantics to ask, but deliberately a bit 446 | * dumbed-down and safer for routine use. Where ask returns a Future that you can then put 447 | * callbacks on, request() takes those callbacks as parameters, and runs them *in this Actor's main thread*. 448 | * 449 | * In other words, I want to be able to say something like: 450 | * 451 | * def receive = { 452 | * ... 453 | * case MyMessage(a, b) => { 454 | * otherActor.request(MyRequest(b)).foreach { 455 | * case OtherResponse(c) => ... 456 | * } 457 | * } 458 | * } 459 | * 460 | * While OtherResponse is lexically part of MyRequest, it actually *runs* during receive, just like 461 | * any other incoming message, so it isn't prone to the threading problems that ask is. 462 | * 463 | * How does this work? Under the hood, it actually does use ask, but in a very specific and constrained 464 | * way. We send the message off using ask, and then hook the resulting Future. When the Future completes, 465 | * we wrap the response and the handler together in a RequestedResponse message, and loop that back 466 | * around as a message to the local Actor. 467 | * 468 | * Note that the original sender is preserved, so the callback can use it without problems. (This is the 469 | * most common error made when using ask, and was one of the motivations for creating Requester.) 470 | * 471 | * Note that, to make this work, the Request trait mixes in its own version of unhandled(). For this to 472 | * work properly, therefore, it is very important that, if your own Actor overrides unhandled, it calls 473 | * super.unhandled() for unknown messages! 474 | * 475 | * That unhandled() override is usually enough to catch the looped-back messages, so you usually just 476 | * need to mix Requester into your Actor. However, if your Actor's receive function is intercepting *all* 477 | * messages (so nothing makes it to unhandled), then you will need to call handleRequestResponse at the 478 | * beginning of your receive; otherwise, your Actor can wind up deadlocked. This can particularly happen 479 | * when using stash() during Actor startup: 480 | * {{{ 481 | * def receive = handleRequestResponse orElse { 482 | * case Start => { 483 | * persistence.request(LoadMe(myId)) foreach { myState => 484 | * setState(myState) 485 | * unstashAll() 486 | * become(mainReceive) 487 | * } 488 | * } 489 | * 490 | * case _ => stash() 491 | * } 492 | * }}} 493 | * In this startup pattern, we are stashing all messages until the persister responds with the Actor's state. 494 | * However, if we didn't have handleRequestResponse there, the response would also get stashed, so the 495 | * Actor would never receive the state message, and the Actor would be stuck. 496 | * 497 | * IMPORTANT: Requester is *not* compatible with stateful versions of become() -- that is, if you are 498 | * using become() in a method where you are capturing the parameters in the closure of become(), 499 | * Requester will probably not work right. This is because the body of the response handler will capture 500 | * the closed-over parameter, and if the Actor has become() something else in the meantime, the handler 501 | * will use the *old* data, not the new. 502 | * 503 | * More generally, Requester should be used with caution if your Actor changes state frequently. 504 | * While it *can* theoretically be used with FSM, it may not be wise to do so, since the state machine 505 | * may no longer be in a compatible state by the time the response is received. Requester is mainly intended 506 | * for Actors that spend most or all of their time in a single state; it generally works quite well for those. 507 | */ 508 | trait Requester extends Actor with RequesterImplicits { 509 | 510 | val requester = this 511 | 512 | /** 513 | * The response from request() will be wrapped up in here and looped around. You shouldn't need to 514 | * use this directly. 515 | */ 516 | case class RequestedResponse[T](response:Try[T], handler:RequestM[T]) { 517 | def invoke = { handler.resolve(response) } 518 | } 519 | 520 | /** 521 | * Override this to specify the timeout for requests 522 | * 523 | * TODO: this is suspicious, since it does not follow Akka's preferred pattern for timeouts. 524 | * We might change how this works. 525 | */ 526 | implicit val requestTimeout = Timeout(10.seconds) 527 | 528 | /** 529 | * Send a request, and specify the handler for the received response. You may also specify a failHandler, 530 | * which will be run if the operation fails for some reason. (Most often, because we didn't receive a 531 | * response within the timeout window.) 532 | */ 533 | def doRequest[T](otherActor:ActorRef, msg:Any, handler:RequestM[T])(implicit tag: ClassTag[T]) = { 534 | doRequestGuts(otherActor ask msg, handler) 535 | } 536 | 537 | def doRequestGuts[T](f:Future[Any], handler:RequestM[T])(implicit tag: ClassTag[T]) = { 538 | val originalSender = sender 539 | import context.dispatcher 540 | val fTyped = f.mapTo[T] 541 | fTyped.onComplete { 542 | case Success(resp) => { 543 | try { 544 | self.tell(RequestedResponse(Success(resp), handler), originalSender) 545 | } catch { 546 | // TBD: is this ever going to plausibly happen? 547 | case ex:Exception => { 548 | self.tell(RequestedResponse(Failure(ex), handler), originalSender) 549 | } 550 | } 551 | } 552 | case Failure(thrown) => { 553 | self.tell(RequestedResponse(Failure(thrown), handler), originalSender) 554 | } 555 | } 556 | } 557 | 558 | /** 559 | * Normally you don't need to invoke this manually -- Requester defines an unhandled() 560 | * override that deals with these responses. But if your receive method intercepts *all* 561 | * messages for some reason (for example, it stashes everything), then you should add 562 | * this at the front of your receive so that it deals with responses. 563 | */ 564 | def handleRequestResponse:Receive = { 565 | case resp:RequestedResponse[_] => resp.invoke 566 | } 567 | 568 | abstract override def unhandled(message: Any): Unit = { 569 | message match { 570 | case resp:RequestedResponse[_] => resp.invoke 571 | case other => super.unhandled(other) 572 | } 573 | } 574 | } 575 | --------------------------------------------------------------------------------