Should you run your daemon/batch in Tomcat?
From time to time you write some daemon or batch scheduled with whatever scheduler (a very expensive one or just a cron entry). These programs are often intended to just manipulate some data to let other applications be functional. Most common usage is to synchronize two datasets but you can find some more advanced use cases.
Often these programs are plain main(String[]) cause it is easier to handle than deploying a webapp and doesn't worth as much effort for something running < 30 minutes a day. This also often means you face some surprinsing challenges:
- how do I get some IoC? We are used to run in TomEE, JBoss, Meecrowave, ... and inherit from the default CDI context, how do I handle that in my main?
- how do I handle logs? In Tomcat (or any container) we are used to configure the logs a certain way but what about this plain main?
- how do I communicate the exit status? If this is a batch I need a particular exit code to communicate to the scheduler the state of the execution, who is responsible to set it? Said otherwise: do I brutally call System.exit(xxx) or do I wait my application to be done before calling it?
- how do I define the lifecycle of my application?
- etc...
Excepted the exit code point all these questions are there just cause you removed the container from a standard development. What if we add it back?
Tomcat to the rescue
Since some time now, Tomcat can be embedded so it is easy enough to write a main with Tomcat. If still a bit raw for you, you can use one of its flavor where most of the work is already done and it is really a one liner to start an embedded application in Tomcat:
- Apache TomEE Embedded
- Apache Meecrowave (microprofile container)
- JBoss Swarm
- Spring Boot
- ...
Once you have Tomcat (and for previous solutions either CDI or Spring) you answered the container and programming model points.
Concretely now you can write a batch with JBatch, relying on CDI for the JBatch components (or if you chose Spring boot you will likely use spring-batch with spring beans).
For the logs you will rely on the container configuration as well.
But wait...this is tomcat so now I need a port? Since a few versions Tomcat embed supports to run without any Connector! It literally means you don't need to bind any port with Tomcat :).
What about the exit code?
That's the beauty of these solutions (which are not modern BTW, OpenEJB/TomEE does it since 17 years): you can - if you desire - own the main and bootstrap logic. They all provide a main but it is trivial to wrap it with a custom one. In such a solution you simply need a way to communicate to the main the exit status.
A way is to use an ExitStatus bean representing this state and add it in CDI. This bean will be responsible:
- to store the status and share it with the main
- let the application finish once the exit status is set and the container is destroyed
Tip: if you don't want your code to depend on ExitStatus you can keep the same idea but using an Event<Execution> which would do the same in an observer decorrelating the main code from the exit status logic.
CDI ExitStatus
I'll use Apache Meecrowave for the container but TomEE Embedded, Wildfly Swarm , ... would use something pretty close.
First we need a server:
import org.apache.meecrowave.Meecrowave;
import javax.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class Launcher {
public static void main(final String[] args) {
final Meecrowave.Builder config = new Meecrowave.Builder();
config.setSkipHttp(true);
try (final Meecrowave meecrowave = new Meecrowave(config).bake()) {
// we need some magic
}
}
}
This main will just start a container without any Connector and deploy the classpath.
Now we need an ExitStatus bean:
import javax.enterprise.inject.Vetoed;
import java.util.concurrent.CountDownLatch;
@Vetoed
public class ExitStatus {
private volatile int status = 0;
private final CountDownLatch latch = new CountDownLatch(1);
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
latch.countDown();
}
public void await() {
try {
latch.await();
} catch (final InterruptedException e) {
Thread.interrupted();
}
}
}
Nothing fancy, a status the application will set and a latch to be able to notify the main we are done.
Wait, we need a bean but this one is @Vetoed? How would it work?
We need the main to have the instance of the ExitStatus class. There are two options there:
- let the bean be @ApplicationScoped and lookup the instance with CDI.current().getBeanManager()
- add our own instance to CDI through a producer
We'll use this last option here but other one works well too. To do so we'll just add a producer to our main (that is why it was decorated with @ApplicationScoped even if the class will not be used as a CDI bean):
import org.apache.meecrowave.Meecrowave;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Produces;
@ApplicationScoped
public class Launcher {
@Produces
public static ExitStatus status = new ExitStatus();
public static void main(final String[] args) {
final Meecrowave.Builder config = new Meecrowave.Builder();
config.setSkipHttp(true);
try (final Meecrowave meecrowave = new Meecrowave(config).bake()) {
status.await();
} finally {
System.exit(status.getStatus());
}
}
}
- status instance is our exit status holder
- the main has been updated to
- await the status is set before exiting
- call System.exit(...) with the right value
Timeout or not?
This solution works well and is actually quite easy to setup. However it has one pitfall: if nothing calls setStatus() then you will hang. To avoid it you can either add a timeout in the main (if after 30 minute we didn't exit then exit with status code xxx) or ensure you run some logic wrapped in a way ensuring setStatus() is called (a JobListener of JBatch can help for instance).
This is not a lot of work but don't forget it, in particular if you run a batch multiple times a day, otherwise you can start having concurrent processes on the machine for the same application which never leads to good results.
Conclusion
So today there is no reason to have to manage N containers and N ways to write applications. You can use the same for all your application types:
- batchs
- daemons
- webapps
- ...
Relying on the same stack and infrastructure allows to share a highly valuable knowledge in your team(s) which is no more a limitation since you now fully control your (flat or not) classpath and bootstrap logic.
Enjoy being embedded :).
From the same author:
In the same category: