Selaa lähdekoodia

fix(client): stop holding the inbound-lock registry mutex while waiting on one inbound

lockInbound acquired the global registry mutex and then blocked on the
per-inbound mutex without releasing the registry first. A slow client
operation holding one inbound's mutex (for example a bulk delete pushing to
an unreachable node) made the next waiter park on that inbound while still
holding the registry mutex, which in turn blocked lockInbound for every
other inbound — freezing client mutations panel-wide. Release the registry
mutex before taking the per-inbound lock.
MHSanaei 1 päivä sitten
vanhempi
sitoutus
ade3a8f870

+ 1 - 1
internal/web/service/client_locks.go

@@ -28,12 +28,12 @@ var (
 
 func lockInbound(inboundId int) *sync.Mutex {
 	inboundMutationLocksMu.Lock()
-	defer inboundMutationLocksMu.Unlock()
 	m, ok := inboundMutationLocks[inboundId]
 	if !ok {
 		m = &sync.Mutex{}
 		inboundMutationLocks[inboundId] = m
 	}
+	inboundMutationLocksMu.Unlock()
 	m.Lock()
 	return m
 }

+ 29 - 0
internal/web/service/client_locks_test.go

@@ -0,0 +1,29 @@
+package service
+
+import (
+	"testing"
+	"time"
+)
+
+// lockInbound must not hold the global registry mutex while it waits on a busy
+// inbound's own mutex, otherwise one slow client operation on a single inbound
+// freezes client mutations on every other inbound panel-wide.
+func TestLockInboundReleasesRegistryMutexWhileWaiting(t *testing.T) {
+	const id = 990006
+	held := lockInbound(id)
+
+	parked := make(chan struct{})
+	go func() {
+		close(parked)
+		lockInbound(id).Unlock()
+	}()
+	<-parked
+	time.Sleep(50 * time.Millisecond)
+
+	if !inboundMutationLocksMu.TryLock() {
+		held.Unlock()
+		t.Fatal("registry mutex is held while a lockInbound caller waits on a busy inbound")
+	}
+	inboundMutationLocksMu.Unlock()
+	held.Unlock()
+}