From 06a91f1d82a93fccd0a2f80e376e8fab4f7ae679 Mon Sep 17 00:00:00 2001 From: Johannes Rudolph Date: Tue, 6 Sep 2016 19:54:27 +0200 Subject: [PATCH 01/23] =htc #21281 render empty params double quoted in headers like `Authorization` --- .../src/main/scala/akka/http/impl/util/Rendering.scala | 4 ++-- .../scala/akka/http/impl/model/parser/HttpHeaderSpec.scala | 4 ++++ .../src/test/scala/akka/http/impl/util/RenderingSpec.scala | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/akka-http-core/src/main/scala/akka/http/impl/util/Rendering.scala b/akka-http-core/src/main/scala/akka/http/impl/util/Rendering.scala index b606cdde24..944010ccf3 100644 --- a/akka-http-core/src/main/scala/akka/http/impl/util/Rendering.scala +++ b/akka-http-core/src/main/scala/akka/http/impl/util/Rendering.scala @@ -185,10 +185,10 @@ private[http] trait Rendering { /** * Renders the given string either directly (if it only contains token chars) - * or in double quotes (if it contains at least one non-token char). + * or in double quotes (if it is empty or contains at least one non-token char). */ def ~~#(s: String): this.type = - if (CharacterClasses.tchar matchesAll s) this ~~ s else ~~#!(s) + if (s.nonEmpty && CharacterClasses.tchar.matchesAll(s)) this ~~ s else ~~#!(s) /** * Renders the given string in double quotes. diff --git a/akka-http-core/src/test/scala/akka/http/impl/model/parser/HttpHeaderSpec.scala b/akka-http-core/src/test/scala/akka/http/impl/model/parser/HttpHeaderSpec.scala index 0e16cfb210..38393a7f38 100644 --- a/akka-http-core/src/test/scala/akka/http/impl/model/parser/HttpHeaderSpec.scala +++ b/akka-http-core/src/test/scala/akka/http/impl/model/parser/HttpHeaderSpec.scala @@ -140,6 +140,8 @@ class HttpHeaderSpec extends FreeSpec with Matchers { """Fancy yes="n:o",nonce=42""") """Authorization: Fancy yes=no,nonce="4\\2"""" =!= Authorization(GenericHttpCredentials("Fancy", Map("yes" → "no", "nonce" → """4\2"""))) + """Authorization: Other yes=no,empty=""""" =!= + Authorization(GenericHttpCredentials("Other", Map("yes" → "no", "empty" → ""))) "Authorization: Basic Qm9iOg==" =!= Authorization(BasicHttpCredentials("Bob", "")) """Authorization: Digest name=Bob""" =!= @@ -182,6 +184,8 @@ class HttpHeaderSpec extends FreeSpec with Matchers { "Content-Disposition: form-data" =!= `Content-Disposition`(ContentDispositionTypes.`form-data`) "Content-Disposition: attachment; name=field1; filename=\"file/txt\"" =!= `Content-Disposition`(ContentDispositionTypes.attachment, Map("name" → "field1", "filename" → "file/txt")) + "Content-Disposition: attachment; name=field1; other=\"\"" =!= + `Content-Disposition`(ContentDispositionTypes.attachment, Map("name" → "field1", "other" → "")) } "Content-Encoding" in { diff --git a/akka-http-core/src/test/scala/akka/http/impl/util/RenderingSpec.scala b/akka-http-core/src/test/scala/akka/http/impl/util/RenderingSpec.scala index 1f8b761605..bd4812db7e 100644 --- a/akka-http-core/src/test/scala/akka/http/impl/util/RenderingSpec.scala +++ b/akka-http-core/src/test/scala/akka/http/impl/util/RenderingSpec.scala @@ -29,7 +29,7 @@ class RenderingSpec extends WordSpec with Matchers { } "correctly render escaped Strings" in { - (new StringRendering ~~# "").get shouldEqual "" + (new StringRendering ~~# "").get shouldEqual "\"\"" (new StringRendering ~~# "hello").get shouldEqual "hello" (new StringRendering ~~# """hel"lo""").get shouldEqual """"hel\"lo"""" } From 7bcf0285a1ff14666bb8b4602e8c00daef805c47 Mon Sep 17 00:00:00 2001 From: gosubpl Date: Wed, 7 Sep 2016 19:12:42 +0200 Subject: [PATCH 02/23] =htc #20793 make Marshaller.fromStatusCode emit empty entity for StatusCode with allowEntity=false --- .../akka/http/scaladsl/model/StatusCode.scala | 2 +- .../testkit/MarshallingTestUtils.scala | 10 ++++++--- .../marshalling/MarshallingSpec.scala | 16 +++++++++++--- .../directives/CodingDirectivesSpec.scala | 21 +++++++++++++++++++ .../PredefinedToResponseMarshallers.scala | 13 ++++++------ 5 files changed, 49 insertions(+), 13 deletions(-) diff --git a/akka-http-core/src/main/scala/akka/http/scaladsl/model/StatusCode.scala b/akka-http-core/src/main/scala/akka/http/scaladsl/model/StatusCode.scala index be577648a9..6a0770fa55 100644 --- a/akka-http-core/src/main/scala/akka/http/scaladsl/model/StatusCode.scala +++ b/akka-http-core/src/main/scala/akka/http/scaladsl/model/StatusCode.scala @@ -107,7 +107,7 @@ object StatusCodes extends ObjectRegistry[Int, StatusCode] { val Created = reg(s(201)("Created", "The request has been fulfilled and resulted in a new resource being created.")) val Accepted = reg(s(202)("Accepted", "The request has been accepted for processing, but the processing has not been completed.")) val NonAuthoritativeInformation = reg(s(203)("Non-Authoritative Information", "The server successfully processed the request, but is returning information that may be from another source.")) - val NoContent = reg(s(204)("No Content", "", allowsEntity = false)) + val NoContent = reg(s(204)("No Content", "The server successfully processed the request and is not returning any content.", allowsEntity = false)) val ResetContent = reg(s(205)("Reset Content", "The server successfully processed the request, but is not returning any content.")) val PartialContent = reg(s(206)("Partial Content", "The server is delivering only part of the resource due to a range header sent by the client.")) val MultiStatus = reg(s(207)("Multi-Status", "The message body that follows is an XML message and can contain a number of separate response codes, depending on how many sub-requests were made.")) diff --git a/akka-http-testkit/src/main/scala/akka/http/scaladsl/testkit/MarshallingTestUtils.scala b/akka-http-testkit/src/main/scala/akka/http/scaladsl/testkit/MarshallingTestUtils.scala index 8558ad985e..2baaeaeccc 100644 --- a/akka-http-testkit/src/main/scala/akka/http/scaladsl/testkit/MarshallingTestUtils.scala +++ b/akka-http-testkit/src/main/scala/akka/http/scaladsl/testkit/MarshallingTestUtils.scala @@ -5,10 +5,10 @@ package akka.http.scaladsl.testkit import scala.concurrent.duration._ -import scala.concurrent.{ ExecutionContext, Await } -import akka.http.scaladsl.unmarshalling.{ Unmarshal, FromEntityUnmarshaller } +import scala.concurrent.{ Await, ExecutionContext } +import akka.http.scaladsl.unmarshalling.{ FromEntityUnmarshaller, Unmarshal } import akka.http.scaladsl.marshalling._ -import akka.http.scaladsl.model.HttpEntity +import akka.http.scaladsl.model.{ HttpEntity, HttpRequest, HttpResponse } import akka.stream.Materializer import scala.util.Try @@ -17,6 +17,10 @@ trait MarshallingTestUtils { def marshal[T: ToEntityMarshaller](value: T)(implicit ec: ExecutionContext, mat: Materializer): HttpEntity.Strict = Await.result(Marshal(value).to[HttpEntity].flatMap(_.toStrict(1.second)), 1.second) + def marshalToResponse[T: ToResponseMarshaller](value: T, request: HttpRequest = HttpRequest())(implicit ec: ExecutionContext, mat: Materializer): HttpResponse = { + Await.result(Marshal(value).toResponseFor(request), 1.second) + } + def unmarshalValue[T: FromEntityUnmarshaller](entity: HttpEntity)(implicit ec: ExecutionContext, mat: Materializer): T = unmarshal(entity).get diff --git a/akka-http-tests/src/test/scala/akka/http/scaladsl/marshalling/MarshallingSpec.scala b/akka-http-tests/src/test/scala/akka/http/scaladsl/marshalling/MarshallingSpec.scala index 4d73ee31a1..ab66c92a98 100644 --- a/akka-http-tests/src/test/scala/akka/http/scaladsl/marshalling/MarshallingSpec.scala +++ b/akka-http-tests/src/test/scala/akka/http/scaladsl/marshalling/MarshallingSpec.scala @@ -23,7 +23,7 @@ class MarshallingSpec extends FreeSpec with Matchers with BeforeAndAfterAll with implicit val materializer = ActorMaterializer() import system.dispatcher - "The PredefinedToEntityMarshallers." - { + "The PredefinedToEntityMarshallers" - { "StringMarshaller should marshal strings to `text/plain` content in UTF-8" in { marshal("Ha“llo") shouldEqual HttpEntity("Ha“llo") } @@ -39,7 +39,17 @@ class MarshallingSpec extends FreeSpec with Matchers with BeforeAndAfterAll with } } - "The GenericMarshallers." - { + "The PredefinedToResponseMarshallers" - { + "fromStatusCode should properly marshal entities that are not supposed to have a body" in { + marshalToResponse(StatusCodes.NoContent) shouldEqual HttpResponse(StatusCodes.NoContent, entity = HttpEntity.Empty) + } + "fromStatusCode should properly marshal entities that contain pre-defined content" in { + marshalToResponse(StatusCodes.EnhanceYourCalm) shouldEqual + HttpResponse(StatusCodes.EnhanceYourCalm, entity = HttpEntity(StatusCodes.EnhanceYourCalm.defaultMessage)) + } + } + + "The GenericMarshallers" - { "optionMarshaller should enable marshalling of Option[T]" in { marshal(Some("Ha“llo")) shouldEqual HttpEntity("Ha“llo") @@ -51,7 +61,7 @@ class MarshallingSpec extends FreeSpec with Matchers with BeforeAndAfterAll with } } - "The MultipartMarshallers." - { + "The MultipartMarshallers" - { "multipartMarshaller should correctly marshal multipart content with" - { "one empty part" in { marshal(Multipart.General(`multipart/mixed`, Multipart.General.BodyPart.Strict(""))) shouldEqual HttpEntity( diff --git a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/CodingDirectivesSpec.scala b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/CodingDirectivesSpec.scala index 9006290233..fa52405e97 100644 --- a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/CodingDirectivesSpec.scala +++ b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/CodingDirectivesSpec.scala @@ -478,6 +478,27 @@ class CodingDirectivesSpec extends RoutingSpec with Inside { } } + "the default marshaller" should { + "allow compressed responses with no body for informational messages" in { + Get() ~> `Accept-Encoding`(HttpEncodings.compress) ~> { + encodeResponse { + complete { StatusCodes.Continue } + } + } ~> check { + status shouldBe StatusCodes.Continue + } + } + "allow gzipped responses with no body for 204 messages" in { + Get() ~> `Accept-Encoding`(HttpEncodings.gzip) ~> { + encodeResponse { + complete { StatusCodes.NoContent } + } + } ~> check { + status shouldBe StatusCodes.NoContent + } + } + } + def compress(input: String, encoder: Encoder): ByteString = { val compressor = encoder.newCompressor compressor.compressAndFlush(ByteString(input)) ++ compressor.finish() diff --git a/akka-http/src/main/scala/akka/http/scaladsl/marshalling/PredefinedToResponseMarshallers.scala b/akka-http/src/main/scala/akka/http/scaladsl/marshalling/PredefinedToResponseMarshallers.scala index 35bc19f3d0..49ff565a37 100644 --- a/akka-http/src/main/scala/akka/http/scaladsl/marshalling/PredefinedToResponseMarshallers.scala +++ b/akka-http/src/main/scala/akka/http/scaladsl/marshalling/PredefinedToResponseMarshallers.scala @@ -5,17 +5,15 @@ package akka.http.scaladsl.marshalling import akka.http.scaladsl.common.EntityStreamingSupport -import akka.stream.impl.ConstantFun - -import scala.collection.immutable -import akka.http.scaladsl.util.FastFuture._ import akka.http.scaladsl.model.MediaTypes._ import akka.http.scaladsl.model._ -import akka.http.scaladsl.server.ContentNegotiator import akka.http.scaladsl.util.FastFuture +import akka.http.scaladsl.util.FastFuture._ +import akka.stream.impl.ConstantFun import akka.stream.scaladsl.Source import akka.util.ByteString +import scala.collection.immutable import scala.language.higherKinds trait PredefinedToResponseMarshallers extends LowPriorityToResponseMarshallerImplicits { @@ -33,7 +31,10 @@ trait PredefinedToResponseMarshallers extends LowPriorityToResponseMarshallerImp implicit val fromStatusCode: TRM[StatusCode] = Marshaller.withOpenCharset(`text/plain`) { (status, charset) ⇒ - HttpResponse(status, entity = HttpEntity(ContentType(`text/plain`, charset), status.defaultMessage)) + val responseEntity = + if (status.allowsEntity) HttpEntity(status.defaultMessage) + else HttpEntity.Empty + HttpResponse(status, entity = responseEntity) } implicit def fromStatusCodeAndValue[S, T](implicit sConv: S ⇒ StatusCode, mt: ToEntityMarshaller[T]): TRM[(S, T)] = From 320271cc315293118772ef98dcfdeaad52b880f0 Mon Sep 17 00:00:00 2001 From: Roland Kuhn Date: Tue, 31 May 2016 08:12:06 +0200 Subject: [PATCH 03/23] new implementation for Akka Typed #21131 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is the first step towards a completely new and optimized actor implementation for Akka Typed. The full previously existing test suite passes for both implementations. The following is an incomplete list of things that remain to be done: * document the semantic differences between untyped and typed, in particular around actor restarts and the delivery ordering guarantees for Terminated messages (also document the difference between ActorSystemImpl and ActorSystemAdapter) * implement EventStream and logging—this currently just delegates to an extra untyped ActorSystem (of course To Be Fixed) * implement dispatcher selection * implement and test queue size limitation * implement optimized message queue instead of CLQ (for zero-allocation messaging) * clean up test log output (something does not work with TestEventListener and EventFilter for ActorSystemImpl tests) * document the capabilities (or more appropriately: the limitations) of interoperability between ActorSystemImpl and ActorSystemAdapter * fix ActorPath UID generation (i.e. make sure that everything gets a meaningful value instead of zero) * re-evaluate throughput/rescheduling logic in ActorCell Oh, and by the way: as per PerformanceSpec (doing simple ping-pong) the new implementation is ca. 30% faster than the adapter over akka-actor :-) --- .../scala/akka/actor/dungeon/DeathWatch.scala | 2 + akka-docs/rst/scala/typed.rst | 8 +- akka-typed/build.sbt | 11 + .../main/scala/akka/typed/ActorContext.scala | 119 ++--- .../src/main/scala/akka/typed/ActorRef.scala | 67 +-- .../main/scala/akka/typed/ActorSystem.scala | 116 +++-- .../src/main/scala/akka/typed/Ask.scala | 77 +-- .../src/main/scala/akka/typed/Behavior.scala | 140 +----- .../main/scala/akka/typed/Dispatchers.scala | 13 + .../src/main/scala/akka/typed/Effects.scala | 35 +- .../src/main/scala/akka/typed/Impl.scala | 123 ----- .../src/main/scala/akka/typed/Inbox.scala | 53 ++- .../scala/akka/typed/MessageAndSignals.scala | 141 ++++++ .../src/main/scala/akka/typed/Ops.scala | 31 -- .../src/main/scala/akka/typed/Props.scala | 40 +- .../src/main/scala/akka/typed/ScalaDSL.scala | 38 +- .../akka/typed/adapter/ActorAdapter.scala | 65 +++ .../typed/adapter/ActorContextAdapter.scala | 59 +++ .../akka/typed/adapter/ActorRefAdapter.scala | 19 + .../typed/adapter/ActorSystemAdapter.scala | 64 +++ .../akka/typed/adapter/PropsAdapter.scala | 24 + .../scala/akka/typed/adapter/package.scala | 43 ++ .../scala/akka/typed/internal/ActorCell.scala | 435 +++++++++++++++++ .../akka/typed/internal/ActorRefImpl.scala | 194 ++++++++ .../akka/typed/internal/ActorSystemImpl.scala | 251 ++++++++++ .../akka/typed/internal/DeathWatch.scala | 202 ++++++++ .../akka/typed/internal/DispatchersImpl.scala | 36 ++ .../typed/internal/SupervisionMechanics.scala | 108 +++++ .../akka/typed/internal/SystemMessage.scala | 230 +++++++++ .../scala/akka/typed/internal/package.scala | 14 + .../scala/akka/typed/patterns/Receiver.scala | 64 +-- .../scala/akka/typed/patterns/Restarter.scala | 54 +++ akka-typed/src/test/resources/reference.conf | 3 + .../scala/akka/typed/ActorContextSpec.scala | 153 +++--- .../test/scala/akka/typed/BehaviorSpec.scala | 179 +++---- .../scala/akka/typed/PerformanceSpec.scala | 71 +-- .../src/test/scala/akka/typed/StepWise.scala | 82 ++-- .../src/test/scala/akka/typed/TypedSpec.scala | 95 +++- .../akka/typed/internal/ActorCellSpec.scala | 443 ++++++++++++++++++ .../akka/typed/internal/ActorSystemSpec.scala | 112 +++++ .../akka/typed/internal/ActorSystemStub.scala | 55 +++ .../typed/internal/ControlledExecutor.scala | 25 + .../scala/akka/typed/internal/DebugRef.scala | 53 +++ .../akka/typed/internal/FunctionRefSpec.scala | 164 +++++++ .../akka/typed/patterns/ReceiverSpec.scala | 39 +- .../typed/patterns/ReceptionistSpec.scala | 43 +- project/MiMa.scala | 5 +- 47 files changed, 3482 insertions(+), 916 deletions(-) create mode 100644 akka-typed/src/main/scala/akka/typed/Dispatchers.scala delete mode 100644 akka-typed/src/main/scala/akka/typed/Impl.scala create mode 100644 akka-typed/src/main/scala/akka/typed/MessageAndSignals.scala delete mode 100644 akka-typed/src/main/scala/akka/typed/Ops.scala create mode 100644 akka-typed/src/main/scala/akka/typed/adapter/ActorAdapter.scala create mode 100644 akka-typed/src/main/scala/akka/typed/adapter/ActorContextAdapter.scala create mode 100644 akka-typed/src/main/scala/akka/typed/adapter/ActorRefAdapter.scala create mode 100644 akka-typed/src/main/scala/akka/typed/adapter/ActorSystemAdapter.scala create mode 100644 akka-typed/src/main/scala/akka/typed/adapter/PropsAdapter.scala create mode 100644 akka-typed/src/main/scala/akka/typed/adapter/package.scala create mode 100644 akka-typed/src/main/scala/akka/typed/internal/ActorCell.scala create mode 100644 akka-typed/src/main/scala/akka/typed/internal/ActorRefImpl.scala create mode 100644 akka-typed/src/main/scala/akka/typed/internal/ActorSystemImpl.scala create mode 100644 akka-typed/src/main/scala/akka/typed/internal/DeathWatch.scala create mode 100644 akka-typed/src/main/scala/akka/typed/internal/DispatchersImpl.scala create mode 100644 akka-typed/src/main/scala/akka/typed/internal/SupervisionMechanics.scala create mode 100644 akka-typed/src/main/scala/akka/typed/internal/SystemMessage.scala create mode 100644 akka-typed/src/main/scala/akka/typed/internal/package.scala create mode 100644 akka-typed/src/main/scala/akka/typed/patterns/Restarter.scala create mode 100644 akka-typed/src/test/scala/akka/typed/internal/ActorCellSpec.scala create mode 100644 akka-typed/src/test/scala/akka/typed/internal/ActorSystemSpec.scala create mode 100644 akka-typed/src/test/scala/akka/typed/internal/ActorSystemStub.scala create mode 100644 akka-typed/src/test/scala/akka/typed/internal/ControlledExecutor.scala create mode 100644 akka-typed/src/test/scala/akka/typed/internal/DebugRef.scala create mode 100644 akka-typed/src/test/scala/akka/typed/internal/FunctionRefSpec.scala diff --git a/akka-actor/src/main/scala/akka/actor/dungeon/DeathWatch.scala b/akka-actor/src/main/scala/akka/actor/dungeon/DeathWatch.scala index 41890385f8..f2bbbc9fb2 100644 --- a/akka-actor/src/main/scala/akka/actor/dungeon/DeathWatch.scala +++ b/akka-actor/src/main/scala/akka/actor/dungeon/DeathWatch.scala @@ -15,6 +15,8 @@ private[akka] trait DeathWatch { this: ActorCell ⇒ private var watchedBy: Set[ActorRef] = ActorCell.emptyActorRefSet private var terminatedQueued: Set[ActorRef] = ActorCell.emptyActorRefSet + def isWatching(ref: ActorRef): Boolean = watching contains ref + override final def watch(subject: ActorRef): ActorRef = subject match { case a: InternalActorRef ⇒ if (a != self && !watchingContains(a)) { diff --git a/akka-docs/rst/scala/typed.rst b/akka-docs/rst/scala/typed.rst index 56bc8e7773..cff938b4d1 100644 --- a/akka-docs/rst/scala/typed.rst +++ b/akka-docs/rst/scala/typed.rst @@ -52,15 +52,15 @@ protocol but Actors can model arbitrarily complex protocols when needed. The protocol is bundled together with the behavior that implements it in a nicely wrapped scope—the ``HelloWorld`` object. -Now we want to try out this Actor, so we must start an Actor system to host it: +Now we want to try out this Actor, so we must start an ActorSystem to host it: .. includecode:: code/docs/akka/typed/IntroSpec.scala#hello-world After importing the Actor’s protocol definition we start an Actor system from the defined behavior, wrapping it in :class:`Props` like an actor on stage. The props we are giving to this one are just the defaults, we could at this point -also configure how and where the Actor should be deployed in a clustered -system. +also configure which thread pool will be used to run it or its mailbox capacity +for incoming messages. As Carl Hewitt said, one Actor is no Actor—it would be quite lonely with nobody to talk to. In this sense the example is a little cruel because we only @@ -72,7 +72,7 @@ Note that the :class:`Future` that is returned by the “ask” operation is properly typed already, no type checks or casts needed. This is possible due to the type information that is part of the message protocol: the ``?`` operator takes as argument a function that accepts an :class:`ActorRef[U]` (which -explains the ``_`` hole in the expression on line 6 above) and the ``replyTo`` +explains the ``_`` hole in the expression on line 7 above) and the ``replyTo`` parameter which we fill in like that is of type ``ActorRef[Greeted]``, which means that the value that fulfills the :class:`Promise` can only be of type :class:`Greeted`. diff --git a/akka-typed/build.sbt b/akka-typed/build.sbt index 3aaf7dc2fa..07a60fdf87 100644 --- a/akka-typed/build.sbt +++ b/akka-typed/build.sbt @@ -5,3 +5,14 @@ AkkaBuild.experimentalSettings Formatting.formatSettings disablePlugins(MimaPlugin) + +initialCommands := """ + import akka.typed._ + import ScalaDSL._ + import scala.concurrent._ + import duration._ + import akka.util.Timeout + implicit val timeout = Timeout(5.seconds) +""" + +cancelable in Global := true diff --git a/akka-typed/src/main/scala/akka/typed/ActorContext.scala b/akka-typed/src/main/scala/akka/typed/ActorContext.scala index f8593a9bee..32f360a94b 100644 --- a/akka-typed/src/main/scala/akka/typed/ActorContext.scala +++ b/akka-typed/src/main/scala/akka/typed/ActorContext.scala @@ -69,17 +69,6 @@ trait ActorContext[T] { */ def spawn[U](props: Props[U], name: String): ActorRef[U] - /** - * Create an untyped child Actor from the given [[akka.actor.Props]] under a randomly chosen name. - * It is good practice to name Actors wherever practical. - */ - def actorOf(props: untyped.Props): untyped.ActorRef - - /** - * Create an untyped child Actor from the given [[akka.actor.Props]] and with the given name. - */ - def actorOf(props: untyped.Props, name: String): untyped.ActorRef - /** * Force the child Actor under the given name to terminate after it finishes * processing its current message. Nothing happens if the ActorRef does not @@ -97,14 +86,6 @@ trait ActorContext[T] { */ def watch[U](other: ActorRef[U]): ActorRef[U] - /** - * Register for [[Terminated]] notification once the Actor identified by the - * given [[akka.actor.ActorRef]] terminates. This notification is also generated when the - * [[ActorSystem]] to which the referenced Actor belongs is declared as - * failed (e.g. in reaction to being unreachable). - */ - def watch(other: akka.actor.ActorRef): akka.actor.ActorRef - /** * Revoke the registration established by `watch`. A [[Terminated]] * notification will not subsequently be received for the referenced Actor. @@ -112,18 +93,17 @@ trait ActorContext[T] { def unwatch[U](other: ActorRef[U]): ActorRef[U] /** - * Revoke the registration established by `watch`. A [[Terminated]] - * notification will not subsequently be received for the referenced Actor. - */ - def unwatch(other: akka.actor.ActorRef): akka.actor.ActorRef - - /** - * Schedule the sending of a [[ReceiveTimeout]] notification in case no other + * Schedule the sending of a notification in case no other * message is received during the given period of time. The timeout starts anew * with each received message. Provide `Duration.Undefined` to switch off this * mechanism. */ - def setReceiveTimeout(d: Duration): Unit + def setReceiveTimeout(d: FiniteDuration, msg: T): Unit + + /** + * Cancel the sending of receive timeout notifications. + */ + def cancelReceiveTimeout(): Unit /** * Schedule the sending of the given message to the given target Actor after @@ -155,46 +135,38 @@ trait ActorContext[T] { * See [[EffectfulActorContext]] for more advanced uses. */ class StubbedActorContext[T]( - val name: String, - override val props: Props[T])( - override implicit val system: ActorSystem[Nothing]) extends ActorContext[T] { + val name: String, + override val props: Props[T], + override val system: ActorSystem[Nothing]) extends ActorContext[T] { - val inbox = Inbox.sync[T](name) + val inbox = Inbox[T](name) override val self = inbox.ref - private var _children = TreeMap.empty[String, Inbox.SyncInbox[_]] + private var _children = TreeMap.empty[String, Inbox[_]] private val childName = Iterator from 1 map (Helpers.base64(_)) override def children: Iterable[ActorRef[Nothing]] = _children.values map (_.ref) override def child(name: String): Option[ActorRef[Nothing]] = _children get name map (_.ref) override def spawnAnonymous[U](props: Props[U]): ActorRef[U] = { - val i = Inbox.sync[U](childName.next()) - _children += i.ref.untypedRef.path.name → i + val i = Inbox[U](childName.next()) + _children += i.ref.path.name → i i.ref } override def spawn[U](props: Props[U], name: String): ActorRef[U] = _children get name match { case Some(_) ⇒ throw new untyped.InvalidActorNameException(s"actor name $name is already taken") case None ⇒ - val i = Inbox.sync[U](name) + // FIXME correct child path for the Inbox ref + val i = Inbox[U](name) _children += name → i i.ref } - override def actorOf(props: untyped.Props): untyped.ActorRef = { - val i = Inbox.sync[Any](childName.next()) - _children += i.ref.untypedRef.path.name → i - i.ref.untypedRef - } - override def actorOf(props: untyped.Props, name: String): untyped.ActorRef = - _children get name match { - case Some(_) ⇒ throw new untyped.InvalidActorNameException(s"actor name $name is already taken") - case None ⇒ - val i = Inbox.sync[Any](name) - _children += name → i - i.ref.untypedRef - } + + /** + * Do not actually stop the child inbox, only simulate the liveness check. + * Removal is asynchronous, explicit removeInbox is needed from outside afterwards. + */ override def stop(child: ActorRef[Nothing]): Boolean = { - // removal is asynchronous, so don’t do it here; explicit removeInbox needed from outside _children.get(child.path.name) match { case None ⇒ false case Some(inbox) ⇒ inbox.ref == child @@ -204,36 +176,33 @@ class StubbedActorContext[T]( def watch(other: akka.actor.ActorRef): other.type = other def unwatch[U](other: ActorRef[U]): ActorRef[U] = other def unwatch(other: akka.actor.ActorRef): other.type = other - def setReceiveTimeout(d: Duration): Unit = () + def setReceiveTimeout(d: FiniteDuration, msg: T): Unit = () + def cancelReceiveTimeout(): Unit = () def schedule[U](delay: FiniteDuration, target: ActorRef[U], msg: U): untyped.Cancellable = new untyped.Cancellable { def cancel() = false def isCancelled = true } - implicit def executionContext: ExecutionContextExecutor = system.executionContext - def spawnAdapter[U](f: U ⇒ T): ActorRef[U] = ??? - def getInbox[U](name: String): Inbox.SyncInbox[U] = _children(name).asInstanceOf[Inbox.SyncInbox[U]] - def removeInbox(name: String): Unit = _children -= name + def executionContext: ExecutionContextExecutor = system.executionContext + + def spawnAdapter[U](f: U ⇒ T): ActorRef[U] = spawnAnonymous(Props.empty) + + /** + * Retrieve the named inbox. The passed ActorRef must be one that was returned + * by one of the spawn methods earlier. + */ + def getInbox[U](child: ActorRef[U]): Inbox[U] = { + val inbox = _children(child.path.name).asInstanceOf[Inbox[U]] + if (inbox.ref != child) throw new IllegalArgumentException(s"$child is not a child of $this") + inbox + } + + /** + * Remove the given inbox from the list of children, for example after + * having simulated its termination. + */ + def removeInbox(child: ActorRef[Nothing]): Unit = _children -= child.path.name + + override def toString: String = s"Inbox($self)" } - -/* - * TODO - * - * Currently running a behavior requires that the context stays the same, since - * the behavior may well close over it and thus a change might not be effective - * at all. Another issue is that there is genuine state within the context that - * is coupled to the behavior’s state: if child actors were created then - * migrating a behavior into a new context will not work. - * - * This note is about remembering the reasons behind this restriction and - * proposes an ActorContextProxy as a (broken) half-solution. Another avenue - * by which a solution may be explored is for Pure behaviors in that they - * may be forced to never remember anything that is immobile. - */ -//class MobileActorContext[T](_name: String, _props: Props[T], _system: ActorSystem[Nothing]) -// extends EffectfulActorContext[T](_name, _props, _system) { -// -//} -// -//class ActorContextProxy[T](var d: ActorContext[T]) extends ActorContext[T] diff --git a/akka-typed/src/main/scala/akka/typed/ActorRef.scala b/akka-typed/src/main/scala/akka/typed/ActorRef.scala index 7985081cb9..2ed1247b40 100644 --- a/akka-typed/src/main/scala/akka/typed/ActorRef.scala +++ b/akka-typed/src/main/scala/akka/typed/ActorRef.scala @@ -3,9 +3,10 @@ */ package akka.typed -import akka.actor.ActorPath +import akka.{ actor ⇒ a } import scala.annotation.unchecked.uncheckedVariance import language.implicitConversions +import scala.concurrent.Future /** * An ActorRef is the identity or address of an Actor instance. It is valid @@ -16,20 +17,18 @@ import language.implicitConversions * [[akka.event.EventStream]] on a best effort basis * (i.e. this delivery is not reliable). */ -abstract class ActorRef[-T] extends java.lang.Comparable[ActorRef[Any]] { this: ScalaActorRef[T] ⇒ +abstract class ActorRef[-T](_path: a.ActorPath) extends java.lang.Comparable[ActorRef[Nothing]] { this: internal.ActorRefImpl[T] ⇒ /** - * INTERNAL API. - * - * Implementation detail. The underlying untyped [[akka.actor.ActorRef]] - * of this typed ActorRef. + * Send a message to the Actor referenced by this ActorRef using *at-most-once* + * messaging semantics. */ - private[akka] def untypedRef: akka.actor.ActorRef + def tell(msg: T): Unit /** * Send a message to the Actor referenced by this ActorRef using *at-most-once* * messaging semantics. */ - def tell(msg: T): Unit = untypedRef ! msg + def !(msg: T): Unit = tell(msg) /** * Unsafe utility method for widening the type accepted by this ActorRef; @@ -44,34 +43,44 @@ abstract class ActorRef[-T] extends java.lang.Comparable[ActorRef[Any]] { this: * and more than one Actor instance can exist with the same path at different * points in time, but not concurrently. */ - def path: ActorPath = untypedRef.path + final val path: a.ActorPath = _path - override def toString = untypedRef.toString - override def equals(other: Any) = other match { - case a: ActorRef[_] ⇒ a.untypedRef == untypedRef - case _ ⇒ false + /** + * Comparison takes path and the unique id of the actor cell into account. + */ + final override def compareTo(other: ActorRef[Nothing]) = { + val x = this.path compareTo other.path + if (x == 0) if (this.path.uid < other.path.uid) -1 else if (this.path.uid == other.path.uid) 0 else 1 + else x } - override def hashCode = untypedRef.hashCode - override def compareTo(other: ActorRef[Any]) = untypedRef.compareTo(other.untypedRef) -} -/** - * This trait is used to hide the `!` method from Java code. - */ -trait ScalaActorRef[-T] { this: ActorRef[T] ⇒ - def !(msg: T): Unit = tell(msg) + final override def hashCode: Int = path.uid + + /** + * Equals takes path and the unique id of the actor cell into account. + */ + final override def equals(that: Any): Boolean = that match { + case other: ActorRef[_] ⇒ path.uid == other.path.uid && path == other.path + case _ ⇒ false + } + + final override def toString: String = s"Actor[${path}#${path.uid}]" } object ActorRef { - private class Combined[T](val untypedRef: akka.actor.ActorRef) extends ActorRef[T] with ScalaActorRef[T] - - implicit def toScalaActorRef[T](ref: ActorRef[T]): ScalaActorRef[T] = ref.asInstanceOf[ScalaActorRef[T]] + /** + * Create an ActorRef from a Future, buffering up to the given number of + * messages in while the Future is not fulfilled. + */ + def apply[T](f: Future[ActorRef[T]], bufferSize: Int = 1000): ActorRef[T] = new internal.FutureRef(FuturePath, bufferSize, f) /** - * Construct a typed ActorRef from an untyped one and a protocol definition - * (i.e. a recipient message type). This can be used to properly represent - * untyped Actors within the typed world, given that they implement the assumed - * protocol. + * Create an ActorRef by providing a function that is invoked for sending + * messages and a termination callback. */ - def apply[T](ref: akka.actor.ActorRef): ActorRef[T] = new Combined[T](ref) + def apply[T](send: (T, internal.FunctionRef[T]) ⇒ Unit, terminate: internal.FunctionRef[T] ⇒ Unit): ActorRef[T] = + new internal.FunctionRef(FunctionPath, send, terminate) + + private[typed] val FuturePath = a.RootActorPath(a.Address("akka.typed.internal", "future")) + private[typed] val FunctionPath = a.RootActorPath(a.Address("akka.typed.internal", "function")) } diff --git a/akka-typed/src/main/scala/akka/typed/ActorSystem.scala b/akka-typed/src/main/scala/akka/typed/ActorSystem.scala index 2d338f2731..f9073eaba8 100644 --- a/akka-typed/src/main/scala/akka/typed/ActorSystem.scala +++ b/akka-typed/src/main/scala/akka/typed/ActorSystem.scala @@ -3,18 +3,13 @@ */ package akka.typed -import akka.event.EventStream +import akka.event.{ LoggingFilter, LoggingAdapter, EventStream } import scala.concurrent.ExecutionContext -import akka.actor.ActorRefProvider +import akka.{ actor ⇒ a } import java.util.concurrent.ThreadFactory -import akka.actor.DynamicAccess -import akka.actor.ActorSystemImpl -import com.typesafe.config.Config -import akka.actor.ExtendedActorSystem -import com.typesafe.config.ConfigFactory -import scala.concurrent.ExecutionContextExecutor -import scala.concurrent.Future -import akka.dispatch.Dispatchers +import com.typesafe.config.{ Config, ConfigFactory } +import scala.concurrent.{ ExecutionContextExecutor, Future } +import akka.typed.adapter.{ PropsAdapter, ActorSystemAdapter } /** * An ActorSystem is home to a hierarchy of Actors. It is created using @@ -23,50 +18,41 @@ import akka.dispatch.Dispatchers * A system also implements the [[ActorRef]] type, and sending a message to * the system directs that message to the root Actor. */ -abstract class ActorSystem[-T](_name: String) extends ActorRef[T] { this: ScalaActorRef[T] ⇒ - - /** - * INTERNAL API. - * - * Access to the underlying (untyped) ActorSystem. - */ - private[akka] val untyped: ExtendedActorSystem +trait ActorSystem[-T] extends ActorRef[T] { this: internal.ActorRefImpl[T] ⇒ /** * The name of this actor system, used to distinguish multiple ones within * the same JVM & class loader. */ - def name: String = _name + def name: String /** * The core settings extracted from the supplied configuration. */ - def settings: akka.actor.ActorSystem.Settings = untyped.settings + def settings: akka.actor.ActorSystem.Settings /** * Log the configuration. */ - def logConfiguration(): Unit = untyped.logConfiguration() + def logConfiguration(): Unit + + def logFilter: LoggingFilter + def log: LoggingAdapter /** * Start-up time in milliseconds since the epoch. */ - def startTime: Long = untyped.startTime + def startTime: Long /** * Up-time of this actor system in seconds. */ - def uptime: Long = untyped.uptime - - /** - * Helper object for looking up configured dispatchers. - */ - def dispatchers: Dispatchers = untyped.dispatchers + def uptime: Long /** * A ThreadFactory that can be used if the transport needs to create any Threads */ - def threadFactory: ThreadFactory = untyped.threadFactory + def threadFactory: ThreadFactory /** * ClassLoader wrapper which is used for reflective accesses internally. This is set @@ -75,65 +61,93 @@ abstract class ActorSystem[-T](_name: String) extends ActorRef[T] { this: ScalaA * set on all threads created by the ActorSystem, if one was set during * creation. */ - def dynamicAccess: DynamicAccess = untyped.dynamicAccess + def dynamicAccess: a.DynamicAccess /** - * The ActorRefProvider is the only entity which creates all actor references within this actor system. + * A generic scheduler that can initiate the execution of tasks after some delay. + * It is recommended to use the ActorContext’s scheduling capabilities for sending + * messages to actors instead of registering a Runnable for execution using this facility. */ - def provider: ActorRefProvider = untyped.provider - - /** - * The user guardian’s untyped [[akka.actor.ActorRef]]. - */ - private[akka] override def untypedRef: akka.actor.ActorRef = untyped.provider.guardian + def scheduler: a.Scheduler /** * Main event bus of this actor system, used for example for logging. */ - def eventStream: EventStream = untyped.eventStream + def eventStream: EventStream + + /** + * Facilities for lookup up thread-pools from configuration. + */ + def dispatchers: Dispatchers /** * The default thread pool of this ActorSystem, configured with settings in `akka.actor.default-dispatcher`. */ - implicit def executionContext: ExecutionContextExecutor = untyped.dispatcher + implicit def executionContext: ExecutionContextExecutor /** * Terminates this actor system. This will stop the guardian actor, which in turn * will recursively stop all its child actors, then the system guardian * (below which the logging actors reside). */ - def terminate(): Future[Terminated] = untyped.terminate().map(t ⇒ Terminated(ActorRef(t.actor))) + def terminate(): Future[Terminated] /** * Returns a Future which will be completed after the ActorSystem has been terminated * and termination hooks have been executed. */ - def whenTerminated: Future[Terminated] = untyped.whenTerminated.map(t ⇒ Terminated(ActorRef(t.actor))) + def whenTerminated: Future[Terminated] /** * The deadLetter address is a destination that will accept (and discard) * every message sent to it. */ - def deadLetters[U]: ActorRef[U] = deadLetterRef - lazy private val deadLetterRef = ActorRef[Any](untyped.deadLetters) + def deadLetters[U]: ActorRef[U] + + /** + * Create a string representation of the actor hierarchy within this system. + * + * The format of the string is subject to change, i.e. no stable “API”. + */ + def printTree: String } object ActorSystem { - private class Impl[T](_name: String, _config: Config, _cl: ClassLoader, _ec: Option[ExecutionContext], _p: Props[T]) - extends ActorSystem[T](_name) with ScalaActorRef[T] { - override private[akka] val untyped: ExtendedActorSystem = new ActorSystemImpl(_name, _config, _cl, _ec, Some(Props.untyped(_p))).start() - } - - private class Wrapper(val untyped: ExtendedActorSystem) extends ActorSystem[Nothing](untyped.name) with ScalaActorRef[Nothing] + import internal._ + /** + * Create an ActorSystem implementation that is optimized for running + * Akka Typed [[Behavior]] hierarchies—this system cannot run untyped + * [[akka.actor.Actor]] instances. + */ def apply[T](name: String, guardianProps: Props[T], config: Option[Config] = None, classLoader: Option[ClassLoader] = None, executionContext: Option[ExecutionContext] = None): ActorSystem[T] = { val cl = classLoader.getOrElse(akka.actor.ActorSystem.findClassLoader()) val appConfig = config.getOrElse(ConfigFactory.load(cl)) - new Impl(name, appConfig, cl, executionContext, guardianProps) + new ActorSystemImpl(name, appConfig, cl, executionContext, guardianProps) } - def apply(untyped: akka.actor.ActorSystem): ActorSystem[Nothing] = new Wrapper(untyped.asInstanceOf[ExtendedActorSystem]) + /** + * Create an ActorSystem based on the untyped [[akka.actor.ActorSystem]] + * which runs Akka Typed [[Behavior]] on an emulation layer. In this + * system typed and untyped actors can coexist. + */ + def adapter[T](name: String, guardianProps: Props[T], + config: Option[Config] = None, + classLoader: Option[ClassLoader] = None, + executionContext: Option[ExecutionContext] = None): ActorSystem[T] = { + val cl = classLoader.getOrElse(akka.actor.ActorSystem.findClassLoader()) + val appConfig = config.getOrElse(ConfigFactory.load(cl)) + val untyped = new a.ActorSystemImpl(name, appConfig, cl, executionContext, Some(PropsAdapter(guardianProps))) + untyped.start() + new ActorSystemAdapter(untyped) + } + + /** + * Wrap an untyped [[akka.actor.ActorSystem]] such that it can be used from + * Akka Typed [[Behavior]]. + */ + def wrap(untyped: a.ActorSystem): ActorSystem[Nothing] = new ActorSystemAdapter(untyped.asInstanceOf[a.ActorSystemImpl]) } diff --git a/akka-typed/src/main/scala/akka/typed/Ask.scala b/akka-typed/src/main/scala/akka/typed/Ask.scala index 1cd33d67a5..708fec1df6 100644 --- a/akka-typed/src/main/scala/akka/typed/Ask.scala +++ b/akka-typed/src/main/scala/akka/typed/Ask.scala @@ -3,12 +3,17 @@ */ package akka.typed -import scala.concurrent.Future +import scala.concurrent.{ Future, Promise } import akka.util.Timeout import akka.actor.InternalActorRef import akka.pattern.AskTimeoutException import akka.pattern.PromiseActorRef import java.lang.IllegalArgumentException +import akka.actor.Scheduler +import akka.typed.internal.FunctionRef +import akka.actor.RootActorPath +import akka.actor.Address +import akka.util.LineNumbers /** * The ask-pattern implements the initiator side of a request–reply protocol. @@ -31,39 +36,57 @@ import java.lang.IllegalArgumentException */ object AskPattern { implicit class Askable[T](val ref: ActorRef[T]) extends AnyVal { - def ?[U](f: ActorRef[U] ⇒ T)(implicit timeout: Timeout): Future[U] = ask(ref, timeout, f) + def ?[U](f: ActorRef[U] ⇒ T)(implicit timeout: Timeout, scheduler: Scheduler): Future[U] = + ref match { + case a: adapter.ActorRefAdapter[_] ⇒ askUntyped(ref, a.untyped, timeout, f) + case a: adapter.ActorSystemAdapter[_] ⇒ askUntyped(ref, a.untyped.guardian, timeout, f) + case _ ⇒ ask(ref, timeout, scheduler, f) + } } - private class PromiseRef[U](actorRef: ActorRef[_], timeout: Timeout) { - val (ref: ActorRef[U], future: Future[U], promiseRef: PromiseActorRef) = actorRef.untypedRef match { - case ref: InternalActorRef if ref.isTerminated ⇒ + private class PromiseRef[U](target: ActorRef[_], untyped: InternalActorRef, timeout: Timeout) { + val (ref: ActorRef[U], future: Future[U], promiseRef: PromiseActorRef) = + if (untyped.isTerminated) ( - ActorRef[U](ref.provider.deadLetters), - Future.failed[U](new AskTimeoutException(s"Recipient[$actorRef] had already been terminated."))) - case ref: InternalActorRef ⇒ - if (timeout.duration.length <= 0) - ( - ActorRef[U](ref.provider.deadLetters), - Future.failed[U](new IllegalArgumentException(s"Timeout length must be positive, question not sent to [$actorRef]"))) - else { - val a = PromiseActorRef(ref.provider, timeout, actorRef, "unknown") - val b = ActorRef[U](a) - (b, a.result.future.asInstanceOf[Future[U]], a) - } - case _ ⇒ throw new IllegalArgumentException(s"cannot create PromiseRef for non-Akka ActorRef (${actorRef.getClass})") - } + adapter.ActorRefAdapter[U](untyped.provider.deadLetters), + Future.failed[U](new AskTimeoutException(s"Recipient[$target] had already been terminated.")), null) + else if (timeout.duration.length <= 0) + ( + adapter.ActorRefAdapter[U](untyped.provider.deadLetters), + Future.failed[U](new IllegalArgumentException(s"Timeout length must be positive, question not sent to [$target]")), null) + else { + val a = PromiseActorRef(untyped.provider, timeout, target, "unknown") + val b = adapter.ActorRefAdapter[U](a) + (b, a.result.future.asInstanceOf[Future[U]], a) + } } - private object PromiseRef { - def apply[U](actorRef: ActorRef[_])(implicit timeout: Timeout) = new PromiseRef[U](actorRef, timeout) - } - - private[typed] def ask[T, U](actorRef: ActorRef[T], timeout: Timeout, f: ActorRef[U] ⇒ T): Future[U] = { - val p = PromiseRef[U](actorRef)(timeout) + private def askUntyped[T, U](target: ActorRef[T], untyped: InternalActorRef, timeout: Timeout, f: ActorRef[U] ⇒ T): Future[U] = { + val p = new PromiseRef[U](target, untyped, timeout) val m = f(p.ref) - p.promiseRef.messageClassName = m.getClass.getName - actorRef ! m + if (p.promiseRef ne null) p.promiseRef.messageClassName = m.getClass.getName + target ! m p.future } + private def ask[T, U](actorRef: ActorRef[T], timeout: Timeout, scheduler: Scheduler, f: ActorRef[U] ⇒ T): Future[U] = { + import akka.dispatch.ExecutionContexts.{ sameThreadExecutionContext ⇒ ec } + val p = Promise[U] + val ref = new FunctionRef[U]( + AskPath, + (msg, self) ⇒ { + p.trySuccess(msg) + self.sendSystem(internal.Terminate()) + }, + (self) ⇒ if (!p.isCompleted) p.tryFailure(new NoSuchElementException("ask pattern terminated before value was received"))) + actorRef ! f(ref) + val d = timeout.duration + val c = scheduler.scheduleOnce(d)(p.tryFailure(new AskTimeoutException(s"did not receive message within $d")))(ec) + val future = p.future + future.andThen { + case _ ⇒ c.cancel() + }(ec) + } + + private[typed] val AskPath = RootActorPath(Address("akka.typed.internal", "ask")) } diff --git a/akka-typed/src/main/scala/akka/typed/Behavior.scala b/akka-typed/src/main/scala/akka/typed/Behavior.scala index ebeb630a0a..5b3e22e44b 100644 --- a/akka-typed/src/main/scala/akka/typed/Behavior.scala +++ b/akka-typed/src/main/scala/akka/typed/Behavior.scala @@ -57,147 +57,12 @@ abstract class Behavior[T] { /* * FIXME - * + * * Closing over ActorContext makes a Behavior immobile: it cannot be moved to * another context and executed there, and therefore it cannot be replicated or * forked either. */ -/** - * System signals are notifications that are generated by the system and - * delivered to the Actor behavior in a reliable fashion (i.e. they are - * guaranteed to arrive in contrast to the at-most-once semantics of normal - * Actor messages). - */ -sealed trait Signal -/** - * Lifecycle signal that is fired upon creation of the Actor. This will be the - * first message that the actor processes. - */ -@SerialVersionUID(1L) -final case object PreStart extends Signal -/** - * Lifecycle signal that is fired upon restart of the Actor before replacing - * the behavior with the fresh one (i.e. this signal is received within the - * behavior that failed). - */ -@SerialVersionUID(1L) -final case class PreRestart(failure: Throwable) extends Signal -/** - * Lifecycle signal that is fired upon restart of the Actor after replacing - * the behavior with the fresh one (i.e. this signal is received within the - * fresh replacement behavior). - */ -@SerialVersionUID(1L) -final case class PostRestart(failure: Throwable) extends Signal -/** - * Lifecycle signal that is fired after this actor and all its child actors - * (transitively) have terminated. The [[Terminated]] signal is only sent to - * registered watchers after this signal has been processed. - * - * IMPORTANT NOTE: if the actor terminated by switching to the - * `Stopped` behavior then this signal will be ignored (i.e. the - * Stopped behavior will do nothing in reaction to it). - */ -@SerialVersionUID(1L) -final case object PostStop extends Signal -/** - * Lifecycle signal that is fired when a direct child actor fails. The child - * actor will be suspended until its fate has been decided. The decision is - * communicated by calling the [[Failed#decide]] method. If this is not - * done then the default behavior is to escalate the failure, which amounts to - * failing this actor with the same exception that the child actor failed with. - */ -@SerialVersionUID(1L) -final case class Failed(cause: Throwable, child: ActorRef[Nothing]) extends Signal { - import Failed._ - - private[this] var _decision: Decision = _ - def decide(decision: Decision): Unit = _decision = decision - def getDecision: Decision = _decision match { - case null ⇒ NoFailureResponse - case x ⇒ x - } -} -/** - * The actor can register for a notification in case no message is received - * within a given time window, and the signal that is raised in this case is - * this one. See also [[ActorContext#setReceiveTimeout]]. - */ -@SerialVersionUID(1L) -final case object ReceiveTimeout extends Signal -/** - * Lifecycle signal that is fired when an Actor that was watched has terminated. - * Watching is performed by invoking the - * [[akka.typed.ActorContext]] `watch` method. The DeathWatch service is - * idempotent, meaning that registering twice has the same effect as registering - * once. Registration does not need to happen before the Actor terminates, a - * notification is guaranteed to arrive after both registration and termination - * have occurred. Termination of a remote Actor can also be effected by declaring - * the Actor’s home system as failed (e.g. as a result of being unreachable). - */ -@SerialVersionUID(1L) -final case class Terminated(ref: ActorRef[Nothing]) extends Signal - -/** - * The parent of an actor decides upon the fate of a failed child actor by - * encapsulating its next behavior in one of the four wrappers defined within - * this class. - * - * Failure responses have an associated precedence that ranks them, which is in - * descending importance: - * - * - Escalate - * - Stop - * - Restart - * - Resume - */ -object Failed { - - sealed trait Decision - - @SerialVersionUID(1L) - case object NoFailureResponse extends Decision - - /** - * Resuming the child actor means that the result of processing the message - * on which it failed is just ignored, the previous state will be used to - * process the next message. The message that triggered the failure will not - * be processed again. - */ - @SerialVersionUID(1L) - case object Resume extends Decision - - /** - * Restarting the child actor means resetting its behavior to the initial - * one that was provided during its creation (i.e. the one which was passed - * into the [[Props]] constructor). The previously failed behavior will - * receive a [[PreRestart]] signal before this happens and the replacement - * behavior will receive a [[PostRestart]] signal afterwards. - */ - @SerialVersionUID(1L) - case object Restart extends Decision - - /** - * Stopping the child actor will free its resources and eventually - * (asynchronously) unregister its name from the parent. Completion of this - * process can be observed by watching the child actor and reacting to its - * [[Terminated]] signal. - */ - @SerialVersionUID(1L) - case object Stop extends Decision - - /** - * The default response to a failure in a child actor is to escalate the - * failure, entailing that the parent actor fails as well. This is equivalent - * to an exception unwinding the call stack, but it applies to the supervision - * hierarchy instead. - */ - @SerialVersionUID(1L) - case object Escalate extends Decision - -} - object Behavior { /** @@ -259,7 +124,7 @@ object Behavior { * behavior) this method unwraps the behavior such that the innermost behavior * is returned, i.e. it removes the decorations. */ - def canonicalize[T](ctx: ActorContext[T], behavior: Behavior[T], current: Behavior[T]): Behavior[T] = + def canonicalize[T](behavior: Behavior[T], current: Behavior[T]): Behavior[T] = behavior match { case `sameBehavior` ⇒ current case `unhandledBehavior` ⇒ current @@ -270,4 +135,3 @@ object Behavior { def isUnhandled[T](behavior: Behavior[T]): Boolean = behavior eq unhandledBehavior } - diff --git a/akka-typed/src/main/scala/akka/typed/Dispatchers.scala b/akka-typed/src/main/scala/akka/typed/Dispatchers.scala new file mode 100644 index 0000000000..169c20cb4f --- /dev/null +++ b/akka-typed/src/main/scala/akka/typed/Dispatchers.scala @@ -0,0 +1,13 @@ +/** + * Copyright (C) 2016 Lightbend Inc. + */ +package akka.typed + +import scala.concurrent.ExecutionContextExecutor +import java.util.concurrent.Executors +import akka.event.LoggingAdapter + +trait Dispatchers { + def lookup(selector: DispatcherSelector): ExecutionContextExecutor + def shutdown(): Unit +} diff --git a/akka-typed/src/main/scala/akka/typed/Effects.scala b/akka-typed/src/main/scala/akka/typed/Effects.scala index 9f08cc9b20..555aee3732 100644 --- a/akka-typed/src/main/scala/akka/typed/Effects.scala +++ b/akka-typed/src/main/scala/akka/typed/Effects.scala @@ -20,7 +20,7 @@ object Effect { @SerialVersionUID(1L) final case class Stopped(childName: String) extends Effect @SerialVersionUID(1L) final case class Watched[T](other: ActorRef[T]) extends Effect @SerialVersionUID(1L) final case class Unwatched[T](other: ActorRef[T]) extends Effect - @SerialVersionUID(1L) final case class ReceiveTimeoutSet(d: Duration) extends Effect + @SerialVersionUID(1L) final case class ReceiveTimeoutSet[T](d: Duration, msg: T) extends Effect @SerialVersionUID(1L) final case class Messaged[U](other: ActorRef[U], msg: U) extends Effect @SerialVersionUID(1L) final case class Scheduled[U](delay: FiniteDuration, target: ActorRef[U], msg: U) extends Effect @SerialVersionUID(1L) case object EmptyEffect extends Effect @@ -31,7 +31,7 @@ object Effect { * on it and otherwise stubs them out like a [[StubbedActorContext]]. */ class EffectfulActorContext[T](_name: String, _props: Props[T], _system: ActorSystem[Nothing]) - extends StubbedActorContext[T](_name, _props)(_system) { + extends StubbedActorContext[T](_name, _props, _system) { import akka.{ actor ⇒ a } import Effect._ @@ -54,27 +54,18 @@ class EffectfulActorContext[T](_name: String, _props: Props[T], _system: ActorSy def currentBehavior: Behavior[T] = current - def run(msg: T): Unit = current = Behavior.canonicalize(this, current.message(this, msg), current) - def signal(signal: Signal): Unit = current = Behavior.canonicalize(this, current.management(this, signal), current) + def run(msg: T): Unit = current = Behavior.canonicalize(current.message(this, msg), current) + def signal(signal: Signal): Unit = current = Behavior.canonicalize(current.management(this, signal), current) override def spawnAnonymous[U](props: Props[U]): ActorRef[U] = { val ref = super.spawnAnonymous(props) - effectQueue.offer(Spawned(ref.untypedRef.path.name)) + effectQueue.offer(Spawned(ref.path.name)) ref } override def spawn[U](props: Props[U], name: String): ActorRef[U] = { effectQueue.offer(Spawned(name)) super.spawn(props, name) } - override def actorOf(props: a.Props): a.ActorRef = { - val ref = super.actorOf(props) - effectQueue.offer(Spawned(ref.path.name)) - ref - } - override def actorOf(props: a.Props, name: String): a.ActorRef = { - effectQueue.offer(Spawned(name)) - super.actorOf(props, name) - } override def stop(child: ActorRef[Nothing]): Boolean = { effectQueue.offer(Stopped(child.path.name)) super.stop(child) @@ -87,17 +78,13 @@ class EffectfulActorContext[T](_name: String, _props: Props[T], _system: ActorSy effectQueue.offer(Unwatched(other)) super.unwatch(other) } - override def watch(other: akka.actor.ActorRef): other.type = { - effectQueue.offer(Watched(ActorRef[Any](other))) - super.watch(other) + override def setReceiveTimeout(d: FiniteDuration, msg: T): Unit = { + effectQueue.offer(ReceiveTimeoutSet(d, msg)) + super.setReceiveTimeout(d, msg) } - override def unwatch(other: akka.actor.ActorRef): other.type = { - effectQueue.offer(Unwatched(ActorRef[Any](other))) - super.unwatch(other) - } - override def setReceiveTimeout(d: Duration): Unit = { - effectQueue.offer(ReceiveTimeoutSet(d)) - super.setReceiveTimeout(d) + override def cancelReceiveTimeout(): Unit = { + effectQueue.offer(ReceiveTimeoutSet(Duration.Undefined, null)) + super.cancelReceiveTimeout() } override def schedule[U](delay: FiniteDuration, target: ActorRef[U], msg: U): a.Cancellable = { effectQueue.offer(Scheduled(delay, target, msg)) diff --git a/akka-typed/src/main/scala/akka/typed/Impl.scala b/akka-typed/src/main/scala/akka/typed/Impl.scala deleted file mode 100644 index f04fd5d955..0000000000 --- a/akka-typed/src/main/scala/akka/typed/Impl.scala +++ /dev/null @@ -1,123 +0,0 @@ -/** - * Copyright (C) 2014-2016 Lightbend Inc. - */ -package akka.typed - -import akka.{ actor ⇒ a } -import scala.concurrent.duration.Duration -import scala.concurrent.duration.FiniteDuration -import scala.concurrent.ExecutionContextExecutor -import akka.event.LoggingReceive -import akka.actor.DeathPactException - -/** - * INTERNAL API. Mapping the execution of a [[Behavior]] onto a good old untyped - * [[akka.actor.Actor]]. - */ -private[typed] class ActorAdapter[T](_initialBehavior: () ⇒ Behavior[T]) extends akka.actor.Actor { - import Behavior._ - - var behavior = _initialBehavior() - val ctx = new ActorContextAdapter[T](context) - - def receive = LoggingReceive { - case akka.actor.Terminated(ref) ⇒ - val msg = Terminated(ActorRef(ref)) - next(behavior.management(ctx, msg), msg) - case akka.actor.ReceiveTimeout ⇒ - next(behavior.management(ctx, ReceiveTimeout), ReceiveTimeout) - case msg ⇒ - val m = msg.asInstanceOf[T] - next(behavior.message(ctx, m), m) - } - - private def next(b: Behavior[T], msg: Any): Unit = { - if (isUnhandled(b)) unhandled(msg) - behavior = canonicalize(ctx, b, behavior) - if (!isAlive(behavior)) { - context.stop(self) - } - } - - override def unhandled(msg: Any): Unit = msg match { - case Terminated(ref) ⇒ throw new DeathPactException(ref.untypedRef) - case other ⇒ super.unhandled(other) - } - - override val supervisorStrategy = a.OneForOneStrategy() { - case ex ⇒ - import Failed._ - import akka.actor.{ SupervisorStrategy ⇒ s } - val f = Failed(ex, ActorRef(sender())) - next(behavior.management(ctx, f), f) - f.getDecision match { - case Resume ⇒ s.Resume - case Restart ⇒ s.Restart - case Stop ⇒ s.Stop - case _ ⇒ s.Escalate - } - } - - override def preStart(): Unit = - next(behavior.management(ctx, PreStart), PreStart) - override def preRestart(reason: Throwable, message: Option[Any]): Unit = - next(behavior.management(ctx, PreRestart(reason)), PreRestart(reason)) - override def postRestart(reason: Throwable): Unit = - next(behavior.management(ctx, PostRestart(reason)), PostRestart(reason)) - override def postStop(): Unit = - next(behavior.management(ctx, PostStop), PostStop) -} - -/** - * INTERNAL API. Wrapping an [[akka.actor.ActorContext]] as an [[ActorContext]]. - */ -private[typed] class ActorContextAdapter[T](ctx: akka.actor.ActorContext) extends ActorContext[T] { - import Ops._ - def self = ActorRef(ctx.self) - def props = Props(ctx.props) - val system = ActorSystem(ctx.system) - def children = ctx.children.map(ActorRef(_)) - def child(name: String) = ctx.child(name).map(ActorRef(_)) - def spawnAnonymous[U](props: Props[U]) = ctx.spawn(props) - def spawn[U](props: Props[U], name: String) = ctx.spawn(props, name) - def actorOf(props: a.Props) = ctx.actorOf(props) - def actorOf(props: a.Props, name: String) = ctx.actorOf(props, name) - def stop(child: ActorRef[Nothing]) = - child.untypedRef match { - case f: akka.actor.FunctionRef ⇒ - val cell = ctx.asInstanceOf[akka.actor.ActorCell] - cell.removeFunctionRef(f) - case _ ⇒ - ctx.child(child.path.name) match { - case Some(ref) if ref == child.untypedRef ⇒ - ctx.stop(child.untypedRef) - true - case _ ⇒ - false // none of our business - } - } - def watch[U](other: ActorRef[U]) = { ctx.watch(other.untypedRef); other } - def watch(other: a.ActorRef) = { ctx.watch(other); other } - def unwatch[U](other: ActorRef[U]) = { ctx.unwatch(other.untypedRef); other } - def unwatch(other: a.ActorRef) = { ctx.unwatch(other); other } - def setReceiveTimeout(d: Duration) = ctx.setReceiveTimeout(d) - def executionContext: ExecutionContextExecutor = ctx.dispatcher - def schedule[U](delay: FiniteDuration, target: ActorRef[U], msg: U): a.Cancellable = { - import ctx.dispatcher - ctx.system.scheduler.scheduleOnce(delay, target.untypedRef, msg) - } - def spawnAdapter[U](f: U ⇒ T) = { - val cell = ctx.asInstanceOf[akka.actor.ActorCell] - val ref = cell.addFunctionRef((_, msg) ⇒ ctx.self ! f(msg.asInstanceOf[U])) - ActorRef[U](ref) - } -} - -/** - * INTERNAL API. A small Actor that translates between message protocols. - */ -private[typed] class MessageWrapper(f: Any ⇒ Any) extends akka.actor.Actor { - def receive = { - case msg ⇒ context.parent ! f(msg) - } -} diff --git a/akka-typed/src/main/scala/akka/typed/Inbox.scala b/akka-typed/src/main/scala/akka/typed/Inbox.scala index b8e9c5bc76..e1ac2289ad 100644 --- a/akka-typed/src/main/scala/akka/typed/Inbox.scala +++ b/akka-typed/src/main/scala/akka/typed/Inbox.scala @@ -10,31 +10,34 @@ import akka.actor.Address import scala.collection.immutable import scala.annotation.tailrec import akka.actor.ActorRefProvider +import java.util.concurrent.ThreadLocalRandom + +class Inbox[T](name: String) { + + private val q = new ConcurrentLinkedQueue[T] + + val ref: ActorRef[T] = { + val uid = ThreadLocalRandom.current().nextInt() + val path = RootActorPath(Address("akka.typed.inbox", "anonymous")).child(name).withUid(uid) + new internal.FunctionRef[T](path, (msg, self) ⇒ q.add(msg), (self) ⇒ ()) + } + + def receiveMsg(): T = q.poll() match { + case null ⇒ throw new NoSuchElementException(s"polling on an empty inbox: $name") + case x ⇒ x + } + + def receiveAll(): immutable.Seq[T] = { + @tailrec def rec(acc: List[T]): List[T] = q.poll() match { + case null ⇒ acc.reverse + case x ⇒ rec(x :: acc) + } + rec(Nil) + } + + def hasMessages: Boolean = q.peek() != null +} object Inbox { - - def sync[T](name: String): SyncInbox[T] = new SyncInbox(name) - - class SyncInbox[T](name: String) { - private val q = new ConcurrentLinkedQueue[T] - private val r = new akka.actor.MinimalActorRef { - override def provider: ActorRefProvider = ??? - override val path: ActorPath = RootActorPath(Address("akka", "SyncInbox")) / name - override def !(msg: Any)(implicit sender: akka.actor.ActorRef) = q.offer(msg.asInstanceOf[T]) - } - - val ref: ActorRef[T] = ActorRef(r) - def receiveMsg(): T = q.poll() match { - case null ⇒ throw new NoSuchElementException(s"polling on an empty inbox: $name") - case x ⇒ x - } - def receiveAll(): immutable.Seq[T] = { - @tailrec def rec(acc: List[T]): List[T] = q.poll() match { - case null ⇒ acc.reverse - case x ⇒ rec(x :: acc) - } - rec(Nil) - } - def hasMessages: Boolean = q.peek() != null - } + def apply[T](name: String): Inbox[T] = new Inbox(name) } diff --git a/akka-typed/src/main/scala/akka/typed/MessageAndSignals.scala b/akka-typed/src/main/scala/akka/typed/MessageAndSignals.scala new file mode 100644 index 0000000000..0f33f6a0a5 --- /dev/null +++ b/akka-typed/src/main/scala/akka/typed/MessageAndSignals.scala @@ -0,0 +1,141 @@ +/** + * Copyright (C) 2016 Lightbend Inc. + */ +package akka.typed + +/** + * Envelope that is published on the eventStream for every message that is + * dropped due to overfull queues. + */ +final case class Dropped(msg: Any, recipient: ActorRef[Nothing]) + +/** + * Exception that an actor fails with if it does not handle a Terminated message. + */ +final case class DeathPactException(ref: ActorRef[Nothing]) extends RuntimeException(s"death pact with $ref was triggered") + +/** + * Envelope for dead letters. + */ +final case class DeadLetter(msg: Any) + +/** + * System signals are notifications that are generated by the system and + * delivered to the Actor behavior in a reliable fashion (i.e. they are + * guaranteed to arrive in contrast to the at-most-once semantics of normal + * Actor messages). + */ +sealed trait Signal + +/** + * Lifecycle signal that is fired upon creation of the Actor. This will be the + * first message that the actor processes. + */ +@SerialVersionUID(1L) +final case object PreStart extends Signal + +/** + * Lifecycle signal that is fired upon restart of the Actor before replacing + * the behavior with the fresh one (i.e. this signal is received within the + * behavior that failed). + */ +@SerialVersionUID(1L) +final case object PreRestart extends Signal + +/** + * Lifecycle signal that is fired upon restart of the Actor after replacing + * the behavior with the fresh one (i.e. this signal is received within the + * fresh replacement behavior). + */ +@SerialVersionUID(1L) +final case object PostRestart extends Signal + +/** + * Lifecycle signal that is fired after this actor and all its child actors + * (transitively) have terminated. The [[Terminated]] signal is only sent to + * registered watchers after this signal has been processed. + * + * IMPORTANT NOTE: if the actor terminated by switching to the + * `Stopped` behavior then this signal will be ignored (i.e. the + * Stopped behavior will do nothing in reaction to it). + */ +@SerialVersionUID(1L) +final case object PostStop extends Signal + +/** + * Lifecycle signal that is fired when an Actor that was watched has terminated. + * Watching is performed by invoking the + * [[akka.typed.ActorContext]] `watch` method. The DeathWatch service is + * idempotent, meaning that registering twice has the same effect as registering + * once. Registration does not need to happen before the Actor terminates, a + * notification is guaranteed to arrive after both registration and termination + * have occurred. Termination of a remote Actor can also be effected by declaring + * the Actor’s home system as failed (e.g. as a result of being unreachable). + */ +@SerialVersionUID(1L) +final case class Terminated(ref: ActorRef[Nothing])(failed: Throwable) extends Signal { + def wasFailed: Boolean = failed ne null + def failure: Throwable = failed + def failureOption: Option[Throwable] = Option(failed) +} + +/** + * FIXME correct this documentation when the Restarter behavior has been implemented + * + * The parent of an actor decides upon the fate of a failed child actor by + * encapsulating its next behavior in one of the four wrappers defined within + * this class. + * + * Failure responses have an associated precedence that ranks them, which is in + * descending importance: + * + * - Escalate + * - Stop + * - Restart + * - Resume + */ +object Failed { + + sealed trait Decision + + @SerialVersionUID(1L) + case object NoFailureResponse extends Decision + + /** + * Resuming the child actor means that the result of processing the message + * on which it failed is just ignored, the previous state will be used to + * process the next message. The message that triggered the failure will not + * be processed again. + */ + @SerialVersionUID(1L) + case object Resume extends Decision + + /** + * Restarting the child actor means resetting its behavior to the initial + * one that was provided during its creation (i.e. the one which was passed + * into the [[Props]] constructor). The previously failed behavior will + * receive a [[PreRestart]] signal before this happens and the replacement + * behavior will receive a [[PostRestart]] signal afterwards. + */ + @SerialVersionUID(1L) + case object Restart extends Decision + + /** + * Stopping the child actor will free its resources and eventually + * (asynchronously) unregister its name from the parent. Completion of this + * process can be observed by watching the child actor and reacting to its + * [[Terminated]] signal. + */ + @SerialVersionUID(1L) + case object Stop extends Decision + + /** + * The default response to a failure in a child actor is to escalate the + * failure, entailing that the parent actor fails as well. This is equivalent + * to an exception unwinding the call stack, but it applies to the supervision + * hierarchy instead. + */ + @SerialVersionUID(1L) + case object Escalate extends Decision + +} diff --git a/akka-typed/src/main/scala/akka/typed/Ops.scala b/akka-typed/src/main/scala/akka/typed/Ops.scala deleted file mode 100644 index 8b1e1cfbbb..0000000000 --- a/akka-typed/src/main/scala/akka/typed/Ops.scala +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright (C) 2014-2016 Lightbend Inc. - */ -package akka.typed - -import language.implicitConversions - -/** - * Import the contents of this object to retrofit the typed APIs onto the - * untyped [[akka.actor.ActorSystem]], [[akka.actor.ActorContext]] and - * [[akka.actor.ActorRef]]. - */ -object Ops { - - implicit class ActorSystemOps(val sys: akka.actor.ActorSystem) extends AnyVal { - def spawn[T](props: Props[T]): ActorRef[T] = - ActorRef(sys.actorOf(Props.untyped(props))) - def spawn[T](props: Props[T], name: String): ActorRef[T] = - ActorRef(sys.actorOf(Props.untyped(props), name)) - } - - implicit class ActorContextOps(val ctx: akka.actor.ActorContext) extends AnyVal { - def spawn[T](props: Props[T]): ActorRef[T] = - ActorRef(ctx.actorOf(Props.untyped(props))) - def spawn[T](props: Props[T], name: String): ActorRef[T] = - ActorRef(ctx.actorOf(Props.untyped(props), name)) - } - - implicit def actorRefAdapter(ref: akka.actor.ActorRef): ActorRef[Any] = ActorRef(ref) - -} diff --git a/akka-typed/src/main/scala/akka/typed/Props.scala b/akka-typed/src/main/scala/akka/typed/Props.scala index ff27b2f2df..ca29cf1571 100644 --- a/akka-typed/src/main/scala/akka/typed/Props.scala +++ b/akka-typed/src/main/scala/akka/typed/Props.scala @@ -3,18 +3,23 @@ */ package akka.typed -import akka.actor.Deploy -import akka.routing.RouterConfig +import java.util.concurrent.Executor +import scala.concurrent.ExecutionContext + +sealed trait DispatcherSelector +case object DispatcherDefault extends DispatcherSelector +final case class DispatcherFromConfig(path: String) extends DispatcherSelector +final case class DispatcherFromExecutor(executor: Executor) extends DispatcherSelector +final case class DispatcherFromExecutionContext(ec: ExecutionContext) extends DispatcherSelector /** * Props describe how to dress up a [[Behavior]] so that it can become an Actor. */ -@SerialVersionUID(1L) -final case class Props[T](creator: () ⇒ Behavior[T], deploy: Deploy) { - def withDispatcher(d: String) = copy(deploy = deploy.copy(dispatcher = d)) - def withMailbox(m: String) = copy(deploy = deploy.copy(mailbox = m)) - def withRouter(r: RouterConfig) = copy(deploy = deploy.copy(routerConfig = r)) - def withDeploy(d: Deploy) = copy(deploy = d) +final case class Props[T](creator: () ⇒ Behavior[T], dispatcher: DispatcherSelector, mailboxCapacity: Int) { + def withDispatcher(configPath: String) = copy(dispatcher = DispatcherFromConfig(configPath)) + def withDispatcher(executor: Executor) = copy(dispatcher = DispatcherFromExecutor(executor)) + def withDispatcher(ec: ExecutionContext) = copy(dispatcher = DispatcherFromExecutionContext(ec)) + def withQueueSize(size: Int) = copy(mailboxCapacity = size) } /** @@ -27,7 +32,7 @@ object Props { * FIXME: investigate the pros and cons of making this take an explicit * function instead of a by-name argument */ - def apply[T](block: ⇒ Behavior[T]): Props[T] = Props(() ⇒ block, akka.actor.Props.defaultDeploy) + def apply[T](block: ⇒ Behavior[T]): Props[T] = Props(() ⇒ block, DispatcherDefault, Int.MaxValue) /** * Props for a Behavior that just ignores all messages. @@ -35,21 +40,4 @@ object Props { def empty[T]: Props[T] = _empty.asInstanceOf[Props[T]] private val _empty: Props[Any] = Props(ScalaDSL.Static[Any] { case _ ⇒ ScalaDSL.Unhandled }) - /** - * INTERNAL API. - */ - private[typed] def untyped[T](p: Props[T]): akka.actor.Props = - new akka.actor.Props(p.deploy, classOf[ActorAdapter[_]], p.creator :: Nil) - - /** - * INTERNAL API. - */ - private[typed] def apply[T](p: akka.actor.Props): Props[T] = { - assert(p.clazz == classOf[ActorAdapter[_]], "typed.Actor must have typed.Props") - p.args match { - case (creator: Function0[_]) :: Nil ⇒ - Props(creator.asInstanceOf[Function0[Behavior[T]]], p.deploy) - case _ ⇒ throw new AssertionError("typed.Actor args must be right") - } - } } diff --git a/akka-typed/src/main/scala/akka/typed/ScalaDSL.scala b/akka-typed/src/main/scala/akka/typed/ScalaDSL.scala index 395a02a529..b0a3488deb 100644 --- a/akka-typed/src/main/scala/akka/typed/ScalaDSL.scala +++ b/akka-typed/src/main/scala/akka/typed/ScalaDSL.scala @@ -44,7 +44,7 @@ object ScalaDSL { private def postProcess(ctx: ActorContext[U], behv: Behavior[T]): Behavior[U] = if (isUnhandled(behv)) Unhandled else if (isAlive(behv)) { - val next = canonicalize(ctx.asInstanceOf[ActorContext[T]], behv, behavior) + val next = canonicalize(behv, behavior) if (next eq behavior) Same else Widened(next, matcher) } else Stopped @@ -134,14 +134,14 @@ object ScalaDSL { final case class Full[T](behavior: PartialFunction[MessageOrSignal[T], Behavior[T]]) extends Behavior[T] { override def management(ctx: ActorContext[T], msg: Signal): Behavior[T] = { lazy val fallback: (MessageOrSignal[T]) ⇒ Behavior[T] = { - case Sig(context, PreRestart(_)) ⇒ + case Sig(context, PreRestart) ⇒ context.children foreach { child ⇒ - context.unwatch(child.untypedRef) + context.unwatch[Nothing](child) context.stop(child) } behavior.applyOrElse(Sig(context, PostStop), fallback) - case Sig(context, PostRestart(_)) ⇒ behavior.applyOrElse(Sig(context, PreStart), fallback) - case _ ⇒ Unhandled + case Sig(context, PostRestart) ⇒ behavior.applyOrElse(Sig(context, PreStart), fallback) + case _ ⇒ Unhandled } behavior.applyOrElse(Sig(ctx, msg), fallback) } @@ -253,11 +253,11 @@ object ScalaDSL { * sides of [[And]] and [[Or]] combinators. */ final case class SynchronousSelf[T](f: ActorRef[T] ⇒ Behavior[T]) extends Behavior[T] { - private val inbox = Inbox.sync[T]("syncbox") + private val inbox = Inbox[T]("synchronousSelf") private var _behavior = f(inbox.ref) private def behavior = _behavior private def setBehavior(ctx: ActorContext[T], b: Behavior[T]): Unit = - _behavior = canonicalize(ctx, b, _behavior) + _behavior = canonicalize(b, _behavior) // FIXME should we protect against infinite loops? @tailrec private def run(ctx: ActorContext[T], next: Behavior[T]): Behavior[T] = { @@ -290,8 +290,8 @@ object ScalaDSL { val r = right.management(ctx, msg) if (isUnhandled(l) && isUnhandled(r)) Unhandled else { - val nextLeft = canonicalize(ctx, l, left) - val nextRight = canonicalize(ctx, r, right) + val nextLeft = canonicalize(l, left) + val nextRight = canonicalize(r, right) val leftAlive = isAlive(nextLeft) val rightAlive = isAlive(nextRight) @@ -307,8 +307,8 @@ object ScalaDSL { val r = right.message(ctx, msg) if (isUnhandled(l) && isUnhandled(r)) Unhandled else { - val nextLeft = canonicalize(ctx, l, left) - val nextRight = canonicalize(ctx, r, right) + val nextLeft = canonicalize(l, left) + val nextRight = canonicalize(r, right) val leftAlive = isAlive(nextLeft) val rightAlive = isAlive(nextRight) @@ -337,11 +337,11 @@ object ScalaDSL { val r = right.management(ctx, msg) if (isUnhandled(r)) Unhandled else { - val nr = canonicalize(ctx, r, right) + val nr = canonicalize(r, right) if (isAlive(nr)) Or(left, nr) else left } case nl ⇒ - val next = canonicalize(ctx, nl, left) + val next = canonicalize(nl, left) if (isAlive(next)) Or(next, right) else right } @@ -351,11 +351,11 @@ object ScalaDSL { val r = right.message(ctx, msg) if (isUnhandled(r)) Unhandled else { - val nr = canonicalize(ctx, r, right) + val nr = canonicalize(r, right) if (isAlive(nr)) Or(left, nr) else left } case nl ⇒ - val next = canonicalize(ctx, nl, left) + val next = canonicalize(nl, left) if (isAlive(next)) Or(next, right) else right } } @@ -394,10 +394,10 @@ object ScalaDSL { FullTotal { case Sig(ctx, signal) ⇒ val behv = behavior(ctx.self) - canonicalize(ctx, behv.management(ctx, signal), behv) + canonicalize(behv.management(ctx, signal), behv) case Msg(ctx, msg) ⇒ val behv = behavior(ctx.self) - canonicalize(ctx, behv.message(ctx, msg), behv) + canonicalize(behv.message(ctx, msg), behv) } /** @@ -418,10 +418,10 @@ object ScalaDSL { FullTotal { case Sig(ctx, signal) ⇒ val behv = behavior(ctx) - canonicalize(ctx, behv.management(ctx, signal), behv) + canonicalize(behv.management(ctx, signal), behv) case Msg(ctx, msg) ⇒ val behv = behavior(ctx) - canonicalize(ctx, behv.message(ctx, msg), behv) + canonicalize(behv.message(ctx, msg), behv) } /** diff --git a/akka-typed/src/main/scala/akka/typed/adapter/ActorAdapter.scala b/akka-typed/src/main/scala/akka/typed/adapter/ActorAdapter.scala new file mode 100644 index 0000000000..bf7237196e --- /dev/null +++ b/akka-typed/src/main/scala/akka/typed/adapter/ActorAdapter.scala @@ -0,0 +1,65 @@ +/** + * Copyright (C) 2016 Lightbend Inc. + */ +package akka.typed +package adapter + +import akka.{ actor ⇒ a } + +private[typed] class ActorAdapter[T](_initialBehavior: () ⇒ Behavior[T]) extends a.Actor { + import Behavior._ + + var behavior: Behavior[T] = _ + + { + behavior = canonicalize(_initialBehavior(), behavior) + if (behavior == null) throw new IllegalStateException("initial behavior cannot be `same` or `unhandled`") + if (!isAlive(behavior)) context.stop(self) + } + + val ctx = new ActorContextAdapter[T](context) + + var failures: Map[a.ActorRef, Throwable] = Map.empty + + def receive = { + case a.Terminated(ref) ⇒ + val msg = + if (failures contains ref) { + val ex = failures(ref) + failures -= ref + Terminated(ActorRefAdapter(ref))(ex) + } else Terminated(ActorRefAdapter(ref))(null) + next(behavior.management(ctx, msg), msg) + case a.ReceiveTimeout ⇒ + next(behavior.message(ctx, ctx.receiveTimeoutMsg), ctx.receiveTimeoutMsg) + case msg: T @unchecked ⇒ + next(behavior.message(ctx, msg), msg) + } + + private def next(b: Behavior[T], msg: Any): Unit = { + if (isUnhandled(b)) unhandled(msg) + behavior = canonicalize(b, behavior) + if (!isAlive(behavior)) context.stop(self) + } + + override def unhandled(msg: Any): Unit = msg match { + case Terminated(ref) ⇒ throw new a.DeathPactException(toUntyped(ref)) + case other ⇒ super.unhandled(other) + } + + override val supervisorStrategy = a.OneForOneStrategy() { + case ex ⇒ + val ref = sender() + if (context.asInstanceOf[a.ActorCell].isWatching(ref)) failures = failures.updated(ref, ex) + a.SupervisorStrategy.Stop + } + + override def preStart(): Unit = + next(behavior.management(ctx, PreStart), PreStart) + override def preRestart(reason: Throwable, message: Option[Any]): Unit = + next(behavior.management(ctx, PreRestart), PreRestart) + override def postRestart(reason: Throwable): Unit = + next(behavior.management(ctx, PostRestart), PostRestart) + override def postStop(): Unit = + next(behavior.management(ctx, PostStop), PostStop) +} diff --git a/akka-typed/src/main/scala/akka/typed/adapter/ActorContextAdapter.scala b/akka-typed/src/main/scala/akka/typed/adapter/ActorContextAdapter.scala new file mode 100644 index 0000000000..5cc02bbf58 --- /dev/null +++ b/akka-typed/src/main/scala/akka/typed/adapter/ActorContextAdapter.scala @@ -0,0 +1,59 @@ +/** + * Copyright (C) 2016 Lightbend Inc. + */ +package akka.typed +package adapter + +import akka.{ actor ⇒ a } +import scala.concurrent.duration._ +import scala.concurrent.ExecutionContextExecutor + +/** + * INTERNAL API. Wrapping an [[akka.actor.ActorContext]] as an [[ActorContext]]. + */ +private[typed] class ActorContextAdapter[T](ctx: a.ActorContext) extends ActorContext[T] { + + override def self = ActorRefAdapter(ctx.self) + override def props = PropsAdapter(ctx.props) + override val system = ActorSystemAdapter(ctx.system) + override def children = ctx.children.map(ActorRefAdapter(_)) + override def child(name: String) = ctx.child(name).map(ActorRefAdapter(_)) + override def spawnAnonymous[U](props: Props[U]) = ctx.spawnAnonymous(props) + override def spawn[U](props: Props[U], name: String) = ctx.spawn(props, name) + override def stop(child: ActorRef[Nothing]) = + toUntyped(child) match { + case f: akka.actor.FunctionRef ⇒ + val cell = ctx.asInstanceOf[akka.actor.ActorCell] + cell.removeFunctionRef(f) + case untyped ⇒ + ctx.child(child.path.name) match { + case Some(`untyped`) ⇒ + ctx.stop(untyped) + true + case _ ⇒ + false // none of our business + } + } + override def watch[U](other: ActorRef[U]) = { ctx.watch(toUntyped(other)); other } + override def unwatch[U](other: ActorRef[U]) = { ctx.unwatch(toUntyped(other)); other } + var receiveTimeoutMsg: T = null.asInstanceOf[T] + override def setReceiveTimeout(d: FiniteDuration, msg: T) = { + receiveTimeoutMsg = msg + ctx.setReceiveTimeout(d) + } + override def cancelReceiveTimeout(): Unit = { + receiveTimeoutMsg = null.asInstanceOf[T] + ctx.setReceiveTimeout(Duration.Undefined) + } + override def executionContext: ExecutionContextExecutor = ctx.dispatcher + override def schedule[U](delay: FiniteDuration, target: ActorRef[U], msg: U): a.Cancellable = { + import ctx.dispatcher + ctx.system.scheduler.scheduleOnce(delay, toUntyped(target), msg) + } + override def spawnAdapter[U](f: U ⇒ T): ActorRef[U] = { + val cell = ctx.asInstanceOf[akka.actor.ActorCell] + val ref = cell.addFunctionRef((_, msg) ⇒ ctx.self ! f(msg.asInstanceOf[U])) + ActorRefAdapter[U](ref) + } + +} diff --git a/akka-typed/src/main/scala/akka/typed/adapter/ActorRefAdapter.scala b/akka-typed/src/main/scala/akka/typed/adapter/ActorRefAdapter.scala new file mode 100644 index 0000000000..2922bc8e0b --- /dev/null +++ b/akka-typed/src/main/scala/akka/typed/adapter/ActorRefAdapter.scala @@ -0,0 +1,19 @@ +/** + * Copyright (C) 2016 Lightbend Inc. + */ +package akka.typed +package adapter + +import akka.{ actor ⇒ a } + +private[typed] class ActorRefAdapter[-T](val untyped: a.InternalActorRef) + extends ActorRef[T](untyped.path) with internal.ActorRefImpl[T] { + + override def tell(msg: T): Unit = untyped ! msg + override def isLocal: Boolean = true + override def sendSystem(signal: internal.SystemMessage): Unit = sendSystemMessage(untyped, signal) +} + +private[typed] object ActorRefAdapter { + def apply[T](untyped: a.ActorRef): ActorRef[T] = new ActorRefAdapter(untyped.asInstanceOf[a.InternalActorRef]) +} diff --git a/akka-typed/src/main/scala/akka/typed/adapter/ActorSystemAdapter.scala b/akka-typed/src/main/scala/akka/typed/adapter/ActorSystemAdapter.scala new file mode 100644 index 0000000000..3b2dd72bd3 --- /dev/null +++ b/akka-typed/src/main/scala/akka/typed/adapter/ActorSystemAdapter.scala @@ -0,0 +1,64 @@ +/** + * Copyright (C) 2016 Lightbend Inc. + */ +package akka.typed +package adapter + +import akka.{ actor ⇒ a } +import akka.dispatch.sysmsg +import scala.concurrent.ExecutionContextExecutor + +/** + * Lightweight wrapper for presenting an untyped ActorSystem to a Behavior (via the context). + * Therefore it does not have a lot of vals, only the whenTerminated Future is cached after + * its transformation because redoing that every time will add extra objects that persis for + * a longer time; in all other cases the wrapper will just be spawned for a single call in + * most circumstances. + */ +private[typed] class ActorSystemAdapter[-T](val untyped: a.ActorSystemImpl) + extends ActorRef[T](a.RootActorPath(a.Address("akka", untyped.name)) / "user") + with ActorSystem[T] with internal.ActorRefImpl[T] { + + // Members declared in akka.typed.ActorRef + override def tell(msg: T): Unit = untyped.guardian ! msg + override def isLocal: Boolean = true + override def sendSystem(signal: internal.SystemMessage): Unit = sendSystemMessage(untyped.guardian, signal) + + // Members declared in akka.typed.ActorSystem + override def deadLetters[U]: ActorRef[U] = ActorRefAdapter(untyped.deadLetters) + override def dispatchers: Dispatchers = new Dispatchers { + override def lookup(selector: DispatcherSelector): ExecutionContextExecutor = + selector match { + case DispatcherDefault ⇒ untyped.dispatcher + case DispatcherFromConfig(str) ⇒ untyped.dispatchers.lookup(str) + case DispatcherFromExecutionContext(_) ⇒ throw new UnsupportedOperationException("cannot use DispatcherFromExecutionContext with ActorSystemAdapter") + case DispatcherFromExecutor(_) ⇒ throw new UnsupportedOperationException("cannot use DispatcherFromExecutor with ActorSystemAdapter") + } + override def shutdown(): Unit = () // there was no shutdown in untyped Akka + } + override def dynamicAccess: a.DynamicAccess = untyped.dynamicAccess + override def eventStream: akka.event.EventStream = untyped.eventStream + implicit override def executionContext: scala.concurrent.ExecutionContextExecutor = untyped.dispatcher + override def log: akka.event.LoggingAdapter = untyped.log + override def logConfiguration(): Unit = untyped.logConfiguration() + override def logFilter: akka.event.LoggingFilter = untyped.logFilter + override def name: String = untyped.name + override def scheduler: akka.actor.Scheduler = untyped.scheduler + override def settings: akka.actor.ActorSystem.Settings = untyped.settings + override def startTime: Long = untyped.startTime + override def threadFactory: java.util.concurrent.ThreadFactory = untyped.threadFactory + override def uptime: Long = untyped.uptime + override def printTree: String = untyped.printTree + + import akka.dispatch.ExecutionContexts.sameThreadExecutionContext + + override def terminate(): scala.concurrent.Future[akka.typed.Terminated] = + untyped.terminate().map(t ⇒ Terminated(ActorRefAdapter(t.actor))(null))(sameThreadExecutionContext) + override lazy val whenTerminated: scala.concurrent.Future[akka.typed.Terminated] = + untyped.whenTerminated.map(t ⇒ Terminated(ActorRefAdapter(t.actor))(null))(sameThreadExecutionContext) + +} + +private[typed] object ActorSystemAdapter { + def apply(untyped: a.ActorSystem): ActorSystem[Nothing] = new ActorSystemAdapter(untyped.asInstanceOf[a.ActorSystemImpl]) +} diff --git a/akka-typed/src/main/scala/akka/typed/adapter/PropsAdapter.scala b/akka-typed/src/main/scala/akka/typed/adapter/PropsAdapter.scala new file mode 100644 index 0000000000..0377f93c4c --- /dev/null +++ b/akka-typed/src/main/scala/akka/typed/adapter/PropsAdapter.scala @@ -0,0 +1,24 @@ +/** + * Copyright (C) 2016 Lightbend Inc. + */ +package akka.typed +package adapter + +import akka.{ actor ⇒ a } + +private[typed] object PropsAdapter { + + // FIXME dispatcher and queue size + def apply(p: Props[_]): a.Props = new a.Props(a.Deploy(), classOf[ActorAdapter[_]], (p.creator: AnyRef) :: Nil) + + def apply[T](p: a.Props): Props[T] = { + assert(p.clazz == classOf[ActorAdapter[_]], "typed.Actor must have typed.Props") + p.args match { + case (creator: Function0[_]) :: Nil ⇒ + // FIXME queue size + Props(creator.asInstanceOf[() ⇒ Behavior[T]], DispatcherFromConfig(p.deploy.dispatcher), Int.MaxValue) + case _ ⇒ throw new AssertionError("typed.Actor args must be right") + } + } + +} diff --git a/akka-typed/src/main/scala/akka/typed/adapter/package.scala b/akka-typed/src/main/scala/akka/typed/adapter/package.scala new file mode 100644 index 0000000000..055fc6a17a --- /dev/null +++ b/akka-typed/src/main/scala/akka/typed/adapter/package.scala @@ -0,0 +1,43 @@ +/** + * Copyright (C) 2016 Lightbend Inc. + */ +package akka.typed + +package object adapter { + + import language.implicitConversions + import akka.dispatch.sysmsg + + implicit class ActorSystemOps(val sys: akka.actor.ActorSystem) extends AnyVal { + def spawnAnonymous[T](props: Props[T]): ActorRef[T] = + ActorRefAdapter(sys.actorOf(PropsAdapter(props))) + def spawn[T](props: Props[T], name: String): ActorRef[T] = + ActorRefAdapter(sys.actorOf(PropsAdapter(props), name)) + } + + implicit class ActorContextOps(val ctx: akka.actor.ActorContext) extends AnyVal { + def spawnAnonymous[T](props: Props[T]): ActorRef[T] = + ActorRefAdapter(ctx.actorOf(PropsAdapter(props))) + def spawn[T](props: Props[T], name: String): ActorRef[T] = + ActorRefAdapter(ctx.actorOf(PropsAdapter(props), name)) + } + + implicit def actorRefAdapter(ref: akka.actor.ActorRef): ActorRef[Any] = ActorRefAdapter(ref) + + private[adapter] def toUntyped[U](ref: ActorRef[U]): akka.actor.InternalActorRef = + ref match { + case adapter: ActorRefAdapter[_] ⇒ adapter.untyped + case _ ⇒ throw new UnsupportedOperationException(s"only adapted untyped ActorRefs permissible ($ref of class ${ref.getClass})") + } + + private[adapter] def sendSystemMessage(untyped: akka.actor.InternalActorRef, signal: internal.SystemMessage): Unit = + signal match { + case internal.Create() ⇒ throw new IllegalStateException("WAT? No, seriously.") + case internal.Terminate() ⇒ untyped.stop() + case internal.Watch(watchee, watcher) ⇒ untyped.sendSystemMessage(sysmsg.Watch(toUntyped(watchee), toUntyped(watcher))) + case internal.Unwatch(watchee, watcher) ⇒ untyped.sendSystemMessage(sysmsg.Unwatch(toUntyped(watchee), toUntyped(watcher))) + case internal.DeathWatchNotification(ref, cause) ⇒ untyped.sendSystemMessage(sysmsg.DeathWatchNotification(toUntyped(ref), true, false)) + case internal.NoMessage ⇒ // just to suppress the warning + } + +} diff --git a/akka-typed/src/main/scala/akka/typed/internal/ActorCell.scala b/akka-typed/src/main/scala/akka/typed/internal/ActorCell.scala new file mode 100644 index 0000000000..5f335e8d08 --- /dev/null +++ b/akka-typed/src/main/scala/akka/typed/internal/ActorCell.scala @@ -0,0 +1,435 @@ +/** + * Copyright (C) 2016 Lightbend Inc. + */ +package akka.typed +package internal + +import akka.actor.InvalidActorNameException +import akka.util.Helpers +import scala.concurrent.duration.{ Duration, FiniteDuration } +import akka.dispatch.ExecutionContexts +import scala.concurrent.ExecutionContextExecutor +import akka.actor.Cancellable +import akka.util.Unsafe.{ instance ⇒ unsafe } +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.Queue +import scala.annotation.{ tailrec, switch } +import scala.util.control.NonFatal +import scala.util.control.Exception.Catcher +import akka.event.Logging.Error +import akka.event.Logging + +/** + * INTERNAL API + */ +object ActorCell { + /* + * Description of the _status field bit structure: + * + * bit 0-29: activation count (number of (system)messages) + * bit 30: terminating (or terminated) + * bit 31: terminated + * + * Activation count is a bit special: + * 0 means inactive + * 1 means active without normal messages (i.e. only system messages) + * N means active with N-1 normal messages (plus possibly system messages) + */ + final val terminatingShift = 30 + + final val activationMask = (1 << terminatingShift) - 1 + // ensure that if all processors enqueue “the last message” concurrently, there is still no overflow + val maxActivations = activationMask - Runtime.getRuntime.availableProcessors - 1 + + final val terminatingBit = 1 << terminatingShift + final val terminatedBit = 1 << 31 + + def isTerminating(status: Int): Boolean = (status & terminatingBit) != 0 + def isTerminated(status: Int): Boolean = status < 0 + def isActive(status: Int): Boolean = (status & ~activationMask) == 0 + + def activations(status: Int): Int = status & activationMask + def messageCount(status: Int): Int = Math.max(0, activations(status) - 1) + + val statusOffset = unsafe.objectFieldOffset(classOf[ActorCell[_]].getDeclaredField("_status")) + val systemQueueOffset = unsafe.objectFieldOffset(classOf[ActorCell[_]].getDeclaredField("_systemQueue")) + + final val DefaultState = 0 + final val SuspendedState = 1 + final val SuspendedWaitForChildrenState = 2 + + final val Debug = false +} + +/** + * INTERNAL API + */ +private[typed] class ActorCell[T]( + override val system: ActorSystem[Nothing], + override val props: Props[T], + val parent: ActorRefImpl[Nothing]) + extends ActorContext[T] with Runnable with SupervisionMechanics[T] with DeathWatch[T] { + import ActorCell._ + + /* + * Implementation of the ActorContext trait. + */ + + protected var childrenMap = Map.empty[String, ActorRefImpl[Nothing]] + protected var terminatingMap = Map.empty[String, ActorRefImpl[Nothing]] + override def children: Iterable[ActorRef[Nothing]] = childrenMap.values + override def child(name: String): Option[ActorRef[Nothing]] = childrenMap.get(name) + protected def removeChild(actor: ActorRefImpl[Nothing]): Unit = { + val n = actor.path.name + childrenMap.get(n) match { + case Some(`actor`) ⇒ childrenMap -= n + case _ ⇒ + terminatingMap.get(n) match { + case Some(`actor`) ⇒ terminatingMap -= n + case _ ⇒ + } + } + } + private[typed] def terminating: Iterable[ActorRef[Nothing]] = terminatingMap.values + + private var _self: ActorRefImpl[T] = _ + private[typed] def setSelf(ref: ActorRefImpl[T]): Unit = _self = ref + override def self: ActorRefImpl[T] = _self + + protected def ctx: ActorContext[T] = this + + override def spawn[U](props: Props[U], name: String): ActorRef[U] = { + if (childrenMap contains name) throw new InvalidActorNameException(s"actor name [$name] is not unique") + if (terminatingMap contains name) throw new InvalidActorNameException(s"actor name [$name] is not yet free") + val cell = new ActorCell[U](system, props, self) + val ref = new LocalActorRef[U](self.path / name, cell) + cell.setSelf(ref) + childrenMap = childrenMap.updated(name, ref) + ref.sendSystem(Create()) + ref + } + + private var nextName = 0L + override def spawnAnonymous[U](props: Props[U]): ActorRef[U] = { + val name = Helpers.base64(nextName) + nextName += 1 + spawn(props, name) + } + + override def stop(child: ActorRef[Nothing]): Boolean = { + val name = child.path.name + childrenMap.get(name) match { + case None ⇒ false + case Some(ref) if ref != child ⇒ false + case Some(ref) ⇒ + ref.sendSystem(Terminate()) + childrenMap -= name + terminatingMap = terminatingMap.updated(name, ref) + true + } + } + + protected def stopAll(): Unit = { + childrenMap.valuesIterator.foreach { ref ⇒ + ref.sendSystem(Terminate()) + terminatingMap = terminatingMap.updated(ref.path.name, ref) + } + childrenMap = Map.empty + } + + override def schedule[U](delay: FiniteDuration, target: ActorRef[U], msg: U): Cancellable = + system.scheduler.scheduleOnce(delay)(target ! msg)(ExecutionContexts.sameThreadExecutionContext) + + override val executionContext: ExecutionContextExecutor = system.dispatchers.lookup(props.dispatcher) + + override def spawnAdapter[U](f: U ⇒ T): ActorRef[U] = { + val name = Helpers.base64(nextName, new java.lang.StringBuilder("$!")) + nextName += 1 + val ref = new FunctionRef[U]( + self.path / name, + (msg, _) ⇒ send(f(msg)), + (self) ⇒ sendSystem(DeathWatchNotification(self, null))) + childrenMap = childrenMap.updated(name, ref) + ref + } + + private[this] var receiveTimeout: (FiniteDuration, T) = null + override def setReceiveTimeout(d: FiniteDuration, msg: T): Unit = { + if (Debug) println(s"$self setting receive timeout of $d, msg $msg") + receiveTimeout = (d, msg) + } + override def cancelReceiveTimeout(): Unit = { + if (Debug) println(s"$self canceling receive timeout") + receiveTimeout = null + } + + /* + * Implementation of the invocation mechanics. + */ + + // see comment in companion object for details + @volatile private[this] var _status: Int = 0 + protected[typed] def getStatus: Int = _status + private[this] val queue: Queue[T] = new ConcurrentLinkedQueue[T] + private[typed] def peekMessage: T = queue.peek() + private[this] val maxQueue: Int = Math.min(props.mailboxCapacity, maxActivations) + @volatile private[this] var _systemQueue: LatestFirstSystemMessageList = SystemMessageList.LNil + + protected def maySend: Boolean = !isTerminating + protected def isTerminating: Boolean = ActorCell.isTerminating(_status) + protected def setTerminating(): Unit = if (!ActorCell.isTerminating(_status)) unsafe.getAndAddInt(this, statusOffset, terminatingBit) + protected def setClosed(): Unit = if (!isTerminated(_status)) unsafe.getAndAddInt(this, statusOffset, terminatedBit) + + private def handleException: Catcher[Unit] = { + case e: InterruptedException ⇒ + publish(Error(e, self.path.toString, getClass, "interrupted during message send")) + Thread.currentThread.interrupt() + case NonFatal(e) ⇒ + publish(Error(e, self.path.toString, getClass, "swallowing exception during message send")) + } + + def send(msg: T): Unit = + try { + val old = unsafe.getAndAddInt(this, statusOffset, 1) + val oldActivations = activations(old) + // this is not an off-by-one: #msgs is activations-1 if >0 + if (oldActivations > maxQueue) { + if (Debug) println(s"[$thread] $self NOT enqueueing $msg at status $old ($oldActivations > $maxQueue)") + // cannot enqueue, need to give back activation token + unsafe.getAndAddInt(this, statusOffset, -1) + system.eventStream.publish(Dropped(msg, self)) + } else if (ActorCell.isTerminating(old)) { + if (Debug) println(s"[$thread] $self NOT enqueueing $msg at status $old (is terminating)") + unsafe.getAndAddInt(this, statusOffset, -1) + system.deadLetters ! msg + } else { + if (Debug) println(s"[$thread] $self enqueueing $msg at status $old") + // need to enqueue; if the actor sees the token but not the message, it will reschedule + queue.add(msg) + if (oldActivations == 0) { + if (Debug) println(s"[$thread] $self being woken up") + unsafe.getAndAddInt(this, statusOffset, 1) // the first 1 was just the “active” bit, now add 1msg + // if the actor was not yet running, set it in motion; spurious wakeups don’t hurt + executionContext.execute(this) + } + } + } catch handleException + + def sendSystem(signal: SystemMessage): Unit = { + @tailrec def needToActivate(): Boolean = { + val currentList = _systemQueue + if (currentList.head == NoMessage) { + system.deadLetters.sorry.sendSystem(signal) + false + } else { + unsafe.compareAndSwapObject(this, systemQueueOffset, currentList.head, (signal :: currentList).head) || { + signal.unlink() + needToActivate() + } + } + } + try { + if (needToActivate()) { + val old = unsafe.getAndAddInt(this, statusOffset, 1) + if (isTerminated(old)) { + // nothing to do + if (Debug) println(s"[$thread] $self NOT enqueueing $signal: terminating") + unsafe.getAndAddInt(this, statusOffset, -1) + } else if (activations(old) == 0) { + // all is good: we signaled the transition to active + if (Debug) println(s"[$thread] $self enqueueing $signal: activating") + executionContext.execute(this) + } else { + // take back that token: we didn’t actually enqueue a normal message and the actor was already active + if (Debug) println(s"[$thread] $self enqueueing $signal: already active") + unsafe.getAndAddInt(this, statusOffset, -1) + } + } else if (Debug) println(s"[$thread] $self NOT enqueueing $signal: terminated") + } catch handleException + } + + /** + * Main entry point into the actor: the ActorCell is a Runnable that is + * enqueued in its Executor whenever it needs to run. The _status field is + * used for coordination such that it is never enqueued more than once at + * any given time, because that would break the Actor Model. + * + * The idea here is to process at most as many messages as were in queued + * upon entry of this method, interleaving each normal message with the + * processing of all system messages that may have accumulated in the + * meantime. If at the end of the processing messages remain in the queue + * then this cell is rescheduled. + * + * All coordination occurs via a single Int field that is only updated in + * wait-free manner (LOCK XADD via unsafe.getAndAddInt), where conflicts are + * resolved by compensating actions. For a description of the bit usage see + * the companion object’s source code. + */ + override final def run(): Unit = { + if (Debug) println(s"[$thread] $self entering run(): interrupted=${Thread.currentThread.isInterrupted}") + val status = _status + val msgs = messageCount(status) + var processed = 0 + try { + unscheduleReceiveTimeout() + if (!isTerminated(status)) { + while (processAllSystemMessages() && !queue.isEmpty() && processed < msgs) { + val msg = queue.poll() + processed += 1 + processMessage(msg) + } + } + scheduleReceiveTimeout() + } catch { + case NonFatal(ex) ⇒ fail(ex) + case ie: InterruptedException ⇒ + fail(ie) + if (Debug) println(s"[$thread] $self interrupting due to catching InterruptedException") + Thread.currentThread.interrupt() + } finally { + // also remove the general activation token + processed += 1 + val prev = unsafe.getAndAddInt(this, statusOffset, -processed) + val now = prev - processed + if (isTerminated(now)) { + // we’re finished + } else if (activations(now) > 0) { + // normal messages pending: reverse the deactivation + unsafe.getAndAddInt(this, statusOffset, 1) + // ... and reschedule + executionContext.execute(this) + } else if (_systemQueue.head != null) { + /* + * System message was enqueued after our last processing, we now need to + * race against the other party because the enqueue might have happened + * before the deactivation (above) and hence not scheduled. + * + * If we win, we reschedule; if we lose, we must remove the attempted + * activation token again. + */ + val again = unsafe.getAndAddInt(this, statusOffset, 1) + if (activations(again) == 0) executionContext.execute(this) + else unsafe.getAndAddInt(this, statusOffset, -1) + } + } + if (Debug) println(s"[$thread] $self exiting run(): interrupted=${Thread.currentThread.isInterrupted}") + } + + protected[typed] var behavior: Behavior[T] = _ + + protected def next(b: Behavior[T], msg: Any): Unit = { + if (Behavior.isUnhandled(b)) unhandled(msg) + behavior = Behavior.canonicalize(b, behavior) + if (!Behavior.isAlive(behavior)) self.sendSystem(Terminate()) + } + + private def unhandled(msg: Any): Unit = msg match { + case Terminated(ref) ⇒ fail(DeathPactException(ref)) + case _ ⇒ // nothing to do + } + + private[this] var receiveTimeoutScheduled: Cancellable = null + private def unscheduleReceiveTimeout(): Unit = + if (receiveTimeoutScheduled ne null) { + receiveTimeoutScheduled.cancel() + receiveTimeoutScheduled = null + } + private def scheduleReceiveTimeout(): Unit = + receiveTimeout match { + case (d, msg) ⇒ + receiveTimeoutScheduled = schedule(d, self, msg) + case other ⇒ + // nothing to do + } + + /** + * Process the messages in the mailbox + */ + private def processMessage(msg: T): Unit = { + if (Debug) println(s"[$thread] $self processing message $msg") + next(behavior.message(this, msg), msg) + if (Thread.interrupted()) + throw new InterruptedException("Interrupted while processing actor messages") + } + + @tailrec + private def systemDrain(next: LatestFirstSystemMessageList): EarliestFirstSystemMessageList = { + val currentList = _systemQueue + if (currentList.head == NoMessage) SystemMessageList.ENil + else if (unsafe.compareAndSwapObject(this, systemQueueOffset, currentList.head, next.head)) currentList.reverse + else systemDrain(next) + } + + /** + * Will at least try to process all queued system messages: in case of + * failure simply drop and go on to the next, because there is nothing to + * restart here (failure is in ActorCell somewhere …). In case the mailbox + * becomes closed (because of processing a Terminate message), dump all + * already dequeued message to deadLetters. + */ + private def processAllSystemMessages(): Boolean = { + var interruption: Throwable = null + var messageList = systemDrain(SystemMessageList.LNil) + var continue = true + while (messageList.nonEmpty && continue) { + val msg = messageList.head + messageList = messageList.tail + msg.unlink() + continue = + try processSignal(msg) + catch { + case ie: InterruptedException ⇒ + fail(ie) + if (Debug) println(s"[$thread] $self interrupting due to catching InterruptedException during system message processing") + Thread.currentThread.interrupt() + true + case ex @ (NonFatal(_) | _: AssertionError) ⇒ + fail(ex) + true + } + /* + * the second part of the condition is necessary to avoid logging an InterruptedException + * from the systemGuardian during shutdown + */ + if (Thread.interrupted() && system.whenTerminated.value.isEmpty) + interruption = new InterruptedException("Interrupted while processing system messages") + // don’t ever execute normal message when system message present! + if (messageList.isEmpty && continue) messageList = systemDrain(SystemMessageList.LNil) + } + /* + * if we closed the mailbox, we must dump the remaining system messages + * to deadLetters (this is essential for DeathWatch) + */ + val dlm = system.deadLetters + if (isTerminated(_status) && messageList.isEmpty) messageList = systemDrain(new LatestFirstSystemMessageList(NoMessage)) + while (messageList.nonEmpty) { + val msg = messageList.head + messageList = messageList.tail + if (Debug) println(s"[$thread] $self dropping dead system message $msg") + msg.unlink() + try dlm.sorry.sendSystem(msg) + catch { + case e: InterruptedException ⇒ interruption = e + case NonFatal(e) ⇒ system.eventStream.publish( + Error(e, self.path.toString, this.getClass, "error while enqueuing " + msg + " to deadLetters: " + e.getMessage)) + } + if (isTerminated(_status) && messageList.isEmpty) messageList = systemDrain(new LatestFirstSystemMessageList(NoMessage)) + } + // if we got an interrupted exception while handling system messages, then rethrow it + if (interruption ne null) { + if (Debug) println(s"[$thread] $self throwing interruption") + Thread.interrupted() // clear interrupted flag before throwing according to java convention + throw interruption + } + continue + } + + // logging is not the main purpose, and if it fails there’s nothing we can do + protected final def publish(e: Logging.LogEvent): Unit = try system.eventStream.publish(e) catch { case NonFatal(_) ⇒ } + + protected final def clazz(o: AnyRef): Class[_] = if (o eq null) this.getClass else o.getClass + + private def thread: String = Thread.currentThread.getName + + override def toString: String = f"ActorCell($self, status = ${_status}%08x, queue = $queue)" +} diff --git a/akka-typed/src/main/scala/akka/typed/internal/ActorRefImpl.scala b/akka-typed/src/main/scala/akka/typed/internal/ActorRefImpl.scala new file mode 100644 index 0000000000..a3d49c88b5 --- /dev/null +++ b/akka-typed/src/main/scala/akka/typed/internal/ActorRefImpl.scala @@ -0,0 +1,194 @@ +/** + * Copyright (C) 2016 Lightbend Inc. + */ +package akka.typed +package internal + +import akka.{ actor ⇒ a } +import akka.dispatch.sysmsg._ +import akka.util.Unsafe.{ instance ⇒ unsafe } +import scala.annotation.tailrec +import scala.util.control.NonFatal +import scala.concurrent.Future +import java.util.ArrayList +import scala.util.{ Success, Failure } + +/** + * Every ActorRef is also an ActorRefImpl, but these two methods shall be + * completely hidden from client code. There is an implicit converter + * available in the package object, enabling `ref.toImpl` (or `ref.toImplN` + * for `ActorRef[Nothing]`—Scala refuses to infer `Nothing` as a type parameter). + */ +private[typed] trait ActorRefImpl[-T] extends ActorRef[T] { + def sendSystem(signal: SystemMessage): Unit + def isLocal: Boolean +} + +/** + * A local ActorRef that is backed by an asynchronous [[ActorCell]]. + */ +private[typed] class LocalActorRef[-T](_path: a.ActorPath, cell: ActorCell[T]) + extends ActorRef[T](_path) with ActorRefImpl[T] { + override def tell(msg: T): Unit = cell.send(msg) + override def sendSystem(signal: SystemMessage): Unit = cell.sendSystem(signal) + final override def isLocal: Boolean = true + private[typed] def getCell: ActorCell[_] = cell +} + +/** + * A local ActorRef that just discards everything that is sent to it. This + * implies that it effectively has an infinite lifecycle, i.e. it never + * terminates (meaning: no Hawking radiation). + */ +private[typed] object BlackholeActorRef + extends ActorRef[Any](a.RootActorPath(a.Address("akka.typed.internal", "blackhole"))) with ActorRefImpl[Any] { + override def tell(msg: Any): Unit = () + override def sendSystem(signal: SystemMessage): Unit = () + final override def isLocal: Boolean = true +} + +/** + * A local synchronous ActorRef that invokes the given function for every message send. + * This reference can be watched and will do the right thing when it receives a [[DeathWatchNotification]]. + * This reference cannot watch other references. + */ +private[typed] final class FunctionRef[-T]( + _path: a.ActorPath, + send: (T, FunctionRef[T]) ⇒ Unit, + _terminate: FunctionRef[T] ⇒ Unit) + extends WatchableRef[T](_path) { + + override def tell(msg: T): Unit = + if (isAlive) + try send(msg, this) catch { + case NonFatal(ex) ⇒ // nothing we can do here + } + else () // we don’t have deadLetters available + + override def sendSystem(signal: SystemMessage): Unit = signal match { + case Create() ⇒ // nothing to do + case DeathWatchNotification(ref, cause) ⇒ // we’re not watching, and we’re not a parent either + case Terminate() ⇒ doTerminate() + case Watch(watchee, watcher) ⇒ if (watchee == this && watcher != this) addWatcher(watcher.sorryForNothing) + case Unwatch(watchee, watcher) ⇒ if (watchee == this && watcher != this) remWatcher(watcher.sorryForNothing) + case NoMessage ⇒ // nothing to do + } + + override def isLocal = true + + override def terminate(): Unit = _terminate(this) +} + +/** + * The mechanics for synthetic ActorRefs that have a lifecycle and support being watched. + */ +private[typed] abstract class WatchableRef[-T](_p: a.ActorPath) extends ActorRef[T](_p) with ActorRefImpl[T] { + import WatchableRef._ + + /** + * Callback that is invoked when this ref has terminated. Even if doTerminate() is + * called multiple times, this callback is invoked only once. + */ + protected def terminate(): Unit + + type S = Set[ActorRefImpl[Nothing]] + @volatile private[this] var _watchedBy: S = Set.empty + + protected def isAlive: Boolean = _watchedBy != null + + protected def doTerminate(): Unit = { + val watchedBy = unsafe.getAndSetObject(this, watchedByOffset, null).asInstanceOf[S] + if (watchedBy != null) { + try terminate() catch { case NonFatal(ex) ⇒ } + if (watchedBy.nonEmpty) watchedBy foreach sendTerminated + } + } + + private def sendTerminated(watcher: ActorRefImpl[Nothing]): Unit = + watcher.sendSystem(DeathWatchNotification(this, null)) + + @tailrec final protected def addWatcher(watcher: ActorRefImpl[Nothing]): Unit = + _watchedBy match { + case null ⇒ sendTerminated(watcher) + case watchedBy ⇒ + if (!watchedBy.contains(watcher)) + if (!unsafe.compareAndSwapObject(this, watchedByOffset, watchedBy, watchedBy + watcher)) + addWatcher(watcher) // try again + } + + @tailrec final protected def remWatcher(watcher: ActorRefImpl[Nothing]): Unit = { + _watchedBy match { + case null ⇒ // do nothing... + case watchedBy ⇒ + if (watchedBy.contains(watcher)) + if (!unsafe.compareAndSwapObject(this, watchedByOffset, watchedBy, watchedBy - watcher)) + remWatcher(watcher) // try again + } + } +} + +private[typed] object WatchableRef { + val watchedByOffset = unsafe.objectFieldOffset(classOf[WatchableRef[_]].getDeclaredField("_watchedBy")) +} + +/** + * A Future of an ActorRef can quite easily be wrapped as an ActorRef since no + * promises are made about delivery delays: as long as the Future is not ready + * messages will be queued, afterwards they get sent without waiting. + */ +private[typed] class FutureRef[-T](_p: a.ActorPath, bufferSize: Int, f: Future[ActorRef[T]]) extends WatchableRef[T](_p) { + import FutureRef._ + + @volatile private[this] var _target: Either[ArrayList[T], ActorRef[T]] = Left(new ArrayList[T]) + + f.onComplete { + case Success(ref) ⇒ + _target match { + case l @ Left(list) ⇒ + list.synchronized { + val it = list.iterator + while (it.hasNext) ref ! it.next() + if (unsafe.compareAndSwapObject(this, targetOffset, l, Right(ref))) + ref.sorry.sendSystem(Watch(ref, this)) + // if this fails, concurrent termination has won and there is no point in watching + } + case _ ⇒ // already terminated + } + case Failure(ex) ⇒ doTerminate() + }(akka.dispatch.ExecutionContexts.sameThreadExecutionContext) + + override def terminate(): Unit = { + val old = unsafe.getAndSetObject(this, targetOffset, Right(BlackholeActorRef)) + old match { + case Right(target: ActorRef[_]) ⇒ target.sorry.sendSystem(Unwatch(target, this)) + case _ ⇒ // nothing to do + } + } + + override def tell(msg: T): Unit = + _target match { + case Left(list) ⇒ + list.synchronized { + if (_target.isRight) tell(msg) + else if (list.size < bufferSize) list.add(msg) + } + case Right(ref) ⇒ ref ! msg + } + + override def sendSystem(signal: SystemMessage): Unit = signal match { + case Create() ⇒ // nothing to do + case DeathWatchNotification(ref, cause) ⇒ + _target = Right(BlackholeActorRef) // avoid sending Unwatch() in this case + doTerminate() // this can only be the result of watching the target + case Terminate() ⇒ doTerminate() + case Watch(watchee, watcher) ⇒ if (watchee == this && watcher != this) addWatcher(watcher.sorryForNothing) + case Unwatch(watchee, watcher) ⇒ if (watchee == this && watcher != this) remWatcher(watcher.sorryForNothing) + case NoMessage ⇒ // nothing to do + } + + override def isLocal = true +} + +private[typed] object FutureRef { + val targetOffset = unsafe.objectFieldOffset(classOf[FutureRef[_]].getDeclaredField("akka$typed$internal$FutureRef$$_target")) +} diff --git a/akka-typed/src/main/scala/akka/typed/internal/ActorSystemImpl.scala b/akka-typed/src/main/scala/akka/typed/internal/ActorSystemImpl.scala new file mode 100644 index 0000000000..e84fc734d6 --- /dev/null +++ b/akka-typed/src/main/scala/akka/typed/internal/ActorSystemImpl.scala @@ -0,0 +1,251 @@ +/** + * Copyright (C) 2016 Lightbend Inc. + */ +package akka.typed +package internal + +import com.typesafe.config.Config +import scala.concurrent.ExecutionContext +import java.util.concurrent.ThreadFactory +import scala.concurrent.{ ExecutionContextExecutor, Future } +import akka.{ actor ⇒ a, dispatch ⇒ d, event ⇒ e } +import scala.util.control.NonFatal +import scala.util.control.ControlThrowable +import scala.collection.immutable +import akka.typed.Dispatchers +import scala.concurrent.Promise +import java.util.concurrent.ConcurrentSkipListSet +import java.util.concurrent.atomic.AtomicBoolean +import scala.collection.JavaConverters._ +import scala.util.Success +import akka.util.Timeout +import java.io.Closeable + +object ActorSystemImpl { + import ScalaDSL._ + + sealed trait SystemCommand + case class CreateSystemActor[T](props: Props[T])(val replyTo: ActorRef[ActorRef[T]]) extends SystemCommand + + val systemGuardianBehavior: Behavior[SystemCommand] = + ContextAware { ctx ⇒ + Static { + case create: CreateSystemActor[t] ⇒ + create.replyTo ! ctx.spawnAnonymous(create.props) + } + } +} + +/* + * Actor Ideas: + + • remoting/clustering is just another set of actors/extensions + +Receptionist: + + • should be a new kind of Extension (where lookup yields ActorRef) + • obtaining a reference may either give a single remote one or a dynamic local proxy that routes to available instances—distinguished using a “stableDestination” flag (for read-your-writes semantics) + • perhaps fold sharding into this: how message routing is done should not matter + +Streams: + + • make new implementation of ActorMaterializer that leverages Envelope removal + • all internal actor creation must be asynchronous + • could offer ActorSystem extension for materializer + • remove downcasts to ActorMaterializer in akka-stream package—replace by proper function passing or Materializer APIs where needed (should make Gearpump happier as well) + • add new Sink/Source for ActorRef[] + +Distributed Data: + + • create new Behaviors around the logic + + * + */ + +private[typed] class ActorSystemImpl[-T]( + override val name: String, + _config: Config, + _cl: ClassLoader, + _ec: Option[ExecutionContext], + _userGuardianProps: Props[T]) + extends ActorRef[T](a.RootActorPath(a.Address("akka", name)) / "user") with ActorSystem[T] with ActorRefImpl[T] { + + import ActorSystemImpl._ + + if (!name.matches("""^[a-zA-Z0-9][a-zA-Z0-9-_]*$""")) + throw new IllegalArgumentException( + "invalid ActorSystem name [" + name + + "], must contain only word characters (i.e. [a-zA-Z0-9] plus non-leading '-' or '_')") + + import a.ActorSystem.Settings + override val settings: Settings = new Settings(_cl, _config, name) + + override def logConfiguration(): Unit = log.info(settings.toString) + + protected def uncaughtExceptionHandler: Thread.UncaughtExceptionHandler = + new Thread.UncaughtExceptionHandler() { + def uncaughtException(thread: Thread, cause: Throwable): Unit = { + cause match { + case NonFatal(_) | _: InterruptedException | _: NotImplementedError | _: ControlThrowable ⇒ log.error(cause, "Uncaught error from thread [{}]", thread.getName) + case _ ⇒ + if (settings.JvmExitOnFatalError) { + try { + log.error(cause, "Uncaught error from thread [{}] shutting down JVM since 'akka.jvm-exit-on-fatal-error' is enabled", thread.getName) + import System.err + err.print("Uncaught error from thread [") + err.print(thread.getName) + err.print("] shutting down JVM since 'akka.jvm-exit-on-fatal-error' is enabled for ActorSystem[") + err.print(name) + err.println("]") + cause.printStackTrace(System.err) + System.err.flush() + } finally { + System.exit(-1) + } + } else { + log.error(cause, "Uncaught fatal error from thread [{}] shutting down ActorSystem [{}]", thread.getName, name) + terminate() + } + } + } + } + + override val threadFactory: d.MonitorableThreadFactory = + d.MonitorableThreadFactory(name, settings.Daemonicity, Option(_cl), uncaughtExceptionHandler) + + override val dynamicAccess: a.DynamicAccess = new a.ReflectiveDynamicAccess(_cl) + + // this provides basic logging (to stdout) until .start() is called below + // FIXME!!! + private val untypedSystem = a.ActorSystem(name + "-untyped", _config) + override def eventStream = untypedSystem.eventStream + + override val logFilter: e.LoggingFilter = { + val arguments = Vector(classOf[Settings] → settings, classOf[e.EventStream] → eventStream) + dynamicAccess.createInstanceFor[e.LoggingFilter](settings.LoggingFilter, arguments).get + } + + override val log: e.LoggingAdapter = new e.BusLogging(eventStream, getClass.getName + "(" + name + ")", this.getClass, logFilter) + + /** + * Create the scheduler service. This one needs one special behavior: if + * Closeable, it MUST execute all outstanding tasks upon .close() in order + * to properly shutdown all dispatchers. + * + * Furthermore, this timer service MUST throw IllegalStateException if it + * cannot schedule a task. Once scheduled, the task MUST be executed. If + * executed upon close(), the task may execute before its timeout. + */ + protected def createScheduler(): a.Scheduler = + dynamicAccess.createInstanceFor[a.Scheduler](settings.SchedulerClass, immutable.Seq( + classOf[Config] → settings.config, + classOf[e.LoggingAdapter] → log, + classOf[ThreadFactory] → threadFactory.withName(threadFactory.name + "-scheduler"))).get + + override val scheduler: a.Scheduler = createScheduler() + private def closeScheduler(): Unit = scheduler match { + case x: Closeable ⇒ x.close() + case _ ⇒ + } + + override val dispatchers: Dispatchers = new DispatchersImpl(settings, log) + override val executionContext: ExecutionContextExecutor = dispatchers.lookup(DispatcherDefault) + + override val startTime: Long = System.currentTimeMillis() + override def uptime: Long = (System.currentTimeMillis() - startTime) / 1000 + + private val terminationPromise: Promise[Terminated] = Promise() + + private val rootPath: a.ActorPath = a.RootActorPath(a.Address("typed", name)) + + private val topLevelActors = new ConcurrentSkipListSet[ActorRefImpl[Nothing]] + private val terminateTriggered = new AtomicBoolean + private val theOneWhoWalksTheBubblesOfSpaceTime: ActorRefImpl[Nothing] = + new ActorRef[Nothing](rootPath) with ActorRefImpl[Nothing] { + override def tell(msg: Nothing): Unit = throw new UnsupportedOperationException("cannot send to theOneWhoWalksTheBubblesOfSpaceTime") + override def sendSystem(signal: SystemMessage): Unit = signal match { + case Terminate() ⇒ + if (terminateTriggered.compareAndSet(false, true)) + topLevelActors.asScala.foreach(ref ⇒ ref.sendSystem(Terminate())) + case DeathWatchNotification(ref, _) ⇒ + topLevelActors.remove(ref) + if (topLevelActors.isEmpty) { + if (terminationPromise.tryComplete(Success(Terminated(this)(null)))) { + closeScheduler() + dispatchers.shutdown() + untypedSystem.terminate() + } + } else if (terminateTriggered.compareAndSet(false, true)) + topLevelActors.asScala.foreach(ref ⇒ ref.sendSystem(Terminate())) + case _ ⇒ // ignore + } + override def isLocal: Boolean = true + } + + private def createTopLevel[U](props: Props[U], name: String): ActorRefImpl[U] = { + val cell = new ActorCell(this, props, theOneWhoWalksTheBubblesOfSpaceTime) + val ref = new LocalActorRef(rootPath / name, cell) + cell.setSelf(ref) + topLevelActors.add(ref) + ref.sendSystem(Create()) + ref + } + + private val systemGuardian: ActorRefImpl[SystemCommand] = createTopLevel(Props(systemGuardianBehavior), "system") + private val userGuardian: ActorRefImpl[T] = createTopLevel(_userGuardianProps, "user") + + override def terminate(): Future[Terminated] = { + theOneWhoWalksTheBubblesOfSpaceTime.sendSystem(Terminate()) + terminationPromise.future + } + override def whenTerminated: Future[Terminated] = terminationPromise.future + + override def deadLetters[U]: ActorRefImpl[U] = + new ActorRef[U](rootPath) with ActorRefImpl[U] { + override def tell(msg: U): Unit = eventStream.publish(DeadLetter(msg)) + override def sendSystem(signal: SystemMessage): Unit = { + signal match { + case Watch(watchee, watcher) ⇒ watcher.sorryForNothing.sendSystem(DeathWatchNotification(watchee, null)) + case _ ⇒ // all good + } + eventStream.publish(DeadLetter(signal)) + } + override def isLocal: Boolean = true + } + + override def tell(msg: T): Unit = userGuardian.tell(msg) + override def sendSystem(msg: SystemMessage): Unit = userGuardian.sendSystem(msg) + override def isLocal: Boolean = true + + def systemActorOf[U](props: Props[U], name: String)(implicit timeout: Timeout): Future[ActorRef[U]] = { + import AskPattern._ + implicit val sched = scheduler + systemGuardian ? CreateSystemActor(props) + } + + def printTree: String = { + def printNode(node: ActorRefImpl[Nothing], indent: String): String = { + node match { + case wc: LocalActorRef[_] ⇒ + val cell = wc.getCell + (if (indent.isEmpty) "-> " else indent.dropRight(1) + "⌊-> ") + + node.path.name + " " + e.Logging.simpleName(node) + " " + + (if (cell.behavior ne null) cell.behavior.getClass else "null") + + " status=" + cell.getStatus + + " nextMsg=" + cell.peekMessage + + (if (cell.children.isEmpty && cell.terminating.isEmpty) "" else "\n") + + ({ + val terminating = cell.terminating.toSeq.sorted.map(r ⇒ printNode(r.sorryForNothing, indent + " T")) + val children = cell.children.toSeq.sorted + val bulk = children.dropRight(1) map (r ⇒ printNode(r.sorryForNothing, indent + " |")) + terminating ++ bulk ++ (children.lastOption map (r ⇒ printNode(r.sorryForNothing, indent + " "))) + } mkString ("\n")) + case _ ⇒ + indent + node.path.name + " " + e.Logging.simpleName(node) + } + } + printNode(systemGuardian, "") + "\n" + + printNode(userGuardian, "") + } + +} diff --git a/akka-typed/src/main/scala/akka/typed/internal/DeathWatch.scala b/akka-typed/src/main/scala/akka/typed/internal/DeathWatch.scala new file mode 100644 index 0000000000..8164bedcfe --- /dev/null +++ b/akka-typed/src/main/scala/akka/typed/internal/DeathWatch.scala @@ -0,0 +1,202 @@ +/** + * Copyright (C) 2009-2016 Lightbend Inc. + */ + +package akka.typed +package internal + +import akka.event.Logging.{ Warning, Debug } +import akka.event.AddressTerminatedTopic +import akka.event.Logging +import akka.actor.Address + +/* + * THOUGHTS + * + * - an ActorRef is a channel that allows sending messages — in particular it is NOT a sender! + * - a channel is scoped by the session it is part of + * - termination means that the session ends because sending further messages is pointless + * - this means that there is no ordering requirement between Terminated and any other received message + */ +private[typed] trait DeathWatch[T] { + + /* + * INTERFACE WITH ACTORCELL + */ + protected def system: ActorSystem[Nothing] + protected def self: ActorRefImpl[T] + protected def parent: ActorRefImpl[Nothing] + protected def behavior: Behavior[T] + protected def next(b: Behavior[T], msg: Any): Unit + protected def childrenMap: Map[String, ActorRefImpl[Nothing]] + protected def terminatingMap: Map[String, ActorRefImpl[Nothing]] + protected def isTerminating: Boolean + protected def ctx: ActorContext[T] + protected def maySend: Boolean + protected def publish(e: Logging.LogEvent): Unit + protected def clazz(obj: AnyRef): Class[_] + + protected def removeChild(actor: ActorRefImpl[Nothing]): Unit + protected def finishTerminate(): Unit + + type ARImpl = ActorRefImpl[Nothing] + + private var watching = Set.empty[ARImpl] + private var watchedBy = Set.empty[ARImpl] + + final def watch[U](_a: ActorRef[U]): ActorRef[U] = { + val a = _a.sorry + if (a != self && !watching.contains(a)) { + maintainAddressTerminatedSubscription(a) { + a.sendSystem(Watch(a, self)) + watching += a + } + } + a + } + + final def unwatch[U](_a: ActorRef[U]): ActorRef[U] = { + val a = _a.sorry + if (a != self && watching.contains(a)) { + a.sendSystem(Unwatch(a, self)) + maintainAddressTerminatedSubscription(a) { + watching -= a + } + } + a + } + + /** + * When this actor is watching the subject of [[akka.actor.Terminated]] message + * it will be propagated to user's receive. + */ + protected def watchedActorTerminated(actor: ARImpl, failure: Throwable): Boolean = { + removeChild(actor) + if (watching.contains(actor)) { + maintainAddressTerminatedSubscription(actor) { + watching -= actor + } + if (maySend) { + val t = Terminated(actor)(failure) + next(behavior.management(ctx, t), t) + } + } + if (isTerminating && terminatingMap.isEmpty) { + finishTerminate() + false + } else true + } + + protected def tellWatchersWeDied(): Unit = + if (watchedBy.nonEmpty) { + try { + // Don't need to send to parent parent since it receives a DWN by default + def sendTerminated(ifLocal: Boolean)(watcher: ARImpl): Unit = + if (watcher.isLocal == ifLocal && watcher != parent) watcher.sendSystem(DeathWatchNotification(self, null)) + + /* + * It is important to notify the remote watchers first, otherwise RemoteDaemon might shut down, causing + * the remoting to shut down as well. At this point Terminated messages to remote watchers are no longer + * deliverable. + * + * The problematic case is: + * 1. Terminated is sent to RemoteDaemon + * 1a. RemoteDaemon is fast enough to notify the terminator actor in RemoteActorRefProvider + * 1b. The terminator is fast enough to enqueue the shutdown command in the remoting + * 2. Only at this point is the Terminated (to be sent remotely) enqueued in the mailbox of remoting + * + * If the remote watchers are notified first, then the mailbox of the Remoting will guarantee the correct order. + */ + watchedBy foreach sendTerminated(ifLocal = false) + watchedBy foreach sendTerminated(ifLocal = true) + } finally { + maintainAddressTerminatedSubscription() { + watchedBy = Set.empty + } + } + } + + protected def unwatchWatchedActors(): Unit = + if (watching.nonEmpty) { + maintainAddressTerminatedSubscription() { + try { + watching.foreach(watchee ⇒ watchee.sendSystem(Unwatch(watchee, self))) + } finally { + watching = Set.empty + } + } + } + + protected def addWatcher(watchee: ARImpl, watcher: ARImpl): Unit = { + val watcheeSelf = watchee == self + val watcherSelf = watcher == self + + if (watcheeSelf && !watcherSelf) { + if (!watchedBy.contains(watcher)) maintainAddressTerminatedSubscription(watcher) { + watchedBy += watcher + if (system.settings.DebugLifecycle) publish(Debug(self.path.toString, clazz(behavior), s"now watched by $watcher")) + } + } else if (!watcheeSelf && watcherSelf) { + watch[Nothing](watchee) + } else { + publish(Warning(self.path.toString, clazz(behavior), "BUG: illegal Watch(%s,%s) for %s".format(watchee, watcher, self))) + } + } + + protected def remWatcher(watchee: ARImpl, watcher: ARImpl): Unit = { + val watcheeSelf = watchee == self + val watcherSelf = watcher == self + + if (watcheeSelf && !watcherSelf) { + if (watchedBy.contains(watcher)) maintainAddressTerminatedSubscription(watcher) { + watchedBy -= watcher + if (system.settings.DebugLifecycle) publish(Debug(self.path.toString, clazz(behavior), s"no longer watched by $watcher")) + } + } else if (!watcheeSelf && watcherSelf) { + unwatch[Nothing](watchee) + } else { + publish(Warning(self.path.toString, clazz(behavior), "BUG: illegal Unwatch(%s,%s) for %s".format(watchee, watcher, self))) + } + } + + protected def addressTerminated(address: Address): Unit = { + // cleanup watchedBy since we know they are dead + maintainAddressTerminatedSubscription() { + for (a ← watchedBy; if a.path.address == address) watchedBy -= a + } + + for (a ← watching; if a.path.address == address) { + self.sendSystem(DeathWatchNotification(a, null)) + } + } + + /** + * Starts subscription to AddressTerminated if not already subscribing and the + * block adds a non-local ref to watching or watchedBy. + * Ends subscription to AddressTerminated if subscribing and the + * block removes the last non-local ref from watching and watchedBy. + */ + private def maintainAddressTerminatedSubscription[U](change: ARImpl = null)(block: ⇒ U): U = { + def isNonLocal(ref: ARImpl) = ref match { + case null ⇒ true + case a ⇒ !a.isLocal + } + + if (isNonLocal(change)) { + def hasNonLocalAddress: Boolean = ((watching exists isNonLocal) || (watchedBy exists isNonLocal)) + val had = hasNonLocalAddress + val result = block + val has = hasNonLocalAddress + if (had && !has) unsubscribeAddressTerminated() + else if (!had && has) subscribeAddressTerminated() + result + } else { + block + } + } + + // FIXME: these will need to be redone once remoting is integrated + private def unsubscribeAddressTerminated(): Unit = ??? + private def subscribeAddressTerminated(): Unit = ??? + +} diff --git a/akka-typed/src/main/scala/akka/typed/internal/DispatchersImpl.scala b/akka-typed/src/main/scala/akka/typed/internal/DispatchersImpl.scala new file mode 100644 index 0000000000..7f544c7264 --- /dev/null +++ b/akka-typed/src/main/scala/akka/typed/internal/DispatchersImpl.scala @@ -0,0 +1,36 @@ +/** + * Copyright (C) 2016 Lightbend Inc. + */ +package akka.typed +package internal + +import scala.concurrent.ExecutionContextExecutor +import scala.concurrent.ExecutionContextExecutorService +import java.util.concurrent.Executors +import akka.event.LoggingAdapter + +class DispatchersImpl(settings: akka.actor.ActorSystem.Settings, log: LoggingAdapter) extends Dispatchers { + private val ex: ExecutionContextExecutorService = new ExecutionContextExecutorService { + val es = Executors.newWorkStealingPool() + + def reportFailure(cause: Throwable): Unit = log.error(cause, "exception caught by default executor") + def execute(command: Runnable): Unit = es.execute(command) + + def awaitTermination(x$1: Long, x$2: java.util.concurrent.TimeUnit): Boolean = es.awaitTermination(x$1, x$2) + def invokeAll[T](x$1: java.util.Collection[_ <: java.util.concurrent.Callable[T]], x$2: Long, x$3: java.util.concurrent.TimeUnit): java.util.List[java.util.concurrent.Future[T]] = es.invokeAll(x$1, x$2, x$3) + def invokeAll[T](x$1: java.util.Collection[_ <: java.util.concurrent.Callable[T]]): java.util.List[java.util.concurrent.Future[T]] = es.invokeAll(x$1) + def invokeAny[T](x$1: java.util.Collection[_ <: java.util.concurrent.Callable[T]], x$2: Long, x$3: java.util.concurrent.TimeUnit): T = es.invokeAny(x$1, x$2, x$3) + def invokeAny[T](x$1: java.util.Collection[_ <: java.util.concurrent.Callable[T]]): T = es.invokeAny(x$1) + def isShutdown(): Boolean = es.isShutdown() + def isTerminated(): Boolean = es.isTerminated() + def shutdown(): Unit = es.shutdown() + def shutdownNow(): java.util.List[Runnable] = es.shutdownNow() + def submit(x$1: Runnable): java.util.concurrent.Future[_] = es.submit(x$1) + def submit[T](x$1: Runnable, x$2: T): java.util.concurrent.Future[T] = es.submit(x$1, x$2) + def submit[T](x$1: java.util.concurrent.Callable[T]): java.util.concurrent.Future[T] = es.submit(x$1) + } + def lookup(selector: DispatcherSelector): ExecutionContextExecutor = ex //FIXME respect selection + def shutdown(): Unit = { + ex.shutdown() + } +} diff --git a/akka-typed/src/main/scala/akka/typed/internal/SupervisionMechanics.scala b/akka-typed/src/main/scala/akka/typed/internal/SupervisionMechanics.scala new file mode 100644 index 0000000000..8e920f4d00 --- /dev/null +++ b/akka-typed/src/main/scala/akka/typed/internal/SupervisionMechanics.scala @@ -0,0 +1,108 @@ +/** + * Copyright (C) 2016 Lightbend Inc. + */ +package akka.typed +package internal + +import scala.annotation.{ tailrec, switch } +import scala.util.control.NonFatal +import scala.util.control.Exception.Catcher +import akka.event.Logging + +/** + * INTERNAL API + */ +private[typed] trait SupervisionMechanics[T] { + import ActorCell._ + + /* + * INTERFACE WITH ACTOR CELL + */ + protected def system: ActorSystem[Nothing] + protected def props: Props[T] + protected def self: ActorRefImpl[T] + protected def parent: ActorRefImpl[Nothing] + protected def behavior: Behavior[T] + protected def behavior_=(b: Behavior[T]): Unit + protected def next(b: Behavior[T], msg: Any): Unit + protected def terminatingMap: Map[String, ActorRefImpl[Nothing]] + protected def stopAll(): Unit + protected def setTerminating(): Unit + protected def setClosed(): Unit + protected def maySend: Boolean + protected def ctx: ActorContext[T] + protected def publish(e: Logging.LogEvent): Unit + protected def clazz(obj: AnyRef): Class[_] + + // INTERFACE WITH DEATHWATCH + protected def addWatcher(watchee: ActorRefImpl[Nothing], watcher: ActorRefImpl[Nothing]): Unit + protected def remWatcher(watchee: ActorRefImpl[Nothing], watcher: ActorRefImpl[Nothing]): Unit + protected def watchedActorTerminated(actor: ActorRefImpl[Nothing], failure: Throwable): Boolean + protected def tellWatchersWeDied(): Unit + protected def unwatchWatchedActors(): Unit + + /** + * Process one system message and return whether further messages shall be processed. + */ + protected def processSignal(message: SystemMessage): Boolean = { + if (ActorCell.Debug) println(s"[${Thread.currentThread.getName}] $self processing system message $message") + message match { + case Watch(watchee, watcher) ⇒ { addWatcher(watchee.sorryForNothing, watcher.sorryForNothing); true } + case Unwatch(watchee, watcher) ⇒ { remWatcher(watchee.sorryForNothing, watcher.sorryForNothing); true } + case DeathWatchNotification(a, f) ⇒ watchedActorTerminated(a.sorryForNothing, f) + case Create() ⇒ create() + case Terminate() ⇒ terminate() + case NoMessage ⇒ false // only here to suppress warning + } + } + + private[this] var _failed: Throwable = null + protected def failed: Throwable = _failed + + protected def fail(thr: Throwable): Unit = { + if (_failed eq null) _failed = thr + publish(Logging.Error(thr, self.path.toString, getClass, thr.getMessage)) + if (maySend) self.sendSystem(Terminate()) + } + + private def create(): Boolean = { + behavior = Behavior.canonicalize(props.creator(), behavior) + if (behavior == null) { + fail(new IllegalStateException("cannot start actor with “same” or “unhandled” behavior, terminating")) + } else { + if (system.settings.DebugLifecycle) + publish(Logging.Debug(self.path.toString, clazz(behavior), "started")) + if (Behavior.isAlive(behavior)) next(behavior.management(ctx, PreStart), PreStart) + else self.sendSystem(Terminate()) + } + true + } + + private def terminate(): Boolean = { + setTerminating() + unwatchWatchedActors() + stopAll() + if (terminatingMap.isEmpty) { + finishTerminate() + false + } else true + } + + protected def finishTerminate(): Unit = { + val a = behavior + /* + * The following order is crucial for things to work properly. Only change this if you're very confident and lucky. + */ + try if (a ne null) a.management(ctx, PostStop) + catch { case NonFatal(ex) ⇒ publish(Logging.Error(ex, self.path.toString, clazz(a), "failure during PostStop")) } + finally try tellWatchersWeDied() + finally try parent.sendSystem(DeathWatchNotification(self, failed)) + finally { + behavior = null + _failed = null + setClosed() + if (system.settings.DebugLifecycle) + publish(Logging.Debug(self.path.toString, clazz(a), "stopped")) + } + } +} diff --git a/akka-typed/src/main/scala/akka/typed/internal/SystemMessage.scala b/akka-typed/src/main/scala/akka/typed/internal/SystemMessage.scala new file mode 100644 index 0000000000..60babe937f --- /dev/null +++ b/akka-typed/src/main/scala/akka/typed/internal/SystemMessage.scala @@ -0,0 +1,230 @@ +/** + * Copyright (C) 2009-2016 Lightbend Inc. + */ +package akka.typed +package internal + +import scala.annotation.tailrec + +/** + * INTERNAL API + * + * Helper companion object for [[LatestFirstSystemMessageList]] and + * [[EarliestFirstSystemMessageList]] + */ +private[typed] object SystemMessageList { + final val LNil: LatestFirstSystemMessageList = new LatestFirstSystemMessageList(null) + final val ENil: EarliestFirstSystemMessageList = new EarliestFirstSystemMessageList(null) + + @tailrec + private[internal] def sizeInner(head: SystemMessage, acc: Int): Int = if (head eq null) acc else sizeInner(head.next, acc + 1) + + @tailrec + private[internal] def reverseInner(head: SystemMessage, acc: SystemMessage): SystemMessage = { + if (head eq null) acc else { + val next = head.next + head.next = acc + reverseInner(next, head) + } + } +} + +/** + * + * INTERNAL API + * + * Value class supporting list operations on system messages. The `next` field of [[SystemMessage]] + * is hidden, and can only accessed through the value classes [[LatestFirstSystemMessageList]] and + * [[EarliestFirstSystemMessageList]], abstracting over the fact that system messages are the + * list nodes themselves. If used properly, this stays a compile time construct without any allocation overhead. + * + * This list is mutable. + * + * The type of the list also encodes that the messages contained are in reverse order, i.e. the head of the list is the + * latest appended element. + * + */ +private[typed] class LatestFirstSystemMessageList(val head: SystemMessage) extends AnyVal { + import SystemMessageList._ + + /** + * Indicates if the list is empty or not. This operation has constant cost. + */ + final def isEmpty: Boolean = head eq null + + /** + * Indicates if the list has at least one element or not. This operation has constant cost. + */ + final def nonEmpty: Boolean = head ne null + + /** + * Indicates if the list is empty or not. This operation has constant cost. + */ + final def size: Int = sizeInner(head, 0) + + /** + * Gives back the list containing all the elements except the first. This operation has constant cost. + * + * *Warning:* as the underlying list nodes (the [[SystemMessage]] instances) are mutable, care + * should be taken when passing the tail to other methods. [[SystemMessage#unlink]] should be + * called on the head if one wants to detach the tail permanently. + */ + final def tail: LatestFirstSystemMessageList = new LatestFirstSystemMessageList(head.next) + + /** + * Reverses the list. This operation mutates the underlying list. The cost of the call to reverse is linear in the + * number of elements. + * + * The type of the returned list is of the opposite order: [[EarliestFirstSystemMessageList]] + */ + final def reverse: EarliestFirstSystemMessageList = new EarliestFirstSystemMessageList(reverseInner(head, null)) + + /** + * Attaches a message to the current head of the list. This operation has constant cost. + */ + final def ::(msg: SystemMessage): LatestFirstSystemMessageList = { + assert(msg ne null) + msg.next = head + new LatestFirstSystemMessageList(msg) + } + +} + +/** + * + * INTERNAL API + * + * Value class supporting list operations on system messages. The `next` field of [[SystemMessage]] + * is hidden, and can only accessed through the value classes [[LatestFirstSystemMessageList]] and + * [[EarliestFirstSystemMessageList]], abstracting over the fact that system messages are the + * list nodes themselves. If used properly, this stays a compile time construct without any allocation overhead. + * + * This list is mutable. + * + * This list type also encodes that the messages contained are in reverse order, i.e. the head of the list is the + * latest appended element. + * + */ +private[typed] class EarliestFirstSystemMessageList(val head: SystemMessage) extends AnyVal { + import SystemMessageList._ + + /** + * Indicates if the list is empty or not. This operation has constant cost. + */ + final def isEmpty: Boolean = head eq null + + /** + * Indicates if the list has at least one element or not. This operation has constant cost. + */ + final def nonEmpty: Boolean = head ne null + + /** + * Indicates if the list is empty or not. This operation has constant cost. + */ + final def size: Int = sizeInner(head, 0) + + /** + * Gives back the list containing all the elements except the first. This operation has constant cost. + * + * *Warning:* as the underlying list nodes (the [[SystemMessage]] instances) are mutable, care + * should be taken when passing the tail to other methods. [[SystemMessage#unlink]] should be + * called on the head if one wants to detach the tail permanently. + */ + final def tail: EarliestFirstSystemMessageList = new EarliestFirstSystemMessageList(head.next) + + /** + * Reverses the list. This operation mutates the underlying list. The cost of the call to reverse is linear in the + * number of elements. + * + * The type of the returned list is of the opposite order: [[LatestFirstSystemMessageList]] + */ + final def reverse: LatestFirstSystemMessageList = new LatestFirstSystemMessageList(reverseInner(head, null)) + + /** + * Attaches a message to the current head of the list. This operation has constant cost. + */ + final def ::(msg: SystemMessage): EarliestFirstSystemMessageList = { + assert(msg ne null) + msg.next = head + new EarliestFirstSystemMessageList(msg) + } + + /** + * Prepends a list in a reversed order to the head of this list. The prepended list will be reversed during the process. + * + * Example: (3, 4, 5) reversePrepend (2, 1, 0) == (0, 1, 2, 3, 4, 5) + * + * The cost of this operation is linear in the size of the list that is to be prepended. + */ + final def reverse_:::(other: LatestFirstSystemMessageList): EarliestFirstSystemMessageList = { + var remaining = other + var result = this + while (remaining.nonEmpty) { + val msg = remaining.head + remaining = remaining.tail + result ::= msg + } + result + } + +} + +/** + * System messages are handled specially: they form their own queue within + * each actor’s mailbox. This queue is encoded in the messages themselves to + * avoid extra allocations and overhead. The next pointer is a normal var, and + * it does not need to be volatile because in the enqueuing method its update + * is immediately succeeded by a volatile write and all reads happen after the + * volatile read in the dequeuing thread. Afterwards, the obtained list of + * system messages is handled in a single thread only and not ever passed around, + * hence no further synchronization is needed. + * + * INTERNAL API + * + * NEVER SEND THE SAME SYSTEM MESSAGE OBJECT TO TWO ACTORS + */ +private[typed] sealed trait SystemMessage extends Serializable { + // Next fields are only modifiable via the SystemMessageList value class + @transient + private[internal] var next: SystemMessage = _ + + def unlink(): Unit = next = null + + def unlinked: Boolean = next eq null +} + +/** + * INTERNAL API + */ +@SerialVersionUID(1L) +private[typed] final case class Create() extends SystemMessage + +/** + * INTERNAL API + */ +@SerialVersionUID(1L) +private[typed] final case class Terminate() extends SystemMessage + +/** + * INTERNAL API + */ +@SerialVersionUID(1L) +private[typed] final case class Watch(watchee: ActorRef[Nothing], watcher: ActorRef[Nothing]) extends SystemMessage + +/** + * INTERNAL API + */ +@SerialVersionUID(1L) +private[typed] final case class Unwatch(watchee: ActorRef[Nothing], watcher: ActorRef[Nothing]) extends SystemMessage + +/** + * INTERNAL API + */ +@SerialVersionUID(1L) +private[akka] final case class DeathWatchNotification(actor: ActorRef[Nothing], failureCause: Throwable) extends SystemMessage + +/** + * INTERNAL API + */ +@SerialVersionUID(1L) +private[akka] case object NoMessage extends SystemMessage // switched into the mailbox to signal termination diff --git a/akka-typed/src/main/scala/akka/typed/internal/package.scala b/akka-typed/src/main/scala/akka/typed/internal/package.scala new file mode 100644 index 0000000000..2898893d2c --- /dev/null +++ b/akka-typed/src/main/scala/akka/typed/internal/package.scala @@ -0,0 +1,14 @@ +package akka.typed + +package object internal { + /* + * These are safe due to the self-type of ActorRef + */ + implicit class ToImpl[U](val ref: ActorRef[U]) extends AnyVal { + def sorry: ActorRefImpl[U] = ref.asInstanceOf[ActorRefImpl[U]] + } + // This one is necessary because Scala refuses to infer Nothing + implicit class ToImplNothing(val ref: ActorRef[Nothing]) extends AnyVal { + def sorryForNothing: ActorRefImpl[Nothing] = ref.asInstanceOf[ActorRefImpl[Nothing]] + } +} diff --git a/akka-typed/src/main/scala/akka/typed/patterns/Receiver.scala b/akka-typed/src/main/scala/akka/typed/patterns/Receiver.scala index 5ea4ab936e..955a77c8c6 100644 --- a/akka-typed/src/main/scala/akka/typed/patterns/Receiver.scala +++ b/akka-typed/src/main/scala/akka/typed/patterns/Receiver.scala @@ -11,13 +11,16 @@ import scala.concurrent.duration.Deadline import akka.typed.ActorContext import java.util.LinkedList import scala.collection.JavaConverters._ -import akka.typed.ReceiveTimeout import scala.collection.immutable.Queue +// FIXME make this nice again once the Actor Algebra is implemented object Receiver { import akka.typed.ScalaDSL._ - sealed trait Command[T] + sealed trait InternalCommand[T] + case class ReceiveTimeout[T]() extends InternalCommand[T] + + sealed trait Command[T] extends InternalCommand[T] /** * Retrieve one message from the Receiver, waiting at most for the given duration. @@ -44,14 +47,14 @@ object Receiver { ContextAware[Any] { ctx ⇒ SynchronousSelf { syncself ⇒ Or( - empty(ctx).widen { case c: Command[t] ⇒ c.asInstanceOf[Command[T]] }, + empty(ctx).widen { case c: InternalCommand[t] ⇒ c.asInstanceOf[InternalCommand[T]] }, Static[Any] { case msg ⇒ syncself ! Enqueue(msg) }) } }.narrow - private def empty[T](ctx: ActorContext[Any]): Behavior[Command[T]] = + private def empty[T](ctx: ActorContext[Any]): Behavior[InternalCommand[T]] = Total { case ExternalAddress(replyTo) ⇒ { replyTo ! ctx.self; Same } case g @ GetOne(d) if d <= Duration.Zero ⇒ { g.replyTo ! GetOneResult(ctx.self, None); Same } @@ -61,7 +64,7 @@ object Receiver { case Enqueue(msg) ⇒ queued(ctx, msg) } - private def queued[T](ctx: ActorContext[Any], t: T): Behavior[Command[T]] = { + private def queued[T](ctx: ActorContext[Any], t: T): Behavior[InternalCommand[T]] = { val queue = new LinkedList[T] queue.add(t) Total { @@ -84,39 +87,36 @@ object Receiver { } private case class Asked[T](replyTo: ActorRef[GetOneResult[T]], deadline: Deadline) - private def asked[T](ctx: ActorContext[Any], queue: Queue[Asked[T]]): Behavior[Command[T]] = { - ctx.setReceiveTimeout(queue.map(_.deadline).min.timeLeft) + private def asked[T](ctx: ActorContext[Any], queue: Queue[Asked[T]]): Behavior[InternalCommand[T]] = { + ctx.setReceiveTimeout(queue.map(_.deadline).min.timeLeft, ReceiveTimeout()) - Full { - case Sig(_, ReceiveTimeout) ⇒ + Total { + case ReceiveTimeout() ⇒ val (overdue, remaining) = queue partition (_.deadline.isOverdue) overdue foreach (a ⇒ a.replyTo ! GetOneResult(ctx.self, None)) if (remaining.isEmpty) { - ctx.setReceiveTimeout(Duration.Undefined) + ctx.cancelReceiveTimeout() empty(ctx) } else asked(ctx, remaining) - case Msg(_, msg) ⇒ - msg match { - case ExternalAddress(replyTo) ⇒ { replyTo ! ctx.self; Same } - case g @ GetOne(d) if d <= Duration.Zero ⇒ - g.replyTo ! GetOneResult(ctx.self, None) - asked(ctx, queue) - case g @ GetOne(d) ⇒ - asked(ctx, queue enqueue Asked(g.replyTo, Deadline.now + d)) - case g @ GetAll(d) if d <= Duration.Zero ⇒ - g.replyTo ! GetAllResult(ctx.self, Nil) - asked(ctx, queue) - case g @ GetAll(d) ⇒ - ctx.schedule(d, ctx.self, GetAll(Duration.Zero)(g.replyTo)) - asked(ctx, queue) - case Enqueue(msg) ⇒ - val (ask, q) = queue.dequeue - ask.replyTo ! GetOneResult(ctx.self, Some(msg)) - if (q.isEmpty) { - ctx.setReceiveTimeout(Duration.Undefined) - empty(ctx) - } else asked(ctx, q) - } + case ExternalAddress(replyTo) ⇒ { replyTo ! ctx.self; Same } + case g @ GetOne(d) if d <= Duration.Zero ⇒ + g.replyTo ! GetOneResult(ctx.self, None) + asked(ctx, queue) + case g @ GetOne(d) ⇒ + asked(ctx, queue enqueue Asked(g.replyTo, Deadline.now + d)) + case g @ GetAll(d) if d <= Duration.Zero ⇒ + g.replyTo ! GetAllResult(ctx.self, Nil) + asked(ctx, queue) + case g @ GetAll(d) ⇒ + ctx.schedule(d, ctx.self, GetAll(Duration.Zero)(g.replyTo)) + asked(ctx, queue) + case Enqueue(msg) ⇒ + val (ask, q) = queue.dequeue + ask.replyTo ! GetOneResult(ctx.self, Some(msg)) + if (q.isEmpty) { + ctx.cancelReceiveTimeout() + empty(ctx) + } else asked(ctx, q) } } } diff --git a/akka-typed/src/main/scala/akka/typed/patterns/Restarter.scala b/akka-typed/src/main/scala/akka/typed/patterns/Restarter.scala new file mode 100644 index 0000000000..0297c1f0f7 --- /dev/null +++ b/akka-typed/src/main/scala/akka/typed/patterns/Restarter.scala @@ -0,0 +1,54 @@ +/** + * Copyright (C) 2016 Lightbend Inc. + */ +package akka.typed +package patterns + +import scala.reflect.ClassTag +import scala.util.control.NonFatal +import akka.event.Logging + +/** + * Simple supervision strategy that restarts the underlying behavior for all + * failures of type Thr. + * + * FIXME add limited restarts and back-off (with limited buffering or vacation responder) + * FIXME write tests that ensure that all Behaviors are okay with getting PostRestart as first signal + */ +final case class Restarter[T, Thr <: Throwable: ClassTag](behavior: () ⇒ Behavior[T], resume: Boolean) extends Behavior[T] { + + private[this] var current = behavior() + + // FIXME remove allocation overhead once finalized + private def canonicalize(ctx: ActorContext[T], block: ⇒ Behavior[T]): Behavior[T] = { + val b = + try block + catch { + case ex: Thr ⇒ + ctx.system.eventStream.publish(Logging.Error(ex, ctx.self.toString, current.getClass, ex.getMessage)) + if (resume) current else restart(ctx) + } + current = Behavior.canonicalize(b, current) + if (Behavior.isAlive(current)) this else ScalaDSL.Stopped + } + + private def restart(ctx: ActorContext[T]): Behavior[T] = { + try current.management(ctx, PreRestart) catch { case NonFatal(_) ⇒ } + current = behavior() + current.management(ctx, PostRestart) + } + + override def management(ctx: ActorContext[T], signal: Signal): Behavior[T] = + canonicalize(ctx, current.management(ctx, signal)) + + override def message(ctx: ActorContext[T], msg: T): Behavior[T] = + canonicalize(ctx, current.message(ctx, msg)) +} + +object Restarter { + class Apply[Thr <: Throwable](c: ClassTag[Thr], resume: Boolean) { + def wrap[T](p: Props[T]) = Props(() ⇒ Restarter(p.creator, resume)(c), p.dispatcher, p.mailboxCapacity) + } + + def apply[Thr <: Throwable: ClassTag](resume: Boolean = false): Apply[Thr] = new Apply(implicitly, resume) +} diff --git a/akka-typed/src/test/resources/reference.conf b/akka-typed/src/test/resources/reference.conf index c369dd5097..e053d9d956 100644 --- a/akka-typed/src/test/resources/reference.conf +++ b/akka-typed/src/test/resources/reference.conf @@ -1,3 +1,6 @@ +akka.loglevel = DEBUG +akka.actor.debug.lifecycle = off + dispatcher-1 { fork-join-executor { parallelism-min=1 diff --git a/akka-typed/src/test/scala/akka/typed/ActorContextSpec.scala b/akka-typed/src/test/scala/akka/typed/ActorContextSpec.scala index 7fce39a353..3a711e9c3d 100644 --- a/akka-typed/src/test/scala/akka/typed/ActorContextSpec.scala +++ b/akka-typed/src/test/scala/akka/typed/ActorContextSpec.scala @@ -4,14 +4,19 @@ import scala.concurrent.duration._ import scala.concurrent.Future import com.typesafe.config.ConfigFactory import akka.actor.DeadLetterSuppression +import akka.typed.ScalaDSL._ +import akka.typed.patterns._ object ActorContextSpec { - import ScalaDSL._ sealed trait Command sealed trait Event + sealed trait Monitor extends Event - final case class GotSignal(signal: Signal) extends Event with DeadLetterSuppression + final case class GotSignal(signal: Signal) extends Monitor with DeadLetterSuppression + final case object GotReceiveTimeout extends Monitor + + final case object ReceiveTimeout extends Command final case class Ping(replyTo: ActorRef[Pong]) extends Command sealed trait Pong extends Event @@ -26,7 +31,7 @@ object ActorContextSpec { final case class Throw(ex: Exception) extends Command - final case class MkChild(name: Option[String], monitor: ActorRef[GotSignal], replyTo: ActorRef[Created]) extends Command + final case class MkChild(name: Option[String], monitor: ActorRef[Monitor], replyTo: ActorRef[Created]) extends Command final case class Created(ref: ActorRef[Command]) extends Event final case class SetTimeout(duration: FiniteDuration, replyTo: ActorRef[TimeoutSet.type]) extends Command @@ -68,16 +73,15 @@ object ActorContextSpec { final case class GetAdapter(replyTo: ActorRef[Adapter]) extends Command final case class Adapter(a: ActorRef[Command]) extends Event - def subject(monitor: ActorRef[GotSignal]): Behavior[Command] = + def subject(monitor: ActorRef[Monitor]): Behavior[Command] = FullTotal { case Sig(ctx, signal) ⇒ monitor ! GotSignal(signal) - signal match { - case f: Failed ⇒ f.decide(Failed.Restart) - case _ ⇒ - } Same case Msg(ctx, message) ⇒ message match { + case ReceiveTimeout ⇒ + monitor ! GotReceiveTimeout + Same case Ping(replyTo) ⇒ replyTo ! Pong1 Same @@ -91,13 +95,16 @@ object ActorContextSpec { throw ex case MkChild(name, mon, replyTo) ⇒ val child = name match { - case None ⇒ ctx.spawnAnonymous(Props(subject(mon))) - case Some(n) ⇒ ctx.spawn(Props(subject(mon)), n) + case None ⇒ ctx.spawnAnonymous(Restarter[Throwable]().wrap(Props(subject(mon)))) + case Some(n) ⇒ ctx.spawn(Restarter[Throwable]().wrap(Props(subject(mon))), n) } replyTo ! Created(child) Same case SetTimeout(d, replyTo) ⇒ - ctx.setReceiveTimeout(d) + d match { + case f: FiniteDuration ⇒ ctx.setReceiveTimeout(f, ReceiveTimeout) + case _ ⇒ ctx.cancelReceiveTimeout() + } replyTo ! TimeoutSet Same case Schedule(delay, target, msg, replyTo) ⇒ @@ -160,7 +167,6 @@ class ActorContextSpec extends TypedSpec(ConfigFactory.parseString( | } |}""".stripMargin)) { import ActorContextSpec._ - import ScalaDSL._ val expectTimeout = 3.seconds @@ -175,10 +181,18 @@ class ActorContextSpec extends TypedSpec(ConfigFactory.parseString( */ def behavior(ctx: ActorContext[Event]): Behavior[Command] - def setup(name: String)(proc: (ActorContext[Event], StepWise.Steps[Event, ActorRef[Command]]) ⇒ StepWise.Steps[Event, _]): Future[TypedSpec.Status] = - runTest(s"$suite-$name")(StepWise[Event] { (ctx, startWith) ⇒ + implicit def system: ActorSystem[TypedSpec.Command] + + private def mySuite: String = + if (system eq nativeSystem) suite + "Native" + else suite + "Adapted" + + def setup(name: String, wrapper: Option[Restarter.Apply[_]] = None)( + proc: (ActorContext[Event], StepWise.Steps[Event, ActorRef[Command]]) ⇒ StepWise.Steps[Event, _]): Future[TypedSpec.Status] = + runTest(s"$mySuite-$name")(StepWise[Event] { (ctx, startWith) ⇒ + val props = wrapper.map(_.wrap(Props(behavior(ctx)))).getOrElse(Props(behavior(ctx))) val steps = - startWith.withKeepTraces(true)(ctx.spawn(Props(behavior(ctx)), "subject")) + startWith.withKeepTraces(true)(ctx.spawn(props, "subject")) .expectMessage(expectTimeout) { (msg, ref) ⇒ msg should ===(GotSignal(PreStart)) ref @@ -246,25 +260,20 @@ class ActorContextSpec extends TypedSpec(ConfigFactory.parseString( } }) - def `01 must correctly wire the lifecycle hooks`(): Unit = sync(setup("ctx01") { (ctx, startWith) ⇒ + def `01 must correctly wire the lifecycle hooks`(): Unit = sync(setup("ctx01", Some(Restarter[Throwable]())) { (ctx, startWith) ⇒ val self = ctx.self val ex = new Exception("KABOOM1") startWith { subj ⇒ val log = muteExpectedException[Exception]("KABOOM1", occurrences = 1) subj ! Throw(ex) (subj, log) - }.expectFailureKeep(expectTimeout) { - case (f, (subj, _)) ⇒ - f.cause should ===(ex) - f.child should ===(subj) - Failed.Restart }.expectMessage(expectTimeout) { case (msg, (subj, log)) ⇒ - msg should ===(GotSignal(PreRestart(ex))) + msg should ===(GotSignal(PreRestart)) log.assertDone(expectTimeout) subj }.expectMessage(expectTimeout) { (msg, subj) ⇒ - msg should ===(GotSignal(PostRestart(ex))) + msg should ===(GotSignal(PostRestart)) ctx.stop(subj) }.expectMessage(expectTimeout) { (msg, _) ⇒ msg should ===(GotSignal(PostStop)) @@ -288,12 +297,11 @@ class ActorContextSpec extends TypedSpec(ConfigFactory.parseString( val log = muteExpectedException[Exception]("KABOOM2", occurrences = 1) child ! Throw(ex) (subj, child, log) - }.expectMultipleMessages(expectTimeout, 3) { + }.expectMultipleMessages(expectTimeout, 2) { case (msgs, (subj, child, log)) ⇒ msgs should ===( - GotSignal(Failed(`ex`, `child`)) :: - ChildEvent(GotSignal(PreRestart(`ex`))) :: - ChildEvent(GotSignal(PostRestart(`ex`))) :: Nil) + ChildEvent(GotSignal(PreRestart)) :: + ChildEvent(GotSignal(PostRestart)) :: Nil) log.assertDone(expectTimeout) child ! BecomeInert(self) // necessary to avoid PostStop/Terminated interference (subj, child) @@ -327,7 +335,7 @@ class ActorContextSpec extends TypedSpec(ConfigFactory.parseString( } }) - def `05 must reset behavior upon Restart`(): Unit = sync(setup("ctx05") { (ctx, startWith) ⇒ + def `05 must reset behavior upon Restart`(): Unit = sync(setup("ctx05", Some(Restarter[Exception]())) { (ctx, startWith) ⇒ val self = ctx.self val ex = new Exception("KABOOM05") startWith @@ -336,48 +344,36 @@ class ActorContextSpec extends TypedSpec(ConfigFactory.parseString( val log = muteExpectedException[Exception]("KABOOM05") subj ! Throw(ex) (subj, log) - }.expectFailureKeep(expectTimeout) { - case (f, (subj, log)) ⇒ - f.child should ===(subj) - f.cause should ===(ex) - Failed.Restart }.expectMessage(expectTimeout) { case (msg, (subj, log)) ⇒ - msg should ===(GotSignal(PostRestart(ex))) + msg should ===(GotSignal(PostRestart)) log.assertDone(expectTimeout) subj - }.stimulate(_ ! Ping(self), _ ⇒ Pong1) + } + .stimulate(_ ! Ping(self), _ ⇒ Pong1) }) - def `06 must not reset behavior upon Resume`(): Unit = sync(setup("ctx06") { (ctx, startWith) ⇒ + def `06 must not reset behavior upon Resume`(): Unit = sync(setup("ctx06", Some(Restarter[Exception](resume = true))) { (ctx, startWith) ⇒ val self = ctx.self - val ex = new Exception("KABOOM05") + val ex = new Exception("KABOOM06") startWith .stimulate(_ ! BecomeInert(self), _ ⇒ BecameInert) .stimulate(_ ! Ping(self), _ ⇒ Pong2).keep { subj ⇒ + muteExpectedException[Exception]("KABOOM06", occurrences = 1) subj ! Throw(ex) - }.expectFailureKeep(expectTimeout) { (f, subj) ⇒ - f.child should ===(subj) - f.cause should ===(ex) - Failed.Resume }.stimulate(_ ! Ping(self), _ ⇒ Pong2) }) def `07 must stop upon Stop`(): Unit = sync(setup("ctx07") { (ctx, startWith) ⇒ val self = ctx.self - val ex = new Exception("KABOOM05") + val ex = new Exception("KABOOM07") startWith .stimulate(_ ! Ping(self), _ ⇒ Pong1).keep { subj ⇒ + muteExpectedException[Exception]("KABOOM07", occurrences = 1) subj ! Throw(ex) ctx.watch(subj) - }.expectFailureKeep(expectTimeout) { (f, subj) ⇒ - f.child should ===(subj) - f.cause should ===(ex) - Failed.Stop - }.expectMessageKeep(expectTimeout) { (msg, _) ⇒ - msg should ===(GotSignal(PostStop)) - }.expectTermination(expectTimeout) { (t, subj) ⇒ - t.ref should ===(subj) + }.expectMulti(expectTimeout, 2) { (msgs, subj) ⇒ + msgs.toSet should ===(Set(Left(Terminated(subj)(null)), Right(GotSignal(PostStop)))) } }) @@ -405,7 +401,7 @@ class ActorContextSpec extends TypedSpec(ConfigFactory.parseString( msg should ===(Watched) child ! Stop }.expectMessage(expectTimeout) { (msg, child) ⇒ - msg should ===(GotSignal(Terminated(child))) + msg should ===(GotSignal(Terminated(child)(null))) } }) @@ -417,11 +413,11 @@ class ActorContextSpec extends TypedSpec(ConfigFactory.parseString( child ! Stop }.expectTermination(expectTimeout) { case (t, (subj, child)) ⇒ - t should ===(Terminated(child)) + t should ===(Terminated(child)(null)) subj ! Watch(child, blackhole) child }.expectMessage(expectTimeout) { (msg, child) ⇒ - msg should ===(GotSignal(Terminated(child))) + msg should ===(GotSignal(Terminated(child)(null))) } }) @@ -441,7 +437,7 @@ class ActorContextSpec extends TypedSpec(ConfigFactory.parseString( child ! Stop child }.expectTermination(expectTimeout) { (t, child) ⇒ - t should ===(Terminated(child)) + t should ===(Terminated(child)(null)) } }) @@ -449,6 +445,7 @@ class ActorContextSpec extends TypedSpec(ConfigFactory.parseString( val self = ctx.self startWith.mkChild(None, ctx.spawnAdapter(ChildEvent), self).keep { case (subj, child) ⇒ + muteExpectedException[DeathPactException]() subj ! Watch(child, self) }.expectMessageKeep(expectTimeout) { case (msg, (subj, child)) ⇒ @@ -458,10 +455,6 @@ class ActorContextSpec extends TypedSpec(ConfigFactory.parseString( case (msg, (subj, child)) ⇒ msg should ===(BecameCareless) child ! Stop - }.expectFailureKeep(expectTimeout) { - case (f, (subj, child)) ⇒ - f.child should ===(subj) - Failed.Stop }.expectMessage(expectTimeout) { case (msg, (subj, child)) ⇒ msg should ===(GotSignal(PostStop)) @@ -493,7 +486,7 @@ class ActorContextSpec extends TypedSpec(ConfigFactory.parseString( startWith .stimulate(_ ! SetTimeout(1.nano, self), _ ⇒ TimeoutSet) .expectMessage(expectTimeout) { (msg, _) ⇒ - msg should ===(GotSignal(ReceiveTimeout)) + msg should ===(GotReceiveTimeout) } }) @@ -525,54 +518,66 @@ class ActorContextSpec extends TypedSpec(ConfigFactory.parseString( msg should ===(Pong1) ctx.stop(subj) adapter - }.expectMessageKeep(expectTimeout) { (msg, _) ⇒ - msg should ===(GotSignal(PostStop)) - }.expectTermination(expectTimeout) { (t, adapter) ⇒ - t.ref should ===(adapter) + }.expectMulti(expectTimeout, 2) { (msgs, adapter) ⇒ + msgs.toSet should ===(Set(Left(Terminated(adapter)(null)), Right(GotSignal(PostStop)))) } }) } - object `An ActorContext` extends Tests { + trait Normal extends Tests { override def suite = "basic" override def behavior(ctx: ActorContext[Event]): Behavior[Command] = subject(ctx.self) } + object `An ActorContext (native)` extends Normal with NativeSystem + object `An ActorContext (adapted)` extends Normal with AdaptedSystem - object `An ActorContext with widened Behavior` extends Tests { + trait Widened extends Tests { override def suite = "widened" override def behavior(ctx: ActorContext[Event]): Behavior[Command] = subject(ctx.self).widen { case x ⇒ x } } + object `An ActorContext with widened Behavior (native)` extends Widened with NativeSystem + object `An ActorContext with widened Behavior (adapted)` extends Widened with AdaptedSystem - object `An ActorContext with SynchronousSelf` extends Tests { + trait SynchronousSelf extends Tests { override def suite = "synchronous" override def behavior(ctx: ActorContext[Event]): Behavior[Command] = SynchronousSelf(self ⇒ subject(ctx.self)) } + object `An ActorContext with SynchronousSelf (native)` extends SynchronousSelf with NativeSystem + object `An ActorContext with SynchronousSelf (adapted)` extends SynchronousSelf with AdaptedSystem - object `An ActorContext with non-matching Tap` extends Tests { + trait NonMatchingTap extends Tests { override def suite = "TapNonMatch" override def behavior(ctx: ActorContext[Event]): Behavior[Command] = Tap({ case null ⇒ }, subject(ctx.self)) } + object `An ActorContext with non-matching Tap (native)` extends NonMatchingTap with NativeSystem + object `An ActorContext with non-matching Tap (adapted)` extends NonMatchingTap with AdaptedSystem - object `An ActorContext with matching Tap` extends Tests { + trait MatchingTap extends Tests { override def suite = "TapMatch" override def behavior(ctx: ActorContext[Event]): Behavior[Command] = Tap({ case _ ⇒ }, subject(ctx.self)) } + object `An ActorContext with matching Tap (native)` extends MatchingTap with NativeSystem + object `An ActorContext with matching Tap (adapted)` extends MatchingTap with AdaptedSystem private val stoppingBehavior = Full[Command] { case Msg(_, Stop) ⇒ Stopped } - object `An ActorContext with And (left)` extends Tests { + trait AndLeft extends Tests { override def suite = "and" override def behavior(ctx: ActorContext[Event]): Behavior[Command] = And(subject(ctx.self), stoppingBehavior) } + object `An ActorContext with And (left, native)` extends AndLeft with NativeSystem + object `An ActorContext with And (left, adapted)` extends AndLeft with AdaptedSystem - object `An ActorContext with And (right)` extends Tests { + trait AndRight extends Tests { override def suite = "and" override def behavior(ctx: ActorContext[Event]): Behavior[Command] = And(stoppingBehavior, subject(ctx.self)) } + object `An ActorContext with And (right, native)` extends AndRight with NativeSystem + object `An ActorContext with And (right, adapted)` extends AndRight with AdaptedSystem - object `An ActorContext with Or (left)` extends Tests { + trait OrLeft extends Tests { override def suite = "basic" override def behavior(ctx: ActorContext[Event]): Behavior[Command] = Or(subject(ctx.self), stoppingBehavior) @@ -581,8 +586,10 @@ class ActorContextSpec extends TypedSpec(ConfigFactory.parseString( ref ! Stop } } + object `An ActorContext with Or (left, native)` extends OrLeft with NativeSystem + object `An ActorContext with Or (left, adapted)` extends OrLeft with AdaptedSystem - object `An ActorContext with Or (right)` extends Tests { + trait OrRight extends Tests { override def suite = "basic" override def behavior(ctx: ActorContext[Event]): Behavior[Command] = Or(stoppingBehavior, subject(ctx.self)) @@ -591,5 +598,7 @@ class ActorContextSpec extends TypedSpec(ConfigFactory.parseString( ref ! Stop } } + object `An ActorContext with Or (right, native)` extends OrRight with NativeSystem + object `An ActorContext with Or (right, adapted)` extends OrRight with AdaptedSystem } diff --git a/akka-typed/src/test/scala/akka/typed/BehaviorSpec.scala b/akka-typed/src/test/scala/akka/typed/BehaviorSpec.scala index 241b3dc794..e21e91bbdc 100644 --- a/akka-typed/src/test/scala/akka/typed/BehaviorSpec.scala +++ b/akka-typed/src/test/scala/akka/typed/BehaviorSpec.scala @@ -27,7 +27,7 @@ class BehaviorSpec extends TypedSpec { } case class GetState(replyTo: ActorRef[State]) extends Command object GetState { - def apply()(implicit inbox: Inbox.SyncInbox[State]): GetState = GetState(inbox.ref) + def apply()(implicit inbox: Inbox[State]): GetState = GetState(inbox.ref) } case class AuxPing(id: Int) extends Command { override def expectedResponse(ctx: ActorContext[Command]): Seq[Event] = Pong :: Nil @@ -47,12 +47,13 @@ class BehaviorSpec extends TypedSpec { val StateB: State = new State { override def toString = "StateB"; override def next = StateA } trait Common { + def system: ActorSystem[TypedSpec.Command] def behavior(monitor: ActorRef[Event]): Behavior[Command] - case class Setup(ctx: EffectfulActorContext[Command], inbox: Inbox.SyncInbox[Event]) + case class Setup(ctx: EffectfulActorContext[Command], inbox: Inbox[Event]) protected def mkCtx(requirePreStart: Boolean = false, factory: (ActorRef[Event]) ⇒ Behavior[Command] = behavior) = { - val inbox = Inbox.sync[Event]("evt") + val inbox = Inbox[Event]("evt") val ctx = new EffectfulActorContext("ctx", Props(factory(inbox.ref)), system) val msgs = inbox.receiveAll() if (requirePreStart) @@ -71,7 +72,7 @@ class BehaviorSpec extends TypedSpec { setup.inbox.receiveAll() should ===(command.expectedResponse(setup.ctx)) setup } - def check[T](command: Command, aux: T*)(implicit inbox: Inbox.SyncInbox[T]): Setup = { + def check[T](command: Command, aux: T*)(implicit inbox: Inbox[T]): Setup = { setup.ctx.run(command) setup.inbox.receiveAll() should ===(command.expectedResponse(setup.ctx)) inbox.receiveAll() should ===(aux) @@ -83,7 +84,7 @@ class BehaviorSpec extends TypedSpec { setup.inbox.receiveAll() should ===(expected ++ expected) setup } - def check2[T](command: Command, aux: T*)(implicit inbox: Inbox.SyncInbox[T]): Setup = { + def check2[T](command: Command, aux: T*)(implicit inbox: Inbox[T]): Setup = { setup.ctx.run(command) val expected = command.expectedResponse(setup.ctx) setup.inbox.receiveAll() should ===(expected ++ expected) @@ -109,65 +110,31 @@ class BehaviorSpec extends TypedSpec { } def `must react to PreRestart`(): Unit = { - mkCtx().check(PreRestart(ex)) + mkCtx().check(PreRestart) } def `must react to PreRestart after a message`(): Unit = { - mkCtx().check(GetSelf).check(PreRestart(ex)) + mkCtx().check(GetSelf).check(PreRestart) } def `must react to PostRestart`(): Unit = { - mkCtx().check(PostRestart(ex)) + mkCtx().check(PostRestart) } def `must react to a message after PostRestart`(): Unit = { - mkCtx().check(PostRestart(ex)).check(GetSelf) - } - - def `must react to Failed`(): Unit = { - val setup @ Setup(ctx, inbox) = mkCtx() - val f = Failed(ex, inbox.ref) - setup.check(f) - f.getDecision should ===(Failed.Restart) - } - - def `must react to Failed after a message`(): Unit = { - val setup @ Setup(ctx, inbox) = mkCtx().check(GetSelf) - val f = Failed(ex, inbox.ref) - setup.check(f) - f.getDecision should ===(Failed.Restart) - } - - def `must react to a message after Failed`(): Unit = { - val setup @ Setup(ctx, inbox) = mkCtx() - val f = Failed(ex, inbox.ref) - setup.check(f) - f.getDecision should ===(Failed.Restart) - setup.check(GetSelf) - } - - def `must react to ReceiveTimeout`(): Unit = { - mkCtx().check(ReceiveTimeout) - } - - def `must react to ReceiveTimeout after a message`(): Unit = { - mkCtx().check(GetSelf).check(ReceiveTimeout) - } - - def `must react to a message after ReceiveTimeout`(): Unit = { - mkCtx().check(ReceiveTimeout).check(GetSelf) + mkCtx().check(PostRestart).check(GetSelf) } def `must react to Terminated`(): Unit = { - mkCtx().check(Terminated(Inbox.sync("x").ref)) + mkCtx().check(Terminated(Inbox("x").ref)(null)) } def `must react to Terminated after a message`(): Unit = { - mkCtx().check(GetSelf).check(Terminated(Inbox.sync("x").ref)) + mkCtx().check(GetSelf).check(Terminated(Inbox("x").ref)(null)) } def `must react to a message after Terminated`(): Unit = { - mkCtx().check(Terminated(Inbox.sync("x").ref)).check(GetSelf) + mkCtx().check(Terminated(Inbox("x").ref)(null)).check(GetSelf) } } @@ -202,7 +169,7 @@ class BehaviorSpec extends TypedSpec { } trait Become extends Common with Unhandled { - private implicit val inbox = Inbox.sync[State]("state") + private implicit val inbox = Inbox[State]("state") def `must be in state A`(): Unit = { mkCtx().check(GetState(), StateA) @@ -227,61 +194,27 @@ class BehaviorSpec extends TypedSpec { } def `must react to PreRestart after swap`(): Unit = { - mkCtx().check(Swap).check(PreRestart(ex)) + mkCtx().check(Swap).check(PreRestart) } def `must react to PreRestart after a message after swap`(): Unit = { - mkCtx().check(Swap).check(GetSelf).check(PreRestart(ex)) + mkCtx().check(Swap).check(GetSelf).check(PreRestart) } def `must react to a message after PostRestart after swap`(): Unit = { - mkCtx().check(PostRestart(ex)).check(Swap).check(GetSelf) - } - - def `must react to Failed after swap`(): Unit = { - val setup @ Setup(ctx, inbox) = mkCtx().check(Swap) - val f = Failed(ex, inbox.ref) - setup.check(f) - f.getDecision should ===(Failed.Restart) - } - - def `must react to Failed after a message after swap`(): Unit = { - val setup @ Setup(ctx, inbox) = mkCtx().check(Swap).check(GetSelf) - val f = Failed(ex, inbox.ref) - setup.check(f) - f.getDecision should ===(Failed.Restart) - } - - def `must react to a message after Failed after swap`(): Unit = { - val setup @ Setup(ctx, inbox) = mkCtx().check(Swap) - val f = Failed(ex, inbox.ref) - setup.check(f) - f.getDecision should ===(Failed.Restart) - setup.check(GetSelf) - } - - def `must react to ReceiveTimeout after swap`(): Unit = { - mkCtx().check(Swap).check(ReceiveTimeout) - } - - def `must react to ReceiveTimeout after a message after swap`(): Unit = { - mkCtx().check(Swap).check(GetSelf).check(ReceiveTimeout) - } - - def `must react to a message after ReceiveTimeout after swap`(): Unit = { - mkCtx().check(Swap).check(ReceiveTimeout).check(GetSelf) + mkCtx().check(PostRestart).check(Swap).check(GetSelf) } def `must react to Terminated after swap`(): Unit = { - mkCtx().check(Swap).check(Terminated(Inbox.sync("x").ref)) + mkCtx().check(Swap).check(Terminated(Inbox("x").ref)(null)) } def `must react to Terminated after a message after swap`(): Unit = { - mkCtx().check(Swap).check(GetSelf).check(Terminated(Inbox.sync("x").ref)) + mkCtx().check(Swap).check(GetSelf).check(Terminated(Inbox("x").ref)(null)) } def `must react to a message after Terminated after swap`(): Unit = { - mkCtx().check(Swap).check(Terminated(Inbox.sync("x").ref)).check(GetSelf) + mkCtx().check(Swap).check(Terminated(Inbox("x").ref)(null)).check(GetSelf) } } @@ -290,10 +223,6 @@ class BehaviorSpec extends TypedSpec { Full { case Sig(ctx, signal) ⇒ monitor ! GotSignal(signal) - signal match { - case f: Failed ⇒ f.decide(Failed.Restart) - case _ ⇒ - } Same case Msg(ctx, GetSelf) ⇒ monitor ! Self(ctx.self) @@ -317,21 +246,19 @@ class BehaviorSpec extends TypedSpec { } } - object `A Full Behavior` extends Messages with BecomeWithLifecycle with Stoppable { + trait FullBehavior extends Messages with BecomeWithLifecycle with Stoppable { override def behavior(monitor: ActorRef[Event]): Behavior[Command] = mkFull(monitor) } + object `A Full Behavior (native)` extends FullBehavior with NativeSystem + object `A Full Behavior (adapted)` extends FullBehavior with AdaptedSystem - object `A FullTotal Behavior` extends Messages with BecomeWithLifecycle with Stoppable { + trait FullTotalBehavior extends Messages with BecomeWithLifecycle with Stoppable { override def behavior(monitor: ActorRef[Event]): Behavior[Command] = behv(monitor, StateA) private def behv(monitor: ActorRef[Event], state: State): Behavior[Command] = { import ScalaDSL.{ FullTotal, Msg, Sig, Same, Unhandled, Stopped } FullTotal { case Sig(ctx, signal) ⇒ monitor ! GotSignal(signal) - signal match { - case f: Failed ⇒ f.decide(Failed.Restart) - case _ ⇒ - } Same case Msg(ctx, GetSelf) ⇒ monitor ! Self(ctx.self) @@ -356,36 +283,48 @@ class BehaviorSpec extends TypedSpec { } } } + object `A FullTotal Behavior (native)` extends FullTotalBehavior with NativeSystem + object `A FullTotal Behavior (adapted)` extends FullTotalBehavior with AdaptedSystem - object `A Widened Behavior` extends Messages with BecomeWithLifecycle with Stoppable { + trait WidenedBehavior extends Messages with BecomeWithLifecycle with Stoppable { override def behavior(monitor: ActorRef[Event]): Behavior[Command] = ScalaDSL.Widened(mkFull(monitor), { case x ⇒ x }) } + object `A Widened Behavior (native)` extends WidenedBehavior with NativeSystem + object `A Widened Behavior (adapted)` extends WidenedBehavior with AdaptedSystem - object `A ContextAware Behavior` extends Messages with BecomeWithLifecycle with Stoppable { + trait ContextAwareBehavior extends Messages with BecomeWithLifecycle with Stoppable { override def behavior(monitor: ActorRef[Event]): Behavior[Command] = ScalaDSL.ContextAware(ctx ⇒ mkFull(monitor)) } + object `A ContextAware Behavior (native)` extends ContextAwareBehavior with NativeSystem + object `A ContextAware Behavior (adapted)` extends ContextAwareBehavior with AdaptedSystem - object `A SelfAware Behavior` extends Messages with BecomeWithLifecycle with Stoppable { + trait SelfAwareBehavior extends Messages with BecomeWithLifecycle with Stoppable { override def behavior(monitor: ActorRef[Event]): Behavior[Command] = ScalaDSL.SelfAware(self ⇒ mkFull(monitor)) } + object `A SelfAware Behavior (native)` extends SelfAwareBehavior with NativeSystem + object `A SelfAware Behavior (adapted)` extends SelfAwareBehavior with AdaptedSystem - object `A non-matching Tap Behavior` extends Messages with BecomeWithLifecycle with Stoppable { + trait NonMatchingTapBehavior extends Messages with BecomeWithLifecycle with Stoppable { override def behavior(monitor: ActorRef[Event]): Behavior[Command] = ScalaDSL.Tap({ case null ⇒ }, mkFull(monitor)) } + object `A non-matching Tap Behavior (native)` extends NonMatchingTapBehavior with NativeSystem + object `A non-matching Tap Behavior (adapted)` extends NonMatchingTapBehavior with AdaptedSystem - object `A matching Tap Behavior` extends Messages with BecomeWithLifecycle with Stoppable { + trait MatchingTapBehavior extends Messages with BecomeWithLifecycle with Stoppable { override def behavior(monitor: ActorRef[Event]): Behavior[Command] = ScalaDSL.Tap({ case _ ⇒ }, mkFull(monitor)) } + object `A matching Tap Behavior (native)` extends MatchingTapBehavior with NativeSystem + object `A matching Tap Behavior (adapted)` extends MatchingTapBehavior with AdaptedSystem - object `A SynchronousSelf Behavior` extends Messages with BecomeWithLifecycle with Stoppable { + trait SynchronousSelfBehavior extends Messages with BecomeWithLifecycle with Stoppable { import ScalaDSL._ - implicit private val inbox = Inbox.sync[Command]("syncself") + implicit private val inbox = Inbox[Command]("syncself") override def behavior(monitor: ActorRef[Event]): Behavior[Command] = SynchronousSelf(self ⇒ mkFull(monitor)) @@ -413,9 +352,11 @@ class BehaviorSpec extends TypedSpec { ctx.currentBehavior should ===(Stopped[Command]) } } + object `A SynchronourSelf Behavior (native)` extends SynchronousSelfBehavior with NativeSystem + object `A SynchronousSelf Behavior (adapted)` extends SynchronousSelfBehavior with AdaptedSystem trait And extends Common { - private implicit val inbox = Inbox.sync[State]("and") + private implicit val inbox = Inbox[State]("and") private def behavior2(monitor: ActorRef[Event]): Behavior[Command] = ScalaDSL.And(mkFull(monitor), mkFull(monitor)) @@ -431,15 +372,19 @@ class BehaviorSpec extends TypedSpec { } } - object `A Behavior combined with And (left)` extends Messages with BecomeWithLifecycle with And { + trait BehaviorAndLeft extends Messages with BecomeWithLifecycle with And { override def behavior(monitor: ActorRef[Event]): Behavior[Command] = ScalaDSL.And(mkFull(monitor), ScalaDSL.Empty) } + object `A Behavior combined with And (left, native)` extends BehaviorAndLeft with NativeSystem + object `A Behavior combined with And (left, adapted)` extends BehaviorAndLeft with NativeSystem - object `A Behavior combined with And (right)` extends Messages with BecomeWithLifecycle with And { + trait BehaviorAndRight extends Messages with BecomeWithLifecycle with And { override def behavior(monitor: ActorRef[Event]): Behavior[Command] = ScalaDSL.And(ScalaDSL.Empty, mkFull(monitor)) } + object `A Behavior combined with And (right, native)` extends BehaviorAndRight with NativeSystem + object `A Behavior combined with And (right, adapted)` extends BehaviorAndRight with NativeSystem trait Or extends Common { private def strange(monitor: ActorRef[Event]): Behavior[Command] = @@ -470,17 +415,21 @@ class BehaviorSpec extends TypedSpec { } } - object `A Behavior combined with Or (left)` extends Messages with BecomeWithLifecycle with Or { + trait BehaviorOrLeft extends Messages with BecomeWithLifecycle with Or { override def behavior(monitor: ActorRef[Event]): Behavior[Command] = ScalaDSL.Or(mkFull(monitor), ScalaDSL.Empty) } + object `A Behavior combined with Or (left, native)` extends BehaviorOrLeft with NativeSystem + object `A Behavior combined with Or (left, adapted)` extends BehaviorOrLeft with NativeSystem - object `A Behavior combined with Or (right)` extends Messages with BecomeWithLifecycle with Or { + trait BehaviorOrRight extends Messages with BecomeWithLifecycle with Or { override def behavior(monitor: ActorRef[Event]): Behavior[Command] = ScalaDSL.Or(ScalaDSL.Empty, mkFull(monitor)) } + object `A Behavior combined with Or (right, native)` extends BehaviorOrRight with NativeSystem + object `A Behavior combined with Or (right, adapted)` extends BehaviorOrRight with NativeSystem - object `A Partial Behavior` extends Messages with Become with Stoppable { + trait PartialBehavior extends Messages with Become with Stoppable { override def behavior(monitor: ActorRef[Event]): Behavior[Command] = behv(monitor, StateA) def behv(monitor: ActorRef[Event], state: State): Behavior[Command] = ScalaDSL.Partial { @@ -502,8 +451,10 @@ class BehaviorSpec extends TypedSpec { case Stop ⇒ ScalaDSL.Stopped } } + object `A Partial Behavior (native)` extends PartialBehavior with NativeSystem + object `A Partial Behavior (adapted)` extends PartialBehavior with AdaptedSystem - object `A Total Behavior` extends Messages with Become with Stoppable { + trait TotalBehavior extends Messages with Become with Stoppable { override def behavior(monitor: ActorRef[Event]): Behavior[Command] = behv(monitor, StateA) def behv(monitor: ActorRef[Event], state: State): Behavior[Command] = ScalaDSL.Total { @@ -527,8 +478,10 @@ class BehaviorSpec extends TypedSpec { case _: AuxPing ⇒ ScalaDSL.Unhandled } } + object `A Total Behavior (native)` extends TotalBehavior with NativeSystem + object `A Total Behavior (adapted)` extends TotalBehavior with AdaptedSystem - object `A Static Behavior` extends Messages { + trait StaticBehavior extends Messages { override def behavior(monitor: ActorRef[Event]): Behavior[Command] = ScalaDSL.Static { case Ping ⇒ monitor ! Pong @@ -541,4 +494,6 @@ class BehaviorSpec extends TypedSpec { case _: AuxPing ⇒ } } + object `A Static Behavior (native)` extends StaticBehavior with NativeSystem + object `A Static Behavior (adapted)` extends StaticBehavior with AdaptedSystem } diff --git a/akka-typed/src/test/scala/akka/typed/PerformanceSpec.scala b/akka-typed/src/test/scala/akka/typed/PerformanceSpec.scala index 3878dada75..2155b21e99 100644 --- a/akka-typed/src/test/scala/akka/typed/PerformanceSpec.scala +++ b/akka-typed/src/test/scala/akka/typed/PerformanceSpec.scala @@ -6,55 +6,62 @@ package akka.typed import ScalaDSL._ import scala.concurrent.duration._ import com.typesafe.config.ConfigFactory +import org.junit.runner.RunWith +import akka.testkit.AkkaSpec +import akka.util.Timeout +@RunWith(classOf[org.scalatest.junit.JUnitRunner]) class PerformanceSpec extends TypedSpec( ConfigFactory.parseString(""" # increase this if you do real benchmarking akka.typed.PerformanceSpec.iterations=100000 """)) { + override def setTimeout = Timeout(20.seconds) + object `A static behavior` { - object `must be fast` { + case class Ping(x: Int, pong: ActorRef[Pong], report: ActorRef[Pong]) + case class Pong(x: Int, ping: ActorRef[Ping], report: ActorRef[Pong]) - case class Ping(x: Int, pong: ActorRef[Pong], report: ActorRef[Pong]) - case class Pong(x: Int, ping: ActorRef[Ping], report: ActorRef[Pong]) + def behavior(pairs: Int, pings: Int, count: Int, executor: String) = + StepWise[Pong] { (ctx, startWith) ⇒ + startWith { - def behavior(pairs: Int, pings: Int, count: Int, executor: String) = - StepWise[Pong] { (ctx, startWith) ⇒ - startWith { + val pinger = Props(SelfAware[Ping](self ⇒ Static { msg ⇒ + if (msg.x == 0) { + msg.report ! Pong(0, self, msg.report) + } else msg.pong ! Pong(msg.x - 1, self, msg.report) + })).withDispatcher(executor) - val pinger = Props(SelfAware[Ping](self ⇒ Static { msg ⇒ - if (msg.x == 0) { - msg.report ! Pong(0, self, msg.report) - } else msg.pong ! Pong(msg.x - 1, self, msg.report) - })).withDispatcher(executor) + val ponger = Props(SelfAware[Pong](self ⇒ Static { msg ⇒ + msg.ping ! Ping(msg.x, self, msg.report) + })).withDispatcher(executor) - val ponger = Props(SelfAware[Pong](self ⇒ Static { msg ⇒ - msg.ping ! Ping(msg.x, self, msg.report) - })).withDispatcher(executor) + val actors = + for (i ← 1 to pairs) + yield (ctx.spawn(pinger, s"pinger-$i"), ctx.spawn(ponger, s"ponger-$i")) - val actors = - for (i ← 1 to pairs) - yield (ctx.spawn(pinger, s"pinger-$i"), ctx.spawn(ponger, s"ponger-$i")) + val start = Deadline.now - val start = Deadline.now + for { + (ping, pong) ← actors + _ ← 1 to pings + } ping ! Ping(count, pong, ctx.self) - for { - (ping, pong) ← actors - _ ← 1 to pings - } ping ! Ping(count, pong, ctx.self) + start + }.expectMultipleMessages(10.seconds, pairs * pings) { (msgs, start) ⇒ + val stop = Deadline.now - start - }.expectMultipleMessages(60.seconds, pairs * pings) { (msgs, start) ⇒ - val stop = Deadline.now - - val rate = 2L * count * pairs * pings / (stop - start).toMillis - info(s"messaging rate was $rate/ms") - } + val rate = 2L * count * pairs * pings / (stop - start).toMillis + info(s"messaging rate was $rate/ms") } + } - val iterations = system.settings.config.getInt("akka.typed.PerformanceSpec.iterations") + val iterations = nativeSystem.settings.config.getInt("akka.typed.PerformanceSpec.iterations") + + trait CommonTests { + implicit def system: ActorSystem[TypedSpec.Command] def `01 when warming up`(): Unit = sync(runTest("01")(behavior(1, 1, iterations, "dispatcher-1"))) def `02 when using a single message on a single thread`(): Unit = sync(runTest("02")(behavior(1, 1, iterations, "dispatcher-1"))) @@ -65,8 +72,10 @@ class PerformanceSpec extends TypedSpec( def `07 when using 4 pairs with 10 messages`(): Unit = sync(runTest("07")(behavior(4, 10, iterations, "dispatcher-8"))) def `08 when using 8 pairs with a single message`(): Unit = sync(runTest("08")(behavior(8, 1, iterations, "dispatcher-8"))) def `09 when using 8 pairs with 10 messages`(): Unit = sync(runTest("09")(behavior(8, 10, iterations, "dispatcher-8"))) - } + + object `must be fast with native ActorSystem` extends CommonTests with NativeSystem + object `must be fast with ActorSystemAdapter` extends CommonTests with AdaptedSystem } } diff --git a/akka-typed/src/test/scala/akka/typed/StepWise.scala b/akka-typed/src/test/scala/akka/typed/StepWise.scala index 7c614072f0..437b2d2994 100644 --- a/akka-typed/src/test/scala/akka/typed/StepWise.scala +++ b/akka-typed/src/test/scala/akka/typed/StepWise.scala @@ -42,8 +42,10 @@ object StepWise { private final case class ThunkV(f: Any ⇒ Any) extends AST private final case class Message(timeout: FiniteDuration, f: (Any, Any) ⇒ Any, trace: Trace) extends AST private final case class MultiMessage(timeout: FiniteDuration, count: Int, f: (Seq[Any], Any) ⇒ Any, trace: Trace) extends AST - private final case class Failure(timeout: FiniteDuration, f: (Failed, Any) ⇒ (Failed.Decision, Any), trace: Trace) extends AST private final case class Termination(timeout: FiniteDuration, f: (Terminated, Any) ⇒ Any, trace: Trace) extends AST + private final case class Multi(timeout: FiniteDuration, count: Int, f: (Seq[Either[Signal, Any]], Any) ⇒ Any, trace: Trace) extends AST + + private case object ReceiveTimeout private sealed trait Trace { def getStackTrace: Array[StackTraceElement] @@ -78,24 +80,24 @@ object StepWise { def expectMultipleMessages[V](timeout: FiniteDuration, count: Int)(f: (Seq[T], U) ⇒ V): Steps[T, V] = copy(ops = MultiMessage(timeout, count, f.asInstanceOf[(Seq[Any], Any) ⇒ Any], getTrace()) :: ops) - def expectFailure[V](timeout: FiniteDuration)(f: (Failed, U) ⇒ (Failed.Decision, V)): Steps[T, V] = - copy(ops = Failure(timeout, f.asInstanceOf[(Failed, Any) ⇒ (Failed.Decision, Any)], getTrace()) :: ops) - def expectTermination[V](timeout: FiniteDuration)(f: (Terminated, U) ⇒ V): Steps[T, V] = copy(ops = Termination(timeout, f.asInstanceOf[(Terminated, Any) ⇒ Any], getTrace()) :: ops) + def expectMulti[V](timeout: FiniteDuration, count: Int)(f: (Seq[Either[Signal, T]], U) ⇒ V): Steps[T, V] = + copy(ops = Multi(timeout, count, f.asInstanceOf[(Seq[Either[Signal, Any]], Any) ⇒ Any], getTrace()) :: ops) + def expectMessageKeep(timeout: FiniteDuration)(f: (T, U) ⇒ Unit): Steps[T, U] = copy(ops = Message(timeout, (msg, value) ⇒ { f.asInstanceOf[(Any, Any) ⇒ Any](msg, value); value }, getTrace()) :: ops) def expectMultipleMessagesKeep(timeout: FiniteDuration, count: Int)(f: (Seq[T], U) ⇒ Unit): Steps[T, U] = copy(ops = MultiMessage(timeout, count, (msgs, value) ⇒ { f.asInstanceOf[(Seq[Any], Any) ⇒ Any](msgs, value); value }, getTrace()) :: ops) - def expectFailureKeep(timeout: FiniteDuration)(f: (Failed, U) ⇒ Failed.Decision): Steps[T, U] = - copy(ops = Failure(timeout, (failed, value) ⇒ f.asInstanceOf[(Failed, Any) ⇒ Failed.Decision](failed, value) → value, getTrace()) :: ops) - def expectTerminationKeep(timeout: FiniteDuration)(f: (Terminated, U) ⇒ Unit): Steps[T, U] = copy(ops = Termination(timeout, (t, value) ⇒ { f.asInstanceOf[(Terminated, Any) ⇒ Any](t, value); value }, getTrace()) :: ops) + def expectMultiKeep(timeout: FiniteDuration, count: Int)(f: (Seq[Either[Signal, T]], U) ⇒ Unit): Steps[T, U] = + copy(ops = Multi(timeout, count, (msgs, value) ⇒ { f.asInstanceOf[(Seq[Either[Signal, Any]], Any) ⇒ Any](msgs, value); value }, getTrace()) :: ops) + def withKeepTraces(b: Boolean): Steps[T, U] = copy(keepTraces = b) } @@ -105,9 +107,9 @@ object StepWise { } def apply[T](f: (ActorContext[T], StartWith[T]) ⇒ Steps[T, _]): Behavior[T] = - Full { - case Sig(ctx, PreStart) ⇒ run(ctx, f(ctx, new StartWith(keepTraces = false)).ops.reverse, ()) - } + Full[Any] { + case Sig(ctx, PreStart) ⇒ run(ctx, f(ctx.asInstanceOf[ActorContext[T]], new StartWith(keepTraces = false)).ops.reverse, ()) + }.narrow private def throwTimeout(trace: Trace, message: String): Nothing = throw new TimeoutException(message) { @@ -125,48 +127,66 @@ object StepWise { } } - private def run[T](ctx: ActorContext[T], ops: List[AST], value: Any): Behavior[T] = + private def run[T](ctx: ActorContext[Any], ops: List[AST], value: Any): Behavior[Any] = ops match { case Thunk(f) :: tail ⇒ run(ctx, tail, f()) case ThunkV(f) :: tail ⇒ run(ctx, tail, f(value)) case Message(t, f, trace) :: tail ⇒ - ctx.setReceiveTimeout(t) + ctx.setReceiveTimeout(t, ReceiveTimeout) Full { - case Sig(_, ReceiveTimeout) ⇒ throwTimeout(trace, s"timeout of $t expired while waiting for a message") - case Msg(_, msg) ⇒ run(ctx, tail, f(msg, value)) - case Sig(_, other) ⇒ throwIllegalState(trace, s"unexpected $other while waiting for a message") + case Msg(_, ReceiveTimeout) ⇒ throwTimeout(trace, s"timeout of $t expired while waiting for a message") + case Msg(_, msg) ⇒ + ctx.cancelReceiveTimeout() + run(ctx, tail, f(msg, value)) + case Sig(_, other) ⇒ throwIllegalState(trace, s"unexpected $other while waiting for a message") } case MultiMessage(t, c, f, trace) :: tail ⇒ val deadline = Deadline.now + t - def behavior(count: Int, acc: List[Any]): Behavior[T] = { - ctx.setReceiveTimeout(deadline.timeLeft) + def behavior(count: Int, acc: List[Any]): Behavior[Any] = { + ctx.setReceiveTimeout(deadline.timeLeft, ReceiveTimeout) Full { - case Sig(_, ReceiveTimeout) ⇒ throwTimeout(trace, s"timeout of $t expired while waiting for $c messages (got only $count)") + case Msg(_, ReceiveTimeout) ⇒ + throwTimeout(trace, s"timeout of $t expired while waiting for $c messages (got only $count)") case Msg(_, msg) ⇒ val nextCount = count + 1 if (nextCount == c) { + ctx.cancelReceiveTimeout() run(ctx, tail, f((msg :: acc).reverse, value)) } else behavior(nextCount, msg :: acc) case Sig(_, other) ⇒ throwIllegalState(trace, s"unexpected $other while waiting for $c messages (got $count valid ones)") } } behavior(0, Nil) - case Failure(t, f, trace) :: tail ⇒ - ctx.setReceiveTimeout(t) - Full { - case Sig(_, ReceiveTimeout) ⇒ throwTimeout(trace, s"timeout of $t expired while waiting for a failure") - case Sig(_, failure: Failed) ⇒ - val (response, v) = f(failure, value) - failure.decide(response) - run(ctx, tail, v) - case other ⇒ throwIllegalState(trace, s"unexpected $other while waiting for a message") + case Multi(t, c, f, trace) :: tail ⇒ + val deadline = Deadline.now + t + def behavior(count: Int, acc: List[Either[Signal, Any]]): Behavior[Any] = { + ctx.setReceiveTimeout(deadline.timeLeft, ReceiveTimeout) + Full { + case Msg(_, ReceiveTimeout) ⇒ + throwTimeout(trace, s"timeout of $t expired while waiting for $c messages (got only $count)") + case Msg(_, msg) ⇒ + val nextCount = count + 1 + if (nextCount == c) { + ctx.cancelReceiveTimeout() + run(ctx, tail, f((Right(msg) :: acc).reverse, value)) + } else behavior(nextCount, Right(msg) :: acc) + case Sig(_, other) ⇒ + val nextCount = count + 1 + if (nextCount == c) { + ctx.cancelReceiveTimeout() + run(ctx, tail, f((Left(other) :: acc).reverse, value)) + } else behavior(nextCount, Left(other) :: acc) + } } + behavior(0, Nil) case Termination(t, f, trace) :: tail ⇒ - ctx.setReceiveTimeout(t) + ctx.setReceiveTimeout(t, ReceiveTimeout) Full { - case Sig(_, ReceiveTimeout) ⇒ throwTimeout(trace, s"timeout of $t expired while waiting for termination") - case Sig(_, t: Terminated) ⇒ run(ctx, tail, f(t, value)) - case other ⇒ throwIllegalState(trace, s"unexpected $other while waiting for termination") + case Msg(_, ReceiveTimeout) ⇒ throwTimeout(trace, s"timeout of $t expired while waiting for termination") + case Sig(_, t: Terminated) ⇒ + ctx.cancelReceiveTimeout() + run(ctx, tail, f(t, value)) + case other ⇒ throwIllegalState(trace, s"unexpected $other while waiting for termination") } case Nil ⇒ Stopped } diff --git a/akka-typed/src/test/scala/akka/typed/TypedSpec.scala b/akka-typed/src/test/scala/akka/typed/TypedSpec.scala index 5e38e24f54..25b9aee260 100644 --- a/akka-typed/src/test/scala/akka/typed/TypedSpec.scala +++ b/akka-typed/src/test/scala/akka/typed/TypedSpec.scala @@ -21,49 +21,79 @@ import akka.testkit.TestEvent.Mute import org.scalatest.concurrent.ScalaFutures import org.scalactic.ConversionCheckedTripleEquals import org.scalactic.Constraint +import org.junit.runner.RunWith +import scala.util.control.NonFatal +import org.scalatest.exceptions.TestFailedException /** * Helper class for writing tests for typed Actors with ScalaTest. */ -abstract class TypedSpec(config: Config) extends Spec with Matchers with BeforeAndAfterAll with ScalaFutures with ConversionCheckedTripleEquals { +@RunWith(classOf[org.scalatest.junit.JUnitRunner]) +class TypedSpecSetup extends Spec with Matchers with BeforeAndAfterAll with ScalaFutures with ConversionCheckedTripleEquals + +/** + * Helper class for writing tests against both ActorSystemImpl and ActorSystemAdapter. + */ +class TypedSpec(val config: Config) extends TypedSpecSetup { import TypedSpec._ import AskPattern._ def this() = this(ConfigFactory.empty) - implicit val system = ActorSystem(TypedSpec.getCallerName(classOf[TypedSpec]), Props(guardian()), Some(config withFallback AkkaSpec.testConf)) + // extension point + def setTimeout: Timeout = Timeout(1.minute) - implicit val timeout = Timeout(1.minute) + val nativeSystem = ActorSystem(AkkaSpec.getCallerName(classOf[TypedSpec]), Props(guardian()), Some(config withFallback AkkaSpec.testConf)) + val adaptedSystem = ActorSystem.adapter(AkkaSpec.getCallerName(classOf[TypedSpec]), Props(guardian()), Some(config withFallback AkkaSpec.testConf)) + + trait NativeSystem { + def system = nativeSystem + } + trait AdaptedSystem { + def system = adaptedSystem + } + + implicit val timeout = setTimeout implicit val patience = PatienceConfig(3.seconds) + implicit val scheduler = nativeSystem.scheduler override def afterAll(): Unit = { - Await.result(system ? (Terminate(_)), timeout.duration): Status + Await.result(nativeSystem ? (Terminate(_)), timeout.duration): Status + Await.result(adaptedSystem ? (Terminate(_)), timeout.duration): Status } // TODO remove after basing on ScalaTest 3 with async support import akka.testkit._ - def await[T](f: Future[T]): T = Await.result(f, 60.seconds.dilated(system.untyped)) + def await[T](f: Future[T]): T = Await.result(f, timeout.duration * 1.1) - val blackhole = await(system ? Create(Props(ScalaDSL.Full[Any] { case _ ⇒ ScalaDSL.Same }), "blackhole")) + val blackhole = await(nativeSystem ? Create(Props(ScalaDSL.Full[Any] { case _ ⇒ ScalaDSL.Same }), "blackhole")) /** * Run an Actor-based test. The test procedure is most conveniently * formulated using the [[StepWise$]] behavior type. */ - def runTest[T: ClassTag](name: String)(behavior: Behavior[T]): Future[Status] = + def runTest[T: ClassTag](name: String)(behavior: Behavior[T])(implicit system: ActorSystem[Command]): Future[Status] = system ? (RunTest(name, Props(behavior), _, timeout.duration)) // TODO remove after basing on ScalaTest 3 with async support - def sync(f: Future[Status]): Unit = { + def sync(f: Future[Status])(implicit system: ActorSystem[Command]): Unit = { def unwrap(ex: Throwable): Throwable = ex match { case ActorInitializationException(_, _, ex) ⇒ ex case other ⇒ other } - await(f) match { - case Success ⇒ () - case Failed(ex) ⇒ throw unwrap(ex) - case Timedout ⇒ fail("test timed out") + try await(f) match { + case Success ⇒ () + case Failed(ex) ⇒ + println(system.printTree) + throw unwrap(ex) + case Timedout ⇒ + println(system.printTree) + fail("test timed out") + } catch { + case NonFatal(ex) ⇒ + println(system.printTree) + throw ex } } @@ -72,7 +102,7 @@ abstract class TypedSpec(config: Config) extends Spec with Matchers with BeforeA source: String = null, start: String = "", pattern: String = null, - occurrences: Int = Int.MaxValue): EventFilter = { + occurrences: Int = Int.MaxValue)(implicit system: ActorSystem[Command]): EventFilter = { val filter = EventFilter(message, source, start, pattern, occurrences) system.eventStream.publish(Mute(filter)) filter @@ -81,7 +111,7 @@ abstract class TypedSpec(config: Config) extends Spec with Matchers with BeforeA /** * Group assertion that ensures that the given inboxes are empty. */ - def assertEmpty(inboxes: Inbox.SyncInbox[_]*): Unit = { + def assertEmpty(inboxes: Inbox[_]*): Unit = { inboxes foreach (i ⇒ withClue(s"inbox $i had messages")(i.hasMessages should be(false))) } @@ -116,20 +146,11 @@ object TypedSpec { def guardian(outstanding: Map[ActorRef[_], ActorRef[Status]] = Map.empty): Behavior[Command] = FullTotal { - case Sig(ctx, f @ t.Failed(ex, test)) ⇒ + case Sig(ctx, t @ Terminated(test)) ⇒ outstanding get test match { case Some(reply) ⇒ - reply ! Failed(ex) - f.decide(t.Failed.Stop) - guardian(outstanding - test) - case None ⇒ - f.decide(t.Failed.Stop) - Same - } - case Sig(ctx, Terminated(test)) ⇒ - outstanding get test match { - case Some(reply) ⇒ - reply ! Success + if (t.failure eq null) reply ! Success + else reply ! Failed(t.failure) guardian(outstanding - test) case None ⇒ Same } @@ -157,3 +178,25 @@ object TypedSpec { reduced.head.replaceFirst(""".*\.""", "").replaceAll("[^a-zA-Z_0-9]", "_") } } + +class TypedSpecSpec extends TypedSpec { + object `A TypedSpec` { + + trait CommonTests { + implicit def system: ActorSystem[TypedSpec.Command] + + def `must report failures`(): Unit = { + a[TestFailedException] must be thrownBy { + sync(runTest("failure")(StepWise[String]((ctx, startWith) ⇒ + startWith { + fail("expected") + } + ))) + } + } + } + + object `when using the native implementation` extends CommonTests with NativeSystem + object `when using the adapted implementation` extends CommonTests with AdaptedSystem + } +} diff --git a/akka-typed/src/test/scala/akka/typed/internal/ActorCellSpec.scala b/akka-typed/src/test/scala/akka/typed/internal/ActorCellSpec.scala new file mode 100644 index 0000000000..ce891182e5 --- /dev/null +++ b/akka-typed/src/test/scala/akka/typed/internal/ActorCellSpec.scala @@ -0,0 +1,443 @@ +/** + * Copyright (C) 2016 Lightbend Inc. + */ +package akka.typed +package internal + +import org.scalactic.ConversionCheckedTripleEquals +import org.scalatest.concurrent.ScalaFutures +import org.scalatest.exceptions.TestFailedException +import org.scalatest._ +import org.junit.runner.RunWith + +@RunWith(classOf[org.scalatest.junit.JUnitRunner]) +class ActorCellSpec extends Spec with Matchers with BeforeAndAfterAll with ScalaFutures with ConversionCheckedTripleEquals { + + import ScalaDSL._ + + val sys = new ActorSystemStub("ActorCellSpec") + def ec = sys.controlledExecutor + + object `An ActorCell` { + + def `must be creatable`(): Unit = { + val parent = new DebugRef[String](sys.path / "creatable", true) + val cell = new ActorCell(sys, Props({ parent ! "created"; Static[String] { s ⇒ parent ! s } }), parent) + debugCell(cell) { + ec.queueSize should ===(0) + cell.sendSystem(Create()) + ec.runOne() + ec.queueSize should ===(0) + parent.receiveAll() should ===(Right("created") :: Nil) + cell.send("hello") + ec.runOne() + ec.queueSize should ===(0) + parent.receiveAll() should ===(Right("hello") :: Nil) + } + } + + def `must be creatable with ???`(): Unit = { + val parent = new DebugRef[String](sys.path / "creatable???", true) + val self = new DebugRef[String](sys.path / "creatableSelf", true) + val ??? = new NotImplementedError + val cell = new ActorCell(sys, Props[String]({ parent ! "created"; throw ??? }), parent) + cell.setSelf(self) + debugCell(cell) { + ec.queueSize should ===(0) + cell.sendSystem(Create()) + ec.runOne() + ec.queueSize should ===(0) + parent.receiveAll() should ===(Right("created") :: Nil) + // explicitly verify termination via self-signal + self.receiveAll() should ===(Left(Terminate()) :: Nil) + cell.sendSystem(Terminate()) + ec.runOne() + ec.queueSize should ===(0) + parent.receiveAll() should ===(Left(DeathWatchNotification(self, ???)) :: Nil) + } + } + + def `must be able to terminate after construction`(): Unit = { + val parent = new DebugRef[String](sys.path / "terminate", true) + val self = new DebugRef[String](sys.path / "terminateSelf", true) + val cell = new ActorCell(sys, Props({ parent ! "created"; Stopped[String] }), parent) + cell.setSelf(self) + debugCell(cell) { + ec.queueSize should ===(0) + cell.sendSystem(Create()) + ec.runOne() + ec.queueSize should ===(0) + parent.receiveAll() should ===(Right("created") :: Nil) + // explicitly verify termination via self-signal + self.receiveAll() should ===(Left(Terminate()) :: Nil) + cell.sendSystem(Terminate()) + ec.runOne() + ec.queueSize should ===(0) + parent.receiveAll() should ===(Left(DeathWatchNotification(self, null)) :: Nil) + } + } + + def `must be able to terminate after PreStart`(): Unit = { + val parent = new DebugRef[String](sys.path / "terminate", true) + val self = new DebugRef[String](sys.path / "terminateSelf", true) + val cell = new ActorCell(sys, Props({ parent ! "created"; Full[String] { case Sig(ctx, PreStart) ⇒ Stopped } }), parent) + cell.setSelf(self) + debugCell(cell) { + ec.queueSize should ===(0) + cell.sendSystem(Create()) + ec.runOne() + ec.queueSize should ===(0) + parent.receiveAll() should ===(Right("created") :: Nil) + // explicitly verify termination via self-signal + self.receiveAll() should ===(Left(Terminate()) :: Nil) + cell.sendSystem(Terminate()) + ec.runOne() + ec.queueSize should ===(0) + parent.receiveAll() should ===(Left(DeathWatchNotification(self, null)) :: Nil) + } + } + + def `must terminate upon failure during processing`(): Unit = { + val parent = new DebugRef[String](sys.path / "terminate", true) + val self = new DebugRef[String](sys.path / "terminateSelf", true) + val ex = new AssertionError + val cell = new ActorCell(sys, Props({ parent ! "created"; Static[String](s ⇒ throw ex) }), parent) + cell.setSelf(self) + debugCell(cell) { + ec.queueSize should ===(0) + cell.sendSystem(Create()) + ec.runOne() + ec.queueSize should ===(0) + parent.receiveAll() should ===(Right("created") :: Nil) + cell.send("") + ec.runOne() + ec.queueSize should ===(0) + // explicitly verify termination via self-signal + self.receiveAll() should ===(Left(Terminate()) :: Nil) + cell.sendSystem(Terminate()) + ec.runOne() + ec.queueSize should ===(0) + parent.receiveAll() should ===(Left(DeathWatchNotification(self, ex)) :: Nil) + } + } + + def `must signal failure when starting behavior is "same"`(): Unit = { + val parent = new DebugRef[String](sys.path / "startSame", true) + val self = new DebugRef[String](sys.path / "startSameSelf", true) + val cell = new ActorCell(sys, Props({ parent ! "created"; Same[String] }), parent) + cell.setSelf(self) + debugCell(cell) { + ec.queueSize should ===(0) + cell.sendSystem(Create()) + ec.runOne() + ec.queueSize should ===(0) + parent.receiveAll() should ===(Right("created") :: Nil) + // explicitly verify termination via self-signal + self.receiveAll() should ===(Left(Terminate()) :: Nil) + cell.sendSystem(Terminate()) + ec.runOne() + ec.queueSize should ===(0) + parent.receiveAll() match { + case Left(DeathWatchNotification(`self`, exc)) :: Nil ⇒ + exc should not be null + exc shouldBe an[IllegalStateException] + exc.getMessage should include("same") + case other ⇒ fail(s"$other was not a DeathWatchNotification") + } + } + } + + def `must signal failure when starting behavior is "unhandled"`(): Unit = { + val parent = new DebugRef[String](sys.path / "startSame", true) + val self = new DebugRef[String](sys.path / "startSameSelf", true) + val cell = new ActorCell(sys, Props({ parent ! "created"; Unhandled[String] }), parent) + cell.setSelf(self) + debugCell(cell) { + ec.queueSize should ===(0) + cell.sendSystem(Create()) + ec.runOne() + ec.queueSize should ===(0) + parent.receiveAll() should ===(Right("created") :: Nil) + // explicitly verify termination via self-signal + self.receiveAll() should ===(Left(Terminate()) :: Nil) + cell.sendSystem(Terminate()) + ec.runOne() + ec.queueSize should ===(0) + parent.receiveAll() match { + case Left(DeathWatchNotification(`self`, exc)) :: Nil ⇒ + exc should not be null + exc shouldBe an[IllegalStateException] + exc.getMessage should include("same") + case other ⇒ fail(s"$other was not a DeathWatchNotification") + } + } + } + + /* + * also tests: + * - must reschedule for self-message + * - must not reschedule for message when already activated + * - must not reschedule for signal when already activated + */ + def `must not execute more messages than were batched naturally`(): Unit = { + val parent = new DebugRef[String](sys.path / "batching", true) + val cell = new ActorCell(sys, Props(SelfAware[String] { self ⇒ Static { s ⇒ self ! s; parent ! s } }), parent) + val ref = new LocalActorRef(parent.path / "child", cell) + cell.setSelf(ref) + debugCell(cell) { + ec.queueSize should ===(0) + cell.sendSystem(Create()) + ec.runOne() + ec.queueSize should ===(0) + parent.receiveAll() should ===(Nil) + cell.send("one") + cell.send("two") + ec.queueSize should ===(1) + ec.runOne() + ec.queueSize should ===(1) + parent.receiveAll() should ===(Right("one") :: Right("two") :: Nil) + ec.runOne() + ec.queueSize should ===(1) + parent.receiveAll() should ===(Right("one") :: Right("two") :: Nil) + cell.send("three") + ec.runOne() + ec.queueSize should ===(1) + parent.receiveAll() should ===(Right("one") :: Right("two") :: Right("three") :: Nil) + cell.sendSystem(Terminate()) + ec.queueSize should ===(1) + ec.runOne() + ec.queueSize should ===(0) + parent.receiveAll() should ===(Left(DeathWatchNotification(ref, null)) :: Nil) + } + } + + def `must signal DeathWatch when terminating normally`(): Unit = { + val parent = new DebugRef[String](sys.path / "watchNormal", true) + val client = new DebugRef[String](parent.path / "client", true) + val cell = new ActorCell(sys, Props(Empty[String]), parent) + val ref = new LocalActorRef(parent.path / "child", cell) + cell.setSelf(ref) + debugCell(cell) { + ec.queueSize should ===(0) + cell.sendSystem(Watch(ref, client)) + cell.sendSystem(Terminate()) + ec.runOne() + ec.queueSize should ===(0) + parent.receiveAll() should ===(Left(DeathWatchNotification(ref, null)) :: Nil) + client.receiveAll() should ===(Left(DeathWatchNotification(ref, null)) :: Nil) + } + } + + /* + * also tests: + * - must turn a DeathWatchNotification into a Terminated signal while watching + * - must terminate with DeathPactException when not handling a Terminated signal + * - must send a Watch message when watching another actor + */ + def `must signal DeathWatch when terminating abnormally`(): Unit = { + val parent = new DebugRef[String](sys.path / "watchAbnormal", true) + val client = new DebugRef[String](parent.path / "client", true) + val other = new DebugRef[String](parent.path / "other", true) + val cell = new ActorCell(sys, Props(ContextAware[String] { ctx ⇒ ctx.watch(parent); Empty }), parent) + val ref = new LocalActorRef(parent.path / "child", cell) + cell.setSelf(ref) + debugCell(cell) { + ec.queueSize should ===(0) + cell.sendSystem(Create()) + ec.runOne() + ec.queueSize should ===(0) + parent.receiveAll() should ===(Left(Watch(parent, ref)) :: Nil) + // test that unwatched termination is ignored + cell.sendSystem(DeathWatchNotification(other, null)) + ec.runOne() + ec.queueSize should ===(0) + parent.receiveAll() should ===(Nil) + // now trigger failure by death pact + cell.sendSystem(Watch(ref, client)) + cell.sendSystem(DeathWatchNotification(parent, null)) + ec.runOne() + ec.queueSize should ===(0) + parent.receiveAll() match { + case Left(DeathWatchNotification(ref, exc)) :: Nil ⇒ + exc should not be null + exc shouldBe a[DeathPactException] + case other ⇒ fail(s"$other was not a DeathWatchNotification") + } + client.receiveAll() should ===(Left(DeathWatchNotification(ref, null)) :: Nil) + } + } + + def `must signal DeathWatch when watching after termination`(): Unit = { + val parent = new DebugRef[String](sys.path / "watchLate", true) + val client = new DebugRef[String](parent.path / "client", true) + val cell = new ActorCell(sys, Props(Stopped[String]), parent) + val ref = new LocalActorRef(parent.path / "child", cell) + cell.setSelf(ref) + debugCell(cell) { + ec.queueSize should ===(0) + cell.sendSystem(Create()) + ec.runOne() + ec.queueSize should ===(0) + parent.receiveAll() should ===(Left(DeathWatchNotification(ref, null)) :: Nil) + cell.sendSystem(Watch(ref, client)) + ec.queueSize should ===(0) + sys.deadLettersInbox.receiveAll() should ===(Left(Watch(ref, client)) :: Nil) + // correct behavior of deadLetters is verified in ActorSystemSpec + } + } + + def `must signal DeathWatch when watching after termination but before deactivation`(): Unit = { + val parent = new DebugRef[String](sys.path / "watchSomewhatLate", true) + val client = new DebugRef[String](parent.path / "client", true) + val cell = new ActorCell(sys, Props(Empty[String]), parent) + val ref = new LocalActorRef(parent.path / "child", cell) + cell.setSelf(ref) + debugCell(cell) { + ec.queueSize should ===(0) + cell.sendSystem(Create()) + ec.runOne() + ec.queueSize should ===(0) + cell.sendSystem(Terminate()) + cell.sendSystem(Watch(ref, client)) + ec.runOne() + ec.queueSize should ===(0) + parent.receiveAll() should ===(Left(DeathWatchNotification(ref, null)) :: Nil) + sys.deadLettersInbox.receiveAll() should ===(Left(Watch(ref, client)) :: Nil) + } + } + + def `must not signal DeathWatch after Unwatch has been processed`(): Unit = { + val parent = new DebugRef[String](sys.path / "watchUnwatch", true) + val client = new DebugRef[String](parent.path / "client", true) + val cell = new ActorCell(sys, Props(Empty[String]), parent) + val ref = new LocalActorRef(parent.path / "child", cell) + cell.setSelf(ref) + debugCell(cell) { + ec.queueSize should ===(0) + cell.sendSystem(Watch(ref, client)) + cell.sendSystem(Unwatch(ref, client)) + cell.sendSystem(Terminate()) + ec.runOne() + ec.queueSize should ===(0) + parent.receiveAll() should ===(Left(DeathWatchNotification(ref, null)) :: Nil) + client.receiveAll() should ===(Nil) + } + } + + def `must send messages to deadLetters after being terminated`(): Unit = { + val parent = new DebugRef[String](sys.path / "sendDeadLetters", true) + val cell = new ActorCell(sys, Props(Stopped[String]), parent) + val ref = new LocalActorRef(parent.path / "child", cell) + cell.setSelf(ref) + debugCell(cell) { + ec.queueSize should ===(0) + cell.sendSystem(Create()) + ec.runOne() + ec.queueSize should ===(0) + parent.receiveAll() should ===(Left(DeathWatchNotification(ref, null)) :: Nil) + cell.send("42") + ec.queueSize should ===(0) + sys.deadLettersInbox.receiveAll() should ===(Right("42") :: Nil) + } + } + + /* + * also tests: + * - child creation + */ + def `must not terminate before children have terminated`(): Unit = { + val parent = new DebugRef[ActorRef[Nothing]](sys.path / "waitForChild", true) + val cell = new ActorCell(sys, Props(ContextAware[String] { ctx ⇒ + ctx.spawn(Props(SelfAware[String] { self ⇒ parent ! self; Empty }), "child") + Empty + }), parent) + val ref = new LocalActorRef(parent.path / "child", cell) + cell.setSelf(ref) + debugCell(cell) { + ec.queueSize should ===(0) + cell.sendSystem(Create()) + ec.runOne() // creating subject + parent.hasSomething should ===(false) + ec.runOne() // creating child + ec.queueSize should ===(0) + val child = parent.receiveAll() match { + case Right(child) :: Nil ⇒ + child.sorryForNothing.sendSystem(Watch(child, parent)) + child + case other ⇒ fail(s"$other was not List(Right())") + } + ec.runOne() + ec.queueSize should ===(0) + cell.sendSystem(Terminate()) + ec.runOne() // begin subject termination, will initiate child termination + parent.hasSomething should ===(false) + ec.runOne() // terminate child + parent.receiveAll() should ===(Left(DeathWatchNotification(child, null)) :: Nil) + ec.runOne() // terminate subject + parent.receiveAll() should ===(Left(DeathWatchNotification(ref, null)) :: Nil) + } + } + + def `must properly terminate if failing while handling Terminated for child actor`(): Unit = { + val parent = new DebugRef[ActorRef[Nothing]](sys.path / "terminateWhenDeathPact", true) + val cell = new ActorCell(sys, Props(ContextAware[String] { ctx ⇒ + ctx.watch(ctx.spawn(Props(SelfAware[String] { self ⇒ parent ! self; Empty }), "child")) + Empty + }), parent) + val ref = new LocalActorRef(parent.path / "child", cell) + cell.setSelf(ref) + debugCell(cell) { + ec.queueSize should ===(0) + cell.sendSystem(Create()) + ec.runOne() // creating subject + parent.hasSomething should ===(false) + ec.runOne() // creating child + ec.queueSize should ===(0) + val child = parent.receiveAll() match { + case Right(child: ActorRefImpl[Nothing]) :: Nil ⇒ + child.sendSystem(Watch(child, parent)) + child + case other ⇒ fail(s"$other was not List(Right())") + } + ec.runOne() + ec.queueSize should ===(0) + child.sendSystem(Terminate()) + ec.runOne() // child terminates and enqueues DeathWatchNotification + parent.receiveAll() should ===(Left(DeathWatchNotification(child, null)) :: Nil) + ec.runOne() // cell fails during Terminated and terminates with DeathPactException + parent.receiveAll() match { + case Left(DeathWatchNotification(`ref`, ex: DeathPactException)) :: Nil ⇒ + ex.getMessage should include("death pact") + case other ⇒ fail(s"$other was not Left(DeathWatchNotification($ref, DeathPactException))") + } + ec.queueSize should ===(0) + } + } + + def `must not terminate twice if failing in PostStop`(): Unit = { + val parent = new DebugRef[String](sys.path / "terminateProperlyPostStop", true) + val cell = new ActorCell(sys, Props(Full[String] { case Sig(_, PostStop) ⇒ ??? }), parent) + val ref = new LocalActorRef(parent.path / "child", cell) + cell.setSelf(ref) + debugCell(cell) { + ec.queueSize should ===(0) + cell.sendSystem(Create()) + ec.runOne() + ec.queueSize should ===(0) + cell.sendSystem(Terminate()) + ec.runOne() + ec.queueSize should ===(0) + parent.receiveAll() should ===(Left(DeathWatchNotification(ref, null)) :: Nil) + } + } + } + + private def debugCell[T, U](cell: ActorCell[T])(block: ⇒ U): U = + try block + catch { + case ex: TestFailedException ⇒ + println(cell) + throw ex + } + +} diff --git a/akka-typed/src/test/scala/akka/typed/internal/ActorSystemSpec.scala b/akka-typed/src/test/scala/akka/typed/internal/ActorSystemSpec.scala new file mode 100644 index 0000000000..79d46e4327 --- /dev/null +++ b/akka-typed/src/test/scala/akka/typed/internal/ActorSystemSpec.scala @@ -0,0 +1,112 @@ +/** + * Copyright (C) 2016 Lightbend Inc. + */ +package akka.typed +package internal + +import org.junit.runner.RunWith +import org.scalactic.ConversionCheckedTripleEquals +import org.scalatest._ +import org.scalatest.exceptions.TestFailedException +import org.scalatest.concurrent.ScalaFutures +import org.scalatest.concurrent.Eventually +import scala.concurrent.duration._ +import scala.concurrent.{ Future, Promise } +import scala.util.control.NonFatal + +@RunWith(classOf[org.scalatest.junit.JUnitRunner]) +class ActorSystemSpec extends Spec with Matchers with BeforeAndAfterAll with ScalaFutures with Eventually with ConversionCheckedTripleEquals { + import ScalaDSL._ + + override implicit val patienceConfig = PatienceConfig(1.second) + + case class Probe(msg: String, replyTo: ActorRef[String]) + + trait CommonTests { + def system[T](name: String, props: Props[T]): ActorSystem[T] + def suite: String + + def withSystem[T](name: String, props: Props[T], doTerminate: Boolean = true)(block: ActorSystem[T] ⇒ Unit): Terminated = { + val sys = system(s"$suite-$name", props) + try { + block(sys) + if (doTerminate) sys.terminate().futureValue else sys.whenTerminated.futureValue + } catch { + case NonFatal(ex) ⇒ + sys.terminate() + throw ex + } + } + + def `must start the guardian actor and terminate when it terminates`(): Unit = { + val t = withSystem("a", Props(Total[Probe] { p ⇒ p.replyTo ! p.msg; Stopped }), doTerminate = false) { sys ⇒ + val inbox = Inbox[String]("a") + sys ! Probe("hello", inbox.ref) + eventually { inbox.hasMessages should ===(true) } + inbox.receiveAll() should ===("hello" :: Nil) + } + val p = t.ref.path + p.name should ===("/") + p.address.system should ===(suite + "-a") + } + + def `must terminate the guardian actor`(): Unit = { + val inbox = Inbox[String]("terminate") + val sys = system("terminate", Props(Full[Probe] { + case Sig(ctx, PostStop) ⇒ + inbox.ref ! "done" + Same + })) + sys.terminate().futureValue + inbox.receiveAll() should ===("done" :: Nil) + } + + def `must log to the event stream`(): Unit = pending + + def `must have a name`(): Unit = + withSystem("name", Props(Empty[String])) { sys ⇒ + sys.name should ===(suite + "-name") + } + + def `must report its uptime`(): Unit = + withSystem("uptime", Props(Empty[String])) { sys ⇒ + sys.uptime should be < 1L + Thread.sleep(1000) + sys.uptime should be >= 1L + } + + def `must have a working thread factory`(): Unit = + withSystem("thread", Props(Empty[String])) { sys ⇒ + val p = Promise[Int] + sys.threadFactory.newThread(new Runnable { + def run(): Unit = p.success(42) + }).start() + p.future.futureValue should ===(42) + } + + def `must be able to run Futures`(): Unit = + withSystem("futures", Props(Empty[String])) { sys ⇒ + val f = Future(42)(sys.executionContext) + f.futureValue should ===(42) + } + + } + + object `An ActorSystemImpl` extends CommonTests { + def system[T](name: String, props: Props[T]): ActorSystem[T] = ActorSystem(name, props) + def suite = "native" + + // this is essential to complete ActorCellSpec, see there + def `must correctly treat Watch dead letters`(): Unit = + withSystem("deadletters", Props(Empty[String])) { sys ⇒ + val client = new DebugRef[Int](sys.path / "debug", true) + sys.deadLetters.sorry.sendSystem(Watch(sys, client)) + client.receiveAll() should ===(Left(DeathWatchNotification(sys, null)) :: Nil) + } + } + + object `An ActorSystemAdapter` extends CommonTests { + def system[T](name: String, props: Props[T]): ActorSystem[T] = ActorSystem.adapter(name, props) + def suite = "adapter" + } +} diff --git a/akka-typed/src/test/scala/akka/typed/internal/ActorSystemStub.scala b/akka-typed/src/test/scala/akka/typed/internal/ActorSystemStub.scala new file mode 100644 index 0000000000..6b309a7664 --- /dev/null +++ b/akka-typed/src/test/scala/akka/typed/internal/ActorSystemStub.scala @@ -0,0 +1,55 @@ +/** + * Copyright (C) 2016 Lightbend Inc. + */ +package akka.typed +package internal + +import akka.{ actor ⇒ a, event ⇒ e } +import scala.concurrent._ +import com.typesafe.config.ConfigFactory +import java.util.concurrent.ThreadFactory + +private[typed] class ActorSystemStub(val name: String) + extends ActorRef[Nothing](a.RootActorPath(a.Address("akka", name)) / "user") + with ActorSystem[Nothing] with ActorRefImpl[Nothing] { + + override val settings: a.ActorSystem.Settings = new a.ActorSystem.Settings(getClass.getClassLoader, ConfigFactory.empty, name) + + override def tell(msg: Nothing): Unit = throw new RuntimeException("must not send message to ActorSystemStub") + + override def isLocal: Boolean = true + override def sendSystem(signal: akka.typed.internal.SystemMessage): Unit = + throw new RuntimeException("must not send SYSTEM message to ActorSystemStub") + + val deadLettersInbox = new DebugRef[Any](path.parent / "deadLetters", true) + override def deadLetters[U]: akka.typed.ActorRef[U] = deadLettersInbox + + val controlledExecutor = new ControlledExecutor + implicit override def executionContext: scala.concurrent.ExecutionContextExecutor = controlledExecutor + override def dispatchers: akka.typed.Dispatchers = new Dispatchers { + def lookup(selector: DispatcherSelector): ExecutionContextExecutor = controlledExecutor + def shutdown(): Unit = () + } + + override def dynamicAccess: a.DynamicAccess = new a.ReflectiveDynamicAccess(getClass.getClassLoader) + override def eventStream: e.EventStream = new e.EventStream + override def logFilter: e.LoggingFilter = throw new UnsupportedOperationException("no log filter") + override def log: e.LoggingAdapter = new e.BusLogging(eventStream, path.parent.toString, getClass) + override def logConfiguration(): Unit = log.info(settings.toString) + + override def scheduler: a.Scheduler = throw new UnsupportedOperationException("no scheduler") + + private val terminationPromise = Promise[Terminated] + override def terminate(): Future[akka.typed.Terminated] = { + terminationPromise.trySuccess(Terminated(this)(null)) + terminationPromise.future + } + override def whenTerminated: Future[akka.typed.Terminated] = terminationPromise.future + override val startTime: Long = System.currentTimeMillis() + override def uptime: Long = System.currentTimeMillis() - startTime + override def threadFactory: java.util.concurrent.ThreadFactory = new ThreadFactory { + override def newThread(r: Runnable): Thread = new Thread(r) + } + + override def printTree: String = "no tree for ActorSystemStub" +} diff --git a/akka-typed/src/test/scala/akka/typed/internal/ControlledExecutor.scala b/akka-typed/src/test/scala/akka/typed/internal/ControlledExecutor.scala new file mode 100644 index 0000000000..6753c33309 --- /dev/null +++ b/akka-typed/src/test/scala/akka/typed/internal/ControlledExecutor.scala @@ -0,0 +1,25 @@ +/** + * Copyright (C) 2016 Lightbend Inc. + */ +package akka.typed.internal + +import scala.concurrent.ExecutionContextExecutor +import java.util.LinkedList + +class ControlledExecutor extends ExecutionContextExecutor { + private val tasks = new LinkedList[Runnable] + + def queueSize: Int = tasks.size() + + def runOne(): Unit = tasks.pop().run() + + def runAll(): Unit = while (!tasks.isEmpty()) runOne() + + def execute(task: Runnable): Unit = { + tasks.add(task) + } + + def reportFailure(cause: Throwable): Unit = { + cause.printStackTrace() + } +} diff --git a/akka-typed/src/test/scala/akka/typed/internal/DebugRef.scala b/akka-typed/src/test/scala/akka/typed/internal/DebugRef.scala new file mode 100644 index 0000000000..2d710e7103 --- /dev/null +++ b/akka-typed/src/test/scala/akka/typed/internal/DebugRef.scala @@ -0,0 +1,53 @@ +/** + * Copyright (C) 2016 Lightbend Inc. + */ +package akka.typed +package internal + +import akka.{ actor ⇒ a } +import java.util.concurrent.ConcurrentLinkedQueue +import scala.annotation.tailrec + +private[typed] class DebugRef[T](_path: a.ActorPath, override val isLocal: Boolean) + extends ActorRef[T](_path) with ActorRefImpl[T] { + + private val q = new ConcurrentLinkedQueue[Either[SystemMessage, T]] + + override def tell(msg: T): Unit = q.add(Right(msg)) + override def sendSystem(signal: SystemMessage): Unit = q.add(Left(signal)) + + def hasMessage: Boolean = q.peek match { + case null ⇒ false + case Left(_) ⇒ false + case Right(_) ⇒ true + } + + def hasSignal: Boolean = q.peek match { + case null ⇒ false + case Left(_) ⇒ true + case Right(_) ⇒ false + } + + def hasSomething: Boolean = q.peek != null + + def receiveMessage(): T = q.poll match { + case null ⇒ throw new NoSuchElementException("empty DebugRef") + case Left(signal) ⇒ throw new IllegalStateException(s"expected message but found signal $signal") + case Right(msg) ⇒ msg + } + + def receiveSignal(): SystemMessage = q.poll match { + case null ⇒ throw new NoSuchElementException("empty DebugRef") + case Left(signal) ⇒ signal + case Right(msg) ⇒ throw new IllegalStateException(s"expected signal but found message $msg") + } + + def receiveAll(): List[Either[SystemMessage, T]] = { + @tailrec def rec(acc: List[Either[SystemMessage, T]]): List[Either[SystemMessage, T]] = + q.poll match { + case null ⇒ acc.reverse + case other ⇒ rec(other :: acc) + } + rec(Nil) + } +} diff --git a/akka-typed/src/test/scala/akka/typed/internal/FunctionRefSpec.scala b/akka-typed/src/test/scala/akka/typed/internal/FunctionRefSpec.scala new file mode 100644 index 0000000000..6cc453154e --- /dev/null +++ b/akka-typed/src/test/scala/akka/typed/internal/FunctionRefSpec.scala @@ -0,0 +1,164 @@ +/** + * Copyright (C) 2016 Lightbend Inc. + */ +package akka.typed +package internal + +import scala.concurrent.{ Promise, Future } + +class FunctionRefSpec extends TypedSpecSetup { + + object `A FunctionRef` { + + def `must forward messages that are received after getting the ActorRef (completed later)`(): Unit = { + val p = Promise[ActorRef[String]] + val ref = ActorRef(p.future) + val target = new DebugRef[String](ref.path / "target", true) + p.success(target) + ref ! "42" + ref ! "43" + target.receiveAll() should ===(Left(Watch(target, ref)) :: Right("42") :: Right("43") :: Nil) + } + + def `must forward messages that are received after getting the ActorRef (already completed)`(): Unit = { + val target = new DebugRef[String](ActorRef.FuturePath / "target", true) + val f = Future.successful(target) + val ref = ActorRef(f) + ref ! "42" + ref ! "43" + target.receiveAll() should ===(Left(Watch(target, ref)) :: Right("42") :: Right("43") :: Nil) + } + + def `must forward messages that are received before getting the ActorRef`(): Unit = { + val p = Promise[ActorRef[String]] + val ref = ActorRef(p.future) + ref ! "42" + ref ! "43" + val target = new DebugRef[String](ref.path / "target", true) + p.success(target) + target.receiveAll() should ===(Right("42") :: Right("43") :: Left(Watch(target, ref)) :: Nil) + } + + def `must notify watchers when the future fails`(): Unit = { + val p = Promise[ActorRef[String]] + val ref = ActorRef(p.future) + val client1 = new DebugRef(ref.path / "c1", true) + + ref.sorry.sendSystem(Watch(ref, client1)) + client1.hasSomething should ===(false) + + p.failure(new Exception) + client1.receiveSignal() should ===(DeathWatchNotification(ref, null)) + client1.hasSomething should ===(false) + + val client2 = new DebugRef(ref.path / "c2", true) + + ref.sorry.sendSystem(Watch(ref, client2)) + client2.receiveSignal() should ===(DeathWatchNotification(ref, null)) + client2.hasSomething should ===(false) + client1.hasSomething should ===(false) + } + + def `must notify watchers when terminated`(): Unit = { + val p = Promise[ActorRef[String]] + val ref = ActorRef(p.future) + val client1 = new DebugRef(ref.path / "c1", true) + + ref.sorry.sendSystem(Watch(ref, client1)) + client1.hasSomething should ===(false) + + ref.sorry.sendSystem(Terminate()) + client1.receiveSignal() should ===(DeathWatchNotification(ref, null)) + client1.hasSomething should ===(false) + + val client2 = new DebugRef(ref.path / "c2", true) + + ref.sorry.sendSystem(Watch(ref, client2)) + client2.receiveSignal() should ===(DeathWatchNotification(ref, null)) + client2.hasSomething should ===(false) + client1.hasSomething should ===(false) + } + + def `must notify watchers when terminated after receiving the target`(): Unit = { + val p = Promise[ActorRef[String]] + val ref = ActorRef(p.future) + val client1 = new DebugRef(ref.path / "c1", true) + + ref.sorry.sendSystem(Watch(ref, client1)) + client1.hasSomething should ===(false) + + val target = new DebugRef[String](ref.path / "target", true) + p.success(target) + ref ! "42" + ref ! "43" + target.receiveAll() should ===(Left(Watch(target, ref)) :: Right("42") :: Right("43") :: Nil) + + ref.sorry.sendSystem(Terminate()) + client1.receiveSignal() should ===(DeathWatchNotification(ref, null)) + client1.hasSomething should ===(false) + target.receiveAll() should ===(Left(Unwatch(target, ref)) :: Nil) + + val client2 = new DebugRef(ref.path / "c2", true) + + ref.sorry.sendSystem(Watch(ref, client2)) + client2.receiveSignal() should ===(DeathWatchNotification(ref, null)) + client2.hasSomething should ===(false) + client1.hasSomething should ===(false) + } + + def `must notify watchers when receiving the target after terminating`(): Unit = { + val p = Promise[ActorRef[String]] + val ref = ActorRef(p.future) + val client1 = new DebugRef(ref.path / "c1", true) + + ref.sorry.sendSystem(Watch(ref, client1)) + client1.hasSomething should ===(false) + + ref.sorry.sendSystem(Terminate()) + client1.receiveSignal() should ===(DeathWatchNotification(ref, null)) + client1.hasSomething should ===(false) + + val target = new DebugRef[String](ref.path / "target", true) + p.success(target) + ref ! "42" + ref ! "43" + target.hasSomething should ===(false) + + val client2 = new DebugRef(ref.path / "c2", true) + + ref.sorry.sendSystem(Watch(ref, client2)) + client2.receiveSignal() should ===(DeathWatchNotification(ref, null)) + client2.hasSomething should ===(false) + client1.hasSomething should ===(false) + } + + def `must notify watchers when the target ActorRef terminates`(): Unit = { + val p = Promise[ActorRef[String]] + val ref = ActorRef(p.future) + val client1 = new DebugRef(ref.path / "c1", true) + + ref.sorry.sendSystem(Watch(ref, client1)) + client1.hasSomething should ===(false) + + val target = new DebugRef[String](ref.path / "target", true) + p.success(target) + ref ! "42" + ref ! "43" + target.receiveAll() should ===(Left(Watch(target, ref)) :: Right("42") :: Right("43") :: Nil) + + ref.sorry.sendSystem(DeathWatchNotification(target, null)) + client1.receiveSignal() should ===(DeathWatchNotification(ref, null)) + client1.hasSomething should ===(false) + target.hasSomething should ===(false) + + val client2 = new DebugRef(ref.path / "c2", true) + + ref.sorry.sendSystem(Watch(ref, client2)) + client2.receiveSignal() should ===(DeathWatchNotification(ref, null)) + client2.hasSomething should ===(false) + client1.hasSomething should ===(false) + } + + } + +} diff --git a/akka-typed/src/test/scala/akka/typed/patterns/ReceiverSpec.scala b/akka-typed/src/test/scala/akka/typed/patterns/ReceiverSpec.scala index 94900b692b..b7d7b4db4c 100644 --- a/akka-typed/src/test/scala/akka/typed/patterns/ReceiverSpec.scala +++ b/akka-typed/src/test/scala/akka/typed/patterns/ReceiverSpec.scala @@ -13,7 +13,7 @@ object ReceiverSpec { class ReceiverSpec extends TypedSpec { import ReceiverSpec._ - private val dummyInbox = Inbox.sync[Replies[Msg]]("dummy") + private val dummyInbox = Inbox[Replies[Msg]]("dummy") private val startingPoints: Seq[Setup] = Seq( Setup("initial", ctx ⇒ behavior[Msg], 0, 0), @@ -36,7 +36,7 @@ class ReceiverSpec extends TypedSpec { private def afterGetOneTimeout(ctx: ActorContext[Command[Msg]]): Behavior[Command[Msg]] = behavior[Msg] .message(ctx, GetOne(1.nano)(dummyInbox.ref)) - .management(ctx, ReceiveTimeout) + .asInstanceOf[Behavior[InternalCommand[Msg]]].message(ctx.asInstanceOf[ActorContext[InternalCommand[Msg]]], ReceiveTimeout()).asInstanceOf[Behavior[Command[Msg]]] private def afterGetAll(ctx: ActorContext[Command[Msg]]): Behavior[Command[Msg]] = behavior[Msg] @@ -50,13 +50,13 @@ class ReceiverSpec extends TypedSpec { .message(ctx, GetAll(Duration.Zero)(dummyInbox.ref)) private def setup(name: String, behv: Behavior[Command[Msg]] = behavior[Msg])( - proc: (EffectfulActorContext[Command[Msg]], EffectfulActorContext[Msg], Inbox.SyncInbox[Replies[Msg]]) ⇒ Unit): Unit = + proc: (EffectfulActorContext[Command[Msg]], EffectfulActorContext[Msg], Inbox[Replies[Msg]]) ⇒ Unit): Unit = for (Setup(description, behv, messages, effects) ← startingPoints) { - val ctx = new EffectfulActorContext("ctx", Props(ScalaDSL.ContextAware(behv)), system) + val ctx = new EffectfulActorContext("ctx", Props(ScalaDSL.ContextAware(behv)), nativeSystem) withClue(s"[running for starting point '$description' (${ctx.currentBehavior})]: ") { dummyInbox.receiveAll() should have size messages ctx.getAllEffects() should have size effects - proc(ctx, ctx.asInstanceOf[EffectfulActorContext[Msg]], Inbox.sync[Replies[Msg]](name)) + proc(ctx, ctx.asInstanceOf[EffectfulActorContext[Msg]], Inbox[Replies[Msg]](name)) } } @@ -68,7 +68,7 @@ class ReceiverSpec extends TypedSpec { */ def `must return "self" as external address`(): Unit = setup("") { (int, ext, _) ⇒ - val inbox = Inbox.sync[ActorRef[Msg]]("extAddr") + val inbox = Inbox[ActorRef[Msg]]("extAddr") int.run(ExternalAddress(inbox.ref)) int.hasEffects should be(false) inbox.receiveAll() should be(List(int.self)) @@ -97,13 +97,14 @@ class ReceiverSpec extends TypedSpec { setup("getOneLater") { (int, ext, inbox) ⇒ int.run(GetOne(1.second)(inbox.ref)) int.getAllEffects() match { - case ReceiveTimeoutSet(d) :: Nil ⇒ d > Duration.Zero should be(true) - case other ⇒ fail(s"$other was not List(ReceiveTimeoutSet(_))") + case ReceiveTimeoutSet(d, _) :: Nil ⇒ d > Duration.Zero should be(true) + case other ⇒ fail(s"$other was not List(ReceiveTimeoutSet(_))") } inbox.hasMessages should be(false) ext.run(Msg(1)) int.getAllEffects() match { - case ReceiveTimeoutSet(d) :: Nil ⇒ d should be theSameInstanceAs (Duration.Undefined) + case ReceiveTimeoutSet(d, _) :: Nil ⇒ d should be theSameInstanceAs (Duration.Undefined) + case other ⇒ fail(s"$other was not List(ReceiveTimeoutSet(_))") } inbox.receiveAll() should be(GetOneResult(int.self, Some(Msg(1))) :: Nil) } @@ -122,16 +123,16 @@ class ReceiverSpec extends TypedSpec { setup("getNoneTimeout") { (int, ext, inbox) ⇒ int.run(GetOne(1.nano)(inbox.ref)) int.getAllEffects() match { - case ReceiveTimeoutSet(d) :: Nil ⇒ // okay - case other ⇒ fail(s"$other was not List(ReceiveTimeoutSet(_))") + case ReceiveTimeoutSet(d, _) :: Nil ⇒ // okay + case other ⇒ fail(s"$other was not List(ReceiveTimeoutSet(_))") } inbox.hasMessages should be(false) // currently this all takes >1ns, but who knows what the future brings Thread.sleep(1) - int.signal(ReceiveTimeout) + int.asInstanceOf[EffectfulActorContext[InternalCommand[Msg]]].run(ReceiveTimeout()) int.getAllEffects() match { - case ReceiveTimeoutSet(d) :: Nil ⇒ d should be theSameInstanceAs (Duration.Undefined) - case other ⇒ fail(s"$other was not List(ReceiveTimeoutSet(_))") + case ReceiveTimeoutSet(d, _) :: Nil ⇒ d should be theSameInstanceAs (Duration.Undefined) + case other ⇒ fail(s"$other was not List(ReceiveTimeoutSet(_))") } inbox.receiveAll() should be(GetOneResult(int.self, None) :: Nil) } @@ -218,8 +219,8 @@ class ReceiverSpec extends TypedSpec { setup("getAllWhileGetOne") { (int, ext, inbox) ⇒ int.run(GetOne(1.second)(inbox.ref)) int.getAllEffects() match { - case ReceiveTimeoutSet(d) :: Nil ⇒ // okay - case other ⇒ fail(s"$other was not List(ReceiveTimeoutSet(_))") + case ReceiveTimeoutSet(d, _) :: Nil ⇒ // okay + case other ⇒ fail(s"$other was not List(ReceiveTimeoutSet(_))") } inbox.hasMessages should be(false) int.run(GetAll(Duration.Zero)(inbox.ref)) @@ -231,13 +232,13 @@ class ReceiverSpec extends TypedSpec { setup("getAllWhileGetOne") { (int, ext, inbox) ⇒ int.run(GetOne(1.second)(inbox.ref)) int.getAllEffects() match { - case ReceiveTimeoutSet(d) :: Nil ⇒ // okay - case other ⇒ fail(s"$other was not List(ReceiveTimeoutSet(_))") + case ReceiveTimeoutSet(d, _) :: Nil ⇒ // okay + case other ⇒ fail(s"$other was not List(ReceiveTimeoutSet(_))") } inbox.hasMessages should be(false) int.run(GetAll(1.nano)(inbox.ref)) val msg = int.getAllEffects() match { - case (s: Scheduled[_]) :: ReceiveTimeoutSet(_) :: Nil ⇒ assertScheduled(s, int.self) + case (s: Scheduled[_]) :: ReceiveTimeoutSet(_, _) :: Nil ⇒ assertScheduled(s, int.self) } inbox.receiveAll() should be(Nil) int.run(msg) diff --git a/akka-typed/src/test/scala/akka/typed/patterns/ReceptionistSpec.scala b/akka-typed/src/test/scala/akka/typed/patterns/ReceptionistSpec.scala index ce74796433..8940722f33 100644 --- a/akka-typed/src/test/scala/akka/typed/patterns/ReceptionistSpec.scala +++ b/akka-typed/src/test/scala/akka/typed/patterns/ReceptionistSpec.scala @@ -19,16 +19,17 @@ class ReceptionistSpec extends TypedSpec { case object ServiceKeyB extends ServiceKey[ServiceB] val propsB = Props(Static[ServiceB](msg ⇒ ())) - object `A Receptionist` { + trait CommonTests { + implicit def system: ActorSystem[TypedSpec.Command] def `must register a service`(): Unit = { val ctx = new EffectfulActorContext("register", Props(behavior), system) - val a = Inbox.sync[ServiceA]("a") - val r = Inbox.sync[Registered[_]]("r") + val a = Inbox[ServiceA]("a") + val r = Inbox[Registered[_]]("r") ctx.run(Register(ServiceKeyA, a.ref)(r.ref)) ctx.getAllEffects() should be(Effect.Watched(a.ref) :: Nil) r.receiveMsg() should be(Registered(ServiceKeyA, a.ref)) - val q = Inbox.sync[Listing[ServiceA]]("q") + val q = Inbox[Listing[ServiceA]]("q") ctx.run(Find(ServiceKeyA)(q.ref)) ctx.getAllEffects() should be(Nil) q.receiveMsg() should be(Listing(ServiceKeyA, Set(a.ref))) @@ -37,14 +38,14 @@ class ReceptionistSpec extends TypedSpec { def `must register two services`(): Unit = { val ctx = new EffectfulActorContext("registertwo", Props(behavior), system) - val a = Inbox.sync[ServiceA]("a") - val r = Inbox.sync[Registered[_]]("r") + val a = Inbox[ServiceA]("a") + val r = Inbox[Registered[_]]("r") ctx.run(Register(ServiceKeyA, a.ref)(r.ref)) r.receiveMsg() should be(Registered(ServiceKeyA, a.ref)) - val b = Inbox.sync[ServiceB]("b") + val b = Inbox[ServiceB]("b") ctx.run(Register(ServiceKeyB, b.ref)(r.ref)) r.receiveMsg() should be(Registered(ServiceKeyB, b.ref)) - val q = Inbox.sync[Listing[_]]("q") + val q = Inbox[Listing[_]]("q") ctx.run(Find(ServiceKeyA)(q.ref)) q.receiveMsg() should be(Listing(ServiceKeyA, Set(a.ref))) ctx.run(Find(ServiceKeyB)(q.ref)) @@ -54,14 +55,14 @@ class ReceptionistSpec extends TypedSpec { def `must register two services with the same key`(): Unit = { val ctx = new EffectfulActorContext("registertwosame", Props(behavior), system) - val a1 = Inbox.sync[ServiceA]("a1") - val r = Inbox.sync[Registered[_]]("r") + val a1 = Inbox[ServiceA]("a1") + val r = Inbox[Registered[_]]("r") ctx.run(Register(ServiceKeyA, a1.ref)(r.ref)) r.receiveMsg() should be(Registered(ServiceKeyA, a1.ref)) - val a2 = Inbox.sync[ServiceA]("a2") + val a2 = Inbox[ServiceA]("a2") ctx.run(Register(ServiceKeyA, a2.ref)(r.ref)) r.receiveMsg() should be(Registered(ServiceKeyA, a2.ref)) - val q = Inbox.sync[Listing[_]]("q") + val q = Inbox[Listing[_]]("q") ctx.run(Find(ServiceKeyA)(q.ref)) q.receiveMsg() should be(Listing(ServiceKeyA, Set(a1.ref, a2.ref))) ctx.run(Find(ServiceKeyB)(q.ref)) @@ -71,31 +72,31 @@ class ReceptionistSpec extends TypedSpec { def `must unregister services when they terminate`(): Unit = { val ctx = new EffectfulActorContext("registertwosame", Props(behavior), system) - val r = Inbox.sync[Registered[_]]("r") - val a = Inbox.sync[ServiceA]("a") + val r = Inbox[Registered[_]]("r") + val a = Inbox[ServiceA]("a") ctx.run(Register(ServiceKeyA, a.ref)(r.ref)) ctx.getEffect() should be(Effect.Watched(a.ref)) r.receiveMsg() should be(Registered(ServiceKeyA, a.ref)) - val b = Inbox.sync[ServiceB]("b") + val b = Inbox[ServiceB]("b") ctx.run(Register(ServiceKeyB, b.ref)(r.ref)) ctx.getEffect() should be(Effect.Watched(b.ref)) r.receiveMsg() should be(Registered(ServiceKeyB, b.ref)) - val c = Inbox.sync[Any]("c") + val c = Inbox[Any]("c") ctx.run(Register(ServiceKeyA, c.ref)(r.ref)) ctx.run(Register(ServiceKeyB, c.ref)(r.ref)) ctx.getAllEffects() should be(Seq(Effect.Watched(c.ref), Effect.Watched(c.ref))) r.receiveMsg() should be(Registered(ServiceKeyA, c.ref)) r.receiveMsg() should be(Registered(ServiceKeyB, c.ref)) - val q = Inbox.sync[Listing[_]]("q") + val q = Inbox[Listing[_]]("q") ctx.run(Find(ServiceKeyA)(q.ref)) q.receiveMsg() should be(Listing(ServiceKeyA, Set(a.ref, c.ref))) ctx.run(Find(ServiceKeyB)(q.ref)) q.receiveMsg() should be(Listing(ServiceKeyB, Set(b.ref, c.ref))) - ctx.signal(Terminated(c.ref)) + ctx.signal(Terminated(c.ref)(null)) ctx.run(Find(ServiceKeyA)(q.ref)) q.receiveMsg() should be(Listing(ServiceKeyA, Set(a.ref))) ctx.run(Find(ServiceKeyB)(q.ref)) @@ -106,7 +107,6 @@ class ReceptionistSpec extends TypedSpec { def `must work with ask`(): Unit = sync(runTest("Receptionist") { StepWise[Registered[ServiceA]] { (ctx, startWith) ⇒ val self = ctx.self - import system.executionContext startWith.withKeepTraces(true) { val r = ctx.spawnAnonymous(Props(behavior)) val s = ctx.spawnAnonymous(propsA) @@ -116,7 +116,7 @@ class ReceptionistSpec extends TypedSpec { }.expectMessage(1.second) { case (msg, (f, s)) ⇒ msg should be(Registered(ServiceKeyA, s)) - f foreach (self ! _) + f.foreach(self ! _)(system.executionContext) s }.expectMessage(1.second) { case (msg, s) ⇒ @@ -127,4 +127,7 @@ class ReceptionistSpec extends TypedSpec { } + object `A Receptionist (native)` extends CommonTests with NativeSystem + object `A Receptionist (adapted)` extends CommonTests with AdaptedSystem + } diff --git a/project/MiMa.scala b/project/MiMa.scala index db82d79c97..4b66bcd0df 100644 --- a/project/MiMa.scala +++ b/project/MiMa.scala @@ -962,7 +962,10 @@ object MiMa extends AutoPlugin { ProblemFilters.exclude[ReversedMissingMethodProblem]("akka.http.scaladsl.model.ws.TextMessage.asScala"), ProblemFilters.exclude[ReversedMissingMethodProblem]("akka.http.scaladsl.model.ws.TextMessage.getStreamedText"), ProblemFilters.exclude[ReversedMissingMethodProblem]("akka.http.scaladsl.model.ws.BinaryMessage.asScala"), - ProblemFilters.exclude[ReversedMissingMethodProblem]("akka.http.scaladsl.model.ws.BinaryMessage.getStreamedData") + ProblemFilters.exclude[ReversedMissingMethodProblem]("akka.http.scaladsl.model.ws.BinaryMessage.getStreamedData"), + + // #21131 new implementation for Akka Typed + ProblemFilters.exclude[ReversedMissingMethodProblem]("akka.actor.dungeon.DeathWatch.isWatching") ) ) } From b3bba1229fd1562603c60b8e5fa9e668a51759f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20P=C5=82achta?= Date: Thu, 8 Sep 2016 11:47:17 +0200 Subject: [PATCH 04/23] Make map stage final and refactor OneBoundedSetup to take a decider. (#21374) * Remove new from Map constructions --- .../impl/fusing/GraphInterpreterSpecKit.scala | 7 ++-- .../fusing/InterpreterSupervisionSpec.scala | 34 +++++++++---------- .../scala/akka/stream/impl/fusing/Ops.scala | 3 +- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/akka-stream-tests/src/test/scala/akka/stream/impl/fusing/GraphInterpreterSpecKit.scala b/akka-stream-tests/src/test/scala/akka/stream/impl/fusing/GraphInterpreterSpecKit.scala index c0bd29dd10..8faabe0aac 100644 --- a/akka-stream-tests/src/test/scala/akka/stream/impl/fusing/GraphInterpreterSpecKit.scala +++ b/akka-stream-tests/src/test/scala/akka/stream/impl/fusing/GraphInterpreterSpecKit.scala @@ -4,6 +4,8 @@ package akka.stream.impl.fusing import akka.event.Logging +import akka.stream.ActorAttributes.SupervisionStrategy +import akka.stream.Supervision.Decider import akka.stream._ import akka.stream.impl.fusing.GraphInterpreter.{ DownstreamBoundaryStageLogic, Failed, GraphAssembly, UpstreamBoundaryStageLogic } import akka.stream.stage.AbstractStage.PushPullGraphStage @@ -307,7 +309,7 @@ trait GraphInterpreterSpecKit extends StreamSpec { } } - abstract class OneBoundedSetup[T](_ops: GraphStageWithMaterializedValue[Shape, Any]*) extends Builder { + abstract class OneBoundedSetupWithDecider[T](decider: Decider, _ops: GraphStageWithMaterializedValue[Shape, Any]*) extends Builder { val ops = _ops.toArray val upstream = new UpstreamOneBoundedProbe[T] @@ -329,7 +331,7 @@ trait GraphInterpreterSpecKit extends StreamSpec { import GraphInterpreter.Boundary var i = 0 - val attributes = Array.fill[Attributes](ops.length)(Attributes.none) + val attributes = Array.fill[Attributes](ops.length)(ActorAttributes.supervisionStrategy(decider)) val ins = Array.ofDim[Inlet[_]](ops.length + 1) val inOwners = Array.ofDim[Int](ops.length + 1) val outs = Array.ofDim[Outlet[_]](ops.length + 1) @@ -429,4 +431,5 @@ trait GraphInterpreterSpecKit extends StreamSpec { } + abstract class OneBoundedSetup[T](_ops: GraphStageWithMaterializedValue[Shape, Any]*) extends OneBoundedSetupWithDecider[T](Supervision.stoppingDecider, _ops: _*) } diff --git a/akka-stream-tests/src/test/scala/akka/stream/impl/fusing/InterpreterSupervisionSpec.scala b/akka-stream-tests/src/test/scala/akka/stream/impl/fusing/InterpreterSupervisionSpec.scala index 355c6453b7..0ffdb7b440 100644 --- a/akka-stream-tests/src/test/scala/akka/stream/impl/fusing/InterpreterSupervisionSpec.scala +++ b/akka-stream-tests/src/test/scala/akka/stream/impl/fusing/InterpreterSupervisionSpec.scala @@ -19,12 +19,6 @@ class InterpreterSupervisionSpec extends StreamSpec with GraphInterpreterSpecKit override def toString = "TE" } - class ResumingMap[In, Out](_f: In ⇒ Out) extends Map(_f) { - - override def createLogic(inheritedAttributes: Attributes): GraphStageLogic = - super.createLogic(inheritedAttributes.and(ActorAttributes.supervisionStrategy(resumingDecider))) - } - "Interpreter error handling" must { "handle external failure" in new OneBoundedSetup[Int](Map((x: Int) ⇒ x + 1)) { @@ -62,8 +56,9 @@ class InterpreterSupervisionSpec extends StreamSpec with GraphInterpreterSpecKit lastEvents() should be(Set(Cancel, OnError(TE))) } - "resume when Map throws" in new OneBoundedSetup[Int]( - new ResumingMap((x: Int) ⇒ if (x == 0) throw TE else x) + "resume when Map throws" in new OneBoundedSetupWithDecider[Int]( + Supervision.resumingDecider, + Map((x: Int) ⇒ if (x == 0) throw TE else x) ) { downstream.requestOne() lastEvents() should be(Set(RequestOne)) @@ -88,10 +83,11 @@ class InterpreterSupervisionSpec extends StreamSpec with GraphInterpreterSpecKit lastEvents() should be(Set(OnNext(4))) } - "resume when Map throws in middle of the chain" in new OneBoundedSetup[Int]( - new ResumingMap((x: Int) ⇒ x + 1), - new ResumingMap((x: Int) ⇒ if (x == 0) throw TE else x + 10), - new ResumingMap((x: Int) ⇒ x + 100) + "resume when Map throws in middle of the chain" in new OneBoundedSetupWithDecider[Int]( + Supervision.resumingDecider, + Map((x: Int) ⇒ x + 1), + Map((x: Int) ⇒ if (x == 0) throw TE else x + 10), + Map((x: Int) ⇒ x + 100) ) { downstream.requestOne() @@ -108,9 +104,10 @@ class InterpreterSupervisionSpec extends StreamSpec with GraphInterpreterSpecKit lastEvents() should be(Set(OnNext(114))) } - "resume when Map throws before Grouped" in new OneBoundedSetup[Int]( - new ResumingMap((x: Int) ⇒ x + 1), - new ResumingMap((x: Int) ⇒ if (x <= 0) throw TE else x + 10), + "resume when Map throws before Grouped" in new OneBoundedSetupWithDecider[Int]( + Supervision.resumingDecider, + Map((x: Int) ⇒ x + 1), + Map((x: Int) ⇒ if (x <= 0) throw TE else x + 10), Grouped(3)) { downstream.requestOne() @@ -128,9 +125,10 @@ class InterpreterSupervisionSpec extends StreamSpec with GraphInterpreterSpecKit lastEvents() should be(Set(OnNext(Vector(13, 14, 15)))) } - "complete after resume when Map throws before Grouped" in new OneBoundedSetup[Int]( - new ResumingMap((x: Int) ⇒ x + 1), - new ResumingMap((x: Int) ⇒ if (x <= 0) throw TE else x + 10), + "complete after resume when Map throws before Grouped" in new OneBoundedSetupWithDecider[Int]( + Supervision.resumingDecider, + Map((x: Int) ⇒ x + 1), + Map((x: Int) ⇒ if (x <= 0) throw TE else x + 10), Grouped(1000)) { downstream.requestOne() diff --git a/akka-stream/src/main/scala/akka/stream/impl/fusing/Ops.scala b/akka-stream/src/main/scala/akka/stream/impl/fusing/Ops.scala index d1dbdb544a..96c7ec907c 100644 --- a/akka-stream/src/main/scala/akka/stream/impl/fusing/Ops.scala +++ b/akka-stream/src/main/scala/akka/stream/impl/fusing/Ops.scala @@ -25,8 +25,7 @@ import akka.stream.impl.Stages.DefaultAttributes /** * INTERNAL API */ -// FIXME: Not final because InterpreterSupervisionSpec. Some better option is needed here -case class Map[In, Out](f: In ⇒ Out) extends GraphStage[FlowShape[In, Out]] { +final case class Map[In, Out](f: In ⇒ Out) extends GraphStage[FlowShape[In, Out]] { val in = Inlet[In]("Map.in") val out = Outlet[Out]("Map.out") override val shape = FlowShape(in, out) From 0f2da7b26b5c4af35be87d2bd4a1a2392365df15 Mon Sep 17 00:00:00 2001 From: Richard Imaoka Date: Fri, 9 Sep 2016 20:15:33 +0900 Subject: [PATCH 05/23] Add instruction for akka-samples (Fixes #21415) (#21416) --- akka-samples/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 akka-samples/README.md diff --git a/akka-samples/README.md b/akka-samples/README.md new file mode 100644 index 0000000000..1059718002 --- /dev/null +++ b/akka-samples/README.md @@ -0,0 +1,8 @@ +Use Lightbend Activator to run samples +-------------------------------------- + +Use [Lightbend Activator](https://www.lightbend.com/activator/download) to run samples in this akka-samples directory. +Follow the instruction on the Activator download page, and the [Activator documentation](https://www.lightbend.com/activator/docs). +Once activator ui is up, you an find akka-sample-* projects by their names. + + From ae084083f63058d123a66e1a154e34e9228728c8 Mon Sep 17 00:00:00 2001 From: "Richard S. Imaoka" Date: Sat, 10 Sep 2016 02:01:06 +0900 Subject: [PATCH 06/23] More descriptive errors from ReplayFilter (Fixes #20394) --- .../akka/persistence/journal/ReplayFilter.scala | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/akka-persistence/src/main/scala/akka/persistence/journal/ReplayFilter.scala b/akka-persistence/src/main/scala/akka/persistence/journal/ReplayFilter.scala index 4df0bb0d4f..25bf19c1fd 100644 --- a/akka-persistence/src/main/scala/akka/persistence/journal/ReplayFilter.scala +++ b/akka-persistence/src/main/scala/akka/persistence/journal/ReplayFilter.scala @@ -75,8 +75,9 @@ private[akka] class ReplayFilter(persistentActor: ActorRef, mode: ReplayFilter.M if (r.persistent.writerUuid == writerUuid) { // from same writer if (r.persistent.sequenceNr < seqNo) { - val errMsg = s"Invalid replayed event [${r.persistent.sequenceNr}] in wrong order from " + - s"writer [${r.persistent.writerUuid}] with persistenceId [${r.persistent.persistenceId}]" + val errMsg = s"Invalid replayed event [sequenceNr=${r.persistent.sequenceNr}, writerUUID=${r.persistent.writerUuid}] as " + + s"the sequenceNr should be equal to or greater than already-processed event [sequenceNr=${seqNo}, writerUUID=${writerUuid}] from the same writer, for the same persistenceId [${r.persistent.persistenceId}]. " + + "Perhaps, events were journaled out of sequence, or duplicate persistentId for different entities?" logIssue(errMsg) mode match { case RepairByDiscardOld ⇒ // discard @@ -92,8 +93,9 @@ private[akka] class ReplayFilter(persistentActor: ActorRef, mode: ReplayFilter.M } else if (oldWriters.contains(r.persistent.writerUuid)) { // from old writer - val errMsg = s"Invalid replayed event [${r.persistent.sequenceNr}] from old " + - s"writer [${r.persistent.writerUuid}] with persistenceId [${r.persistent.persistenceId}]" + val errMsg = s"Invalid replayed event [sequenceNr=${r.persistent.sequenceNr}, writerUUID=${r.persistent.writerUuid}]. " + + s"There was already a newer writer whose last replayed event was [sequenceNr=${seqNo}, writerUUID=${writerUuid}] for the same persistenceId [${r.persistent.persistenceId}]." + + "Perhaps, the old writer kept journaling messages after the new writer created, or duplicate persistentId for different entities?" logIssue(errMsg) mode match { case RepairByDiscardOld ⇒ // discard @@ -112,13 +114,14 @@ private[akka] class ReplayFilter(persistentActor: ActorRef, mode: ReplayFilter.M writerUuid = r.persistent.writerUuid seqNo = r.persistent.sequenceNr - // clear the buffer from messages from other writers with higher seqNo + // clear the buffer for messages from old writers with higher seqNo val iter = buffer.iterator() while (iter.hasNext()) { val msg = iter.next() if (msg.persistent.sequenceNr >= seqNo) { - val errMsg = s"Invalid replayed event [${msg.persistent.sequenceNr}] in buffer from old " + - s"writer [${msg.persistent.writerUuid}] with persistenceId [${msg.persistent.persistenceId}]" + val errMsg = s"Invalid replayed event [sequenceNr=${r.persistent.sequenceNr}, writerUUID=${r.persistent.writerUuid}] from a new writer. " + + s"An older writer already sent an event [sequenceNr=${msg.persistent.sequenceNr}, writerUUID=${msg.persistent.writerUuid}] whose sequence number was equal or greater for the same persistenceId [${r.persistent.persistenceId}]. " + + "Perhaps, the new writer journaled the event out of sequence, or duplicate persistentId for different entities?" logIssue(errMsg) mode match { case RepairByDiscardOld ⇒ iter.remove() // discard From 75bbd7d00b62343821f7b35dfab546876ee608e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Andr=C3=A9n?= Date: Mon, 12 Sep 2016 11:59:38 +0200 Subject: [PATCH 07/23] -doc Remove stale other language bindings doc page #21426 (#21432) --- akka-docs/rst/additional/index.rst | 1 - akka-docs/rst/additional/language-bindings.rst | 17 ----------------- 2 files changed, 18 deletions(-) delete mode 100644 akka-docs/rst/additional/language-bindings.rst diff --git a/akka-docs/rst/additional/index.rst b/akka-docs/rst/additional/index.rst index 03e8a81966..bc8a124c92 100644 --- a/akka-docs/rst/additional/index.rst +++ b/akka-docs/rst/additional/index.rst @@ -7,5 +7,4 @@ Additional Information ../common/binary-compatibility-rules faq books - language-bindings osgi diff --git a/akka-docs/rst/additional/language-bindings.rst b/akka-docs/rst/additional/language-bindings.rst deleted file mode 100644 index a9e386dd67..0000000000 --- a/akka-docs/rst/additional/language-bindings.rst +++ /dev/null @@ -1,17 +0,0 @@ -Other Language Bindings -======================= - -JRuby ------ - -Read more here: ``_. - -Groovy/Groovy++ ---------------- - -Read more here: ``_. - -Clojure -------- - -Read more here: ``_. From 98bfbbb4b3c0f394eaa340dbdd5dbf4db5200c52 Mon Sep 17 00:00:00 2001 From: Richard Imaoka Date: Mon, 12 Sep 2016 23:18:06 +0900 Subject: [PATCH 08/23] Add documentation for ReplayFilter #20301 --- akka-docs/rst/java/persistence.rst | 33 +++++++++++++++++++++++++++++ akka-docs/rst/scala/persistence.rst | 33 +++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/akka-docs/rst/java/persistence.rst b/akka-docs/rst/java/persistence.rst index 29aceb61d8..95a785e06b 100644 --- a/akka-docs/rst/java/persistence.rst +++ b/akka-docs/rst/java/persistence.rst @@ -454,6 +454,39 @@ mechanism when ``persist()`` is used. Notice the early stop behaviour that occur .. includecode:: code/docs/persistence/PersistenceDocTest.java#safe-shutdown-example-bad .. includecode:: code/docs/persistence/PersistenceDocTest.java#safe-shutdown-example-good + +.. _replay-filter-java: + +Replay Filter +------------- +There could be cases where event streams are corrupted and multiple writers (i.e. multiple persistent actor instances) +journaled different messages with the same sequence number. +In such a case, you can configure how you filter replayed messages from multiple writers, upon recovery. + +In your configuration, under the ``akka.persistence.journal.xxx.replay-filter`` section (where ``xxx`` is your journal plugin id), +you can select the replay filter ``mode`` from one of the following values: + +* repair-by-discard-old +* fail +* warn +* off + +For example, if you configure the replay filter for leveldb plugin, it looks like this:: + + # The replay filter can detect a corrupt event stream by inspecting + # sequence numbers and writerUuid when replaying events. + akka.persistence.journal.leveldb.replay-filter { + # What the filter should do when detecting invalid events. + # Supported values: + # `repair-by-discard-old` : discard events from old writers, + # warning is logged + # `fail` : fail the replay, error is logged + # `warn` : log warning but emit events untouched + # `off` : disable this feature completely + mode = repair-by-discard-old + } + + .. _persistent-views-java: Persistent Views diff --git a/akka-docs/rst/scala/persistence.rst b/akka-docs/rst/scala/persistence.rst index 4cd8870625..edec6242d4 100644 --- a/akka-docs/rst/scala/persistence.rst +++ b/akka-docs/rst/scala/persistence.rst @@ -123,6 +123,8 @@ It contains instructions on how to run the ``PersistentActorExample``. Note that when using ``become`` from ``receiveRecover`` it will still only use the ``receiveRecover`` behavior when replaying the events. When replay is completed it will use the new behavior. +.. _persistence-id-scala: + Identifiers ----------- @@ -440,6 +442,37 @@ mechanism when ``persist()`` is used. Notice the early stop behaviour that occur .. includecode:: code/docs/persistence/PersistenceDocSpec.scala#safe-shutdown-example-bad .. includecode:: code/docs/persistence/PersistenceDocSpec.scala#safe-shutdown-example-good +.. _replay-filter-scala: + +Replay Filter +------------- +There could be cases where event streams are corrupted and multiple writers (i.e. multiple persistent actor instances) +journaled different messages with the same sequence number. +In such a case, you can configure how you filter replayed messages from multiple writers, upon recovery. + +In your configuration, under the ``akka.persistence.journal.xxx.replay-filter`` section (where ``xxx`` is your journal plugin id), +you can select the replay filter ``mode`` from one of the following values: + +* repair-by-discard-old +* fail +* warn +* off + +For example, if you configure the replay filter for leveldb plugin, it looks like this:: + + # The replay filter can detect a corrupt event stream by inspecting + # sequence numbers and writerUuid when replaying events. + akka.persistence.journal.leveldb.replay-filter { + # What the filter should do when detecting invalid events. + # Supported values: + # `repair-by-discard-old` : discard events from old writers, + # warning is logged + # `fail` : fail the replay, error is logged + # `warn` : log warning but emit events untouched + # `off` : disable this feature completely + mode = repair-by-discard-old + } + .. _persistent-views: Persistent Views From 47433bc65c5874173ae7810a328a5b3583e535ae Mon Sep 17 00:00:00 2001 From: drewhk Date: Tue, 13 Sep 2016 09:02:53 +0200 Subject: [PATCH 09/23] Fix initialization order in BroadcastHub caused by an otherwise innocent race #21362 --- .../main/scala/akka/stream/scaladsl/Hub.scala | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/akka-stream/src/main/scala/akka/stream/scaladsl/Hub.scala b/akka-stream/src/main/scala/akka/stream/scaladsl/Hub.scala index f94dca794c..1099b9008a 100644 --- a/akka-stream/src/main/scala/akka/stream/scaladsl/Hub.scala +++ b/akka-stream/src/main/scala/akka/stream/scaladsl/Hub.scala @@ -586,7 +586,7 @@ private[akka] class BroadcastHub[T](bufferSize: Int) extends GraphStageWithMater override def createLogic(inheritedAttributes: Attributes): GraphStageLogic = new GraphStageLogic(shape) with OutHandler { private[this] var untilNextAdvanceSignal = DemandThreshold private[this] val id = idCounter.getAndIncrement() - private[this] var initialized = false + private[this] var offsetInitialized = false private[this] var hubCallback: AsyncCallback[HubEvent] = _ /* @@ -604,6 +604,7 @@ private[akka] class BroadcastHub[T](bufferSize: Int) extends GraphStageWithMater val onHubReady: Try[AsyncCallback[HubEvent]] ⇒ Unit = { case Success(callback) ⇒ hubCallback = callback + if (isAvailable(out) && offsetInitialized) onPull() callback.invoke(RegistrationPending) case Failure(ex) ⇒ failStage(ex) @@ -621,12 +622,19 @@ private[akka] class BroadcastHub[T](bufferSize: Int) extends GraphStageWithMater } } + /* + * Note that there is a potential race here. First we add ourselves to the pending registrations, then + * we send RegistrationPending. However, another downstream might have triggered our registration by its + * own RegistrationPending message, since we are in the list already. + * This means we might receive an onCommand(Initialize(offset)) *before* onHubReady fires so it is important + * to only serve elements after both offsetInitialized = true and hubCallback is not null. + */ register() } override def onPull(): Unit = { - if (initialized) { + if (offsetInitialized && (hubCallback ne null)) { val elem = logic.poll(offset) elem match { @@ -661,10 +669,10 @@ private[akka] class BroadcastHub[T](bufferSize: Int) extends GraphStageWithMater case Wakeup ⇒ if (isAvailable(out)) onPull() case Initialize(initialOffset) ⇒ - initialized = true + offsetInitialized = true previousPublishedOffset = initialOffset offset = initialOffset - if (isAvailable(out)) onPull() + if (isAvailable(out) && (hubCallback ne null)) onPull() } setHandler(out, this) From a88c8a0bbee67818e32d73eb30bbaa199e9b120b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20L=C3=BCneberg?= Date: Tue, 13 Sep 2016 10:19:37 +0200 Subject: [PATCH 10/23] Add javadsl FormData.create(Iterable) overload #21303 (#21344) --- .../main/java/akka/http/javadsl/model/FormData.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/akka-http-core/src/main/java/akka/http/javadsl/model/FormData.java b/akka-http-core/src/main/java/akka/http/javadsl/model/FormData.java index d48b1c3ff5..656407b0f8 100644 --- a/akka-http-core/src/main/java/akka/http/javadsl/model/FormData.java +++ b/akka-http-core/src/main/java/akka/http/javadsl/model/FormData.java @@ -4,9 +4,10 @@ package akka.http.javadsl.model; -import akka.japi.Pair; import java.util.Map; +import akka.japi.Pair; + /** * Simple model for `application/x-www-form-urlencoded` form data. */ @@ -51,4 +52,11 @@ public final class FormData { public static FormData create(Map params) { return new FormData(Query.create(params)); } + + /** + * Creates a FormData from the given parameters. + */ + public static FormData create(Iterable> params) { + return new FormData(Query.create(params)); + } } From b2f0ca6750596cafff5b11207576b42ffa60a4aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Endre=20S=C3=A1ndor=20Varga?= Date: Mon, 12 Sep 2016 16:50:02 +0200 Subject: [PATCH 11/23] #21446: Completion events must not be swallowed if chasing --- .../impl/fusing/ChasingEventsSpec.scala | 115 + .../fusing/GraphInterpreterPortsSpec.scala | 2323 +++++++++-------- .../impl/fusing/GraphInterpreterSpecKit.scala | 48 +- .../stream/impl/fusing/GraphInterpreter.scala | 12 + 4 files changed, 1329 insertions(+), 1169 deletions(-) create mode 100644 akka-stream-tests/src/test/scala/akka/stream/impl/fusing/ChasingEventsSpec.scala diff --git a/akka-stream-tests/src/test/scala/akka/stream/impl/fusing/ChasingEventsSpec.scala b/akka-stream-tests/src/test/scala/akka/stream/impl/fusing/ChasingEventsSpec.scala new file mode 100644 index 0000000000..8f4f69f77f --- /dev/null +++ b/akka-stream-tests/src/test/scala/akka/stream/impl/fusing/ChasingEventsSpec.scala @@ -0,0 +1,115 @@ +/** + * Copyright (C) 2015-2016 Lightbend Inc. + */ +package akka.stream.impl.fusing + +import akka.stream.scaladsl.{ Sink, Source } +import akka.stream._ +import akka.stream.stage.{ GraphStage, GraphStageLogic, InHandler, OutHandler } +import akka.stream.testkit.Utils.TE +import akka.stream.testkit.{ TestPublisher, TestSubscriber } +import akka.testkit.AkkaSpec + +class ChasingEventsSpec extends AkkaSpec { + + implicit val materializer = ActorMaterializer(ActorMaterializerSettings(system).withFuzzing(false)) + + class CancelInChasedPull extends GraphStage[FlowShape[Int, Int]] { + val in = Inlet[Int]("Propagate.in") + val out = Outlet[Int]("Propagate.out") + override val shape: FlowShape[Int, Int] = FlowShape(in, out) + + override def createLogic(inheritedAttributes: Attributes): GraphStageLogic = new GraphStageLogic(shape) with InHandler with OutHandler { + private var first = true + override def onPush(): Unit = push(out, grab(in)) + override def onPull(): Unit = { + pull(in) + if (!first) cancel(in) + first = false + } + + setHandlers(in, out, this) + } + } + + class CompleteInChasedPush extends GraphStage[FlowShape[Int, Int]] { + val in = Inlet[Int]("Propagate.in") + val out = Outlet[Int]("Propagate.out") + override val shape: FlowShape[Int, Int] = FlowShape(in, out) + + override def createLogic(inheritedAttributes: Attributes): GraphStageLogic = new GraphStageLogic(shape) with InHandler with OutHandler { + private var first = true + override def onPush(): Unit = { + push(out, grab(in)) + complete(out) + } + override def onPull(): Unit = pull(in) + + setHandlers(in, out, this) + } + } + + class FailureInChasedPush extends GraphStage[FlowShape[Int, Int]] { + val in = Inlet[Int]("Propagate.in") + val out = Outlet[Int]("Propagate.out") + override val shape: FlowShape[Int, Int] = FlowShape(in, out) + + override def createLogic(inheritedAttributes: Attributes): GraphStageLogic = new GraphStageLogic(shape) with InHandler with OutHandler { + private var first = true + override def onPush(): Unit = { + push(out, grab(in)) + fail(out, TE("test failure")) + } + override def onPull(): Unit = pull(in) + + setHandlers(in, out, this) + } + } + + class ChasableSink extends GraphStage[SinkShape[Int]] { + val in = Inlet[Int]("Chaseable.in") + override val shape: SinkShape[Int] = SinkShape(in) + + @throws(classOf[Exception]) + override def createLogic(inheritedAttributes: Attributes): GraphStageLogic = new GraphStageLogic(shape) with InHandler { + override def preStart(): Unit = pull(in) + override def onPush(): Unit = pull(in) + setHandler(in, this) + } + } + + "Event chasing" must { + + "propagate cancel if enqueued immediately after pull" in { + val upstream = TestPublisher.probe[Int]() + + Source.fromPublisher(upstream).via(new CancelInChasedPull).runWith(Sink.ignore) + + upstream.sendNext(0) + upstream.expectCancellation() + + } + + "propagate complete if enqueued immediately after push" in { + val downstream = TestSubscriber.probe[Int]() + + Source(1 to 10).via(new CompleteInChasedPush).runWith(Sink.fromSubscriber(downstream)) + + downstream.requestNext(1) + downstream.expectComplete() + + } + + "propagate failure if enqueued immediately after push" in { + val downstream = TestSubscriber.probe[Int]() + + Source(1 to 10).via(new FailureInChasedPush).runWith(Sink.fromSubscriber(downstream)) + + downstream.requestNext(1) + downstream.expectError() + + } + + } + +} diff --git a/akka-stream-tests/src/test/scala/akka/stream/impl/fusing/GraphInterpreterPortsSpec.scala b/akka-stream-tests/src/test/scala/akka/stream/impl/fusing/GraphInterpreterPortsSpec.scala index 0bc6b81c37..37e7754190 100644 --- a/akka-stream-tests/src/test/scala/akka/stream/impl/fusing/GraphInterpreterPortsSpec.scala +++ b/akka-stream-tests/src/test/scala/akka/stream/impl/fusing/GraphInterpreterPortsSpec.scala @@ -12,1166 +12,1169 @@ class GraphInterpreterPortsSpec extends StreamSpec with GraphInterpreterSpecKit // FIXME test failure scenarios - "properly transition on push and pull" in new PortTestSetup { - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(false) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(false) - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - - in.pull() - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(false) - in.isAvailable should be(false) - in.hasBeenPulled should be(true) - in.isClosed should be(false) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - - stepAll() - - lastEvents() should be(Set(RequestOne(out))) - out.isAvailable should be(true) - out.isClosed should be(false) - in.isAvailable should be(false) - in.hasBeenPulled should be(true) - in.isClosed should be(false) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { in.grab() } - - out.push(0) - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(false) - in.isAvailable should be(false) - in.hasBeenPulled should be(true) - in.isClosed should be(false) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - - stepAll() - - lastEvents() should be(Set(OnNext(in, 0))) - out.isAvailable should be(false) - out.isClosed should be(false) - in.isAvailable should be(true) - in.hasBeenPulled should be(false) - in.isClosed should be(false) - an[IllegalArgumentException] should be thrownBy { out.push(0) } - - in.grab() should ===(0) - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(false) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(false) - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - - // Cycle completed - } - - "drop ungrabbed element on pull" in new PortTestSetup { - in.pull() - step() - clearEvents() - out.push(0) - step() - - lastEvents() should be(Set(OnNext(in, 0))) - - in.pull() - - an[IllegalArgumentException] should be thrownBy { in.grab() } - } - - "propagate complete while downstream is active" in new PortTestSetup { - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(false) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(false) - - out.complete() - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(false) - an[IllegalArgumentException] should be thrownBy { out.push(0) } - - stepAll() - - lastEvents() should be(Set(OnComplete(in))) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - - in.cancel() // This should have no effect now - stepAll() - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - - out.complete() // This should have no effect now - stepAll() - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - } - - "propagate complete while upstream is active" in new PortTestSetup { - in.pull() - stepAll() - - lastEvents() should be(Set(RequestOne(out))) - out.isAvailable should be(true) - out.isClosed should be(false) - in.isAvailable should be(false) - in.hasBeenPulled should be(true) - in.isClosed should be(false) - - out.complete() - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(true) - in.isClosed should be(false) - an[IllegalArgumentException] should be thrownBy { out.push(0) } - - stepAll() - - lastEvents() should be(Set(OnComplete(in))) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - - in.cancel() // This should have no effect now - stepAll() - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - - out.complete() // This should have no effect now - stepAll() - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - - } - - "propagate complete while pull is in flight" in new PortTestSetup { - in.pull() - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(false) - in.isAvailable should be(false) - in.hasBeenPulled should be(true) - in.isClosed should be(false) - - out.complete() - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(true) - in.isClosed should be(false) - an[IllegalArgumentException] should be thrownBy { out.push(0) } - - stepAll() - - lastEvents() should be(Set(OnComplete(in))) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - - in.cancel() // This should have no effect now - stepAll() - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - - out.complete() // This should have no effect now - stepAll() - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - } - - "propagate complete while push is in flight" in new PortTestSetup { - in.pull() - stepAll() - clearEvents() - - out.push(0) - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(false) - in.isAvailable should be(false) - in.hasBeenPulled should be(true) - in.isClosed should be(false) - - out.complete() - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(true) - in.isClosed should be(false) - an[IllegalArgumentException] should be thrownBy { out.push(0) } - - step() - - lastEvents() should be(Set(OnNext(in, 0))) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(true) - in.hasBeenPulled should be(false) - in.isClosed should be(false) - an[IllegalArgumentException] should be thrownBy { out.push(0) } - in.grab() should ===(0) - an[IllegalArgumentException] should be thrownBy { in.grab() } - - step() - - lastEvents() should be(Set(OnComplete(in))) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - - out.complete() // This should have no effect now - stepAll() - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - } - - "propagate complete while push is in flight and keep ungrabbed element" in new PortTestSetup { - in.pull() - stepAll() - clearEvents() - - out.push(0) - out.complete() - step() - - lastEvents() should be(Set(OnNext(in, 0))) - step() - - lastEvents() should be(Set(OnComplete(in))) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(true) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - in.grab() should ===(0) - } - - "propagate complete while push is in flight and pulled after the push" in new PortTestSetup { - in.pull() - stepAll() - clearEvents() - - out.push(0) - out.complete() - step() - - lastEvents() should be(Set(OnNext(in, 0))) - in.grab() should ===(0) - - in.pull() - stepAll() - - lastEvents() should be(Set(OnComplete(in))) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - } - - "ignore pull while completing" in new PortTestSetup { - out.complete() - in.pull() - // While the pull event is not enqueued at this point, we should still report the state correctly - in.hasBeenPulled should be(true) - - stepAll() - - lastEvents() should be(Set(OnComplete(in))) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - } - - "propagate cancel while downstream is active" in new PortTestSetup { - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(false) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(false) - - in.cancel() - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(false) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { in.grab() } - - stepAll() - - lastEvents() should be(Set(Cancel(out))) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - - out.complete() // This should have no effect now - stepAll() - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - - in.cancel() // This should have no effect now - stepAll() - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - } - - "propagate cancel while upstream is active" in new PortTestSetup { - in.pull() - stepAll() - - lastEvents() should be(Set(RequestOne(out))) - out.isAvailable should be(true) - out.isClosed should be(false) - in.isAvailable should be(false) - in.hasBeenPulled should be(true) - in.isClosed should be(false) - - in.cancel() - - lastEvents() should be(Set.empty) - out.isAvailable should be(true) - out.isClosed should be(false) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { in.grab() } - - stepAll() - - lastEvents() should be(Set(Cancel(out))) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - - out.complete() // This should have no effect now - stepAll() - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - - in.cancel() // This should have no effect now - stepAll() - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - - } - - "propagate cancel while pull is in flight" in new PortTestSetup { - in.pull() - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(false) - in.isAvailable should be(false) - in.hasBeenPulled should be(true) - in.isClosed should be(false) - - in.cancel() - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(false) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - - stepAll() - - lastEvents() should be(Set(Cancel(out))) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - - out.complete() // This should have no effect now - stepAll() - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - - in.cancel() // This should have no effect now - stepAll() - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - } - - "propagate cancel while push is in flight" in new PortTestSetup { - in.pull() - stepAll() - clearEvents() - - out.push(0) - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(false) - in.isAvailable should be(false) - in.hasBeenPulled should be(true) - in.isClosed should be(false) - - in.cancel() - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(false) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - - stepAll() - - lastEvents() should be(Set(Cancel(out))) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - - out.complete() // This should have no effect now - stepAll() - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - - in.cancel() // This should have no effect now - stepAll() - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - } - - "ignore push while cancelling" in new PortTestSetup { - in.pull() - stepAll() - clearEvents() - - in.cancel() - out.push(0) - // While the push event is not enqueued at this point, we should still report the state correctly - out.isAvailable should be(false) - - stepAll() - - lastEvents() should be(Set(Cancel(out))) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - } - - "clear ungrabbed element even when cancelled" in new PortTestSetup { - in.pull() - stepAll() - clearEvents() - out.push(0) - stepAll() - - lastEvents() should be(Set(OnNext(in, 0))) - - in.cancel() - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(false) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { in.grab() } - - stepAll() - lastEvents() should be(Set(Cancel(out))) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - } - - "ignore any completion if they are concurrent (cancel first)" in new PortTestSetup { - in.cancel() - out.complete() - - stepAll() - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - } - - "ignore any completion if they are concurrent (complete first)" in new PortTestSetup { - out.complete() - in.cancel() - - stepAll() - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - } - - "ignore completion from a push-complete if cancelled while in flight" in new PortTestSetup { - in.pull() - stepAll() - clearEvents() - - out.push(0) - out.complete() - in.cancel() - - stepAll() - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - } - - "ignore completion from a push-complete if cancelled after onPush" in new PortTestSetup { - in.pull() - stepAll() - clearEvents() - - out.push(0) - out.complete() - - step() - - lastEvents() should be(Set(OnNext(in, 0))) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(true) - in.hasBeenPulled should be(false) - in.isClosed should be(false) - an[IllegalArgumentException] should be thrownBy { out.push(0) } - in.grab() should ===(0) - - in.cancel() - stepAll() - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - } - - "not allow to grab element before it arrives" in new PortTestSetup { - in.pull() - stepAll() - out.push(0) - - an[IllegalArgumentException] should be thrownBy { in.grab() } - } - - "not allow to grab element if already cancelled" in new PortTestSetup { - in.pull() - stepAll() - - out.push(0) - in.cancel() - - stepAll() - - an[IllegalArgumentException] should be thrownBy { in.grab() } - } - - "propagate failure while downstream is active" in new PortTestSetup { - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(false) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(false) - - out.fail(TE("test")) - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(false) - an[IllegalArgumentException] should be thrownBy { out.push(0) } - - stepAll() - - lastEvents() should be(Set(OnError(in, TE("test")))) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - - in.cancel() // This should have no effect now - stepAll() - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - - out.complete() // This should have no effect now - stepAll() - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - } - - "propagate failure while upstream is active" in new PortTestSetup { - in.pull() - stepAll() - - lastEvents() should be(Set(RequestOne(out))) - out.isAvailable should be(true) - out.isClosed should be(false) - in.isAvailable should be(false) - in.hasBeenPulled should be(true) - in.isClosed should be(false) - - out.fail(TE("test")) - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(true) - in.isClosed should be(false) - an[IllegalArgumentException] should be thrownBy { out.push(0) } - - stepAll() - - lastEvents() should be(Set(OnError(in, TE("test")))) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - - in.cancel() // This should have no effect now - stepAll() - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - - out.complete() // This should have no effect now - stepAll() - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - - } - - "propagate failure while pull is in flight" in new PortTestSetup { - in.pull() - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(false) - in.isAvailable should be(false) - in.hasBeenPulled should be(true) - in.isClosed should be(false) - - out.fail(TE("test")) - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(true) - in.isClosed should be(false) - an[IllegalArgumentException] should be thrownBy { out.push(0) } - - stepAll() - - lastEvents() should be(Set(OnError(in, TE("test")))) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - - in.cancel() // This should have no effect now - stepAll() - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - - out.complete() // This should have no effect now - stepAll() - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - } - - "propagate failure while push is in flight" in new PortTestSetup { - in.pull() - stepAll() - clearEvents() - - out.push(0) - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(false) - in.isAvailable should be(false) - in.hasBeenPulled should be(true) - in.isClosed should be(false) - - out.fail(TE("test")) - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(true) - in.isClosed should be(false) - an[IllegalArgumentException] should be thrownBy { out.push(0) } - - step() - - lastEvents() should be(Set(OnNext(in, 0))) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(true) - in.hasBeenPulled should be(false) - in.isClosed should be(false) - an[IllegalArgumentException] should be thrownBy { out.push(0) } - in.grab() should ===(0) - an[IllegalArgumentException] should be thrownBy { in.grab() } - - step() - - lastEvents() should be(Set(OnError(in, TE("test")))) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - - out.complete() // This should have no effect now - stepAll() - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - } - - "propagate failure while push is in flight and keep ungrabbed element" in new PortTestSetup { - in.pull() - stepAll() - clearEvents() - - out.push(0) - out.fail(TE("test")) - step() - - lastEvents() should be(Set(OnNext(in, 0))) - step() - - lastEvents() should be(Set(OnError(in, TE("test")))) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(true) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - in.grab() should ===(0) - } - - "ignore pull while failing" in new PortTestSetup { - out.fail(TE("test")) - in.pull() - in.hasBeenPulled should be(true) - - stepAll() - - lastEvents() should be(Set(OnError(in, TE("test")))) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - } - - "ignore any failure completion if they are concurrent (cancel first)" in new PortTestSetup { - in.cancel() - out.fail(TE("test")) - - stepAll() - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - } - - "ignore any failure completion if they are concurrent (complete first)" in new PortTestSetup { - out.fail(TE("test")) - in.cancel() - - stepAll() - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - } - - "ignore failure from a push-then-fail if cancelled while in flight" in new PortTestSetup { - in.pull() - stepAll() - clearEvents() - - out.push(0) - out.fail(TE("test")) - in.cancel() - - stepAll() - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } - } - - "ignore failure from a push-then-fail if cancelled after onPush" in new PortTestSetup { - in.pull() - stepAll() - clearEvents() - - out.push(0) - out.fail(TE("test")) - - step() - - lastEvents() should be(Set(OnNext(in, 0))) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(true) - in.hasBeenPulled should be(false) - in.isClosed should be(false) - an[IllegalArgumentException] should be thrownBy { out.push(0) } - in.grab() should ===(0) - - in.cancel() - stepAll() - - lastEvents() should be(Set.empty) - out.isAvailable should be(false) - out.isClosed should be(true) - in.isAvailable should be(false) - in.hasBeenPulled should be(false) - in.isClosed should be(true) - an[IllegalArgumentException] should be thrownBy { in.pull() } - an[IllegalArgumentException] should be thrownBy { out.push(0) } - an[IllegalArgumentException] should be thrownBy { in.grab() } + for (chasing ← List(false, true)) { + + s"properly transition on push and pull (chasing = $chasing)" in new PortTestSetup(chasing) { + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(false) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(false) + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + + in.pull() + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(false) + in.isAvailable should be(false) + in.hasBeenPulled should be(true) + in.isClosed should be(false) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + + stepAll() + + lastEvents() should be(Set(RequestOne(out))) + out.isAvailable should be(true) + out.isClosed should be(false) + in.isAvailable should be(false) + in.hasBeenPulled should be(true) + in.isClosed should be(false) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { in.grab() } + + out.push(0) + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(false) + in.isAvailable should be(false) + in.hasBeenPulled should be(true) + in.isClosed should be(false) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + + stepAll() + + lastEvents() should be(Set(OnNext(in, 0))) + out.isAvailable should be(false) + out.isClosed should be(false) + in.isAvailable should be(true) + in.hasBeenPulled should be(false) + in.isClosed should be(false) + an[IllegalArgumentException] should be thrownBy { out.push(0) } + + in.grab() should ===(0) + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(false) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(false) + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + + // Cycle completed + } + + s"drop ungrabbed element on pull (chasing = $chasing)" in new PortTestSetup(chasing) { + in.pull() + step() + clearEvents() + out.push(0) + step() + + lastEvents() should be(Set(OnNext(in, 0))) + + in.pull() + + an[IllegalArgumentException] should be thrownBy { in.grab() } + } + + s"propagate complete while downstream is active (chasing = $chasing)" in new PortTestSetup(chasing) { + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(false) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(false) + + out.complete() + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(false) + an[IllegalArgumentException] should be thrownBy { out.push(0) } + + stepAll() + + lastEvents() should be(Set(OnComplete(in))) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + + in.cancel() // This should have no effect now + stepAll() + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + + out.complete() // This should have no effect now + stepAll() + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + } + + s"propagate complete while upstream is active (chasing = $chasing)" in new PortTestSetup(chasing) { + in.pull() + stepAll() + + lastEvents() should be(Set(RequestOne(out))) + out.isAvailable should be(true) + out.isClosed should be(false) + in.isAvailable should be(false) + in.hasBeenPulled should be(true) + in.isClosed should be(false) + + out.complete() + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(true) + in.isClosed should be(false) + an[IllegalArgumentException] should be thrownBy { out.push(0) } + + stepAll() + + lastEvents() should be(Set(OnComplete(in))) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + + in.cancel() // This should have no effect now + stepAll() + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + + out.complete() // This should have no effect now + stepAll() + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + + } + + s"propagate complete while pull is in flight (chasing = $chasing)" in new PortTestSetup(chasing) { + in.pull() + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(false) + in.isAvailable should be(false) + in.hasBeenPulled should be(true) + in.isClosed should be(false) + + out.complete() + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(true) + in.isClosed should be(false) + an[IllegalArgumentException] should be thrownBy { out.push(0) } + + stepAll() + + lastEvents() should be(Set(OnComplete(in))) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + + in.cancel() // This should have no effect now + stepAll() + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + + out.complete() // This should have no effect now + stepAll() + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + } + + s"propagate complete while push is in flight (chasing = $chasing)" in new PortTestSetup(chasing) { + in.pull() + stepAll() + clearEvents() + + out.push(0) + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(false) + in.isAvailable should be(false) + in.hasBeenPulled should be(true) + in.isClosed should be(false) + + out.complete() + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(true) + in.isClosed should be(false) + an[IllegalArgumentException] should be thrownBy { out.push(0) } + + step() + + lastEvents() should be(Set(OnNext(in, 0))) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(true) + in.hasBeenPulled should be(false) + in.isClosed should be(false) + an[IllegalArgumentException] should be thrownBy { out.push(0) } + in.grab() should ===(0) + an[IllegalArgumentException] should be thrownBy { in.grab() } + + step() + + lastEvents() should be(Set(OnComplete(in))) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + + out.complete() // This should have no effect now + stepAll() + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + } + + s"propagate complete while push is in flight and keep ungrabbed element (chasing = $chasing)" in new PortTestSetup(chasing) { + in.pull() + stepAll() + clearEvents() + + out.push(0) + out.complete() + step() + + lastEvents() should be(Set(OnNext(in, 0))) + step() + + lastEvents() should be(Set(OnComplete(in))) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(true) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + in.grab() should ===(0) + } + + s"propagate complete while push is in flight and pulled after the push (chasing = $chasing)" in new PortTestSetup(chasing) { + in.pull() + stepAll() + clearEvents() + + out.push(0) + out.complete() + step() + + lastEvents() should be(Set(OnNext(in, 0))) + in.grab() should ===(0) + + in.pull() + stepAll() + + lastEvents() should be(Set(OnComplete(in))) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + } + + s"ignore pull while completing (chasing = $chasing)" in new PortTestSetup(chasing) { + out.complete() + in.pull() + // While the pull event is not enqueued at this point, we should still report the state correctly + in.hasBeenPulled should be(true) + + stepAll() + + lastEvents() should be(Set(OnComplete(in))) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + } + + s"propagate cancel while downstream is active (chasing = $chasing)" in new PortTestSetup(chasing) { + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(false) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(false) + + in.cancel() + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(false) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { in.grab() } + + stepAll() + + lastEvents() should be(Set(Cancel(out))) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + + out.complete() // This should have no effect now + stepAll() + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + + in.cancel() // This should have no effect now + stepAll() + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + } + + s"propagate cancel while upstream is active (chasing = $chasing)" in new PortTestSetup(chasing) { + in.pull() + stepAll() + + lastEvents() should be(Set(RequestOne(out))) + out.isAvailable should be(true) + out.isClosed should be(false) + in.isAvailable should be(false) + in.hasBeenPulled should be(true) + in.isClosed should be(false) + + in.cancel() + + lastEvents() should be(Set.empty) + out.isAvailable should be(true) + out.isClosed should be(false) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { in.grab() } + + stepAll() + + lastEvents() should be(Set(Cancel(out))) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + + out.complete() // This should have no effect now + stepAll() + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + + in.cancel() // This should have no effect now + stepAll() + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + + } + + s"propagate cancel while pull is in flight (chasing = $chasing)" in new PortTestSetup(chasing) { + in.pull() + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(false) + in.isAvailable should be(false) + in.hasBeenPulled should be(true) + in.isClosed should be(false) + + in.cancel() + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(false) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + + stepAll() + + lastEvents() should be(Set(Cancel(out))) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + + out.complete() // This should have no effect now + stepAll() + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + + in.cancel() // This should have no effect now + stepAll() + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + } + + s"propagate cancel while push is in flight (chasing = $chasing)" in new PortTestSetup(chasing) { + in.pull() + stepAll() + clearEvents() + + out.push(0) + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(false) + in.isAvailable should be(false) + in.hasBeenPulled should be(true) + in.isClosed should be(false) + + in.cancel() + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(false) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + + stepAll() + + lastEvents() should be(Set(Cancel(out))) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + + out.complete() // This should have no effect now + stepAll() + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + + in.cancel() // This should have no effect now + stepAll() + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + } + + s"ignore push while cancelling (chasing = $chasing)" in new PortTestSetup(chasing) { + in.pull() + stepAll() + clearEvents() + + in.cancel() + out.push(0) + // While the push event is not enqueued at this point, we should still report the state correctly + out.isAvailable should be(false) + + stepAll() + + lastEvents() should be(Set(Cancel(out))) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + } + + s"clear ungrabbed element even when cancelled (chasing = $chasing)" in new PortTestSetup(chasing) { + in.pull() + stepAll() + clearEvents() + out.push(0) + stepAll() + + lastEvents() should be(Set(OnNext(in, 0))) + + in.cancel() + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(false) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { in.grab() } + + stepAll() + lastEvents() should be(Set(Cancel(out))) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + } + + s"ignore any completion if they are concurrent (cancel first) (chasing = $chasing)" in new PortTestSetup(chasing) { + in.cancel() + out.complete() + + stepAll() + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + } + + s"ignore any completion if they are concurrent (complete first) (chasing = $chasing)" in new PortTestSetup(chasing) { + out.complete() + in.cancel() + + stepAll() + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + } + + s"ignore completion from a push-complete if cancelled while in flight (chasing = $chasing)" in new PortTestSetup(chasing) { + in.pull() + stepAll() + clearEvents() + + out.push(0) + out.complete() + in.cancel() + + stepAll() + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + } + + s"ignore completion from a push-complete if cancelled after onPush (chasing = $chasing)" in new PortTestSetup(chasing) { + in.pull() + stepAll() + clearEvents() + + out.push(0) + out.complete() + + step() + + lastEvents() should be(Set(OnNext(in, 0))) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(true) + in.hasBeenPulled should be(false) + in.isClosed should be(false) + an[IllegalArgumentException] should be thrownBy { out.push(0) } + in.grab() should ===(0) + + in.cancel() + stepAll() + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + } + + s"not allow to grab element before it arrives (chasing = $chasing)" in new PortTestSetup(chasing) { + in.pull() + stepAll() + out.push(0) + + an[IllegalArgumentException] should be thrownBy { in.grab() } + } + + s"not allow to grab element if already cancelled (chasing = $chasing)" in new PortTestSetup(chasing) { + in.pull() + stepAll() + + out.push(0) + in.cancel() + + stepAll() + + an[IllegalArgumentException] should be thrownBy { in.grab() } + } + + s"propagate failure while downstream is active (chasing = $chasing)" in new PortTestSetup(chasing) { + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(false) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(false) + + out.fail(TE("test")) + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(false) + an[IllegalArgumentException] should be thrownBy { out.push(0) } + + stepAll() + + lastEvents() should be(Set(OnError(in, TE("test")))) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + + in.cancel() // This should have no effect now + stepAll() + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + + out.complete() // This should have no effect now + stepAll() + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + } + + s"propagate failure while upstream is active (chasing = $chasing)" in new PortTestSetup(chasing) { + in.pull() + stepAll() + + lastEvents() should be(Set(RequestOne(out))) + out.isAvailable should be(true) + out.isClosed should be(false) + in.isAvailable should be(false) + in.hasBeenPulled should be(true) + in.isClosed should be(false) + + out.fail(TE("test")) + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(true) + in.isClosed should be(false) + an[IllegalArgumentException] should be thrownBy { out.push(0) } + + stepAll() + + lastEvents() should be(Set(OnError(in, TE("test")))) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + + in.cancel() // This should have no effect now + stepAll() + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + + out.complete() // This should have no effect now + stepAll() + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + + } + + s"propagate failure while pull is in flight (chasing = $chasing)" in new PortTestSetup(chasing) { + in.pull() + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(false) + in.isAvailable should be(false) + in.hasBeenPulled should be(true) + in.isClosed should be(false) + + out.fail(TE("test")) + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(true) + in.isClosed should be(false) + an[IllegalArgumentException] should be thrownBy { out.push(0) } + + stepAll() + + lastEvents() should be(Set(OnError(in, TE("test")))) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + + in.cancel() // This should have no effect now + stepAll() + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + + out.complete() // This should have no effect now + stepAll() + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + } + + s"propagate failure while push is in flight (chasing = $chasing)" in new PortTestSetup(chasing) { + in.pull() + stepAll() + clearEvents() + + out.push(0) + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(false) + in.isAvailable should be(false) + in.hasBeenPulled should be(true) + in.isClosed should be(false) + + out.fail(TE("test")) + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(true) + in.isClosed should be(false) + an[IllegalArgumentException] should be thrownBy { out.push(0) } + + step() + + lastEvents() should be(Set(OnNext(in, 0))) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(true) + in.hasBeenPulled should be(false) + in.isClosed should be(false) + an[IllegalArgumentException] should be thrownBy { out.push(0) } + in.grab() should ===(0) + an[IllegalArgumentException] should be thrownBy { in.grab() } + + step() + + lastEvents() should be(Set(OnError(in, TE("test")))) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + + out.complete() // This should have no effect now + stepAll() + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + } + + s"propagate failure while push is in flight and keep ungrabbed element (chasing = $chasing)" in new PortTestSetup(chasing) { + in.pull() + stepAll() + clearEvents() + + out.push(0) + out.fail(TE("test")) + step() + + lastEvents() should be(Set(OnNext(in, 0))) + step() + + lastEvents() should be(Set(OnError(in, TE("test")))) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(true) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + in.grab() should ===(0) + } + + s"ignore pull while failing (chasing = $chasing)" in new PortTestSetup(chasing) { + out.fail(TE("test")) + in.pull() + in.hasBeenPulled should be(true) + + stepAll() + + lastEvents() should be(Set(OnError(in, TE("test")))) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + } + + s"ignore any failure completion if they are concurrent (cancel first) (chasing = $chasing)" in new PortTestSetup(chasing) { + in.cancel() + out.fail(TE("test")) + + stepAll() + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + } + + s"ignore any failure completion if they are concurrent (complete first) (chasing = $chasing)" in new PortTestSetup(chasing) { + out.fail(TE("test")) + in.cancel() + + stepAll() + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + } + + s"ignore failure from a push-then-fail if cancelled while in flight (chasing = $chasing)" in new PortTestSetup(chasing) { + in.pull() + stepAll() + clearEvents() + + out.push(0) + out.fail(TE("test")) + in.cancel() + + stepAll() + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + } + + s"ignore failure from a push-then-fail if cancelled after onPush (chasing = $chasing)" in new PortTestSetup(chasing) { + in.pull() + stepAll() + clearEvents() + + out.push(0) + out.fail(TE("test")) + + step() + + lastEvents() should be(Set(OnNext(in, 0))) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(true) + in.hasBeenPulled should be(false) + in.isClosed should be(false) + an[IllegalArgumentException] should be thrownBy { out.push(0) } + in.grab() should ===(0) + + in.cancel() + stepAll() + + lastEvents() should be(Set.empty) + out.isAvailable should be(false) + out.isClosed should be(true) + in.isAvailable should be(false) + in.hasBeenPulled should be(false) + in.isClosed should be(true) + an[IllegalArgumentException] should be thrownBy { in.pull() } + an[IllegalArgumentException] should be thrownBy { out.push(0) } + an[IllegalArgumentException] should be thrownBy { in.grab() } + } } } diff --git a/akka-stream-tests/src/test/scala/akka/stream/impl/fusing/GraphInterpreterSpecKit.scala b/akka-stream-tests/src/test/scala/akka/stream/impl/fusing/GraphInterpreterSpecKit.scala index c0bd29dd10..f571886fe4 100644 --- a/akka-stream-tests/src/test/scala/akka/stream/impl/fusing/GraphInterpreterSpecKit.scala +++ b/akka-stream-tests/src/test/scala/akka/stream/impl/fusing/GraphInterpreterSpecKit.scala @@ -176,10 +176,29 @@ trait GraphInterpreterSpecKit extends StreamSpec { } - abstract class PortTestSetup extends TestSetup { + abstract class PortTestSetup(chasing: Boolean = false) extends TestSetup { val out = new UpstreamPortProbe[Int] val in = new DownstreamPortProbe[Int] + class EventPropagateStage extends GraphStage[FlowShape[Int, Int]] { + val in = Inlet[Int]("Propagate.in") + val out = Outlet[Int]("Propagate.out") + override val shape: FlowShape[Int, Int] = FlowShape(in, out) + + override def createLogic(inheritedAttributes: Attributes): GraphStageLogic = new GraphStageLogic(shape) with InHandler with OutHandler { + override def onPush(): Unit = push(out, grab(in)) + override def onPull(): Unit = pull(in) + override def onUpstreamFinish(): Unit = complete(out) + override def onUpstreamFailure(ex: Throwable): Unit = fail(out, ex) + override def onDownstreamFinish(): Unit = cancel(in) + + setHandlers(in, out, this) + } + } + + // step() means different depending whether we have a stage between the two probes or not + override def step(): Unit = interpreter.execute(eventLimit = if (!chasing) 1 else 2) + class UpstreamPortProbe[T] extends UpstreamProbe[T]("upstreamPort") { def isAvailable: Boolean = isAvailable(out) def isClosed: Boolean = isClosed(out) @@ -215,16 +234,27 @@ trait GraphInterpreterSpecKit extends StreamSpec { }) } - private val assembly = new GraphAssembly( - stages = Array.empty, - originalAttributes = Array.empty, - ins = Array(null), - inOwners = Array(-1), - outs = Array(null), - outOwners = Array(-1)) + private val assembly = if (!chasing) { + new GraphAssembly( + stages = Array.empty, + originalAttributes = Array.empty, + ins = Array(null), + inOwners = Array(-1), + outs = Array(null), + outOwners = Array(-1)) + } else { + val propagateStage = new EventPropagateStage + new GraphAssembly( + stages = Array(propagateStage), + originalAttributes = Array(Attributes.none), + ins = Array(propagateStage.in, null), + inOwners = Array(0, -1), + outs = Array(null, propagateStage.out), + outOwners = Array(-1, 0)) + } manualInit(assembly) - interpreter.attachDownstreamBoundary(interpreter.connections(0), in) + interpreter.attachDownstreamBoundary(interpreter.connections(if (chasing) 1 else 0), in) interpreter.attachUpstreamBoundary(interpreter.connections(0), out) interpreter.init(null) } diff --git a/akka-stream/src/main/scala/akka/stream/impl/fusing/GraphInterpreter.scala b/akka-stream/src/main/scala/akka/stream/impl/fusing/GraphInterpreter.scala index 57a1d6ea5c..dfc8fc8d6c 100644 --- a/akka-stream/src/main/scala/akka/stream/impl/fusing/GraphInterpreter.scala +++ b/akka-stream/src/main/scala/akka/stream/impl/fusing/GraphInterpreter.scala @@ -842,6 +842,12 @@ final class GraphInterpreter( connection.portState = currentState | (OutClosed | InFailed) connection.slot = Failed(ex, connection.slot) if ((currentState & (Pulling | Pushing)) == 0) enqueue(connection) + else if (chasedPush eq connection) { + // Abort chasing so Failure is not lost (chasing does NOT decode the event but assumes it to be a PUSH + // but we just changed the event!) + chasedPush = NoEvent + enqueue(connection) + } } if ((currentState & OutClosed) == 0) completeConnection(connection.outOwnerId) } @@ -853,6 +859,12 @@ final class GraphInterpreter( if ((currentState & OutClosed) == 0) { connection.slot = Empty if ((currentState & (Pulling | Pushing | InClosed)) == 0) enqueue(connection) + else if (chasedPull eq connection) { + // Abort chasing so Cancel is not lost (chasing does NOT decode the event but assumes it to be a PULL + // but we just changed the event!) + chasedPull = NoEvent + enqueue(connection) + } } if ((currentState & InClosed) == 0) completeConnection(connection.inOwnerId) } From e65e63e8c1e536d86f34ecae78f3d6f4699ce6d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Andr=C3=A9n?= Date: Tue, 13 Sep 2016 10:51:40 +0200 Subject: [PATCH 12/23] =per Higher recovery timeout in PersistentActorRecoveryTimeoutSpec to not fail on slow/gc #20728 (#21445) --- .../akka/persistence/PersistentActorRecoveryTimeoutSpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/akka-persistence/src/test/scala/akka/persistence/PersistentActorRecoveryTimeoutSpec.scala b/akka-persistence/src/test/scala/akka/persistence/PersistentActorRecoveryTimeoutSpec.scala index 2bb291a65f..2c275ce521 100644 --- a/akka-persistence/src/test/scala/akka/persistence/PersistentActorRecoveryTimeoutSpec.scala +++ b/akka-persistence/src/test/scala/akka/persistence/PersistentActorRecoveryTimeoutSpec.scala @@ -15,7 +15,7 @@ object PersistentActorRecoveryTimeoutSpec { SteppingInmemJournal.config(PersistentActorRecoveryTimeoutSpec.journalId).withFallback( ConfigFactory.parseString( """ - |akka.persistence.journal.stepping-inmem.recovery-event-timeout=100ms + |akka.persistence.journal.stepping-inmem.recovery-event-timeout=1s """.stripMargin)).withFallback(PersistenceSpec.config("stepping-inmem", "PersistentActorRecoveryTimeoutSpec")) class TestActor(probe: ActorRef) extends NamedPersistentActor("recovery-timeout-actor") { From 74162ddc9f58df1910837162cff2ac05c4ae8f49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Andre=CC=81n?= Date: Tue, 13 Sep 2016 11:46:18 +0200 Subject: [PATCH 13/23] Verbose logging in LeveldbAtLeastOnceDeliverySpec to figure out #20724 --- .../test/scala/akka/persistence/AtLeastOnceDeliverySpec.scala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/akka-persistence/src/test/scala/akka/persistence/AtLeastOnceDeliverySpec.scala b/akka-persistence/src/test/scala/akka/persistence/AtLeastOnceDeliverySpec.scala index 9d8d936816..7f0f2a00bc 100644 --- a/akka-persistence/src/test/scala/akka/persistence/AtLeastOnceDeliverySpec.scala +++ b/akka-persistence/src/test/scala/akka/persistence/AtLeastOnceDeliverySpec.scala @@ -408,6 +408,8 @@ abstract class AtLeastOnceDeliverySpec(config: Config) extends PersistenceSpec(c } class LeveldbAtLeastOnceDeliverySpec extends AtLeastOnceDeliverySpec( - PersistenceSpec.config("leveldb", "AtLeastOnceDeliverySpec")) + PersistenceSpec.config("leveldb", "AtLeastOnceDeliverySpec") + .withFallback(ConfigFactory.parseString("akka.loglevel=debug")) // to iron out #20724 +) class InmemAtLeastOnceDeliverySpec extends AtLeastOnceDeliverySpec(PersistenceSpec.config("inmem", "AtLeastOnceDeliverySpec")) From 9b73fefdce55e904479ecaaaa93802c4e545fc81 Mon Sep 17 00:00:00 2001 From: gosubpl Date: Tue, 13 Sep 2016 14:32:02 +0200 Subject: [PATCH 14/23] =htc don't encode known to be empty entities (#21393) (#21396) * =htc don't encode known to be empty entities (#21393) * =htc added missing headers in the improved marshaller (#21393) --- .../marshalling/MarshallingSpec.scala | 8 +++++++ .../directives/CodingDirectivesSpec.scala | 23 +++++++++++++++++++ .../akka/http/scaladsl/coding/Encoder.scala | 5 +++- .../PredefinedToResponseMarshallers.scala | 13 ++++++++++- 4 files changed, 47 insertions(+), 2 deletions(-) diff --git a/akka-http-tests/src/test/scala/akka/http/scaladsl/marshalling/MarshallingSpec.scala b/akka-http-tests/src/test/scala/akka/http/scaladsl/marshalling/MarshallingSpec.scala index ab66c92a98..fb8426b77f 100644 --- a/akka-http-tests/src/test/scala/akka/http/scaladsl/marshalling/MarshallingSpec.scala +++ b/akka-http-tests/src/test/scala/akka/http/scaladsl/marshalling/MarshallingSpec.scala @@ -47,6 +47,14 @@ class MarshallingSpec extends FreeSpec with Matchers with BeforeAndAfterAll with marshalToResponse(StatusCodes.EnhanceYourCalm) shouldEqual HttpResponse(StatusCodes.EnhanceYourCalm, entity = HttpEntity(StatusCodes.EnhanceYourCalm.defaultMessage)) } + "fromStatusCodeAndHeadersAndValue should properly marshal entities that are not supposed to have a body" in { + marshalToResponse((StatusCodes.NoContent, "This Content was intentionally left blank.")) shouldEqual + HttpResponse(StatusCodes.NoContent, entity = HttpEntity.Empty) + } + "fromStatusCodeAndHeadersAndValue should properly marshal entities that contain pre-defined content" in { + marshalToResponse((StatusCodes.EnhanceYourCalm, "Patience, young padawan!")) shouldEqual + HttpResponse(StatusCodes.EnhanceYourCalm, entity = HttpEntity("Patience, young padawan!")) + } } "The GenericMarshallers" - { diff --git a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/CodingDirectivesSpec.scala b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/CodingDirectivesSpec.scala index fa52405e97..12024906ae 100644 --- a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/CodingDirectivesSpec.scala +++ b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/CodingDirectivesSpec.scala @@ -176,6 +176,29 @@ class CodingDirectivesSpec extends RoutingSpec with Inside { encodeResponseWith(Deflate) { nope } } ~> check { strictify(responseEntity) shouldEqual HttpEntity(ContentType(`text/plain`, `UTF-8`), nopeDeflated) } } + "not encode the response content with GZIP if the response is of status not allowing entity" in { + Post() ~> { + encodeResponseWith(Gzip) { complete { StatusCodes.NoContent } } + } ~> check { + response should haveNoContentEncoding + response shouldEqual HttpResponse(StatusCodes.NoContent, entity = HttpEntity.Empty) + } + } + "not encode the response content with Deflate if the response is of status not allowing entity" in { + Post() ~> { + encodeResponseWith(Deflate) { complete((100, "Let's continue!")) } + } ~> check { + response should haveNoContentEncoding + response shouldEqual HttpResponse(StatusCodes.Continue, entity = HttpEntity.Empty) + } + } + "encode the response content with GZIP if the response is of status allowing entity" in { + Post() ~> { + encodeResponseWith(Gzip) { nope } + } ~> check { + response should haveContentEncoding(gzip) + } + } } "the Gzip encoder" should { diff --git a/akka-http/src/main/scala/akka/http/scaladsl/coding/Encoder.scala b/akka-http/src/main/scala/akka/http/scaladsl/coding/Encoder.scala index 6204cd8927..0f6a0e2af1 100644 --- a/akka-http/src/main/scala/akka/http/scaladsl/coding/Encoder.scala +++ b/akka-http/src/main/scala/akka/http/scaladsl/coding/Encoder.scala @@ -43,7 +43,10 @@ trait Encoder { } object Encoder { - val DefaultFilter: HttpMessage ⇒ Boolean = isCompressible _ + val DefaultFilter: HttpMessage ⇒ Boolean = { + case req: HttpRequest ⇒ isCompressible(req) + case res @ HttpResponse(status, _, _, _) ⇒ isCompressible(res) && status.allowsEntity + } private[coding] def isCompressible(msg: HttpMessage): Boolean = msg.entity.contentType.mediaType.isCompressible diff --git a/akka-http/src/main/scala/akka/http/scaladsl/marshalling/PredefinedToResponseMarshallers.scala b/akka-http/src/main/scala/akka/http/scaladsl/marshalling/PredefinedToResponseMarshallers.scala index 49ff565a37..d587566a20 100644 --- a/akka-http/src/main/scala/akka/http/scaladsl/marshalling/PredefinedToResponseMarshallers.scala +++ b/akka-http/src/main/scala/akka/http/scaladsl/marshalling/PredefinedToResponseMarshallers.scala @@ -37,6 +37,16 @@ trait PredefinedToResponseMarshallers extends LowPriorityToResponseMarshallerImp HttpResponse(status, entity = responseEntity) } + implicit val fromStatusCodeAndHeaders: TRM[(StatusCode, immutable.Seq[HttpHeader])] = + Marshaller.withOpenCharset(`text/plain`) { (statusAndHeaders, charset) ⇒ + val status = statusAndHeaders._1 + val headers = statusAndHeaders._2 + val responseEntity = + if (status.allowsEntity) HttpEntity(status.defaultMessage) + else HttpEntity.Empty + HttpResponse(status, headers, entity = responseEntity) + } + implicit def fromStatusCodeAndValue[S, T](implicit sConv: S ⇒ StatusCode, mt: ToEntityMarshaller[T]): TRM[(S, T)] = fromStatusCodeAndHeadersAndValue[T] compose { case (status, value) ⇒ (sConv(status), Nil, value) } @@ -47,7 +57,8 @@ trait PredefinedToResponseMarshallers extends LowPriorityToResponseMarshallerImp implicit def fromStatusCodeAndHeadersAndValue[T](implicit mt: ToEntityMarshaller[T]): TRM[(StatusCode, immutable.Seq[HttpHeader], T)] = Marshaller(implicit ec ⇒ { - case (status, headers, value) ⇒ mt(value).fast map (_ map (_ map (HttpResponse(status, headers, _)))) + case (status, headers, value) if (status.allowsEntity) ⇒ mt(value).fast map (_ map (_ map (HttpResponse(status, headers, _)))) + case (status, headers, _) ⇒ fromStatusCodeAndHeaders((status, headers)) }) implicit def fromEntityStreamingSupportAndByteStringMarshaller[T, M](implicit s: EntityStreamingSupport, m: ToByteStringMarshaller[T]): ToResponseMarshaller[Source[T, M]] = { From 79d8ec87fcd91da7ce80440bdb874f53d8da5311 Mon Sep 17 00:00:00 2001 From: Patrik Nordwall Date: Tue, 13 Sep 2016 16:10:49 +0200 Subject: [PATCH 15/23] Delay should not pull when buffer is full with Backpressure strategy, #21334 Additionally * nano time conversion and calculation fix * refactoring of Delay.onPush --- .../akka/stream/scaladsl/FlowDelaySpec.scala | 13 +++ .../scala/akka/stream/impl/fusing/Ops.scala | 82 +++++++++++++------ 2 files changed, 68 insertions(+), 27 deletions(-) diff --git a/akka-stream-tests/src/test/scala/akka/stream/scaladsl/FlowDelaySpec.scala b/akka-stream-tests/src/test/scala/akka/stream/scaladsl/FlowDelaySpec.scala index e094929011..adcc979174 100644 --- a/akka-stream-tests/src/test/scala/akka/stream/scaladsl/FlowDelaySpec.scala +++ b/akka-stream-tests/src/test/scala/akka/stream/scaladsl/FlowDelaySpec.scala @@ -13,12 +13,14 @@ import akka.stream.{ ActorMaterializer, Attributes, BufferOverflowException, Del import scala.concurrent.{ Await, Future } import scala.concurrent.duration._ import scala.util.control.NoStackTrace +import akka.stream.ThrottleMode class FlowDelaySpec extends StreamSpec { implicit val materializer = ActorMaterializer() "A Delay" must { + "deliver elements with some time shift" in { Await.result( Source(1 to 10).delay(1.seconds).grouped(100).runWith(Sink.head), @@ -156,5 +158,16 @@ class FlowDelaySpec extends StreamSpec { expectMsg(Done) } + "not overflow buffer when DelayOverflowStrategy.backpressure" in { + val probe = Source(1 to 6).delay(100.millis, DelayOverflowStrategy.backpressure) + .withAttributes(Attributes.inputBuffer(2, 2)) + .throttle(1, 200.millis, 1, ThrottleMode.Shaping) + .runWith(TestSink.probe) + + probe.request(10) + .expectNextN(1 to 6) + .expectComplete() + } + } } diff --git a/akka-stream/src/main/scala/akka/stream/impl/fusing/Ops.scala b/akka-stream/src/main/scala/akka/stream/impl/fusing/Ops.scala index 96c7ec907c..57d63a9ee6 100644 --- a/akka-stream/src/main/scala/akka/stream/impl/fusing/Ops.scala +++ b/akka-stream/src/main/scala/akka/stream/impl/fusing/Ops.scala @@ -3,6 +3,7 @@ */ package akka.stream.impl.fusing +import java.util.concurrent.TimeUnit.NANOSECONDS import akka.event.Logging.LogLevel import akka.event.{ LogSource, Logging, LoggingAdapter } import akka.stream.Attributes.{ InputBuffer, LogLevels } @@ -1310,74 +1311,101 @@ final class Delay[T](val d: FiniteDuration, val strategy: DelayOverflowStrategy) case None ⇒ throw new IllegalStateException(s"Couldn't find InputBuffer Attribute for $this") case Some(InputBuffer(min, max)) ⇒ max } + val delayMillis = d.toMillis var buffer: BufferImpl[(Long, T)] = _ // buffer has pairs timestamp with upstream element - var willStop = false override def preStart(): Unit = buffer = BufferImpl(size, materializer) - //FIXME rewrite into distinct strategy functions to avoid matching on strategy for every input when full - def onPush(): Unit = { - if (buffer.isFull) strategy match { - case EmitEarly ⇒ + val onPushWhenBufferFull: () ⇒ Unit = strategy match { + case EmitEarly ⇒ + () ⇒ { if (!isTimerActive(timerName)) push(out, buffer.dequeue()._2) else { cancelTimer(timerName) onTimer(timerName) } - case DropHead ⇒ + } + case DropHead ⇒ + () ⇒ { buffer.dropHead() - grabAndPull(true) - case DropTail ⇒ + grabAndPull() + } + case DropTail ⇒ + () ⇒ { buffer.dropTail() - grabAndPull(true) - case DropNew ⇒ + grabAndPull() + } + case DropNew ⇒ + () ⇒ { grab(in) if (!isTimerActive(timerName)) scheduleOnce(timerName, d) - case DropBuffer ⇒ + } + case DropBuffer ⇒ + () ⇒ { buffer.clear() - grabAndPull(true) - case Fail ⇒ + grabAndPull() + } + case Fail ⇒ + () ⇒ { failStage(new BufferOverflowException(s"Buffer overflow for delay combinator (max capacity was: $size)!")) - case Backpressure ⇒ throw new IllegalStateException("Delay buffer must never overflow in Backpressure mode") - } + } + case Backpressure ⇒ + () ⇒ { + throw new IllegalStateException("Delay buffer must never overflow in Backpressure mode") + } + } + + def onPush(): Unit = { + if (buffer.isFull) + onPushWhenBufferFull() else { - grabAndPull(strategy != Backpressure || buffer.used < size - 1) - if (!isTimerActive(timerName)) scheduleOnce(timerName, d) + grabAndPull() + if (!isTimerActive(timerName)) { + scheduleOnce(timerName, d) + } } } - def grabAndPull(pullCondition: Boolean): Unit = { + def pullCondition: Boolean = + strategy != Backpressure || buffer.used < size + + def grabAndPull(): Unit = { buffer.enqueue((System.nanoTime(), grab(in))) if (pullCondition) pull(in) } - override def onUpstreamFinish(): Unit = { - if (isAvailable(out) && isTimerActive(timerName)) willStop = true - else completeStage() - } + override def onUpstreamFinish(): Unit = + completeIfReady() def onPull(): Unit = { if (!isTimerActive(timerName) && !buffer.isEmpty && nextElementWaitTime() < 0) push(out, buffer.dequeue()._2) - if (!willStop && !hasBeenPulled(in)) pull(in) + if (!isClosed(in) && !hasBeenPulled(in) && pullCondition) + pull(in) + completeIfReady() } setHandler(in, this) setHandler(out, this) - def completeIfReady(): Unit = if (willStop && buffer.isEmpty) completeStage() + def completeIfReady(): Unit = if (isClosed(in) && buffer.isEmpty) completeStage() - def nextElementWaitTime(): Long = d.toMillis - (System.nanoTime() - buffer.peek()._1) * 1000 * 1000 + def nextElementWaitTime(): Long = { + delayMillis - NANOSECONDS.toMillis(System.nanoTime() - buffer.peek()._1) + } final override protected def onTimer(key: Any): Unit = { - push(out, buffer.dequeue()._2) + if (isAvailable(out)) + push(out, buffer.dequeue()._2) + if (!buffer.isEmpty) { val waitTime = nextElementWaitTime() - if (waitTime > 10) scheduleOnce(timerName, waitTime.millis) + if (waitTime > 10) + scheduleOnce(timerName, waitTime.millis) } completeIfReady() } From 3531e901e903eeb6ea625e4351c1c2d436d79004 Mon Sep 17 00:00:00 2001 From: Patrik Nordwall Date: Wed, 14 Sep 2016 13:21:41 +0200 Subject: [PATCH 16/23] harden ConstantRateEntityRecoveryStrategySpec, #21230 --- ...nstantRateEntityRecoveryStrategySpec.scala | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/akka-cluster-sharding/src/test/scala/akka/cluster/sharding/ConstantRateEntityRecoveryStrategySpec.scala b/akka-cluster-sharding/src/test/scala/akka/cluster/sharding/ConstantRateEntityRecoveryStrategySpec.scala index 454218ca0f..1db037e190 100644 --- a/akka-cluster-sharding/src/test/scala/akka/cluster/sharding/ConstantRateEntityRecoveryStrategySpec.scala +++ b/akka-cluster-sharding/src/test/scala/akka/cluster/sharding/ConstantRateEntityRecoveryStrategySpec.scala @@ -5,36 +5,36 @@ import akka.testkit.AkkaSpec import scala.concurrent.{ Await, Future } import scala.concurrent.duration._ -import scala.language.postfixOps class ConstantRateEntityRecoveryStrategySpec extends AkkaSpec { import system.dispatcher - val strategy = EntityRecoveryStrategy.constantStrategy(system, 500 millis, 2) + val strategy = EntityRecoveryStrategy.constantStrategy(system, 1.second, 2) "ConstantRateEntityRecoveryStrategy" must { "recover entities" in { val entities = Set[EntityId]("1", "2", "3", "4", "5") - val startTime = System.currentTimeMillis() + val startTime = System.nanoTime() val resultWithTimes = strategy.recoverEntities(entities).map( - _.map(entityIds ⇒ (entityIds, System.currentTimeMillis() - startTime)) - ) + _.map(entityIds ⇒ (entityIds → (System.nanoTime() - startTime).nanos))) - val result = Await.result(Future.sequence(resultWithTimes), 4 seconds).toList.sortWith(_._2 < _._2) + val result = Await.result(Future.sequence(resultWithTimes), 6.seconds) + .toVector.sortBy { case (_, duration) ⇒ duration } result.size should ===(3) val scheduledEntities = result.map(_._1) - scheduledEntities.head.size should ===(2) + scheduledEntities(0).size should ===(2) scheduledEntities(1).size should ===(2) scheduledEntities(2).size should ===(1) - scheduledEntities.foldLeft(Set[EntityId]())(_ ++ _) should ===(entities) + scheduledEntities.flatten.toSet should ===(entities) - val times = result.map(_._2) + val timesMillis = result.map(_._2.toMillis) - times.head should ===(500L +- 30L) - times(1) should ===(1000L +- 30L) - times(2) should ===(1500L +- 30L) + // scheduling will not happen too early + timesMillis(0) should ===(1400L +- 500) + timesMillis(1) should ===(2400L +- 500L) + timesMillis(2) should ===(3400L +- 500L) } "not recover when no entities to recover" in { From e493bdc1b80a549575d8e161e9e2e831c407bd1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20Andr=C3=A9n?= Date: Wed, 14 Sep 2016 13:22:06 +0200 Subject: [PATCH 17/23] Remove hardcoded port number in TcpConnectionSpec, #21375 --- .../src/test/scala/akka/io/TcpConnectionSpec.scala | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/akka-actor-tests/src/test/scala/akka/io/TcpConnectionSpec.scala b/akka-actor-tests/src/test/scala/akka/io/TcpConnectionSpec.scala index 5272f0b471..5524a0a678 100644 --- a/akka-actor-tests/src/test/scala/akka/io/TcpConnectionSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/io/TcpConnectionSpec.scala @@ -5,11 +5,12 @@ package akka.io import java.io.{ File, IOException } -import java.net.{ ServerSocket, URLClassLoader, InetSocketAddress } +import java.net.{ InetSocketAddress, ServerSocket, URLClassLoader } import java.nio.ByteBuffer import java.nio.channels._ import java.nio.channels.spi.SelectorProvider import java.nio.channels.SelectionKey._ + import com.typesafe.config.ConfigFactory import scala.annotation.tailrec @@ -21,8 +22,8 @@ import akka.io.Tcp._ import akka.io.SelectionHandler._ import akka.io.Inet.SocketOption import akka.actor._ -import akka.testkit.{ AkkaSpec, EventFilter, TestActorRef, TestProbe } -import akka.util.{ Helpers, ByteString } +import akka.testkit.{ AkkaSpec, EventFilter, SocketUtil, TestActorRef, TestProbe } +import akka.util.{ ByteString, Helpers } import akka.testkit.SocketUtil._ import java.util.Random @@ -826,7 +827,8 @@ class TcpConnectionSpec extends AkkaSpec(""" "report abort before handler is registered (reproducer from #15033)" in { // This test needs the OP_CONNECT workaround on Windows, see original report #15033 and parent ticket #15766 - val bindAddress = new InetSocketAddress(23402) + val port = SocketUtil.temporaryServerAddress().getPort + val bindAddress = new InetSocketAddress(port) val serverSocket = new ServerSocket(bindAddress.getPort, 100, bindAddress.getAddress) val connectionProbe = TestProbe() From c1a840b2e9dfcd8d06fa06074461fbd972233a15 Mon Sep 17 00:00:00 2001 From: Ortigali Date: Tue, 20 Sep 2016 12:34:11 +0500 Subject: [PATCH 18/23] MDC support for LoggingReceive #21361 --- .../scala/akka/event/LoggingReceiveSpec.scala | 17 +++++++++++++++++ .../main/scala/akka/event/LoggingReceive.scala | 16 +++++++++++----- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/akka-actor-tests/src/test/scala/akka/event/LoggingReceiveSpec.scala b/akka-actor-tests/src/test/scala/akka/event/LoggingReceiveSpec.scala index 02e84f44d1..b9fbff4f02 100644 --- a/akka-actor-tests/src/test/scala/akka/event/LoggingReceiveSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/event/LoggingReceiveSpec.scala @@ -118,6 +118,23 @@ class LoggingReceiveSpec extends WordSpec with BeforeAndAfterAll { } } + "log with MDC" in { + new TestKit(appLogging) { + system.eventStream.subscribe(testActor, classOf[Logging.Debug]) + val myMDC = Map("hello" → "mdc") + val a = system.actorOf(Props(new Actor with DiagnosticActorLogging { + override def mdc(currentMessage: Any) = myMDC + def receive = LoggingReceive { + case "hello" ⇒ + } + })) + a ! "hello" + expectMsgPF(hint = "Logging.Debug2") { + case m: Logging.Debug2 if m.mdc == myMDC ⇒ () + } + } + } + } "An Actor" must { diff --git a/akka-actor/src/main/scala/akka/event/LoggingReceive.scala b/akka-actor/src/main/scala/akka/event/LoggingReceive.scala index 4bd5776af2..f4af3fd48f 100644 --- a/akka-actor/src/main/scala/akka/event/LoggingReceive.scala +++ b/akka-actor/src/main/scala/akka/event/LoggingReceive.scala @@ -8,6 +8,7 @@ import language.existentials import akka.actor.Actor.Receive import akka.actor.ActorContext import akka.actor.ActorCell +import akka.actor.DiagnosticActorLogging import akka.event.Logging.Debug object LoggingReceive { @@ -52,13 +53,18 @@ class LoggingReceive(source: Option[AnyRef], r: Receive, label: Option[String])( def isDefinedAt(o: Any): Boolean = { val handled = r.isDefinedAt(o) if (context.system.eventStream.logLevel >= Logging.DebugLevel) { - val (str, clazz) = LogSource.fromAnyRef(source getOrElse context.asInstanceOf[ActorCell].actor) - context.system.eventStream.publish(Debug(str, clazz, "received " + (if (handled) "handled" else "unhandled") + " message " + o - + " from " + context.sender() - + (label match { + val src = source getOrElse context.asInstanceOf[ActorCell].actor + val (str, clazz) = LogSource.fromAnyRef(src) + val message = "received " + (if (handled) "handled" else "unhandled") + " message " + o + " from " + context.sender() + + (label match { case Some(l) ⇒ " in state " + l case _ ⇒ "" - }))) + }) + val event = src match { + case a: DiagnosticActorLogging ⇒ Debug(str, clazz, message, a.log.mdc) + case _ ⇒ Debug(str, clazz, message) + } + context.system.eventStream.publish(event) } handled } From 94d7237d1744db81efad321d538acb1f05b2c2ba Mon Sep 17 00:00:00 2001 From: Nafer Sanabria Date: Wed, 21 Sep 2016 01:41:56 -0500 Subject: [PATCH 19/23] +str add zipWithIndex to FlowOps #21290 --- akka-docs/rst/java/stream/stages-overview.rst | 10 +++++ .../rst/scala/stream/stages-overview.rst | 10 +++++ .../akka/stream/DslConsistencySpec.scala | 2 +- .../scaladsl/FlowZipWithIndexSpec.scala | 38 +++++++++++++++++++ .../main/scala/akka/stream/impl/Stages.scala | 6 +-- .../main/scala/akka/stream/javadsl/Flow.scala | 21 +++++++++- .../scala/akka/stream/javadsl/Source.scala | 15 ++++++++ .../scala/akka/stream/javadsl/SubFlow.scala | 15 ++++++++ .../scala/akka/stream/javadsl/SubSource.scala | 15 ++++++++ .../scala/akka/stream/scaladsl/Flow.scala | 23 +++++++++++ project/MiMa.scala | 4 ++ 11 files changed, 151 insertions(+), 8 deletions(-) create mode 100644 akka-stream-tests/src/test/scala/akka/stream/scaladsl/FlowZipWithIndexSpec.scala diff --git a/akka-docs/rst/java/stream/stages-overview.rst b/akka-docs/rst/java/stream/stages-overview.rst index 228ac0b580..41b73442bb 100644 --- a/akka-docs/rst/java/stream/stages-overview.rst +++ b/akka-docs/rst/java/stream/stages-overview.rst @@ -1230,6 +1230,16 @@ returned value downstream. **completes** when any upstream completes +zipWithIndex +^^^^^^^ +Zips elements of current flow with its indices. + +**emits** upstream emits an element and is paired with their index + +**backpressures** when downstream backpressures + +**completes** when upstream completes + concat ^^^^^^ After completion of the original upstream the elements of the given source will be emitted. diff --git a/akka-docs/rst/scala/stream/stages-overview.rst b/akka-docs/rst/scala/stream/stages-overview.rst index 7181cad197..7236b8635e 100644 --- a/akka-docs/rst/scala/stream/stages-overview.rst +++ b/akka-docs/rst/scala/stream/stages-overview.rst @@ -1222,6 +1222,16 @@ returned value downstream. **completes** when any upstream completes +zipWithIndex +^^^^^^^ +Zips elements of current flow with its indices. + +**emits** upstream emits an element and is paired with their index + +**backpressures** when downstream backpressures + +**completes** when upstream completes + concat ^^^^^^ After completion of the original upstream the elements of the given source will be emitted. diff --git a/akka-stream-tests/src/test/scala/akka/stream/DslConsistencySpec.scala b/akka-stream-tests/src/test/scala/akka/stream/DslConsistencySpec.scala index 235e295937..fd6873a981 100644 --- a/akka-stream-tests/src/test/scala/akka/stream/DslConsistencySpec.scala +++ b/akka-stream-tests/src/test/scala/akka/stream/DslConsistencySpec.scala @@ -66,7 +66,7 @@ class DslConsistencySpec extends WordSpec with Matchers { ("Flow" → List[Class[_]](sFlowClass, jFlowClass)) :: ("SubFlow" → List[Class[_]](sSubFlowClass, jSubFlowClass)) :: ("Sink" → List[Class[_]](sSinkClass, jSinkClass)) :: - ("RunanbleFlow" → List[Class[_]](sRunnableGraphClass, jRunnableGraphClass)) :: + ("RunnableFlow" → List[Class[_]](sRunnableGraphClass, jRunnableGraphClass)) :: Nil foreach { case (element, classes) ⇒ diff --git a/akka-stream-tests/src/test/scala/akka/stream/scaladsl/FlowZipWithIndexSpec.scala b/akka-stream-tests/src/test/scala/akka/stream/scaladsl/FlowZipWithIndexSpec.scala new file mode 100644 index 0000000000..e1e12df5d6 --- /dev/null +++ b/akka-stream-tests/src/test/scala/akka/stream/scaladsl/FlowZipWithIndexSpec.scala @@ -0,0 +1,38 @@ +/** + * Copyright (C) 2016 Lightbend Inc. + */ +package akka.stream.scaladsl + +import akka.stream.testkit.Utils._ +import akka.stream.{ ActorMaterializer, ActorMaterializerSettings } +import akka.stream.testkit.{ StreamSpec, TestSubscriber } + +class FlowZipWithIndexSpec extends StreamSpec { + + val settings = ActorMaterializerSettings(system) + .withInputBuffer(initialSize = 2, maxSize = 16) + + implicit val materializer = ActorMaterializer(settings) + + "A ZipWithIndex for Flow " must { + + "work in the happy case" in assertAllStagesStopped { + val probe = TestSubscriber.manualProbe[(Int, Long)]() + Source(7 to 10).zipWithIndex.runWith(Sink.fromSubscriber(probe)) + + val subscription = probe.expectSubscription() + + subscription.request(2) + probe.expectNext((7, 0L)) + probe.expectNext((8, 1L)) + + subscription.request(1) + probe.expectNext((9, 2L)) + subscription.request(1) + probe.expectNext((10, 3L)) + + probe.expectComplete() + } + + } +} diff --git a/akka-stream/src/main/scala/akka/stream/impl/Stages.scala b/akka-stream/src/main/scala/akka/stream/impl/Stages.scala index c64a01b012..5e3ee846aa 100644 --- a/akka-stream/src/main/scala/akka/stream/impl/Stages.scala +++ b/akka-stream/src/main/scala/akka/stream/impl/Stages.scala @@ -3,17 +3,12 @@ */ package akka.stream.impl -import akka.event.LoggingAdapter import akka.stream.ActorAttributes.SupervisionStrategy import akka.stream.Attributes._ import akka.stream.Supervision.Decider import akka.stream._ -import akka.stream.impl.StreamLayout._ import akka.stream.stage.AbstractStage.PushPullGraphStage import akka.stream.stage.Stage -import org.reactivestreams.Processor - -import scala.collection.immutable /** * INTERNAL API @@ -81,6 +76,7 @@ object Stages { val zip = name("zip") val zipN = name("zipN") val zipWithN = name("zipWithN") + val zipWithIndex = name("zipWithIndex") val unzip = name("unzip") val concat = name("concat") val orElse = name("orElse") diff --git a/akka-stream/src/main/scala/akka/stream/javadsl/Flow.scala b/akka-stream/src/main/scala/akka/stream/javadsl/Flow.scala index 9232aa090e..6f70412664 100644 --- a/akka-stream/src/main/scala/akka/stream/javadsl/Flow.scala +++ b/akka-stream/src/main/scala/akka/stream/javadsl/Flow.scala @@ -3,18 +3,20 @@ */ package akka.stream.javadsl -import akka.{ NotUsed, Done } +import akka.{ Done, NotUsed } import akka.event.LoggingAdapter -import akka.japi.{ function, Pair } +import akka.japi.{ Pair, function } import akka.stream.impl.{ ConstantFun, StreamLayout } import akka.stream._ import akka.stream.stage.Stage import org.reactivestreams.Processor + import scala.annotation.unchecked.uncheckedVariance import scala.concurrent.duration.FiniteDuration import akka.japi.Util import java.util.Comparator import java.util.concurrent.CompletionStage + import scala.compat.java8.FutureConverters._ object Flow { @@ -1634,6 +1636,21 @@ final class Flow[-In, +Out, +Mat](delegate: scaladsl.Flow[In, Out, Mat]) extends matF: function.Function2[Mat, M, M2]): javadsl.Flow[In, Out3, M2] = new Flow(delegate.zipWithMat[Out2, Out3, M, M2](that)(combinerToScala(combine))(combinerToScala(matF))) + /** + * Combine the elements of current flow into a stream of tuples consisting + * of all elements paired with their index. Indices start at 0. + * + * '''Emits when''' upstream emits an element and is paired with their index + * + * '''Backpressures when''' downstream backpressures + * + * '''Completes when''' upstream completes + * + * '''Cancels when''' downstream cancels + */ + def zipWithIndex: Flow[In, Pair[Out @uncheckedVariance, Long], Mat] = + new Flow(delegate.zipWithIndex.map { case (elem, index) ⇒ Pair(elem, index) }) + /** * If the first element has not passed through this stage before the provided timeout, the stream is failed * with a [[java.util.concurrent.TimeoutException]]. diff --git a/akka-stream/src/main/scala/akka/stream/javadsl/Source.scala b/akka-stream/src/main/scala/akka/stream/javadsl/Source.scala index abb3f0ead8..7020455889 100644 --- a/akka-stream/src/main/scala/akka/stream/javadsl/Source.scala +++ b/akka-stream/src/main/scala/akka/stream/javadsl/Source.scala @@ -873,6 +873,21 @@ final class Source[+Out, +Mat](delegate: scaladsl.Source[Out, Mat]) extends Grap matF: function.Function2[Mat, M, M2]): javadsl.Source[Out3, M2] = new Source(delegate.zipWithMat[Out2, Out3, M, M2](that)(combinerToScala(combine))(combinerToScala(matF))) + /** + * Combine the elements of current [[Source]] into a stream of tuples consisting + * of all elements paired with their index. Indices start at 0. + * + * '''Emits when''' upstream emits an element and is paired with their index + * + * '''Backpressures when''' downstream backpressures + * + * '''Completes when''' upstream completes + * + * '''Cancels when''' downstream cancels + */ + def zipWithIndex: javadsl.Source[Pair[Out @uncheckedVariance, Long], Mat] = + new Source(delegate.zipWithIndex.map { case (elem, index) ⇒ Pair(elem, index) }) + /** * Shortcut for running this `Source` with a foreach procedure. The given procedure is invoked * for each received element. diff --git a/akka-stream/src/main/scala/akka/stream/javadsl/SubFlow.scala b/akka-stream/src/main/scala/akka/stream/javadsl/SubFlow.scala index 2feb262ee8..1bea108a97 100644 --- a/akka-stream/src/main/scala/akka/stream/javadsl/SubFlow.scala +++ b/akka-stream/src/main/scala/akka/stream/javadsl/SubFlow.scala @@ -1112,6 +1112,21 @@ class SubFlow[-In, +Out, +Mat](delegate: scaladsl.SubFlow[Out, Mat, scaladsl.Flo combine: function.Function2[Out, Out2, Out3]): SubFlow[In, Out3, Mat] = new SubFlow(delegate.zipWith[Out2, Out3](that)(combinerToScala(combine))) + /** + * Combine the elements of current [[Flow]] into a stream of tuples consisting + * of all elements paired with their index. Indices start at 0. + * + * '''Emits when''' upstream emits an element and is paired with their index + * + * '''Backpressures when''' downstream backpressures + * + * '''Completes when''' upstream completes + * + * '''Cancels when''' downstream cancels + */ + def zipWithIndex: SubFlow[In, akka.japi.Pair[Out @uncheckedVariance, Long], Mat] = + new SubFlow(delegate.zipWithIndex.map { case (elem, index) ⇒ akka.japi.Pair(elem, index) }) + /** * If the first element has not passed through this stage before the provided timeout, the stream is failed * with a [[java.util.concurrent.TimeoutException]]. diff --git a/akka-stream/src/main/scala/akka/stream/javadsl/SubSource.scala b/akka-stream/src/main/scala/akka/stream/javadsl/SubSource.scala index 4cd8bc01e3..8113b0154a 100644 --- a/akka-stream/src/main/scala/akka/stream/javadsl/SubSource.scala +++ b/akka-stream/src/main/scala/akka/stream/javadsl/SubSource.scala @@ -1111,6 +1111,21 @@ class SubSource[+Out, +Mat](delegate: scaladsl.SubFlow[Out, Mat, scaladsl.Source combine: function.Function2[Out, Out2, Out3]): SubSource[Out3, Mat] = new SubSource(delegate.zipWith[Out2, Out3](that)(combinerToScala(combine))) + /** + * Combine the elements of current [[Source]] into a stream of tuples consisting + * of all elements paired with their index. Indices start at 0. + * + * '''Emits when''' upstream emits an element and is paired with their index + * + * '''Backpressures when''' downstream backpressures + * + * '''Completes when''' upstream completes + * + * '''Cancels when''' downstream cancels + */ + def zipWithIndex: javadsl.SubSource[akka.japi.Pair[Out @uncheckedVariance, Long], Mat] = + new SubSource(delegate.zipWithIndex.map { case (elem, index) ⇒ akka.japi.Pair(elem, index) }) + /** * If the first element has not passed through this stage before the provided timeout, the stream is failed * with a [[java.util.concurrent.TimeoutException]]. diff --git a/akka-stream/src/main/scala/akka/stream/scaladsl/Flow.scala b/akka-stream/src/main/scala/akka/stream/scaladsl/Flow.scala index cc6c1d3a10..f5f2a9677f 100644 --- a/akka-stream/src/main/scala/akka/stream/scaladsl/Flow.scala +++ b/akka-stream/src/main/scala/akka/stream/scaladsl/Flow.scala @@ -1655,6 +1655,29 @@ trait FlowOps[+Out, +Mat] { FlowShape(zip.in0, zip.out) } + /** + * Combine the elements of current flow into a stream of tuples consisting + * of all elements paired with their index. Indices start at 0. + * + * '''Emits when''' upstream emits an element and is paired with their index + * + * '''Backpressures when''' downstream backpressures + * + * '''Completes when''' upstream completes + * + * '''Cancels when''' downstream cancels + */ + def zipWithIndex: Repr[(Out, Long)] = { + statefulMapConcat[(Out, Long)] { () ⇒ + var index: Long = 0L + elem ⇒ { + val zipped = (elem, index) + index += 1 + immutable.Iterable[(Out, Long)](zipped) + } + } + } + /** * Interleave is a deterministic merge of the given [[Source]] with elements of this [[Flow]]. * It first emits `segmentSize` number of elements from this flow to downstream, then - same amount for `that` diff --git a/project/MiMa.scala b/project/MiMa.scala index 4b66bcd0df..fb1fed0814 100644 --- a/project/MiMa.scala +++ b/project/MiMa.scala @@ -966,6 +966,10 @@ object MiMa extends AutoPlugin { // #21131 new implementation for Akka Typed ProblemFilters.exclude[ReversedMissingMethodProblem]("akka.actor.dungeon.DeathWatch.isWatching") + ), + "2.4.10" -> Seq( + // #21290 new zipWithIndex flow op + ProblemFilters.exclude[ReversedMissingMethodProblem]("akka.stream.scaladsl.FlowOps.zipWithIndex") ) ) } From f8f88284517b5c57d39d2f79e285517cd6d29a2d Mon Sep 17 00:00:00 2001 From: monkey-mas Date: Thu, 22 Sep 2016 12:40:54 +0900 Subject: [PATCH 20/23] =act improve ByteString#take(...) (#21438) Currently, we use ByteStringBuilder to create a new ByteString instance, which would not be quite efficient. Instead of doing this, we can do as follows so that we can achieve better performance: 1. Seek the index of _last_ vector element we need to _take_ 2. Find the number of characters left to take from the _last_ ByteString1 element. 3. Create ByteString based on the information we obtained from 1 and 2 Then we just need to create a new Vector[ByteString1] at most twice, which should be better than the current implementation, i.e., _append_ a new element every time we check bytestrings(Vector[ByteString1]) element, which ends up O(N) _append_ execution where _N_ is the length of bytestrings. --- .../test/scala/akka/util/ByteStringSpec.scala | 16 +++++++++ .../src/main/scala/akka/util/ByteString.scala | 34 ++++++++++++------- 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/akka-actor-tests/src/test/scala/akka/util/ByteStringSpec.scala b/akka-actor-tests/src/test/scala/akka/util/ByteStringSpec.scala index 1514635c8a..a6a614475c 100644 --- a/akka-actor-tests/src/test/scala/akka/util/ByteStringSpec.scala +++ b/akka-actor-tests/src/test/scala/akka/util/ByteStringSpec.scala @@ -308,6 +308,19 @@ class ByteStringSpec extends WordSpec with Matchers with Checkers { ByteString1.fromString("ab").drop(2) should ===(ByteString("")) ByteString1.fromString("ab").drop(3) should ===(ByteString("")) } + "take" in { + ByteString1.empty.take(-1) should ===(ByteString("")) + ByteString1.empty.take(0) should ===(ByteString("")) + ByteString1.empty.take(1) should ===(ByteString("")) + ByteString1.fromString("a").take(1) should ===(ByteString("a")) + ByteString1.fromString("ab").take(-1) should ===(ByteString("")) + ByteString1.fromString("ab").take(0) should ===(ByteString("")) + ByteString1.fromString("ab").take(1) should ===(ByteString("a")) + ByteString1.fromString("ab").take(2) should ===(ByteString("ab")) + ByteString1.fromString("ab").take(3) should ===(ByteString("ab")) + ByteString1.fromString("0123456789").take(3).drop(1) should ===(ByteString("12")) + ByteString1.fromString("0123456789").take(10).take(8).drop(3).take(5) should ===(ByteString("34567")) + } } "ByteString1C" must { "drop(0)" in { @@ -415,6 +428,9 @@ class ByteStringSpec extends WordSpec with Matchers with Checkers { ByteStrings(ByteString1.fromString("a"), ByteString1.fromString("bc")).dropRight(3) should ===(ByteString("")) } "take" in { + ByteString.empty.take(-1) should ===(ByteString("")) + ByteString.empty.take(0) should ===(ByteString("")) + ByteString.empty.take(1) should ===(ByteString("")) ByteStrings(ByteString1.fromString("a"), ByteString1.fromString("bc")).drop(1).take(0) should ===(ByteString("")) ByteStrings(ByteString1.fromString("a"), ByteString1.fromString("bc")).drop(1).take(-1) should ===(ByteString("")) ByteStrings(ByteString1.fromString("a"), ByteString1.fromString("bc")).drop(1).take(-2) should ===(ByteString("")) diff --git a/akka-actor/src/main/scala/akka/util/ByteString.scala b/akka-actor/src/main/scala/akka/util/ByteString.scala index d2600320d8..8196495d7c 100644 --- a/akka-actor/src/main/scala/akka/util/ByteString.scala +++ b/akka-actor/src/main/scala/akka/util/ByteString.scala @@ -249,8 +249,11 @@ object ByteString { } override def take(n: Int): ByteString = - if (n <= 0) ByteString.empty - else ByteString1(bytes, startIndex, Math.min(n, length)) + if (n <= 0) ByteString.empty else take1(n) + + private[akka] def take1(n: Int): ByteString1 = + if (n >= length) this + else ByteString1(bytes, startIndex, n) override def slice(from: Int, until: Int): ByteString = drop(from).take(until - Math.max(0, from)) @@ -432,18 +435,23 @@ object ByteString { bytestrings.foreach(_.writeToOutputStream(os)) } - override def take(n: Int): ByteString = { - @tailrec def take0(n: Int, b: ByteStringBuilder, bs: Vector[ByteString1]): ByteString = - if (bs.isEmpty || n <= 0) b.result - else { - val head = bs.head - if (n <= head.length) b.append(head.take(n)).result - else take0(n - head.length, b.append(head), bs.tail) - } - + override def take(n: Int): ByteString = if (n <= 0) ByteString.empty else if (n >= length) this - else take0(n, ByteString.newBuilder, bytestrings) + else take0(n) + + private[akka] def take0(n: Int): ByteString = { + @tailrec def go(last: Int, restToTake: Int): (Int, Int) = { + val bs = bytestrings(last) + if (bs.length > restToTake) (last, restToTake) + else go(last + 1, restToTake - bs.length) + } + + val (last, restToTake) = go(0, n) + + if (last == 0) bytestrings(last).take(restToTake) + else if (restToTake == 0) new ByteStrings(bytestrings.take(last), n) + else new ByteStrings(bytestrings.take(last) :+ bytestrings(last).take1(restToTake), n) } override def dropRight(n: Int): ByteString = @@ -469,7 +477,7 @@ object ByteString { override def drop(n: Int): ByteString = if (n <= 0) this - else if (n > length) ByteString.empty + else if (n >= length) ByteString.empty else drop0(n) private def drop0(n: Int): ByteString = { From ddb2b5cd28289a658e29e9fa67d94c1404730125 Mon Sep 17 00:00:00 2001 From: Patrik Nordwall Date: Fri, 23 Sep 2016 18:20:28 +0200 Subject: [PATCH 21/23] fix typo in futures.rst (#21536) --- akka-docs/rst/java/futures.rst | 2 +- akka-docs/rst/scala/futures.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/akka-docs/rst/java/futures.rst b/akka-docs/rst/java/futures.rst index 0415bdbffa..ea39963a45 100644 --- a/akka-docs/rst/java/futures.rst +++ b/akka-docs/rst/java/futures.rst @@ -187,7 +187,7 @@ Callbacks --------- Sometimes you just want to listen to a ``Future`` being completed, and react to that not by creating a new Future, but by side-effecting. -For this Scala supports ``onComplete``, ``onSuccess`` and ``onFailure``, of which the latter two are specializations of the first. +For this Scala supports ``onComplete``, ``onSuccess`` and ``onFailure``, of which the last two are specializations of the first. .. includecode:: code/docs/future/FutureDocTest.java :include: onSuccess diff --git a/akka-docs/rst/scala/futures.rst b/akka-docs/rst/scala/futures.rst index 52a665f46e..705e620708 100644 --- a/akka-docs/rst/scala/futures.rst +++ b/akka-docs/rst/scala/futures.rst @@ -227,7 +227,7 @@ Callbacks --------- Sometimes you just want to listen to a ``Future`` being completed, and react to that not by creating a new ``Future``, but by side-effecting. -For this Scala supports ``onComplete``, ``onSuccess`` and ``onFailure``, of which the latter two are specializations of the first. +For this Scala supports ``onComplete``, ``onSuccess`` and ``onFailure``, of which the last two are specializations of the first. .. includecode:: code/docs/future/FutureDocSpec.scala :include: onSuccess From 6e0b8b98f387df8a289d1045ca8e5a1a3a7e96de Mon Sep 17 00:00:00 2001 From: KAWACHI Takashi Date: Sun, 25 Sep 2016 08:24:08 +0900 Subject: [PATCH 22/23] Fixed typo in GraphInterpreter.scala (#21552) --- .../main/scala/akka/stream/impl/fusing/GraphInterpreter.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/akka-stream/src/main/scala/akka/stream/impl/fusing/GraphInterpreter.scala b/akka-stream/src/main/scala/akka/stream/impl/fusing/GraphInterpreter.scala index dfc8fc8d6c..cea476ef47 100644 --- a/akka-stream/src/main/scala/akka/stream/impl/fusing/GraphInterpreter.scala +++ b/akka-stream/src/main/scala/akka/stream/impl/fusing/GraphInterpreter.scala @@ -316,7 +316,7 @@ object GraphInterpreter { * * From an external viewpoint, the GraphInterpreter takes an assembly of graph processing stages encoded as a * [[GraphInterpreter#GraphAssembly]] object and provides facilities to execute and interact with this assembly. - * The lifecylce of the Interpreter is roughly the following: + * The lifecycle of the Interpreter is roughly the following: * - Boundary logics are attached via [[attachDownstreamBoundary()]] and [[attachUpstreamBoundary()]] * - [[init()]] is called * - [[execute()]] is called whenever there is need for execution, providing an upper limit on the processed events From d37fa8ec0243c33cb65d76cb99fa980e3c78e8d5 Mon Sep 17 00:00:00 2001 From: Patrik Nordwall Date: Tue, 27 Sep 2016 13:21:49 +0200 Subject: [PATCH 23/23] remove Range.Inclusive subclass (toString), #21548 * remove Range.Inclusive subclass (toString), #21548 * update java8-compat dependency for Scala 2.12-RC1 --- akka-stream/src/main/scala/akka/stream/javadsl/Source.scala | 4 +--- project/Dependencies.scala | 5 ++--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/akka-stream/src/main/scala/akka/stream/javadsl/Source.scala b/akka-stream/src/main/scala/akka/stream/javadsl/Source.scala index 7020455889..8d827acde7 100644 --- a/akka-stream/src/main/scala/akka/stream/javadsl/Source.scala +++ b/akka-stream/src/main/scala/akka/stream/javadsl/Source.scala @@ -156,9 +156,7 @@ object Source { def range(start: Int, end: Int, step: Int): javadsl.Source[Integer, NotUsed] = fromIterator[Integer](new function.Creator[util.Iterator[Integer]]() { def create(): util.Iterator[Integer] = - new Inclusive(start, end, step) { - override def toString: String = s"Range($start to $end, step = $step)" - }.iterator.asJava.asInstanceOf[util.Iterator[Integer]] + Range.inclusive(start, end, step).iterator.asJava.asInstanceOf[util.Iterator[Integer]] }) /** diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 70ec6834d0..efce292b53 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -16,7 +16,7 @@ object Dependencies { val junitVersion = "4.12" val Versions = Seq( - crossScalaVersions := Seq("2.11.8"), // "2.12.0-M4" + crossScalaVersions := Seq("2.11.8"), // "2.12.0-RC1" scalaVersion := crossScalaVersions.value.head, scalaStmVersion := sys.props.get("akka.build.scalaStmVersion").getOrElse("0.7"), scalaCheckVersion := sys.props.get("akka.build.scalaCheckVersion").getOrElse("1.13.2"), @@ -28,8 +28,7 @@ object Dependencies { }, java8CompatVersion := { scalaVersion.value match { - case "2.12.0-M4" => "0.8.0-RC1" - case "2.12.0-M5" => "0.8.0-RC3" + case x if x.startsWith("2.12.0-RC1") => "0.8.0-RC7" case _ => "0.7.0" } }