Files
IoT/internal/service/bridge/bridge.go
T

117 lines
4.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 → телеметрия терялась).
//
// Отправка в SQS — через диспатчер (канал + worker-пул): колбэк MQTT
// не блокируется сетью (фикс CRITICAL из ревью 2026-08-16).
dispatcher := newSQSDispatcher(sqsClient, queueURL, log)
dispatcher.start(ctx)
handler := newMessageHandler(dispatcher, log)
client, err := connectMQTT(ctx, cfg, log, handler)
if err != nil {
dispatcher.closeAndWait()
return err
}
<-ctx.Done()
log.Info("bridge: shutting down")
// Порядок: сначала отключить MQTT (источник), затем слить остаток в SQS.
client.Disconnect(2000)
dispatcher.closeAndWait()
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)
c.Disconnect(100) // принудительный реконнект (подписка обязательна)
return
}
if token.Error() != nil {
log.Error("bridge: resubscribe failed", "filter", telemetryTopicFilter, "err", token.Error())
c.Disconnect(100) // принудительный реконнект (подписка обязательна)
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
}