Skip to content

Commit 5b36ce3

Browse files
bbasicBecir Basic
and
Becir Basic
authored
Fixes the concurrency issue of calling the Next() proxy target on RRB (#2409)
* Fixes the concurrency issue of calling the `Next()` proxy target on round robin balancer - fixed concurrency issue in `AddTarget()` - moved `rand.New()` to the random balancer initializer func. - internal code reorganized eliminating unnecessary pointer redirection - employing `sync.Mutex` instead of `RWMutex` which brings additional overhead of tracking readers and writers. No need for that since the guarded code has no long-running operations, hence no realistic congestion. - added additional guards without which the code would otherwise panic (e.g., the case where a random value is calculation when targets list is empty) - added descriptions for func return values, what to expect in which case. - Improve code test coverage --------- Co-authored-by: Becir Basic <[email protected]>
1 parent 1e575b7 commit 5b36ce3

File tree

2 files changed

+52
-22
lines changed

2 files changed

+52
-22
lines changed

middleware/proxy.go

+40-19
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import (
1212
"regexp"
1313
"strings"
1414
"sync"
15-
"sync/atomic"
1615
"time"
1716

1817
"github.com/labstack/echo/v4"
@@ -79,19 +78,20 @@ type (
7978

8079
commonBalancer struct {
8180
targets []*ProxyTarget
82-
mutex sync.RWMutex
81+
mutex sync.Mutex
8382
}
8483

8584
// RandomBalancer implements a random load balancing technique.
8685
randomBalancer struct {
87-
*commonBalancer
86+
commonBalancer
8887
random *rand.Rand
8988
}
9089

9190
// RoundRobinBalancer implements a round-robin load balancing technique.
9291
roundRobinBalancer struct {
93-
*commonBalancer
94-
i uint32
92+
commonBalancer
93+
// tracking the index on `targets` slice for the next `*ProxyTarget` to be used
94+
i int
9595
}
9696
)
9797

@@ -143,32 +143,37 @@ func proxyRaw(t *ProxyTarget, c echo.Context) http.Handler {
143143

144144
// NewRandomBalancer returns a random proxy balancer.
145145
func NewRandomBalancer(targets []*ProxyTarget) ProxyBalancer {
146-
b := &randomBalancer{commonBalancer: new(commonBalancer)}
146+
b := randomBalancer{}
147147
b.targets = targets
148-
return b
148+
b.random = rand.New(rand.NewSource(int64(time.Now().Nanosecond())))
149+
return &b
149150
}
150151

151152
// NewRoundRobinBalancer returns a round-robin proxy balancer.
152153
func NewRoundRobinBalancer(targets []*ProxyTarget) ProxyBalancer {
153-
b := &roundRobinBalancer{commonBalancer: new(commonBalancer)}
154+
b := roundRobinBalancer{}
154155
b.targets = targets
155-
return b
156+
return &b
156157
}
157158

158-
// AddTarget adds an upstream target to the list.
159+
// AddTarget adds an upstream target to the list and returns `true`.
160+
//
161+
// However, if a target with the same name already exists then the operation is aborted returning `false`.
159162
func (b *commonBalancer) AddTarget(target *ProxyTarget) bool {
163+
b.mutex.Lock()
164+
defer b.mutex.Unlock()
160165
for _, t := range b.targets {
161166
if t.Name == target.Name {
162167
return false
163168
}
164169
}
165-
b.mutex.Lock()
166-
defer b.mutex.Unlock()
167170
b.targets = append(b.targets, target)
168171
return true
169172
}
170173

171-
// RemoveTarget removes an upstream target from the list.
174+
// RemoveTarget removes an upstream target from the list by name.
175+
//
176+
// Returns `true` on success, `false` if no target with the name is found.
172177
func (b *commonBalancer) RemoveTarget(name string) bool {
173178
b.mutex.Lock()
174179
defer b.mutex.Unlock()
@@ -182,20 +187,36 @@ func (b *commonBalancer) RemoveTarget(name string) bool {
182187
}
183188

184189
// Next randomly returns an upstream target.
190+
//
191+
// Note: `nil` is returned in case upstream target list is empty.
185192
func (b *randomBalancer) Next(c echo.Context) *ProxyTarget {
186-
if b.random == nil {
187-
b.random = rand.New(rand.NewSource(int64(time.Now().Nanosecond())))
193+
b.mutex.Lock()
194+
defer b.mutex.Unlock()
195+
if len(b.targets) == 0 {
196+
return nil
197+
} else if len(b.targets) == 1 {
198+
return b.targets[0]
188199
}
189-
b.mutex.RLock()
190-
defer b.mutex.RUnlock()
191200
return b.targets[b.random.Intn(len(b.targets))]
192201
}
193202

194203
// Next returns an upstream target using round-robin technique.
204+
//
205+
// Note: `nil` is returned in case upstream target list is empty.
195206
func (b *roundRobinBalancer) Next(c echo.Context) *ProxyTarget {
196-
b.i = b.i % uint32(len(b.targets))
207+
b.mutex.Lock()
208+
defer b.mutex.Unlock()
209+
if len(b.targets) == 0 {
210+
return nil
211+
} else if len(b.targets) == 1 {
212+
return b.targets[0]
213+
}
214+
// reset the index if out of bounds
215+
if b.i >= len(b.targets) {
216+
b.i = 0
217+
}
197218
t := b.targets[b.i]
198-
atomic.AddUint32(&b.i, 1)
219+
b.i++
199220
return t
200221
}
201222

middleware/proxy_test.go

+12-3
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ func TestProxy(t *testing.T) {
122122
}
123123

124124
type testProvider struct {
125-
*commonBalancer
125+
commonBalancer
126126
target *ProxyTarget
127127
err error
128128
}
@@ -143,7 +143,7 @@ func TestTargetProvider(t *testing.T) {
143143
url1, _ := url.Parse(t1.URL)
144144

145145
e := echo.New()
146-
tp := &testProvider{commonBalancer: new(commonBalancer)}
146+
tp := &testProvider{}
147147
tp.target = &ProxyTarget{Name: "target 1", URL: url1}
148148
e.Use(Proxy(tp))
149149
rec := httptest.NewRecorder()
@@ -158,7 +158,7 @@ func TestFailNextTarget(t *testing.T) {
158158
assert.Nil(t, err)
159159

160160
e := echo.New()
161-
tp := &testProvider{commonBalancer: new(commonBalancer)}
161+
tp := &testProvider{}
162162
tp.target = &ProxyTarget{Name: "target 1", URL: url1}
163163
tp.err = echo.NewHTTPError(http.StatusInternalServerError, "method could not select target")
164164

@@ -422,3 +422,12 @@ func TestClientCancelConnectionResultsHTTPCode499(t *testing.T) {
422422
timeoutStop.Done()
423423
assert.Equal(t, 499, rec.Code)
424424
}
425+
426+
// Assert balancer with empty targets does return `nil` on `Next()`
427+
func TestProxyBalancerWithNoTargets(t *testing.T) {
428+
rb := NewRandomBalancer(nil)
429+
assert.Nil(t, rb.Next(nil))
430+
431+
rrb := NewRoundRobinBalancer([]*ProxyTarget{})
432+
assert.Nil(t, rrb.Next(nil))
433+
}

0 commit comments

Comments
 (0)