{"record":{"id":"87e6bac563191cef","repo":"netbirdio/netbird","slug":"already-waiting-for-peer-to-come-online","errorCode":null,"errorMessage":"already waiting for peer to come online","messagePattern":"already waiting for peer to come online","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"shared/relay/client/peer_subscription.go","lineNumber":86,"sourceCode":"\tfor _, peerID := range peersID {\n\t\tif _, ok := s.listenForOfflinePeers[peerID]; ok {\n\t\t\trelevantPeers = append(relevantPeers, peerID)\n\t\t}\n\t}\n\ts.mu.Unlock()\n\n\tif len(relevantPeers) > 0 {\n\t\ts.offlineCallback(relevantPeers)\n\t}\n}\n\n// WaitToBeOnlineAndSubscribe waits for a specific peer to come online and subscribes to its state changes.\nfunc (s *PeersStateSubscription) WaitToBeOnlineAndSubscribe(ctx context.Context, peerID messages.PeerID) error {\n\t// Check if already waiting for this peer\n\ts.mu.Lock()\n\tif _, exists := s.waitingPeers[peerID]; exists {\n\t\ts.mu.Unlock()\n\t\treturn errors.New(\"already waiting for peer to come online\")\n\t}\n\n\t// Create a channel to wait for the peer to come online\n\twaitCh := make(chan struct{}, 1)\n\ts.waitingPeers[peerID] = waitCh\n\ts.listenForOfflinePeers[peerID] = struct{}{}\n\ts.mu.Unlock()\n\n\tif err := s.subscribeStateChange(peerID); err != nil {\n\t\ts.log.Errorf(\"failed to subscribe to peer state: %s\", err)\n\t\ts.mu.Lock()\n\t\tif ch, exists := s.waitingPeers[peerID]; exists && ch == waitCh {\n\t\t\tclose(waitCh)\n\t\t\tdelete(s.waitingPeers, peerID)\n\t\t\tdelete(s.listenForOfflinePeers, peerID)\n\t\t}\n\t\ts.mu.Unlock()\n\t\treturn err","sourceCodeStart":68,"sourceCodeEnd":104,"githubUrl":"https://github.com/netbirdio/netbird/blob/93e97f4bf1ad715072dcb3fb6cdb1763431b5a9c/shared/relay/client/peer_subscription.go#L68-L104","documentation":"PeersStateSubscription tracks one in-flight wait per peer in its waitingPeers map; WaitToBeOnlineAndSubscribe returns this error when a wait for the same peerID is already pending, instead of double-subscribing. The one-subscription-per-peer behavior is by design, so the error signals a duplicate wait rather than a broken state.","triggerScenarios":"Two goroutines calling WaitToBeOnlineAndSubscribe for the same offline peer (e.g. racing connection attempts), or a retry loop starting a new wait before the previous one completed and cleaned up its entry.","commonSituations":"Concurrent dials to the same peer in tests or custom clients; reconnect storms re-issuing waits; missing cancellation of the first wait's context.","solutions":["Track outstanding waits per peer and skip or join the existing one","Cancel the first wait (its context) and let it clean up before starting another","Treat the error as benign idempotence feedback rather than a failure"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"if inflight, ok := myWaits[peerID]; ok {\n\t<-inflight.done // join the existing wait instead of starting a second one\n\treturn nil\n}\nerr := subscription.WaitToBeOnlineAndSubscribe(ctx, peerID)","typeGuard":null,"tryCatchPattern":"if err := sub.WaitToBeOnlineAndSubscribe(ctx, peerID); err != nil {\n\tif err.Error() == \"already waiting for peer to come online\" {\n\t\treturn nil // benign: a wait is already active for this peer\n\t}\n\treturn err\n}","preventionTips":["Keep your own per-peer wait registry so calls are issued once","Cancel the previous wait's context before retrying","Serialize connection attempts per peer with a mutex or single worker"],"tags":["relay","peer","concurrency","subscription"],"backgroundTag":null,"analyzedSha":"93e97f4bf1ad715072dcb3fb6cdb1763431b5a9c","analyzedAt":"2026-08-16T03:09:19.136Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}