Skip to content

FamilyAid

devlaam edited this page Sep 13, 2023 · 9 revisions

FamilyAid: Actors within a hierarchical structure

Text compliant with Leucine version 0.5.1

For 0.5.2: $\textcolor{red}{\mbox{\scriptsize{}PAGE UNDER CONSTRUCTION}}$

Introduction

Sometimes a process consists of several services that belong together. One as part of the other, or just a situation where, when one of the services stops, the others do not serve any purpose any more. In that case it might be handy to bring them together as a family. In a family a parent keeps track of all the children and when the parent die so do all the children (a bit unnatural perhaps). This shutdown is performed in a well defined, organized fashion. Also, it is possible to send messages to all children at once, or address a child based on the name.

The families come in two categories. The most simple is the family tree: FamilyTree, where each member is able to receive the same messages, so its easy to pass them down the tree. It takes away some of the type rigor of course The other category consists of a set of three types mixins: FamilyRoot, FamilyBranch and FamilyLeaf. These come in different flavors as well (Relay of Relayed) which makes their use a little less straight forward.

The Family Tree

An actor becomes part of a family tree by mixin in FamilyTree. It is required to supply the type of the actor itself as type parameter. All members of the actor tree are instances of this class. Since all actors except the root have a parent, the parent is an option of the same type.

Let us look at yet an other useless example. We want a tree that is able to explore itself with forward and backward messages. And of course the tree must recursively create itself with given number of children and for a certain number of levels. Therefore we define the following letters in the companion object.

/* Actor is of the type Accept without managed State */
object Tree extends AcceptDefine, Stateless :
  sealed trait Letter extends Actor.Letter[Actor]
  /* Message to create the tree structure. The maximal number of levels
   * is given by depth, the number of actors created in each level given
   * by width. */
  case class  Create(width: Int, depth: Int) extends Letter
  /* Message to traverse the tree in forward direction. */
  case object Forward extends Letter
  /* Message to traverse the tree in backwards direction. */
  case object Backward extends Letter

The class that comes with it:

/* Actor that recursively enters its structure to create and investigate itself.
 * The root of the actor structure has no parent, therefore the parent is optional in this case. */
class Tree(name: String, val parent: Option[Tree]) extends AcceptActor(Tree,name), FamilyTree[Tree] :

  /* Write the results of this actor to the console. */
  private def write(action: String) = println(s"$action $path")

  /* New children must be created with their parent as parameter.*/
  private def newChild(i: Int) = Tree(s"F$i",Some(this))

  /* Handle the incoming letters. */
  final protected def receive(letter: Letter, sender: Sender): Unit = letter match
    /* This message creates <width> new children for this actor. */
    case Tree.Create(width,level) =>
      /* Create 'width' number of new children. */
      (1 to width).foreach(newChild)
      /* In case we are not yet on the last level, relay this creation order
       * to the next level. */
      if (level > 1) then relayAll(Tree.Create(width,level - 1))
    /* This message will travel forward through the tree structure. */
    case Tree.Forward =>
      /* Report that we are in the forward traversal. */
      write("=>>")
      /* Relay the message to all children, and see if we succeeded. */
      val relayed = relayAll(Tree.Forward)
      /* In case there were no children to accept the message, we are at the
       * end of the structure and start the traversal backwards. */
      if relayed == 0 then parent.foreach(_ ! Tree.Backward)
    /* This message will travel backward through the tree structure. */
    case Tree.Backward =>
      /* Report that we are in the backward traversal. */
      write("<<=")
      /* If we still have a parent, continue the backward traversal. If not,
       * we reached the root, we subtract one from the returns. When returns
       * hit zero, the traversal is complete and we may finish.*/
      parent.foreach(_ ! Tree.Backward)

  /* Report that the actor was created. */
  write("Created")

Note the use of relayAll in this example. This only works if the types allow sending letters from the parent to the child. In case of the FamilyTree where all types are the same for each actor and AcceptActor this works.

The only thing that is left to do is set it all in motion.

/* Main entry point for the demo. */
object Main :
  /* We must provide a default sender. */
  given Actor.Anonymous = Actor.Anonymous
  /* Create the root of the tree. */
  private val tree = new Tree("F0",None)

  def main(args: Array[String]): Unit =
    /* Create the all branches of the tree*/
    tree ! Tree.Create(2,3)
    /* Give it some time to complete */
    Thread.sleep(200)
    /* Send a message upwards. */
    tree ! Tree.Forward
    /* Give it some time to complete */
    Thread.sleep(200)

