Play Framework 2.X and blocking database call

2020-05-24 19:55发布

问题:

I'm a little confused.

From the documentation:

Play default thread pool - This is the default thread pool in which all application code in Play Framework is executed, excluding some iteratees code. It is an Akka dispatcher, and can be configured by configuring Akka, described below. By default, it has one thread per processor.

Does it bring benefit to wrap a blocking database call in a Future, the call to the Future being itself wrapped by an async controller (returning it), in order to let the default thread pool handling other users requests?

It would just move the blocking code inside another thread (from a dedicated ExecutionContext), but leave the Action unblocked.

I came across this post but I'm not satisfied with the given answer.
Indeed, if I let the database call blocking within the default thread pool, wouldn't it potentially prevent handling other users requests that does not depend on database in the meantime?

Note: My database (Neo4j) hasn't an async driver.

回答1:

There are a few ways to handle blocking calls. I can't say which is best, as it would most certainly depend on specific use cases, and require a ton of benchmarking.

By default, Play handles requests using a thread pool with one thread per cpu core. So, if you're running your Play app on a quad-core cpu, for example, it will only be able to handle 4 concurrent requests if they're using blocking calls to the database. So yes, all other incoming requests will have to wait until one of the threads had been freed up.

The simplest solution is to increase the number of threads Play uses to process requests in the default thread pool (in application.conf):

play {
   akka {
     akka.loggers = ["akka.event.slf4j.Slf4jLogger"]
     loglevel = WARNING
     actor {
        default-dispatcher = {
           fork-join-executor {
             parallelism-min = 300
             parallelism-max = 300
           }
        }
     }
   }
}

The next option is the one you mention in your question--offloading blocking database calls to another ExecutionContext. You can configure a separate thread pool within application.conf like so:

database-io {
    fork-join-executor {
       parallelism-factor = 10.0
    }
}

This will create a 10 threads per cpu core in the pool called database-io, and can be accessed within Play like so:

val dbExecutor: ExecutionContext = Akka.system.dispatchers.lookup("database-io")

val something = Future(someBlockingCallToDb())(dbExecutor)

This will allow the default thread pool to handle more requests while it's waiting for the Future to complete. A third option would be to use an Actor to handle the database calls, but that's more complicated and beyond the scope of this question.

The bottom line is, yes, use a larger thread pool or a different ExecutionContext for blocking calls, as you never want to block in the default thread pool if you can help it.

This is all outlined in the Play Documentation for Thread Pools. (latest version)