diff --git a/go.mod b/go.mod index 0ac8746..4f2cdec 100644 --- a/go.mod +++ b/go.mod @@ -48,10 +48,12 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.22.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/hayageek/threadsafe v1.0.1 // indirect github.com/klauspost/compress v1.17.9 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/lufia/plan9stats v0.0.0-20240819163618-b1d8f4d146e7 // indirect diff --git a/go.sum b/go.sum index 3ffd4f1..52f5d36 100644 --- a/go.sum +++ b/go.sum @@ -141,6 +141,8 @@ github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5x github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 h1:zN2lZNZRflqFyxVaTIU61KNKQ9C0055u9CAfpmqUvo4= +github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3/go.mod h1:nPpo7qLxd6XL3hWJG/O60sR8ZKfMCiIoNap5GvD12KU= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= @@ -161,6 +163,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hayageek/threadsafe v1.0.1 h1:QTMJrninAaGQE7+CYdJWPzTlnhcBPwhxD5GDdvIf5oU= +github.com/hayageek/threadsafe v1.0.1/go.mod h1:Uhu/endHEMkw2SsIk1I4xYTGTnWvuGhWOBPIFWO+mPk= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kinbiko/jsonassert v1.1.1 h1:DB12divY+YB+cVpHULLuKePSi6+ui4M/shHSzJISkSE= github.com/kinbiko/jsonassert v1.1.1/go.mod h1:NO4lzrogohtIdNUNzx8sdzB55M4R4Q1bsrWVdqQ7C+A= diff --git a/internal/checks/organization/octopus_unused_projects_check.go b/internal/checks/organization/octopus_unused_projects_check.go index 434b136..383a0b7 100644 --- a/internal/checks/organization/octopus_unused_projects_check.go +++ b/internal/checks/organization/octopus_unused_projects_check.go @@ -1,6 +1,7 @@ package organization import ( + "context" "errors" "fmt" "github.com/OctopusDeploy/go-octopusdeploy/v2/pkg/client" @@ -8,7 +9,10 @@ import ( "github.com/OctopusSolutionsEngineering/OctopusRecommendationEngine/internal/checks" "github.com/OctopusSolutionsEngineering/OctopusRecommendationEngine/internal/client_wrapper" "github.com/OctopusSolutionsEngineering/OctopusRecommendationEngine/internal/config" + "github.com/OctopusSolutionsEngineering/OctopusRecommendationEngine/internal/executor" + "github.com/hayageek/threadsafe" "go.uber.org/zap" + "golang.org/x/sync/errgroup" "strings" "time" ) @@ -52,44 +56,65 @@ func (o OctopusUnusedProjectsCheck) Execute() (checks.OctopusCheckResult, error) return o.errorHandler.HandleError(o.Id(), checks.Organization, err) } - unusedProjects := []string{} + g, _ := errgroup.WithContext(context.Background()) + g.SetLimit(executor.CheckParallelTasks) + + unusedProjects := threadsafe.NewSlice[string]() + goroutineErrors := threadsafe.NewSlice[error]() + for i, project := range projects { - zap.L().Debug(o.Id() + " " + fmt.Sprintf("%.2f", float32(i+1)/float32(len(projects))*100) + "% complete") + i := i - // Ignore disabled projects - if project.IsDisabled { - continue - } + g.Go(func() error { + zap.L().Debug(o.Id() + " " + fmt.Sprintf("%.2f", float32(i+1)/float32(len(projects))*100) + "% complete") - projectHasTask := false + // Ignore disabled projects + if project.IsDisabled { + return nil + } - tasks, err := o.client.Tasks.Get(tasks.TasksQuery{ - Project: project.ID, - Skip: 0, - Take: 100, - }) + projectHasTask := false - if err != nil { - return o.errorHandler.HandleError(o.Id(), checks.Organization, err) - } + tasks, err := o.client.Tasks.Get(tasks.TasksQuery{ + Project: project.ID, + Skip: 0, + Take: 100, + }) - for _, task := range tasks.Items { - if task.StartTime != nil && task.StartTime.After(time.Now().Add(-time.Hour*24*time.Duration(o.config.MaxDaysSinceLastTask))) { - projectHasTask = true - break + if err != nil { + goroutineErrors.Append(err) + return nil } - } - if !projectHasTask { - unusedProjects = append(unusedProjects, project.Name) - } + for _, task := range tasks.Items { + if task.StartTime != nil && task.StartTime.After(time.Now().Add(-time.Hour*24*time.Duration(o.config.MaxDaysSinceLastTask))) { + projectHasTask = true + break + } + } + + if !projectHasTask { + unusedProjects.Append(project.Name) + } + + return nil + }) + } + + if err := g.Wait(); err != nil { + return nil, err + } + + // Treat the first error as the root cause + if goroutineErrors.Length() > 0 { + return o.errorHandler.HandleError(o.Id(), checks.Organization, goroutineErrors.Values()[0]) } daysString := fmt.Sprintf("%d", o.config.MaxDaysSinceLastTask) - if len(unusedProjects) > 0 { + if unusedProjects.Length() > 0 { return checks.NewOctopusCheckResultImpl( - "The following projects have not had any tasks in "+daysString+" days:\n"+strings.Join(unusedProjects, "\n"), + "The following projects have not had any tasks in "+daysString+" days:\n"+strings.Join(unusedProjects.Values(), "\n"), o.Id(), "", checks.Warning, diff --git a/internal/executor/octopus_check_executor.go b/internal/executor/octopus_check_executor.go index 9aa54f0..2fe9a14 100644 --- a/internal/executor/octopus_check_executor.go +++ b/internal/executor/octopus_check_executor.go @@ -8,6 +8,7 @@ import ( ) const ParallelTasks = 15 +const CheckParallelTasks = 2 // OctopusCheckExecutor is responsible for running each lint check and returning the results. It deals with things // like retries and error handling.