The life cycle of an application

An application's life cycle has two states: running and stopped. These are times when the state of the application changes. At times, we need to perform some operations right before or after a state change has occurred or is about to occur.

Play applications use a Netty server. For this, a class with the same name is used. It is defined as follows:

class NettyServer(appProvider: ApplicationProvider, port: Option[Int], sslPort: Option[Int] = None, address: String = "0.0.0.0", val mode: Mode.Mode = Mode.Prod) extends Server with ServerWithStop { … }

This class is responsible for binding or bootstrapping the application to the server.

The ApplicationProvider trait is defined as follows:

trait ApplicationProvider {
  def path: File
  def get: Try[Application]
  def handleWebCommand(requestHeader: play.api.mvc.RequestHeader): Option[Result] = None
}

An implementation of ApplicationProvider must create and initialize an application. Currently, there are three different implementations of ApplicationProvider. They are as follows:

  • StaticApplication: This is to be used in the production mode (the mode where code changes do not affect an already running application).
  • ReloadableApplication: This is to be used in the development mode (this is a mode where continuous compilation is enabled so that developers can see the impact of changes in an application as and when they are saved, if the application is up and running).
  • TestApplication: This is to be used in the testing mode (the mode where a fake application is started through the tests).

StaticApplication and ReloadableApplication both initialize a DefaultApplication. StaticApplication is used in the production mode and is defined as follows:

class StaticApplication(applicationPath: File) extends ApplicationProvider {

  val application = new DefaultApplication(applicationPath, this.getClass.getClassLoader, None, Mode.Prod)

  Play.start(application)

  def get = Success(application)
  def path = applicationPath
}

ReloadableApplication is used in the development mode but, since the class definition is huge, let's see the relevant lines of code where DefaultApplication is used:

class ReloadableApplication(buildLink: BuildLink, buildDocHandler: BuildDocHandler) extends ApplicationProvider {
...
// First, stop the old application if it exists
    Play.stop()

    val newApplication = new DefaultApplication(reloadable.path, projectClassloader, Some(new SourceMapper {
        def sourceOf(className: String, line: Option[Int]) = {
          Option(buildLink.findSource(className, line.map(_.asInstanceOf[java.lang.Integer]).orNull)).flatMap {
            case Array(file: java.io.File, null) => Some((file, None))
            case Array(file: java.io.File, line: java.lang.Integer) => Some((file, Some(line)))
            case _ => None
            }
          }
              }), Mode.Dev) with DevSettings {
                import scala.collection.JavaConverters._
                lazy val devSettings: Map[String, String] = buildLink.settings.asScala.toMap
              }

              Play.start(newApplication)
...
}

For StaticApplication, the application is created and started just once whereas, in the case of ReloadableApplication, the existing application is stopped and a new one is created and started. The ReloadableApplication is for the development mode, so as to allow developers to make changes and see them reflected without the hassle of reloading the application manually every time.

The usage of ApplicationProvider and NettyServer is similar to this:

val appProvider = new ReloadableApplication(buildLink, buildDocHandler)
val server = new NettyServer(appProvider, httpPort, httpsPort, mode = Mode.Dev)

In the following section, we will discuss the methods available in GlobalSettings, which enable us to hook into the application's life cycle.

Meddling with an application's life cycle

Consider that our application has the following specifications:

  • Prior to starting the application, we need to ensure that the /opt/dev/appName directory exists and is accessible by the application. A method in our application called ResourceHandler.initialize does this task.
  • Create the required schema on startup using the DBHandler.createSchema method. This method does not drop the schema if it already exists. This ensures that the application's data is not lost on restarting the application and the schema is generated only when the application is first started.
  • Create e-mail application logs when the application is stopped using the Mailer.sendLogs method. This method sends the application logs as an attachment in an e-mail to the emailId set in a configuration file as adminEmail. This is used to track the cause for the application's shutdown.

Play provides methods that allow us to hook into the application's life cycle and complete such tasks. The GlobalSettings trait has methods that assist in doing so. These can be overridden by the Global object, if required.

To cater to the specifications of the application described earlier, all we need to do in a Play application is define a Global object, as shown here:

object Global extends GlobalSettings {

