Files
SafelyYouCodingChallenge/go/internal/store/heartbeat.go
T

95 lines
2.1 KiB
Go

package store
import (
"errors"
"sync"
"time"
)
// ErrOutOfSequence is returned when a heartbeat timestamp is earlier than
// the latest accepted heartbeat for that device.
var ErrOutOfSequence = errors.New("heartbeat out of sequence")
// heartbeatHistory tracks one device's heartbeat timeline.
// Unexported: only Store methods expose behavior.
type heartbeatHistory struct {
mu sync.Mutex
count int64
first time.Time
last time.Time
}
func (h *heartbeatHistory) add(sentAt time.Time) error {
h.mu.Lock()
defer h.mu.Unlock()
if h.first.IsZero() {
h.first = sentAt
h.last = sentAt
h.count = 1
return nil
}
// Equal timestamps are allowed (matches C#: sentAt < LastHeartBeat only).
if sentAt.Before(h.last) {
return ErrOutOfSequence
}
h.last = sentAt
h.count++
return nil
}
func (h *heartbeatHistory) uptime() float64 {
h.mu.Lock()
defer h.mu.Unlock()
if h.first.IsZero() {
return 0
}
minutes := h.last.Sub(h.first).Minutes()
// Single heartbeat (or identical timestamps): treat span as one minute.
if minutes == 0 {
minutes = 1
}
return float64(h.count) / minutes * 100
}
// historyFor returns the per-device history, creating it if needed.
// The store-level lock only covers map insert/lookup; callers then use
// the history's own lock for mutations/reads.
func (s *Store) historyFor(id DeviceID) *heartbeatHistory {
s.mu.Lock()
defer s.mu.Unlock()
h, ok := s.heartbeats[id]
if !ok {
h = &heartbeatHistory{}
s.heartbeats[id] = h
}
return h
}
// AddHeartbeat records a heartbeat for deviceID.
// Returns ErrOutOfSequence if sentAt is earlier than the last accepted beat.
func (s *Store) AddHeartbeat(deviceID DeviceID, sentAt time.Time) error {
return s.historyFor(deviceID).add(sentAt)
}
// Uptime returns uptime percent for deviceID:
//
// (heartbeatCount / minutesBetweenFirstAndLast) * 100
//
// Unknown / never-seen devices return 0. A zero-length span is treated as 1 minute.
func (s *Store) Uptime(deviceID DeviceID) float64 {
s.mu.Lock()
h, ok := s.heartbeats[deviceID]
s.mu.Unlock()
if !ok {
return 0
}
return h.uptime()
}