@@ -26,10 +26,8 @@ public class HttpRateLimitRetryTest {
2626
2727 private final int RANDOM_INT = 234 ;
2828
29- @ Before
30- public void before () throws Exception {
31- http = new Http .HttpBuilder ("GET" , "example.test" , "/foo/bar" ).build ();
32- http = Mockito .spy (http );
29+ private void setupHttp (Http client ) throws Exception {
30+ http = Mockito .spy (client );
3331
3432 Field httpClientField = Http .class .getDeclaredField ("httpClient" );
3533 httpClientField .setAccessible (true );
@@ -39,6 +37,12 @@ public void before() throws Exception {
3937 Mockito .doNothing ().when (http ).sleep (Mockito .any (Long .class ));
4038 }
4139
40+ @ Before
41+ public void before () throws Exception {
42+ Http client = new Http .HttpBuilder ("GET" , "example.test" , "/foo/bar" ).build ();
43+ setupHttp (client );
44+ }
45+
4246 @ Test
4347 public void testSingleRateLimitRetry () throws Exception {
4448 final List <Response > responses = new ArrayList <Response >();
@@ -128,4 +132,98 @@ public Call answer(InvocationOnMock invocationOnMock) throws Throwable {
128132 assertEquals (16000L + RANDOM_INT , (long ) sleepTimes .get (4 ));
129133 assertEquals (32000L + RANDOM_INT , (long ) sleepTimes .get (5 ));
130134 }
135+
136+ @ Test
137+ public void testMaxBackoffZeroDisablesRetry () throws Exception {
138+ Http customHttp = new Http .HttpBuilder ("GET" , "example.test" , "/foo/bar" )
139+ .useMaxBackoffMs (0 )
140+ .build ();
141+ setupHttp (customHttp );
142+
143+ final List <Response > responses = new ArrayList <Response >();
144+
145+ Mockito .when (httpClient .newCall (Mockito .any (Request .class ))).thenAnswer (new Answer <Call >() {
146+ @ Override
147+ public Call answer (InvocationOnMock invocationOnMock ) throws Throwable {
148+ Call call = Mockito .mock (Call .class );
149+
150+ Response resp = new Response .Builder ()
151+ .protocol (Protocol .HTTP_2 )
152+ .code (429 )
153+ .request ((Request ) invocationOnMock .getArguments ()[0 ])
154+ .message ("HTTP 429" )
155+ .build ();
156+ responses .add (resp );
157+ Mockito .when (call .execute ()).thenReturn (resp );
158+
159+ return call ;
160+ }
161+ });
162+
163+ Response actualRes = http .executeHttpRequest ();
164+ assertEquals (1 , responses .size ());
165+ assertEquals (429 , actualRes .code ());
166+
167+ // Verify no sleep was called
168+ Mockito .verify (http , Mockito .never ()).sleep (Mockito .any (Long .class ));
169+ }
170+
171+ @ Test
172+ public void testMaxBackoffCustomLimit () throws Exception {
173+ Http customHttp = new Http .HttpBuilder ("GET" , "example.test" , "/foo/bar" )
174+ .useMaxBackoffMs (4000 )
175+ .build ();
176+ setupHttp (customHttp );
177+
178+ final List <Response > responses = new ArrayList <Response >();
179+
180+ Mockito .when (httpClient .newCall (Mockito .any (Request .class ))).thenAnswer (new Answer <Call >() {
181+ @ Override
182+ public Call answer (InvocationOnMock invocationOnMock ) throws Throwable {
183+ Call call = Mockito .mock (Call .class );
184+
185+ Response resp = new Response .Builder ()
186+ .protocol (Protocol .HTTP_2 )
187+ .code (429 )
188+ .request ((Request ) invocationOnMock .getArguments ()[0 ])
189+ .message ("HTTP 429" )
190+ .build ();
191+ responses .add (resp );
192+ Mockito .when (call .execute ()).thenReturn (resp );
193+
194+ return call ;
195+ }
196+ });
197+
198+ // With maxBackoff=4000, retries at 1000, 2000, 4000, then 8000 > 4000 exits
199+ // That's 4 total requests (1 initial + 3 retries)
200+ Response actualRes = http .executeHttpRequest ();
201+ assertEquals (4 , responses .size ());
202+ assertEquals (429 , actualRes .code ());
203+
204+ ArgumentCaptor <Long > sleepCapture = ArgumentCaptor .forClass (Long .class );
205+ Mockito .verify (http , Mockito .times (3 )).sleep (sleepCapture .capture ());
206+ List <Long > sleepTimes = sleepCapture .getAllValues ();
207+ assertEquals (1000L + RANDOM_INT , (long ) sleepTimes .get (0 ));
208+ assertEquals (2000L + RANDOM_INT , (long ) sleepTimes .get (1 ));
209+ assertEquals (4000L + RANDOM_INT , (long ) sleepTimes .get (2 ));
210+ }
211+
212+ @ Test
213+ public void testDefaultMaxBackoffIsUsedWhenNotSpecified () throws Exception {
214+ Http defaultHttp = new Http .HttpBuilder ("GET" , "example.test" , "/foo/bar" ).build ();
215+
216+ Field maxBackoffField = Http .class .getDeclaredField ("maxBackoffMs" );
217+ maxBackoffField .setAccessible (true );
218+ long actualMaxBackoff = (long ) maxBackoffField .get (defaultHttp );
219+
220+ assertEquals (Http .MAX_BACKOFF_MS , actualMaxBackoff );
221+ }
222+
223+ @ Test (expected = IllegalArgumentException .class )
224+ public void testMaxBackoffNegativeThrows () {
225+ new Http .HttpBuilder ("GET" , "example.test" , "/foo/bar" )
226+ .useMaxBackoffMs (-1 )
227+ .build ();
228+ }
131229}
0 commit comments