diff --git a/CHANGELOG.md b/CHANGELOG.md index e786529837..f7a639966d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ All notable changes to this project will be documented in this file. Note that ` ### Added +* inject-server: Add `c.t.inject.server.TwitterServer#setup` lifecycle callback method. This is + run at the end of the `postInjectorStartup` phase as is primarily intended as a way for + servers to start pub-sub components on which the server depends. User should prefer this method + over overidding the `c.t.inject.server.TwitterServer#postWarmup` @Lifecycle-annotated method as + the callback does not require a call its super implementation for the server to correctly start + and is ideally less error-prone to use. ``PHAB_ID=D135827`` + * inject-app: Add `c.t.inject.annotations.Flags#named` for getting an implementation of an `@Flag` annotation. This is useful when trying to get or bind an instance of an `@Flag` annotated type. ``PHAB_ID=D140831`` diff --git a/doc/src/sphinx/user-guide/getting-started/lifecycle.rst b/doc/src/sphinx/user-guide/getting-started/lifecycle.rst index ea944bcd68..b02c096bcd 100644 --- a/doc/src/sphinx/user-guide/getting-started/lifecycle.rst +++ b/doc/src/sphinx/user-guide/getting-started/lifecycle.rst @@ -9,10 +9,12 @@ and provides methods which can be implemented for running or starting core logic This is done for several reasons: -- To ensure that flag parsing and module installation to build the object graph is done in the correct - order such that the injector is properly configured for use before a user attempts attempts to access flags. +- To ensure that `flag <./flags.html>`__ parsing and `module <./modules.html>`__ installation to + build the object graph is done in the correct order such that the injector is properly configured + before a user attempts attempts to access flags. -- Ensure that object promotion and garbage collection is properly handled *before* accepting traffic to a server. +- Ensure that object promotion and garbage collection is properly handled *before* accepting traffic + to a server. - Expose any external interface *before* reporting a server is "healthy". Otherwise a server may report it is healthy before binding to a port — which may fail. Depending on how monitoring is @@ -21,16 +23,19 @@ This is done for several reasons: on some frequency) it could be some interval before the server is recognized as unhealthy when in fact it did not start properly as it could not bind to a port. -Thus you do not have access to the `app `__ -or `server `__ `main` method. Instead, any -logic should be contained in overriding an ``@Lifecycle``-annotated method or in the app or server -callbacks |c.t.inject.app.App#run|_ or |c.t.inject.server.TwitterServer#start|_ methods, respectively. +Thus you do not have access to the `App `__ +or `TwitterServer `__ +`main` method. Instead, any logic should be contained in overriding an ``@Lifecycle``-annotated +method or in the application or server callbacks. .. caution:: - If you override an ``@Lifecycle``-annotated method you **MUST** first call `super()` in the method to ensure that framework lifecycle events happen accordingly. + If you override an ``@Lifecycle``-annotated method you **MUST** first call + `super.lifecycleMethod()` in your override to ensure that framework lifecycle events happen + accordingly. - With all lifecycle methods, it is important to **not performing any blocking operations** as you will prevent the server from starting. +See the `Creating an injectable App <../app/index.html>`__ and +`Creating an injectable TwitterServer <../twitter-server/index.html>`__ sections for more information. Startup ------- @@ -42,60 +47,175 @@ At a high-level, the start-up lifecycle of a Finatra server looks like: Shutdown -------- -Upon *graceful* shutdown, all registered `onExit {...}` blocks are executed. See `c.t.app.App#exits `__). +Upon *graceful* shutdown of an application or a server, all registered `onExit`, `closeOnExit`, and +`closeOnExitLast` blocks are executed. See +`c.t.app.App#exits `__ +and `c.t.app.App#lastExits `_. -This includes closing the `TwitterServer `__ `HTTP Admin Interface `__, firing the `TwitterModuleLifecycle#singletonShutdown `__ -on all installed modules, and for extensions of the `HttpServer <../http/server.html>`__ or `ThriftServer <../thrift/server.html>`__ traits closing any external interfaces. +For a server, this includes closing the `TwitterServer `__ +`HTTP Admin Interface `__ +and shutting down and closing all installed modules. For extensions of the +`HttpServer <../http/server.html>`__ or `ThriftServer <../thrift/server.html>`__ traits this also +includes closing any external interfaces. .. admonition:: Important - Note that the order of execution for all registered `onExit {...}` blocks is not guaranteed. Thus it is, up to implementors to enforce any desired ordering. + Note that the order of execution for all registered `onExit` and `closeOnExit` blocks is not + guaranteed as they are executed on graceful shutdown roughly in parallel. Thus it is up to + implementors to enforce any desired ordering. - For example, you have code which is reading from a queue (via a "listener"), transforming the data, and then publishing (via a "publisher") to another queue. - When main application is exiting you most likely want to close the "listener" first to ensure that you transform and publish all available data before closing the "publisher". +For example, you have code which is reading from a queue (via a "subscriber"), transforming the +data, and then publishing (via a "publisher") to another queue. When the main application is exiting +you most likely want to close the "subscriber" first to ensure that you transform and publish all +available data before closing the "publisher". +Assuming, that both objects are a |c.t.util.Closable|_ type, a simple way to close them would be: - Assuming, that the `close()` method of both returns `Future[Unit]`, e.g. like `c.t.util.Closable `__, - a way of doing this could be: +.. code:: scala - .. code:: Scala + closeOnExit(subscriber) + closeOnExit(publisher) - onExit { - Await.result(listener.close()) - Await.result(publisher.close()) - } +However, the "subscriber" and the "publisher" would close roughly in parallel +which could lead to data inconsistencies in your server if the "subscriber" is still reading before +the "publisher" has closed. - In the example code above, we simply await on the close of the "listener" first and then the "publisher" thus ensuring that the "listener" - will close before the "publisher". +Ordering `onExit` and `closeOnExit` functions? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - This is instead of registering separate `onExit {...}` blocks for each: +Assuming, that the `#close()` method of both returns `Future[Unit]`, e.g. like a |c.t.util.Closable|_, +a way of doing this could be: - .. code:: Scala +.. code:: scala - onExit { - Await.result(listener.close()) - } + onExit { + Await.result(subscriber.close(defaultCloseGracePeriod)) + Await.result(publisher.close(defaultCloseGracePeriod)) + } - onExit { - Await.result(publisher.close()) - } +where the `defaultCloseGracePeriod` is the `c.t.app.App#defaultCloseGracePeriod `__ +function. - Which in this example, would possibly close the "publisher" before the "listener" meaning that the server would still be reading data but unable to publish it. +In the above example we simply await on the `#close()` of the "subscriber" first and then the +`#close()` of the "publisher" thus ensuring that the "subscriber" will close before the "publisher". + +However, we are not providing a timeout to the `Await.result`, which we should ideally do as +well since we do not want to accidentally block our server shutdown if the `defaultCloseGracePeriod` +is set to something high or infinite (e.g., `Time.Top `__). + +But if we don't know the configured value of the `defaultCloseGracePeriod` this makes things +complicated. We could just hardcode a value for the Await, or not use the `defaultCloseGracePeriod`: + +.. code:: scala + + onExit { + Await.result(subscriber.close(defaultCloseGracePeriod), 5.seconds) + Await.result(publisher.close(defaultCloseGracePeriod), 5.seconds) + } + + ... + + onExit { + Await.result(subscriber.close(4.seconds), 5.seconds) + Await.result(publisher.close(4.seconds), 5.seconds) + } + +However, this is obviously not ideal and there is an easier way. You can enforce the ordering of closing Closables +by using `closeOnExitLast`. + +A |c.t.util.Closable|_ passed to `closeOnExitLast` will be closed *after* all `onExit` and +`closeOnExit` functions are executed. E.g., + +.. code:: scala + + closeOnExit(subscriber) + closeOnExitLast(publisher) + +In this code the "publisher" is guaranteed be closed **after** the "subscriber". + +.. note:: All the exit functions: `onExit`, `closeOnExit`, and `closeOnExitLast` use the + `defaultCloseGracePeriod` as their close "deadline" and will raise a `TimeoutException` if + all the `exits` (collected `onExit`, `closeOnExit` functions) do not close within the deadline. + And if the `lastExits` (collected `closeOnExitLast` functions) do not close within the deadline. + +If you have multiple |c.t.util.Closable|_ objects you want to close in parallel and one you want to +close after all the others, you could do: + +.. code:: scala + + closeOnExit(subscriberA) + closeOnExit(subscriberB) + closeOnExit(subscriberC) + closeOnExitLast(publisher) + +The "publisher" is guaranteed be closed **after** the closing of "subscriberA", "subscriberB", and +"subscriberC". + +What to do if you don't have a |c.t.util.Closable|_? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can simply use the `onExit` block to perform any shutdown logic, or you can wrap a function in +a |c.t.util.Closable|_ to be passed to `closeOnExit` or `closeOnExitLast`. + +For example: + +.. code:: scala + + onExit { + DatabaseConnection.drain() + Await.result(someFutureOperation, 2.seconds) + } + + closeOnExit { + Closable.make { deadline => + prepWork.start() + anotherFutureOperation + } + } + + closeOnExitLast { + Closable.make { deadline => + queue.blockingStop(deadline) + Future.Unit + } + } + +You can also wrap multiple functions in a Closable: + +.. code:: scala + + closeOnExit { + Closable.make { deadline => + database.drain() + fileCleanUp.do() + pushData(deadline) + Future.Unit + } + } + +Again the code in `onExit` and `closeOnExit` will be run in parallel and guaranteed to close +before the functions in `closeOnExitLast`. + +.. note:: Multiple `closeOnExitLast` Closables will be closed in parallel with each other but + **after** all `onExit` and `closeOnExit` functions have closed. Modules ------- -Modules provide hooks into the Lifecycle as well that allow instances being provided to the object graph to be plugged into the overall application or server lifecycle. See the `Module Lifecycle <../getting-started/modules.html#module-lifecycle>`__ section for more information. +Modules provide hooks into the Lifecycle as well that allow instances being provided to the object +graph to be plugged into the overall application or server lifecycle. See the +`Module Lifecycle <../getting-started/modules.html#module-lifecycle>`__ section for more information. More Information ---------------- -As noted in the diagram in the `Startup <#startup>`__ section there can be a non-trivial lifecycle especially in the case of a `TwitterServer `__. -For more information on how to create an injectable `c.t.app.App `__ or a `c.t.server.TwitterServer `__ -see the `Creating an injectable App <../app/index.html>`__ and `Creating an injectable TwitterServer <../twitter_server/index.html>`__ sections. +As noted in the diagram in the `Startup <#startup>`__ section the lifecycle or an application can be +non-trivial -- especially in the case of a `TwitterServer `__. -.. |c.t.inject.app.App#run| replace:: ``c.t.inject.app.App#run`` -.. _c.t.inject.app.App#run: ../app/index.html#app-run +For more information on how to create an injectable `c.t.app.App `__ +or a `c.t.server.TwitterServer `__ +see the `Creating an injectable App <../app/index.html>`__ and +`Creating an injectable TwitterServer <../twitter-server/index.html>`__ sections. -.. |c.t.inject.server.TwitterServer#start| replace:: ``c.t.inject.server.TwitterServer#start`` -.. _c.t.inject.server.TwitterServer#start: ../twitter_server/index.html#twitterserver-start +.. |c.t.util.Closable| replace:: `c.t.util.Closable` +.. _c.t.util.Closable: https://github.com/twitter/util/blob/develop/util-core/src/main/scala/com/twitter/util/Closable.scala diff --git a/doc/src/sphinx/user-guide/twitter-server/index.rst b/doc/src/sphinx/user-guide/twitter-server/index.rst index 6158bd96dc..aed9c404cd 100644 --- a/doc/src/sphinx/user-guide/twitter-server/index.rst +++ b/doc/src/sphinx/user-guide/twitter-server/index.rst @@ -1,121 +1,197 @@ +.. _injectable_twitter_server: + Creating an injectable |TwitterServer|_ ======================================= -To create an injectable |c.t.server.TwitterServer|_, first depend on the `inject-server` library. Then -use the `inject framework <../getting-started/framework.html#inject>`__ to create an injectable -TwitterServer. Finatra provides an injectable version of the |c.t.server.TwitterServer|_ trait: -|c.t.inject.server.TwitterServer|_. +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., -Extending the |c.t.inject.server.TwitterServer|_ trait creates an injectable |TwitterServer|_. +with sbt: -This allows for the use of `dependency injection <../getting-started/basics.html#dependency-injection>`__ -in a |TwitterServer|_ with support for `modules <../getting-started/modules.html>`__ which allows -for `powerful feature testing <../testing/index.html#types-of-tests>`__ of the server. +.. parsed-literal:: -Example -------- + "com.twitter" %% "inject-server" % "\ |release|\ ", + "ch.qos.logback" % "logback-classic" % versions.logback, + +For more information on logging with Finatra see: +`Introduction to Logging With Finatra <../logging/index.html#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|_: .. code:: scala - import com.twitter.inject.Logging - import com.twitter.inject.server.TwitterServer + import com.twitter.inject.server.TwitterServer + + object MyServerMain extends MyServer - object MyTwitterServerMain extends MyTwitterServer + class MyServer extends TwitterServer { - class MyTwitterServer - extends TwitterServer - with Logging { + override protected def start(): Unit = { + // It is important to remember to NOT BLOCK this method. + ... + } + } - override val modules = Seq( - MyModule1) +This will use the `inject framework <../getting-started/framework.html#inject>`__ to create an +injectable `TwitterServer`. Finatra provides |c.t.inject.server.TwitterServer|_ as an injectable +version of the `TwitterServer `__ +|c.t.server.TwitterServer|_ trait. - @Lifecycle - override protected def postWarmup(): Unit = { - super.postWarmup() +Specifically, this allows for the use of `dependency injection <../getting-started/basics.html#dependency-injection>`__ +in a |TwitterServer|_ with support for `Modules <../getting-started/modules.html>`__ allowing for +`powerful feature testing <../testing/index.html#types-of-tests>`__ of the server. - // Bind any external interfaces here, register them to be closed on server exit, and add them - // to the list of awaitables that the server will block on by call the this.await(Awaitable*) method. +Advanced Example +---------------- - // It is important to remember to NOT BLOCK this method. - } +.. code:: scala - override protected def start(): Unit = { - // Any additional server startup logic to perform after the server is reported healthy goes here. + import com.twitter.inject.Logging + import com.twitter.inject.server.TwitterServer - // It is important to remember to NOT BLOCK this method. - } - } + object MyTwitterServerMain extends MyTwitterServer -The two points of entry to the |c.t.inject.server.TwitterServer|_ are the ``#postWarmup`` and ``#start`` methods. + class MyTwitterServer + extends TwitterServer + with Logging { -`TwitterServer#postWarmup` --------------------------- + override val modules = Seq( + MyModule1) -If you look at the `Startup Lifecycle <../getting-started/lifecycle.html#startup>`__ you will see that ``postWarmup`` is executed -after server warmup has been performed and thus after `object promotion and initial garbage collection <../getting-started/lifecycle.html#application-and-server-lifecycle>`__, -but before reporting that the server is healthy (via the `HTTP Admin Interface `__ -``/health`` endpoint). + 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 + ... + } -In the |HttpServer|_ and |ThriftServer|_ traits the framework will bind any external interfaces and -perform announcing of the server in `postWarmup` ``@Lifecycle`` method. External interface binding and -announcing is done at this point in the lifecycle such that if binding the external interface to a -port fails, we do not report the server as healthy in the -`HTTP Admin Interface `__. + override protected def warmup(): Unit = { + handle[MyWarmupHandler]() + } -.. note:: + @Lifecycle + override protected def postWarmup(): Unit = { + super.postWarmup() - Put any logic to happen before the server is announced as "healthy" in the ``postWarmup`` lifecycle method. Typically, this - is any binding of an external interface but could be any logic that must be done to present the server as "healthy". + // It is important to remember to NOT BLOCK this method and to call SUPER. + ... + } - Remember, that when overriding any ``@Lifecycle``-annotated method you **MUST** first call `super()` in the method to ensure - the framework lifecycle events happen accordingly. + override protected def start(): Unit = { + // It is important to remember to NOT BLOCK this method + ... + } + } - As with all lifecycle methods, it is important to **not perform any blocking operations** as you will prevent the server - from 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: |FuturePools|_ for creating named pools. +Overriding Server Lifecycle Functions +------------------------------------- -Any exception thrown from this method will fail the server startup. +You can hook into the server startup lifecycle by overriding ``@Lifecycle``-annotated methods or +provided lifecycle callbacks. -`TwitterServer#start` +``@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` --------------------- -Any logic to be run after the server is reported as healthy, bound to an external interface and fully started is placed in the -``#start`` method. This is typically starting long live background processes, starting a processor for incoming data from an -external interface (e.g., it 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. +The `#setup()` lifecycle callback method is executed at the end of the +`TwitterServer#postInjectorStartup` ``@Lifecycle``-annotated method +(see: `Startup Lifecycle <../getting-started/lifecycle.html#startup>`__). 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 <../getting-started/modules.html#module-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: |FuturePools|_ for creating named pools. + +`TwitterServer#warmup` +---------------------- -.. note:: +For detailed information see `HTTP Server Warmup <../http/warmup.html>`__, or +`Thrift Server Warmup <../thrift/warmup.html>`__. + +`TwitterServer#start` +--------------------- - Put any work to happen after the server is bound to an external port and announced as healthy in the ``start`` lifecycle method. +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 aspart of server startup. - Remember, that when overriding any ``@Lifecycle``-annotated method you **MUST** first call `super()` in the method to ensure - the framework lifecycle events happen accordingly. +What Goes Here? +^^^^^^^^^^^^^^^ - As with all lifecycle methods, it is important to **not perform any blocking operations** as you will prevent the server - from 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: |FuturePools|_ for creating named pools. +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. -.. important:: +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 `__. - Users should prefer using the |HttpServer|_ or |ThriftServer|_ traits from |finatra-http|_ and - |finatra-thrift|_ respectively, for serving HTTP or Thrift external interfaces over overriding - lifecycle methods. +See the Finatra utility: |FuturePools|_ for creating named pools. Testing ------- -For details see the `Testing with Finatra <../testing/index.html>`__ section and the Finatra `examples `__ for detailed examples with tests. +For details see the `Testing with Finatra <../testing/index.html>`__ 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 <../getting-started/lifecycle.html>`__ section -which contains details around the order of lifecycle events during `startup <../getting-started/lifecycle.html#startup>`__ and -considerations during `shutdown <../getting-started/lifecycle.html#shutdown>`__. +For more information on the server lifecycle see the `Application and Server Lifecycle <../getting-started/lifecycle.html>`__ +section which contains details around the order of lifecycle events during `startup <../getting-started/lifecycle.html#startup>`__ +and considerations during `shutdown <../getting-started/lifecycle.html#shutdown>`__. .. |c.t.inject.server.TwitterServer| replace:: ``c.t.inject.server.TwitterServer`` .. _c.t.inject.server.TwitterServer: https://github.com/twitter/finatra/blob/develop/inject/inject-server/src/main/scala/com/twitter/inject/server/TwitterServer.scala diff --git a/inject/inject-app/src/main/scala/com/twitter/inject/app/App.scala b/inject/inject-app/src/main/scala/com/twitter/inject/app/App.scala index 78f108a932..f3738b457a 100644 --- a/inject/inject-app/src/main/scala/com/twitter/inject/app/App.scala +++ b/inject/inject-app/src/main/scala/com/twitter/inject/app/App.scala @@ -150,7 +150,7 @@ trait App extends com.twitter.app.App with Logging { } /** ONLY INTENDED FOR USE BY THE FRAMEWORK. */ - protected[inject] def loadModules() = { + protected[inject] def loadModules(): InstalledModules = { InstalledModules.create( flags = flag.getAll(includeGlobal = false).toSeq, modules = requiredModules, diff --git a/inject/inject-server/src/main/scala/com/twitter/inject/server/Awaiter.scala b/inject/inject-server/src/main/scala/com/twitter/inject/server/Awaiter.scala new file mode 100644 index 0000000000..0701bb0c5b --- /dev/null +++ b/inject/inject-server/src/main/scala/com/twitter/inject/server/Awaiter.scala @@ -0,0 +1,30 @@ +package com.twitter.inject.server + +import com.twitter.finagle.util.DefaultTimer +import com.twitter.util.{Await, Awaitable, Duration, Future} +import java.util.concurrent.CountDownLatch + +private[server] object Awaiter { + + /** + * Awaits for ''any'' [[Awaitable]] to be ready (exit) and will then exit. This is + * different than [[Await.all]] which only exits once ''all'' Awaitables are ready. + * @param awaitables the list of [[Awaitable]]s to await. + * @param period the interval on which to check each [[Awaitable.isReady]] + */ + def any( + awaitables: Iterable[Awaitable[_]], + period: Duration + ): Future[Unit] = { + // exit if any Awaitable is ready + val latch = new CountDownLatch(1) + val task = DefaultTimer.schedule(period) { + if (awaitables.exists(Await.isReady)) + latch.countDown() + } + + // we don't set a timeout because it should already be ready + latch.await() + task.close() + } +} diff --git a/inject/inject-server/src/main/scala/com/twitter/inject/server/TwitterServer.scala b/inject/inject-server/src/main/scala/com/twitter/inject/server/TwitterServer.scala index 8a40f48896..94455b9959 100644 --- a/inject/inject-server/src/main/scala/com/twitter/inject/server/TwitterServer.scala +++ b/inject/inject-server/src/main/scala/com/twitter/inject/server/TwitterServer.scala @@ -3,7 +3,6 @@ package com.twitter.inject.server import com.google.inject.Module import com.twitter.conversions.time._ import com.twitter.finagle.client.ClientRegistry -import com.twitter.finagle.util.DefaultTimer import com.twitter.inject.Logging import com.twitter.inject.annotations.Lifecycle import com.twitter.inject.app.App @@ -11,24 +10,66 @@ import com.twitter.inject.modules.StatsReceiverModule import com.twitter.inject.utils.Handler import com.twitter.server.Lifecycle.Warmup import com.twitter.server.internal.FinagleBuildRevision -import com.twitter.util.{Await, Awaitable, Duration} -import java.util.concurrent.{ConcurrentLinkedQueue, CountDownLatch} +import com.twitter.util.{Await, Awaitable} +import java.util.concurrent.ConcurrentLinkedQueue import scala.collection.JavaConverters._ /** AbstractTwitterServer for usage from Java */ abstract class AbstractTwitterServer extends TwitterServer /** - * A [[com.twitter.server.TwitterServer]] that supports injection and [[com.twitter.inject.TwitterModule]] modules. + * A [[com.twitter.server.TwitterServer]] that supports injection and [[com.twitter.inject.TwitterModule]] + * modules. * - * To use, override the appropriate @Lifecycle and callback method(s). Make sure when overriding @Lifecycle methods - * to call the super implementation, otherwise critical lifecycle set-up may not occur causing your server to either - * function improperly or outright fail. + * To use, override the appropriate @Lifecycle and callback method(s). Make sure when overriding + * {{{@Lifecycle}}} methods to call the super implementation, otherwise critical lifecycle set-up may not + * occur causing your server to either function improperly or outright fail. * - * Typically, you will only need to interact with the following methods: + * If you are extending this trait, to implement your server, override the start() function, e.g., * - * postWarmup -- create and bind any external interface(s). See [[com.twitter.inject.app.App#postWarmup]] - * start -- callback executed after the injector is created and all @Lifecycle methods have completed. + * {{{ + * import com.twitter.inject.server.TwitterServer + * + * object MyServerMain extends MyServer + * + * class MyServer extends TwitterServer { + * override protected def start(): Unit = { + * // YOUR CODE HERE + * + * await(someAwaitable) + * } + * } + * }}} + * + * Note, you do not need to await on the `adminHttpServer` as this is done for you by the framework. + * + * Server Lifecycle: + * +-------------------------------------------------------------------------+ + * | Life Cycle Method | Ancillary Function(s) | + * +-------------------------------------------------------------------------+ + * | loadModules() | | + * +-------------------------------------------------------------------------+ + * | modules.postInjectorStartup() | foreach.singletonStartup() | + * +-------------------------------------------------------------------------+ + * | postInjectorStartup() | resolveFinagleClientsOnStartup(), | + * | | FinagleBuildRevision.register(), | + * | | setup() | + * +-------------------------------------------------------------------------+ + * | warmup() | | + * +-------------------------------------------------------------------------+ + * | beforePostWarmup() | LifeCycle.Warmup.prebindWarmup() | + * +-------------------------------------------------------------------------+ + * | postWarmup() (binds ext ports)| disable or announce admin server | + * +-------------------------------------------------------------------------+ + * | afterpostwarmup() | LifeCycle.Warmup.warmupComplete() | + * +-------------------------------------------------------------------------+ + * | setAppStarted() | | + * +-------------------------------------------------------------------------+ + * | run() | start() | + * +-------------------------------------------------------------------------+ + * | Await on awaitables | + * +-------------------------------------------------------------------------+ + * @see [[https://twitter.github.io/finatra/user-guide/twitter-server/index.html Creating an Injectable TwitterServer]] */ trait TwitterServer extends App @@ -46,7 +87,6 @@ trait TwitterServer /* Mutable State */ private[inject] val awaitables: ConcurrentLinkedQueue[Awaitable[_]] = new ConcurrentLinkedQueue() - private[this] val CheckDuration: Duration = 1.second premain { awaitables.add(adminHttpServer) @@ -85,7 +125,7 @@ trait TwitterServer } protected def await(awaitables: Awaitable[_]*): Unit = { - awaitables foreach await + awaitables.foreach(await) } /** @@ -100,7 +140,32 @@ trait TwitterServer } /** - * Callback method executed after the injector is created and all + * Callback method which is executed specifically in the `postInjectorStartup` lifecycle + * phase of this server. + * + * This is AFTER the injector is created but BEFORE server warmup has been performed. + * + * This method is thus suitable for starting and awaiting on PubSub publishers or subscribers. + * + * The server is NOT signaled to be started until AFTER this method has executed + * thus it is imperative that this method is NOT BLOCKED as it will cause the server to not + * complete startup. + * + * This method can be used to start long-lived processes that run in + * separate threads from the main() thread. It is expected that you manage + * these threads manually, e.g., by using a [[com.twitter.util.FuturePool]]. + * + * If you override this method to instantiate any [[com.twitter.util.Awaitable]] it is expected + * that you add the [[com.twitter.util.Awaitable]] to the list of `Awaitables` using the + * [[await[T <: Awaitable[_]](awaitable: T): Unit]] function if you want the server to exit + * when the [[com.twitter.util.Awaitable]] exits. + * + * Any exceptions thrown in this method will result in the server exiting. + */ + protected def setup(): Unit = {} + + /** + * Callback method which is executed after the injector is created and all * lifecycle methods have fully completed. It is NOT expected that * you block in this method as you will prevent completion * of the server lifecycle. @@ -115,27 +180,15 @@ trait TwitterServer * * Any exceptions thrown in this method will result in the server exiting. */ - @Lifecycle protected def start(): Unit = {} /* Overrides */ override final def main(): Unit = { - super.main() // Call inject.App.main() to create injector + super.main() // Call inject.App.main() to create Injector info("Startup complete, server ready.") - - // exit if any of the awaitables is ready - val latch = new CountDownLatch(1) - val awaits = awaitables.asScala - val task = DefaultTimer.schedule(CheckDuration) { - if (awaits.exists(Await.isReady)) - latch.countDown() - } - - // we don't set a timeout because it should already be ready - latch.await() - task.close() + Awaiter.any(awaitables.asScala, period = 1.second) } /** @@ -145,26 +198,49 @@ trait TwitterServer start() } + /** + * After creation of the Injector. Before any other lifecycle methods. + * + * @note You MUST call `super.postInjectorStartup()` in any overridden definition of this + * method. Failure to do so may cause your server to not completely startup. + * + * @note It is NOT expected that you block in this method as you will prevent completion + * of the server lifecycle. + */ @Lifecycle override protected def postInjectorStartup(): Unit = { super.postInjectorStartup() if (resolveFinagleClientsOnStartup) { info("Resolving Finagle clients before warmup") + /* This PURPOSELY BLOCKS on a Future that will complete when Finagle client resolution is + completed. We purposely await on client resolution here as we want to BLOCK server startup + on the client resolution for clients to be fully resolved in time for the warmup phase. It + is not expected that overrides of this method perform any other blocking that will prevent + server startup from completing. */ Await.ready { - ClientRegistry.expAllRegisteredClientsResolved() onSuccess { clients => + ClientRegistry.expAllRegisteredClientsResolved().onSuccess { clients => info("Done resolving clients: " + clients.mkString("[", ", ", "]") + ".") } } } FinagleBuildRevision.register(injector) + + // run any setup logic + setup() } /** * After warmup but before accepting traffic promote to old gen * (which triggers gc). * + * @note You MUST call `super.beforePostWarmup()` in any overridden definition of this + * method. Failure to do so may cause your server to not completely startup. + * + * @note It is NOT expected that you block in this method as you will prevent completion + * of the server lifecycle. + * * @see [[com.twitter.server.Lifecycle.Warmup#prebindWarmup]] * @see [[com.twitter.inject.app.App#beforePostWarmup]] */ @@ -182,7 +258,10 @@ trait TwitterServer * [[com.twitter.finagle.ListeningServer]]) to the list of Awaitables using the * [[await[T <: Awaitable[_]](awaitable: T): Unit]] function. * - * It is NOT expected that you block in this method as you will prevent completion + * @note You MUST call `super.postWarmup()` in any overridden definition of this + * method. Failure to do so may cause your server to not completely startup. + * + * @note It is NOT expected that you block in this method as you will prevent completion * of the server lifecycle. */ @Lifecycle @@ -202,6 +281,12 @@ trait TwitterServer * After postWarmup, all external servers have been started, and we can now * enable our health endpoint. * + * @note You MUST call `super.afterPostWarmup()` in any overridden definition of this + * method. Failure to do so may cause your server to not completely startup. + * + * @note It is NOT expected that you block in this method as you will prevent completion + * of the server lifecycle. + * * @see [[com.twitter.server.Lifecycle.Warmup#warmupComplete]] * @see [[com.twitter.inject.app.App#afterPostwarmup]] */