github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/tests/integration_tests/api_v2/request.go (about)

     1  // Copyright 2023 PingCAP, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package main
    15  
    16  import (
    17  	"bytes"
    18  	"context"
    19  	"encoding/json"
    20  	"errors"
    21  	"fmt"
    22  	"io"
    23  	"net/http"
    24  	"net/url"
    25  	"path"
    26  	"strings"
    27  	"time"
    28  
    29  	"github.com/pingcap/log"
    30  	"github.com/pingcap/tiflow/cdc/api/middleware"
    31  	"github.com/pingcap/tiflow/cdc/model"
    32  	cerrors "github.com/pingcap/tiflow/pkg/errors"
    33  	"github.com/pingcap/tiflow/pkg/httputil"
    34  	"github.com/pingcap/tiflow/pkg/retry"
    35  	"github.com/pingcap/tiflow/pkg/version"
    36  	"go.uber.org/zap"
    37  )
    38  
    39  const (
    40  	defaultBackoffBaseDelayInMs = 500
    41  	defaultBackoffMaxDelayInMs  = 30 * 1000
    42  	// The write operations other than 'GET' are not idempotent,
    43  	// so we only try one time in request.Do() and let users specify the retrying behaviour
    44  	defaultMaxRetries = 1
    45  )
    46  
    47  type HTTPMethod int
    48  
    49  const (
    50  	HTTPMethodPost = iota + 1
    51  	HTTPMethodPut
    52  	HTTPMethodGet
    53  	HTTPMethodDelete
    54  )
    55  
    56  // String implements Stringer.String.
    57  func (h HTTPMethod) String() string {
    58  	switch h {
    59  	case HTTPMethodPost:
    60  		return "POST"
    61  	case HTTPMethodPut:
    62  		return "PUT"
    63  	case HTTPMethodGet:
    64  		return "GET"
    65  	case HTTPMethodDelete:
    66  		return "DELETE"
    67  	default:
    68  		return "unknown"
    69  	}
    70  }
    71  
    72  // CDCRESTClient defines a TiCDC RESTful client
    73  type CDCRESTClient struct {
    74  	// base is the root URL for all invocations of the client.
    75  	base *url.URL
    76  
    77  	// versionedAPIPath is a http url prefix with api version. eg. /api/v1.
    78  	versionedAPIPath string
    79  
    80  	// Client is a wrapped http client.
    81  	Client *httputil.Client
    82  }
    83  
    84  // NewCDCRESTClient creates a new CDCRESTClient.
    85  func NewCDCRESTClient(baseURL *url.URL, versionedAPIPath string, client *httputil.Client) (*CDCRESTClient, error) {
    86  	if !strings.HasSuffix(baseURL.Path, "/") {
    87  		baseURL.Path += "/"
    88  	}
    89  	baseURL.RawQuery = ""
    90  	baseURL.Fragment = ""
    91  
    92  	return &CDCRESTClient{
    93  		base:             baseURL,
    94  		versionedAPIPath: versionedAPIPath,
    95  		Client:           client,
    96  	}, nil
    97  }
    98  
    99  // Method begins a request with a http method (GET, POST, PUT, DELETE).
   100  func (c *CDCRESTClient) Method(method HTTPMethod) *Request {
   101  	return NewRequest(c).WithMethod(method)
   102  }
   103  
   104  // Post begins a POST request. Short for c.Method(HTTPMethodPost).
   105  func (c *CDCRESTClient) Post() *Request {
   106  	return c.Method(HTTPMethodPost)
   107  }
   108  
   109  // Put begins a PUT request. Short for c.Method(HTTPMethodPut).
   110  func (c *CDCRESTClient) Put() *Request {
   111  	return c.Method(HTTPMethodPut)
   112  }
   113  
   114  // Delete begins a DELETE request. Short for c.Method(HTTPMethodDelete).
   115  func (c *CDCRESTClient) Delete() *Request {
   116  	return c.Method(HTTPMethodDelete)
   117  }
   118  
   119  // Get begins a GET request. Short for c.Method(HTTPMethodGet).
   120  func (c *CDCRESTClient) Get() *Request {
   121  	return c.Method(HTTPMethodGet)
   122  }
   123  
   124  // Request allows for building up a request to cdc server in a chained fasion.
   125  // Any errors are stored until the end of your call, so you only have to
   126  // check once.
   127  type Request struct {
   128  	c       *CDCRESTClient
   129  	timeout time.Duration
   130  
   131  	// generic components accessible via setters
   132  	method     HTTPMethod
   133  	pathPrefix string
   134  	params     url.Values
   135  	headers    http.Header
   136  
   137  	// retry options
   138  	backoffBaseDelay time.Duration
   139  	backoffMaxDelay  time.Duration
   140  	maxRetries       uint64
   141  
   142  	// output
   143  	err  error
   144  	body io.Reader
   145  }
   146  
   147  // NewRequest creates a new request.
   148  func NewRequest(c *CDCRESTClient) *Request {
   149  	var pathPrefix string
   150  	if c.base != nil {
   151  		pathPrefix = path.Join("/", c.base.Path, c.versionedAPIPath)
   152  	} else {
   153  		pathPrefix = path.Join("/", c.versionedAPIPath)
   154  	}
   155  
   156  	var timeout time.Duration
   157  	if c.Client != nil {
   158  		timeout = c.Client.Timeout()
   159  	}
   160  
   161  	r := &Request{
   162  		c:          c,
   163  		timeout:    timeout,
   164  		pathPrefix: pathPrefix,
   165  		maxRetries: 1,
   166  	}
   167  	r.WithHeader("Accept", "application/json")
   168  	r.WithHeader(middleware.ClientVersionHeader, version.ReleaseVersion)
   169  	return r
   170  }
   171  
   172  // NewRequestWithClient creates a Request with an embedded CDCRESTClient for test.
   173  func NewRequestWithClient(base *url.URL, versionedAPIPath string, client *httputil.Client) *Request {
   174  	return NewRequest(&CDCRESTClient{
   175  		base:             base,
   176  		versionedAPIPath: versionedAPIPath,
   177  		Client:           client,
   178  	})
   179  }
   180  
   181  // WithPrefix adds segments to the beginning of request url.
   182  func (r *Request) WithPrefix(segments ...string) *Request {
   183  	if r.err != nil {
   184  		return r
   185  	}
   186  
   187  	r.pathPrefix = path.Join(r.pathPrefix, path.Join(segments...))
   188  	return r
   189  }
   190  
   191  // WithURI sets the server relative URI.
   192  func (r *Request) WithURI(uri string) *Request {
   193  	if r.err != nil {
   194  		return r
   195  	}
   196  	u, err := url.Parse(uri)
   197  	if err != nil {
   198  		r.err = err
   199  		return r
   200  	}
   201  	r.pathPrefix = path.Join(r.pathPrefix, u.Path)
   202  	vals := u.Query()
   203  	if len(vals) > 0 {
   204  		if r.params == nil {
   205  			r.params = make(url.Values)
   206  		}
   207  		for k, v := range vals {
   208  			r.params[k] = v
   209  		}
   210  	}
   211  	return r
   212  }
   213  
   214  // WithParam sets the http request query params.
   215  func (r *Request) WithParam(name, value string) *Request {
   216  	if r.err != nil {
   217  		return r
   218  	}
   219  	if r.params == nil {
   220  		r.params = make(url.Values)
   221  	}
   222  	r.params[name] = append(r.params[name], value)
   223  	return r
   224  }
   225  
   226  // WithMethod sets the method this request will use.
   227  func (r *Request) WithMethod(method HTTPMethod) *Request {
   228  	r.method = method
   229  	return r
   230  }
   231  
   232  // WithHeader set the http request header.
   233  func (r *Request) WithHeader(key string, values ...string) *Request {
   234  	if r.headers == nil {
   235  		r.headers = http.Header{}
   236  	}
   237  	r.headers.Del(key)
   238  	for _, value := range values {
   239  		r.headers.Add(key, value)
   240  	}
   241  	return r
   242  }
   243  
   244  // WithTimeout specifies overall timeout of a request.
   245  func (r *Request) WithTimeout(d time.Duration) *Request {
   246  	if r.err != nil {
   247  		return r
   248  	}
   249  	r.timeout = d
   250  	return r
   251  }
   252  
   253  // WithBackoffBaseDelay specifies the base backoff sleep duration.
   254  func (r *Request) WithBackoffBaseDelay(delay time.Duration) *Request {
   255  	if r.err != nil {
   256  		return r
   257  	}
   258  	r.backoffBaseDelay = delay
   259  	return r
   260  }
   261  
   262  // WithBackoffMaxDelay specifies the maximum backoff sleep duration.
   263  func (r *Request) WithBackoffMaxDelay(delay time.Duration) *Request {
   264  	if r.err != nil {
   265  		return r
   266  	}
   267  	r.backoffMaxDelay = delay
   268  	return r
   269  }
   270  
   271  // WithMaxRetries specifies the maximum times a request will retry.
   272  func (r *Request) WithMaxRetries(maxRetries uint64) *Request {
   273  	if r.err != nil {
   274  		return r
   275  	}
   276  	if maxRetries > 0 {
   277  		r.maxRetries = maxRetries
   278  	} else {
   279  		r.maxRetries = defaultMaxRetries
   280  	}
   281  	return r
   282  }
   283  
   284  // WithBody makes http request use obj as its body.
   285  // only supports two types now:
   286  //  1. io.Reader
   287  //  2. type which can be json marshalled
   288  func (r *Request) WithBody(obj interface{}) *Request {
   289  	if r.err != nil {
   290  		return r
   291  	}
   292  
   293  	if rd, ok := obj.(io.Reader); ok {
   294  		r.body = rd
   295  	} else {
   296  		b, err := json.Marshal(obj)
   297  		if err != nil {
   298  			r.err = err
   299  			return r
   300  		}
   301  		r.body = bytes.NewReader(b)
   302  		r.WithHeader("Content-Type", "application/json")
   303  	}
   304  	return r
   305  }
   306  
   307  // URL returns the current working URL.
   308  func (r *Request) URL() *url.URL {
   309  	p := r.pathPrefix
   310  
   311  	finalURL := &url.URL{}
   312  	if r.c.base != nil {
   313  		*finalURL = *r.c.base
   314  	}
   315  	finalURL.Path = p
   316  
   317  	query := url.Values{}
   318  	for key, values := range r.params {
   319  		for _, value := range values {
   320  			query.Add(key, value)
   321  		}
   322  	}
   323  
   324  	finalURL.RawQuery = query.Encode()
   325  	return finalURL
   326  }
   327  
   328  func (r *Request) newHTTPRequest(ctx context.Context) (*http.Request, error) {
   329  	url := r.URL().String()
   330  	req, err := http.NewRequest(r.method.String(), url, r.body)
   331  	if err != nil {
   332  		return nil, err
   333  	}
   334  	req = req.WithContext(ctx)
   335  	req.Header = r.headers
   336  	return req, nil
   337  }
   338  
   339  // Do formats and executes the request.
   340  func (r *Request) Do(ctx context.Context) (res *Result) {
   341  	if r.err != nil {
   342  		log.Info("error in request", zap.Error(r.err))
   343  		return &Result{err: r.err}
   344  	}
   345  
   346  	client := r.c.Client
   347  	if client == nil {
   348  		client = &httputil.Client{}
   349  	}
   350  
   351  	if r.timeout > 0 {
   352  		var cancel context.CancelFunc
   353  		ctx, cancel = context.WithTimeout(ctx, r.timeout)
   354  		defer cancel()
   355  	}
   356  
   357  	baseDelay := r.backoffBaseDelay.Milliseconds()
   358  	if baseDelay == 0 {
   359  		baseDelay = defaultBackoffBaseDelayInMs
   360  	}
   361  	maxDelay := r.backoffMaxDelay.Milliseconds()
   362  	if maxDelay == 0 {
   363  		maxDelay = defaultBackoffMaxDelayInMs
   364  	}
   365  	maxRetries := r.maxRetries
   366  	if maxRetries <= 0 {
   367  		maxRetries = defaultMaxRetries
   368  	}
   369  
   370  	fn := func() error {
   371  		req, err := r.newHTTPRequest(ctx)
   372  		if err != nil {
   373  			return err
   374  		}
   375  		// rewind the request body when r.body is not nil
   376  		if seeker, ok := r.body.(io.Seeker); ok && r.body != nil {
   377  			if _, err := seeker.Seek(0, 0); err != nil {
   378  				return cerrors.ErrRewindRequestBodyError
   379  			}
   380  		}
   381  
   382  		resp, err := client.Do(req)
   383  		if err != nil {
   384  			log.Error("failed to send a http request", zap.Error(err))
   385  			return err
   386  		}
   387  
   388  		defer func() {
   389  			if resp == nil {
   390  				return
   391  			}
   392  			// close the body to let the TCP connection be reused after reconnecting
   393  			// see https://github.com/golang/go/blob/go1.18.1/src/net/http/response.go#L62-L64
   394  			_, _ = io.Copy(io.Discard, resp.Body)
   395  			resp.Body.Close()
   396  		}()
   397  
   398  		res = r.checkResponse(resp)
   399  		if res.Error() != nil {
   400  			return res.Error()
   401  		}
   402  		return nil
   403  	}
   404  
   405  	var err error
   406  	if maxRetries > 1 {
   407  		err = retry.Do(ctx, fn,
   408  			retry.WithBackoffBaseDelay(baseDelay),
   409  			retry.WithBackoffMaxDelay(maxDelay),
   410  			retry.WithMaxTries(maxRetries),
   411  			retry.WithIsRetryableErr(cerrors.IsRetryableError),
   412  		)
   413  	} else {
   414  		err = fn()
   415  	}
   416  
   417  	if res == nil && err != nil {
   418  		return &Result{err: err}
   419  	}
   420  
   421  	return
   422  }
   423  
   424  // check http response and unmarshal error message if necessary.
   425  func (r *Request) checkResponse(resp *http.Response) *Result {
   426  	var body []byte
   427  	if resp.Body != nil {
   428  		data, err := io.ReadAll(resp.Body)
   429  		if err != nil {
   430  			return &Result{err: err}
   431  		}
   432  		body = data
   433  	}
   434  
   435  	contentType := resp.Header.Get("Content-Type")
   436  	if resp.StatusCode < http.StatusOK || resp.StatusCode > http.StatusPartialContent {
   437  		var jsonErr model.HTTPError
   438  		err := json.Unmarshal(body, &jsonErr)
   439  		if err == nil {
   440  			err = errors.New(jsonErr.Error)
   441  		} else {
   442  			err = fmt.Errorf(
   443  				"call cdc api failed, url=%s, "+
   444  					"code=%d, contentType=%s, response=%s",
   445  				r.URL().String(),
   446  				resp.StatusCode, contentType, string(body))
   447  		}
   448  
   449  		return &Result{
   450  			body:        body,
   451  			contentType: contentType,
   452  			statusCode:  resp.StatusCode,
   453  			err:         err,
   454  		}
   455  	}
   456  
   457  	return &Result{
   458  		body:        body,
   459  		contentType: contentType,
   460  		statusCode:  resp.StatusCode,
   461  	}
   462  }
   463  
   464  // Result contains the result of calling Request.Do().
   465  type Result struct {
   466  	body        []byte
   467  	contentType string
   468  	err         error
   469  	statusCode  int
   470  }
   471  
   472  // Raw returns the raw result.
   473  func (r Result) Raw() ([]byte, error) {
   474  	return r.body, r.err
   475  }
   476  
   477  // Error returns the request error.
   478  func (r Result) Error() error {
   479  	return r.err
   480  }
   481  
   482  // Into stores the http response body into obj.
   483  func (r Result) Into(obj interface{}) error {
   484  	if r.err != nil {
   485  		return r.err
   486  	}
   487  
   488  	if len(r.body) == 0 {
   489  		return cerrors.ErrZeroLengthResponseBody.GenWithStackByArgs(r.statusCode)
   490  	}
   491  
   492  	return json.Unmarshal(r.body, obj)
   493  }