flowable/flowable-engine · warning · ActivitiOptimisticLockingException

Could not lock process instance

Error message

Could not lock process instance

What it means

The engine uses optimistic DB locking to grant exclusive async-job acquisition to one executor node. updateProcessInstanceLockTime issues an UPDATE of the lock columns; if the update affects 0 rows, the expected process instance row was not in the state assumed (deleted, or lock version changed) and ActivitiOptimisticLockingException is thrown.

Source

Thrown at modules/flowable5-engine/src/main/java/org/activiti/engine/impl/persistence/entity/ExecutionEntityManager.java:226

        getDbSqlSession().update("updateExecutionTenantIdForDeployment", params);
    }

    public void updateProcessInstanceLockTime(String processInstanceId) {
        CommandContext commandContext = Context.getCommandContext();
        Date expirationTime = commandContext.getProcessEngineConfiguration().getClock().getCurrentTime();
        int lockMillis = commandContext.getProcessEngineConfiguration().getAsyncExecutorAsyncJobLockTimeInMillis();
        GregorianCalendar lockCal = new GregorianCalendar();
        lockCal.setTime(expirationTime);
        lockCal.add(Calendar.MILLISECOND, lockMillis);

        HashMap<String, Object> params = new HashMap<>();
        params.put("id", processInstanceId);
        params.put("lockTime", lockCal.getTime());
        params.put("expirationTime", expirationTime);

        int result = getDbSqlSession().update("updateProcessInstanceLockTime", params);
        if (result == 0) {
            throw new ActivitiOptimisticLockingException("Could not lock process instance");
        }
    }

    public void clearProcessInstanceLockTime(String processInstanceId) {
        HashMap<String, Object> params = new HashMap<>();
        params.put("id", processInstanceId);

        getDbSqlSession().update("clearProcessInstanceLockTime", params);
    }

}

View on GitHub (pinned to d6d39ce1c6)

Solutions

  1. Treat this as a transient contention signal: re-fetch the job/process instance and retry the acquisition if it still exists.
  2. Verify the job's process instance still exists; if not, remove/expire the stale job rather than locking.
  3. Reduce executor contention: tune async executor lock time and pool size so fewer nodes grab the same jobs simultaneously.
  4. Upgrade to a Flowable version where job acquisition handles vanished instances gracefully (job is skipped instead of failing).
Defensive patterns

Strategy: retry

Validate before calling

Job job = managementService.createJobQuery().jobId(jobId).singleResult();
if (job == null || job.getProcessInstanceId() == null) return; // stale job, skip lock attempt

Try / catch

try {
    acquireAndLock(instanceId);
} catch (ActivitiOptimisticLockingException e) {
    // another executor won the race or instance vanished: back off and retry once
    Thread.sleep(retryBackoffMs);
    retryAcquisition(instanceId);
}

Prevention

When it happens

Trigger: Async executor/ job acquisition calls updateProcessInstanceLockTime for a process instance that was deleted concurrently by another transaction, or whose exclusive-lock row changed between read and update; multiple job executors racing for the same exclusive job.

Common situations: Clustered deployments with several async executor nodes contending for jobs; jobs whose process instance was deleted while still queued; stale job entries surviving instance deletion in older versions.

Related errors


AI-assisted analysis of flowable/flowable-engine@d6d39ce1c6 (2026-09-11). Data as JSON: /api/errors/ef22b7b152423076. Report an issue: GitHub.