Skip to content

Commit

Permalink
merge: #8822
Browse files Browse the repository at this point in the history
8822: Business rule task can define decisionId as expression r=korthout a=korthout

## Description

<!-- Please explain the changes you made here. -->
A business rule task with a called decision needs to specify a decisionId for the decision that is called. It should be possible to refer to a decision based on the evaluation result of an expression. So, it should be possible to define the decisionId as an expression. See #8779.

This adds support to define the `decisionId` of a `zeebe:calledDecision` as an expression (i.e. prefixed with `=`; e.g. `= variableContainingDecisionId`), while maintaining the ability to define it as a static value (i.e. not prefixed by `=`; e.g. `an_actual_decision_id`). For example, if the `decisionId` is an expression `= variableContainingDecisionId`, then the value of the `variableContainingDecisionId` variable should be used as the id of the decision that is called.

The `decisionId` expression is evaluated during the activation of the business rule task, just after input mapping and just before the decision is evaluated. This means you can use the result of input mappings in the `decisionId` expression.

If the evaluation fails, an incident of type `EXTRACT_VALUE_ERROR` is raised, similar to other expression evaluation failures. Resolving the incident will re-apply input mappings, re-evaluate the `decisionId` expression and then call the decision.

## Related issues

<!-- Which issues are closed by this PR or are related -->

closes #8779



Co-authored-by: Nico Korthout <nico.korthout@camunda.com>
  • Loading branch information
zeebe-bors-cloud[bot] and korthout committed Feb 22, 2022
2 parents 166f9f6 + d1f8c49 commit dfd0c9b
Show file tree
Hide file tree
Showing 8 changed files with 166 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ public BpmnBehaviorsImpl(
zeebeState,
eventTriggerBehavior,
stateWriter,
zeebeState.getKeyGenerator());
zeebeState.getKeyGenerator(),
expressionBehavior);

