flowable/flowable-engine · error · FlowableIllegalArgumentException
does not hold a lock on the requested job
Error message
does not hold a lock on the requested job
What it means
Thrown by resolveJob() as FlowableIllegalArgumentException when the given workerId does not equal the job's lockOwner. Flowable pessimistically locks acquired jobs to a specific worker; only the lock owner may complete, fail, or terminate it. This guards against two workers mutating the same job concurrently.
Solutions
- Use the exact same workerId for acquire and for complete/fail/terminate calls
- Catch FlowableIllegalArgumentException and re-acquire the job instead of forcing completion
- Check lock duration: if processing exceeds the lock time, extend the lock or increase the acquire lock duration
- Ensure worker ids are stable per instance or per logical consumer group consistently with your locking strategy
Example fix
// before // acquiring with workerId "worker-42", completing later with whatever id managementService.createExternalWorkerJobCompletionBuilder(jobId).workerId(currentName).complete(); // after managementService.createExternalWorkerJobCompletionBuilder(jobId).workerId(ACQUIRED_WORKER_ID).complete();
Defensive patterns
Strategy: try-catch
Validate before calling
boolean ownsJob = workerId != null && workerId.equals(job.getLockOwner());
Try / catch
try {
completionBuilder.externalJobId(jobId).workerId(workerId).complete();
} catch (FlowableIllegalArgumentException e) {
LOGGER.warn("Lock lost on job {}: {}", jobId, e.getMessage());
// re-acquire or give the job back
} Prevention
- Use one identical workerId for acquire and complete
- Set lock durations comfortably above worst-case processing time
- Handle lock loss by re-acquiring rather than retrying completion with the same stale lock
When it happens
Trigger: Worker A tries to complete a job that worker B currently has locked; workerId changed (restart with new generated id) while old lock persists; executing a completion command on a job whose lock expired and was re-acquired by another worker; hardcoding or misconfiguring workerId so it differs from the acquisition one.
Common situations: Multiple replicas of a worker sharing one workerId configuration that is actually instance-specific; auto-scaling generating new worker ids while old handlers still run; job timeout re-assignment between acquire and complete.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- Cannot delete when the job is being executed. Try again…
- Cannot delete job when the job is being executed. Try again…
- Could not acquire lock " + lockName + ". Current lock…
- Could not find a scope execution for compensation boundary…
- Exception while autodeploying DMN definitions for resource
AI-assisted analysis of flowable/flowable-engine@d6d39ce1c6 (2026-09-11).
Data as JSON: /api/errors/2d53b2ef43d234bd.
Report an issue: GitHub.
Appendix: source
Thrown at modules/flowable-job-service/src/main/java/org/flowable/job/service/impl/cmd/AbstractExternalWorkerJobCmd.java:74
protected ExternalWorkerJobEntity resolveJob() {
if (StringUtils.isEmpty(externalJobId)) {
throw new FlowableIllegalArgumentException("externalJobId must not be empty");
}
if (StringUtils.isEmpty(workerId)) {
throw new FlowableIllegalArgumentException("workerId must not be empty");
}
ExternalWorkerJobEntityManager externalWorkerJobEntityManager = jobServiceConfiguration.getExternalWorkerJobEntityManager();
ExternalWorkerJobEntity job = externalWorkerJobEntityManager.findById(externalJobId);
if (job == null) {
throw new FlowableObjectNotFoundException("No External Worker job found for id: " + externalJobId, ExternalWorkerJobEntity.class);
}
if (!Objects.equals(workerId, job.getLockOwner())) {
throw new FlowableIllegalArgumentException(workerId + " does not hold a lock on the requested job");
}
return job;
}
}
View on GitHub (pinned to d6d39ce1c6)