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 }