├── project └── build.properties ├── src ├── test │ ├── resources │ │ └── application.conf │ └── scala │ │ └── com │ │ └── rolandkuhn │ │ └── akka_typed_session │ │ ├── auditdemo │ │ ├── AkkaTyped.scala │ │ ├── Messages.scala │ │ ├── ProcessBased.scala │ │ └── Actor.scala │ │ ├── AkkaSpec.scala │ │ ├── TypedSpec.scala │ │ └── ProcessSpec.scala └── main │ └── scala │ └── com │ └── rolandkuhn │ └── akka_typed_session │ ├── package.scala │ ├── Actor.scala │ ├── MapAdapter.scala │ ├── State.scala │ ├── Process.scala │ ├── Effects.scala │ ├── Operation.scala │ ├── ScalaDSL.scala │ └── internal │ └── ProcessImpl.scala ├── .gitignore ├── CONTRIBUTING.md ├── .travis.yml ├── LICENSE └── README.md /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.13 2 | -------------------------------------------------------------------------------- /src/test/resources/application.conf: -------------------------------------------------------------------------------- 1 | akka.actor.debug.lifecycle = on 2 | akka.loglevel = DEBUG -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .project 2 | .classpath 3 | .settings/ 4 | .cache* 5 | target/ 6 | /.target/ 7 | /bin/ 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Before you start please [sign the Contributor License Agreement](https://www.clahub.com/agreements/rkuhn/akka-typed-session). 4 | 5 | PRs can only be merged once the CLA has been signed and Travis has run successfully. For possibly controversial change proposals it is recommended to 6 | open an issue first and initiate a discussion on the proposed change. 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | dist: trusty 3 | language: scala 4 | 5 | script: 6 | - sbt test 7 | 8 | cache: 9 | directories: 10 | - $HOME/.ivy2/cache 11 | - $HOME/.sbt/boot/ 12 | 13 | before_cache: 14 | # Tricks to avoid unnecessary cache updates 15 | - find $HOME/.ivy2 -name "ivydata-*.properties" -delete 16 | - find $HOME/.sbt -name "*.lock" -delete 17 | 18 | jdk: 19 | - openjdk8 20 | -------------------------------------------------------------------------------- /src/test/scala/com/rolandkuhn/akka_typed_session/auditdemo/AkkaTyped.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Roland Kuhn 3 | */ 4 | package com.rolandkuhn.akka_typed_session.auditdemo 5 | 6 | case class Greet(whom: String) 7 | 8 | class Greeter extends akka.actor.Actor { 9 | def receive = { 10 | case Greet(whom) => println(s"Hello $whom!") 11 | } 12 | } 13 | 14 | object Greeter { 15 | import akka.typed.scaladsl.Actor 16 | val behavior = 17 | Actor.immutable[Greet] { (ctx, greet) => 18 | println(s"Hello ${greet.whom}!") 19 | Actor.same 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software is licensed under the Apache 2 license, quoted below. 2 | 3 | Copyright 2009-2014 Typesafe Inc. [http://www.typesafe.com] 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); you may not 6 | use this file except in compliance with the License. You may obtain a copy of 7 | the License at 8 | 9 | [http://www.apache.org/licenses/LICENSE-2.0] 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | License for the specific language governing permissions and limitations under 15 | the License. 16 | 17 | --------------- 18 | 19 | Licenses for dependency projects can be found here: 20 | [http://akka.io/docs/akka/snapshot/project/licenses.html] 21 | -------------------------------------------------------------------------------- /src/main/scala/com/rolandkuhn/akka_typed_session/package.scala: -------------------------------------------------------------------------------- 1 | package com.rolandkuhn 2 | 3 | import language.implicitConversions 4 | 5 | package object akka_typed_session { 6 | 7 | /** 8 | * This implicit expresses that operations that do not use their input channel can be used in any context. 9 | */ 10 | private[akka_typed_session] implicit def nothingIsSomething[T, U, E <: E1, E1 <: Effects](op: Operation[Nothing, T, E]): Operation[U, T, E1] = 11 | op.asInstanceOf[Operation[U, T, E1]] 12 | 13 | private[akka_typed_session] implicit class WithEffects[S, O](op: Operation[S, O, _]) { 14 | def withEffects[E <: Effects]: Operation[S, O, E] = op.asInstanceOf[Operation[S, O, E]] 15 | } 16 | implicit class WithoutEffects[S, O](op: Operation[S, O, _]) { 17 | def ignoreEffects: Operation[S, O, _0] = op.asInstanceOf[Operation[S, O, _0]] 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/scala/com/rolandkuhn/akka_typed_session/Actor.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Roland Kuhn 3 | */ 4 | package com.rolandkuhn.akka_typed_session 5 | 6 | import akka.typed.ActorRef 7 | 8 | /** 9 | * The main ActorRef of an Actor hosting [[Process]] instances accepts this 10 | * type of messages. The “main process” is the one with which the Actor is 11 | * spawned and which may fork or call other processes. Its input of type `T` 12 | * can be reached using [[MainCmd]] messages. Other subtypes are used for 13 | * internal purposes. 14 | */ 15 | sealed trait ActorCmd[+T] 16 | /** 17 | * Send a message to the “main process” of an Actor hosting processes. Note 18 | * that this message is routed via the host Actor’s behavior and then through 19 | * the [[Process]] mailbox of the main process. 20 | */ 21 | case class MainCmd[+T](cmd: T) extends ActorCmd[T] 22 | trait InternalActorCmd[+T] extends ActorCmd[T] 23 | 24 | /** 25 | * Forking a process creates a sub-Actor within the current actor that is 26 | * executed concurrently. This sub-Actor [[Process]] has its own [[ActorRef]] 27 | * and it can be canceled. 28 | */ 29 | trait SubActor[-T] { 30 | def ref: ActorRef[T] 31 | def cancel(): Unit 32 | } 33 | -------------------------------------------------------------------------------- /src/test/scala/com/rolandkuhn/akka_typed_session/auditdemo/Messages.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Roland Kuhn 3 | */ 4 | package com.rolandkuhn.akka_typed_session 5 | package auditdemo 6 | 7 | import akka.typed.ActorRef 8 | import java.net.URI 9 | import java.util.UUID 10 | import akka.typed.patterns.Receptionist.ServiceKey 11 | 12 | case object AuditService extends ServiceKey[LogActivity] 13 | case class LogActivity(who: ActorRef[Nothing], what: String, id: Long, replyTo: ActorRef[ActivityLogged]) 14 | case class ActivityLogged(who: ActorRef[Nothing], id: Long) 15 | 16 | sealed trait PaymentService 17 | case class Authorize(payer: URI, amount: BigDecimal, id: UUID, replyTo: ActorRef[PaymentResult]) extends PaymentService 18 | case class Capture(id: UUID, amount: BigDecimal, replyTo: ActorRef[PaymentResult]) extends PaymentService 19 | case class Void(id: UUID, replyTo: ActorRef[PaymentResult]) extends PaymentService 20 | case class Refund(id: UUID, replyTo: ActorRef[PaymentResult]) extends PaymentService 21 | 22 | sealed trait PaymentResult 23 | case class PaymentSuccess(id: UUID) extends PaymentResult 24 | case class PaymentRejected(id: UUID, reason: String) extends PaymentResult 25 | case class IdUnkwown(id: UUID) extends PaymentResult 26 | -------------------------------------------------------------------------------- /src/main/scala/com/rolandkuhn/akka_typed_session/MapAdapter.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Roland Kuhn 3 | */ 4 | package com.rolandkuhn.akka_typed_session 5 | 6 | /** 7 | * Helper to make `Operation.map` or `Operation.foreach` behave like `flatMap` when needed. 8 | */ 9 | sealed trait MapAdapter[Self, Out, Mapped, EOut <: Effects] { 10 | def lift[O](f: O ⇒ Out): O ⇒ Operation[Self, Mapped, EOut] 11 | } 12 | /** 13 | * Helper to make `Operation.map` or `Operation.foreach` behave like `flatMap` when needed. 14 | */ 15 | object MapAdapter extends MapAdapterLow { 16 | private val _adapter = 17 | new MapAdapter[Any, Operation[Any, Any, _0], Any, _0] { 18 | override def lift[O](f: O ⇒ Operation[Any, Any, _0]): O ⇒ Operation[Any, Any, _0] = f 19 | } 20 | 21 | implicit def mapAdapterOperation[Self, M, E <: Effects]: MapAdapter[Self, Operation[Self, M, E], M, E] = 22 | _adapter.asInstanceOf[MapAdapter[Self, Operation[Self, M, E], M, E]] 23 | } 24 | /** 25 | * Helper to make `Operation.map` or `Operation.foreach` behave like `flatMap` when needed. 26 | */ 27 | trait MapAdapterLow { 28 | private val _adapter = 29 | new MapAdapter[Any, Any, Any, _0] { 30 | override def lift[O](f: O ⇒ Any): O ⇒ Operation[Any, Any, _0] = o ⇒ Impl.Return(f(o)) 31 | } 32 | 33 | implicit def mapAdapterAny[Self, Out]: MapAdapter[Self, Out, Out, _0] = 34 | _adapter.asInstanceOf[MapAdapter[Self, Out, Out, _0]] 35 | } 36 | -------------------------------------------------------------------------------- /src/test/scala/com/rolandkuhn/akka_typed_session/AkkaSpec.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2009-2017 Lightbend Inc. 3 | */ 4 | package com.rolandkuhn.akka_typed_session 5 | 6 | import org.scalactic.Constraint 7 | 8 | import language.postfixOps 9 | import org.scalatest.{ BeforeAndAfterAll, WordSpecLike } 10 | import org.scalatest.Matchers 11 | import akka.actor.ActorSystem 12 | import akka.event.{ Logging, LoggingAdapter } 13 | 14 | import scala.concurrent.duration._ 15 | import scala.concurrent.Future 16 | import com.typesafe.config.{ Config, ConfigFactory } 17 | import akka.dispatch.Dispatchers 18 | import akka.testkit._ 19 | import akka.testkit.TestEvent._ 20 | import org.scalactic.ConversionCheckedTripleEquals 21 | import org.scalatest.concurrent.ScalaFutures 22 | import org.scalatest.time.Span 23 | 24 | object AkkaSpec { 25 | val testConf: Config = ConfigFactory.parseString(""" 26 | akka { 27 | loggers = ["akka.testkit.TestEventListener"] 28 | typed.loggers = ["akka.typed.testkit.TestEventListener"] 29 | loglevel = "WARNING" 30 | stdout-loglevel = "WARNING" 31 | actor { 32 | default-dispatcher { 33 | executor = "fork-join-executor" 34 | fork-join-executor { 35 | parallelism-min = 8 36 | parallelism-factor = 2.0 37 | parallelism-max = 8 38 | } 39 | } 40 | } 41 | } 42 | """) 43 | 44 | def mapToConfig(map: Map[String, Any]): Config = { 45 | import scala.collection.JavaConverters._ 46 | ConfigFactory.parseMap(map.asJava) 47 | } 48 | 49 | def getCallerName(clazz: Class[_]): String = { 50 | val s = (Thread.currentThread.getStackTrace map (_.getClassName) drop 1) 51 | .dropWhile(_ matches "(java.lang.Thread|.*AkkaSpec.?$|.*StreamSpec.?$)") 52 | val reduced = s.lastIndexWhere(_ == clazz.getName) match { 53 | case -1 ⇒ s 54 | case z ⇒ s drop (z + 1) 55 | } 56 | reduced.head.replaceFirst(""".*\.""", "").replaceAll("[^a-zA-Z_0-9]", "_") 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/main/scala/com/rolandkuhn/akka_typed_session/State.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Roland Kuhn 3 | */ 4 | package com.rolandkuhn.akka_typed_session 5 | 6 | /** 7 | * A key into the Actor’s state map that allows access both for read and 8 | * update operations. Updates are modeled by emitting events of the specified 9 | * type. The updates are applied to the state in the order in which they are 10 | * emitted. For persistent state data please refer to [[PersistentStateKey]] 11 | * and for ephemeral non-event-sourced data take a look at [[SimpleStateKey]]. 12 | */ 13 | trait StateKey[T] { 14 | type Event 15 | def apply(state: T, event: Event): T 16 | def initial: T 17 | } 18 | 19 | /** 20 | * Event type emitted in conjunction with [[SimpleStateKey]], the only 21 | * implementation is [[SetState]]. 22 | */ 23 | sealed trait SetStateEvent[T] { 24 | def value: T 25 | } 26 | /** 27 | * Event type that instructs the state of a [[SimpleStateKey]] to be 28 | * replaced with the given value. 29 | */ 30 | final case class SetState[T](override val value: T) extends SetStateEvent[T] with Seq[SetStateEvent[T]] { 31 | def iterator: Iterator[SetStateEvent[T]] = Iterator.single(this) 32 | def apply(idx: Int): SetStateEvent[T] = 33 | if (idx == 0) this 34 | else throw new IndexOutOfBoundsException(s"$idx (for single-element sequence)") 35 | def length: Int = 1 36 | } 37 | 38 | /** 39 | * Use this key for state that shall neither be persistent nor event-sourced. 40 | * In effect this turns `updateState` into access to a State monad identified 41 | * by this key instance. 42 | * 43 | * Beware that reference equality is used to identify this key: you should 44 | * create the key as a `val` inside a top-level `object`. 45 | */ 46 | final class SimpleStateKey[T](override val initial: T) extends StateKey[T] { 47 | type Event = SetStateEvent[T] 48 | def apply(state: T, event: SetStateEvent[T]) = event.value 49 | override def toString: String = f"SimpleStateKey@$hashCode%08X($initial)" 50 | } 51 | 52 | /** 53 | * The data for a [[StateKey]] of this kind can be marked as persistent by 54 | * invoking `replayPersistentState`—this will first replay the stored events 55 | * and subsequently commit all emitted events to the journal before applying 56 | * them to the state. 57 | * 58 | * FIXME persistence is not yet implemented 59 | */ 60 | //trait PersistentStateKey[T] extends StateKey[T] { 61 | // def clazz: Class[Event] 62 | //} 63 | -------------------------------------------------------------------------------- /src/test/scala/com/rolandkuhn/akka_typed_session/auditdemo/ProcessBased.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Roland Kuhn 3 | */ 4 | package com.rolandkuhn.akka_typed_session 5 | package auditdemo 6 | 7 | import ScalaDSL._ 8 | import java.net.URI 9 | import akka.typed.ActorRef 10 | import akka.Done 11 | import scala.util.Random 12 | import scala.concurrent.duration._ 13 | import java.util.UUID 14 | import shapeless.{ :+:, CNil } 15 | 16 | object ProcessBased { 17 | 18 | def getMoney[R](from: URI, amount: BigDecimal, 19 | payments: ActorRef[PaymentService], 20 | replyTo: ActorRef[R], msg: R) = 21 | OpDSL[Nothing] { implicit opDSL => 22 | for { 23 | self <- opActorSelf 24 | audit <- opCall(getService(AuditService).named("getAudit")) 25 | _ <- opCall(doAudit(audit, self, "starting payment").named("preAudit")) 26 | _ <- opCall(doPayment(from, amount, payments).named("payment")) 27 | _ <- opCall(doAudit(audit, self, "payment finished").named("postAudit")) 28 | } yield replyTo ! msg 29 | } 30 | 31 | private def doAudit(audit: ActorRef[LogActivity], who: ActorRef[Nothing], msg: String) = 32 | OpDSL[ActivityLogged] { implicit opDSL => 33 | val id = Random.nextLong() 34 | for { 35 | self <- opProcessSelf 36 | _ <- opSend(audit, LogActivity(who, msg, id, self)) 37 | ActivityLogged(`who`, `id`) <- opRead 38 | } yield Done 39 | }.withTimeout(3.seconds) 40 | 41 | private def doPayment(from: URI, amount: BigDecimal, payments: ActorRef[PaymentService]) = 42 | OpDSL[PaymentResult] { implicit opDSL => 43 | val uuid = UUID.randomUUID() 44 | for { 45 | self <- opProcessSelf 46 | _ <- opSend(payments, Authorize(from, amount, uuid, self)) 47 | PaymentSuccess(`uuid`) <- opRead 48 | _ <- opSend(payments, Capture(uuid, amount, self)) 49 | PaymentSuccess(`uuid`) <- opRead 50 | } yield Done 51 | }.withTimeout(3.seconds) 52 | 53 | object GetMoneyProtocol extends E.Protocol { 54 | type Session = // 55 | E.Send[LogActivity] :: // preAudit 56 | E.Read[ActivityLogged] :: // 57 | E.Choice[(E.Halt :: _0) :+: _0 :+: CNil] :: // possibly terminate 58 | E.Send[Authorize] :: // do payment 59 | E.Read[PaymentResult] :: // 60 | E.Choice[(E.Halt :: _0) :+: _0 :+: CNil] :: // possibly terminate 61 | E.Send[Capture] :: // 62 | E.Read[PaymentResult] :: // 63 | E.Choice[(E.Halt :: _0) :+: _0 :+: CNil] :: // possibly terminate 64 | E.Send[LogActivity] :: // postAudit 65 | E.Read[ActivityLogged] :: // 66 | E.Choice[(E.Halt :: _0) :+: _0 :+: CNil] :: // possibly terminate 67 | _0 68 | } 69 | 70 | // compile-time verification 71 | private def verify = E.vetExternalProtocol(GetMoneyProtocol, getMoney(???, ???, ???, ???, ???)) 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/test/scala/com/rolandkuhn/akka_typed_session/auditdemo/Actor.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Roland Kuhn 3 | */ 4 | package com.rolandkuhn.akka_typed_session.auditdemo 5 | 6 | import java.net.URI 7 | import akka.typed.{ ActorRef, Behavior, Signal, Terminated } 8 | import akka.typed.scaladsl.{ Actor, ActorContext } 9 | import scala.util.Random 10 | import scala.concurrent.duration._ 11 | import java.util.concurrent.TimeoutException 12 | import java.util.UUID 13 | 14 | object ActorBased { 15 | 16 | sealed trait Msg 17 | private case object AuditDone extends Msg 18 | private case object PaymentDone extends Msg 19 | 20 | def getMoney[R](from: URI, amount: BigDecimal, 21 | payments: ActorRef[PaymentService], audit: ActorRef[LogActivity], 22 | replyTo: ActorRef[R], msg: R) = 23 | Actor.deferred[Msg] { ctx => 24 | ctx.watch(ctx.spawn(doAudit(audit, ctx.self, "starting payment"), "preAudit")) 25 | Actor.immutable[Msg] { 26 | case (ctx, AuditDone) => 27 | ctx.watch(ctx.spawn(doPayment(from, amount, payments, ctx.self), "payment")) 28 | Actor.immutable[Msg] { 29 | case (ctx, PaymentDone) => 30 | ctx.watch(ctx.spawn(doAudit(audit, ctx.self, "payment finished"), "postAudit")) 31 | Actor.immutable[Msg] { 32 | case (ctx, AuditDone) => 33 | replyTo ! msg 34 | Actor.stopped 35 | } 36 | } onSignal ignoreStop 37 | } onSignal ignoreStop 38 | } 39 | 40 | private val ignoreStop: PartialFunction[(ActorContext[Msg], Signal), Behavior[Msg]] = { 41 | case (ctx, t: Terminated) if !t.wasFailed => Actor.same 42 | } 43 | 44 | private def doAudit(audit: ActorRef[LogActivity], who: ActorRef[AuditDone.type], msg: String) = 45 | Actor.deferred[ActivityLogged] { ctx => 46 | val id = Random.nextLong() 47 | audit ! LogActivity(who, msg, id, ctx.self) 48 | ctx.schedule(3.seconds, ctx.self, ActivityLogged(null, 0L)) 49 | 50 | Actor.immutable { (ctx, msg) => 51 | if (msg.who == null) throw new TimeoutException 52 | else if (msg.id != id) throw new IllegalStateException 53 | else { 54 | who ! AuditDone 55 | Actor.stopped 56 | } 57 | } 58 | } 59 | 60 | private def doPayment(from: URI, amount: BigDecimal, payments: ActorRef[PaymentService], replyTo: ActorRef[PaymentDone.type]) = 61 | Actor.deferred[PaymentResult] { ctx => 62 | val uuid = UUID.randomUUID() 63 | payments ! Authorize(from, amount, uuid, ctx.self) 64 | ctx.schedule(3.seconds, ctx.self, IdUnkwown(null)) 65 | 66 | Actor.immutable { 67 | case (ctx, PaymentSuccess(`uuid`)) => 68 | payments ! Capture(uuid, amount, ctx.self) 69 | Actor.immutable { 70 | case (ctx, PaymentSuccess(`uuid`)) => 71 | replyTo ! PaymentDone 72 | Actor.stopped 73 | } 74 | // otherwise die with MatchError 75 | } 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/main/scala/com/rolandkuhn/akka_typed_session/Process.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Roland Kuhn 3 | */ 4 | package com.rolandkuhn.akka_typed_session 5 | 6 | import scala.concurrent.duration.Duration 7 | import akka.{ actor => a } 8 | import akka.typed.Behavior 9 | import akka.typed.scaladsl.Actor 10 | import shapeless.{ :+:, CNil } 11 | 12 | /** 13 | * A Process runs the given operation steps in a context that provides the 14 | * needed [[ActorRef]] of type `S` as the self-reference. Every process is 15 | * allotted a maximum lifetime after which the entire Actor fails; you may 16 | * set this to `Duration.Inf` for a server process. For non-fatal timeouts 17 | * take a look at [[ScalaProcess#forAndCancel]]. 18 | * 19 | * The `name` of a Process is used as part of the process’ ActorRef name and 20 | * must therefore adhere to the path segment grammar of the URI specification. 21 | */ 22 | final case class Process[S, +Out, E <: Effects]( 23 | name: String, timeout: Duration, mailboxCapacity: Int, operation: Operation[S, Out, E]) { 24 | if (name != "") a.ActorPath.validatePathElement(name) 25 | 26 | /** 27 | * Execute the given computation and process step after having completed 28 | * the current step. The current step’s computed value will be used as 29 | * input for the next computation. 30 | */ 31 | def flatMap[T, EE <: Effects](f: Out ⇒ Operation[S, T, EE])(implicit p: E.ops.Prepend[E, EE]): Process[S, T, p.Out] = 32 | copy(operation = Impl.FlatMap(operation, f)) 33 | 34 | /** 35 | * Map the value computed by this process step by the given function, 36 | * flattening the result if it is an [[Operation]] (by executing the 37 | * operation and using its result as the mapped value). 38 | * 39 | * The reason behind flattening when possible is to allow the formulation 40 | * of infinite process loops (as performed for example by server processes 41 | * that respond to any number of requests) using for-comprehensions. 42 | * Without this flattening a final pointless `map` step would be added 43 | * for each iteration, eventually leading to an OutOfMemoryError. 44 | */ 45 | def map[T, Mapped, EOut <: Effects](f: Out ⇒ T)( 46 | implicit ev: MapAdapter[S, T, Mapped, EOut], 47 | p: E.ops.Prepend[E, EOut]): Process[S, Mapped, p.Out] = flatMap(ev.lift(f)) 48 | 49 | /** 50 | * Only continue this process if the given predicate is fulfilled, terminate 51 | * it otherwise. 52 | */ 53 | def filter(p: Out ⇒ Boolean)( 54 | implicit pr: E.ops.Prepend[E, E.Choice[(E.Halt :: _0) :+: _0 :+: CNil] :: _0]): Process[S, Out, pr.Out] = 55 | flatMap(o ⇒ 56 | ScalaDSL.opChoice(p(o), Impl.Return(o): Operation[S, Out, _0]).orElse(Impl.ShortCircuit: Operation[S, Out, E.Halt :: _0]) 57 | ) 58 | 59 | /** 60 | * Only continue this process if the given predicate is fulfilled, terminate 61 | * it otherwise. 62 | */ 63 | def withFilter(p: Out ⇒ Boolean)( 64 | implicit pr: E.ops.Prepend[E, E.Choice[(E.Halt :: _0) :+: _0 :+: CNil] :: _0]): Process[S, Out, pr.Out] = 65 | flatMap(o ⇒ 66 | ScalaDSL.opChoice(p(o), Impl.Return(o): Operation[S, Out, _0]).orElse(Impl.ShortCircuit: Operation[S, Out, E.Halt :: _0]) 67 | ) 68 | 69 | /** 70 | * Create a copy with modified name. 71 | */ 72 | def named(name: String): Process[S, Out, E] = copy(name = name) 73 | 74 | /** 75 | * Create a copy with modified timeout parameter. 76 | */ 77 | def withTimeout(timeout: Duration): Process[S, Out, E] = copy(timeout = timeout) 78 | 79 | /** 80 | * Create a copy with modified mailbox capacity. 81 | */ 82 | def withMailboxCapacity(mailboxCapacity: Int): Process[S, Out, E] = copy(mailboxCapacity = mailboxCapacity) 83 | 84 | /** 85 | * Convert to a runnable [[Behavior]], e.g. for being used as the guardian of an [[ActorSystem]]. 86 | */ 87 | def toBehavior: Behavior[ActorCmd[S]] = Actor.deferred(ctx => new internal.ProcessInterpreter(this, ctx).execute(ctx)) 88 | } 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/rkuhn/akka-typed-session.svg?branch=master)](https://travis-ci.org/rkuhn/akka-typed-session) 2 | 3 | # Akka Typed Session 4 | 5 | Add-on to Akka Typed that tracks effects for use with Session Types. 6 | 7 | ## Example 8 | 9 | Assume this message protocol: 10 | 11 | ~~~scala 12 | case class AuthRequest(credentials: String)(replyTo: ActorRef[AuthResult]) 13 | 14 | sealed trait AuthResult 15 | case object AuthRejected extends AuthResult 16 | case class AuthSuccess(token: ActorRef[Command]) extends AuthResult 17 | 18 | sealed trait Command 19 | case object DoIt extends Command 20 | ~~~ 21 | 22 | The process to be followed is that first an `AuthRequest` is sent, answered by 23 | an `AuthResult` that may or may not unlock further communication of `Command` 24 | messages. A more formal definition of this sequence is the following: 25 | 26 | ~~~scala 27 | trait Protocol { 28 | type Session <: Effects 29 | } 30 | 31 | object MyProto extends Protocol { 32 | type Session = // 33 | Send[AuthRequest] :: // first ask for authentication 34 | Read[AuthResult] :: // then read the response 35 | Choice[(Halt :: _0) :+: _0 :+: CNil] :: // then possibly terminate if rejected 36 | Send[Command] :: _0 // then send a command 37 | } 38 | ~~~ 39 | 40 | Implementing the first part of this exchange is the familiar ask pattern or request–response. 41 | We can use the process DSL to factor this out into a utility function: 42 | 43 | ~~~scala 44 | def opAsk[T, U](target: ActorRef[T], msg: ActorRef[U] => T) = 45 | OpDSL[U] { implicit opDSL => 46 | for { 47 | self <- opProcessSelf 48 | _ <- opSend(target, msg(self)) 49 | } yield opRead 50 | } 51 | ~~~ 52 | 53 | Here the `OpDSL` constructor provides an environment in which the behavior behind a typed 54 | `ActorRef[U]` can be defined. The `opProcessSelf` is an operation that when run will yield 55 | the aforementioned `ActorRef[U]`. `opSend` is an operation that when run will send the given 56 | message to the given target. As the last step in this mini-protocol `opRead` awaits the 57 | reception of a message sent to `self`, i.e. the received message will be of type `U`. 58 | 59 | *It should be noted that in this process DSL algebra the `.map()` combinator has the same behavior 60 | as `.flatMap()` where possible, i.e. it will flatten if the returned value is an `Operation`. This 61 | is necessary in order to avoid memory leaks for infinite processing loops (as seen e.g. in 62 | server processes that respond to an unbounded number of requests).* 63 | 64 | Now we can use this ask operation in the context of the larger overall process. Calling such 65 | a compound operation is done via the `opCall` operator. 66 | 67 | ~~~scala 68 | val auth: ActorRef[AuthRequest] = ??? // assume we get the authentication endpoint from somewhere 69 | 70 | val p = OpDSL[String] { implicit opDSL ⇒ 71 | for { 72 | AuthSuccess(token) <- opCall(opAsk(auth, AuthRequest("secret")).named("getAuth")) 73 | } yield opSend(token, DoIt) 74 | } 75 | ~~~ 76 | 77 | The resulting type of `p` is not just an Operation with `String` for the self-type, yielding `Unit` (the result of `opSend`), 78 | but it also tracks the effects that occur when executing the whole process. We can assert that the externally visible effects 79 | of sending and receiving messages match the protocol definition given above by asking the compiler to construct a proof: 80 | 81 | ~~~scala 82 | def vetProtocol[E <: Effects, F <: Effects](p: Protocol, op: Operation[_, _, E])( 83 | implicit f: E.ops.FilterAux[E, ExternalEffect, F], ev: F <:< p.Session): Unit = () 84 | 85 | vetProtocol(MyProto, p) 86 | ~~~ 87 | 88 | This works in two steps: first the list of effects E (which is somewhat like an HList with a special node for infinite loops) 89 | is filtered so that only effects remain that are subtypes of `ExternalEffect`, yielding the list F. Then this list is compared 90 | to the `Session` type member of the given protocol to see whether it is a subtype. 91 | 92 | The result is that the whole program only compiles if the process performs all the required externally visible effects in 93 | the right order. If a step is forgotten or duplicated then the `vetProtocol` invocation will raise a type error. 94 | 95 | ## Legal 96 | 97 | See the LICENSE file for details on licensing and CONTRIBUTING for the contributor’s guide. 98 | 99 | Copyright 2017 Roland Kuhn 100 | -------------------------------------------------------------------------------- /src/main/scala/com/rolandkuhn/akka_typed_session/Effects.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Roland Kuhn 3 | */ 4 | package com.rolandkuhn.akka_typed_session 5 | 6 | import akka.typed._ 7 | import shapeless.{ Coproduct, :+:, CNil } 8 | import shapeless.test.illTyped 9 | import scala.annotation.implicitNotFound 10 | 11 | sealed trait Effect 12 | sealed trait ExternalEffect extends Effect 13 | 14 | object E { 15 | sealed abstract class Read[-T] extends ExternalEffect 16 | sealed abstract class Send[+T] extends ExternalEffect 17 | sealed abstract class Fork[+E <: Effects] extends Effect 18 | sealed abstract class Spawn[+E <: Effects] extends Effect 19 | sealed abstract class Choice[+C <: Coproduct] extends ExternalEffect 20 | sealed abstract class Halt extends Effect 21 | 22 | object ops { 23 | import language.higherKinds 24 | 25 | @implicitNotFound("Cannot prepend ${First} to ${Second} (e.g. due to infinite loop in the first argument)") 26 | sealed trait Prepend[First <: Effects, Second <: Effects] { 27 | type Out <: Effects 28 | } 29 | type PrependAux[F <: Effects, S <: Effects, O <: Effects] = Prepend[F, S] { type Out = O } 30 | 31 | sealed trait PrependLowLow { 32 | implicit def prepend[H <: Effect, T <: Effects, S <: Effects]( 33 | implicit ev: Prepend[T, S]): PrependAux[H :: T, S, H :: ev.Out] = null 34 | } 35 | sealed trait PrependLow extends PrependLowLow { 36 | implicit def prependNil[F <: _0, S <: Effects]: PrependAux[F, S, S] = null 37 | } 38 | object Prepend extends PrependLow { 39 | implicit def prependToNil[F <: Effects, S <: _0]: PrependAux[F, S, F] = null 40 | } 41 | 42 | sealed trait Filter[E <: Effects, U] { 43 | type Out <: Effects 44 | } 45 | type FilterAux[E <: Effects, U, O <: Effects] = Filter[E, U] { type Out = O } 46 | 47 | sealed trait FilterLow { 48 | implicit def notFound[H <: Effect, T <: Effects, U](implicit f: Filter[T, U], ev: NoSub[H, U]): FilterAux[H :: T, U, f.Out] = null 49 | implicit def loop[E <: Effects, U](implicit f: Filter[E, U]): FilterAux[Loop[E], U, Loop[f.Out]] = null 50 | } 51 | object Filter extends FilterLow { 52 | implicit def nil[U]: FilterAux[_0, U, _0] = null 53 | implicit def found[H <: Effect, T <: Effects, U >: H](implicit f: Filter[T, U]): FilterAux[H :: T, U, H :: f.Out] = null 54 | } 55 | 56 | sealed trait NoSub[T, U] 57 | implicit def noSub1[T, U]: NoSub[T, U] = null 58 | implicit def noSub2[T <: U, U]: NoSub[T, U] = null 59 | implicit def noSub3[T <: U, U]: NoSub[T, U] = null 60 | } 61 | 62 | trait Protocol { 63 | type Session <: Effects 64 | } 65 | 66 | def vetExternalProtocol[E <: Effects, F <: Effects](p: Protocol, op: Operation[_, _, E])( 67 | implicit f: E.ops.FilterAux[E, ExternalEffect, F], 68 | ev: F <:< p.Session): Unit = () 69 | 70 | trait OpFunAux[OpFun, E <: Effects] 71 | object OpFunAux { 72 | implicit def opFun1[E <: Effects]: OpFunAux[Function1[_, Operation[_, _, E]], E] = null 73 | implicit def opFun2[E <: Effects]: OpFunAux[Function2[_, _, Operation[_, _, E]], E] = null 74 | implicit def opFun3[E <: Effects]: OpFunAux[Function3[_, _, _, Operation[_, _, E]], E] = null 75 | implicit def opFun4[E <: Effects]: OpFunAux[Function4[_, _, _, _, Operation[_, _, E]], E] = null 76 | implicit def opFun5[E <: Effects]: OpFunAux[Function5[_, _, _, _, _, Operation[_, _, E]], E] = null 77 | implicit def opFun6[E <: Effects]: OpFunAux[Function6[_, _, _, _, _, _, Operation[_, _, E]], E] = null 78 | implicit def opFun7[E <: Effects]: OpFunAux[Function7[_, _, _, _, _, _, _, Operation[_, _, E]], E] = null 79 | } 80 | } 81 | 82 | sealed trait Effects 83 | sealed abstract class _0 extends Effects 84 | sealed abstract class ::[+H <: Effect, +T <: Effects] extends Effects 85 | sealed abstract class Loop[+E <: Effects] extends Effects 86 | 87 | object EffectsTest { 88 | import E._ 89 | type A = E.Read[Any] 90 | type B = E.Send[Any] 91 | type C = E.Fork[_0] 92 | type D = E.Spawn[_0] 93 | 94 | illTyped("implicitly[ops.NoSub[String, Any]]") 95 | illTyped("implicitly[ops.NoSub[String, String]]") 96 | implicitly[ops.NoSub[String, Int]] 97 | implicitly[ops.NoSub[Any, String]] 98 | 99 | implicitly[ops.PrependAux[_0, _0, _0]] 100 | implicitly[ops.PrependAux[_0, A :: B :: _0, A :: B :: _0]] 101 | implicitly[ops.PrependAux[A :: B :: _0, _0, A :: B :: _0]] 102 | implicitly[ops.PrependAux[A :: B :: _0, C :: D :: _0, A :: B :: C :: D :: _0]] 103 | 104 | implicitly[ops.FilterAux[A :: B :: C :: D :: _0, ExternalEffect, A :: B :: _0]] 105 | implicitly[ops.FilterAux[Loop[_0], ExternalEffect, Loop[_0]]] 106 | implicitly[ops.FilterAux[A :: Loop[_0], ExternalEffect, A :: Loop[_0]]] 107 | implicitly[ops.FilterAux[A :: B :: C :: Loop[D :: A :: C :: B :: D :: _0], ExternalEffect, A :: B :: Loop[A :: B :: _0]]] 108 | 109 | case class AuthRequest(credentials: String)(replyTo: ActorRef[AuthResult]) 110 | 111 | sealed trait AuthResult 112 | case object AuthRejected extends AuthResult 113 | case class AuthSuccess(token: ActorRef[Command]) extends AuthResult 114 | 115 | sealed trait Command 116 | case object DoIt extends Command 117 | 118 | object MyProto extends Protocol { 119 | import E._ 120 | 121 | type Session = // 122 | Send[AuthRequest] :: // first ask for authentication 123 | Read[AuthResult] :: // then read the response 124 | Choice[(Halt :: _0) :+: _0 :+: CNil] :: // then possibly terminate if rejected 125 | Send[Command] :: _0 // then send a command 126 | } 127 | 128 | import ScalaDSL._ 129 | 130 | def opAsk[T, U](target: ActorRef[T], msg: ActorRef[U] => T) = 131 | OpDSL[U] { implicit opDSL => 132 | for { 133 | self <- opProcessSelf 134 | _ <- opSend(target, msg(self)) 135 | } yield opRead 136 | } 137 | 138 | val auth: ActorRef[AuthRequest] = ??? 139 | val p = OpDSL[String] { implicit opDSL ⇒ 140 | for { 141 | AuthSuccess(token) <- opCall(opAsk(auth, AuthRequest("secret")).named("getAuth")) 142 | } yield opSend(token, DoIt) 143 | } 144 | 145 | vetExternalProtocol(MyProto, p) 146 | 147 | } 148 | -------------------------------------------------------------------------------- /src/main/scala/com/rolandkuhn/akka_typed_session/Operation.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2017 Roland Kuhn 3 | */ 4 | package com.rolandkuhn.akka_typed_session 5 | 6 | import akka.typed.{ ActorSystem, ActorRef, Behavior, Props } 7 | import akka.{ actor => a } 8 | import shapeless.{ Coproduct, :+:, CNil } 9 | import shapeless.ops._ 10 | import scala.concurrent.duration._ 11 | 12 | /** 13 | * An Operation is a step executed by a [[Process]]. It exists in a context 14 | * characterized by the process’ ActorRef of type `S` and computes 15 | * a value of type `Out` when executed. 16 | * 17 | * Operations are created by using the `op*` methods of [[ScalaProcess]] 18 | * inside an [[OpDSL]] environment. 19 | */ 20 | sealed trait Operation[S, +Out, E <: Effects] { 21 | 22 | /** 23 | * Execute the given computation and process step after having completed 24 | * the current step. The current step’s computed value will be used as 25 | * input for the next computation. 26 | */ 27 | def flatMap[T, EE <: Effects](f: Out ⇒ Operation[S, T, EE])(implicit p: E.ops.Prepend[E, EE]): Operation[S, T, p.Out] = Impl.FlatMap(this, f) 28 | 29 | /** 30 | * Map the value computed by this process step by the given function, 31 | * flattening the result if it is an [[Operation]] (by executing the 32 | * operation and using its result as the mapped value). 33 | * 34 | * The reason behind flattening when possible is to allow the formulation 35 | * of infinite process loops (as performed for example by server processes 36 | * that respond to any number of requests) using for-comprehensions. 37 | * Without this flattening a final pointless `map` step would be added 38 | * for each iteration, eventually leading to an OutOfMemoryError. 39 | */ 40 | def map[T, Mapped, EOut <: Effects](f: Out ⇒ T)( 41 | implicit ev: MapAdapter[S, T, Mapped, EOut], 42 | p: E.ops.Prepend[E, EOut]): Operation[S, Mapped, p.Out] = flatMap(ev.lift(f)) 43 | 44 | /** 45 | * Only continue this process if the given predicate is fulfilled, terminate 46 | * it otherwise. 47 | */ 48 | def filter(p: Out ⇒ Boolean)( 49 | implicit pr: E.ops.Prepend[E, E.Choice[(E.Halt :: _0) :+: _0 :+: CNil] :: _0]): Operation[S, Out, pr.Out] = 50 | flatMap(o ⇒ 51 | ScalaDSL.opChoice(p(o), Impl.Return(o): Operation[S, Out, _0]).orElse(Impl.ShortCircuit: Operation[S, Out, E.Halt :: _0]) 52 | ) 53 | 54 | /** 55 | * Only continue this process if the given predicate is fulfilled, terminate 56 | * it otherwise. 57 | */ 58 | def withFilter(p: Out ⇒ Boolean)( 59 | implicit pr: E.ops.Prepend[E, E.Choice[(E.Halt :: _0) :+: _0 :+: CNil] :: _0]): Operation[S, Out, pr.Out] = 60 | flatMap(o ⇒ 61 | ScalaDSL.opChoice(p(o), Impl.Return(o): Operation[S, Out, _0]).orElse(Impl.ShortCircuit: Operation[S, Out, E.Halt :: _0]) 62 | ) 63 | 64 | /** 65 | * Wrap as a [[Process]] with infinite timeout and a mailbox capacity of 1. 66 | * Small processes that are called or chained often interact in a fully 67 | * sequential fashion, where these defaults make sense. 68 | */ 69 | def named(name: String): Process[S, Out, E] = Process(name, Duration.Inf, 1, this) 70 | 71 | /** 72 | * Wrap as a [[Process]] with the given mailbox capacity and infinite timeout. 73 | */ 74 | def withMailboxCapacity(mailboxCapacity: Int): Process[S, Out, E] = named("").withMailboxCapacity(mailboxCapacity) 75 | 76 | /** 77 | * Wrap as a [[Process]] with the given timeout and a mailbox capacity of 1. 78 | */ 79 | def withTimeout(timeout: Duration): Process[S, Out, E] = named("").withTimeout(timeout) 80 | 81 | /** 82 | * Wrap as a [[Process]] but without a name and convert to a [[Behavior]]. 83 | */ 84 | def toBehavior: Behavior[ActorCmd[S]] = named("main").toBehavior 85 | 86 | } 87 | 88 | /* 89 | * These are the private values that make up the core algebra. 90 | */ 91 | 92 | object Impl { 93 | final case class FlatMap[S, Out1, Out2, E1 <: Effects, E2 <: Effects, E <: Effects]( 94 | first: Operation[S, Out1, E1], andThen: Out1 ⇒ Operation[S, Out2, E2]) extends Operation[S, Out2, E] { 95 | override def toString: String = s"FlatMap($first)" 96 | } 97 | case object ShortCircuit extends Operation[Nothing, Nothing, E.Halt :: _0] { 98 | override def flatMap[T, E <: Effects](f: Nothing ⇒ Operation[Nothing, T, E])( 99 | implicit p: E.ops.Prepend[E.Halt :: _0, E]): Operation[Nothing, T, p.Out] = this.asInstanceOf[Operation[Nothing, T, p.Out]] 100 | } 101 | 102 | case object System extends Operation[Nothing, ActorSystem[Nothing], _0] 103 | case object Read extends Operation[Nothing, Nothing, E.Read[Any] :: _0] 104 | case object ProcessSelf extends Operation[Nothing, ActorRef[Any], _0] 105 | case object ActorSelf extends Operation[Nothing, ActorRef[ActorCmd[Nothing]], _0] 106 | final case class Choice[S, T, E <: Coproduct](ops: Operation[S, T, _0]) extends Operation[S, T, E.Choice[E] :: _0] 107 | final case class Return[T](value: T) extends Operation[Nothing, T, _0] 108 | final case class Call[S, T, E <: Effects](process: Process[S, T, E], replacement: Option[T]) extends Operation[Nothing, T, E] 109 | final case class Fork[S, E <: Effects](process: Process[S, Any, E]) extends Operation[Nothing, SubActor[S], E.Fork[E] :: _0] 110 | final case class Spawn[S, E <: Effects](process: Process[S, Any, E], deployment: Props) extends Operation[Nothing, ActorRef[ActorCmd[S]], E.Spawn[E] :: _0] 111 | final case class Schedule[T](delay: FiniteDuration, msg: T, target: ActorRef[T]) extends Operation[Nothing, a.Cancellable, E.Send[T] :: _0] 112 | sealed trait AbstractWatchRef { type Msg } 113 | final case class WatchRef[T](watchee: ActorRef[Nothing], target: ActorRef[T], msg: T, onFailure: Throwable ⇒ Option[T]) 114 | extends Operation[Nothing, a.Cancellable, _0] with AbstractWatchRef { 115 | type Msg = T 116 | override def equals(other: Any) = super.equals(other) 117 | override def hashCode() = super.hashCode() 118 | } 119 | //final case class Replay[T](key: StateKey[T]) extends Operation[Nothing, T] 120 | //final case class Snapshot[T](key: StateKey[T]) extends Operation[Nothing, T] 121 | final case class State[S, T <: StateKey[S], Ev, Ex](key: T { type Event = Ev }, afterUpdates: Boolean, transform: S ⇒ (Seq[Ev], Ex)) extends Operation[Nothing, Ex, _0] 122 | final case class StateR[S, T <: StateKey[S], Ev](key: T { type Event = Ev }, afterUpdates: Boolean, transform: S ⇒ Seq[Ev]) extends Operation[Nothing, S, _0] 123 | final case class Forget[T](key: StateKey[T]) extends Operation[Nothing, akka.Done, _0] 124 | final case class Cleanup(cleanup: () ⇒ Unit) extends Operation[Nothing, akka.Done, _0] 125 | } 126 | -------------------------------------------------------------------------------- /src/test/scala/com/rolandkuhn/akka_typed_session/TypedSpec.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2014-2017 Lightbend Inc. 3 | */ 4 | package com.rolandkuhn.akka_typed_session 5 | 6 | import org.scalatest.refspec.RefSpec 7 | import org.scalatest.Matchers 8 | import org.scalatest.BeforeAndAfterAll 9 | 10 | import scala.concurrent.Await 11 | import scala.concurrent.duration._ 12 | import scala.concurrent.Future 13 | import com.typesafe.config.Config 14 | import com.typesafe.config.ConfigFactory 15 | import akka.util.Timeout 16 | 17 | import scala.reflect.ClassTag 18 | import akka.actor.ActorInitializationException 19 | import akka.typed._ 20 | 21 | import language.existentials 22 | import akka.testkit.TestEvent.Mute 23 | import akka.typed.scaladsl.Actor._ 24 | import org.scalatest.concurrent.ScalaFutures 25 | import org.scalactic.TypeCheckedTripleEquals 26 | import org.scalactic.CanEqual 27 | 28 | import scala.util.control.NonFatal 29 | import akka.typed.scaladsl.AskPattern 30 | 31 | import scala.util.control.NoStackTrace 32 | import akka.typed.testkit.{ Inbox, TestKitSettings } 33 | import org.scalatest.time.Span 34 | import akka.testkit.EventFilter 35 | 36 | /** 37 | * Helper class for writing tests for typed Actors with ScalaTest. 38 | */ 39 | class TypedSpecSetup extends RefSpec with Matchers with BeforeAndAfterAll with ScalaFutures with TypeCheckedTripleEquals { 40 | 41 | // TODO hook this up with config like in akka-testkit/AkkaSpec? 42 | implicit val akkaPatience = PatienceConfig(3.seconds, Span(100, org.scalatest.time.Millis)) 43 | 44 | } 45 | 46 | /** 47 | * Helper class for writing tests against both ActorSystemImpl and ActorSystemAdapter. 48 | */ 49 | abstract class TypedSpec(val config: Config) extends TypedSpecSetup { 50 | import TypedSpec._ 51 | import AskPattern._ 52 | 53 | def this() = this(ConfigFactory.empty) 54 | 55 | def this(config: String) = this(ConfigFactory.parseString(config)) 56 | 57 | // extension point 58 | def setTimeout: Timeout = Timeout(1.minute) 59 | 60 | private var nativeSystemUsed = false 61 | lazy val nativeSystem: ActorSystem[TypedSpec.Command] = { 62 | val sys = ActorSystem(AkkaSpec.getCallerName(classOf[TypedSpec]), guardian(), config = Some(config withFallback AkkaSpec.testConf)) 63 | nativeSystemUsed = true 64 | sys 65 | } 66 | private var adaptedSystemUsed = false 67 | lazy val adaptedSystem: ActorSystem[TypedSpec.Command] = { 68 | val sys = ActorSystem.adapter(AkkaSpec.getCallerName(classOf[TypedSpec]), guardian(), config = Some(config withFallback AkkaSpec.testConf)) 69 | adaptedSystemUsed = true 70 | sys 71 | } 72 | 73 | implicit val timeout = setTimeout 74 | implicit def scheduler = nativeSystem.scheduler 75 | 76 | trait StartSupport { 77 | def system: ActorSystem[TypedSpec.Command] 78 | 79 | private val nameCounter = Iterator.from(0) 80 | def nextName(prefix: String = "a"): String = s"$prefix-${nameCounter.next()}" 81 | 82 | def start[T](behv: Behavior[T]): ActorRef[T] = { 83 | import akka.typed.scaladsl.AskPattern._ 84 | import akka.typed.testkit.scaladsl._ 85 | implicit val testSettings = TestKitSettings(system) 86 | Await.result(system ? TypedSpec.Create(behv, nextName()), 3.seconds.dilated) 87 | } 88 | } 89 | 90 | trait NativeSystem { 91 | def system: ActorSystem[TypedSpec.Command] = nativeSystem 92 | } 93 | 94 | trait AdaptedSystem { 95 | def system: ActorSystem[TypedSpec.Command] = adaptedSystem 96 | } 97 | 98 | override def afterAll(): Unit = { 99 | if (nativeSystemUsed) 100 | Await.result(nativeSystem.terminate, timeout.duration) 101 | if (adaptedSystemUsed) 102 | Await.result(adaptedSystem.terminate, timeout.duration) 103 | } 104 | 105 | // TODO remove after basing on ScalaTest 3 with async support 106 | import akka.testkit._ 107 | def await[T](f: Future[T]): T = Await.result(f, timeout.duration * 1.1) 108 | 109 | lazy val blackhole = await(nativeSystem ? Create(immutable[Any] { case _ ⇒ same }, "blackhole")) 110 | 111 | /** 112 | * Run an Actor-based test. The test procedure is most conveniently 113 | * formulated using the [[StepWise$]] behavior type. 114 | */ 115 | def runTest[T: ClassTag](name: String)(behavior: Behavior[T])(implicit system: ActorSystem[Command]): Future[Status] = 116 | system ? (RunTest(name, behavior, _, timeout.duration)) 117 | 118 | // TODO remove after basing on ScalaTest 3 with async support 119 | def sync(f: Future[Status])(implicit system: ActorSystem[Command]): Unit = { 120 | def unwrap(ex: Throwable): Throwable = ex match { 121 | case ActorInitializationException(_, _, ex) ⇒ ex 122 | case other ⇒ other 123 | } 124 | 125 | try await(f) match { 126 | case Success ⇒ () 127 | case Failed(ex) ⇒ 128 | unwrap(ex) match { 129 | case ex2: TypedSpec.SimulatedException ⇒ 130 | throw ex2 131 | case _ ⇒ 132 | println(system.printTree) 133 | throw unwrap(ex) 134 | } 135 | case Timedout ⇒ 136 | println(system.printTree) 137 | fail("test timed out") 138 | } catch { 139 | case ex: TypedSpec.SimulatedException ⇒ 140 | throw ex 141 | case NonFatal(ex) ⇒ 142 | println(system.printTree) 143 | throw ex 144 | } 145 | } 146 | 147 | def muteExpectedException[T <: Exception: ClassTag]( 148 | message: String = null, 149 | source: String = null, 150 | start: String = "", 151 | pattern: String = null, 152 | occurrences: Int = Int.MaxValue)(implicit system: ActorSystem[Command]): EventFilter = { 153 | val filter = EventFilter(message, source, start, pattern, occurrences) 154 | system.eventStream.publish(Mute(filter)) 155 | filter 156 | } 157 | 158 | /** 159 | * Group assertion that ensures that the given inboxes are empty. 160 | */ 161 | def assertEmpty(inboxes: Inbox[_]*): Unit = { 162 | inboxes foreach (i ⇒ withClue(s"inbox $i had messages")(i.hasMessages should be(false))) 163 | } 164 | 165 | // for ScalaTest === compare of Class objects 166 | implicit def classEqualityConstraint[A, B]: CanEqual[Class[A], Class[B]] = 167 | new CanEqual[Class[A], Class[B]] { 168 | def areEqual(a: Class[A], b: Class[B]) = a == b 169 | } 170 | 171 | implicit def setEqualityConstraint[A, T <: Set[_ <: A]]: CanEqual[Set[A], T] = 172 | new CanEqual[Set[A], T] { 173 | def areEqual(a: Set[A], b: T) = a == b 174 | } 175 | } 176 | 177 | object TypedSpec { 178 | import akka.{ typed ⇒ t } 179 | 180 | sealed abstract class Start 181 | case object Start extends Start 182 | 183 | sealed trait Command 184 | case class RunTest[T](name: String, behavior: Behavior[T], replyTo: ActorRef[Status], timeout: FiniteDuration) extends Command 185 | case class Terminate(reply: ActorRef[Status]) extends Command 186 | case class Create[T](behavior: Behavior[T], name: String)(val replyTo: ActorRef[ActorRef[T]]) extends Command 187 | 188 | sealed trait Status 189 | case object Success extends Status 190 | case class Failed(thr: Throwable) extends Status 191 | case object Timedout extends Status 192 | 193 | class SimulatedException(message: String) extends RuntimeException(message) with NoStackTrace 194 | 195 | def guardian(outstanding: Map[ActorRef[_], ActorRef[Status]] = Map.empty): Behavior[Command] = 196 | immutable[Command] { 197 | case (ctx, r: RunTest[t]) ⇒ 198 | val test = ctx.spawn(r.behavior, r.name) 199 | ctx.schedule(r.timeout, r.replyTo, Timedout) 200 | ctx.watch(test) 201 | guardian(outstanding + ((test, r.replyTo))) 202 | case (_, Terminate(reply)) ⇒ 203 | reply ! Success 204 | stopped 205 | case (ctx, c: Create[t]) ⇒ 206 | c.replyTo ! ctx.spawn(c.behavior, c.name) 207 | same 208 | } onSignal { 209 | case (ctx, t @ Terminated(test)) ⇒ 210 | outstanding get test match { 211 | case Some(reply) ⇒ 212 | if (t.failure eq null) reply ! Success 213 | else reply ! Failed(t.failure) 214 | guardian(outstanding - test) 215 | case None ⇒ same 216 | } 217 | case _ ⇒ same 218 | } 219 | 220 | def getCallerName(clazz: Class[_]): String = { 221 | val s = (Thread.currentThread.getStackTrace map (_.getClassName) drop 1) 222 | .dropWhile(_ matches "(java.lang.Thread|.*TypedSpec.?$)") 223 | val reduced = s.lastIndexWhere(_ == clazz.getName) match { 224 | case -1 ⇒ s 225 | case z ⇒ s drop (z + 1) 226 | } 227 | reduced.head.replaceFirst(""".*\.""", "").replaceAll("[^a-zA-Z_0-9]", "_") 228 | } 229 | } 230 | 231 | class TypedSpecSpec extends TypedSpec { 232 | 233 | object `A TypedSpec` { 234 | 235 | trait CommonTests { 236 | implicit def system: ActorSystem[TypedSpec.Command] 237 | 238 | def `must report failures`(): Unit = { 239 | val f = 240 | if (system == nativeSystem) muteExpectedException[TypedSpec.SimulatedException](occurrences = 1) 241 | else muteExpectedException[ActorInitializationException](occurrences = 1) 242 | a[TypedSpec.SimulatedException] must be thrownBy { 243 | sync(runTest("failure")(deferred[String] { ctx => 244 | throw new TypedSpec.SimulatedException("expected") 245 | })) 246 | } 247 | f.assertDone(1.second) 248 | } 249 | } 250 | 251 | object `when using the native implementation` extends CommonTests with NativeSystem 252 | object `when using the adapted implementation` extends CommonTests with AdaptedSystem 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /src/main/scala/com/rolandkuhn/akka_typed_session/ScalaDSL.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2016 Lightbend Inc. 3 | */ 4 | package com.rolandkuhn.akka_typed_session 5 | 6 | import akka.typed._ 7 | import scala.concurrent.duration._ 8 | import akka.{ actor ⇒ a } 9 | import scala.util.control.NoStackTrace 10 | import shapeless.{ Coproduct, :+:, CNil } 11 | import shapeless.ops.coproduct 12 | import akka.typed.patterns.Receptionist 13 | 14 | /** 15 | * A DSL for writing reusable behavior pieces that are executed concurrently 16 | * within Actors. 17 | * 18 | * Terminology: 19 | * 20 | * - a Process has a 1:1 relationship with an ActorRef 21 | * - an Operation is a step that a Process takes and that produces a value 22 | * - Processes are concurrent, but not distributed: all failures stop the entire Actor 23 | * - each Process has its own identity (due to ActorRef), and the Actor has its own 24 | * identity (an ActorRef[ActorCmd[_]]); processSelf is the Process’ identity, actorSelf is the Actor’s 25 | * - process timeout means failure 26 | * - every Actor has a KV store for state 27 | * 28 | * - querying by key (using a single element per slot) 29 | * - updating is an Operation that produces events that are applied to the state 30 | * - persistence can be plugged in transparently (NOT YET IMPLEMENTED) 31 | * - recovery means acquiring state initially (which might trigger internal replay) 32 | */ 33 | object ScalaDSL { 34 | 35 | /** 36 | * Exception type that is thrown by the `retry` facility when after the 37 | * given number of retries still no value has been obtained. 38 | */ 39 | final class RetriesExceeded(message: String) extends RuntimeException(message) with NoStackTrace 40 | 41 | /** 42 | * This is a compile-time marker for the type of self-reference expected by 43 | * the process that is being described. No methods can be called on a value 44 | * of this type. It is used as follows: 45 | * 46 | * {{{ 47 | * OpDSL[MyType] { implicit opDSL => 48 | * ... // use Operation operators here 49 | * } 50 | * }}} 51 | */ 52 | sealed trait OpDSL extends Any { 53 | type Self 54 | } 55 | 56 | /** 57 | * This object offers different constructors that provide a scope within 58 | * which [[Operation]] values can be created using the `op*` methods. The 59 | * common characteristic of these constructors is that they lift their 60 | * contents completely into the resulting process description, in other 61 | * words the code within is only evaluated once the [[Operation]] has been 62 | * called, forked, or spawned within an Actor. 63 | * 64 | * It is strongly recommended to always use the same name for the required 65 | * implicit function argument (`opDSL` in the examples below) in order to 66 | * achieve proper scoping for nested declarations. 67 | * 68 | * Usage for single-shot processes: 69 | * {{{ 70 | * OpDSL[MyType] { implicit opDSL => 71 | * for { 72 | * x <- step1 73 | * y <- step2 74 | * ... 75 | * } ... 76 | * } 77 | * }}} 78 | * 79 | * Usage for bounded repetition (will run the whole process three times 80 | * in this example and yield a list of the three results): 81 | * {{{ 82 | * OpDSL.loop[MyType](3) { implicit opDSL => 83 | * for { 84 | * x <- step1 85 | * y <- step2 86 | * ... 87 | * } ... 88 | * } 89 | * }}} 90 | * 91 | * Usage for infinite repetition, for example when writing a server process: 92 | * {{{ 93 | * OpDSL.loopInf[MyType] { implicit opDSL => 94 | * for { 95 | * x <- step1 96 | * y <- step2 97 | * ... 98 | * } ... 99 | * } 100 | * }}} 101 | */ 102 | object OpDSL { 103 | private val _unit: Operation[Nothing, Null, _0] = opUnit(null)(null: OpDSL { type Self = Nothing }) 104 | private def unit[S, Out]: Operation[S, Out, _0] = _unit.asInstanceOf[Operation[S, Out, _0]] 105 | 106 | def loopInf[S]: NextLoopInf[S] = nextLoopInf.asInstanceOf[NextLoopInf[S]] 107 | trait NextLoopInf[S] { 108 | def apply[U, E <: Effects](body: OpDSL { type Self = S } ⇒ Operation[S, U, E]): Operation[S, Nothing, Loop[E]] = { 109 | lazy val l: Operation[S, Nothing, E] = unit[S, OpDSL { type Self = S }].flatMap(body).withEffects[_0].flatMap(_ ⇒ l) 110 | l.withEffects[Loop[E]] 111 | } 112 | } 113 | private object nextLoopInf extends NextLoopInf[Nothing] 114 | 115 | def apply[T]: Next[T] = next.asInstanceOf[Next[T]] 116 | trait Next[T] { 117 | def apply[U, E <: Effects](body: OpDSL { type Self = T } ⇒ Operation[T, U, E]): Operation[T, U, E] = 118 | unit[T, OpDSL { type Self = T }].flatMap(body) 119 | } 120 | private object next extends Next[Nothing] 121 | 122 | trait NextStep[T] { 123 | def apply[U, E <: Effects](mailboxCapacity: Int, body: OpDSL { type Self = T } ⇒ Operation[T, U, E])( 124 | implicit opDSL: OpDSL): Operation[opDSL.Self, U, E] = 125 | Impl.Call(Process("nextStep", Duration.Inf, mailboxCapacity, body(null)), None) 126 | } 127 | object nextStep extends NextStep[Nothing] 128 | } 129 | 130 | /* 131 | * The core operations: keep these minimal! 132 | */ 133 | 134 | /** 135 | * Obtain a reference to the ActorSystem in which this process is running. 136 | */ 137 | def opSystem(implicit opDSL: OpDSL): Operation[opDSL.Self, ActorSystem[Nothing], _0] = Impl.System 138 | 139 | /** 140 | * Read a message from this process’ input channel. 141 | */ 142 | def opRead(implicit opDSL: OpDSL): Operation[opDSL.Self, opDSL.Self, E.Read[opDSL.Self] :: _0] = Impl.Read 143 | 144 | def opSend[T](target: ActorRef[T], msg: T)(implicit opDSL: OpDSL): Operation[opDSL.Self, a.Cancellable, E.Send[T] :: _0] = 145 | opSchedule(Duration.Zero, target, msg) 146 | 147 | /** 148 | * Obtain this process’ [[ActorRef]], not to be confused with the ActorRef of the Actor this process is running in. 149 | */ 150 | def opProcessSelf(implicit opDSL: OpDSL): Operation[opDSL.Self, ActorRef[opDSL.Self], _0] = Impl.ProcessSelf 151 | 152 | /** 153 | * Obtain the [[ActorRef]] of the Actor this process is running in. 154 | */ 155 | def opActorSelf(implicit opDSL: OpDSL): Operation[opDSL.Self, ActorRef[ActorCmd[Nothing]], _0] = Impl.ActorSelf 156 | 157 | /** 158 | * Lift a plain value into a process that returns that value. 159 | */ 160 | def opUnit[U](value: U)(implicit opDSL: OpDSL): Operation[opDSL.Self, U, _0] = Impl.Return(value) 161 | 162 | /** 163 | * Start a list of choices. The effects of the choices are accumulated in 164 | * reverse order in the Coproduct within the Choice effect. 165 | * 166 | * {{{ 167 | * opChoice(x > 5, opRead) 168 | * .elseIf(x > 0, opUnit(42)) 169 | * .orElse(opAsk(someActor, GetNumber)) 170 | * : Operation[Int, Int, Choice[ 171 | * (Send[GetNumber] :: Read[Int] :: _0) :+: 172 | * _0 :+: 173 | * (Read[Int] :: _0) :+: 174 | * CNil] :: _0] 175 | * }}} 176 | */ 177 | def opChoice[S, O, E <: Effects](p: Boolean, op: ⇒ Operation[S, O, E]): OpChoice[S, Operation[S, O, _0], CNil, E :+: CNil, Operation[S, O, _0], O] = 178 | if (p) new OpChoice(Some(Coproduct(op.ignoreEffects))) 179 | else new OpChoice(None) 180 | 181 | class OpChoice[S, H <: Operation[S, _, _], T <: Coproduct, E0 <: Coproduct, +O <: Operation[S, Output, _], Output](ops: Option[H :+: T])( 182 | implicit u: coproduct.Unifier.Aux[H :+: T, O]) { 183 | 184 | def elseIf[O1 >: Output, E1 <: Effects](p: => Boolean, op: => Operation[S, O1, E1])( 185 | implicit u1: coproduct.Unifier.Aux[Operation[S, O1, _0] :+: H :+: T, Operation[S, O1, _0]]): OpChoice[S, Operation[S, O1, _0], H :+: T, E1 :+: E0, Operation[S, O1, _0], O1] = { 186 | val ret = ops match { 187 | case Some(c) => Some(c.extendLeft[Operation[S, O1, _0]]) 188 | case None => if (p) Some(Coproduct[Operation[S, O1, _0] :+: H :+: T](op.ignoreEffects)) else None 189 | } 190 | new OpChoice(ret) 191 | } 192 | 193 | def orElse[O1 >: Output, E1 <: Effects](op: => Operation[S, O1, E1])( 194 | implicit u1: coproduct.Unifier.Aux[Operation[S, O1, _0] :+: H :+: T, Operation[S, O1, _0]]): Operation[S, O1, E.Choice[E1 :+: E0] :: _0] = { 195 | val ret = ops match { 196 | case Some(c) => c.extendLeft[Operation[S, O1, _0]] 197 | case None => Coproduct[Operation[S, O1, _0] :+: H :+: T](op.ignoreEffects) 198 | } 199 | Impl.Choice[S, O1, E1 :+: E0](u1(ret)) 200 | } 201 | } 202 | 203 | /** 204 | * Execute the given process within the current Actor, await and return that process’ result. 205 | * If the process does not return a result (due to a non-matching `filter` expression), the 206 | * replacement value is used if the provided Option contains a value. 207 | */ 208 | def opCall[Self, Out, E <: Effects](process: Process[Self, Out, E], replacement: Option[Out] = None)( 209 | implicit opDSL: OpDSL): Operation[opDSL.Self, Out, E] = 210 | Impl.Call(process, replacement) 211 | 212 | /** 213 | * Create and execute a process with a self reference of the given type, 214 | * await and return that process’ result. This is equivalent to creating 215 | * a process with [[OpDSL]] and using `call` to execute it. A replacement 216 | * value is not provided; if recovery from a halted subprocess is desired 217 | * please use `opCall` directly. 218 | */ 219 | def opNextStep[T]: OpDSL.NextStep[T] = 220 | OpDSL.nextStep.asInstanceOf[OpDSL.NextStep[T]] 221 | 222 | /** 223 | * Execute the given process within the current Actor, concurrently with the 224 | * current process. The value computed by the forked process cannot be 225 | * observed, instead you would have the forked process send a message to the 226 | * current process to communicate results. The returned [[SubActor]] reference 227 | * can be used to send messages to the forked process or to cancel it. 228 | */ 229 | def opFork[Self, E <: Effects](process: Process[Self, Any, E])(implicit opDSL: OpDSL): Operation[opDSL.Self, SubActor[Self], E.Fork[E] :: _0] = 230 | Impl.Fork(process) 231 | 232 | /** 233 | * Execute the given process in a newly spawned child Actor of the current 234 | * Actor. The new Actor is fully encapsulated behind the [[ActorRef]] that 235 | * is returned. 236 | * 237 | * The mailboxCapacity for the Actor is configured using the optional 238 | * [[DeploymentConfig]] while the initial process’ process mailbox is 239 | * limited based on the [[Process]] configuration as usual. When sizing 240 | * the Actor mailbox capacity you need to consider that communication 241 | * between the processes hosted by that Actor and timeouts also go through 242 | * this mailbox. 243 | */ 244 | def opSpawn[Self, E <: Effects](process: Process[Self, Any, E], deployment: Props = Props.empty)( 245 | implicit opDSL: OpDSL): Operation[opDSL.Self, ActorRef[ActorCmd[Self]], E.Spawn[E] :: _0] = 246 | Impl.Spawn(process, deployment) 247 | 248 | /** 249 | * Schedule a message to be sent after the given delay has elapsed. 250 | */ 251 | def opSchedule[T](delay: FiniteDuration, target: ActorRef[T], msg: T)(implicit opDSL: OpDSL): Operation[opDSL.Self, a.Cancellable, E.Send[T] :: _0] = 252 | Impl.Schedule(delay, msg, target) 253 | 254 | /** 255 | * Watch the given [[ActorRef]] and send the specified message to the given 256 | * target when the watched actor has terminated. The returned Cancellable 257 | * can be used to unwatch the watchee, which will inhibit the message from 258 | * being dispatched—it might still be delivered if it was previously dispatched. 259 | * 260 | * If `onFailure` is provided it can override the value to be sent if the 261 | * watched Actor failed and was a child Actor of the Actor hosting this process. 262 | */ 263 | def opWatch[T](watchee: ActorRef[Nothing], target: ActorRef[T], msg: T, onFailure: Throwable ⇒ Option[T] = any2none)( 264 | implicit opDSL: OpDSL): Operation[opDSL.Self, a.Cancellable, _0] = 265 | Impl.WatchRef(watchee, target, msg, onFailure) 266 | 267 | val any2none = (_: Any) ⇒ None 268 | private val _any2Nil = (state: Any) ⇒ Nil → state 269 | private def any2Nil[T] = _any2Nil.asInstanceOf[T ⇒ (Nil.type, T)] 270 | 271 | /** 272 | * Read the state stored for the given [[StateKey]], suspending this process 273 | * until after all outstanding updates for the key have been completed if 274 | * `afterUpdates` is `true`. 275 | */ 276 | def opReadState[T](key: StateKey[T], afterUpdates: Boolean = true)(implicit opDSL: OpDSL): Operation[opDSL.Self, T, _0] = 277 | Impl.State[T, StateKey[T], key.Event, T](key, afterUpdates, any2Nil) 278 | 279 | /** 280 | * Update the state stored for the given [[StateKey]] by emitting events that 281 | * are applied to the state in order, suspending this process 282 | * until after all outstanding updates for the key have been completed if 283 | * `afterUpdates` is `true`. The return value is determined by the transform 284 | * function based on the current state; if you want to return the state that 285 | * results from having applied the emitted events then please see 286 | * [[ScalaProcess#opUpdateAndReadState]]. 287 | */ 288 | def opUpdateState[T, Ev, Ex](key: StateKey[T] { type Event = Ev }, afterUpdates: Boolean = true)( 289 | transform: T ⇒ (Seq[Ev], Ex))(implicit opDSL: OpDSL): Operation[opDSL.Self, Ex, _0] = 290 | Impl.State(key, afterUpdates, transform) 291 | 292 | /** 293 | * Update the state by emitting a sequence of events, returning the updated state. The 294 | * process is suspended until after all outstanding updates for the key have been 295 | * completed if `afterUpdates` is `true`. 296 | */ 297 | def opUpdateAndReadState[T, Ev](key: StateKey[T] { type Event = Ev }, afterUpdates: Boolean = true)( 298 | transform: T ⇒ Seq[Ev])(implicit opDSL: OpDSL): Operation[opDSL.Self, T, _0] = 299 | Impl.StateR(key, afterUpdates, transform) 300 | 301 | /** 302 | * FIXME not yet implemented 303 | * 304 | * Instruct the Actor to persist the state for the given [[StateKey]] after 305 | * all currently outstanding updates for this key have been completed, 306 | * suspending this process until done. 307 | */ 308 | //def opTakeSnapshot[T](key: PersistentStateKey[T])(implicit opDSL: OpDSL): Operation[opDSL.Self, T] = 309 | // Snapshot(key) 310 | 311 | /** 312 | * FIXME not yet implemented 313 | * 314 | * Restore the state for the given [[StateKey]] from persistent event storage. 315 | * If a snapshot is found it will be used as the starting point for the replay, 316 | * otherwise events are replayed from the beginning of the event log, starting 317 | * with the given initial data as the state before the first event is applied. 318 | */ 319 | //def opReplayPersistentState[T](key: PersistentStateKey[T])(implicit opDSL: OpDSL): Operation[opDSL.Self, T] = 320 | // Replay(key) 321 | 322 | /** 323 | * Remove the given [[StateKey]] from this Actor’s storage. The slot can be 324 | * filled again using `updateState` or `replayPersistentState`. 325 | */ 326 | def opForgetState[T](key: StateKey[T])(implicit opDSL: OpDSL): Operation[opDSL.Self, akka.Done, _0] = 327 | Impl.Forget(key) 328 | 329 | /** 330 | * Run the given cleanup handler after the operations that will be chained 331 | * off of this one, i.e. this operation must be further transformed to make 332 | * sense. 333 | * 334 | * Usage with explicit combinators: 335 | * {{{ 336 | * opCleanup(() => doCleanup()) 337 | * .flatMap { _ => 338 | * ... 339 | * } // doCleanup() will run here 340 | * .flatMap { ... } 341 | * }}} 342 | * 343 | * Usage with for-expressions: 344 | * {{{ 345 | * (for { 346 | * resource <- obtainResource 347 | * _ <- opCleanup(() => doCleanup(resource)) 348 | * ... 349 | * } yield ... 350 | * ) // doCleanup() will run here 351 | * .flatMap { ... } 352 | * }}} 353 | * 354 | * Unorthodox usage: 355 | * {{{ 356 | * (for { 357 | * resource <- obtainResource 358 | * ... 359 | * } yield opCleanup(() => doCleanup(resource)) 360 | * ) // doCleanup() will run here 361 | * .flatMap { ... } 362 | * }}} 363 | */ 364 | def opCleanup(cleanup: () ⇒ Unit)(implicit opDSL: OpDSL): Operation[opDSL.Self, akka.Done, _0] = 365 | Impl.Cleanup(cleanup) 366 | 367 | /** 368 | * Terminate processing here, ignoring further transformations. If this process 369 | * has been called by another process then the `replacement` argument to `opCall` 370 | * determines whether the calling process continues or halts as well: if no 371 | * replacement is given, processing cannot go on. 372 | */ 373 | def opHalt(implicit opDSL: OpDSL): Operation[opDSL.Self, Nothing, E.Halt :: _0] = Impl.ShortCircuit 374 | 375 | // FIXME opChildList 376 | // FIXME opProcessList 377 | // FIXME opTerminate 378 | // FIXME opStopChild 379 | // FIXME opAsk(Main) 380 | // FIXME opParallel 381 | // FIXME opUpdate(Read)SimpleState 382 | 383 | /* 384 | * Derived operations 385 | */ 386 | 387 | /** 388 | * Suspend the process for the given time interval and deliver the specified 389 | * value afterwards. This is especially useful as a timeout value for `firstOf`. 390 | */ 391 | def delay[T](time: FiniteDuration, value: T): Operation[T, T, _0] = 392 | OpDSL[T] { implicit opDSL ⇒ 393 | for { 394 | self ← opProcessSelf 395 | _ ← opSchedule(time, self, value) 396 | } yield opRead 397 | }.ignoreEffects 398 | 399 | /** 400 | * Fork the given process, but also fork another process that will cancel the 401 | * first process after the given timeout. 402 | */ 403 | def forkAndCancel[T, E <: Effects](timeout: FiniteDuration, process: Process[T, Any, E])( 404 | implicit opDSL: OpDSL): Operation[opDSL.Self, SubActor[T], E.Fork[E] :: E.Fork[E.Send[Boolean] :: E.Read[Boolean] :: E.Choice[(E.Halt :: _0) :+: _0 :+: CNil] :: _0] :: _0] = { 405 | def guard(sub: SubActor[T]) = OpDSL[Boolean] { implicit opDSL ⇒ 406 | for { 407 | self ← opProcessSelf 408 | _ ← opWatch(sub.ref, self, false) 409 | _ ← opSchedule(timeout, self, true) 410 | cancel ← opRead 411 | if cancel 412 | } yield sub.cancel() 413 | } 414 | 415 | for { 416 | sub ← opFork(process) 417 | _ ← opFork(guard(sub).named("cancelAfter")) 418 | } yield sub 419 | } 420 | 421 | /** 422 | * Fork the given processes the return the first value emitted by any one of 423 | * them. As soon as one process has yielded its value all others are canceled. 424 | * 425 | * TODO figure out effects 426 | */ 427 | def firstOf[T](processes: Process[_, T, _ <: Effects]*): Operation[T, T, _0] = { 428 | def forkAll(self: ActorRef[T], index: Int = 0, 429 | p: List[Process[_, T, _ <: Effects]] = processes.toList, 430 | acc: List[SubActor[Nothing]] = Nil)(implicit opDSL: OpDSL { type Self = T }): Operation[T, List[SubActor[Nothing]], _0] = 431 | p match { 432 | case Nil ⇒ opUnit(acc) 433 | case x :: xs ⇒ 434 | opFork(x.copy(name = s"$index-${x.name}").map(self ! _)) 435 | .map(sub ⇒ forkAll(self, index + 1, xs, sub :: acc)) 436 | .ignoreEffects 437 | } 438 | OpDSL[T] { implicit opDSL ⇒ 439 | for { 440 | self ← opProcessSelf 441 | subs ← forkAll(self) 442 | value ← opRead 443 | } yield { 444 | subs.foreach(_.cancel()) 445 | value 446 | } 447 | }.ignoreEffects 448 | } 449 | 450 | /** 451 | * Retry the given process the specified number of times, always bounding 452 | * the wait time by the given timeout and canceling the fruitless process. 453 | * If the number of retries is exhausted, the entire Actor will be failed. 454 | * 455 | * FIXME effects need more thought 456 | */ 457 | def retry[S, T, E <: Effects](timeout: FiniteDuration, retries: Int, ops: Process[S, T, E])(implicit opDSL: OpDSL): Operation[opDSL.Self, T, E] = { 458 | opCall(firstOf(ops.map(Some(_)), delay(timeout, None).named("retryTimeout")).named("firstOf")) 459 | .flatMap { 460 | case Some(res) ⇒ opUnit(res).withEffects[E] 461 | case None if retries > 0 ⇒ retry(timeout, retries - 1, ops) 462 | case None ⇒ throw new RetriesExceeded(s"process ${ops.name} has been retried $retries times with timeout $timeout") 463 | } 464 | } 465 | 466 | // FIXME effects 467 | def getService[T](key: Receptionist.ServiceKey[T]): Operation[Receptionist.Listing[T], ActorRef[T], _0] = { 468 | import Receptionist._ 469 | OpDSL[Listing[T]] { implicit opDSL => 470 | retry(1.second, 10, (for { 471 | sys <- opSystem 472 | self <- opProcessSelf 473 | _ <- opSend(sys.receptionist, Find(key)(self)) 474 | Listing(_, addresses) <- opRead 475 | } yield addresses.headOption.map(opUnit(_)).getOrElse(opHalt.ignoreEffects)).named("askReceptionist")).ignoreEffects 476 | } 477 | } 478 | 479 | } 480 | -------------------------------------------------------------------------------- /src/main/scala/com/rolandkuhn/akka_typed_session/internal/ProcessImpl.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2016 Lightbend Inc. 3 | */ 4 | package com.rolandkuhn.akka_typed_session 5 | package internal 6 | 7 | import akka.{ actor ⇒ a } 8 | import java.util.concurrent.ArrayBlockingQueue 9 | import akka.typed._ 10 | import akka.typed.scaladsl.Actor 11 | import ScalaDSL._ 12 | import scala.concurrent.duration._ 13 | import scala.annotation.tailrec 14 | import scala.util.control.NonFatal 15 | import java.util.LinkedList 16 | import scala.collection.immutable.TreeSet 17 | import akka.actor.Cancellable 18 | import java.util.concurrent.TimeoutException 19 | import akka.util.TypedMultiMap 20 | import akka.Done 21 | import akka.event.Logging 22 | 23 | /** 24 | * Implementation notes: 25 | * 26 | * - a process is a tree of AST nodes, where each leaf is a producer of a process 27 | * (in fact the inner nodes are all FlatMap) 28 | * - interpreter has a list of currently existing processes 29 | * - processes may be active (i.e. waiting for external input) or passive (i.e. 30 | * waiting for internal progress) 31 | * - external messages and internal completions are events that are enqueued 32 | * - event processing runs in FIFO order, which ensures some fairness between 33 | * concurrent processes 34 | * - what is stored is actually a Traversal of a tree which keeps the back-links 35 | * pointing towards the root; this is cheaper than rewriting the trees 36 | * - the maximum event queue size is bounded by #processes (multiple 37 | * events from the same channel can be coalesced inside that channel by a counter) 38 | * - this way even complex process trees can be executed with minimal allocations 39 | * (fixed-size preallocated arrays for event queue and back-links, preallocated 40 | * processes can even be reentrant due to separate unique Traversals) 41 | * 42 | * TODO: 43 | * enable noticing when watchee failed 44 | */ 45 | private[akka_typed_session] object ProcessInterpreter { 46 | 47 | sealed trait TraversalState 48 | case object HasValue extends TraversalState 49 | case object NeedsTrampoline extends TraversalState 50 | case object NeedsExternalInput extends TraversalState 51 | case object NeedsInternalInput extends TraversalState 52 | 53 | // used when ShortCircuit stops the process 54 | case object NoValue 55 | 56 | final case class RunCleanup(cleanup: () ⇒ Unit) 57 | 58 | val Debug = false 59 | 60 | /* 61 | * The normal ordering provides a transitive order that does not cope well 62 | * with wrap-arounds. This ordering is only transitive if all values to be 63 | * sorted are within a 290 year interval, but then it does the right thing 64 | * regardless of where a wrap-around occurs. 65 | */ 66 | implicit val timeoutOrdering: Ordering[Deadline] = 67 | new Ordering[Deadline] { 68 | override def compare(a: Deadline, b: Deadline): Int = { 69 | val diff = a.time.toNanos - b.time.toNanos 70 | if (diff > 0) 1 else if (diff < 0) -1 else 0 71 | } 72 | } 73 | val emptyTimeouts = TreeSet.empty[Deadline] 74 | val notScheduled: Cancellable = new Cancellable { 75 | override def cancel(): Boolean = false 76 | override def isCancelled: Boolean = true 77 | } 78 | 79 | val wrapReturn = (o: Any) ⇒ Impl.Return(o).asInstanceOf[Operation[Any, Any, _0]] 80 | 81 | case class Timeout(deadline: Deadline) extends InternalActorCmd[Nothing] 82 | } 83 | 84 | /** 85 | * Important: must call .execute(ctx) upon creation and return its result as the 86 | * next behavior! 87 | */ 88 | private[akka_typed_session] class ProcessInterpreter[T]( 89 | initial: ⇒ Process[T, Any, _], 90 | ctx: scaladsl.ActorContext[ActorCmd[T]]) extends ExtensibleBehavior[ActorCmd[T]] { 91 | import ProcessInterpreter._ 92 | 93 | // FIXME data structures to be optimized 94 | private var internalTriggers = Map.empty[Traversal[_], Traversal[_]] 95 | private val queue = new LinkedList[Traversal[_]] 96 | private var processRoots = Set.empty[Traversal[_]] 97 | private var timeouts = emptyTimeouts 98 | private var timeoutTask = notScheduled 99 | private var watchMap = Map.empty[ActorRef[Nothing], Set[Impl.AbstractWatchRef]] 100 | private var stateMap = Map.empty[StateKey[_], Any] 101 | private val mainProcess: Traversal[T] = new Traversal(initial, ctx) 102 | 103 | if (mainProcess.state == HasValue) triggerCompletions(ctx, mainProcess) 104 | else processRoots += mainProcess 105 | 106 | def receiveSignal(ctx: ActorContext[ActorCmd[T]], msg: Signal): Behavior[ActorCmd[T]] = { 107 | msg match { 108 | case PostStop ⇒ 109 | processRoots.foreach(_.cancel()) 110 | Actor.same 111 | case t @ Terminated(ref) ⇒ 112 | watchMap.get(ref) match { 113 | case None ⇒ Actor.unhandled 114 | case Some(set) ⇒ 115 | if (t.failure == null) set.foreach { case w: Impl.WatchRef[tpe] ⇒ w.target ! w.msg } 116 | else set.foreach { case w: Impl.WatchRef[tpe] ⇒ w.target ! w.onFailure(t.failure).getOrElse(w.msg) } 117 | watchMap -= ref 118 | Actor.same 119 | } 120 | case _ ⇒ Actor.same 121 | } 122 | } 123 | 124 | def receiveMessage(ctx: ActorContext[ActorCmd[T]], msg: ActorCmd[T]): Behavior[ActorCmd[T]] = { 125 | // for paranoia: if Timeout message is lost due to bounded mailbox (costs 50ns if nonEmpty) 126 | if (timeouts.nonEmpty && Deadline.now.time.toNanos - timeouts.head.time.toNanos >= 0) 127 | throw new TimeoutException("process timeout expired") 128 | 129 | msg match { 130 | case t: Traversal[_] ⇒ 131 | if (Debug) println(s"${ctx.asScala.self} got message for $t") 132 | if (t.isAlive) { 133 | t.registerReceipt() 134 | if (t.state == NeedsExternalInput) { 135 | t.dispatchInput(t.receiveOne(), t) 136 | triggerCompletions(ctx.asScala, t) 137 | } 138 | } 139 | execute(ctx.asScala) 140 | case Timeout(_) ⇒ 141 | // won’t get here anyway due to the clock check above, but is included for documentation 142 | Actor.same 143 | case MainCmd(cmd) ⇒ 144 | mainProcess.ref ! cmd 145 | Actor.same 146 | case _ ⇒ Actor.unhandled 147 | } 148 | } 149 | 150 | /** 151 | * Consume the queue of outstanding triggers. 152 | */ 153 | def execute(ctx: scaladsl.ActorContext[ActorCmd[T]]): Behavior[ActorCmd[T]] = { 154 | while (!queue.isEmpty()) { 155 | val traversal = queue.poll() 156 | if (traversal.state == NeedsTrampoline) traversal.dispatchTrampoline() 157 | triggerCompletions(ctx, traversal) 158 | } 159 | if (Debug) { 160 | val roots = processRoots.map(t ⇒ s"${t.process.name}(${t.ref.path.name})") 161 | val refs = ctx.children.map(_.path.name) 162 | println(s"${ctx.self} execute run finished, roots = $roots, children = $refs, timeouts = $timeouts, watchMap = $watchMap") 163 | } 164 | if (processRoots.isEmpty) Actor.stopped else this 165 | } 166 | 167 | /** 168 | * This only notifies potential listeners of the computed value of a finished 169 | * process; the process must clean itself up beforehand. 170 | */ 171 | @tailrec private def triggerCompletions(ctx: scaladsl.ActorContext[ActorCmd[T]], traversal: Traversal[_]): Unit = 172 | if (traversal.state == HasValue) { 173 | if (Debug) println(s"${ctx.self} finished $traversal") 174 | internalTriggers.get(traversal) match { 175 | case None ⇒ // nobody listening 176 | case Some(t) ⇒ 177 | internalTriggers -= traversal 178 | t.dispatchInput(traversal.getValue, traversal) 179 | triggerCompletions(ctx, t) 180 | } 181 | } 182 | 183 | def addTimeout(ctx: scaladsl.ActorContext[ActorCmd[T]], f: FiniteDuration): Deadline = { 184 | var d = Deadline.now + f 185 | while (timeouts contains d) d += 1.nanosecond 186 | if (Debug) println(s"${ctx.self} adding $d") 187 | if (timeouts.isEmpty || timeouts.head > d) scheduleTimeout(ctx, d) 188 | timeouts += d 189 | d 190 | } 191 | 192 | def removeTimeout(ctx: scaladsl.ActorContext[ActorCmd[T]], d: Deadline): Unit = { 193 | if (Debug) println(s"${ctx.self} removing $d") 194 | timeouts -= d 195 | if (timeouts.isEmpty) { 196 | timeoutTask.cancel() 197 | timeoutTask = notScheduled 198 | } else { 199 | val head = timeouts.head 200 | if (head > d) scheduleTimeout(ctx, head) 201 | } 202 | } 203 | 204 | def scheduleTimeout(ctx: scaladsl.ActorContext[ActorCmd[T]], d: Deadline): Unit = { 205 | if (Debug) println(s"${ctx.self} scheduling $d") 206 | timeoutTask.cancel() 207 | timeoutTask = ctx.schedule(d.timeLeft, ctx.self, Timeout(d)) 208 | } 209 | 210 | def watch(ctx: scaladsl.ActorContext[ActorCmd[T]], w: Impl.WatchRef[_]): Cancellable = { 211 | val watchee = w.watchee 212 | val set: Set[Impl.AbstractWatchRef] = watchMap.get(watchee) match { 213 | case None ⇒ 214 | ctx.watch(watchee) 215 | Set(w) 216 | case Some(s) ⇒ s + w 217 | } 218 | watchMap = watchMap.updated(watchee, set) 219 | new Cancellable { 220 | def cancel(): Boolean = { 221 | watchMap.get(watchee) match { 222 | case None ⇒ false 223 | case Some(s) ⇒ 224 | if (s.contains(w)) { 225 | val next = s - w 226 | if (next.isEmpty) { 227 | watchMap -= watchee 228 | ctx.unwatch(watchee) 229 | } else { 230 | watchMap = watchMap.updated(watchee, next) 231 | } 232 | true 233 | } else false 234 | } 235 | } 236 | def isCancelled: Boolean = { 237 | watchMap.get(watchee).forall(!_.contains(w)) 238 | } 239 | } 240 | } 241 | 242 | def getState[KT](key: StateKey[KT]): KT = { 243 | stateMap.get(key) match { 244 | case None ⇒ key.initial 245 | case Some(v) ⇒ v.asInstanceOf[KT] 246 | } 247 | } 248 | 249 | def setState[KT](key: StateKey[KT], value: KT): Unit = { 250 | stateMap = stateMap.updated(key, value) 251 | } 252 | 253 | private class Traversal[Tself](val process: Process[Tself, Any, _], ctx: scaladsl.ActorContext[ActorCmd[T]]) 254 | extends InternalActorCmd[Nothing] with Function1[Tself, ActorCmd[T]] 255 | with SubActor[Tself] { 256 | 257 | val deadline = process.timeout match { 258 | case f: FiniteDuration ⇒ addTimeout(ctx, f) 259 | case _ ⇒ null 260 | } 261 | 262 | /* 263 | * Implementation of the queue aspect and InternalActorCmd as well as for spawnAdapter 264 | */ 265 | 266 | private val mailQueue = new ArrayBlockingQueue[Tself](process.mailboxCapacity) // FIXME replace with lock-free queue 267 | private var toRead = 0 268 | 269 | val parent = ctx.self 270 | 271 | def registerReceipt(): Unit = toRead += 1 272 | def canReceive: Boolean = toRead > 0 273 | def receiveOne(): Tself = { 274 | toRead -= 1 275 | mailQueue.poll() 276 | } 277 | def isAlive: Boolean = toRead >= 0 278 | 279 | def apply(msg: Tself): ActorCmd[T] = 280 | if (mailQueue.offer(msg)) { 281 | if (Debug) println(s"$ref accepting message $msg") 282 | this 283 | } else { 284 | if (Debug) println(s"$ref dropping message $msg") 285 | null // adapter drops nulls 286 | } 287 | 288 | override val ref: ActorRef[Tself] = ctx.spawnAdapter(this, process.name) 289 | 290 | /* 291 | * Implementation of traversal logic 292 | */ 293 | 294 | if (Debug) println(s"${ctx.self} new traversal for $process") 295 | 296 | override def toString: String = 297 | if (Debug) { 298 | val stackList = stack.toList.map { 299 | case null ⇒ "" 300 | case t: Traversal[_] ⇒ "Traversal" 301 | case Impl.FlatMap(_, _) ⇒ "FlatMap" 302 | case other ⇒ other.toString 303 | } 304 | s"Traversal(${ref.path.name}, ${process.name}, $state, $stackList, $ptr)" 305 | } else super.toString 306 | 307 | @tailrec private def depth(op: Operation[_, Any, _], d: Int = 0): Int = { 308 | import Impl._ 309 | op match { 310 | case FlatMap(next, _) ⇒ depth(next, d + 1) 311 | case Choice(ops) => depth(ops, d) 312 | case Read | Call(_, _) | Cleanup(_) ⇒ d + 2 313 | case _ ⇒ d + 1 314 | } 315 | } 316 | 317 | /* 318 | * The state defines what is on the stack: 319 | * - HasValue means stack only contains the single end result 320 | * - NeedsTrampoline: pop value, then pop operation that needs it 321 | * - NeedsExternalInput: pop valueOrInput, then pop operation 322 | * - NeedsInternalInput: pop valueOrTraversal, then pop operation 323 | */ 324 | private var stack = new Array[AnyRef](5) 325 | private var ptr = 0 326 | private var _state: TraversalState = initialize(process.operation) 327 | 328 | private def push(v: Any): Unit = { 329 | stack(ptr) = v.asInstanceOf[AnyRef] 330 | ptr += 1 331 | } 332 | private def pop(): AnyRef = { 333 | ptr -= 1 334 | val ret = stack(ptr) 335 | stack(ptr) = null 336 | ret 337 | } 338 | private def peek(): AnyRef = 339 | if (ptr == 0) null else stack(ptr - 1) 340 | private def ensureSpace(n: Int): Unit = 341 | if (stack.length - ptr < n) { 342 | val larger = new Array[AnyRef](n + ptr) 343 | java.lang.System.arraycopy(stack, 0, larger, 0, ptr) 344 | stack = larger 345 | } 346 | 347 | private def valueOrTrampoline() = 348 | if (ptr == 1) { 349 | shutdown() 350 | HasValue 351 | } else if (peek() == NoValue) { 352 | runCleanups() 353 | push(NoValue) 354 | shutdown() 355 | HasValue 356 | } else { 357 | queue.add(this) 358 | NeedsTrampoline 359 | } 360 | 361 | private def triggerOn(t: Traversal[_]): t.type = { 362 | internalTriggers += (t → this) 363 | t 364 | } 365 | 366 | def getValue: Any = { 367 | assert(_state == HasValue) 368 | stack(0) 369 | } 370 | 371 | /** 372 | * Obtain the current state for this Traversal. 373 | */ 374 | def state: TraversalState = _state 375 | 376 | private def initialize(node: Operation[_, Any, _]): TraversalState = { 377 | import Impl._ 378 | 379 | @tailrec def rec(node: Operation[_, Any, _]): TraversalState = 380 | node match { 381 | case FlatMap(first, _) ⇒ 382 | push(node) 383 | rec(first) 384 | case Choice(ops) => 385 | rec(ops) 386 | case ShortCircuit ⇒ 387 | push(NoValue) 388 | valueOrTrampoline() 389 | case System ⇒ 390 | push(ctx.system) 391 | valueOrTrampoline() 392 | case Read ⇒ 393 | if (canReceive) { 394 | push(receiveOne()) 395 | valueOrTrampoline() 396 | } else { 397 | push(node) 398 | push(this) 399 | NeedsExternalInput 400 | } 401 | case ProcessSelf ⇒ 402 | push(ref) 403 | valueOrTrampoline() 404 | case ActorSelf ⇒ 405 | push(ctx.self) 406 | valueOrTrampoline() 407 | case Return(value) ⇒ 408 | push(value) 409 | valueOrTrampoline() 410 | case Call(process, _) ⇒ 411 | push(node) 412 | push(triggerOn(new Traversal(process, ctx))) 413 | NeedsInternalInput 414 | case Fork(other) ⇒ 415 | val t = new Traversal(other, ctx) 416 | processRoots += t 417 | push(t) 418 | valueOrTrampoline() 419 | case Spawn(proc @ Process(name, timeout, mailboxCapacity, ops), deployment) ⇒ 420 | val ref = 421 | if (name == "") ctx.spawnAnonymous(proc.toBehavior, deployment) 422 | else ctx.spawn(proc.toBehavior, name, deployment) 423 | push(ref) 424 | valueOrTrampoline() 425 | case Schedule(delay, msg, target) ⇒ 426 | push(ctx.schedule(delay, target, msg)) 427 | valueOrTrampoline() 428 | case w: WatchRef[_] ⇒ 429 | push(watch(ctx, w)) 430 | valueOrTrampoline() 431 | case state: State[s, k, ev, ex] ⇒ 432 | val current = getState(state.key) 433 | val (events, read) = state.transform(current) 434 | val next = events.foldLeft(current)(state.key.apply(_, _)) 435 | setState(state.key, next) 436 | push(read) 437 | valueOrTrampoline() 438 | case state: StateR[s, k, ev] ⇒ 439 | val current = getState(state.key) 440 | val events = state.transform(current) 441 | val next = events.foldLeft(current)(state.key.apply(_, _)) 442 | setState(state.key, next) 443 | push(next) 444 | valueOrTrampoline() 445 | case Forget(key) ⇒ 446 | stateMap -= key 447 | push(Done) 448 | valueOrTrampoline() 449 | case Cleanup(cleanup) ⇒ 450 | val f @ FlatMap(_, _) = pop() // this is ensured at the end of initialize() 451 | push(RunCleanup(cleanup)) 452 | push(f) 453 | push(Done) 454 | valueOrTrampoline() 455 | } 456 | 457 | node match { 458 | case _: Cleanup ⇒ 459 | /* 460 | * a naked Cleanup cannot be pushed because it expects a FlatMap 461 | * beneath itself; making rec() robust against that would lead to 462 | * erroneous execution order for the actual cleanup when stack is 463 | * not empty right now 464 | */ 465 | ensureSpace(depth(node) + 1) 466 | rec(FlatMap(node.asInstanceOf[Operation[Any, Any, Effects]], wrapReturn)) 467 | case _ ⇒ 468 | ensureSpace(depth(node)) 469 | rec(node) 470 | } 471 | } 472 | 473 | def dispatchInput(value: Any, source: Traversal[_]): Unit = { 474 | if (Debug) println(s"${ctx.self} dispatching input $value from ${source.process.name} to $this") 475 | _state match { 476 | case NeedsInternalInput ⇒ 477 | assert(source eq pop()) 478 | val Impl.Call(proc, replacement) = pop() 479 | assert(source.process eq proc) 480 | if (value != NoValue) push(value) else push(replacement.getOrElse(NoValue)) 481 | _state = valueOrTrampoline() 482 | case NeedsExternalInput ⇒ 483 | assert(this eq pop()) 484 | assert(Impl.Read eq pop()) 485 | push(value) 486 | _state = valueOrTrampoline() 487 | case _ ⇒ throw new AssertionError 488 | } 489 | } 490 | 491 | def dispatchTrampoline(): Unit = { 492 | if (Debug) println(s"${ctx.self} dispatching trampoline for $this") 493 | assert(_state == NeedsTrampoline) 494 | val value = pop() 495 | pop() match { 496 | case Impl.FlatMap(_, cont) ⇒ 497 | val contOps = cont(value) 498 | if (Debug) println(s"${ctx.self} flatMap yielded $contOps") 499 | _state = initialize(contOps) 500 | case RunCleanup(cleanup) ⇒ 501 | cleanup() 502 | push(value) 503 | _state = valueOrTrampoline() 504 | } 505 | } 506 | 507 | def cancel(): Unit = { 508 | if (Debug) println(s"${ctx.self} canceling $this") 509 | @tailrec def rec(t: Traversal[_], acc: List[RunCleanup]): List[RunCleanup] = 510 | if (t.isAlive) { 511 | t.shutdown() 512 | t._state match { 513 | case HasValue ⇒ acc 514 | case NeedsTrampoline ⇒ addCleanups(t, acc) 515 | case NeedsExternalInput ⇒ addCleanups(t, acc) 516 | case NeedsInternalInput ⇒ 517 | val next = pop().asInstanceOf[Traversal[_]] 518 | internalTriggers -= next 519 | rec(next, addCleanups(t, acc)) 520 | } 521 | } else Nil 522 | @tailrec def addCleanups(t: Traversal[_], acc: List[RunCleanup], idx: Int = 0): List[RunCleanup] = 523 | if (idx < t.ptr) { 524 | t.stack(idx) match { 525 | case r: RunCleanup ⇒ addCleanups(t, r :: acc, idx + 1) 526 | case _ ⇒ addCleanups(t, acc, idx + 1) 527 | } 528 | } else acc 529 | 530 | // run cleanup actions in reverse order, catching exceptions 531 | rec(this, Nil) foreach { run ⇒ 532 | if (Debug) println(s"${ctx.self} running cleanup action") 533 | try run.cleanup() 534 | catch { 535 | case NonFatal(ex) ⇒ 536 | ctx.system.eventStream.publish(Logging.Error(ex, ctx.self.toString, ProcessInterpreter.this.getClass, 537 | s"exception in cleanup handler while canceling process ${process.name}: ${ex.getMessage}")) 538 | } 539 | } 540 | } 541 | 542 | /* 543 | * This method is used when voluntarily giving up, hence failure should 544 | * fail the Actor (which will in turn run all remaining cleanups that are 545 | * still on the stack). 546 | */ 547 | private def runCleanups(): Unit = { 548 | while (ptr > 0) { 549 | pop() match { 550 | case RunCleanup(cleanup) ⇒ cleanup() 551 | case _ ⇒ 552 | } 553 | } 554 | } 555 | 556 | private def shutdown(): Unit = { 557 | if (Debug) println(s"${ctx.self} shutting down") 558 | ctx.stop(ref) 559 | toRead = -1 560 | processRoots -= this 561 | if (deadline != null) removeTimeout(ctx, deadline) 562 | } 563 | 564 | } 565 | 566 | } 567 | -------------------------------------------------------------------------------- /src/test/scala/com/rolandkuhn/akka_typed_session/ProcessSpec.scala: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (C) 2016 Lightbend Inc. 3 | */ 4 | package com.rolandkuhn.akka_typed_session 5 | 6 | import akka.typed._ 7 | import ScalaDSL._ 8 | import akka.typed.patterns.Receptionist._ 9 | import scala.concurrent.duration._ 10 | import akka.typed.scaladsl.AskPattern._ 11 | import org.scalatest.Succeeded 12 | import akka.actor.InvalidActorNameException 13 | import akka.Done 14 | import java.util.concurrent.TimeoutException 15 | import org.scalatest.prop.PropertyChecks 16 | import scala.collection.immutable.TreeSet 17 | import scala.util.Random 18 | import akka.typed.testkit._ 19 | 20 | object ProcessSpec { 21 | 22 | sealed abstract class RequestService extends ServiceKey[Request] 23 | object RequestService extends RequestService 24 | 25 | case class Request(req: String, replyTo: ActorRef[Response]) 26 | case class Response(res: String) 27 | 28 | sealed abstract class LoginService extends ServiceKey[Login] 29 | object LoginService extends LoginService 30 | 31 | case class Login(replyTo: ActorRef[AuthResult]) 32 | sealed trait AuthResult 33 | case object AuthRejected extends AuthResult 34 | case class AuthSuccess(next: ActorRef[Store]) extends AuthResult 35 | 36 | sealed trait Store 37 | case class GetData(replyTo: ActorRef[DataResult]) extends Store 38 | case class DataResult(msg: String) 39 | } 40 | 41 | class ProcessSpec extends TypedSpec { 42 | import ProcessSpec._ 43 | 44 | trait CommonTests { 45 | implicit def system: ActorSystem[TypedSpec.Command] 46 | 47 | def `demonstrates working processes`(): Unit = { 48 | 49 | def register[T](server: ActorRef[T], key: ServiceKey[T]) = 50 | OpDSL[Registered[T]] { implicit opDSL ⇒ 51 | for { 52 | self ← opProcessSelf 53 | sys ← opSystem 54 | } yield { 55 | sys.receptionist ! Register(key, server)(self) 56 | opRead 57 | } 58 | } 59 | 60 | val backendStore = 61 | OpDSL.loopInf[Store] { implicit opDSL ⇒ 62 | for (GetData(replyTo) ← opRead) yield { 63 | replyTo ! DataResult("yeehah") 64 | } 65 | } 66 | 67 | val backend = 68 | OpDSL[Login] { implicit opDSL ⇒ 69 | for { 70 | self ← opProcessSelf 71 | _ ← opCall(register(self, LoginService).named("registerBackend")) 72 | store ← opFork(backendStore.named("store")) 73 | } yield OpDSL.loopInf { _ ⇒ 74 | for (Login(replyTo) ← opRead) yield { 75 | replyTo ! AuthSuccess(store.ref) 76 | } 77 | } 78 | } 79 | 80 | val getBackend = 81 | OpDSL[Listing[Login]] { implicit opDSL ⇒ 82 | for { 83 | self ← opProcessSelf 84 | system ← opSystem 85 | _ = system.receptionist ! Find(LoginService)(self) 86 | } yield opRead 87 | } 88 | 89 | def talkWithBackend(backend: ActorRef[Login], req: Request) = 90 | OpDSL[AuthResult] { implicit opDSL ⇒ 91 | for { 92 | self ← opProcessSelf 93 | _ ← opUnit({ backend ! Login(self) }) 94 | AuthSuccess(store) ← opRead 95 | data ← opNextStep[DataResult](1, { implicit opDSL ⇒ 96 | for { 97 | self ← opProcessSelf 98 | _ = store ! GetData(self) 99 | } yield opRead 100 | }) 101 | } yield req.replyTo ! Response(data.msg) 102 | } 103 | 104 | val server = 105 | OpDSL[Request] { implicit op ⇒ 106 | for { 107 | _ ← opSpawn(backend.named("backend")) 108 | self ← opProcessSelf 109 | _ ← retry(1.second, 3, register(self, RequestService).named("register")) 110 | backend ← retry(1.second, 3, getBackend.named("getBackend")) 111 | } yield OpDSL.loopInf { _ ⇒ 112 | for (req ← opRead) yield forkAndCancel(5.seconds, talkWithBackend(backend.addresses.head, req).named("worker")) 113 | } 114 | } 115 | 116 | sync(runTest("complexOperations") { 117 | OpDSL[Response] { implicit opDSL ⇒ 118 | for { 119 | serverRef ← opSpawn(server.named("server").withMailboxCapacity(20)) 120 | self ← opProcessSelf 121 | // } yield OpDSL.loop(2) { _ ⇒ 122 | // for { 123 | // _ ← opUnit(serverRef ! MainCmd(Request("hello", self))) 124 | // msg ← opRead 125 | // } yield msg should ===(Response("yeehah")) 126 | // }.map { results ⇒ 127 | // results should ===(List(Succeeded, Succeeded)) 128 | // } 129 | _ ← opUnit(serverRef ! MainCmd(Request("hello", self))) 130 | msg1 ← opRead 131 | succ1 = msg1 should ===(Response("yeehah")) 132 | _ ← opUnit(serverRef ! MainCmd(Request("hello", self))) 133 | msg2 ← opRead 134 | succ2 = msg2 should ===(Response("yeehah")) 135 | } yield (succ1, succ2) should ===((Succeeded, Succeeded)) 136 | }.withTimeout(3.seconds).toBehavior 137 | }) 138 | } 139 | 140 | def `must spawn`(): Unit = sync(runTest("spawn") { 141 | OpDSL[Done] { implicit opDSL ⇒ 142 | for { 143 | child ← opSpawn(OpDSL[ActorRef[Done]] { implicit opDSL ⇒ 144 | opRead.map(_ ! Done) 145 | }.named("child").withMailboxCapacity(2)) 146 | self ← opProcessSelf 147 | _ = child ! MainCmd(self) 148 | msg ← opRead 149 | } yield msg should ===(Done) 150 | }.withTimeout(3.seconds).toBehavior 151 | }) 152 | 153 | def `must spawn anonymously`(): Unit = sync(runTest("spawnAnonymous") { 154 | OpDSL[Done] { implicit opDSL ⇒ 155 | for { 156 | child ← opSpawn(OpDSL[ActorRef[Done]] { implicit opDSL ⇒ 157 | opRead.map(_ ! Done) 158 | }.withMailboxCapacity(2)) 159 | self ← opProcessSelf 160 | _ = child ! MainCmd(self) 161 | msg ← opRead 162 | } yield msg should ===(Done) 163 | }.withTimeout(3.seconds).toBehavior 164 | }) 165 | 166 | def `must watch`(): Unit = sync(runTest("watch") { 167 | OpDSL[Done] { implicit opDSL ⇒ 168 | for { 169 | self ← opProcessSelf 170 | child ← opSpawn(opUnit(()).named("unit")) 171 | _ ← opWatch(child, self, Done) 172 | } yield opRead 173 | }.withTimeout(3.seconds).toBehavior 174 | }) 175 | 176 | def `must watch and report failure`(): Unit = sync(runTest("watch") { 177 | OpDSL[Throwable] { implicit opDSL ⇒ 178 | for { 179 | self ← opProcessSelf 180 | filter = muteExpectedException[TimeoutException](occurrences = 1) 181 | child ← opSpawn(opRead.withTimeout(10.millis)) 182 | _ ← opWatch(child, self, null, Some(_)) 183 | thr ← opRead 184 | } yield { 185 | thr shouldBe a[TimeoutException] 186 | filter.awaitDone(100.millis) 187 | } 188 | }.withTimeout(3.seconds).toBehavior 189 | }) 190 | 191 | def `must unwatch`(): Unit = sync(runTest("unwatch") { 192 | OpDSL[String] { implicit opDSL ⇒ 193 | for { 194 | self ← opProcessSelf 195 | child ← opSpawn(opUnit(()).named("unit")) 196 | cancellable ← opWatch(child, self, "dead") 197 | _ ← opSchedule(50.millis, self, "alive") 198 | msg ← { cancellable.cancel(); opRead } 199 | } yield msg should ===("alive") 200 | }.withTimeout(3.seconds).toBehavior 201 | }) 202 | 203 | def `must respect timeouts`(): Unit = sync(runTest("timeout") { 204 | OpDSL[Done] { implicit opDSL ⇒ 205 | for { 206 | self ← opProcessSelf 207 | filter = muteExpectedException[TimeoutException](occurrences = 1) 208 | child ← opSpawn(opRead.named("read").withTimeout(10.millis)) 209 | _ ← opWatch(child, self, Done) 210 | _ ← opRead 211 | } yield filter.awaitDone(100.millis) 212 | }.withTimeout(3.seconds).toBehavior 213 | }) 214 | 215 | def `must cancel timeouts`(): Unit = sync(runTest("timeout") { 216 | val childProc = OpDSL[String] { implicit opDSL ⇒ 217 | for { 218 | self ← opProcessSelf 219 | _ ← opFork(OpDSL[String] { _ ⇒ self ! ""; opRead }.named("read").withTimeout(1.second)) 220 | } yield opRead 221 | }.named("child").withTimeout(100.millis) 222 | 223 | OpDSL[Done] { implicit opDSL ⇒ 224 | for { 225 | self ← opProcessSelf 226 | start = Deadline.now 227 | filter = muteExpectedException[TimeoutException](occurrences = 1) 228 | child ← opSpawn(childProc) 229 | _ ← opWatch(child, self, Done) 230 | _ ← opRead 231 | } yield { 232 | // weird: without this I get diverging implicits on the `>` 233 | import FiniteDuration.FiniteDurationIsOrdered 234 | (Deadline.now - start) should be > 1.second 235 | filter.awaitDone(100.millis) 236 | } 237 | }.withTimeout(3.seconds).toBehavior 238 | }) 239 | 240 | def `must name process refs appropriately`(): Unit = sync(runTest("naming") { 241 | OpDSL[Done] { implicit opDSL ⇒ 242 | opProcessSelf.map { self ⇒ 243 | val name = self.path.name 244 | withClue(s" name=$name") { 245 | name.substring(0, 1) should ===("$") 246 | name.substring(name.length - 5) should ===("-read") 247 | } 248 | } 249 | }.named("read").toBehavior 250 | }) 251 | 252 | // TODO dropping messages on a subactor ref 253 | 254 | // TODO dropping messages on the main ref including warning when dropping Traversals (or better: make it robust) 255 | } 256 | 257 | object `A ProcessDSL (native)` extends CommonTests with NativeSystem { 258 | 259 | private def assertStopping(ctx: EffectfulActorContext[_], n: Int): Unit = { 260 | val stopping = ctx.getAllEffects() 261 | stopping.size should ===(n) 262 | stopping.collect { case Effect.Stopped(_) => true }.size should ===(n) 263 | } 264 | 265 | def `must reject invalid process names early`(): Unit = { 266 | a[InvalidActorNameException] mustBe thrownBy { 267 | opRead(null).named("$hello") 268 | } 269 | a[InvalidActorNameException] mustBe thrownBy { 270 | opRead(null).named("hello").copy(name = "$hello") 271 | } 272 | a[InvalidActorNameException] mustBe thrownBy { 273 | Process("$hello", Duration.Inf, 1, null) 274 | } 275 | } 276 | 277 | def `must name process refs appropriately (EffectfulActorContext)`(): Unit = { 278 | val ctx = new EffectfulActorContext("name", OpDSL[ActorRef[Done]] { implicit opDSL ⇒ 279 | opRead 280 | }.named("read").toBehavior, 1, system) 281 | val Effect.Spawned(name) :: Nil = ctx.getAllEffects() 282 | withClue(s" name=$name") { 283 | name.substring(0, 1) should ===("$") 284 | // FIXME #22938 name.substring(name.length - 5) should ===("-read") 285 | } 286 | ctx.getAllEffects() should ===(Nil) 287 | } 288 | 289 | def `must read`(): Unit = { 290 | val ret = Inbox[Done]("readRet") 291 | val ctx = new EffectfulActorContext("read", OpDSL[ActorRef[Done]] { implicit opDSL ⇒ 292 | opRead.map(_ ! Done) 293 | }.named("read").toBehavior, 1, system) 294 | 295 | val Effect.Spawned(procName) = ctx.getEffect() 296 | ctx.hasEffects should ===(false) 297 | val procInbox = ctx.childInbox[ActorRef[Done]](procName) 298 | 299 | ctx.run(MainCmd(ret.ref)) 300 | procInbox.receiveAll() should ===(List(ret.ref)) 301 | 302 | val t = ctx.selfInbox.receiveMsg() 303 | t match { 304 | case sub: SubActor[_] ⇒ sub.ref.path.name should ===(procName) 305 | case other ⇒ fail(s"expected SubActor, got $other") 306 | } 307 | ctx.run(t) 308 | assertStopping(ctx, 1) 309 | ctx.selfInbox.receiveAll() should ===(Nil) 310 | ret.receiveAll() should ===(List(Done)) 311 | ctx.isAlive should ===(false) 312 | } 313 | 314 | def `must call`(): Unit = { 315 | val ret = Inbox[Done]("callRet") 316 | val ctx = new EffectfulActorContext("call", OpDSL[ActorRef[Done]] { implicit opDSL ⇒ 317 | opRead.flatMap(replyTo ⇒ opCall(OpDSL[String] { implicit opDSL ⇒ 318 | opUnit(replyTo ! Done) 319 | }.named("called"))) 320 | }.named("call").toBehavior, 1, system) 321 | 322 | val Effect.Spawned(procName) = ctx.getEffect() 323 | ctx.hasEffects should ===(false) 324 | val procInbox = ctx.childInbox[ActorRef[Done]](procName) 325 | 326 | ctx.run(MainCmd(ret.ref)) 327 | procInbox.receiveAll() should ===(List(ret.ref)) 328 | 329 | val t = ctx.selfInbox.receiveMsg() 330 | t match { 331 | case sub: SubActor[_] ⇒ sub.ref.path.name should ===(procName) 332 | case other ⇒ fail(s"expected SubActor, got $other") 333 | } 334 | ctx.run(t) 335 | val Effect.Spawned(calledName) = ctx.getEffect() 336 | 337 | assertStopping(ctx, 2) 338 | ctx.selfInbox.receiveAll() should ===(Nil) 339 | ret.receiveAll() should ===(List(Done)) 340 | ctx.isAlive should ===(false) 341 | } 342 | 343 | def `must fork`(): Unit = { 344 | val ret = Inbox[Done]("callRet") 345 | val ctx = new EffectfulActorContext("call", OpDSL[ActorRef[Done]] { implicit opDSL ⇒ 346 | opFork(opRead.map(_ ! Done).named("forkee")) 347 | .map { sub ⇒ 348 | opRead.map(sub.ref ! _) 349 | } 350 | }.named("call").toBehavior, 1, system) 351 | 352 | val Effect.Spawned(procName) = ctx.getEffect() 353 | val procInbox = ctx.childInbox[ActorRef[Done]](procName) 354 | 355 | val Effect.Spawned(forkName) = ctx.getEffect() 356 | val forkInbox = ctx.childInbox[ActorRef[Done]](forkName) 357 | ctx.hasEffects should ===(false) 358 | 359 | ctx.run(MainCmd(ret.ref)) 360 | procInbox.receiveAll() should ===(List(ret.ref)) 361 | ctx.getAllEffects() should ===(Nil) 362 | 363 | val t1 = ctx.selfInbox.receiveMsg() 364 | t1 match { 365 | case sub: SubActor[_] ⇒ sub.ref.path.name should ===(procName) 366 | case other ⇒ fail(s"expected SubActor, got $other") 367 | } 368 | 369 | ctx.run(t1) 370 | forkInbox.receiveAll() should ===(List(ret.ref)) 371 | assertStopping(ctx, 1) 372 | 373 | val t2 = ctx.selfInbox.receiveMsg() 374 | t2 match { 375 | case sub: SubActor[_] ⇒ sub.ref.path.name should ===(forkName) 376 | case other ⇒ fail(s"expected SubActor, got $other") 377 | } 378 | 379 | ctx.run(t2) 380 | assertStopping(ctx, 1) 381 | ctx.selfInbox.receiveAll() should ===(Nil) 382 | ret.receiveAll() should ===(List(Done)) 383 | ctx.isAlive should ===(false) 384 | } 385 | 386 | def `must return all the things`(): Unit = { 387 | case class Info(sys: ActorSystem[Nothing], proc: ActorRef[Nothing], actor: ActorRef[Nothing], value: Int) 388 | val ret = Inbox[Info]("thingsRet") 389 | val ctx = new EffectfulActorContext("things", OpDSL[ActorRef[Done]] { implicit opDSL ⇒ 390 | for { 391 | sys ← opSystem 392 | proc ← opProcessSelf 393 | actor ← opActorSelf 394 | value ← opUnit(42) 395 | } yield ret.ref ! Info(sys, proc, actor, value) 396 | }.named("things").toBehavior, 1, system) 397 | 398 | val Effect.Spawned(procName) = ctx.getEffect() 399 | assertStopping(ctx, 1) 400 | ctx.isAlive should ===(false) 401 | 402 | val Info(sys, proc, actor, value) = ret.receiveMsg() 403 | ret.hasMessages should ===(false) 404 | sys should ===(system) 405 | proc.path.name should ===(procName) 406 | actor.path should ===(proc.path.parent) 407 | value should ===(42) 408 | } 409 | 410 | def `must filter`(): Unit = { 411 | val ctx = new EffectfulActorContext("filter", OpDSL[String] { implicit opDSL ⇒ 412 | for { 413 | self ← opProcessSelf 414 | if false 415 | } yield opRead 416 | }.toBehavior, 1, system) 417 | 418 | val Effect.Spawned(procName) = ctx.getEffect() 419 | assertStopping(ctx, 1) 420 | ctx.isAlive should ===(false) 421 | } 422 | 423 | def `must filter across call`(): Unit = { 424 | val ctx = new EffectfulActorContext("filter", OpDSL[String] { implicit opDSL ⇒ 425 | val callee = 426 | for { 427 | self ← opProcessSelf 428 | if false 429 | } yield opRead 430 | 431 | for { 432 | _ ← opCall(callee.named("callee")) 433 | } yield opRead 434 | }.toBehavior, 1, system) 435 | 436 | val Effect.Spawned(procName) = ctx.getEffect() 437 | val Effect.Spawned(calleeName) = ctx.getEffect() 438 | // FIXME #22938 calleeName should endWith("-callee") 439 | assertStopping(ctx, 2) 440 | ctx.isAlive should ===(false) 441 | } 442 | 443 | def `must filter across call with replacement value`(): Unit = { 444 | var received: String = null 445 | val ctx = new EffectfulActorContext("filter", OpDSL[String] { implicit opDSL ⇒ 446 | val callee = 447 | for { 448 | self ← opProcessSelf 449 | if false 450 | } yield opRead 451 | 452 | for { 453 | result ← opCall(callee.named("callee"), Some("hello")) 454 | } yield { 455 | received = result 456 | opRead 457 | } 458 | }.toBehavior, 1, system) 459 | 460 | val Effect.Spawned(_) = ctx.getEffect() 461 | val Effect.Spawned(calleeName) = ctx.getEffect() 462 | // FIXME #22938 calleeName should endWith("-callee") 463 | assertStopping(ctx, 1) 464 | ctx.isAlive should ===(true) 465 | received should ===("hello") 466 | } 467 | 468 | def `must cleanup at the right times`(): Unit = { 469 | var calls = List.empty[Int] 470 | def call(n: Int): Unit = calls ::= n 471 | 472 | val ctx = new EffectfulActorContext("cleanup", OpDSL[String] { implicit opDSL ⇒ 473 | (for { 474 | _ ← opProcessSelf 475 | _ = call(0) 476 | _ ← opCleanup(() ⇒ call(1)) 477 | _ ← opUnit(call(2)) 478 | } yield opCleanup(() ⇒ call(3)) 479 | ).map { msg ⇒ 480 | msg should ===(Done) 481 | call(4) 482 | } 483 | }.toBehavior, 1, system) 484 | 485 | val Effect.Spawned(_) = ctx.getEffect() 486 | assertStopping(ctx, 1) 487 | ctx.isAlive should ===(false) 488 | calls.reverse should ===(List(0, 2, 3, 1, 4)) 489 | } 490 | 491 | def `must cleanup when short-circuiting`(): Unit = { 492 | var calls = List.empty[Int] 493 | def call(n: Int): Unit = calls ::= n 494 | 495 | val ctx = new EffectfulActorContext("cleanup", OpDSL[String] { implicit opDSL ⇒ 496 | val callee = 497 | for { 498 | _ ← opProcessSelf 499 | _ ← opUnit(call(10)) 500 | _ ← opCleanup(() ⇒ call(11)) 501 | if false 502 | } yield call(12) 503 | 504 | (for { 505 | _ ← opProcessSelf 506 | _ = call(0) 507 | _ ← opCleanup(() ⇒ call(1)) 508 | _ ← opCall(callee.named("callee")) 509 | } yield opCleanup(() ⇒ call(3)) 510 | ).map { _ ⇒ 511 | call(4) 512 | } 513 | }.toBehavior, 1, system) 514 | 515 | val Effect.Spawned(_) = ctx.getEffect() 516 | val Effect.Spawned(calleeName) = ctx.getEffect() 517 | // FIXME #22938 calleeName should endWith("-callee") 518 | assertStopping(ctx, 2) 519 | ctx.isAlive should ===(false) 520 | calls.reverse should ===(List(0, 10, 11, 1)) 521 | } 522 | 523 | def `must cleanup when short-circuiting with replacement`(): Unit = { 524 | var calls = List.empty[Int] 525 | def call(n: Int): Unit = calls ::= n 526 | 527 | val ctx = new EffectfulActorContext("cleanup", OpDSL[String] { implicit opDSL ⇒ 528 | val callee = 529 | for { 530 | _ ← opProcessSelf 531 | _ ← opUnit(call(10)) 532 | _ ← opCleanup(() ⇒ call(11)) 533 | _ ← opCleanup(() ⇒ call(12)) 534 | if false 535 | } yield call(13) 536 | 537 | (for { 538 | _ ← opProcessSelf 539 | _ = call(0) 540 | _ ← opCleanup(() ⇒ call(1)) 541 | _ ← opCall(callee.named("callee"), Some("hello")) 542 | } yield opCleanup(() ⇒ call(3)) 543 | ).map { msg ⇒ 544 | msg should ===(Done) 545 | call(4) 546 | } 547 | }.toBehavior, 1, system) 548 | 549 | val Effect.Spawned(_) = ctx.getEffect() 550 | val Effect.Spawned(calleeName) = ctx.getEffect() 551 | // FIXME #22938 calleeName should endWith("-callee") 552 | assertStopping(ctx, 2) 553 | ctx.isAlive should ===(false) 554 | calls.reverse should ===(List(0, 10, 12, 11, 3, 1, 4)) 555 | } 556 | 557 | def `must cleanup at the right times when failing in cleanup`(): Unit = { 558 | var calls = List.empty[Int] 559 | def call(n: Int): Unit = calls ::= n 560 | 561 | val ctx = new EffectfulActorContext("cleanup", OpDSL[String] { implicit opDSL ⇒ 562 | (for { 563 | _ ← opCleanup(() ⇒ call(0)) 564 | _ ← opCleanup(() ⇒ call(1)) 565 | _ ← opCleanup(() ⇒ throw new Exception("expected")) 566 | _ ← opRead 567 | } yield opCleanup(() ⇒ call(3)) 568 | ).map { _ ⇒ 569 | call(4) 570 | } 571 | }.toBehavior, 1, system) 572 | 573 | val Effect.Spawned(mainName) = ctx.getEffect() 574 | ctx.getAllEffects() should ===(Nil) 575 | 576 | ctx.run(MainCmd("")) 577 | ctx.childInbox[String](mainName).receiveAll() should ===(List("")) 578 | val t = ctx.selfInbox.receiveMsg() 579 | a[Exception] shouldBe thrownBy { 580 | ctx.run(t) 581 | } 582 | assertStopping(ctx, 1) 583 | calls.reverse should ===(List(3, 1, 0)) 584 | } 585 | 586 | def `must cleanup at the right times when failing somewhere else`(): Unit = { 587 | var calls = List.empty[Int] 588 | def call(n: Int): Unit = calls ::= n 589 | 590 | val ctx = new EffectfulActorContext("cleanup", OpDSL[String] { implicit opDSL ⇒ 591 | for { 592 | _ ← opFork( 593 | (for { 594 | _ ← opCleanup(() ⇒ call(0)) 595 | _ ← opCleanup(() ⇒ call(1)) 596 | } yield opRead).named("fork")) 597 | _ ← opRead 598 | } yield throw new Exception("expected") 599 | }.toBehavior, 1, system) 600 | 601 | val Effect.Spawned(mainName) = ctx.getEffect() 602 | val Effect.Spawned(forkName) = ctx.getEffect() 603 | // FIXME #22938 forkName should endWith("-fork") 604 | ctx.getAllEffects() should ===(Nil) 605 | 606 | ctx.run(MainCmd("")) 607 | ctx.childInbox[String](mainName).receiveAll() should ===(List("")) 608 | val t = ctx.selfInbox.receiveMsg() 609 | a[Exception] shouldBe thrownBy { 610 | ctx.run(t) 611 | } 612 | assertStopping(ctx, 2) 613 | calls.reverse should ===(List(1, 0)) 614 | } 615 | 616 | def `must handle ephemeral state`(): Unit = { 617 | case class Add(num: Int) 618 | object Key extends StateKey[Int] { 619 | type Event = Add 620 | def initial = 0 621 | def apply(s: Int, ev: Add) = s + ev.num 622 | def clazz = classOf[Add] 623 | } 624 | 625 | var values = List.empty[Int] 626 | def publish(n: Int): Unit = values ::= n 627 | 628 | val ctx = new EffectfulActorContext("state", OpDSL[String] { implicit opDSL ⇒ 629 | for { 630 | i1 ← opUpdateState(Key)(i ⇒ { publish(i); List(Add(2)) → 5 }) 631 | _ = publish(i1) 632 | i2 ← opUpdateAndReadState(Key)(i ⇒ { publish(i); List(Add(2)) }) 633 | _ = publish(i2) 634 | Done ← opForgetState(Key) 635 | i3 ← opReadState(Key) 636 | } yield publish(i3) 637 | }.toBehavior, 1, system) 638 | 639 | val Effect.Spawned(_) = ctx.getEffect() 640 | assertStopping(ctx, 1) 641 | ctx.isAlive should ===(false) 642 | values.reverse should ===(List(0, 5, 2, 4, 0)) 643 | } 644 | 645 | } 646 | 647 | object `A ProcessDSL (adapted)` extends CommonTests with AdaptedSystem { 648 | pending // awaiting fix for #22934 in akka/akka 649 | } 650 | 651 | object `A TimeoutOrdering` extends PropertyChecks { 652 | 653 | def `must sort correctly`(): Unit = { 654 | forAll { (l: List[Int]) ⇒ 655 | val offsets = (TreeSet.empty[Int] ++ l.filterNot(_ == 1)).toVector 656 | val deadlines = offsets.map(o ⇒ Deadline((Long.MaxValue + o).nanos)) 657 | val mapping = deadlines.zip(offsets).toMap 658 | val shuffled = Random.shuffle(deadlines) 659 | val sorted = TreeSet.empty(internal.ProcessInterpreter.timeoutOrdering) ++ shuffled 660 | withClue(s" mapping=$mapping shuffled=$shuffled sorted=$sorted") { 661 | sorted.toVector.map(mapping) should ===(offsets) 662 | } 663 | } 664 | } 665 | 666 | } 667 | 668 | } 669 | --------------------------------------------------------------------------------