Привет! Я Вероника из Clevertec, занимаюсь бэкендом на банковском проекте. Этот текст написан из желания помочь разработчикам, которым только предстоит познакомиться с Camunda. Что это, для чего, как работает и почему восьмая версия совсем не похожа на предыдущую? Делюсь своим опытом, добытым путём ошибок.

Как я не подружилась с Camunda на старте

Сейчас я работаю на проекте, где Camunda активно используют. Перед стартом проекта о платформе я ничего не слышала, поэтому посмотрела видео про Camunda 7 и, посчитав свои знания исчерпывающими, радостно окунулась в разработку. Каково же было моё удивление, когда оказалось, что Camunda 7 не равно Camunda 8.

В чем главное различие? 8 версия полностью основана на Zeebe. Это абсолютно другая архитектура, в ней нет PostgreSQL и нет зависимости от реляционной базы данных. Также 8 версия разворачивается отдельно, а не встраивается зависимостью в SpringBoot. Кроме того, в новой версии исключили значительное количество элементов. Но они потихоньку возвращаются.

Со всем этим мне пришлось разобраться и подружиться. А потом захотелось поделиться здесь инструкцией для тех, кто ещё на старте этого пути. Я пишу на java, поэтому и примеры у меня будут на java.

Идём в основы и разбираемся с BPMN

Для начала я вам рекомендую подружиться с сайтом официальной документации. Не могу сказать, что он юзер-френдли, но пользоваться им придется часто.

Хотя в документации настоятельно советует пользоваться Kubernetes с Minikube или KIND для разработки на локальной машине, я предпочитаю пользоваться Docker. Делюсь ссылкой на гит. В корне проекта есть докер-компоуз, чтобы вы смогли локально развернуть у себя Camunda.

Что такое Camunda вообще? Camunda Platform 8 управляет сложными бизнес-процессами, которые охватывают людей, системы и устройства. С помощью Camunda бизнес-пользователи сотрудничают с разработчиками для моделирования и автоматизации сквозных процессов с использованием блок-схем на основе BPMN, а также таблиц решений DMN, которые способствуют скорости, масштабированию и логике принятия решений. Замечу, что Camunda 8 позиционируется себя как универсальный оркестратор процессов. Если простым языком, то это BPMN-диаграмма, положенная на ваш java-код. 

Немного подробностей о BPMN-диаграмме. Business Process Model and Notation (нотация моделирования бизнес-процессов) – это совокупность блок-схем, с помощью которых отображаются бизнес-процессы. BPMN-диаграмма показывает, в какой последовательности совершаются рабочие действия и перемещаются потоки информации. BPMN-диаграмма облегчает жизнь всем членам команды, наглядно и просто отражая процессы, которые происходят в вашем проекте.

Мой совет – участвовать в разработке процессов BPMN совместно с аналитиком. Если этого не делать, то может получиться так:

Переходим к вечеру пятницы. Ставим задачу


Для понимания, как переложить BPMN-диаграмму на java-код, давайте отрисуем небольшой процесс. Для этого вам понадобится Camunda Modeler, который можно скачать здесь.

Постановка задачи: наступила пятница, необходимо принять решение, как провести вечер.

Каждый процесс должен иметь начало и конец. Как видите, в нашем процессе они также присутствуют. На проекте мы развернули отдельный микросервис, в котором происходит деплой bpmn-диаграмм. Для этого используется зависимость spring-zeebe-starter. Обратите внимание, что сейчас уже актуальна версия выше.

<dependency>
   <groupId>io.camunda</groupId>
   <artifactId>spring-zeebe-starter</artifactId>
   <version>8.0.7</version>
</dependency>

Также необходимо поместить аннотацию @EnableZeebeClient в Spring Boot Application. Кроме того, я указываю дополнительно аннотацию @ZeebeDeployment(resources = { "classpath*:/process/*.bpmn"}), в которой прописываю, где именно лежат диаграммы.

