Skip to content

Commit 1803100

Browse files
Merge pull request #599 from smarthall/improve_proxy_experience
Speed up proxy selection code
2 parents e4671e1 + 8506ceb commit 1803100

File tree

2 files changed

+117
-35
lines changed

2 files changed

+117
-35
lines changed

pkg/cli/config/config.go

+95-20
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package config
22

33
import (
4+
"context"
45
"fmt"
56
"net/http"
67
"net/url"
78
"os"
89
"path/filepath"
10+
"sync"
911
"time"
1012

1113
logger "github.com/sirupsen/logrus"
@@ -52,6 +54,7 @@ const (
5254
JiraConfigForAccessRequestsKey = "jira-config-for-access-requests"
5355
prodEnvNameDefaultValue = "production"
5456
JiraBaseURLDefaultValue = "https://issues.redhat.com"
57+
proxyTestTimeout = 10 * time.Second
5558
)
5659

5760
var JiraConfigForAccessRequestsDefaultValue = AccessRequestsJiraConfiguration{
@@ -190,43 +193,115 @@ func GetBackplaneConfiguration() (bpConfig BackplaneConfiguration, err error) {
190193
return bpConfig, nil
191194
}
192195

193-
var clientDo = func(client *http.Client, req *http.Request) (*http.Response, error) {
194-
return client.Do(req)
196+
var testProxy = func(ctx context.Context, testURL string, proxyURL url.URL) error {
197+
// Try call the test URL via the proxy
198+
client := &http.Client{
199+
Transport: &http.Transport{Proxy: http.ProxyURL(&proxyURL)},
200+
}
201+
req, _ := http.NewRequestWithContext(ctx, "GET", testURL, nil)
202+
resp, err := client.Do(req)
203+
204+
// Check the result
205+
if err != nil {
206+
return fmt.Errorf("proxy returned an error %v", err)
207+
}
208+
if resp.StatusCode != http.StatusOK {
209+
return fmt.Errorf("expected response code 200 but got %d", resp.StatusCode)
210+
}
211+
212+
return nil
195213
}
196214

197215
func (config *BackplaneConfiguration) getFirstWorkingProxyURL(s []string) string {
198-
bpURL := config.URL + "/healthz"
216+
if len(s) == 0 {
217+
logger.Debug("No proxy to use")
218+
return ""
219+
}
199220

200-
client := &http.Client{
201-
Timeout: 5 * time.Second,
221+
// If we only have one proxy, there is no need to waste time on tests, just use that one
222+
if len(s) == 1 {
223+
logger.Debug("Only one proxy to choose from, automatically using it")
224+
return s[0]
202225
}
203226

227+
// Context to time out or cancel all tests once we are done
228+
ctx, cancel := context.WithTimeout(context.Background(), proxyTestTimeout)
229+
var wg sync.WaitGroup
230+
ch := make(chan *url.URL)
231+
232+
bpURL := config.URL + "/healthz"
233+
234+
failures := 0
204235
for _, p := range s {
236+
// Parse the proxy URL
205237
proxyURL, err := url.ParseRequestURI(p)
206238
if err != nil {
207239
logger.Debugf("proxy-url: '%v' could not be parsed.", p)
240+
failures++
208241
continue
209242
}
210243

211-
client.Transport = &http.Transport{Proxy: http.ProxyURL(proxyURL)}
212-
req, _ := http.NewRequest("GET", bpURL, nil)
213-
resp, err := clientDo(client, req)
214-
if err != nil {
215-
logger.Infof("Proxy: %s returned an error: %s", proxyURL, err)
216-
continue
217-
}
218-
if resp.StatusCode == http.StatusOK {
219-
return p
220-
}
221-
logger.Infof("proxy: %s did not pass healthcheck, expected response code 200, got %d, discarding", p, resp.StatusCode)
244+
wg.Add(1)
245+
go func(proxyURL url.URL) {
246+
defer wg.Done()
247+
248+
// Do the proxy test
249+
proxyErr := testProxy(ctx, bpURL, proxyURL)
250+
if proxyErr != nil {
251+
logger.Infof("Discarding proxy %s due to error: %s", proxyURL.String(), proxyErr)
252+
ch <- nil
253+
return
254+
}
255+
256+
// This test succeeded, send to the main thread
257+
ch <- &proxyURL
258+
}(*proxyURL)
222259
}
223260

224-
if len(s) > 0 {
225-
logger.Infof("falling back to first proxy-url after all proxies failed health checks: %s", s[0])
226-
return s[0]
261+
// Default to the first
262+
chosenURL := s[0]
263+
264+
// Loop until all tests have failed or we get a single success
265+
loop:
266+
for failures < len(s) {
267+
select {
268+
case proxyURL := <-ch: // A proxy returned a result
269+
// nil means the test failed
270+
if proxyURL == nil {
271+
failures++
272+
continue
273+
}
274+
275+
// This proxy passed
276+
chosenURL = proxyURL.String()
277+
logger.Infof("proxy that responded first was %s", chosenURL)
278+
279+
break loop
280+
281+
case <-ctx.Done(): // We timed out waiting for a proxy to pass
282+
logger.Warnf("falling back to first proxy-url after all proxies timed out: %s", s[0])
283+
284+
break loop
285+
}
227286
}
228287

229-
return ""
288+
// Cancel any remaining requests
289+
cancel()
290+
291+
// Ignore any other valid proxies, until the channel is closed
292+
go func() {
293+
for lateProxy := range ch {
294+
if lateProxy != nil {
295+
logger.Infof("proxy %s responded too late", lateProxy)
296+
}
297+
}
298+
}()
299+
300+
// Wait for goroutines to end, then close the channel
301+
wg.Wait()
302+
close(ch)
303+
304+
return chosenURL
230305
}
231306

232307
func validateConfig() error {

pkg/cli/config/config_test.go

+22-15
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package config
22

33
import (
4+
"context"
5+
"fmt"
46
"net/http"
57
"net/http/httptest"
8+
"net/url"
69
"os"
710
"testing"
811

@@ -86,10 +89,10 @@ func TestGetBackplaneConnection(t *testing.T) {
8689

8790
func TestBackplaneConfiguration_getFirstWorkingProxyURL(t *testing.T) {
8891
tests := []struct {
89-
name string
90-
proxies []string
91-
clientDoFunc func(client *http.Client, req *http.Request) (*http.Response, error)
92-
want string
92+
name string
93+
proxies []string
94+
testFunc func(ctx context.Context, testURL string, proxyURL url.URL) error
95+
want string
9396
}{
9497
{
9598
name: "invalid-format-proxy",
@@ -102,26 +105,30 @@ func TestBackplaneConfiguration_getFirstWorkingProxyURL(t *testing.T) {
102105
want: "-",
103106
},
104107
{
105-
name: "valid-proxies",
108+
name: "single-valid-proxy",
106109
proxies: []string{"https://proxy.invalid"},
107-
clientDoFunc: func(client *http.Client, req *http.Request) (*http.Response, error) {
108-
return &http.Response{StatusCode: http.StatusOK}, nil
110+
testFunc: func(ctx context.Context, testURL string, proxyURL url.URL) error {
111+
return nil
109112
},
110113
want: "https://proxy.invalid",
111114
},
112115
{
113-
name: "multiple-valid-proxies",
114-
proxies: []string{"https://proxy.invalid", "https://dummy.proxy.invalid"},
115-
clientDoFunc: func(client *http.Client, req *http.Request) (*http.Response, error) {
116-
return &http.Response{StatusCode: http.StatusOK}, nil
116+
name: "one-proxy-fails",
117+
proxies: []string{"http://this.proxy.succeeds", "http://this.proxy.fails"},
118+
testFunc: func(ctx context.Context, testURL string, proxyURL url.URL) error {
119+
if proxyURL.Host == "this.proxy.succeeds" {
120+
return nil
121+
}
122+
123+
return fmt.Errorf("Testing Error")
117124
},
118-
want: "https://proxy.invalid",
125+
want: "http://this.proxy.succeeds",
119126
},
120127
{
121128
name: "multiple-mixed-proxies",
122129
proxies: []string{"-", "gellso", "https://proxy.invalid"},
123-
clientDoFunc: func(client *http.Client, req *http.Request) (*http.Response, error) {
124-
return &http.Response{StatusCode: http.StatusOK}, nil
130+
testFunc: func(ctx context.Context, testURL string, proxyURL url.URL) error {
131+
return nil
125132
},
126133
want: "https://proxy.invalid",
127134
},
@@ -132,7 +139,7 @@ func TestBackplaneConfiguration_getFirstWorkingProxyURL(t *testing.T) {
132139
_, _ = w.Write([]byte("dummy data"))
133140
}))
134141

135-
clientDo = tt.clientDoFunc
142+
testProxy = tt.testFunc
136143

137144
config := &BackplaneConfiguration{
138145
URL: svr.URL,

0 commit comments

Comments
 (0)