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

Proposal for refreshable client configuration #171

Merged
merged 42 commits into from
Oct 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
8eac168
Add generated client refreshables
gcampbell12 Feb 1, 2021
6ed15c7
Make runtime URIs reloadable (#146)
gcampbell12 Feb 3, 2021
cb23ce5
wip
bmoylan Mar 31, 2021
e7967a1
wip
bmoylan Apr 7, 2021
686c1ed
Merge branch 'develop' into bm/refreshable
bmoylan Apr 7, 2021
0cd309b
wip
bmoylan Apr 7, 2021
451ff22
working
bmoylan Apr 7, 2021
74cb218
fix
bmoylan Apr 7, 2021
08f755c
rename
bmoylan Apr 7, 2021
1502238
update
bmoylan Apr 8, 2021
a110ce5
refreshables branch
bmoylan Apr 8, 2021
cababfa
update
bmoylan Apr 8, 2021
eda75a6
start test
bmoylan Apr 9, 2021
54df7b9
Merge branch 'develop' into bm/refreshable
bmoylan Apr 13, 2021
1ad1079
wip
bmoylan Apr 17, 2021
c8bbe69
Merge branch 'develop' into bm/refreshable
bmoylan Apr 17, 2021
64b3846
wip
bmoylan Apr 23, 2021
1851025
wip
bmoylan Apr 24, 2021
e1f19a2
Merge branch 'develop' into bm/refreshable
bmoylan Apr 24, 2021
53694bb
refreshable
bmoylan Apr 24, 2021
cb1820a
cleanup
bmoylan Apr 24, 2021
4ff7270
updates
bmoylan Apr 25, 2021
a33ec41
regen
bmoylan Apr 26, 2021
678ddcc
Merge branch 'develop' into bm/refreshable
bmoylan May 23, 2021
2b0a148
CR
bmoylan May 23, 2021
6d1e2f9
changelog
bmoylan May 23, 2021
9e93cde
comment on http2
bmoylan May 23, 2021
b4c58c7
port over http2 improvement
bmoylan Sep 18, 2021
d470c5b
Merge branch 'develop' into bm/refreshable
bmoylan Sep 18, 2021
38414e3
generate
bmoylan Sep 18, 2021
a83ce7b
minor cleanup
bmoylan Sep 18, 2021
31ef1f7
panic
bmoylan Sep 20, 2021
5b66960
h2 params
bmoylan Sep 20, 2021
25f8328
Merge branch 'develop' into bm/refreshable
bmoylan Sep 25, 2021
e648619
require uris
bmoylan Sep 25, 2021
e34b201
update
bmoylan Sep 25, 2021
9d3d5e9
require uris
bmoylan Sep 25, 2021
b6596a1
Merge branch 'develop' into bm/refreshable
bmoylan Sep 28, 2021
50d3799
Merge branch 'develop' into bm/refreshable
bmoylan Oct 2, 2021
888f130
Merge branch 'develop' into bm/refreshable
bmoylan Oct 4, 2021
c8d4615
Merge branch 'develop' into bm/refreshable
bmoylan Oct 8, 2021
a60fd95
comments
bmoylan Oct 8, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions changelog/@unreleased/pr-171.v2.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type: feature
feature:
description: Add NewClientFromRefreshableConfig and handle live-reloading configuration changes.
links:
- https://github.com/palantir/conjure-go-runtime/pull/171
18 changes: 17 additions & 1 deletion conjure-go-client/httpclient/authn.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import (
"context"
"fmt"
"net/http"

"github.com/palantir/pkg/refreshable"
)

// TokenProvider accepts a context and returns either:
Expand All @@ -33,7 +35,7 @@ type authTokenMiddleware struct {
provideToken TokenProvider
}

// AuthTokenProviderMiddleware wraps an existing round tripper with a token providing round tripper.
// RoundTrip wraps an existing round tripper with a token providing round tripper.
// It sets the Authorization header using a newly provided token for each request.
func (h *authTokenMiddleware) RoundTrip(req *http.Request, next http.RoundTripper) (*http.Response, error) {
token, err := h.provideToken(req.Context())
Expand All @@ -43,3 +45,17 @@ func (h *authTokenMiddleware) RoundTrip(req *http.Request, next http.RoundTrippe
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
return next.RoundTrip(req)
}

func newAuthTokenMiddlewareFromRefreshable(token refreshable.StringPtr) Middleware {
return &conditionalMiddleware{
Disabled: refreshable.NewBool(token.MapStringPtr(func(s *string) interface{} {
return s == nil
})),
Delegate: &authTokenMiddleware{provideToken: func(ctx context.Context) (string, error) {
if s := token.CurrentStringPtr(); s != nil {
return *s, nil
}
return "", nil
}},
}
}
91 changes: 40 additions & 51 deletions conjure-go-client/httpclient/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ import (
"strings"

"github.com/palantir/conjure-go-runtime/v2/conjure-go-client/httpclient/internal"
"github.com/palantir/conjure-go-runtime/v2/conjure-go-client/httpclient/internal/refreshingclient"
"github.com/palantir/pkg/bytesbuffers"
"github.com/palantir/pkg/retry"
"github.com/palantir/pkg/refreshable"
werror "github.com/palantir/witchcraft-go-error"
"github.com/palantir/witchcraft-go-logging/wlog/svclog/svc1log"
"github.com/palantir/witchcraft-go-tracing/wtracing"
)

// A Client executes requests to a configured service.
Expand All @@ -50,16 +50,15 @@ type Client interface {
}

type clientImpl struct {
client http.Client
client RefreshableHTTPClient
middlewares []Middleware
errorDecoderMiddleware Middleware
metricsMiddleware Middleware
recoveryMiddleware Middleware

uriScorer internal.URIScoringMiddleware
maxAttempts int
disableTraceHeaderPropagation bool
backoffOptions []retry.Option
bufferPool bytesbuffers.Pool
uriScorer internal.RefreshableURIScoringMiddleware
maxAttempts refreshable.IntPtr // 0 means no limit. If nil, uses 2*len(uris).
backoffOptions refreshingclient.RefreshableRetryParams
bufferPool bytesbuffers.Pool
}

func (c *clientImpl) Get(ctx context.Context, params ...RequestParam) (*http.Response, error) {
Expand All @@ -83,15 +82,22 @@ func (c *clientImpl) Delete(ctx context.Context, params ...RequestParam) (*http.
}

func (c *clientImpl) Do(ctx context.Context, params ...RequestParam) (*http.Response, error) {
uris := c.uriScorer.GetURIsInOrderOfIncreasingScore()
uris := c.uriScorer.CurrentURIScoringMiddleware().GetURIsInOrderOfIncreasingScore()
if len(uris) == 0 {
return nil, werror.ErrorWithContextParams(ctx, "no base URIs are configured")
}

attempts := 2 * len(uris)
if c.maxAttempts != nil {
if confMaxAttempts := c.maxAttempts.CurrentIntPtr(); confMaxAttempts != nil {
attempts = *confMaxAttempts
}
}

var err error
var resp *http.Response

retrier := internal.NewRequestRetrier(uris, retry.Start(ctx, c.backoffOptions...), c.maxAttempts)
retrier := internal.NewRequestRetrier(uris, c.backoffOptions.CurrentRetryParams().Start(ctx), attempts)
for {
uri, isRelocated := retrier.GetNextURI(resp, err)
if uri == "" {
Expand All @@ -117,7 +123,7 @@ func (c *clientImpl) doOnce(

// 1. create the request
b := &requestBuilder{
headers: c.initializeRequestHeaders(ctx),
headers: make(http.Header),
query: make(url.Values),
bodyMiddleware: &bodyMiddleware{bufferPool: c.bufferPool},
}
Expand All @@ -139,12 +145,12 @@ func (c *clientImpl) doOnce(
}

if b.method == "" {
return nil, werror.Error("httpclient: use WithRequestMethod() to specify HTTP method")
return nil, werror.ErrorWithContextParams(ctx, "httpclient: use WithRequestMethod() to specify HTTP method")
}
reqURI := joinURIAndPath(baseURI, b.path)
req, err := http.NewRequest(b.method, reqURI, nil)
if err != nil {
return nil, werror.Wrap(err, "failed to build new HTTP request")
return nil, werror.WrapWithContextParams(ctx, err, "failed to build new HTTP request")
}
req = req.WithContext(ctx)
req.Header = b.headers
Expand All @@ -154,29 +160,23 @@ func (c *clientImpl) doOnce(

// 2. create the transport and client
// shallow copy so we can overwrite the Transport with a wrapped one.
clientCopy := c.client
transport := clientCopy.Transport // start with the concrete http.Transport from the client

middlewares := []Middleware{
// must precede the error decoders because they return a nil response and the metrics need the status code of
// the raw response.
c.metricsMiddleware,
// must precede the error decoders because they return a nil response and the scorer needs the status code of
// the raw response.
c.uriScorer,
// must precede the client error decoder
b.errorDecoderMiddleware,
// must precede the body middleware so it can read the response body
c.errorDecoderMiddleware,
}
// must precede the body middleware so it can read the request body
middlewares = append(middlewares, c.middlewares...)
middlewares = append(middlewares, b.bodyMiddleware)
for _, middleware := range middlewares {
if middleware != nil {
transport = wrapTransport(transport, middleware)
}
}
clientCopy := *c.client.CurrentHTTPClient()
transport := clientCopy.Transport // start with the client's transport configured with default middleware

// must precede the error decoders to read the status code of the raw response.
transport = wrapTransport(transport, c.uriScorer.CurrentURIScoringMiddleware())
// request decoder must precede the client decoder
// must precede the body middleware to read the response body
transport = wrapTransport(transport, b.errorDecoderMiddleware, c.errorDecoderMiddleware)
// must precede the body middleware to read the request body
transport = wrapTransport(transport, c.middlewares...)
// must wrap inner middlewares to mutate the return values
transport = wrapTransport(transport, b.bodyMiddleware)
// must be the outermost middleware to recover panics in the rest of the request flow
// there is a second, inner recoveryMiddleware in the client's default middlewares so that panics
// inside the inner-most RoundTrip benefit from traceIDs and loggers set on the context.
transport = wrapTransport(transport, c.recoveryMiddleware)
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like this also gets added in the client Build method. Should we remove it from there in favor of wiring it up here to ensure it's the last middleware?

Copy link
Contributor

Choose a reason for hiding this comment

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

ping on this question. The `(b *httpclientBuilder) Build) adds the recoveryMiddleware to the transport which is then wired through to here. So I would assume that when we create the copy of the transport it already contains the recovery middleware, along with the metrics/trace middlewares.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah sorry, I added some additional comments to explain but there are two so that if the panic is deep enough our loggers benefit from the traceID on the context

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok that makes sense. Thanks for adding the comment.


clientCopy.Transport = transport

// 3. execute the request using the client to get and handle the response
Expand All @@ -188,14 +188,14 @@ func (c *clientImpl) doOnce(
internal.DrainBody(resp)
}

return resp, unwrapURLError(respErr)
return resp, unwrapURLError(ctx, respErr)
}

// unwrapURLError converts a *url.Error to a werror. We need this because all
// errors from the stdlib's client.Do are wrapped in *url.Error, and if we
// were to blindly return that we would lose any werror params stored on the
// underlying Err.
func unwrapURLError(respErr error) error {
func unwrapURLError(ctx context.Context, respErr error) error {
if respErr == nil {
return nil
}
Expand All @@ -213,18 +213,7 @@ func unwrapURLError(respErr error) error {
werror.UnsafeParam("requestPath", parsedURL.Path))
}

return werror.Wrap(urlErr.Err, "httpclient request failed", params...)
}

func (c *clientImpl) initializeRequestHeaders(ctx context.Context) http.Header {
headers := make(http.Header)
if !c.disableTraceHeaderPropagation {
traceID := wtracing.TraceIDFromContext(ctx)
if traceID != "" {
headers.Set(traceIDHeaderKey, string(traceID))
}
}
return headers
return werror.WrapWithContextParams(ctx, urlErr.Err, "httpclient request failed", params...)
}

func joinURIAndPath(baseURI, reqPath string) string {
Expand Down