В application.yml следует сконфигурировать подключение к Zeebe broker. Для локальной разработки с помощью Docker это будет иметь вид:

zeebe.client:
 broker.gateway-address: localhost:26500
zeebe:
 client:
   security:
     plaintext: true

Но процесс необходимо стартовать. Вы можете достучаться до микросервиса посредством REST-запроса, передав необходимые параметры в теле запроса.

@RequiredArgsConstructor
@RestController
@RequestMapping("/start")
public class StartController {

    private final StartService startService;

    @PostMapping()
    @SneakyThrows
    public ProcessData startProcess(@RequestBody ProcessVariables request) {
        return startService.startProcess(request);
    }
}
@Data
public class ProcessVariables {

    /**
     * Количество денег в начале вечера пятницы
     */
    @NotNull
    @Min(value = 50, message = "sumOfMoney should not be less than 50")
    @Max(value = 150, message = "sumOfMoney should not be greater than 150")
    private Integer sumOfMoney;
}

Далее мы создаем новый инстанс процесса и передаем в него переменные, которые нам понадобятся для процесса. Что важно: bpmnProcessId соответствует Id процесса, которое указано на возможной диаграмме пятничного вечера. В variables я показываю, что в них можно добавить не только те, которые пришли из запроса, но и дополнить необходимыми. Например, вы сходите в базу данных и дополните uuid.

@Service
@RequiredArgsConstructor
@Slf4j
public class StartService {

    private static final String PROCESS_NAME = "FRIDAY_EVENING_PROCESS";

    private final ZeebeClient zeebe;

    public ProcessData startProcess(ProcessVariables request) {
        String uuid = UUID.randomUUID().toString();
        ProcessInstanceEvent instanceEvent = zeebe.newCreateInstanceCommand()
            .bpmnProcessId(PROCESS_NAME)
            .latestVersion()
            .variables(Map.of("sumOfMoney", request.getSumOfMoney(),
                "messageId", uuid))
            .send().join();

        return new ProcessData()
          .setProcessInstanceKey(instanceEvent.getProcessInstanceKey());
    }
}

Разберём типы задач

Camunda 8 реализует различные типы задач, но в основном вы будете пользоваться Сервисными задачами.

Нужно сопоставить ZeebeWorker, который будет реализовывать сервисную задачу.

@Component
@Slf4j
public class DecideHowSpendFridayNightTask {

    @ZeebeWorker(type = "decideHowSpendFridayNight", autoComplete = true)
    public ProcessVariables decideHowSpendFridayNight(ActivatedJob job) {
        log.info("works worker decideHowSpendFridayNight");
        ProcessVariables variables = job.getVariablesAsType(ProcessVariables.class);
        variables.setSumOfMoney(variables.getSumOfMoney() + 10);
        log.info("sumOfMoney after increase is {}", variables.getSumOfMoney());
        return variables;
    }
}

Как можно увидеть, типы совпадают, поэтому когда токен дойдет до задачи с типом “decideHowSpendFridayNight”, начнется выполнение кода. Обратите внимание, если вы используете autoComplete = true, нет необходимости завершать обработку задачи самостоятельно, интеграция с Spring сделает это за вас.

При помощи job.getVariablesAsType(), вы можете получить свой собственный класс, в котором сопоставляются переменные процесса.

Пара слов о шлюзах

В процессах удобно использовать различные шлюзы. Здесь я для понимания привела Эксклюзивные Шлюзы и Параллельный Шлюз.

Эксклюзивные Шлюзы (Условия) включаются в состав бизнес-процесса для разделения потока операций на несколько альтернативных маршрутов. Для нашего экземпляра процесса может быть выбран лишь один из предложенных маршрутов.

Параллельные шлюзы необходимы для объединения и создания параллельных маршрутов.

