flowable/flowable-engine · error · ActivitiOptimisticLockingException

One of the entities <persistentObjectClass> was updated by a

Error message

One of the entities <persistentObjectClass> was updated by another transaction concurrently while trying to do a bulk delete

What it means

During a bulk delete, DbSqlSession.execute() checks whether the deleted entities implement HasRevision; if so, it requires the number of deleted rows to equal the number of entities submitted. If fewer rows were deleted, some rows were modified (revision changed) by another concurrent transaction, so it throws ActivitiOptimisticLockingException. This prevents silently deleting rows whose state was changed after they were loaded.

Source

Thrown at modules/flowable5-engine/src/main/java/org/activiti/engine/impl/db/DbSqlSession.java:307

        @Override
        public void execute() {

            if (persistentObjects.isEmpty()) {
                return;
            }

            String bulkDeleteStatement = dbSqlSessionFactory.getBulkDeleteStatement(persistentObjectClass);
            bulkDeleteStatement = dbSqlSessionFactory.mapStatement(bulkDeleteStatement);
            if (bulkDeleteStatement == null) {
                throw new ActivitiException("no bulk delete statement for " + persistentObjectClass + " in the mapping files");
            }

            // It only makes sense to check for optimistic locking exceptions for objects that actually have a revision
            if (persistentObjects.get(0) instanceof HasRevision) {
                int nrOfRowsDeleted = sqlSession.delete(bulkDeleteStatement, persistentObjects);
                if (nrOfRowsDeleted < persistentObjects.size()) {
                    throw new ActivitiOptimisticLockingException("One of the entities " + persistentObjectClass
                            + " was updated by another transaction concurrently while trying to do a bulk delete");
                }
            } else {
                sqlSession.delete(bulkDeleteStatement, persistentObjects);
            }
        }

        @Override
        public Class<? extends PersistentObject> getPersistentObjectClass() {
            return persistentObjectClass;
        }

        public void setPersistentObjectClass(
                Class<? extends PersistentObject> persistentObjectClass) {
            this.persistentObjectClass = persistentObjectClass;
        }

        public List<PersistentObject> getPersistentObjects() {

View on GitHub (pinned to d6d39ce1c6)

Solutions

  1. Re-fetch the entities in the same transaction right before the bulk delete so revisions are current, then retry the delete.
  2. Catch ActivitiOptimisticLockingException and retry the whole operation with backoff; the conflicting transaction has already committed.
  3. Reduce concurrency on the affected entity (e.g. async executor locking, exclusive jobs) so competing writers don't interleave.
  4. If rows are legitimately gone, verify whether another flow already deleted them and treat the delete as idempotent.

Example fix

// before: delete with stale entities
List<TaskEntity> stale = loadTasks(); // fetched long ago
commandContext.getDbSqlSession().deleteAll(stale);

// after: retry on optimistic lock
try {
  commandContext.getDbSqlSession().deleteAll(loadTasks()); // fresh fetch
} catch (ActivitiOptimisticLockingException e) {
  retryWithBackoff(() -> commandContext.getDbSqlSession().deleteAll(loadTasks()));
}
Defensive patterns

Strategy: retry

Validate before calling

// Re-check revisions immediately before bulk delete
boolean fresh = persistentObjects.stream()
    .filter(po -> po instanceof HasRevision)
    .allMatch(po -> revisionInDb(po.getId()) == ((HasRevision) po).getRevision());
if (!fresh) { reloadEntities(); }

Try / catch

try {
    dbSqlSession.deleteAll(entities);
} catch (ActivitiOptimisticLockingException e) {
    // reload entities with current revisions and retry with backoff
    retryWithBackoff(3, () -> dbSqlSession.deleteAll(loadFresh()));
}

Prevention

When it happens

Trigger: Two transactions load the same revisioned entity (implements HasRevision); one updates it (bumping REV_), the other executes a bulk delete containing the stale entity; sqlSession.delete returns nrOfRowsDeleted < persistentObjects.size().

Common situations: Concurrent job execution or task updates competing with a bulk delete in the same process-engine cluster; long-running transactions holding stale entity snapshots; retry storms where two threads process the same execution.

Related errors


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