@@ -7,7 +7,9 @@ package inspect
77
88import (
99 "context"
10+ "math"
1011 "testing"
12+ "time"
1113
1214 "github.com/cockroachdb/cockroach/pkg/base"
1315 "github.com/cockroachdb/cockroach/pkg/jobs"
@@ -17,6 +19,7 @@ import (
1719 "github.com/cockroachdb/cockroach/pkg/security/username"
1820 "github.com/cockroachdb/cockroach/pkg/sql/execinfrapb"
1921 "github.com/cockroachdb/cockroach/pkg/sql/isql"
22+ "github.com/cockroachdb/cockroach/pkg/testutils"
2023 "github.com/cockroachdb/cockroach/pkg/testutils/serverutils"
2124 "github.com/cockroachdb/cockroach/pkg/util/leaktest"
2225 "github.com/cockroachdb/cockroach/pkg/util/log"
@@ -437,3 +440,182 @@ func createProcessorProgressUpdate(
437440
438441 return meta , nil
439442}
443+
444+ func TestInspectProgressTracker_ProgressFlushConditions (t * testing.T ) {
445+ defer leaktest .AfterTest (t )()
446+ defer log .Scope (t ).Close (t )
447+
448+ ctx , s , _ , _ , cleanup := setupProgressTestInfra (t )
449+ defer cleanup ()
450+
451+ const totalChecks = 1000
452+
453+ testCases := []struct {
454+ name string
455+ setupFunc func (t * testing.T , tracker * inspectProgressTracker , job * jobs.Job )
456+ expectedFraction float32
457+ expectUncheckpointedSpans bool
458+ }{
459+ {
460+ name : "initial state - no progress updates" ,
461+ setupFunc : func (t * testing.T , tracker * inspectProgressTracker , job * jobs.Job ) {
462+ require .NoError (t , tracker .initJobProgress (ctx , totalChecks , 0 ))
463+ },
464+ expectedFraction : 0.0 ,
465+ expectUncheckpointedSpans : false ,
466+ },
467+ {
468+ name : "check count updates without spans" ,
469+ setupFunc : func (t * testing.T , tracker * inspectProgressTracker , job * jobs.Job ) {
470+ require .NoError (t , tracker .initJobProgress (ctx , totalChecks , 0 ))
471+
472+ // Send progress updates with check counts but no spans.
473+ meta , err := createProcessorProgressUpdate (100 , false , nil )
474+ require .NoError (t , err )
475+ _ , err = tracker .updateProgressCache (meta )
476+ require .NoError (t , err )
477+ },
478+ expectedFraction : 0.1 ,
479+ expectUncheckpointedSpans : false ,
480+ },
481+ {
482+ name : "span updates trigger checkpoint need" ,
483+ setupFunc : func (t * testing.T , tracker * inspectProgressTracker , job * jobs.Job ) {
484+ require .NoError (t , tracker .initJobProgress (ctx , totalChecks , 0 ))
485+
486+ // Send progress with completed spans.
487+ spans := []roachpb.Span {{Key : roachpb .Key ("a" ), EndKey : roachpb .Key ("m" )}}
488+ meta , err := createProcessorProgressUpdate (50 , false , spans )
489+ require .NoError (t , err )
490+ _ , err = tracker .updateProgressCache (meta )
491+ require .NoError (t , err )
492+ },
493+ expectedFraction : 0.05 ,
494+ expectUncheckpointedSpans : true ,
495+ },
496+ {
497+ name : "automatic flush clears uncheckpointed state" ,
498+ setupFunc : func (t * testing.T , tracker * inspectProgressTracker , job * jobs.Job ) {
499+ require .NoError (t , tracker .initJobProgress (ctx , totalChecks , 0 ))
500+
501+ // Send progress with completed spans.
502+ spans := []roachpb.Span {{Key : roachpb .Key ("a" ), EndKey : roachpb .Key ("m" )}}
503+ meta , err := createProcessorProgressUpdate (100 , false , spans )
504+ require .NoError (t , err )
505+ _ , err = tracker .updateProgressCache (meta )
506+ require .NoError (t , err )
507+
508+ // Wait for automatic checkpoint flush.
509+ testutils .SucceedsSoon (t , func () error {
510+ if tracker .hasUncheckpointedSpans () {
511+ return errors .New ("still has uncheckpointed spans" )
512+ }
513+ return nil
514+ })
515+ },
516+ expectedFraction : 0.1 ,
517+ expectUncheckpointedSpans : false ,
518+ },
519+ {
520+ name : "multiple span updates merge and checkpoint" ,
521+ setupFunc : func (t * testing.T , tracker * inspectProgressTracker , job * jobs.Job ) {
522+ require .NoError (t , tracker .initJobProgress (ctx , totalChecks , 0 ))
523+
524+ // Send multiple progress updates with different spans.
525+ spans1 := []roachpb.Span {{Key : roachpb .Key ("a" ), EndKey : roachpb .Key ("d" )}}
526+ meta1 , err := createProcessorProgressUpdate (100 , false , spans1 )
527+ require .NoError (t , err )
528+ _ , err = tracker .updateProgressCache (meta1 )
529+ require .NoError (t , err )
530+
531+ spans2 := []roachpb.Span {{Key : roachpb .Key ("d" ), EndKey : roachpb .Key ("g" )}}
532+ meta2 , err := createProcessorProgressUpdate (100 , false , spans2 )
533+ require .NoError (t , err )
534+ _ , err = tracker .updateProgressCache (meta2 )
535+ require .NoError (t , err )
536+
537+ // Wait for checkpoint to complete.
538+ testutils .SucceedsSoon (t , func () error {
539+ if tracker .hasUncheckpointedSpans () {
540+ return errors .New ("still has uncheckpointed spans" )
541+ }
542+ return nil
543+ })
544+ },
545+ expectedFraction : 0.2 ,
546+ expectUncheckpointedSpans : false ,
547+ },
548+ {
549+ name : "immediate flush on drained processor" ,
550+ setupFunc : func (t * testing.T , tracker * inspectProgressTracker , job * jobs.Job ) {
551+ require .NoError (t , tracker .initJobProgress (ctx , totalChecks , 0 ))
552+
553+ // Send progress with drained=true to trigger immediate flush.
554+ spans := []roachpb.Span {{Key : roachpb .Key ("a" ), EndKey : roachpb .Key ("z" )}}
555+ meta , err := createProcessorProgressUpdate (500 , true , spans )
556+ require .NoError (t , err )
557+ require .NoError (t , tracker .handleProgressUpdate (ctx , meta ))
558+
559+ // Immediate flush should have happened, so no uncheckpointed spans.
560+ require .False (t , tracker .hasUncheckpointedSpans ())
561+ },
562+ expectedFraction : 0.5 ,
563+ expectUncheckpointedSpans : false ,
564+ },
565+ }
566+
567+ for _ , tc := range testCases {
568+ t .Run (tc .name , func (t * testing.T ) {
569+ // Create a fresh job for each test case.
570+ record := jobs.Record {
571+ Details : jobspb.InspectDetails {},
572+ Progress : jobspb.InspectProgress {},
573+ Username : username .TestUserName (),
574+ }
575+
576+ freshJob , err := s .JobRegistry ().(* jobs.Registry ).CreateJobWithTxn (ctx , record , s .JobRegistry ().(* jobs.Registry ).MakeJobID (), nil )
577+ require .NoError (t , err )
578+
579+ freshTracker := newInspectProgressTracker (freshJob , & s .ClusterSettings ().SV , s .InternalDB ().(isql.DB ))
580+ defer freshTracker .terminateTracker ()
581+
582+ // Override intervals for faster testing.
583+ const fastCheckpointInterval = 10 * time .Millisecond
584+ const fastFractionInterval = 5 * time .Millisecond
585+ freshTracker .checkpointInterval = func () time.Duration { return fastCheckpointInterval }
586+ freshTracker .fractionInterval = func () time.Duration { return fastFractionInterval }
587+
588+ // Run the test setup.
589+ tc .setupFunc (t , freshTracker , freshJob )
590+
591+ // Verify uncheckpointed spans state.
592+ require .Equal (t , tc .expectUncheckpointedSpans , freshTracker .hasUncheckpointedSpans (),
593+ "unexpected uncheckpointed spans state" )
594+
595+ // Verify fraction complete.
596+ progress := freshJob .Progress ()
597+ fractionCompleted , ok := progress .Progress .(* jobspb.Progress_FractionCompleted )
598+ require .True (t , ok , "progress should be FractionCompleted type" )
599+ if tc .expectedFraction == 0.0 {
600+ // For zero expected fraction, check immediately.
601+ require .Equal (t , tc .expectedFraction , fractionCompleted .FractionCompleted )
602+ } else {
603+ // For non-zero expected fraction, wait for async flush to complete.
604+ testutils .SucceedsSoon (t , func () error {
605+ progress = freshJob .Progress ()
606+ fractionCompleted , ok = progress .Progress .(* jobspb.Progress_FractionCompleted )
607+ if ! ok {
608+ return errors .New ("progress should be FractionCompleted type" )
609+ }
610+ // Check if fraction is within epsilon (1% tolerance).
611+ const epsilon = 0.01
612+ if math .Abs (float64 (fractionCompleted .FractionCompleted - tc .expectedFraction )) > epsilon {
613+ return errors .Newf ("fraction complete not within epsilon: expected %f ± %f, got %f" ,
614+ tc .expectedFraction , epsilon , fractionCompleted .FractionCompleted )
615+ }
616+ return nil
617+ })
618+ }
619+ })
620+ }
621+ }
0 commit comments