Skip to content

Commit a5ca5e7

Browse files
ti-chi-botlhy1024
andauthored
resource control: fix unsafe usage of timer.Reset (tikv#8877) (tikv#8901)
close tikv#8876 Signed-off-by: lhy1024 <admin@liudos.us> Co-authored-by: lhy1024 <admin@liudos.us>
1 parent bc028fc commit a5ca5e7

28 files changed

Lines changed: 470 additions & 82 deletions

File tree

‎client/pd_service_discovery.go‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,14 +188,16 @@ func (c *pdServiceDiscovery) Init() error {
188188

189189
func (c *pdServiceDiscovery) initRetry(f func() error) error {
190190
var err error
191+
ticker := time.NewTicker(time.Second)
192+
defer ticker.Stop()
191193
for i := 0; i < c.option.maxRetryTimes; i++ {
192194
if err = f(); err == nil {
193195
return nil
194196
}
195197
select {
196198
case <-c.ctx.Done():
197199
return err
198-
case <-time.After(time.Second):
200+
case <-ticker.C:
199201
}
200202
}
201203
return errors.WithStack(err)

‎client/resource_group/controller/controller.go‎

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"github.com/prometheus/client_golang/prometheus"
3232
pd "github.com/tikv/pd/client"
3333
"github.com/tikv/pd/client/errs"
34+
"github.com/tikv/pd/client/timerutil"
3435
atomicutil "go.uber.org/atomic"
3536
"go.uber.org/zap"
3637
)
@@ -286,17 +287,17 @@ func (c *ResourceGroupsController) Start(ctx context.Context) {
286287
watchMetaChannel, err = c.provider.Watch(ctx, pd.GroupSettingsPathPrefixBytes, pd.WithRev(metaRevision), pd.WithPrefix(), pd.WithPrevKV())
287288
if err != nil {
288289
log.Warn("watch resource group meta failed", zap.Error(err))
289-
watchRetryTimer.Reset(watchRetryInterval)
290+
timerutil.SafeResetTimer(watchRetryTimer, watchRetryInterval)
290291
failpoint.Inject("watchStreamError", func() {
291-
watchRetryTimer.Reset(20 * time.Millisecond)
292+
timerutil.SafeResetTimer(watchRetryTimer, 20*time.Millisecond)
292293
})
293294
}
294295
}
295296
if watchConfigChannel == nil {
296297
watchConfigChannel, err = c.provider.Watch(ctx, pd.ControllerConfigPathPrefixBytes, pd.WithRev(cfgRevision), pd.WithPrefix())
297298
if err != nil {
298299
log.Warn("watch resource group config failed", zap.Error(err))
299-
watchRetryTimer.Reset(watchRetryInterval)
300+
timerutil.SafeResetTimer(watchRetryTimer, watchRetryInterval)
300301
}
301302
}
302303
case <-emergencyTokenAcquisitionTicker.C:
@@ -330,9 +331,9 @@ func (c *ResourceGroupsController) Start(ctx context.Context) {
330331
})
331332
if !ok {
332333
watchMetaChannel = nil
333-
watchRetryTimer.Reset(watchRetryInterval)
334+
timerutil.SafeResetTimer(watchRetryTimer, watchRetryInterval)
334335
failpoint.Inject("watchStreamError", func() {
335-
watchRetryTimer.Reset(20 * time.Millisecond)
336+
timerutil.SafeResetTimer(watchRetryTimer, 20*time.Millisecond)
336337
})
337338
continue
338339
}
@@ -366,9 +367,9 @@ func (c *ResourceGroupsController) Start(ctx context.Context) {
366367
case resp, ok := <-watchConfigChannel:
367368
if !ok {
368369
watchConfigChannel = nil
369-
watchRetryTimer.Reset(watchRetryInterval)
370+
timerutil.SafeResetTimer(watchRetryTimer, watchRetryInterval)
370371
failpoint.Inject("watchStreamError", func() {
371-
watchRetryTimer.Reset(20 * time.Millisecond)
372+
timerutil.SafeResetTimer(watchRetryTimer, 20*time.Millisecond)
372373
})
373374
continue
374375
}
@@ -521,7 +522,7 @@ func (c *ResourceGroupsController) sendTokenBucketRequests(ctx context.Context,
521522
ClientUniqueId: c.clientUniqueID,
522523
}
523524
if c.ruConfig.DegradedModeWaitDuration > 0 && c.responseDeadlineCh == nil {
524-
c.run.responseDeadline.Reset(c.ruConfig.DegradedModeWaitDuration)
525+
timerutil.SafeResetTimer(c.run.responseDeadline, c.ruConfig.DegradedModeWaitDuration)
525526
c.responseDeadlineCh = c.run.responseDeadline.C
526527
}
527528
go func() {

‎client/resource_manager_client.go‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,8 @@ func (c *client) tryResourceManagerConnect(ctx context.Context, connection *reso
348348
err error
349349
stream rmpb.ResourceManager_AcquireTokenBucketsClient
350350
)
351+
ticker := time.NewTicker(retryInterval)
352+
defer ticker.Stop()
351353
for i := 0; i < maxRetryTimes; i++ {
352354
cc, err := c.resourceManagerClient()
353355
if err != nil {
@@ -365,7 +367,7 @@ func (c *client) tryResourceManagerConnect(ctx context.Context, connection *reso
365367
select {
366368
case <-ctx.Done():
367369
return err
368-
case <-time.After(retryInterval):
370+
case <-ticker.C:
369371
}
370372
}
371373
return err

‎client/retry/backoff.go‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,11 @@ func (bo *BackOffer) Exec(
3434
fn func() error,
3535
) error {
3636
if err := fn(); err != nil {
37+
timer := time.NewTimer(bo.nextInterval())
38+
defer timer.Stop()
3739
select {
3840
case <-ctx.Done():
39-
case <-time.After(bo.nextInterval()):
41+
case <-timer.C:
4042
failpoint.Inject("backOffExecute", func() {
4143
testBackOffExecuteFlag = true
4244
})

‎client/timerutil/pool.go‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright 2020 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// Note: This file is copied from https://go-review.googlesource.com/c/go/+/276133
6+
7+
package timerutil
8+
9+
import (
10+
"sync"
11+
"time"
12+
)
13+
14+
// GlobalTimerPool is a global pool for reusing *time.Timer.
15+
var GlobalTimerPool TimerPool
16+
17+
// TimerPool is a wrapper of sync.Pool which caches *time.Timer for reuse.
18+
type TimerPool struct {
19+
pool sync.Pool
20+
}
21+
22+
// Get returns a timer with a given duration.
23+
func (tp *TimerPool) Get(d time.Duration) *time.Timer {
24+
if v := tp.pool.Get(); v != nil {
25+
timer := v.(*time.Timer)
26+
timer.Reset(d)
27+
return timer
28+
}
29+
return time.NewTimer(d)
30+
}
31+
32+
// Put tries to call timer.Stop() before putting it back into pool,
33+
// if the timer.Stop() returns false (it has either already expired or been stopped),
34+
// have a shot at draining the channel with residual time if there is one.
35+
func (tp *TimerPool) Put(timer *time.Timer) {
36+
if !timer.Stop() {
37+
select {
38+
case <-timer.C:
39+
default:
40+
}
41+
}
42+
tp.pool.Put(timer)
43+
}

‎client/timerutil/pool_test.go‎

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Copyright 2020 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
// Note: This file is copied from https://go-review.googlesource.com/c/go/+/276133
6+
7+
package timerutil
8+
9+
import (
10+
"testing"
11+
"time"
12+
)
13+
14+
func TestTimerPool(t *testing.T) {
15+
var tp TimerPool
16+
17+
for i := 0; i < 100; i++ {
18+
timer := tp.Get(20 * time.Millisecond)
19+
20+
select {
21+
case <-timer.C:
22+
t.Errorf("timer expired too early")
23+
continue
24+
default:
25+
}
26+
27+
select {
28+
case <-time.After(100 * time.Millisecond):
29+
t.Errorf("timer didn't expire on time")
30+
case <-timer.C:
31+
}
32+
33+
tp.Put(timer)
34+
}
35+
}
36+
37+
const timeout = 10 * time.Millisecond
38+
39+
func BenchmarkTimerUtilization(b *testing.B) {
40+
b.Run("TimerWithPool", func(b *testing.B) {
41+
for i := 0; i < b.N; i++ {
42+
t := GlobalTimerPool.Get(timeout)
43+
GlobalTimerPool.Put(t)
44+
}
45+
})
46+
b.Run("TimerWithoutPool", func(b *testing.B) {
47+
for i := 0; i < b.N; i++ {
48+
t := time.NewTimer(timeout)
49+
t.Stop()
50+
}
51+
})
52+
}
53+
54+
func BenchmarkTimerPoolParallel(b *testing.B) {
55+
b.RunParallel(func(pb *testing.PB) {
56+
for pb.Next() {
57+
t := GlobalTimerPool.Get(timeout)
58+
GlobalTimerPool.Put(t)
59+
}
60+
})
61+
}
62+
63+
func BenchmarkTimerNativeParallel(b *testing.B) {
64+
b.RunParallel(func(pb *testing.PB) {
65+
for pb.Next() {
66+
t := time.NewTimer(timeout)
67+
t.Stop()
68+
}
69+
})
70+
}

‎client/timerutil/util.go‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright 2024 TiKV Project Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package timerutil
16+
17+
import "time"
18+
19+
// SafeResetTimer is used to reset timer safely.
20+
// Before Go 1.23, the only safe way to use Reset was to call Timer.Stop and explicitly drain the timer first.
21+
// We need be careful here, see more details in the comments of Timer.Reset.
22+
// https://pkg.go.dev/time@master#Timer.Reset
23+
func SafeResetTimer(t *time.Timer, d time.Duration) {
24+
// Stop the timer if it's not stopped.
25+
if !t.Stop() {
26+
select {
27+
case <-t.C: // try to drain from the channel
28+
default:
29+
}
30+
}
31+
t.Reset(d)
32+
}

0 commit comments

Comments
 (0)