Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,5 @@ const (
// Load balancing strategies.
const (
RoundRobinStrategy = "ROUND_ROBIN"
RANDOMStrategy = "RANDOM"
)
2 changes: 1 addition & 1 deletion gatewayd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ servers:
address: 0.0.0.0:15432
loadBalancer:
# Load balancer strategies can be found in config/constants.go
strategy: ROUND_ROBIN
strategy: RANDOM # ROUND_ROBIN, RANDOM
enableTicker: False
tickInterval: 5s # duration
enableTLS: False
Expand Down
2 changes: 2 additions & 0 deletions network/loadbalancer.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ func NewLoadBalancerStrategy(server *Server) (LoadBalancerStrategy, *gerr.Gatewa
switch server.LoadbalancerStrategyName {
case config.RoundRobinStrategy:
return NewRoundRobin(server), nil
case config.RANDOMStrategy:
return NewRandom(server), nil
default:
return nil, gerr.ErrLoadBalancerStrategyNotFound
}
Expand Down
52 changes: 52 additions & 0 deletions network/random.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package network

import (
"crypto/rand"
"errors"
"fmt"
"math/big"
"sync"

gerr "github.com/gatewayd-io/gatewayd/errors"
)

// Random is a struct that holds a list of proxies and a mutex for thread safety.
type Random struct {
mu sync.Mutex
proxies []IProxy
}

// NewRandom creates a new Random instance with the given server's proxies.
func NewRandom(server *Server) *Random {
return &Random{
proxies: server.Proxies,
}
}

// NextProxy returns a random proxy from the list.
func (r *Random) NextProxy() (IProxy, *gerr.GatewayDError) {
r.mu.Lock()
defer r.mu.Unlock()

proxiesLen := len(r.proxies)
if proxiesLen == 0 {
return nil, gerr.ErrNoProxiesAvailable.Wrap(errors.New("proxy list is empty"))
}

randomIndex, err := randInt(proxiesLen)
if err != nil {
return nil, gerr.ErrNoProxiesAvailable.Wrap(err)
}

return r.proxies[randomIndex], nil
}

// randInt generates a random integer between 0 and max-1 using crypto/rand.
func randInt(max int) (int, error) {
// Generate a secure random number
n, err := rand.Int(rand.Reader, big.NewInt(int64(max)))
if err != nil {
return 0, fmt.Errorf("failed to generate random integer: %w", err)
}
return int(n.Int64()), nil
}
95 changes: 95 additions & 0 deletions network/random_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package network

import (
"sync"
"testing"

gerr "github.com/gatewayd-io/gatewayd/errors"
"github.com/stretchr/testify/assert"
)

// TestNewRandom verifies that the NewRandom function properly initializes
// a Random object with the provided server and its associated proxies.
// The test ensures that the Random object is not nil and that the number of
// proxies in the Random object matches the number of proxies in the Server.
func TestNewRandom(t *testing.T) {
proxies := []IProxy{&MockProxy{}, &MockProxy{}}
server := &Server{Proxies: proxies}
random := NewRandom(server)

assert.NotNil(t, random)
assert.Equal(t, len(proxies), len(random.proxies))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assert.Equal(t, len(proxies), len(random.proxies))
assert.Len(t, random.proxies, len(proxies))

}

// TestGetNextProxy checks the behavior of the NextProxy method in various
// scenarios, including when proxies are available, when no proxies are available,
// and the randomness of proxy selection.
//
// - The first sub-test confirms that NextProxy returns a valid proxy when available.
// - The second sub-test ensures that an error is returned when there are no proxies available.
// - The third sub-test checks if the proxy selection is random by comparing two subsequent calls.
func TestGetNextProxy(t *testing.T) {
t.Run("Returns a proxy when proxies are available", func(t *testing.T) {
proxies := []IProxy{&MockProxy{}, &MockProxy{}}
server := &Server{Proxies: proxies}
random := NewRandom(server)

proxy, err := random.NextProxy()

assert.Nil(t, err)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assert.Nil(t, err)
assert.NoError(t, err)

assert.Contains(t, proxies, proxy)
})

t.Run("Returns error when no proxies are available", func(t *testing.T) {
server := &Server{Proxies: []IProxy{}}
random := NewRandom(server)

proxy, err := random.NextProxy()

assert.Nil(t, proxy)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assert.Nil(t, proxy)
assert.Nil(t, proxy)
require.NotNil(t, err)

Otherwise next line will panic when there is no error

I'm unsure if the gerr implents Error()

If so, I would use

Suggested change
assert.Nil(t, proxy)
assert.Nil(t, proxy)
require.Error(t, err)

assert.Equal(t, gerr.ErrNoProxiesAvailable.Message, err.Message)
})
t.Run("Random selection of proxies", func(t *testing.T) {
proxies := []IProxy{&MockProxy{}, &MockProxy{}}
server := &Server{Proxies: proxies}
random := NewRandom(server)

proxy1, _ := random.NextProxy()
proxy2, _ := random.NextProxy()
Comment on lines +57 to +58

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
proxy1, _ := random.NextProxy()
proxy2, _ := random.NextProxy()
proxy1, err := random.NextProxy()
require.NoError(t, err)
proxy2, err := random.NextProxy()
require.NoError(t, err)


assert.Contains(t, proxies, proxy1)
assert.Contains(t, proxies, proxy2)
// It's possible that proxy1 and proxy2 are the same, but if we run this
// test enough times, they should occasionally be different.
})
}

// TestConcurrencySafety ensures that the Random object is safe for concurrent
// use by multiple goroutines. The test launches multiple goroutines that
// concurrently call the NextProxy method. It then verifies that all returned
// proxies are part of the expected set of proxies, ensuring thread safety.
func TestConcurrencySafety(t *testing.T) {
proxies := []IProxy{&MockProxy{}, &MockProxy{}}
server := &Server{Proxies: proxies}
random := NewRandom(server)

var waitGroup sync.WaitGroup
numGoroutines := 100
proxyChan := make(chan IProxy, numGoroutines)

for range numGoroutines {
waitGroup.Add(1)
go func() {
defer waitGroup.Done()
proxy, _ := random.NextProxy()
proxyChan <- proxy
}()
}

waitGroup.Wait()
close(proxyChan)

for proxy := range proxyChan {
assert.Contains(t, proxies, proxy)
}
}