diff --git a/modules/flowable-cmmn-engine-configurator/src/test/java/org/flowable/cmmn/test/VerifyDatabaseOperationsTest.java b/modules/flowable-cmmn-engine-configurator/src/test/java/org/flowable/cmmn/test/VerifyDatabaseOperationsTest.java index 61604962409..8d8909589dd 100644 --- a/modules/flowable-cmmn-engine-configurator/src/test/java/org/flowable/cmmn/test/VerifyDatabaseOperationsTest.java +++ b/modules/flowable-cmmn-engine-configurator/src/test/java/org/flowable/cmmn/test/VerifyDatabaseOperationsTest.java @@ -14,9 +14,15 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; +import static org.flowable.common.engine.impl.persistence.deploy.AbstractSameDeploymentDefinitionLookup.parentDeploymentKey; import java.util.Map; +import org.flowable.cmmn.api.repository.CaseDefinition; +import org.flowable.cmmn.api.repository.CmmnDeployment; +import org.flowable.cmmn.api.repository.CmmnDeploymentBuilder; +import org.flowable.cmmn.api.runtime.CaseInstanceBuilder; +import org.flowable.cmmn.api.runtime.CaseInstanceQuery; import org.flowable.common.engine.impl.cfg.CommandExecutorImpl; import org.flowable.common.engine.impl.db.DbSqlSessionFactory; import org.flowable.common.engine.impl.interceptor.CommandExecutor; @@ -61,6 +67,7 @@ protected void setUp() { @AfterEach protected void tearDown() { + setCacheSameDeploymentDefinitionMisses(false); ((ProcessEngineConfigurationImpl) processEngineConfiguration).addSessionFactory(oldDbSqlSessionFactory); FlowableProfiler.getInstance().reset(); } @@ -86,6 +93,21 @@ public void testCaseWithProcessTaskFromSameDeployment() { entry("selectEntityLinksByQuery", 2L) ); assertThat(processEngineRuntimeService.createProcessInstanceQuery().count()).isOne(); + + Map secondSelects = profileDatabaseSelects(cmmnEngineConfiguration.getCommandExecutor(), "org.flowable.cmmn.engine.impl.cmd.StartCaseInstanceCmd", + () -> cmmnRuntimeService.createCaseInstanceBuilder() + .caseDefinitionKey("oneProcessTaskCase") + .start()); + + // The process definition comes from the same deployment cache, only the suspension check selects it by id + assertThat(secondSelects).containsOnly( + entry("selectEntityLinksByQuery", 2L), + entry("selectLatestCaseDefinitionByKey", 1L), + entry("selectPlanItemInstancesByCaseInstanceIdAndPlanItemId", 1L), + entry("selectById org.flowable.engine.impl.persistence.entity.ProcessDefinitionEntityImpl", 1L), + entry("selectProcessDefinitionInfoByProcessDefinitionId", 1L) + ); + assertThat(processEngineRuntimeService.createProcessInstanceQuery().count()).isEqualTo(2); } @Test @@ -108,6 +130,20 @@ public void testCaseWithCaseTaskFromSameDeployment() { entry("selectEntityLinksByQuery", 2L) ); assertThat(cmmnRuntimeService.createCaseInstanceQuery().caseDefinitionKey("oneHumanTaskCase").count()).isOne(); + + Map secondSelects = profileDatabaseSelects(cmmnEngineConfiguration.getCommandExecutor(), "org.flowable.cmmn.engine.impl.cmd.StartCaseInstanceCmd", + () -> cmmnRuntimeService.createCaseInstanceBuilder() + .caseDefinitionKey("oneCaseTaskCase") + .start()); + + // The cached case definition id is verified by selecting the case definition by id + assertThat(secondSelects).containsOnly( + entry("selectById org.flowable.cmmn.engine.impl.persistence.entity.CaseDefinitionEntityImpl", 1L), + entry("selectEntityLinksByQuery", 2L), + entry("selectLatestCaseDefinitionByKey", 1L), + entry("selectPlanItemInstancesByCaseInstanceIdAndPlanItemId", 2L) + ); + assertThat(cmmnRuntimeService.createCaseInstanceQuery().caseDefinitionKey("oneHumanTaskCase").count()).isEqualTo(2); } @Test @@ -131,6 +167,378 @@ public void testProcessWithCaseTaskFromSameDeployment() { entry("selectEntityLinksByQuery", 2L) ); assertThat(cmmnRuntimeService.createCaseInstanceQuery().count()).isOne(); + + Map secondSelects = profileDatabaseSelects(processEngineConfiguration.getCommandExecutor(), "org.flowable.engine.impl.cmd.StartProcessInstanceCmd", + () -> processEngineRuntimeService.createProcessInstanceBuilder() + .processDefinitionKey("caseTask") + .start()); + + assertThat(secondSelects).containsOnly( + entry("selectById org.flowable.cmmn.engine.impl.persistence.entity.CaseDefinitionEntityImpl", 1L), + entry("selectEntityLinksByQuery", 2L), + entry("selectLatestProcessDefinitionByKey", 1L), + entry("selectPlanItemInstancesByCaseInstanceIdAndPlanItemId", 1L), + entry("selectProcessDefinitionInfoByProcessDefinitionId", 1L) + ); + assertThat(cmmnRuntimeService.createCaseInstanceQuery().count()).isEqualTo(2); + } + + @Test + public void testStartCaseWithParentDeploymentIdTwice() { + CmmnDeployment deployment = deployCase("oneHumanTaskCase.cmmn", null, null); + assertThat(deployment.getParentDeploymentId()).isEqualTo(deployment.getId()); + + Map firstSelects = startCaseWithParentDeploymentId("oneHumanTaskCase", deployment.getId(), null, false); + assertThat(firstSelects).containsOnly( + entry("selectCaseDefinitionByParentDeploymentAndKey", 1L), + entry("selectEntityLinksByQuery", 1L), + entry("selectPlanItemInstancesByCaseInstanceIdAndPlanItemId", 1L) + ); + + Map secondSelects = startCaseWithParentDeploymentId("oneHumanTaskCase", deployment.getId(), null, false); + assertThat(secondSelects).containsOnly( + entry("selectById org.flowable.cmmn.engine.impl.persistence.entity.CaseDefinitionEntityImpl", 1L), + entry("selectEntityLinksByQuery", 1L), + entry("selectPlanItemInstancesByCaseInstanceIdAndPlanItemId", 1L) + ); + assertThat(cmmnRuntimeService.createCaseInstanceQuery().caseDefinitionKey("oneHumanTaskCase").count()).isEqualTo(2); + } + + @Test + public void testStartCaseWithParentDeploymentIdTwiceWithoutVerifyingCachedDefinitions() { + cmmnEngineConfiguration.setVerifyCachedSameDeploymentDefinitions(false); + try { + CmmnDeployment deployment = deployCase("oneHumanTaskCase.cmmn", null, null); + + Map firstSelects = startCaseWithParentDeploymentId("oneHumanTaskCase", deployment.getId(), null, false); + assertThat(firstSelects).containsOnly( + entry("selectCaseDefinitionByParentDeploymentAndKey", 1L), + entry("selectEntityLinksByQuery", 1L), + entry("selectPlanItemInstancesByCaseInstanceIdAndPlanItemId", 1L) + ); + + Map secondSelects = startCaseWithParentDeploymentId("oneHumanTaskCase", deployment.getId(), null, false); + assertThat(secondSelects).containsOnly( + entry("selectEntityLinksByQuery", 1L), + entry("selectPlanItemInstancesByCaseInstanceIdAndPlanItemId", 1L) + ); + assertCaseInstancesDeployedTo("oneHumanTaskCase", 2, deployment.getId()); + } finally { + cmmnEngineConfiguration.setVerifyCachedSameDeploymentDefinitions(true); + } + } + + @Test + public void testStartCaseWithParentDeploymentIdMissForOtherTenantDoesNotHideDefinition() { + setCacheSameDeploymentDefinitionMisses(true); + CmmnDeployment parent = deployCase("oneHumanTaskCase.cmmn", null, null); + + // Start for tenant X, the parent has no case definition for that tenant and the fallback finds the one without tenant + Map firstSelects = startCaseWithParentDeploymentId("oneHumanTaskCase", parent.getId(), "X", true); + assertThat(firstSelects).containsOnly( + entry("selectCaseDefinitionByParentDeploymentAndKeyAndTenantId", 1L), + entry("selectLatestCaseDefinitionByKeyAndTenantId", 1L), + entry("selectLatestCaseDefinitionByKey", 1L), + entry("selectEntityLinksByQuery", 1L), + entry("selectPlanItemInstancesByCaseInstanceIdAndPlanItemId", 1L) + ); + assertCaseInstancesDeployedTo(cmmnRuntimeService.createCaseInstanceQuery().caseInstanceTenantId("X"), 1, parent.getId()); + + // Start without tenant, the miss for tenant X must not hide the case definition + Map secondSelects = startCaseWithParentDeploymentId("oneHumanTaskCase", parent.getId(), null, false); + assertThat(secondSelects).containsOnly( + entry("selectCaseDefinitionByParentDeploymentAndKey", 1L), + entry("selectEntityLinksByQuery", 1L), + entry("selectPlanItemInstancesByCaseInstanceIdAndPlanItemId", 1L) + ); + assertThat(cmmnRuntimeService.createCaseInstanceQuery().caseDefinitionKey("oneHumanTaskCase").list()) + .hasSize(2) + .extracting(caseInstance -> deploymentIdOf(caseInstance.getCaseDefinitionId())) + .containsOnly(parent.getId()); + } + + @Test + public void testStartCaseWithParentDeploymentIdMissTwice() { + CmmnDeployment parent = deployCase("oneServiceTask.cmmn", null, null); + CmmnDeployment latest = deployCase("oneHumanTaskCase.cmmn", null, null); + + Map firstSelects = startCaseWithParentDeploymentId("oneHumanTaskCase", parent.getId(), null, false); + assertThat(firstSelects).containsOnly( + entry("selectCaseDefinitionByParentDeploymentAndKey", 1L), + entry("selectEntityLinksByQuery", 1L), + entry("selectLatestCaseDefinitionByKey", 1L), + entry("selectPlanItemInstancesByCaseInstanceIdAndPlanItemId", 1L) + ); + + Map secondSelects = startCaseWithParentDeploymentId("oneHumanTaskCase", parent.getId(), null, false); + assertThat(secondSelects).containsOnly( + entry("selectCaseDefinitionByParentDeploymentAndKey", 1L), + entry("selectEntityLinksByQuery", 1L), + entry("selectLatestCaseDefinitionByKey", 1L), + entry("selectPlanItemInstancesByCaseInstanceIdAndPlanItemId", 1L) + ); + assertCaseInstancesDeployedTo("oneHumanTaskCase", 2, latest.getId()); + } + + @Test + public void testStartCaseWithParentDeploymentIdMissTwiceWithCachedMisses() { + setCacheSameDeploymentDefinitionMisses(true); + CmmnDeployment parent = deployCase("oneServiceTask.cmmn", null, null); + CmmnDeployment latest = deployCase("oneHumanTaskCase.cmmn", null, null); + + Map firstSelects = startCaseWithParentDeploymentId("oneHumanTaskCase", parent.getId(), null, false); + assertThat(firstSelects).containsOnly( + entry("selectCaseDefinitionByParentDeploymentAndKey", 1L), + entry("selectEntityLinksByQuery", 1L), + entry("selectLatestCaseDefinitionByKey", 1L), + entry("selectPlanItemInstancesByCaseInstanceIdAndPlanItemId", 1L) + ); + + Map secondSelects = startCaseWithParentDeploymentId("oneHumanTaskCase", parent.getId(), null, false); + assertThat(secondSelects).containsOnly( + entry("selectEntityLinksByQuery", 1L), + entry("selectLatestCaseDefinitionByKey", 1L), + entry("selectPlanItemInstancesByCaseInstanceIdAndPlanItemId", 1L) + ); + assertCaseInstancesDeployedTo("oneHumanTaskCase", 2, latest.getId()); + } + + @Test + public void testStartCaseWithParentDeploymentIdMissThenChildDeployed() { + setCacheSameDeploymentDefinitionMisses(true); + CmmnDeployment parent = deployCase("oneServiceTask.cmmn", null, null); + CmmnDeployment latest = deployCase("oneHumanTaskCase.cmmn", null, null); + + Map firstSelects = startCaseWithParentDeploymentId("oneHumanTaskCase", parent.getId(), null, false); + assertThat(firstSelects).containsOnly( + entry("selectCaseDefinitionByParentDeploymentAndKey", 1L), + entry("selectEntityLinksByQuery", 1L), + entry("selectLatestCaseDefinitionByKey", 1L), + entry("selectPlanItemInstancesByCaseInstanceIdAndPlanItemId", 1L) + ); + assertCaseInstancesDeployedTo("oneHumanTaskCase", 1, latest.getId()); + + CmmnDeployment child = deployCase("oneHumanTaskCase.cmmn", parent.getId(), null); + + Map secondSelects = startCaseWithParentDeploymentId("oneHumanTaskCase", parent.getId(), null, false); + assertThat(secondSelects).containsOnly( + entry("selectCaseDefinitionByParentDeploymentAndKey", 1L), + entry("selectEntityLinksByQuery", 1L), + entry("selectPlanItemInstancesByCaseInstanceIdAndPlanItemId", 1L) + ); + assertThat(cmmnRuntimeService.createCaseInstanceQuery().caseDefinitionKey("oneHumanTaskCase").count()).isEqualTo(2); + assertThat(cmmnRepositoryService.createCaseDefinitionQuery().deploymentId(child.getId()).singleResult()).isNotNull(); + assertThat(cmmnRuntimeService.createCaseInstanceQuery().caseDefinitionKey("oneHumanTaskCase").list()) + .extracting(caseInstance -> deploymentIdOf(caseInstance.getCaseDefinitionId())) + .containsExactlyInAnyOrder(latest.getId(), child.getId()); + } + + @Test + public void testStartCaseWithParentDeploymentIdMissPicksUpNewVersion() { + startCaseWithParentDeploymentIdMissPicksUpNewVersion(false); + } + + @Test + public void testStartCaseWithParentDeploymentIdMissPicksUpNewVersionWithCachedMisses() { + setCacheSameDeploymentDefinitionMisses(true); + startCaseWithParentDeploymentIdMissPicksUpNewVersion(true); + } + + protected void startCaseWithParentDeploymentIdMissPicksUpNewVersion(boolean cachedMisses) { + CmmnDeployment parent = deployCase("oneServiceTask.cmmn", null, null); + CmmnDeployment latest1 = deployCase("oneHumanTaskCase.cmmn", null, null); + + Map firstSelects = startCaseWithParentDeploymentId("oneHumanTaskCase", parent.getId(), null, false); + assertThat(firstSelects).containsOnly( + entry("selectCaseDefinitionByParentDeploymentAndKey", 1L), + entry("selectEntityLinksByQuery", 1L), + entry("selectLatestCaseDefinitionByKey", 1L), + entry("selectPlanItemInstancesByCaseInstanceIdAndPlanItemId", 1L) + ); + assertCaseInstancesDeployedTo("oneHumanTaskCase", 1, latest1.getId()); + + CmmnDeployment latest2 = deployCase("oneHumanTaskCase.cmmn", null, null); + + Map secondSelects = startCaseWithParentDeploymentId("oneHumanTaskCase", parent.getId(), null, false); + if (cachedMisses) { + assertThat(secondSelects).containsOnly( + entry("selectEntityLinksByQuery", 1L), + entry("selectLatestCaseDefinitionByKey", 1L), + entry("selectPlanItemInstancesByCaseInstanceIdAndPlanItemId", 1L) + ); + } else { + assertThat(secondSelects).containsOnly( + entry("selectCaseDefinitionByParentDeploymentAndKey", 1L), + entry("selectEntityLinksByQuery", 1L), + entry("selectLatestCaseDefinitionByKey", 1L), + entry("selectPlanItemInstancesByCaseInstanceIdAndPlanItemId", 1L) + ); + } + assertThat(cmmnRuntimeService.createCaseInstanceQuery().caseDefinitionKey("oneHumanTaskCase").list()) + .extracting(caseInstance -> deploymentIdOf(caseInstance.getCaseDefinitionId())) + .containsExactlyInAnyOrder(latest1.getId(), latest2.getId()); + } + + @Test + public void testStartCaseWithParentDeploymentIdAfterChildUndeployed() { + CmmnDeployment parent = deployCase("oneServiceTask.cmmn", null, null); + CmmnDeployment child = deployCase("oneHumanTaskCase.cmmn", parent.getId(), null); + CmmnDeployment latest = deployCase("oneHumanTaskCase.cmmn", null, null); + + Map firstSelects = startCaseWithParentDeploymentId("oneHumanTaskCase", parent.getId(), null, false); + assertThat(firstSelects).containsOnly( + entry("selectCaseDefinitionByParentDeploymentAndKey", 1L), + entry("selectEntityLinksByQuery", 1L), + entry("selectPlanItemInstancesByCaseInstanceIdAndPlanItemId", 1L) + ); + assertCaseInstancesDeployedTo("oneHumanTaskCase", 1, child.getId()); + + cmmnRepositoryService.deleteDeployment(child.getId(), true); + + Map secondSelects = startCaseWithParentDeploymentId("oneHumanTaskCase", parent.getId(), null, false); + assertThat(secondSelects).containsOnly( + entry("selectCaseDefinitionByParentDeploymentAndKey", 1L), + entry("selectEntityLinksByQuery", 1L), + entry("selectLatestCaseDefinitionByKey", 1L), + entry("selectPlanItemInstancesByCaseInstanceIdAndPlanItemId", 1L) + ); + assertCaseInstancesDeployedTo("oneHumanTaskCase", 1, latest.getId()); + } + + @Test + public void testStartCaseWithParentDeploymentIdWithStaleCachedDefinition() { + CmmnDeployment parent = deployCase("oneServiceTask.cmmn", null, null); + CmmnDeployment child1 = deployCase("oneHumanTaskCase.cmmn", parent.getId(), null); + + startCaseWithParentDeploymentId("oneHumanTaskCase", parent.getId(), null, false); + assertCaseInstancesDeployedTo("oneHumanTaskCase", 1, child1.getId()); + String cacheKey = parentDeploymentKey(parent.getId(), "oneHumanTaskCase"); + String staleCaseDefinitionId = cmmnRepositoryService.createCaseDefinitionQuery().deploymentId(child1.getId()).singleResult().getId(); + assertThat(cmmnEngineConfiguration.getSameDeploymentDefinitionCache().get(cacheKey)).isEqualTo(staleCaseDefinitionId); + + cmmnRepositoryService.deleteDeployment(child1.getId(), true); + CmmnDeployment child2 = deployCase("oneHumanTaskCase.cmmn", parent.getId(), null); + + // Simulate a node that did not see the undeploy and the deploy + cmmnEngineConfiguration.getSameDeploymentDefinitionCache().add(cacheKey, staleCaseDefinitionId); + + Map selects = startCaseWithParentDeploymentId("oneHumanTaskCase", parent.getId(), null, false); + assertThat(selects).containsOnly( + entry("selectById org.flowable.cmmn.engine.impl.persistence.entity.CaseDefinitionEntityImpl", 1L), + entry("selectCaseDefinitionByParentDeploymentAndKey", 1L), + entry("selectEntityLinksByQuery", 1L), + entry("selectPlanItemInstancesByCaseInstanceIdAndPlanItemId", 1L) + ); + assertCaseInstancesDeployedTo("oneHumanTaskCase", 1, child2.getId()); + String caseDefinitionId = cmmnRepositoryService.createCaseDefinitionQuery().deploymentId(child2.getId()).singleResult().getId(); + assertThat(cmmnEngineConfiguration.getSameDeploymentDefinitionCache().get(cacheKey)).isEqualTo(caseDefinitionId); + } + + @Test + public void testStartCaseWithParentDeploymentIdOtherTenant() { + CmmnDeployment parent = deployCase("oneHumanTaskCase.cmmn", null, "tenantA"); + CmmnDeployment otherTenant = deployCase("oneHumanTaskCase.cmmn", null, "tenantB"); + + Map firstSelects = startCaseWithParentDeploymentId("oneHumanTaskCase", parent.getId(), "tenantA", false); + assertThat(firstSelects).containsOnly( + entry("selectCaseDefinitionByParentDeploymentAndKeyAndTenantId", 1L), + entry("selectEntityLinksByQuery", 1L), + entry("selectPlanItemInstancesByCaseInstanceIdAndPlanItemId", 1L) + ); + assertCaseInstancesDeployedTo(cmmnRuntimeService.createCaseInstanceQuery().caseInstanceTenantId("tenantA"), 1, parent.getId()); + + Map secondSelects = startCaseWithParentDeploymentId("oneHumanTaskCase", parent.getId(), "tenantB", false); + assertThat(secondSelects).containsOnly( + entry("selectById org.flowable.cmmn.engine.impl.persistence.entity.CaseDefinitionEntityImpl", 1L), + entry("selectCaseDefinitionByParentDeploymentAndKeyAndTenantId", 1L), + entry("selectEntityLinksByQuery", 1L), + entry("selectLatestCaseDefinitionByKeyAndTenantId", 1L), + entry("selectPlanItemInstancesByCaseInstanceIdAndPlanItemId", 1L) + ); + + Map thirdSelects = startCaseWithParentDeploymentId("oneHumanTaskCase", parent.getId(), "tenantB", false); + assertThat(thirdSelects).containsOnly( + entry("selectById org.flowable.cmmn.engine.impl.persistence.entity.CaseDefinitionEntityImpl", 1L), + entry("selectCaseDefinitionByParentDeploymentAndKeyAndTenantId", 1L), + entry("selectEntityLinksByQuery", 1L), + entry("selectLatestCaseDefinitionByKeyAndTenantId", 1L), + entry("selectPlanItemInstancesByCaseInstanceIdAndPlanItemId", 1L) + ); + assertCaseInstancesDeployedTo(cmmnRuntimeService.createCaseInstanceQuery().caseInstanceTenantId("tenantB"), 2, otherTenant.getId()); + } + + @Test + public void testStartCaseWithParentDeploymentIdDefaultTenantFallback() { + CmmnDeployment parent = deployCase("oneServiceTask.cmmn", null, "tenantA"); + CmmnDeployment defaultTenant = deployCase("oneHumanTaskCase.cmmn", null, null); + + Map firstSelects = startCaseWithParentDeploymentId("oneHumanTaskCase", parent.getId(), "tenantA", true); + assertThat(firstSelects).containsOnly( + entry("selectCaseDefinitionByParentDeploymentAndKeyAndTenantId", 1L), + entry("selectEntityLinksByQuery", 1L), + entry("selectLatestCaseDefinitionByKey", 1L), + entry("selectLatestCaseDefinitionByKeyAndTenantId", 1L), + entry("selectPlanItemInstancesByCaseInstanceIdAndPlanItemId", 1L) + ); + + Map secondSelects = startCaseWithParentDeploymentId("oneHumanTaskCase", parent.getId(), "tenantA", true); + assertThat(secondSelects).containsOnly( + entry("selectCaseDefinitionByParentDeploymentAndKeyAndTenantId", 1L), + entry("selectEntityLinksByQuery", 1L), + entry("selectLatestCaseDefinitionByKey", 1L), + entry("selectLatestCaseDefinitionByKeyAndTenantId", 1L), + entry("selectPlanItemInstancesByCaseInstanceIdAndPlanItemId", 1L) + ); + assertCaseInstancesDeployedTo("oneHumanTaskCase", 2, defaultTenant.getId()); + } + + protected CmmnDeployment deployCase(String resource, String parentDeploymentId, String tenantId) { + CmmnDeploymentBuilder builder = cmmnRepositoryService.createDeployment() + .addClasspathResource("org/flowable/cmmn/test/" + resource); + if (parentDeploymentId != null) { + builder.parentDeploymentId(parentDeploymentId); + } + if (tenantId != null) { + builder.tenantId(tenantId); + } + return builder.deploy(); + } + + protected Map startCaseWithParentDeploymentId(String caseDefinitionKey, String parentDeploymentId, String tenantId, + boolean fallbackToDefaultTenant) { + return profileDatabaseSelects(cmmnEngineConfiguration.getCommandExecutor(), "org.flowable.cmmn.engine.impl.cmd.StartCaseInstanceCmd", () -> { + CaseInstanceBuilder builder = cmmnRuntimeService.createCaseInstanceBuilder() + .caseDefinitionKey(caseDefinitionKey) + .caseDefinitionParentDeploymentId(parentDeploymentId); + if (tenantId != null) { + builder.tenantId(tenantId); + } + if (fallbackToDefaultTenant) { + builder.fallbackToDefaultTenant(); + } + builder.start(); + }); + } + + protected String deploymentIdOf(String caseDefinitionId) { + CaseDefinition caseDefinition = cmmnRepositoryService.getCaseDefinition(caseDefinitionId); + return caseDefinition.getDeploymentId(); + } + + protected void assertCaseInstancesDeployedTo(String caseDefinitionKey, int expectedCount, String expectedDeploymentId) { + assertCaseInstancesDeployedTo(cmmnRuntimeService.createCaseInstanceQuery().caseDefinitionKey(caseDefinitionKey), expectedCount, expectedDeploymentId); + } + + protected void assertCaseInstancesDeployedTo(CaseInstanceQuery query, int expectedCount, String expectedDeploymentId) { + assertThat(query.list()) + .hasSize(expectedCount) + .extracting(caseInstance -> deploymentIdOf(caseInstance.getCaseDefinitionId())) + .containsOnly(expectedDeploymentId); + } + + protected void setCacheSameDeploymentDefinitionMisses(boolean cacheSameDeploymentDefinitionMisses) { + cmmnEngineConfiguration.setCacheSameDeploymentDefinitionMisses(cacheSameDeploymentDefinitionMisses); + processEngineConfiguration.setCacheSameDeploymentDefinitionMisses(cacheSameDeploymentDefinitionMisses); } protected Map profileDatabaseSelects(CommandExecutor commandExecutor, String commandClass, Runnable runnable) { diff --git a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/CmmnEngineConfiguration.java b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/CmmnEngineConfiguration.java index 054976eb307..67bd53024e3 100644 --- a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/CmmnEngineConfiguration.java +++ b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/CmmnEngineConfiguration.java @@ -84,6 +84,7 @@ import org.flowable.cmmn.engine.impl.deployer.CaseDefinitionDiagramHelper; import org.flowable.cmmn.engine.impl.deployer.CmmnDeployer; import org.flowable.cmmn.engine.impl.deployer.CmmnDeploymentManager; +import org.flowable.cmmn.engine.impl.deployer.SameDeploymentCaseDefinitionLookup; import org.flowable.cmmn.engine.impl.el.CmmnExpressionManager; import org.flowable.cmmn.engine.impl.eventregistry.CmmnEventInstanceOutParameterHandler; import org.flowable.cmmn.engine.impl.eventregistry.CmmnEventInstanceOutParameterHandlerImpl; @@ -424,6 +425,17 @@ public class CmmnEngineConfiguration extends AbstractBuildableEngineConfiguratio protected int caseDefinitionCacheLimit = -1; protected DeploymentCache caseDefinitionCache; + protected DeploymentCache sameDeploymentDefinitionCache; + + /** + * Set to true (the default) to read a cached definition id from the database on every hit (one select by id), so that + * a definition that was undeployed on another node is detected and the lookup falls back to the query. + *

+ * When false, the cached definition is taken from the definition cache without a select, and a definition that was + * undeployed on another cluster node is still returned on this node until it is evicted or the node is restarted. + * This can only happen when a single child deployment is deleted while its parent deployment stays. + */ + protected boolean verifyCachedSameDeploymentDefinitions = true; protected CmmnParser cmmnParser; protected List preCmmnParseHandlers; @@ -1161,6 +1173,14 @@ protected void initCaseDefinitionCache() { caseDefinitionCache = new DefaultDeploymentCache<>(caseDefinitionCacheLimit); } } + + if (sameDeploymentDefinitionCache == null) { + if (caseDefinitionCacheLimit <= 0) { + sameDeploymentDefinitionCache = new DefaultDeploymentCache<>(); + } else { + sameDeploymentDefinitionCache = new DefaultDeploymentCache<>(caseDefinitionCacheLimit); + } + } } protected void initDeploymentManager() { @@ -1172,6 +1192,10 @@ protected void initDeploymentManager() { deploymentManager.setCaseDefinitionEntityManager(caseDefinitionEntityManager); deploymentManager.setDeploymentEntityManager(cmmnDeploymentEntityManager); } + + if (deploymentManager.getSameDeploymentCaseDefinitionLookup() == null) { + deploymentManager.setSameDeploymentCaseDefinitionLookup(new SameDeploymentCaseDefinitionLookup(sameDeploymentDefinitionCache, this)); + } } public void initCmmnParser() { @@ -2529,6 +2553,24 @@ public CmmnEngineConfiguration setCaseDefinitionCache(DeploymentCache getSameDeploymentDefinitionCache() { + return sameDeploymentDefinitionCache; + } + + public CmmnEngineConfiguration setSameDeploymentDefinitionCache(DeploymentCache sameDeploymentDefinitionCache) { + this.sameDeploymentDefinitionCache = sameDeploymentDefinitionCache; + return this; + } + + public boolean isVerifyCachedSameDeploymentDefinitions() { + return verifyCachedSameDeploymentDefinitions; + } + + public CmmnEngineConfiguration setVerifyCachedSameDeploymentDefinitions(boolean verifyCachedSameDeploymentDefinitions) { + this.verifyCachedSameDeploymentDefinitions = verifyCachedSameDeploymentDefinitions; + return this; + } + public ProcessInstanceService getProcessInstanceService() { return processInstanceService; } diff --git a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/deployer/CmmnDeployer.java b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/deployer/CmmnDeployer.java index 3a3e1bca214..dd79f044160 100644 --- a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/deployer/CmmnDeployer.java +++ b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/deployer/CmmnDeployer.java @@ -12,6 +12,7 @@ */ package org.flowable.cmmn.engine.impl.deployer; +import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; import java.util.LinkedHashSet; @@ -103,6 +104,7 @@ public void deploy(EngineDeployment deployment, Map deploymentSe Map mapOfNewCaseDefinitionToPreviousVersion = getPreviousVersionsOfCaseDefinitions(parseResult); setCaseDefinitionVersionsAndIds(parseResult, mapOfNewCaseDefinitionToPreviousVersion); persistCaseDefinitions(parseResult); + evictSameDeploymentCaseDefinitions(parseResult); updateEventSubscriptions(parseResult, mapOfNewCaseDefinitionToPreviousVersion); } else { @@ -113,6 +115,15 @@ public void deploy(EngineDeployment deployment, Map deploymentSe updateCachingAndArtifacts(parseResult); } + protected void evictSameDeploymentCaseDefinitions(CmmnParseResult parseResult) { + List caseDefinitionKeys = new ArrayList<>(); + for (CaseDefinitionEntity caseDefinition : parseResult.getAllCaseDefinitions()) { + caseDefinitionKeys.add(caseDefinition.getKey()); + } + cmmnEngineConfiguration.getDeploymentManager() + .evictSameDeploymentCaseDefinitions(((CmmnDeploymentEntity) parseResult.getDeployment()).getParentDeploymentId(), caseDefinitionKeys); + } + public static boolean isCmmnResource(String resourceName) { for (String suffix : CMMN_RESOURCE_SUFFIXES) { if (resourceName.endsWith(suffix)) { diff --git a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/deployer/CmmnDeploymentManager.java b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/deployer/CmmnDeploymentManager.java index 6878e751607..ca6c9256496 100644 --- a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/deployer/CmmnDeploymentManager.java +++ b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/deployer/CmmnDeploymentManager.java @@ -14,10 +14,12 @@ package org.flowable.cmmn.engine.impl.deployer; import java.util.ArrayList; +import java.util.Collection; import java.util.Comparator; import java.util.List; import java.util.Map; +import org.apache.commons.lang3.StringUtils; import org.flowable.cmmn.api.repository.CaseDefinition; import org.flowable.cmmn.engine.CmmnEngineConfiguration; import org.flowable.cmmn.engine.impl.persistence.entity.CaseDefinitionEntity; @@ -35,11 +37,13 @@ import org.flowable.common.engine.impl.EngineDeployer; import org.flowable.common.engine.impl.event.FlowableEntityEventImpl; import org.flowable.common.engine.impl.interceptor.EngineConfigurationConstants; +import org.flowable.common.engine.impl.persistence.deploy.AbstractSameDeploymentDefinitionLookup; import org.flowable.common.engine.impl.persistence.deploy.DeploymentCache; public class CmmnDeploymentManager { protected DeploymentCache caseDefinitionCache; + protected SameDeploymentCaseDefinitionLookup sameDeploymentCaseDefinitionLookup; protected List deployers; protected CmmnEngineConfiguration cmmnEngineConfiguration; protected CmmnDeploymentEntityManager deploymentEntityManager; @@ -121,6 +125,32 @@ public CaseDefinitionCacheEntry resolveCaseDefinition(CaseDefinition caseDefinit return cachedCaseDefinition; } + /** + * Returns the case definition with the given key in the deployments of the given parent deployment, or null when + * there is none. + */ + public CaseDefinition findCaseDefinitionByParentDeploymentAndKey(String parentDeploymentId, String caseDefinitionKey, String tenantId) { + String cacheKey = AbstractSameDeploymentDefinitionLookup.parentDeploymentKey(parentDeploymentId, caseDefinitionKey); + return sameDeploymentCaseDefinitionLookup.find(cacheKey, tenantId, sameDeploymentCaseDefinitionLookup.isCacheMisses(), () -> { + if (tenantId != null && !CmmnEngineConfiguration.NO_TENANT_ID.equals(tenantId)) { + return caseDefinitionEntityManager.findCaseDefinitionByParentDeploymentAndKeyAndTenantId(parentDeploymentId, caseDefinitionKey, tenantId); + } + return caseDefinitionEntityManager.findCaseDefinitionByParentDeploymentAndKey(parentDeploymentId, caseDefinitionKey); + }); + } + + /** + * Removes the cached lookups of the given case definition keys in the given parent deployment. + */ + public void evictSameDeploymentCaseDefinitions(String parentDeploymentId, Collection caseDefinitionKeys) { + if (StringUtils.isEmpty(parentDeploymentId)) { + return; + } + for (String caseDefinitionKey : caseDefinitionKeys) { + sameDeploymentCaseDefinitionLookup.evict(AbstractSameDeploymentDefinitionLookup.parentDeploymentKey(parentDeploymentId, caseDefinitionKey)); + } + } + public void removeDeployment(String deploymentId) { removeDeployment(deploymentId, true); } @@ -143,13 +173,24 @@ public void removeDeployment(String deploymentId, boolean cascade) { deploymentEntityManager.deleteDeploymentAndRelatedData(deploymentId, cascade); FlowableEventDispatcher eventDispatcher = cmmnEngineConfiguration.getEventDispatcher(); + List caseDefinitionKeys = new ArrayList<>(); for (CaseDefinition caseDefinition : caseDefinitions) { caseDefinitionCache.remove(caseDefinition.getId()); + caseDefinitionKeys.add(caseDefinition.getKey()); if (eventDispatcher != null && eventDispatcher.isEnabled()) { eventDispatcher.dispatchEvent(new FlowableEntityEventImpl(caseDefinition, FlowableEngineEventType.DEFINITION_UNDEPLOYED), EngineConfigurationConstants.KEY_CMMN_ENGINE_CONFIG); } } + evictSameDeploymentCaseDefinitions(deployment.getParentDeploymentId(), caseDefinitionKeys); + } + + public SameDeploymentCaseDefinitionLookup getSameDeploymentCaseDefinitionLookup() { + return sameDeploymentCaseDefinitionLookup; + } + + public void setSameDeploymentCaseDefinitionLookup(SameDeploymentCaseDefinitionLookup sameDeploymentCaseDefinitionLookup) { + this.sameDeploymentCaseDefinitionLookup = sameDeploymentCaseDefinitionLookup; } public List getDeployers() { diff --git a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/deployer/SameDeploymentCaseDefinitionLookup.java b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/deployer/SameDeploymentCaseDefinitionLookup.java new file mode 100644 index 00000000000..0f1f7b00762 --- /dev/null +++ b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/deployer/SameDeploymentCaseDefinitionLookup.java @@ -0,0 +1,48 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flowable.cmmn.engine.impl.deployer; + +import org.flowable.cmmn.api.repository.CaseDefinition; +import org.flowable.cmmn.engine.CmmnEngineConfiguration; +import org.flowable.cmmn.engine.impl.persistence.entity.deploy.CaseDefinitionCacheEntry; +import org.flowable.common.engine.impl.persistence.deploy.AbstractSameDeploymentDefinitionLookup; +import org.flowable.common.engine.impl.persistence.deploy.DeploymentCache; + +/** + * Looks up case definitions by key in the deployments of a parent deployment, caching the found definition ids. + */ +public class SameDeploymentCaseDefinitionLookup extends AbstractSameDeploymentDefinitionLookup { + + protected CmmnEngineConfiguration cmmnEngineConfiguration; + + public SameDeploymentCaseDefinitionLookup(DeploymentCache cache, CmmnEngineConfiguration cmmnEngineConfiguration) { + super(cache, cmmnEngineConfiguration); + this.cmmnEngineConfiguration = cmmnEngineConfiguration; + } + + @Override + protected boolean isVerifyCachedDefinitions() { + return cmmnEngineConfiguration.isVerifyCachedSameDeploymentDefinitions(); + } + + @Override + protected CaseDefinition findCachedDefinition(String definitionId) { + CaseDefinitionCacheEntry cacheEntry = cmmnEngineConfiguration.getCaseDefinitionCache().get(definitionId); + return cacheEntry != null ? cacheEntry.getCaseDefinition() : null; + } + + @Override + protected CaseDefinition findPersistedDefinition(String definitionId) { + return cmmnEngineConfiguration.getCaseDefinitionEntityManager().findById(definitionId); + } +} diff --git a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/runtime/CaseInstanceHelperImpl.java b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/runtime/CaseInstanceHelperImpl.java index a15bbcacb6b..d37d0db95ff 100644 --- a/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/runtime/CaseInstanceHelperImpl.java +++ b/modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/runtime/CaseInstanceHelperImpl.java @@ -121,8 +121,8 @@ public CaseDefinition resolveCaseDefinition(String caseDefinitionKey, String ten if (tenantId != null && !CmmnEngineConfiguration.NO_TENANT_ID.equals(tenantId)) { if (parentDeploymentId != null) { - caseDefinition = caseDefinitionEntityManager - .findCaseDefinitionByParentDeploymentAndKeyAndTenantId(parentDeploymentId, caseDefinitionKey, tenantId); + caseDefinition = cmmnEngineConfiguration.getDeploymentManager() + .findCaseDefinitionByParentDeploymentAndKey(parentDeploymentId, caseDefinitionKey, tenantId); } if (caseDefinition == null) { caseDefinition = caseDefinitionEntityManager.findLatestCaseDefinitionByKeyAndTenantId(caseDefinitionKey, tenantId); @@ -137,7 +137,8 @@ public CaseDefinition resolveCaseDefinition(String caseDefinitionKey, String ten } } else { if (parentDeploymentId != null) { - caseDefinition = caseDefinitionEntityManager.findCaseDefinitionByParentDeploymentAndKey(parentDeploymentId, caseDefinitionKey); + caseDefinition = cmmnEngineConfiguration.getDeploymentManager() + .findCaseDefinitionByParentDeploymentAndKey(parentDeploymentId, caseDefinitionKey, tenantId); } if (caseDefinition == null) { caseDefinition = caseDefinitionEntityManager.findLatestCaseDefinitionByKey(caseDefinitionKey); diff --git a/modules/flowable-dmn-engine-configurator/src/test/java/org/flowable/dmn/engine/test/VerifyDatabaseOperationsTest.java b/modules/flowable-dmn-engine-configurator/src/test/java/org/flowable/dmn/engine/test/VerifyDatabaseOperationsTest.java index c5494c012f2..2527d500384 100644 --- a/modules/flowable-dmn-engine-configurator/src/test/java/org/flowable/dmn/engine/test/VerifyDatabaseOperationsTest.java +++ b/modules/flowable-dmn-engine-configurator/src/test/java/org/flowable/dmn/engine/test/VerifyDatabaseOperationsTest.java @@ -14,7 +14,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; +import static org.flowable.common.engine.impl.persistence.deploy.AbstractSameDeploymentDefinitionLookup.parentDeploymentKey; +import java.util.List; import java.util.Map; import org.flowable.cmmn.api.CmmnRepositoryService; @@ -28,9 +30,13 @@ import org.flowable.common.engine.impl.interceptor.CommandExecutor; import org.flowable.common.engine.impl.interceptor.CommandInterceptor; import org.flowable.common.engine.impl.interceptor.EngineConfigurationConstants; +import org.flowable.dmn.api.DecisionExecutionAuditContainer; import org.flowable.dmn.api.DmnDecision; +import org.flowable.dmn.api.DmnDecisionService; import org.flowable.dmn.api.DmnDeployment; +import org.flowable.dmn.api.DmnDeploymentBuilder; import org.flowable.dmn.api.DmnRepositoryService; +import org.flowable.dmn.api.ExecuteDecisionBuilder; import org.flowable.dmn.engine.DmnEngineConfiguration; import org.flowable.engine.ProcessEngine; import org.flowable.engine.HistoryService; @@ -56,14 +62,19 @@ @DisabledIfSystemProperty(named = "disableWhen", matches = "cockroachdb") public class VerifyDatabaseOperationsTest { + protected static final String KEY_RESOURCE = "org/flowable/dmn/engine/test/deployment/simple.dmn"; + protected static final String OTHER_RESOURCE = "org/flowable/cmmn/test/runtime/DecisionTaskTest.testDecisionServiceTask.dmn"; + protected ProcessEngineConfigurationImpl processEngineConfiguration; protected CmmnEngineConfiguration cmmnEngineConfiguration; + protected DmnEngineConfiguration dmnEngineConfiguration; protected RepositoryService repositoryService; protected RuntimeService runtimeService; protected HistoryService historyService; protected CmmnRepositoryService cmmnRepositoryService; protected CmmnRuntimeService cmmnRuntimeService; protected DmnRepositoryService dmnRepositoryService; + protected DmnDecisionService dmnDecisionService; protected DbSqlSessionFactory oldDbSqlSessionFactory; @@ -72,7 +83,7 @@ protected void setUp(ProcessEngine processEngine) { this.processEngineConfiguration = (ProcessEngineConfigurationImpl) processEngine.getProcessEngineConfiguration(); this.cmmnEngineConfiguration = (CmmnEngineConfiguration) processEngineConfiguration.getEngineConfigurations() .get(EngineConfigurationConstants.KEY_CMMN_ENGINE_CONFIG); - DmnEngineConfiguration dmnEngineConfiguration = (DmnEngineConfiguration) processEngineConfiguration.getEngineConfigurations() + this.dmnEngineConfiguration = (DmnEngineConfiguration) processEngineConfiguration.getEngineConfigurations() .get(EngineConfigurationConstants.KEY_DMN_ENGINE_CONFIG); this.repositoryService = processEngine.getRepositoryService(); this.runtimeService = processEngine.getRuntimeService(); @@ -80,6 +91,7 @@ protected void setUp(ProcessEngine processEngine) { this.cmmnRepositoryService = cmmnEngineConfiguration.getCmmnRepositoryService(); this.cmmnRuntimeService = cmmnEngineConfiguration.getCmmnRuntimeService(); this.dmnRepositoryService = dmnEngineConfiguration.getDmnRepositoryService(); + this.dmnDecisionService = dmnEngineConfiguration.getDmnDecisionService(); // The process, CMMN and DMN engine share the session factories this.oldDbSqlSessionFactory = processEngineConfiguration.getDbSqlSessionFactory(); @@ -101,6 +113,7 @@ protected void setUp(ProcessEngine processEngine) { @AfterEach protected void tearDown() { + setCacheSameDeploymentDefinitionMisses(false); processEngineConfiguration.addSessionFactory(oldDbSqlSessionFactory); FlowableProfiler.getInstance().reset(); @@ -143,6 +156,27 @@ public void testProcessWithDecisionTaskFromSameDeployment() { assertThat(historyService.createHistoricVariableInstanceQuery().variableName("outputVariable1").singleResult()) .extracting(HistoricVariableInstance::getValue) .isEqualTo("result1"); + + Map secondSelects = profileDatabaseSelects(processEngineConfiguration.getCommandExecutor(), + "org.flowable.engine.impl.cmd.StartProcessInstanceCmd", + () -> runtimeService.createProcessInstanceBuilder() + .processDefinitionKey("oneDecisionTaskProcess") + .variable("inputVariable1", 1) + .start()); + + // The cached decision id is verified by selecting the decision by id + assertThat(secondSelects).containsOnly( + entry("selectById org.flowable.dmn.engine.impl.persistence.entity.DecisionEntityImpl", 1L), + entry("selectChildExecutionsByProcessInstanceId", 1L), + entry("selectEntityLinksByQuery", 1L), + entry("selectExecutionsByParentExecutionId", 2L), + entry("selectLatestProcessDefinitionByKey", 1L), + entry("selectProcessDefinitionInfoByProcessDefinitionId", 1L), + entry("selectVariablesByQuery", 1L) + ); + assertThat(historyService.createHistoricVariableInstanceQuery().variableName("outputVariable1").list()) + .extracting(HistoricVariableInstance::getValue) + .containsExactly("result1", "result1"); } @Test @@ -169,6 +203,25 @@ public void testCaseWithDecisionTaskFromSameDeployment() { ); CaseInstance caseInstance = cmmnRuntimeService.createCaseInstanceQuery().caseDefinitionKey("oneDecisionTaskCase").singleResult(); assertThat(cmmnRuntimeService.getVariable(caseInstance.getId(), "resultVar")).isEqualTo("executed"); + + Map secondSelects = profileDatabaseSelects(cmmnEngineConfiguration.getCommandExecutor(), + "org.flowable.cmmn.engine.impl.cmd.StartCaseInstanceCmd", + () -> cmmnRuntimeService.createCaseInstanceBuilder() + .caseDefinitionKey("oneDecisionTaskCase") + .variable("testVar", "test2") + .start()); + + assertThat(secondSelects).containsOnly( + entry("selectById org.flowable.dmn.engine.impl.persistence.entity.DecisionEntityImpl", 1L), + entry("selectEventSubscriptionsByScopeIdAndType", 1L), + entry("selectLatestCaseDefinitionByKey", 1L), + entry("selectPlanItemInstancesByCaseInstanceIdAndPlanItemId", 1L) + ); + List caseInstances = cmmnRuntimeService.createCaseInstanceQuery().caseDefinitionKey("oneDecisionTaskCase").list(); + assertThat(caseInstances).hasSize(2); + assertThat(caseInstances) + .extracting(instance -> cmmnRuntimeService.getVariable(instance.getId(), "resultVar")) + .containsExactly("executed", "executed"); } @Test @@ -213,6 +266,397 @@ public void testGetDecisionsForCaseDefinition() { ); } + @Test + public void testExecuteDecisionWithParentDeploymentIdTwice() { + DmnDeployment deployment = deployDecision(KEY_RESOURCE, null, null); + assertThat(deployment.getParentDeploymentId()).isEqualTo(deployment.getId()); + + DecisionExecution first = executeWithParent(deployment.getId(), null, false); + assertThat(first.selects).containsOnly( + entry("selectDecisionByDeploymentAndKey", 1L), + entry("selectDmnDeploymentsByQueryCriteria", 1L) + ); + assertExecuted(first, deployment); + + DecisionExecution second = executeWithParent(deployment.getId(), null, false); + assertThat(second.selects).containsOnly( + entry("selectById org.flowable.dmn.engine.impl.persistence.entity.DecisionEntityImpl", 1L) + ); + assertExecuted(second, deployment); + } + + @Test + public void testExecuteDecisionWithParentDeploymentIdTwiceWithoutVerifyingCachedDefinitions() { + dmnEngineConfiguration.setVerifyCachedSameDeploymentDefinitions(false); + try { + DmnDeployment deployment = deployDecision(KEY_RESOURCE, null, null); + + DecisionExecution first = executeWithParent(deployment.getId(), null, false); + assertThat(first.selects).containsOnly( + entry("selectDecisionByDeploymentAndKey", 1L), + entry("selectDmnDeploymentsByQueryCriteria", 1L) + ); + assertExecuted(first, deployment); + + DecisionExecution second = executeWithParent(deployment.getId(), null, false); + assertThat(second.selects).isEmpty(); + assertExecuted(second, deployment); + } finally { + dmnEngineConfiguration.setVerifyCachedSameDeploymentDefinitions(true); + } + } + + @Test + public void testExecuteDecisionWithParentDeploymentIdMissForOtherTenantDoesNotHideDefinition() { + setCacheSameDeploymentDefinitionMisses(true); + DmnDeployment parent = deployDecision(KEY_RESOURCE, null, null); + + // Execute for tenant X, the parent has no decision for that tenant and the fallback finds the one without tenant + DecisionExecution first = executeWithParent(parent.getId(), "X", true); + assertThat(first.selects).containsOnly( + entry("selectDecisionByDeploymentAndKeyAndTenantId", 1L), + entry("selectDmnDeploymentsByQueryCriteria", 1L), + entry("selectLatestDecisionByKey", 1L), + entry("selectLatestDecisionByKeyAndTenantId", 1L) + ); + assertExecuted(first, parent); + + // Execute without tenant, the miss for tenant X must not hide the decision + DecisionExecution second = executeWithParent(parent.getId(), null, false); + assertThat(second.selects).containsOnly( + entry("selectDecisionByDeploymentAndKey", 1L), + entry("selectDmnDeploymentsByQueryCriteria", 1L) + ); + assertExecuted(second, parent); + } + + @Test + public void testExecuteDecisionWithParentDeploymentIdMissTwice() { + DmnDeployment parent = deployDecision(OTHER_RESOURCE, null, null); + DmnDeployment latest = deployDecision(KEY_RESOURCE, null, null); + + DecisionExecution first = executeWithParent(parent.getId(), null, false); + assertThat(first.selects).containsOnly( + entry("selectDecisionByDeploymentAndKey", 1L), + entry("selectDmnDeploymentsByQueryCriteria", 1L), + entry("selectLatestDecisionByKey", 1L) + ); + assertExecuted(first, latest); + + DecisionExecution second = executeWithParent(parent.getId(), null, false); + assertThat(second.selects).containsOnly( + entry("selectDecisionByDeploymentAndKey", 1L), + entry("selectDmnDeploymentsByQueryCriteria", 1L), + entry("selectLatestDecisionByKey", 1L) + ); + assertExecuted(second, latest); + } + + @Test + public void testExecuteDecisionWithParentDeploymentIdMissTwiceWithCachedMisses() { + setCacheSameDeploymentDefinitionMisses(true); + DmnDeployment parent = deployDecision(OTHER_RESOURCE, null, null); + DmnDeployment latest = deployDecision(KEY_RESOURCE, null, null); + + DecisionExecution first = executeWithParent(parent.getId(), null, false); + assertThat(first.selects).containsOnly( + entry("selectDecisionByDeploymentAndKey", 1L), + entry("selectDmnDeploymentsByQueryCriteria", 1L), + entry("selectLatestDecisionByKey", 1L) + ); + assertExecuted(first, latest); + + DecisionExecution second = executeWithParent(parent.getId(), null, false); + assertThat(second.selects).containsOnly( + entry("selectLatestDecisionByKey", 1L) + ); + assertExecuted(second, latest); + } + + @Test + public void testExecuteDecisionWithParentDeploymentIdMissThenChildDeployed() { + setCacheSameDeploymentDefinitionMisses(true); + DmnDeployment parent = deployDecision(OTHER_RESOURCE, null, null); + DmnDeployment latest = deployDecision(KEY_RESOURCE, null, null); + + DecisionExecution first = executeWithParent(parent.getId(), null, false); + assertThat(first.selects).containsOnly( + entry("selectDecisionByDeploymentAndKey", 1L), + entry("selectDmnDeploymentsByQueryCriteria", 1L), + entry("selectLatestDecisionByKey", 1L) + ); + assertExecuted(first, latest); + + // Only the first deployment with the parent id is checked and that is the parent itself, so the child is not found + deployDecision(KEY_RESOURCE, parent.getId(), null); + DmnDeployment latest2 = deployDecision(KEY_RESOURCE, null, null); + + DecisionExecution second = executeWithParent(parent.getId(), null, false); + assertThat(second.selects).containsOnly( + entry("selectDecisionByDeploymentAndKey", 1L), + entry("selectDmnDeploymentsByQueryCriteria", 1L), + entry("selectLatestDecisionByKey", 1L) + ); + assertExecuted(second, latest2); + } + + @Test + public void testExecuteDecisionWithParentDeploymentIdMissPicksUpNewVersion() { + DmnDeployment parent = deployDecision(OTHER_RESOURCE, null, null); + DmnDeployment latest1 = deployDecision(KEY_RESOURCE, null, null); + + DecisionExecution first = executeWithParent(parent.getId(), null, false); + assertThat(first.selects).containsOnly( + entry("selectDecisionByDeploymentAndKey", 1L), + entry("selectDmnDeploymentsByQueryCriteria", 1L), + entry("selectLatestDecisionByKey", 1L) + ); + assertExecuted(first, latest1); + + DmnDeployment latest2 = deployDecision(KEY_RESOURCE, null, null); + + DecisionExecution second = executeWithParent(parent.getId(), null, false); + assertThat(second.selects).containsOnly( + entry("selectDecisionByDeploymentAndKey", 1L), + entry("selectDmnDeploymentsByQueryCriteria", 1L), + entry("selectLatestDecisionByKey", 1L) + ); + assertExecuted(second, latest2); + } + + @Test + public void testExecuteDecisionWithParentDeploymentIdMissPicksUpNewVersionWithCachedMisses() { + setCacheSameDeploymentDefinitionMisses(true); + DmnDeployment parent = deployDecision(OTHER_RESOURCE, null, null); + DmnDeployment latest1 = deployDecision(KEY_RESOURCE, null, null); + + DecisionExecution first = executeWithParent(parent.getId(), null, false); + assertThat(first.selects).containsOnly( + entry("selectDecisionByDeploymentAndKey", 1L), + entry("selectDmnDeploymentsByQueryCriteria", 1L), + entry("selectLatestDecisionByKey", 1L) + ); + assertExecuted(first, latest1); + + DmnDeployment latest2 = deployDecision(KEY_RESOURCE, null, null); + + DecisionExecution second = executeWithParent(parent.getId(), null, false); + assertThat(second.selects).containsOnly( + entry("selectLatestDecisionByKey", 1L) + ); + assertExecuted(second, latest2); + } + + @Test + public void testExecuteDecisionWithParentDeploymentIdAfterChildUndeployed() { + DmnDeployment parent = deployDecision(OTHER_RESOURCE, null, null); + DmnDeployment child = deployDecision(KEY_RESOURCE, parent.getId(), null); + DmnDeployment latest = deployDecision(KEY_RESOURCE, null, null); + + // Only the first deployment with the parent id is checked and that is the parent itself, so the child is not found + DecisionExecution first = executeWithParent(parent.getId(), null, false); + assertThat(first.selects).containsOnly( + entry("selectDecisionByDeploymentAndKey", 1L), + entry("selectDmnDeploymentsByQueryCriteria", 1L), + entry("selectLatestDecisionByKey", 1L) + ); + assertExecuted(first, latest); + + dmnRepositoryService.deleteDeployment(child.getId()); + + DecisionExecution second = executeWithParent(parent.getId(), null, false); + assertThat(second.selects).containsOnly( + entry("selectDecisionByDeploymentAndKey", 1L), + entry("selectDmnDeploymentsByQueryCriteria", 1L), + entry("selectLatestDecisionByKey", 1L) + ); + assertExecuted(second, latest); + } + + @Test + public void testExecuteDecisionWithParentDeploymentIdOtherTenant() { + DmnDeployment parent = deployDecision(KEY_RESOURCE, null, "tenantA"); + deployDecision(KEY_RESOURCE, null, "tenantB"); + DmnDeployment tenantBLatest = deployDecision(KEY_RESOURCE, null, "tenantB"); + + DecisionExecution first = executeWithParent(parent.getId(), "tenantA", false); + assertThat(first.selects).containsOnly( + entry("selectDecisionByDeploymentAndKeyAndTenantId", 1L), + entry("selectDmnDeploymentsByQueryCriteria", 1L) + ); + assertExecuted(first, parent); + + DecisionExecution second = executeWithParent(parent.getId(), "tenantB", false); + assertThat(second.selects).containsOnly( + entry("selectById org.flowable.dmn.engine.impl.persistence.entity.DecisionEntityImpl", 1L), + entry("selectDecisionByDeploymentAndKeyAndTenantId", 1L), + entry("selectDmnDeploymentsByQueryCriteria", 1L), + entry("selectLatestDecisionByKeyAndTenantId", 1L) + ); + assertExecuted(second, tenantBLatest); + + DecisionExecution third = executeWithParent(parent.getId(), "tenantB", false); + assertThat(third.selects).containsOnly( + entry("selectById org.flowable.dmn.engine.impl.persistence.entity.DecisionEntityImpl", 1L), + entry("selectDecisionByDeploymentAndKeyAndTenantId", 1L), + entry("selectDmnDeploymentsByQueryCriteria", 1L), + entry("selectLatestDecisionByKeyAndTenantId", 1L) + ); + assertExecuted(third, tenantBLatest); + } + + @Test + public void testExecuteDecisionWithParentDeploymentIdDefaultTenantFallback() { + DmnDeployment parent = deployDecision(OTHER_RESOURCE, null, "tenantA"); + DmnDeployment noTenant = deployDecision(KEY_RESOURCE, null, null); + + DecisionExecution first = executeWithParent(parent.getId(), "tenantA", true); + assertThat(first.selects).containsOnly( + entry("selectDecisionByDeploymentAndKeyAndTenantId", 1L), + entry("selectDmnDeploymentsByQueryCriteria", 1L), + entry("selectLatestDecisionByKey", 1L), + entry("selectLatestDecisionByKeyAndTenantId", 1L) + ); + assertExecuted(first, noTenant); + + DecisionExecution second = executeWithParent(parent.getId(), "tenantA", true); + assertThat(second.selects).containsOnly( + entry("selectDecisionByDeploymentAndKeyAndTenantId", 1L), + entry("selectDmnDeploymentsByQueryCriteria", 1L), + entry("selectLatestDecisionByKey", 1L), + entry("selectLatestDecisionByKeyAndTenantId", 1L) + ); + assertExecuted(second, noTenant); + } + + @Test + public void testExecuteDecisionWithParentDeploymentIdExternalParentAfterChildUndeployed() { + String externalParentId = "externalParent"; + DmnDeployment child = deployDecision(KEY_RESOURCE, externalParentId, null); + DmnDeployment latest = deployDecision(KEY_RESOURCE, null, null); + + DecisionExecution first = executeWithParent(externalParentId, null, false); + assertThat(first.selects).containsOnly( + entry("selectDecisionByDeploymentAndKey", 1L), + entry("selectDmnDeploymentsByQueryCriteria", 1L) + ); + assertExecuted(first, child); + + DecisionExecution second = executeWithParent(externalParentId, null, false); + assertThat(second.selects).containsOnly( + entry("selectById org.flowable.dmn.engine.impl.persistence.entity.DecisionEntityImpl", 1L) + ); + assertExecuted(second, child); + + dmnRepositoryService.deleteDeployment(child.getId()); + + DecisionExecution third = executeWithParent(externalParentId, null, false); + assertThat(third.selects).containsOnly( + entry("selectDmnDeploymentsByQueryCriteria", 1L), + entry("selectLatestDecisionByKey", 1L) + ); + assertExecuted(third, latest); + } + + @Test + public void testExecuteDecisionWithParentDeploymentIdExternalParentMissThenChildDeployed() { + setCacheSameDeploymentDefinitionMisses(true); + String externalParentId = "externalParent"; + DmnDeployment latest = deployDecision(KEY_RESOURCE, null, null); + + DecisionExecution first = executeWithParent(externalParentId, null, false); + assertThat(first.selects).containsOnly( + entry("selectDmnDeploymentsByQueryCriteria", 1L), + entry("selectLatestDecisionByKey", 1L) + ); + assertExecuted(first, latest); + + DmnDeployment child = deployDecision(KEY_RESOURCE, externalParentId, null); + deployDecision(KEY_RESOURCE, null, null); + + DecisionExecution second = executeWithParent(externalParentId, null, false); + assertThat(second.selects).containsOnly( + entry("selectDecisionByDeploymentAndKey", 1L), + entry("selectDmnDeploymentsByQueryCriteria", 1L) + ); + assertExecuted(second, child); + } + + @Test + public void testExecuteDecisionWithParentDeploymentIdExternalParentWithStaleCachedDefinition() { + String externalParentId = "externalParent"; + DmnDeployment child1 = deployDecision(KEY_RESOURCE, externalParentId, null); + + DecisionExecution first = executeWithParent(externalParentId, null, false); + assertExecuted(first, child1); + String cacheKey = parentDeploymentKey(externalParentId, "decision1"); + String staleDecisionId = dmnRepositoryService.createDecisionQuery().deploymentId(child1.getId()).singleResult().getId(); + assertThat(dmnEngineConfiguration.getSameDeploymentDefinitionCache().get(cacheKey)).isEqualTo(staleDecisionId); + + dmnRepositoryService.deleteDeployment(child1.getId()); + DmnDeployment child2 = deployDecision(KEY_RESOURCE, externalParentId, null); + + // Simulate a node that did not see the undeploy and the deploy + dmnEngineConfiguration.getSameDeploymentDefinitionCache().add(cacheKey, staleDecisionId); + + DecisionExecution second = executeWithParent(externalParentId, null, false); + assertThat(second.selects).containsOnly( + entry("selectById org.flowable.dmn.engine.impl.persistence.entity.DecisionEntityImpl", 1L), + entry("selectDecisionByDeploymentAndKey", 1L), + entry("selectDmnDeploymentsByQueryCriteria", 1L) + ); + assertExecuted(second, child2); + String decisionId = dmnRepositoryService.createDecisionQuery().deploymentId(child2.getId()).singleResult().getId(); + assertThat(dmnEngineConfiguration.getSameDeploymentDefinitionCache().get(cacheKey)).isEqualTo(decisionId); + } + + protected void setCacheSameDeploymentDefinitionMisses(boolean cacheSameDeploymentDefinitionMisses) { + dmnEngineConfiguration.setCacheSameDeploymentDefinitionMisses(cacheSameDeploymentDefinitionMisses); + } + + protected DmnDeployment deployDecision(String resource, String parentDeploymentId, String tenantId) { + DmnDeploymentBuilder builder = dmnRepositoryService.createDeployment().addClasspathResource(resource); + if (parentDeploymentId != null) { + builder.parentDeploymentId(parentDeploymentId); + } + if (tenantId != null) { + builder.tenantId(tenantId); + } + return builder.deploy(); + } + + protected DecisionExecution executeWithParent(String parentDeploymentId, String tenantId, boolean fallbackToDefaultTenant) { + DecisionExecution execution = new DecisionExecution(); + execution.selects = profileDatabaseSelects(dmnEngineConfiguration.getCommandExecutor(), + "org.flowable.dmn.engine.impl.cmd.EvaluateDecisionCmd", + () -> { + ExecuteDecisionBuilder builder = dmnDecisionService.createExecuteDecisionBuilder() + .decisionKey("decision1") + .parentDeploymentId(parentDeploymentId) + .variable("inputVariable1", 1); + if (tenantId != null) { + builder.tenantId(tenantId); + } + if (fallbackToDefaultTenant) { + builder.fallbackToDefaultTenant(); + } + execution.audit = builder.executeWithAuditTrail(); + }); + return execution; + } + + protected void assertExecuted(DecisionExecution execution, DmnDeployment expectedDeployment) { + DmnDecision expectedDecision = dmnRepositoryService.createDecisionQuery().deploymentId(expectedDeployment.getId()).singleResult(); + assertThat(execution.audit.getDecisionVersion()).isEqualTo(expectedDecision.getVersion()); + assertThat(execution.audit.getDecisionResult()).containsExactly(Map.of("outputVariable1", "result1")); + } + + protected static class DecisionExecution { + + protected Map selects; + protected DecisionExecutionAuditContainer audit; + } + protected Map profileDatabaseSelects(CommandExecutor commandExecutor, String commandClass, Runnable runnable) { // Only the outer command is profiled, the commands of the other engines reuse its command context CommandExecutorImpl commandExecutorImpl = (CommandExecutorImpl) commandExecutor; diff --git a/modules/flowable-dmn-engine/src/main/java/org/flowable/dmn/engine/DmnEngineConfiguration.java b/modules/flowable-dmn-engine/src/main/java/org/flowable/dmn/engine/DmnEngineConfiguration.java index be2de7e5f2c..8b75331a244 100755 --- a/modules/flowable-dmn-engine/src/main/java/org/flowable/dmn/engine/DmnEngineConfiguration.java +++ b/modules/flowable-dmn-engine/src/main/java/org/flowable/dmn/engine/DmnEngineConfiguration.java @@ -89,6 +89,7 @@ import org.flowable.dmn.engine.impl.persistence.deploy.DecisionCacheEntry; import org.flowable.dmn.engine.impl.persistence.deploy.Deployer; import org.flowable.dmn.engine.impl.persistence.deploy.DeploymentManager; +import org.flowable.dmn.engine.impl.persistence.deploy.SameDeploymentDecisionLookup; import org.flowable.dmn.engine.impl.persistence.entity.DecisionEntityManager; import org.flowable.dmn.engine.impl.persistence.entity.DefinitionEntityManagerImpl; import org.flowable.dmn.engine.impl.persistence.entity.DmnDeploymentEntityManager; @@ -179,6 +180,17 @@ public class DmnEngineConfiguration extends AbstractBuildableEngineConfiguration protected int decisionCacheLimit = -1; // By default, no limit protected DeploymentCache definitionCache; + protected DeploymentCache sameDeploymentDefinitionCache; + + /** + * Set to true (the default) to read a cached definition id from the database on every hit (one select by id), so that + * a definition that was undeployed on another node is detected and the lookup falls back to the query. + *

+ * When false, the cached definition is taken from the definition cache without a select, and a definition that was + * undeployed on another cluster node is still returned on this node until it is evicted or the node is restarted. + * This can only happen when a single child deployment is deleted while its parent deployment stays. + */ + protected boolean verifyCachedSameDeploymentDefinitions = true; // HIT POLICIES protected Map hitPolicyBehaviors; @@ -494,10 +506,19 @@ protected void initDeployers() { } } + if (sameDeploymentDefinitionCache == null) { + if (decisionCacheLimit <= 0) { + sameDeploymentDefinitionCache = new DefaultDeploymentCache<>(); + } else { + sameDeploymentDefinitionCache = new DefaultDeploymentCache<>(decisionCacheLimit); + } + } + deploymentManager = new DeploymentManager(definitionCache, this); deploymentManager.setDeployers(deployers); deploymentManager.setDeploymentEntityManager(deploymentEntityManager); deploymentManager.setDecisionEntityManager(decisionEntityManager); + deploymentManager.setSameDeploymentDecisionLookup(new SameDeploymentDecisionLookup(sameDeploymentDefinitionCache, this)); } public Collection getDefaultDeployers() { @@ -940,6 +961,24 @@ public DmnEngineConfiguration setDefinitionCache(DeploymentCache getSameDeploymentDefinitionCache() { + return sameDeploymentDefinitionCache; + } + + public DmnEngineConfiguration setSameDeploymentDefinitionCache(DeploymentCache sameDeploymentDefinitionCache) { + this.sameDeploymentDefinitionCache = sameDeploymentDefinitionCache; + return this; + } + + public boolean isVerifyCachedSameDeploymentDefinitions() { + return verifyCachedSameDeploymentDefinitions; + } + + public DmnEngineConfiguration setVerifyCachedSameDeploymentDefinitions(boolean verifyCachedSameDeploymentDefinitions) { + this.verifyCachedSameDeploymentDefinitions = verifyCachedSameDeploymentDefinitions; + return this; + } + public DmnDeploymentDataManager getDeploymentDataManager() { return deploymentDataManager; } diff --git a/modules/flowable-dmn-engine/src/main/java/org/flowable/dmn/engine/impl/cmd/AbstractExecuteDecisionCmd.java b/modules/flowable-dmn-engine/src/main/java/org/flowable/dmn/engine/impl/cmd/AbstractExecuteDecisionCmd.java index efba0d4c0b2..70024b60f28 100644 --- a/modules/flowable-dmn-engine/src/main/java/org/flowable/dmn/engine/impl/cmd/AbstractExecuteDecisionCmd.java +++ b/modules/flowable-dmn-engine/src/main/java/org/flowable/dmn/engine/impl/cmd/AbstractExecuteDecisionCmd.java @@ -13,7 +13,6 @@ package org.flowable.dmn.engine.impl.cmd; import java.io.Serializable; -import java.util.List; import java.util.Map; import org.apache.commons.lang3.StringUtils; @@ -22,10 +21,8 @@ import org.flowable.common.engine.api.scope.ScopeTypes; import org.flowable.common.engine.impl.interceptor.CommandContext; import org.flowable.dmn.api.DmnDecision; -import org.flowable.dmn.api.DmnDeployment; import org.flowable.dmn.api.ExecuteDecisionContext; import org.flowable.dmn.engine.DmnEngineConfiguration; -import org.flowable.dmn.engine.impl.DmnDeploymentQueryImpl; import org.flowable.dmn.engine.impl.ExecuteDecisionBuilderImpl; import org.flowable.dmn.engine.impl.persistence.deploy.DecisionCacheEntry; import org.flowable.dmn.engine.impl.persistence.entity.DecisionEntityManager; @@ -76,14 +73,9 @@ protected DmnDefinition resolveDefinition() { if (StringUtils.isNotEmpty(decisionKey) && StringUtils.isNotEmpty(parentDeploymentId) && !dmnEngineConfiguration.isAlwaysLookupLatestDefinitionVersion() && StringUtils.isNotEmpty(tenantId)) { - - List dmnDeployments = CommandContextUtil.getDeploymentEntityManager().findDeploymentsByQueryCriteria( - new DmnDeploymentQueryImpl().parentDeploymentId(parentDeploymentId)); - if (dmnDeployments != null && dmnDeployments.size() != 0) { - decision = decisionEntityManager.findDecisionByDeploymentAndKeyAndTenantId( - dmnDeployments.get(0).getId(), decisionKey, tenantId); - } + decision = dmnEngineConfiguration.getDeploymentManager() + .findDecisionByParentDeploymentAndKey(parentDeploymentId, decisionKey, tenantId); if (decision == null) { // If there is no decision table found linked to the deployment id, try to find one without a specific deployment id. @@ -118,13 +110,9 @@ protected DmnDefinition resolveDefinition() { } else if (StringUtils.isNotEmpty(decisionKey) && StringUtils.isNotEmpty(parentDeploymentId) && !dmnEngineConfiguration.isAlwaysLookupLatestDefinitionVersion()) { - - List dmnDeployments = CommandContextUtil.getDeploymentEntityManager().findDeploymentsByQueryCriteria( - new DmnDeploymentQueryImpl().parentDeploymentId(parentDeploymentId)); - if (dmnDeployments != null && dmnDeployments.size() != 0) { - decision = decisionEntityManager.findDecisionByDeploymentAndKey(dmnDeployments.get(0).getId(), decisionKey); - } + decision = dmnEngineConfiguration.getDeploymentManager() + .findDecisionByParentDeploymentAndKey(parentDeploymentId, decisionKey, tenantId); if (decision == null) { // If there is no decision table found linked to the deployment id, try to find one without a specific deployment id. diff --git a/modules/flowable-dmn-engine/src/main/java/org/flowable/dmn/engine/impl/deployer/DmnDeployer.java b/modules/flowable-dmn-engine/src/main/java/org/flowable/dmn/engine/impl/deployer/DmnDeployer.java index 469578e064e..6a3df5b0e28 100755 --- a/modules/flowable-dmn-engine/src/main/java/org/flowable/dmn/engine/impl/deployer/DmnDeployer.java +++ b/modules/flowable-dmn-engine/src/main/java/org/flowable/dmn/engine/impl/deployer/DmnDeployer.java @@ -12,7 +12,9 @@ */ package org.flowable.dmn.engine.impl.deployer; +import java.util.ArrayList; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import org.flowable.common.engine.api.delegate.event.FlowableEngineEventType; @@ -62,6 +64,13 @@ public void deploy(DmnDeploymentEntity deployment, Map deploymen Map mapOfNewDefinitionToPreviousVersion = getPreviousVersionsOfDecisions(parsedDeployment); setDecisionVersionsAndIds(parsedDeployment, mapOfNewDefinitionToPreviousVersion); persistDecisions(parsedDeployment); + + List decisionKeys = new ArrayList<>(); + for (DecisionEntity decision : parsedDeployment.getAllDecisions()) { + decisionKeys.add(decision.getKey()); + } + CommandContextUtil.getDmnEngineConfiguration().getDeploymentManager() + .evictSameDeploymentDecisions(parsedDeployment.getDeployment().getParentDeploymentId(), decisionKeys); } else { makeDecisionsConsistentWithPersistedVersions(parsedDeployment); } diff --git a/modules/flowable-dmn-engine/src/main/java/org/flowable/dmn/engine/impl/persistence/deploy/DeploymentManager.java b/modules/flowable-dmn-engine/src/main/java/org/flowable/dmn/engine/impl/persistence/deploy/DeploymentManager.java index a5686fc8c9e..ad698f86093 100644 --- a/modules/flowable-dmn-engine/src/main/java/org/flowable/dmn/engine/impl/persistence/deploy/DeploymentManager.java +++ b/modules/flowable-dmn-engine/src/main/java/org/flowable/dmn/engine/impl/persistence/deploy/DeploymentManager.java @@ -12,18 +12,24 @@ */ package org.flowable.dmn.engine.impl.persistence.deploy; +import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Map; +import org.apache.commons.lang3.StringUtils; import org.flowable.common.engine.api.FlowableException; import org.flowable.common.engine.api.FlowableObjectNotFoundException; import org.flowable.common.engine.api.delegate.event.FlowableEngineEventType; import org.flowable.common.engine.api.delegate.event.FlowableEventDispatcher; import org.flowable.common.engine.impl.event.FlowableEntityEventImpl; +import org.flowable.common.engine.impl.persistence.deploy.AbstractSameDeploymentDefinitionLookup; import org.flowable.common.engine.impl.persistence.deploy.DeploymentCache; import org.flowable.dmn.api.DmnDecision; +import org.flowable.dmn.api.DmnDeployment; import org.flowable.dmn.engine.DmnEngineConfiguration; import org.flowable.dmn.engine.impl.DecisionQueryImpl; +import org.flowable.dmn.engine.impl.DmnDeploymentQueryImpl; import org.flowable.dmn.engine.impl.persistence.entity.DecisionEntity; import org.flowable.dmn.engine.impl.persistence.entity.DecisionEntityManager; import org.flowable.dmn.engine.impl.persistence.entity.DmnDeploymentEntity; @@ -43,12 +49,43 @@ public class DeploymentManager { protected List deployers; protected DecisionEntityManager decisionEntityManager; protected DmnDeploymentEntityManager deploymentEntityManager; + protected SameDeploymentDecisionLookup sameDeploymentDecisionLookup; public DeploymentManager(DeploymentCache decisionCache, DmnEngineConfiguration engineConfig) { this.decisionCache = decisionCache; this.engineConfig = engineConfig; } + /** + * Returns the decision with the given key in the first deployment of the given parent deployment, or null when there is none. + */ + public DecisionEntity findDecisionByParentDeploymentAndKey(String parentDeploymentId, String decisionKey, String tenantId) { + String cacheKey = AbstractSameDeploymentDefinitionLookup.parentDeploymentKey(parentDeploymentId, decisionKey); + return sameDeploymentDecisionLookup.find(cacheKey, tenantId, sameDeploymentDecisionLookup.isCacheMisses(), () -> { + List dmnDeployments = deploymentEntityManager.findDeploymentsByQueryCriteria( + new DmnDeploymentQueryImpl().parentDeploymentId(parentDeploymentId)); + if (dmnDeployments == null || dmnDeployments.isEmpty()) { + return null; + } + if (StringUtils.isNotEmpty(tenantId)) { + return decisionEntityManager.findDecisionByDeploymentAndKeyAndTenantId(dmnDeployments.get(0).getId(), decisionKey, tenantId); + } + return decisionEntityManager.findDecisionByDeploymentAndKey(dmnDeployments.get(0).getId(), decisionKey); + }); + } + + /** + * Removes the cached lookups of the given decision keys for the given parent deployment. + */ + public void evictSameDeploymentDecisions(String parentDeploymentId, Collection decisionKeys) { + if (StringUtils.isEmpty(parentDeploymentId)) { + return; + } + for (String decisionKey : decisionKeys) { + sameDeploymentDecisionLookup.evict(AbstractSameDeploymentDefinitionLookup.parentDeploymentKey(parentDeploymentId, decisionKey)); + } + } + public void deploy(DmnDeploymentEntity deployment) { deploy(deployment, null); } @@ -173,13 +210,16 @@ public void removeDeployment(String deploymentId) { deploymentEntityManager.deleteDeployment(deploymentId); FlowableEventDispatcher eventDispatcher = engineConfig.getEventDispatcher(); + List decisionKeys = new ArrayList<>(); for (DmnDecision definition : definitions) { decisionCache.remove(definition.getId()); + decisionKeys.add(definition.getKey()); if (eventDispatcher != null && eventDispatcher.isEnabled()) { eventDispatcher.dispatchEvent(new FlowableEntityEventImpl(definition, FlowableEngineEventType.DEFINITION_UNDEPLOYED), engineConfig.getEngineCfgKey()); } } + evictSameDeploymentDecisions(deployment.getParentDeploymentId(), decisionKeys); } public List getDeployers() { @@ -198,6 +238,14 @@ public void setDecisionCache(DeploymentCache decisionCache) this.decisionCache = decisionCache; } + public SameDeploymentDecisionLookup getSameDeploymentDecisionLookup() { + return sameDeploymentDecisionLookup; + } + + public void setSameDeploymentDecisionLookup(SameDeploymentDecisionLookup sameDeploymentDecisionLookup) { + this.sameDeploymentDecisionLookup = sameDeploymentDecisionLookup; + } + public DecisionEntityManager getDecisionEntityManager() { return decisionEntityManager; } diff --git a/modules/flowable-dmn-engine/src/main/java/org/flowable/dmn/engine/impl/persistence/deploy/SameDeploymentDecisionLookup.java b/modules/flowable-dmn-engine/src/main/java/org/flowable/dmn/engine/impl/persistence/deploy/SameDeploymentDecisionLookup.java new file mode 100644 index 00000000000..d2cd9b90192 --- /dev/null +++ b/modules/flowable-dmn-engine/src/main/java/org/flowable/dmn/engine/impl/persistence/deploy/SameDeploymentDecisionLookup.java @@ -0,0 +1,47 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flowable.dmn.engine.impl.persistence.deploy; + +import org.flowable.common.engine.impl.persistence.deploy.AbstractSameDeploymentDefinitionLookup; +import org.flowable.common.engine.impl.persistence.deploy.DeploymentCache; +import org.flowable.dmn.engine.DmnEngineConfiguration; +import org.flowable.dmn.engine.impl.persistence.entity.DecisionEntity; + +/** + * Caches the decisions found by key in the first deployment of a parent deployment. + */ +public class SameDeploymentDecisionLookup extends AbstractSameDeploymentDefinitionLookup { + + protected DmnEngineConfiguration dmnEngineConfiguration; + + public SameDeploymentDecisionLookup(DeploymentCache cache, DmnEngineConfiguration dmnEngineConfiguration) { + super(cache, dmnEngineConfiguration); + this.dmnEngineConfiguration = dmnEngineConfiguration; + } + + @Override + protected boolean isVerifyCachedDefinitions() { + return dmnEngineConfiguration.isVerifyCachedSameDeploymentDefinitions(); + } + + @Override + protected DecisionEntity findCachedDefinition(String definitionId) { + DecisionCacheEntry cacheEntry = dmnEngineConfiguration.getDefinitionCache().get(definitionId); + return cacheEntry != null ? cacheEntry.getDecisionEntity() : null; + } + + @Override + protected DecisionEntity findPersistedDefinition(String definitionId) { + return dmnEngineConfiguration.getDecisionEntityManager().findById(definitionId); + } +} diff --git a/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/AbstractEngineConfiguration.java b/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/AbstractEngineConfiguration.java index 48a9a83ca35..dbdacf092fa 100755 --- a/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/AbstractEngineConfiguration.java +++ b/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/AbstractEngineConfiguration.java @@ -317,6 +317,17 @@ public abstract class AbstractEngineConfiguration { * Set to true if the latest version of a definition should be retrieved, ignoring a possible parent deployment id value */ protected boolean alwaysLookupLatestDefinitionVersion; + + /** + * Set to true to cache that a parent deployment has no definition with a given key, so that later lookups skip the + * parent deployment query and go straight to the latest definition fallback. Found definitions are always cached. + *

+ * In a cluster, a node that did not perform the deployment keeps such a cached miss when a new child deployment + * with that key is added under an existing parent deployment, until the entry is evicted from the cache or the node + * is restarted. Cached definitions recover by themselves, cached misses do not. Only enable this when new child + * deployments are not added under existing parent deployments. + */ + protected boolean cacheSameDeploymentDefinitionMisses; /** * Set to true if by default lookups should fallback to the default tenant (an empty string by default or a defined tenant value) @@ -1756,6 +1767,15 @@ public AbstractEngineConfiguration setAlwaysLookupLatestDefinitionVersion(boolea return this; } + public boolean isCacheSameDeploymentDefinitionMisses() { + return cacheSameDeploymentDefinitionMisses; + } + + public AbstractEngineConfiguration setCacheSameDeploymentDefinitionMisses(boolean cacheSameDeploymentDefinitionMisses) { + this.cacheSameDeploymentDefinitionMisses = cacheSameDeploymentDefinitionMisses; + return this; + } + public boolean isFallbackToDefaultTenant() { return fallbackToDefaultTenant; } diff --git a/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/persistence/deploy/AbstractSameDeploymentDefinitionLookup.java b/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/persistence/deploy/AbstractSameDeploymentDefinitionLookup.java new file mode 100644 index 00000000000..c399080fc83 --- /dev/null +++ b/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/persistence/deploy/AbstractSameDeploymentDefinitionLookup.java @@ -0,0 +1,136 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flowable.common.engine.impl.persistence.deploy; + +import java.util.Objects; +import java.util.function.Supplier; + +import org.apache.commons.lang3.StringUtils; +import org.flowable.common.engine.api.repository.FlowableDefinition; +import org.flowable.common.engine.impl.AbstractEngineConfiguration; + +/** + * Caches the id of the definition with a given key in the deployments of a parent deployment (or in a deployment), so + * that repeated lookups do not need to query the database. A miss is only cached when + * {@link AbstractEngineConfiguration#isCacheSameDeploymentDefinitionMisses()} is enabled, or when the caller always + * caches misses. A cached miss applies to the tenant it was recorded for, a lookup for another tenant is treated as + * not cached. A cached miss for a deployment that is deleted later stays in the cache until the cache limit evicts it. + * + * @param the type of the definition + */ +public abstract class AbstractSameDeploymentDefinitionLookup { + + protected static final String NO_DEFINITION_PREFIX = "__noDefinition__:"; + + protected DeploymentCache cache; + protected AbstractEngineConfiguration engineConfiguration; + + public AbstractSameDeploymentDefinitionLookup(DeploymentCache cache, AbstractEngineConfiguration engineConfiguration) { + this.cache = cache; + this.engineConfiguration = engineConfiguration; + } + + public static String parentDeploymentKey(String parentDeploymentId, String key) { + return "parent:" + parentDeploymentId + ":" + key; + } + + public static String deploymentKey(String deploymentId, String key) { + return "deployment:" + deploymentId + ":" + key; + } + + /** + * Returns the definition for the cache key, or null when there is none and the caller should use its fallback. + */ + public T find(String cacheKey, String tenantId, boolean cacheMisses, Supplier query) { + String cachedValue = cache.get(cacheKey); + String noDefinition = noDefinitionMarker(tenantId); + if (noDefinition.equals(cachedValue)) { + return null; + } + + if (cachedValue != null && !cachedValue.startsWith(NO_DEFINITION_PREFIX)) { + T definition = findDefinitionById(cachedValue); + if (definition == null) { + cache.remove(cacheKey); + } else if (isSameTenant(definition.getTenantId(), tenantId)) { + return definition; + } else { + return query.get(); + } + } + + T definition = query.get(); + if (definition != null) { + cache.add(cacheKey, definition.getId()); + } else if (cacheMisses) { + cache.add(cacheKey, noDefinition); + } + return definition; + } + + protected String noDefinitionMarker(String tenantId) { + return NO_DEFINITION_PREFIX + StringUtils.defaultString(tenantId); + } + + public boolean isCacheMisses() { + return engineConfiguration.isCacheSameDeploymentDefinitionMisses(); + } + + public void evict(String cacheKey) { + cache.remove(cacheKey); + } + + protected boolean isSameTenant(String definitionTenantId, String tenantId) { + return Objects.equals(StringUtils.defaultString(definitionTenantId), StringUtils.defaultString(tenantId)); + } + + /** + * Returns the definition with the given id, or null when it no longer exists. + */ + protected T findDefinitionById(String definitionId) { + if (!isVerifyCachedDefinitions()) { + T definition = findCachedDefinition(definitionId); + if (definition != null) { + return definition; + } + } + return findPersistedDefinition(definitionId); + } + + /** + * Returns whether a cached definition id is always verified against the database. + */ + protected boolean isVerifyCachedDefinitions() { + return true; + } + + /** + * Returns the definition from the definition cache, or null when it is not cached or the engine always verifies. + */ + protected T findCachedDefinition(String definitionId) { + return null; + } + + /** + * Returns the definition from the database, or null when it does not exist. + */ + protected abstract T findPersistedDefinition(String definitionId); + + public DeploymentCache getCache() { + return cache; + } + + public void setCache(DeploymentCache cache) { + this.cache = cache; + } +} diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/CallActivityBehavior.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/CallActivityBehavior.java index 668f018978e..39040e8e84a 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/CallActivityBehavior.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/behavior/CallActivityBehavior.java @@ -47,7 +47,6 @@ import org.flowable.engine.impl.delegate.SubProcessActivityBehavior; import org.flowable.engine.impl.persistence.entity.ExecutionEntity; import org.flowable.engine.impl.persistence.entity.ExecutionEntityManager; -import org.flowable.engine.impl.persistence.entity.ProcessDefinitionEntity; import org.flowable.engine.impl.persistence.entity.ProcessDefinitionEntityManager; import org.flowable.engine.impl.util.CommandContextUtil; import org.flowable.engine.impl.util.EntityLinkUtil; @@ -311,15 +310,12 @@ protected ProcessDefinition getProcessDefinitionByKey(DelegateExecution executio String tenantId = execution.getTenantId(); ProcessDefinitionEntityManager processDefinitionEntityManager = Context.getProcessEngineConfiguration().getProcessDefinitionEntityManager(); - ProcessDefinitionEntity processDefinition; + ProcessDefinition processDefinition; if (isSameDeployment) { String deploymentId = ProcessDefinitionUtil.getProcessDefinition(execution.getProcessDefinitionId()).getDeploymentId(); - if (tenantId == null || ProcessEngineConfiguration.NO_TENANT_ID.equals(tenantId)) { - processDefinition = processDefinitionEntityManager.findProcessDefinitionByDeploymentAndKey(deploymentId, processDefinitionKey); - } else { - processDefinition = processDefinitionEntityManager.findProcessDefinitionByDeploymentAndKeyAndTenantId(deploymentId, processDefinitionKey, tenantId); - } + processDefinition = processEngineConfiguration.getDeploymentManager() + .findProcessDefinitionByDeploymentAndKey(deploymentId, processDefinitionKey, tenantId); if (processDefinition != null) { return processDefinition; diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/deployer/BpmnDeployer.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/deployer/BpmnDeployer.java index 4218d890ed0..2b11f80f04c 100755 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/deployer/BpmnDeployer.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/bpmn/deployer/BpmnDeployer.java @@ -12,6 +12,7 @@ */ package org.flowable.engine.impl.bpmn.deployer; +import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; @@ -99,7 +100,14 @@ public void deploy(EngineDeployment deployment, Map deploymentSe setDerivedProcessDefinitionVersionsAndIds(parsedDeployment, mapOfNewProcessDefinitionToPreviousDerivedVersion, deploymentSettings); persistProcessDefinitionsAndAuthorizations(parsedDeployment); } - + + List processDefinitionKeys = new ArrayList<>(); + for (ProcessDefinitionEntity processDefinition : parsedDeployment.getAllProcessDefinitions()) { + processDefinitionKeys.add(processDefinition.getKey()); + } + CommandContextUtil.getProcessEngineConfiguration().getDeploymentManager() + .evictSameDeploymentProcessDefinitions(parsedDeployment.getDeployment().getParentDeploymentId(), processDefinitionKeys); + } else { makeProcessDefinitionsConsistentWithPersistedVersions(parsedDeployment); } diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/cfg/ProcessEngineConfigurationImpl.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/cfg/ProcessEngineConfigurationImpl.java index d50d8a7f8b8..edb50f1ba03 100755 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/cfg/ProcessEngineConfigurationImpl.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/cfg/ProcessEngineConfigurationImpl.java @@ -293,6 +293,7 @@ import org.flowable.engine.impl.persistence.deploy.ProcessDefinitionCacheEntry; import org.flowable.engine.impl.persistence.deploy.ProcessDefinitionInfoCache; import org.flowable.engine.impl.persistence.deploy.ProcessDefinitionInfoCacheObject; +import org.flowable.engine.impl.persistence.deploy.SameDeploymentProcessDefinitionLookup; import org.flowable.engine.impl.persistence.entity.ActivityInstanceEntityManager; import org.flowable.engine.impl.persistence.entity.ActivityInstanceEntityManagerImpl; import org.flowable.engine.impl.persistence.entity.AttachmentEntityManager; @@ -572,6 +573,7 @@ public abstract class ProcessEngineConfigurationImpl extends ProcessEngineConfig protected int processDefinitionCacheLimit = -1; // By default, no limit protected DeploymentCache processDefinitionCache; + protected DeploymentCache sameDeploymentDefinitionCache; protected int processDefinitionInfoCacheLimit = -1; // By default, no limit protected DeploymentCache processDefinitionInfoCache; @@ -1670,6 +1672,13 @@ public void initProcessDefinitionCache() { processDefinitionCache = new DefaultDeploymentCache<>(processDefinitionCacheLimit); } } + if (sameDeploymentDefinitionCache == null) { + if (processDefinitionCacheLimit <= 0) { + sameDeploymentDefinitionCache = new DefaultDeploymentCache<>(); + } else { + sameDeploymentDefinitionCache = new DefaultDeploymentCache<>(processDefinitionCacheLimit); + } + } } public void initProcessDefinitionInfoCache() { @@ -1727,6 +1736,10 @@ public void initDeployers() { deploymentManager.setDeploymentEntityManager(deploymentEntityManager); } + if (deploymentManager.getSameDeploymentProcessDefinitionLookup() == null) { + deploymentManager.setSameDeploymentProcessDefinitionLookup(new SameDeploymentProcessDefinitionLookup(sameDeploymentDefinitionCache, this)); + } + if (appResourceConverter == null) { appResourceConverter = new AppResourceConverterImpl(objectMapper); } @@ -3896,6 +3909,15 @@ public ProcessEngineConfigurationImpl setProcessDefinitionCacheLimit(int process return this; } + public DeploymentCache getSameDeploymentDefinitionCache() { + return sameDeploymentDefinitionCache; + } + + public ProcessEngineConfigurationImpl setSameDeploymentDefinitionCache(DeploymentCache sameDeploymentDefinitionCache) { + this.sameDeploymentDefinitionCache = sameDeploymentDefinitionCache; + return this; + } + public DeploymentCache getProcessDefinitionCache() { return processDefinitionCache; } diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/persistence/deploy/DeploymentManager.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/persistence/deploy/DeploymentManager.java index f6623fba7a2..42cfb508f51 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/persistence/deploy/DeploymentManager.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/persistence/deploy/DeploymentManager.java @@ -13,9 +13,12 @@ package org.flowable.engine.impl.persistence.deploy; +import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Map; +import org.apache.commons.lang3.StringUtils; import org.flowable.bpmn.model.BpmnModel; import org.flowable.common.engine.api.FlowableException; import org.flowable.common.engine.api.FlowableIllegalArgumentException; @@ -23,7 +26,9 @@ import org.flowable.common.engine.api.delegate.event.FlowableEngineEventType; import org.flowable.common.engine.api.delegate.event.FlowableEventDispatcher; import org.flowable.common.engine.impl.EngineDeployer; +import org.flowable.common.engine.impl.persistence.deploy.AbstractSameDeploymentDefinitionLookup; import org.flowable.common.engine.impl.persistence.deploy.DeploymentCache; +import org.flowable.engine.ProcessEngineConfiguration; import org.flowable.engine.app.AppModel; import org.flowable.engine.delegate.event.impl.FlowableEventBuilder; import org.flowable.engine.impl.ProcessDefinitionQueryImpl; @@ -47,6 +52,7 @@ public class DeploymentManager { protected DeploymentCache processDefinitionInfoCache; protected DeploymentCache appResourceCache; protected DeploymentCache knowledgeBaseCache; // Needs to be object to avoid an import to Drools in this core class + protected SameDeploymentProcessDefinitionLookup sameDeploymentProcessDefinitionLookup; protected List deployers; protected ProcessEngineConfigurationImpl processEngineConfiguration; @@ -177,6 +183,46 @@ public AppModel getAppResourceModel(String deploymentId) { return (AppModel) appResourceObject; } + /** + * Returns the process definition with the given key in the deployments of the given parent deployment, or null when + * there is none. + */ + public ProcessDefinition findProcessDefinitionByParentDeploymentAndKey(String parentDeploymentId, String processDefinitionKey, String tenantId) { + String cacheKey = AbstractSameDeploymentDefinitionLookup.parentDeploymentKey(parentDeploymentId, processDefinitionKey); + return sameDeploymentProcessDefinitionLookup.find(cacheKey, tenantId, sameDeploymentProcessDefinitionLookup.isCacheMisses(), () -> { + if (tenantId != null && !ProcessEngineConfiguration.NO_TENANT_ID.equals(tenantId)) { + return processDefinitionEntityManager.findProcessDefinitionByParentDeploymentAndKeyAndTenantId(parentDeploymentId, processDefinitionKey, tenantId); + } + return processDefinitionEntityManager.findProcessDefinitionByParentDeploymentAndKey(parentDeploymentId, processDefinitionKey); + }); + } + + /** + * Returns the process definition with the given key in the given deployment, or null when there is none. A + * deployment never changes, so a miss is always cached. + */ + public ProcessDefinition findProcessDefinitionByDeploymentAndKey(String deploymentId, String processDefinitionKey, String tenantId) { + String cacheKey = AbstractSameDeploymentDefinitionLookup.deploymentKey(deploymentId, processDefinitionKey); + return sameDeploymentProcessDefinitionLookup.find(cacheKey, tenantId, true, () -> { + if (tenantId == null || ProcessEngineConfiguration.NO_TENANT_ID.equals(tenantId)) { + return processDefinitionEntityManager.findProcessDefinitionByDeploymentAndKey(deploymentId, processDefinitionKey); + } + return processDefinitionEntityManager.findProcessDefinitionByDeploymentAndKeyAndTenantId(deploymentId, processDefinitionKey, tenantId); + }); + } + + /** + * Removes the cached lookups of the given process definition keys in the given parent deployment. + */ + public void evictSameDeploymentProcessDefinitions(String parentDeploymentId, Collection processDefinitionKeys) { + if (StringUtils.isEmpty(parentDeploymentId)) { + return; + } + for (String processDefinitionKey : processDefinitionKeys) { + sameDeploymentProcessDefinitionLookup.evict(AbstractSameDeploymentDefinitionLookup.parentDeploymentKey(parentDeploymentId, processDefinitionKey)); + } + } + public void removeDeployment(String deploymentId, boolean cascade) { DeploymentEntity deployment = deploymentEntityManager.findById(deploymentId); @@ -218,9 +264,15 @@ public void removeDeployment(String deploymentId, boolean cascade) { processEngineConfiguration.getEngineCfgKey()); } + List processDefinitionKeys = new ArrayList<>(processDefinitions.size()); for (ProcessDefinition processDefinition : processDefinitions) { processDefinitionCache.remove(processDefinition.getId()); processDefinitionInfoCache.remove(processDefinition.getId()); + processDefinitionKeys.add(processDefinition.getKey()); + } + evictSameDeploymentProcessDefinitions(deployment.getParentDeploymentId(), processDefinitionKeys); + for (String processDefinitionKey : processDefinitionKeys) { + sameDeploymentProcessDefinitionLookup.evict(AbstractSameDeploymentDefinitionLookup.deploymentKey(deploymentId, processDefinitionKey)); } appResourceCache.remove(deploymentId); @@ -246,6 +298,14 @@ public void setProcessDefinitionCache(DeploymentCache getProcessDefinitionInfoCache() { return processDefinitionInfoCache; } diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/persistence/deploy/SameDeploymentProcessDefinitionLookup.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/persistence/deploy/SameDeploymentProcessDefinitionLookup.java new file mode 100644 index 00000000000..af39357dc26 --- /dev/null +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/persistence/deploy/SameDeploymentProcessDefinitionLookup.java @@ -0,0 +1,36 @@ +/* Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.flowable.engine.impl.persistence.deploy; + +import org.flowable.common.engine.impl.persistence.deploy.AbstractSameDeploymentDefinitionLookup; +import org.flowable.common.engine.impl.persistence.deploy.DeploymentCache; +import org.flowable.engine.impl.cfg.ProcessEngineConfigurationImpl; +import org.flowable.engine.repository.ProcessDefinition; + +/** + * Caches the id of the process definition with a given key in the deployments of a parent deployment (or in a deployment). + */ +public class SameDeploymentProcessDefinitionLookup extends AbstractSameDeploymentDefinitionLookup { + + protected ProcessEngineConfigurationImpl processEngineConfiguration; + + public SameDeploymentProcessDefinitionLookup(DeploymentCache cache, ProcessEngineConfigurationImpl processEngineConfiguration) { + super(cache, processEngineConfiguration); + this.processEngineConfiguration = processEngineConfiguration; + } + + @Override + protected ProcessDefinition findPersistedDefinition(String definitionId) { + return processEngineConfiguration.getProcessDefinitionEntityManager().findById(definitionId); + } +} diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/util/ProcessInstanceHelper.java b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/util/ProcessInstanceHelper.java index 5320b069d82..48e416bc2ef 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/impl/util/ProcessInstanceHelper.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/impl/util/ProcessInstanceHelper.java @@ -389,8 +389,8 @@ public ProcessDefinition resolveProcessDefinition(String processDefinitionKey, S if (tenantId != null && !ProcessEngineConfiguration.NO_TENANT_ID.equals(tenantId)) { if (parentDeploymentId != null) { - processDefinition = processDefinitionEntityManager - .findProcessDefinitionByParentDeploymentAndKeyAndTenantId(parentDeploymentId, processDefinitionKey, tenantId); + processDefinition = processEngineConfiguration.getDeploymentManager() + .findProcessDefinitionByParentDeploymentAndKey(parentDeploymentId, processDefinitionKey, tenantId); } if (processDefinition == null) { processDefinition = processDefinitionEntityManager @@ -409,8 +409,8 @@ public ProcessDefinition resolveProcessDefinition(String processDefinitionKey, S } } else { if (parentDeploymentId != null) { - processDefinition = processDefinitionEntityManager - .findProcessDefinitionByParentDeploymentAndKey(parentDeploymentId, processDefinitionKey); + processDefinition = processEngineConfiguration.getDeploymentManager() + .findProcessDefinitionByParentDeploymentAndKey(parentDeploymentId, processDefinitionKey, tenantId); } if (processDefinition == null) { processDefinition = processDefinitionEntityManager diff --git a/modules/flowable-engine/src/main/java/org/flowable/engine/test/profiler/ProfilingDbSqlSessionFactory.java b/modules/flowable-engine/src/main/java/org/flowable/engine/test/profiler/ProfilingDbSqlSessionFactory.java index ec46ef071e8..3de1d6f981b 100644 --- a/modules/flowable-engine/src/main/java/org/flowable/engine/test/profiler/ProfilingDbSqlSessionFactory.java +++ b/modules/flowable-engine/src/main/java/org/flowable/engine/test/profiler/ProfilingDbSqlSessionFactory.java @@ -12,9 +12,11 @@ */ package org.flowable.engine.test.profiler; +import org.flowable.common.engine.impl.db.DbSqlSession; import org.flowable.common.engine.impl.db.DbSqlSessionFactory; import org.flowable.common.engine.impl.interceptor.CommandContext; import org.flowable.common.engine.impl.interceptor.Session; +import org.flowable.engine.impl.cmd.GetNextIdBlockCmd; import org.flowable.engine.impl.util.CommandContextUtil; /** @@ -28,6 +30,11 @@ public ProfilingDbSqlSessionFactory(boolean usePrefixId) { @Override public Session openSession(CommandContext commandContext) { + if (commandContext.getCommand() instanceof GetNextIdBlockCmd) { + // The id block can be fetched through a command executor that is not profiled, + // its operations should not be attributed to the profiled command that needed the id + return new DbSqlSession(this, CommandContextUtil.getEntityCache(commandContext)); + } return new ProfilingDbSqlSession(this, CommandContextUtil.getEntityCache(commandContext)); } } diff --git a/modules/flowable-engine/src/test/java/org/flowable/engine/test/cfg/executioncount/VerifyDatabaseOperationsTest.java b/modules/flowable-engine/src/test/java/org/flowable/engine/test/cfg/executioncount/VerifyDatabaseOperationsTest.java index e2c01ff9ae6..9fc8221efc0 100644 --- a/modules/flowable-engine/src/test/java/org/flowable/engine/test/cfg/executioncount/VerifyDatabaseOperationsTest.java +++ b/modules/flowable-engine/src/test/java/org/flowable/engine/test/cfg/executioncount/VerifyDatabaseOperationsTest.java @@ -13,6 +13,7 @@ package org.flowable.engine.test.cfg.executioncount; import static org.assertj.core.api.Assertions.assertThat; +import static org.flowable.common.engine.impl.persistence.deploy.AbstractSameDeploymentDefinitionLookup.parentDeploymentKey; import java.util.HashMap; import java.util.HashSet; @@ -25,9 +26,13 @@ import org.flowable.common.engine.impl.interceptor.CommandInterceptor; import org.flowable.common.engine.impl.interceptor.EngineConfigurationConstants; import org.flowable.engine.history.HistoricActivityInstance; +import org.flowable.engine.history.HistoricProcessInstance; import org.flowable.engine.impl.db.EntityDependencyOrder; import org.flowable.engine.impl.test.PluggableFlowableTestCase; import org.flowable.engine.repository.Deployment; +import org.flowable.engine.repository.DeploymentBuilder; +import org.flowable.engine.runtime.ProcessInstance; +import org.flowable.engine.runtime.ProcessInstanceBuilder; import org.flowable.engine.test.profiler.CommandStats; import org.flowable.engine.test.profiler.ConsoleLogger; import org.flowable.engine.test.profiler.FlowableProfiler; @@ -765,6 +770,430 @@ private Map createVariables(long count, String prefix) { return vars; } + @Test + public void testStartWithParentDeploymentIdTwice() { + if (!processEngineConfiguration.isAsyncHistoryEnabled()) { + Deployment deployment = deployResource("process01.bpmn20.xml"); + assertThat(deployment.getParentDeploymentId()).isEqualTo(deployment.getId()); + + // First start + ProcessInstance first = startWithParentAndProfile("process01", deployment.getId(), null, false); + assertDatabaseSelects("StartProcessInstanceCmd", + "selectProcessDefinitionByParentDeploymentAndKey", 1L, + "selectVariablesByQuery", 1L, + "selectEntityLinksByQuery", 1L); + assertThat(deploymentIdOf(first)).isEqualTo(deployment.getId()); + + // Second start + ProcessInstance second = startWithParentAndProfile("process01", deployment.getId(), null, false); + assertDatabaseSelects("StartProcessInstanceCmd", + "selectById org.flowable.engine.impl.persistence.entity.ProcessDefinitionEntityImpl", 1L, + "selectVariablesByQuery", 1L, + "selectEntityLinksByQuery", 1L); + assertThat(deploymentIdOf(second)).isEqualTo(deployment.getId()); + } + } + + @Test + public void testCallActivityWithSameDeploymentTwice() { + if (!processEngineConfiguration.isAsyncHistoryEnabled()) { + processEngineConfiguration.getPerformanceSettings().setEnableEagerExecutionTreeFetching(false); + + deployMultiple("process10.bpmn20.xml", "process01.bpmn20.xml"); + + // First start + startProcessInstanceAndProfile("process10"); + stopProfiling(); + assertDatabaseSelects("StartProcessInstanceCmd", + "selectProcessDefinitionByDeploymentAndKey", 1L, + "selectLatestProcessDefinitionByKey", 1L, + "selectVariablesByQuery", 2L, + "selectEntityLinksByQuery", 2L, + "selectExecutionsByParentExecutionId", 4L, + "selectChildExecutionsByProcessInstanceId", 2L); + + // Second start + startProcessInstanceAndProfile("process10"); + stopProfiling(); + assertDatabaseSelects("StartProcessInstanceCmd", + "selectById org.flowable.engine.impl.persistence.entity.ProcessDefinitionEntityImpl", 1L, + "selectLatestProcessDefinitionByKey", 1L, + "selectVariablesByQuery", 2L, + "selectEntityLinksByQuery", 2L, + "selectExecutionsByParentExecutionId", 4L, + "selectChildExecutionsByProcessInstanceId", 2L); + } + } + + @Test + public void testStartWithParentDeploymentIdMissTwice() { + assertStartWithParentDeploymentIdMissTwice(false); + } + + @Test + public void testStartWithParentDeploymentIdMissTwiceWithCachedMisses() { + assertStartWithParentDeploymentIdMissTwice(true); + } + + protected void assertStartWithParentDeploymentIdMissTwice(boolean cacheMisses) { + if (!processEngineConfiguration.isAsyncHistoryEnabled()) { + setCacheSameDeploymentDefinitionMisses(cacheMisses); + try { + Deployment parent = deployResource("process02.bpmn20.xml"); + Deployment latest = deployResource("process01.bpmn20.xml"); + + // First start, the parent has no process01 so it falls back to the latest + ProcessInstance first = startWithParentAndProfile("process01", parent.getId(), null, false); + assertDatabaseSelects("StartProcessInstanceCmd", + "selectProcessDefinitionByParentDeploymentAndKey", 1L, + "selectLatestProcessDefinitionByKey", 1L, + "selectVariablesByQuery", 1L, + "selectEntityLinksByQuery", 1L); + assertThat(deploymentIdOf(first)).isEqualTo(latest.getId()); + + // Second start + ProcessInstance second = startWithParentAndProfile("process01", parent.getId(), null, false); + if (cacheMisses) { + assertDatabaseSelects("StartProcessInstanceCmd", + "selectLatestProcessDefinitionByKey", 1L, + "selectVariablesByQuery", 1L, + "selectEntityLinksByQuery", 1L); + } else { + assertDatabaseSelects("StartProcessInstanceCmd", + "selectProcessDefinitionByParentDeploymentAndKey", 1L, + "selectLatestProcessDefinitionByKey", 1L, + "selectVariablesByQuery", 1L, + "selectEntityLinksByQuery", 1L); + } + assertThat(deploymentIdOf(second)).isEqualTo(latest.getId()); + } finally { + setCacheSameDeploymentDefinitionMisses(false); + } + } + } + + @Test + public void testStartWithParentDeploymentIdMissThenChildDeployed() { + if (!processEngineConfiguration.isAsyncHistoryEnabled()) { + setCacheSameDeploymentDefinitionMisses(true); + try { + Deployment parent = deployResource("process02.bpmn20.xml"); + Deployment latest = deployResource("process01.bpmn20.xml"); + + // First start, the miss is (expected to be) cached + ProcessInstance first = startWithParentAndProfile("process01", parent.getId(), null, false); + assertDatabaseSelects("StartProcessInstanceCmd", + "selectProcessDefinitionByParentDeploymentAndKey", 1L, + "selectLatestProcessDefinitionByKey", 1L, + "selectVariablesByQuery", 1L, + "selectEntityLinksByQuery", 1L); + assertThat(deploymentIdOf(first)).isEqualTo(latest.getId()); + + Deployment child = repositoryService.createDeployment() + .addClasspathResource("org/flowable/engine/test/cfg/executioncount/process01.bpmn20.xml") + .parentDeploymentId(parent.getId()) + .deploy(); + + // Second start, after the child was deployed + ProcessInstance second = startWithParentAndProfile("process01", parent.getId(), null, false); + assertDatabaseSelects("StartProcessInstanceCmd", + "selectProcessDefinitionByParentDeploymentAndKey", 1L, + "selectVariablesByQuery", 1L, + "selectEntityLinksByQuery", 1L); + assertThat(deploymentIdOf(second)).isEqualTo(child.getId()); + } finally { + setCacheSameDeploymentDefinitionMisses(false); + } + } + } + + @Test + public void testStartWithParentDeploymentIdMissPicksUpNewVersion() { + assertStartWithParentDeploymentIdMissPicksUpNewVersion(false); + } + + @Test + public void testStartWithParentDeploymentIdMissPicksUpNewVersionWithCachedMisses() { + assertStartWithParentDeploymentIdMissPicksUpNewVersion(true); + } + + protected void assertStartWithParentDeploymentIdMissPicksUpNewVersion(boolean cacheMisses) { + if (!processEngineConfiguration.isAsyncHistoryEnabled()) { + setCacheSameDeploymentDefinitionMisses(cacheMisses); + try { + Deployment parent = deployResource("process02.bpmn20.xml"); + Deployment latest1 = deployResource("process01.bpmn20.xml"); + + ProcessInstance first = startWithParentAndProfile("process01", parent.getId(), null, false); + assertDatabaseSelects("StartProcessInstanceCmd", + "selectProcessDefinitionByParentDeploymentAndKey", 1L, + "selectLatestProcessDefinitionByKey", 1L, + "selectVariablesByQuery", 1L, + "selectEntityLinksByQuery", 1L); + assertThat(deploymentIdOf(first)).isEqualTo(latest1.getId()); + + Deployment latest2 = deployResource("process01.bpmn20.xml"); + + // Second start, after a new version was deployed + ProcessInstance second = startWithParentAndProfile("process01", parent.getId(), null, false); + if (cacheMisses) { + assertDatabaseSelects("StartProcessInstanceCmd", + "selectLatestProcessDefinitionByKey", 1L, + "selectVariablesByQuery", 1L, + "selectEntityLinksByQuery", 1L); + } else { + assertDatabaseSelects("StartProcessInstanceCmd", + "selectProcessDefinitionByParentDeploymentAndKey", 1L, + "selectLatestProcessDefinitionByKey", 1L, + "selectVariablesByQuery", 1L, + "selectEntityLinksByQuery", 1L); + } + assertThat(deploymentIdOf(second)).isEqualTo(latest2.getId()); + } finally { + setCacheSameDeploymentDefinitionMisses(false); + } + } + } + + @Test + public void testStartWithParentDeploymentIdAfterChildUndeployed() { + if (!processEngineConfiguration.isAsyncHistoryEnabled()) { + Deployment parent = deployResource("process02.bpmn20.xml"); + Deployment child = repositoryService.createDeployment() + .addClasspathResource("org/flowable/engine/test/cfg/executioncount/process01.bpmn20.xml") + .parentDeploymentId(parent.getId()) + .deploy(); + Deployment latest = deployResource("process01.bpmn20.xml"); + + ProcessInstance first = startWithParentAndProfile("process01", parent.getId(), null, false); + assertDatabaseSelects("StartProcessInstanceCmd", + "selectProcessDefinitionByParentDeploymentAndKey", 1L, + "selectVariablesByQuery", 1L, + "selectEntityLinksByQuery", 1L); + assertThat(deploymentIdOf(first)).isEqualTo(child.getId()); + + repositoryService.deleteDeployment(child.getId(), true); + + // Second start, after the child was undeployed + ProcessInstance second = startWithParentAndProfile("process01", parent.getId(), null, false); + assertDatabaseSelects("StartProcessInstanceCmd", + "selectProcessDefinitionByParentDeploymentAndKey", 1L, + "selectLatestProcessDefinitionByKey", 1L, + "selectVariablesByQuery", 1L, + "selectEntityLinksByQuery", 1L); + assertThat(deploymentIdOf(second)).isEqualTo(latest.getId()); + } + } + + @Test + public void testStartWithParentDeploymentIdWithStaleCachedDefinition() { + if (!processEngineConfiguration.isAsyncHistoryEnabled()) { + Deployment parent = deployResource("process02.bpmn20.xml"); + Deployment child1 = repositoryService.createDeployment() + .addClasspathResource("org/flowable/engine/test/cfg/executioncount/process01.bpmn20.xml") + .parentDeploymentId(parent.getId()) + .deploy(); + + ProcessInstance first = startWithParentAndProfile("process01", parent.getId(), null, false); + assertThat(deploymentIdOf(first)).isEqualTo(child1.getId()); + String cacheKey = parentDeploymentKey(parent.getId(), "process01"); + String staleProcessDefinitionId = first.getProcessDefinitionId(); + assertThat(processEngineConfiguration.getSameDeploymentDefinitionCache().get(cacheKey)).isEqualTo(staleProcessDefinitionId); + + repositoryService.deleteDeployment(child1.getId(), true); + Deployment child2 = repositoryService.createDeployment() + .addClasspathResource("org/flowable/engine/test/cfg/executioncount/process01.bpmn20.xml") + .parentDeploymentId(parent.getId()) + .deploy(); + + // Simulate a node that did not see the undeploy and the deploy + processEngineConfiguration.getSameDeploymentDefinitionCache().add(cacheKey, staleProcessDefinitionId); + + ProcessInstance second = startWithParentAndProfile("process01", parent.getId(), null, false); + assertDatabaseSelects("StartProcessInstanceCmd", + "selectById org.flowable.engine.impl.persistence.entity.ProcessDefinitionEntityImpl", 1L, + "selectProcessDefinitionByParentDeploymentAndKey", 1L, + "selectVariablesByQuery", 1L, + "selectEntityLinksByQuery", 1L); + assertThat(deploymentIdOf(second)).isEqualTo(child2.getId()); + assertThat(processEngineConfiguration.getSameDeploymentDefinitionCache().get(cacheKey)).isEqualTo(second.getProcessDefinitionId()); + } + } + + @Test + public void testStartWithParentDeploymentIdOtherTenant() { + if (!processEngineConfiguration.isAsyncHistoryEnabled()) { + Deployment parent = deployResourceForTenant("process01.bpmn20.xml", "tenantA"); + Deployment other = deployResourceForTenant("process01.bpmn20.xml", "tenantB"); + + // Start in the tenant of the parent + ProcessInstance first = startWithParentAndProfile("process01", parent.getId(), "tenantA", false); + assertDatabaseSelects("StartProcessInstanceCmd", + "selectProcessDefinitionByParentDeploymentAndKeyAndTenantId", 1L, + "selectVariablesByQuery", 1L, + "selectEntityLinksByQuery", 1L); + assertThat(deploymentIdOf(first)).isEqualTo(parent.getId()); + + // First start in another tenant + ProcessInstance second = startWithParentAndProfile("process01", parent.getId(), "tenantB", false); + assertDatabaseSelects("StartProcessInstanceCmd", + "selectById org.flowable.engine.impl.persistence.entity.ProcessDefinitionEntityImpl", 1L, + "selectProcessDefinitionByParentDeploymentAndKeyAndTenantId", 1L, + "selectLatestProcessDefinitionByKeyAndTenantId", 1L, + "selectVariablesByQuery", 1L, + "selectEntityLinksByQuery", 1L); + assertThat(deploymentIdOf(second)).isEqualTo(other.getId()); + + // Second start in another tenant + ProcessInstance third = startWithParentAndProfile("process01", parent.getId(), "tenantB", false); + assertDatabaseSelects("StartProcessInstanceCmd", + "selectById org.flowable.engine.impl.persistence.entity.ProcessDefinitionEntityImpl", 1L, + "selectProcessDefinitionByParentDeploymentAndKeyAndTenantId", 1L, + "selectLatestProcessDefinitionByKeyAndTenantId", 1L, + "selectVariablesByQuery", 1L, + "selectEntityLinksByQuery", 1L); + assertThat(deploymentIdOf(third)).isEqualTo(other.getId()); + } + } + + @Test + public void testStartWithParentDeploymentIdDefaultTenantFallback() { + if (!processEngineConfiguration.isAsyncHistoryEnabled()) { + Deployment parent = deployResourceForTenant("process02.bpmn20.xml", "tenantA"); + Deployment noTenant = deployResource("process01.bpmn20.xml"); + + // First start + ProcessInstance first = startWithParentAndProfile("process01", parent.getId(), "tenantA", true); + assertDatabaseSelects("StartProcessInstanceCmd", + "selectProcessDefinitionByParentDeploymentAndKeyAndTenantId", 1L, + "selectLatestProcessDefinitionByKey", 1L, + "selectLatestProcessDefinitionByKeyAndTenantId", 1L, + "selectVariablesByQuery", 1L, + "selectEntityLinksByQuery", 1L); + assertThat(deploymentIdOf(first)).isEqualTo(noTenant.getId()); + + // Second start + ProcessInstance second = startWithParentAndProfile("process01", parent.getId(), "tenantA", true); + assertDatabaseSelects("StartProcessInstanceCmd", + "selectProcessDefinitionByParentDeploymentAndKeyAndTenantId", 1L, + "selectLatestProcessDefinitionByKey", 1L, + "selectLatestProcessDefinitionByKeyAndTenantId", 1L, + "selectVariablesByQuery", 1L, + "selectEntityLinksByQuery", 1L); + assertThat(deploymentIdOf(second)).isEqualTo(noTenant.getId()); + } + } + + @Test + public void testCallActivityWithSameDeploymentMissTwice() { + if (!processEngineConfiguration.isAsyncHistoryEnabled()) { + processEngineConfiguration.getPerformanceSettings().setEnableEagerExecutionTreeFetching(false); + + deployResource("process10.bpmn20.xml"); + deployResource("process01.bpmn20.xml"); + + // First start, the called process is not in the deployment of the caller + startProcessInstanceAndProfile("process10"); + stopProfiling(); + assertDatabaseSelects("StartProcessInstanceCmd", + "selectProcessDefinitionByDeploymentAndKey", 1L, + "selectLatestProcessDefinitionByKey", 2L, + "selectVariablesByQuery", 2L, + "selectEntityLinksByQuery", 2L, + "selectExecutionsByParentExecutionId", 4L, + "selectChildExecutionsByProcessInstanceId", 2L); + + // Second start + startProcessInstanceAndProfile("process10"); + stopProfiling(); + assertDatabaseSelects("StartProcessInstanceCmd", + "selectLatestProcessDefinitionByKey", 2L, + "selectVariablesByQuery", 2L, + "selectEntityLinksByQuery", 2L, + "selectExecutionsByParentExecutionId", 4L, + "selectChildExecutionsByProcessInstanceId", 2L); + } + } + + @Test + public void testStartWithParentDeploymentIdMissForOtherTenantDoesNotHideDefinition() { + if (!processEngineConfiguration.isAsyncHistoryEnabled()) { + setCacheSameDeploymentDefinitionMisses(true); + try { + Deployment parent = deployResource("process01.bpmn20.xml"); + + // Start for tenant X, the parent has no definition for that tenant and the fallback finds the one without tenant + ProcessInstance first = startWithParentAndProfile("process01", parent.getId(), "X", true); + assertDatabaseSelects("StartProcessInstanceCmd", + "selectProcessDefinitionByParentDeploymentAndKeyAndTenantId", 1L, + "selectLatestProcessDefinitionByKeyAndTenantId", 1L, + "selectLatestProcessDefinitionByKey", 1L, + "selectVariablesByQuery", 1L, + "selectEntityLinksByQuery", 1L); + assertThat(deploymentIdOf(first)).isEqualTo(parent.getId()); + + // Start without tenant, the miss for tenant X must not hide the definition + ProcessInstance second = startWithParentAndProfile("process01", parent.getId(), null, false); + assertDatabaseSelects("StartProcessInstanceCmd", + "selectProcessDefinitionByParentDeploymentAndKey", 1L, + "selectVariablesByQuery", 1L, + "selectEntityLinksByQuery", 1L); + assertThat(deploymentIdOf(second)).isEqualTo(parent.getId()); + } finally { + setCacheSameDeploymentDefinitionMisses(false); + } + } + } + + @Test + public void testCallActivityWithSameDeploymentMissForOtherTenantDoesNotHideDefinition() { + if (!processEngineConfiguration.isAsyncHistoryEnabled()) { + boolean oldFallbackToDefaultTenant = processEngineConfiguration.isFallbackToDefaultTenant(); + processEngineConfiguration.setFallbackToDefaultTenant(true); + try { + processEngineConfiguration.getPerformanceSettings().setEnableEagerExecutionTreeFetching(false); + + Deployment deployment = deployMultiple("process10.bpmn20.xml", "process01.bpmn20.xml"); + Deployment latest = deployResource("process01.bpmn20.xml"); + + // Start for tenant X, the call activity looks up in the deployment for tenant X and misses + restartProfiling("Profiling session"); + ProcessInstance first = runtimeService.createProcessInstanceBuilder() + .processDefinitionKey("process10") + .tenantId("X") + .fallbackToDefaultTenant() + .start(); + stopProfiling(); + assertDatabaseSelects("StartProcessInstanceCmd", + "selectProcessDefinitionByDeploymentAndKeyAndTenantId", 1L, + "selectLatestProcessDefinitionByKeyAndTenantId", 2L, + "selectLatestProcessDefinitionByKey", 2L, + "selectVariablesByQuery", 2L, + "selectEntityLinksByQuery", 2L, + "selectExecutionsByParentExecutionId", 4L, + "selectChildExecutionsByProcessInstanceId", 2L); + assertThat(calledDeploymentIdOf(first)).isEqualTo(latest.getId()); + + // Start without tenant, the miss for tenant X must not hide the definition in the deployment + restartProfiling("Profiling session"); + ProcessInstance second = runtimeService.createProcessInstanceBuilder() + .processDefinitionKey("process10") + .start(); + stopProfiling(); + assertDatabaseSelects("StartProcessInstanceCmd", + "selectProcessDefinitionByDeploymentAndKey", 1L, + "selectLatestProcessDefinitionByKey", 1L, + "selectVariablesByQuery", 2L, + "selectEntityLinksByQuery", 2L, + "selectExecutionsByParentExecutionId", 4L, + "selectChildExecutionsByProcessInstanceId", 2L); + assertThat(calledDeploymentIdOf(second)).isEqualTo(deployment.getId()); + } finally { + processEngineConfiguration.setFallbackToDefaultTenant(oldFallbackToDefaultTenant); + } + } + } + // --------------------------------- // HELPERS // --------------------------------- @@ -891,6 +1320,56 @@ protected CommandStats getStatsForCommand(String commandClass, Map + + + + + + + + + + + \ No newline at end of file