Skip to content

Commit

Permalink
[merge]555e1a83
Browse files Browse the repository at this point in the history
[feature] automatic conversion of incident to bpmn error
PRD-164974
  • Loading branch information
minakh1993 committed Jul 1, 2023
1 parent 7a58990 commit ee8d0bf
Show file tree
Hide file tree
Showing 17 changed files with 723 additions and 67 deletions.
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,44 @@ provided apis as far is as below:
> VersionApi
other apis can be added easily via [CamundaServiceConfig](./tosan-camunda-client-spring-boot-starter/src/main/java/com/tosan/camunda/camundaclient/CamundaServiceConfig.java)
### changing to bpmn error instead of incident automatically
sometimes it's needed to raise bpmn error after retry count of a repeatable exception is passed. in normal
scenario when retry count reach to 0 incident is created but in some cases this behaviour might not be intended.
for changing this behaviour you can use camunda client exclusive annotation instead of ExternalTaskSubscription.
```
@Component
@CamundaClientExternalTaskSubscription(topicName = "incidentServiceTask",
processDefinitionKey = "SimpleProcess",
includeExtensionProperties = true,
changeIncidentToBpmnError = true)
public class TestCamundaClientWorker implements ExternalWorker {
@Override
public Worker getWorker() {
return WorkerType.SIMPLE_WORKER;
}
/**
* be careful when changeIncidentToBpmnError is enabled you must define boundary error event for the task.
*
* @param externalTask
* @param externalTaskService
*/
@Override
public void execute(ExternalTask externalTask, ExternalTaskService externalTaskService) {
try {
if (true) {
throw new CamundaClientTestRuntimeException();
}
} catch (CamundaClientTestException e) {
externalTaskService.handleBpmnError(externalTask, e.getClass().getSimpleName(), e.getMessage(), variables);
}
}
}
```
when this flag (changeIncidentToBpmnError) is set to true, after retry count reach to 0 handleBpmnError will be called.
so you have to be careful if this flag is present and have true value you must define error boundary event for your task.
the default value of this flag is false and after retry count reach to zero, incident will be created.
non-repeatable errors will result into incident with presence or absence of this flag so that non predicated errors get detected.

