---
title: Как работает движок прогонов
description: Каждое исполнение пайплайна как поток типизированных событий — транспорт Redis Streams, единственный идемпотентный писатель в БД, самолечащиеся вачдоги и конкурентность, чувствующая провайдера в реальном времени.
locale: ru
version: 0.2.0
canonical: https://enterium-developer-docs.pages.dev/developers/ru/how-it-works/run-engine/
---

> Documentation index: https://enterium-developer-docs.pages.dev/developers/llms.txt — every page of this portal has a Markdown twin at <route>.md; the frozen API contract is at https://enterium-developer-docs.pages.dev/developers/api/openapi.json.

**Проблема:** исполнение пайплайна — долгий, склонный к отказам процесс,
трогающий LLM-провайдеров, базу и сборщик сайтов. Если его состояние
живёт в памяти того процесса, которому довелось его исполнять, краш
теряет правду о случившемся, а два процесса, пишущие «состояние», дают
две правды.

## Слой 1 — один вход, стримы между, один писатель

Создание прогона — единый шов: дубль-предохранитель, бюджетный preflight,
резолв версии из каталога, строка `pipeline_run` — затем джоб-сообщение в
Redis Stream. Исполнение отвечает **только событиями**.

```mermaid
sequenceDiagram
    participant API as Platform API
    participant J as runs:jobs (stream)
    participant W as Native worker
    participant E as runs:events (stream)
    participant P as Projector
    participant DB as PostgreSQL
    participant SSE as SSE clients
    API->>J: enqueue job (XADD)
    J->>W: consumer group delivers
    W->>E: typed events: run_started,<br/>step_completed, run_succeeded…
    E->>P: consumer group delivers
    P->>DB: apply_event (sole DB writer)
    P->>SSE: fan-out via Redis Pub/Sub
```

Вес несут два решения:

- **Типизированные фабрики событий с JSON-Schema контрактом.** Воркеры
  никогда не собирают event-словари руками — фабрики штампуют версию
  схемы и валидируют поля, а схема едет закоммиченным контрактом с
  фикстурами на каждый тип события. Мок-эмулятор веб-UI проигрывает те же
  фикстуры, так что фронтенд разрабатывается против точного wire-формата.
- **Проектор — единственный писатель.** Всё остальное эмитит; применяет
  один потребитель. Идемпотентность структурна: счётчики шагов считаются
  как *distinct терминальные шаги*, а не инкременты, поэтому
  at-least-once редоставка не может посчитать дважды. Отравленное
  сообщение подтверждается и захватывается, а не ретраится вечно.

## Слой 2 — состояния прогона и кто вправе их двигать

```mermaid
stateDiagram-v2
    [*] --> queued: create_run
    queued --> running: worker picks up
    queued --> cancelled: cancel
    running --> succeeded
    running --> failed: step error (fail-loud)
    running --> interrupted: watchdog: heartbeat lost
```

Честно: сегодня эти переходы держатся конвенцией между писателями
(проектор, вачдог, ретрай), а не формальной таблицей предохранителей —
явный FSM с chaos-тестами измеренного восстановления это
спроектированная, ещё не принятая эволюция. Что *живо* — самолечащаяся
пара:

- **вачдог** помечает прогоны с протухшим heartbeat как `interrupted`
  (упавший воркер не может оставить прогон «running» навсегда);
- **рипер** возвращает брошенные *джоб-сообщения* из consumer-группы,
  переставляет их в очередь до ретрай-кэпа, затем — в dead-letter. Двое
  никогда не пересекаются: прогон, упавший по собственным заслугам,
  терминален и не трогается.

## Слой 3 — инпуты замораживаются до первого шага

Инпуты прогона (профиль бренда, профиль автора, конфиг сбора) собираются
**один раз** на старте в замороженный типизированный контракт с
отредактированными секретами — шаги читают `ctx.inputs` и никогда не
перечитывают базу посреди прогона. Правило, которое это обеспечивает:
чистая граница между *что тебе дали* и *что ты произвёл*. Оно существует
потому, что оба провала реально случались: бренд-гайдлайны, тихо
обрезанные ad-hoc чтением, и конфиг-поле, мутированное шагом 1 и
съеденное шагом 6 как «инпут».

## Слой 4 — конкурентность, чувствующая провайдера

Глобальный кэп LLM-конкурентности — не константа, а контур управления:

```mermaid
flowchart LR
    A[LLM adapter<br/>reads x-ratelimit-* headers] -- telemetry --> R[(Redis)]
    R --> D[Autoscaler tick<br/>C* = min of provider ·<br/>DB pool · host · ceiling]
    D -- set cap --> R
    R -- read cap --> W[Workers acquire slots]
    W --> A
```

**Sense** — каждый живой LLM-ответ роняет rate-limit телеметрию в Redis
(fail-soft: телеметрия не может сломать вызов). **Decide** —
периодический тик считает кэп как *минимум нескольких лимитеров* и
применяет AIMD (поднимай медленно, режь резко, мгновенный срез на 429).
**Actuate** — кэп это Redis-ключ с TTL; умрёт контур управления — воркеры
сами откатятся к статической настройке.

Почему несколько лимитеров, а не только провайдер: живой эксперимент
нашёл *тихую стену* — выше некоторой конкурентности исчерпывался **пул
БД**, пока LLM-провайдер и CPU выглядели зелёными, и пропускная
способность падала в ноль. Связывающее ограничение на практике — пул, а
не провайдер. Гигиена границ: контексту прогонов import-линтером
запрещено лезть во внутренности интеграций, поэтому Sense и Decide
общаются строго через Redis — контур управления уважает те же модульные
границы, что и всё остальное.

## Компромиссы

Отмена действует на прогоны *в очереди* немедленно; уже исполняющийся
прогон на середине не прерывается — он добегает до завершения или
падения (канал управления для прерывания на бегу есть, его потребитель —
в роадмапе). Exactly-once — по построению, а не по формальному
replay-тесту. Оба — задокументированные границы, не сюрпризы.

## Пощупать за две минуты

`POST /api/v1/runs` запускает прогон; смотри его вживую по SSE-каналу
прогонов или читай его строки `pipeline_run` / `run_step` — каждое
утверждение выше видно в этих данных.
