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

  1. Use the exact same workerId for acquire and for complete/fail/terminate calls
  2. Catch FlowableIllegalArgumentException and re-acquire the job instead of forcing completion
  3. Check lock duration: if processing exceeds the lock time, extend the lock or increase the acquire lock duration
  4. 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

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


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)