@@ -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+ }
0 commit comments