Bug 63389 - Enable Servlet Warmup for Containerization
Summary: Enable Servlet Warmup for Containerization
Status: NEW
Alias: None
Product: Tomcat 9
Classification: Unclassified
Component: Catalina (show other bugs)
Version: 9.0.x
Hardware: PC Linux
: P2 enhancement (vote)
Target Milestone: -----
Assignee: Tomcat Developers Mailing List
URL:
Keywords:
Depends on:
Blocks:
 
Reported: 2019-04-27 21:09 UTC by Igal Sapir
Modified: 2022-01-25 23:39 UTC (History)
0 users



Attachments

Note You need to log in before you can comment on or make changes to this bug.
Description Igal Sapir 2019-04-27 21:09:30 UTC
Some servlets have longer initialization time than others.  In containerizations in general, with Docker being the most popular ATM, it is a common practice to do a Warm Up during building the container image, so that instead of running the servlet for the first time when the container is run, the servlet is initialized while building the container image.

That way, the startup time of the container is much faster as there is no need to wait for initialization.

Currently, one hack that many use is to launch Tomcat during the container build process, wait for an arbitrary length of time, e.g. 10s or 20s, and then shut Tomcat down.

A nice enhancement would be if we could provide a listener which will accept a servlet name as an argument, and will shut down Tomcat as soon as that servlet initialization is completed.

A better way would have been to implement this as a ServletContextListener [1], but unfortunately the misnomered method contextInitialized() is called when the context initialization starts and not when it ends.


[1] https://docs.oracle.com/javaee/7/api/javax/servlet/ServletContextListener.html
Comment 1 Mark Thomas 2019-04-29 11:41:58 UTC
Start and stop are synchronous. Calling start() then stop() should be sufficient. Doing that with the current startup scripts is a little trickier because you need to wait for the shutdown port to open.

Something like:
./catalina.sh start && sleep 1 && ./catalina.sh stop

should work however long the web application takes to start up.
Comment 2 Igal Sapir 2019-04-29 18:08:33 UTC
(In reply to Mark Thomas from comment #1)
> Start and stop are synchronous. Calling start() then stop() should be
> sufficient. Doing that with the current startup scripts is a little trickier
> because you need to wait for the shutdown port to open.
> 
> Something like:
> ./catalina.sh start && sleep 1 && ./catalina.sh stop
> 
> should work however long the web application takes to start up.

On my laptop the servlet takes about 3 seconds to initialize (though on some cloud deployments I have seen it take as much as 15s).

From testing the above compounded command it seems to me that the shutdown port only opens after the servlet initialization, so waiting 1 or 2s does not work [1].

If we could start listening the shutdown port at the beginning of the process, and then it can be a blocking operation until start completes, then that would be a great a solution.

Thoughts?

[1] /workspace/src/tomcat-master$ output/build/bin/catalina.sh start && sleep 2 && output/build/bin/catalina.sh stop
Using CATALINA_BASE:   /workspace/test/catalina-base
Using CATALINA_HOME:   /workspace/src/tomcat-master/output/build
Using CATALINA_TMPDIR: /workspace/test/catalina-base/temp
Using JRE_HOME:        /opt/java/jdk1.8.0_202
Using CLASSPATH:       /workspace/src/tomcat-master/output/build/bin/bootstrap.jar:/workspace/src/tomcat-master/output/build/bin/tomcat-juli.jar
Tomcat started.
Using CATALINA_BASE:   /workspace/test/catalina-base
Using CATALINA_HOME:   /workspace/src/tomcat-master/output/build
Using CATALINA_TMPDIR: /workspace/test/catalina-base/temp
Using JRE_HOME:        /opt/java/jdk1.8.0_202
Using CLASSPATH:       /workspace/src/tomcat-master/output/build/bin/bootstrap.jar:/workspace/src/tomcat-master/output/build/bin/tomcat-juli.jar
Apr 29, 2019 10:58:34 AM org.apache.catalina.startup.Catalina stopServer
SEVERE: Could not contact [localhost:8005] (base port [8005] and offset [0]). Tomcat may not be running.
Apr 29, 2019 10:58:34 AM org.apache.catalina.startup.Catalina stopServer
SEVERE: Error stopping Catalina
java.net.ConnectException: Connection refused (Connection refused)
	at java.net.PlainSocketImpl.socketConnect(Native Method)
	at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350)
	at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206)
	at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188)
	at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392)
	at java.net.Socket.connect(Socket.java:589)
	at java.net.Socket.connect(Socket.java:538)
	at java.net.Socket.<init>(Socket.java:434)
	at java.net.Socket.<init>(Socket.java:211)
	at org.apache.catalina.startup.Catalina.stopServer(Catalina.java:513)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.apache.catalina.startup.Bootstrap.stopServer(Bootstrap.java:390)
	at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:480)
