Sep 24, 2018

[Golang] Protoactor-go 101: How actors communicate with each other

Designing actor-based program is all about dividing tasks into smaller pieces. Fine-grained actors concentrate on their tasks, collaborate with other actors and accomplish a big task as a whole. Hence mastering actors' communication mechanism and modeling well-defined messages are always the keys to designing an actor system. This article describes protoactor-go's actor categories, their messaging methods and how those methods differ on referencing sender actors.
See my previous article, [Golang] Protoactor-go 101: Introduction to golang's actor model implementation, for protoactor-go's basic concepts and terms.

TL;DR

While there are several kinds of actors, those actors share a unified interface to communicate with each other. Various methods are provided for their communication, but always use Request() to acknowledge the recipient actor who the sender actor is. When that is not an option, include the sender actor's actor.PID in the sending message.

Example codes

Example codes that cover all communication means for all actor implementations are located at github.com/oklahomer/protoactor-go-sender-example. Minimal examples are introduced in this article for description, but visit this repository for comprehensive examples.

Premise: Three major kinds of actors

protoactor-go comes with three kinds of actors: local, remote and cluster grain.

  • Local ... Those actors located in the same process.
  • Remote ... Actors located in different processes or servers. An actor is considered to be "local" when addressed from within the same process; while this is "remote" when addressed across a network. Because a message is sent over a network, message serialization is required. Protocol Buffers is used for this task in protoactor-go.
  • Cluster grain ... A kind of remote actor but the lifecycle and other complexity are taken care of by protoactor-go library. Cluster topology is managed by consul and a grain can be addressed over a network. Consul manages the cluster membership and the availability of each node.
