Реализация rule-engine-service: RabbitMQ → правила из PostgreSQL → gRPC

Слушает telemetry.new_reading (ручной ack/nack, реквизишн только при
транспортных ошибках gRPC), кэширует активные automation_rules в памяти
с периодическим обновлением из PostgreSQL (JOIN с devices — резолвит
внутренние ID в external_id, которым оперируют MQTT/Redis/gRPC). Условия
правил (>,<,>=,<=,=,!=) оцениваются обобщённо, без привязки к конкретным
типам устройств. При срабатывании вызывает device-control-service по gRPC
и публикует automation.rule_triggered в RabbitMQ.

Проверено сквозным тестом через docker compose на полном пайплайне:
mosquitto_pub → ingest-service (ClickHouse + telemetry.new_reading) →
rule-engine-service (совпадение правила) → device-control-service (gRPC →
Redis desired_state + MQTT-команда) → automation.rule_triggered. Показание
ниже порога проверено отдельно — правило корректно не срабатывает.
This commit is contained in:
2026-07-26 21:18:59 +05:00
parent 3eb84aae6c
commit abc83faf05
20 changed files with 1334 additions and 10 deletions
+41 -7
View File
@@ -1,18 +1,52 @@
# rule-engine-service (Go)
Статус: пока не реализован.
Статус: реализован (MVP).
Зона ответственности:
- Потребляет события «новое показание» из RabbitMQ (асинхронно — здесь
надёжность доставки важнее задержки).
- Загружает активные `automation_rules` для нужной зоны/устройства из
PostgreSQL, кэшируя в памяти/Redis, чтобы не ходить в БД на каждое событие.
- Потребляет события «новое показание» из RabbitMQ (очередь
`telemetry.new_reading`, публикуемая ingest-service) — асинхронно, с ручным
ack/nack: надёжность доставки здесь важнее задержки.
- Загружает активные `automation_rules` из PostgreSQL в кэш в памяти
(обновляется по тикеру), чтобы не ходить в БД на каждое событие.
- Оценивает условия правил обобщённо: `sensor_type X operator value` против
показания — никаких зашитых названий датчиков/устройств, всё берётся из
строки `automation_rules`.
- При срабатывании: вызывает device-control-service по gRPC (нужен быстрый
результат успех/неудача) и публикует событие в notification-service через
RabbitMQ (не критично к задержке).
результат успех/неудача) и публикует событие `automation.rule_triggered` в
RabbitMQ для будущего notification-service (не критично к задержке).
Не входит в зону ответственности: прямое взаимодействие с MQTT или Redis —
изменения состояния устройства всегда идут через gRPC API device-control-service.
## Архитектурные решения
- **automation_rules хранит внутренние ID Postgres, а не внешние device_id.**
`condition_source_device_id`/`target_device_id` — это FK на `devices.id`
(bigserial), а MQTT/Redis/gRPC везде оперируют `devices.external_id`
(строка). Поэтому кэш правил грузится JOIN'ом `automation_rules` с
`devices` (дважды — для source и target), резолвя оба ID в external_id
один раз при загрузке, а не на каждое событие.
- **Retry — только на транспортных ошибках.** Если gRPC-вызов
device-control-service не удался физически (сеть, сервис недоступен) —
событие из RabbitMQ nack'ается с `requeue=true`, всё правило
переигрывается позже. Если устройство само отклонило команду
(`CommandResult.success=false`, например неизвестный device_id) — это
финальный исход, ack, повторов не будет. Осознанное упрощение: если из
нескольких правил на одно показание одно не удалось из-за сети, при
повторной доставке переиграются ВСЕ правила читающие это показание, включая
уже успешно сработавшие — дедупликация не реализована, для MVP это
приемлемо (команды идемпотентны: turn_on дважды — не проблема).
- **Кэш правил — только для чтения активных правил**, никакой записи назад в
Postgres. CRUD правил — это зона ответственности Laravel (этап 2).
## Запуск
```bash
cd services/rule-engine-service
go test ./...
go build ./cmd/rule-engine-service
```
Конфигурация — через переменные окружения (секция rule-engine-service в
корневом `.env.example`). Как и device-control-service, использует
`proto/device_control` через `replace` на `../../proto` в `go.mod`.