In Scala we often use
Future to deal with concurrency, especially when working with the Play! Framework. To operate,
Future needs an implicit
ExecutionContext, which manages the concurrency. Fortunately, if you forget one, the compiler is very helpful with its error message:
[error] /path/to/some/File.scala:7:16: Cannot find an implicit ExecutionContext. You might pass [error] an (implicit ec: ExecutionContext) parameter to your method [error] or import scala.concurrent.ExecutionContext.Implicits.global. [error] println(fut.map(_ + 1)) [error] ^
This might prompt a developer to, I don’t know, import
scala.concurrent.ExecutionContext.Implicits.global. If he does, the error goes away and all seems well.
But all is not well. The global
ExecutionContext that Scala provides, which is configurable and backed by a work-stealing thread pool, will be adequate in many cases, but not all. In fact, the Play! Framework defines its own implicit
play.api.libs.concurrent.Execution.defaultContext, also accessible implicitly through
play.api.libs.concurrent.Execution.Implicits.defaultContext, which is a very different thing. In Play! applications, it makes sense to use it instead of Scala’s global one. That means you need to be careful to import the right one; if you decide to use Play!’s
ExecutionContext, you don’t want to import Scala’s by mistake. This is a bad thing, because then you have two different
ExecutionContexts competing for the same resources. The compiler’s helpfully intended suggestion to import the global
ExecutionContext is quite dangerous here!
Fortunately, it’s quite easy to add a rule to Scalastyle that forbids the import of
scala.concurrent.ExecutionContext.Implicits.global. This solves much of the problem, but not all of it, because of course there’s other ways you can still (accidentally or not) use the global
ExecutionContext. For instance, you could declare an
implicit val ec = scala.concurrent.ExecutionContext.Implicits.global in your class. Also, there’s still the non-implicit alias
scala.concurrent.ExecutionContext.global that you could use. But doing that assumes a level of intent that goes beyond blindly following the compiler’s suggestion, and it should be dealt with easily enough in a code review. So if we use Scalastyle and do proper code reviews, we’re done, right?
In my previous project, our team wasn’t aware of the difference between Scala’s
ExecutionContext and Play!’s
ExecutionContext. This led to some developers importing one, and others the other
ExecutionContext. Code reviews don’t matter if reviewers don’t know what to look for.
When we finally did figure it out, it was an easy thing to globally search and replace one
ExecutionContext import with another, but upon reflection, importing an implicit global
ExecutionContext seems like a strange thing to begin with. An
ExecutionContext is a dependency of a class, and should be injected as such. That way it becomes much easier to change the details of your application’s
ExecutionContext later, for instance when performance issues come to light after running in production for a month and you need to make a tweak.
However, whatever your chosen dependency injection method is (using a framework like Guice or MacWire, or doing it manually), it’s an awful lot of work to refactor a large codebase from global imports to constructor parameters. Therefore, in my current project we decided to be very deliberate about
ExecutionContext from the beginning so we wouldn’t have to do a big rewrite later on.
We used Scalastyle to forbid the import of both Play!’s
ExecutionContext and the global one. Of course we made an exception for the place where the actual wiring takes place. But we felt that didn’t go far enough, as it’s still very easy to get hold of the global
ExecutionContext even with the import restrictions in place, either through
scala.concurrent.ExecutionContext.global (which refer to the same
ExecutionContext under the hood). We wanted something stronger.
In the end, we found ArchUnit, a tool that lets you unit-test your architecture using a fluent and very readable interface. We settled on the following ScalaTest code:
This test test checks every class file in the application for references to the method
global in either
ExecutionContext’s companion object or
ArchUnit is designed with Java in mind, not Scala, so we had to make a few adjustments. For instance, in
global is defined as a
lazy val. Under the hood, that gets compiled into a method, so we have to use ArchUnit’s
A very nice thing about ArchUnit is the fact that you can override its default error messages (which are already pretty good) with your own messages using the
as method. Another nice thing: with
because, you can specify a reason for the ArchUnit rule, which is also included in the error message.
Of course there are also disadvantages to this approach. First, ArchUnit will slow down your test suite. It can cache things to make subsequent rules much faster, but if you only have one rule (like we do at the time of writing), that doesn’t help. Also, it’s a heavy tool to use for (arguably?) little benefit, as Scalastyle can get you to 90% of where you need to be.
But we originally chose Scala partly because its tools and culture are very focused on letting the compiler and the tools do as much of the work as possible. We use its type system to prevent as many bugs as possible, and complement that with tests to check for things that the compiler can’t. Confusion around
ExecutionContext has bitten me hard before, so for us it was worth it to invest in a tool that helps us be more deliberate. In that sense, ArchUnit fits our team very well, and we’ll be looking out for more ways to leverage it in —pardon the pun— future.