Comment 3 Christopher Schultz 2019-04-29 18:29:44 UTC
I'm definitely missing something, here. Launching Tomcat and then shutting it down saves zero time on the next launch except maybe to create a directory or two and unpack any WAR archives being deployed.

If you want to warm the application, you'll want to pre-compile all the JSPs either using jspc or by just spamming the application with web requests.

So what exactly is the point, here?

I can see use-cases for building something like this (some people want to know when Tomcat is "really started"), but for triggering a shutdown to "warm" an app server? I don't get it.
Comment 4 Igal Sapir 2019-04-29 18:42:54 UTC
(In reply to Christopher Schultz from comment #3)
> I'm definitely missing something, here. Launching Tomcat and then shutting
> it down saves zero time on the next launch except maybe to create a
> directory or two and unpack any WAR archives being deployed.

This is not for creating the Tomcat directory structure, but all of the initialization of the servlet itself.
 
> If you want to warm the application, you'll want to pre-compile all the JSPs
> either using jspc or by just spamming the application with web requests.
> 
> So what exactly is the point, here?

This is not a JSP per-se, even though it follows the JSP interface.  In this case it is the Lucee Application Server that implements a CFML engine.

> I can see use-cases for building something like this (some people want to
> know when Tomcat is "really started"), but for triggering a shutdown to
> "warm" an app server? I don't get it.

The Application Server can take a while on first run because according to configuration settings it might download extensions and install them, etc.  Without downloading extensions it takes about 3.5s to launch on my latptop.  With extensions it can take up to 60s.

In monolith deployments that might have been acceptable, but in Docker or server-less deployments it is not.

You can find more information in the Lucee ticket system [1], where I actually resolved the issue for Lucee but it feels like a hack and would be much better if resolved on the Tomcat level as 1) it will be cleaner, and 2) it will help other projects who might need this feature.


