106 lines
4.0 KiB
Go
106 lines
4.0 KiB
Go
// Package bridge — MQTT-подписка на телеметрию устройств → shared-SQS.
|
||
//
|
||
// Роль старого mqtt-bridge, но внутри монолита: подключается к EMQX как
|
||
// MQTT-клиент, подписывается на "+/telemetry/+" и шлёт envelope в SQS.
|
||
package bridge
|
||
|
||
import (
|
||
"context"
|
||
"log/slog"
|
||
"time"
|
||
|
||
"github.com/aws/aws-sdk-go-v2/service/sqs"
|
||
mqtt "github.com/eclipse/paho.mqtt.golang"
|
||
|
||
"gitea.services.ngcloud.ru/Nail/IoT/internal/service/config"
|
||
"gitea.services.ngcloud.ru/Nail/IoT/internal/service/sqsclient"
|
||
)
|
||
|
||
// telemetryTopicFilter — все namespace, все устройства.
|
||
const telemetryTopicFilter = "+/telemetry/+"
|
||
|
||
// Run — бесконечный цикл bridge: подключение → подписка → ожидание сигнала завершения.
|
||
func Run(ctx context.Context, cfg *config.Config, sqsClient *sqs.Client, log *slog.Logger) error {
|
||
log.Info("bridge: starting", "broker", cfg.MQTTBrokerURL, "client_id", cfg.MQTTClientID)
|
||
|
||
// URL очереди получаем один раз при старте (как старый consumer).
|
||
queueURL, err := sqsclient.ResolveQueueURL(ctx, sqsClient, cfg.SQSQueueName)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
log.Info("bridge: SQS queue resolved", "queue", cfg.SQSQueueName)
|
||
|
||
// Обработчик сообщений передаём в connectMQTT: подписка выполняется в
|
||
// OnConnectHandler, т.е. повторяется при каждом (ре)подключении.
|
||
// Иначе после потери сессии EMQX бридж оставался бы без подписки
|
||
// (прецедент 2026-08-16: реконнект без resubscribe → телеметрия терялась).
|
||
handler := newMessageHandler(ctx, sqsClient, queueURL, log)
|
||
|
||
client, err := connectMQTT(ctx, cfg, log, handler)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer client.Disconnect(250)
|
||
|
||
<-ctx.Done()
|
||
log.Info("bridge: shutting down")
|
||
return nil
|
||
}
|
||
|
||
// errSubscribeTimeout — не дождались подтверждения подписки.
|
||
var errSubscribeTimeout = errBridge("MQTT subscribe timeout")
|
||
|
||
// errBridge — ошибки bridge.
|
||
type errBridge string
|
||
|
||
func (e errBridge) Error() string { return string(e) }
|
||
|
||
// connectMQTT устанавливает подключение к EMQX с автореконнектом.
|
||
// handler передаётся в OnConnectHandler: подписка повторяется при каждом подключении.
|
||
func connectMQTT(ctx context.Context, cfg *config.Config, log *slog.Logger, handler mqtt.MessageHandler) (mqtt.Client, error) {
|
||
opts := mqtt.NewClientOptions()
|
||
opts.AddBroker(cfg.MQTTBrokerURL)
|
||
opts.SetClientID(cfg.MQTTClientID)
|
||
opts.SetUsername(cfg.MQTTUsername)
|
||
opts.SetPassword(cfg.MQTTPassword)
|
||
opts.SetAutoReconnect(true)
|
||
opts.SetConnectRetry(true)
|
||
opts.SetConnectRetryInterval(5 * time.Second)
|
||
opts.SetKeepAlive(30 * time.Second)
|
||
opts.SetCleanSession(false)
|
||
|
||
opts.SetConnectionLostHandler(func(_ mqtt.Client, err error) {
|
||
log.Warn("bridge: MQTT connection lost, reconnecting...", "err", err)
|
||
})
|
||
opts.SetReconnectingHandler(func(_ mqtt.Client, _ *mqtt.ClientOptions) {
|
||
log.Info("bridge: MQTT reconnecting...")
|
||
})
|
||
opts.SetOnConnectHandler(func(c mqtt.Client) {
|
||
log.Info("bridge: MQTT connected")
|
||
token := c.Subscribe(telemetryTopicFilter, 1, handler)
|
||
if !token.WaitTimeout(10 * time.Second) {
|
||
log.Error("bridge: resubscribe timeout", "filter", telemetryTopicFilter)
|
||
return
|
||
}
|
||
if token.Error() != nil {
|
||
log.Error("bridge: resubscribe failed", "filter", telemetryTopicFilter, "err", token.Error())
|
||
return
|
||
}
|
||
log.Info("bridge: subscribed", "filter", telemetryTopicFilter)
|
||
})
|
||
|
||
client := mqtt.NewClient(opts)
|
||
token := client.Connect()
|
||
select {
|
||
case <-ctx.Done():
|
||
return nil, ctx.Err()
|
||
case <-time.After(30 * time.Second):
|
||
return nil, errBridge("bridge: MQTT connect timeout")
|
||
case <-token.Done():
|
||
}
|
||
if token.Error() != nil {
|
||
return nil, token.Error()
|
||
}
|
||
return client, nil
|
||
}
|