From b12cd28503ffdc8fc28d14cd635690e38def629f Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Wed, 27 Aug 2025 04:02:28 -0600 Subject: [PATCH] fix: installer action goroutine count Signed-off-by: Terry Howe --- pkg/action/install.go | 11 ++++++++++- pkg/action/install_test.go | 29 +++++++++++++---------------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/pkg/action/install.go b/pkg/action/install.go index 276009b5c..b5b45bd42 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -30,6 +30,7 @@ import ( "path/filepath" "strings" "sync" + "sync/atomic" "text/template" "time" @@ -126,7 +127,8 @@ type Install struct { TakeOwnership bool PostRenderer postrender.PostRenderer // Lock to control raceconditions when the process receives a SIGTERM - Lock sync.Mutex + Lock sync.Mutex + goroutineCount atomic.Int32 } // ChartPathOptions captures common options used for controlling chart paths @@ -446,8 +448,10 @@ func (i *Install) performInstallCtx(ctx context.Context, rel *release.Release, t resultChan := make(chan Msg, 1) go func() { + i.goroutineCount.Add(1) rel, err := i.performInstall(rel, toBeAdopted, resources) resultChan <- Msg{rel, err} + i.goroutineCount.Add(-1) }() select { case <-ctx.Done(): @@ -458,6 +462,11 @@ func (i *Install) performInstallCtx(ctx context.Context, rel *release.Release, t } } +// getGoroutineCount return the number of running routines +func (i *Install) getGoroutineCount() int32 { + return i.goroutineCount.Load() +} + // isDryRun returns true if Upgrade is set to run as a DryRun func (i *Install) isDryRun() bool { if i.DryRun || i.DryRunOption == "client" || i.DryRunOption == "server" || i.DryRunOption == "true" { diff --git a/pkg/action/install_test.go b/pkg/action/install_test.go index f567b3df4..fa9cfb222 100644 --- a/pkg/action/install_test.go +++ b/pkg/action/install_test.go @@ -28,7 +28,6 @@ import ( "os" "path/filepath" "regexp" - "runtime" "strings" "testing" "time" @@ -330,8 +329,8 @@ func TestInstallRelease_WithChartAndDependencyParentNotes(t *testing.T) { } rel, err := instAction.cfg.Releases.Get(res.Name, res.Version) - is.Equal("with-notes", rel.Name) is.NoError(err) + is.Equal("with-notes", rel.Name) is.Equal("parent", rel.Info.Notes) is.Equal(rel.Info.Description, "Install complete") } @@ -349,8 +348,8 @@ func TestInstallRelease_WithChartAndDependencyAllNotes(t *testing.T) { } rel, err := instAction.cfg.Releases.Get(res.Name, res.Version) - is.Equal("with-notes", rel.Name) is.NoError(err) + is.Equal("with-notes", rel.Name) // test run can return as either 'parent\nchild' or 'child\nparent' if !strings.Contains(rel.Info.Notes, "parent") && !strings.Contains(rel.Info.Notes, "child") { t.Fatalf("Expected 'parent\nchild' or 'child\nparent', got '%s'", rel.Info.Notes) @@ -454,9 +453,7 @@ func TestInstallReleaseIncorrectTemplate_DryRun(t *testing.T) { if err == nil { t.Fatalf("Install should fail containing error: %s", expectedErr) } - if err != nil { - is.Contains(err.Error(), expectedErr) - } + is.Contains(err.Error(), expectedErr) } func TestInstallRelease_NoHooks(t *testing.T) { @@ -541,14 +538,14 @@ func TestInstallRelease_Wait(t *testing.T) { instAction.WaitStrategy = kube.StatusWatcherStrategy vals := map[string]interface{}{} - goroutines := runtime.NumGoroutine() + goroutines := instAction.getGoroutineCount() res, err := instAction.Run(buildChart(), vals) is.Error(err) is.Contains(res.Info.Description, "I timed out") is.Equal(res.Info.Status, release.StatusFailed) - is.Equal(goroutines, runtime.NumGoroutine()) + is.Equal(goroutines, instAction.getGoroutineCount()) } func TestInstallRelease_Wait_Interrupted(t *testing.T) { is := assert.New(t) @@ -563,15 +560,15 @@ func TestInstallRelease_Wait_Interrupted(t *testing.T) { ctx, cancel := context.WithCancel(t.Context()) time.AfterFunc(time.Second, cancel) - goroutines := runtime.NumGoroutine() + goroutines := instAction.getGoroutineCount() _, err := instAction.RunWithContext(ctx, buildChart(), vals) is.Error(err) is.Contains(err.Error(), "context canceled") - is.Equal(goroutines+1, runtime.NumGoroutine()) // installation goroutine still is in background - time.Sleep(10 * time.Second) // wait for goroutine to finish - is.Equal(goroutines, runtime.NumGoroutine()) + is.Equal(goroutines+1, instAction.getGoroutineCount()) // installation goroutine still is in background + time.Sleep(10 * time.Second) // wait for goroutine to finish + is.Equal(goroutines, instAction.getGoroutineCount()) } func TestInstallRelease_WaitForJobs(t *testing.T) { is := assert.New(t) @@ -647,7 +644,7 @@ func TestInstallRelease_RollbackOnFailure_Interrupted(t *testing.T) { ctx, cancel := context.WithCancel(t.Context()) time.AfterFunc(time.Second, cancel) - goroutines := runtime.NumGoroutine() + goroutines := instAction.getGoroutineCount() res, err := instAction.RunWithContext(ctx, buildChart(), vals) is.Error(err) @@ -659,9 +656,9 @@ func TestInstallRelease_RollbackOnFailure_Interrupted(t *testing.T) { _, err = instAction.cfg.Releases.Get(res.Name, res.Version) is.Error(err) is.Equal(err, driver.ErrReleaseNotFound) - is.Equal(goroutines+1, runtime.NumGoroutine()) // installation goroutine still is in background - time.Sleep(10 * time.Second) // wait for goroutine to finish - is.Equal(goroutines, runtime.NumGoroutine()) + is.Equal(goroutines+1, instAction.getGoroutineCount()) // installation goroutine still is in background + time.Sleep(10 * time.Second) // wait for goroutine to finish + is.Equal(goroutines, instAction.getGoroutineCount()) } func TestNameTemplate(t *testing.T) {