Добавлен gRPC-контракт proto/device_control (TurnOn/TurnOff/SetLevel), общий Go-модуль proto/ для переиспользования сгенерированного кода. device-control-service принимает команды по gRPC, обновляет desired_state в Redis и публикует их в MQTT; слушает devices/+/telemetry и devices/+/ack для отметки живости устройства и обновления reported_state; горутина health-check переводит устройство в offline по таймауту и публикует событие в RabbitMQ (device.status_changed) для будущего notification-service. Проверено сквозным тестом через docker compose: grpcurl TurnOn/SetLevel меняет desired_state и уходит в MQTT, mosquitto_pub с ack обновляет reported_state и статус, health-check автоматически переводит устройство в offline по истечении таймаута.
94 lines
2.2 KiB
Go
94 lines
2.2 KiB
Go
// Package healthcheck periodically flips devices to offline when they stop
|
|
// sending telemetry/acks, per the platform's Device Shadow health-check
|
|
// design (a goroutine + graceful shutdown, deliberately simple).
|
|
package healthcheck
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"time"
|
|
)
|
|
|
|
// DeviceStore is the subset of shadow.Store the checker needs, kept as an
|
|
// interface so tests don't require a real Redis.
|
|
type DeviceStore interface {
|
|
KnownDevices(ctx context.Context) ([]string, error)
|
|
MarkOfflineIfStale(ctx context.Context, deviceID string, timeout time.Duration) (bool, error)
|
|
}
|
|
|
|
// OnOfflineFunc is called once per device that just transitioned to offline.
|
|
type OnOfflineFunc func(deviceID string)
|
|
|
|
type Checker struct {
|
|
store DeviceStore
|
|
timeout time.Duration
|
|
interval time.Duration
|
|
onOffline OnOfflineFunc
|
|
logger *slog.Logger
|
|
|
|
stop chan struct{}
|
|
done chan struct{}
|
|
}
|
|
|
|
func New(store DeviceStore, timeout, interval time.Duration, onOffline OnOfflineFunc, logger *slog.Logger) *Checker {
|
|
return &Checker{
|
|
store: store,
|
|
timeout: timeout,
|
|
interval: interval,
|
|
onOffline: onOffline,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// Start begins the periodic scan loop. Call once.
|
|
func (c *Checker) Start() {
|
|
c.stop = make(chan struct{})
|
|
c.done = make(chan struct{})
|
|
go c.loop()
|
|
}
|
|
|
|
// Stop signals the loop to exit and waits for it to finish.
|
|
func (c *Checker) Stop() {
|
|
close(c.stop)
|
|
<-c.done
|
|
}
|
|
|
|
func (c *Checker) loop() {
|
|
defer close(c.done)
|
|
ticker := time.NewTicker(c.interval)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
c.checkAll()
|
|
case <-c.stop:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Checker) checkAll() {
|
|
ctx, cancel := context.WithTimeout(context.Background(), c.interval)
|
|
defer cancel()
|
|
|
|
ids, err := c.store.KnownDevices(ctx)
|
|
if err != nil {
|
|
c.logger.Error("healthcheck: list known devices failed", "error", err)
|
|
return
|
|
}
|
|
|
|
for _, id := range ids {
|
|
changed, err := c.store.MarkOfflineIfStale(ctx, id, c.timeout)
|
|
if err != nil {
|
|
c.logger.Error("healthcheck: check device failed", "device_id", id, "error", err)
|
|
continue
|
|
}
|
|
if changed {
|
|
c.logger.Info("device marked offline", "device_id", id)
|
|
if c.onOffline != nil {
|
|
c.onOffline(id)
|
|
}
|
|
}
|
|
}
|
|
}
|