flowable/flowable-engine · error · FlowableIllegalArgumentException

{workerId} does not hold a lock on the requested job

Error message

{workerId} does not hold a lock on the requested job

What it means

resolveJob compares the supplied workerId with the job's lockOwner and throws FlowableIllegalArgumentException when they differ. Only the worker that currently holds the lock is allowed to complete, fail, or release the job, preventing one worker from manipulating another worker's job.

Source

Thrown at modules/flowable-engine/src/main/java/org/flowable/engine/impl/cmd/AbstractExternalWorkerJobCmd.java:91

    protected ExternalWorkerJobEntity resolveJob(CommandContext commandContext) {
        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)

Solutions

  1. Use the exact workerId that acquired the job for all subsequent complete/fail/release calls on it.
  2. Give each worker instance a stable unique workerId (e.g. hostname+PID persisted across restarts).
  3. If the lock was lost, treat the job as no longer owned and re-acquire; optionally catch the exception and refresh the job state.

Example fix

// before
service.complete(job.getId(), "shared-worker", variables); // lock held by worker-7
// after
service.complete(job.getId(), job.getLockOwner(), variables); // pass acquiring workerId
Defensive patterns

Strategy: try-catch

Validate before calling

ExternalWorkerJob job = jobService.createExternalWorkerJobQuery().externalJobId(jobId).singleResult();
if (job != null && !Objects.equals(workerId, job.getLockOwner())) {
    // lock lost: re-acquire instead of completing
}

Try / catch

try {
    jobService.complete(jobId, workerId, variables);
} catch (FlowableIllegalArgumentException e) {
    if (e.getMessage().contains("does not hold a lock")) {
        // lock expired or lost: re-acquire the job
    }
}

Prevention

When it happens

Trigger: Calling complete/fail/release with a workerId different from the one used at acquisition time (job.getLockOwner()), e.g. after the lock expired and was re-acquired by another worker, or with a duplicate/default workerId.

Common situations: Multiple workers sharing a hardcoded workerId then colliding on lock ownership; job lock expired and re-acquired by a different instance; a restart generated a new workerId while the client reused an old job reference; load-balanced consumers using per-request ids.

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/127ee67e3f64fe52. Report an issue: GitHub.