aboutsummaryrefslogtreecommitdiff
path: root/hotline/stats.go
diff options
context:
space:
mode:
authorJeff Halter <868228+jhalter@users.noreply.github.com>2026-05-28 16:22:24 -0700
committerJeff Halter <868228+jhalter@users.noreply.github.com>2026-05-28 16:22:24 -0700
commitb772019454ebb804c313e717ae98eb68430e780e (patch)
treea12ffc1e5de4d7e1612edaea03174043a6c55784 /hotline/stats.go
parent2f46f87177234070044b5304ca3e0db471699ad8 (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.go98
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,
}
}