Running Multiple Local Tomcats with Cargo and Gradle

We are currently using Cargo in combination with Gradle to implement consumer based tests for one of our projects. In order to do so, we created a Gradle script to deploy a service web app into a Tomcat container via Cargo and pass its URL to our consumer test suite.

As long as we run the build script locally, everything works fine. But we noticed that it failed every once in a while when running on certain build agents in our TeamCity build pipeline. The failures where always either caused by missing write permissions to the /tmp/cargo directory or because the Tomcat port was already in use.

So we took a closer look at the unreliable agents and realized to our surprise that they shared the same machine. Up until this point we just assumed that every build agent had its dedicated environment, so we didn’t really worry about things like conflicting ports or shared files.

Being fairly new to Gradle, Cargo and especially the Gradle version of the Cargo plugin, it took me some time to figure out how to isolate our Cargo run from the outside world. In the rest of this article I’m going to show you how I did it.

The Situation

There are two major problems we need to take care of. The first one is pretty obvious: all network ports need to be determined dynamically. This is a best-practice for build scripts that are shared between different environments anyway, so it is a welcome improvement.

The second problem is a bit more surprising. Cargo uses the java.io.tmpdir as default working directory. Most of the time this will simply be /tmp. At least it was on our build server. Unless this path is changed, all Cargo runs will work on the same directory and consequently interfere with each other. So we need to figure out how to change this path.

Changing the Ports

As I mentioned before, I’m fairly new to Gradle, so I was pleasently surprised to find out that it comes with a class called AvailablePortFinder. As the name suggests, this little helper allows you to conveniently find available ports. Great! That’s exactly what we need in order to instruct Cargo to use different ports when firing up Tomcat. However there is a small caveat regarding its use coming directly from the Gradle guys:

If possible, it’s preferable to let the party creating the server socket select the port (e.g. with new ServerSocket(0)) and then query it for the port chosen. With this class, there is always a risk that someone else grabs the port between the time it is returned from getNextAvailable() and the time the socket is created.

Unfortunately that’s not an option for code we don’t control, so we have to live with the small risk that someone else could grab the port before our Tomcat can occupy it.

Now how many ports do we need to change and how do we tell Cargo to do so? In case of Tomcat the answer turns out to be three: the HTTP port, the AJP port and the RMI port.

A look into the Cargo documentation and this blog post reveals the properties we can use to change these ports:

  • cargo.servlet.port for HTTP
  • cargo.tomcat.ajp.port for AJP
  • cargo.rmi.port for RMI

They can be configured in the cargo.local.containerProperties section of the Cargo configuration. The resulting build script should look similar to this:

def availablePortFinder = AvailablePortFinder.createPrivate()  
def tomcatDownloadUrl = 'http://…/apache-tomcat-7.0.50.zip'

cargo {  
  containerId = 'tomcat7x'  
  deployable {  
    …  
  }  

  local {  
    …  
    installer {  
      installUrl = tomcatDownloadUrl  
      downloadDir = file("$buildDir/download")  
      extractDir = file("$buildDir/extract")  
    }  

    containerProperties {  
      property 'cargo.servlet.port', availablePortFinder.nextAvailable  
      property 'cargo.tomcat.ajp.port', availablePortFinder.nextAvailable  
      property 'cargo.rmi.port', availablePortFinder.nextAvailable  
    }  
  }  
}

cargoStartLocal.finalizedBy cargoStopLocal  

This sucessfully solves the port problem. So let’s move on to the next one.

Changing the Working Directory

Changing the working directory turned out to be a bit tricky. In theory it can be changed via the two configuration properties homeDir and configHomeDir in the local Cargo configuration. But for some reason changing the directory to a location in my $buildDir resulted in the following errors:

Directory '/my/project/home/build/cargo' specified for property 'homeDir' does not exist.  
Directory '/my/project/home/build/cargo' specified for property 'configHomeDir' does not exist.  

It looks like Cargo doesn’t automatically create these directories, so we have to do it manually by running a custom task right before cargoStartLocal:

def cargoHome = "$buildDir/cargo"  
…  
cargo {  
  containerId = 'tomcat7x'  
  …  
  local {  
    homeDir = file(cargoHome)  
    configHomeDir = file(cargoHome)  
  }  
}

task createCargoHome() {  
  doLast {  
    if (!file(cargoHome).exists() && !file(cargoHome).mkdirs()) {  
      println "Failed to create directory '${cargoHome}'"  
    }  
  }  
}

// This will create the Cargo home directory before Cargo runs  
cargoStartLocal.dependsOn createCargoHome  
…  

That’ll do it! Cargo will now create all its files in the project build directory, so it won’t interfere with other builds anymore. Here you can find an example build script which combines both solutions and adds some more context.

I hope this article saves you the time to figure this out all by yourself. If you have any questions or ideas how to improve this solution please contact me at @SQiShER or leave a comment.