  override def beforeStart(app: Application): Unit = {
    ResourceHandler.initialize
  }
  
  override def onStart(app: Application):Unit={
    DBHandler.createSchema
  }

  override def onStop(app: Application): Unit = {
    Mailer.sendLogs
  }
}

The ResourceHandler.initialize, DBHandler.createSchema, and Mailer.sendLogs methods are specific to our application and are defined by us, not provided by Play.

Now that we know how to hook into the application's life cycle, let's scrutinize how it works.

Digging deeper into the application's life cycle we can see that all the implementations of ApplicationProvider use the Play.start method to initialize an application. The Play.start method is defined as follows:

def start(app: Application) {

    // First stop previous app if exists
    stop()

    _currentApp = app

    // Ensure routes are eagerly loaded, so that the reverse routers are correctly
    // initialized before plugins are started.
    app.routes
    Threads.withContextClassLoader(classloader(app)) {
      app.plugins.foreach(_.onStart())
    }

    app.mode match {
      case Mode.Test =>
      case mode => logger.info("Application started (" + mode + ")")
    }

  }

This method ensures that each plugin's onStart method is called right after the application is set as _currentApp. GlobalPlugin, is added by default to all the Play applications, and is defined as:

class GlobalPlugin(app: Application) extends Plugin {

  // Call before start now
  app.global.beforeStart(app)

  // Called when the application starts.
  override def onStart() {
    app.global.onStart(app)
  }

  //Called when the application stops.
  override def onStop() {
    app.global.onStop(app)
  }

}

In the preceding snippet, app.global refers to the GlobalSettings defined for the application. Therefore, the GlobalPlugin ensures that the appropriate methods of the application's GlobalSettings are called.

The beforeStart method is called on initialization of the plugin.

Now, we just need to figure out how onStop is called. Once an application is stopped, ApplicationProvider does not have control, so the Java runtime shutdown hook is used to ensure that certain tasks are executed once the application is stopped. Here is a look at the relevant lines from the NettyServer.createServer method:

Runtime.getRuntime.addShutdownHook(new Thread { 
        override def run { 
          server.stop() 
        } 
      })

Here, runtime is java.lang.Runtime (Java docs for the same are available at http://docs.oracle.com/javase/7/docs/api/java/lang/Runtime.html) and the server is an instance of NettyServer. NettyServer's stop method is defined as:

override def stop() {

    try {
      Play.stop()
    } catch {
      case NonFatal(e) => Play.logger.error("Error while stopping the application", e)
    }

    try {
      super.stop()
    } catch {
      case NonFatal(e) => Play.logger.error("Error while stopping logger", e)
    }

    mode match {
      case Mode.Test =>
      case _ => Play.logger.info("Stopping server...")
    }

    // First, close all opened sockets
    allChannels.close().awaitUninterruptibly()

    // Release the HTTP server
    HTTP.foreach(_._1.releaseExternalResources())

    // Release the HTTPS server if needed
    HTTPS.foreach(_._1.releaseExternalResources())

    mode match {
      case Mode.Dev =>
        Invoker.lazySystem.close()
        Execution.lazyContext.close()
      case _ => ()
    }
  }

Here, the Invoker.lazySystem.close() call is used to shut down the ActorSystem used internally within a Play application. The Execution.lazyContext.close() call is to shut down Play's internal ExecutionContext.

The Play.stop method is defined as follows:

 def stop() {
    Option(_currentApp).map { app =>
      Threads.withContextClassLoader(classloader(app)) {
        app.plugins.reverse.foreach { p =>
          try {
            p.onStop()
          } catch { case NonFatal(e) => logger.warn("Error stopping plugin", e) }
        }
      }
    }
    _currentApp = null
  }

This method calls the onStop method of all the registered plugins in reverse order, so the GlobalPlugin's onStop method is called and it eventually calls the onStop method of the GlobalSetting defined for the application. Any errors encountered in this process are logged as warnings since the application is going to be stopped.

We can now add any task within the application's life cycle, such as creating database schemas before starting, initializing global objects, or scheduling jobs (using Akka Scheduler or Quartz, and so on) on starting and cleaning temporary data when stopping.

We've covered the application's life cycle, now let's look into the request-response life cycle.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.168.8