MSE XXL-JOB extends the open-source version with a graceful shutdown feature. Before an application shuts down, it notifies the scheduler to stop dispatching new tasks and waits for all running tasks to complete before safely taking the application offline—enabling restarts without service interruptions. This topic explains how to enable graceful shutdown in Alibaba Cloud’s XXL-JOB and helps you manage real-world scenarios involving application restarts or decommissioning.
Overview of graceful shutdown for scheduled tasks
In production environments, scheduled tasks within an application run continuously at fixed intervals. When the application restarts during deployment, running tasks are forcibly interrupted. This can result in incomplete data and a sharp drop in scheduling success rates, ultimately compromising business data integrity. Key issues include the following:
-
Task execution interruption: A task is running when the application process stops, interrupting business logic and potentially leaving data incomplete.
-
Scheduling failure during deployment: During restarts, the scheduler may assign tasks to nodes that are shutting down, causing failures and reducing overall processing efficiency.
Therefore, in scenarios involving scheduled task execution, implement graceful shutdown to ensure smooth operation during rolling deployments and restarts.
Implementing graceful shutdown with open-source XXL-JOB
Execution model of open-source XXL-JOB and graceful shutdown challenges
The current open-source XXL-JOB lacks built-in support for graceful shutdown during continuous task execution. To achieve this, you must customize the code. Before modifying XXL-JOB, analyze its end-to-end task dispatch and execution flow. This flow involves two modules: XXL-JOB Admin and XXL-Job Executor.
This section explains the core interaction flow of XXL-Job and highlights logic points affecting graceful shutdown—providing a reference for customization. Key issues include the following:
Issue 1: Delayed traffic removal from shutting-down nodes
When an executor shuts down, it fails to update the scheduler’s list of active executors promptly. As a result, the scheduler may still assign tasks to the shutting-down node, causing failures.
Logic 1: Executor registration
-
After your application starts with the XXL-JOB SDK, it initializes the ExecutorRegistryThread to continuously send heartbeats to the scheduling center.
-
The scheduling center uses JobRegistryHelper to write the registered executor information into the xxl_job_registry database table.
-
JobRegistryHelper runs a background thread that periodically updates the address_list field (the actual list used) in the xxl_job_group table.
// async execute
registryOrRemoveThreadPool.execute(new Runnable() {
@Override
public void run() {
int ret = XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().registryUpdate(registryParam.getRegistryGroup(), registryParam.getRegistryKey(), registryParam.getRegistryValue(), new Date());
if (ret < 1) {
XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().registrySave(registryParam.getRegistryGroup(), registryParam.getRegistryKey(), registryParam.getRegistryValue(), new Date());
}
// fresh
freshGroupRegistryInfo(registryParam);
}
}
);
return ReturnT.SUCCESS;
}
registryMonitorThread = new Thread(new Runnable() {
@Override
public void run() {
while (!toStop) {
try {
// auto registry group
List<XxlJobGroup> groupList = XxlJobAdminConfig.getAdminConfig().getXxlJobGroupDao().findByAddressType(0);
if (groupList!=null && !groupList.isEmpty()) {
// remove dead address (admin/executor)
List<Integer> ids = XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().findDead(RegistryConfig.DEAD_TIMEOUT, new Date());
if (ids!=null && ids.size()>0) {...}
// fresh online address (admin/executor)
HashMap<String, List<String>> appAddressMap = new HashMap<>();
List<XxlJobRegistry> list = XxlJobAdminConfig.getAdminConfig().getXxlJobRegistryDao().findAll(RegistryConfig.DEAD_TIMEOUT, new Date());
if (list != null) {...}
// fresh group address
for (XxlJobGroup group: groupList) {
List<String> registryList = appAddressMap.get(group.getAppname());
String addressListStr = null;
if (registryList!=null && !registryList.isEmpty()) {...}
group.setAddressList(addressListStr);
group.setUpdateTime(new Date());
XxlJobAdminConfig.getAdminConfig().getXxlJobGroupDao().update(group);
}
}
} catch (Exception e) {...}
try {
TimeUnit.SECONDS.sleep(RegistryConfig.BEAT_TIMEOUT);
} catch (InterruptedException e) {
if (!toStop) {...}
}
}
logger.info(">>>>>>>>>>> xxl-job, job registry monitor thread stop");
}
});
Logic 2: Selecting an online executor
-
When triggered by the scheduler thread, a task is handed to XxlJobTrigger for execution.
-
Before execution, XxlJobTrigger reads the available executor list from the address_list field in the xxl_job_group table.
-
ExecutorRouter selects one machine from this list based on the configured routing policy.
-
After selection, the task is dispatched via RPC to the chosen IP node. If the selected node is already shutting down, the trigger fails.
public static void trigger(int jobId,
TriggerTypeEnum triggerType,
int failRetryCount,
String executorShardingParam,
String executorParam,
String addressList) {
// load data
XxlJobInfo jobInfo = XxlJobAdminConfig.getAdminConfig().getXxlJobInfoDao().loadById(jobId);
if (jobInfo == null) {
logger.warn(">>>>>>>>>>> trigger fail, jobId invalid, jobId={}", jobId);
return;
}
if (executorParam != null) {
jobInfo.setExecutorParam(executorParam);
}
int finalFailRetryCount = failRetryCount>=0?failRetryCount:jobInfo.getExecutorFailRetryCount();
XxlJobGroup group = XxlJobAdminConfig.getAdminConfig().getXxlJobGroupDao().load(jobInfo.getJobGroup()); // IDE flags an error in this load method
// cover addressList
if (addressList!=null && addressList.trim().length()>0) {
group.setAddressType(1);
group.setAddressList(addressList.trim());
}
}
Summary: Because executor registration and task-triggering use asynchronously updated lists, there is a delay in refreshing the list of available online executors.
Issue 2: Forced task interruption
Currently, when XXL-Job Executor shuts down, it immediately interrupts running task threads (marking them as failed) and discards all queued tasks (also marking them as failed).
Logic 1: Dispatching tasks to executors
-
When an executor receives a task, it creates a JobThread for that task ID to handle execution.
-
The current trigger request is added to the task thread’s execution queue. Different blocking strategies yield different behaviors.
-
The JobThread continuously loops, reading trigger records from the queue and executing the corresponding JobHandler to process business logic.
-
After execution, the result is submitted to the TriggerCallbackThread’s callback queue, and the thread proceeds to the next execution.
-
During shutdown, XxlJobExecutor.destroy interrupts running threads and clears pending scheduling requests from the queue.
Logic 2: Reporting execution results
-
TriggerCallbackThread continuously processes the result queue and batches results to the scheduling center.
-
If sending results fails, they are written to a local file for retry.
-
The scheduling center updates execution records in the database upon receiving results.
public void destroy(){
// destroy executor-server
stopEmbedServer();
// destroy jobThreadRepository
if (jobThreadRepository.size() > 0) {
for (Map.Entry<Integer, JobThread> item: jobThreadRepository.entrySet()) {
JobThread oldJobThread = removeJobThread(item.getKey(), "web container destroy and kill the job.");
// wait for job thread push result to callback queue
if (oldJobThread != null) {
try {
oldJobThread.join();
} catch (InterruptedException e) {
logger.error(">>>>>>>>>>> xxl-job, JobThread destroy(join) error, jobId:{}", item.getKey(), e);
}
}
}
jobThreadRepository.clear();
}
jobHandlerRepository.clear();
// destroy JobLogFileCleanThread
JobLogFileCleanThread.getInstance().toStop();
// destroy TriggerCallbackThread
TriggerCallbackThread.getInstance().toStop();
}
Summary: During shutdown, removeJobThread directly interrupts running task threads, and queued tasks are ignored and marked as failed.
Implementing graceful shutdown in open-source XXL-JOB
Based on the execution model of open-source XXL-JOB, implement graceful shutdown using the core three-step approach: first stop new traffic, then wait for in-flight tasks to finish, and finally shut down the process.
In Spring Boot mode, the com.xxl.job.core.executor.XxlJobExecutor#destroy method in the XXL-JOB Core module is automatically called during application shutdown. It includes cleanup logic for the executor but does not fully support graceful shutdown. Customize the code as follows:
Step 1: Remove the node from traffic
-
The XxlJobExecutor#destroy method calls stopEmbedServer(), which stops heartbeat registration and sends a registryRemove request to remove the current executor from the scheduler.
-
The scheduler removes the node from the xxl_job_registry table. However, as explained earlier, the actual dispatch uses the address_list in the xxl_job_group table—which is not updated synchronously—so traffic removal is incomplete.
-
Modify the scheduler to complete traffic removal. Choose one of the following options:
-
In JobRegistryHelper.registryRemove, add logic to refresh the address_list in the xxl_job_group table, or implement this refresh in freshGroupRegistryInfo.
-
Modify XxlJobTrigger#trigger() to read the address list directly from the xxl_job_registry table for auto-registered groups.
After this change, traffic removal is complete.
Step 2: Wait for in-flight tasks to complete
-
Modify the XxlJobExecutor#destroy method to wait for all queued tasks to finish. Use the following code as a reference:
public void destroy(){
// destroy executor-server
stopEmbedServer();
// destroy jobThreadRepository
if (jobThreadRepository.size() > 0) {
List keyList = new ArrayList(jobThreadRepository.keySet());
for (int i=0; i < keyList.size(); i++) {
JobThread jobThread = jobThreadRepository.get(keyList.get(i));
// Wait for all tasks in the queue to complete
while (jobThread != null && jobThread.isRunningOrHasQueue()) {
try {
TimeUnit.SECONDS.sleep(1L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
jobHandlerRepository.clear();
// destroy JobLogFileCleanThread
JobLogFileCleanThread.getInstance().toStop();
// destroy TriggerCallbackThread
TriggerCallbackThread.getInstance().toStop();
}
At this point, waiting for running tasks is complete. You can also customize shutdown logic for different task types.
Step 3: Shut down the application process
-
During deployment, use kill -15 in your deployment script to trigger the JVM shutdown hook. Add a timeout to force-kill if needed.
-
Integrate graceful shutdown with Spring Boot Actuator and use the /actuator/shutdown endpoint to take the application offline.
Prerequisites
-
Database engine version must be 2.1.0 or later. For details, see XXL-JOB engine versions.
-
Clients must integrate the SchedulerX plugin package. For details, see XXL-JOB plugin versions.
<dependency>
<groupId>com.aliyun.schedulerx</groupId>
<artifactId>schedulerx3-plugin-xxljob</artifactId>
<version>latest version</version>
</dependency>
How to enable graceful shutdown
Configure and enable graceful shutdown based on your deployment scenario. This process has two main steps:
Step 1: Initialize integration in executor frameworks
Depending on your application’s deployment model, integrate initialization differently.
Model 1: Spring Boot applications (recommended)
If your application uses Spring Boot to integrate XXL-Job executor, graceful shutdown is automatically enabled. Complete these two steps:
-
Add the SchedulerX plugin Maven dependency. For details, see XXL-JOB plugin versions.
<dependency>
<groupId>com.aliyun.schedulerx</groupId>
<artifactId>schedulerx3-plugin-xxljob</artifactId>
<version>latest version</version>
</dependency>
-
Add application configuration to enable graceful shutdown. For parameter details, see Parameter settings.
# Enable graceful shutdown
xxl.job.executor.shutdownMode=WAIT_ALL
Model 2: Spring applications
If your application is a Spring-based web app, in addition to adding the Maven dependency and startup parameters (see Model 1: Spring Boot applications (recommended)), initialize XxlJobExecutorEnhancerInitializer by adding the following to web.xml:
<web-app>
<context-param>
<!-- Enhance xxljob executor with Spring ApplicationContextInitializer -->
<param-name>globalInitializerClasses</param-name>
<param-value>com.aliyun.schedulerx.xxljob.enhance.XxlJobExecutorEnhancerInitializer</param-value>
</context-param>
</web-app>
Model 3: Frameless Java applications
If your application uses the frameless approach from the XXL-Job executor example and starts via pure Java code, initialize graceful shutdown through custom code. First, add the Maven dependency and startup parameters (see Model 1: Spring Boot applications (recommended)). Reference implementation:
-
Before starting the executor, add: EnhancerLoader.load(xxlJobProp) to load enhancements.
-
Before starting the executor, add: Runtime.getRuntime().addShutdownHook(...) to register a shutdown hook.
Sample code
public static void main(String[] args) {
try {
// load executor prop
Properties xxlJobProp = FrameLessXxlJobConfig.loadProperties("xxl-job-executor.properties");
// Initialize and enhance xxl-job executor
EnhancerLoader.load(xxlJobProp);
// start xxl-job executor
FrameLessXxlJobConfig.getInstance().initXxlJobExecutor(xxlJobProp);
// Add graceful shutdown hook
Runtime.getRuntime().addShutdownHook(new Thread(){
@Override
public void run() {
FrameLessXxlJobConfig.getInstance().destroyXxlJobExecutor();
}
});
// Blocks until interrupted
while (true) {
try {
TimeUnit.HOURS.sleep(1);
} catch (InterruptedException e) {
break;
}
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
} finally {
// destroy
FrameLessXxlJobConfig.getInstance().destroyXxlJobExecutor();
}
}
Step 2: Handle application shutdown
Self-managed deployment using kill -15
In self-managed CI/CD pipelines, the application stop step typically uses a stop.sh script. Include graceful shutdown logic in this script. See the following example.
Sample shutdown script:
# After successful startup, the process ID is written to app.pid
PID="{application deployment path}/app.pid"
FORCE=1
if [ -f ${PID} ]; then
TARGET_PID=`cat ${PID}`
kill -15 ${TARGET_PID}
loop=1
while(( $loop<=5 ))
do
## health check to confirm the process has stopped; customize based on your app
health
if [ $? == 0 ]; then
echo "check $loop times, current app has not stop yet."
sleep 5s
let "loop++"
else
FORCE=0
break
fi
done
if [ $FORCE -eq 1 ]; then
echo "App(pid:${TARGET_PID}) stop timeout, forced termination."
kill -9 ${TARGET_PID}
fi
rm -rf ${PID}
echo "App(pid:${TARGET_PID}) stopped successful."
fi
Kubernetes containerized deployment using PreStop
Kubernetes pod lifecycle management supports graceful shutdown by default. You can also use a preStop hook with an exec script or HTTP request to implement custom logic.
-
Default behavior: If your app is PID 1 in the container, Kubernetes sends SIGTERM first for graceful shutdown.
-
Custom preStop: For complex multi-process containers, configure a preStop script to stop the app using kill -15 PID or call a predefined stop.sh script.
Important
The pod’s terminationGracePeriodSeconds setting controls the maximum graceful shutdown time (default: 30 seconds). Configure it based on your business needs.
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
replicas: 3
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
containers:
- name: my-app-container
image: my-app-image:latest
lifecycle:
preStop:
exec:
# command: ["/bin/sh", "-c", "kill -15 PID && sleep 30"]
command: ["/bin/sh", "-c", "script path/stop.sh"]
Automatic integration on Alibaba Cloud application platforms
Coming soon.
Parameter settings
Configure the following parameter in your application to enable graceful shutdown. It supports two shutdown strategies.
# Graceful shutdown mode: WAIT_ALL (wait for all tasks) or WAIT_RUNNING (wait only for running tasks).
# If not set, XXL-JOB uses its original logic (graceful shutdown disabled by default).
xxl.job.executor.shutdownMode=WAIT_ALL
|
Shutdown mode
|
Description
|
|
Wait for all (WAIT_ALL)
|
(Recommended) The application waits for all received tasks—both running and queued—to complete before exiting.
|
|
Wait for running (WAIT_RUNNING)
|
The application waits only for tasks currently being processed. Queued tasks are discarded.
|