{"record":{"id":"b3b7ea3ceba2a2ae","repo":"flowable/flowable-engine","slug":"optimistic-locking-exception-using-global-acquire-b3b7ea","errorCode":null,"errorMessage":"Optimistic locking exception (using global acquire lock) for engine {}","messagePattern":"Optimistic locking exception \\(using global acquire lock\\) for engine (.+?)","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"modules/flowable-job-service/src/main/java/org/flowable/job/service/impl/asyncexecutor/AcquireTimerJobsRunnable.java","lineNumber":244,"sourceCode":"                commandExecutor.execute(new BulkMoveTimerJobsToExecutableJobsCmd(jobManager, timerJobs));\n            } else {\n                commandExecutor.execute(new MoveTimerJobsToExecutableJobsCmd(jobManager, timerJobs));\n            }\n\n        } catch (FlowableOptimisticLockingException optimisticLockingException) {\n            logOptimisticLockingException(optimisticLockingException);\n            unlockTimerJobs(timerJobs); // jobs have been acquired before, so need to unlock when exception happens here\n\n        } catch (Throwable t) {\n            LOGGER.warn(\"exception during timer job move for engine {}. Exception message: {}\", getEngineName(), t.getMessage(), t);\n            unlockTimerJobs(timerJobs); // jobs have been acquired before, so need to unlock when exception happens here\n\n        }\n    }\n\n    protected void logOptimisticLockingException(FlowableOptimisticLockingException optimisticLockingException) {\n        if (configuration.isGlobalAcquireLockEnabled()) {\n            LOGGER.warn(\"Optimistic locking exception (using global acquire lock) for engine {}\", getEngineName(), optimisticLockingException);\n\n        } else {\n            LOGGER.debug(\n                \"Optimistic locking exception during async job acquisition. If you have multiple async executors running against the same database, \" +\n                    \"this exception means that this thread tried to acquire a due async job, which already was acquired by another \" +\n                    \"async executor acquisition thread.This is expected behavior in a clustered environment. \" +\n                    \"You can ignore this message if you indeed have multiple async executor acquisition threads running against the same database. \" +\n                    \"For engine {}. Exception message: {}\",\n                getEngineName(), optimisticLockingException.getMessage());\n\n        }\n    }\n\n    protected void sleep(long millisToWait) {\n        if (millisToWait > 0) {\n            try {\n                if (LOGGER.isDebugEnabled()) {\n                    LOGGER.debug(\"timer job acquisition thread for engine {} sleeping for {} millis\", getEngineName(), millisToWait);","sourceCodeStart":226,"sourceCodeEnd":262,"githubUrl":"https://github.com/flowable/flowable-engine/blob/d6d39ce1c69ff244f2d9dc6af756a9b95e865586/modules/flowable-job-service/src/main/java/org/flowable/job/service/impl/asyncexecutor/AcquireTimerJobsRunnable.java#L226-L262","documentation":"WARN log emitted by logOptimisticLockingException when a FlowableOptimisticLockingException occurs during timer job acquisition or move while the global acquire lock is enabled. It indicates two threads raced to modify the same timer job entity despite the global lock, so the DB optimistic-lock version check rejected one of them.","triggerScenarios":"Concurrent update of the same timer job row during acquisition or the move-to-executable phase with globalAcquireLockEnabled=true; stale entity versions from a previous failed cycle.","commonSituations":"Clustered nodes with inconsistently enabled global locks; long-running move operations overlapping the next acquire cycle; lock owner mismatches after node restarts.","solutions":["Ensure globalAcquireLockEnabled is identically configured on every node","Verify the lock is actually held for the whole acquire+move cycle (check custom lock manager TTL vs cycle duration)","Ignore if rare — the runnable logs it and unlocks affected jobs, and the job is retried","Check for clock skew between nodes that could shorten effective lock TTL"],"exampleFix":"// before: asymmetric config\nnodeA: asyncExecutor.setGlobalAcquireLockEnabled(true);\nnodeB: asyncExecutor.setGlobalAcquireLockEnabled(false);\n// after: identical on all nodes\nasyncExecutor.setGlobalAcquireLockEnabled(true);","handlingStrategy":"retry","validationCode":"if (nodes.stream().map(n -> n.getGlobalAcquireLockEnabled()).distinct().count() > 1) throw new IllegalStateException(\"inconsistent global lock config\");","typeGuard":null,"tryCatchPattern":"try {\n    moveJobs();\n} catch (FlowableOptimisticLockingException e) {\n    unlockTimerJobs(timerJobs);\n    // transient: job will be retried next cycle\n}","preventionTips":["Keep globalAcquireLockEnabled uniform across the cluster","Ensure lock TTL exceeds acquire+move cycle duration","Synchronize clocks across nodes"],"tags":["flowable","timer-jobs","optimistic-locking","global-lock","cluster"],"backgroundTag":"conflicting-config-options","analyzedSha":"d6d39ce1c69ff244f2d9dc6af756a9b95e865586","analyzedAt":"2026-09-11T06:41:19.413Z","contentChangedAt":"2026-09-11T06:41:19.413Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}