@@ -28,6 +28,7 @@ import (
2828 "sync"
2929 "time"
3030
31+ "github.com/devtron-labs/common-lib/async"
3132 "github.com/devtron-labs/devtron/api/bean"
3233 "github.com/gogo/protobuf/proto"
3334 "github.com/grpc-ecosystem/grpc-gateway/runtime"
@@ -45,19 +46,22 @@ type Pump interface {
4546}
4647
4748type PumpImpl struct {
48- logger * zap.SugaredLogger
49+ logger * zap.SugaredLogger
50+ asyncRunnable * async.Runnable
4951}
5052
51- func NewPumpImpl (logger * zap.SugaredLogger ) * PumpImpl {
53+ func NewPumpImpl (logger * zap.SugaredLogger , asyncRunnable * async. Runnable ) * PumpImpl {
5254 return & PumpImpl {
53- logger : logger ,
55+ logger : logger ,
56+ asyncRunnable : asyncRunnable ,
5457 }
5558}
5659
5760func (impl PumpImpl ) StartK8sStreamWithHeartBeat (ctx context.Context , w http.ResponseWriter , isReconnect bool , stream io.ReadCloser , err error ) {
5861 f , ok := w .(http.Flusher )
5962 if ! ok {
6063 http .Error (w , "unexpected server doesnt support streaming" , http .StatusInternalServerError )
64+ return // was missing: f is nil past this point
6165 }
6266
6367 w .Header ().Set ("Transfer-Encoding" , "chunked" )
@@ -81,18 +85,33 @@ func (impl PumpImpl) StartK8sStreamWithHeartBeat(ctx context.Context, w http.Res
8185 return
8286 }
8387 }
84- // heartbeat start
88+ // sync.Once ensures stream.Close is idempotent: the goroutine may call it
89+ // via the ctx.Done path, and the deferred cleanup calls it too.
90+ var closeOnce sync.Once
91+ closeStream := func () { closeOnce .Do (func () { stream .Close () }) }
92+
8593 ticker := time .NewTicker (30 * time .Second )
86- done := make (chan struct {}) // close(done) never blocks, so no buffer needed
94+ done := make (chan struct {})
8795 var mux sync.Mutex
8896
89- go func () {
97+ // WaitGroup restores the happens-before edge that the old `done <- true`
98+ // blocking send provided. wg.Wait() in the defer ensures the goroutine has
99+ // fully exited before this function returns, preventing f.Flush() from being
100+ // called after net/http.finishRequest() recycles the ResponseWriter.
101+ var wg sync.WaitGroup
102+ wg .Add (1 )
103+
104+ // asyncRunnable.Execute wraps fn in a panic-recovering goroutine with metrics.
105+ // wg.Done() is deferred inside fn so it fires even when Execute's recover()
106+ // catches a panic (Go runs inner defers before outer recover).
107+ impl .asyncRunnable .Execute (func () {
108+ defer wg .Done ()
90109 for {
91110 select {
92111 case <- done :
93112 return
94113 case <- ctx .Done ():
95- stream . Close () // unblocks the blocking bufReader.ReadString below
114+ closeStream () // unblocks bufReader.ReadString in the main loop
96115 return
97116 case t := <- ticker .C :
98117 mux .Lock ()
@@ -107,17 +126,20 @@ func (impl PumpImpl) StartK8sStreamWithHeartBeat(ctx context.Context, w http.Res
107126 }
108127 }
109128 }
110- }()
129+ })
130+
111131 defer func () {
132+ close (done ) // unblock goroutine's select
112133 ticker .Stop ()
113- stream .Close () // idempotent: safe to call after goroutine already closed it
114- close (done ) // signals goroutine to exit if still running
134+ wg .Wait () // block until heartbeat goroutine has fully exited;
135+ // only after this does the handler return and net/http
136+ // reclaim the ResponseWriter
137+ closeStream () // safe: goroutine no longer touches stream after wg.Wait()
115138 }()
116139
117140 bufReader := bufio .NewReader (stream )
118141 eof := false
119142 for ! eof {
120- // fast-exit: if ctx expired between reads, return immediately
121143 select {
122144 case <- ctx .Done ():
123145 return
@@ -132,40 +154,36 @@ func (impl PumpImpl) StartK8sStreamWithHeartBeat(ctx context.Context, w http.Res
132154 }
133155 } else if err != nil {
134156 if ctx .Err () != nil {
135- // stream was closed because ctx expired — not an application error
136157 return
137158 }
138159 impl .logger .Errorw ("error in reading buffer string, StartK8sStreamWithHeartBeat" , "err" , err )
139160 return
140161 }
141162 log = strings .TrimSpace (log )
142163 if log == "" {
143- continue // blank line mid-stream: skip without aborting
164+ continue
144165 }
145166 splitLog := strings .SplitN (log , " " , 2 )
146167 if len (splitLog ) < 2 {
147- continue // no space separator: not a valid log line, skip
168+ continue
148169 }
149170 parsedTime , err := time .Parse (time .RFC3339 , splitLog [0 ])
150171 if err != nil {
151- impl .logger .Errorw ("error in writing data over sse " , "err" , err )
152- return
172+ impl .logger .Errorw ("error parsing log timestamp, skipping line " , "err" , err )
173+ continue
153174 }
154175 eventId := strconv .FormatInt (parsedTime .UnixNano (), 10 )
155176 mux .Lock ()
156- if len (splitLog ) == 2 {
157- err = impl .sendEvent ([]byte (eventId ), nil , []byte (splitLog [1 ]), w )
158- }
159- if err == nil {
177+ sendErr := impl .sendEvent ([]byte (eventId ), nil , []byte (splitLog [1 ]), w )
178+ if sendErr == nil {
160179 f .Flush ()
161180 }
162181 mux .Unlock ()
163- if err != nil {
164- impl .logger .Errorw ("error in writing data over sse" , "err" , err )
182+ if sendErr != nil {
183+ impl .logger .Errorw ("error in writing data over sse" , "err" , sendErr )
165184 return
166185 }
167186 }
168- // heartbeat end
169187}
170188
171189func (impl PumpImpl ) StartStreamWithTransformer (w http.ResponseWriter , recv func () (proto.Message , error ), err error , transformer func (interface {}) interface {}) {
0 commit comments