github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/jobs/update.go (about) 1 // Copyright 2019 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 "fmt" 16 "strings" 17 18 "github.com/cockroachdb/cockroach/pkg/jobs/jobspb" 19 "github.com/cockroachdb/cockroach/pkg/kv" 20 "github.com/cockroachdb/cockroach/pkg/security" 21 "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" 22 "github.com/cockroachdb/cockroach/pkg/sql/sqlbase" 23 "github.com/cockroachdb/cockroach/pkg/util/protoutil" 24 "github.com/cockroachdb/cockroach/pkg/util/timeutil" 25 "github.com/cockroachdb/errors" 26 ) 27 28 // UpdateFn is the callback passed to Job.Update. It is called from the context 29 // of a transaction and is passed the current metadata for the job. The callback 30 // can modify metadata using the JobUpdater and the changes will be persisted 31 // within the same transaction. 32 // 33 // The function is free to modify contents of JobMetadata in place (but the 34 // changes will be ignored unless JobUpdater is used). 35 type UpdateFn func(txn *kv.Txn, md JobMetadata, ju *JobUpdater) error 36 37 // JobMetadata groups the job metadata values passed to UpdateFn. 38 type JobMetadata struct { 39 ID int64 40 Status Status 41 Payload *jobspb.Payload 42 Progress *jobspb.Progress 43 } 44 45 // CheckRunningOrReverting returns an InvalidStatusError if md.Status is not 46 // StatusRunning or StatusReverting. 47 func (md *JobMetadata) CheckRunningOrReverting() error { 48 if md.Status != StatusRunning && md.Status != StatusReverting { 49 return &InvalidStatusError{md.ID, md.Status, "update progress on", md.Payload.Error} 50 } 51 return nil 52 } 53 54 // JobUpdater accumulates changes to job metadata that are to be persisted. 55 type JobUpdater struct { 56 md JobMetadata 57 } 58 59 // UpdateStatus sets a new status (to be persisted). 60 func (ju *JobUpdater) UpdateStatus(status Status) { 61 ju.md.Status = status 62 } 63 64 // UpdatePayload sets a new Payload (to be persisted). 65 // 66 // WARNING: the payload can be large (resulting in a large KV for each version); 67 // it shouldn't be updated frequently. 68 func (ju *JobUpdater) UpdatePayload(payload *jobspb.Payload) { 69 ju.md.Payload = payload 70 } 71 72 // UpdateProgress sets a new Progress (to be persisted). 73 func (ju *JobUpdater) UpdateProgress(progress *jobspb.Progress) { 74 ju.md.Progress = progress 75 } 76 77 func (ju *JobUpdater) hasUpdates() bool { 78 return ju.md != JobMetadata{} 79 } 80 81 // Update is used to read the metadata for a job and potentially update it. 82 // 83 // The updateFn is called in the context of a transaction and is passed the 84 // current metadata for the job. It can choose to update parts of the metadata 85 // using the JobUpdater, causing them to be updated within the same transaction. 86 // 87 // Sample usage: 88 // 89 // err := j.Update(ctx, func(_ *client.Txn, md jobs.JobMetadata, ju *jobs.JobUpdater) error { 90 // if md.Status != StatusRunning { 91 // return errors.New("job no longer running") 92 // } 93 // md.UpdateStatus(StatusPaused) 94 // // <modify md.Payload> 95 // md.UpdatePayload(md.Payload) 96 // } 97 // 98 // Note that there are various convenience wrappers (like FractionProgressed) 99 // defined in jobs.go. 100 func (j *Job) Update(ctx context.Context, updateFn UpdateFn) error { 101 if j.id == nil { 102 return errors.New("Job: cannot update: job not created") 103 } 104 105 var payload *jobspb.Payload 106 var progress *jobspb.Progress 107 if err := j.runInTxn(ctx, func(ctx context.Context, txn *kv.Txn) error { 108 const selectStmt = "SELECT status, payload, progress FROM system.jobs WHERE id = $1" 109 row, err := j.registry.ex.QueryRowEx( 110 ctx, "log-job", txn, sqlbase.InternalExecutorSessionDataOverride{User: security.RootUser}, 111 selectStmt, *j.id) 112 if err != nil { 113 return err 114 } 115 if row == nil { 116 return errors.Errorf("no such job %d found", *j.id) 117 } 118 119 statusString, ok := row[0].(*tree.DString) 120 if !ok { 121 return errors.Errorf("Job: expected string status on job %d, but got %T", *j.id, statusString) 122 } 123 status := Status(*statusString) 124 if payload, err = UnmarshalPayload(row[1]); err != nil { 125 return err 126 } 127 if progress, err = UnmarshalProgress(row[2]); err != nil { 128 return err 129 } 130 131 md := JobMetadata{ 132 ID: *j.id, 133 Status: status, 134 Payload: payload, 135 Progress: progress, 136 } 137 var ju JobUpdater 138 if err := updateFn(txn, md, &ju); err != nil { 139 return err 140 } 141 142 if !ju.hasUpdates() { 143 return nil 144 } 145 146 // Build a statement of the following form, depending on which properties 147 // need updating: 148 // 149 // UPDATE system.jobs 150 // SET 151 // [status = $2,] 152 // [payload = $y,] 153 // [progress = $z] 154 // WHERE 155 // id = $1 156 157 var setters []string 158 params := []interface{}{*j.id} // $1 is always the job ID. 159 addSetter := func(column string, value interface{}) { 160 params = append(params, value) 161 setters = append(setters, fmt.Sprintf("%s = $%d", column, len(params))) 162 } 163 164 if ju.md.Status != "" { 165 addSetter("status", ju.md.Status) 166 } 167 168 if ju.md.Payload != nil { 169 payload = ju.md.Payload 170 payloadBytes, err := protoutil.Marshal(payload) 171 if err != nil { 172 return err 173 } 174 addSetter("payload", payloadBytes) 175 } 176 177 if ju.md.Progress != nil { 178 progress = ju.md.Progress 179 progress.ModifiedMicros = timeutil.ToUnixMicros(txn.ReadTimestamp().GoTime()) 180 progressBytes, err := protoutil.Marshal(progress) 181 if err != nil { 182 return err 183 } 184 addSetter("progress", progressBytes) 185 } 186 187 updateStmt := fmt.Sprintf( 188 "UPDATE system.jobs SET %s WHERE id = $1", 189 strings.Join(setters, ", "), 190 ) 191 n, err := j.registry.ex.Exec(ctx, "job-update", txn, updateStmt, params...) 192 if err != nil { 193 return err 194 } 195 if n != 1 { 196 return errors.Errorf( 197 "Job: expected exactly one row affected, but %d rows affected by job update", n, 198 ) 199 } 200 return nil 201 }); err != nil { 202 return err 203 } 204 if payload != nil { 205 j.mu.Lock() 206 j.mu.payload = *payload 207 j.mu.Unlock() 208 } 209 if progress != nil { 210 j.mu.Lock() 211 j.mu.progress = *progress 212 j.mu.Unlock() 213 } 214 return nil 215 }