В нашем примере в зависимости от количества денег вы либо останетесь дома, либо пойдете в клуб. Причем, чтобы вы ни выбрали, параллельно с этим вы будете общаться с друзьями в мессенджере. Обратите внимание: к задаче “пойти в клуб” присоединено событие типа “ошибка” (error event).

Error в обязательном порядке обладает наименованием и кодом. При наступлении определенной ситуации, можно выбросить ZeebeBpmnError – и процесс пойдет по пути экстренного отъезда домой.

@Component
@Slf4j
public class GoToClubTask {

    @ZeebeWorker(type = "goToClub", autoComplete = true)
    public Contact goToClub() {
        log.info("works worker goToClub");
        //рандомно определяем количество выпитых шотов
        int numberOfShots = (int) (Math.random() * 15);
        if (numberOfShots > 8) {
            throw new ZeebeBpmnError(
              "TOO_MUCH_ALCOHOL", "you drank shots= " + numberOfShots);
        } else {
            //уходим с контактом HR о работе
            return new Contact().setTgContact("@hr_contact");
        }
    }
}

Если шотов было выпито более чем 8, выбрасывается ZeebeBpmnError, errorCode который совпадает с тем, который указан в диаграмме. 

Хочу отметить, что можно порождать новые переменные процесса, не только те, которые указаны на старте. Например в воркере с типом “goToClub”, который указан выше, при удачном стечении обстоятельств можно получить контакт HR, и tgContact станет переменной процесса, которую можно отловить в другой сервисной задаче. 

В сервисной задаче “Проснуться утром в субботу отдохнувшим и бодрым” я хочу отловить все переменные, которые существуют в процессе

@Component
@Slf4j
public class WakeUpOnSaturdayMorningTask {

    @ZeebeWorker(type = "wakeUpOnSaturdayMorning", autoComplete = true, 
                 forceFetchAllVariables = true)
    public void wakeUpOnSaturdayMorning(ActivatedJob job) {
        log.info("works worker wakeUpOnSaturdayMorning");
        AllVariables allVariables = job.getVariablesAsType(AllVariables.class);
        log.info("messageId {}, tgContact {}, sumOfMoney {}",
            allVariables.getMessageId(), allVariables.getTgContact(), 
                 allVariables.getSumOfMoney());
    }
}

Для этого мне необходимо прописать флаг forceFetchAllVariables=true – и все переменные процесса станут вам доступны. Как можно видеть, я собрала их в отдельный класс AllVariables.

Закончу описанием сообщения

В сервисной задаче “Периодически общаться с друзьями”, мы отправляем сообщение, которое отлавливаем в процессе и тем самым стартуем задачу “Принять приглашение встретиться с другом”.

Наше сообщение обведено пунктирным кругом. Это значит, что сообщение не прерывающееся. То есть, основной процесс не остановится, вы и примете приглашение, и продолжите находиться либо в клубе, либо дома с пиццей.

Посмотрим внимательнее, как отправить сообщение.

@Component
@RequiredArgsConstructor
@Slf4j
public class ChatWithFriendsPeriodicallyTask {
    private final ZeebeClient zeebe;

    @ZeebeWorker(type = "chatWithFriendsPeriodically", autoComplete = true)
    public void chatWithFriendsPeriodically(@ZeebeVariable String messageId) {
        log.info("works worker chatWithFriendsPeriodically");

        PublishMessageResponse message = zeebe.newPublishMessageCommand()
            .messageName("MEET_MESSAGE")
            .correlationKey(messageId)
            .send()
            .join();
        log.info("There were sent message with messageKey {} and correlationKey {}", 
                 message.getMessageKey(), messageId);
    }
}

Для этого в сервисной задаче необходимо опубликовать сообщение, передав туда наименование сообщения и ключ корреляции. Как видно, в нашем случае ключом корреляции выступает messageId. Наименование сообщения плюс его ключ корреляции составляют уникальность сообщения. Вы будете уверены, что такое сообщение вызовется только один раз.

