Skip to content

Commit 637b665

Browse files
committed
testutils/lint: add a linter to prohibit ignoring cancel func
We recently fixed a couple of issues (one was fresh, another was dormant for several years) where we had a memory leak because the cancel function returned on `stop.Stopper.WithCancelOnQuiesce` was ignored. In some cases (like when we have a singleton component that has lifetime matching that of the server) this is benign, but in some other cases this can be problematic. In order to prevent similar memory leaks from sneaking in in the future, this commit adds a linter that prohibits ignoring the cancel func. For the cases where it's benign, we add `nolint:quiesce` option. Release note: None
1 parent 9b7bfc2 commit 637b665

File tree

8 files changed

+66
-7
lines changed

8 files changed

+66
-7
lines changed

pkg/ccl/cliccl/mt_proxy.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,9 @@ func initLogging(cmd *cobra.Command) (ctx context.Context, stopper *stop.Stopper
108108
if err != nil {
109109
return ctx, nil, err
110110
}
111-
ctx, _ = stopper.WithCancelOnQuiesce(ctx)
111+
// The proxy will shutdown via the stopper, so we can ignore the
112+
// cancellation function here.
113+
ctx, _ = stopper.WithCancelOnQuiesce(ctx) // nolint:quiesce
112114
return ctx, stopper, err
113115
}
114116

pkg/ccl/sqlproxyccl/balancer/balancer.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,10 @@ func NewBalancer(
223223
}
224224

225225
// Ensure that ctx gets cancelled on stopper's quiescing.
226-
ctx, _ = stopper.WithCancelOnQuiesce(ctx)
226+
//
227+
// The balancer shares the same lifetime as the proxy which will shutdown
228+
// via the stopper, so we can ignore the cancellation function here.
229+
ctx, _ = stopper.WithCancelOnQuiesce(ctx) // nolint:quiesce
227230

228231
q, err := newRebalancerQueue(ctx, metrics)
229232
if err != nil {

pkg/ccl/sqlproxyccl/balancer/conn_tracker.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,10 @@ func NewConnTracker(
4848
ctx context.Context, stopper *stop.Stopper, timeSource timeutil.TimeSource,
4949
) (*ConnTracker, error) {
5050
// Ensure that ctx gets cancelled on stopper's quiescing.
51-
ctx, _ = stopper.WithCancelOnQuiesce(ctx)
51+
//
52+
// The tracker shares the same lifetime as the proxy which will shutdown
53+
// via the stopper, so we can ignore the cancellation function here.
54+
ctx, _ = stopper.WithCancelOnQuiesce(ctx) // nolint:quiesce
5255

5356
if timeSource == nil {
5457
timeSource = timeutil.DefaultTimeSource{}

pkg/ccl/sqlproxyccl/proxy_handler.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,9 @@ func newProxyHandler(
194194
proxyMetrics *metrics,
195195
options ProxyOptions,
196196
) (*proxyHandler, error) {
197-
ctx, _ = stopper.WithCancelOnQuiesce(ctx)
197+
// The proxy handler shares the same lifetime as the proxy which will shutdown
198+
// via the stopper, so we can ignore the cancellation function here.
199+
ctx, _ = stopper.WithCancelOnQuiesce(ctx) // nolint:quiesce
198200

199201
handler := proxyHandler{
200202
stopper: stopper,

pkg/rpc/context.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -546,7 +546,9 @@ func NewContext(ctx context.Context, opts ContextOptions) *Context {
546546
}
547547
}
548548

549-
masterCtx, _ := opts.Stopper.WithCancelOnQuiesce(ctx)
549+
// The RPC context has the same lifetime as the stopper (which often matches
550+
// the lifetime of the server), so we can ignore the cancellation function.
551+
masterCtx, _ := opts.Stopper.WithCancelOnQuiesce(ctx) // nolint:quiesce
550552

551553
secCtx := NewSecurityContext(
552554
SecurityContextOptions{

pkg/sql/stats/automatic_stats.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -533,7 +533,9 @@ func (r *Refresher) SetDraining() {
533533
func (r *Refresher) Start(
534534
ctx context.Context, stopper *stop.Stopper, refreshInterval time.Duration,
535535
) error {
536-
stoppingCtx, _ := stopper.WithCancelOnQuiesce(context.Background())
536+
// The refresher has the same lifetime as the server, so the cancellation
537+
// function can be ignored and it'll be called by the stopper.
538+
stoppingCtx, _ := stopper.WithCancelOnQuiesce(context.Background()) // nolint:quiesce
537539
bgCtx := r.AnnotateCtx(stoppingCtx)
538540
r.startedTasksWG.Add(1)
539541
if err := stopper.RunAsyncTask(bgCtx, "refresher", func(ctx context.Context) {

pkg/sql/stmtdiagnostics/statement_diagnostics.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,9 @@ func NewRegistry(db isql.DB, st *cluster.Settings) *Registry {
150150

151151
// Start will start the polling loop for the Registry.
152152
func (r *Registry) Start(ctx context.Context, stopper *stop.Stopper) {
153-
ctx, _ = stopper.WithCancelOnQuiesce(ctx)
153+
// The registry has the same lifetime as the server, so the cancellation
154+
// function can be ignored and it'll be called by the stopper.
155+
ctx, _ = stopper.WithCancelOnQuiesce(ctx) // nolint:quiesce
154156

155157
// Since background statement diagnostics collection is not under user
156158
// control, exclude it from cost accounting and control.

pkg/testutils/lint/lint_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2593,6 +2593,49 @@ func TestLint(t *testing.T) {
25932593
}
25942594
})
25952595

2596+
// This linter prohibits ignoring the context.CancelFunc that is returned on
2597+
// stop.Stopper.WithCancelOnQuiesce call (which can result in a memory
2598+
// leak).
2599+
//
2600+
// If the context is derived for a server singleton and has the same
2601+
// lifetime as the server, this linter can be ignored with
2602+
// 'nolint:quiesce' comment.
2603+
t.Run("TestWithCancelOnQuiesce", func(t *testing.T) {
2604+
t.Parallel()
2605+
cmd, stderr, filter, err := dirCmd(
2606+
pkgDir,
2607+
"git",
2608+
"grep",
2609+
"-nE",
2610+
`_.*WithCancelOnQuiesce`,
2611+
"--",
2612+
"*",
2613+
":!*_test.go",
2614+
)
2615+
if err != nil {
2616+
t.Fatal(err)
2617+
}
2618+
2619+
if err := cmd.Start(); err != nil {
2620+
t.Fatal(err)
2621+
}
2622+
2623+
if err := stream.ForEach(stream.Sequence(
2624+
filter,
2625+
stream.GrepNot(`nolint:quiesce`),
2626+
), func(s string) {
2627+
t.Errorf("\n%s <- forbidden; ensure the cancellation function is called", s)
2628+
}); err != nil {
2629+
t.Error(err)
2630+
}
2631+
2632+
if err := cmd.Wait(); err != nil {
2633+
if out := stderr.String(); len(out) > 0 {
2634+
t.Fatalf("err=%s, stderr=%s", err, out)
2635+
}
2636+
}
2637+
})
2638+
25962639
// RoachVet is expensive memory-wise and thus should not run with t.Parallel().
25972640
// RoachVet includes all of the passes of `go vet` plus first-party additions.
25982641
// See pkg/cmd/roachvet.

0 commit comments

Comments
 (0)