Реализация 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
@@ -0,0 +1,50 @@
package rulecache
import (
"context"
"fmt"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
// activeRulesQuery resolves condition_source_device_id/target_device_id (FKs
// into devices.id) to devices.external_id, since every other part of the
// platform (MQTT, Redis, gRPC) keys devices by their external ID, not
// PostgreSQL's internal bigserial ID.
const activeRulesQuery = `
SELECT
r.id AS rule_id,
r.zone_id AS zone_id,
src.external_id AS source_external_id,
tgt.external_id AS target_external_id,
r.condition_sensor_type AS condition_sensor_type,
r.condition_operator AS condition_operator,
r.condition_value AS condition_value,
r.action_type AS action_type,
r.action_params AS action_params
FROM automation_rules r
JOIN devices src ON src.id = r.condition_source_device_id
JOIN devices tgt ON tgt.id = r.target_device_id
WHERE r.is_active = true
`
type PostgresFetcher struct {
pool *pgxpool.Pool
}
func NewPostgresFetcher(pool *pgxpool.Pool) *PostgresFetcher {
return &PostgresFetcher{pool: pool}
}
func (f *PostgresFetcher) FetchActiveRules(ctx context.Context) ([]Row, error) {
dbRows, err := f.pool.Query(ctx, activeRulesQuery)
if err != nil {
return nil, fmt.Errorf("query active rules: %w", err)
}
rows, err := pgx.CollectRows(dbRows, pgx.RowToStructByName[Row])
if err != nil {
return nil, fmt.Errorf("scan active rules: %w", err)
}
return rows, nil
}