github.com/pf-qiu/concourse/v6@v6.7.3-0.20201207032516-1f455d73275f/atc/scheduler/buildstarter.go (about)

     1  package scheduler
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  
     7  	"code.cloudfoundry.org/lager"
     8  	"github.com/pf-qiu/concourse/v6/atc"
     9  	"github.com/pf-qiu/concourse/v6/atc/db"
    10  	"github.com/pf-qiu/concourse/v6/atc/metric"
    11  )
    12  
    13  //go:generate counterfeiter . BuildStarter
    14  
    15  type BuildStarter interface {
    16  	TryStartPendingBuildsForJob(
    17  		logger lager.Logger,
    18  		job db.SchedulerJob,
    19  		inputs db.InputConfigs,
    20  	) (bool, error)
    21  }
    22  
    23  //go:generate counterfeiter . BuildPlanner
    24  
    25  type BuildPlanner interface {
    26  	Create(atc.StepConfig, db.SchedulerResources, atc.VersionedResourceTypes, []db.BuildInput) (atc.Plan, error)
    27  }
    28  
    29  type Build interface {
    30  	db.Build
    31  
    32  	IsReadyToDetermineInputs(lager.Logger) (bool, error)
    33  	BuildInputs(context.Context) ([]db.BuildInput, bool, error)
    34  }
    35  
    36  func NewBuildStarter(
    37  	planner BuildPlanner,
    38  	algorithm Algorithm,
    39  ) BuildStarter {
    40  	return &buildStarter{
    41  		planner:   planner,
    42  		algorithm: algorithm,
    43  	}
    44  }
    45  
    46  type buildStarter struct {
    47  	planner   BuildPlanner
    48  	algorithm Algorithm
    49  }
    50  
    51  func (s *buildStarter) TryStartPendingBuildsForJob(
    52  	logger lager.Logger,
    53  	job db.SchedulerJob,
    54  	jobInputs db.InputConfigs,
    55  ) (bool, error) {
    56  	nextPendingBuilds, err := job.GetPendingBuilds()
    57  	if err != nil {
    58  		return false, fmt.Errorf("get pending builds: %w", err)
    59  	}
    60  
    61  	buildsToSchedule := s.constructBuilds(job, jobInputs, nextPendingBuilds)
    62  
    63  	var needsRetry bool
    64  	for _, nextSchedulableBuild := range buildsToSchedule {
    65  		results, err := s.tryStartNextPendingBuild(logger, nextSchedulableBuild, job)
    66  		if err != nil {
    67  			return false, err
    68  		}
    69  
    70  		if results.finished {
    71  			// If the build is successfully aborted, errored or started, continue
    72  			// onto the next pending build
    73  			continue
    74  		}
    75  
    76  		if !results.scheduled || !results.readyToDetermineInputs {
    77  			// If max in flight is reached or a manually triggered build has not
    78  			// checked all resources, stop scheduling and retry later
    79  			needsRetry = true
    80  			break
    81  		}
    82  
    83  		if !results.inputsDetermined {
    84  			if nextSchedulableBuild.RerunOf() != 0 {
    85  				// If it is a rerun build, continue on to next build. We don't want to
    86  				// stop scheduling other builds because of a rerun build cannot
    87  				// determine inputs
    88  				continue
    89  			} else {
    90  				// If it is a regular scheduler build, stop scheduling because it is
    91  				// failing to determine inputs
    92  				break
    93  			}
    94  		}
    95  	}
    96  
    97  	return needsRetry, nil
    98  }
    99  
   100  func (s *buildStarter) constructBuilds(job db.Job, jobInputs db.InputConfigs, builds []db.Build) []Build {
   101  	var buildsToSchedule []Build
   102  
   103  	for _, nextPendingBuild := range builds {
   104  		if nextPendingBuild.IsManuallyTriggered() {
   105  			buildsToSchedule = append(buildsToSchedule, &manualTriggerBuild{
   106  				Build:     nextPendingBuild,
   107  				algorithm: s.algorithm,
   108  				job:       job,
   109  				jobInputs: jobInputs,
   110  			})
   111  		} else if nextPendingBuild.RerunOf() != 0 {
   112  			buildsToSchedule = append(buildsToSchedule, &rerunBuild{
   113  				Build: nextPendingBuild,
   114  			})
   115  		} else {
   116  			buildsToSchedule = append(buildsToSchedule, &schedulerBuild{
   117  				Build: nextPendingBuild,
   118  			})
   119  		}
   120  	}
   121  
   122  	return buildsToSchedule
   123  }
   124  
   125  type startResults struct {
   126  	finished               bool
   127  	scheduled              bool
   128  	readyToDetermineInputs bool
   129  	inputsDetermined       bool
   130  }
   131  
   132  func (s *buildStarter) tryStartNextPendingBuild(
   133  	logger lager.Logger,
   134  	nextPendingBuild Build,
   135  	job db.SchedulerJob,
   136  ) (startResults, error) {
   137  	logger = logger.Session("try-start-next-pending-build", lager.Data{
   138  		"build-id":   nextPendingBuild.ID(),
   139  		"build-name": nextPendingBuild.Name(),
   140  	})
   141  
   142  	if nextPendingBuild.IsAborted() {
   143  		logger.Debug("cancel-aborted-pending-build")
   144  
   145  		err := nextPendingBuild.Finish(db.BuildStatusAborted)
   146  		if err != nil {
   147  			return startResults{}, fmt.Errorf("finish aborted build: %w", err)
   148  		}
   149  
   150  		return startResults{
   151  			finished: true,
   152  		}, nil
   153  	}
   154  
   155  	scheduled, err := job.ScheduleBuild(nextPendingBuild)
   156  	if err != nil {
   157  		return startResults{}, fmt.Errorf("schedule build: %w", err)
   158  	}
   159  
   160  	if !scheduled {
   161  		logger.Debug("build-not-scheduled")
   162  		return startResults{
   163  			scheduled: scheduled,
   164  		}, nil
   165  	}
   166  
   167  	readyToDetermineInputs, err := nextPendingBuild.IsReadyToDetermineInputs(logger)
   168  	if err != nil {
   169  		return startResults{}, fmt.Errorf("ready to determine inputs: %w", err)
   170  	}
   171  
   172  	if !readyToDetermineInputs {
   173  		return startResults{
   174  			scheduled:              scheduled,
   175  			readyToDetermineInputs: readyToDetermineInputs,
   176  		}, nil
   177  	}
   178  
   179  	buildInputs, inputsDetermined, err := nextPendingBuild.BuildInputs(context.TODO())
   180  	if err != nil {
   181  		return startResults{}, fmt.Errorf("get build inputs: %w", err)
   182  	}
   183  
   184  	if !inputsDetermined {
   185  		logger.Debug("build-inputs-not-found")
   186  
   187  		// don't retry when build inputs are not found because this is due to the
   188  		// inputs being unsatisfiable
   189  		return startResults{
   190  			scheduled:              scheduled,
   191  			readyToDetermineInputs: readyToDetermineInputs,
   192  			inputsDetermined:       inputsDetermined,
   193  		}, nil
   194  	}
   195  
   196  	config, err := job.Config()
   197  	if err != nil {
   198  		return startResults{}, fmt.Errorf("config: %w", err)
   199  	}
   200  
   201  	plan, err := s.planner.Create(config.StepConfig(), job.Resources, job.ResourceTypes, buildInputs)
   202  	if err != nil {
   203  		logger.Error("failed-to-create-build-plan", err)
   204  
   205  		// Don't use ErrorBuild because it logs a build event, and this build hasn't started
   206  		if err = nextPendingBuild.Finish(db.BuildStatusErrored); err != nil {
   207  			logger.Error("failed-to-mark-build-as-errored", err)
   208  			return startResults{}, fmt.Errorf("finish build: %w", err)
   209  		}
   210  
   211  		return startResults{
   212  			finished: true,
   213  		}, nil
   214  	}
   215  
   216  	started, err := nextPendingBuild.Start(plan)
   217  	if err != nil {
   218  		logger.Error("failed-to-mark-build-as-started", err)
   219  		return startResults{}, fmt.Errorf("start build: %w", err)
   220  	}
   221  
   222  	if !started {
   223  		if err = nextPendingBuild.Finish(db.BuildStatusAborted); err != nil {
   224  			logger.Error("failed-to-mark-build-as-finished", err)
   225  			return startResults{}, fmt.Errorf("finish build: %w", err)
   226  		}
   227  
   228  		return startResults{
   229  			finished: true,
   230  		}, nil
   231  	}
   232  
   233  	metric.Metrics.BuildsStarted.Inc()
   234  
   235  	return startResults{
   236  		finished: true,
   237  	}, nil
   238  }