final review comments
- make it EXPERIMENTAL - shuffle docs around to be less scary - reuse sameThreadExecutionContext in CircuitBreaker - typos
This commit is contained in:
parent
1b331dc547
commit
b127ab0d4f
5 changed files with 240 additions and 222 deletions
|
|
@ -13,20 +13,13 @@ import scala.concurrent.{ ExecutionContext, Future, Promise, Await }
|
||||||
import scala.concurrent.duration._
|
import scala.concurrent.duration._
|
||||||
import scala.util.control.NonFatal
|
import scala.util.control.NonFatal
|
||||||
import scala.util.Success
|
import scala.util.Success
|
||||||
|
import akka.dispatch.ExecutionContexts.sameThreadExecutionContext
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Companion object providing factory methods for Circuit Breaker which runs callbacks in caller's thread
|
* Companion object providing factory methods for Circuit Breaker which runs callbacks in caller's thread
|
||||||
*/
|
*/
|
||||||
object CircuitBreaker {
|
object CircuitBreaker {
|
||||||
|
|
||||||
/**
|
|
||||||
* Synchronous execution context to run in caller's thread - used by companion object factory methods
|
|
||||||
*/
|
|
||||||
private[CircuitBreaker] val syncExecutionContext = new ExecutionContext {
|
|
||||||
override def execute(runnable: Runnable): Unit = runnable.run()
|
|
||||||
override def reportFailure(t: Throwable): Unit = ()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callbacks run in caller's thread when using withSyncCircuitBreaker, and in same ExecutionContext as the passed
|
* Callbacks run in caller's thread when using withSyncCircuitBreaker, and in same ExecutionContext as the passed
|
||||||
* in Future when using withCircuitBreaker. To use another ExecutionContext for the callbacks you can specify the
|
* in Future when using withCircuitBreaker. To use another ExecutionContext for the callbacks you can specify the
|
||||||
|
|
@ -38,7 +31,7 @@ object CircuitBreaker {
|
||||||
* @param resetTimeout [[scala.concurrent.duration.FiniteDuration]] of time after which to attempt to close the circuit
|
* @param resetTimeout [[scala.concurrent.duration.FiniteDuration]] of time after which to attempt to close the circuit
|
||||||
*/
|
*/
|
||||||
def apply(scheduler: Scheduler, maxFailures: Int, callTimeout: FiniteDuration, resetTimeout: FiniteDuration): CircuitBreaker =
|
def apply(scheduler: Scheduler, maxFailures: Int, callTimeout: FiniteDuration, resetTimeout: FiniteDuration): CircuitBreaker =
|
||||||
new CircuitBreaker(scheduler, maxFailures, callTimeout, resetTimeout)(syncExecutionContext)
|
new CircuitBreaker(scheduler, maxFailures, callTimeout, resetTimeout)(sameThreadExecutionContext)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callbacks run in caller's thread when using withSyncCircuitBreaker, and in same ExecutionContext as the passed
|
* Callbacks run in caller's thread when using withSyncCircuitBreaker, and in same ExecutionContext as the passed
|
||||||
|
|
@ -301,7 +294,7 @@ class CircuitBreaker(scheduler: Scheduler, maxFailures: Int, callTimeout: Finite
|
||||||
bodyFuture.onComplete({
|
bodyFuture.onComplete({
|
||||||
case s: Success[_] if !deadline.isOverdue() ⇒ callSucceeds()
|
case s: Success[_] if !deadline.isOverdue() ⇒ callSucceeds()
|
||||||
case _ ⇒ callFails()
|
case _ ⇒ callFails()
|
||||||
})(CircuitBreaker.syncExecutionContext)
|
})(sameThreadExecutionContext)
|
||||||
bodyFuture
|
bodyFuture
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -151,11 +151,10 @@ object Helpers {
|
||||||
|
|
||||||
final def unwrapMsgType(u: Universe)(msg: u.Type): u.Type = {
|
final def unwrapMsgType(u: Universe)(msg: u.Type): u.Type = {
|
||||||
import u._
|
import u._
|
||||||
if (msg <:< typeOf[WrappedMessage[_, _]])
|
msg match {
|
||||||
msg match {
|
case TypeRef(_, _, x :: _) if msg <:< typeOf[WrappedMessage[_, _]] ⇒ x
|
||||||
case TypeRef(_, _, x :: _) ⇒ x
|
case x ⇒ x
|
||||||
}
|
}
|
||||||
else msg
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -57,10 +57,10 @@ class ChannelDocSpec extends AkkaSpec {
|
||||||
|
|
||||||
import ChannelDocSpec._
|
import ChannelDocSpec._
|
||||||
|
|
||||||
class A
|
class MsgA
|
||||||
class B
|
class MsgB
|
||||||
class C
|
class MsgC
|
||||||
class D
|
class MsgD
|
||||||
|
|
||||||
"demonstrate why Typed Channels" in {
|
"demonstrate why Typed Channels" in {
|
||||||
def someActor = testActor
|
def someActor = testActor
|
||||||
|
|
@ -79,7 +79,7 @@ class ChannelDocSpec extends AkkaSpec {
|
||||||
|
|
||||||
type Example = //
|
type Example = //
|
||||||
//#motivation-types
|
//#motivation-types
|
||||||
(A, B) :+: (C, D) :+: TNil
|
(MsgA, MsgB) :+: (MsgC, MsgD) :+: TNil
|
||||||
//#motivation-types
|
//#motivation-types
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -114,12 +114,12 @@ class ChannelDocSpec extends AkkaSpec {
|
||||||
implicit val dummySender: ChannelRef[(Any, Nothing) :+: TNil] = ???
|
implicit val dummySender: ChannelRef[(Any, Nothing) :+: TNil] = ???
|
||||||
implicit val timeout: Timeout = ??? // for the ask operations
|
implicit val timeout: Timeout = ??? // for the ask operations
|
||||||
|
|
||||||
val channelA: ChannelRef[(A, B) :+: TNil] = ???
|
val channelA: ChannelRef[(MsgA, MsgB) :+: TNil] = ???
|
||||||
val channelB: ChannelRef[(B, C) :+: TNil] = ???
|
val channelB: ChannelRef[(MsgB, MsgC) :+: TNil] = ???
|
||||||
val channelC: ChannelRef[(C, D) :+: TNil] = ???
|
val channelC: ChannelRef[(MsgC, MsgD) :+: TNil] = ???
|
||||||
|
|
||||||
val a = new A
|
val a = new MsgA
|
||||||
val fA = Future { new A }
|
val fA = Future { new MsgA }
|
||||||
|
|
||||||
channelA <-!- a // send a to channelA
|
channelA <-!- a // send a to channelA
|
||||||
a -!-> channelA // same thing as above
|
a -!-> channelA // same thing as above
|
||||||
|
|
@ -128,8 +128,8 @@ class ChannelDocSpec extends AkkaSpec {
|
||||||
fA -!-> channelA // same thing as above
|
fA -!-> channelA // same thing as above
|
||||||
|
|
||||||
// ask the actor; return type given in full for illustration
|
// ask the actor; return type given in full for illustration
|
||||||
val fB: Future[WrappedMessage[(B, Nothing) :+: TNil, B]] = channelA <-?- a
|
val fB: Future[WrappedMessage[(MsgB, Nothing) :+: TNil, MsgB]] = channelA <-?- a
|
||||||
val fBunwrapped: Future[B] = fB.lub
|
val fBunwrapped: Future[MsgB] = fB.lub
|
||||||
|
|
||||||
a -?-> channelA // same thing as above
|
a -?-> channelA // same thing as above
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,14 @@
|
||||||
.. _typed-channels:
|
.. _typed-channels:
|
||||||
|
|
||||||
##############
|
#############################
|
||||||
Typed Channels
|
Typed Channels (EXPERIMENTAL)
|
||||||
##############
|
#############################
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
*This is a preview of the upcoming Typed Channels support, its API may change
|
||||||
|
during development up to the released version where the EXPERIMENTAL label is
|
||||||
|
removed.*
|
||||||
|
|
||||||
Motivation
|
Motivation
|
||||||
==========
|
==========
|
||||||
|
|
@ -43,197 +49,7 @@ to be useful, the system must be as reliable as you would expect a type system
|
||||||
to be. This means that unless you step outside of it (i.e. doing the
|
to be. This means that unless you step outside of it (i.e. doing the
|
||||||
equivalent of ``.asInstanceOf[_]``) you shall be protected, failures shall be
|
equivalent of ``.asInstanceOf[_]``) you shall be protected, failures shall be
|
||||||
recognized and flagged. There are a number of challenges included in this
|
recognized and flagged. There are a number of challenges included in this
|
||||||
requirement, which are discussed in the following sections. If you are reading
|
requirement, which are discussed in `The Design Background`_ below.
|
||||||
this chapter for the first time and are not currently interested in exactly why
|
|
||||||
things are as they are, you may skip ahead to `Terminology`_.
|
|
||||||
|
|
||||||
The Type Pollution Problem
|
|
||||||
--------------------------
|
|
||||||
|
|
||||||
What if an actor accepts two different types of messages? It might be a main
|
|
||||||
communications channel which is forwarded to worker actors for performing some
|
|
||||||
long-running and/or dangerous task, plus an administrative channel for the
|
|
||||||
routing of requests. Or it might be a generic message throttler which accepts a
|
|
||||||
generic channel for passing it through (which delay where appropriate) and a
|
|
||||||
management channel for setting the throttling rate. In the second case it is
|
|
||||||
especially easy to see that those two channels will probably not be related,
|
|
||||||
their types will not be derived from a meaningful common supertype; instead the
|
|
||||||
least upper bound will probably be :class:`AnyRef`. If a typed channel
|
|
||||||
reference only had the capability to express a single type, this type would
|
|
||||||
then be no restriction anymore.
|
|
||||||
|
|
||||||
One solution to this is to never expose references describe more than one
|
|
||||||
channel at a time. But where would these references come from? It would be very
|
|
||||||
difficult to make this construction process type-safe, and it would also be an
|
|
||||||
inconvenient restriction, since message ordering guarantees only apply for the
|
|
||||||
same sender–receive pair, and if there are relations between the messages sent
|
|
||||||
on multiple channels those would need more boilerplate code to realize than if
|
|
||||||
all interaction were possible through a single reference.
|
|
||||||
|
|
||||||
The other solution thus is to express multiple channel types by a single
|
|
||||||
channel reference, which requires the implementation of type lists and
|
|
||||||
computations on these. And as we will see below it also requires the
|
|
||||||
specification of possibly multiple reply channels per input type, hence a type
|
|
||||||
map. The implementation chosen uses type lists like this:
|
|
||||||
|
|
||||||
.. includecode:: code/docs/channels/ChannelDocSpec.scala#motivation-types
|
|
||||||
|
|
||||||
This type expresses two channels: type ``A`` may stimulate replies of type
|
|
||||||
``B``, while type ``C`` may evoke replies of type ``D``. The type operator
|
|
||||||
``:+:`` is a binary type which form a list of these channel definitions, and
|
|
||||||
like every good list it ends with an empty terminator ``TNil``.
|
|
||||||
|
|
||||||
The Reply Problem
|
|
||||||
-----------------
|
|
||||||
|
|
||||||
Akka actors have the power to reply to any message they receive, which is also
|
|
||||||
a message send and shall also be covered by typed channels. Since the sending
|
|
||||||
actor is the one which will also receive the reply, this needs to be verified.
|
|
||||||
The solution to this problem is that in addition to the ``self`` reference,
|
|
||||||
which is implicitly picked up as the sender for untyped actor interactions,
|
|
||||||
there is also a ``selfChannel`` which describes the typed channels handled by
|
|
||||||
this actor. Thus at the call site of the message send it must be verified that
|
|
||||||
this actor can actually handle the reply for that given message send.
|
|
||||||
|
|
||||||
The Sender Ping-Pong Problem
|
|
||||||
----------------------------
|
|
||||||
|
|
||||||
After successfully sending a message to an actor over a typed channel, that
|
|
||||||
actor will have a reference to the message’s sender, because normal Akka
|
|
||||||
message processing rules apply. For this sender reference there must exist a
|
|
||||||
typed channel reference which describes the possible reply types which are
|
|
||||||
applicable for each of the incoming message channels. We will see below how
|
|
||||||
this reference is provided in the code, the problem we want to highlight here
|
|
||||||
is a different one: the nature of any sender reference is that it is highly
|
|
||||||
dynamic, the compiler cannot possibly know who sent the message we are
|
|
||||||
currently processing.
|
|
||||||
|
|
||||||
But this does not mean that all hope is lost: the solution is to do *all*
|
|
||||||
type-checking at the call site of the message send. The receiving actor just
|
|
||||||
needs to declare its channel descriptions in its own type, and channel
|
|
||||||
references are derived at construction from this type (implying the existence
|
|
||||||
of a typed ``actorOf``). Then the actor knows for each received message type
|
|
||||||
which the allowed reply types are. The typed channel for the sender reference
|
|
||||||
hence has the reply types for the current input channel as its own input types,
|
|
||||||
but what should the reply types be? This is the ping-pong problem:
|
|
||||||
|
|
||||||
* ActorA sends MsgA to ActorB
|
|
||||||
|
|
||||||
* ActorB replies with MsgB
|
|
||||||
|
|
||||||
* ActorA replies with MsgC
|
|
||||||
|
|
||||||
Every “reply” uses the sender channel, which is dynamic and hence only known
|
|
||||||
partially. But ActorB did not know who sent the message it just replied to and
|
|
||||||
hence it cannot check that it can process the possible replies following that
|
|
||||||
message send. Only ActorA could have known, because it knows its own channels
|
|
||||||
as well as ActorB’s channels completely. The solution is thus to recursively
|
|
||||||
verify the message send, following all reply channels until all possible
|
|
||||||
message types to be sent have been verified. This sounds horribly complex, but
|
|
||||||
the algorithm for doing so actually has a worst-case complexity of O(N) where N
|
|
||||||
is the number of input channels of ActorA or ActorB, whoever has fewer.
|
|
||||||
|
|
||||||
The Parent Problem
|
|
||||||
------------------
|
|
||||||
|
|
||||||
There is one other actor reference which is available to ever actor: its
|
|
||||||
parent. Since the child–parent relationship is established permanently when the
|
|
||||||
child is created by the parent, this problem is easily solvable by encoding the
|
|
||||||
requirements of the child for its parent channel in its type signature having
|
|
||||||
the typed variant of ``actorOf`` verify this against the ``selfChannel``.
|
|
||||||
|
|
||||||
Anecdotally, since the guardian actor does not care at all about message sent
|
|
||||||
to it, top-level type channel actors must declare their parent channel to be
|
|
||||||
empty.
|
|
||||||
|
|
||||||
The Exposure/Restriction Problem
|
|
||||||
--------------------------------
|
|
||||||
|
|
||||||
An actor may provide more than one service, either itself or by proxy, each
|
|
||||||
with their own set of channels. Only having references for the full set of
|
|
||||||
channels leads to a too wide spread of capabilities: in the example of the
|
|
||||||
message rate throttling actor its management channel is only meant to be used
|
|
||||||
by the actor which inserted it, not by the two actors between it was inserted.
|
|
||||||
Hence the manager will have to create a channel reference which excludes the
|
|
||||||
management channels before handing out the reference to other actors.
|
|
||||||
|
|
||||||
Another variant of this problem is an actor which handles a channel whose input
|
|
||||||
type is a supertype for a number of derived channels. It should be allowed to
|
|
||||||
use the “superchannel” in place of any of the subchannels, but not the other
|
|
||||||
way around. The intuitive approach would be to model this by making the channel
|
|
||||||
reference contravariant in its channel types and define those channel types
|
|
||||||
accordingly. This does not work nicely, however, because Scala’s type system is
|
|
||||||
not well-suited to modeling such calculations on unordered type lists; it might
|
|
||||||
be possible but its implementation would be forbiddingly complex.
|
|
||||||
|
|
||||||
Therefore this topic gained traction as macros became available: being able to
|
|
||||||
write down type calculations using standard collections and their
|
|
||||||
transformations reduces the implementation to a handful of lines. The “narrow”
|
|
||||||
operation implemented this way allows all narrowing of input channels and
|
|
||||||
widening of output channels down to ``(Nothing, Any)`` (which is to say:
|
|
||||||
removal).
|
|
||||||
|
|
||||||
The Forwarding Problem
|
|
||||||
----------------------
|
|
||||||
|
|
||||||
One important feature of actors mentioned above is their composability which is
|
|
||||||
enabled by being able to forward or delegate messages. It is the nature of this
|
|
||||||
process that the sending party is not aware of the true destination of the
|
|
||||||
message, it only sees the façade in front of it. Above we have seen that the
|
|
||||||
sender ping-pong problem requires all verification to be performed at the
|
|
||||||
sender’s end, but if the sender does not know the final recipient, how can it
|
|
||||||
check that the message exchange is type-safe?
|
|
||||||
|
|
||||||
The forwarding party—the middle-man—is also not in the position to make this
|
|
||||||
call, since all it has is the incomplete sender channel which is lacking reply
|
|
||||||
type information. The problem which arises lies precisely in these reply
|
|
||||||
sequences: the ping-pong scheme was verified against the middle-man, and if the
|
|
||||||
final recipient would reply to the forwarded request, that sender reference
|
|
||||||
would belong to a different channel and there is no single location in the
|
|
||||||
source code where all these pieces are known at compile time.
|
|
||||||
|
|
||||||
The solution to this problem is not to allow forwarding in the normal untyped
|
|
||||||
:class:`ActorRef` sense. Replies must always be sent by the recipient of the
|
|
||||||
original message in order for the type checks at the sender site to be
|
|
||||||
effective. Since forwarding is an important communication pattern among actors,
|
|
||||||
support for it is thus provided in the form of the :meth:`ask` pattern combined
|
|
||||||
with the :meth:`pipe` pattern, which both are not add-ons but fully integrated
|
|
||||||
operations among typed channels.
|
|
||||||
|
|
||||||
The JVM Erasure Problem
|
|
||||||
-----------------------
|
|
||||||
|
|
||||||
When an actor with typed channels receives a message, this message needs to be
|
|
||||||
dispatched internally to the right channel, so that the right sender channel
|
|
||||||
can be presented and so on. This dispatch needs to work with the information
|
|
||||||
contained in the message, which due to the erasure of generic type information
|
|
||||||
is an incomplete image of the true channel types. Those full types exist only
|
|
||||||
at compile-time and reifying them into TypeTags at runtime for every message
|
|
||||||
send would be prohibitively expensive. This means that channels which erase to
|
|
||||||
the same JVM type cannot coexist within the same actor, message would not be
|
|
||||||
routable reliably in that case.
|
|
||||||
|
|
||||||
The Actor Lookup Problem
|
|
||||||
------------------------
|
|
||||||
|
|
||||||
Everything up to this point has assumed that channel references are passed from
|
|
||||||
their point of creation to their point of use directly and in the regime of
|
|
||||||
strong, unerased types. This can also happen between actors by embedding them
|
|
||||||
in case classes with proper type information. But one particular useful feature
|
|
||||||
of Akka actors is that they have a stable identity by which they can be found,
|
|
||||||
a unique name. This name is represented as a :class:`String` and naturally does
|
|
||||||
not bear any type information concerning the actor’s channels. Thus, when
|
|
||||||
looking up an actor with ``system.actorFor(...)`` you will only get an untyped
|
|
||||||
:class:`ActorRef` and not a channel reference. This :class:`ActorRef` can of
|
|
||||||
course manually be wrapped in a channel reference bearing the desired channels,
|
|
||||||
but this is not a type-safe operation.
|
|
||||||
|
|
||||||
The solution in this case must be a runtime check. There is an operation to
|
|
||||||
“narrow” an :class:`ActorRef` to a channel reference of given type, which
|
|
||||||
behind the scenes will send a message to the designated actor with a TypeTag
|
|
||||||
representing the requested channels. The actor will check these against its own
|
|
||||||
TypeTag and reply with the verification result. This check uses the same code
|
|
||||||
as the compile-time “narrow” operation introduced above.
|
|
||||||
|
|
||||||
Terminology
|
Terminology
|
||||||
===========
|
===========
|
||||||
|
|
@ -477,7 +293,217 @@ possible if that subchannel was declared up-front.
|
||||||
TypeTags are currently (Scala 2.10.0) not serializable, hence narrowing of
|
TypeTags are currently (Scala 2.10.0) not serializable, hence narrowing of
|
||||||
:class:`ActorRef` does not work for remote references.
|
:class:`ActorRef` does not work for remote references.
|
||||||
|
|
||||||
|
The Design Background
|
||||||
|
=====================
|
||||||
|
|
||||||
|
This section outlines the most prominent challenges encountered during the
|
||||||
|
development of Typed Channels and the rationale for their solutions. It is not
|
||||||
|
necessary to understand this material in order to use Typed Channels, but it
|
||||||
|
may be useful to explain why certain things are as they are.
|
||||||
|
|
||||||
|
The Type Pollution Problem
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
What if an actor accepts two different types of messages? It might be a main
|
||||||
|
communications channel which is forwarded to worker actors for performing some
|
||||||
|
long-running and/or dangerous task, plus an administrative channel for the
|
||||||
|
routing of requests. Or it might be a generic message throttler which accepts a
|
||||||
|
generic channel for passing it through (which delay where appropriate) and a
|
||||||
|
management channel for setting the throttling rate. In the second case it is
|
||||||
|
especially easy to see that those two channels will probably not be related,
|
||||||
|
their types will not be derived from a meaningful common supertype; instead the
|
||||||
|
least upper bound will probably be :class:`AnyRef`. If a typed channel
|
||||||
|
reference only had the capability to express a single type, this type would
|
||||||
|
then be no restriction anymore.
|
||||||
|
|
||||||
|
One solution to this is to never expose references describe more than one
|
||||||
|
channel at a time. But where would these references come from? It would be very
|
||||||
|
difficult to make this construction process type-safe, and it would also be an
|
||||||
|
inconvenient restriction, since message ordering guarantees only apply for the
|
||||||
|
same sender–receive pair, and if there are relations between the messages sent
|
||||||
|
on multiple channels those would need more boilerplate code to realize than if
|
||||||
|
all interaction were possible through a single reference.
|
||||||
|
|
||||||
|
The other solution thus is to express multiple channel types by a single
|
||||||
|
channel reference, which requires the implementation of type lists and
|
||||||
|
computations on these. And as we will see below it also requires the
|
||||||
|
specification of possibly multiple reply channels per input type, hence a type
|
||||||
|
map. The implementation chosen uses type lists like this:
|
||||||
|
|
||||||
|
.. includecode:: code/docs/channels/ChannelDocSpec.scala#motivation-types
|
||||||
|
|
||||||
|
This type expresses two channels: type ``A`` may stimulate replies of type
|
||||||
|
``B``, while type ``C`` may evoke replies of type ``D``. The type operator
|
||||||
|
``:+:`` is a binary type which form a list of these channel definitions, and
|
||||||
|
like every good list it ends with an empty terminator ``TNil``.
|
||||||
|
|
||||||
|
The Reply Problem
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
Akka actors have the power to reply to any message they receive, which is also
|
||||||
|
a message send and shall also be covered by typed channels. Since the sending
|
||||||
|
actor is the one which will also receive the reply, this needs to be verified.
|
||||||
|
The solution to this problem is that in addition to the ``self`` reference,
|
||||||
|
which is implicitly picked up as the sender for untyped actor interactions,
|
||||||
|
there is also a ``selfChannel`` which describes the typed channels handled by
|
||||||
|
this actor. Thus at the call site of the message send it must be verified that
|
||||||
|
this actor can actually handle the reply for that given message send.
|
||||||
|
|
||||||
|
The Sender Ping-Pong Problem
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
After successfully sending a message to an actor over a typed channel, that
|
||||||
|
actor will have a reference to the message’s sender, because normal Akka
|
||||||
|
message processing rules apply. For this sender reference there must exist a
|
||||||
|
typed channel reference which describes the possible reply types which are
|
||||||
|
applicable for each of the incoming message channels. We will see below how
|
||||||
|
this reference is provided in the code, the problem we want to highlight here
|
||||||
|
is a different one: the nature of any sender reference is that it is highly
|
||||||
|
dynamic, the compiler cannot possibly know who sent the message we are
|
||||||
|
currently processing.
|
||||||
|
|
||||||
|
But this does not mean that all hope is lost: the solution is to do *all*
|
||||||
|
type-checking at the call site of the message send. The receiving actor just
|
||||||
|
needs to declare its channel descriptions in its own type, and channel
|
||||||
|
references are derived at construction from this type (implying the existence
|
||||||
|
of a typed ``actorOf``). Then the actor knows for each received message type
|
||||||
|
which the allowed reply types are. The typed channel for the sender reference
|
||||||
|
hence has the reply types for the current input channel as its own input types,
|
||||||
|
but what should the reply types be? This is the ping-pong problem:
|
||||||
|
|
||||||
|
* ActorA sends MsgA to ActorB
|
||||||
|
|
||||||
|
* ActorB replies with MsgB
|
||||||
|
|
||||||
|
* ActorA replies with MsgC
|
||||||
|
|
||||||
|
Every “reply” uses the sender channel, which is dynamic and hence only known
|
||||||
|
partially. But ActorB did not know who sent the message it just replied to and
|
||||||
|
hence it cannot check that it can process the possible replies following that
|
||||||
|
message send. Only ActorA could have known, because it knows its own channels
|
||||||
|
as well as ActorB’s channels completely. The solution is thus to recursively
|
||||||
|
verify the message send, following all reply channels until all possible
|
||||||
|
message types to be sent have been verified. This sounds horribly complex, but
|
||||||
|
the algorithm for doing so actually has a worst-case complexity of O(N) where N
|
||||||
|
is the number of input channels of ActorA or ActorB, whoever has fewer.
|
||||||
|
|
||||||
|
The Parent Problem
|
||||||
|
------------------
|
||||||
|
|
||||||
|
There is one other actor reference which is available to every actor: its
|
||||||
|
parent. Since the child–parent relationship is established permanently when the
|
||||||
|
child is created by the parent, this problem is easily solvable by encoding the
|
||||||
|
requirements of the child for its parent channel in its type signature having
|
||||||
|
the typed variant of ``actorOf`` verify this against the ``selfChannel``.
|
||||||
|
|
||||||
|
Anecdotally, since the guardian actor does not care at all about message sent
|
||||||
|
to it, top-level type channel actors must declare their parent channel to be
|
||||||
|
empty.
|
||||||
|
|
||||||
|
The Exposure/Restriction Problem
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
An actor may provide more than one service, either itself or by proxy, each
|
||||||
|
with their own set of channels. Only having references for the full set of
|
||||||
|
channels leads to a too wide spread of capabilities: in the example of the
|
||||||
|
message rate throttling actor its management channel is only meant to be used
|
||||||
|
by the actor which inserted it, not by the two actors between it was inserted.
|
||||||
|
Hence the manager will have to create a channel reference which excludes the
|
||||||
|
management channels before handing out the reference to other actors.
|
||||||
|
|
||||||
|
Another variant of this problem is an actor which handles a channel whose input
|
||||||
|
type is a supertype for a number of derived channels. It should be allowed to
|
||||||
|
use the “superchannel” in place of any of the subchannels, but not the other
|
||||||
|
way around. The intuitive approach would be to model this by making the channel
|
||||||
|
reference contravariant in its channel types and define those channel types
|
||||||
|
accordingly. This does not work nicely, however, because Scala’s type system is
|
||||||
|
not well-suited to modeling such calculations on unordered type lists; it might
|
||||||
|
be possible but its implementation would be forbiddingly complex.
|
||||||
|
|
||||||
|
Therefore this topic gained traction as macros became available: being able to
|
||||||
|
write down type calculations using standard collections and their
|
||||||
|
transformations reduces the implementation to a handful of lines. The “narrow”
|
||||||
|
operation implemented this way allows all narrowing of input channels and
|
||||||
|
widening of output channels down to ``(Nothing, Any)`` (which is to say:
|
||||||
|
removal).
|
||||||
|
|
||||||
|
The Forwarding Problem
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
One important feature of actors mentioned above is their composability which is
|
||||||
|
enabled by being able to forward or delegate messages. It is the nature of this
|
||||||
|
process that the sending party is not aware of the true destination of the
|
||||||
|
message, it only sees the façade in front of it. Above we have seen that the
|
||||||
|
sender ping-pong problem requires all verification to be performed at the
|
||||||
|
sender’s end, but if the sender does not know the final recipient, how can it
|
||||||
|
check that the message exchange is type-safe?
|
||||||
|
|
||||||
|
The forwarding party—the middle-man—is also not in the position to make this
|
||||||
|
call, since all it has is the incomplete sender channel which is lacking reply
|
||||||
|
type information. The problem which arises lies precisely in these reply
|
||||||
|
sequences: the ping-pong scheme was verified against the middle-man, and if the
|
||||||
|
final recipient would reply to the forwarded request, that sender reference
|
||||||
|
would belong to a different channel and there is no single location in the
|
||||||
|
source code where all these pieces are known at compile time.
|
||||||
|
|
||||||
|
The solution to this problem is not to allow forwarding in the normal untyped
|
||||||
|
:class:`ActorRef` sense. Replies must always be sent by the recipient of the
|
||||||
|
original message in order for the type checks at the sender site to be
|
||||||
|
effective. Since forwarding is an important communication pattern among actors,
|
||||||
|
support for it is thus provided in the form of the :meth:`ask` pattern combined
|
||||||
|
with the :meth:`pipe` pattern, which both are not add-ons but fully integrated
|
||||||
|
operations among typed channels.
|
||||||
|
|
||||||
|
The JVM Erasure Problem
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
When an actor with typed channels receives a message, this message needs to be
|
||||||
|
dispatched internally to the right channel, so that the right sender channel
|
||||||
|
can be presented and so on. This dispatch needs to work with the information
|
||||||
|
contained in the message, which due to the erasure of generic type information
|
||||||
|
is an incomplete image of the true channel types. Those full types exist only
|
||||||
|
at compile-time and reifying them into TypeTags at runtime for every message
|
||||||
|
send would be prohibitively expensive. This means that channels which erase to
|
||||||
|
the same JVM type cannot coexist within the same actor, message would not be
|
||||||
|
routable reliably in that case.
|
||||||
|
|
||||||
|
The Actor Lookup Problem
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
Everything up to this point has assumed that channel references are passed from
|
||||||
|
their point of creation to their point of use directly and in the regime of
|
||||||
|
strong, unerased types. This can also happen between actors by embedding them
|
||||||
|
in case classes with proper type information. But one particular useful feature
|
||||||
|
of Akka actors is that they have a stable identity by which they can be found,
|
||||||
|
a unique name. This name is represented as a :class:`String` and naturally does
|
||||||
|
not bear any type information concerning the actor’s channels. Thus, when
|
||||||
|
looking up an actor with ``system.actorFor(...)`` you will only get an untyped
|
||||||
|
:class:`ActorRef` and not a channel reference. This :class:`ActorRef` can of
|
||||||
|
course manually be wrapped in a channel reference bearing the desired channels,
|
||||||
|
but this is not a type-safe operation.
|
||||||
|
|
||||||
|
The solution in this case must be a runtime check. There is an operation to
|
||||||
|
“narrow” an :class:`ActorRef` to a channel reference of given type, which
|
||||||
|
behind the scenes will send a message to the designated actor with a TypeTag
|
||||||
|
representing the requested channels. The actor will check these against its own
|
||||||
|
TypeTag and reply with the verification result. This check uses the same code
|
||||||
|
as the compile-time “narrow” operation introduced above.
|
||||||
|
|
||||||
How to read The Types
|
How to read The Types
|
||||||
=====================
|
=====================
|
||||||
|
|
||||||
|
In case of errors in your code the compiler will try to inform you in the most
|
||||||
|
precise way it can, and that will then contain types like this::
|
||||||
|
|
||||||
|
akka.channels.:+:[(com.example.Request, com.example.Reply),
|
||||||
|
akka.channels.:+:[(com.example.Command, Nothing), TNil]]
|
||||||
|
|
||||||
|
These types look unwieldy because of two things: they use fully qualified names
|
||||||
|
for all the types (thankfully using the ``()`` sugar for :class:`Tuple2`), and
|
||||||
|
they do not employ infix notation. That same type there might look like this in
|
||||||
|
your source code::
|
||||||
|
|
||||||
|
(Request, Reply) :+: (Command, Nothing) :+: TNil
|
||||||
|
|
||||||
|
As soon as someone finds the time, it would be nice if the IDEs learned to
|
||||||
|
print types making use of the file’s import statements and infix notation.
|
||||||
|
|
|
||||||
|
|
@ -437,7 +437,7 @@ object AkkaBuild extends Build {
|
||||||
) configs (MultiJvm)
|
) configs (MultiJvm)
|
||||||
|
|
||||||
lazy val channels = Project(
|
lazy val channels = Project(
|
||||||
id = "akka-channels",
|
id = "akka-channels-experimental",
|
||||||
base = file("akka-channels"),
|
base = file("akka-channels"),
|
||||||
dependencies = Seq(actor),
|
dependencies = Seq(actor),
|
||||||
settings = defaultSettings ++ Seq(
|
settings = defaultSettings ++ Seq(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue