github.com/0chain/gosdk@v1.17.11/core/resty/resty.go (about)

     1  package resty
     2  
     3  import (
     4  	"context"
     5  	"io"
     6  	"io/ioutil"
     7  	"net"
     8  	"net/http"
     9  	"sync"
    10  	"time"
    11  
    12  	"github.com/0chain/gosdk/core/sys"
    13  )
    14  
    15  func clone(m map[string]string) map[string]string {
    16  	cl := make(map[string]string, len(m))
    17  	for k, v := range m {
    18  		cl[k] = v
    19  	}
    20  
    21  	return cl
    22  }
    23  
    24  // New create a Resty instance.
    25  func New(opts ...Option) *Resty {
    26  	r := &Resty{
    27  		// Default timeout to use for HTTP requests when either the parent context doesn't have a timeout set
    28  		// or the context's timeout is longer than DefaultRequestTimeout.
    29  		timeout: DefaultRequestTimeout,
    30  		retry:   DefaultRetry,
    31  		header:  clone(DefaultHeader),
    32  	}
    33  
    34  	for _, option := range opts {
    35  		option(r)
    36  	}
    37  
    38  	if r.transport == nil {
    39  		if DefaultTransport == nil {
    40  			DefaultTransport = &http.Transport{
    41  				Dial: (&net.Dialer{
    42  					Timeout: DefaultDialTimeout,
    43  				}).Dial,
    44  				TLSHandshakeTimeout: DefaultDialTimeout,
    45  			}
    46  		}
    47  		r.transport = DefaultTransport
    48  	}
    49  
    50  	if r.client == nil {
    51  		r.client = CreateClient(r.transport, r.timeout)
    52  	}
    53  
    54  	return r
    55  }
    56  
    57  // Client http client
    58  type Client interface {
    59  	Do(req *http.Request) (*http.Response, error)
    60  }
    61  
    62  // Handle handler of http response
    63  type Handle func(req *http.Request, resp *http.Response, respBody []byte, cf context.CancelFunc, err error) error
    64  
    65  // Option set restry option
    66  type Option func(*Resty)
    67  
    68  // Resty HTTP and REST client library with parallel feature
    69  type Resty struct {
    70  	ctx        context.Context
    71  	cancelFunc context.CancelFunc
    72  	qty        int
    73  	done       chan Result
    74  
    75  	transport          *http.Transport
    76  	client             Client
    77  	handle             Handle
    78  	requestInterceptor func(req *http.Request) error
    79  
    80  	timeout time.Duration
    81  	retry   int
    82  	header  map[string]string
    83  }
    84  
    85  // Then is used to call the handle function when the request has completed processing
    86  func (r *Resty) Then(fn Handle) *Resty {
    87  	if r == nil {
    88  		return r
    89  	}
    90  	r.handle = fn
    91  	return r
    92  }
    93  
    94  // DoGet executes http requests with GET method in parallel
    95  func (r *Resty) DoGet(ctx context.Context, urls ...string) *Resty {
    96  	return r.Do(ctx, http.MethodGet, nil, urls...)
    97  }
    98  
    99  // DoPost executes http requests with POST method in parallel
   100  func (r *Resty) DoPost(ctx context.Context, body io.Reader, urls ...string) *Resty {
   101  	return r.Do(ctx, http.MethodPost, body, urls...)
   102  }
   103  
   104  // DoPut executes http requests with PUT method in parallel
   105  func (r *Resty) DoPut(ctx context.Context, body io.Reader, urls ...string) *Resty {
   106  	return r.Do(ctx, http.MethodPut, body, urls...)
   107  }
   108  
   109  // DoDelete executes http requests with DELETE method in parallel
   110  func (r *Resty) DoDelete(ctx context.Context, urls ...string) *Resty {
   111  	return r.Do(ctx, http.MethodDelete, nil, urls...)
   112  }
   113  
   114  func (r *Resty) Do(ctx context.Context, method string, body io.Reader, urls ...string) *Resty {
   115  	r.ctx, r.cancelFunc = context.WithCancel(ctx)
   116  
   117  	r.qty = len(urls)
   118  	r.done = make(chan Result, r.qty)
   119  
   120  	bodyReader := body
   121  
   122  	for _, url := range urls {
   123  		go func(url string) {
   124  			req, err := http.NewRequest(method, url, bodyReader)
   125  			if err != nil {
   126  				r.done <- Result{Request: req, Response: nil, Err: err}
   127  				return
   128  			}
   129  			for key, value := range r.header {
   130  				req.Header.Set(key, value)
   131  			}
   132  			// re-use http connection if it is possible
   133  			req.Header.Set("Connection", "keep-alive")
   134  
   135  			if r.requestInterceptor != nil {
   136  				if err := r.requestInterceptor(req); err != nil {
   137  					r.done <- Result{Request: req, Response: nil, Err: err}
   138  					return
   139  				}
   140  			}
   141  			r.httpDo(req.WithContext(r.ctx))
   142  		}(url)
   143  	}
   144  	return r
   145  }
   146  
   147  func (r *Resty) httpDo(req *http.Request) {
   148  	wg := &sync.WaitGroup{}
   149  	wg.Add(1)
   150  
   151  	go func(request *http.Request) {
   152  		defer wg.Done()
   153  		var resp *http.Response
   154  		var err error
   155  		if r.retry > 0 {
   156  			for i := 1; ; i++ {
   157  				var bodyCopy io.ReadCloser
   158  				if (request.Method == http.MethodPost || request.Method == http.MethodPut) && request.Body != nil {
   159  					// clone io.ReadCloser to fix retry issue https://github.com/golang/go/issues/36095
   160  					bodyCopy, _ = request.GetBody() //nolint: errcheck
   161  				}
   162  
   163  				resp, err = r.client.Do(request)
   164  				//success: 200,201,202,204
   165  				if resp != nil && (resp.StatusCode == http.StatusOK ||
   166  					resp.StatusCode == http.StatusCreated ||
   167  					resp.StatusCode == http.StatusAccepted ||
   168  					resp.StatusCode == http.StatusNoContent) {
   169  					break
   170  				}
   171  				// close body ReadClose to release resource before retrying it
   172  				if resp != nil && resp.Body != nil {
   173  					// don't close it if it is latest retry
   174  					if i < r.retry {
   175  						resp.Body.Close()
   176  					}
   177  				}
   178  
   179  				if i == r.retry {
   180  					break
   181  				}
   182  
   183  				if resp != nil && resp.StatusCode == http.StatusTooManyRequests {
   184  					sys.Sleep(1 * time.Second)
   185  				}
   186  
   187  				if (request.Method == http.MethodPost || request.Method == http.MethodPut) && request.Body != nil {
   188  					request.Body = bodyCopy
   189  				}
   190  			}
   191  		} else {
   192  			resp, err = r.client.Do(request.WithContext(r.ctx))
   193  		}
   194  
   195  		result := Result{Request: request, Response: resp, Err: err}
   196  		if resp != nil {
   197  			// read and close body to reuse http connection
   198  			buf, err := ioutil.ReadAll(resp.Body)
   199  			if err != nil {
   200  				result.Err = err
   201  			} else {
   202  				resp.Body.Close() //nolint: errcheck
   203  				result.ResponseBody = buf
   204  			}
   205  		}
   206  		r.done <- result
   207  	}(req)
   208  	wg.Wait()
   209  }
   210  
   211  // Wait waits for all the requests to be completed
   212  func (r *Resty) Wait() []error {
   213  	defer func() {
   214  		// call cancelFunc, to avoid to memory leaks
   215  		if r.cancelFunc != nil {
   216  			r.cancelFunc()
   217  		}
   218  	}()
   219  	errs := make([]error, 0, r.qty)
   220  	done := 0
   221  
   222  	// no urls
   223  	if r.qty == 0 {
   224  		return errs
   225  	}
   226  	for {
   227  		select {
   228  		case <-r.ctx.Done():
   229  			if r.ctx.Err() == context.DeadlineExceeded {
   230  				return []error{r.ctx.Err()}
   231  			}
   232  			return errs
   233  
   234  		case result := <-r.done:
   235  			if r.handle != nil {
   236  				err := r.handle(result.Request, result.Response, result.ResponseBody, r.cancelFunc, result.Err)
   237  				if err != nil {
   238  					errs = append(errs, err)
   239  				} else if result.Err != nil {
   240  					// if r.handle doesn't return any error, then append result.Err if it is not nil
   241  					errs = append(errs, result.Err)
   242  				}
   243  			} else {
   244  				if result.Err != nil {
   245  					errs = append(errs, result.Err)
   246  				}
   247  			}
   248  		}
   249  		done++
   250  		if done >= r.qty {
   251  			return errs
   252  		}
   253  	}
   254  }
   255  
   256  // First successful result or errors
   257  func (r *Resty) First() []error {
   258  	defer func() {
   259  		// call cancelFunc, avoid to memory leak issue
   260  		if r.cancelFunc != nil {
   261  			r.cancelFunc()
   262  		}
   263  	}()
   264  
   265  	errs := make([]error, 0, r.qty)
   266  	done := 0
   267  
   268  	// no urls
   269  	if r.qty == 0 {
   270  		return errs
   271  	}
   272  
   273  	for {
   274  		select {
   275  		case <-r.ctx.Done():
   276  			return errs
   277  
   278  		case result := <-r.done:
   279  
   280  			if r.handle != nil {
   281  				err := r.handle(result.Request, result.Response, result.ResponseBody, r.cancelFunc, result.Err)
   282  
   283  				if err != nil {
   284  					errs = append(errs, err)
   285  				} else {
   286  					return nil
   287  				}
   288  			} else {
   289  				if result.Err != nil {
   290  					errs = append(errs, result.Err)
   291  				} else {
   292  					return nil
   293  				}
   294  
   295  			}
   296  		}
   297  
   298  		done++
   299  
   300  		if done >= r.qty {
   301  			return errs
   302  		}
   303  
   304  	}
   305  }