stateBehavior = new BpmnStateBehavior(zeebeState, variableBehavior);
stateTransitionGuard = new ProcessInstanceStateTransitionGuard(stateBehavior);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import io.camunda.zeebe.dmn.impl.VariablesContext;
import io.camunda.zeebe.engine.processing.bpmn.BpmnElementContext;
import io.camunda.zeebe.engine.processing.common.EventTriggerBehavior;
import io.camunda.zeebe.engine.processing.common.ExpressionProcessor;
import io.camunda.zeebe.engine.processing.common.Failure;
import io.camunda.zeebe.engine.processing.deployment.model.element.ExecutableCalledDecision;
import io.camunda.zeebe.engine.processing.streamprocessor.writers.StateWriter;
Expand Down Expand Up @@ -50,19 +51,22 @@ public final class BpmnDecisionBehavior {
private final VariableState variableState;
private final StateWriter stateWriter;
private final KeyGenerator keyGenerator;
private final ExpressionProcessor expressionBehavior;

public BpmnDecisionBehavior(
final DecisionEngine decisionEngine,
final ZeebeState zeebeState,
final EventTriggerBehavior eventTriggerBehavior,
final StateWriter stateWriter,
final KeyGenerator keyGenerator) {
final KeyGenerator keyGenerator,
final ExpressionProcessor expressionBehavior) {
this.decisionEngine = decisionEngine;
decisionState = zeebeState.getDecisionState();
variableState = zeebeState.getVariableState();
this.eventTriggerBehavior = eventTriggerBehavior;
this.stateWriter = stateWriter;
this.keyGenerator = keyGenerator;
this.expressionBehavior = expressionBehavior;
}

/**
Expand All @@ -76,20 +80,26 @@ public Either<Failure, DecisionEvaluationResult> evaluateDecision(
final ExecutableCalledDecision element, final BpmnElementContext context) {
final var scopeKey = context.getElementInstanceKey();

final var decisionIdOrFailure = evalDecisionIdExpression(element, scopeKey);
if (decisionIdOrFailure.isLeft()) {
return Either.left(decisionIdOrFailure.getLeft());
}

final var decisionId = decisionIdOrFailure.get();
// todo(#8571): avoid parsing drg every time
final var decisionOrFailure = findDecisionById(element.getDecisionId());
final var decisionOrFailure = findDecisionById(decisionId);
final var resultOrFailure =
decisionOrFailure
.flatMap(this::findDrgByDecision)
.mapLeft(
failure ->
new Failure(
"Expected to evaluate decision '%s', but %s"
.formatted(element.getDecisionId(), failure.getMessage())))
.formatted(decisionId, failure.getMessage())))
.flatMap(drg -> parseDrg(drg.getResource()))
// all the above failures have the same error type and the correct scope
.mapLeft(f -> new Failure(f.getMessage(), ErrorType.CALLED_DECISION_ERROR, scopeKey))
.flatMap(drg -> evaluateDecisionInDrg(drg, element.getDecisionId(), scopeKey));
.flatMap(drg -> evaluateDecisionInDrg(drg, decisionId, scopeKey));

resultOrFailure.ifRight(
result -> {
Expand All @@ -107,6 +117,11 @@ public Either<Failure, DecisionEvaluationResult> evaluateDecision(
return resultOrFailure;
}

private Either<Failure, String> evalDecisionIdExpression(
final ExecutableCalledDecision element, final long scopeKey) {
return expressionBehavior.evaluateStringExpression(element.getDecisionId(), scopeKey);
}

private Either<Failure, PersistedDecision> findDecisionById(final String decisionId) {
return Either.ofOptional(
decisionState.findLatestDecisionById(BufferUtil.wrapString(decisionId)))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,25 @@
*/
package io.camunda.zeebe.engine.processing.deployment.model.element;

import io.camunda.zeebe.el.Expression;

public final class ExecutableBusinessRuleTask extends ExecutableJobWorkerTask
implements ExecutableCalledDecision {

private String decisionId;
private Expression decisionId;
private String resultVariable;

public ExecutableBusinessRuleTask(final String id) {
super(id);
}

@Override
public String getDecisionId() {
public Expression getDecisionId() {
return decisionId;
}

@Override
public void setDecisionId(final String decisionId) {
public void setDecisionId(final Expression decisionId) {
this.decisionId = decisionId;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@
*/
package io.camunda.zeebe.engine.processing.deployment.model.element;

import io.camunda.zeebe.el.Expression;

/** A representation of an element that calls a decision. For example, a business rule task. */
public interface ExecutableCalledDecision {

String getDecisionId();
Expression getDecisionId();

void setDecisionId(String decisionId);
void setDecisionId(Expression decisionId);

String getResultVariable();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,6 @@ public void transform(final BusinessRuleTask element, final TransformContext con
taskHeadersTransformer.transform(executableTask, taskHeaders, element);

final var calledDecision = element.getSingleExtensionElement(ZeebeCalledDecision.class);
calledDecisionTransformer.transform(executableTask, calledDecision);
calledDecisionTransformer.transform(executableTask, context, calledDecision);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,25 @@
package io.camunda.zeebe.engine.processing.deployment.model.transformer.zeebe;

import io.camunda.zeebe.engine.processing.deployment.model.element.ExecutableCalledDecision;
import io.camunda.zeebe.engine.processing.deployment.model.transformation.TransformContext;
import io.camunda.zeebe.model.bpmn.instance.zeebe.ZeebeCalledDecision;

public final class CalledDecisionTransformer {

public void transform(
final ExecutableCalledDecision executableElement, final ZeebeCalledDecision calledDecision) {
final ExecutableCalledDecision executableElement,
final TransformContext context,
final ZeebeCalledDecision calledDecision) {

if (calledDecision == null) {
return;
}

final var decisionId = calledDecision.getDecisionId();
executableElement.setDecisionId(decisionId);
final var expressionLanguage = context.getExpressionLanguage();

final var decisionIdExpression =
expressionLanguage.parseExpression(calledDecision.getDecisionId());
executableElement.setDecisionId(decisionIdExpression);

final var resultVariable = calledDecision.getResultVariable();
executableElement.setResultVariable(resultVariable);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -334,4 +334,38 @@ public void shouldWriteDecisionEvaluationEvent() {
});
});
}

@Test
public void shouldCallDecisionWithDecisionIdExpression() {
// given
ENGINE
.deployment()
.withXmlClasspathResource(DMN_RESOURCE)
.withXmlResource(
processWithBusinessRuleTask(
t ->
t.zeebeCalledDecisionIdExpression("decisionIdVariable")
.zeebeResultVariable(RESULT_VARIABLE)))
.deploy();

// when
final long processInstanceKey =
ENGINE
.processInstance()
.ofBpmnProcessId(PROCESS_ID)
.withVariables(
Map.ofEntries(
Map.entry("decisionIdVariable", "jedi_or_sith"),
Map.entry("lightsaberColor", "blue")))
.create();

// then
Assertions.assertThat(
RecordingExporter.variableRecords(VariableIntent.CREATED)
.withProcessInstanceKey(processInstanceKey)
.withName(RESULT_VARIABLE)
.exists())
.as("Decision is evaluated successfully")
.isTrue();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public class BusinessRuleTaskIncidentTest {

private static final String DMN_RESOURCE = "/dmn/drg-force-user.dmn";
private static final String DECISION_ID = "jedi_or_sith";
private static final String DECISION_ID_VARIABLE = "decisionIdVariable";
private static final String RESULT_VARIABLE = "result";

@Rule public final EngineRule engine = EngineRule.singlePartition();
Expand Down Expand Up @@ -131,6 +132,39 @@ public void shouldCreateIncidentIfDecisionEvaluationFailed() {
""");
}

@Test
public void shouldCreateIncidentIfDecisionIdExpressionEvaluationFailed() {
// given
engine
.deployment()
.withXmlClasspathResource(DMN_RESOURCE)
.withXmlResource(
processWithBusinessRuleTask(
b ->
b.zeebeCalledDecisionIdExpression(DECISION_ID_VARIABLE)
.zeebeResultVariable(RESULT_VARIABLE)))
.deploy();

// when
final long processInstanceKey = engine.processInstance().ofBpmnProcessId(PROCESS_ID).create();

final var taskActivating =
RecordingExporter.processInstanceRecords(ProcessInstanceIntent.ELEMENT_ACTIVATING)
.withProcessInstanceKey(processInstanceKey)
.withElementId(TASK_ELEMENT_ID)
.withElementType(BpmnElementType.BUSINESS_RULE_TASK)
.getFirst();

// then
assertIncidentCreated(processInstanceKey, taskActivating.getKey())
.hasErrorType(ErrorType.EXTRACT_VALUE_ERROR)
.hasErrorMessage(
"""
failed to evaluate expression 'decisionIdVariable': \
no variable found for name 'decisionIdVariable'\
""");
}

@Test
public void shouldResolveIncidentAndCreateNewIncidentWhenContinuationFails() {
// given
Expand Down Expand Up @@ -273,4 +307,62 @@ public void shouldResolveIncidentAfterDecisionEvaluationFailed() {
.describedAs("business rule task is successfully completed")
.isTrue();
}

@Test
public void shouldResolveIncidentAfterDecisionIdExpressionEvaluationFailed() {
// given
engine
.deployment()
.withXmlClasspathResource(DMN_RESOURCE)
.withXmlResource(
processWithBusinessRuleTask(
b ->
b.zeebeCalledDecisionIdExpression(DECISION_ID_VARIABLE)
.zeebeResultVariable(RESULT_VARIABLE)))
.deploy();

final long processInstanceKey =
engine
.processInstance()
.ofBpmnProcessId(PROCESS_ID)
.withVariables(Maps.of(entry("lightsaberColor", "blue")))
.create();

final var incidentCreated =
RecordingExporter.incidentRecords(IncidentIntent.CREATED)
.withProcessInstanceKey(processInstanceKey)
.getFirst();

// when

// ... update state to resolve issue
engine
.variables()
.ofScope(incidentCreated.getValue().getElementInstanceKey())
.withDocument(Maps.of(entry(DECISION_ID_VARIABLE, DECISION_ID)))
.update();

// ... resolve incident
engine.incident().ofInstance(processInstanceKey).withKey(incidentCreated.getKey()).resolve();

// then
assertThat(
RecordingExporter.records()
.limitToProcessInstance(processInstanceKey)
.incidentRecords()
.onlyEvents())
.extracting(Record::getKey, Record::getIntent)
.describedAs("created incident is resolved and no new incident is created")
.containsExactly(
tuple(incidentCreated.getKey(), IncidentIntent.CREATED),
tuple(incidentCreated.getKey(), IncidentIntent.RESOLVED));

assertThat(
RecordingExporter.processInstanceRecords(ProcessInstanceIntent.ELEMENT_COMPLETED)
.withProcessInstanceKey(processInstanceKey)
.withElementId(TASK_ELEMENT_ID)
.exists())
.describedAs("business rule task is successfully completed")
.isTrue();
}
}

0 comments on commit dfd0c9b

Please sign in to comment.