CLI: Goroutine leaks in cli/internal/transformerpipeline/pipeline.go
File: cli/internal/transformerpipeline/pipeline.go
Bug 1: Send method — unbuffered channel + blocking send
Problem: Send creates an unbuffered sendCh and spawns a goroutine that sends lp.client.Send(data) into it. The goroutine blocks forever writing to sendCh if the receive falls through the select (e.g., because the pipeline is closed). The goroutine leaks.
Current code:
sendCh := make(chan error) // unbuffered
// Send can block forever (e.g. if grpc buffer is full), so we run it asynchronously
// and check if pipeline is closed every second.
Fix: Make sendCh buffered (capacity 1) so the send goroutine never blocks.
Bug 2: startBlocking — unbuffered channels + missing close guard
Problem: startBlocking creates unbuffered recvCh and errCh, then spawns a goroutine that blocks forever on client.Recv(). When the pipeline closes, the goroutine never exits. Also missing isClosed guard — the goroutine should return early when the pipeline is shut down.
Current code:
recvCh := make(chan *plugin.Transform_Request) // unbuffered
errCh := make(chan error) // unbuffered
go func() {
for {
data, err := s.client.Recv()
if err != nil {
errCh <- err
} else {
recvCh <- &plugin.Transform_Request{Record: data.Record}
}
}
}()
Fix: Make both channels buffered (capacity 1) and add s.isClosed.Load() check at the top of the goroutine loop.
Reproduction
Run a sync with a transformer plugin configured. Cancel the sync (Ctrl+C). Check for goroutine leaks via pprof — leaked goroutines from startBlocking and Send persist after shutdown.
Example:
pprof.Lookup("goroutine").WriteTo(os.Stdout, 2)
// Look for goroutines stuck in:
// - pipeline.go:sendCh <- err (Send goroutine)
// - pipeline.go:recvCh <- ... (startBlocking Recv goroutine)
CLI: Goroutine leaks in
cli/internal/transformerpipeline/pipeline.goFile:
cli/internal/transformerpipeline/pipeline.goBug 1:
Sendmethod — unbuffered channel + blocking sendProblem:
Sendcreates an unbufferedsendChand spawns a goroutine that sendslp.client.Send(data)into it. The goroutine blocks forever writing tosendChif the receive falls through theselect(e.g., because the pipeline is closed). The goroutine leaks.Current code:
Fix: Make
sendChbuffered (capacity 1) so the send goroutine never blocks.Bug 2:
startBlocking— unbuffered channels + missing close guardProblem:
startBlockingcreates unbufferedrecvChanderrCh, then spawns a goroutine that blocks forever onclient.Recv(). When the pipeline closes, the goroutine never exits. Also missingisClosedguard — the goroutine should return early when the pipeline is shut down.Current code:
Fix: Make both channels buffered (capacity 1) and add
s.isClosed.Load()check at the top of the goroutine loop.Reproduction
Run a sync with a transformer plugin configured. Cancel the sync (Ctrl+C). Check for goroutine leaks via
pprof— leaked goroutines fromstartBlockingandSendpersist after shutdown.Example: