Skip to content

Commit 1b1732c

Browse files
committed
HTTP client factory for per-request clients
1 parent 40b0cad commit 1b1732c

File tree

1 file changed

+66
-21
lines changed

1 file changed

+66
-21
lines changed

client.go

+66-21
Original file line numberDiff line numberDiff line change
@@ -401,11 +401,24 @@ type ErrorHandler func(resp *http.Response, err error, numTries int) (*http.Resp
401401
// PrepareRetry is called before retry operation. It can be used for example to re-sign the request
402402
type PrepareRetry func(req *http.Request) error
403403

404+
type HTTPClient interface {
405+
// Do performs an HTTP request and returns an HTTP response.
406+
Do(*http.Request) (*http.Response, error)
407+
// Done is called when the client is no longer needed.
408+
Done()
409+
}
410+
411+
type HTTPClientFactory interface {
412+
// New returns an HTTP client to use for a request, including retries.
413+
New() HTTPClient
414+
}
415+
404416
// Client is used to make HTTP requests. It adds additional functionality
405417
// like automatic retries to tolerate minor outages.
406418
type Client struct {
407-
HTTPClient *http.Client // Internal HTTP client.
408-
Logger interface{} // Customer logger instance. Can be either Logger or LeveledLogger
419+
HTTPClient *http.Client // Internal HTTP client. This field is used if set, otherwise HTTPClientFactory is used.
420+
HTTPClientFactory HTTPClientFactory
421+
Logger interface{} // Customer logger instance. Can be either Logger or LeveledLogger
409422

410423
RetryWaitMin time.Duration // Minimum time to wait
411424
RetryWaitMax time.Duration // Maximum time to wait
@@ -433,19 +446,18 @@ type Client struct {
433446
PrepareRetry PrepareRetry
434447

435448
loggerInit sync.Once
436-
clientInit sync.Once
437449
}
438450

439451
// NewClient creates a new Client with default settings.
440452
func NewClient() *Client {
441453
return &Client{
442-
HTTPClient: cleanhttp.DefaultPooledClient(),
443-
Logger: defaultLogger,
444-
RetryWaitMin: defaultRetryWaitMin,
445-
RetryWaitMax: defaultRetryWaitMax,
446-
RetryMax: defaultRetryMax,
447-
CheckRetry: DefaultRetryPolicy,
448-
Backoff: DefaultBackoff,
454+
HTTPClientFactory: &CleanPooledClientFactory{},
455+
Logger: defaultLogger,
456+
RetryWaitMin: defaultRetryWaitMin,
457+
RetryWaitMax: defaultRetryWaitMax,
458+
RetryMax: defaultRetryMax,
459+
CheckRetry: DefaultRetryPolicy,
460+
Backoff: DefaultBackoff,
449461
}
450462
}
451463

@@ -647,12 +659,6 @@ func PassthroughErrorHandler(resp *http.Response, err error, _ int) (*http.Respo
647659

648660
// Do wraps calling an HTTP method with retries.
649661
func (c *Client) Do(req *Request) (*http.Response, error) {
650-
c.clientInit.Do(func() {
651-
if c.HTTPClient == nil {
652-
c.HTTPClient = cleanhttp.DefaultPooledClient()
653-
}
654-
})
655-
656662
logger := c.logger()
657663

658664
if logger != nil {
@@ -664,6 +670,9 @@ func (c *Client) Do(req *Request) (*http.Response, error) {
664670
}
665671
}
666672

673+
httpClient := c.getHTTPClient()
674+
defer httpClient.Done()
675+
667676
var resp *http.Response
668677
var attempt int
669678
var shouldRetry bool
@@ -677,7 +686,6 @@ func (c *Client) Do(req *Request) (*http.Response, error) {
677686
if req.body != nil {
678687
body, err := req.body()
679688
if err != nil {
680-
c.HTTPClient.CloseIdleConnections()
681689
return resp, err
682690
}
683691
if c, ok := body.(io.ReadCloser); ok {
@@ -699,7 +707,8 @@ func (c *Client) Do(req *Request) (*http.Response, error) {
699707
}
700708

701709
// Attempt the request
702-
resp, doErr = c.HTTPClient.Do(req.Request)
710+
711+
resp, doErr = httpClient.Do(req.Request)
703712

704713
// Check if we should continue with retries.
705714
shouldRetry, checkErr = c.CheckRetry(req.Context(), resp, doErr)
@@ -768,7 +777,6 @@ func (c *Client) Do(req *Request) (*http.Response, error) {
768777
select {
769778
case <-req.Context().Done():
770779
timer.Stop()
771-
c.HTTPClient.CloseIdleConnections()
772780
return nil, req.Context().Err()
773781
case <-timer.C:
774782
}
@@ -791,8 +799,6 @@ func (c *Client) Do(req *Request) (*http.Response, error) {
791799
return resp, nil
792800
}
793801

794-
defer c.HTTPClient.CloseIdleConnections()
795-
796802
var err error
797803
if prepareErr != nil {
798804
err = prepareErr
@@ -841,6 +847,19 @@ func (c *Client) drainBody(body io.ReadCloser) {
841847
}
842848
}
843849

850+
func (c *Client) getHTTPClient() HTTPClient {
851+
if c.HTTPClient != nil {
852+
return &idleConnectionsClosingClient{
853+
httpClient: c.HTTPClient,
854+
}
855+
}
856+
clientFactory := c.HTTPClientFactory
857+
if clientFactory == nil {
858+
clientFactory = &CleanPooledClientFactory{}
859+
}
860+
return clientFactory.New()
861+
}
862+
844863
// Get is a shortcut for doing a GET request without making a new client.
845864
func Get(url string) (*http.Response, error) {
846865
return defaultClient.Get(url)
@@ -917,3 +936,29 @@ func redactURL(u *url.URL) string {
917936
}
918937
return ru.String()
919938
}
939+
940+
var (
941+
_ HTTPClientFactory = &CleanPooledClientFactory{}
942+
_ HTTPClient = &idleConnectionsClosingClient{}
943+
)
944+
945+
type CleanPooledClientFactory struct {
946+
}
947+
948+
func (f *CleanPooledClientFactory) New() HTTPClient {
949+
return &idleConnectionsClosingClient{
950+
httpClient: cleanhttp.DefaultPooledClient(),
951+
}
952+
}
953+
954+
type idleConnectionsClosingClient struct {
955+
httpClient *http.Client
956+
}
957+
958+
func (c *idleConnectionsClosingClient) Do(req *http.Request) (*http.Response, error) {
959+
return c.httpClient.Do(req)
960+
}
961+
962+
func (c *idleConnectionsClosingClient) Done() {
963+
c.httpClient.CloseIdleConnections()
964+
}

0 commit comments

Comments
 (0)