Естественно, наименование сообщения и его ключ должны быть отражены не только в коде, но и на BPNM-диаграмме. 

Запустим процесс и отследим по логам, что происходит

Когда вы стартовали докер компоуз, на порту 8081 развернулся operate - ui, позволяющий мониторить и анализировать процессы. То есть в нем вы увидите диаграмму, которая была задеплоена. Также operate позволяет в реальном времени отслеживать, на каком шаге находится экземпляр процесса.

Мы видим, что процесс пошел по развилке “пойти в клуб”, ошибки выброшено не было, и в то же время мы успешно пообщались с друзьями и приняли приглашение на встречу.

В сервисных задачах я не писала логику, но на самом деле в них можно делать что угодно: ходить в базу данных, стучаться в сторонние микросервисы, получать сообщения из Kafka.

Как я использую Camunda в реальности?

Отойдём от метафорических примеров и пятничного вечера. Я работаю на банковском проекте, и там эту технологию используют очень широко. Например, мы получаем онлайн-согласия клиента, получаем данные из ЕСИА (Единая система идентификации и аутентификации), обновляем документы клиента и передаем запрашиваемые данные на госуслуги. В нашем случае это удобно и для бизнеса, и для разработчиков.

Нужна ли Camunda всем? Однозначно нет. 

Давайте обсудим ваш опыт использования в комментариях.

