{"record":{"id":"53a12c79f65f5ebd","repo":"flowable/flowable-engine","slug":"one-of-the-entities-persistentobjectclass-was-up","errorCode":null,"errorMessage":"One of the entities <persistentObjectClass> was updated by another transaction concurrently while trying to do a bulk delete","messagePattern":"One of the entities <persistentObjectClass> was updated by another transaction concurrently while trying to do a bulk delete","errorType":"exception","errorClass":"ActivitiOptimisticLockingException","httpStatus":null,"severity":"error","filePath":"modules/flowable5-engine/src/main/java/org/activiti/engine/impl/db/DbSqlSession.java","lineNumber":307,"sourceCode":"\n        @Override\n        public void execute() {\n\n            if (persistentObjects.isEmpty()) {\n                return;\n            }\n\n            String bulkDeleteStatement = dbSqlSessionFactory.getBulkDeleteStatement(persistentObjectClass);\n            bulkDeleteStatement = dbSqlSessionFactory.mapStatement(bulkDeleteStatement);\n            if (bulkDeleteStatement == null) {\n                throw new ActivitiException(\"no bulk delete statement for \" + persistentObjectClass + \" in the mapping files\");\n            }\n\n            // It only makes sense to check for optimistic locking exceptions for objects that actually have a revision\n            if (persistentObjects.get(0) instanceof HasRevision) {\n                int nrOfRowsDeleted = sqlSession.delete(bulkDeleteStatement, persistentObjects);\n                if (nrOfRowsDeleted < persistentObjects.size()) {\n                    throw new ActivitiOptimisticLockingException(\"One of the entities \" + persistentObjectClass\n                            + \" was updated by another transaction concurrently while trying to do a bulk delete\");\n                }\n            } else {\n                sqlSession.delete(bulkDeleteStatement, persistentObjects);\n            }\n        }\n\n        @Override\n        public Class<? extends PersistentObject> getPersistentObjectClass() {\n            return persistentObjectClass;\n        }\n\n        public void setPersistentObjectClass(\n                Class<? extends PersistentObject> persistentObjectClass) {\n            this.persistentObjectClass = persistentObjectClass;\n        }\n\n        public List<PersistentObject> getPersistentObjects() {","sourceCodeStart":289,"sourceCodeEnd":325,"githubUrl":"https://github.com/flowable/flowable-engine/blob/d6d39ce1c69ff244f2d9dc6af756a9b95e865586/modules/flowable5-engine/src/main/java/org/activiti/engine/impl/db/DbSqlSession.java#L289-L325","documentation":"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.","triggerScenarios":"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().","commonSituations":"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.","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."],"exampleFix":"// before: delete with stale entities\nList<TaskEntity> stale = loadTasks(); // fetched long ago\ncommandContext.getDbSqlSession().deleteAll(stale);\n\n// after: retry on optimistic lock\ntry {\n  commandContext.getDbSqlSession().deleteAll(loadTasks()); // fresh fetch\n} catch (ActivitiOptimisticLockingException e) {\n  retryWithBackoff(() -> commandContext.getDbSqlSession().deleteAll(loadTasks()));\n}","handlingStrategy":"retry","validationCode":"// Re-check revisions immediately before bulk delete\nboolean fresh = persistentObjects.stream()\n    .filter(po -> po instanceof HasRevision)\n    .allMatch(po -> revisionInDb(po.getId()) == ((HasRevision) po).getRevision());\nif (!fresh) { reloadEntities(); }","typeGuard":null,"tryCatchPattern":"try {\n    dbSqlSession.deleteAll(entities);\n} catch (ActivitiOptimisticLockingException e) {\n    // reload entities with current revisions and retry with backoff\n    retryWithBackoff(3, () -> dbSqlSession.deleteAll(loadFresh()));\n}","preventionTips":["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."],"tags":["database","optimistic-locking","concurrency","bulk-delete"],"backgroundTag":"optimistic-lock-conflict","analyzedSha":"d6d39ce1c69ff244f2d9dc6af756a9b95e865586","analyzedAt":"2026-09-11T06:41:19.413Z","contentChangedAt":"2026-09-11T06:41:19.413Z","schemaVersion":2},"datasetVersion":"2026-09-14T11:17:12.474Z"}