Notes about Play Framework and the ExecutionContext
In Scala, we talk a lot about non-blocking or asynchronous operations, and while using Play Framework, you are encouraged to use those, which forces you to deal with Future[T]
and its tightly coupled dependency, the ExecutionContext
.
While the Play Framework - Thread Pools best practices docs do a good job explaining how to use the thread pools and execution contexts, new people to Scala tend to follow the same mistakes.
Here I’m detailing the approach we follow at wiringbits, we hope you find it useful too.
As a summary:
- Avoid the global
ExecutionContext
. - Avoid using the default
ExecutionContext
for everything. - Avoid specific execution contexts that depend on akka, use a base trait instead.
- Ensure that your specific execution contexts are singletons.
- Ensure your tests use your specific execution contexts to avoid runtime errors.
Avoid the global ExecutionContext
It should be clear that the global execution context must not be used in any place, while it’s common to just add this line while experimenting, you shouldn’t it:
import scala.concurrent.ExecutionContext.Implicits.global
Instead, use the default play execution context, which can be injected easily on the class constructor (unless you stopped using Guice which is the default way to do Dependency Injection), like:
import javax.inject.Inject
import scala.concurrent.{ExecutionContext, Future}
class MyController @Inject() (implicit ec: ExecutionContext) { ... }
Avoid using the default ExecutionContext for everything
Making the ExecutionContext
dependency explicit has several advantages, what matters on this post is that it forces you to think about the operations your class/method is performing, when something may block the current thread, you are better using a typed execution context, like:
package net.wiringbits.executors
import javax.inject.{Inject, Singleton}
import akka.actor.ActorSystem
import play.api.libs.concurrent.CustomExecutionContext
import scala.concurrent.ExecutionContext
trait DatabaseExecutionContext extends ExecutionContext
object DatabaseExecutionContext {
@Singleton
class AkkaBased @Inject() (system: ActorSystem)
extends CustomExecutionContext(system, "database.dispatcher")
with DatabaseExecutionContext
}
Which requires you to update your application.conf
to load the guice module and define the database.dispatcher
thread pool, like:
play.modules.enabled += "net.wiringbits.modules.ExecutorsModule"
database.dispatcher {
executor = "thread-pool-executor"
throughput = 1
thread-pool-executor {
fixed-pool-size = ${fixedConnectionPool}
}
}
This prevents using the wrong execution context for blocking operations, in this case for the database operations.
Ensure that your specific execution contexts are singletons
Note that the custom context is marked as Singleton
, if you read the akka docs, you’ll understand, we need the same thread pool for every class:
/**
* Returns a dispatcher as specified in configuration. Please note that this
* method _may_ create and return a NEW dispatcher, _every_ call (depending on the `MessageDispatcherConfigurator`dispatcher config the id points to).
*/
Avoid specific execution contexts that depend on akka, use a base trait instead
As you saw, we use a base trait, which allow us to write tests without needing to bring akka to them, but, you need to add the guice module to specify it’s implementation, for example:
package net.wiringbits.modules
import com.google.inject.AbstractModule
import net.wiringbits.executors._
class ExecutorsModule extends AbstractModule {
override def configure(): Unit = {
bind(classOf[DatabaseExecutionContext]).to(classOf[DatabaseExecutionContext.AkkaBased]).asEagerSingleton()
}
}
Then, you can easily fake the typed contexts for your tests:
implicit val globalEC: ExecutionContext = scala.concurrent.ExecutionContext.global
implicit val databaseEC: DatabaseExecutionContext = new DatabaseExecutionContext {
override def execute(runnable: Runnable): Unit = globalEC.execute(runnable)
override def reportFailure(cause: Throwable): Unit = globalEC.reportFailure(cause)
}
Ensure your tests use you specific execution contexts to avoid runtime errors.
At last, as creating the custom execution context depends on the application.conf
file (due to calling CustomExecutionContext(system, "database.dispatcher")
), make sure that some of your tests use those specific contexts to catch runtime errors, otherwise, your application will likely fail to start due to the ConfigurationException
being thrown by akka.
More
This is the approach we use for our projects, feel free to check these examples:
The source of this post can be found here