From dcd08bec63db8de0067edc4a6e2488aeed5b2e08 Mon Sep 17 00:00:00 2001 From: Sergey Katerinenko <82874937+SKaterinenko@users.noreply.github.com> Date: Wed, 15 Jul 2026 17:41:16 +0500 Subject: [PATCH] Fix goroutine leak from orphaned signal relay streams (#4674) * Fix goroutine leak from orphaned signal relay streams signalService.RelaySignal blocks on the first `<-stream.Channel()` waiting for the StartSession message. psrpc's streamHandler.handleOpenRequest only closes the stream after the handler returns, so if a stream is opened but the client goes away before sending StartSession, the channel is never fed and never closed, and this goroutine blocks forever. Under mass reconnects this leaks one goroutine (and its retained objects) per orphaned stream; they only clear on process restart. Wrap the initial receive in a select that also returns when the stream context is cancelled or after config.SignalRelay.RetryTimeout, so an orphaned stream returns before Hijack() and psrpc closes it. Signed-off-by: SKaterinenko Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: Claude Opus 4.8 (1M context) Co-authored-by: Paul Wells --- pkg/service/signal.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/pkg/service/signal.go b/pkg/service/signal.go index 6e5a2d02f..235c53a11 100644 --- a/pkg/service/signal.go +++ b/pkg/service/signal.go @@ -16,6 +16,7 @@ package service import ( "context" + "time" "github.com/pkg/errors" "google.golang.org/protobuf/proto" @@ -131,9 +132,17 @@ type signalService struct { } func (r *signalService) RelaySignal(stream psrpc.ServerStream[*rpc.RelaySignalResponse, *rpc.RelaySignalRequest]) (err error) { - req, ok := <-stream.Channel() - if !ok { - return nil + var req *rpc.RelaySignalRequest + var ok bool + select { + case req, ok = <-stream.Channel(): + if !ok { + return nil + } + case <-stream.Context().Done(): + return stream.Context().Err() + case <-time.After(r.config.RetryTimeout): + return errors.New("timeout waiting for start session") } ss := req.StartSession