Search code examples
scalaplayframeworkguiceopen-telemetrytrino

Force loading a class before HikariCP initiates a connection in Play Framework with Guice


I have a use case where I need to enforce one of my application class (a singleton service) is loaded before HikariCP initializes and creates connection to the database.

As HikariCP is initialized "automatically" by Play out-of-the-box, I don't know how to enforce it.

Basically, I have a class like the following:

@Singleton
class MyClassThatNeedToBeLoadedBeforeHikariCP() {

  // Setup some global state for OpenTelemetry

}

If I wanted to initialize it before another class of my own, I would use regular DI:

@Singleton
class OtherClassToLoadAfterMyClass() @Inject()(myClass: MyClassThatNeedToBeLoadedBeforeHikariCP) {
}

But this "other class" is actually HikariCP.

How to define a dependency (in terms of Guice) between HikariCP and a class of my own?


To give the complete context, I need to do this because I'm using a JDBC driver that tries to setup some global state as well and if initialized before my MyClassThatNeedToBeLoadedBeforeHikariCP class, then my class will fail to be initialized.

This is because under the hood, OpenTelemetry global state can only be defined once.

See related discussion in the issue tracked of the JDBC driver (Trino).

And to help reach visibility on this topic, here's the stack trace one might get if encountering the same issue:

Caused by: java.lang.IllegalStateException: GlobalOpenTelemetry.set has already been called. GlobalOpenTelemetry.set must be called only once before any calls to GlobalOpenTelemetry.get. If you are using the OpenTelemetrySdk, use OpenTelemetrySdkBuilder.buildAndRegisterGlobal instead. Previous invocation set to cause of this exception.
         at io.opentelemetry.api.GlobalOpenTelemetry.set(GlobalOpenTelemetry.java:104)
         at io.opentelemetry.sdk.OpenTelemetrySdkBuilder.buildAndRegisterGlobal(OpenTelemetrySdkBuilder.java:85)
         at com.myapp.metrics.setup.OTELService.<init>(OTELService.scala:41)
         ^^^^^^^^^ == MyClassThatNeedToBeLoadedBeforeHikariCP
         at com.myapp.metrics.setup.OTELService$$FastClassByGuice$$1270092a.newInstance(<generated>)
         at com.google.inject.internal.DefaultConstructionProxyFactory$FastClassProxy.newInstance(DefaultConstructionProxyFactory.java:89)
...
Caused by: java.lang.Throwable
         at io.opentelemetry.api.GlobalOpenTelemetry.set(GlobalOpenTelemetry.java:112)
         at io.opentelemetry.api.GlobalOpenTelemetry.get(GlobalOpenTelemetry.java:82)
         at io.trino.jdbc.NonRegisteringTrinoDriver.instrumentClient(NonRegisteringTrinoDriver.java:68)
         at io.trino.jdbc.NonRegisteringTrinoDriver.connect(NonRegisteringTrinoDriver.java:62)
         at com.zaxxer.hikari.util.DriverDataSource.getConnection(DriverDataSource.java:138)
         at com.zaxxer.hikari.pool.PoolBase.newConnection(PoolBase.java:358)

Solution

  • The solution is to:

    • disable the HikariCP module bundled with Play
    • create and enable a custom module to replace it, adding the dependency to the MyClassThatNeedToBeLoadedBeforeHikariCP class

    See relevant part of the Play documentation.


    In Play configuration file:

    play.modules.disabled += "play.api.db.HikariCPModule"
    play.modules.enabled += "com.myapp.modules.HikariCPModuleWithCustomDependency"
    

    And create the following module:

    class HikariCPModuleWithCustomDependency extends SimpleModule(
      bind[ConnectionPool].to[HikariCPConnectionPoolWithCustomDependency]
    )
    
    @Singleton
    class HikariCPConnectionPoolWithCustomDependency @Inject() (
        environment: Environment,
        @nowarn myClass: MyClassThatNeedToBeLoadedBeforeHikariCP
      )
      extends HikariCPConnectionPool(environment) {}