Files
xboard-node/internal/tracker/tracker.go
T

262 lines
6.9 KiB
Go

package tracker
import (
"sync"
"sync/atomic"
"github.com/cedar2025/xboard-node/internal/nlog"
)
// snapshot is an immutable point-in-time view of tracker state.
// It is swapped atomically so readers never block writers.
type snapshot struct {
traffic map[int][2]int64 // userID → [upload, download] delta
aliveIPs map[int]map[string]bool // userID → set of source IPs
online map[int]int // userID → distinct IP count
connCount int
inSpeed int64
outSpeed int64
}
// Tracker computes per-user traffic deltas from cumulative counters
// provided by the kernel, and accumulates totals for panel reporting.
//
// Architecture: the kernel maintains per-user atomic counters and IP sets.
// Each tick, the service calls Process() with cumulative per-user traffic.
// Tracker computes deltas against the previous cycle's values — O(users),
// not O(connections).
//
// Thread safety: Process() acquires mu to update internal state, then
// atomically publishes a new snapshot. All read methods (Flush*,
// CurrentOnline, LogStats, *Speed) read the snapshot lock-free.
// This eliminates contention between the 10s Process tick and the 60s
// flush/push tick.
type Tracker struct {
mu sync.Mutex
// lastSeen stores the cumulative traffic from the previous Process() call.
// Protected by mu — only written by Process.
lastSeen map[int][2]int64 // userID → [upload, download] cumulative
// pending accumulates deltas between flushes.
// Protected by mu — written by Process, drained by FlushTraffic.
pendingTraffic map[int][2]int64
// live holds the current snapshot, swapped atomically.
// Readers load this pointer without any lock.
live atomic.Pointer[snapshot]
// aliveIPsBuf is a reusable buffer for FlushAliveIPs output.
// Avoids allocating a new map+slice every 60s.
aliveIPsBuf map[int][]string
}
func New() *Tracker {
t := &Tracker{
lastSeen: make(map[int][2]int64),
pendingTraffic: make(map[int][2]int64),
aliveIPsBuf: make(map[int][]string),
}
// Publish initial empty snapshot.
t.live.Store(&snapshot{
traffic: make(map[int][2]int64),
aliveIPs: make(map[int]map[string]bool),
online: make(map[int]int),
})
return t
}
// Process computes per-user traffic deltas from cumulative kernel counters.
// Also stores alive IPs and connection count. O(users).
//
// This is the only writer to lastSeen and pendingTraffic.
// After computing deltas, it publishes a new snapshot for lock-free reads.
func (t *Tracker) Process(
cumTraffic map[int][2]int64,
kernelAliveIPs map[int]map[string]bool,
connCount int,
) {
t.mu.Lock()
defer t.mu.Unlock()
var cycleIn, cycleOut int64
for uid, cum := range cumTraffic {
prev := t.lastSeen[uid]
deltaUp := cum[0] - prev[0]
deltaDown := cum[1] - prev[1]
// Guard against counter reset (kernel restart).
if deltaUp < 0 {
deltaUp = cum[0]
}
if deltaDown < 0 {
deltaDown = cum[1]
}
t.lastSeen[uid] = cum
if deltaUp > 0 || deltaDown > 0 {
cur := t.pendingTraffic[uid]
cur[0] += deltaUp
cur[1] += deltaDown
t.pendingTraffic[uid] = cur
cycleOut += deltaUp
cycleIn += deltaDown
}
}
// Compute online from alive IPs.
online := make(map[int]int, len(kernelAliveIPs))
for uid, ips := range kernelAliveIPs {
online[uid] = len(ips)
}
// Publish new snapshot (readers will see this atomically).
t.live.Store(&snapshot{
traffic: copyTrafficMap(t.pendingTraffic),
aliveIPs: kernelAliveIPs, // kernel provides fresh copy each tick
online: online,
connCount: connCount,
inSpeed: cycleIn,
outSpeed: cycleOut,
})
}
// FlushTraffic returns accumulated per-user traffic and resets the pending buffer.
// Lock-free: reads from live snapshot, then acquires mu only to drain.
func (t *Tracker) FlushTraffic() map[int][2]int64 {
t.mu.Lock()
data := t.pendingTraffic
t.pendingTraffic = make(map[int][2]int64, len(data))
t.mu.Unlock()
return data
}
// RestoreTraffic adds traffic back (used when push to panel fails).
func (t *Tracker) RestoreTraffic(data map[int][2]int64) {
t.mu.Lock()
for uid, d := range data {
cur := t.pendingTraffic[uid]
cur[0] += d[0]
cur[1] += d[1]
t.pendingTraffic[uid] = cur
}
t.mu.Unlock()
}
// HasTraffic returns true if there is accumulated traffic to report.
func (t *Tracker) HasTraffic() bool {
t.mu.Lock()
defer t.mu.Unlock()
return len(t.pendingTraffic) > 0
}
// FlushAliveIPs returns per-user alive IPs.
// Reuses internal buffer to avoid allocation every 60s.
func (t *Tracker) FlushAliveIPs() map[int][]string {
s := t.live.Load()
t.mu.Lock()
defer t.mu.Unlock()
// Clear old buffer entries.
for k := range t.aliveIPsBuf {
delete(t.aliveIPsBuf, k)
}
// Fill buffer from snapshot.
for uid, ips := range s.aliveIPs {
buf := t.aliveIPsBuf[uid]
if buf == nil {
buf = make([]string, 0, len(ips))
}
buf = buf[:0]
for ip := range ips {
buf = append(buf, ip)
}
t.aliveIPsBuf[uid] = buf
}
return t.aliveIPsBuf
}
// CurrentOnline returns a snapshot copy of user_id → device count (distinct IPs).
// Lock-free: reads from live snapshot.
func (t *Tracker) CurrentOnline() map[int]int {
s := t.live.Load()
cp := make(map[int]int, len(s.online))
for k, v := range s.online {
cp[k] = v
}
return cp
}
// RestoreAliveIPs merges alive IPs back in (used when push to panel fails).
// Note: this operates on the buffer, which will be overwritten next Process().
func (t *Tracker) RestoreAliveIPs(data map[int][]string) {
t.mu.Lock()
defer t.mu.Unlock()
for uid, ipList := range data {
ips := t.aliveIPsBuf[uid]
if ips == nil {
ips = make([]string, 0, len(ipList))
}
for _, ip := range ipList {
// Dedup check
found := false
for _, existing := range ips {
if existing == ip {
found = true
break
}
}
if !found {
ips = append(ips, ip)
}
}
t.aliveIPsBuf[uid] = ips
}
}
// LogStats logs current tracking statistics.
// Lock-free: reads from live snapshot.
func (t *Tracker) LogStats() {
s := t.live.Load()
nlog.TrackerStats(s.connCount, len(s.online))
}
// ActiveConnections returns the last observed active connection count.
// Lock-free: reads from live snapshot.
func (t *Tracker) ActiveConnections() int {
return t.live.Load().connCount
}
// TotalConnections is deprecated — no longer tracked per-connection.
// Returns 0 for backward compatibility.
func (t *Tracker) TotalConnections() int64 {
return 0
}
// InboundSpeed returns the last observed inbound (download) speed in bytes/second.
// Lock-free: reads from live snapshot.
func (t *Tracker) InboundSpeed() int64 {
return t.live.Load().inSpeed / 10
}
// OutboundSpeed returns the last observed outbound (upload) speed in bytes/second.
// Lock-free: reads from live snapshot.
func (t *Tracker) OutboundSpeed() int64 {
return t.live.Load().outSpeed / 10
}
// copyTrafficMap creates a shallow copy of the traffic map.
func copyTrafficMap(src map[int][2]int64) map[int][2]int64 {
dst := make(map[int][2]int64, len(src))
for k, v := range src {
dst[k] = v
}
return dst
}