260 lines
7.0 KiB
Go
260 lines
7.0 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))
|
|
}
|
|
// Use map for O(n) dedup instead of O(n²) linear search
|
|
existMap := make(map[string]struct{}, len(ips)+len(ipList))
|
|
for _, existing := range ips {
|
|
existMap[existing] = struct{}{}
|
|
}
|
|
for _, ip := range ipList {
|
|
if _, exists := existMap[ip]; !exists {
|
|
ips = append(ips, ip)
|
|
existMap[ip] = struct{}{}
|
|
}
|
|
}
|
|
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
|
|
}
|