@@ -127,34 +127,65 @@ func (rx *ResumableUpload) reportProgress(old, updated int64) {
127
127
}
128
128
129
129
// transferChunk performs a single HTTP request to upload a single chunk.
130
+ // It uses a goroutine to perform the upload and a timer to enforce ChunkTransferTimeout.
130
131
func (rx * ResumableUpload ) transferChunk (ctx context.Context , chunk io.Reader , off , size int64 , done bool ) (* http.Response , error ) {
131
- // rCtx is derived from a context with a defined ChunkTransferTimeout with non-zero value.
132
- // If a particular request exceeds this transfer time for getting response, the rCtx deadline will be exceeded,
133
- // triggering a retry of the request.
134
- var rCtx context.Context
135
- var cancel context.CancelFunc
136
-
137
- rCtx = ctx
138
- if rx .ChunkTransferTimeout != 0 {
139
- rCtx , cancel = context .WithTimeout (ctx , rx .ChunkTransferTimeout )
140
- defer cancel ()
132
+ // If no timeout is specified, perform the request synchronously without a timer.
133
+ if rx .ChunkTransferTimeout == 0 {
134
+ res , err := rx .doUploadRequest (ctx , chunk , off , size , done )
135
+ if err != nil {
136
+ return res , err
137
+ }
138
+ return res , nil
141
139
}
142
140
143
- res , err := rx .doUploadRequest (rCtx , chunk , off , size , done )
144
- if err != nil {
145
- return res , err
146
- }
141
+ // Start a timer for the ChunkTransferTimeout duration.
142
+ timer := time .NewTimer (rx .ChunkTransferTimeout )
147
143
148
- // We sent "X-GUploader-No-308: yes" (see comment elsewhere in
149
- // this file), so we don't expect to get a 308.
150
- if res . StatusCode == 308 {
151
- return nil , errors . New ( "unexpected 308 response status code" )
144
+ // A struct to hold the result from the goroutine.
145
+ type uploadResult struct {
146
+ res * http. Response
147
+ err error
152
148
}
153
149
154
- if res .StatusCode == http .StatusOK {
155
- rx .reportProgress (off , off + int64 (size ))
150
+ // A buffered channel to receive the result of the upload.
151
+ resultCh := make (chan uploadResult , 1 )
152
+
153
+ // Create a cancellable context for the upload request. This allows us to
154
+ // abort the request if the timer fires first.
155
+ rCtx , cancel := context .WithCancel (ctx )
156
+ // NOTE: We do NOT use `defer cancel()` here. The context must remain valid
157
+ // for the caller to read the response body of a successful request.
158
+ // Cancellation is handled manually on timeout paths.
159
+
160
+ // Starting the chunk upload in parallel.
161
+ go func () {
162
+ res , err := rx .doUploadRequest (rCtx , chunk , off , size , done )
163
+ resultCh <- uploadResult {res : res , err : err }
164
+ }()
165
+
166
+ // Wait for timer to fire or result channel to have the uploadResult or ctx to be cancelled.
167
+ select {
168
+ // Note: Calling cancel() will guarantee that the goroutine finishes,
169
+ // so these two cases will never block forever on draining the resultCh.
170
+ case <- ctx .Done ():
171
+ // Context is cancelled for the overall upload.
172
+ cancel ()
173
+ // Drain resultCh.
174
+ <- resultCh
175
+ return nil , ctx .Err ()
176
+ case <- timer .C :
177
+ // Chunk Transfer timer fired before resultCh so we return context.DeadlineExceeded.
178
+ cancel ()
179
+ // Drain resultCh.
180
+ <- resultCh
181
+ return nil , context .DeadlineExceeded
182
+ case result := <- resultCh :
183
+ // Handle the result from the upload.
184
+ if result .err != nil {
185
+ return result .res , result .err
186
+ }
187
+ return result .res , nil
156
188
}
157
- return res , nil
158
189
}
159
190
160
191
// uploadChunkWithRetries attempts to upload a single chunk, with retries
@@ -164,14 +195,14 @@ func (rx *ResumableUpload) uploadChunkWithRetries(ctx context.Context, chunk io.
164
195
shouldRetry := rx .Retry .errorFunc ()
165
196
166
197
// Configure single chunk retry deadline.
167
- retryDeadline := defaultRetryDeadline
198
+ chunkRetryDeadline := defaultRetryDeadline
168
199
if rx .ChunkRetryDeadline != 0 {
169
- retryDeadline = rx .ChunkRetryDeadline
200
+ chunkRetryDeadline = rx .ChunkRetryDeadline
170
201
}
171
202
172
203
// Each chunk gets its own initialized-at-zero backoff and invocation ID.
173
204
bo := rx .Retry .backoff ()
174
- quitAfterTimer := time .NewTimer (retryDeadline )
205
+ quitAfterTimer := time .NewTimer (chunkRetryDeadline )
175
206
defer quitAfterTimer .Stop ()
176
207
rx .attempts = 1
177
208
rx .invocationID = uuid .New ().String ()
@@ -184,20 +215,20 @@ func (rx *ResumableUpload) uploadChunkWithRetries(ctx context.Context, chunk io.
184
215
for {
185
216
// Wait for the backoff period, unless the context is canceled or the
186
217
// retry deadline is hit.
187
- pauseTimer := time .NewTimer (pause )
218
+ backoffPauseTimer := time .NewTimer (pause )
188
219
select {
189
220
case <- ctx .Done ():
190
- pauseTimer .Stop ()
221
+ backoffPauseTimer .Stop ()
191
222
if err == nil {
192
223
err = ctx .Err ()
193
224
}
194
225
return resp , err
195
- case <- pauseTimer .C :
226
+ case <- backoffPauseTimer .C :
196
227
case <- quitAfterTimer .C :
197
- pauseTimer .Stop ()
228
+ backoffPauseTimer .Stop ()
198
229
return resp , err
199
230
}
200
- pauseTimer .Stop ()
231
+ backoffPauseTimer .Stop ()
201
232
202
233
// Check for context cancellation or timeout once more. If more than one
203
234
// case in the select statement above was satisfied at the same time, Go
@@ -233,6 +264,11 @@ func (rx *ResumableUpload) uploadChunkWithRetries(ctx context.Context, chunk io.
233
264
if resp != nil {
234
265
status = resp .StatusCode
235
266
}
267
+ // We sent "X-GUploader-No-308: yes" (see comment elsewhere in
268
+ // this file), so we don't expect to get a 308.
269
+ if status == 308 {
270
+ return nil , errors .New ("unexpected 308 response status code" )
271
+ }
236
272
// Chunk upload should be retried if the ChunkTransferTimeout is non-zero and err is context deadline exceeded
237
273
// or we encounter a retryable error.
238
274
if (rx .ChunkTransferTimeout != 0 && errors .Is (err , context .DeadlineExceeded )) || shouldRetry (status , err ) {
@@ -283,7 +319,9 @@ func (rx *ResumableUpload) Upload(ctx context.Context) (*http.Response, error) {
283
319
if resp == nil {
284
320
return nil , fmt .Errorf ("upload request to %v not sent, choose larger value for ChunkRetryDeadline" , rx .URI )
285
321
}
286
-
322
+ if resp .StatusCode == http .StatusOK {
323
+ rx .reportProgress (off , off + int64 (size ))
324
+ }
287
325
if statusResumeIncomplete (resp ) {
288
326
// The upload is not yet complete, but the server has acknowledged this chunk.
289
327
// We don't have anything to do with the response body.
0 commit comments