Skip to content

Commit ec00eaf

Browse files
committed
🧹 chore(proxy): address round-robin review feedback
1 parent a5115b6 commit ec00eaf

6 files changed

Lines changed: 112 additions & 122 deletions

File tree

app.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1205,11 +1205,12 @@ func (e *Error) Error() string {
12051205

12061206
// NewError creates a new Error instance with an optional message
12071207
func NewError(code int, message ...string) *Error {
1208-
err := &Error{Code: code}
1208+
err := &Error{
1209+
Code: code,
1210+
Message: utils.StatusMessage(code),
1211+
}
12091212
if len(message) > 0 {
12101213
err.Message = message[0]
1211-
} else {
1212-
err.Message = utils.StatusMessage(code)
12131214
}
12141215
return err
12151216
}

app_test.go

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1982,22 +1982,9 @@ func Benchmark_NewError_Parallel(b *testing.B) {
19821982
// go test -run Test_NewError
19831983
func Test_NewError(t *testing.T) {
19841984
t.Parallel()
1985-
1986-
t.Run("custom message", func(t *testing.T) {
1987-
t.Parallel()
1988-
1989-
e := NewError(StatusForbidden, "permission denied")
1990-
require.Equal(t, StatusForbidden, e.Code)
1991-
require.Equal(t, "permission denied", e.Message)
1992-
})
1993-
1994-
t.Run("status message", func(t *testing.T) {
1995-
t.Parallel()
1996-
1997-
e := NewError(StatusForbidden)
1998-
require.Equal(t, StatusForbidden, e.Code)
1999-
require.Equal(t, "Forbidden", e.Message)
2000-
})
1985+
e := NewError(StatusForbidden, "permission denied")
1986+
require.Equal(t, StatusForbidden, e.Code)
1987+
require.Equal(t, "permission denied", e.Message)
20011988
}
20021989

20031990
// go test -run Test_NewError_Format

middleware/proxy/bench_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"net/http"
77
"net/http/httptest"
88
"net/url"
9+
"sync"
910
"testing"
1011

1112
"github.com/gofiber/fiber/v3"
@@ -273,6 +274,51 @@ func BenchmarkDomainForward_HostMatchPath(b *testing.B) {
273274
}
274275
}
275276

277+
func BenchmarkURLRoundrobinGet(b *testing.B) {
278+
pool := []*url.URL{
279+
{Scheme: schemeHTTP, Host: "a.example"},
280+
{Scheme: schemeHTTP, Host: "b.example"},
281+
{Scheme: schemeHTTP, Host: "c.example"},
282+
}
283+
b.Run("atomic", func(b *testing.B) {
284+
r := &urlRoundrobin{pool: pool}
285+
b.ReportAllocs()
286+
b.RunParallel(func(pb *testing.PB) {
287+
for pb.Next() {
288+
_ = r.get()
289+
}
290+
})
291+
})
292+
b.Run("mutex", func(b *testing.B) {
293+
r := &mutexURLRoundrobin{pool: pool}
294+
b.ReportAllocs()
295+
b.RunParallel(func(pb *testing.PB) {
296+
for pb.Next() {
297+
_ = r.get()
298+
}
299+
})
300+
})
301+
}
302+
303+
type mutexURLRoundrobin struct {
304+
pool []*url.URL
305+
306+
current int
307+
sync.Mutex
308+
}
309+
310+
func (r *mutexURLRoundrobin) get() *url.URL {
311+
r.Lock()
312+
defer r.Unlock()
313+
314+
if r.current >= len(r.pool) {
315+
r.current %= len(r.pool)
316+
}
317+
result := r.pool[r.current]
318+
r.current++
319+
return result
320+
}
321+
276322
// noopRoundTripper is the minimum surface for fasthttp.RoundTripper used
277323
// by BenchmarkDomainForward_HostMatchPath. It returns 204 No Content
278324
// without touching the network.

middleware/proxy/roundrobin_bench_test.go

Lines changed: 0 additions & 52 deletions
This file was deleted.

middleware/proxy/roundrobin_test.go

Lines changed: 0 additions & 51 deletions
This file was deleted.

middleware/proxy/security_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99
"net/http/httptest"
1010
"net/url"
1111
"strings"
12+
"sync"
13+
"sync/atomic"
1214
"testing"
1315
"time"
1416

@@ -854,6 +856,63 @@ func Test_Security_BalancerForward_InvalidUpstreamPanicsAtConstruction(t *testin
854856
})
855857
}
856858

859+
// Test_Security_RoundRobin_WrapsAround covers both the normal cyclic
860+
// selection and recovery from a counter value near the uint64 limit.
861+
func Test_Security_RoundRobin_WrapsAround(t *testing.T) {
862+
t.Parallel()
863+
a := &url.URL{Scheme: schemeHTTP, Host: "a"}
864+
b := &url.URL{Scheme: schemeHTTP, Host: "b"}
865+
c := &url.URL{Scheme: schemeHTTP, Host: "c"}
866+
867+
t.Run("current index", func(t *testing.T) {
868+
t.Parallel()
869+
870+
r := &urlRoundrobin{pool: []*url.URL{a, b}}
871+
r.next.Store(5)
872+
require.Same(t, b, r.get(), "5 %% 2 should select pool[1]")
873+
require.Same(t, a, r.get())
874+
})
875+
876+
t.Run("counter limit", func(t *testing.T) {
877+
t.Parallel()
878+
879+
r := &urlRoundrobin{pool: []*url.URL{a, b, c}}
880+
r.next.Store(^uint64(0) - 1)
881+
require.Same(t, c, r.get())
882+
require.Same(t, a, r.get())
883+
require.Same(t, b, r.get())
884+
})
885+
}
886+
887+
func Test_Security_RoundRobin_ConcurrentSelection(t *testing.T) {
888+
t.Parallel()
889+
pool := []*url.URL{
890+
{Scheme: schemeHTTP, Host: "a"},
891+
{Scheme: schemeHTTP, Host: "b"},
892+
{Scheme: schemeHTTP, Host: "c"},
893+
}
894+
r := &urlRoundrobin{pool: pool}
895+
896+
var counts [3]atomic.Int32
897+
var wg sync.WaitGroup
898+
for range 300 {
899+
wg.Go(func() {
900+
selected := r.get()
901+
for i, candidate := range pool {
902+
if selected == candidate {
903+
counts[i].Add(1)
904+
return
905+
}
906+
}
907+
})
908+
}
909+
wg.Wait()
910+
911+
for i := range counts {
912+
require.Equal(t, int32(100), counts[i].Load())
913+
}
914+
}
915+
857916
// Test_Security_Balancer_PanicsOnInvalidUpstream covers the panic
858917
// branch when a server fails validation at Config-time.
859918
func Test_Security_Balancer_PanicsOnInvalidUpstream(t *testing.T) {

0 commit comments

Comments
 (0)