Skip to content

Commit e764212

Browse files
craig[bot]mw5h
andcommitted
Merge #160348
160348: rowexec: fix deadlock when processors panic during execution r=mw5h a=mw5h Before this change, when the sampleAggregator or sampler processors panicked during row processing, cleanup code (ConsumerClosed() and ProducerDone()) was never executed. This left producer goroutines blocked indefinitely on channel sends, which prevented the flow from completing. During cluster drain operations, this caused the drain to hang indefinitely waiting for flows to finish. This change adds deferred cleanup to both processors' Run() methods, ensuring that ConsumerClosed() is called even when a panic occurs. This unblocks stuck producers and allows panics to be properly recovered without causing deadlocks. A new test verifies the fix by injecting a panic via testing knob and confirming that producer goroutines complete successfully. Fixes: #160337 Release note (bug fix): Fixed a deadlock that could occur when a statistics creation task panicked. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Matt White <matt.white@cockroachlabs.com>
2 parents 59ac59e + a5197e0 commit e764212

4 files changed

Lines changed: 211 additions & 18 deletions

File tree

pkg/sql/execinfra/server_config.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,10 @@ type TestingKnobs struct {
348348
// TableReaderStartScanCb, when non-nil, will be called whenever the
349349
// TableReader processor starts its scan.
350350
TableReaderStartScanCb func()
351+
352+
// SampleAggregatorTestingKnobRowHook, if non-nil, is called for each row
353+
// processed by the sample aggregator. Used for testing, e.g., to inject panics.
354+
SampleAggregatorTestingKnobRowHook func()
351355
}
352356

353357
// ModuleTestingKnobs is part of the base.ModuleTestingKnobs interface.

pkg/sql/rowexec/sample_aggregator.go

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -191,15 +191,21 @@ func (s *sampleAggregator) Run(ctx context.Context, output execinfra.RowReceiver
191191
ctx = s.StartInternal(ctx, sampleAggregatorProcName)
192192
s.input.Start(ctx)
193193

194-
earlyExit, err := s.mainLoop(ctx, output)
195-
if err != nil {
196-
execinfra.DrainAndClose(ctx, s.FlowCtx, s.input, output, err)
197-
} else if !earlyExit {
198-
execinfra.SendTraceData(ctx, s.FlowCtx, output)
199-
s.input.ConsumerClosed()
200-
output.ProducerDone()
201-
}
202-
s.MoveToDraining(nil /* err */)
194+
// Use defer to ensure cleanup happens even on panic (fix for issue #160337).
195+
var earlyExit bool
196+
var err error
197+
defer func() {
198+
if err != nil {
199+
execinfra.DrainAndClose(ctx, s.FlowCtx, s.input, output, err)
200+
} else if !earlyExit {
201+
execinfra.SendTraceData(ctx, s.FlowCtx, output)
202+
s.input.ConsumerClosed()
203+
output.ProducerDone()
204+
}
205+
s.MoveToDraining(nil /* err */)
206+
}()
207+
208+
earlyExit, err = s.mainLoop(ctx, output)
203209
}
204210

