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

     1  // Package cmn provides common constants, types, and utilities for AIS clients
     2  // and AIStore.
     3  /*
     4   * Copyright (c) 2018-2024, NVIDIA CORPORATION. All rights reserved.
     5   */
     6  package cmn
     7  
     8  import (
     9  	"bytes"
    10  	"context"
    11  	"fmt"
    12  	"io"
    13  	"net/http"
    14  	"net/url"
    15  	"os"
    16  	"path/filepath"
    17  	"runtime"
    18  	"strings"
    19  	"sync"
    20  	"time"
    21  
    22  	"github.com/NVIDIA/aistore/api/apc"
    23  	"github.com/NVIDIA/aistore/cmn/cos"
    24  	"github.com/NVIDIA/aistore/cmn/debug"
    25  	"github.com/NVIDIA/aistore/cmn/nlog"
    26  	"github.com/NVIDIA/aistore/sys"
    27  	jsoniter "github.com/json-iterator/go"
    28  )
    29  
    30  const (
    31  	RetryLogVerbose = iota
    32  	RetryLogQuiet
    33  	RetryLogOff
    34  )
    35  
    36  type (
    37  	// usage 1: initialize and fill out HTTP request.
    38  	// usage 2: intra-cluster control-plane (except streams)
    39  	// usage 3: PUT and APPEND API
    40  	// BodyR optimizes-out allocations - if non-nil and implements `io.Closer`, will always be closed by `client.Do`
    41  	HreqArgs struct {
    42  		BodyR    io.Reader
    43  		Header   http.Header // request headers
    44  		Query    url.Values  // query, e.g. ?a=x&b=y&c=z
    45  		RawQuery string      // raw query
    46  		Method   string
    47  		Base     string // base URL, e.g. http://xyz.abc
    48  		Path     string // path URL, e.g. /x/y/z
    49  		Body     []byte
    50  	}
    51  
    52  	RetryArgs struct {
    53  		Call    func() (int, error)
    54  		IsFatal func(error) bool
    55  
    56  		Action string
    57  		Caller string
    58  
    59  		SoftErr uint // How many retires on ConnectionRefused or ConnectionReset error.
    60  		HardErr uint // How many retries on any other error.
    61  		Sleep   time.Duration
    62  
    63  		Verbosity int  // Determine the verbosity level.
    64  		BackOff   bool // If requests should be retried less and less often.
    65  		IsClient  bool // true: client (e.g. dev tools, etc.)
    66  	}
    67  )
    68  
    69  // PrependProtocol prepends protocol in URL in case it is missing.
    70  // By default it adds `http://` to the URL.
    71  func PrependProtocol(url string, protocol ...string) string {
    72  	if url == "" || strings.Contains(url, "://") {
    73  		return url
    74  	}
    75  	proto := "http"
    76  	if len(protocol) == 1 {
    77  		proto = protocol[0]
    78  	}
    79  	return proto + "://" + url // rfc2396.txt
    80  }
    81  
    82  // Ref: https://www.rfc-editor.org/rfc/rfc7233#section-2.1
    83  // (compare w/ htrange.contentRange)
    84  func MakeRangeHdr(start, length int64) string {
    85  	debug.Assert(start != 0 || length != 0)
    86  	return fmt.Sprintf("%s%d-%d", cos.HdrRangeValPrefix, start, start+length-1)
    87  }
    88  
    89  // ParseURL splits URL path at "/" and matches resulting items against the specified, if any.
    90  // - splitAfter == true:  strings.Split() the entire path;
    91  // - splitAfter == false: strings.SplitN(len(itemsPresent)+itemsAfter)
    92  // Returns all items that follow the specified `items`.
    93  func ParseURL(path string, itemsPresent []string, itemsAfter int, splitAfter bool) ([]string, error) {
    94  	var (
    95  		split []string
    96  		l     = len(itemsPresent)
    97  	)
    98  	if path != "" && path[0] == '/' {
    99  		path = path[1:] // remove leading slash
   100  	}
   101  	if splitAfter {
   102  		split = strings.Split(path, "/")
   103  	} else {
   104  		split = strings.SplitN(path, "/", l+max(1, itemsAfter))
   105  	}
   106  
   107  	apiItems := split[:0] // filtering without allocation
   108  	for _, item := range split {
   109  		if item != "" { // omit empty
   110  			apiItems = append(apiItems, item)
   111  		}
   112  	}
   113  	if len(apiItems) < l {
   114  		return nil, fmt.Errorf("invalid URL '%s': expected %d items, got %d", path, l, len(apiItems))
   115  	}
   116  	for idx, item := range itemsPresent {
   117  		if item != apiItems[idx] {
   118  			return nil, fmt.Errorf("invalid URL '%s': expected '%s', got '%s'", path, item, apiItems[idx])
   119  		}
   120  	}
   121  
   122  	apiItems = apiItems[l:]
   123  	if len(apiItems) < itemsAfter {
   124  		return nil, fmt.Errorf("URL '%s' is too short: expected %d items, got %d", path, itemsAfter+l, len(apiItems)+l)
   125  	}
   126  	if len(apiItems) > itemsAfter && !splitAfter {
   127  		return nil, fmt.Errorf("URL '%s' is too long: expected %d items, got %d", path, itemsAfter+l, len(apiItems)+l)
   128  	}
   129  	return apiItems, nil
   130  }
   131  
   132  func ReadBytes(r *http.Request) (b []byte, err error) {
   133  	var e error
   134  
   135  	b, e = io.ReadAll(r.Body)
   136  	if e != nil {
   137  		err = fmt.Errorf("failed to read %s request, err: %v", r.Method, e)
   138  		if e == io.EOF {
   139  			trailer := r.Trailer.Get("Error")
   140  			if trailer != "" {
   141  				err = fmt.Errorf("failed to read %s request, err: %v, trailer: %s", r.Method, e, trailer)
   142  			}
   143  		}
   144  	}
   145  	cos.Close(r.Body)
   146  
   147  	return b, err
   148  }
   149  
   150  func ReadJSON(w http.ResponseWriter, r *http.Request, out any) (err error) {
   151  	err = jsoniter.NewDecoder(r.Body).Decode(out)
   152  	cos.Close(r.Body)
   153  	if err == nil {
   154  		return
   155  	}
   156  	return WriteErrJSON(w, r, out, err)
   157  }
   158  
   159  func WriteErrJSON(w http.ResponseWriter, r *http.Request, out any, err error) error {
   160  	at := thisNodeName
   161  	if thisNodeName == "" {
   162  		at = r.URL.Path
   163  	}
   164  	err = fmt.Errorf(FmtErrUnmarshal, at, fmt.Sprintf("[%T]", out), r.Method, err)
   165  	if _, file, line, ok := runtime.Caller(2); ok {
   166  		f := filepath.Base(file)
   167  		err = fmt.Errorf("%v (%s, #%d)", err, f, line)
   168  	}
   169  	WriteErr(w, r, err)
   170  	return err
   171  }
   172  
   173  // Copies headers from original request(from client) to
   174  // a new one(inter-cluster call)
   175  func copyHeaders(src http.Header, dst *http.Header) {
   176  	for k, values := range src {
   177  		for _, v := range values {
   178  			dst.Set(k, v)
   179  		}
   180  	}
   181  }
   182  
   183  func NetworkCallWithRetry(args *RetryArgs) (err error) {
   184  	var (
   185  		hardErrCnt, softErrCnt, iter uint
   186  		status                       int
   187  		nonEmptyErr                  error
   188  		callerStr                    string
   189  		sleep                        = args.Sleep
   190  	)
   191  	if args.Sleep == 0 {
   192  		if args.IsClient {
   193  			args.Sleep = time.Second / 2
   194  		} else {
   195  			args.Sleep = Rom.CplaneOperation() / 4
   196  		}
   197  	}
   198  	if args.Caller != "" {
   199  		callerStr = args.Caller + ": "
   200  	}
   201  	if args.Action == "" {
   202  		args.Action = "call"
   203  	}
   204  	for hardErrCnt, softErrCnt, iter = uint(0), uint(0), uint(1); ; iter++ {
   205  		if status, err = args.Call(); err == nil {
   206  			if args.Verbosity == RetryLogVerbose && (hardErrCnt > 0 || softErrCnt > 0) {
   207  				nlog.Warningf("%s Successful %s after (soft/hard errors: %d/%d, last: %v)",
   208  					callerStr, args.Action, softErrCnt, hardErrCnt, nonEmptyErr)
   209  			}
   210  			return
   211  		}
   212  		// handle
   213  		nonEmptyErr = err
   214  		if args.IsFatal != nil && args.IsFatal(err) {
   215  			return
   216  		}
   217  		if args.Verbosity == RetryLogVerbose {
   218  			nlog.Errorf("%s Failed to %s, iter %d, err: %v(%d)", callerStr, args.Action, iter, err, status)
   219  		}
   220  		if cos.IsRetriableConnErr(err) {
   221  			softErrCnt++
   222  		} else {
   223  			hardErrCnt++
   224  		}
   225  		if args.BackOff && iter > 1 {
   226  			if args.IsClient {
   227  				sleep = min(sleep+(args.Sleep/2), 4*time.Second)
   228  			} else {
   229  				sleep = min(sleep+(args.Sleep/2), Rom.MaxKeepalive())
   230  			}
   231  		}
   232  		if hardErrCnt > args.HardErr || softErrCnt > args.SoftErr {
   233  			break
   234  		}
   235  		time.Sleep(sleep)
   236  	}
   237  	// Quiet: print once the summary (Verbose: no need)
   238  	if args.Verbosity == RetryLogQuiet {
   239  		nlog.Errorf("%sFailed to %s (soft/hard errors: %d/%d, last: %v)",
   240  			callerStr, args.Action, softErrCnt, hardErrCnt, err)
   241  	}
   242  	return
   243  }
   244  
   245  func ParseReadHeaderTimeout() (_ time.Duration, isSet bool) {
   246  	val := os.Getenv(apc.EnvReadHeaderTimeout)
   247  	if val == "" {
   248  		return 0, false
   249  	}
   250  	timeout, err := time.ParseDuration(val)
   251  	if err != nil {
   252  		nlog.Errorf("invalid env '%s = %s': %v - ignoring, proceeding with default = %v",
   253  			apc.EnvReadHeaderTimeout, val, err, apc.ReadHeaderTimeout)
   254  		return 0, false
   255  	}
   256  	return timeout, true
   257  }
   258  
   259  //////////////
   260  // HreqArgs //
   261  //////////////
   262  
   263  var (
   264  	hraPool sync.Pool
   265  	hra0    HreqArgs
   266  )
   267  
   268  func AllocHra() (a *HreqArgs) {
   269  	if v := hraPool.Get(); v != nil {
   270  		a = v.(*HreqArgs)
   271  		return
   272  	}
   273  	return &HreqArgs{}
   274  }
   275  
   276  func FreeHra(a *HreqArgs) {
   277  	*a = hra0
   278  	hraPool.Put(a)
   279  }
   280  
   281  func (u *HreqArgs) URL() string {
   282  	url := cos.JoinPath(u.Base, u.Path)
   283  	if u.RawQuery != "" {
   284  		return url + "?" + u.RawQuery
   285  	}
   286  	if rawq := u.Query.Encode(); rawq != "" {
   287  		return url + "?" + rawq
   288  	}
   289  	return url
   290  }
   291  
   292  func (u *HreqArgs) Req() (*http.Request, error) {
   293  	r := u.BodyR
   294  	if r == nil && u.Body != nil {
   295  		r = bytes.NewBuffer(u.Body)
   296  	}
   297  	req, err := http.NewRequest(u.Method, u.URL(), r)
   298  	if err != nil {
   299  		return nil, err
   300  	}
   301  	if u.Header != nil {
   302  		copyHeaders(u.Header, &req.Header)
   303  	}
   304  	return req, nil
   305  }
   306  
   307  // ReqWithCancel creates request with ability to cancel it.
   308  func (u *HreqArgs) ReqWithCancel() (*http.Request, context.Context, context.CancelFunc, error) {
   309  	req, err := u.Req()
   310  	if err != nil {
   311  		return nil, nil, nil, err
   312  	}
   313  	if u.Method == http.MethodPost || u.Method == http.MethodPut {
   314  		req.Header.Set(cos.HdrContentType, cos.ContentJSON)
   315  	}
   316  	ctx, cancel := context.WithCancel(context.Background())
   317  	req = req.WithContext(ctx)
   318  	return req, ctx, cancel, nil
   319  }
   320  
   321  func (u *HreqArgs) ReqWithTimeout(timeout time.Duration) (*http.Request, context.Context, context.CancelFunc, error) {
   322  	req, err := u.Req()
   323  	if err != nil {
   324  		return nil, nil, nil, err
   325  	}
   326  	if u.Method == http.MethodPost || u.Method == http.MethodPut {
   327  		req.Header.Set(cos.HdrContentType, cos.ContentJSON)
   328  	}
   329  	ctx, cancel := context.WithTimeout(context.Background(), timeout)
   330  	req = req.WithContext(ctx)
   331  	return req, ctx, cancel, nil
   332  }
   333  
   334  //
   335  // number of intra-cluster broadcasting goroutines
   336  //
   337  
   338  func MaxParallelism() int { return max(sys.NumCPU(), 4) }