package main import ( "context" "encoding/json" "fmt" "log" "log/slog" "os" "os/signal" "syscall" "time" "example.com/mqtt5-learning/internal/config" "example.com/mqtt5-learning/internal/contracts" "example.com/mqtt5-learning/internal/deviceview" "example.com/mqtt5-learning/internal/dispatch" "example.com/mqtt5-learning/internal/mqtt" ) func main() { if err := run(); err != nil { log.Fatal(err) } } func run() error { cfg, err := config.Load(os.Args[1:]) if err != nil { return fmt.Errorf("failed to load config: %w", err) } lifecycleCtx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() transportCtx, stopTransport := context.WithCancel(context.Background()) defer stopTransport() store := deviceview.NewStore() service, err := deviceview.NewService(store, slog.Default(), 90*time.Second) if err != nil { return fmt.Errorf("create device-view service: %w", err) } telemetryFilter, err := contracts.DeviceFilter(contracts.KindTelemetry) if err != nil { return err } responsesFilter, err := contracts.DeviceFilter(contracts.KindResponses) if err != nil { return err } commandTopic, err := contracts.DeviceTopic(cfg.MQTT.DeviceID, contracts.KindCommands) if err != nil { return err } inbox := make(chan mqtt.Message, 16) manager, err := mqtt.Start(transportCtx, cfg, []contracts.TopicFilter{telemetryFilter, responsesFilter}, inbox) if err != nil { return fmt.Errorf("failed to start MQTT client: %w", err) } defer mqtt.Stop(manager) telemetryPool := dispatch.New(context.Background(), 2, 16) importantPool := dispatch.New(context.Background(), 2, 32) receiver := &dispatch.Receiver{ Telemetry: telemetryPool, Important: importantPool, Process: service.HandleMessage, } defer func() { drainCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() started := time.Now() if err := dispatch.Shutdown(drainCtx, telemetryPool, importantPool); err != nil { log.Printf("dispatcher shutdown incomplete: %v", err) } log.Printf("dispatcher drain finished elapsed=%s telemetry=%+v important=%+v", time.Since(started), telemetryPool.Stats(), importantPool.Stats()) }() ticker := time.NewTicker(10 * time.Second) defer ticker.Stop() runID := time.Now().UnixNano() sequence := 0 for { select { case <-lifecycleCtx.Done(): log.Print("shutting down; draining accepted work before MQTT disconnect") return nil case message := <-inbox: outcome, submitErr := receiver.Submit(lifecycleCtx, message) if submitErr != nil || outcome != dispatch.Accepted { slog.Warn("message admission rejected", "topic", message.Topic, "outcome", outcome, "error", submitErr) } case <-ticker.C: sequence++ id := fmt.Sprintf("ping-%d-%d", runID, sequence) payload, marshalErr := json.Marshal(contracts.Command{ SchemaVersion: contracts.SchemaVersionOne, MessageID: id, DeviceID: cfg.MQTT.DeviceID, OccurredAt: time.Now().UTC().Format(time.RFC3339), CommandID: id, Command: "ping", }) if marshalErr != nil { return fmt.Errorf("failed to marshal payload: %w", marshalErr) } if publishErr := mqtt.Publish(lifecycleCtx, manager, commandTopic, payload); publishErr != nil { log.Printf("command publication failed or outcome uncertain: %v", publishErr) } } } }