{"record":{"id":"5cab29ae5665fb32","repo":"flowable/flowable-engine","slug":"exception-during-timer-job-move-for-engine-exc","errorCode":null,"errorMessage":"exception during timer job move for engine {}. Exception message: {}","messagePattern":"exception during timer job move for engine (.+?)\\. Exception message: (.+?)","errorType":"console","errorClass":null,"httpStatus":null,"severity":"error","filePath":"modules/flowable-job-service/src/main/java/org/flowable/job/service/impl/asyncexecutor/AcquireTimerJobsRunnable.java","lineNumber":236,"sourceCode":"        lifecycleListener.stopAcquiring(getEngineName());\n\n        return millisToWait;\n    }\n\n    protected void executeMoveTimerJobsToExecutableJobs(List<TimerJobEntity> timerJobs) {\n        try {\n            if (configuration.isGlobalAcquireLockEnabled()) {\n                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","sourceCodeStart":218,"sourceCodeEnd":254,"githubUrl":"https://github.com/flowable/flowable-engine/blob/d6d39ce1c69ff244f2d9dc6af756a9b95e865586/modules/flowable-job-service/src/main/java/org/flowable/job/service/impl/asyncexecutor/AcquireTimerJobsRunnable.java#L218-L254","documentation":"WARN log from the timer-job move step: a non-optimistic-lock Throwable was thrown while moving acquired timer jobs from the timer table to the executable job table. Because the jobs were already acquired (locked), the runnable unlocks them (unlockTimerJobs) before returning so they are not stranded.","triggerScenarios":"Exception during executeMoveTimerJobsToExecutableJobs: failure in MoveTimerJobsCmd / bulk move operation, DB errors while writing to ACT_RU_JOB, or version conflicts other than optimistic locking.","commonSituations":"DB constraint violations during the move; connection failure between acquire and move; duplicated timer job rows after a failed earlier migration.","solutions":["Read the chained stack trace for the underlying DB/command error","Verify the affected jobs were unlocked (the code calls unlockTimerJobs); if stranded locked jobs remain, reset their lock owner/time manually","Check ACT_RU_TIMER_JOB and ACT_RU_JOB for rows violating constraints (e.g. duplicate IDs)","Retry the cycle; jobs that were unlocked will be re-acquired"],"exampleFix":"// manually unstick stranded locked timer jobs\nUPDATE ACT_RU_TIMER_JOB SET LOCK_OWNER_ = NULL, LOCK_EXP_TIME_ = NULL WHERE LOCK_OWNER_ = 'dead-node';","handlingStrategy":"retry","validationCode":"// detect stranded locked timer jobs periodically\nSELECT LOCK_OWNER_, COUNT(*) FROM ACT_RU_TIMER_JOB WHERE LOCK_EXP_TIME_ < NOW() GROUP BY LOCK_OWNER_;","typeGuard":null,"tryCatchPattern":"try {\n    moveTimerJobs();\n} catch (Throwable t) {\n    unlockTimerJobs(timerJobs); // always release acquired locks before retry\n}","preventionTips":["Always unlock acquired jobs on failure (as the framework does)","Check for constraint conflicts between timer and executable job tables","Monitor for locked jobs with expired lock times"],"tags":["flowable","timer-jobs","job-move","database","async-executor"],"backgroundTag":"database-write-failed","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"}