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 }