Creating Actor Networks in Akka
In the previous article we’ve implemented a standalone guessing game, but actors are built for much more complex problems such as concurrent communication over a network. A turn-based multiplayer game such as a card or tile game is the perfect example for that use case, and that’s what we’d like to achieve eventually. But first, we need to figure out how to allow actors to communicate with each other over a networking layer, and possibly with a high enough level of abstraction so that we can model our business rules without having to think about any low-level stuff.
Thankfully, Akka does this abstraction so well that the whole process is almost entirely transparent. Let’s see how!
Basic Example
If we look at the Actor Path section in the first article we’ll see that the actor path notation allows for references to remote actor systems through the use of akka.tcp
as the protocol prefix. For instance, a path such as akka.tcp://[email protected]:9000
refers to an actor system called Foo
that is running on host 192.168.1.14
through the port 9000
.
Building up on this, let’s create we have two actor systems called Foo
and Bar
, have them contain aptly-named singular actors, namely FooActor
and BarActor
, and let them communicate with each other.
Foo
Let’s name our first project as remote-foo
. First, create a folder called remote-foo
under a folder of your choice, and place a file called build.sbt
at its root folder. This is where we’ll declare our dependencies and set up build processes.
See also: https://github.com/ygunayer/remote-foo/tree/simple
remote-foo/build.sbt
1 | val akkaVersion = "2.5.0" |
Next, we’ll need to create both the main class that serves as the entry point, and also the FooActor
Note: Feel free to change the package name from com.yalingunayer.foo
to whatever you like. Just don’t forget to change the folder structure as well!
remote-foo/src/main/scala/com/yalingunayer/foo/FooActor.scala
1 | package com.yalingunayer.foo |
remote-foo/src/main/scala/com/yalingunayer/foo/Application.scala
1 | package com.yalingunayer.foo |
Nothing fancy, but here comes the important part. This is where we enable Akka’s remoting capabilities by telling it to instantiate remote references for actors.
Note: Don’t worry about the explicitly defined hostname and port number for now.
remote-foo/src/main/resources/application.conf
1 | akka { |
Bar
The second project, remote-bar
, will do the hard work (not really) of locating the FooActor
and sending it a message.
The build file is pretty much the same.
See also: https://github.com/ygunayer/remote-bar/tree/simple
remote-bar/build.sbt
1 | val akkaVersion = "2.5.0" |
remote-bar/src/main/scala/com/yalingunayer/BarActor.scala
1 | package com.yalingunayer.bar |
Another straightforward entry class.
remote-bar/src/main/scala/com/yalingunayer/Application.scala
1 | package com.yalingunayer.bar |
And a remoting configuration that’s exactly the same as Foo
‘s except for the port number.
remote-foo/src/main/resources/application.conf
1 | akka { |
Demonstration
Now that we’ve set up our projects, the only thing left to do is to actually run them.
First, foo
1 | $ cd remote-foo |
And next, bar
1 | $ cd remote-bar |
As soon as we run bar, foo
will also output the following line:
1 | Received a message: Oh, hi Mark! |
So there you go, our first remoting example!
Routing Example
One problem we had with our first example was that we had to specify the exact path to the FooActor
, which was completely arbitrary, and we had no way of scaling it.
As with any addressed message delivery problem, the most obvious solution to this is to implement a routing mechanism. Thankfully, Akka already has the concept of routers, so we don’t have to re-invent the wheel.
Routing means hierarchy, so in order to route incoming messages we’ll need a supervisor actor which will employ one of the following algorithms to route messages to its routees:
akka.routing.RoundRobinRoutingLogic
akka.routing.RandomRoutingLogic
akka.routing.SmallestMailboxRoutingLogic
akka.routing.BroadcastRoutingLogic
akka.routing.ScatterGatherFirstCompletedRoutingLogic
akka.routing.TailChoppingRoutingLogic
akka.routing.ConsistentHashingRoutingLogic
Among these the most suitable candidate for our example is a simple round-robin routing mechanism, so let’s implement it.
We can either configure our routers programmatically, or through a configuration file. We already have a configuration file, so we’ll simply use it.
Let’s update the application.conf
file on remote-foo
.
See also: https://github.com/ygunayer/remote-foo/tree/routing
remote-foo/src/main/resources/application.conf
1 | akka { |
Hooray, instant round-robin load balancing! The next thing we need to do is to inform Akka that we’re routing our messages at /foo
remote-foo/src/main/scala/com/yalingunayer/foo/Application.scala
1 | package com.yalingunayer.foo |
Notice how this time we’ve named our supervising router actor foo
because we expect it to be a singleton. The last thing to do is to update remote-bar
so that it sends a message or two to remote-foo
‘s /foo
endpoint.
See also: https://github.com/ygunayer/remote-bar/tree/routing
remote-bar/src/main/scala/com/yalingunayer/BarActor.scala
1 | package com.yalingunayer.bar |
That’s it! Here’s how things look when we run our actor systems.
First, foo
1 | $ cd remote-foo |
Next, bar
1 | $ cd remote-bar |
This time we sent two messages, so remote-foo
will print two outputs.
1 | Received a message: Oh, hi Mark! |
Hassle-free Application Linking via Docker
Another issue with our first two examples was the fact that we had to specify the exact IP address and port to our applications, which couples them with the configuration so strongly that we have no way of scaling them, nor can we deploy them easily. This is a perfect use case for Docker as it will allow us to simply use service links and not care about the port number thanks to the built-in private networking capabilities.
Disclaimer: This section assumes that the reader has at least some experience or familiarity with Docker and its concepts
Building
In order to Dockerize our apps, we’ll first have to build them into executable files so we can create Docker images that contain them. Like Maven or Gradle, sbt doesn’t have a native way of generating executables by default, so we’ll integrate a plugin to do that for us. To do that, simply create a file on the path project/plugins.sbt
with the following content for both of the projects:
remote-foo/project/plugins.sbt and remote-bar/project/plugins.sbt
1 | addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.2.0-M8") |
And then, place the statement enablePlugins(JavaAppPackaging)
at the 2nd line in the build.sbt
files of both projects. Both files should look like this:
remote-foo/build.sbt and remote-bar/build.sbt
1 | val akkaVersion = "2.5.0" |
We can now use the command sbt stage
, which will generate an executable version of our app under the path target/universal/stage/bin
.
Dockerized Example
Setting Up the Containers
Now that we have the executable versions of our apps, we can now create Docker images for both of them.
Let’s go with remote-foo
first, and create an extremely simple Dockerfile at its root folder. Note how this assumes that our executable app is placed at /app
, which we’ll link using docker-compose
.
remote-foo/Dockerfile
1 | FROM java:8-jdk |
And the other Dockerfile for the other project. Pretty much the same as remote-foo
‘s.
remote-bar/Dockerfile
1 | FROM java:8-jdk |
Next up, we’ll need a docker-compose
configuration so we can tie both apps together. Place a file called docker-compose.yml
at a folder that contains both projects.
docker-compose.yml
1 | version: '2' |
As you might already tell, this file simply tells Docker to mount the folder that contains the executables on the host at the path /app
on the guest, build both Dockerfile
s contained in the project folders, create a private network among the containers that expose their 47000 ports to each other, and then link remote-foo
to remote-bar
so it’s accessible.
Updating the Code
Before moving on to the final step, let’s make minor adjustments on our applications to reflect these changes.
- Change the
hostname
from127.0.0.1
toremote-foo
onremote-foo/src/main/resources/application.conf
- Change the
hostname
from127.0.0.1
toremote-foo
andport
from47001
to47000
onremote-bar/src/main/resources/application.conf
- Change the target URL from
akka.tcp://[email protected]:47000/user/foo
toakka.tcp://Foo@remote-foo:47000/user/foo
onBarActor
For the exact differences, see the relevant diff
entries on the GitHub repo: remote-foo, remote-bar
And for the final versions of both repos, visit them on GitHub:
remote-foo: https://github.com/ygunayer/remote-foo/tree/dockerize
remote-bar: https://github.com/ygunayer/remote-bar/tree/dockerize
Running the Containers
Once the file is ready, simply navigate to its containing folder and run docker-compose up -d
. Here’s a sample output:
1 | $ docker-compose up -d |
And that’s it! Not only has Docker built our images, it has also created and run the containers, so the apps have probably communicated with each other already. To validate the results, simply display the output of both containers:
Note: Refer to the last two lines of the previous output for the names of the containers that you’ll need to provide to docker logs
Here’s the output from remote-bar
1 | $ docker logs remoting_remote-foo_1 |
And here’s from remote-foo
1 | $ docker logs remoting_remote-foo_1 |
Conclusion
So there you go. We now have two actor systems running not only on separate processes, but even different (virtual) OSes, and we could easily place one system on a machine and the other on a different one and they’d still be able to communicate with each other.
But this is only the first step in creating our multiplayer game, and we’ll get to that in the next article. Stay tuned!