Комментарии (20)


  1. Horocek
    21.06.2024 21:51
    +4

    Я не понял для чего нужна комунда


    1. ververesk Автор
      21.06.2024 21:51

      мы как-то внутри команды собирали встречу, дабы решить, так в каких моментах таки использовать камунду, если кратко, то.
      много взаимодействий между микросервисами
      много шагов
      нет фронта
      Это очень краткая выжимка, возможно, напишу статью об этом


  1. TldrWiki
    21.06.2024 21:51

    Спасибо за статью. Camunda редкостная гадость потому что параллельно программной логике вносит дополнительный путь исполнения что затрудняет разработку и отладку. Уйдет в прошлое как gwt.


    1. ververesk Автор
      21.06.2024 21:51

      Спасибо, что было интересно. Не соглашусь, что Camunda редкостная гадость, если честно, я была такого же мнения, но после многочисленных шагов принятия, могу сказать, что в определенных моментах она прямо хорошо ложится на процесс


  1. waterman
    21.06.2024 21:51

    Temporal рулез, камунда мастдай.


  1. vdshat
    21.06.2024 21:51

    Любой BPM фреймворк нацелен на то, чтобы ввести уровень абстракции для дальнейшего переиспользования больших блоков процесса и эффективного развития процессов. Даже если вы работаете на уровне небольших приложений есть выгода от использования BPM. Например, визарды, заполение форм можно сделать с помощью BPM или workflow.

    В случае встроенного BPM отладка такая же как в обычном приложении. Если BPM серверный, то либо удаленная отладка, либо как в любом распределенном приложении. Опять же хороший тон - присать тесты на процесс перед заливкой и проблем будет значительно меньше.


    1. ververesk Автор
      21.06.2024 21:51

      Про тесты, да, это действительно невероятно удобно.


    1. TldrWiki
      21.06.2024 21:51
      +1

      Вы данные храните в единой базе данных или они у вас размазаны по икселькам, json на удаленной стороне и в очереди сообщений? Почему тогда путь выполнения вы разделяете на программный и камундный?

      Про переиспользование, а что камунда сама по себе работает, без кода со стороны приложения?


      1. ververesk Автор
        21.06.2024 21:51

        Смотрите, в камунде есть процесс variables, ограничение по ним вроде 4 мегабайта, но могу ошибаться. Так что в этих переменных мы храним uuid и другие технические данные. Поясню, если мне надо достать какую то информацию из базы данных и что то с ней сделать, то я получаю uuid из process variables, и у же по нему достаю нужные мне информацию. То есть вся основная информация хранится в базе данных.


        1. TldrWiki
          21.06.2024 21:51

          Во-первых в Камунде можно хранить любые данные. Во-вторых я не про хранение данных, а про разделение потока выполнения. Я применил принцип single source of truth к потоку выполнения приложения, который при внедрении камунды раздваивается.


          1. ververesk Автор
            21.06.2024 21:51

            не слышала честно говоря про этот принцип, однозначно почитаю, спасибо


          1. ayrtonSK
            21.06.2024 21:51

            Чтобы хранить в камунде данные, надо чтобы они там как то оказались из более надёжного источника данных, вы периодически синхронизируете кучу данных из другой бд?


    1. ayrtonSK
      21.06.2024 21:51

      Отладка в камунде это боль, так как контекстом то она не рулит, а рулят микросервисы и действительно это превращается в туже боль отладки микросервисов, особенно если кто то решил намельчить и использовал один паттерн микросервис- одна таблица. А по хорошему то надо, чтобы вся бизнес логика жила в одном sub домене core домена, тогда и проблемы не будет.

      Тесты писать безусловно надо, поделитесь как на камунду вы тесты пишите?


  1. ggo
    21.06.2024 21:51

    Camunda 8-й версии научилась "из коробки" не выпадать в осадок, если вызываемые синхронные сервисы замедлились или таймаутят?


    1. ververesk Автор
      21.06.2024 21:51

      У нас выпадала в осадок, пока мы не переписали на реактив вызовы к сторонним сервисам


      1. ayrtonSK
        21.06.2024 21:51

        А можно чуть подробнее?


    1. ayrtonSK
      21.06.2024 21:51

      В смысле процессы падали?


  1. titulusdesiderio
    21.06.2024 21:51

    Можно ли версионировать через git это всё?


    1. ayrtonSK
      21.06.2024 21:51

      Да, заливайте bpmn файл в гит, это xml


  1. ayrtonSK
    21.06.2024 21:51

    Ценность Camunda для меня сомнительная, ещё в 2013ом мы послали в урну Activity, мама камунды, ибо все это выглядело не используемо. Тогда мы сделали свой движок, который обладал контектом, а Bmpn движки обладают недоконтекстом, а набором переменных, которые надо как то туда собрать, в итоге более менее серьёзный процесс выпадает в какой-то плохо сопровождаемый проект, где сложно понять почему процесс оказался тут... ( кто поставил значения переменным, на основе которых принималось решение).

    Сейчас используем Camunda, для банков это плюс, они типа её знают и поэтому заносят в плюсы.

    Что по факту:

    1. Ни какой гибкости камунды нет, любое изменение, потребует знания не только сложной Camunda, но и java, python... Где реализуется ServiceTask. Да, визуально выглядит просто, но на деле, помимо аналитика, это всегда требует разработчика, например в приведеном примере надо до клуба сбегать за пивком..

    2. Это поражает другую проблему - у вас знания, как эта штука работает, разделяется на 2 системы и 2 головы и возникает необходимость синхронизация этого.

    3. Отладка, это боль и страдания, когда ты не можешь вначале понять где проблема, отдебажить все это, а пытаешься понять, как такой контекст переменных образовался.

    4. Ещё +1 система отказа и головной боли, для админов

    Какие мысли по этому, лучше все написать на одной системе или на java/... или на Camunda, звучит странно, но большую часть проблем тогда удастся решить.

    Но в Camunda нельзя видеть историю и её nocode движок очень слабо развит, т. е технически задачу не решить без code.

    На java/... , не будет визуальности диаграммы и типа лёгкости внесения изменений,что "типа" надо архитектору/аналитику банка.

    Кто как решает эту проблему?

    Кто и как управляет контекстом Camunda? (устанавливаете все переменные непосредственно перед принятием решения? Или где придётся) или может как то по другому?

    Идеального решения у меня нет, есть видение, кнопок возможно придётся реальзовать