Skip to content

Commit d19c2f6

Browse files
veirfunaavanufriev
andauthored
Added options for configuring removal of idle connections (#674)
* Added options for configuring removal of idle connections * Removing idle connections in a separate goroutine * Removed lastUsage and stopChan. Used AfterFunc * Start timer only when number of idle wires is greater than minSize * Improvements after review --------- Co-authored-by: avanufriev <[email protected]>
1 parent 5965fb9 commit d19c2f6

File tree

5 files changed

+185
-35
lines changed

5 files changed

+185
-35
lines changed

mux.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,9 @@ func newMux(dst string, option *ClientOption, init, dead wire, wireFn wireFn, wi
9999
for i := 0; i < len(m.wire); i++ {
100100
m.wire[i].Store(init)
101101
}
102-
m.dpool = newPool(option.BlockingPoolSize, dead, wireFn)
103-
m.spool = newPool(option.BlockingPoolSize, dead, wireNoBgFn)
102+
103+
m.dpool = newPool(option.BlockingPoolSize, dead, option.IdleConnTTL, option.BlockingPoolMinSize, wireFn)
104+
m.spool = newPool(option.BlockingPoolSize, dead, option.IdleConnTTL, option.BlockingPoolMinSize, wireNoBgFn)
104105
return m
105106
}
106107

pipe_test.go

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1005,7 +1005,7 @@ func TestDoStreamRecycle(t *testing.T) {
10051005
go func() {
10061006
mock.Expect("PING").ReplyString("OK")
10071007
}()
1008-
conns := newPool(1, nil, nil)
1008+
conns := newPool(1, nil, 0, 0, nil)
10091009
s := p.DoStream(context.Background(), conns, cmds.NewCompleted([]string{"PING"}))
10101010
buf := bytes.NewBuffer(nil)
10111011
if err := s.Error(); err != nil {
@@ -1058,7 +1058,7 @@ func TestDoStreamRecycleDestinationFull(t *testing.T) {
10581058
go func() {
10591059
mock.Expect("PING").ReplyBlobString("OK")
10601060
}()
1061-
conns := newPool(1, nil, nil)
1061+
conns := newPool(1, nil, 0, 0, nil)
10621062
s := p.DoStream(context.Background(), conns, cmds.NewCompleted([]string{"PING"}))
10631063
buf := &limitedbuffer{buf: make([]byte, 1)}
10641064
if err := s.Error(); err != nil {
@@ -1091,7 +1091,7 @@ func TestDoMultiStreamRecycle(t *testing.T) {
10911091
go func() {
10921092
mock.Expect("PING").Expect("PING").ReplyString("OK").ReplyString("OK")
10931093
}()
1094-
conns := newPool(1, nil, nil)
1094+
conns := newPool(1, nil, 0, 0, nil)
10951095
s := p.DoMultiStream(context.Background(), conns, cmds.NewCompleted([]string{"PING"}), cmds.NewCompleted([]string{"PING"}))
10961096
buf := bytes.NewBuffer(nil)
10971097
if err := s.Error(); err != nil {
@@ -1124,7 +1124,7 @@ func TestDoMultiStreamRecycleDestinationFull(t *testing.T) {
11241124
go func() {
11251125
mock.Expect("PING").Expect("PING").ReplyBlobString("OK").ReplyBlobString("OK")
11261126
}()
1127-
conns := newPool(1, nil, nil)
1127+
conns := newPool(1, nil, 0, 0, nil)
11281128
s := p.DoMultiStream(context.Background(), conns, cmds.NewCompleted([]string{"PING"}), cmds.NewCompleted([]string{"PING"}))
11291129
buf := &limitedbuffer{buf: make([]byte, 1)}
11301130
if err := s.Error(); err != nil {
@@ -3569,7 +3569,7 @@ func TestAlreadyCanceledContext(t *testing.T) {
35693569
t.Fatalf("unexpected err %v", err)
35703570
}
35713571

3572-
cp := newPool(1, nil, nil)
3572+
cp := newPool(1, nil, 0, 0, nil)
35733573
if s := p.DoStream(ctx, cp, cmds.NewCompleted([]string{"GET", "a"})); !errors.Is(s.Error(), context.Canceled) {
35743574
t.Fatalf("unexpected err %v", s.Error())
35753575
}
@@ -3614,7 +3614,7 @@ func TestCancelContext_DoStream(t *testing.T) {
36143614
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
36153615
defer cancel()
36163616

3617-
cp := newPool(1, nil, nil)
3617+
cp := newPool(1, nil, 0, 0, nil)
36183618
s := p.DoStream(ctx, cp, cmds.NewCompleted([]string{"GET", "a"}))
36193619
if err := s.Error(); err != io.EOF && !strings.Contains(err.Error(), "i/o") {
36203620
t.Fatalf("unexpected err %v", err)
@@ -3631,7 +3631,7 @@ func TestWriteDeadlineIsShorterThanContextDeadline_DoStream(t *testing.T) {
36313631
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
36323632
defer cancel()
36333633

3634-
cp := newPool(1, nil, nil)
3634+
cp := newPool(1, nil, 0, 0, nil)
36353635
startTime := time.Now()
36363636
s := p.DoStream(ctx, cp, cmds.NewCompleted([]string{"GET", "a"}))
36373637
if err := s.Error(); err != io.EOF && !strings.Contains(err.Error(), "i/o") {
@@ -3652,7 +3652,7 @@ func TestWriteDeadlineIsNoShorterThanContextDeadline_DoStreamBlocked(t *testing.
36523652
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
36533653
defer cancel()
36543654

3655-
cp := newPool(1, nil, nil)
3655+
cp := newPool(1, nil, 0, 0, nil)
36563656
startTime := time.Now()
36573657
s := p.DoStream(ctx, cp, cmds.NewBlockingCompleted([]string{"BLPOP", "a"}))
36583658
if err := s.Error(); err != io.EOF && !strings.Contains(err.Error(), "i/o") {
@@ -3727,7 +3727,7 @@ func TestCancelContext_DoMultiStream(t *testing.T) {
37273727
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
37283728
defer cancel()
37293729

3730-
cp := newPool(1, nil, nil)
3730+
cp := newPool(1, nil, 0, 0, nil)
37313731
s := p.DoMultiStream(ctx, cp, cmds.NewCompleted([]string{"GET", "a"}))
37323732
if err := s.Error(); err != io.EOF && !strings.Contains(err.Error(), "i/o") {
37333733
t.Fatalf("unexpected err %v", err)
@@ -3744,7 +3744,7 @@ func TestWriteDeadlineIsShorterThanContextDeadline_DoMultiStream(t *testing.T) {
37443744
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
37453745
defer cancel()
37463746

3747-
cp := newPool(1, nil, nil)
3747+
cp := newPool(1, nil, 0, 0, nil)
37483748
startTime := time.Now()
37493749
s := p.DoMultiStream(ctx, cp, cmds.NewCompleted([]string{"GET", "a"}))
37503750
if err := s.Error(); err != io.EOF && !strings.Contains(err.Error(), "i/o") {
@@ -3765,7 +3765,7 @@ func TestWriteDeadlineIsNoShorterThanContextDeadline_DoMultiStreamBlocked(t *tes
37653765
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
37663766
defer cancel()
37673767

3768-
cp := newPool(1, nil, nil)
3768+
cp := newPool(1, nil, 0, 0, nil)
37693769
startTime := time.Now()
37703770
s := p.DoMultiStream(ctx, cp, cmds.NewBlockingCompleted([]string{"BLPOP", "a"}))
37713771
if err := s.Error(); err != io.EOF && !strings.Contains(err.Error(), "i/o") {
@@ -3797,7 +3797,7 @@ func TestTimeout_DoStream(t *testing.T) {
37973797
defer ShouldNotLeaked(SetupLeakDetection())
37983798
p, _, _, _ := setup(t, ClientOption{ConnWriteTimeout: time.Millisecond * 30})
37993799

3800-
cp := newPool(1, nil, nil)
3800+
cp := newPool(1, nil, 0, 0, nil)
38013801

38023802
s := p.DoStream(context.Background(), cp, cmds.NewCompleted([]string{"GET", "a"}))
38033803
if err := s.Error(); err != io.EOF && !strings.Contains(err.Error(), "i/o") {
@@ -3817,7 +3817,7 @@ func TestForceClose_DoStream_Block(t *testing.T) {
38173817
p.Close()
38183818
}()
38193819

3820-
cp := newPool(1, nil, nil)
3820+
cp := newPool(1, nil, 0, 0, nil)
38213821

38223822
s := p.DoStream(context.Background(), cp, cmds.NewBlockingCompleted([]string{"GET", "a"}))
38233823
if s.Error() != nil {
@@ -3874,7 +3874,7 @@ func TestTimeout_DoMultiStream(t *testing.T) {
38743874
defer ShouldNotLeaked(SetupLeakDetection())
38753875
p, _, _, _ := setup(t, ClientOption{ConnWriteTimeout: time.Millisecond * 30})
38763876

3877-
cp := newPool(1, nil, nil)
3877+
cp := newPool(1, nil, 0, 0, nil)
38783878

38793879
s := p.DoMultiStream(context.Background(), cp, cmds.NewCompleted([]string{"GET", "a"}))
38803880
if err := s.Error(); err != io.EOF && !strings.Contains(err.Error(), "i/o") {
@@ -3894,7 +3894,7 @@ func TestForceClose_DoMultiStream_Block(t *testing.T) {
38943894
p.Close()
38953895
}()
38963896

3897-
cp := newPool(1, nil, nil)
3897+
cp := newPool(1, nil, 0, 0, nil)
38983898

38993899
s := p.DoMultiStream(context.Background(), cp, cmds.NewBlockingCompleted([]string{"GET", "a"}))
39003900
if s.Error() != nil {

pool.go

Lines changed: 65 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,39 @@
11
package rueidis
22

3-
import "sync"
3+
import (
4+
"sync"
5+
"time"
6+
)
47

5-
func newPool(cap int, dead wire, makeFn func() wire) *pool {
8+
func newPool(cap int, dead wire, idleConnTTL time.Duration, minSize int, makeFn func() wire) *pool {
69
if cap <= 0 {
710
cap = DefaultPoolSize
811
}
912

1013
return &pool{
11-
size: 0,
12-
cap: cap,
13-
dead: dead,
14-
make: makeFn,
15-
list: make([]wire, 0, 4),
16-
cond: sync.NewCond(&sync.Mutex{}),
14+
size: 0,
15+
minSize: minSize,
16+
cap: cap,
17+
dead: dead,
18+
make: makeFn,
19+
list: make([]wire, 0, 4),
20+
cond: sync.NewCond(&sync.Mutex{}),
21+
idleConnTTL: idleConnTTL,
1722
}
1823
}
1924

2025
type pool struct {
21-
dead wire
22-
cond *sync.Cond
23-
make func() wire
24-
list []wire
25-
size int
26-
cap int
27-
down bool
26+
dead wire
27+
cond *sync.Cond
28+
make func() wire
29+
list []wire
30+
size int
31+
minSize int
32+
cap int
33+
down bool
34+
idleConnTTL time.Duration
35+
timer *time.Timer
36+
timerIsActive bool
2837
}
2938

3039
func (p *pool) Acquire() (v wire) {
@@ -50,6 +59,7 @@ func (p *pool) Store(v wire) {
5059
p.cond.L.Lock()
5160
if !p.down && v.Error() == nil {
5261
p.list = append(p.list, v)
62+
p.startTimerIfNeeded()
5363
} else {
5464
p.size--
5565
v.Close()
@@ -61,9 +71,49 @@ func (p *pool) Store(v wire) {
6171
func (p *pool) Close() {
6272
p.cond.L.Lock()
6373
p.down = true
74+
p.stopTimer()
6475
for _, w := range p.list {
6576
w.Close()
6677
}
6778
p.cond.L.Unlock()
6879
p.cond.Broadcast()
6980
}
81+
82+
func (p *pool) startTimerIfNeeded() {
83+
if p.idleConnTTL == 0 || p.timerIsActive || len(p.list) <= p.minSize {
84+
return
85+
}
86+
87+
p.timerIsActive = true
88+
if p.timer == nil {
89+
p.timer = time.AfterFunc(p.idleConnTTL, p.removeIdleConns)
90+
} else {
91+
p.timer.Reset(p.idleConnTTL)
92+
}
93+
}
94+
95+
func (p *pool) removeIdleConns() {
96+
p.cond.L.Lock()
97+
defer p.cond.L.Unlock()
98+
99+
if p.down || len(p.list) <= p.minSize {
100+
return
101+
}
102+
103+
newLen := min(p.minSize, len(p.list))
104+
for i, w := range p.list[newLen:] {
105+
w.Close()
106+
p.list[newLen+i] = nil
107+
p.size--
108+
}
109+
110+
p.list = p.list[:newLen]
111+
p.timerIsActive = false
112+
}
113+
114+
func (p *pool) stopTimer() {
115+
p.timerIsActive = false
116+
if p.timer != nil {
117+
p.timer.Stop()
118+
}
119+
}

pool_test.go

Lines changed: 93 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"runtime"
66
"sync/atomic"
77
"testing"
8+
"time"
89
)
910

1011
var dead = deadFn()
@@ -14,7 +15,7 @@ func TestPool(t *testing.T) {
1415
defer ShouldNotLeaked(SetupLeakDetection())
1516
setup := func(size int) (*pool, *int32) {
1617
var count int32
17-
return newPool(size, dead, func() wire {
18+
return newPool(size, dead, 0, 0, func() wire {
1819
atomic.AddInt32(&count, 1)
1920
closed := false
2021
return &mockWire{
@@ -32,7 +33,7 @@ func TestPool(t *testing.T) {
3233
}
3334

3435
t.Run("DefaultPoolSize", func(t *testing.T) {
35-
p := newPool(0, dead, func() wire { return nil })
36+
p := newPool(0, dead, 0, 0, func() wire { return nil })
3637
if cap(p.list) == 0 {
3738
t.Fatalf("DefaultPoolSize is not applied")
3839
}
@@ -180,7 +181,7 @@ func TestPoolError(t *testing.T) {
180181
defer ShouldNotLeaked(SetupLeakDetection())
181182
setup := func(size int) (*pool, *int32) {
182183
var count int32
183-
return newPool(size, dead, func() wire {
184+
return newPool(size, dead, 0, 0, func() wire {
184185
w := &pipe{}
185186
w.pshks.Store(emptypshks)
186187
c := atomic.AddInt32(&count, 1)
@@ -211,3 +212,92 @@ func TestPoolError(t *testing.T) {
211212
}
212213
})
213214
}
215+
216+
func TestPoolWithIdleTTL(t *testing.T) {
217+
defer ShouldNotLeaked(SetupLeakDetection())
218+
setup := func(size int, ttl time.Duration, minSize int) *pool {
219+
return newPool(size, dead, ttl, minSize, func() wire {
220+
closed := false
221+
return &mockWire{
222+
CloseFn: func() {
223+
closed = true
224+
},
225+
ErrorFn: func() error {
226+
if closed {
227+
return ErrClosing
228+
}
229+
return nil
230+
},
231+
}
232+
})
233+
}
234+
235+
t.Run("Removing idle conns. Min size is not 0", func(t *testing.T) {
236+
minSize := 3
237+
p := setup(0, time.Millisecond*50, minSize)
238+
conns := make([]wire, 10)
239+
240+
for i := 0; i < 2; i++ {
241+
for i := range conns {
242+
w := p.Acquire()
243+
conns[i] = w
244+
}
245+
246+
for _, w := range conns {
247+
p.Store(w)
248+
}
249+
250+
time.Sleep(time.Millisecond * 60)
251+
p.cond.Broadcast()
252+
time.Sleep(time.Millisecond * 40)
253+
254+
p.cond.L.Lock()
255+
if p.size != minSize {
256+
defer p.cond.L.Unlock()
257+
t.Fatalf("size must be equal to %d, actual: %d", minSize, p.size)
258+
}
259+
260+
if len(p.list) != minSize {
261+
defer p.cond.L.Unlock()
262+
t.Fatalf("pool len must equal to %d, actual: %d", minSize, len(p.list))
263+
}
264+
p.cond.L.Unlock()
265+
}
266+
267+
p.Close()
268+
})
269+
270+
t.Run("Removing idle conns. Min size is 0", func(t *testing.T) {
271+
p := setup(0, time.Millisecond*50, 0)
272+
conns := make([]wire, 10)
273+
274+
for i := 0; i < 2; i++ {
275+
for i := range conns {
276+
w := p.Acquire()
277+
conns[i] = w
278+
}
279+
280+
for _, w := range conns {
281+
p.Store(w)
282+
}
283+
284+
time.Sleep(time.Millisecond * 60)
285+
p.cond.Broadcast()
286+
time.Sleep(time.Millisecond * 40)
287+
288+
p.cond.L.Lock()
289+
if p.size != 0 {
290+
defer p.cond.L.Unlock()
291+
t.Fatalf("size must be equal to 0, actual: %d", p.size)
292+
}
293+
294+
if len(p.list) != 0 {
295+
defer p.cond.L.Unlock()
296+
t.Fatalf("pool len must equal to 0, actual: %d", len(p.list))
297+
}
298+
p.cond.L.Unlock()
299+
}
300+
301+
p.Close()
302+
})
303+
}

0 commit comments

Comments
 (0)