Creating an injectable TwitterServer

Lifecycle

If you haven’t already, take a look at the TwitterServer lifecycle documentation. You may also want to look at the util-app App lifecycle documentation as well since c.t.server.TwitterServer is an extension of util-app App and thus inherits the util-app App lifecycle.

Getting Started

To create an injectable c.t.server.TwitterServer, first depend on the inject-server library. We also recommend using Logback as your SLF4J implementation. E.g.,

with sbt:

"com.twitter" %% "inject-server" % "24.2.0",
"ch.qos.logback" % "logback-classic" % versions.logback,

For more information on logging with Finatra see: Introduction to Logging With Finatra.

Create a new class which extends c.t.inject.server.TwitterServer:

Basic Example

An example of a simple injectable TwitterServer:

import com.twitter.inject.server.TwitterServer

object MyServerMain extends MyServer

class MyServer extends TwitterServer {

  override protected def start(): Unit = {
    // It is important to remember to NOT BLOCK this method.
    ...
  }
}

This will use the inject framework to create an injectable TwitterServer. Finatra provides c.t.inject.server.TwitterServer as an injectable version of the TwitterServer c.t.server.TwitterServer trait.

Specifically, this allows for the use of dependency injection in a TwitterServer with support for Modules allowing for powerful feature testing of the server.

Advanced Example

import com.twitter.inject.server.TwitterServer

object MyTwitterServerMain extends MyTwitterServer

class MyTwitterServer
  extends TwitterServer {

  override val modules = Seq(
    MyModule1)

  override protected def setup(): Unit = {
    // Create/start a pub-sub component and add it to the list of Awaitables, e.g., await(component)
    // It is important to remember to NOT BLOCK this method
    ...
  }

  override protected def warmup(): Unit = {
    handle[MyWarmupHandler]()
  }

  @Lifecycle
  override protected def postWarmup(): Unit = {
    super.postWarmup()

    // It is important to remember to NOT BLOCK this method and to call SUPER.
    ...
  }

  override protected def start(): Unit = {
    // It is important to remember to NOT BLOCK this method
    ...
  }
}

Overriding Server Lifecycle Functions

You can hook into the server startup lifecycle by overriding @Lifecycle-annotated methods or provided lifecycle callbacks.

@Lifecycle-annotated Methods

  • postInjectorStartup - after creation and initialization of the Injector.

  • beforePostWarmup - this phase performs object promotion before the binding of any external interface (which typically means the server will start accepting traffic) occurs in postWarmup

  • postWarmup - the framework binds the external HTTP interface.

  • afterPostWarmup - signal the server is healthy, via Lifecycle.Warmup.warmupComplete.

Caution

When overriding a server @Lifecycle-annotated method you MUST first call super.lifecycleMethod() in your overridden implementation to ensure that the server correctly completes the startup process.

Lifecycle Callbacks

  • setup() - called at the end of the postInjectorStartup() phase.

  • warmup() - allows for user-defined server warmup.

  • start() - called at the end of the server main before awaiting on any Awaitables.

The main points of entry to the c.t.inject.server.TwitterServer are the lifecycle callbacks: #setup, #warmup, and #start.

TwitterServer#setup

The #setup() lifecycle callback method is executed at the end of the TwitterServer#postInjectorStartup @Lifecycle-annotated method (see: Startup Lifecycle). That is, after the creation of the Injector but before server warmup has been performed allowing for anything created or started in this callback to be used in warmup and for instances to be promoted to old gen during object promotion in the beforePostWarmup lifecycle phase.

Note: in the HttpServer and ThriftServer traits from finatra-http and finatra-thrift respectively, routing is configured in the postInjectorStartup lifecycle phase. However any logic in the #setup callback will executed after all installed modules have started (see: TwitterModule Lifecycle) and before HttpRouter or ThriftRouter configuration.

What Goes Here?

Any logic to execute before object promotion and before the server warmup is performed. This is thus before any external interface has been bound and thus before the server is announced as “healthy”.

Any exception thrown from this method will fail the server startup.

When overriding any lifecycle methods and callbacks, it is important to not perform any blocking operations as you will prevent the server from properly starting. If there is blocking work that must be done, it is strongly recommended that you perform this work in a FuturePool.

See the Finatra utility: c.t.finatra.utils.FuturePools for creating named pools.

TwitterServer#warmup

For detailed information see HTTP Server Warmup, or Thrift Server Warmup.

TwitterServer#start

Any logic to be run after the server is reported as healthy, bound to an external interface, and before awaiting on any Awaitables is placed in the #start() method. This is typically starting long live background processes, starting any processor that should only be started once the external interface has been successfully bound to port and is accepting traffic, or any other work that must be completed as part of server startup. See the Awaiting Awaitables section for more information.

What Goes Here?

Work to happen after the server is bound to any external port, has performed warmup, object promotion, and is announced as “healthy”.

Any exception thrown from this method will fail the server startup.

When overriding any lifecycle methods and callbacks, it is important to not perform any blocking operations in your override as you will prevent the server from properly starting. If there is blocking work that must be done, it is strongly recommended that you perform this work in a FuturePool.

See the Finatra utility: c.t.finatra.utils.FuturePools for creating named pools.

Awaiting Awaitables

If you have long-lived processes which your server starts that you want to ensure exit when the server exits or trigger the server to exit if the process exits, you should register them as an c.t.util.Awaitable using the c.t.inject.server.TwitterServer#await callback function.

The purpose of using this callback is to entangle all the Awaitables within your server such that if any of the Awaitables exit, the entire server process exits. For example, when starting a regular HTTP or Thrift server, you have two ListeningServers in process: the TwitterServer HTTP Admin Interface and the started external server. If you await (block) on one of the servers and not the other, you can get into a case where the server not being awaited exits but the process continues to wait on the other server to satisfy the blocking Awaitable and thus does not exit.

Why is this bad?

As an example, if you await on just the external interface, the TwitterServer HTTP Admin Interface may exit because of an error, causing health checking and metrics reporting to fail but your server process would remain running until killed. Conversely, if you await on just the HTTP Admin Interface the external server may exit but the admin interface continues to report itself as healthy.

Register Awaitables

The way to ensure that the exiting of a single Awaitable triggers exiting of any other Awaitable is to register each Awaitable with the server using the c.t.inject.server.TwitterServer#await callback function. As the last step of the server startup lifecycle, the server will entangle all given Awaitables.

Testing

For details see the Testing with Finatra section and the Finatra examples for detailed examples with tests.

More Information

For more information on the server lifecycle see the Application and Server Lifecycle section which contains details around the order of lifecycle events during startup and considerations during shutdown.