aboutsummaryrefslogtreecommitdiff
path: root/internal
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 /internal
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 'internal')
-rw-r--r--internal/mobius/api_test.go33
1 files changed, 15 insertions, 18 deletions
diff --git a/internal/mobius/api_test.go b/internal/mobius/api_test.go
index d448b1b..ae9916b 100644
--- a/internal/mobius/api_test.go
+++ b/internal/mobius/api_test.go
@@ -173,19 +173,15 @@ func (m *mockClientMgr) Delete(id hotline.ClientID) {
}
type mockCounter struct {
- vals map[string]interface{}
+ vals hotline.StatValues
}
-func (m *mockCounter) Increment(_ ...int) {}
-func (m *mockCounter) Decrement(_ int) {}
-func (m *mockCounter) Set(_, _ int) {}
-func (m *mockCounter) Get(_ int) int { return 0 }
-func (m *mockCounter) Values() map[string]interface{} {
- if m.vals == nil {
- return map[string]interface{}{}
- }
- return m.vals
-}
+func (m *mockCounter) Increment(_ ...hotline.StatKey) {}
+func (m *mockCounter) Decrement(_ ...hotline.StatKey) {}
+func (m *mockCounter) Set(_ hotline.StatKey, _ int) {}
+func (m *mockCounter) Max(_ hotline.StatKey, _ int) {}
+func (m *mockCounter) Get(_ hotline.StatKey) int { return 0 }
+func (m *mockCounter) Values() hotline.StatValues { return m.vals }
// --- Test helper ---
@@ -651,9 +647,9 @@ func TestShutdownHandler(t *testing.T) {
func TestRenderStats(t *testing.T) {
t.Run("returns JSON stats", func(t *testing.T) {
srv, _, _, counter := newTestAPIServer(t, "")
- counter.vals = map[string]interface{}{
- "connections": 42,
- "downloads": 10,
+ counter.vals = hotline.StatValues{
+ CurrentlyConnected: 42,
+ DownloadCounter: 10,
}
req := httptest.NewRequest(http.MethodGet, "/api/v1/stats", nil)
@@ -666,11 +662,11 @@ func TestRenderStats(t *testing.T) {
var stats map[string]interface{}
err := json.Unmarshal(rr.Body.Bytes(), &stats)
require.NoError(t, err)
- assert.Equal(t, float64(42), stats["connections"])
- assert.Equal(t, float64(10), stats["downloads"])
+ assert.Equal(t, float64(42), stats["CurrentlyConnected"])
+ assert.Equal(t, float64(10), stats["DownloadCounter"])
})
- t.Run("returns empty stats when no data", func(t *testing.T) {
+ t.Run("returns zero-valued stats when no data", func(t *testing.T) {
srv, _, _, _ := newTestAPIServer(t, "")
req := httptest.NewRequest(http.MethodGet, "/api/v1/stats", nil)
@@ -683,6 +679,7 @@ func TestRenderStats(t *testing.T) {
var stats map[string]interface{}
err := json.Unmarshal(rr.Body.Bytes(), &stats)
require.NoError(t, err)
- assert.Empty(t, stats)
+ assert.Equal(t, float64(0), stats["CurrentlyConnected"])
+ assert.Contains(t, stats, "Since")
})
}