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
- Re-fetch the entities in the same transaction right before the bulk delete so revisions are current, then retry the delete.
- Catch ActivitiOptimisticLockingException and retry the whole operation with backoff; the conflicting transaction has already committed.
- Reduce concurrency on the affected entity (e.g. async executor locking, exclusive jobs) so competing writers don't interleave.
- 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
- Fetch entities inside the same transaction that deletes them; never carry entity snapshots across transactions.
- Use exclusive jobs / executor locking to avoid two nodes mutating the same execution concurrently.
- Retry commands on ActivitiOptimisticLockingException — it is transient by design.
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
- ${updatedObject} was updated by another transaction concurre
- ${entity} was updated by another transaction concurrently
- <persistentObject> was updated by another transaction concur
- <updatedObject> was updated by another transaction concurren
- Could not lock case instance
AI-assisted analysis of flowable/flowable-engine@d6d39ce1c6 (2026-09-11).
Data as JSON: /api/errors/53a12c79f65f5ebd.
Report an issue: GitHub.