From 1b29995ecec132bec6d1b61420375fba6ebb1082 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Sun, 28 Jun 2026 12:45:33 +0200 Subject: [PATCH] [client] Fix blocked status lock via relay manager path (#6547) * peer/status: move relay-state reads off the main mux GetRelayStates held d.mux (RLock) while calling into the relay Manager (RelayStates/RelayConnectError/ServerURLs). Those calls can be slow or block on the relay manager's own locks while it is reconnecting, which kept the central Status mutex held and stalled every peer state writer (UpdatePeerState, ReplaceOfflinePeers, etc.) contending for it. Guard relayMgr/relayStates with a dedicated muxRelays mutex and release it before invoking the relay Manager, so the relay read path no longer contends with the hot peer-state writers on d.mux. * peer/status: clone relay states in nil-manager path Return a cloned snapshot of d.relayStates when relayMgr is nil so callers cannot mutate the shared cached state, matching the non-nil path. --- client/internal/peer/status.go | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/client/internal/peer/status.go b/client/internal/peer/status.go index 3e5c56dd2..e48ac333c 100644 --- a/client/internal/peer/status.go +++ b/client/internal/peer/status.go @@ -192,6 +192,7 @@ func (s *StatusChangeSubscription) Events() chan map[string]RouterState { // Pure read methods take RLock; anything that mutates state takes Lock. type Status struct { mux sync.RWMutex + muxRelays sync.RWMutex peers map[string]State ipToKey map[string]string changeNotify map[string]map[string]*StatusChangeSubscription // map[peerID]map[subscriptionID]*StatusChangeSubscription @@ -244,8 +245,8 @@ func NewRecorder(mgmAddress string) *Status { } func (d *Status) SetRelayMgr(manager *relayClient.Manager) { - d.mux.Lock() - defer d.mux.Unlock() + d.muxRelays.Lock() + defer d.muxRelays.Unlock() d.relayMgr = manager } @@ -906,8 +907,8 @@ func (d *Status) MarkSignalConnected() { } func (d *Status) UpdateRelayStates(relayResults []relay.ProbeResult) { - d.mux.Lock() - defer d.mux.Unlock() + d.muxRelays.Lock() + defer d.muxRelays.Unlock() d.relayStates = relayResults } @@ -1018,24 +1019,26 @@ func (d *Status) GetSignalState() SignalState { // GetRelayStates returns the stun/turn/permanent relay states func (d *Status) GetRelayStates() []relay.ProbeResult { - d.mux.RLock() - defer d.mux.RUnlock() + d.muxRelays.RLock() if d.relayMgr == nil { - return d.relayStates + defer d.muxRelays.RUnlock() + return slices.Clone(d.relayStates) } + relayMgr := d.relayMgr // extend the list of stun, turn servers with the relay server connections relayStates := slices.Clone(d.relayStates) + d.muxRelays.RUnlock() - states := d.relayMgr.RelayStates() + states := relayMgr.RelayStates() if len(states) == 0 { // no relay connection tracked yet; surface configured servers as // unavailable with the real reconnect error when known err := relayClient.ErrRelayClientNotConnected - if connErr := d.relayMgr.RelayConnectError(); connErr != nil { + if connErr := relayMgr.RelayConnectError(); connErr != nil { err = connErr } - for _, r := range d.relayMgr.ServerURLs() { + for _, r := range relayMgr.ServerURLs() { relayStates = append(relayStates, relay.ProbeResult{ URI: r, Err: err,