Discovering an actor

Actor discovery is an alternative way to obtain an actor reference. The preferred way is still to incorporate actor references into the message protocol.

Akka provides the possibility to look up a single child actor by name (only exact match) with the following method:

def child(name: String): Option[ActorRef[Nothing]]

This returns a reference to a child actor if a child with such a name exists and is alive. Please note that because of the return type of this method, the result needs to be coerced to the proper type by the use of the narrow method of ActorRef

Another method that allows us to look up all of the children of an actor that are alive is as follows:

def children: Iterable[ActorRef[Nothing]]

The type of result is, again, a collection of ActorRefs with no particular type.

It is arguable that the lookup methods we have described here are of little use because of their basically untyped nature. Akka Typed offers a better alternative in the form of the receptionist.

The receptionist is a (cluster) singleton actor that is available on the actor system level and can be obtained from the ActorContext using the following call chain:

val receptionist: ActorRef[Receptionist.Command] = context.system.receptionist

The receptionist is just an actor of type [Receptionist.Command], so let's investigate the Receptionist.Command type to understand what it is capable of.

There are three concrete implementations of the abstract Command class: RegisterFind, and Subscribe.

Register is used for associating the given ActorRef with the provided ServiceKey. It is possible to register multiple references for the same key. The registration is automatically removed from the receptionist if the registered actor is stopped.

By providing an optional reference, it is possible to provide another actor, who should be notified if the service was successfully registered.

Find is a mechanism for asking the receptionist about all currently known registered actors for the given ServiceKey. The receptionist responds with a Set of known actor references (which are called services) that are registered to the given key, and a key itself wrapped in a Listing. Find can be used to implement one-time queries to the receptionist.

Subscribe is a way to implement push behavior for the receptionist. An actor can use subscribe to receive notifications about all added or removed services for some predefined key.

In our example, the Manager actor is used to provide a Boy with a reference to the seller actor. The Boy is supposed to communicate with the provided reference. In the previous chapter, we used untyped Akka's remote lookup to get this reference. In the typed environment, we will utilize the receptionist for this purpose.

This is how it is done.

First, the seller behavior needs to register itself with the receptionist at the moment it is initialized:

import akka.actor.typed.receptionist.Receptionist._

val
SellerKey = ServiceKey[SellByList]("GrocerySeller")

val seller = Behaviors.setup { ctx ⇒
ctx.system.receptionist ! Register(SellerKey, ctx.self)
Behaviors.receiveMessage[SellByList] {
case SellByList(list, toWhom) ⇒
import list._
toWhom ! Groceries(eggs, flour, sugar, chocolate)
Behaviors.same
}
}

The Shop defines the SellerKey that will be used by the actor to register as a service and by the service clients to look up the seller's reference.

Next, we introduce a new type of behavior constructor—Behaviors.setup. setup is a behavior factory. It takes the behavior constructor as a by-name parameter and creates the behavior at the moment the actor is started (as opposed to the moment the behavior is constructed). We need to use this factory for two reasons:

  • We need our actor to be instantiated so that we can access its context
  • We want our Seller to register itself exactly once

After registering the Seller with the receptionist, the real behavior is constructed. The behavior itself is just accepting the SellByList messages and responding with the Groceries that are to be provided to the toWhom reference.

On the opposite side of the receptionist, the Manager actor needs to look up the Seller and use its reference to guide the Boy:

def idle: Behavior[Command] = Behaviors.setup { context =>
implicit val lookupTimeout: Timeout = 1.second
context.ask(context.system.receptionist)(Find(Shop.SellerKey)) {
case Success(listing: Listing) =>
listing
.serviceInstances(Shop.SellerKey)
.headOption
.map { seller =>
LookupSeller(seller)
}
.getOrElse {
NoSeller
}
case Failure(_) =>
NoSeller
}

There is quite a bit going on here. Once again, we're using setup to define the behavior.

Looking up actors is an asynchronous operation, and in this case, we utilize the ask pattern to keep the code concise. Ask needs to know how long it is allowed to wait for the answer, so, in the second line, we define a lookupTimeout.

Then, we call the ask method that's available in the actor context and provide a reference of a receptionist as an actor to be asked. The second parameter is the receptionist's Find command, which is given a seller key. Normally, the Find command takes a second parameter that defines a receiver for the response, but as it is used quite often together with ask, there is a special constructor that allows for the nice syntax we are using in this snippet.

The case literal, which comes next, defines a transformation that must be applied to the response before actually sending it back to the asking actor. It deconstructs and converts the receptionist's response so that it is either a NoSeller or just one OneSeller.

Next, we have to deal with the converted response by defining a behavior which is returned as a result of this lengthy factory method:

Behaviors.receiveMessagePartial {
case OneSeller(seller) =>
val boy = context.spawn(Boy.goShopping, "Boy")
boy ! GoShopping(shoppingList, seller, context.self)
waitingForGroceries
case NoSeller =>
context.log.error("Seller could not be found")
idle
}

In the current manager's behavior, we only expect a small subset of all of the possible messages to arrive. We're using receiveMessagePartial to avoid compiler warnings for unhandled message types.

In this case, if there is no seller, we can use the log that's available in the actor's context to report this condition and return the current behavior.

In this case, if there is a Seller available, we instantiate a Boy and use it to transfer a shoppingList to this seller. Note how we used context.self as a second parameter for the GoShopping message. By doing this, we're making it possible to use the provided manager's reference to persuade the Seller to send groceries directly to the Manager, and then the Boy can immediately stop itself after sending the message:

object Boy {
final case class GoShopping(shoppingList: ShoppingList,
seller: ActorRef[SellByList],
manager: ActorRef[Manager.Command])

val goShopping = Behaviors.receiveMessage[GoShopping] {
case GoShopping(shoppingList, seller, manager) =>
seller ! SellByList(shoppingList, manager)
Behaviors.stopped
}
}

Here, we have seen how the GoShopping command prohibits us from interchanging actor references for the seller and manager, as this could easily happen in the case of untyped Akka.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.224.59.192