11import re
2- import importlib
32from unittest .mock import patch
43
54import httpx
@@ -90,27 +89,16 @@ def test_upload_file_parts_to_s3__error_status(data_file, respx_mock):
9089
9190
9291class TestS3FileDataUploaderRetry :
93- # It is done as it is to patch retry decorator to minimize a retry interval
92+ # Patch only the wait strategy so tests exercise the production retry policy.
9493 def setup_method (self ):
95- s3_retry = tenacity .retry (
96- stop = tenacity .stop_after_attempt (3 ),
97- wait = tenacity .wait_none (),
98- retry = tenacity .retry_if_exception (s3_httpx_client ._allowed_to_retry ),
99- reraise = True ,
100- )
101- # Now patch the decorator where the decorator is being imported from
102- patch (
103- "opik.s3_httpx_client.s3_retry" ,
104- lambda x : s3_retry (x ),
94+ patch .object (
95+ s3_file_uploader .S3FileDataUploader ._send_data_part .retry ,
96+ "wait" ,
97+ tenacity .wait_none (),
10598 ).start ()
106- # Reloads the module which applies our patched decorator
107- importlib .reload (s3_file_uploader )
10899
109100 def teardown_method (self ):
110- # Stops all patches started with start()
111101 patch .stopall ()
112- # Reload our module, which restores the original decorator
113- importlib .reload (s3_file_uploader )
114102
115103 @pytest .mark .parametrize ("status_code" , [500 , 502 , 503 , 504 ])
116104 def test_upload_file_parts_to_s3__retryable_status__retries (
@@ -128,8 +116,10 @@ def test_upload_file_parts_to_s3__retryable_status__retries(
128116 "https://s3.amazonaws.com/bucket/3" ,
129117 ]
130118 rx_url = re .compile ("https://s3\\ .amazonaws\\ .com/bucket/*" )
119+ requests : list [tuple [httpx .URL , bytes ]] = []
131120
132121 def retry_side_effect (request , route ):
122+ requests .append ((request .url , request .content ))
133123 if route .call_count < 1 :
134124 return httpx .Response (status_code )
135125 else :
@@ -154,6 +144,7 @@ def retry_side_effect(request, route):
154144
155145 route = respx .put (rx_url )
156146 assert route .call_count == 3 + 1
147+ assert requests [0 ] == requests [1 ]
157148
158149 def test_upload_file_parts_to_s3__remote_protocol_error__retries (
159150 self , data_file , respx_mock
@@ -170,8 +161,10 @@ def test_upload_file_parts_to_s3__remote_protocol_error__retries(
170161 "https://s3.amazonaws.com/bucket/3" ,
171162 ]
172163 rx_url = re .compile ("https://s3\\ .amazonaws\\ .com/bucket/*" )
164+ requests : list [tuple [httpx .URL , bytes ]] = []
173165
174166 def retry_side_effect (request , route ):
167+ requests .append ((request .url , request .content ))
175168 if route .call_count < 1 :
176169 raise httpx .RemoteProtocolError (
177170 "Server disconnected without sending a response" ,
@@ -197,3 +190,45 @@ def retry_side_effect(request, route):
197190
198191 route = respx .put (rx_url )
199192 assert route .call_count == 3 + 1
193+ assert requests [0 ] == requests [1 ]
194+
195+ def test_upload_file_parts_to_s3__remote_protocol_error_exhausted__retains_for_replay (
196+ self , data_file , respx_mock
197+ ):
198+ file_parts = file_parts_strategy .FilePartsStrategy (
199+ file_path = data_file .name ,
200+ file_size = conftest .FILE_SIZE ,
201+ )
202+ pre_sign_urls = [
203+ "https://s3.amazonaws.com/bucket/1" ,
204+ "https://s3.amazonaws.com/bucket/2" ,
205+ "https://s3.amazonaws.com/bucket/3" ,
206+ ]
207+ rx_url = re .compile ("https://s3\\ .amazonaws\\ .com/bucket/*" )
208+
209+ def remote_protocol_error (request , route ):
210+ raise httpx .RemoteProtocolError (
211+ "Server disconnected without sending a response" ,
212+ request = request ,
213+ )
214+
215+ respx_mock .put (rx_url ).mock (side_effect = remote_protocol_error )
216+
217+ uploader = s3_file_uploader .S3FileDataUploader (
218+ file_parts = file_parts ,
219+ pre_sign_urls = pre_sign_urls ,
220+ httpx_client = s3_httpx_client .get (),
221+ )
222+
223+ with pytest .raises (s3_upload_error .S3UploadFileError ) as exc_info :
224+ uploader .upload ()
225+
226+ upload_error = exc_info .value
227+ assert upload_error .connection_error is True
228+ assert isinstance (upload_error .__cause__ , httpx .RemoteProtocolError )
229+ assert str (upload_error .__cause__ ) == (
230+ "Server disconnected without sending a response"
231+ )
232+
233+ route = respx .put (rx_url )
234+ assert route .call_count == 3
0 commit comments