Fix StreamRow/Close race condition with atomic coordination

Fixes issue #4790 where StreamRow() could panic with "send on closed
channel" when called concurrently with Close().

Solution:
- Added atomic.Bool closed flag to track Result state
- StreamRow() checks closed flag before sending to prevent most races
- Added defer/recover to gracefully handle edge case where Close()
  occurs between check and send
- Rows sent after close are silently dropped (safe behavior)

This approach avoids deadlocks by not holding locks during channel
send operations, while using atomic operations for coordination.

Test now passes consistently without panics.

Fixes #4790

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Nathan Wallace
2025-11-16 16:22:01 -05:00
parent 94633db3f9
commit cf47ffb77e

View File

@@ -2,14 +2,17 @@ package queryresult
import (
"sync"
"sync/atomic"
"github.com/turbot/pipe-fittings/v2/queryresult"
)
// Result wraps queryresult.Result[TimingResultStream] with idempotent Close()
// and synchronization to prevent race between StreamRow and Close
type Result struct {
*queryresult.Result[TimingResultStream]
closeOnce sync.Once
closed atomic.Bool
}
func NewResult(cols []*queryresult.ColumnDef) *Result {
@@ -21,12 +24,25 @@ func NewResult(cols []*queryresult.ColumnDef) *Result {
// Close closes the row channel in an idempotent manner
func (r *Result) Close() {
r.closeOnce.Do(func() {
r.closed.Store(true)
r.Result.Close()
})
}
// StreamRow sends a row to the result stream
// StreamRow wraps the underlying StreamRow with synchronization to prevent panic on closed channel
func (r *Result) StreamRow(row []interface{}) {
// Check if already closed - if so, silently drop the row
if r.closed.Load() {
return
}
// Use recover to gracefully handle the race where channel closes between check and send
defer func() {
if r := recover(); r != nil {
// Channel was closed between our check and the send - this is okay, just drop the row
}
}()
r.Result.StreamRow(row)
}