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

     1  package algorithm
     2  
     3  import (
     4  	"context"
     5  	"sort"
     6  	"strconv"
     7  
     8  	"github.com/pf-qiu/concourse/v6/atc/db"
     9  	"github.com/pf-qiu/concourse/v6/tracing"
    10  	"go.opentelemetry.io/otel/api/trace"
    11  	"go.opentelemetry.io/otel/codes"
    12  	"go.opentelemetry.io/otel/label"
    13  )
    14  
    15  type versionCandidate struct {
    16  	Version             db.ResourceVersion
    17  	VouchedForBy        map[int]bool
    18  	SourceBuildIds      []int
    19  	HasNextEveryVersion bool
    20  }
    21  
    22  func newCandidateVersion(version db.ResourceVersion) *versionCandidate {
    23  	return &versionCandidate{
    24  		Version:        version,
    25  		VouchedForBy:   map[int]bool{},
    26  		SourceBuildIds: []int{},
    27  	}
    28  }
    29  
    30  type groupResolver struct {
    31  	vdb          db.VersionsDB
    32  	inputConfigs db.InputConfigs
    33  
    34  	pins        []db.ResourceVersion
    35  	orderedJobs [][]int
    36  	candidates  []*versionCandidate
    37  
    38  	doomedCandidates []*versionCandidate
    39  
    40  	lastUsedPassedBuilds map[int]db.BuildCursor
    41  }
    42  
    43  func NewGroupResolver(vdb db.VersionsDB, inputConfigs db.InputConfigs) Resolver {
    44  	return &groupResolver{
    45  		vdb:              vdb,
    46  		inputConfigs:     inputConfigs,
    47  		pins:             make([]db.ResourceVersion, len(inputConfigs)),
    48  		orderedJobs:      make([][]int, len(inputConfigs)),
    49  		candidates:       make([]*versionCandidate, len(inputConfigs)),
    50  		doomedCandidates: make([]*versionCandidate, len(inputConfigs)),
    51  	}
    52  }
    53  
    54  func (r *groupResolver) InputConfigs() db.InputConfigs {
    55  	return r.inputConfigs
    56  }
    57  
    58  func (r *groupResolver) Resolve(ctx context.Context) (map[string]*versionCandidate, db.ResolutionFailure, error) {
    59  	ctx, span := tracing.StartSpan(ctx, "groupResolver.Resolve", tracing.Attrs{
    60  		"inputs": r.inputConfigs.String(),
    61  	})
    62  	defer span.End()
    63  
    64  	for i, cfg := range r.inputConfigs {
    65  		if cfg.PinnedVersion == nil {
    66  			continue
    67  		}
    68  
    69  		version, found, err := r.vdb.FindVersionOfResource(ctx, cfg.ResourceID, cfg.PinnedVersion)
    70  		if err != nil {
    71  			tracing.End(span, err)
    72  			return nil, "", err
    73  		}
    74  
    75  		if !found {
    76  			notFoundErr := db.PinnedVersionNotFound{PinnedVersion: cfg.PinnedVersion}
    77  			span.SetStatus(codes.InvalidArgument, "")
    78  			return nil, notFoundErr.String(), nil
    79  		}
    80  
    81  		r.pins[i] = version
    82  	}
    83  
    84  	resolved, failure, err := r.tryResolve(ctx)
    85  	if err != nil {
    86  		tracing.End(span, err)
    87  		return nil, "", err
    88  	}
    89  
    90  	if !resolved {
    91  		span.SetAttributes(label.String("failure", string(failure)))
    92  		span.SetStatus(codes.NotFound, "")
    93  		return nil, failure, nil
    94  	}
    95  
    96  	finalCandidates := map[string]*versionCandidate{}
    97  	for i, input := range r.inputConfigs {
    98  		finalCandidates[input.Name] = r.candidates[i]
    99  	}
   100  
   101  	span.SetStatus(codes.OK, "")
   102  	return finalCandidates, "", nil
   103  }
   104  
   105  func (r *groupResolver) tryResolve(ctx context.Context) (bool, db.ResolutionFailure, error) {
   106  	ctx, span := tracing.StartSpan(ctx, "groupResolver.tryResolve", tracing.Attrs{
   107  		"inputs": r.inputConfigs.String(),
   108  	})
   109  	defer span.End()
   110  
   111  	for inputIndex := range r.inputConfigs {
   112  		worked, failure, err := r.trySatisfyPassedConstraintsForInput(ctx, inputIndex)
   113  		if err != nil {
   114  			tracing.End(span, err)
   115  			return false, "", err
   116  		}
   117  
   118  		if !worked {
   119  			// input was not satisfiable
   120  			span.SetStatus(codes.NotFound, "")
   121  			return false, failure, nil
   122  		}
   123  	}
   124  
   125  	// got to the end of all the inputs
   126  	span.SetStatus(codes.OK, "")
   127  	return true, "", nil
   128  }
   129  
   130  func (r *groupResolver) trySatisfyPassedConstraintsForInput(ctx context.Context, inputIndex int) (bool, db.ResolutionFailure, error) {
   131  	inputConfig := r.inputConfigs[inputIndex]
   132  	currentJobID := inputConfig.JobID
   133  
   134  	ctx, span := tracing.StartSpan(ctx, "groupResolver.trySatisfyPassedConstraintsForInput", tracing.Attrs{
   135  		"input": inputConfig.Name,
   136  	})
   137  	defer span.End()
   138  
   139  	// current candidate, if coming from a recursive call
   140  	currentCandidate := r.candidates[inputIndex]
   141  
   142  	// deterministically order the passed jobs for this input
   143  	orderedJobs := r.orderJobs(inputConfig.Passed)
   144  
   145  	for _, passedJobID := range orderedJobs {
   146  		if currentCandidate != nil {
   147  			// coming from recursive call; we've already got a candidate
   148  			if currentCandidate.VouchedForBy[passedJobID] {
   149  				// we've already been here; continue to the next job
   150  				continue
   151  			}
   152  		}
   153  
   154  		builds, skip, err := r.paginatedBuilds(ctx, inputConfig, currentCandidate, currentJobID, passedJobID)
   155  		if err != nil {
   156  			tracing.End(span, err)
   157  			return false, "", err
   158  		}
   159  
   160  		if skip {
   161  			span.AddEvent(ctx, "deferring selection to other jobs", label.Int("passedJobID", passedJobID))
   162  			continue
   163  		}
   164  
   165  		worked, err := r.tryJobBuilds(ctx, inputIndex, passedJobID, builds)
   166  		if err != nil {
   167  			tracing.End(span, err)
   168  			return false, "", err
   169  		}
   170  
   171  		if worked {
   172  			// resolving recursively worked!
   173  			break
   174  		} else {
   175  			span.SetStatus(codes.NotFound, "")
   176  			return false, db.NoSatisfiableBuilds, nil
   177  		}
   178  	}
   179  
   180  	// all passed constraints were satisfied
   181  	span.SetStatus(codes.OK, "")
   182  	return true, "", nil
   183  }
   184  
   185  func (r *groupResolver) tryJobBuilds(ctx context.Context, inputIndex int, passedJobID int, builds db.PaginatedBuilds) (bool, error) {
   186  	ctx, span := tracing.StartSpan(ctx, "groupResolver.tryJobBuilds", tracing.Attrs{})
   187  	defer span.End()
   188  
   189  	span.SetAttributes(label.Int("passedJobID", passedJobID))
   190  
   191  	for {
   192  		buildID, ok, err := builds.Next(ctx)
   193  		if err != nil {
   194  			tracing.End(span, err)
   195  			return false, err
   196  		}
   197  
   198  		if !ok {
   199  			// reached the end of the builds
   200  			span.SetStatus(codes.ResourceExhausted, "")
   201  			return false, nil
   202  		}
   203  
   204  		worked, err := r.tryBuildOutputs(ctx, inputIndex, passedJobID, buildID, builds.HasNext())
   205  		if err != nil {
   206  			tracing.End(span, err)
   207  			return false, err
   208  		}
   209  
   210  		if worked {
   211  			span.SetStatus(codes.OK, "")
   212  			return true, nil
   213  		}
   214  	}
   215  }
   216  
   217  func (r *groupResolver) tryBuildOutputs(ctx context.Context, resolvingIdx, jobID, buildID int, hasNext bool) (bool, error) {
   218  	ctx, span := tracing.StartSpan(ctx, "groupResolver.tryBuildOutputs", tracing.Attrs{})
   219  	defer span.End()
   220  
   221  	span.SetAttributes(label.Int("buildID", buildID))
   222  
   223  	outputs, err := r.vdb.SuccessfulBuildOutputs(ctx, buildID)
   224  	if err != nil {
   225  		tracing.End(span, err)
   226  		return false, err
   227  	}
   228  
   229  	restore := map[int]*versionCandidate{}
   230  	var mismatch bool
   231  
   232  	// loop over the resource versions that came out of this build set
   233  outputs:
   234  	for _, output := range outputs {
   235  		// try to pin each candidate to the versions from this build
   236  		for c, candidate := range r.candidates {
   237  			if _, ok := restore[c]; ok {
   238  				// we have already set a new version for this candidate within this
   239  				// build, so continue attempting the existing version
   240  				continue
   241  			}
   242  
   243  			var related bool
   244  			related, mismatch, err = r.outputIsRelatedAndMatches(ctx, span, output, c, jobID)
   245  			if err != nil {
   246  				tracing.End(span, err)
   247  				return false, err
   248  			}
   249  
   250  			if mismatch {
   251  				// build contained a different version than the one we already have for
   252  				// that candidate, so let's try a different build
   253  				break outputs
   254  			} else if !related {
   255  				// output is not even relevant to this candidate; move on
   256  				continue
   257  			}
   258  
   259  			if candidate == nil {
   260  				exists, err := r.vdb.VersionExists(ctx, output.ResourceID, output.Version)
   261  				if err != nil {
   262  					tracing.End(span, err)
   263  					return false, err
   264  				}
   265  
   266  				if !exists {
   267  					break outputs
   268  				}
   269  			}
   270  
   271  			// if this doesn't work out, restore it to either nil or the
   272  			// candidate *without* the job vouching for it
   273  			restore[c] = candidate
   274  
   275  			span.AddEvent(
   276  				ctx,
   277  				"vouching for candidate",
   278  				label.Int("resourceID", output.ResourceID),
   279  				label.String("version", string(output.Version)),
   280  			)
   281  
   282  			r.candidates[c] = r.vouchForCandidate(candidate, output.Version, jobID, buildID, hasNext)
   283  		}
   284  	}
   285  
   286  	// we found a candidate for ourselves and the rest are OK too - recurse
   287  	if r.candidates[resolvingIdx] != nil && r.candidates[resolvingIdx].VouchedForBy[jobID] && !mismatch {
   288  		if r.candidatesAreDoomed() {
   289  			span.AddEvent(
   290  				ctx,
   291  				"candidates are doomed",
   292  			)
   293  		} else {
   294  			worked, _, err := r.tryResolve(ctx)
   295  			if err != nil {
   296  				tracing.End(span, err)
   297  				return false, err
   298  			}
   299  
   300  			if worked {
   301  				// this build's candidates satisfied everything else!
   302  				span.SetStatus(codes.OK, "")
   303  				return true, nil
   304  			}
   305  
   306  			r.doomCandidates()
   307  		}
   308  	}
   309  
   310  	for c, candidate := range restore {
   311  		// either there was a mismatch or resolving didn't work; go on to the
   312  		// next output set
   313  		r.candidates[c] = candidate
   314  	}
   315  
   316  	span.SetStatus(codes.InvalidArgument, "")
   317  	return false, nil
   318  }
   319  
   320  func (r *groupResolver) doomCandidates() {
   321  	for i, c := range r.candidates {
   322  		r.doomedCandidates[i] = c
   323  	}
   324  }
   325  
   326  func (r *groupResolver) candidatesAreDoomed() bool {
   327  	for i, c := range r.candidates {
   328  		doomed := r.doomedCandidates[i]
   329  
   330  		if c == nil && doomed == nil {
   331  			continue
   332  		}
   333  
   334  		if c == nil && doomed != nil {
   335  			return false
   336  		}
   337  
   338  		if c != nil && doomed == nil {
   339  			return false
   340  		}
   341  
   342  		if doomed.Version != c.Version {
   343  			return false
   344  		}
   345  	}
   346  
   347  	return true
   348  }
   349  
   350  func (r *groupResolver) paginatedBuilds(ctx context.Context, currentInputConfig db.InputConfig, currentCandidate *versionCandidate, currentJobID int, passedJobID int) (db.PaginatedBuilds, bool, error) {
   351  	constraints := r.constrainingCandidates(passedJobID)
   352  
   353  	if currentInputConfig.UseEveryVersion {
   354  		if r.lastUsedPassedBuilds == nil {
   355  			lastUsedBuildIDs := map[int]db.BuildCursor{}
   356  
   357  			buildID, found, err := r.vdb.LatestBuildUsingLatestVersion(ctx, currentJobID, currentInputConfig.ResourceID)
   358  			if err != nil {
   359  				return db.PaginatedBuilds{}, false, err
   360  			}
   361  
   362  			if found {
   363  				lastUsedBuildIDs, err = r.vdb.LatestBuildPipes(ctx, buildID)
   364  				if err != nil {
   365  					return db.PaginatedBuilds{}, false, err
   366  				}
   367  
   368  				r.lastUsedPassedBuilds = lastUsedBuildIDs
   369  			}
   370  		}
   371  
   372  		relatedPassedBuilds := map[int]db.BuildCursor{}
   373  		for jobID, build := range r.lastUsedPassedBuilds {
   374  			if currentInputConfig.Passed[jobID] {
   375  				relatedPassedBuilds[jobID] = build
   376  			}
   377  		}
   378  
   379  		lastUsedBuild, hasUsedJob := relatedPassedBuilds[passedJobID]
   380  		if hasUsedJob {
   381  			var paginatedBuilds db.PaginatedBuilds
   382  			var err error
   383  
   384  			if currentCandidate != nil {
   385  				paginatedBuilds, err = r.vdb.UnusedBuildsVersionConstrained(ctx, passedJobID, lastUsedBuild, constraints)
   386  			} else {
   387  				paginatedBuilds, err = r.vdb.UnusedBuilds(ctx, passedJobID, lastUsedBuild)
   388  			}
   389  
   390  			return paginatedBuilds, false, err
   391  		} else if currentCandidate == nil && len(relatedPassedBuilds) > 0 {
   392  			// we've run with version: every and passed: before, just not with this
   393  			// job, and there's no candidate yet, so skip it for now and let the
   394  			// algorithm continue from where the other jobs left off rather than
   395  			// starting from 'latest'
   396  			//
   397  			// this job will eventually vouch for it during the recursive resolve
   398  			// call
   399  			return db.PaginatedBuilds{}, true, nil
   400  		}
   401  	}
   402  
   403  	var paginatedBuilds db.PaginatedBuilds
   404  	var err error
   405  	if currentCandidate != nil {
   406  		paginatedBuilds, err = r.vdb.SuccessfulBuildsVersionConstrained(ctx, passedJobID, constraints)
   407  	} else {
   408  		paginatedBuilds = r.vdb.SuccessfulBuilds(ctx, passedJobID)
   409  	}
   410  
   411  	return paginatedBuilds, false, err
   412  }
   413  
   414  func (r *groupResolver) constrainingCandidates(passedJobID int) map[string][]string {
   415  	constrainingCandidates := map[string][]string{}
   416  	for passedIndex, passedInput := range r.inputConfigs {
   417  		if passedInput.Passed[passedJobID] && r.candidates[passedIndex] != nil {
   418  			resID := strconv.Itoa(passedInput.ResourceID)
   419  			constrainingCandidates[resID] = append(constrainingCandidates[resID], string(r.candidates[passedIndex].Version))
   420  		}
   421  	}
   422  
   423  	return constrainingCandidates
   424  }
   425  
   426  func (r *groupResolver) outputIsRelatedAndMatches(ctx context.Context, span trace.Span, output db.AlgorithmVersion, candidateIdx int, passedJobID int) (bool, bool, error) {
   427  	inputConfig := r.inputConfigs[candidateIdx]
   428  	candidate := r.candidates[candidateIdx]
   429  
   430  	if inputConfig.ResourceID != output.ResourceID {
   431  		// unrelated; different resource
   432  		return false, false, nil
   433  	}
   434  
   435  	if !inputConfig.Passed[passedJobID] {
   436  		// unrelated; this input is unaffected by the current job
   437  		return false, false, nil
   438  	}
   439  
   440  	if candidate != nil && candidate.Version != output.Version {
   441  		// we have already chosen a version for the candidate but it's different
   442  		// from the version provided by this output
   443  		return false, true, nil
   444  	}
   445  
   446  	disabled, err := r.vdb.VersionIsDisabled(ctx, output.ResourceID, output.Version)
   447  	if err != nil {
   448  		return false, false, err
   449  	}
   450  
   451  	if disabled {
   452  		// this version is disabled so it cannot be used
   453  		span.AddEvent(
   454  			ctx,
   455  			"version disabled",
   456  			label.Int("resourceID", output.ResourceID),
   457  			label.String("version", string(output.Version)),
   458  		)
   459  		return false, false, nil
   460  	}
   461  
   462  	if inputConfig.PinnedVersion != nil && r.pins[candidateIdx] != output.Version {
   463  		// input is both pinned and assigned a 'passed' constraint, but the pinned
   464  		// version doesn't match the job's output version
   465  
   466  		span.AddEvent(
   467  			ctx,
   468  			"pin mismatch",
   469  			label.Int("resourceID", output.ResourceID),
   470  			label.String("outputHas", string(output.Version)),
   471  			label.String("pinHas", string(r.pins[candidateIdx])),
   472  		)
   473  
   474  		return false, false, nil
   475  	}
   476  
   477  	return true, false, nil
   478  }
   479  
   480  func (r *groupResolver) vouchForCandidate(oldCandidate *versionCandidate, version db.ResourceVersion, passedJobID int, passedBuildID int, hasNext bool) *versionCandidate {
   481  	// create a new candidate with the new version
   482  	newCandidate := newCandidateVersion(version)
   483  
   484  	// carry over the vouchers from the previous state of the candidate
   485  	if oldCandidate != nil {
   486  		for vJobID := range oldCandidate.VouchedForBy {
   487  			newCandidate.VouchedForBy[vJobID] = true
   488  		}
   489  
   490  		if len(oldCandidate.SourceBuildIds) != 0 {
   491  			for _, sourceBuildId := range oldCandidate.SourceBuildIds {
   492  				newCandidate.SourceBuildIds = append(newCandidate.SourceBuildIds, sourceBuildId)
   493  			}
   494  		}
   495  
   496  		newCandidate.HasNextEveryVersion = oldCandidate.HasNextEveryVersion
   497  	}
   498  
   499  	// vouch for the version with this new passed job and append the passed build
   500  	// that we used the outputs of to satisfy the input constraints. The source
   501  	// build IDs are used for the build pipes table.
   502  	newCandidate.VouchedForBy[passedJobID] = true
   503  	newCandidate.SourceBuildIds = append(newCandidate.SourceBuildIds, passedBuildID)
   504  	newCandidate.HasNextEveryVersion = newCandidate.HasNextEveryVersion || hasNext
   505  
   506  	return newCandidate
   507  }
   508  
   509  func (r *groupResolver) orderJobs(jobIDs map[int]bool) []int {
   510  	orderedJobs := []int{}
   511  	for id, _ := range jobIDs {
   512  		orderedJobs = append(orderedJobs, id)
   513  	}
   514  
   515  	sort.Ints(orderedJobs)
   516  
   517  	return orderedJobs
   518  }