// 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 }