@@ -19,6 +19,7 @@ import (
1919 "sync"
2020 "sync/atomic"
2121 "testing"
22+ "testing/synctest"
2223 "time"
2324
2425 "github.com/aws/aws-sdk-go-v2/aws"
@@ -36,6 +37,12 @@ const (
3637 uploadsPath = "uploads"
3738)
3839
40+ type multipartRoundTripFunc func (* http.Request ) (* http.Response , error )
41+
42+ func (f multipartRoundTripFunc ) RoundTrip (req * http.Request ) (* http.Response , error ) {
43+ return f (req )
44+ }
45+
3946// createTestMultipartUploader creates a test uploader with a mock HTTP client
4047func createTestMultipartUploader (t * testing.T , handler http.HandlerFunc , retryConfig ... RetryConfig ) * MultipartUploader {
4148 t .Helper ()
@@ -145,6 +152,103 @@ func TestGCPUploadFileInParallelAbortsOnPartFailure(t *testing.T) {
145152 require .True (t , aborted .Load (), "failed parallel upload should abort the multipart upload" )
146153}
147154
155+ func TestMultipartUploaderUploadPartsZeroConcurrencyDoesNotDeadlock (t * testing.T ) {
156+ t .Parallel ()
157+
158+ inputPath := writeTempFile (t , []byte ("x" ))
159+ input , err := os .Open (inputPath )
160+ require .NoError (t , err )
161+ t .Cleanup (func () { require .NoError (t , input .Close ()) })
162+
163+ uploader := createTestMultipartUploader (t , func (w http.ResponseWriter , r * http.Request ) {
164+ if r .Method != http .MethodPut {
165+ t .Errorf ("unexpected request method: %s" , r .Method )
166+
167+ return
168+ }
169+ if _ , err := io .Copy (io .Discard , r .Body ); err != nil {
170+ t .Errorf ("read request body: %v" , err )
171+
172+ return
173+ }
174+ w .Header ().Set ("ETag" , `"etag"` )
175+ w .WriteHeader (http .StatusOK )
176+ })
177+
178+ done := make (chan error , 1 )
179+ go func () {
180+ _ , err := uploader .uploadParts (t .Context (), 0 , 1 , 1 , input , "upload-id" )
181+ done <- err
182+ }()
183+
184+ select {
185+ case err := <- done :
186+ require .NoError (t , err )
187+ case <- time .After (time .Second ):
188+ t .Fatal ("zero upload concurrency blocked part admission" )
189+ }
190+ }
191+
192+ func TestMultipartUploaderUploadPartsNegativeConcurrencyUsesOneWorker (t * testing.T ) {
193+ t .Parallel ()
194+
195+ inputPath := filepath .Join (t .TempDir (), "multipart.bin" )
196+ input , err := os .OpenFile (inputPath , os .O_CREATE | os .O_RDWR , 0o600 )
197+ require .NoError (t , err )
198+ t .Cleanup (func () { require .NoError (t , input .Close ()) })
199+ require .NoError (t , input .Truncate (gcpMultipartUploadChunkSize + 1 ))
200+
201+ synctest .Test (t , func (t * testing.T ) {
202+ var active atomic.Int32
203+ var maxActive atomic.Int32
204+ release := make (chan struct {})
205+
206+ client := retryablehttp .NewClient ()
207+ client .RetryMax = 0
208+ client .Logger = nil
209+ client .HTTPClient .Transport = multipartRoundTripFunc (func (req * http.Request ) (* http.Response , error ) {
210+ current := active .Add (1 )
211+ defer active .Add (- 1 )
212+ for {
213+ previous := maxActive .Load ()
214+ if current <= previous || maxActive .CompareAndSwap (previous , current ) {
215+ break
216+ }
217+ }
218+
219+ <- release
220+
221+ response := new (http.Response )
222+ response .StatusCode = http .StatusOK
223+ response .Header = make (http.Header )
224+ response .Header .Set ("ETag" , `"etag"` )
225+ response .Body = http .NoBody
226+ response .Request = req
227+
228+ return response , nil
229+ })
230+
231+ uploader := new (MultipartUploader )
232+ uploader .objectName = "object"
233+ uploader .token = "token"
234+ uploader .client = client
235+ uploader .baseURL = "http://storage.test"
236+
237+ done := make (chan error , 1 )
238+ go func () {
239+ _ , err := uploader .uploadParts (t .Context (), - 1 , 2 , gcpMultipartUploadChunkSize + 1 , input , "upload-id" )
240+ done <- err
241+ }()
242+
243+ synctest .Wait ()
244+ gotMax := maxActive .Load ()
245+ close (release )
246+ require .NoError (t , <- done )
247+ require .Equal (t , int32 (1 ), gotMax , "negative concurrency admitted multiple GCS parts" )
248+ require .Zero (t , active .Load ())
249+ })
250+ }
251+
148252func TestMultipartUploader_InitiateUpload_Success (t * testing.T ) {
149253 t .Parallel ()
150254 expectedUploadID := "test-upload-id-123"
0 commit comments