205211
// Close is part of the execinfra.Processor interface.
@@ -251,6 +257,7 @@ func (s *sampleAggregator) mainLoop(
251257
var da tree.DatumAlloc
252258
for {
253259
row, meta := s.input.Next()
260+
254261
if meta != nil {
255262
if meta.SamplerProgress != nil {
256263
rowsProcessed += meta.SamplerProgress.RowsProcessed
@@ -298,6 +305,8 @@ func (s *sampleAggregator) mainLoop(
298305
}
299306
if row == nil {
300307
break
308+
} else if s.FlowCtx.Cfg.TestingKnobs.SampleAggregatorTestingKnobRowHook != nil {
309+
s.FlowCtx.Cfg.TestingKnobs.SampleAggregatorTestingKnobRowHook()
301310
}
302311

303312
// There are four kinds of rows. They should be identified in this order:

pkg/sql/rowexec/sample_aggregator_test.go

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import (
1010
gosql "database/sql"
1111
"fmt"
1212
"reflect"
13+
"sync"
1314
"testing"
15+
"time"
1416

1517
"github.com/cockroachdb/cockroach/pkg/base"
1618
"github.com/cockroachdb/cockroach/pkg/settings/cluster"
@@ -20,15 +22,18 @@ import (
2022
"github.com/cockroachdb/cockroach/pkg/sql/execinfrapb"
2123
"github.com/cockroachdb/cockroach/pkg/sql/execversion"
2224
"github.com/cockroachdb/cockroach/pkg/sql/randgen"
25+
"github.com/cockroachdb/cockroach/pkg/sql/rowenc"
2326
"github.com/cockroachdb/cockroach/pkg/sql/sem/eval"
2427
"github.com/cockroachdb/cockroach/pkg/sql/sem/tree"
2528
"github.com/cockroachdb/cockroach/pkg/sql/stats"
2629
"github.com/cockroachdb/cockroach/pkg/sql/types"
2730
"github.com/cockroachdb/cockroach/pkg/testutils/distsqlutils"
2831
"github.com/cockroachdb/cockroach/pkg/testutils/serverutils"
32+
"github.com/cockroachdb/cockroach/pkg/testutils/skip"
2933
"github.com/cockroachdb/cockroach/pkg/testutils/sqlutils"
3034
"github.com/cockroachdb/cockroach/pkg/util/leaktest"
3135
"github.com/cockroachdb/cockroach/pkg/util/log"
36+
"github.com/cockroachdb/cockroach/pkg/util/mon"
3237
"github.com/cockroachdb/cockroach/pkg/util/protoutil"
3338
"github.com/cockroachdb/cockroach/pkg/util/randutil"
3439
"github.com/cockroachdb/errors"
@@ -450,3 +455,172 @@ func TestSampleAggregator(t *testing.T) {
450455
})
451456
}
452457
}
458+
459+
// TestPanicDeadlock verifies that the defer-based cleanup fix prevents the deadlock
460+
// using the real sampleAggregator processor.
461+
//
462+
// The original deadlock scenario (issue #160337):
463+
// 1. A processor panics during execution (without deferred cleanup)
464+
// 2. Producer goroutines are blocked sending to it via RowChannel
465+
// 3. The panic is recovered but the processor never calls ConsumerClosed()
466+
// 4. Producers remain stuck on channel sends indefinitely
467+
// 5. Wait() blocks forever waiting for producer goroutines
468+
// 6. Cleanup is never called, so UnregisterFlow never happens
469+
// 7. Drain waits forever for the flow to unregister
470+
//
471+
// The fix uses defer to ensure ConsumerClosed() is called even on panic,
472+
// which drains the channel and unblocks stuck producers.
473+
func TestPanicDeadlock(t *testing.T) {
474+
defer leaktest.AfterTest(t)()
475+
skip.UnderStress(t, "test has a 10-second timeout to detect deadlock")
476+
477+
ctx, cancel := context.WithCancel(context.Background())
478+
defer cancel()
479+
480+
// Set up minimal infrastructure for sampleAggregator
481+
st := cluster.MakeTestingClusterSettings()
482+
evalCtx := eval.MakeTestingEvalContext(st)
483+
defer evalCtx.Stop(ctx)
484+
485+
monitor := mon.NewMonitor(mon.Options{
486+
Name: mon.MakeName("test"),
487+
Settings: st,
488+
})
489+
monitor.Start(ctx, nil, mon.NewStandaloneBudget(1<<30))
490+
defer monitor.Stop(ctx)
491+
492+
// Set up testing knob to inject panic after first row
493+
var rowsSeen int
494+
flowCtx := &execinfra.FlowCtx{
495+
EvalCtx: &evalCtx,
496+
Mon: monitor,
497+
Cfg: &execinfra.ServerConfig{
498+
Settings: st,
499+
TestingKnobs: execinfra.TestingKnobs{
500+
SampleAggregatorTestingKnobRowHook: func() {
501+
rowsSeen++
502+
if rowsSeen >= 1 {
503+
panic("sampleAggregator test: injected panic")
504+
}
505+
},
506+
},
507+
},
508+
}
509+
510+
// SampleAggregator expects sampler output format: original columns + 8 metadata columns
511+
// Sampler adds: rank, sketch_idx, num_rows, num_nulls, size, sketch_data, inv_col_idx, inv_idx_key
512+
samplerOutTypes := []*types.T{
513+
types.Int, // original column (the data being sampled)
514+
types.Int, // rank
515+
types.Int, // sketch index
516+
types.Int, // num rows
517+
types.Int, // num nulls
518+
types.Int, // size
519+
types.Bytes, // sketch data
520+
types.Int, // inverted column index
521+
types.Bytes, // inverted index key
522+
}
523+
524+
// Use unbuffered channel to ensure blocking happens immediately
525+
rowChan := &execinfra.RowChannel{}
526+
rowChan.InitWithBufSizeAndNumSenders(samplerOutTypes, 0 /* unbuffered */, 1 /* numSenders */)
527+
rowChan.Start(ctx)
528+
529+
// Create the real sampleAggregator
530+
spec := &execinfrapb.SampleAggregatorSpec{
531+
SampleSize: 100,
532+
MinSampleSize: 10,
533+
Sketches: []execinfrapb.SketchSpec{
534+
{
535+
Columns: []uint32{0},
536+
GenerateHistogram: false,
537+
StatName: "test",
538+
},
539+
},
540+
}
541+
post := &execinfrapb.PostProcessSpec{}
542+
543+
proc, err := newSampleAggregator(ctx, flowCtx, 0 /* processorID */, spec, rowChan, post)
544+
if err != nil {
545+
t.Fatal(err)
546+
}
547+
548+
// Create output channel (sampleAggregator outputs stats results)
549+
outputChan := &execinfra.RowChannel{}
550+
outputChan.InitWithBufSizeAndNumSenders([]*types.T{types.Bytes}, 10, 1)
551+
outputChan.Start(ctx)
552+
553+
// Track producer goroutine
554+
var producerWg sync.WaitGroup
555+
producerWg.Add(1)
556+
557+
// Start producer goroutine that sends rows to the processor
558+
go func() {
559+
defer producerWg.Done()
560+
defer rowChan.ProducerDone()
561+
562+
// Create a sampler-format row with all 9 columns
563+
row := rowenc.EncDatumRow{
564+
rowenc.DatumToEncDatumUnsafe(types.Int, tree.NewDInt(1)), // original column
565+
rowenc.DatumToEncDatumUnsafe(types.Int, tree.NewDInt(0)), // rank
566+
rowenc.DatumToEncDatumUnsafe(types.Int, tree.NewDInt(0)), // sketch index
567+
rowenc.DatumToEncDatumUnsafe(types.Int, tree.NewDInt(1)), // num rows
568+
rowenc.DatumToEncDatumUnsafe(types.Int, tree.NewDInt(0)), // num nulls
569+
rowenc.DatumToEncDatumUnsafe(types.Int, tree.NewDInt(0)), // size
570+
rowenc.DatumToEncDatumUnsafe(types.Bytes, tree.NewDBytes("")), // sketch data
571+
rowenc.DatumToEncDatumUnsafe(types.Int, tree.NewDInt(-1)), // inverted column index (-1 = not used)
572+
rowenc.DatumToEncDatumUnsafe(types.Bytes, tree.NewDBytes("")), // inverted index key
573+
}
574+
575+
// Send multiple rows. The processor will panic after reading the first one.
576+
// WITH FIX: The defer in sampleAggregator.Run() calls ConsumerClosed() which drains the channel
577+
// WITHOUT FIX: Producer would stay blocked forever
578+
for i := 0; i < 5; i++ {
579+
rowChan.Push(row, nil)
580+
}
581+
}()
582+
583+
// Run the processor in a separate goroutine (simulates flow.Run)
584+
processorDone := make(chan bool)
585+
var processorPanic interface{}
586+
587+
go func() {
588+
defer func() {
589+
// Simulates Wait() catching the panic
590+
processorPanic = recover()
591+
if processorPanic != nil {
592+
// Simulates ctxCancel() being called
593+
cancel()
594+
}
595+
close(processorDone)
596+
}()
597+
598+
proc.Run(ctx, outputChan)
599+
}()
600+
601+
// Wait for processor to panic and exit
602+
<-processorDone
603+
604+
if processorPanic == nil {
605+
t.Fatal("expected processor to panic, but it didn't")
606+
}
607+
608+
// Now try to wait for producer goroutine with a timeout
609+
// WITH THE FIX: This should complete quickly because the defer calls
610+
// ConsumerClosed(), which drains the channel and unblocks the producer
611+
producersDone := make(chan bool)
612+
go func() {
613+
producerWg.Wait()
614+
close(producersDone)
615+
}()
616+
617+
select {
618+
case <-producersDone:
619+
// SUCCESS: Producer finished (this is what we want with the fix)
620+
t.Log("Producer finished successfully after panic - fix is working")
621+
case <-time.After(10 * time.Second):
622+
// FAILURE: Producer is deadlocked (the fix is not working)
623+
t.Fatal("DEADLOCK: Producer goroutine is still blocked 10 seconds after panic. " +
624+
"The defer-based fix in sampleAggregator is not working correctly.")
625+
}
626+
}

pkg/sql/rowexec/sampler.go

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -213,15 +213,21 @@ func (s *samplerProcessor) Run(ctx context.Context, output execinfra.RowReceiver
213213
ctx = s.StartInternal(ctx, samplerProcName)
214214
s.input.Start(ctx)
215215

216-
earlyExit, err := s.mainLoop(ctx, output)
217-
if err != nil {
218-
execinfra.DrainAndClose(ctx, s.FlowCtx, s.input, output, err)
219-
} else if !earlyExit {
220-
execinfra.SendTraceData(ctx, s.FlowCtx, output)
221-
s.input.ConsumerClosed()
222-
output.ProducerDone()
223-
}
224-
s.MoveToDraining(nil /* err */)
216+
// Use defer to ensure cleanup happens even on panic (fix for issue #160337).
217+
var earlyExit bool
218+
var err error
219+
defer func() {
220+
if err != nil {
221+
execinfra.DrainAndClose(ctx, s.FlowCtx, s.input, output, err)
222+
} else if !earlyExit {
223+
execinfra.SendTraceData(ctx, s.FlowCtx, output)
224+
s.input.ConsumerClosed()
225+
output.ProducerDone()
226+
}
227+
s.MoveToDraining(nil /* err */)
228+
}()
229+
230+
earlyExit, err = s.mainLoop(ctx, output)
225231
}
226232

227233
func (s *samplerProcessor) mainLoop(

0 commit comments

Comments
 (0)