提交 02b96241 编写于 作者: J Joram Barrez

ACT-1890: Multi tenancy support: QA fix + deployment migration

上级 8819e935
......@@ -89,6 +89,40 @@ public interface RepositoryService {
* for the given deploymentId.
*/
InputStream getResourceAsStream(String deploymentId, String resourceName);
/**
*
* EXPERIMENTAL FEATURE!
*
* Changes the tenant identifier of a deployment to match the given tenant identifier.
* This change will cascade to any related entity:
* - process definitions related to the deployment
* - process instances related to those process definitions
* - executions related to those process instances
* - tasks related to those process instances
* - jobs related to the process definitions and process instances
*
* This method can be used in the case that there was no tenant identifier set
* on the deployment or those entities before.
*
* This method can be used to remove a tenant identifier from the
* deployment and related entities (simply pass null).
*
* Important: no optimistic locking will be done while executing the tenant identifier change!
*
* This is an experimental feature, mainly because it WILL NOT work
* properly in a clustered environment without special care:
* suppose some process instance is in flight. The process definition is in the
* process definition cache. When a task or job is created when continuing the process
* instance, the process definition cache will be consulted to get the process definition
* and from it the tenant identifier. Since it's cached, it will not be the new tenant identifier.
* This method does clear the cache for this engineinstance , but it will not be cleared
* on other nodes in a cluster (unless using a shared process definition cache).
*
* @param deploymentId The id of the deployment of which the tenant identifier will be changed.
* @param newTenantId The new tenant identifier.
*/
void changeDeploymentTenantId(String deploymentId, String newTenantId);
/** Query process definitions. */
ProcessDefinitionQuery createProcessDefinitionQuery();
......
......@@ -23,6 +23,7 @@ import org.activiti.engine.impl.cmd.ActivateProcessDefinitionCmd;
import org.activiti.engine.impl.cmd.AddEditorSourceExtraForModelCmd;
import org.activiti.engine.impl.cmd.AddEditorSourceForModelCmd;
import org.activiti.engine.impl.cmd.AddIdentityLinkForProcessDefinitionCmd;
import org.activiti.engine.impl.cmd.ChangeDeploymentTenantIdCmd;
import org.activiti.engine.impl.cmd.CreateModelCmd;
import org.activiti.engine.impl.cmd.DeleteDeploymentCmd;
import org.activiti.engine.impl.cmd.DeleteIdentityLinkForProcessDefinitionCmd;
......@@ -108,6 +109,11 @@ public class RepositoryServiceImpl extends ServiceImpl implements RepositoryServ
public InputStream getResourceAsStream(String deploymentId, String resourceName) {
return commandExecutor.execute(new GetDeploymentResourceCmd(deploymentId, resourceName));
}
@Override
public void changeDeploymentTenantId(String deploymentId, String newTenantId) {
commandExecutor.execute(new ChangeDeploymentTenantIdCmd(deploymentId, newTenantId));
}
public DeploymentQuery createDeploymentQuery() {
return new DeploymentQueryImpl(commandExecutor);
......
......@@ -208,6 +208,12 @@ public class BpmnDeployer implements Deployer {
for (TimerDeclarationImpl timerDeclaration : timerDeclarations) {
TimerEntity timer = timerDeclaration.prepareTimerEntity(null);
timer.setProcessDefinitionId(processDefinition.getId());
// Inherit timer (if appliccable)
if (processDefinition.getTenantId() != null) {
timer.setTenantId(processDefinition.getTenantId());
}
timers.add(timer);
}
}
......
/* 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.activiti.engine.impl.cmd;
import java.io.Serializable;
import java.util.List;
import org.activiti.engine.ActivitiIllegalArgumentException;
import org.activiti.engine.ActivitiObjectNotFoundException;
import org.activiti.engine.impl.interceptor.Command;
import org.activiti.engine.impl.interceptor.CommandContext;
import org.activiti.engine.impl.persistence.entity.DeploymentEntity;
import org.activiti.engine.repository.Deployment;
import org.activiti.engine.repository.ProcessDefinition;
/**
* @author Joram Barrez
*/
public class ChangeDeploymentTenantIdCmd implements Command<Void>, Serializable {
private static final long serialVersionUID = 1L;
protected String deploymentId;
protected String newTenantId;
public ChangeDeploymentTenantIdCmd(String deploymentId, String newTenantId) {
this.deploymentId = deploymentId;
this.newTenantId = newTenantId;
}
public Void execute(CommandContext commandContext) {
if (deploymentId == null) {
throw new ActivitiIllegalArgumentException("deploymentId is null");
}
// Impl note: tenant id is allowed to be null, no check needed
// Update all entities
DeploymentEntity deployment = commandContext.getDeploymentEntityManager().findDeploymentById(deploymentId);
if (deployment == null) {
throw new ActivitiObjectNotFoundException("Could not find deployment with id " + deploymentId, Deployment.class);
}
deployment.setTenantId(newTenantId);
// Doing process instances, executions and tasks with direct SQL updates (otherwise would not be performant)
commandContext.getProcessDefinitionEntityManager().updateProcessDefinitionTenantIdForDeployment(deploymentId, newTenantId);
commandContext.getExecutionEntityManager().updateExecutionTenantIdForDeployment(deploymentId, newTenantId);
commandContext.getTaskEntityManager().updateTaskTenantIdForDeployment(deploymentId, newTenantId);
commandContext.getJobEntityManager().updateJobTenantIdForDeployment(deploymentId, newTenantId);
// Doing process definitions in memory, cause we need to clear the process definition cache
List<ProcessDefinition> processDefinitions = commandContext.getDbSqlSession()
.createProcessDefinitionQuery()
.deploymentId(deploymentId).list();
for (ProcessDefinition processDefinition : processDefinitions) {
commandContext.getProcessEngineConfiguration().getProcessDefinitionCache().remove(processDefinition.getId());
}
// Clear process definition cache
commandContext.getProcessEngineConfiguration().getProcessDefinitionCache().clear();
return null;
}
}
......@@ -141,7 +141,7 @@ public class TimerDeclarationImpl implements Serializable {
timer.setProcessDefinitionId(executionEntity.getProcessDefinitionId());
// Inherit tenant identifier (if applicable)
if (executionEntity.getTenantId() != null) {
if (executionEntity != null && executionEntity.getTenantId() != null) {
timer.setTenantId(executionEntity.getTenantId());
}
}
......
......@@ -75,6 +75,7 @@ public class DeploymentEntity implements Serializable, Deployment, PersistentObj
public Object getPersistentState() {
Map<String, Object> persistentState = new HashMap<String, Object>();
persistentState.put("category", this.category);
persistentState.put("tenantId", tenantId);
return persistentState;
}
......
......@@ -166,5 +166,12 @@ public class ExecutionEntityManager extends AbstractManager {
public long findExecutionCountByNativeQuery(Map<String, Object> parameterMap) {
return (Long) getDbSqlSession().selectOne("selectExecutionCountByNativeQuery", parameterMap);
}
public void updateExecutionTenantIdForDeployment(String deploymentId, String newTenantId) {
HashMap<String, Object> params = new HashMap<String, Object>();
params.put("deploymentId", deploymentId);
params.put("tenantId", newTenantId);
getDbSqlSession().getSqlSession().update("updateExecutionTenantIdForDeployment", params);
}
}
......@@ -96,7 +96,7 @@ public abstract class JobEntity implements Serializable, Job, PersistentObject,
execution.addJob(this);
// Inherit tenant if (if applicable)
if (execution.getTenantId() != null) {
if (execution != null && execution.getTenantId() != null) {
setTenantId(execution.getTenantId());
}
}
......
......@@ -147,5 +147,12 @@ public class JobEntityManager extends AbstractManager {
public long findJobCountByQueryCriteria(JobQueryImpl jobQuery) {
return (Long) getDbSqlSession().selectOne("selectJobCountByQueryCriteria", jobQuery);
}
public void updateJobTenantIdForDeployment(String deploymentId, String newTenantId) {
HashMap<String, Object> params = new HashMap<String, Object>();
params.put("deploymentId", deploymentId);
params.put("tenantId", newTenantId);
getDbSqlSession().getSqlSession().update("updateJobTenantIdForDeployment", params);
}
}
......@@ -97,5 +97,12 @@ public class ProcessDefinitionEntityManager extends AbstractManager {
public long findProcessDefinitionCountByNativeQuery(Map<String, Object> parameterMap) {
return (Long) getDbSqlSession().selectOne("selectProcessDefinitionCountByNativeQuery", parameterMap);
}
public void updateProcessDefinitionTenantIdForDeployment(String deploymentId, String newTenantId) {
HashMap<String, Object> params = new HashMap<String, Object>();
params.put("deploymentId", deploymentId);
params.put("tenantId", newTenantId);
getDbSqlSession().getSqlSession().update("updateProcessDefinitionTenantIdForDeploymentId", params);
}
}
......@@ -115,7 +115,7 @@ public class TaskEntity extends VariableScopeImpl implements Task, DelegateTask,
dbSqlSession.insert(this);
// Inherit tenant id (if applicable)
if (execution.getTenantId() != null) {
if (execution != null && execution.getTenantId() != null) {
setTenantId(execution.getTenantId());
}
......
......@@ -14,6 +14,7 @@
package org.activiti.engine.impl.persistence.entity;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
......@@ -191,4 +192,12 @@ public class TaskEntityManager extends AbstractManager {
.deleteHistoricTaskInstanceById(taskId);
}
}
public void updateTaskTenantIdForDeployment(String deploymentId, String newTenantId) {
HashMap<String, Object> params = new HashMap<String, Object>();
params.put("deploymentId", deploymentId);
params.put("tenantId", newTenantId);
getDbSqlSession().getSqlSession().update("updateTaskTenantIdForDeployment", params);
}
}
......@@ -15,7 +15,8 @@
<update id="updateDeployment" parameterType="org.activiti.engine.impl.persistence.entity.DeploymentEntity">
update ${prefix}ACT_RE_DEPLOYMENT set
CATEGORY_ = #{category, jdbcType=VARCHAR}
CATEGORY_ = #{category, jdbcType=VARCHAR},
TENANT_ID_ = #{tenantId, jdbcType=VARCHAR}
where ID_ = #{id, jdbcType=VARCHAR}
</update>
......
......@@ -47,6 +47,18 @@
and REV_ = #{revision, jdbcType=INTEGER}
</update>
<update id="updateExecutionTenantIdForDeployment" parameterType="java.util.Map">
update ${prefix}ACT_RU_EXECUTION set
TENANT_ID_ = #{tenantId, jdbcType=VARCHAR}
where
ID_ in (
SELECT E.ID_ from ${prefix}ACT_RU_EXECUTION E
inner join ${prefix}ACT_RE_PROCDEF P on E.PROC_DEF_ID_ = P.ID_
inner join ${prefix}ACT_RE_DEPLOYMENT D on P.DEPLOYMENT_ID_ = D.ID_
where D.ID_ = #{deploymentId, jdbcType=VARCHAR}
)
</update>
<!-- EXECUTION DELETE -->
<delete id="deleteExecution" parameterType="org.activiti.engine.impl.persistence.entity.ExecutionEntity">
......
......@@ -18,6 +18,19 @@
delete from ${prefix}ACT_RU_JOB where ID_ = #{id} and REV_ = #{revision}
</delete>
<!-- JOB UPDATE STATEMENTS -->
<update id="updateJobTenantIdForDeployment" parameterType="java.util.Map">
update ${prefix}ACT_RU_JOB set
TENANT_ID_ = #{tenantId, jdbcType=VARCHAR}
where
ID_ in (
SELECT J.ID_ from ${prefix}ACT_RU_JOB J
inner join ${prefix}ACT_RE_PROCDEF P on J.PROC_DEF_ID_ = P.ID_
inner join ${prefix}ACT_RE_DEPLOYMENT D on P.DEPLOYMENT_ID_ = D.ID_
where D.ID_ = #{deploymentId, jdbcType=VARCHAR}
)
</update>
<!-- JOB RESULTMAP (FOR TIMER AND MESSAGE) -->
<resultMap id="jobResultMap" type="org.activiti.engine.impl.persistence.entity.JobEntity">
......
......@@ -33,6 +33,17 @@
where ID_ = #{id, jdbcType=VARCHAR}
and REV_ = #{revision, jdbcType=INTEGER}
</update>
<update id="updateProcessDefinitionTenantIdForDeploymentId" parameterType="java.util.Map">
update ${prefix}ACT_RE_PROCDEF set
TENANT_ID_ = #{tenantId, jdbcType=VARCHAR}
where
ID_ in (
SELECT P.ID_ from ${prefix}ACT_RE_PROCDEF P
inner join ${prefix}ACT_RE_DEPLOYMENT D on P.DEPLOYMENT_ID_ = D.ID_
where D.ID_ = #{deploymentId, jdbcType=VARCHAR}
)
</update>
<!-- PROCESSDEFINITION DELETE -->
......
......@@ -69,6 +69,18 @@
and REV_ = #{revision, jdbcType=INTEGER}
</update>
<update id="updateTaskTenantIdForDeployment" parameterType="java.util.Map">
update ${prefix}ACT_RU_TASK set
TENANT_ID_ = #{tenantId, jdbcType=VARCHAR}
where
ID_ in (
SELECT T.ID_ from ${prefix}ACT_RU_TASK T
inner join ${prefix}ACT_RE_PROCDEF P on T.PROC_DEF_ID_ = P.ID_
inner join ${prefix}ACT_RE_DEPLOYMENT D on P.DEPLOYMENT_ID_ = D.ID_
where D.ID_ = #{deploymentId, jdbcType=VARCHAR}
)
</update>
<!-- TASK DELETE -->
<delete id="deleteTask" parameterType="org.activiti.engine.impl.persistence.entity.TaskEntity">
delete from ${prefix}ACT_RU_TASK where ID_ = #{id} and REV_ = #{revision}
......
......@@ -4,9 +4,9 @@ import java.util.ArrayList;
import java.util.List;
import org.activiti.engine.impl.test.PluggableActivitiTestCase;
import org.activiti.engine.repository.Deployment;
import org.activiti.engine.repository.Model;
import org.activiti.engine.runtime.Job;
import org.activiti.engine.runtime.ProcessInstance;
/**
* A test case for the various implications of the tenancy support (tenant id column to entities + query support)
......@@ -213,12 +213,14 @@ public class TenancyTest extends PluggableActivitiTestCase {
.deploy()
.getId();
// Start process instance
runtimeService.startProcessInstanceByKey("testJobTenancy");
// Verify Job tenancy (process start timer)
// verify job (timer start)
Job job = managementService.createJobQuery().singleResult();
assertEquals(TEST_TENANT_ID, job.getTenantId());
managementService.executeJob(job.getId());
// Verify Job tenancy (process intermediary timer)
job = managementService.createJobQuery().singleResult();
assertEquals(TEST_TENANT_ID, job.getTenantId());
// Start process, and verify async job has correct tenant id
managementService.executeJob(job.getId());
......@@ -233,12 +235,15 @@ public class TenancyTest extends PluggableActivitiTestCase {
.addClasspathResource("org/activiti/engine/test/api/tenant/TenancyTest.testJobTenancy.bpmn20.xml")
.deploy()
.getId();
runtimeService.startProcessInstanceByKey("testJobTenancy");
job = managementService.createJobQuery().singleResult();
assertEquals(null, job.getTenantId());
managementService.executeJob(job.getId());
job = managementService.createJobQuery().singleResult();
assertEquals(null, job.getTenantId());
job = managementService.createJobQuery().singleResult();
assertEquals(null, job.getTenantId());
managementService.executeJob(job.getId());
job = managementService.createJobQuery().singleResult();
assertEquals(null, job.getTenantId());
managementService.executeJob(job.getId());
job = managementService.createJobQuery().singleResult();
assertEquals(null, job.getTenantId());
// clean up
repositoryService.deleteDeployment(deploymentId, true);
......@@ -277,5 +282,130 @@ public class TenancyTest extends PluggableActivitiTestCase {
}
}
public void testChangeDeploymentTenantId() {
// Generate 8 tasks with tenant
String processDefinitionIdWithTenant = deployTestProcessWithTwoTasksWithTestTenant();
int nrOfProcessInstancesWithTenant = 4;
for (int i=0; i<nrOfProcessInstancesWithTenant; i++) {
runtimeService.startProcessInstanceById(processDefinitionIdWithTenant);
}
// Generate 10 tasks without tenant
String processDefinitionIdNoTenant = deployTwoTasksTestProcess();
int nrOfProcessInstancesNoTenant = 5;
for (int i = 0; i < nrOfProcessInstancesNoTenant; i++) {
runtimeService.startProcessInstanceById(processDefinitionIdNoTenant);
}
// Migrate deployment with tenant to another tenant
String newTenantId = "NEW TENANT ID";
String deploymentId = repositoryService.createProcessDefinitionQuery()
.processDefinitionId(processDefinitionIdWithTenant).singleResult().getDeploymentId();
repositoryService.changeDeploymentTenantId(deploymentId, newTenantId);
// Verify tenant id
Deployment deployment = repositoryService.createDeploymentQuery().deploymentId(deploymentId).singleResult();
assertEquals(newTenantId, deployment.getTenantId());
// Verify deployment
assertEquals(2, repositoryService.createDeploymentQuery().list().size());
assertEquals(0, repositoryService.createDeploymentQuery().deploymentTenantId(TEST_TENANT_ID).list().size());
assertEquals(1, repositoryService.createDeploymentQuery().deploymentTenantId(newTenantId).list().size());
assertEquals(1, repositoryService.createDeploymentQuery().deploymentWithoutTenantId().list().size());
// Verify process definition
assertEquals(2, repositoryService.createProcessDefinitionQuery().list().size());
assertEquals(0, repositoryService.createProcessDefinitionQuery().processDefinitionTenantId(TEST_TENANT_ID).list().size());
assertEquals(1, repositoryService.createProcessDefinitionQuery().processDefinitionTenantId(newTenantId).list().size());
assertEquals(1, repositoryService.createProcessDefinitionQuery().processDefinitionTenantId(newTenantId).list().size());
// Verify process instances
assertEquals(nrOfProcessInstancesNoTenant + nrOfProcessInstancesWithTenant, runtimeService.createProcessInstanceQuery().list().size());
assertEquals(0, runtimeService.createProcessInstanceQuery().processInstanceTenantId(TEST_TENANT_ID).list().size());
assertEquals(nrOfProcessInstancesWithTenant, runtimeService.createProcessInstanceQuery().processInstanceTenantId(newTenantId).list().size());
assertEquals(nrOfProcessInstancesNoTenant, runtimeService.createProcessInstanceQuery().processInstanceWithoutTenantId().list().size());
// Verify executions
assertEquals(3 * (nrOfProcessInstancesNoTenant + nrOfProcessInstancesWithTenant), runtimeService.createExecutionQuery().list().size());
assertEquals(3 * nrOfProcessInstancesNoTenant, runtimeService.createExecutionQuery().executionWithoutTenantId().list().size());
assertEquals(0, runtimeService.createExecutionQuery().executionTenantId(TEST_TENANT_ID).list().size());
assertEquals(3 * nrOfProcessInstancesWithTenant, runtimeService.createExecutionQuery().executionTenantId(newTenantId).list().size());
assertEquals(3 * nrOfProcessInstancesWithTenant, runtimeService.createExecutionQuery().executionTenantIdLike("NEW%").list().size());
// Verify tasks
assertEquals(2 * (nrOfProcessInstancesNoTenant + nrOfProcessInstancesWithTenant), taskService.createTaskQuery().list().size());
assertEquals(0, taskService.createTaskQuery().taskTenantId(TEST_TENANT_ID).list().size());
assertEquals(2 * nrOfProcessInstancesWithTenant, taskService.createTaskQuery().taskTenantId(newTenantId).list().size());
assertEquals(2 * nrOfProcessInstancesNoTenant, taskService.createTaskQuery().taskWithoutTenantId().list().size());
// Remove the tenant id and verify results
repositoryService.changeDeploymentTenantId(deploymentId, null);
// Verify deployment
assertEquals(2, repositoryService.createDeploymentQuery().list().size());
assertEquals(0, repositoryService.createDeploymentQuery().deploymentTenantId(TEST_TENANT_ID).list().size());
assertEquals(0, repositoryService.createDeploymentQuery().deploymentTenantId(newTenantId).list().size());
assertEquals(2, repositoryService.createDeploymentQuery().deploymentWithoutTenantId().list().size());
// Verify process definition
assertEquals(2, repositoryService.createProcessDefinitionQuery().list().size());
assertEquals(0, repositoryService.createProcessDefinitionQuery().processDefinitionTenantId(TEST_TENANT_ID).list().size());
assertEquals(0, repositoryService.createProcessDefinitionQuery().processDefinitionTenantId(newTenantId).list().size());
// Verify process instances
assertEquals(nrOfProcessInstancesNoTenant + nrOfProcessInstancesWithTenant, runtimeService.createProcessInstanceQuery().list().size());
assertEquals(0, runtimeService.createProcessInstanceQuery().processInstanceTenantId(TEST_TENANT_ID).list().size());
assertEquals(0, runtimeService.createProcessInstanceQuery().processInstanceTenantId(newTenantId).list().size());
assertEquals(nrOfProcessInstancesNoTenant + nrOfProcessInstancesWithTenant, runtimeService.createProcessInstanceQuery().processInstanceWithoutTenantId().list().size());
// Verify executions
assertEquals(3 * (nrOfProcessInstancesNoTenant + nrOfProcessInstancesWithTenant), runtimeService.createExecutionQuery().list().size());
assertEquals(3 * (nrOfProcessInstancesNoTenant + nrOfProcessInstancesWithTenant), runtimeService.createExecutionQuery().executionWithoutTenantId().list().size());
assertEquals(0, runtimeService.createExecutionQuery().executionTenantId(TEST_TENANT_ID).list().size());
assertEquals(0, runtimeService.createExecutionQuery().executionTenantId(newTenantId).list().size());
// Verify tasks
assertEquals(2 * (nrOfProcessInstancesNoTenant + nrOfProcessInstancesWithTenant), taskService.createTaskQuery().list().size());
assertEquals(0, taskService.createTaskQuery().taskTenantId(TEST_TENANT_ID).list().size());
assertEquals(0, taskService.createTaskQuery().taskTenantId(newTenantId).list().size());
assertEquals(2 * (nrOfProcessInstancesNoTenant + nrOfProcessInstancesWithTenant), taskService.createTaskQuery().taskWithoutTenantId().list().size());
}
public void testJobTenancyAfterTenantChange() {
// Deploy process with a timer and an async step AND with a tenant
String deploymentId = repositoryService.createDeployment()
.addClasspathResource("org/activiti/engine/test/api/tenant/TenancyTest.testJobTenancy.bpmn20.xml")
.tenantId(TEST_TENANT_ID)
.deploy()
.getId();
String newTenant = "newTenant";
repositoryService.changeDeploymentTenantId(deploymentId, newTenant);
// verify job (timer start)
Job job = managementService.createJobQuery().singleResult();
assertEquals(newTenant, job.getTenantId());
managementService.executeJob(job.getId());
// Verify Job tenancy (process intermediary timer)
job = managementService.createJobQuery().singleResult();
assertEquals(newTenant, job.getTenantId());
// Start process, and verify async job has correct tenant id
managementService.executeJob(job.getId());
job = managementService.createJobQuery().singleResult();
assertEquals(newTenant, job.getTenantId());
// Finish process
managementService.executeJob(job.getId());
// clean up
repositoryService.deleteDeployment(deploymentId, true);
}
}
......@@ -50,7 +50,7 @@ public class ManagementServiceTest extends PluggableActivitiTestCase {
TableMetaData tableMetaData = managementService.getTableMetaData(tablePrefix+"ACT_RU_TASK");
assertEquals(tableMetaData.getColumnNames().size(), tableMetaData.getColumnTypes().size());
assertEquals(17, tableMetaData.getColumnNames().size());
assertEquals(18, tableMetaData.getColumnNames().size());
int assigneeIndex = tableMetaData.getColumnNames().indexOf("ASSIGNEE_");
int createTimeIndex = tableMetaData.getColumnNames().indexOf("CREATE_TIME_");
......
......@@ -5,7 +5,11 @@
<process id="testJobTenancy">
<documentation>This is a process for testing purposes</documentation>
<startEvent id="theStart" />
<startEvent id="theStart" >
<timerEventDefinition>
<timeDate>2025-03-11T12:13:14</timeDate>
</timerEventDefinition>
</startEvent>
<sequenceFlow id="flow1" sourceRef="theStart" targetRef="timer" />
<intermediateCatchEvent id="timer">
<timerEventDefinition>
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册