Skip to content

Commit

Permalink
merge: #10442
Browse files Browse the repository at this point in the history
10442: feat: support terminate end events r=saig0 a=saig0

## Description

Add support for BPMN terminate end events. See #8789 (comment) on how the BPMN element should work.

The implementation doesn't follow the BPMN spec in one point: the flow scope that contains the terminate end event is not terminated but completed. Reasoning:
- The state of the flow scope is a detail that doesn't influence the core behavior. In both cases, the process instance should continue, for example, by taking the outgoing sequence flow. The difference is not visible to process participants but only when monitoring the process instance, for example, in Operate.
- It fits better with the existing implementation. It would be a bigger effort to continue the process instance (e.g. taking the outgoing sequence flow) when the flow scope is terminated. As a result, we would end up in more complex code.
- It aligns with the behavior of Camunda Platform 7. 

Side note: I implemented the major parts during a [Live Hacking session](https://www.twitch.tv/videos/1584245006). 🎥 

## Related issues

closes #8789



Co-authored-by: Philipp Ossler <philipp.ossler@gmail.com>
  • Loading branch information
zeebe-bors-camunda[bot] and saig0 committed Sep 23, 2022
2 parents 67da38b + 186faea commit c770682
Show file tree
Hide file tree
Showing 12 changed files with 770 additions and 54 deletions.
Expand Up @@ -19,6 +19,7 @@
import io.camunda.zeebe.model.bpmn.BpmnModelInstance;
import io.camunda.zeebe.model.bpmn.instance.EndEvent;
import io.camunda.zeebe.model.bpmn.instance.ErrorEventDefinition;
import io.camunda.zeebe.model.bpmn.instance.TerminateEventDefinition;

/**
* @author Sebastian Menski
Expand Down Expand Up @@ -71,4 +72,17 @@ public ErrorEventDefinitionBuilder errorEventDefinition() {
element.getEventDefinitions().add(errorEventDefinition);
return new ErrorEventDefinitionBuilder(modelInstance, errorEventDefinition);
}

/**
* Creates a terminate event definition and add it to the end event. It morphs the end event into
* a terminate end event.
*
* @return the builder object
*/
public B terminate() {
final TerminateEventDefinition terminateEventDefinition =
createInstance(TerminateEventDefinition.class);
element.getEventDefinitions().add(terminateEventDefinition);
return myself;
}
}
Expand Up @@ -19,6 +19,7 @@
import io.camunda.zeebe.model.bpmn.instance.ErrorEventDefinition;
import io.camunda.zeebe.model.bpmn.instance.EventDefinition;
import io.camunda.zeebe.model.bpmn.instance.MessageEventDefinition;
import io.camunda.zeebe.model.bpmn.instance.TerminateEventDefinition;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
Expand All @@ -28,7 +29,8 @@
public class EndEventValidator implements ModelElementValidator<EndEvent> {

private static final List<Class<? extends EventDefinition>> SUPPORTED_EVENT_DEFINITIONS =
Arrays.asList(ErrorEventDefinition.class, MessageEventDefinition.class);
Arrays.asList(
ErrorEventDefinition.class, MessageEventDefinition.class, TerminateEventDefinition.class);

@Override
public Class<EndEvent> getElementType() {
Expand Down Expand Up @@ -59,7 +61,7 @@ private void validateEventDefinition(
def -> {
if (SUPPORTED_EVENT_DEFINITIONS.stream().noneMatch(type -> type.isInstance(def))) {
validationResultCollector.addError(
0, "End events must be one of: none, error or message");
0, "End events must be one of: none, error, message, or terminate");
}
});
}
Expand Down
Expand Up @@ -18,6 +18,7 @@
import io.camunda.zeebe.model.bpmn.instance.ErrorEventDefinition;
import io.camunda.zeebe.model.bpmn.instance.EventDefinition;
import io.camunda.zeebe.model.bpmn.instance.MessageEventDefinition;
import io.camunda.zeebe.model.bpmn.instance.TerminateEventDefinition;
import io.camunda.zeebe.model.bpmn.instance.TimerEventDefinition;
import java.util.Arrays;
import java.util.List;
Expand All @@ -28,7 +29,10 @@ public class EventDefinitionValidator implements ModelElementValidator<EventDefi

private static final List<Class<? extends EventDefinition>> SUPPORTED_EVENT_DEFINITIONS =
Arrays.asList(
MessageEventDefinition.class, TimerEventDefinition.class, ErrorEventDefinition.class);
MessageEventDefinition.class,
TimerEventDefinition.class,
ErrorEventDefinition.class,
TerminateEventDefinition.class);

