Tareas externas de Camunda: una herramienta poderosa para crear aplicaciones con una arquitectura resistente y escalable

imagen

En Tinkoff, utilizamos el marco Camunda + Spring para desarrollar sistemas de automatización de procesos comerciales . Describimos los procesos empresariales utilizando BPMN (notación de gestión de procesos empresariales) en forma de diagramas de flujo.

El elemento más utilizado en nuestros diagramas son las tareas de servicio (rectángulo de engranaje). Camunda admite dos formas de realizar tareas de servicio :

  1. Usando una llamada síncrona a código java.
  2. Crear una tarea externa.

El segundo método le permite realizar tareas utilizando sistemas externos, por ejemplo, si necesita llamar a una aplicación de camunda desde otra o incluso delegar el trabajo a cualquier sistema externo.

imagen
Ejemplo de diagrama BPMN

Esto es útil cuando tiene la intención de reutilizar la lógica en múltiples aplicaciones. O cuando quiere apegarse a la arquitectura de microservicios. Por ejemplo, separando los servicios que se ocupan de los procesos comerciales de los servicios que implementan tareas técnicas, como la generación de informes o el envío de correos.

Junto con esta característica, una tarea externa proporciona una arquitectura escalable y resistente. Para comprender por qué sucede esto, primero debe comprender cómo funciona la tarea externa en los niveles BPMN y de aplicación.

Tarea externa en BPMN


La tarea externa implica la creación de una tarea que puede realizar un controlador externo. La esencia del patrón de tareas externas es que:

  1. , «» , «».
  2. camunda , , .
  3. camunda (/).

En el diagrama anterior, describí un proceso ficticio en el que queremos obtener una lista de usuarios, enviarles un anuncio y después de 2 horas calcular el número de aplicaciones después del boletín de marketing. Y, si hay más de 10 aplicaciones, aumente la selección para el próximo envío.

Quiero que mi aplicación camunda sea responsable solo de los procesos comerciales, y cualquier otra aplicación debería ser responsable del correo electrónico. En este caso, el patrón de tareas externas funciona bien para mí . En mi proceso, simplemente crearé una tarea de boletín electrónico y esperaré a que algún controlador externo la complete.

Para crear una tarea externa en el diagrama , debe:

  1. Crea una tarea regular .
  2. Cambie su tipo a tarea de servicio .
  3. Establezca la implementación en externa .
  4. Especifique el valor del campo Tema .

imagen

Tema es el nombre de la cola en la que se agregarán tareas de un tipo y a las que se suscribirá un controlador externo.

Ahora que hay una tarea externa en el proceso , puede iniciarla, pero no se ejecutará, ya que nadie la está procesando.

Trabajador externo de tareas


El patrón de tareas externas es bueno, ya que le permite implementar el procesamiento de tareas en cualquier idioma, utilizando cualquier herramienta que pueda realizar solicitudes HTTP .

El siguiente es un ejemplo del blog de camunda . El ejemplo implementa un controlador de JavaScript externo que, cada 20 segundos, solicita una lista de tareas de procesamiento de camunda . Si hay tareas, las envía y notifica a camunda sobre la finalización de la tarea.

const baseUrl = 'http://localhost:8080/my-app/rest';
const workerSettings = {
 workerId: 'worker01', // some unique name for the current worker instance
 maxTasks: 5,
 topics: [
   {
     topicName: 'sendEmail',
     lockDuration: 10000, // How much time the worker thinks he needs to process the task
     variables: ['video'] // Which variables should be returned in the response (to avoid additional REST calls to read data)
   }]};
const requestParams = {method: 'POST', headers: {contentType: 'application/json'}};

function pollExternalTasks() {
 return fetch(`${baseUrl}/external-task/fetchAndLock`, {
   ...requestParams,
   body: JSON.stringify(workerSettings)
 })
}

function processExternalTask(result = []) {
 return Promise.all(result.map(externalTask => {
   sendEmail(externalTask); // Here the actual work would be done

   return fetch(`${baseUrl}/external-task/${externalTask.id}/complete`, {
     ...requestParams,
     body: JSON.stringify({workerId: workerSettings.workerId}),
   })
 }));
}

setInterval(() => {
 pollExternalTasks().then(processExternalTask)
}, 20000);

Como puede ver en el código anterior, los métodos clave para manejar tareas externas son fetchAndLock y complete . El primer método solicita una lista de tareas y asegura su implementación, y el segundo informa sobre la finalización de la tarea. Además de estos dos métodos, existen otros, puede leer sobre ellos en la documentación oficial .

Cliente de tareas externas de Camunda


imagen

Para implementar el procesamiento de tareas externas , camunda proporcionó clientes Javascript y Java que le permiten crear manejadores de tareas externos en solo unas pocas líneas. También hay una guía detallada que describe los principios básicos del procesamiento de tareas externas, nuevamente con ejemplos en Javascript y Java .