[1] https://luceeserver.atlassian.net/browse/LDEV-1196
Comment 5 Christopher Schultz 2019-04-29 18:49:20 UTC
(In reply to Igal Sapir from comment #4)
> (In reply to Christopher Schultz from comment #3)
> > I'm definitely missing something, here. Launching Tomcat and then shutting
> > it down saves zero time on the next launch except maybe to create a
> > directory or two and unpack any WAR archives being deployed.
> 
> This is not for creating the Tomcat directory structure, but all of the
> initialization of the servlet itself.

So if the servlet does something like downloading a bunch of files or whatever? I mean, the JVM stops, so reading db info into a cache or something doesn't help. Just making sure that this has nothing to do with Tomcat itself, which won't benefit at all.

> > If you want to warm the application, you'll want to pre-compile all the JSPs
> > either using jspc or by just spamming the application with web requests.
> > 
> > So what exactly is the point, here?
> 
> This is not a JSP per-se, even though it follows the JSP interface.  In this
> case it is the Lucee Application Server that implements a CFML engine.

Gotcha. I'm curious what the Lucee engine does on startup that happens ONE TIME instead of each time the application is launched.

> > I can see use-cases for building something like this (some people want to
> > know when Tomcat is "really started"), but for triggering a shutdown to
> > "warm" an app server? I don't get it.
> 
> The Application Server can take a while on first run because according to
> configuration settings it might download extensions and install them, etc. 
> Without downloading extensions it takes about 3.5s to launch on my latptop. 
> With extensions it can take up to 60s.

So these are extensions for Lucee?

> In monolith deployments that might have been acceptable, but in Docker or
> server-less deployments it is not.

I have to admit that I find "serverless servlets" an amusing construct.
Comment 6 Igal Sapir 2019-04-29 21:07:12 UTC
(In reply to Christopher Schultz from comment #5)
> (In reply to Igal Sapir from comment #4)
> > (In reply to Christopher Schultz from comment #3)
> > > I'm definitely missing something, here. Launching Tomcat and then shutting
> > > it down saves zero time on the next launch except maybe to create a
> > > directory or two and unpack any WAR archives being deployed.
> > 
> > This is not for creating the Tomcat directory structure, but all of the
> > initialization of the servlet itself.
> 
> So if the servlet does something like downloading a bunch of files or
> whatever? I mean, the JVM stops, so reading db info into a cache or
> something doesn't help. Just making sure that this has nothing to do with
> Tomcat itself, which won't benefit at all.

Right.  Not Tomcat itself.

> 
> > > If you want to warm the application, you'll want to pre-compile all the JSPs
> > > either using jspc or by just spamming the application with web requests.
> > > 
> > > So what exactly is the point, here?
> > 
> > This is not a JSP per-se, even though it follows the JSP interface.  In this
> > case it is the Lucee Application Server that implements a CFML engine.
> 
> Gotcha. I'm curious what the Lucee engine does on startup that happens ONE
> TIME instead of each time the application is launched.

In addition to Extensions (more below), some of the initial setup is to create directories and files for the application during initialization.  That only happens once so the first time Lucee is launched takes much longer than subsequent launches.  

This is exactly the issue that this enhancement proposes to fix.  Do the first launch during the `docker build` process so that `docker run` will start much faster.

> 
> > > I can see use-cases for building something like this (some people want to
> > > know when Tomcat is "really started"), but for triggering a shutdown to
> > > "warm" an app server? I don't get it.
> > 
> > The Application Server can take a while on first run because according to
> > configuration settings it might download extensions and install them, etc. 
> > Without downloading extensions it takes about 3.5s to launch on my latptop. 
> > With extensions it can take up to 60s.
> 
> So these are extensions for Lucee?

Yes.  Lucee has many different extensions, e.g. database drivers, cache providers, PDF generators, Chart generators, etc.  Some extensions are provided by the Lucee project [1] while others provided by 3rd party vendors.  Developers can configure which extensions their application requires to control the image size.

> 
> > In monolith deployments that might have been acceptable, but in Docker or
> > server-less deployments it is not.
> 
> I have to admit that I find "serverless servlets" an amusing construct.

;-)

[1] https://github.com/search?p=3&q=org%3Alucee+ext&type=Repositories
Comment 7 Mark Thomas 2019-05-15 07:28:06 UTC
Thinking about this some more, I think the best way to handle this is with a new "warmup" command. Implemented in a similar manner to configtest it would call server.start() followed by server.stop().
Comment 8 Christopher Schultz 2019-05-15 16:00:29 UTC
I have a slight objection to the term "warmup" being used, here.

Can we call it something else?

Nothing brilliant comes to mind.

"updown"?
"startstop"?
"launchtest"?

Perhaps such a special launch command could provide some output such as "OK" if everything is okay, or some kind of error words/codes which describe what happened. For example, if one of the connectors failed to start properly, we could report that failure. Perhaps the same for any configured auto-deploy application that failed to start cleanly.
Comment 9 Mark Thomas 2019-05-15 16:05:00 UTC
Since this is synchronous, we can use the exit code to signal success or failure.
Comment 10 Eugène Adell 2019-07-09 20:28:53 UTC
(In reply to Igal Sapir from comment #0)
> Currently, one hack that many use is to launch Tomcat during the container
> build process, wait for an arbitrary length of time, e.g. 10s or 20s, and
> then shut Tomcat down.

From your question and some comments, if the problem is with the "arbitrary length of time", I suggest to rely on JMX particularly on the Connector stateName value. It is changing from INITIALIZED to STARTED once Tomcat has finished deploying. If you can bring ANT within your docker, the effort is not that huge, you just need 2 targets, one for monitoring the Connector state, and one for stopping Tomcat.

This is the result that I get with a servlet called at the startup and that sleeps 60 during its initialization :

catalina.out
09-Jul-2019 22:10:55.804 INFO [localhost-startStop-5] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of web application directory [/home/eadell/tests/apache-tomcat-8.5.15/webapps/host-manager] has finished in [27] ms
09-Jul-2019 22:10:55.879 FINE [localhost-startStop-4] org.apache.catalina.authenticator.AuthenticatorBase.startInternal No SingleSignOn Valve is present
09-Jul-2019 22:10:56.055 INFO [localhost-startStop-4] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of web application directory [/home/eadell/tests/apache-tomcat-8.5.15/webapps/examples] has finished in [793] ms
09-Jul-2019 22:11:55.779 INFO [localhost-startStop-3] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of web application directory [/home/eadell/tests/apache-tomcat-8.5.15/webapps/slowstart] has finished in [60,518] ms
09-Jul-2019 22:11:55.790 INFO [main] org.apache.coyote.AbstractProtocol.start Starting ProtocolHandler ["http-nio-8280"]
09-Jul-2019 22:11:55.811 INFO [main] org.apache.coyote.AbstractProtocol.start Starting ProtocolHandler ["http-nio-8281"]
09-Jul-2019 22:11:55.815 INFO [main] org.apache.catalina.startup.Catalina.start Server startup in 60719 ms
09-Jul-2019 22:11:57.411 INFO [main] org.apache.catalina.core.StandardServer.await A valid shutdown command was received via the shutdown port. Stopping the Server instance.
09-Jul-2019 22:11:57.412 INFO [main] org.apache.coyote.AbstractProtocol.pause Pausing ProtocolHandler ["http-nio-8280"]
09-Jul-2019 22:11:57.469 INFO [main] org.apache.coyote.AbstractProtocol.pause Pausing ProtocolHandler ["http-nio-8281"]
09-Jul-2019 22:11:57.521 INFO [main] org.apache.catalina.core.StandardService.stopInternal Stopping service [Catalina]
destroying LongInitServlet

shell output
$ time (bin/startup.sh ; ant -f anted-jmx/project.xml buildAndStop)
Using CATALINA_BASE:   /home/eadell/tests/apache-tomcat
Using CATALINA_HOME:   /home/eadell/tests/apache-tomcat
Using CATALINA_TMPDIR: /home/eadell/tests/apache-tomcat/temp
Using JRE_HOME:        /usr/local/java/1.9
Using CLASSPATH:       /home/eadell/tests/apache-tomcat/bin/bootstrap.jar:/home/eadell/tests/apache-tomcat/bin/tomcat-juli.jar
Tomcat started.
Buildfile: /home/eadell/tests/apache-tomcat-8.5.15/anted-jmx/project.xml

waitMBEAN:
     [echo] Server url alive

shutdown:
     [echo] shutting down ${tomcat.catalina_base}

buildAndStop:

BUILD SUCCESSFUL
Total time: 1 minute 3 seconds

real    1m4.673s
user    0m5.157s
sys     0m0.600s


Here are the ANT targets :

<target name="waitMBEAN">
  <jmxOpen host="${jmx.server.name}" port="${jmx.server.port}" username="controlRole" password="******" />
  <waitfor maxwait="1200" maxwaitunit="second" timeoutproperty="server.timeout" >
      <jmxCondition
        operation="=="
        name="Catalina:type=Connector,port=8281"
        attribute="stateName" value="STARTED"
      />
  </waitfor>
  <fail if="server.timeout" message="Server url don't answer inside 1200 sec" />
  <echo message="Tomcat finished starting" />
</target>

<target name="shutdown" description="stop tomcat">
    <echo message="shutting down ${tomcat.catalina_base}" />
    <exec executable="/bin/bash">
        <arg line="${catalina.home}/bin/shutdown.sh" />
    </exec>
</target>

<target name="buildAndStop" depends="waitMBEAN,shutdown" />

If required you could also add targets to call servlets, and stop once you have finished these calls.

Of course, remember to use a random JMX password then you would not compromise all of your Tomcats if something bad happened.