Files
nmon/internal/poller/devloop.go
jleung 1ed97a540d
Some checks failed
ci / lint (push) Has been cancelled
ci / test (ubuntu-24.04-arm) (push) Has been cancelled
ci / test (ubuntu-latest) (push) Has been cancelled
ci / postgres (push) Has been cancelled
ci / openapi (push) Has been cancelled
Maintainability cleanup (collection paths): 63 findings to 0; fix flaky poller tests and a production timer race; M2 gate passed
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lx9FNLG2CqeBbVGGd9bFsP
2026-10-22 19:25:08 +00:00

127 lines
3.4 KiB
Go

package poller
import (
"context"
"errors"
"sync"
"sync/atomic"
"time"
"github.com/v10networks/nmon/internal/selfmon"
"github.com/v10networks/nmon/internal/store"
)
var errDeadline = errors.New("poller: cycle deadline")
// devLoop is the pair of goroutines serving one device.
type devLoop struct {
p *Poller
id int64
slug string
cancel context.CancelFunc
tgt atomic.Pointer[target]
wake chan struct{} // the target changed
kick chan struct{} // poll the primary group now
duration selfmon.Gauge
missed, skipped, mismatch, reboots, discontins selfmon.Counter
mu sync.Mutex // guards st
st devState
// Touched only by the SNMP goroutine.
snmpUp *bool // last state logged
persisted string
up uptimeTracker
disc map[string]int64 // last ifCounterDiscontinuityTime (ticks) by interface ID
}
func (l *devLoop) setTarget(t *target) {
l.tgt.Store(t)
select {
case l.wake <- struct{}{}:
default:
}
}
func (l *devLoop) status() DeviceStatus {
t := l.tgt.Load()
l.mu.Lock()
defer l.mu.Unlock()
s := DeviceStatus{
Device: l.slug, DeviceID: l.id, State: l.st.state, Varbinds: l.st.varbinds,
ConsecutiveFailures: l.st.consecFail, MissedDeadlines: l.st.consecMissed,
LastCycleDuration: l.st.lastDur, LastCycle: l.st.lastCycle, NextCycle: l.st.next, LastError: l.st.lastErr,
}
if t != nil {
s.Profile = t.profile
s.Interfaces = t.ifaces
if len(t.groups) > 0 {
s.BaseInterval = t.groups[0].base
s.Interval = l.st.interval(s.BaseInterval)
}
}
return s
}
func (l *devLoop) varbinds() int {
l.mu.Lock()
defer l.mu.Unlock()
return l.st.varbinds
}
func (l *devLoop) effInterval(g *group) time.Duration {
l.mu.Lock()
defer l.mu.Unlock()
return l.st.interval(g.base)
}
func (l *devLoop) failing() bool {
l.mu.Lock()
defer l.mu.Unlock()
return l.st.consecFail > 0
}
// persistState stores a change of poll state, keeping the device's warnings and
// leaving a device in maintenance alone.
func (l *devLoop) persistState(ctx context.Context, want string) {
if want == l.persisted {
return
}
d, err := l.p.o.Store.GetDeviceByID(ctx, l.id)
if err != nil {
l.p.log.Warn("read poll state", "device", l.slug, "err", err)
return
}
if d.PollState != store.PollMaintenance && d.PollState != want {
if err := l.p.o.Store.SetPollState(ctx, l.id, want, []string(d.Warnings)); err != nil {
l.p.log.Warn("persist poll state", "device", l.slug, "state", want, "err", err)
return
}
l.p.log.Info("poll state", "device", l.slug, "state", want)
}
l.persisted = want
}
func (l *devLoop) rebooted(ctx context.Context, ev rebootEvent) {
l.reboots.Inc()
l.p.log.Warn("device rebooted", "device", l.slug, "uptime_before_s", ev.before, "uptime_after_s", ev.after)
err := l.p.o.Store.AppendAudit(ctx, "poller", "device_rebooted", l.slug,
map[string]any{"uptime_seconds": ev.before}, map[string]any{"uptime_seconds": ev.after})
if err != nil {
l.p.log.Warn("audit reboot", "device", l.slug, "err", err)
}
}
func (l *devLoop) discontinuity(ctx context.Context, t *target, ev discEvent) {
if !ev.first {
l.discontins.Inc()
l.p.log.Info("counter discontinuity", "device", l.slug, "iface", ev.ifID, "ticks", ev.ticks)
}
if cs, ok := l.p.o.Store.(store.CounterDiscontinuities); ok {
if err := cs.SetCounterDiscontinuity(ctx, ev.ifID, ev.ticks); err != nil {
l.p.log.Warn("record counter discontinuity", "device", l.slug, "iface", ev.ifID, "err", err)
}
}
}