From 6951316800a0cb29a1cd07f2430ddcb8d2bc99a4 Mon Sep 17 00:00:00 2001 From: Devin Trejo Date: Fri, 26 Aug 2022 13:03:07 -0400 Subject: [PATCH 1/5] improvement: Implement round robin uri scoring as a URI selector. --- .../httpclient/internal/rr_selector.go | 49 +++++++++++++++++++ .../httpclient/internal/rr_selector_test.go | 45 +++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 conjure-go-client/httpclient/internal/rr_selector.go create mode 100644 conjure-go-client/httpclient/internal/rr_selector_test.go diff --git a/conjure-go-client/httpclient/internal/rr_selector.go b/conjure-go-client/httpclient/internal/rr_selector.go new file mode 100644 index 00000000..4456ce9b --- /dev/null +++ b/conjure-go-client/httpclient/internal/rr_selector.go @@ -0,0 +1,49 @@ +// Copyright (c) 2022 Palantir Technologies. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package internal + +import ( + "net/http" + "sync" +) + +type roundRobinSelector struct { + sync.Mutex + nanoClock func() int64 + + offset int +} + +// NewRoundRobinURISelector returns a URI scorer that uses a round robin algorithm for selecting URIs when scoring +// using a rand.Rand seeded by the nanoClock function. The middleware no-ops on each request. +func NewRoundRobinURISelector(nanoClock func() int64) URISelector { + return &roundRobinSelector{ + nanoClock: nanoClock, + } +} + +// Select implements Selector interface +func (s *roundRobinSelector) Select(uris []string, _ http.Header) ([]string, error) { + s.Lock() + defer s.Unlock() + + s.offset = (s.offset + 1) % len(uris) + + return []string{uris[s.offset]}, nil +} + +func (s *roundRobinSelector) RoundTrip(req *http.Request, next http.RoundTripper) (*http.Response, error) { + return next.RoundTrip(req) +} diff --git a/conjure-go-client/httpclient/internal/rr_selector_test.go b/conjure-go-client/httpclient/internal/rr_selector_test.go new file mode 100644 index 00000000..cb0606ba --- /dev/null +++ b/conjure-go-client/httpclient/internal/rr_selector_test.go @@ -0,0 +1,45 @@ +// Copyright (c) 2022 Palantir Technologies. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package internal + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestRoundRobinSelector_Select(t *testing.T) { + uris := []string{"uri1", "uri2", "uri3", "uri4", "uri5"} + scorer := NewRoundRobinURISelector(func() int64 { return time.Now().UnixNano() }) + + const iterations = 100 + observed := make(map[string]int, iterations) + for i := 0; i < iterations; i++ { + uri, err := scorer.Select(uris, nil) + assert.NoError(t, err) + assert.Len(t, uri, 1) + observed[uri[0]] = observed[uri[0]] + 1 + } + + occurences := make([]int, 0, len(observed)) + for _, count := range observed { + occurences = append(occurences, count) + } + + for _, v := range occurences { + assert.Equal(t, occurences[0], v) + } +} From 64f46903017c6aa4cff45faf266fb9fa565a5b76 Mon Sep 17 00:00:00 2001 From: svc-changelog Date: Fri, 26 Aug 2022 17:05:25 +0000 Subject: [PATCH 2/5] Add generated changelog entries --- changelog/@unreleased/pr-351.v2.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelog/@unreleased/pr-351.v2.yml diff --git a/changelog/@unreleased/pr-351.v2.yml b/changelog/@unreleased/pr-351.v2.yml new file mode 100644 index 00000000..1c4585cd --- /dev/null +++ b/changelog/@unreleased/pr-351.v2.yml @@ -0,0 +1,5 @@ +type: improvement +improvement: + description: Implement round robin scorer as URISelector interface + links: + - https://github.com/palantir/conjure-go-runtime/pull/351 From 3a08f61f7704efff788eb5106af55bc8923b6067 Mon Sep 17 00:00:00 2001 From: Devin Trejo Date: Fri, 26 Aug 2022 13:19:50 -0400 Subject: [PATCH 3/5] Handle uri updates by randomizing offest on reinit. --- .../httpclient/internal/rr_selector.go | 32 ++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/conjure-go-client/httpclient/internal/rr_selector.go b/conjure-go-client/httpclient/internal/rr_selector.go index 4456ce9b..f8625dcf 100644 --- a/conjure-go-client/httpclient/internal/rr_selector.go +++ b/conjure-go-client/httpclient/internal/rr_selector.go @@ -15,22 +15,25 @@ package internal import ( + "math/rand" "net/http" "sync" ) type roundRobinSelector struct { sync.Mutex - nanoClock func() int64 + source rand.Source - offset int + prevURIs []string + offset int } // NewRoundRobinURISelector returns a URI scorer that uses a round robin algorithm for selecting URIs when scoring // using a rand.Rand seeded by the nanoClock function. The middleware no-ops on each request. func NewRoundRobinURISelector(nanoClock func() int64) URISelector { return &roundRobinSelector{ - nanoClock: nanoClock, + source: rand.NewSource(nanoClock()), + prevURIs: []string{}, } } @@ -39,11 +42,32 @@ func (s *roundRobinSelector) Select(uris []string, _ http.Header) ([]string, err s.Lock() defer s.Unlock() + s.updateURIs(uris) s.offset = (s.offset + 1) % len(uris) - return []string{uris[s.offset]}, nil } +// updateURIs determines whether we need to update the stored prevURIs because the current set of URIs differ from the +// last observed URIs. When the URIs we randomize to get a new offest. +func (s *roundRobinSelector) updateURIs(uris []string) { + reset := false + if len(s.prevURIs) == 0 { + reset = true + } + for i, uri := range s.prevURIs { + if uri != uris[i] { + reset = true + break + } + } + + if reset { + s.prevURIs = uris + // randomize offset on reinit + s.offset = rand.New(s.source).Intn(len(uris)) + } +} + func (s *roundRobinSelector) RoundTrip(req *http.Request, next http.RoundTripper) (*http.Response, error) { return next.RoundTrip(req) } From a7a5f5f2a951fccce19c4bd6582d4fd61b4ab7fa Mon Sep 17 00:00:00 2001 From: Devin Trejo Date: Fri, 26 Aug 2022 15:47:08 -0400 Subject: [PATCH 4/5] docstring. --- conjure-go-client/httpclient/internal/rr_selector.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/conjure-go-client/httpclient/internal/rr_selector.go b/conjure-go-client/httpclient/internal/rr_selector.go index f8625dcf..5ff95f72 100644 --- a/conjure-go-client/httpclient/internal/rr_selector.go +++ b/conjure-go-client/httpclient/internal/rr_selector.go @@ -29,7 +29,8 @@ type roundRobinSelector struct { } // NewRoundRobinURISelector returns a URI scorer that uses a round robin algorithm for selecting URIs when scoring -// using a rand.Rand seeded by the nanoClock function. The middleware no-ops on each request. +// using a rand.Rand seeded by the nanoClock function. The middleware no-ops on each request. This selector will always +// return one URI. func NewRoundRobinURISelector(nanoClock func() int64) URISelector { return &roundRobinSelector{ source: rand.NewSource(nanoClock()), From 8ac8585e44a6ef2217314db758767616b7ed6546 Mon Sep 17 00:00:00 2001 From: Devin Trejo Date: Fri, 26 Aug 2022 16:18:44 -0400 Subject: [PATCH 5/5] Test for empty set of provided uris --- .../httpclient/internal/rr_selector.go | 5 +++ .../httpclient/internal/rr_selector_test.go | 44 +++++++++++-------- 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/conjure-go-client/httpclient/internal/rr_selector.go b/conjure-go-client/httpclient/internal/rr_selector.go index 5ff95f72..67d97134 100644 --- a/conjure-go-client/httpclient/internal/rr_selector.go +++ b/conjure-go-client/httpclient/internal/rr_selector.go @@ -18,6 +18,8 @@ import ( "math/rand" "net/http" "sync" + + werror "github.com/palantir/witchcraft-go-error" ) type roundRobinSelector struct { @@ -42,6 +44,9 @@ func NewRoundRobinURISelector(nanoClock func() int64) URISelector { func (s *roundRobinSelector) Select(uris []string, _ http.Header) ([]string, error) { s.Lock() defer s.Unlock() + if len(uris) == 0 { + return nil, werror.Error("no valid uris provided to round robin uri-selector") + } s.updateURIs(uris) s.offset = (s.offset + 1) % len(uris) diff --git a/conjure-go-client/httpclient/internal/rr_selector_test.go b/conjure-go-client/httpclient/internal/rr_selector_test.go index cb0606ba..38637999 100644 --- a/conjure-go-client/httpclient/internal/rr_selector_test.go +++ b/conjure-go-client/httpclient/internal/rr_selector_test.go @@ -19,27 +19,35 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestRoundRobinSelector_Select(t *testing.T) { - uris := []string{"uri1", "uri2", "uri3", "uri4", "uri5"} scorer := NewRoundRobinURISelector(func() int64 { return time.Now().UnixNano() }) - const iterations = 100 - observed := make(map[string]int, iterations) - for i := 0; i < iterations; i++ { - uri, err := scorer.Select(uris, nil) - assert.NoError(t, err) - assert.Len(t, uri, 1) - observed[uri[0]] = observed[uri[0]] + 1 - } - - occurences := make([]int, 0, len(observed)) - for _, count := range observed { - occurences = append(occurences, count) - } - - for _, v := range occurences { - assert.Equal(t, occurences[0], v) - } + t.Run("round robins across valid connections", func(t *testing.T) { + uris := []string{"uri1", "uri2", "uri3", "uri4", "uri5"} + const iterations = 100 + observed := make(map[string]int, iterations) + for i := 0; i < iterations; i++ { + uri, err := scorer.Select(uris, nil) + assert.NoError(t, err) + assert.Len(t, uri, 1) + observed[uri[0]] = observed[uri[0]] + 1 + } + + occurences := make([]int, 0, len(observed)) + for _, count := range observed { + occurences = append(occurences, count) + } + + for _, v := range occurences { + assert.Equal(t, occurences[0], v) + } + }) + + t.Run("erorrs with empty set of provided uris", func(t *testing.T) { + _, err := scorer.Select([]string{}, nil) + require.Error(t, err) + }) }