Un ejemplo de implementación de un controlador externo usando ExternalTaskClient :

public class App {
   public static void main(String... args) {
       // bootstrap the client
       ExternalTaskClient client = ExternalTaskClient.create()
           .baseUrl("http://localhost:8080/engine-rest")
           .asyncResponseTimeout(1000)
           .build();

       // subscribe to the topic
       client.subscribe("sendEmail").handler((externalTask, externalTaskService) -> {
           try {
               String result = sendEmail(externalTask)
               Map<String, Object> variables = new HashMap<>();

               variables.put("result", result);
               externalTaskService.complete(externalTask, variables);
               System.out.println("The External Task " + externalTask.getId() + " has been completed!");
           } catch (e: Exception) {
               externalTaskService.handleFailure(externalTask, e.message, e.stackTrace.toString())
           }
       }).open();
   }
}

Si su tarea requiere no solo realizar alguna acción sincrónica, sino iniciar todo el proceso, puede hacerlo, por ejemplo, iniciando el proceso a través de RuntimeService :

@Service
class EmailWorker(
   private val runtimeService: RuntimeService
) {
   val builder = ExternalTaskClientBuilderImpl().baseUrl("http://localhost:8080").workerId("myWorker")
   val taskClient = builder.build()
   val engineClient = (builder as ExternalTaskClientBuilderImpl).engineClient

   @PostConstruct
   fun init() {
       taskClient
           .subscribe("sendEmail")
           .lockDuration(10000)
           .handler { externalTask, externalService ->
               runtimeService.startProcessInstanceByKey(
                   "SendEmailProcess",
                   externalTask.getVariable("emailId"),
                   mapOf(
                       "text" to externalTask.getVariable("text"),
                       "email" to externalTask.getVariable("email")
                   )
               )
           }
           .open()
   }


   @PreDestroy
   fun destroy() {
       taskClient.stop()
   }
}

// Delegate from SendEmailProcess process
@Component
class EmailResultDelegate(private val emailWorker: EmailWorker) {
   fun doExecute(execution: DelegateExecution) {
       emailWorker.engineClient.complete(
           execution.readVar(EXTERNAL_TASK_ID),
           mapOf("result" to "Success")
       )
   }
}

En este ejemplo, el manejador de tareas externas ( EmailWorker ), cuando se recibe la tarea, inicia el proceso SendEmailProcess .

Imagine que este proceso realiza algunas acciones necesarias para enviar un boletín informativo y al final llama a EmailResultDelegate , que, a su vez, completa la tarea externa .

Beneficios arquitectónicos de la tarea externa


Vale la pena señalar que hay una manera de iniciar el proceso en otra aplicación camunda de una manera más simple: POST: / rest / process-definition / key / $ {id} / start

Cuando usa REST , no tiene ninguna garantía de transacción. Pero después de todo, también trabajamos con tareas externas a través de REST , ¿cuál es la diferencia entonces?

La diferencia es que no llamamos al servicio externo directamente, sino que solo publicamos tareas que pueden procesarse. Considere un ejemplo:

imagen

algún controlador externo toma una tarea que ahora se le asigna, pero cuando se recibe una respuesta, la conexión se desconecta. Ahora del lado de camundauna tarea que no se procesará se bloquea porque el controlador externo no recibió una respuesta. Pero esto no da miedo: en camunda para tareas externas hay un tiempo de espera por el cual la tarea volverá a la cola nuevamente y alguien más puede manejarla.

Ahora veamos el caso en que un controlador externo recibió una tarea, la completó y llamó al método completo , que falló debido a problemas de red. Ahora no podrá comprender si la tarea se completó con éxito en camunda o no. Puede intentarlo nuevamente, pero existe la posibilidad de que los problemas de red continúen.

En este caso, la mejor solución sería ignorar el problema. Si la tarea se completó con éxito, entonces todo está en orden. De lo contrario, después del tiempo de espera, la tarea volverá a estar disponible para su procesamiento. Pero esto significa que su controlador externo debe ser idempotente o contener lógica para deduplicar tareas.

Puede ocurrir un problema similar al iniciar un nuevo proceso, por lo que debe verificar las instancias existentes con los mismos datos, por ejemplo, businessKey .

Además de la tarea externa de alta tolerancia a fallasle permite escalar fácilmente controladores externos e implementarlos en cualquier lenguaje de programación. Al mismo tiempo, el patrón ayuda a implementar microservicios para que se influyan mutuamente lo menos posible, lo que aumenta su estabilidad.

Más sobre la tarea externa :
https://docs.camunda.org/manual/latest/user-guide/process-engine/external-tasks/
https://docs.camunda.org/manual/latest/reference/rest/external -task /
https://docs.camunda.org/manual/latest/user-guide/ext-client/

All Articles