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>
This commit is contained in:
2026-06-13 22:58:59 +02:00
parent 23bccf5434
commit 6953899045
3 changed files with 66 additions and 7 deletions
+4 -4
View File
@@ -355,9 +355,9 @@ func (s *Server) deployHandler(w http.ResponseWriter, r *http.Request, name stri
// renders those sections.
if alreadyDeployed {
// App-to-app integrations
if meta.HasIntegrations() && s.integrationMgr != nil {
if im := s.integrationMgr.Load(); meta.HasIntegrations() && im != nil {
data["HasIntegrations"] = true
data["Integrations"] = s.integrationMgr.ListForProvider(meta.Slug)
data["Integrations"] = im.ListForProvider(meta.Slug)
}
// Geo-restriction per-app data
@@ -1430,8 +1430,8 @@ func (s *Server) syncFileBrowserMounts(resetDBOnChange bool) {
}
// Re-apply active integrations into config.yaml (before container restart)
if s.integrationMgr != nil {
s.integrationMgr.ReapplyConfigForTarget("filebrowser")
if im := s.integrationMgr.Load(); im != nil {
im.ReapplyConfigForTarget("filebrowser")
}
// Generate and write compose (includes config.yaml mount)