@@ -85,14 +85,18 @@ def test_upload_file_parts_to_s3__error_status(data_file, respx_mock):
8585 with pytest .raises (s3_upload_error .S3UploadFileError ):
8686 uploader .upload ()
8787
88+ route = respx .put (rx_url )
89+ assert route .call_count == 1
90+
8891
8992class TestS3FileDataUploaderRetry :
9093 # It is done as it is to patch retry decorator to minimize a retry interval
9194 def setup_method (self ):
9295 s3_retry = tenacity .retry (
9396 stop = tenacity .stop_after_attempt (3 ),
94- wait = tenacity .wait_exponential ( multiplier = 1 , min = 0.5 , max = 2 ),
97+ wait = tenacity .wait_none ( ),
9598 retry = tenacity .retry_if_exception (s3_httpx_client ._allowed_to_retry ),
99+ reraise = True ,
96100 )
97101 # Now patch the decorator where the decorator is being imported from
98102 patch (
@@ -108,7 +112,10 @@ def teardown_method(self):
108112 # Reload our module, which restores the original decorator
109113 importlib .reload (s3_file_uploader )
110114
111- def test_upload_file_parts_to_s3__retry_on_500 (self , data_file , respx_mock ):
115+ @pytest .mark .parametrize ("status_code" , [500 , 502 , 503 , 504 ])
116+ def test_upload_file_parts_to_s3__retryable_status__retries (
117+ self , data_file , respx_mock , status_code
118+ ):
112119 max_file_part_size = 5 * 1024 * 1024
113120 file_parts = file_parts_strategy .FilePartsStrategy (
114121 file_path = data_file .name ,
@@ -124,7 +131,7 @@ def test_upload_file_parts_to_s3__retry_on_500(self, data_file, respx_mock):
124131
125132 def retry_side_effect (request , route ):
126133 if route .call_count < 1 :
127- return httpx .Response (500 )
134+ return httpx .Response (status_code )
128135 else :
129136 return httpx .Response (200 , headers = {"ETag" : "e-tag" })
130137
@@ -146,4 +153,47 @@ def retry_side_effect(request, route):
146153 assert monitor .bytes_sent == conftest .FILE_SIZE
147154
148155 route = respx .put (rx_url )
149- assert route .call_count == 3 + 1 # we have one retry due to 500
156+ assert route .call_count == 3 + 1
157+
158+ def test_upload_file_parts_to_s3__remote_protocol_error__retries (
159+ self , data_file , respx_mock
160+ ):
161+ max_file_part_size = 5 * 1024 * 1024
162+ file_parts = file_parts_strategy .FilePartsStrategy (
163+ file_path = data_file .name ,
164+ file_size = conftest .FILE_SIZE ,
165+ max_file_part_size = max_file_part_size ,
166+ )
167+ pre_sign_urls = [
168+ "https://s3.amazonaws.com/bucket/1" ,
169+ "https://s3.amazonaws.com/bucket/2" ,
170+ "https://s3.amazonaws.com/bucket/3" ,
171+ ]
172+ rx_url = re .compile ("https://s3\\ .amazonaws\\ .com/bucket/*" )
173+
174+ def retry_side_effect (request , route ):
175+ if route .call_count < 1 :
176+ raise httpx .RemoteProtocolError (
177+ "Server disconnected without sending a response" ,
178+ request = request ,
179+ )
180+ return httpx .Response (200 , headers = {"ETag" : "e-tag" })
181+
182+ respx_mock .put (rx_url ).mock (side_effect = retry_side_effect )
183+
184+ httpx_client = s3_httpx_client .get ()
185+ monitor = file_upload_monitor .FileUploadMonitor ()
186+
187+ uploader = s3_file_uploader .S3FileDataUploader (
188+ file_parts = file_parts ,
189+ pre_sign_urls = pre_sign_urls ,
190+ httpx_client = httpx_client ,
191+ monitor = monitor ,
192+ )
193+
194+ uploader .upload ()
195+
196+ assert monitor .bytes_sent == conftest .FILE_SIZE
197+
198+ route = respx .put (rx_url )
199+ assert route .call_count == 3 + 1
0 commit comments