### Sample Project

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,8 @@ public class Constants {
* worker variable name in MDC
*/
public static final String WORKER_MDC_FIELD_NAME = "worker";

/**
* error code for times when incident change into bpmn error
*/
public static final String INCIDENT_TO_BPMN_ERROR_CODE = "incidentToBpmnError";
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import com.tosan.camunda.camundaclient.external.aspect.ExternalTaskMdcAspect;
import com.tosan.camunda.camundaclient.external.aspect.ExternalTaskResultAspect;
import com.tosan.camunda.camundaclient.feign.aspect.FeignUndeclaredThrowableExceptionAspect;
import com.tosan.camunda.camundaclient.util.ExternalTaskResultUtil;
import com.tosan.client.http.core.HttpClientProperties;
import com.tosan.client.http.starter.configuration.AbstractFeignConfiguration;
import com.tosan.client.http.starter.impl.feign.CustomErrorDecoder;
Expand Down Expand Up @@ -227,8 +228,13 @@ public ExternalTaskLogAspect externalTaskLogAspect() {
}

@Bean
public ExternalTaskResultAspect externalTaskResultAspect(CamundaClientConfig camundaClientConfig) {
return new ExternalTaskResultAspect(camundaClientConfig);
public ExternalTaskResultAspect externalTaskResultAspect(ExternalTaskResultUtil externalTaskResultUtil) {
return new ExternalTaskResultAspect(externalTaskResultUtil);
}

@Bean
public ExternalTaskResultUtil externalTaskResultUtil(CamundaClientConfig camundaClientConfig) {
return new ExternalTaskResultUtil(camundaClientConfig.getRetry());
}

@Bean
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package com.tosan.camunda.camundaclient.config;

import org.camunda.bpm.client.spring.SpringTopicSubscription;
import org.camunda.bpm.client.spring.annotation.ExternalTaskSubscription;
import org.springframework.core.annotation.AliasFor;

import java.lang.annotation.*;

/**
* @author M.khoshnevisan
* @since 6/26/2023
*/
@Documented
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@ExternalTaskSubscription
public @interface CamundaClientExternalTaskSubscription {

String STRING_NULL_VALUE = "$null$";
long LONG_NULL_VALUE = Long.MIN_VALUE;
/**
* @return autoOpen <ul>
* <li>{@code true}: the client immediately starts to fetch for External Tasks
* <li>{@code false}: topic subscription can be opened after application start by calling
* {@link SpringTopicSubscription#open()}
*/
@AliasFor(annotation = ExternalTaskSubscription.class)
boolean autoOpen() default true;

/**
* Alias for {@link #value()}.
*
* @return topicName of the Service Task in the BPMN process model the client subscribes to
*/
@AliasFor(annotation = ExternalTaskSubscription.class)
String topicName() default STRING_NULL_VALUE;

/**
* Alias for {@link #topicName()}.
*
* @return topicName of the Service Task in the BPMN process model the client subscribes to
*/
@AliasFor(annotation = ExternalTaskSubscription.class)
String value() default STRING_NULL_VALUE;

/**
* @return lockDuration <ul>
* <li> in milliseconds to lock the external tasks
* <li> must be greater than zero
* <li> the default lock duration is 20 seconds (20,000 milliseconds)
* <li> overrides the lock duration configured on bootstrapping the client
* </ul>
*/
@AliasFor(annotation = ExternalTaskSubscription.class)
long lockDuration() default LONG_NULL_VALUE;

/**
* @return variableNames of variables which are supposed to be retrieved. All variables are
* retrieved by default.
*/
@AliasFor(annotation = ExternalTaskSubscription.class)
String[] variableNames() default {STRING_NULL_VALUE};

/**
* @return localVariables
* whether or not variables from greater scope than the external task
* should be fetched. <code>false</code> means all variables visible
* in the scope of the external task will be fetched,
* <code>true</code> means only local variables (to the scope of the
* external task) will be fetched
*/
@AliasFor(annotation = ExternalTaskSubscription.class)
boolean localVariables() default false;

/**
* @return businessKey to filter for external tasks that are supposed to be fetched and locked
*/
@AliasFor(annotation = ExternalTaskSubscription.class)
String businessKey() default STRING_NULL_VALUE;

/**
* @return processDefinitionId to filter for external tasks that are supposed to be fetched and
* locked
*/
@AliasFor(annotation = ExternalTaskSubscription.class)
String processDefinitionId() default STRING_NULL_VALUE;

/**
* @return processDefinitionIds to filter for external tasks that are supposed to be fetched and
* locked
*/
@AliasFor(annotation = ExternalTaskSubscription.class)
String[] processDefinitionIdIn() default {STRING_NULL_VALUE};

/**
* @return processDefinitionKey to filter for external tasks that are supposed to be fetched and
* locked
*/
@AliasFor(annotation = ExternalTaskSubscription.class)
String processDefinitionKey() default STRING_NULL_VALUE;

/**
* @return processDefinitionKeyIn to filter for external tasks that are supposed to be fetched
* and locked
*/
@AliasFor(annotation = ExternalTaskSubscription.class)
String[] processDefinitionKeyIn() default {STRING_NULL_VALUE};

/**
* @return processDefinitionVersionTag to filter for external tasks that are supposed to be
* fetched and locked
*/
@AliasFor(annotation = ExternalTaskSubscription.class)
String processDefinitionVersionTag() default STRING_NULL_VALUE;

/**
* @return processVariables of which the external tasks to be retrieved are related to. Each
* value is instance of {@link ExternalTaskSubscription.ProcessVariable}
*/
@AliasFor(annotation = ExternalTaskSubscription.class)
ExternalTaskSubscription.ProcessVariable[] processVariables()
default {@ExternalTaskSubscription.ProcessVariable(name = STRING_NULL_VALUE, value = STRING_NULL_VALUE)};

/**
* @return Filter for external tasks without tenant
*/
@AliasFor(annotation = ExternalTaskSubscription.class)
boolean withoutTenantId() default false;

/**
* @return tenantIds to filter for external tasks that are supposed to be fetched and locked
*/
@AliasFor(annotation = ExternalTaskSubscription.class)
String[] tenantIdIn() default {STRING_NULL_VALUE};

/**
* @return includeExtensionProperties
* whether or not to include custom extension properties for fetched
* external tasks. <code>true</code> means all extensionProperties
* defined in the external task activity will be provided.
* <code>false</code> means custom extension properties are not
* available within the external-task-client. The default is
* <code>false</code>.
*/
@AliasFor(annotation = ExternalTaskSubscription.class)
boolean includeExtensionProperties() default false;

/**
* @return changeIncidentToBpmnError
* whether incident change into bpmnError after retry times passed
* this option work for repeatable exceptions
*/
boolean changeIncidentToBpmnError() default false;
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,92 +2,59 @@

import com.tosan.camunda.api.CamundaClientRuntimeIncident;
import com.tosan.camunda.api.ExceptionIncidentState;
import com.tosan.camunda.camundaclient.config.CamundaClientConfig;
import com.tosan.camunda.camundaclient.config.ExternalTaskInfo;
import com.tosan.camunda.camundaclient.config.RetryConfig;
import com.tosan.camunda.camundaclient.config.CamundaClientExternalTaskSubscription;
import com.tosan.camunda.camundaclient.util.ExternalTaskResultUtil;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.camunda.bpm.client.task.ExternalTask;
import org.camunda.bpm.client.task.ExternalTaskService;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Objects;

import static com.tosan.camunda.camundaclient.external.ExternalTaskService.getTaskInfo;

/**
* @author M.khoshnevisan
* @since 11/1/2021
*/
@Aspect
@Order(40)
@Slf4j
@AllArgsConstructor
public class ExternalTaskResultAspect extends ExternalTaskBaseAspect {

private final RetryConfig retryConfig;

public ExternalTaskResultAspect(CamundaClientConfig camundaClientConfig) {
this.retryConfig = camundaClientConfig.getRetry();
}
private final ExternalTaskResultUtil externalTaskResultUtil;

@Around(value = "externalTaskHandler()")
public Object sendResults(ProceedingJoinPoint pjp) throws Throwable {
boolean convertToBpmnError = checkConvertToBpmnErrorInCaseOfIncident(pjp);
try {
Object proceed = pjp.proceed();
declareTaskCompleted(pjp);
externalTaskResultUtil.declareTaskCompleted(pjp.getArgs());
return proceed;
} catch (Exception e) {
if (e instanceof CamundaClientRuntimeIncident) {
CamundaClientRuntimeIncident camundaClientRuntimeIncident = (CamundaClientRuntimeIncident) e;
handleException(camundaClientRuntimeIncident.getExceptionIncidentState(), e, pjp);
CamundaClientRuntimeIncident runtimeIncident = (CamundaClientRuntimeIncident) e;
externalTaskResultUtil.handleException(runtimeIncident.getExceptionIncidentState(), e, pjp.getArgs(), convertToBpmnError);
} else {
handleException(ExceptionIncidentState.NON_REPEATABLE, e, pjp);
externalTaskResultUtil.handleException(ExceptionIncidentState.NON_REPEATABLE, e, pjp.getArgs(), convertToBpmnError);
}
throw e;
}
}

private void declareTaskCompleted(ProceedingJoinPoint pjp) {
Object[] args = pjp.getArgs();
ExternalTask externalTask = (ExternalTask) args[0];
ExternalTaskService externalTaskService = (ExternalTaskService) args[1];
ExternalTaskInfo taskInfo = getTaskInfo(externalTask);
externalTaskService.complete(externalTask, taskInfo.getVariables());
}

private void handleException(ExceptionIncidentState exceptionIncidentState,
Exception e, ProceedingJoinPoint pjp) {

Object[] args = pjp.getArgs();
ExternalTask externalTask = (ExternalTask) args[0];
ExternalTaskService externalTaskService = (ExternalTaskService) args[1];
ExternalTaskInfo taskInfo = getTaskInfo(externalTask);
int retryCount = exceptionIncidentState.equals(ExceptionIncidentState.NON_REPEATABLE) ? 0 :
calculateRetries(externalTask.getRetries());
int retryInterval = (retryCount == 0 ? 0 : retryConfig.getRetryInterval());
log.info("determined retry count:{} with retry interval{}", retryCount, retryInterval);
externalTaskService.handleFailure(externalTask.getId(), e.getMessage(), getStackTrace(e),
retryCount, retryInterval, taskInfo.getVariables(), null);
}

private int calculateRetries(Integer retries) {
if (retries == null) {
return retryConfig.getRetryCount();
}
return retries <= 0 ? retries : retries - 1;
}

private String getStackTrace(final Throwable throwable) {
if (Objects.nonNull(throwable.getCause())) {
return this.getStackTrace(throwable.getCause());
private boolean checkConvertToBpmnErrorInCaseOfIncident(ProceedingJoinPoint pjp) {
try {
MethodSignature signature = (MethodSignature) pjp.getSignature();
Class declaringType = signature.getDeclaringType();
CamundaClientExternalTaskSubscription externalTaskSubscription = (CamundaClientExternalTaskSubscription)
declaringType.getAnnotation(CamundaClientExternalTaskSubscription.class);
if (externalTaskSubscription == null) {
return false;
}
return externalTaskSubscription.changeIncidentToBpmnError();
} catch (Exception e) {
log.error("error on extracting bpmn error to incident", e);
return false;
}
StringWriter stackTrace = new StringWriter();
PrintWriter printWriter = new PrintWriter(stackTrace, true);
throwable.printStackTrace(printWriter);
return stackTrace.getBuffer().toString();
}
}
Loading

0 comments on commit ee8d0bf

Please sign in to comment.