{"record":{"id":"d90882296bb21249","repo":"flowable/flowable-engine","slug":"workerid-does-not-hold-a-lock-on-the-requested-d90882","errorCode":null,"errorMessage":"${workerId} does not hold a lock on the requested job","messagePattern":"(.+?) does not hold a lock on the requested job","errorType":"exception","errorClass":"FlowableForbiddenException","httpStatus":403,"severity":"error","filePath":"modules/flowable-external-job-rest/src/main/java/org/flowable/external/job/rest/service/api/acquire/ExternalWorkerAcquireJobResource.java","lineNumber":110,"sourceCode":"\n    @ApiOperation(value = \"Complete an External Worker Jobs\", code = 204, tags = { \"Acquire and Execute\" })\n    @ApiResponses({\n            @ApiResponse(code = 204, message = \"Indicates the job was successfully completed.\"),\n            @ApiResponse(code = 400, message = \"Indicates the request was invalid.\"),\n            @ApiResponse(code = 403, message = \"Indicates the user does not have the rights complete the job.\"),\n            @ApiResponse(code = 404, message = \"Indicates the job does not exist.\"),\n    })\n    @PostMapping(value = \"/acquire/jobs/{jobId}/complete\", produces = \"application/json\")\n    public ResponseEntity<?> completeJob(@PathVariable String jobId, @RequestBody ExternalWorkerJobCompleteRequest request) {\n        String workerId = request.getWorkerId();\n        if (StringUtils.isEmpty(workerId)) {\n            throw new FlowableIllegalArgumentException(\"workerId is required\");\n        }\n\n        ExternalWorkerJob job = getExternalWorkerJobById(jobId);\n\n        if (!workerId.equals(job.getLockOwner())) {\n            throw new FlowableForbiddenException(workerId + \" does not hold a lock on the requested job\");\n        }\n\n        if (job.getProcessInstanceId() != null) {\n            if (managementService != null) {\n                if (restApiInterceptor != null) {\n                    restApiInterceptor.completeExternalWorkerJob(job, request);\n                }\n\n                managementService.createExternalWorkerCompletionBuilder(job.getId(), workerId)\n                        .variables(extractVariables(request.getVariables()))\n                        .complete();\n            } else {\n                throw new FlowableException(\"Cannot complete BPMN job. There is no BPMN engine available\");\n            }\n        } else if (ScopeTypes.CMMN.equals(job.getScopeType())) {\n            if (cmmnManagementService != null) {\n                if (restApiInterceptor != null) {\n                    restApiInterceptor.completeExternalWorkerJob(job, request);","sourceCodeStart":92,"sourceCodeEnd":128,"githubUrl":"https://github.com/flowable/flowable-engine/blob/d6d39ce1c69ff244f2d9dc6af756a9b95e865586/modules/flowable-external-job-rest/src/main/java/org/flowable/external/job/rest/service/api/acquire/ExternalWorkerAcquireJobResource.java#L92-L128","documentation":"FlowableForbiddenException thrown in completeJob when the supplied workerId differs from the job's current lock owner (job.getLockOwner()). The engine only lets the worker that acquired (and still holds) the lock complete the job. Mapped to HTTP 403.","triggerScenarios":"POST /external-worker/acquire/jobs/{jobId}/complete where request workerId != ACT_RU_EXTERNAL_JOB.LOCK_OWNER_ — e.g. a different worker instance attempts completion, or the lock expired and was re-acquired by another worker.","commonSituations":"Multiple workers sharing a workerId prefix but sending different actual ids; lock duration expired, job re-acquired elsewhere, then original worker tries to complete; load-balanced workers handling each other's acquired jobs; restart of worker generating a new random id while retrying completion of an old job.","solutions":["Use exactly the workerId that was sent during acquisition when completing.","Increase lockDuration or renew locks if long-running work causes lock expiry.","On 403, re-acquire the topic and reprocess rather than retrying completion.","Ensure only one worker instance processes a given acquired job."],"exampleFix":"// before\nacquire(workerId: \"worker-A\"); complete(jobId, workerId: \"worker-B\"); // 403\n// after\nAcquiredJob job = acquire(\"worker-A\");\ncomplete(job.getId(), \"worker-A\"); // same id","handlingStrategy":"try-catch","validationCode":"// client can pre-check only if it stored the lock owner from acquisition\nboolean isLockOwner = jobId.equals(currentLock.getJobId()) && workerId.equals(currentLock.getWorkerId());","typeGuard":null,"tryCatchPattern":"try {\n    restClient.complete(jobId, request);\n} catch (HttpClientErrorException.Forbidden e) {\n    log.warn(\"Lost lock on job {} (expired or re-acquired); re-acquiring\", jobId);\n    reacquireAndRetry(work);\n}","preventionTips":["Keep lockDuration comfortably larger than worst-case processing time.","Complete jobs promptly before the lock expires.","Never share acquired jobs across worker instances.","Regenerate deterministic workerIds across restarts only if safe, else reprocess on 403."],"tags":["rest","forbidden","locking","external-worker"],"backgroundTag":"permission-denied","analyzedSha":"d6d39ce1c69ff244f2d9dc6af756a9b95e865586","analyzedAt":"2026-09-11T06:41:19.413Z","contentChangedAt":"2026-09-11T06:41:19.413Z","schemaVersion":2},"datasetVersion":"2026-09-14T11:17:12.474Z"}