Jelajahi Sumber

fix: hold MapActionBindingsLock while building the default dashboard

buildDefaultDashboard ranged over MapActionBindings without the read lock
that every other reader takes. When a config or entity reload rebuilt the
map at the same time (e.g. during Init), the Go runtime aborted with
"fatal error: concurrent map iteration and map write" and took the whole
server down.

Adds a test that builds the default dashboard while the action map is
rebuilt; under -race it reported the race before this change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
alper 3 minggu lalu
induk
melakukan
cc6a83bfbc

+ 4 - 0
service/internal/api/dashboards.go

@@ -148,6 +148,8 @@ func buildDefaultDashboard(rr *DashboardRenderRequest) *apiv1.Dashboard {
 		Contents: make([]*apiv1.DashboardComponent, 0),
 		Contents: make([]*apiv1.DashboardComponent, 0),
 	}
 	}
 
 
+	rr.ex.MapActionBindingsLock.RLock()
+
 	for _, binding := range rr.ex.MapActionBindings {
 	for _, binding := range rr.ex.MapActionBindings {
 		if !defaultBindingEligibleForDashboard(rr, binding) {
 		if !defaultBindingEligibleForDashboard(rr, binding) {
 			continue
 			continue
@@ -158,6 +160,8 @@ func buildDefaultDashboard(rr *DashboardRenderRequest) *apiv1.Dashboard {
 		}
 		}
 	}
 	}
 
 
+	rr.ex.MapActionBindingsLock.RUnlock()
+
 	if len(fieldset.Contents) > 0 {
 	if len(fieldset.Contents) > 0 {
 		fieldset.Contents = sortDashboardComponents(fieldset.Contents)
 		fieldset.Contents = sortDashboardComponents(fieldset.Contents)
 		db.Contents = append(db.Contents, fieldset)
 		db.Contents = append(db.Contents, fieldset)

+ 44 - 0
service/internal/api/dashboards_race_test.go

@@ -0,0 +1,44 @@
+package api
+
+import (
+	"sync"
+	"testing"
+
+	"github.com/OliveTin/OliveTin/internal/executor"
+)
+
+// TestBuildDefaultDashboardConcurrentWithRebuild guards against a crash when the
+// default dashboard is built (e.g. by Init) while a config or entity reload
+// rebuilds the action map. Without the read lock in buildDefaultDashboard, the
+// Go runtime aborts with "concurrent map iteration and map write"; run with
+// -race to see the data race directly.
+func TestBuildDefaultDashboardConcurrentWithRebuild(t *testing.T) {
+	cfg, _, adminUser := buildViewPermissionTestConfig(t)
+	ex := executor.DefaultExecutor(cfg)
+	ex.RebuildActionMap()
+
+	rr := &DashboardRenderRequest{
+		AuthenticatedUser: adminUser,
+		cfg:               cfg,
+		ex:                ex,
+	}
+
+	var wg sync.WaitGroup
+	wg.Add(2)
+
+	go func() {
+		defer wg.Done()
+		for i := 0; i < 200; i++ {
+			ex.RebuildActionMap()
+		}
+	}()
+
+	go func() {
+		defer wg.Done()
+		for i := 0; i < 200; i++ {
+			buildDefaultDashboard(rr)
+		}
+	}()
+
+	wg.Wait()
+}