From 9da07111e2b9bd3fc67afa021f993b1cc80b582b Mon Sep 17 00:00:00 2001 From: Erick Pintor Date: Tue, 23 Apr 2024 11:10:45 -0300 Subject: [PATCH] ENG-6200: Separate pure http handling into `apiRequest` (#137) --- client.go | 26 ++++++++-- config.go | 12 ++--- request.go | 128 ++++++++++++++++++++++++++++---------------------- serializer.go | 2 +- 4 files changed, 101 insertions(+), 67 deletions(-) diff --git a/client.go b/client.go index 822c6c4..ff1f5a3 100644 --- a/client.go +++ b/client.go @@ -11,6 +11,7 @@ import ( "math/rand" "net" "net/http" + "net/url" "os" "strconv" "strings" @@ -70,6 +71,9 @@ type Client struct { maxAttempts int maxBackoff time.Duration + + // lazily cached URLs + queryURL *url.URL } // NewDefaultClient initialize a [fauna.Client] with recommend default settings @@ -176,6 +180,16 @@ func NewClient(secret string, timeouts Timeouts, configFns ...ClientConfigFn) *C return client } +func (c *Client) parseQueryURL() (url *url.URL, err error) { + if c.queryURL != nil { + url = c.queryURL + } else if url, err = url.Parse(c.url); err == nil { + url = url.JoinPath("query", "1") + c.queryURL = url + } + return +} + func (c *Client) doWithRetry(req *http.Request) (attempts int, r *http.Response, err error) { req2 := req.Clone(req.Context()) body, rerr := io.ReadAll(req.Body) @@ -247,17 +261,19 @@ func (c *Client) backoff(attempt int) (sleep time.Duration) { // Query invoke fql optionally set multiple [QueryOptFn] func (c *Client) Query(fql *Query, opts ...QueryOptFn) (*QuerySuccess, error) { - req := &fqlRequest{ - Context: c.ctx, - Query: fql, - Headers: c.headers, + req := &queryRequest{ + apiRequest: apiRequest{ + Context: c.ctx, + Headers: c.headers, + }, + Query: fql, } for _, queryOptionFn := range opts { queryOptionFn(req) } - return c.do(req) + return req.do(c) } // Paginate invoke fql with pagination optionally set multiple [QueryOptFn] diff --git a/config.go b/config.go index 5a44c4f..bd1c5bc 100644 --- a/config.go +++ b/config.go @@ -93,18 +93,18 @@ func URL(url string) ClientConfigFn { } // QueryOptFn function to set options on the [Client.Query] -type QueryOptFn func(req *fqlRequest) +type QueryOptFn func(req *queryRequest) // QueryContext set the [context.Context] for a single [Client.Query] func QueryContext(ctx context.Context) QueryOptFn { - return func(req *fqlRequest) { + return func(req *queryRequest) { req.Context = ctx } } // Tags set the tags header on a single [Client.Query] func Tags(tags map[string]string) QueryOptFn { - return func(req *fqlRequest) { + return func(req *queryRequest) { if val, exists := req.Headers[HeaderTags]; exists { req.Headers[HeaderTags] = argsStringFromMap(tags, strings.Split(val, ",")...) } else { @@ -115,19 +115,19 @@ func Tags(tags map[string]string) QueryOptFn { // Traceparent sets the header on a single [Client.Query] func Traceparent(id string) QueryOptFn { - return func(req *fqlRequest) { req.Headers[HeaderTraceparent] = id } + return func(req *queryRequest) { req.Headers[HeaderTraceparent] = id } } // Timeout set the query timeout on a single [Client.Query] func Timeout(dur time.Duration) QueryOptFn { - return func(req *fqlRequest) { + return func(req *queryRequest) { req.Headers[HeaderQueryTimeoutMs] = fmt.Sprintf("%d", dur.Milliseconds()) } } // Typecheck sets the header on a single [Client.Query] func Typecheck(enabled bool) QueryOptFn { - return func(req *fqlRequest) { req.Headers[HeaderTypecheck] = fmt.Sprintf("%v", enabled) } + return func(req *queryRequest) { req.Headers[HeaderTypecheck] = fmt.Sprintf("%v", enabled) } } func argsStringFromMap(input map[string]string, currentArgs ...string) string { diff --git a/request.go b/request.go index a1d5655..9438b79 100644 --- a/request.go +++ b/request.go @@ -11,11 +11,42 @@ import ( "strings" ) -type fqlRequest struct { - Context context.Context - Headers map[string]string - Query any `fauna:"query"` - Arguments map[string]any `fauna:"arguments"` +type apiRequest struct { + Context context.Context + Headers map[string]string +} + +func (apiReq *apiRequest) post(cli *Client, url *url.URL, bytesOut []byte) (attempts int, httpRes *http.Response, err error) { + var httpReq *http.Request + if httpReq, err = http.NewRequestWithContext( + apiReq.Context, + http.MethodPost, + url.String(), + bytes.NewReader(bytesOut), + ); err != nil { + err = fmt.Errorf("failed to init request: %w", err) + return + } + + httpReq.Header.Set(headerAuthorization, `Bearer `+cli.secret) + if lastTxnTs := cli.lastTxnTime.string(); lastTxnTs != "" { + httpReq.Header.Set(HeaderLastTxnTs, lastTxnTs) + } + + for k, v := range apiReq.Headers { + httpReq.Header.Set(k, v) + } + + if attempts, httpRes, err = cli.doWithRetry(httpReq); err != nil { + err = ErrNetwork(fmt.Errorf("network error: %w", err)) + } + return +} + +type queryRequest struct { + apiRequest + Query any + Arguments map[string]any } type queryResponse struct { @@ -43,72 +74,59 @@ func (r *queryResponse) queryTags() map[string]string { return ret } - -func (c *Client) do(request *fqlRequest) (*QuerySuccess, error) { - bytesOut, bytesErr := marshal(request) - if bytesErr != nil { - return nil, fmt.Errorf("marshal request failed: %w", bytesErr) - } - - reqURL, urlErr := url.Parse(c.url) - if urlErr != nil { - return nil, urlErr - } - - if path, err := url.JoinPath(reqURL.Path, "query", "1"); err != nil { - return nil, err - } else { - reqURL.Path = path +func (qReq *queryRequest) do(cli *Client) (qSus *QuerySuccess, err error) { + var bytesOut []byte + if bytesOut, err = marshal(qReq); err != nil { + err = fmt.Errorf("marshal request failed: %w", err) + return } - req, reqErr := http.NewRequestWithContext(request.Context, http.MethodPost, reqURL.String(), bytes.NewReader(bytesOut)) - if reqErr != nil { - return nil, fmt.Errorf("failed to init request: %w", reqErr) + var queryURL *url.URL + if queryURL, err = cli.parseQueryURL(); err != nil { + return } - req.Header.Set(headerAuthorization, `Bearer `+c.secret) - if lastTxnTs := c.lastTxnTime.string(); lastTxnTs != "" { - req.Header.Set(HeaderLastTxnTs, lastTxnTs) + var ( + attempts int + httpRes *http.Response + ) + if attempts, httpRes, err = qReq.post(cli, queryURL, bytesOut); err != nil { + return } - for k, v := range request.Headers { - req.Header.Set(k, v) - } + var ( + qRes queryResponse + bytesIn []byte + ) - attempts, r, doErr := c.doWithRetry(req) - if doErr != nil { - return nil, ErrNetwork(fmt.Errorf("network error: %w", doErr)) + if bytesIn, err = io.ReadAll(httpRes.Body); err != nil { + err = fmt.Errorf("failed to read response body: %w", err) + return } - var res queryResponse - - bin, readErr := io.ReadAll(r.Body) - if readErr != nil { - return nil, fmt.Errorf("failed to read response body: %w", readErr) + if err = json.Unmarshal(bytesIn, &qRes); err != nil { + err = fmt.Errorf("failed to umarmshal response: %w", err) + return } - if unmarshalErr := json.Unmarshal(bin, &res); unmarshalErr != nil { - return nil, fmt.Errorf("failed to umarmshal response: %w", unmarshalErr) - } + cli.lastTxnTime.sync(qRes.TxnTime) + qRes.Header = httpRes.Header - c.lastTxnTime.sync(res.TxnTime) - res.Header = r.Header - - if serviceErr := getErrFauna(r.StatusCode, &res, attempts); serviceErr != nil { - return nil, serviceErr + if err = getErrFauna(httpRes.StatusCode, &qRes, attempts); err != nil { + return } - data, decodeErr := decode(res.Data) - if decodeErr != nil { - return nil, fmt.Errorf("failed to decode data: %w", decodeErr) + var data any + if data, err = decode(qRes.Data); err != nil { + err = fmt.Errorf("failed to decode data: %w", err) + return } - ret := &QuerySuccess{ - QueryInfo: newQueryInfo(&res), + qSus = &QuerySuccess{ + QueryInfo: newQueryInfo(&qRes), Data: data, - StaticType: res.StaticType, + StaticType: qRes.StaticType, } - ret.Stats.Attempts = attempts - - return ret, nil + qSus.Stats.Attempts = attempts + return } diff --git a/serializer.go b/serializer.go index a3b12e2..36cffed 100644 --- a/serializer.go +++ b/serializer.go @@ -469,7 +469,7 @@ func encode(v any, hint string) (any, error) { case time.Time: return encodeTime(vt, hint) - case fqlRequest: + case queryRequest: query, err := encode(vt.Query, hint) if err != nil { return nil, err