@Override
public Class<EventDefinition> getElementType() {
Expand Down
@@ -0,0 +1,148 @@
/*
* Copyright © 2017 camunda services GmbH (info@camunda.com)
*
* 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 io.camunda.zeebe.model.bpmn.validation;

import static io.camunda.zeebe.model.bpmn.validation.ExpectedValidationResult.expect;

import io.camunda.zeebe.model.bpmn.Bpmn;
import io.camunda.zeebe.model.bpmn.BpmnModelInstance;
import io.camunda.zeebe.model.bpmn.builder.EndEventBuilder;
import io.camunda.zeebe.model.bpmn.builder.StartEventBuilder;
import io.camunda.zeebe.model.bpmn.instance.CancelEventDefinition;
import io.camunda.zeebe.model.bpmn.instance.CompensateEventDefinition;
import io.camunda.zeebe.model.bpmn.instance.EndEvent;
import io.camunda.zeebe.model.bpmn.instance.ErrorEventDefinition;
import io.camunda.zeebe.model.bpmn.instance.EscalationEventDefinition;
import io.camunda.zeebe.model.bpmn.instance.EventDefinition;
import io.camunda.zeebe.model.bpmn.instance.MessageEventDefinition;
import io.camunda.zeebe.model.bpmn.instance.SignalEventDefinition;
import io.camunda.zeebe.model.bpmn.instance.TerminateEventDefinition;
import java.util.function.UnaryOperator;
import java.util.stream.Stream;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

class ZeebeEndEventValidationTest {

private static final String END_EVENT_ID = "end";

@ParameterizedTest(name = "[{index}] event type = {0}")
@MethodSource("supportedEndEventTypes")
@DisplayName("Should support end event of the given type")
void supportedEndEventTypes(final EndEventTypeBuilder endEventTypeBuilder) {
// given
final BpmnModelInstance process = createProcessWithEndEvent(endEventTypeBuilder);

// when/then
ProcessValidationUtil.assertThatProcessIsValid(process);
}

@ParameterizedTest(name = "[{index}] event type = {0}")
@MethodSource("unsupportedEndEventTypes")
@DisplayName("Should not support end event of the given type")
void unsupportedEndEventTypes(final EndEventTypeBuilder endEventTypeBuilder) {
// given
final BpmnModelInstance process = createProcessWithEndEvent(endEventTypeBuilder);

// when/then
ProcessValidationUtil.assertThatProcessHasViolations(
process,
expect(END_EVENT_ID, "End events must be one of: none, error, message, or terminate"),
expect(endEventTypeBuilder.eventType, "Event definition of this type is not supported"));
}

@Test
@DisplayName("An end event should not have an outgoing sequence flow")
void outgoingSequenceFlow() {
// given
final BpmnModelInstance process =
Bpmn.createExecutableProcess("process")
.startEvent()
.endEvent(END_EVENT_ID)
// an activity after the end event
.manualTask()
.done();

// when/then
ProcessValidationUtil.assertThatProcessHasViolations(
process,
expect(
EndEvent.class, "End events must not have outgoing sequence flows to other elements."));
}

private static BpmnModelInstance createProcessWithEndEvent(
final EndEventTypeBuilder endEventTypeBuilder) {
final StartEventBuilder processBuilder = Bpmn.createExecutableProcess("process").startEvent();
endEventTypeBuilder.build(processBuilder.endEvent(END_EVENT_ID));
return processBuilder.done();
}

private static Stream<EndEventTypeBuilder> supportedEndEventTypes() {
return Stream.of(
new EndEventTypeBuilder(null, endEvent -> endEvent),
new EndEventTypeBuilder(
ErrorEventDefinition.class, endEvent -> endEvent.error("error-code")),
new EndEventTypeBuilder(
MessageEventDefinition.class,
endEvent -> endEvent.message("message-name").zeebeJobType("job-type")),
new EndEventTypeBuilder(TerminateEventDefinition.class, EndEventBuilder::terminate));
}

private static Stream<EndEventTypeBuilder> unsupportedEndEventTypes() {
return Stream.of(
new EndEventTypeBuilder(
SignalEventDefinition.class, endEvent -> endEvent.signal("signal-name")),
new EndEventTypeBuilder(
EscalationEventDefinition.class, endEvent -> endEvent.escalation("escalation-code")),
new EndEventTypeBuilder(
CompensateEventDefinition.class,
endEvent -> endEvent.compensateEventDefinition().compensateEventDefinitionDone()),
new EndEventTypeBuilder(
CancelEventDefinition.class,
endEvent -> {
// currently, we don't have a builder for cancel events
final CancelEventDefinition cancelEventDefinition =
endEvent.getElement().getModelInstance().newInstance(CancelEventDefinition.class);
endEvent.getElement().getEventDefinitions().add(cancelEventDefinition);
return endEvent;
}));
}

private static final class EndEventTypeBuilder {
private final String eventTypeName;
private final Class<? extends EventDefinition> eventType;
private final UnaryOperator<EndEventBuilder> elementModifier;

private EndEventTypeBuilder(
final Class<? extends EventDefinition> eventType,
final UnaryOperator<EndEventBuilder> elementModifier) {
this.eventType = eventType;
eventTypeName = eventType == null ? "none" : eventType.getSimpleName();
this.elementModifier = elementModifier;
}

public EndEventBuilder build(final EndEventBuilder endEventBuilder) {
return elementModifier.apply(endEventBuilder);
}

@Override
public String toString() {
return eventTypeName;
}
}
}
Expand Up @@ -23,7 +23,6 @@
import io.camunda.zeebe.model.bpmn.builder.AbstractCatchEventBuilder;
import io.camunda.zeebe.model.bpmn.builder.ProcessBuilder;
import io.camunda.zeebe.model.bpmn.instance.CompensateEventDefinition;
import io.camunda.zeebe.model.bpmn.instance.EndEvent;
import io.camunda.zeebe.model.bpmn.instance.IntermediateCatchEvent;
import io.camunda.zeebe.model.bpmn.instance.SignalEventDefinition;
import java.util.Arrays;
Expand All @@ -38,28 +37,6 @@ public static Object[][] parameters() {
Bpmn.createExecutableProcess("process").done(),
singletonList(expect("process", "Must have at least one start event"))
},
{
Bpmn.createExecutableProcess("process")
.startEvent()
.endEvent("end")
.signalEventDefinition("foo")
.id("eventDefinition")
.done(),
Arrays.asList(
expect("end", "End events must be one of: none, error or message"),
expect("eventDefinition", "Event definition of this type is not supported"))
},
{
Bpmn.createExecutableProcess("process")
.startEvent()
.endEvent()
.serviceTask("task", tb -> tb.zeebeJobType("task"))
.done(),
singletonList(
expect(
EndEvent.class,
"End events must not have outgoing sequence flows to other elements."))
},
{
Bpmn.createExecutableProcess("process")
.startEvent()
Expand Down
Expand Up @@ -15,7 +15,6 @@
import io.camunda.zeebe.engine.processing.bpmn.behavior.BpmnStateTransitionBehavior;
import io.camunda.zeebe.engine.processing.bpmn.behavior.BpmnVariableMappingBehavior;
import io.camunda.zeebe.engine.processing.deployment.model.element.ExecutableFlowElementContainer;
import io.camunda.zeebe.protocol.record.intent.ProcessInstanceIntent;

public final class EventSubProcessProcessor
implements BpmnElementContainerProcessor<ExecutableFlowElementContainer> {
Expand Down Expand Up @@ -93,12 +92,20 @@ public void onChildTerminated(
final ExecutableFlowElementContainer element,
final BpmnElementContext flowScopeContext,
final BpmnElementContext childContext) {
final var flowScopeInstance = stateBehavior.getElementInstance(flowScopeContext);

if (childContext == null
|| (flowScopeContext.getIntent() == ProcessInstanceIntent.ELEMENT_TERMINATING
&& stateBehavior.canBeTerminated(childContext))) {
final var terminated = stateTransitionBehavior.transitionToTerminated(flowScopeContext);
stateTransitionBehavior.onElementTerminated(element, terminated);
if (childContext == null || stateBehavior.canBeTerminated(childContext)) {

if (flowScopeInstance.isTerminating()) {
// the event subprocess was terminated by its flow scope
final var terminated = stateTransitionBehavior.transitionToTerminated(flowScopeContext);
stateTransitionBehavior.onElementTerminated(element, terminated);

} else if (flowScopeInstance.isActive()) {
// the child element instances were terminated by a terminate end event in the
// event subprocess
stateTransitionBehavior.completeElement(flowScopeContext);
}
}
}
}
Expand Up @@ -148,10 +148,19 @@ public void onChildTerminated(
flowScopeContext));

} else if (stateBehavior.canBeTerminated(childContext)) {
transitionTo(
element,
flowScopeContext,
context -> Either.right(stateTransitionBehavior.transitionToTerminated(context)));

final var flowScopeInstance = stateBehavior.getElementInstance(flowScopeContext);
if (flowScopeInstance.isTerminating()) {
// the process instance was canceled, or interrupted by a parent process instance
transitionTo(
element,
flowScopeContext,
context -> Either.right(stateTransitionBehavior.transitionToTerminated(context)));

} else if (flowScopeInstance.isActive()) {
// the child element instances were terminated by a terminate end event in the process
stateTransitionBehavior.completeElement(flowScopeContext);
}
}
}

Expand Down
Expand Up @@ -110,6 +110,7 @@ public void onChildTerminated(
final BpmnElementContext subProcessContext,
final BpmnElementContext childContext) {
final var flowScopeInstance = stateBehavior.getFlowScopeInstance(subProcessContext);
final var subProcessInstance = stateBehavior.getElementInstance(subProcessContext);

if (stateBehavior.isInterrupted(subProcessContext)) {
// an interrupting event subprocess was triggered
Expand Down Expand Up @@ -139,9 +140,17 @@ public void onChildTerminated(
terminated);
},
() -> {
final var terminated =
stateTransitionBehavior.transitionToTerminated(subProcessContext);
stateTransitionBehavior.onElementTerminated(element, terminated);
if (subProcessInstance.isTerminating()) {
// the subprocess was terminated by its flow scope
final var terminated =
stateTransitionBehavior.transitionToTerminated(subProcessContext);
stateTransitionBehavior.onElementTerminated(element, terminated);

} else if (subProcessInstance.isActive()) {
// the child element instances were terminated by a terminate end event in the
// subprocess
stateTransitionBehavior.completeElement(subProcessContext);
}
});
}
}
Expand Down

0 comments on commit c770682

Please sign in to comment.