Refactor MQTT course and bound backend message processing
Move lesson source into the teaching structure, align the course path with the learner's Go and DevOps experience, and add the interactive lesson guides. Add advanced DevOps lifecycle study as a separate section. Bound MQTT message work and drain accepted jobs during shutdown; align the backend implementation with the revised course through Section 6.
This commit is contained in:
+63
-20
@@ -5,12 +5,16 @@ import (
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -26,47 +30,86 @@ func run() error {
|
||||
return fmt.Errorf("failed to load config: %w", err)
|
||||
}
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
lifecycleCtx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
transportCtx, stopTransport := context.WithCancel(context.Background())
|
||||
defer stopTransport()
|
||||
|
||||
base := "lab/v1/devices/" + cfg.MQTT.DeviceID
|
||||
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(ctx, cfg, []string{base + "/telemetry", base + "/responses"}, inbox)
|
||||
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 <-ctx.Done():
|
||||
log.Print("shutting down")
|
||||
case <-lifecycleCtx.Done():
|
||||
log.Print("shutting down; draining accepted work before MQTT disconnect")
|
||||
return nil
|
||||
case msg := <-inbox:
|
||||
log.Printf("received message on topic %s retained=%t payload=%s", msg.Topic, msg.Retain, string(msg.Payload))
|
||||
|
||||
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++
|
||||
payload, err := json.Marshal(struct {
|
||||
ID string `json:"id"`
|
||||
Command string `json:"command"`
|
||||
}{
|
||||
ID: fmt.Sprintf("ping-%d-%d", runID, sequence), Command: "ping",
|
||||
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 err != nil {
|
||||
return fmt.Errorf("failed to marshal payload: %w", err)
|
||||
if marshalErr != nil {
|
||||
return fmt.Errorf("failed to marshal payload: %w", marshalErr)
|
||||
}
|
||||
if err := mqtt.Publish(ctx, manager, base+"/commands", payload); err != nil {
|
||||
log.Printf("command publication failed or outcome uncertain: %v", err)
|
||||
if publishErr := mqtt.Publish(lifecycleCtx, manager, commandTopic, payload); publishErr != nil {
|
||||
log.Printf("command publication failed or outcome uncertain: %v", publishErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user