github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/jobs/progress.go (about)

     1  // Copyright 2017 The Cockroach Authors.
     2  //
     3  // Use of this software is governed by the Business Source License
     4  // included in the file licenses/BSL.txt.
     5  //
     6  // As of the Change Date specified in that file, in accordance with
     7  // the Business Source License, use of this software will be governed
     8  // by the Apache License, Version 2.0, included in the file
     9  // licenses/APL.txt.
    10  
    11  package jobs
    12  
    13  import (
    14  	"context"
    15  	"time"
    16  
    17  	"github.com/cockroachdb/cockroach/pkg/jobs/jobspb"
    18  	"github.com/cockroachdb/cockroach/pkg/util/syncutil"
    19  	"github.com/cockroachdb/cockroach/pkg/util/timeutil"
    20  )
    21  
    22  // For both backups and restores, we compute progress as the number of completed
    23  // export or import requests, respectively, divided by the total number of
    24  // requests. To avoid hammering the system.jobs table, when a response comes
    25  // back, we issue a progress update only if a) it's been a duration of
    26  // progressTimeThreshold since the last update, or b) the difference between the
    27  // last logged fractionCompleted and the current fractionCompleted is more than
    28  // progressFractionThreshold.
    29  var (
    30  	progressTimeThreshold             = 15 * time.Second
    31  	progressFractionThreshold float32 = 0.05
    32  )
    33  
    34  // TestingSetProgressThresholds overrides batching limits to update more often.
    35  func TestingSetProgressThresholds() func() {
    36  	oldFraction := progressFractionThreshold
    37  	oldDuration := progressTimeThreshold
    38  
    39  	progressFractionThreshold = 0.0001
    40  	progressTimeThreshold = time.Microsecond
    41  
    42  	return func() {
    43  		progressFractionThreshold = oldFraction
    44  		progressTimeThreshold = oldDuration
    45  	}
    46  }
    47  
    48  // ChunkProgressLogger is a helper for managing the progress state on a job. For
    49  // a given job, it assumes there are some number of chunks of work to do and
    50  // tracks the completion progress as chunks are reported as done (via Loop).
    51  // It then updates the actual job periodically using a ProgressUpdateBatcher.
    52  type ChunkProgressLogger struct {
    53  	// These fields must be externally initialized.
    54  	expectedChunks       int
    55  	completedChunks      int
    56  	perChunkContribution float32
    57  
    58  	batcher ProgressUpdateBatcher
    59  }
    60  
    61  // ProgressUpdateOnly is for use with NewChunkProgressLogger to just update job
    62  // progress fraction (ie. when a custom func with side-effects is not needed).
    63  var ProgressUpdateOnly func(context.Context, jobspb.ProgressDetails)
    64  
    65  // NewChunkProgressLogger returns a ChunkProgressLogger.
    66  func NewChunkProgressLogger(
    67  	j *Job,
    68  	expectedChunks int,
    69  	startFraction float32,
    70  	progressedFn func(context.Context, jobspb.ProgressDetails),
    71  ) *ChunkProgressLogger {
    72  	return &ChunkProgressLogger{
    73  		expectedChunks:       expectedChunks,
    74  		perChunkContribution: (1.0 - startFraction) * 1.0 / float32(expectedChunks),
    75  		batcher: ProgressUpdateBatcher{
    76  			completed: startFraction,
    77  			reported:  startFraction,
    78  			Report: func(ctx context.Context, pct float32) error {
    79  				return j.FractionProgressed(ctx, func(ctx context.Context, details jobspb.ProgressDetails) float32 {
    80  					if progressedFn != nil {
    81  						progressedFn(ctx, details)
    82  					}
    83  					return pct
    84  				})
    85  			},
    86  		},
    87  	}
    88  }
    89  
    90  // chunkFinished marks one chunk of the job as completed. If either the time or
    91  // fraction threshold has been reached, the progress update will be persisted to
    92  // system.jobs.
    93  func (jpl *ChunkProgressLogger) chunkFinished(ctx context.Context) error {
    94  	jpl.completedChunks++
    95  	return jpl.batcher.Add(ctx, jpl.perChunkContribution)
    96  }
    97  
    98  // Loop calls chunkFinished for every message received over chunkCh. It exits
    99  // when chunkCh is closed, when totalChunks messages have been received, or when
   100  // the context is canceled.
   101  func (jpl *ChunkProgressLogger) Loop(ctx context.Context, chunkCh <-chan struct{}) error {
   102  	for {
   103  		select {
   104  		case _, ok := <-chunkCh:
   105  			if !ok {
   106  				return nil
   107  			}
   108  			if err := jpl.chunkFinished(ctx); err != nil {
   109  				return err
   110  			}
   111  			if jpl.completedChunks == jpl.expectedChunks {
   112  				return jpl.batcher.Done(ctx)
   113  			}
   114  		case <-ctx.Done():
   115  			return ctx.Err()
   116  		}
   117  	}
   118  }
   119  
   120  // ProgressUpdateBatcher is a helper for tracking progress as it is made and
   121  // calling a progress update function when it has meaningfully advanced (e.g. by
   122  // more than 5%), while ensuring updates also are not done too often (by default
   123  // not less than 30s apart).
   124  type ProgressUpdateBatcher struct {
   125  	// Report is the function called to record progress
   126  	Report func(context.Context, float32) error
   127  
   128  	syncutil.Mutex
   129  	// completed is the fraction of a proc's work completed
   130  	completed float32
   131  	// reported is the most recently reported value of completed
   132  	reported float32
   133  	// lastReported is when we last called report
   134  	lastReported time.Time
   135  }
   136  
   137  // Add records some additional progress made and checks there has been enough
   138  // change in the completed progress (and enough time has passed) to report the
   139  // new progress amount.
   140  func (p *ProgressUpdateBatcher) Add(ctx context.Context, delta float32) error {
   141  	p.Lock()
   142  	p.completed += delta
   143  	completed := p.completed
   144  	shouldReport := p.completed-p.reported > progressFractionThreshold
   145  	shouldReport = shouldReport && p.lastReported.Add(progressTimeThreshold).Before(timeutil.Now())
   146  
   147  	if shouldReport {
   148  		p.reported = p.completed
   149  		p.lastReported = timeutil.Now()
   150  	}
   151  	p.Unlock()
   152  
   153  	if shouldReport {
   154  		return p.Report(ctx, completed)
   155  	}
   156  	return nil
   157  }
   158  
   159  // Done allows the batcher to report any meaningful unreported progress, without
   160  // worrying about update frequency now that it is done.
   161  func (p *ProgressUpdateBatcher) Done(ctx context.Context) error {
   162  	p.Lock()
   163  	completed := p.completed
   164  	shouldReport := completed-p.reported > progressFractionThreshold
   165  	p.Unlock()
   166  
   167  	if shouldReport {
   168  		return p.Report(ctx, completed)
   169  	}
   170  	return nil
   171  }