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 }