Files
felhom-controller/controller/internal/web/integrationmgr_race_test.go
T
admin 6953899045 fix(M25): atomic.Pointer for Server.integrationMgr (constructor-goroutine race)
NewServer launches the SyncFileBrowserMounts goroutine (reads integrationMgr)
from the constructor, BEFORE main.go's SetIntegrationManager write — so the
init-only happens-before that covers the other Set* fields does NOT hold here,
making it a genuine data race (handlers.go:358/360/1433 reads vs server.go:162
write). Converted the field to atomic.Pointer[integrations.Manager]; setter
Stores, all 3 readers Load(). Regression test reproduces the concurrent access
(clean under -race; flags on the pre-fix plain-pointer field).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 22:58:59 +02:00

55 lines
1.6 KiB
Go

package web
import (
"sync"
"testing"
"gitea.dooplex.hu/admin/felhom-controller/internal/integrations"
)
// TestIntegrationManagerNoRace is a regression test for M25. NewServer launches
// the SyncFileBrowserMounts goroutine (which reads s.integrationMgr) from inside
// the constructor, BEFORE main.go calls SetIntegrationManager. With a plain
// pointer field that write/read pair is a data race; this test reproduces the
// concurrent access and must run clean under `go test -race`.
//
// On the pre-fix code (plain `integrationMgr *integrations.Manager`, written via
// `s.integrationMgr = mgr` and read via `s.integrationMgr`) the race detector
// flags this. With the atomic.Pointer field it is clean. Run with:
//
// go test -race ./internal/web/ -run IntegrationManagerNoRace
func TestIntegrationManagerNoRace(t *testing.T) {
var s Server // zero value: atomic.Pointer field is usable as-is
const iters = 2000
var wg sync.WaitGroup
// Writer — mirrors main.go calling SetIntegrationManager after construction.
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < iters; i++ {
s.SetIntegrationManager(&integrations.Manager{})
}
}()
// Reader — mirrors the constructor-launched SyncFileBrowserMounts goroutine
// reading the field concurrently with the write above.
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < iters; i++ {
_ = s.integrationMgr.Load()
}
}()
wg.Wait()
// And the value round-trips.
m := &integrations.Manager{}
s.SetIntegrationManager(m)
if s.integrationMgr.Load() != m {
t.Fatal("SetIntegrationManager/Load round-trip mismatch")
}
}