Field Notes

Guice and Play, part 3: eager doesn't mean at boot

Guice and Play, part 3: eager doesn't mean at boot

In part 1 I let Guice build the graph of my Play 3 template, and in part 2 I bound my first trait in a Module. Everything so far was cheap to create and had nothing to clean up. This part is about the first object that owns a real resource: a MongoDB client, with a connection pool and its own threads.

A dependency nobody was using

The template already had mongo-scala-driver 5.13.0 in build.sbt and a mongo { uri, database } block in application.conf, but no code used them. I added one class that owns the client:

@Singleton
class Mongo @Inject() (config: Configuration, lifecycle: ApplicationLifecycle) extends LazyLogging {

  private val client: MongoClient = MongoClient(config.get[String]("mongo.uri"))
  val database: MongoDatabase = client.getDatabase(config.get[String]("mongo.database"))

  logger.info(s"Mongo client created (instance ${System.identityHashCode(this)})")

  // Runs when the application stops, including every dev-mode reload.
  lifecycle.addStopHook { () =>
    logger.info(s"Closing Mongo client (instance ${System.identityHashCode(this)})")
    Future.successful(client.close())
  }
}

ApplicationLifecycle is just another constructor parameter. Play binds it, Guice passes it in, and the class registers its own cleanup. The Play docs add a warning I agree with: a component that registers a stop hook must be a singleton, otherwise every new instance adds another hook.

Play runs stop hooks in reverse order of creation. A component's dependencies were created before it, so they are still usable while its stop hook runs.

Scope and timing are two different settings

No class injects Mongo yet. With only @Singleton, Guice would never create it, because Guice creates instances only when something asks for them. So the binding goes in the module:

class Module extends AbstractModule {
  override def configure(): Unit = {
    // Nobody injects Mongo yet, so a plain @Singleton would never be built.
    bind(classOf[Mongo]).asEagerSingleton()
  }
}

That gave me a distinction I had not made before. @Singleton on the class says how many instances: one per injector. asEagerSingleton() on the binding says when: as soon as the injector is built, not on the first lookup. The stop hook says how long it lives: until the application stops.

What the tests showed

sbt test passed all six tests. The three GuiceGraphSpec cases from part 2 only ask for Clocky and Greeter. None of them requests Mongo, and still each one produced a pair of log lines:

Six log lines from sbt test: three pairs of Mongo client created and Closing Mongo client, each pair with its own instance hash

Each running(app) builds a new injector, so each test gets its own client. The stop hook closes it at the end of the test.

Each running(app) is one application and one injector. The eager binding created a client even though no test used it, and the stop hook closed it when running stopped the app. The three different identity hashes are the important detail: @Singleton means one per injector, not one per JVM.

The surprise: sbt run printed nothing

Then I ran sbt run, waited, and did not see "Mongo client created". My first thought was that asEagerSingleton() did not work.

It did. In dev mode Play starts only the HTTP server. The application, and so the injector, is built when the first request arrives. In Play's DevServerStart, each request asks the build whether the classes changed. If they did, Play stops the old application (stop hooks run) and loads a new one. The Play docs describe it from the other side: in dev mode, eager bindings are not fully initialized until the first request.

Timeline table: in dev mode nothing is created at sbt run, the client is created on the first request, and a code change closes the old client and creates a new one; in prod mode the client is created at process start and closed at shutdown

Eager means when the injector is built. In dev mode that is the first request, and again after every reload.

So after a code change and a refresh, the log shows "Closing" for the old instance, then "created" with a new identity hash. Without the stop hook, every reload would leave the previous client's pool and threads running in the same JVM.

Prod mode, and a Windows detour

In prod mode Play builds the application at startup, so eager singletons are created before the first request. I tried sbt runProd on Windows, and the generated start script failed immediately:

The input line is too long.
The syntax of the command is incorrect.

By default the start script passes every jar to java -cp, and with Play's dependencies that command is longer than cmd accepts. The fix is one plugin from sbt-native-packager, documented under long classpaths:

lazy val root = (project in file(".")).enablePlugins(PlayScala, LauncherJarPlugin)

LauncherJarPlugin generates a jar with no classes, only a manifest containing the main class and the classpath. The script then runs java -jar on that jar, so the command line stays short.

Eager does not mean connected

The comment in my module says that eager creation means "a bad config fails at startup". That is only partly true. A missing mongo.uri key throws when the class is built. But MongoClient(...) does not open a connection in the constructor; the driver connects in the background. A client that was created successfully does not prove that MongoDB is reachable. Real readiness checks are for a later part.

Compared with the cake pattern

With compile-time DI, a val in a components trait is also created when the components are created, and in dev mode that is also on the first request. Neither approach changes the timing.

The difference is where the cleanup lives. In my cake wiring, the code that creates a resource and the code that closes it are usually in different places. With Guice, Mongo receives ApplicationLifecycle and registers its own stop hook, so creation and teardown are in the same file.

Next up, part 4: replacing the wrapper class with a @Provides method, so code can inject MongoDatabase directly. MongoClient has no @Inject constructor, so Guice needs to be told how to build it.

Reference: Play 3.0.x, Scala dependency injection.

Keep reading