We kept the number children per level and the numbers of levels low to ensure a manageable output. See in Scastie that this results in:

Created F0
Created F0.F1
Created F0.F2
Created F0.F2.F1
Created F0.F2.F2
Created F0.F1.F1
Created F0.F1.F2
Created F0.F2.F1.F1
Created F0.F2.F2.F1
Created F0.F2.F2.F2
Created F0.F1.F1.F1
Created F0.F1.F1.F2
Created F0.F2.F1.F2
Created F0.F1.F2.F1
Created F0.F1.F2.F2
=>> F0
=>> F0.F2
=>> F0.F1
=>> F0.F2.F2
=>> F0.F2.F1
=>> F0.F1.F1
=>> F0.F1.F2
=>> F0.F2.F2.F1
=>> F0.F2.F1.F1
=>> F0.F2.F2.F2
=>> F0.F2.F1.F2
=>> F0.F1.F1.F1
=>> F0.F1.F1.F2
=>> F0.F1.F2.F2
=>> F0.F1.F2.F1
<<= F0.F2.F1
<<= F0.F2.F2
<<= F0.F1.F2
<<= F0.F1.F1
<<= F0.F2.F2
<<= F0.F2
<<= F0.F1
<<= F0.F1.F1
<<= F0
<<= F0.F2
<<= F0
<<= F0
<<= F0.F2.F1
<<= F0.F1.F2
<<= F0.F1
<<= F0
<<= F0.F2
<<= F0.F1
<<= F0.F2
<<= F0
<<= F0
<<= F0.F1
<<= F0
<<= F0

In the first stage all the actors are created, and each reports their existence by its family path (succession of names). In the second stage the Forward message travels down the tree until it reaches the end in every leaf. This subsequently generates an Backward message that is send upwards until it reaches the root of the tree. The number of times <<= F0 appears thus equals the number of leaves in the tree.

The Family Set

If you need different letters on each level of a family you cannot use FamilyTree and a special mixin is needed per level. These are:

  • FamilyRoot: For the actors the from the root of the family, which does not have a parent.
  • FamilyBranch: For the actors that have a parent, as well as some children
  • FamilyLeaf: For the actors that do not (may not) have children.

On each actor you can define your own Accept type and Letter type as with other actors. Sending from parent to child is then possible like sending messages between two unrelated actors: with type matching.

If you need to relay messages from the parent to one or more of its children, based on the name for example (so not directly to the instance), some preparations must be made. The relayed letters must be present the letter definitions of all children, and be explicitly defined in the definition of the mixin at the parent. In case we have only one level this would require the changes:

  • FamilyRoot => FamilyRoot(familyDefine)
  • FamilyLeaf => FamilyLeafRelayed

The parameter familyDefine: Define is of the type Define <: FamilyDefine, a bit like the AcceptDefine from the regular actors. Your Define should then contain definitions for these types:

  type FamilyAccept <: Actor
  type FamilyCommon <: FamilyAccept
  type MyFamilyLetter[Sender >: FamilyCommon <: FamilyAccept] <: Actor.Letter[Sender]

Note that, if you want to send such messages from an other actor to the parent first, these must of course also be part of the letter definitions of the parent. This is not automatically the case.

If you have FamilyBranch as well there are more flavors to choose from:

  • FamilyBranch => FamilyBranchRelay(familyDefine), FamilyBranchRelayed or FamilyBranchRelayRelayed(familyDefine)

depending on the situation regarding relaying messages and/or receiving relayed messages.

The children should all be the same actor type if relaying is used, and the parent can be different. But it is easier if parent and children actor types are equal as well, since you may than use the ActorDefine of the parent as FamilyDefine for the children. The FamilyCommon will be correctly defined.

Since i am not yet satisfied with the design on this point, and changes are to be expected, no examples will be presented here for now. Refer to the Scala documentation at those methods for the moment.

$\textcolor{red}{\mbox{\scriptsize{}PAGE UNDER CONSTRUCTION}}$

Internals of an actor

Clone this wiki locally