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.
This commit is contained in:
MHSanaei
2026-07-14 23:50:34 +02:00
parent a77c365fe4
commit ade3a8f870
2 changed files with 30 additions and 1 deletions
+1 -1
View File
@@ -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
View File
@@ -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()
}