github.com/square/finch@v0.0.0-20240412205204-6530c03e2b96/proto/proto.go (about)

     1  // Copyright 2023 Block, Inc.
     2  
     3  package proto
     4  
     5  import (
     6  	"bytes"
     7  	"context"
     8  	"encoding/json"
     9  	"errors"
    10  	"io"
    11  	"log"
    12  	"net/http"
    13  	"net/url"
    14  	"strings"
    15  	"time"
    16  
    17  	"github.com/square/finch"
    18  )
    19  
    20  var ErrFailed = errors.New("request failed after attempts, or context cancelled")
    21  
    22  type R struct {
    23  	Timeout time.Duration
    24  	Wait    time.Duration
    25  	Tries   int
    26  }
    27  
    28  type Client struct {
    29  	name       string
    30  	serverAddr string
    31  	// --
    32  	client      *http.Client
    33  	StageId     string
    34  	PrintErrors bool
    35  }
    36  
    37  func NewClient(name, server string) *Client {
    38  	return &Client{
    39  		name:       name,
    40  		serverAddr: server,
    41  		// --
    42  		client: finch.MakeHTTPClient(),
    43  	}
    44  }
    45  
    46  func (c *Client) Get(ctx context.Context, endpoint string, params [][]string, r R) (*http.Response, []byte, error) {
    47  	return c.request(ctx, "GET", endpoint, params, nil, r)
    48  }
    49  
    50  func (c *Client) Send(ctx context.Context, endpoint string, data interface{}, r R) error {
    51  	_, _, err := c.request(ctx, "POST", endpoint, nil, data, r)
    52  	return err
    53  }
    54  
    55  func (c *Client) request(ctx context.Context, method string, endpoint string, params [][]string, data interface{}, r R) (*http.Response, []byte, error) {
    56  	url := c.URL(endpoint, params)
    57  	finch.Debug("%s %s", method, url)
    58  
    59  	buf := new(bytes.Buffer)
    60  	if data != nil {
    61  		json.NewEncoder(buf).Encode(data)
    62  	}
    63  
    64  	var err error
    65  	var body []byte
    66  	var req *http.Request
    67  	var resp *http.Response
    68  	try := 0
    69  	for r.Tries == -1 || try < r.Tries {
    70  		try += 1
    71  		ctxReq, cancelReq := context.WithTimeout(ctx, r.Timeout)
    72  		req, _ = http.NewRequestWithContext(ctxReq, method, url, buf)
    73  		resp, err = c.client.Do(req)
    74  		cancelReq()
    75  		if err != nil {
    76  			goto RETRY
    77  		}
    78  
    79  		body, err = io.ReadAll(resp.Body)
    80  		resp.Body.Close()
    81  		if err != nil {
    82  			return nil, nil, err
    83  		}
    84  
    85  		switch resp.StatusCode {
    86  		case http.StatusOK:
    87  			return resp, body, nil // success
    88  		case http.StatusResetContent:
    89  			return resp, nil, nil // reset
    90  		default:
    91  			goto RETRY
    92  		}
    93  
    94  	RETRY:
    95  		if ctx.Err() != nil {
    96  			return nil, nil, ctx.Err()
    97  		}
    98  		finch.Debug("%v", err)
    99  		if c.PrintErrors && try%20 == 0 {
   100  			log.Printf("Request error, retrying: %v", err)
   101  		}
   102  		time.Sleep(r.Wait)
   103  	}
   104  	return nil, nil, ErrFailed
   105  }
   106  
   107  func (c *Client) URL(path string, params [][]string) string {
   108  	// Every request requires 'name=...' to tell server this client's name.
   109  	// It's not a hostname, just a user-defined name for the remote compute instance.
   110  	u := c.serverAddr + path + "?name=" + url.QueryEscape(c.name)
   111  	n := len(params) + 1
   112  	escaped := make([]string, len(params)+1)
   113  	for i := range params {
   114  		escaped[i] = params[i][0] + "=" + url.QueryEscape(params[i][1])
   115  	}
   116  	escaped[n-1] = "stage-id=" + c.StageId
   117  	u += "&" + strings.Join(escaped, "&")
   118  	return u
   119  }