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
6 changes: 2 additions & 4 deletions health/polling.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ package health
import (
"context"
"io"
"math/rand"
"math/rand/v2"
"net/http"
"net/url"
"time"
Expand Down Expand Up @@ -48,7 +48,6 @@ func NewPollingChecker(config PollingCheckerConfig, prober Prober) Checker {
healthyThreshold: config.HealthyThreshold,
unhealthyThreshold: config.UnhealthyThreshold,
prober: prober,
rnd: internal.NewLockedRand(),
clock: internal.NewRealClock(),
}
}
Expand Down Expand Up @@ -128,7 +127,6 @@ type pollingChecker struct {
healthyThreshold int

prober Prober
rnd *rand.Rand
clock internal.Clock
}

Expand Down Expand Up @@ -206,7 +204,7 @@ func (r *pollingChecker) calcJitter(interval time.Duration) time.Duration {
}

// This may lose precision if your interval is longer than ~104 days.
return time.Duration(float64(interval) + ((r.rnd.Float64()*2 - 1) * r.scaledJitter))
return time.Duration(float64(interval) + ((rand.Float64()*2 - 1) * r.scaledJitter)) //nolint:gosec // does not need to be cryptographically secure
}

type pollingCheckerTask struct {
Expand Down
75 changes: 13 additions & 62 deletions internal/rand.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,70 +15,21 @@
package internal

import (
"hash/maphash"
"math/rand"
"sync"
randv2 "math/rand/v2"
)

// This is based off discussion in a Reddit thread about creating new instances of
// rand.Rand that are properly seeded, to avoid the global rand's synchronization
// overhead. In particular, the use maphash.Hash to create a high-quality seed for
// creating new instances of *rand.Rand.
// https://www.reddit.com/r/golang/comments/m9b0yp/comment/grotn1f/

// NewRand returns a properly seeded *rand.Rand. The seed is computed using
// the "hash/maphash" package, which can be used concurrently and is
// lock-free. Effectively, we're using runtime.fastrand to seed a new
// rand.Rand.
func NewRand() *rand.Rand {
seed := (&maphash.Hash{}).Sum64()
return rand.New(rand.NewSource(int64(seed))) //nolint:gosec // don't need cryptographic RNG
}

// NewLockedRand is just like NewRand except the returned value uses a
// mutex to enable safe usage from concurrent goroutines.
// NewRand returns a properly seeded *rand.Rand.
//
// Despite having mutex overhead, this is better than using the global rand
// because you don't have to worry about other code linked into the same
// program that might be abusing the global rand:
// 1. It is possible for code to call rand.Seed, which could cause issues
// with the quality of the pseudo-random number sequence.
// 2. It is possible for code to make *heavy* use of the global rand, which
// can mean extensive lock contention on its mutex, which will slow down
// clients trying to generate a random number.
//
// By creating a new locked *rand.Rand, nothing else will be using it and
// contending over the mutex except other code that has access to the same
// instance.
func NewLockedRand() *rand.Rand {
seed := (&maphash.Hash{}).Sum64()
//nolint:forcetypeassert,errcheck // specs say value returned by NewSource implements Source64
src := rand.NewSource(int64(seed)).(rand.Source64)
return rand.New(&lockedSource{src: src}) //nolint:gosec // don't need cryptographic RNG
}

type lockedSource struct {
mu sync.Mutex
// +checklocks:mu
src rand.Source64
}

func (l *lockedSource) Int63() int64 {
l.mu.Lock()
ret := l.src.Int63()
l.mu.Unlock()
return ret
}

func (l *lockedSource) Uint64() uint64 {
l.mu.Lock()
ret := l.src.Uint64()
l.mu.Unlock()
return ret
}

func (l *lockedSource) Seed(seed int64) {
l.mu.Lock()
l.src.Seed(seed)
l.mu.Unlock()
// The returned value is not thread-safe. If you need a thread-safe random
// number generator, use the top-level functions of the "math/rand/v2"
// package. They are not as fast as the Go 1 ("math/rand") generator, but
// they are much faster if access to the Go 1 generator must be guarded
// by a mutex.
func NewRand() *rand.Rand {
// The top-level functions in "math/rand/v2" use the same per-thread,
// lock-free RNGs as the "hash/maphash" package. So we use that to
// generate a seed. Thereafter, we use the Go 1 generator in "math/rand"
// since its RNG is a bit faster than the offerings in "math/rand/v2".
return rand.New(rand.NewSource(randv2.Int64())) //nolint:gosec // don't need cryptographic RNG
}
13 changes: 4 additions & 9 deletions picker/poweroftwo.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,11 @@
package picker

import (
"math/rand"
"math/rand/v2"
"net/http"
"sync/atomic"

"github.com/bufbuild/httplb/conn"
"github.com/bufbuild/httplb/internal"
)

// NewPowerOfTwo creates pickers that select two connections at random
Expand Down Expand Up @@ -49,15 +48,11 @@ func NewPowerOfTwo(prev Picker, allConns conn.Conns) Picker {
}
}

return &powerOfTwo{
conns: newConns,
rng: internal.NewLockedRand(),
}
return &powerOfTwo{conns: newConns}
}

type powerOfTwo struct {
conns []*powerOfTwoConnItem
rng *rand.Rand
}

type powerOfTwoConnItem struct {
Expand All @@ -67,8 +62,8 @@ type powerOfTwoConnItem struct {
}

func (p *powerOfTwo) Pick(*http.Request) (conn conn.Conn, whenDone func(), err error) {
entry1 := p.conns[p.rng.Intn(len(p.conns))]
entry2 := p.conns[p.rng.Intn(len(p.conns))]
entry1 := p.conns[rand.IntN(len(p.conns))] //nolint:gosec // does not need to be cryptographically secure
entry2 := p.conns[rand.IntN(len(p.conns))] //nolint:gosec // does not need to be cryptographically secure

var entry *powerOfTwoConnItem
if uint64(entry1.load.Load()) < uint64(entry2.load.Load()) {
Expand Down
6 changes: 3 additions & 3 deletions picker/random.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,16 @@
package picker

import (
"math/rand/v2"
"net/http"

"github.com/bufbuild/httplb/conn"
"github.com/bufbuild/httplb/internal"
)

// NewRandom creates pickers that picks a connections at random.
func NewRandom(_ Picker, allConns conn.Conns) Picker {
rnd := internal.NewLockedRand()
return pickerFunc(func(*http.Request) (conn conn.Conn, whenDone func(), err error) {
return allConns.Get(rnd.Intn(allConns.Len())), nil, nil
return allConns.Get(rand.IntN(allConns.Len())), //nolint:gosec // does not need to be cryptographically secure
nil, nil
})
}