diff options
| author | Jeff Halter <868228+jhalter@users.noreply.github.com> | 2026-05-28 16:22:24 -0700 |
|---|---|---|
| committer | Jeff Halter <868228+jhalter@users.noreply.github.com> | 2026-05-28 16:22:24 -0700 |
| commit | b772019454ebb804c313e717ae98eb68430e780e (patch) | |
| tree | a12ffc1e5de4d7e1612edaea03174043a6c55784 /hotline/stats.go | |
| parent | 2f46f87177234070044b5304ca3e0db471699ad8 (diff) | |
Refactor Stats to typed keys and fix peak-tracking race
Replace the map-backed Stats counter with a fixed [numStats]int array
indexed by a new StatKey enum, eliminating the hand-maintained map
initialization and the parallel string-keyed Values() map.
Fix a check-then-act race in the connection peak tracking: the old
Get-then-Set across two lock acquisitions could let concurrent
connections clobber each other's update. The new atomic Max method does
the compare-and-set under a single lock.
Values() now returns a typed StatValues struct whose JSON tags preserve
the existing /api/v1/stats wire format.
Diffstat (limited to 'hotline/stats.go')
| -rw-r--r-- | hotline/stats.go | 98 |
1 files changed, 60 insertions, 38 deletions
diff --git a/hotline/stats.go b/hotline/stats.go index 9731601..f272f5b 100644 --- a/hotline/stats.go +++ b/hotline/stats.go @@ -5,9 +5,12 @@ import ( "time" ) -// Stat counter keys +// StatKey identifies a single stat counter. +type StatKey int + +// Stat counter keys. numStats must remain last; it sizes the counter array. const ( - StatCurrentlyConnected = iota + StatCurrentlyConnected StatKey = iota StatDownloadsInProgress StatUploadsInProgress StatWaitingDownloads @@ -15,40 +18,45 @@ const ( StatConnectionCounter StatDownloadCounter StatUploadCounter + + numStats ) type Counter interface { - Increment(keys ...int) - Decrement(key int) - Set(key, val int) - Get(key int) int - Values() map[string]interface{} + Increment(keys ...StatKey) + Decrement(keys ...StatKey) + Set(key StatKey, val int) + Max(key StatKey, val int) + Get(key StatKey) int + Values() StatValues +} + +// StatValues is a point-in-time snapshot of all counters. Its JSON tags define +// the wire format served at GET /api/v1/stats. +type StatValues struct { + CurrentlyConnected int `json:"CurrentlyConnected"` + DownloadsInProgress int `json:"DownloadsInProgress"` + UploadsInProgress int `json:"UploadsInProgress"` + WaitingDownloads int `json:"WaitingDownloads"` + ConnectionPeak int `json:"ConnectionPeak"` + ConnectionCounter int `json:"ConnectionCounter"` + DownloadCounter int `json:"DownloadCounter"` + UploadCounter int `json:"UploadCounter"` + Since time.Time `json:"Since"` } type Stats struct { - stats map[int]int + stats [numStats]int since time.Time mu sync.RWMutex } func NewStats() *Stats { - return &Stats{ - since: time.Now(), - stats: map[int]int{ - StatCurrentlyConnected: 0, - StatDownloadsInProgress: 0, - StatUploadsInProgress: 0, - StatWaitingDownloads: 0, - StatConnectionPeak: 0, - StatDownloadCounter: 0, - StatUploadCounter: 0, - StatConnectionCounter: 0, - }, - } + return &Stats{since: time.Now()} } -func (s *Stats) Increment(keys ...int) { +func (s *Stats) Increment(keys ...StatKey) { s.mu.Lock() defer s.mu.Unlock() @@ -57,42 +65,56 @@ func (s *Stats) Increment(keys ...int) { } } -func (s *Stats) Decrement(key int) { +func (s *Stats) Decrement(keys ...StatKey) { s.mu.Lock() defer s.mu.Unlock() - if s.stats[key] > 0 { - s.stats[key]-- + for _, key := range keys { + if s.stats[key] > 0 { + s.stats[key]-- + } } } -func (s *Stats) Set(key, val int) { +func (s *Stats) Set(key StatKey, val int) { s.mu.Lock() defer s.mu.Unlock() s.stats[key] = val } -func (s *Stats) Get(key int) int { +// Max sets key to val only if val is greater than the current value. The +// compare-and-set happens under a single lock so concurrent callers cannot +// clobber each other's update. +func (s *Stats) Max(key StatKey, val int) { + s.mu.Lock() + defer s.mu.Unlock() + + if val > s.stats[key] { + s.stats[key] = val + } +} + +func (s *Stats) Get(key StatKey) int { s.mu.RLock() defer s.mu.RUnlock() return s.stats[key] } -func (s *Stats) Values() map[string]interface{} { +func (s *Stats) Values() StatValues { s.mu.RLock() defer s.mu.RUnlock() - return map[string]interface{}{ - "CurrentlyConnected": s.stats[StatCurrentlyConnected], - "DownloadsInProgress": s.stats[StatDownloadsInProgress], - "UploadsInProgress": s.stats[StatUploadsInProgress], - "WaitingDownloads": s.stats[StatWaitingDownloads], - "ConnectionPeak": s.stats[StatConnectionPeak], - "ConnectionCounter": s.stats[StatConnectionCounter], - "DownloadCounter": s.stats[StatDownloadCounter], - "UploadCounter": s.stats[StatUploadCounter], - "Since": s.since, + return StatValues{ + CurrentlyConnected: s.stats[StatCurrentlyConnected], + DownloadsInProgress: s.stats[StatDownloadsInProgress], + UploadsInProgress: s.stats[StatUploadsInProgress], + WaitingDownloads: s.stats[StatWaitingDownloads], + ConnectionPeak: s.stats[StatConnectionPeak], + ConnectionCounter: s.stats[StatConnectionCounter], + DownloadCounter: s.stats[StatDownloadCounter], + UploadCounter: s.stats[StatUploadCounter], + Since: s.since, } } |