github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/sql/gcjob/gc_job.go (about)

     1  // Copyright 2020 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 gcjob
    12  
    13  import (
    14  	"context"
    15  	"time"
    16  
    17  	"github.com/cockroachdb/cockroach/pkg/jobs"
    18  	"github.com/cockroachdb/cockroach/pkg/jobs/jobspb"
    19  	"github.com/cockroachdb/cockroach/pkg/roachpb"
    20  	"github.com/cockroachdb/cockroach/pkg/settings/cluster"
    21  	"github.com/cockroachdb/cockroach/pkg/sql"
    22  	"github.com/cockroachdb/cockroach/pkg/sql/sem/tree"
    23  	"github.com/cockroachdb/cockroach/pkg/sql/sqlbase"
    24  	"github.com/cockroachdb/cockroach/pkg/util/log"
    25  	"github.com/cockroachdb/cockroach/pkg/util/timeutil"
    26  	"github.com/cockroachdb/errors"
    27  )
    28  
    29  var (
    30  	// MaxSQLGCInterval is the longest the polling interval between checking if
    31  	// elements should be GC'd.
    32  	MaxSQLGCInterval = 5 * time.Minute
    33  )
    34  
    35  // SetSmallMaxGCIntervalForTest sets the MaxSQLGCInterval and then returns a closure
    36  // that resets it.
    37  // This is to be used in tests like:
    38  //    defer SetSmallMaxGCIntervalForTest()
    39  func SetSmallMaxGCIntervalForTest() func() {
    40  	oldInterval := MaxSQLGCInterval
    41  	MaxSQLGCInterval = 500 * time.Millisecond
    42  	return func() {
    43  		MaxSQLGCInterval = oldInterval
    44  	}
    45  }
    46  
    47  type schemaChangeGCResumer struct {
    48  	jobID int64
    49  }
    50  
    51  // performGC GCs any schema elements that are in the DELETING state and returns
    52  // a bool indicating if it GC'd any elements.
    53  func performGC(
    54  	ctx context.Context,
    55  	execCfg *sql.ExecutorConfig,
    56  	details *jobspb.SchemaChangeGCDetails,
    57  	progress *jobspb.SchemaChangeGCProgress,
    58  ) (bool, error) {
    59  	didGC := false
    60  	if details.Indexes != nil {
    61  		if didGCIndex, err := gcIndexes(ctx, execCfg, details.ParentID, progress); err != nil {
    62  			return false, errors.Wrap(err, "attempting to GC indexes")
    63  		} else if didGCIndex {
    64  			didGC = true
    65  		}
    66  	} else if details.Tables != nil {
    67  		if didGCTable, err := gcTables(ctx, execCfg, progress); err != nil {
    68  			return false, errors.Wrap(err, "attempting to GC tables")
    69  		} else if didGCTable {
    70  			didGC = true
    71  		}
    72  
    73  		// Drop database zone config when all the tables have been GCed.
    74  		if details.ParentID != sqlbase.InvalidID && isDoneGC(progress) {
    75  			if err := deleteDatabaseZoneConfig(ctx, execCfg.DB, details.ParentID); err != nil {
    76  				return false, errors.Wrap(err, "deleting database zone config")
    77  			}
    78  		}
    79  	}
    80  	return didGC, nil
    81  }
    82  
    83  // Resume is part of the jobs.Resumer interface.
    84  func (r schemaChangeGCResumer) Resume(
    85  	ctx context.Context, phs interface{}, _ chan<- tree.Datums,
    86  ) error {
    87  	p := phs.(sql.PlanHookState)
    88  	// TODO(pbardea): Wait for no versions.
    89  	execCfg := p.ExecCfg()
    90  	if fn := execCfg.GCJobTestingKnobs.RunBeforeResume; fn != nil {
    91  		if err := fn(r.jobID); err != nil {
    92  			return err
    93  		}
    94  	}
    95  	details, progress, err := initDetailsAndProgress(ctx, execCfg, r.jobID)
    96  	if err != nil {
    97  		return err
    98  	}
    99  	zoneCfgFilter, gossipUpdateC := setupConfigWatcher(execCfg)
   100  	tableDropTimes, indexDropTimes := getDropTimes(details)
   101  
   102  	allTables := getAllTablesWaitingForGC(details, progress)
   103  	if len(allTables) == 0 {
   104  		return nil
   105  	}
   106  	expired, earliestDeadline := refreshTables(ctx, execCfg, allTables, tableDropTimes, indexDropTimes, r.jobID, progress)
   107  	timerDuration := timeutil.Until(earliestDeadline)
   108  	if expired {
   109  		timerDuration = 0
   110  	} else if timerDuration > MaxSQLGCInterval {
   111  		timerDuration = MaxSQLGCInterval
   112  	}
   113  	timer := timeutil.NewTimer()
   114  	defer timer.Stop()
   115  	timer.Reset(timerDuration)
   116  
   117  	for {
   118  		select {
   119  		case <-gossipUpdateC:
   120  			// Upon notification of a gossip update, update the status of the relevant schema elements.
   121  			if log.V(2) {
   122  				log.Info(ctx, "received a new system config")
   123  			}
   124  			// TODO (lucy): Currently we're calling refreshTables on every zone config
   125  			// update to any table. We should really be only updating a cached
   126  			// TTL whenever we get an update on one of the tables/indexes (or the db)
   127  			// that this job is responsible for, and computing the earliest deadline
   128  			// from our set of cached TTL values.
   129  			cfg := execCfg.Gossip.DeprecatedSystemConfig(47150)
   130  			zoneConfigUpdated := false
   131  			zoneCfgFilter.ForModified(cfg, func(kv roachpb.KeyValue) {
   132  				zoneConfigUpdated = true
   133  			})
   134  			if !zoneConfigUpdated {
   135  				log.VEventf(ctx, 2, "no zone config updates, continuing")
   136  				continue
   137  			}
   138  			remainingTables := getAllTablesWaitingForGC(details, progress)
   139  			if len(remainingTables) == 0 {
   140  				return nil
   141  			}
   142  			expired, earliestDeadline = refreshTables(ctx, execCfg, remainingTables, tableDropTimes, indexDropTimes, r.jobID, progress)
   143  			timerDuration := time.Until(earliestDeadline)
   144  			if expired {
   145  				timerDuration = 0
   146  			} else if timerDuration > MaxSQLGCInterval {
   147  				timerDuration = MaxSQLGCInterval
   148  			}
   149  
   150  			timer.Reset(timerDuration)
   151  		case <-timer.C:
   152  			timer.Read = true
   153  			if log.V(2) {
   154  				log.Info(ctx, "SchemaChangeGC timer triggered")
   155  			}
   156  			// Refresh the status of all tables in case any GC TTLs have changed.
   157  			remainingTables := getAllTablesWaitingForGC(details, progress)
   158  			_, earliestDeadline = refreshTables(ctx, execCfg, remainingTables, tableDropTimes, indexDropTimes, r.jobID, progress)
   159  
   160  			if didWork, err := performGC(ctx, execCfg, details, progress); err != nil {
   161  				return err
   162  			} else if didWork {
   163  				persistProgress(ctx, execCfg, r.jobID, progress)
   164  			}
   165  
   166  			if isDoneGC(progress) {
   167  				return nil
   168  			}
   169  
   170  			// Schedule the next check for GC.
   171  			timerDuration := time.Until(earliestDeadline)
   172  			if timerDuration > MaxSQLGCInterval {
   173  				timerDuration = MaxSQLGCInterval
   174  			}
   175  			timer.Reset(timerDuration)
   176  		case <-ctx.Done():
   177  			return ctx.Err()
   178  		}
   179  	}
   180  }
   181  
   182  // OnFailOrCancel is part of the jobs.Resumer interface.
   183  func (r schemaChangeGCResumer) OnFailOrCancel(context.Context, interface{}) error {
   184  	return nil
   185  }
   186  
   187  func init() {
   188  	createResumerFn := func(job *jobs.Job, settings *cluster.Settings) jobs.Resumer {
   189  		return &schemaChangeGCResumer{
   190  			jobID: *job.ID(),
   191  		}
   192  	}
   193  	jobs.RegisterConstructor(jobspb.TypeSchemaChangeGC, createResumerFn)
   194  }