Вынос health-check-service в отдельный сервис
Тикер офлайн-детекции (internal/healthcheck) переехал из device-control-service в новый самостоятельный Go-сервис — как и планировалось с самого начала (см. изначальный README-заглушку). Детекция online остаётся в device-control-service: она происходит как побочный эффект уже имеющейся там MQTT-подписки на devices/+/telemetry и devices/+/ack, заводить для этого отдельный сервис с дублирующей MQTT-подпиской избыточно. health-check-service — свой Go-модуль (без зависимости от proto/, сборка из собственного контекста), читает только узкое read+offline-подмножество Device Shadow keyspace в Redis (devices:known, status, last_seen) — полный Shadow API с записью desired/reported state остаётся только в device-control-service. Метрика device_control_health_transitions_total переименована в device_health_transitions_total и теперь публикуется с этим именем из ДВУХ сервисов (device-control-service — direction=online, health-check-service — direction=offline): Prometheus агрегирует одноимённые метрики с разных таргетов прозрачно, поэтому Grafana- дашборд адаптирован простой сменой имени метрики в запросе, без переделки панели. Проверено вживую через docker compose: реальный online→offline переход (публикация тестовой телеметрии + ожидание таймаута) корректно долетает до Redis и RabbitMQ (device.status_changed), Prometheus видит новый scrape-таргет как up.
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user