-
Notifications
You must be signed in to change notification settings - Fork 9
Parallel tasks don't release terminal after completion — signal handler goroutine leak #177
Description
Description
When running parallel tasks (e.g., rr test with parallel: [test-backend, test-frontend]), the process does not return control to the terminal after all tasks complete successfully. The terminal appears to hang
indefinitely.
Single tasks (rr test-backend, rr test-frontend) work fine and exit cleanly.
Root Cause
RunParallelTask in /internal/cli/parallel.go (~line 186) registers a signal handler but never cleans it up:
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigChan
cancel()
}()
- signal.Notify is called but signal.Stop is never called after execution completes
- The goroutine blocks forever on <-sigChan since no signal arrives during normal execution
- The active signal registration keeps Go's runtime intercepting SIGINT/SIGTERM instead of allowing default process termination
The non-parallel workflow path in /internal/cli/workflow.go handles this correctly:
func (w *WorkflowContext) Close() {
w.closeOnce.Do(func() {
signal.Stop(w.signalChan)
close(w.signalChan)
})
}
The same missing cleanup also exists in:
- /internal/cli/run.go in runRepeated() (~line 419)
- /internal/cli/task.go in runTaskRepeated() (~line 972)
Suggested Fix
Add signal.Stop and close after the orchestrator returns, matching the pattern in workflow.go:
result, err := orchestrator.Run(ctx)
signal.Stop(sigChan)
close(sigChan)
And update the goroutine to handle the closed channel:
go func() {
sig, ok := <-sigChan
if !ok {
return
}
_ = sig
cancel()
}()
Apply the same fix to run.go and task.go.
Reproduction
.rr.yaml:
hosts: [host-1, host-2]
tasks:
task-a:
run: echo "done A"
task-b:
run: echo "done B"
test:
parallel: [task-a, task-b]
fail_fast: false
rr test # hangs after both tasks complete
Environment
- rr v0.19.0
- macOS (Darwin 25.2.0)
- Go 1.22+