github.com/grafana/tanka@v0.26.1-0.20240506093700-c22cfc35c21a/pkg/tanka/parallel.go (about)

     1  package tanka
     2  
     3  import (
     4  	"fmt"
     5  	"path/filepath"
     6  	"time"
     7  
     8  	"k8s.io/apimachinery/pkg/labels"
     9  
    10  	"github.com/grafana/tanka/pkg/jsonnet/jpath"
    11  	"github.com/grafana/tanka/pkg/spec/v1alpha1"
    12  	"github.com/pkg/errors"
    13  	"github.com/rs/zerolog/log"
    14  )
    15  
    16  const defaultParallelism = 8
    17  
    18  type parallelOpts struct {
    19  	Opts
    20  	Selector    labels.Selector
    21  	Parallelism int
    22  }
    23  
    24  // parallelLoadEnvironments evaluates multiple environments in parallel
    25  func parallelLoadEnvironments(envs []*v1alpha1.Environment, opts parallelOpts) ([]*v1alpha1.Environment, error) {
    26  	jobsCh := make(chan parallelJob)
    27  	outCh := make(chan parallelOut, len(envs))
    28  
    29  	if opts.Parallelism <= 0 {
    30  		opts.Parallelism = defaultParallelism
    31  	}
    32  
    33  	if opts.Parallelism > len(envs) {
    34  		log.Info().Int("parallelism", opts.Parallelism).Int("envs", len(envs)).Msg("Reducing parallelism to match number of environments")
    35  		opts.Parallelism = len(envs)
    36  	}
    37  
    38  	for i := 0; i < opts.Parallelism; i++ {
    39  		go parallelWorker(jobsCh, outCh)
    40  	}
    41  
    42  	for _, env := range envs {
    43  		o := opts.Opts
    44  
    45  		if env.Spec.ExportJsonnetImplementation != "" {
    46  			log.Trace().
    47  				Str("name", env.Metadata.Name).
    48  				Str("implementation", env.Spec.ExportJsonnetImplementation).
    49  				Msg("Using custom Jsonnet implementation")
    50  			o.JsonnetImplementation = env.Spec.ExportJsonnetImplementation
    51  		}
    52  
    53  		// TODO: This is required because the map[string]string in here is not
    54  		// concurrency-safe. Instead of putting this burden on the caller, find
    55  		// a way to handle this inside the jsonnet package. A possible way would
    56  		// be to make the jsonnet package less general, more tightly coupling it
    57  		// to Tanka workflow thus being able to handle such cases
    58  		o.JsonnetOpts = o.JsonnetOpts.Clone()
    59  
    60  		o.Name = env.Metadata.Name
    61  		path := env.Metadata.Namespace
    62  		rootDir, err := jpath.FindRoot(path)
    63  		if err != nil {
    64  			return nil, errors.Wrap(err, "finding root")
    65  		}
    66  		jobsCh <- parallelJob{
    67  			path: filepath.Join(rootDir, path),
    68  			opts: o,
    69  		}
    70  	}
    71  	close(jobsCh)
    72  
    73  	var outenvs []*v1alpha1.Environment
    74  	var errors []error
    75  	for i := 0; i < len(envs); i++ {
    76  		out := <-outCh
    77  		if out.err != nil {
    78  			errors = append(errors, out.err)
    79  			continue
    80  		}
    81  		if opts.Selector == nil || opts.Selector.Empty() || opts.Selector.Matches(out.env.Metadata) {
    82  			outenvs = append(outenvs, out.env)
    83  		}
    84  	}
    85  
    86  	if len(errors) != 0 {
    87  		return outenvs, ErrParallel{errors: errors}
    88  	}
    89  
    90  	return outenvs, nil
    91  }
    92  
    93  type parallelJob struct {
    94  	path string
    95  	opts Opts
    96  }
    97  
    98  type parallelOut struct {
    99  	env *v1alpha1.Environment
   100  	err error
   101  }
   102  
   103  func parallelWorker(jobsCh <-chan parallelJob, outCh chan parallelOut) {
   104  	for job := range jobsCh {
   105  		log.Debug().Str("name", job.opts.Name).Str("path", job.path).Msg("Loading environment")
   106  		startTime := time.Now()
   107  
   108  		env, err := LoadEnvironment(job.path, job.opts)
   109  		if err != nil {
   110  			err = fmt.Errorf("%s:\n %w", job.path, err)
   111  		}
   112  		outCh <- parallelOut{env: env, err: err}
   113  
   114  		log.Debug().Str("name", job.opts.Name).Str("path", job.path).Dur("duration_ms", time.Since(startTime)).Msg("Finished loading environment")
   115  	}
   116  }