Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

shuffle targets during parse phase #343

Merged
merged 2 commits into from
May 15, 2024
Merged
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
48 changes: 29 additions & 19 deletions go/vt/vtgateproxy/discovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ type JSONGateResolverBuilder struct {
targets map[string][]targetHost
resolvers []*JSONGateResolver

rand *rand.Rand
Copy link
Author

Choose a reason for hiding this comment

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

I think it's ok to just use the global rand in the rand package here. We actually introduced a bug when we added GetTargets because rand.Rand is not safe to use concurrently (the top level package funcs are safe)

Copy link
Collaborator

Choose a reason for hiding this comment

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

That's fine, but then we need to call rand.Seed during Init somewhere

Copy link
Collaborator

@rjlaine rjlaine May 14, 2024

Choose a reason for hiding this comment

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

rand.Seed was deprecated in go1.20, so if we want to rebase off a current branch, we might want to avoid it. If possible, I'd like to avoid using package level global instances like rand.globalRandGenerator because they can be poisoned by other packages. If concurrency is a problem, could we use a rand.Rand and a sync.Mutex?

Copy link
Author

Choose a reason for hiding this comment

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

noted, rewritten to use a rand.Rand with a sync.Mutex

sorter *shuffleSorter
ticker *time.Ticker
checksum []byte
}
Expand Down Expand Up @@ -114,6 +114,7 @@ func RegisterJSONGateResolver(
poolTypeField: poolTypeField,
affinityField: affinityField,
affinityValue: affinityValue,
sorter: newShuffleSorter(),
}

resolver.Register(jsonDiscovery)
Expand All @@ -133,9 +134,6 @@ func (*JSONGateResolverBuilder) Scheme() string { return "vtgate" }

// Parse and validate the format of the file and start watching for changes
func (b *JSONGateResolverBuilder) start() error {

b.rand = rand.New(rand.NewSource(time.Now().UnixNano()))

// Perform the initial parse
_, err := b.parse()
if err != nil {
Expand Down Expand Up @@ -288,11 +286,7 @@ func (b *JSONGateResolverBuilder) parse() (bool, error) {
}

for poolType := range targets {
if b.affinityField != "" {
sort.Slice(targets[poolType], func(i, j int) bool {
return b.affinityValue == targets[poolType][i].Affinity
})
}
b.sorter.shuffleSort(targets[poolType], b.affinityField, b.affinityValue)
if len(targets[poolType]) > *numConnections {
targets[poolType] = targets[poolType][:*numConnections]
}
Expand Down Expand Up @@ -324,32 +318,48 @@ func (b *JSONGateResolverBuilder) GetTargets(poolType string) []targetHost {
targets = append(targets, b.targets[poolType]...)
b.mu.RUnlock()

// Shuffle to ensure every host has a different order to iterate through, putting
// the affinity matching (e.g. same az) hosts at the front and the non-matching ones
// at the end.
//
// Only need to do n-1 swaps since the last host is always in the right place.
b.sorter.shuffleSort(targets, b.affinityField, b.affinityValue)

return targets
}

type shuffleSorter struct {
rand *rand.Rand
mu *sync.Mutex
}

func newShuffleSorter() *shuffleSorter {
return &shuffleSorter{
rand: rand.New(rand.NewSource(time.Now().UnixNano())),
mu: &sync.Mutex{},
}
}

// shuffleSort shuffles a slice of targetHost to ensure every host has a
// different order to iterate through, putting the affinity matching (e.g. same
// az) hosts at the front and the non-matching ones at the end.
func (s *shuffleSorter) shuffleSort(targets []targetHost, affinityField, affinityValue string) {
n := len(targets)
head := 0
// Only need to do n-1 swaps since the last host is always in the right place.
tail := n - 1
for i := 0; i < n-1; i++ {
j := head + b.rand.Intn(tail-head+1)
s.mu.Lock()
j := head + s.rand.Intn(tail-head+1)
s.mu.Unlock()
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is likely irrelevant since we're not ever calling this under load, but my .02 would be that we don't need to take and drop the lock so often, and instead just taking it once at the beginning of sort and dropping at end would be fine.


if *affinityField != "" && *affinityValue == targets[j].Affinity {
if affinityField != "" && affinityValue == targets[j].Affinity {
targets[head], targets[j] = targets[j], targets[head]
head++
} else {
targets[tail], targets[j] = targets[j], targets[tail]
tail--
}
}

return targets
}

// Update the current list of hosts for the given resolver
func (b *JSONGateResolverBuilder) update(r *JSONGateResolver) {

log.V(100).Infof("resolving target %s to %d connections\n", r.target.URL.String(), *numConnections)

targets := b.GetTargets(r.poolType)
Expand Down
Loading