github.com/NVIDIA/aistore@v1.3.23-0.20240517131212-7df6609be51d/api/object.go (about)

     1  // Package api provides native Go-based API/SDK over HTTP(S).
     2  /*
     3   * Copyright (c) 2018-2024, NVIDIA CORPORATION. All rights reserved.
     4   */
     5  package api
     6  
     7  import (
     8  	"encoding/hex"
     9  	"fmt"
    10  	"io"
    11  	"net/http"
    12  	"net/textproto"
    13  	"net/url"
    14  	"strconv"
    15  	"time"
    16  
    17  	"github.com/NVIDIA/aistore/api/apc"
    18  	"github.com/NVIDIA/aistore/cmn"
    19  	"github.com/NVIDIA/aistore/cmn/cos"
    20  )
    21  
    22  const (
    23  	httpMaxRetries = 5                      // maximum number of retries for an HTTP request
    24  	httpRetrySleep = 100 * time.Millisecond // a sleep between HTTP request retries
    25  
    26  	// Sleep between HTTP retries for error[rate of change requests exceeds limit] - must be > 1s:
    27  	// From https://cloud.google.com/storage/quotas#objects
    28  	// * "There is an update limit on each object of once per second..."
    29  	httpRetryRateSleep = 1500 * time.Millisecond
    30  )
    31  
    32  // GET (object)
    33  type (
    34  	GetArgs struct {
    35  		// If not specified (or same: if `nil`), Writer defaults to `io.Discard`
    36  		// (in other words, with no writer the object that is being read will be discarded)
    37  		Writer io.Writer
    38  
    39  		// Currently, this (optional) Query field can (optionally) carry:
    40  		// - `apc.QparamETLName`: named ETL to transform the object (i.e., perform "inline transformation")
    41  		// - `apc.QparamOrigURL`: GET from a vanilla http(s) location (`ht://` bucket with the corresponding `OrigURLBck`)
    42  		// - `apc.QparamSilent`: do not log errors
    43  		// - `apc.QparamLatestVer`: get latest version from the associated Cloud bucket; see also: `ValidateWarmGet`
    44  		// - and a group of parameters used to read aistore-supported serialized archives ("shards"), namely:
    45  		//   - `apc.QparamArchpath`
    46  		//   - `apc.QparamArchmime`
    47  		//   - `apc.QparamArchregx`
    48  		//   - `apc.QparamArchmode`
    49  		Query url.Values
    50  
    51  		// The field is used to facilitate a) range read, and b) blob download
    52  		// E.g. range:
    53  		// * Header.Set(cos.HdrRange, fmt.Sprintf("bytes=%d-%d", fromOffset, toOffset))
    54  		//   For range formatting, see https://www.rfc-editor.org/rfc/rfc7233#section-2.1
    55  		// E.g. blob download:
    56  		// * Header.Set(apc.HdrBlobDownload, "true")
    57  		Header http.Header
    58  	}
    59  
    60  	// `ObjAttrs` represents object attributes and can be further used to retrieve
    61  	// the object's size, checksum, version, and other metadata.
    62  	//
    63  	// Note that while `GetObject()` and related GET APIs return `ObjAttrs`,
    64  	// `HeadObject()` API returns `cmn.ObjectProps` - a superset.
    65  	ObjAttrs struct {
    66  		wrespHeader http.Header
    67  		n           int64
    68  	}
    69  )
    70  
    71  // PUT, APPEND, PROMOTE (object)
    72  type (
    73  	NewRequestCB func(args *cmn.HreqArgs) (*http.Request, error)
    74  
    75  	PutArgs struct {
    76  		Reader cos.ReadOpenCloser
    77  
    78  		// optional; if provided:
    79  		// - if object exists: load the object's metadata, compare checksums - skip writing if equal
    80  		// - otherwise, compare the two checksums upon writing (aka, "end-to-end protection")
    81  		Cksum *cos.Cksum
    82  
    83  		BaseParams BaseParams
    84  
    85  		Bck     cmn.Bck
    86  		ObjName string
    87  
    88  		Size uint64 // optional
    89  
    90  		// Skip loading existing object's metadata in order to
    91  		// compare its Checksum and update its existing Version (if exists);
    92  		// can be used to reduce PUT latency when:
    93  		// - we massively write a new content into a bucket, and/or
    94  		// - we simply don't care.
    95  		SkipVC bool
    96  	}
    97  
    98  	// (see also: api.PutApndArchArgs)
    99  	AppendArgs struct {
   100  		Reader     cos.ReadOpenCloser
   101  		BaseParams BaseParams
   102  		Bck        cmn.Bck
   103  		Object     string
   104  		Handle     string
   105  		Size       int64
   106  	}
   107  	FlushArgs struct {
   108  		Cksum      *cos.Cksum
   109  		BaseParams BaseParams
   110  		Bck        cmn.Bck
   111  		Object     string
   112  		Handle     string
   113  	}
   114  )
   115  
   116  // Archive files and directories (see related: cmn.ArchiveBckMsg)
   117  type PutApndArchArgs struct {
   118  	ArchPath string // filename _in_ archive
   119  	Mime     string // user-specified mime type (NOTE: takes precedence if defined)
   120  	Flags    int64  // apc.ArchAppend and apc.ArchAppendIfExist (the former requires destination shard to exist)
   121  	PutArgs
   122  }
   123  
   124  /////////////
   125  // GetArgs //
   126  /////////////
   127  
   128  func (args *GetArgs) ret() (w io.Writer, q url.Values, hdr http.Header) {
   129  	w = io.Discard
   130  	if args == nil {
   131  		return
   132  	}
   133  	if args.Writer != nil {
   134  		w = args.Writer
   135  	}
   136  	q, hdr = args.Query, args.Header
   137  	return
   138  }
   139  
   140  //////////////
   141  // ObjAttrs //
   142  //////////////
   143  
   144  // most often used (convenience) method
   145  func (oah *ObjAttrs) Size() int64 {
   146  	if oah.n == 0 { // unlikely
   147  		oah.n = oah.Attrs().Size
   148  	}
   149  	return oah.n
   150  }
   151  
   152  func (oah *ObjAttrs) Attrs() (out cmn.ObjAttrs) {
   153  	out.Cksum = out.FromHeader(oah.wrespHeader)
   154  	return
   155  }
   156  
   157  // e.g. usage: range read response
   158  func (oah *ObjAttrs) RespHeader() http.Header {
   159  	return oah.wrespHeader
   160  }
   161  
   162  // If GetArgs.Writer is specified GetObject will use it to write the response body;
   163  // otherwise, it'll `io.Discard` the latter.
   164  //
   165  // `io.Copy` is used internally to copy response bytes from the request to the writer.
   166  //
   167  // Returns `ObjAttrs` that can be further used to get the size and other object metadata.
   168  func GetObject(bp BaseParams, bck cmn.Bck, objName string, args *GetArgs) (oah ObjAttrs, err error) {
   169  	var (
   170  		wresp     *wrappedResp
   171  		w, q, hdr = args.ret()
   172  	)
   173  	bp.Method = http.MethodGet
   174  	reqParams := AllocRp()
   175  	{
   176  		reqParams.BaseParams = bp
   177  		reqParams.Path = apc.URLPathObjects.Join(bck.Name, objName)
   178  		reqParams.Query = bck.NewQuery()
   179  		reqParams.Header = hdr
   180  	}
   181  	// copy qparams over, if any
   182  	for k, vs := range q {
   183  		var v string
   184  		if len(vs) > 0 {
   185  			v = vs[0]
   186  		}
   187  		reqParams.Query.Set(k, v)
   188  	}
   189  	wresp, err = reqParams.doWriter(w)
   190  	FreeRp(reqParams)
   191  	if err == nil {
   192  		oah.wrespHeader, oah.n = wresp.Header, wresp.n
   193  	}
   194  	return
   195  }
   196  
   197  // Same as above with checksum validation.
   198  //
   199  // Returns `cmn.ErrInvalidCksum` when the expected and actual checksum values
   200  // are different.
   201  func GetObjectWithValidation(bp BaseParams, bck cmn.Bck, objName string, args *GetArgs) (oah ObjAttrs, err error) {
   202  	w, q, hdr := args.ret()
   203  	bp.Method = http.MethodGet
   204  	reqParams := AllocRp()
   205  	{
   206  		reqParams.BaseParams = bp
   207  		reqParams.Path = apc.URLPathObjects.Join(bck.Name, objName)
   208  		reqParams.Query = bck.AddToQuery(q)
   209  		reqParams.Header = hdr
   210  	}
   211  
   212  	var (
   213  		resp  *http.Response
   214  		wresp *wrappedResp
   215  	)
   216  	resp, err = reqParams.do()
   217  	if err != nil {
   218  		return
   219  	}
   220  
   221  	wresp, err = reqParams.readValidate(resp, w)
   222  	cos.DrainReader(resp.Body)
   223  	resp.Body.Close()
   224  	FreeRp(reqParams)
   225  	if err == nil {
   226  		oah.wrespHeader, oah.n = wresp.Header, wresp.n
   227  	} else if err.Error() == errNilCksum {
   228  		err = fmt.Errorf("%s is not checksummed, cannot validate", bck.Cname(objName))
   229  	}
   230  	return
   231  }
   232  
   233  // GetObjectReader returns reader of the requested object. It does not read body
   234  // bytes, nor validates a checksum. Caller is responsible for closing the reader.
   235  func GetObjectReader(bp BaseParams, bck cmn.Bck, objName string, args *GetArgs) (r io.ReadCloser, size int64, err error) {
   236  	_, q, hdr := args.ret()
   237  	q = bck.AddToQuery(q)
   238  	bp.Method = http.MethodGet
   239  	reqParams := AllocRp()
   240  	{
   241  		reqParams.BaseParams = bp
   242  		reqParams.Path = apc.URLPathObjects.Join(bck.Name, objName)
   243  		reqParams.Query = q
   244  		reqParams.Header = hdr
   245  	}
   246  	r, size, err = reqParams.doReader()
   247  	FreeRp(reqParams)
   248  	return
   249  }
   250  
   251  /////////////
   252  // PutArgs //
   253  /////////////
   254  
   255  func (args *PutArgs) getBody() (io.ReadCloser, error) { return args.Reader.Open() }
   256  
   257  func (args *PutArgs) put(reqArgs *cmn.HreqArgs) (*http.Request, error) {
   258  	req, err := reqArgs.Req()
   259  	if err != nil {
   260  		return nil, newErrCreateHTTPRequest(err)
   261  	}
   262  	// Go http doesn't automatically set this for files, so to handle redirect we do it here.
   263  	req.GetBody = args.getBody
   264  	if args.Cksum != nil && args.Cksum.Ty() != cos.ChecksumNone {
   265  		req.Header.Set(apc.HdrObjCksumType, args.Cksum.Ty())
   266  		ckVal := args.Cksum.Value()
   267  		if ckVal == "" {
   268  			_, ckhash, err := cos.CopyAndChecksum(io.Discard, args.Reader, nil, args.Cksum.Ty())
   269  			if err != nil {
   270  				return nil, newErrCreateHTTPRequest(err)
   271  			}
   272  			ckVal = hex.EncodeToString(ckhash.Sum())
   273  		}
   274  		req.Header.Set(apc.HdrObjCksumVal, ckVal)
   275  	}
   276  	if args.Size != 0 {
   277  		req.ContentLength = int64(args.Size) // as per https://tools.ietf.org/html/rfc7230#section-3.3.2
   278  	}
   279  	SetAuxHeaders(req, &args.BaseParams)
   280  	return req, nil
   281  }
   282  
   283  ////////////////
   284  // AppendArgs //
   285  ////////////////
   286  
   287  func (args *AppendArgs) getBody() (io.ReadCloser, error) { return args.Reader.Open() }
   288  
   289  func (args *AppendArgs) _append(reqArgs *cmn.HreqArgs) (*http.Request, error) {
   290  	req, err := reqArgs.Req()
   291  	if err != nil {
   292  		return nil, newErrCreateHTTPRequest(err)
   293  	}
   294  	// The HTTP package doesn't automatically set this for files, so it has to be done manually
   295  	// If it wasn't set, we would need to deal with the redirect manually.
   296  	req.GetBody = args.getBody
   297  	if args.Size != 0 {
   298  		req.ContentLength = args.Size // as per https://tools.ietf.org/html/rfc7230#section-3.3.2
   299  	}
   300  	SetAuxHeaders(req, &args.BaseParams)
   301  	return req, nil
   302  }
   303  
   304  // HeadObject returns object properties; can be conventionally used to establish in-cluster presence.
   305  // - fltPresence:  as per QparamFltPresence enum (for values and comments, see api/apc/query.go)
   306  // - silent==true: not to log (not-found) error
   307  func HeadObject(bp BaseParams, bck cmn.Bck, objName string, fltPresence int, silent bool) (*cmn.ObjectProps, error) {
   308  	bp.Method = http.MethodHead
   309  
   310  	q := bck.NewQuery()
   311  	q.Set(apc.QparamFltPresence, strconv.Itoa(fltPresence))
   312  	if silent {
   313  		q.Set(apc.QparamSilent, "true")
   314  	}
   315  
   316  	reqParams := AllocRp()
   317  	defer FreeRp(reqParams)
   318  	{
   319  		reqParams.BaseParams = bp
   320  		reqParams.Path = apc.URLPathObjects.Join(bck.Name, objName)
   321  		reqParams.Query = q
   322  	}
   323  	hdr, _, err := reqParams.doReqHdr()
   324  	if err != nil {
   325  		return nil, err
   326  	}
   327  	if fltPresence == apc.FltPresentNoProps {
   328  		return nil, err
   329  	}
   330  
   331  	// first, cnm.ObjAttrs (NOTE: compare with `headObject()` in target.go)
   332  	op := &cmn.ObjectProps{}
   333  	op.Cksum = op.ObjAttrs.FromHeader(hdr)
   334  	// second, all the rest
   335  	err = cmn.IterFields(op, func(tag string, field cmn.IterField) (error, bool) {
   336  		headerName := apc.PropToHeader(tag)
   337  		// skip the missing ones
   338  		if _, ok := hdr[textproto.CanonicalMIMEHeaderKey(headerName)]; !ok {
   339  			return nil, false
   340  		}
   341  		// single-value
   342  		return field.SetValue(hdr.Get(headerName), true /*force*/), false
   343  	}, cmn.IterOpts{OnlyRead: false})
   344  	if err != nil {
   345  		return nil, err
   346  	}
   347  	return op, nil
   348  }
   349  
   350  // Given cos.StrKVs (map[string]string) keys and values, sets object's custom properties.
   351  // By default, adds new or updates existing custom keys.
   352  // Use `setNewCustomMDFlag` to _replace_ all existing keys with the specified (new) ones.
   353  // See also: HeadObject() and apc.HdrObjCustomMD
   354  func SetObjectCustomProps(bp BaseParams, bck cmn.Bck, objName string, custom cos.StrKVs, setNew bool) error {
   355  	var (
   356  		actMsg = apc.ActMsg{Value: custom}
   357  		q      url.Values
   358  	)
   359  	if setNew {
   360  		q = make(url.Values, 4)
   361  		q = bck.AddToQuery(q)
   362  		q.Set(apc.QparamNewCustom, "true")
   363  	} else {
   364  		q = bck.AddToQuery(q)
   365  	}
   366  	bp.Method = http.MethodPatch
   367  	reqParams := AllocRp()
   368  	{
   369  		reqParams.BaseParams = bp
   370  		reqParams.Path = apc.URLPathObjects.Join(bck.Name, objName)
   371  		reqParams.Body = cos.MustMarshal(actMsg)
   372  		reqParams.Header = http.Header{cos.HdrContentType: []string{cos.ContentJSON}}
   373  		reqParams.Query = q
   374  	}
   375  	err := reqParams.DoRequest()
   376  	FreeRp(reqParams)
   377  	return err
   378  }
   379  
   380  func DeleteObject(bp BaseParams, bck cmn.Bck, objName string) error {
   381  	bp.Method = http.MethodDelete
   382  	reqParams := AllocRp()
   383  	{
   384  		reqParams.BaseParams = bp
   385  		reqParams.Path = apc.URLPathObjects.Join(bck.Name, objName)
   386  		reqParams.Query = bck.NewQuery()
   387  	}
   388  	err := reqParams.DoRequest()
   389  	FreeRp(reqParams)
   390  	return err
   391  }
   392  
   393  func EvictObject(bp BaseParams, bck cmn.Bck, objName string) error {
   394  	bp.Method = http.MethodDelete
   395  	actMsg := apc.ActMsg{Action: apc.ActEvictObjects, Name: cos.JoinWords(bck.Name, objName)}
   396  	reqParams := AllocRp()
   397  	{
   398  		reqParams.BaseParams = bp
   399  		reqParams.Path = apc.URLPathObjects.Join(bck.Name, objName)
   400  		reqParams.Body = cos.MustMarshal(actMsg)
   401  		reqParams.Header = http.Header{cos.HdrContentType: []string{cos.ContentJSON}}
   402  		reqParams.Query = bck.NewQuery()
   403  	}
   404  	err := reqParams.DoRequest()
   405  	FreeRp(reqParams)
   406  	return err
   407  }
   408  
   409  // prefetch object - a convenience method added for "symmetry" with the evict (above)
   410  // - compare with api.PrefetchList and api.PrefetchRange
   411  func PrefetchObject(bp BaseParams, bck cmn.Bck, objName string) (string, error) {
   412  	var msg apc.PrefetchMsg
   413  	msg.ObjNames = []string{objName}
   414  	return Prefetch(bp, bck, msg)
   415  }
   416  
   417  // PutObject PUTs the specified reader (`args.Reader`) as a new object
   418  // (or a new version of the object) it in the specified bucket.
   419  //
   420  // Assumes that `args.Reader` is already opened and ready for usage.
   421  // Returns `ObjAttrs` that can be further used to get the size and other object metadata.
   422  func PutObject(args *PutArgs) (oah ObjAttrs, err error) {
   423  	var (
   424  		resp  *http.Response
   425  		query = args.Bck.NewQuery()
   426  	)
   427  	if args.SkipVC {
   428  		query.Set(apc.QparamSkipVC, "true")
   429  	}
   430  	reqArgs := cmn.AllocHra()
   431  	{
   432  		reqArgs.Method = http.MethodPut
   433  		reqArgs.Base = args.BaseParams.URL
   434  		reqArgs.Path = apc.URLPathObjects.Join(args.Bck.Name, args.ObjName)
   435  		reqArgs.Query = query
   436  		reqArgs.BodyR = args.Reader
   437  	}
   438  	resp, err = DoWithRetry(args.BaseParams.Client, args.put, reqArgs) //nolint:bodyclose // is closed inside
   439  	cmn.FreeHra(reqArgs)
   440  	if err == nil {
   441  		oah.wrespHeader = resp.Header
   442  	}
   443  	return
   444  }
   445  
   446  // Archive the content of a reader (`args.Reader` - e.g., an open file).
   447  // Destination, depending on the options, can be an existing (.tar, .tgz or .tar.gz, .zip, .tar.lz4)
   448  // formatted object (aka "shard") or a new one (or, a new version).
   449  // ---
   450  // For the updated list of supported archival formats -- aka MIME types -- see cmn/cos/archive.go.
   451  // --
   452  // See also:
   453  // - api.ArchiveMultiObj(msg.AppendIfExists = true)
   454  // - api.AppendObject
   455  func PutApndArch(args *PutApndArchArgs) (err error) {
   456  	q := make(url.Values, 4)
   457  	q = args.Bck.AddToQuery(q)
   458  	q.Set(apc.QparamArchpath, args.ArchPath)
   459  	q.Set(apc.QparamArchmime, args.Mime)
   460  
   461  	reqArgs := cmn.AllocHra()
   462  	{
   463  		reqArgs.Method = http.MethodPut
   464  		reqArgs.Base = args.BaseParams.URL
   465  		reqArgs.Path = apc.URLPathObjects.Join(args.Bck.Name, args.ObjName)
   466  		reqArgs.Query = q
   467  		reqArgs.BodyR = args.Reader
   468  	}
   469  	if args.Flags != 0 {
   470  		flags := strconv.FormatInt(args.Flags, 10)
   471  		reqArgs.Header = http.Header{apc.HdrPutApndArchFlags: []string{flags}}
   472  	}
   473  	putArgs := &args.PutArgs
   474  	_, err = DoWithRetry(args.BaseParams.Client, putArgs.put, reqArgs) //nolint:bodyclose // is closed inside
   475  	cmn.FreeHra(reqArgs)
   476  	return
   477  }
   478  
   479  // AppendObject adds a reader (`args.Reader` - e.g., an open file) to an object.
   480  // The API can be called multiple times - each call returns a handle
   481  // that may be used for subsequent append requests.
   482  // Once all the "appending" is done, the caller must call `api.FlushObject`
   483  // to finalize the object.
   484  // NOTE: object becomes visible and accessible only _after_ the call to `api.FlushObject`.
   485  func AppendObject(args *AppendArgs) (string /*handle*/, error) {
   486  	q := make(url.Values, 4)
   487  	q.Set(apc.QparamAppendType, apc.AppendOp)
   488  	q.Set(apc.QparamAppendHandle, args.Handle)
   489  	q = args.Bck.AddToQuery(q)
   490  
   491  	reqArgs := cmn.AllocHra()
   492  	{
   493  		reqArgs.Method = http.MethodPut
   494  		reqArgs.Base = args.BaseParams.URL
   495  		reqArgs.Path = apc.URLPathObjects.Join(args.Bck.Name, args.Object)
   496  		reqArgs.Query = q
   497  		reqArgs.BodyR = args.Reader
   498  	}
   499  	wresp, err := DoWithRetry(args.BaseParams.Client, args._append, reqArgs) //nolint:bodyclose // it's closed inside
   500  	cmn.FreeHra(reqArgs)
   501  	if err != nil {
   502  		return "", err
   503  	}
   504  	return wresp.Header.Get(apc.HdrAppendHandle), err
   505  }
   506  
   507  // FlushObject must be called after all the appends (via `api.AppendObject`).
   508  // To "flush", it uses the handle returned by `api.AppendObject`.
   509  // This call will create a fully operational and accessible object.
   510  func FlushObject(args *FlushArgs) error {
   511  	var (
   512  		header http.Header
   513  		q      = make(url.Values, 4)
   514  		method = args.BaseParams.Method
   515  	)
   516  	q.Set(apc.QparamAppendType, apc.FlushOp)
   517  	q.Set(apc.QparamAppendHandle, args.Handle)
   518  	q = args.Bck.AddToQuery(q)
   519  
   520  	if args.Cksum != nil && args.Cksum.Ty() != cos.ChecksumNone {
   521  		header = make(http.Header)
   522  		header.Set(apc.HdrObjCksumType, args.Cksum.Ty())
   523  		header.Set(apc.HdrObjCksumVal, args.Cksum.Val())
   524  	}
   525  	args.BaseParams.Method = http.MethodPut
   526  	reqParams := AllocRp()
   527  	{
   528  		reqParams.BaseParams = args.BaseParams
   529  		reqParams.Path = apc.URLPathObjects.Join(args.Bck.Name, args.Object)
   530  		reqParams.Query = q
   531  		reqParams.Header = header
   532  	}
   533  	err := reqParams.DoRequest()
   534  	FreeRp(reqParams)
   535  	args.BaseParams.Method = method
   536  	return err
   537  }
   538  
   539  // RenameObject renames object name from `oldName` to `newName`. Works only
   540  // across single, specified bucket.
   541  func RenameObject(bp BaseParams, bck cmn.Bck, oldName, newName string) error {
   542  	bp.Method = http.MethodPost
   543  	reqParams := AllocRp()
   544  	{
   545  		reqParams.BaseParams = bp
   546  		reqParams.Path = apc.URLPathObjects.Join(bck.Name, oldName)
   547  		reqParams.Body = cos.MustMarshal(apc.ActMsg{Action: apc.ActRenameObject, Name: newName})
   548  		reqParams.Header = http.Header{cos.HdrContentType: []string{cos.ContentJSON}}
   549  		reqParams.Query = bck.NewQuery()
   550  	}
   551  	err := reqParams.DoRequest()
   552  	FreeRp(reqParams)
   553  	return err
   554  }
   555  
   556  // promote files and directories to ais objects
   557  func Promote(bp BaseParams, bck cmn.Bck, args *apc.PromoteArgs) (xid string, err error) {
   558  	actMsg := apc.ActMsg{Action: apc.ActPromote, Name: args.SrcFQN, Value: args}
   559  	bp.Method = http.MethodPost
   560  	reqParams := AllocRp()
   561  	{
   562  		reqParams.BaseParams = bp
   563  		reqParams.Path = apc.URLPathObjects.Join(bck.Name)
   564  		reqParams.Body = cos.MustMarshal(actMsg)
   565  		reqParams.Header = http.Header{cos.HdrContentType: []string{cos.ContentJSON}}
   566  		reqParams.Query = bck.NewQuery()
   567  	}
   568  	_, err = reqParams.doReqStr(&xid)
   569  	FreeRp(reqParams)
   570  	return xid, err
   571  }
   572  
   573  // DoWithRetry executes `http-client.Do` and retries *retriable connection errors*,
   574  // such as "broken pipe" and "connection refused".
   575  // This function always closes the `reqArgs.BodR`, even in case of error.
   576  // Usage: PUT and simlar requests that transfer payload from the user side.
   577  // NOTE: always closes request body reader (reqArgs.BodyR) - explicitly or via Do()
   578  // TODO: refactor
   579  func DoWithRetry(client *http.Client, cb NewRequestCB, reqArgs *cmn.HreqArgs) (resp *http.Response, err error) {
   580  	var (
   581  		req    *http.Request
   582  		doErr  error
   583  		sleep  = httpRetrySleep
   584  		reader = reqArgs.BodyR.(cos.ReadOpenCloser)
   585  	)
   586  	// first time
   587  	if req, err = cb(reqArgs); err != nil {
   588  		cos.Close(reader)
   589  		return
   590  	}
   591  	resp, doErr = client.Do(req)
   592  	err = doErr
   593  	if !_retry(doErr, resp) {
   594  		goto exit
   595  	}
   596  	if resp != nil && resp.StatusCode == http.StatusTooManyRequests {
   597  		sleep = httpRetryRateSleep
   598  	}
   599  
   600  	// retry
   601  	for range httpMaxRetries {
   602  		var r io.ReadCloser
   603  		time.Sleep(sleep)
   604  		sleep += sleep / 2
   605  		if r, err = reader.Open(); err != nil {
   606  			_close(resp, doErr)
   607  			return
   608  		}
   609  		reqArgs.BodyR = r
   610  
   611  		if req, err = cb(reqArgs); err != nil {
   612  			cos.Close(r)
   613  			_close(resp, doErr)
   614  			return
   615  		}
   616  		_close(resp, doErr)
   617  		resp, doErr = client.Do(req)
   618  		err = doErr
   619  		if !_retry(doErr, resp) {
   620  			goto exit
   621  		}
   622  	}
   623  exit:
   624  	if err == nil {
   625  		reqParams := AllocRp()
   626  		err = reqParams.checkResp(resp)
   627  		cos.DrainReader(resp.Body)
   628  		FreeRp(reqParams)
   629  	}
   630  	_close(resp, doErr)
   631  	return
   632  }
   633  
   634  func _close(resp *http.Response, doErr error) {
   635  	if resp != nil && doErr == nil {
   636  		cos.Close(resp.Body)
   637  	}
   638  }
   639  
   640  func _retry(err error, resp *http.Response) bool {
   641  	if resp != nil && resp.StatusCode == http.StatusTooManyRequests {
   642  		return true
   643  	}
   644  	return err != nil && cos.IsRetriableConnErr(err)
   645  }