Scala

Akka Typed Actors: Exploring the receptionist pattern

In this article we’ll explore another of Akka-Typed patterns. This time we’ll show you how you can use the receptionist patterns. This is the third and last article on a series on Akka-Typed. The other two articles can also be found on this site. If you don’t know anything about Akka-Typed yet, it’s a good idea to first read the “First steps with Akka Typed article”:

The idea behind the Receptionist pattern is very simple and is explained by the ScalaDoc:

/**
 * A Receptionist is an entry point into an Actor hierarchy where select Actors
 * publish their identity together with the protocols that they implement. Other
 * Actors need only know the Receptionist’s identity in order to be able to use
 * the services of the registered Actors.
 */
object Receptionist {
...

So basically a client only needs to know how to access the receptionist and from there it can access any other actor which is registered at the Receptionist. So let’s first create a couple of actors that we register with the receptionist. For this we define two very simple actors:

 /**
   * Simple service and protocol. Does nothing special, just print out
   * the received message.
   */
  object FirstService {

    sealed trait FirstServiceMsg
    final case class FirstServiceMsg1(msg: String) extends FirstServiceMsg

    val behavior = Static[FirstServiceMsg] {
      case msg:FirstServiceMsg => println("First Service Receiver: " + msg)
    }
  }

  /**
   * Another simple service and protocol. Does nothing special, just print out
   * the received message.
   */
  object SecondService {

    sealed trait SecondServiceMsg
    final case class SecondServiceMsg1(msg: String) extends SecondServiceMsg

    val behavior = Static[SecondServiceMsg] {
      case msg:SecondServiceMsg => println("Second Service Receiver: " + msg)
    }
  }

Nothing special, just two services that print out each message that they receive. Besides these two services we also create a service which will act as a client to these two services:

  object SenderService {

    sealed trait SenderMsg
    final case class registerAddresses(firstServices: Set[ActorRef[FirstServiceMsg]], secondServices: Set[ActorRef[SecondServiceMsg]]) extends SenderMsg
    final case class sendMessage(msg: String) extends SenderMsg

    val behavior = Total[SenderMsg] {
      case registerAddresses(firstRefs, secondRefs) => {
        Static {
          case sendMessage(msg) => {
            firstRefs.foreach(_ ! FirstServiceMsg1(msg))
            secondRefs.foreach(_ ! SecondServiceMsg1(msg))
          }
        }
      }
      case _ => Same
    }
  }

As you can see, also a rather basic actor, which changes behavior once it receives a registerAddresses message. After it has changed its behavior it’ll act on sendMessage messages to call the registered services. Now how do we tie all this together?

For this we create another actor which kicks off this scenario for us:

  val scenario2 = {
    Full[Unit] {
      case Sig(ctx, PreStart) => {
        val receptionist = ctx.spawn(Props(Receptionist.behavior), "receptionist");

        // register three actors that can work with the FirstServiceMsg protocol
        val service1a = ctx.spawn(Props(FirstService.behavior), "service1a")
        val service1b = ctx.spawn(Props(FirstService.behavior), "service1b")
        val service1c = ctx.spawn(Props(FirstService.behavior), "service1c")

        // register three actors that can work with the SecondServiceMsg protocol
        val service2a = ctx.spawn(Props(SecondService.behavior), "service2a")
        val service2b = ctx.spawn(Props(SecondService.behavior), "service2b")
        val service2c = ctx.spawn(Props(SecondService.behavior), "service2c")

        // and the actor that will eventually send messages
        val sender = ctx.spawn(Props(SenderService.behavior),"sender")

        // define the service keys we'll use for registering
        val serviceKey1 = new ServiceKey[FirstServiceMsg] {}
        val serviceKey2 = new ServiceKey[SecondServiceMsg] {}

        // register the services with the receptionise
        val responseWrapperFirst = ctx.spawnAdapter[Registered[FirstServiceMsg]] {case _ =>}
        val responseWrapperSecond = ctx.spawnAdapter[Registered[SecondServiceMsg]] {case _ =>}

        receptionist ! Register(serviceKey1, service1a)(responseWrapperFirst)
        receptionist ! Register(serviceKey1, service1b)(responseWrapperFirst)
        receptionist ! Register(serviceKey1, service1c)(responseWrapperFirst)

        receptionist ! Register(serviceKey2, service2a)(responseWrapperSecond)
        receptionist ! Register(serviceKey2, service2b)(responseWrapperSecond)
        receptionist ! Register(serviceKey2, service2c)(responseWrapperSecond)

        // as a client we can now ask the receptionist to give us the actor references for services
        // that implement a specific protocol. We pass the result to the sender service. Ugly way
        // for now, but more to demonstrate how it works.
        val getListingWrapper = ctx.spawnAdapter[Listing[FirstServiceMsg]] {
          case firsts : Listing[FirstServiceMsg] => {
            val secondWrapper = ctx.spawnAdapter[Listing[SecondServiceMsg]] {
              case seconds : Listing[SecondServiceMsg] => {
                sender ! registerAddresses(firsts.addresses, seconds.addresses)
              }
            }
            receptionist ! Find[SecondServiceMsg](serviceKey2)(secondWrapper)
          }
        }
        // get message from the first lookup, and pass it to the adapter, which will look up the
        // second
        receptionist ! Find[FirstServiceMsg](serviceKey1)(getListingWrapper)

        // now wait a bit to make sure that through the receptionist we get a list of target actorrefs
        Thread.sleep(200)

        // these are sent to all the registered service implementations
        sender ! sendMessage("Hello1")
        sender ! sendMessage("Hello2")

        Same
      }
    }
  }

That is a lot of code but it is really easy to see what is happening. The first thing we do is use ctx.spawn to create a set of child actors of the type we just discussed. Once we’ve defined the actors we need to register our FirstService and SecondService actors with the receptionist. For this we need to send a message like this:

/**
   * Associate the given  with the given . Multiple
   * registrations can be made for the same key. Unregistration is implied by
   * the end of the referenced Actor’s lifecycle.
   */
  final case class Register[T](key: ServiceKey[T], address: ActorRef[T])(val replyTo: ActorRef[Registered[T]]) extends Command

In our example we do that in the following way:

