github.com/pf-qiu/concourse/v6@v6.7.3-0.20201207032516-1f455d73275f/atc/exec/in_parallel.go (about) 1 package exec 2 3 import ( 4 "context" 5 "errors" 6 "sync/atomic" 7 8 "github.com/pf-qiu/concourse/v6/atc" 9 "github.com/pf-qiu/concourse/v6/atc/util" 10 "github.com/hashicorp/go-multierror" 11 ) 12 13 // InParallelStep is a step of steps to run in parallel. 14 type InParallelStep struct { 15 steps []Step 16 maxInFlight atc.MaxInFlightConfig 17 failFast bool 18 } 19 20 // InParallel constructs an InParallelStep. 21 func InParallel(steps []Step, limit int, failFast bool) InParallelStep { 22 maxInFlight := atc.MaxInFlightConfig{Limit: limit} 23 if limit < 1 { 24 maxInFlight.All = true 25 } 26 return InParallelStep{ 27 steps: steps, 28 maxInFlight: maxInFlight, 29 failFast: failFast, 30 } 31 } 32 33 // Run executes all steps in order and ensures that the number of running steps 34 // does not exceed the optional limit to parallelism. By default the limit is equal 35 // to the number of steps, which means all steps will all be executed in parallel. 36 // 37 // Fail fast can be used to abort running steps if any steps exit with an error. When set 38 // to false, parallel wil wait for all the steps to exit even if a step fails or errors. 39 // 40 // Cancelling a parallel step means that any outstanding steps will not be scheduled to run. 41 // After all steps finish, their errors (if any) will be collected and returned as a 42 // single error. 43 func (step InParallelStep) Run(ctx context.Context, state RunState) (bool, error) { 44 return parallelExecutor{ 45 stepName: "in_parallel", 46 47 maxInFlight: &step.maxInFlight, 48 failFast: step.failFast, 49 count: len(step.steps), 50 51 runFunc: func(ctx context.Context, i int) (bool, error) { 52 return step.steps[i].Run(ctx, state) 53 }, 54 }.run(ctx) 55 } 56 57 type parallelExecutor struct { 58 stepName string 59 60 maxInFlight *atc.MaxInFlightConfig 61 failFast bool 62 count int 63 64 runFunc func(ctx context.Context, i int) (bool, error) 65 } 66 67 func (p parallelExecutor) run(ctx context.Context) (bool, error) { 68 var ( 69 errs = make(chan error, p.count) 70 sem = make(chan bool, p.maxInFlight.EffectiveLimit(p.count)) 71 executedSteps int 72 ) 73 74 runCtx, cancel := context.WithCancel(ctx) 75 defer cancel() 76 77 var numFailures uint32 = 0 78 for i := 0; i < p.count; i++ { 79 i := i 80 sem <- true 81 if runCtx.Err() != nil { 82 break 83 } 84 go func() { 85 defer func() { 86 err := util.DumpPanic(recover(), "%s step", p.stepName) 87 if err != nil { 88 errs <- err 89 } 90 }() 91 defer func() { 92 <-sem 93 }() 94 95 succeeded, err := p.runFunc(runCtx, i) 96 if !succeeded { 97 atomic.AddUint32(&numFailures, 1) 98 if p.failFast { 99 cancel() 100 } 101 } 102 errs <- err 103 }() 104 executedSteps++ 105 } 106 107 var result error 108 for i := 0; i < executedSteps; i++ { 109 err := <-errs 110 if err != nil && !errors.Is(err, context.Canceled) { 111 // The Run context being cancelled only means that one or more steps failed, not 112 // in_parallel itself. If we return context.Canceled error messages the step will 113 // be marked as errored instead of failed, and therefore they should be ignored. 114 result = multierror.Append(result, err) 115 } 116 } 117 118 if ctx.Err() != nil { 119 return false, ctx.Err() 120 } 121 122 if result != nil { 123 return false, result 124 } 125 126 allStepsSuccessful := atomic.LoadUint32(&numFailures) == 0 127 return allStepsSuccessful, nil 128 }