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  }