go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/api/internal/gensupport/resumable.go (about) 1 // Copyright 2016 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package gensupport 6 7 import ( 8 "context" 9 "errors" 10 "fmt" 11 "io" 12 "net/http" 13 "strings" 14 "sync" 15 "time" 16 17 "github.com/google/uuid" 18 ) 19 20 // ResumableUpload is used by the generated APIs to provide resumable uploads. 21 // It is not used by developers directly. 22 type ResumableUpload struct { 23 Client *http.Client 24 // URI is the resumable resource destination provided by the server after specifying "&uploadType=resumable". 25 URI string 26 UserAgent string // User-Agent for header of the request 27 // Media is the object being uploaded. 28 Media *MediaBuffer 29 // MediaType defines the media type, e.g. "image/jpeg". 30 MediaType string 31 32 mu sync.Mutex // guards progress 33 progress int64 // number of bytes uploaded so far 34 35 // Callback is an optional function that will be periodically called with the cumulative number of bytes uploaded. 36 Callback func(int64) 37 38 // Retry optionally configures retries for requests made against the upload. 39 Retry *RetryConfig 40 41 // ChunkRetryDeadline configures the per-chunk deadline after which no further 42 // retries should happen. 43 ChunkRetryDeadline time.Duration 44 45 // Track current request invocation ID and attempt count for retry metrics 46 // and idempotency headers. 47 invocationID string 48 attempts int 49 } 50 51 // Progress returns the number of bytes uploaded at this point. 52 func (rx *ResumableUpload) Progress() int64 { 53 rx.mu.Lock() 54 defer rx.mu.Unlock() 55 return rx.progress 56 } 57 58 // doUploadRequest performs a single HTTP request to upload data. 59 // off specifies the offset in rx.Media from which data is drawn. 60 // size is the number of bytes in data. 61 // final specifies whether data is the final chunk to be uploaded. 62 func (rx *ResumableUpload) doUploadRequest(ctx context.Context, data io.Reader, off, size int64, final bool) (*http.Response, error) { 63 req, err := http.NewRequest("POST", rx.URI, data) 64 if err != nil { 65 return nil, err 66 } 67 68 req.ContentLength = size 69 var contentRange string 70 if final { 71 if size == 0 { 72 contentRange = fmt.Sprintf("bytes */%v", off) 73 } else { 74 contentRange = fmt.Sprintf("bytes %v-%v/%v", off, off+size-1, off+size) 75 } 76 } else { 77 contentRange = fmt.Sprintf("bytes %v-%v/*", off, off+size-1) 78 } 79 req.Header.Set("Content-Range", contentRange) 80 req.Header.Set("Content-Type", rx.MediaType) 81 req.Header.Set("User-Agent", rx.UserAgent) 82 83 // TODO(b/274504690): Consider dropping gccl-invocation-id key since it 84 // duplicates the X-Goog-Gcs-Idempotency-Token header (added in v0.115.0). 85 baseXGoogHeader := "gl-go/" + GoVersion() + " gdcl/" + "luci-go" 86 invocationHeader := fmt.Sprintf("gccl-invocation-id/%s gccl-attempt-count/%d", rx.invocationID, rx.attempts) 87 req.Header.Set("X-Goog-Api-Client", strings.Join([]string{baseXGoogHeader, invocationHeader}, " ")) 88 89 // Set idempotency token header which is used by GCS uploads. 90 req.Header.Set("X-Goog-Gcs-Idempotency-Token", rx.invocationID) 91 92 // Google's upload endpoint uses status code 308 for a 93 // different purpose than the "308 Permanent Redirect" 94 // since-standardized in RFC 7238. Because of the conflict in 95 // semantics, Google added this new request header which 96 // causes it to not use "308" and instead reply with 200 OK 97 // and sets the upload-specific "X-HTTP-Status-Code-Override: 98 // 308" response header. 99 req.Header.Set("X-GUploader-No-308", "yes") 100 101 return SendRequest(ctx, rx.Client, req) 102 } 103 104 func statusResumeIncomplete(resp *http.Response) bool { 105 // This is how the server signals "status resume incomplete" 106 // when X-GUploader-No-308 is set to "yes": 107 return resp != nil && resp.Header.Get("X-Http-Status-Code-Override") == "308" 108 } 109 110 // reportProgress calls a user-supplied callback to report upload progress. 111 // If old==updated, the callback is not called. 112 func (rx *ResumableUpload) reportProgress(old, updated int64) { 113 if updated-old == 0 { 114 return 115 } 116 rx.mu.Lock() 117 rx.progress = updated 118 rx.mu.Unlock() 119 if rx.Callback != nil { 120 rx.Callback(updated) 121 } 122 } 123 124 // transferChunk performs a single HTTP request to upload a single chunk from rx.Media. 125 func (rx *ResumableUpload) transferChunk(ctx context.Context) (*http.Response, error) { 126 chunk, off, size, err := rx.Media.Chunk() 127 128 done := err == io.EOF 129 if !done && err != nil { 130 return nil, err 131 } 132 133 res, err := rx.doUploadRequest(ctx, chunk, off, int64(size), done) 134 if err != nil { 135 return res, err 136 } 137 138 // We sent "X-GUploader-No-308: yes" (see comment elsewhere in 139 // this file), so we don't expect to get a 308. 140 if res.StatusCode == 308 { 141 return nil, errors.New("unexpected 308 response status code") 142 } 143 144 if res.StatusCode == http.StatusOK { 145 rx.reportProgress(off, off+int64(size)) 146 } 147 148 if statusResumeIncomplete(res) { 149 rx.Media.Next() 150 } 151 return res, nil 152 } 153 154 // Upload starts the process of a resumable upload with a cancellable context. 155 // It retries using the provided back off strategy until cancelled or the 156 // strategy indicates to stop retrying. 157 // It is called from the auto-generated API code and is not visible to the user. 158 // Before sending an HTTP request, Upload calls any registered hook functions, 159 // and calls the returned functions after the request returns (see send.go). 160 // rx is private to the auto-generated API code. 161 // Exactly one of resp or err will be nil. If resp is non-nil, the caller must call resp.Body.Close. 162 func (rx *ResumableUpload) Upload(ctx context.Context) (resp *http.Response, err error) { 163 164 // There are a couple of cases where it's possible for err and resp to both 165 // be non-nil. However, we expose a simpler contract to our callers: exactly 166 // one of resp and err will be non-nil. This means that any response body 167 // must be closed here before returning a non-nil error. 168 var prepareReturn = func(resp *http.Response, err error) (*http.Response, error) { 169 if err != nil { 170 if resp != nil && resp.Body != nil { 171 resp.Body.Close() 172 } 173 return nil, err 174 } 175 // This case is very unlikely but possible only if rx.ChunkRetryDeadline is 176 // set to a very small value, in which case no requests will be sent before 177 // the deadline. Return an error to avoid causing a panic. 178 if resp == nil { 179 return nil, fmt.Errorf("upload request to %v not sent, choose larger value for ChunkRetryDealine", rx.URI) 180 } 181 return resp, nil 182 } 183 // Configure retryable error criteria. 184 errorFunc := rx.Retry.errorFunc() 185 186 // Configure per-chunk retry deadline. 187 var retryDeadline time.Duration 188 if rx.ChunkRetryDeadline != 0 { 189 retryDeadline = rx.ChunkRetryDeadline 190 } else { 191 retryDeadline = defaultRetryDeadline 192 } 193 194 // Send all chunks. 195 for { 196 var pause time.Duration 197 198 // Each chunk gets its own initialized-at-zero backoff and invocation ID. 199 bo := rx.Retry.backoff() 200 quitAfterTimer := time.NewTimer(retryDeadline) 201 rx.attempts = 1 202 rx.invocationID = uuid.New().String() 203 204 // Retry loop for a single chunk. 205 for { 206 pauseTimer := time.NewTimer(pause) 207 select { 208 case <-ctx.Done(): 209 quitAfterTimer.Stop() 210 pauseTimer.Stop() 211 if err == nil { 212 err = ctx.Err() 213 } 214 return prepareReturn(resp, err) 215 case <-pauseTimer.C: 216 case <-quitAfterTimer.C: 217 pauseTimer.Stop() 218 return prepareReturn(resp, err) 219 } 220 pauseTimer.Stop() 221 222 // Check for context cancellation or timeout once more. If more than one 223 // case in the select statement above was satisfied at the same time, Go 224 // will choose one arbitrarily. 225 // That can cause an operation to go through even if the context was 226 // canceled before or the timeout was reached. 227 select { 228 case <-ctx.Done(): 229 quitAfterTimer.Stop() 230 if err == nil { 231 err = ctx.Err() 232 } 233 return prepareReturn(resp, err) 234 case <-quitAfterTimer.C: 235 return prepareReturn(resp, err) 236 default: 237 } 238 239 resp, err = rx.transferChunk(ctx) 240 241 var status int 242 if resp != nil { 243 status = resp.StatusCode 244 } 245 246 // Check if we should retry the request. 247 if !errorFunc(status, err) { 248 quitAfterTimer.Stop() 249 break 250 } 251 252 rx.attempts++ 253 pause = bo.Pause() 254 if resp != nil && resp.Body != nil { 255 resp.Body.Close() 256 } 257 } 258 259 // If the chunk was uploaded successfully, but there's still 260 // more to go, upload the next chunk without any delay. 261 if statusResumeIncomplete(resp) { 262 resp.Body.Close() 263 continue 264 } 265 266 return prepareReturn(resp, err) 267 } 268 }