Skip to content

Commit

Permalink
ENG-6200: Separate pure http handling into apiRequest (#137)
Browse files Browse the repository at this point in the history
  • Loading branch information
erickpintor committed Apr 23, 2024
1 parent 7b6e37a commit 9da0711
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 67 deletions.
26 changes: 21 additions & 5 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"math/rand"
"net"
"net/http"
"net/url"
"os"
"strconv"
"strings"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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]
Expand Down
12 changes: 6 additions & 6 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
128 changes: 73 additions & 55 deletions request.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
2 changes: 1 addition & 1 deletion serializer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 9da0711

Please sign in to comment.