        // define the service keys we'll use for registering
        val serviceKey1 = new ServiceKey[FirstServiceMsg] {}
        val serviceKey2 = new ServiceKey[SecondServiceMsg] {}

        // register the services with the receptionise
        val responseWrapperFirst = ctx.spawnAdapter[Registered[FirstServiceMsg]] {case _ =>}
        val responseWrapperSecond = ctx.spawnAdapter[Registered[SecondServiceMsg]] {case _ =>}

        receptionist ! Register(serviceKey1, service1a)(responseWrapperFirst)
        receptionist ! Register(serviceKey1, service1b)(responseWrapperFirst)
        receptionist ! Register(serviceKey1, service1c)(responseWrapperFirst)

        receptionist ! Register(serviceKey2, service2a)(responseWrapperSecond)
        receptionist ! Register(serviceKey2, service2b)(responseWrapperSecond)
        receptionist ! Register(serviceKey2, service2c)(responseWrapperSecond)

As you can see we create an adapter to handle the replyTo parameter of the Register message (where we just ignore the response in this case). We then use these adapters to register our service actors with the receptionist. At this point we’ve registered our services with the receptionist and can now use the Find message:

/**
   * Query the Receptionist for a list of all Actors implementing the given
   * protocol.
   */
  final case class Find[T](key: ServiceKey[T])(val replyTo: ActorRef[Listing[T]]) extends Command

.. to get a list of registered actors for a specific type:

        // as a client we can now ask the receptionist to give us the actor references for services
        // that implement a specific protocol. We pass the result to the sender service. Ugly way
        // for now, but more to demonstrate how it works.
        val getListingWrapper = ctx.spawnAdapter[Listing[FirstServiceMsg]] {
          case firsts : Listing[FirstServiceMsg] => {
            val secondWrapper = ctx.spawnAdapter[Listing[SecondServiceMsg]] {
              case seconds : Listing[SecondServiceMsg] => {
                sender ! registerAddresses(firsts.addresses, seconds.addresses)
              }
            }
            receptionist ! Find[SecondServiceMsg](serviceKey2)(secondWrapper)
          }
        }
        // get message from the first lookup, and pass it to the adapter, which will look up the
        // second
        receptionist ! Find[FirstServiceMsg](serviceKey1)(getListingWrapper)

Important to note here is line 99:

 sender ! registerAddresses(firsts.addresses, seconds.addresses)

Here we sent the information about the registered services to our actor which serves as a client.

Now all that is left to do is send a sendMessage message to the sender, and it should be sent to all the registered services:

       // these are sent to all the registered service implementations
        sender ! sendMessage("Hello1")
        sender ! sendMessage("Hello2")

When we now run this, the output looks like this:

First Service Receiver: FirstServiceMsg1(Hello1)
First Service Receiver: FirstServiceMsg1(Hello1)
First Service Receiver: FirstServiceMsg1(Hello1)
Second Service Receiver: SecondServiceMsg1(Hello1)
First Service Receiver: FirstServiceMsg1(Hello2)
First Service Receiver: FirstServiceMsg1(Hello2)
Second Service Receiver: SecondServiceMsg1(Hello1)
First Service Receiver: FirstServiceMsg1(Hello2)
Second Service Receiver: SecondServiceMsg1(Hello1)
Second Service Receiver: SecondServiceMsg1(Hello2)
Second Service Receiver: SecondServiceMsg1(Hello2)
Second Service Receiver: SecondServiceMsg1(Hello2)

Easy right!

Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button