Tomcat v7 Parallel Deployment

Something that has long plagued web development is a difficulty updating running applications. The typical operation is to cut users off from the application (sometimes brutally), stop the running application, un-deploy the current version, deploy a new version, start the new version, and finally allow users to return to the application. In some cases it’s possible to re-deploy applications in place, but it will usually still result in an interruption to the user as the application is cleaned and restarted.

The Apache Tomcat v7 brings with it a feature called Parallel Deployment, which allows deploying more than one application to the same context. This feature allows any activities going on with a session-based application to continue interacting with the “old” version even as a new version is deployed. This results in an effective zero-downtime solution, as long as your app behaves.

The Basics

The documentation for Parallel Deployment is pretty brief (http://tomcat.apache.org/tomcat-7.0-doc/config/context.html#Parallel_deployment). It outlines two very important concepts, which I’ll discuss here, but with a little more detail.

Context Configuration

There is a hint in the documentation that there’s a way to do parallel deployment by naming the context in the server.xml file. This is soundly defeated by the next comment in the documentation which suggests that the server.xml file is not the place to define contexts. This also defeats our purpose as the server must restart for the server.xml to be re-parsed. Since that’s not our goal for this discussion, we’re going to skip that entirely.

The key to getting Tomcat to recognize a parallel deployment is by identifying the version of the WAR or directory containing the application. Without configuration in the server.xml file, the assumption is that autoDeploy (or deployOnStartup) is enabled for the server. AutoDeploy is on by default in the out-of-the-box server configuration, as this snippet from the server.xml shows:

<Host name=”localhost” appBase=”webapps” unpackWARs=”true” autoDeploy=”true”>

Note that deployOnStartup is also Tomcat’s default way of operating, but it doesn’t really help the zero-downtime aspect of this, although it does still support Parallel Deployment.

A downside to enabling autoDeploy is that the server will periodically scan the appBase (webapps) for new WAR files and directories. This is a pretty small impact in overall performance, but a necessary evil for Parallel Deployment to function and allow zero downtime.

Naming Deployments

The second part of this is the naming of the files and directories. Whether deploying the applications as WAR files or exploded directories, the naming works the same. When using Parallel deployment, there’s a tighter relationship to the deployed file and the context.

For many of us, it’s the almost the same relationship we’ve been dealing with for a while, especially when using the autoDeploy feature. That is that the context of the application is the name of the folder or WAR file (sans extension); so if we create a folder named “foo” in our appBase (webapps) folder, or copy a file named “foo.war” into the webapps folder, we will have an application we can reach via “http://www.ourserver.tld/foo” (insert your real URL there). There’s also the special case of a ROOT folder or ROOT.war, which gives us our root context (or http://www.ourserver.tld/ to continue the example.

The addition to naming the deployment file is the ##version of the context. When deploying a WAR file, we name our file with the additional version, such as foo##0.war or in a folder named foo#0. The ##version is compared as a string, so it can be anything that makes sense to you (foo##dev01.war), as long as the names are ascending string comparisons.

Something to watch out for even on case-insensitive systems is name and version collision. Tomcat takes care of collisions by replacing the first deployment with later variations of the same context by changing the case to whatever was there first. That is, if you deploy a version of an application, say foo##A.war, to your server, and then later try to update it with a different version but the same, say foo##a.war, not only is the comparison done with a case-insensitive comparison, but the new deployment will replace the old deployment.

Additionally, even if you change the context but keep the version, to Foo##a.war, it will replace the old deployment. This can be very frustrating if trying to deploy similarly named contexts with the same versioning scheme.

This is only a concern when the potential collision exists. Should you deploy a new context with a different version, you’ll end up with the expected different context and version. That is, if you deploy foo##a.war and Foo##b.war, you’ll end up with two contexts, not an update of the first. For this reason, you might need to put a separator in the version part of the file name, say foo##lower-a.war and Foo#capital-a.war to get the desired separate but similar contexts. Or just be careful to not have similar contexts.

To keep it simple, name your contexts and versions with an insensitive and ascending string comparison in mind. Oh, and try to avoid context and version collisions.

Once the name collisions are avoided, the versioning becomes a simple case of “put a later version with the same context name.” Most of us will likely use some kind of numeric format for our versions. I like to use the date I’m deploying. Something like foo##20120301.war would tell me that I deployed an app on 1 March, 2012; the zero-padding and Y-M-D ensures that the later days in the year come after the earlier days. If I deploy again the next day, I’d change the version to foo##20120302.war. I’ll add the time for those frequently deployed (e.g., development) applications, making the name foo##20120301-1415.war for an app I deployed at 2:15PM on 1 March, 2012. That’s usually enough resolution, but you can get as creative as you need to, just by remembering it’s a string comparison.

Deploying

There are essentially three ways to deploy applications to Tomcat servers with autoDeploy turned on; copy WAR files, copy directories, and using the Manager application.

Whether copying with WAR files or directories, you need access to the server’s file system. Copy the file or folder to the directory specified by the Host appBase, usually webapps. After a cycle, Tomcat will recognize the new content is there and deploy the application. It’s recommended to archive the application into a WAR file and allow Tomcat to create the directory, exploding what it needs (which will be everything unless unpackWARs is set to false).

When using the Tomcat manager, there are actually two ways to deploy. The easier way is to use the “WAR file to deploy” and select the WAR file, which should be named with the ##version. This will copy the WAR file to the appBase folder and deploy as if you’d copied it there directly.

The other way to deploy using the Tomcat manager requires the archive to exist on the server (or on a resource accessible as a path on the server). Using the “deploy directory or WAR file located on the server” section, as the name suggests, you can select any WAR file or folder that is structured correctly. I still recommend using WAR files. Here, however, the name of the file doesn’t matter, and instead you must put the ##version in the context path, entered as /foo##version in order to be recognized.

Using Versions

Now that we’ve got a handle on getting the applications deployed, it’s important to understand how Tomcat distributes the requests to the applications. With the old way of deploying things, there was always at most one version of an application running in a specific context on a server. With Parallel Deployment, however, can have many versions running at the same time.

Since the objective is zero-downtime deployments of updated applications, we’ll make the assumption that the application is in use. Of course, if an application is not in use, the old manner of remove and reinstall deployment still work, although we can still use Parallel Deployment. A key thing to remember about how Tomcat handles this is that it is tied to the user’s session.

No Session Established

If the user doesn’t have a session, either because they are a new user or because they haven’t done anything in the application to involve a session, they will always be routed to the newest version of an application. Here’s a super-simple application to test this out (and a quick show-off of Servlet 3.0).

Fire up your favorite IDE or text editor (heck, it’s a two-file example, so use the command line all you want!) and create a WEB-INF/web.xml with this simple bit in it:

<?xml version=“1.0” encoding=“UTF-8”?>
<
web-app xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance”
xmlns
=“http://java.sun.com/xml/ns/javaee”
xmlns:web
=“http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd”
xsi:schemaLocation
=“http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd”

id
=“WebApp_ID” version=“3.0”>
</
web-app>

Then create a Servlet that just shows us we’re running our application:

@WebServlet(urlPatterns = { "/index.htm" })
public class SampleServlet extends HttpServlet {
  private static String startTime = null;
  private static final AtomicLong visitors = new AtomicLong(0l);
  private final DateFormat dateFormat = SimpleDateFormat.getDateTimeInstance();

  @Override
  protected void doGet(HttpServletRequest httpServletRequest,
                        HttpServletResponse httpServletResponse) 
                  throws ServletException, IOException {
    if (startTime == null) {
      final Calendar calendar = Calendar.getInstance();
      startTime = dateFormat.format(calendar.getTime());
    }
    httpServletResponse.setContentType("text/html");
    httpServletResponse.getWriter().println(
        String.format(
            "<html><head><title>Example</title></head><body>" +
            "<p>Revision: A<br/>Start Time: %s<br/>Page Visits: %d</p></body></html>",
            startTime,
            visitors.incrementAndGet()));
  }
}

This very simple application will remember the start-time of the application (based on the first visit) and keep count of page views. Compile and package this application and deploy it to a Tomcat v7 server using the name and version like ROOT##001.war so we can visit it by simply hitting http://localhost:8080 (or wherever you your have Tomcat’s HTTP port listening).

Visiting the page will reveal our version, the time of our first visit and the number of times we hit refresh. Hitting refresh a few times will reveal that the revision won’t change (because it’s a string literal), and the time shouldn’t unless the server restarts.

Change our application a little, say by updating the version line to say “B” instead of “A,” as the following shows:

“<p>Revision: B<br/>Start Time: %s<br/>Page Visits: %d</p></body></html>”

Then deploy it without touching the other application with Parallel Deployment by naming the deployable ROOT##002.war.

Now when we visit the page, the revision will be different but won’t change with successive visits. Likewise, the time should be different, but won’t change. And the counter should have been reset to one and increment from the start again.

All of this shows us that we’ve shifted to the new application without any effort on the users part. This shift works because Tomcat investigated the request, didn’t see a session attached, and sent the request to the newest running version.

With a Session Established

In more robust applications, something always seems to be tucked in a session. When a session is established Parallel Deployment will keep current users going to the same version of the application for as long as that version is running. If an updated version is deployed, new users will use the new application while old users will continue using the old version until their session expires.

Here’s some tweaks to our simple example Servlet that will show us how this works.

@WebServlet(urlPatterns = { "/index.htm" })
public class SampleServlet extends HttpServlet {
  private static String startTime = null;
  private static final AtomicLong visitors = new AtomicLong(0l);
  private final DateFormat dateFormat = SimpleDateFormat.getDateTimeInstance();

  @Override
  protected void doGet(HttpServletRequest httpServletRequest,
                        HttpServletResponse httpServletResponse) 
                  throws ServletException, IOException {
    if ("true".equals(httpServletRequest.getParameter("forget"))) {
      httpServletRequest.getSession(true).invalidate();
      httpServletResponse.sendRedirect(httpServletRequest.getRequestURI());
      return;
    }
    final Calendar calendar = Calendar.getInstance();
    if (startTime == null) {
      startTime = dateFormat.format(calendar.getTime());
    }
    final HttpSession httpSession = httpServletRequest.getSession(true);
    if (httpSession.isNew()) {
      httpSession.setAttribute("userArrived",
          dateFormat.format(calendar.getTime()));
    }
    httpServletResponse.setContentType("text/html");
    httpServletResponse.getWriter().println(
        String.format(
            "<html><head><title>Example</title></head><body>" +
            "<p>Revision: C<br/>Start Time: %s<br/>Page Visits: %d<br/>" +
            "Session ID: %s<br/>Session Started: %s</p><p><a href="%s">" +
            "Refresh</a> <a href="%s?forget=true">New Session</a></p></body></html>",
            startTime,
            visitors.incrementAndGet(),
            httpSession.getId(),
            httpSession.getAttribute("userArrived"),
            httpServletRequest.getRequestURI(),
            httpServletRequest.getRequestURI()));
  }
}

Package and deploy this application as ROOT##003.war, and when visiting the application, the revision should change (it’s still a static), and the start time should be different, but not change after the first request. The counter is still there and should increment. Two new attributes are shown, giving us our session ID and the time our session started. We’ve also added a link to refresh the view, and another to refresh the view with a new session. Clicking this second link should show the visits increment and give us a new session ID and start time.

Let’s again make a simple change to the application, such as changing the “C” to a “D” in the string:

“<p>Revision: D<br/>Start Time: %s<br/>Page Visits: %d<br/>”

Package this application as ROOT##004.war. Visit the page again, so we’re sure to have a session with version ##003, and then deploy this version of the application. If we refresh the application, we’ll still see version “C” is displayed with that start time and visitor count. Our session ID or start time shouldn’t change; just the page visit count.

If we end that session by hitting the “new session” link, we should see that we are greeted by revision “D” with its new time and visitor count. If we open a new browser (perhaps after closing the one that’s open) we’ll see we’re also greeted by revision “D” when we first visit.

All four of our applications are still deployed and running, but Tomcat is directing existing sessions to the version with which they’re associated, and new sessions to the latest version. The first two versions of the application didn’t involve sessions, so they are fairly irrelevant for this discussion. The last two versions do maintain sessions, so they will figure into this routing that Tomcat has to do when new versions are deployed.

As we see in our simple example, any expired session that returns to the server will get a new session in the new version. This will also be the case if an old version, to which a visitor would be routed, is stopped; their session would no longer be valid, so Tomcat would route them to the newest version of the application.

Runtime Considerations

Astute readers will notice that there are static variables in these Servlets (I know, they aren’t necessary as the Servlet won’t be unloaded until the application dies, but I did it to make this point). Static variables are bad form as they are loaded once in a JVM, per classloader. While operating in the same application server, and more importantly in the same JVM, they are not instances of the same class. This is not because of our simple edits, either; if you don’t change the class and deploy the same WAR as a new version the same classes the results will be the same. This is because each version of the application is treated as a separate web application, so each has its own classloader.

This separation may not occur for common classes loaded by Tomcat for all applications. It may take some care to find and eliminate conflicts with different versions of the same application.

The same consideration needs to be made for other resources, such as JNDI and database connection pools that may be managed by Tomcat. Any access to any kind of file system resource or even network services need to be considered as well. They need to be able to allow access from multiple applications.

A good guideline would be if you can deploy the application twice with different contexts, you should be safe to deploy then as separate versions of the same context.

Rolling-back

One benefit of Parallel Deployment is that the old application stays running until we stop it. Further, it’s still deployed, until we un-deploy it. This allows us to stop new applications and have the old versions pick up new sessions.

A better approach, if a rollback is required, is to redeploy the old app as a new version. There might need to be some management to track these repeated versions, but it allows the users to keep working without losing their sessions.

Of course, if the new application is flawed, it might be the case that it shouldn’t keep running, so the forced stop will be better way to handle the situation.

Cleaning Up

Another important consideration is that the application should be able to be shutdown cleanly. This is a good thing to have in an application anyway, but there have been more than a few applications that can only be stopped by killing the server. It isn’t critical that an application be able to be shutdown cleanly, but it is important for the purpose of cleaning up the server without having to force it to stop. If we have to stop the server to remove old versions, we lose our zero-downtime capabilities.

As mentioned, all four versions of our sample application are deployed and still running. For this application, it is probably no impact on the server. However, if the server is restarted, all of the applications will still be deployed and will restart. This can be a problem in a real application, especially one that consumes resources the server might want for the new versions.

As the new versions are deployed, new sessions will go to them. As old versions of the application are no longer used, they can be stopped and undeployed. This can be done through the Tomcat manager or by removing the appropriate WAR file from the server; Tomcat will remove the related directory when the application is shutdown. It’s a little rougher on the server to remove applications deployed in exploded folders, but that works by deleting the folder; note that some operating systems may hold file locks inside the folder that inhibits its deletion.

Identifying Versions

Since it’s a Tomcat-specific feature, there’s no straight-up Java way to get the version of the application that is handling your request.

There are a few reasons one might want to know the running version, like logging and reporting errors. The best solution I can make is to take apart the Servlet’s deployed path and parse out the string between the ## and the / or end of the string. This is easily done in a pair of substrings with Apache Commons StringUtils.

runningVersion = StringUtils.substringBefore(
                     StringUtils.substringAfter(
                         servletConfig.getServletContext().getRealPath("/"),
                         "##"),
                     "/");

The ServletContext method getRealPath() will translate the requested URL (in this case the default root document) into the actual path on the file system from which the application is served (more correctly, the folder from which the page is served). Our example application, using the above real path, we’ll get a string like the following:

/opt/apache-tomcat-7.0.23/webapps/blog##005/

Of course, this only works with exploded JARs, but since that’s the default mechanism Tomcat uses, we should be OK. It also only works if there is version information in the path; this example is ineffective for deployments that don’t contain version information, and will instead contain an empty string. Looking at that real path string, we can see our version near the end, so by parsing out the substring between the ## and / that follows, we finally receive our version number of 005.

Careful readers will note that we’ve only deployed four versions of the application, but my version number is higher. I modified the little Servlet, to ensure this works, and deployed that as a fifth version!

@WebServlet(urlPatterns = { "/index.htm" })
public class SampleServlet extends HttpServlet {
  private final DateFormat dateFormat = SimpleDateFormat.getDateTimeInstance();
  private String runningVersion = null;
  private String startTime = null;
  private final AtomicLong visitors = new AtomicLong(0l);

  @Override
  public void init(ServletConfig servletConfig) throws ServletException {
    super.init(servletConfig);
    final Calendar calendar = Calendar.getInstance();
    startTime = dateFormat.format(calendar.getTime());

    runningVersion = StringUtils.substringBefore(
                       StringUtils.substringAfter(
                         servletConfig.getServletContext().getRealPath("/"),
                         "##"),
                       "/");
  }

  @Override
  protected void doGet(HttpServletRequest httpServletRequest,
                        HttpServletResponse httpServletResponse) 
                  throws ServletException, IOException {
    if ("true".equals(httpServletRequest.getParameter("forget"))) {
      httpServletRequest.getSession(true).invalidate();
      httpServletResponse.sendRedirect(httpServletRequest.getRequestURI());
      return;
    }
    final HttpSession httpSession = httpServletRequest.getSession(true);
    if (httpSession.isNew()) {
      final Calendar calendar = Calendar.getInstance();
      httpSession.setAttribute("userArrived",
          dateFormat.format(calendar.getTime()));
    }
    httpServletResponse.setContentType("text/html");
    httpServletResponse.getWriter().println(
        String.format(
            "<html><head><title>Example</title></head><body>" +
            "<p>Revision: %s<br/>Start Time: %s<br/>Page Visits: %d<br/>" +
            "Session ID: %s<br/>Session Started: %s<br/>Real Path: %s</p>" +
            "<p><a href="%s">Refresh</a> " +
            "<a href="%s?forget=true">New Session</a></p></body></html>",
            runningVersion,
            startTime,
            visitors.incrementAndGet(),
            httpSession.getId(),
            httpSession.getAttribute("userArrived"),
            httpSession.getServletContext().getRealPath(
                httpServletRequest.getRequestURL().toString()),
            httpServletRequest.getRequestURI(),
            httpServletRequest.getRequestURI()));
  }
}

This new version will show the interpreted version number, show the time of the first request, the count of visitors, and session information as before. It will also show the real path of the requested URL (which you’ll notice is an absolute URL and comes out a little different than shown above).

Conclusion

This just about covers what Parallel Deployment brings to the table. When I first heard about it I was skeptical and had doubts of its capabilities. Simple applications such as these examples are fine for showing how things work, but don’t really help if you have real world needs.

For a lot of applications, Parallel Deployment will work fantastically. It will allow additional features to be put into place, bugs to be fixed, and even changes in style, all without interrupting active visitors. They will continue to use the version they’re on, but won’t lose their shopping carts or other unsaved activities. Sure, they won’t benefit from the enhancements or fixes until they end their visit, but interrupting the user might be drastic and result in lost work or revenue.

A common upgrade has to do with underlying resources. It may be that a reason for an updated application has to do with changes to a database or a file system resource or an external system. If the impact is drastic enough it may be the case that Parallel Deployment will not meet the needs of these kinds of upgrades, as the old application may start to fail as the underlying resources are stopped and upgraded or otherwise changed. For these changes, of course, there’s always stop and re-deploy.

One thought on “Tomcat v7 Parallel Deployment

  1. Stefan says:

    It seems like a great feature, however i don’t know why TomEE (which is basically tomcat) does not want to interpret the version correctly and thus throws illegal argument exception. After a few re-deployments it goes through but that’s not really good for production.

    This happens also on restart that he does not detect the good version of the file and just crashes the application.

    Did you have any problems like this?

    Also, great in-depth article!

  2. Alex says:

    Very helpful article, thanks a lot. The documentation on the Tomcat site is a little thin on this subject, so it was nice to read something more in-depth.

  3. Javier says:

    Hello

    I managed to install multiple versions of my webservice using the nomenclature to generate the war

    ## myWebService 2.war
    ## myWebService 3.war

    But whenever I access mediente URL, I agree to the version 3. Is there any way to accerder to version 2 without stop Tomcat?

    Cheers

Leave a Reply

Your email address will not be published. Required fields are marked *

*

*