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 }