Skip to content

Commit 5fb0e2b

Browse files
committed
bugfix: prevent segfaults in connection_pool
After the patch ConnectionPool.getNextConnection() does not return (nil, nil). It always return a connection or an error. The check Connection.ConnectedNow() does not have sence because the connection may be closed right after the call. The code just complicates the logic and does not protect against anything. A chain of two atomic operations IsEmpty() + GetNextConnection() wrong because it leads too a race condition. Part of #208
1 parent 2779422 commit 5fb0e2b

File tree

5 files changed

+94
-59
lines changed

5 files changed

+94
-59
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Versioning](http://semver.org/spec/v2.0.0.html) except to the first release.
1717
- Mode type description in the connection_pool subpackage (#208)
1818
- Missed Role type constants in the connection_pool subpackage (#208)
1919
- ConnectionPool does not close UnknownRole connections (#208)
20+
- Segmentation faults in ConnectionPool requests after disconnect (#208)
2021

2122
## [1.8.0] - 2022-08-17
2223

connection_pool/connection_pool.go

Lines changed: 29 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -130,13 +130,20 @@ func (connPool *ConnectionPool) ConnectedNow(mode Mode) (bool, error) {
130130
if connPool.getState() != connConnected {
131131
return false, nil
132132
}
133-
134-
conn, err := connPool.getNextConnection(mode)
135-
if err != nil || conn == nil {
136-
return false, err
133+
switch mode {
134+
case ANY:
135+
return !connPool.anyPool.IsEmpty(), nil
136+
case RW:
137+
return !connPool.rwPool.IsEmpty(), nil
138+
case RO:
139+
return !connPool.roPool.IsEmpty(), nil
140+
case PreferRW:
141+
fallthrough
142+
case PreferRO:
143+
return !connPool.rwPool.IsEmpty() || !connPool.roPool.IsEmpty(), nil
144+
default:
145+
return false, ErrNoHealthyInstance
137146
}
138-
139-
return conn.ConnectedNow(), nil
140147
}
141148

142149
// ConfiguredTimeout gets timeout of current connection.
@@ -751,49 +758,34 @@ func (connPool *ConnectionPool) getNextConnection(mode Mode) (*tarantool.Connect
751758

752759
switch mode {
753760
case ANY:
754-
if connPool.anyPool.IsEmpty() {
755-
return nil, ErrNoHealthyInstance
761+
if next := connPool.anyPool.GetNextConnection(); next != nil {
762+
return next, nil
756763
}
757-
758-
return connPool.anyPool.GetNextConnection(), nil
759-
760764
case RW:
761-
if connPool.rwPool.IsEmpty() {
762-
return nil, ErrNoRwInstance
765+
if next := connPool.rwPool.GetNextConnection(); next != nil {
766+
return next, nil
763767
}
764-
765-
return connPool.rwPool.GetNextConnection(), nil
766-
768+
return nil, ErrNoRwInstance
767769
case RO:
768-
if connPool.roPool.IsEmpty() {
769-
return nil, ErrNoRoInstance
770+
if next := connPool.roPool.GetNextConnection(); next != nil {
771+
return next, nil
770772
}
771-
772-
return connPool.roPool.GetNextConnection(), nil
773-
773+
return nil, ErrNoRoInstance
774774
case PreferRW:
775-
if !connPool.rwPool.IsEmpty() {
776-
return connPool.rwPool.GetNextConnection(), nil
775+
if next := connPool.rwPool.GetNextConnection(); next != nil {
776+
return next, nil
777777
}
778-
779-
if !connPool.roPool.IsEmpty() {
780-
return connPool.roPool.GetNextConnection(), nil
778+
if next := connPool.roPool.GetNextConnection(); next != nil {
779+
return next, nil
781780
}
782-
783-
return nil, ErrNoHealthyInstance
784-
785781
case PreferRO:
786-
if !connPool.roPool.IsEmpty() {
787-
return connPool.roPool.GetNextConnection(), nil
782+
if next := connPool.roPool.GetNextConnection(); next != nil {
783+
return next, nil
788784
}
789-
790-
if !connPool.rwPool.IsEmpty() {
791-
return connPool.rwPool.GetNextConnection(), nil
785+
if next := connPool.rwPool.GetNextConnection(); next != nil {
786+
return next, nil
792787
}
793-
794-
return nil, ErrNoHealthyInstance
795788
}
796-
797789
return nil, ErrNoHealthyInstance
798790
}
799791

connection_pool/connection_pool_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,42 @@ func TestClose(t *testing.T) {
208208
require.Nil(t, err)
209209
}
210210

211+
func TestRequestOnClosed(t *testing.T) {
212+
server1 := servers[0]
213+
server2 := servers[1]
214+
215+
connPool, err := connection_pool.Connect([]string{server1, server2}, connOpts)
216+
require.Nilf(t, err, "failed to connect")
217+
require.NotNilf(t, connPool, "conn is nil after Connect")
218+
219+
defer connPool.Close()
220+
221+
test_helpers.StopTarantoolWithCleanup(instances[0])
222+
test_helpers.StopTarantoolWithCleanup(instances[1])
223+
224+
args := test_helpers.CheckStatusesArgs{
225+
ConnPool: connPool,
226+
Mode: connection_pool.ANY,
227+
Servers: []string{server1, server2},
228+
ExpectedPoolStatus: false,
229+
ExpectedStatuses: map[string]bool{
230+
server1: false,
231+
server2: false,
232+
},
233+
}
234+
err = test_helpers.Retry(test_helpers.CheckPoolStatuses, args, defaultCountRetry, defaultTimeoutRetry)
235+
require.Nil(t, err)
236+
237+
_, err = connPool.Ping(connection_pool.ANY)
238+
require.NotNilf(t, err, "err is nil after Ping")
239+
240+
err = test_helpers.RestartTarantool(&instances[0])
241+
require.Nilf(t, err, "failed to restart tarantool")
242+
243+
err = test_helpers.RestartTarantool(&instances[1])
244+
require.Nilf(t, err, "failed to restart tarantool")
245+
}
246+
211247
func TestCall17(t *testing.T) {
212248
roles := []bool{false, true, false, false, true}
213249

connection_pool/round_robin.go

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,16 @@ package connection_pool
22

33
import (
44
"sync"
5-
"sync/atomic"
65

76
"github.com/tarantool/go-tarantool"
87
)
98

109
type RoundRobinStrategy struct {
1110
conns []*tarantool.Connection
12-
indexByAddr map[string]int
11+
indexByAddr map[string]uint
1312
mutex sync.RWMutex
14-
size int
15-
current uint64
13+
size uint
14+
current uint
1615
}
1716

1817
func (r *RoundRobinStrategy) GetConnByAddr(addr string) *tarantool.Connection {
@@ -66,29 +65,18 @@ func (r *RoundRobinStrategy) GetNextConnection() *tarantool.Connection {
6665
r.mutex.RLock()
6766
defer r.mutex.RUnlock()
6867

69-
// We want to iterate through the elements in a circular order
70-
// so the first element in cycle is connections[next]
71-
// and the last one is connections[next + length].
72-
next := r.nextIndex()
73-
cycleLen := len(r.conns) + next
74-
for i := next; i < cycleLen; i++ {
75-
idx := i % len(r.conns)
76-
if r.conns[idx].ConnectedNow() {
77-
if i != next {
78-
atomic.StoreUint64(&r.current, uint64(idx))
79-
}
80-
return r.conns[idx]
81-
}
68+
if r.size == 0 {
69+
return nil
8270
}
83-
84-
return nil
71+
return r.conns[r.nextIndex()]
8572
}
8673

8774
func NewEmptyRoundRobin(size int) *RoundRobinStrategy {
8875
return &RoundRobinStrategy{
8976
conns: make([]*tarantool.Connection, 0, size),
90-
indexByAddr: make(map[string]int),
77+
indexByAddr: make(map[string]uint),
9178
size: 0,
79+
current: 0,
9280
}
9381
}
9482

@@ -105,6 +93,8 @@ func (r *RoundRobinStrategy) AddConn(addr string, conn *tarantool.Connection) {
10593
}
10694
}
10795

108-
func (r *RoundRobinStrategy) nextIndex() int {
109-
return int(atomic.AddUint64(&r.current, uint64(1)) % uint64(len(r.conns)))
96+
func (r *RoundRobinStrategy) nextIndex() uint {
97+
ret := r.current % r.size
98+
r.current++
99+
return ret
110100
}

connection_pool/round_robin_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,20 @@ func TestRoundRobinAddDuplicateDelete(t *testing.T) {
5252
}
5353
}
5454

55+
func TestRoundRobinGetNextConnection(t *testing.T) {
56+
rr := NewEmptyRoundRobin(10)
57+
58+
addrs := []string{validAddr1, validAddr2}
59+
conns := []*tarantool.Connection{&tarantool.Connection{}, &tarantool.Connection{}}
5560

61+
for i, addr := range addrs {
62+
rr.AddConn(addr, conns[i])
63+
}
64+
65+
expectedConns := []*tarantool.Connection{conns[0], conns[1], conns[0], conns[1]}
66+
for i, expected := range expectedConns {
67+
if rr.GetNextConnection() != expected {
68+
t.Errorf("Unexpected connection on %d call", i)
69+
}
70+
}
71+
}

0 commit comments

Comments
 (0)