From 34911b2778cd2d0e22618a765ef304320b17466c Mon Sep 17 00:00:00 2001 From: Filip Hrisafov Date: Fri, 16 Oct 2026 14:52:07 +0200 Subject: [PATCH 1/4] Cache the process definitions found by parent deployment or deployment and key Starting a process instance with a parent deployment id, the CMMN process task with same deployment and the call activity with same deployment now cache the id of the found process definition. A cached id is checked by selecting the definition by id, so the parent deployment query does not run again. Misses of the parent deployment lookup are only cached when cacheSameDeploymentDefinitionMisses is enabled; misses of the call activity lookup are always cached. Deploying and removing a deployment evicts the entries of its definition keys. --- .../test/VerifyDatabaseOperationsTest.java | 15 + .../impl/AbstractEngineConfiguration.java | 20 + ...bstractSameDeploymentDefinitionLookup.java | 136 +++++ .../bpmn/behavior/CallActivityBehavior.java | 10 +- .../impl/bpmn/deployer/BpmnDeployer.java | 10 +- .../cfg/ProcessEngineConfigurationImpl.java | 22 + .../persistence/deploy/DeploymentManager.java | 60 +++ ...SameDeploymentProcessDefinitionLookup.java | 36 ++ .../impl/util/ProcessInstanceHelper.java | 8 +- .../VerifyDatabaseOperationsTest.java | 479 ++++++++++++++++++ .../cfg/executioncount/process10.bpmn20.xml | 13 + 11 files changed, 797 insertions(+), 12 deletions(-) create mode 100644 modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/persistence/deploy/AbstractSameDeploymentDefinitionLookup.java create mode 100644 modules/flowable-engine/src/main/java/org/flowable/engine/impl/persistence/deploy/SameDeploymentProcessDefinitionLookup.java create mode 100644 modules/flowable-engine/src/test/resources/org/flowable/engine/test/cfg/executioncount/process10.bpmn20.xml 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..a87f9b2b12c 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 @@ -86,6 +86,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 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/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 From f63dafbb77bcc540ece74210b819ad12ccf0e7b9 Mon Sep 17 00:00:00 2001 From: Filip Hrisafov Date: Fri, 16 Oct 2026 14:52:07 +0200 Subject: [PATCH 2/4] Cache the case definitions found by parent deployment and key Starting a case instance with a parent deployment id, the CMMN case task with same deployment and the BPMN case task now cache the id of the found case definition. By default a cached id is checked by selecting the definition by id; with verifyCachedSameDeploymentDefinitions disabled the definition is taken from the case definition cache. Deploying and removing a deployment evicts the entries of its case definition keys. --- .../test/VerifyDatabaseOperationsTest.java | 393 ++++++++++++++++++ .../cmmn/engine/CmmnEngineConfiguration.java | 42 ++ .../engine/impl/deployer/CmmnDeployer.java | 11 + .../impl/deployer/CmmnDeploymentManager.java | 41 ++ .../SameDeploymentCaseDefinitionLookup.java | 48 +++ .../impl/runtime/CaseInstanceHelperImpl.java | 7 +- 6 files changed, 539 insertions(+), 3 deletions(-) create mode 100644 modules/flowable-cmmn-engine/src/main/java/org/flowable/cmmn/engine/impl/deployer/SameDeploymentCaseDefinitionLookup.java 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 a87f9b2b12c..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(); } @@ -123,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 @@ -146,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); From 5331e2a811365a133c3e79bc1373cd6a9c30da63 Mon Sep 17 00:00:00 2001 From: Filip Hrisafov Date: Fri, 16 Oct 2026 14:52:07 +0200 Subject: [PATCH 3/4] Cache the decisions found by parent deployment and key Executing a decision with a parent deployment id, which includes the BPMN business rule task and the CMMN decision task, now caches the id of the found decision. The deployment query by parent and the decision select no longer run on a hit. By default a cached id is checked by selecting the decision by id; with verifyCachedSameDeploymentDefinitions disabled the decision is taken from the decision cache. Deploying and removing a deployment evicts the entries of its decision keys. --- .../test/VerifyDatabaseOperationsTest.java | 446 +++++++++++++++++- .../dmn/engine/DmnEngineConfiguration.java | 39 ++ .../impl/cmd/AbstractExecuteDecisionCmd.java | 20 +- .../dmn/engine/impl/deployer/DmnDeployer.java | 9 + .../persistence/deploy/DeploymentManager.java | 48 ++ .../deploy/SameDeploymentDecisionLookup.java | 47 ++ 6 files changed, 592 insertions(+), 17 deletions(-) create mode 100644 modules/flowable-dmn-engine/src/main/java/org/flowable/dmn/engine/impl/persistence/deploy/SameDeploymentDecisionLookup.java 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); + } +} From 46844a6effc95bf3e050524dc601f4ae7d17468f Mon Sep 17 00:00:00 2001 From: Filip Hrisafov Date: Wed, 21 Oct 2026 14:16:45 +0200 Subject: [PATCH 4/4] Do not profile the id block fetching in the profiling db sql session factory The DbIdGenerator fetches the next id block through the process engine command executor. When a command of another engine is profiled, the select of the id block was attributed to that command. --- .../engine/test/profiler/ProfilingDbSqlSessionFactory.java | 7 +++++++ 1 file changed, 7 insertions(+) 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)); } }