Skip to content

Commit fd1c4fc

Browse files
committed
Remove historic entity link orphans on task, process and case deletion
Historic entity-link cleanup only deleted rows where the deleted entity appeared as the SCOPE_ID_, leaving orphan rows where it appeared as the REFERENCE_SCOPE_ID_. Subsequent reads of those links could return entries pointing at scopes that no longer exist. Add deleteHistoricEntityLinksByScopeIdOrReferenceScopeIdAndScopeType and bulkDeleteHistoricEntityLinksForScopeTypeAndScopeIdsOrReferenceScopeIds on HistoricEntityLinkService and use them from TaskHelper#deleteHistoricTask (BPMN and CMMN, single and bulk), DefaultHistoryManager#recordProcessInstanceDeleted / recordBulkDeleteProcessInstances, DefaultCmmnHistoryManager#recordHistoricCaseInstanceDeleted, and CmmnHistoryHelper#bulkDeleteHistoricCaseInstances. A single SQL delete now removes every row mentioning the deleted entity in either direction.
1 parent 888eef6 commit fd1c4fc

14 files changed

Lines changed: 301 additions & 4 deletions

File tree

modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/history/CmmnHistoryHelper.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public static void bulkDeleteHistoricCaseInstances(Collection<String> caseInstan
4949

5050
if (cmmnEngineConfiguration.isEnableEntityLinks()) {
5151
cmmnEngineConfiguration.getEntityLinkServiceConfiguration().getHistoricEntityLinkService()
52-
.bulkDeleteHistoricEntityLinksForScopeTypeAndScopeIds(ScopeTypes.CMMN, caseInstanceIds);
52+
.bulkDeleteHistoricEntityLinksForScopeTypeAndScopeIdsOrReferenceScopeIds(ScopeTypes.CMMN, caseInstanceIds);
5353
}
5454

5555
HistoricVariableInstanceEntityManager historicVariableInstanceEntityManager = cmmnEngineConfiguration.getVariableServiceConfiguration().getHistoricVariableInstanceEntityManager();

modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/history/DefaultCmmnHistoryManager.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ public void recordHistoricCaseInstanceDeleted(String caseInstanceId, String tena
213213

214214
if (cmmnEngineConfiguration.isEnableEntityLinks()) {
215215
cmmnEngineConfiguration.getEntityLinkServiceConfiguration().getHistoricEntityLinkService()
216-
.deleteHistoricEntityLinksByScopeIdAndScopeType(historicCaseInstance.getId(), ScopeTypes.CMMN);
216+
.deleteHistoricEntityLinksByScopeIdOrReferenceScopeIdAndScopeType(historicCaseInstance.getId(), ScopeTypes.CMMN);
217217
}
218218

219219
HistoricVariableInstanceEntityManager historicVariableInstanceEntityManager = cmmnEngineConfiguration.getVariableServiceConfiguration().getHistoricVariableInstanceEntityManager();

modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/task/TaskHelper.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,11 @@ public static void deleteHistoricTask(String taskId, CmmnEngineConfiguration cmm
224224

225225
cmmnEngineConfiguration.getIdentityLinkServiceConfiguration().getHistoricIdentityLinkService().deleteHistoricIdentityLinksByTaskId(taskId);
226226

227+
if (cmmnEngineConfiguration.isEnableEntityLinks()) {
228+
cmmnEngineConfiguration.getEntityLinkServiceConfiguration().getHistoricEntityLinkService()
229+
.deleteHistoricEntityLinksByScopeIdOrReferenceScopeIdAndScopeType(taskId, ScopeTypes.TASK);
230+
}
231+
227232
historicTaskService.deleteHistoricTask(historicTaskInstance);
228233
}
229234
}
@@ -283,6 +288,11 @@ protected static void bulkDeleteHistoricTaskInstances(Collection<String> taskIds
283288
cmmnEngineConfiguration.getVariableServiceConfiguration().getHistoricVariableService().bulkDeleteHistoricVariableInstancesByTaskIds(taskIds);
284289
cmmnEngineConfiguration.getIdentityLinkServiceConfiguration().getHistoricIdentityLinkService().bulkDeleteHistoricIdentityLinksForTaskIds(taskIds);
285290

291+
if (cmmnEngineConfiguration.isEnableEntityLinks()) {
292+
cmmnEngineConfiguration.getEntityLinkServiceConfiguration().getHistoricEntityLinkService()
293+
.bulkDeleteHistoricEntityLinksForScopeTypeAndScopeIdsOrReferenceScopeIds(ScopeTypes.TASK, taskIds);
294+
}
295+
286296
historicTaskService.bulkDeleteHistoricTaskInstances(taskIds);
287297

288298
historicTaskService.bulkDeleteHistoricTaskLogEntriesForTaskIds(taskIds);

modules/flowable-cmmn-engine/src/test/java/org/flowable/cmmn/test/history/BulkCaseInstanceDeleteTest.java

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
import java.util.List;
2121

2222
import org.flowable.cmmn.api.runtime.CaseInstance;
23+
import org.flowable.cmmn.api.runtime.PlanItemInstance;
24+
import org.flowable.cmmn.api.runtime.PlanItemInstanceState;
2325
import org.flowable.cmmn.engine.impl.util.CommandContextUtil;
2426
import org.flowable.cmmn.engine.test.CmmnDeployment;
2527
import org.flowable.cmmn.test.FlowableCmmnTestCase;
@@ -328,6 +330,93 @@ public ByteArrayEntity execute(CommandContext commandContext) {
328330
}
329331
}
330332

333+
@Test
334+
@CmmnDeployment(resources = "org/flowable/cmmn/test/one-human-task-model.cmmn")
335+
public void deleteHistoricTaskInstanceRemovesReferenceScopeOrphans() {
336+
HistoryLevel historyLevel = cmmnEngineConfiguration.getHistoryLevel();
337+
try {
338+
cmmnEngineConfiguration.setHistoryLevel(HistoryLevel.FULL);
339+
CaseInstance caseInstance = cmmnRuntimeService.createCaseInstanceBuilder()
340+
.caseDefinitionKey("oneTaskCase")
341+
.start();
342+
343+
Task task = cmmnTaskService.createTaskQuery().caseInstanceId(caseInstance.getId()).singleResult();
344+
345+
waitForAsyncHistoryExecutorToProcessAllJobs();
346+
347+
// The case-task entity link points at the task as the REFERENCE_SCOPE_ID_.
348+
assertThat(cmmnHistoryService.getHistoricEntityLinkParentsForTask(task.getId())).isNotEmpty();
349+
350+
cmmnTaskService.complete(task.getId());
351+
waitForAsyncHistoryExecutorToProcessAllJobs();
352+
353+
cmmnHistoryService.deleteHistoricTaskInstance(task.getId());
354+
355+
assertThat(cmmnHistoryService.getHistoricEntityLinkParentsForTask(task.getId())).isEmpty();
356+
assertThat(cmmnHistoryService.getHistoricEntityLinkChildrenForTask(task.getId())).isEmpty();
357+
358+
cmmnHistoryService.deleteHistoricCaseInstance(caseInstance.getId());
359+
} finally {
360+
cmmnEngineConfiguration.setHistoryLevel(historyLevel);
361+
}
362+
}
363+
364+
@Test
365+
@CmmnDeployment(resources = {
366+
"org/flowable/cmmn/test/runtime/CaseTaskTest.testBasicBlocking.cmmn",
367+
"org/flowable/cmmn/test/runtime/oneTaskCase.cmmn"
368+
})
369+
public void deleteHistoricCaseInstanceRemovesReferenceScopeOrphans() {
370+
HistoryLevel historyLevel = cmmnEngineConfiguration.getHistoryLevel();
371+
try {
372+
cmmnEngineConfiguration.setHistoryLevel(HistoryLevel.FULL);
373+
CaseInstance parentCase = cmmnRuntimeService.createCaseInstanceBuilder()
374+
.caseDefinitionKey("myCase")
375+
.start();
376+
377+
// Trigger Task One so the entry sentry on the case task fires and the sub-case starts.
378+
PlanItemInstance taskOnePlanItem = cmmnRuntimeService.createPlanItemInstanceQuery()
379+
.caseInstanceId(parentCase.getId())
380+
.planItemDefinitionId("task1")
381+
.planItemInstanceState(PlanItemInstanceState.ACTIVE)
382+
.singleResult();
383+
cmmnRuntimeService.triggerPlanItemInstance(taskOnePlanItem.getId());
384+
385+
CaseInstance subCase = cmmnRuntimeService.createCaseInstanceQuery()
386+
.caseDefinitionKey("oneTaskCase")
387+
.singleResult();
388+
assertThat(subCase).isNotNull();
389+
390+
PlanItemInstance subTaskPlanItem = cmmnRuntimeService.createPlanItemInstanceQuery()
391+
.caseInstanceId(subCase.getId())
392+
.planItemInstanceState(PlanItemInstanceState.ACTIVE)
393+
.singleResult();
394+
cmmnRuntimeService.triggerPlanItemInstance(subTaskPlanItem.getId());
395+
396+
waitForAsyncHistoryExecutorToProcessAllJobs();
397+
398+
// The sub-case is propagated as the REFERENCE_SCOPE_ID_ in the parent's child entity link.
399+
assertThat(cmmnHistoryService.getHistoricEntityLinkParentsForCaseInstance(subCase.getId())).isNotEmpty();
400+
401+
cmmnHistoryService.deleteHistoricCaseInstance(subCase.getId());
402+
403+
assertThat(cmmnHistoryService.getHistoricEntityLinkParentsForCaseInstance(subCase.getId())).isEmpty();
404+
assertThat(cmmnHistoryService.getHistoricEntityLinkChildrenForCaseInstance(subCase.getId())).isEmpty();
405+
406+
// Trigger Task Two so the parent case completes, then drop it from history.
407+
PlanItemInstance taskTwoPlanItem = cmmnRuntimeService.createPlanItemInstanceQuery()
408+
.caseInstanceId(parentCase.getId())
409+
.planItemDefinitionId("task2")
410+
.planItemInstanceState(PlanItemInstanceState.ACTIVE)
411+
.singleResult();
412+
cmmnRuntimeService.triggerPlanItemInstance(taskTwoPlanItem.getId());
413+
waitForAsyncHistoryExecutorToProcessAllJobs();
414+
cmmnHistoryService.deleteHistoricCaseInstance(parentCase.getId());
415+
} finally {
416+
cmmnEngineConfiguration.setHistoryLevel(historyLevel);
417+
}
418+
}
419+
331420
protected void validateEmptyHistoricDataForCaseInstance(String caseInstanceId) {
332421
assertThat(cmmnHistoryService.createHistoricVariableInstanceQuery().caseInstanceId(caseInstanceId).list()).hasSize(0);
333422
assertThat(cmmnHistoryService.getHistoricIdentityLinksForCaseInstance(caseInstanceId)).hasSize(0);

modules/flowable-engine/src/main/java/org/flowable/engine/impl/history/DefaultHistoryManager.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,8 @@ public void recordProcessInstanceDeleted(String processInstanceId, String proces
144144
processEngineConfiguration.getIdentityLinkServiceConfiguration().getHistoricIdentityLinkService().deleteHistoricIdentityLinksByProcessInstanceId(processInstanceId);
145145

146146
if (processEngineConfiguration.isEnableEntityLinks()) {
147-
processEngineConfiguration.getEntityLinkServiceConfiguration().getHistoricEntityLinkService().deleteHistoricEntityLinksByScopeIdAndScopeType(processInstanceId, ScopeTypes.BPMN);
147+
processEngineConfiguration.getEntityLinkServiceConfiguration().getHistoricEntityLinkService()
148+
.deleteHistoricEntityLinksByScopeIdOrReferenceScopeIdAndScopeType(processInstanceId, ScopeTypes.BPMN);
148149
}
149150

150151
getCommentEntityManager().deleteCommentsByProcessInstanceId(processInstanceId);
@@ -183,7 +184,8 @@ public void recordBulkDeleteProcessInstances(Collection<String> processInstanceI
183184
processEngineConfiguration.getIdentityLinkServiceConfiguration().getHistoricIdentityLinkService().bulkDeleteHistoricIdentityLinksForProcessInstanceIds(processInstanceIds);
184185

185186
if (processEngineConfiguration.isEnableEntityLinks()) {
186-
processEngineConfiguration.getEntityLinkServiceConfiguration().getHistoricEntityLinkService().bulkDeleteHistoricEntityLinksForScopeTypeAndScopeIds(ScopeTypes.BPMN, processInstanceIds);
187+
processEngineConfiguration.getEntityLinkServiceConfiguration().getHistoricEntityLinkService()
188+
.bulkDeleteHistoricEntityLinksForScopeTypeAndScopeIdsOrReferenceScopeIds(ScopeTypes.BPMN, processInstanceIds);
187189
}
188190

189191
getCommentEntityManager().bulkDeleteCommentsForProcessInstanceIds(processInstanceIds);

modules/flowable-engine/src/main/java/org/flowable/engine/impl/util/TaskHelper.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -660,6 +660,11 @@ public static void deleteHistoricTask(String taskId) {
660660
}
661661
processEngineConfiguration.getIdentityLinkServiceConfiguration().getHistoricIdentityLinkService().deleteHistoricIdentityLinksByTaskId(taskId);
662662

663+
if (processEngineConfiguration.isEnableEntityLinks()) {
664+
processEngineConfiguration.getEntityLinkServiceConfiguration().getHistoricEntityLinkService()
665+
.deleteHistoricEntityLinksByScopeIdOrReferenceScopeIdAndScopeType(taskId, ScopeTypes.TASK);
666+
}
667+
663668
historicTaskService.deleteHistoricTask(historicTaskInstance);
664669
}
665670
}
@@ -712,6 +717,11 @@ protected static void bulkDeleteHistoricTaskInstances(Collection<String> taskIds
712717
processEngineConfiguration.getVariableServiceConfiguration().getHistoricVariableService().bulkDeleteHistoricVariableInstancesByTaskIds(taskIds);
713718
processEngineConfiguration.getIdentityLinkServiceConfiguration().getHistoricIdentityLinkService().bulkDeleteHistoricIdentityLinksForTaskIds(taskIds);
714719

720+
if (processEngineConfiguration.isEnableEntityLinks()) {
721+
processEngineConfiguration.getEntityLinkServiceConfiguration().getHistoricEntityLinkService()
722+
.bulkDeleteHistoricEntityLinksForScopeTypeAndScopeIdsOrReferenceScopeIds(ScopeTypes.TASK, taskIds);
723+
}
724+
715725
historicTaskService.bulkDeleteHistoricTaskInstances(taskIds);
716726
historicTaskService.bulkDeleteHistoricTaskLogEntriesForTaskIds(taskIds);
717727
}

modules/flowable-engine/src/test/java/org/flowable/engine/test/history/BulkDeleteHistoricProcessInstanceTest.java

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,104 @@ public List<HistoricEntityLink> execute(CommandContext commandContext) {
456456
}
457457
}
458458

459+
@Test
460+
@Deployment(resources = { "org/flowable/engine/test/history/callActivity.bpmn20.xml",
461+
"org/flowable/engine/test/history/subProcess.bpmn20.xml" })
462+
public void deleteHistoricTaskInstanceRemovesReferenceScopeOrphans() {
463+
HistoryLevel historyLevel = processEngineConfiguration.getHistoryLevel();
464+
try {
465+
processEngineConfiguration.setHistoryLevel(HistoryLevel.FULL);
466+
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("callActivity");
467+
468+
Task mainTask = taskService.createTaskQuery().processInstanceId(processInstance.getId()).singleResult();
469+
taskService.complete(mainTask.getId());
470+
471+
ProcessInstance subProcessInstance = runtimeService.createProcessInstanceQuery().superProcessInstanceId(processInstance.getId()).singleResult();
472+
Task subTask = taskService.createTaskQuery().processInstanceId(subProcessInstance.getId()).singleResult();
473+
String subTaskId = subTask.getId();
474+
475+
// Complete the sub-task so it has an endTime; deleteHistoricTaskInstance only works on completed tasks.
476+
taskService.complete(subTaskId);
477+
waitForHistoryJobExecutorToProcessAllJobs(10000, 400);
478+
479+
// The sub-process task carries a parent link (root scope = parent process instance) before delete.
480+
assertThat(historyService.getHistoricEntityLinkParentsForTask(subTaskId)).isNotEmpty();
481+
482+
historyService.deleteHistoricTaskInstance(subTaskId);
483+
484+
// After deleting only the historic task, no row in ACT_HI_ENTITYLINK may still reference it.
485+
assertThat(historyService.getHistoricEntityLinkParentsForTask(subTaskId)).isEmpty();
486+
assertThat(historyService.getHistoricEntityLinkChildrenForTask(subTaskId)).isEmpty();
487+
488+
historyService.deleteHistoricProcessInstance(processInstance.getId());
489+
} finally {
490+
processEngineConfiguration.setHistoryLevel(historyLevel);
491+
}
492+
}
493+
494+
@Test
495+
@Deployment(resources = { "org/flowable/engine/test/history/callActivity.bpmn20.xml",
496+
"org/flowable/engine/test/history/subProcess.bpmn20.xml" })
497+
public void deleteHistoricProcessInstanceRemovesReferenceScopeOrphans() {
498+
HistoryLevel historyLevel = processEngineConfiguration.getHistoryLevel();
499+
try {
500+
processEngineConfiguration.setHistoryLevel(HistoryLevel.FULL);
501+
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("callActivity");
502+
503+
Task mainTask = taskService.createTaskQuery().processInstanceId(processInstance.getId()).singleResult();
504+
taskService.complete(mainTask.getId());
505+
506+
ProcessInstance subProcessInstance = runtimeService.createProcessInstanceQuery().superProcessInstanceId(processInstance.getId()).singleResult();
507+
Task subTask = taskService.createTaskQuery().processInstanceId(subProcessInstance.getId()).singleResult();
508+
taskService.complete(subTask.getId());
509+
510+
waitForHistoryJobExecutorToProcessAllJobs(10000, 400);
511+
512+
// The sub-process is propagated as the REFERENCE_SCOPE_ID_ in the parent's child entity link.
513+
assertThat(historyService.getHistoricEntityLinkParentsForProcessInstance(subProcessInstance.getId())).isNotEmpty();
514+
515+
historyService.deleteHistoricProcessInstance(subProcessInstance.getId());
516+
517+
assertThat(historyService.getHistoricEntityLinkParentsForProcessInstance(subProcessInstance.getId())).isEmpty();
518+
assertThat(historyService.getHistoricEntityLinkChildrenForProcessInstance(subProcessInstance.getId())).isEmpty();
519+
520+
historyService.deleteHistoricProcessInstance(processInstance.getId());
521+
} finally {
522+
processEngineConfiguration.setHistoryLevel(historyLevel);
523+
}
524+
}
525+
526+
@Test
527+
@Deployment(resources = { "org/flowable/engine/test/history/callActivity.bpmn20.xml",
528+
"org/flowable/engine/test/history/subProcess.bpmn20.xml" })
529+
public void bulkDeleteHistoricProcessInstancesRemovesReferenceScopeOrphans() {
530+
HistoryLevel historyLevel = processEngineConfiguration.getHistoryLevel();
531+
try {
532+
processEngineConfiguration.setHistoryLevel(HistoryLevel.FULL);
533+
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("callActivity");
534+
535+
Task mainTask = taskService.createTaskQuery().processInstanceId(processInstance.getId()).singleResult();
536+
taskService.complete(mainTask.getId());
537+
538+
ProcessInstance subProcessInstance = runtimeService.createProcessInstanceQuery().superProcessInstanceId(processInstance.getId()).singleResult();
539+
Task subTask = taskService.createTaskQuery().processInstanceId(subProcessInstance.getId()).singleResult();
540+
taskService.complete(subTask.getId());
541+
542+
waitForHistoryJobExecutorToProcessAllJobs(10000, 400);
543+
544+
assertThat(historyService.getHistoricEntityLinkParentsForProcessInstance(subProcessInstance.getId())).isNotEmpty();
545+
546+
historyService.bulkDeleteHistoricProcessInstances(Collections.singletonList(subProcessInstance.getId()));
547+
548+
assertThat(historyService.getHistoricEntityLinkParentsForProcessInstance(subProcessInstance.getId())).isEmpty();
549+
assertThat(historyService.getHistoricEntityLinkChildrenForProcessInstance(subProcessInstance.getId())).isEmpty();
550+
551+
historyService.deleteHistoricProcessInstance(processInstance.getId());
552+
} finally {
553+
processEngineConfiguration.setHistoryLevel(historyLevel);
554+
}
555+
}
556+
459557
protected void validateEmptyHistoricDataForProcessInstance(String processInstanceId) {
460558
assertThat(historyService.createHistoricDetailQuery().processInstanceId(processInstanceId).list()).hasSize(0);
461559
assertThat(historyService.createHistoricVariableInstanceQuery().processInstanceId(processInstanceId).list()).hasSize(0);

modules/flowable-entitylink-service-api/src/main/java/org/flowable/entitylink/api/history/HistoricEntityLinkService.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,22 @@ default List<HistoricEntityLink> findHistoricEntityLinksByScopeDefinitionIdAndSc
6666

6767
void deleteHistoricEntityLinksByScopeIdAndScopeType(String scopeId, String scopeType);
6868

69+
/**
70+
* Delete every historic entity link where the given id appears as either the scope id
71+
* (with the given scope type) or the reference scope id (with the given reference scope type).
72+
* Used when an entity is removed from history to ensure no orphan rows are left pointing at it
73+
* from either direction.
74+
*/
75+
void deleteHistoricEntityLinksByScopeIdOrReferenceScopeIdAndScopeType(String id, String scopeType);
76+
6977
void deleteHistoricEntityLinksByScopeDefinitionIdAndScopeType(String scopeDefinitionId, String scopeType);
7078

7179
void bulkDeleteHistoricEntityLinksForScopeTypeAndScopeIds(String scopeType, Collection<String> scopeIds);
80+
81+
/**
82+
* Bulk variant of {@link #deleteHistoricEntityLinksByScopeIdOrReferenceScopeIdAndScopeType(String, String)}.
83+
*/
84+
void bulkDeleteHistoricEntityLinksForScopeTypeAndScopeIdsOrReferenceScopeIds(String scopeType, Collection<String> ids);
7285

7386
void deleteHistoricEntityLinksForNonExistingProcessInstances();
7487

0 commit comments

Comments
 (0)