§Integrating with Akka
Akka uses the Actor Model to raise the abstraction level and provide a better platform to build correct concurrent and scalable applications. For fault-tolerance it adopts the ‘Let it crash’ model, which has been used with great success in the telecoms industry to build applications that self-heal - systems that never stop. Actors also provide the abstraction for transparent distribution and the basis for truly scalable and fault-tolerant applications.
§The application actor system
Akka can work with several containers called actor systems. An actor system manages the resources it is configured to use in order to run the actors which it contains.
A Play application defines a special actor system to be used by the application. This actor system follows the application life-cycle and restarts automatically when the application restarts.
§Writing actors
To start using Akka, you need to write an actor. Below is a simple actor that simply says hello to whoever asks it to.
import akka.actor._
object HelloActor {
def props = Props[HelloActor]
case class SayHello(name: String)
}
class HelloActor extends Actor {
import HelloActor._
def receive = {
case SayHello(name: String) =>
sender() ! "Hello, " + name
}
}
This actor follows a few Akka conventions:
- The messages it sends/receives, or its protocol, are defined on its companion object
- It also defines a
props
method on its companion object that returns the props for creating it
§Creating and using actors
To create and/or use an actor, you need an ActorSystem
. This can be obtained by declaring a dependency on an ActorSystem, like so:
import play.api.mvc._
import akka.actor._
import javax.inject._
import actors.HelloActor
@Singleton
class Application @Inject() (system: ActorSystem,
cc:ControllerComponents)
extends AbstractController(cc) {
val helloActor = system.actorOf(HelloActor.props, "hello-actor")
//...
}
The actorOf
method is used to create a new actor. Notice that we’ve declared this controller to be a singleton. This is necessary since we are creating the actor and storing a reference to it, if the controller was not scoped as singleton, this would mean a new actor would be created every time the controller was created, which would ultimate throw an exception because you can’t have two actors in the same system with the same name.
§Asking things of actors
The most basic thing that you can do with an actor is send it a message. When you send a message to an actor, there is no response, it’s fire and forget. This is also known as the tell pattern.
In a web application however, the tell pattern is often not useful, since HTTP is a protocol that has requests and responses. In this case, it is much more likely that you will want to use the ask pattern. The ask pattern returns a Future
, which you can then map to your own result type.
Below is an example of using our HelloActor
with the ask pattern:
import scala.concurrent.duration._
import akka.pattern.ask
implicit val timeout: Timeout = 5.seconds
def sayHello(name: String) = Action.async {
(helloActor ? SayHello(name)).mapTo[String].map { message =>
Ok(message)
}
}
A few things to notice:
- The ask pattern needs to be imported, and then this provides a
?
operator on the actor. - The return type of the ask is a
Future[Any]
, usually the first thing you will want to do after asking actor is map that to the type you are expecting, using themapTo
method. - An implicit timeout is needed in scope - the ask pattern must have a timeout. If the actor takes longer than that to respond, the returned future will be completed with a timeout error.
§Dependency injecting actors
If you prefer, you can have Guice instantiate your actors and bind actor refs to them for your controllers and components to depend on.
For example, if you wanted to have an actor that depended on the Play configuration, you might do this:
import akka.actor._
import javax.inject._
import play.api.Configuration
object ConfiguredActor {
case object GetConfig
}
class ConfiguredActor @Inject() (configuration: Configuration) extends Actor {
import ConfiguredActor._
val config = configuration.getOptional[String]("my.config").getOrElse("none")
def receive = {
case GetConfig =>
sender() ! config
}
}
Play provides some helpers to help providing actor bindings. These allow the actor itself to be dependency injected, and allows the actor ref for the actor to be injected into other components. To bind an actor using these helpers, create a module as described in the dependency injection documentation, then mix in the AkkaGuiceSupport
trait and use the bindActor
method to bind the actor:
import com.google.inject.AbstractModule
import play.api.libs.concurrent.AkkaGuiceSupport
import actors.ConfiguredActor
class MyModule extends AbstractModule with AkkaGuiceSupport {
def configure = {
bindActor[ConfiguredActor]("configured-actor")
}
}
This actor will both be named configured-actor
, and will also be qualified with the configured-actor
name for injection. You can now depend on the actor in your controllers and other components:
import play.api.mvc._
import akka.actor._
import akka.pattern.ask
import akka.util.Timeout
import javax.inject._
import actors.ConfiguredActor._
import scala.concurrent.ExecutionContext
import scala.concurrent.duration._
@Singleton
class Application @Inject() (@Named("configured-actor") configuredActor: ActorRef, components: ControllerComponents)
(implicit ec: ExecutionContext) extends AbstractController(components) {
implicit val timeout: Timeout = 5.seconds
def getConfig = Action.async {
(configuredActor ? GetConfig).mapTo[String].map { message =>
Ok(message)
}
}
}
§Dependency injecting child actors
The above is good for injecting root actors, but many of the actors you create will be child actors that are not bound to the lifecycle of the Play app, and may have additional state passed to them.
In order to assist in dependency injecting child actors, Play utilises Guice’s AssistedInject support.
Let’s say you have the following actor, which depends configuration to be injected, plus a key:
import akka.actor._
import javax.inject._
import com.google.inject.assistedinject.Assisted
import play.api.Configuration
object ConfiguredChildActor {
case object GetConfig
trait Factory {
def apply(key: String): Actor
}
}
class ConfiguredChildActor @Inject() (configuration: Configuration,
@Assisted key: String) extends Actor {
import ConfiguredChildActor._
val config = configuration.getOptional[String](key).getOrElse("none")
def receive = {
case GetConfig =>
sender() ! config
}
}
Note that the key
parameter is declared to be @Assisted
, this tells that it’s going to be manually provided.
We’ve also defined a Factory
trait, this takes the key
, and returns an Actor
. We won’t implement this, Guice will do that for us, providing an implementation that not only passes our key
parameter, but also locates the Configuration
dependency and injects that. Since the trait just returns an Actor
, when testing this actor we can inject a factory that returns any actor, for example this allows us to inject a mocked child actor, instead of the actual one.
Now, the actor that depends on this can extend InjectedActorSupport
, and it can depend on the factory we created:
import akka.actor._
import javax.inject._
import play.api.libs.concurrent.InjectedActorSupport
object ParentActor {
case class GetChild(key: String)
}
class ParentActor @Inject() (
childFactory: ConfiguredChildActor.Factory
) extends Actor with InjectedActorSupport {
import ParentActor._
def receive = {
case GetChild(key: String) =>
val child: ActorRef = injectedChild(childFactory(key), key)
sender() ! child
}
}
It uses the injectedChild
to create and get a reference to the child actor, passing in the key. The second parameter (key
in this example) will be used as the child actor’s name.
Finally, we need to bind our actors. In our module, we use the bindActorFactory
method to bind the parent actor, and also bind the child factory to the child implementation:
import com.google.inject.AbstractModule
import play.api.libs.concurrent.AkkaGuiceSupport
import actors._
class MyModule extends AbstractModule with AkkaGuiceSupport {
def configure = {
bindActor[ParentActor]("parent-actor")
bindActorFactory[ConfiguredChildActor, ConfiguredChildActor.Factory]
}
}
This will get Guice to automatically bind an instance of ConfiguredChildActor.Factory
, which will provide an instance of Configuration
to ConfiguredChildActor
when it’s instantiated.
§Configuration
The default actor system configuration is read from the Play application configuration file. For example, to configure the default dispatcher of the application actor system, add these lines to the conf/application.conf
file:
akka.actor.default-dispatcher.fork-join-executor.parallelism-max = 64
akka.actor.debug.receive = on
For Akka logging configuration, see configuring logging.
§Changing configuration prefix
In case you want to use the akka.*
settings for another Akka actor system, you can tell Play to load its Akka settings from another location.
play.akka.config = "my-akka"
Now settings will be read from the my-akka
prefix instead of the akka
prefix.
my-akka.actor.default-dispatcher.fork-join-executor.pool-size-max = 64
my-akka.actor.debug.receive = on
§Built-in actor system name
By default the name of the Play actor system is application
. You can change this via an entry in the conf/application.conf
:
play.akka.actor-system = "custom-name"
Note: This feature is useful if you want to put your play application ActorSystem in an Akka cluster.
§Using your own Actor system
While we recommend you use the built in actor system, as it sets up everything such as the correct classloader, lifecycle hooks, etc, there is nothing stopping you from using your own actor system. It is important however to ensure you do the following:
- Register a stop hook to shut the actor system down when Play shuts down
- Pass in the correct classloader from the Play Environment otherwise Akka won’t be able to find your applications classes
- Ensure that either you change the location that Play reads it’s akka configuration from using
play.akka.config
, or that you don’t read your akka configuration from the defaultakka
config, as this will cause problems such as when the systems try to bind to the same remote ports
§Akka Coordinated Shutdown
Play terminates the built-in Akka actor system using Akka’s Coordinated Shutdown. By default Play will run the Coordinated Shutdown using only the last phase where the actor system is terminated. You can override that settings using:
# Runs Akka CoordinatedShutdown for Play's actor system
# from phase "service-stop". See Akka docs on Coordinated
# Shutdown for other phase names. In Play, this defaults
# to "actor-system-terminate"
play.akka.run-cs-from-phase = "service-stop"
If you are using extra actor systems in your Play Application and tests, make sure they are all terminated and feel free to migrate your termination code from the traditional actorSystem.terminate()
to the new Coordinated Shutdown
§Akka Cluster
You can make your Play application join an existing Akka Cluster. In that case it is recommended that you leave the cluster gracefully. Play ships with Akka’s Coordinated Shutdown since Play 2.6 which can take care of that graceful leave but it is disabled.
If you are joining an Akka Cluster with your Play application and you already have custom Cluster Leave code it is recommended that you replace it and enable Akka’s handling using play.akka.run-cs-from-phase
described above and set it to run from, at least, before-cluster-shutdown
phase. See Akka docs for more details.
§Updating Akka version
If you want to use a newer version of Akka, one that is not used by Play yet, you can add the following to your build.sbt
file:
// The newer Akka version you want to use.
val akkaVersion = "2.5.16"
// Akka dependencies used by Play
libraryDependencies ++= Seq(
"com.typesafe.akka" %% "akka-actor" % akkaVersion,
"com.typesafe.akka" %% "akka-stream" % akkaVersion,
"com.typesafe.akka" %% "akka-slf4j" % akkaVersion,
// Only if you are using Akka Testkit
"com.typesafe.akka" %% "akka-testkit" % akkaVersion
)
Of course, other Akka artifacts can be added transitively. Use sbt-dependency-graph to better inspect your build and check which ones you need to add explicitly.
If you also want to update Akka HTTP, you should also add its dependencies explicitly:
// The newer Akka HTTP version you want to use.
val akkaHTTPVersion = "10.1.4"
// Akka HTTP dependencies used by Play
libraryDependencies ++= Seq(
"com.typesafe.akka" %% "akka-http-core" % akkaHTTPVersion,
// Add this one if you are using HTTP/2
"com.typesafe.akka" %% "akka-http2-support" % akkaHTTPVersion
)
Note: When doing such updates, keep in mind that you need to follow Akka’s Binary Compatibility Rules. And if you are manually adding other Akka artifacts, remember to keep the version of all the Akka artifacts consistent since mixed versioning is not allowed.
Note: When resolving dependencies, sbt will get the newest one declared for this project or added transitively. It means that if Play depends on a newer Akka (or Akka HTTP) version than the one you are declaring, Play version wins. See more details about how sbt does evictions here.