From 0c3b44d504559dda5c315230736d927943c56470 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 18:04:13 +0000 Subject: [PATCH 1/3] Initial plan From 2d5d8b11f5e8d46c097b2ed5aa69ca6ecab97f18 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 18:08:28 +0000 Subject: [PATCH 2/3] refactor(leak): replace init() guard with lazy sync.Once check The compatibility guard for pprof goroutine profile label format now runs lazily via sync.Once on the first call to Leaked(), instead of eagerly in init(). This avoids overhead and potential panics for programs that import the assertions package but never use NoGoRoutineLeak. Signed-off-by: GitHub Copilot Co-authored-by: fredbi <14262513+fredbi@users.noreply.github.com> --- internal/leak/leak.go | 93 +++++++++++++++++++++++-------------------- 1 file changed, 50 insertions(+), 43 deletions(-) diff --git a/internal/leak/leak.go b/internal/leak/leak.go index 259a9315f..f7a256020 100644 --- a/internal/leak/leak.go +++ b/internal/leak/leak.go @@ -25,52 +25,57 @@ const ( waitFactor = 2 ) -func init() { //nolint:gochecknoinits // this init check is justify by the use of an internal volatile API. - // check that the profile API behaves as expected or panic. - // - // The exact format of the labels reported in the profile stack is not documented and not guaranteed - // to remain stable across go versions. We panic here to detect as early as possible any go API change - // so we can quickly adapt to a new format. - // - // Even though we don't parse the complete stack, our detection method remains sentitive to labels formatting, - // e.g. "labels: {key:value}". - var wg sync.WaitGroup - blocker := make(chan struct{}) - id := uniqueLabel() - labels := pprof.Labels(labelKey, id) - pprof.Do(context.Background(), labels, func(_ context.Context) { - wg.Add(1) - go func() { - defer wg.Done() - <-blocker // leaked: blocks forever until cleanup - }() - }) - needle := buildNeedle(id) - profile := captureProfile() - match := extractLabeledBlocks(profile, needle) - if match == "" { - // goroutine may not be scheduled yet: wait a bit before taking a decision - - wait := time.Microsecond - for range maxAttempts { - time.Sleep(wait) // brief retry: goroutines may be mid-exit. - profile = captureProfile() - match = extractLabeledBlocks(profile, needle) - if match != "" { - break - } +var compatOnce sync.Once - // retry — goroutine might still be exiting - // wait exponential backoff, capped to maxWait - wait = min(wait*waitFactor, maxWait) +// ensureCompatible checks that the pprof goroutine profile labels +// are formatted as expected. It runs at most once and panics if the +// format has changed. +// +// The exact format of the labels reported in the profile stack is not +// documented and not guaranteed to remain stable across Go versions. +// This lazy guard detects any Go API change at the point of first use +// so we can quickly adapt to a new format, without imposing overhead +// on programs that never call [Leaked]. +func ensureCompatible() { + compatOnce.Do(func() { + var wg sync.WaitGroup + blocker := make(chan struct{}) + id := uniqueLabel() + labels := pprof.Labels(labelKey, id) + pprof.Do(context.Background(), labels, func(_ context.Context) { + wg.Add(1) + go func() { + defer wg.Done() + <-blocker // leaked: blocks forever until cleanup + }() + }) + needle := buildNeedle(id) + profile := captureProfile() + match := extractLabeledBlocks(profile, needle) + if match == "" { + // goroutine may not be scheduled yet: wait a bit before taking a decision + + wait := time.Microsecond + for range maxAttempts { + time.Sleep(wait) // brief retry: goroutines may be mid-exit. + profile = captureProfile() + match = extractLabeledBlocks(profile, needle) + if match != "" { + break + } + + // retry — goroutine might still be exiting + // wait exponential backoff, capped to maxWait + wait = min(wait*waitFactor, maxWait) + } } - } - close(blocker) - wg.Wait() - if match == "" { - panic("unrecognized goroutine profile format: go API has changed unexpectedly") - } + close(blocker) + wg.Wait() + if match == "" { + panic("unrecognized goroutine profile format: go API has changed unexpectedly") + } + }) } // Leaked instruments the tested function with a [pprof] label. @@ -81,6 +86,8 @@ func init() { //nolint:gochecknoinits // this init check is justify by the use o // Returns the matching portion of the profile text if leaks are found, // or the empty string if clean. func Leaked(ctx context.Context, tested func()) string { + ensureCompatible() + id := uniqueLabel() labels := pprof.Labels(labelKey, id) From f0a59706b7b492a08a8a41252607bb73f9f88017 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 18:36:40 +0000 Subject: [PATCH 3/3] fix(leak): address linting issues - nolint:gochecknoglobals and wg.Go - Add //nolint:gochecknoglobals annotation with justification on compatOnce - Replace wg.Add(1)/go func()/defer wg.Done() with wg.Go() (Go 1.25) Signed-off-by: GitHub Copilot Co-authored-by: fredbi <14262513+fredbi@users.noreply.github.com> --- internal/leak/leak.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/internal/leak/leak.go b/internal/leak/leak.go index f7a256020..deab0d56f 100644 --- a/internal/leak/leak.go +++ b/internal/leak/leak.go @@ -25,7 +25,7 @@ const ( waitFactor = 2 ) -var compatOnce sync.Once +var compatOnce sync.Once //nolint:gochecknoglobals // lazy guard for undocumented pprof label format // ensureCompatible checks that the pprof goroutine profile labels // are formatted as expected. It runs at most once and panics if the @@ -43,11 +43,9 @@ func ensureCompatible() { id := uniqueLabel() labels := pprof.Labels(labelKey, id) pprof.Do(context.Background(), labels, func(_ context.Context) { - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { <-blocker // leaked: blocks forever until cleanup - }() + }) }) needle := buildNeedle(id) profile := captureProfile()