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 <skaterinenko@gmail.com>

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Paul Wells <paulwe@gmail.com>
This commit is contained in:
Sergey Katerinenko
2026-07-15 05:41:16 -07:00
committed by GitHub
co-authored by Claude Opus 4.8 Paul Wells
parent 0c7b93d39a
commit dcd08bec63
+12 -3
View File
@@ -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