Tarefas externas do Camunda - uma ferramenta poderosa para criar aplicativos com arquitetura resiliente e escalável

imagem

Em Tinkoff, usamos a estrutura Camunda + Spring para desenvolver sistemas de automação de processos de negócios . Descrevemos processos de negócios usando o BPMN (Business Process Management Notation) na forma de fluxogramas.

O elemento mais usado em nossos diagramas são as tarefas de serviço (retângulo de engrenagem). Camunda suporta duas maneiras de executar tarefas de serviço :

  1. Usando uma chamada síncrona para código java.
  2. Criando uma tarefa externa.

O segundo método permite executar tarefas usando sistemas externos - por exemplo, se você precisar chamar um aplicativo camunda de outro ou mesmo delegar trabalho a qualquer sistema externo.

imagem
Exemplo de diagrama do BPMN

Isso é útil quando você pretende reutilizar a lógica em vários aplicativos. Ou quando você deseja manter a arquitetura de microsserviço. Por exemplo, separando serviços que lidam com processos de negócios de serviços que implementam tarefas técnicas, como gerar relatórios ou correspondência.

Juntamente com esse recurso, uma tarefa externa fornece uma arquitetura escalável e resiliente. Para entender por que isso acontece, primeiro você precisa entender como a tarefa externa funciona nos níveis de aplicativo e BPMN .

Tarefa externa no BPMN


Tarefa externa envolve a criação de uma tarefa que pode ser executada por um manipulador externo. A essência do padrão de tarefa externa é que:

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

No diagrama acima, descrevi um processo fictício no qual queremos obter uma lista de usuários, enviá-los um anúncio e após 2 horas calcular o número de aplicativos após o boletim de marketing. E, se houver mais de 10 aplicativos, aumente a seleção para a próxima correspondência.

Quero que meu aplicativo camunda seja responsável apenas pelos processos de negócios e qualquer outro aplicativo deve ser responsável por enviar e-mails. Nesse caso, o padrão de tarefa externa funciona bem para mim . No meu processo, vou simplesmente criar uma tarefa de boletim informativo por email e aguardar que ela seja concluída por algum manipulador externo.

Para criar uma tarefa externa no diagrama , você deve:

  1. Crie uma tarefa regular .
  2. Altere seu tipo para tarefa de serviço .
  3. Defina a implementação como externa .
  4. Especifique o valor do campo Tópico .

imagem

Tópico é o nome da fila na qual as tarefas de um tipo serão adicionadas e às quais um manipulador externo se inscreverá.

Agora que existe uma tarefa externa no processo , você pode iniciá-la, mas ela não será executada, pois ninguém a está processando.

Trabalhador de tarefas externas


O padrão de tarefa externa é bom, pois permite implementar o processamento de tarefas em qualquer idioma, usando qualquer ferramenta que possa executar solicitações HTTP .

A seguir, um exemplo do blog camunda . O exemplo implementa um manipulador javascript externo que, a cada 20 segundos, solicita uma lista de tarefas de processamento da camunda . Se houver tarefas, ele as envia e notifica a camunda sobre a conclusão da tarefa.

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 você pode ver no código acima, os principais métodos para lidar com tarefas externas são fetchAndLock e completos . O primeiro método solicita uma lista de tarefas e protege sua implementação, e o segundo informa sobre a conclusão da tarefa. Além desses dois métodos, existem outros, você pode ler sobre eles na documentação oficial .

Cliente de tarefa externa Camunda


imagem

Para implementar o processamento de tarefas externas , a camunda forneceu clientes Javascript e Java que permitem criar manipuladores de tarefas externos em apenas algumas linhas. Há também um guia detalhado que descreve os princípios básicos do processamento de tarefas externas - novamente com exemplos em Javascript e Java .

Um exemplo de implementação de um manipulador 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();
   }
}

Se sua tarefa exigir não apenas executar alguma ação síncrona, mas iniciar todo o processo, você poderá fazê-lo, por exemplo, iniciando o processo por meio do 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")
       )
   }
}

Neste exemplo, o manipulador de tarefas externo ( EmailWorker ), quando a tarefa é recebida, inicia o processo SendEmailProcess .

Imagine que esse processo execute algumas ações necessárias para enviar um boletim informativo e, no final, chame EmailResultDelegate , que, por sua vez, completa a tarefa externa .

Benefícios arquitetônicos da tarefa externa


Vale ressaltar que existe uma maneira de iniciar o processo em outro aplicativo camunda de uma maneira mais simples: POST: / rest / definição do processo / chave / $ {id} / start

Quando você usa o REST , você não tem nenhuma garantia de transação. Afinal, também trabalhamos com tarefas externas por meio do REST , qual é a diferença então?

A diferença é que não chamamos o serviço externo diretamente, mas apenas publicamos tarefas que podem ser processadas. Considere um exemplo:

imagem

algum manipulador externo executa uma tarefa que agora está atribuída a ele, mas quando uma resposta é recebida, a conexão é desconectada. Agora do lado da camundauma tarefa que não será processada é bloqueada porque o manipulador externo não recebeu uma resposta. Mas isso não é assustador: na camunda para tarefas externas, existe um tempo limite pelo qual a tarefa retorna à fila novamente e outra pessoa pode lidar com isso.

Agora, vejamos o caso em que um manipulador externo recebeu uma tarefa, a concluiu e chamou o método complete , que falhou devido a problemas de rede. Agora você não será capaz de entender se a tarefa foi concluída com êxito em camunda ou não. Você pode tentar novamente, mas há uma chance de os problemas de rede continuarem.

Nesse caso, a melhor solução seria ignorar o problema. Se a tarefa foi concluída com êxito, tudo está em ordem. Caso contrário, após o tempo limite, a tarefa estará novamente disponível para processamento. Mas isso significa que seu manipulador externo deve ser idempotente ou conter lógica para desduplicar tarefas.

Um problema semelhante pode ocorrer ao iniciar um novo processo, portanto, você deve verificar as instâncias existentes com os mesmos dados, por exemplo, businessKey .

Além da tarefa externa de alta tolerância a falhaspermite escalar facilmente manipuladores externos e implementá-los em qualquer linguagem de programação. Ao mesmo tempo, o padrão ajuda a implementar microsserviços para que eles se influenciem o menos possível, aumentando assim sua estabilidade.

Mais sobre tarefas externas :
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