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  }