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) }