Перейти к содержанию

Локальная настройка Camunda для разработки

Инструкция по развёртыванию и отладке BPMN-процессов Planiqum на локальной машине.

Если что-то в этом документе противоречит актуальному состоянию кода, в первую очередь проверяйте src/planiqum/camunda_workflow/service/run_listener.py, src/planiqum/camunda_workflow/launch_strategy/*.py и переменные окружения CAMUNDA_* / RABBIT_MQ_* в .env.

1. Необходимые сервисы

Сервис Контейнер URL / адрес Креды Назначение
Camunda BPM Platform camunda_app Web UI: http://localhost:8080/camunda · REST: http://localhost:8080/engine-rest demo / demo Оркестрация BPMN
Camunda DB (Postgres 12) camunda_db localhost:5434 sa / sa, БД camunda Хранилище Camunda
RabbitMQ 3.12 + management rabbitmq AMQP: localhost:5672 · UI: http://localhost:15672 admin / password Очереди message, run_workflow, close_task
Redis planiqum_core_dev_redis localhost:6379 Celery + Planiqum кэш
Postgres основной pg_container localhost:5432 из .env БД Planiqum

Сервисы поднимаются через docker-compose-deb.yml:

cd <project_root>
docker compose -f docker-compose-deb.yml --env-file .env up -d camunda_db camunda rabbitmq

Проверка доступности:

curl -sS -o /dev/null -w "camunda rest: %{http_code}\n" http://localhost:8080/engine-rest/engine
curl -sS -o /dev/null -w "camunda ui  : %{http_code}\n" http://localhost:8080/camunda/
curl -sS -o /dev/null -w "rabbit ui   : %{http_code}\n" -u admin:password http://localhost:15672/api/overview

Все ответы должны быть 200.

2. Переменные окружения (.env)

CI_COMMIT_BRANCH=dev

CAMUNDA_HOST=http://localhost
CAMUNDA_PORT=8080
CAMUNDA_PORT_OUT=8080
CAMUNDA_DB_HOST=camunda_db
CAMUNDA_DB_PORT_IN=5432
CAMUNDA_DB_PORT_OUT=5434
CAMUNDA_DB_NAME=camunda
CAMUNDA_DB_USER=sa
CAMUNDA_DB_PASSWORD=sa

RABBIT_MQ_HOST=localhost
RABBIT_MQ_PORT=5672
RABBIT_MQ_USER=admin
RABBIT_MQ_PASSWORD=password

Флаг CAMUNDA_FRONTEND_SCRIPT_EXECUTION управляет тем, где исполняются скрипты, запущенные через UI. Для BPMN-потока (Camunda → workflow-listener → RabbitMQ → run_listener → run_worker) он не влияет.

3. Архитектура локального запуска

На локальной машине нет разделения «сервер приложений / сервер алгоритмов» (в проде оно есть, см. Workflow). Все компоненты запускаются на одном хосте в четырёх параллельных процессах:

# 0. Активация venv
cd <project_root>
source venv/bin/activate

# Терминал 1 — Django web
python manage.py runserver 0.0.0.0:8000

# Терминал 2 — Celery (если процесс или зависимые скрипты его используют)
celery -A planiqum.core worker -l info

# Терминал 3 — consumer RabbitMQ + спавнер run_worker (ОБЯЗАТЕЛЬНЫЙ)
python manage.py run_listener

# Терминал 4 — локальный listener Camunda External Tasks (ОБЯЗАТЕЛЬНЫЙ)
python infrastructure/scripts/local_workflow_listener.py

Терминалы 3 и 4 обязательны — без них Camunda будет крутиться вхолостую.

Что каждый процесс делает

  • workflow-listener (терминал 4) подписывается на топик start_listener_process. Когда Camunda доходит до Service Task с этим топиком, скрипт скачивает BPMN-схему процесса, парсит её (parse_bpmn) и кладёт список задач в очередь RabbitMQ run_workflow.

  • manage.py run_listener (терминал 3, код: src/planiqum/camunda_workflow/service/run_listener.py) слушает очереди message, run_workflow, close_task. Получив run_workflow, создаёт/обновляет WorkerController и для каждой External Task из списка спавнит отдельный subprocess: python manage.py run_worker <topic> --data <json> --not_run True --variant <variant>.

  • manage.py run_worker через ExecutionStrategyFactory выбирает стратегию (ComplexProcess для флага --not_run=True), подписывается на топик своего Service Task, получает External Task, вызывает Script.execute(...) и закрывает задачу (task.complete()).

4. Контракт BPMN для Planiqum

Чтобы BPMN-процесс работал с существующим раннером, он должен соответствовать следующим требованиям.

4.1. Первый Service Task — входная точка

<bpmn:serviceTask id="ListenerEntry"
                  name="Start Listener Process"
                  camunda:type="external"
                  camunda:topic="start_listener_process"/>

Без этого таска процесс не попадёт в Planiqum — именно топик start_listener_process слушает workflow-listener.

4.2. Алгоритмические Service Task

<bpmn:serviceTask id="MyTask"
                  name="My Algorithm"
                  camunda:type="external"
                  camunda:topic="my_algorithm_topic">
  <bpmn:extensionElements>
    <camunda:inputOutput>
      <camunda:inputParameter name="script_natural_key">app_name__script_shortname</camunda:inputParameter>
      <camunda:inputParameter name="variant">my_variant</camunda:inputParameter>
    </camunda:inputOutput>
  </bpmn:extensionElements>
</bpmn:serviceTask>

Имя топика = первый позиционный аргумент manage.py run_worker. ComplexProcess подписывается на этот топик и внутри callback'а запускает RunProcessSingle (одиночная задача) или RunThread (параллельная, если is_parallel=true).

4.3. Переменные процесса

Переменная Кто читает Назначение
script_natural_key ComplexProcess.start_process_by_single Какой Script выполнять (формат: app_name__script_shortname). Проверить актуальные ключи: Script.objects.all().values('app_name', 'shortname').
variant ComplexProcessScript.execute(variant=…) Ключ варианта скрипта. Может быть пустым или отсутствовать.
is_test run_process(key, is_test=…) Тестовый прогон: ComplexProcess делает dry-run (закрывает таски без выполнения скриптов).
group ComplexProcess.__get_group_ids Для параллельных задач — имя GroupTemplate, из которого берутся group_id для разветвления.

4.4. Параллельный (multi-instance) subprocess

Для параллельного выполнения задач по группам используется subProcess с multiInstanceLoopCharacteristics:

<bpmn:subProcess id="SubProcess_my_worker">
  <bpmn:multiInstanceLoopCharacteristics
      camunda:collection="ids" camunda:elementVariable="id" />
  <bpmn:startEvent id="Event_start">
    <bpmn:outgoing>Flow_1</bpmn:outgoing>
  </bpmn:startEvent>
  <bpmn:serviceTask id="my_worker" name="My Worker"
                    camunda:type="external" camunda:topic="my_worker_topic">
    <bpmn:extensionElements>
      <camunda:inputOutput>
        <camunda:inputParameter name="script_natural_key">app__script</camunda:inputParameter>
        <camunda:inputParameter name="group">my_group_template</camunda:inputParameter>
      </camunda:inputOutput>
    </bpmn:extensionElements>
    <bpmn:incoming>Flow_1</bpmn:incoming>
    <bpmn:outgoing>Flow_2</bpmn:outgoing>
  </bpmn:serviceTask>
  <bpmn:endEvent id="Event_end">
    <bpmn:incoming>Flow_2</bpmn:incoming>
  </bpmn:endEvent>
  <bpmn:sequenceFlow id="Flow_1" sourceRef="Event_start" targetRef="my_worker" />
  <bpmn:sequenceFlow id="Flow_2" sourceRef="my_worker" targetRef="Event_end" />
</bpmn:subProcess>
  • group — имя GroupTemplate; ComplexProcess.__get_group_ids берёт из него group_id для каждой параллельной ветви.
  • Коллекция ids формируется на предыдущем шаге через task.complete({'ids': groups}) — флаг is_next_parallel в парсере BPMN.
  • WorkerParallelTask автоматически прокидывает в options: process_instance_id, group_id, filter_.

5. Деплой BPMN в Camunda

Автоматического деплоя из Planiqum нет. Доступные варианты:

A. Camunda Modeler

  1. Скачать Camunda Modeler.
  2. Открыть .bpmn, в правом сайдбаре нажать Deploy current diagram → REST Endpoint http://localhost:8080/engine-rest → Deploy.

B. REST API

curl -X POST http://localhost:8080/engine-rest/deployment/create \
  -F "deployment-name=my_process" \
  -F "enable-duplicate-filtering=true" \
  -F "deploy-changed-only=true" \
  -F "data=@path/to/my_process.bpmn"

Проверить список процессов:

curl -s http://localhost:8080/engine-rest/process-definition | jq '.[] | {key, name, version}'

6. Запуск процесса

A. Из Django shell

from planiqum.camunda_workflow.service import CamundaFacade
CamundaFacade().start_process(
    process_key="my_process_key",   # = атрибут id у <bpmn:process>
    variables={
        "is_test": True,            # dry-run
    },
)

B. Через REST

curl -X POST http://localhost:8080/engine-rest/process-definition/key/my_process_key/start \
  -H "Content-Type: application/json" \
  -d '{"variables": {"is_test": {"value": true, "type": "Boolean"}}}'

C. Через run_process

from planiqum.camunda_workflow.tasks import run_process
run_process(key="my_process_key", is_test=True)

7. Отладка

7.1. Что смотреть в первую очередь

  • Cockpit (http://localhost:8080/camunda/app/cockpit/) — список running process instances, переменные, incident'ы. Если процесс «застрял», он висит на External Task, который никто не забрал.
  • External Tasks в Cockpit — видно, на каком топике лежит лок (start_listener_process → не запущен workflow-listener; любой другой топик → не запущен manage.py run_listener или run_worker упал).
  • RabbitMQ UI (http://localhost:15672) → вкладка Queues — должны быть run_workflow, message, close_task. Если в очереди висят необработанные сообщения — listener не читает (проверьте auth/host в .env).
  • Логи Docker:
docker logs -f camunda_app
docker logs -f rabbitmq

7.2. Частые ошибки

Симптом Причина Решение
External Task на start_listener_process висит бесконечно Не запущен workflow-listener Запустить python infrastructure/scripts/local_workflow_listener.py
Сообщения копятся в RabbitMQ run_workflow Не запущен manage.py run_listener Запустить его в отдельном терминале
run_worker падает: Script.DoesNotExist Неверный script_natural_key в BPMN Проверить: Script.objects.all().values('app_name', 'shortname')
run_worker падает без трейса в консоли Трейс в TaskResult.traceback TaskResult.objects.filter(task_id='<process_instance_id>').first().traceback
Multi-instance не разворачивается Нет ids в переменных процесса Добавить fan-out Service Task перед subprocess (см. раздел 4.4)
Camunda очень медленная Эмуляция amd64→arm64 на Apple Silicon Ожидаемо для разработки; для нагрузки — собрать arm64-образ

7.3. Сброс состояния

Очистить все instance'ы (не трогая deployments):

python manage.py clear_camunda

Удалить все deployments и instance'ы:

for d in $(curl -s http://localhost:8080/engine-rest/deployment | jq -r '.[].id'); do
  curl -s -X DELETE "http://localhost:8080/engine-rest/deployment/$d?cascade=true"
done

Очистить очереди RabbitMQ:

curl -s -u admin:password -X DELETE http://localhost:15672/api/queues/%2F/run_workflow
curl -s -u admin:password -X DELETE http://localhost:15672/api/queues/%2F/message
curl -s -u admin:password -X DELETE http://localhost:15672/api/queues/%2F/close_task

8. Отличия локальной среды от прода

  • Один хост vs два: в проде manage.py run_listener и run_worker крутятся на отдельном «сервере алгоритмов». Код тот же, разделение определяется тем, какая машина запускает run_listener.
  • Производительность: Camunda на amd64 через QEMU (Apple Silicon) нормальна для отладки, но не подходит для нагрузочных тестов.
  • Креды RabbitMQ: docker-compose использует admin/password, а не дефолтные guest/guest из settings.py. Значения должны быть в .env.

9. Полезные файлы

Файл Назначение
src/planiqum/camunda_workflow/service/run_listener.py Consumer RabbitMQ, спавнер run_worker
src/planiqum/camunda_workflow/service/run_process/parallel.py WorkerParallelTask, RunThread — параллельное выполнение
src/planiqum/camunda_workflow/service/run_process/single.py RunProcessSingle — одиночный Service Task
src/planiqum/camunda_workflow/launch_strategy/complex_process.py Стратегия для External Task с --not_run True
src/planiqum/camunda_workflow/service/parser_bmp.py Парсер BPMN (camunda:topic, inputParameter, multiInstanceLoopCharacteristics)
src/planiqum/camunda_workflow/service/camunda_facade.py Обёртка над engine-rest: start_process, fetch_and_lock, complete_task
infrastructure/scripts/local_workflow_listener.py Локальная замена внешнего workflow-listener

10. Чеклист перед запуском BPMN

  1. Docker-сервисы запущены (Camunda, RabbitMQ).
  2. .env содержит CAMUNDA_* и RABBIT_MQ_* (см. раздел 2).
  3. Запущены: manage.py run_listener и local_workflow_listener.py.
  4. BPMN задеплоен (см. раздел 5), process_key совпадает с id в <bpmn:process>.
  5. Первый Service Task — camunda:topic="start_listener_process".
  6. Каждый Service Task имеет script_natural_key в inputParameter.
  7. Multi-instance subprocess имеет camunda:collection="ids" и на предыдущем шаге выставляется коллекция ids.