Thanks to the location transparency, an actor can communicate with other actors in the same way without worrying about where the recipient actors are located at. In addition to those basic communication means, a cluster grain has an extra mechanism to provide RPC based interface.
Each actor is encapsulated in an actor.PID instance so developers communicate with actors via methods provided by this actor.PID. (actor.Context also provides equivalent methods, but these can be considered as wrappers for actor.PID's corresponding methods.) One important thing to remember is that above actors are not the only entities encapsulated in actor.PIDs. As a matter of fact, any actor.Process implementation including mailbox, Future mechanism and others are also encapsulated in actor.PIDs. This may be familiar to those with Erlang background. Understanding this becomes vital when one tries referring to message sender actor. The rest of this article is going to describe each messaging method and how a recipient actor can refer to the sending actor.

Communication methods

Below are the common communication methods -- Tell(), Request() and RequestFuture() -- and RPC based method for cluster grain. Examples in this article all demonstrate local actor messaging because local and remote actors share a common messaging interface. Visit my example repository to cover all messaging implementations of local, remote and cluster grain.

Tell() tells nothing about the sender 

To send a message to an actor, one may call actor.PID's Tell() method. When a message is sent from outside of an actor system by calling PID.Tell(), the recipient actor fails to refer to the sending actor with Context.Sender()This is pretty obvious. Because the message is sent from outside, there is no such thing as sending actor. Below is an example:
package main

import (
 "github.com/AsynkronIT/protoactor-go/actor"
 "time"
)

type ping struct{}

type pong struct{}

func main() {
 props := actor.FromFunc(func(ctx actor.Context) {
  switch ctx.Message().(type) {
  case *ping:
   // This fails to get sender
   // because the message came
   // from outside of actor system
   //
   // Below execution leads to dead letter
   // 2018/09/14 22:40:02 [ACTOR] [DeadLetter] pid="nil" message=&{} sender="nil"
   ctx.Respond(&pong{})

   // Below execution causes a panic since Sender() returns nil.
   // Actor crashes and that causes supervisor to restart this failing actor.
   // 2018/09/14 22:40:02 [MAILBOX] [ACTOR] Recovering actor="nonhost/$1" reason="runtime error: invalid memory address or nil pointer dereference" stacktrace="github.com/AsynkronIT/protoactor-go/actor.(*PID).ref:26"
   // 2018/09/14 22:40:02 [ACTOR] [SUPERVISION] actor="nonhost/$1" directive="RestartDirective" reason="runtime error: invalid memory address or nil pointer dereference"
   ctx.Sender().Tell(&pong{})

  }
 })

 pid := actor.Spawn(props)
 pid.Tell(&ping{})

 time.Sleep(1 * time.Second) // Just to make sure system ends after actor execution
}
In the above example, a message is directly sent to an actor from outside of an actor system. Therefore the recipient actor fails to refer to the sending actor. With Akka, this behavior is similar to set ActorRef#noSender as the second argument of ActorRef#tell -- when the recipient tries to respond, the message goes to the dead letter mailbox.

When a message is sent from one actor to another, there indeed is a sender-recipient relationship. Recipient actor's contextual information, actor.Context, appears to provide such information for us. Below is an example code that tries to refer to the sender actor with actor.Context:
package main

import (
 "github.com/AsynkronIT/protoactor-go/actor"
 "log"
 "time"
)

type pong struct {
}

type ping struct {
}

type pingActor struct {
 pongPid *actor.PID
}

func (p *pingActor) Receive(ctx actor.Context) {
 switch ctx.Message().(type) {
 case struct{}:
  // Below does not set ctx.Self() as sender,
  // and hence the recipient has no knowledge of the sender
  // even though the message is sent from another actor via actor.Context.
  //
  ctx.Tell(p.pongPid, &ping{})

 case *pong:
  log.Print("Received pong message")

 }
}

func main() {
 pongProps := actor.FromFunc(func(ctx actor.Context) {
  switch ctx.Message().(type) {
  case *ping:
   log.Print("Received ping message")

   // 2018/09/15 02:01:27 [ACTOR] [DeadLetter] pid="nil" message=&{} sender="nil"
   ctx.Respond(&pong{})

   // 2018/09/15 02:01:27 [MAILBOX] [ACTOR] Recovering actor="nonhost/$1" reason="runtime error: invalid memory address or nil pointer dereference" stacktrace="github.com/AsynkronIT/protoactor-go/actor.(*PID).ref:26"
   // 2018/09/15 02:01:27 [ACTOR] [SUPERVISION] actor="nonhost/$1" directive="RestartDirective" reason="runtime error: invalid memory address or nil pointer dereference"
   ctx.Sender().Tell(&pong{})

  default:

  }
 })
 pongPid := actor.Spawn(pongProps)

 pingProps := actor.FromProducer(func() actor.Actor {
  return &pingActor{
   pongPid: pongPid,
  }
 })
 pingPid := actor.Spawn(pingProps)
 pingPid.Tell(struct{}{})
 time.Sleep(1 * time.Second) // Just to make sure system ends after actor execution
}
However, the recipient fails to refer to the sender actor in the same way it failed in the previous example. This may seem odd, but let us take a look at actor.Context's implementation. A call to Context.Tell() is proxied to Context.sendUserMessage(), where the message is stuffed into actor.MessageEnvelope with nil Sender field as below:
func (ctx *localContext) Tell(pid *PID, message interface{}) {
 ctx.sendUserMessage(pid, message)
}

func (ctx *localContext) sendUserMessage(pid *PID, message interface{}) {
 if ctx.outboundMiddleware != nil {
  if env, ok := message.(*MessageEnvelope); ok {
   ctx.outboundMiddleware(ctx, pid, env)
  } else {
   ctx.outboundMiddleware(ctx, pid, &MessageEnvelope{
    Header:  nil,
    Message: message,
    Sender:  nil,
   })
  }
 } else {
  pid.ref().SendUserMessage(pid, message)
 }
}
That is why a recipient cannot refer to the sender even though the messaging occurs between two actors and such contextual information seems to be available. The above code fragment suggests that passing actor.MessageEnvelope with pre-filled Sender field should tell the sending actor to the recipient. This actually works because all actor.MessageEnvelope's fields are public and accessible, but this is a cumbersome job. There should be a way to do that.

Request() lets a recipient request for the sender reference

A second messaging method is Request(). This lets developers set who the sender actor is, and the recipient actor can reply to the sender actor by calling Context.Respond() or by calling Context.Sender().Tell(). Below is the method signature.
// Request sends a messages asynchronously to the PID. The actor may send a response back via respondTo, which is
// available to the receiving actor via Context.Sender
func (pid *PID) Request(message interface{}, respondTo *PID) {
 env := &MessageEnvelope{
  Message: message,
  Header:  nil,
  Sender:  respondTo,
 }
 pid.ref().SendUserMessage(pid, env)
}
Above signature may look more like Akka's ActorRef#tell than Tell() in a way that a developer can set a sender actor, more precisely a sending actor.PID in this case, as a second argument. An actor.PID and an actor.Context both have Request() method and they behave equivalently as described in the below example:
package main

import (
 "github.com/AsynkronIT/protoactor-go/actor"
 "log"
 "time"
)

type pong struct {
}

type ping struct {
}

type pingActor struct {
 pongPid *actor.PID
}

func (p *pingActor) Receive(ctx actor.Context) {
 switch ctx.Message().(type) {
 case struct{}:
  // Below both send a message with sender information
  ctx.Request(p.pongPid, &ping{})
  p.pongPid.Request(&ping{}, ctx.Self())

 case *pong:
  log.Print("Received pong message")

 }
}

func main() {
 pongProps := actor.FromFunc(func(ctx actor.Context) {
  switch ctx.Message().(type) {
  case *ping:
   log.Print("Received ping message")

   // Below both work
   ctx.Respond(&pong{})
   ctx.Sender().Tell(&pong{})

  default:

  }
 })
 pongPid := actor.Spawn(pongProps)

 pingProps := actor.FromProducer(func() actor.Actor {
  return &pingActor{
   pongPid: pongPid,
  }
 })
 pingPid := actor.Spawn(pingProps)
 pingPid.Tell(struct{}{})
 time.Sleep(1 * time.Second) // Just to make sure system ends after actor execution
}

This not only works for request-response model, but also works to propagate the sending actor identity to subsequent actor calls.

RequestFuture() only has its future

The last method is ReqeustFuture(). This can be used as an extension of Request() where an actor.Future is returned to the requester. However, its behavior differs slightly but significantly when the recipient actor tries referring to the sender with Context.Sender() and treating this as a reference to the sender actor. Below is a simple example that demonstrates a regular request-response model:
package main

import (
 "github.com/AsynkronIT/protoactor-go/actor"
 "log"
 "time"
)

type pong struct {
}

type ping struct {
}

type pingActor struct {
 pongPid *actor.PID
}

func (p *pingActor) Receive(ctx actor.Context) {
 switch ctx.Message().(type) {
 case struct{}:
  // Below both work.
  //
  //future := p.pongPid.RequestFuture(&ping{}, time.Second)
  future := ctx.RequestFuture(p.pongPid, &ping{}, time.Second)
  result, err := future.Result()
  if err != nil {
   log.Print(err.Error())
   return
  }
  log.Printf("Received %#v", result)

 case *pong:
  // Never comes here.
  // When the pong actor responds to the sender,
  // the sender is not a ping actor but a future process.
  log.Print("Received pong message")

 }
}

func main() {
 pongProps := actor.FromFunc(func(ctx actor.Context) {
  switch ctx.Message().(type) {
  case *ping:
   log.Print("Received ping message")
   // Below both work in this example, but their behavior slightly differ.
   // ctx.Sender().Tell() panics and recovers if the sender is nil;
   // while ctx.Respond() checks the presence of sender and redirects the message to dead letter process
   // when sender is absent.
   //
   //ctx.Sender().Tell(&pong{})
   ctx.Respond(&pong{})

  default:

  }
 })
 pongPid := actor.Spawn(pongProps)

 pingProps := actor.FromProducer(func() actor.Actor {
  return &pingActor{
   pongPid: pongPid,
  }
 })
 pingPid := actor.Spawn(pingProps)
 pingPid.Tell(struct{}{})
 time.Sleep(1 * time.Second) // Just to make sure system ends after actor execution
}
Now the below example demonstrates how Request() and RequestFuture() behave differently when Context.Sender() or Context.Respond() is called to refer to the sender actor's actor.PID. The code structure is almost the same as the previous example besides that below tries to send back multiple messages to the sender actor.
package main

import (
 "github.com/AsynkronIT/protoactor-go/actor"
 "log"
 "time"
)

type pong struct {
}

type ping struct {
}

type pingActor struct {
 pongPid *actor.PID
}

func (p *pingActor) Receive(ctx actor.Context) {
 switch ctx.Message().(type) {
 case struct{}:
  // Below both work.
  //
  //future := p.pongPid.RequestFuture(&ping{}, time.Second)
  future := ctx.RequestFuture(p.pongPid, &ping{}, time.Second)
  result, err := future.Result()
  if err != nil {
   log.Print(err.Error())
   return
  }
  log.Printf("Received %#v", result)

 case *pong:
  // Never comes here.
  // When the pong actor responds to the sender,
  // the sender is not a ping actor but a future process.
  log.Print("Received pong message")

 }
}

func main() {
 pongProps := actor.FromFunc(func(ctx actor.Context) {
  switch ctx.Message().(type) {
  case *ping:
   log.Print("Received ping message")
   // Below both work in this example, but their behavior slightly differ.
   // ctx.Sender().Tell() panics and recovers if the sender is nil;
   // while ctx.Respond() checks the presence of sender and redirects the message to dead letter process
   // when sender is absent.
   //
   //ctx.Sender().Tell(&pong{})
   ctx.Respond(&pong{})

   // Take a look at the id field.
   // 2018/09/23 10:58:53 &actor.PID{Address:"nonhost", Id:"future$3", p:(*actor.Process)(0xc4200ea010)}
   log.Printf("%#v", ctx.Sender())

   // Below all fail because the sender PID does not represents the sender actor,
   // but the sending Future process and the Future process ends when the first payload is returned.
   ctx.Sender().Tell(&pong{})
   ctx.Respond(&pong{})
   ctx.Sender().Tell(&pong{})
   ctx.Respond(&pong{})
   ctx.Sender().Tell(&pong{})
   ctx.Respond(&pong{})
   ctx.Sender().Tell(&pong{})
   ctx.Respond(&pong{})

  default:

  }
 })
 pongPid := actor.Spawn(pongProps)

 pingProps := actor.FromProducer(func() actor.Actor {
  return &pingActor{
   pongPid: pongPid,
  }
 })
 pingPid := actor.Spawn(pingProps)
 pingPid.Tell(struct{}{})
 time.Sleep(1 * time.Second) // Just to make sure system ends after actor execution
}
Remember, as briefly introduced in the "Premise" section, an actor.PID not only encapsulates an actor.Actor instance but also encapsulates any actor.Process implementation. The concept of "process" and its representation, PID, are quite similar to those of Erlang in this way. With that said, let us take a closer look at how the above example behaves under the hood. First, two processes for actor PIDs are explicitly created by the developer: pingPid and pongPid. When pingPid sends a message to pongPid, another process is implicitly created by protoactor-go: that of actor.Future. And this actor.Future process is set as the sender PID when communication takes place.
func (ctx *localContext) RequestFuture(pid *PID, message interface{}, timeout time.Duration) *Future {
 future := NewFuture(timeout)
 env := &MessageEnvelope{
  Header:  nil,
  Message: message,
  Sender:  future.PID(),
 }
 ctx.sendUserMessage(pid, env)

 return future
}
When the recipient actor's process, pongPid, receives the message and respond to the sender, the "sender" is not actually pingPid but the actor.Future's process. After one message is sent back to pingPid, the actor.Future process ends and therefore the subsequent calls to Context.Respond() or Context.Sender() from pongPid fail to refer to the sender. So when the passing of sender actor's PID is vital for the recipient's task execution, use Request() or include the sender actor's actor.PID in the sending message so the recipient can refer to the sender actor for sure.

Cluster grain's unique RPC based messaging

Actors can communicate with Cluster grains just like communicating with remote actors. In fact, protoactor-go's cluster mechanism is implemented on top of actor.remote implementation. However, this cluster mechanism adopts the idea of Microsoft Orleans where the actor lifecycle and other major tasks are managed by the actor framework to ease the developer's work. This effort includes the introduction of handy RPC based communication protocol. Communication with cluster grains still use Protocol Buffers for serialization and deserialization, but this goes a bit further by providing a wrapper for gRPC service calls.
By using gograin protoc plugin, a code is generated for gRPC services. This code provides an actor.Actor implementation where Receive() receives a message from another actor, deserializes it and calls a corresponding method depending on the incoming message type. Developers only have to implement a method for each gRPC service. The returning value of the implemented method is returned to the sender actor.  One thing to notice is that this remote gRPC call is implemented with RequestFuture() under the hood. So when the method tries referring to the sender by Context.Sender(), the returned actor.PID is not a representation of the sender actor but an actor.Future. The example contains a relatively large amount of code so visit my example repository for details. Directory layout is as below:

  • messages ... This includes messages shared by sender and recipient actors. protos_protoactor.go contains the code generated by gograin protoc plugin. This is used for the gRPC based communication.
  • cluster-ping-grpc and cluster-pong-grpc ... These provide implementations for ping actor and pong actor. They communicate over gRPC based protocol.
  • cluster-ping-future, cluster-ping-request, cluster-ping-tell and cluster-pong ... These are examples that communicate with actor.remote implementation without the gRPC service.

Conclusion

While there are several kinds of actors, those actors have unified ways to communicate with other actors no matter where they are located at. However, because an actor.PID is not only a representation of an actor process but also a representation of any actor.Process implementation, extra work may be required for a recipient actor to refer to the sender actor since the returning actor.PID of Context.Sender() is not necessarily a sender actor's representation. To ensure that the recipient actor can refer to the sender actor, include the sender actor's PID in the sending message or use Request(). Visit github.com/oklahomer/protoactor-go-sender-example for more comprehensive examples.

Jul 22, 2018

[Golang] Protoactor-go 101: Introduction to golang's actor model implementation

A year has passed since I officially launched go-sarah. While this bot framework had been a great help with my ChatOps, I found myself becoming more and more interested in designing chat system as a whole. Not just a text-based communication tool or its varied extension; but as a customizable event aggregation system that provides and consumes any conceivable event varied from virtual to real-life. In the course of its server-side design, Golang's actor model implementation, protoactor-go, seemed like a good option. However, protoactor-go is still in its Beta phase and has less documentation at this point in time. This article describes what I have learned about this product. The basic of actor model is not going to be covered, but for those who are interested, my previous post "Yet another Akka introduction for dummies" might be a help.
Unless otherwise noted, this introduction is based on the latest version as of 2018-07-21.

Terms, Concepts, and Common Types

  • Message ... With the nature of the actor model, a message plays an important part to let actors interact with others. Messages internally fall into two categories:
    • User message ... Messages defined by developers for actor interaction.
    • System message ... Messages defined by protoactor-go for internal use that mainly handles the actor life cycle.
  • PID ... actor.PID is a container that combines a unique identifier, the address and a reference to actor.Process altogether. Since this provides interfaces for others to interact with the underlying actor, this can be seen as an actor reference if one is familiar with Akka. Or simply a Pid if familiar with Erlang. However, this is very important to remember that actor process is not the only entity that a PID encapsulates.
  • Process ... actor.Process defines a common interface that all interacting "process" must implement. In this project, the concepts of process and PID are quite similar to those of Erlang. Understanding that PID is not necessarily a representation of actor process is vital when referring to actor messaging context. This distinction and its importance are described in the follow-up article, [Golang] Protoactor-go 101: How actors communicate with each other. Its implementation varies depending on each role:
    • Router ... router.process receives a message and broadcasts it to all subordinating actors, "routees." 
    • Local process ... actor.localProcess has a reference to a mailbox. On message reception,  this enqueues the message to its mailbox so the actor can receive this for further procedure.
    • Remote process ... On contrary to a local process, this represents an actor that exists in a remote environment. On message reception, this serializes the message and sends it to the destination host.
    • Guardian process ... When a developer passes a "guardian"'s supervisor strategy for actor constructor, a parent actor is created with this supervisor strategy along with the actor itself. This parent "guardian" actor will take care of the child actor's uncontrollable state. This should be effective when the constructing actor is the "root actor" -- an actor without a parent actor -- but customized supervision is still required. When multiple actor constructions contain the same settings for guardian supervision, only one guardian actor is created and this becomes the parent of all actors with the same settings.
    • Future process ... actor.futureProcess provides some dedicated features for Future related tasks.
    • Dead letter process ... actor.deadLetterProcess provides features to handle "dead letters." A dead letter is a message that failed to reach target because, for example, the target actor did not exist or was already stopped. This dead letter process publishes actor.DeadLetterEvent to the event stream, so a developer can detect the dead letter by subscribing to the event via eventstream.Subscribe().
  • Mailbox ... This works as a queue to receive incoming messages, store them temporarily and pass them to its coupled actor when the actor is ready for message execution. The actor is to receive the message one at a time, execute its task and alter its state if necessary. Mailbox implements mailbox.Inbound interface.
    • Default mailbox ... mailbox.defaultMailbox not only receives incoming messages as a mailbox.Inbound implementation, but also coordinates the actor invocation schedule with its mailbox.Dispatcher implementation entity. This mailbox also contains mailbox.MessageInvoker implementation as its entity and its methods are called by mailbox.Dispatcher for actor invocation purpose. actor.localContext implements mailbox.MessageInvoker.
  • Context ... This is equivalent to Akka's ActorCoontext. This contains contextual information and contextual methods for the underlying actor such as below:
    • References to watching actors and methods to watch/unwatch other actors
    • A reference to the actor who sent the currently processing message and a method to access to this
    • Methods to pass a message to another actor
    • etc...
  • Middleware ... Zero or more pre-registered procedures can be executed around actor invocation, which enables an AOP-like approach to modify behavior.
    • Inbound middleware ... actor.InboundMiddleware is a middleware that is executed on message reception. A developer may register one or more middleware via Props.WithMiddleware().
    • Outbound middleware ... actor.OutboundMiddleware is a middleware that is executed on message sending. A developer may register one or more middleware via Props.WithOutboundMiddleware().
  • Router ... router sub-package provides a series of mechanism that routes a given message to one or more of its routees.
    • Broadcast router ... Broadcast given message to all of its routee actors.
    • Round robin router ... Send given message to one of its routee actors chosen by round-robin manner
    • Random router ... Send given message to a randomly chosen routee actor.
  • Event Stream ... eventstream.EventStream is a mechanism to publish and subscribe given event where the event is an empty interface, interface{}. So the developer can technically publish and subscribe to any desired event. By default an instance of eventstream.EventStream is cached in package local manner and is used to publish and subscribe events such as dead letter messages.

Actor Construction

To construct a new actor and acquire a reference to this, a developer can feed an actor.Props to actor.Spawn or actor.SpawnNamed. The struct called actor.Props is a set of configuration for actor construction. actor.Props can be initialized with helper functions listed below:
  • actor.FromProducer() ... Pass a function that returns an actor.Actor implementation. This returns a pointer to actor.Props, which contains a set of configurations for actor construction.
  • actor.FromFunc() ... Pass a function that satisfies actor.ActorFunc type, which receives exactly the same arguments as Actor.Recieve(). This is a handy wrapper of actor.FromProducer.
  • actor.FromSpawnFunc() ... Pass a function that satisfies actor.SpawnFunc type. on actor construction, this function is called with a series of arguments containing id, actor.Props and parent PID to construct a new actor. When this function is not set, actor.DefaultSpawner is used.
  • actor.FromInstance() ... Deprecated.
Additional configuration can be added via its setter methods with "With" prefix. See example code.

Spawner -- Construct actor and initiate its life cycle

A developer feeds a prepared actor.Props to actor.Spawn() or actor.SpawnNamed() depending on the requirement to initialize an actor, its context, and its mailbox. In any construction flow, Props.spawn() is called. To alter this spawning behavior, an alternative function can be set with actor.FromSpawnFunc() or Props.WithSpawnFunc() to override the default behavior. When none is set, actor.DefaultSpawner is used by default. Its behavior is as below:
  • The default spawner creates an instance of actor.localProcess, which is an actor.Process implementation.
  • Add the instance to actor.ProcessRegistry.
    • The registry returns an error if given id is already registered.
  •  Create new actor.localContext which is an actor.Context implementation. This stores all contextual data.
  • Mailbox is created for the context. To modify the behavior of mailbox, use Props.WithDispatcher() and Props.WithMailbox().
  • Created mailbox is stored in the actor.localProcess instance.
  • The pointer to the process is set to actor.PID's field.
  • actor.localContext also has a reference to the actor.PID as "self."
  • Start mailbox
  • Enqueue mailbox a startedMessage as a system message which is an instance of actor.Started.
When construction is done and the actor life cycle is successfully started, actor.PID for the new actor is returned.

Child Actor construction

With the introduced actor construction procedure, a developer can create any "root actor," an actor with no parent. To achieve a hierarchized actor system, use actor.Context's Spawn() or SpawnNamed() method. Those methods work similarly to actor.Spawn() and actor.SpawnNamed(), but the single and biggest difference is that they create a parent-child relationship between the spawning actor and the newly created actor. They work as below:

  • Check if Props.guardianStrategy is set
    • If set, it panics. Because the calling actor is going to be the parent and be obligated to be a supervisor, there is no need to set one. This strategy is to create a parent actor for customized supervision as introduced in the first section.
  • Call Props.spawn()
    • The ID has a form of {parent-id}/{child-id}
    • Own PID is set as a parent for the new actor
  • Add created actors actor.PID to its children
  • Start watching the created actor.PID to subscribe its life cycle event

 Supervisor Strategy

This is a parent actor's responsibility to take care of its child actor's exceptional state. When a child actor can no longer control its state, based on the "let-it-crash" philosophy, child actor notifies such situation to parent actor by panic(). The parent actor receives such notification with recover() and decides how to treat such failing actor. This decision is made by a customizable actor.SupervisorStrategy. When no strategy is explicitly set by a developer, actor.defaultSupervisorStrategy is set on actor construction.
The supervision flow is as follows:
  • A mailbox passes a message to Actor.Recieve() via target actor context's localContext.InvokeUserMessage().
  • In Actor.Receive(), the actor calls panic().
  • Caller mailbox catches such uncontrollable state with recover().
  • The mailbox calls localContext.EscalateFailure(), where localContext is that of the failing actor.
    • In localContext.EscalateFailure(), this tells itself to suspend any incoming message till recovery is done.
    • Create actor.Failure instance that holds failing reason and other statistical information, where "reason" is the argument passed to panic().
    • Judges if the failing actor has any parent
      • If none is found, the failing actor is the "root actor" so the actor.Failure is passed to actor.handleRootFactor().
      • If found, this passes actor.Failure to parent's PID.sendSystemMessage() to notify failing state
        • The message is enqueued to parent actor's mailbox
        • Parent's mailbox calls its localContext.InvokeSystemMessage.
        • actor.Failure is passed to localContext.handleFailure
        • If its actor.Actor entity itself implements actor.SupervisorStrategy, its HandleFailure() is called.
        • If not, its supervisor entity's handleFailure() is called.
        • In HandleFailure(), decide recovery policy and call localContext.(ResumeChildren|RestartChildren|StopChildren|EscalateFailure).

Upcoming Interface Change

A huge interface change is expected according to the issue "Design / API Changes upcoming."

Further Readings

See below articles for more information: