From 558dd8aea05673171c02920c1dd98d18bb1ea88e Mon Sep 17 00:00:00 2001 From: SAY-5 Date: Fri, 1 May 2026 23:10:11 -0700 Subject: [PATCH 1/3] types/project: collect service results in local map to avoid race Closes #852. Concurrent collector goroutine writing newProject.Services raced with the dispatch loop reading it. Signed-off-by: SAY-5 --- types/project.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/types/project.go b/types/project.go index 66365faed..d509e1057 100644 --- a/types/project.go +++ b/types/project.go @@ -870,19 +870,18 @@ func (p *Project) WithServicesTransform(fn func(name string, s ServiceConfig) (S newProject := p.deepCopy() eg, ctx := errgroup.WithContext(context.Background()) + collected := Services{} eg.Go(func() error { - s := Services{} for expect > 0 { select { case <-ctx.Done(): // interrupted as some goroutine returned an error return nil case r := <-resultCh: - s[r.name] = r.service + collected[r.name] = r.service expect-- } } - newProject.Services = s return nil }) for n, s := range newProject.Services { @@ -900,7 +899,11 @@ func (p *Project) WithServicesTransform(fn func(name string, s ServiceConfig) (S return nil }) } - return newProject, eg.Wait() + if err := eg.Wait(); err != nil { + return newProject, err + } + newProject.Services = collected + return newProject, nil } // CheckContainerNameUnicity validate project doesn't have services declaring the same container_name From 2c3cd4473fa59bb8987f61b73d73fd33ea94b5de Mon Sep 17 00:00:00 2001 From: Sai Asish Y Date: Tue, 26 May 2026 19:26:14 -0700 Subject: [PATCH 2/3] test(types): add concurrent transform test for WithServicesTransform Signed-off-by: Sai Asish Y --- types/project_test.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/types/project_test.go b/types/project_test.go index 1abd5ca61..ee0110236 100644 --- a/types/project_test.go +++ b/types/project_test.go @@ -659,3 +659,23 @@ func TestProject_WithServicesEnvironmentResolved(t *testing.T) { func ptr[T any](s T) *T { return &s } + +func TestWithServicesTransform_concurrent(t *testing.T) { + const n = 100 + p := &Project{Services: Services{}} + for i := 0; i < n; i++ { + p.Services[fmt.Sprintf("svc_%d", i)] = ServiceConfig{Image: fmt.Sprintf("img_%d", i)} + } + + got, err := p.WithServicesTransform(func(name string, s ServiceConfig) (ServiceConfig, error) { + s.Image = s.Image + "-transformed" + return s, nil + }) + assert.NilError(t, err) + assert.Equal(t, len(got.Services), n) + for i := 0; i < n; i++ { + name := fmt.Sprintf("svc_%d", i) + want := fmt.Sprintf("img_%d-transformed", i) + assert.Equal(t, got.Services[name].Image, want) + } +} From 70ee31b124cb4b13cf8a7727e0e6d653b78fa286 Mon Sep 17 00:00:00 2001 From: Sai Asish Y Date: Thu, 28 May 2026 10:51:55 -0700 Subject: [PATCH 3/3] test(types): satisfy revive/gocritic in WithServicesTransform race test Signed-off-by: Sai Asish Y --- types/project_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/types/project_test.go b/types/project_test.go index ee0110236..bb77fdf25 100644 --- a/types/project_test.go +++ b/types/project_test.go @@ -667,8 +667,8 @@ func TestWithServicesTransform_concurrent(t *testing.T) { p.Services[fmt.Sprintf("svc_%d", i)] = ServiceConfig{Image: fmt.Sprintf("img_%d", i)} } - got, err := p.WithServicesTransform(func(name string, s ServiceConfig) (ServiceConfig, error) { - s.Image = s.Image + "-transformed" + got, err := p.WithServicesTransform(func(_ string, s ServiceConfig) (ServiceConfig, error) { + s.Image += "-transformed" return s, nil }) assert.NilError(t, err)