// iot-service — монолит IoT на платформе Nubes (контейнер «Простой HTTP»). // // Три роли в одном процессе: // - REST API + MQTT auth/acl (:9090, устройства в PostgreSQL); // - bridge — MQTT-подписка EMQX → shared-SQS; // - consumer — shared-SQS → per-tenant PostgreSQL. // // Наследие старого IoT (k8s, оператор, CRD) НЕ используется. package main import ( "context" "errors" "log/slog" "net/http" "os" "os/signal" "sync" "syscall" "time" "gitea.services.ngcloud.ru/Nail/IoT/internal/service/api" "gitea.services.ngcloud.ru/Nail/IoT/internal/service/api/handler" "gitea.services.ngcloud.ru/Nail/IoT/internal/service/bridge" "gitea.services.ngcloud.ru/Nail/IoT/internal/service/config" "gitea.services.ngcloud.ru/Nail/IoT/internal/service/consumer" "gitea.services.ngcloud.ru/Nail/IoT/internal/service/sqsclient" "gitea.services.ngcloud.ru/Nail/IoT/internal/service/store" "gitea.services.ngcloud.ru/Nail/IoT/internal/storage/iotpg" ) // version — подставляется через ldflags при сборке. var version = "dev" func main() { cfg, err := config.Load() if err != nil { slog.Error("config", "err", err) os.Exit(1) } log := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: cfg.LogLevel})) slog.SetDefault(log) log.Info("iot-service starting", "version", version, "port", cfg.APIPort) ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT) defer cancel() // PostgreSQL — устройства и телеметрия. storeCtx, storeCancel := context.WithTimeout(ctx, 15*time.Second) devices, err := store.Open(storeCtx, cfg.IOTPGDSN) storeCancel() if err != nil { log.Error("devices store", "err", err) os.Exit(1) } defer devices.Close() log.Info("devices store ready") // Служебная строка бриджа в iot_devices — для PG-auth EMQX (хвост 3). ensureCtx, ensureCancel := context.WithTimeout(ctx, 10*time.Second) err = devices.EnsureBridgeDevice(ensureCtx, cfg.MQTTUsername, cfg.MQTTPassword) ensureCancel() if err != nil { log.Error("ensure bridge device", "err", err) os.Exit(1) } log.Info("bridge device ensured", "username", cfg.MQTTUsername) iotStore, err := iotpg.New(cfg.IOTPGDSN, log) if err != nil { log.Error("iotpg store", "err", err) os.Exit(1) } defer iotStore.Close() log.Info("telemetry store ready") sqsClient := sqsclient.New(cfg) h := &handler.Handler{ Devices: devices, IoTPG: iotStore, SQS: sqsClient, Cfg: cfg, Log: log, Version: version, StartedAt: time.Now(), } // HTTP API — в основном потоке. router := api.NewRouter(h, log, cfg.AuthTestMode, cfg.JwtHMACSecret, version) server := &http.Server{ Addr: ":" + cfg.APIPort, Handler: router, ReadHeaderTimeout: 10 * time.Second, } // bridge + consumer — в фоне. var wg sync.WaitGroup wg.Add(2) go func() { defer wg.Done() if err := bridge.Run(ctx, cfg, sqsClient, log); err != nil && !errors.Is(err, context.Canceled) { log.Error("bridge stopped", "err", err) } }() go func() { defer wg.Done() if err := consumer.Run(ctx, cfg, sqsClient, iotStore, log); err != nil && !errors.Is(err, context.Canceled) { log.Error("consumer stopped", "err", err) } }() // Сервер гаснет ПОСЛЕДНИМ: сначала останавливаются bridge+consumer // (ctx → disconnect MQTT → flush SQS), потом HTTP. Иначе платформа // может убить контейнер по liveness, пока bridge/consumer ещё работают // (фикс MEDIUM из ревью 2026-08-16). go func() { <-ctx.Done() wg.Wait() shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second) defer shutdownCancel() _ = server.Shutdown(shutdownCtx) }() log.Info("iot-service listening", "addr", server.Addr) if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { log.Error("http server", "err", err) os.Exit(1) } wg.Wait() log.Info("iot-service stopped") }