From ade3a8f87033ddc8a684045866863b8374353f28 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Tue, 14 Jul 2026 23:50:34 +0200 Subject: [PATCH] fix(client): stop holding the inbound-lock registry mutex while waiting on one inbound MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- internal/web/service/client_locks.go | 2 +- internal/web/service/client_locks_test.go | 29 +++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 internal/web/service/client_locks_test.go diff --git a/internal/web/service/client_locks.go b/internal/web/service/client_locks.go index 3ea0f9beb..61f26c0b0 100644 --- a/internal/web/service/client_locks.go +++ b/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 } diff --git a/internal/web/service/client_locks_test.go b/internal/web/service/client_locks_test.go new file mode 100644 index 000000000..a6d6daeb9 --- /dev/null +++ b/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() +}