Skip to content

Commit bdd7923

Browse files
Copilotfredbi
andauthored
refactor(leak): replace init() guard with lazy sync.Once check (#82)
* Initial plan * 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 <copilot@github.com> Co-authored-by: fredbi <14262513+fredbi@users.noreply.github.com> * 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 <copilot@github.com> Co-authored-by: fredbi <14262513+fredbi@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: fredbi <14262513+fredbi@users.noreply.github.com>
1 parent 5700b9e commit bdd7923

File tree

1 file changed

+48
-43
lines changed

1 file changed

+48
-43
lines changed

internal/leak/leak.go

Lines changed: 48 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -25,52 +25,55 @@ const (
2525
waitFactor = 2
2626
)
2727

28-
func init() { //nolint:gochecknoinits // this init check is justify by the use of an internal volatile API.
29-
// check that the profile API behaves as expected or panic.
30-
//
31-
// The exact format of the labels reported in the profile stack is not documented and not guaranteed
32-
// to remain stable across go versions. We panic here to detect as early as possible any go API change
33-
// so we can quickly adapt to a new format.
34-
//
35-
// Even though we don't parse the complete stack, our detection method remains sentitive to labels formatting,
36-
// e.g. "labels: {key:value}".
37-
var wg sync.WaitGroup
38-
blocker := make(chan struct{})
39-
id := uniqueLabel()
40-
labels := pprof.Labels(labelKey, id)
41-
pprof.Do(context.Background(), labels, func(_ context.Context) {
42-
wg.Add(1)
43-
go func() {
44-
defer wg.Done()
45-
<-blocker // leaked: blocks forever until cleanup
46-
}()
47-
})
48-
needle := buildNeedle(id)
49-
profile := captureProfile()
50-
match := extractLabeledBlocks(profile, needle)
51-
if match == "" {
52-
// goroutine may not be scheduled yet: wait a bit before taking a decision
53-
54-
wait := time.Microsecond
55-
for range maxAttempts {
56-
time.Sleep(wait) // brief retry: goroutines may be mid-exit.
57-
profile = captureProfile()
58-
match = extractLabeledBlocks(profile, needle)
59-
if match != "" {
60-
break
61-
}
28+
var compatOnce sync.Once //nolint:gochecknoglobals // lazy guard for undocumented pprof label format
6229

63-
// retry — goroutine might still be exiting
64-
// wait exponential backoff, capped to maxWait
65-
wait = min(wait*waitFactor, maxWait)
30+
// ensureCompatible checks that the pprof goroutine profile labels
31+
// are formatted as expected. It runs at most once and panics if the
32+
// format has changed.
33+
//
34+
// The exact format of the labels reported in the profile stack is not
35+
// documented and not guaranteed to remain stable across Go versions.
36+
// This lazy guard detects any Go API change at the point of first use
37+
// so we can quickly adapt to a new format, without imposing overhead
38+
// on programs that never call [Leaked].
39+
func ensureCompatible() {
40+
compatOnce.Do(func() {
41+
var wg sync.WaitGroup
42+
blocker := make(chan struct{})
43+
id := uniqueLabel()
44+
labels := pprof.Labels(labelKey, id)
45+
pprof.Do(context.Background(), labels, func(_ context.Context) {
46+
wg.Go(func() {
47+
<-blocker // leaked: blocks forever until cleanup
48+
})
49+
})
50+
needle := buildNeedle(id)
51+
profile := captureProfile()
52+
match := extractLabeledBlocks(profile, needle)
53+
if match == "" {
54+
// goroutine may not be scheduled yet: wait a bit before taking a decision
55+
56+
wait := time.Microsecond
57+
for range maxAttempts {
58+
time.Sleep(wait) // brief retry: goroutines may be mid-exit.
59+
profile = captureProfile()
60+
match = extractLabeledBlocks(profile, needle)
61+
if match != "" {
62+
break
63+
}
64+
65+
// retry — goroutine might still be exiting
66+
// wait exponential backoff, capped to maxWait
67+
wait = min(wait*waitFactor, maxWait)
68+
}
6669
}
67-
}
6870

69-
close(blocker)
70-
wg.Wait()
71-
if match == "" {
72-
panic("unrecognized goroutine profile format: go API has changed unexpectedly")
73-
}
71+
close(blocker)
72+
wg.Wait()
73+
if match == "" {
74+
panic("unrecognized goroutine profile format: go API has changed unexpectedly")
75+
}
76+
})
7477
}
7578

7679
// Leaked instruments the tested function with a [pprof] label.
@@ -81,6 +84,8 @@ func init() { //nolint:gochecknoinits // this init check is justify by the use o
8184
// Returns the matching portion of the profile text if leaks are found,
8285
// or the empty string if clean.
8386
func Leaked(ctx context.Context, tested func()) string {
87+
ensureCompatible()
88+
8489
id := uniqueLabel()
8590
labels := pprof.Labels(labelKey, id)
8691

0 commit comments

Comments
 (0)