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() +}