1 /*
2  * Copyright 2018 Google LLC
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions are
6  * met:
7  *
8  *     * Redistributions of source code must retain the above copyright
9  * notice, this list of conditions and the following disclaimer.
10  *     * Redistributions in binary form must reproduce the above
11  * copyright notice, this list of conditions and the following disclaimer
12  * in the documentation and/or other materials provided with the
13  * distribution.
14  *     * Neither the name of Google LLC nor the names of its
15  * contributors may be used to endorse or promote products derived from
16  * this software without specific prior written permission.
17  *
18  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29  */
30 package com.google.api.gax.retrying;
31 
32 import static com.google.common.truth.Truth.assertThat;
33 import static org.junit.Assert.assertEquals;
34 import static org.junit.Assert.assertFalse;
35 import static org.junit.Assert.assertTrue;
36 
37 import com.google.api.gax.core.FakeApiClock;
38 import com.google.api.gax.rpc.testing.FakeCallContext;
39 import org.junit.Test;
40 import org.junit.runner.RunWith;
41 import org.junit.runners.JUnit4;
42 import org.threeten.bp.Duration;
43 
44 @RunWith(JUnit4.class)
45 public class ExponentialRetryAlgorithmTest {
46   private final FakeApiClock clock = new FakeApiClock(0L);
47   private final RetrySettings retrySettings =
48       RetrySettings.newBuilder()
49           .setMaxAttempts(6)
50           .setInitialRetryDelay(Duration.ofMillis(1L))
51           .setRetryDelayMultiplier(2.0)
52           .setMaxRetryDelay(Duration.ofMillis(8L))
53           .setInitialRpcTimeout(Duration.ofMillis(1L))
54           .setRpcTimeoutMultiplier(2.0)
55           .setMaxRpcTimeout(Duration.ofMillis(8L))
56           .setTotalTimeout(Duration.ofMillis(200L))
57           .build();
58   private final ExponentialRetryAlgorithm algorithm =
59       new ExponentialRetryAlgorithm(retrySettings, clock);
60   private final RetrySettings retrySettingsOverride =
61       RetrySettings.newBuilder()
62           .setMaxAttempts(3)
63           .setInitialRetryDelay(Duration.ofMillis(2L))
64           .setRetryDelayMultiplier(3.0)
65           .setMaxRetryDelay(Duration.ofMillis(18L))
66           .setInitialRpcTimeout(Duration.ofMillis(2L))
67           .setRpcTimeoutMultiplier(3.0)
68           .setMaxRpcTimeout(Duration.ofMillis(18L))
69           .setTotalTimeout(Duration.ofMillis(300L))
70           .build();
71   private final RetryingContext retryingContext =
72       FakeCallContext.createDefault().withRetrySettings(retrySettingsOverride);
73 
74   @Test
testCreateFirstAttempt()75   public void testCreateFirstAttempt() {
76     TimedAttemptSettings attempt = algorithm.createFirstAttempt();
77 
78     // Checking only the most core values, to not make this test too implementation specific.
79     assertEquals(0, attempt.getAttemptCount());
80     assertEquals(0, attempt.getOverallAttemptCount());
81     assertEquals(Duration.ZERO, attempt.getRetryDelay());
82     assertEquals(Duration.ZERO, attempt.getRandomizedRetryDelay());
83     assertEquals(Duration.ofMillis(1L), attempt.getRpcTimeout());
84     assertEquals(Duration.ZERO, attempt.getRetryDelay());
85   }
86 
87   @Test
testCreateFirstAttemptOverride()88   public void testCreateFirstAttemptOverride() {
89     TimedAttemptSettings attempt = algorithm.createFirstAttempt(retryingContext);
90 
91     // Checking only the most core values, to not make this test too implementation specific.
92     assertEquals(0, attempt.getAttemptCount());
93     assertEquals(0, attempt.getOverallAttemptCount());
94     assertEquals(Duration.ZERO, attempt.getRetryDelay());
95     assertEquals(Duration.ZERO, attempt.getRandomizedRetryDelay());
96     assertEquals(retrySettingsOverride.getInitialRpcTimeout(), attempt.getRpcTimeout());
97     assertEquals(Duration.ZERO, attempt.getRetryDelay());
98   }
99 
100   @Test
testCreateNextAttempt()101   public void testCreateNextAttempt() {
102     TimedAttemptSettings firstAttempt = algorithm.createFirstAttempt();
103     TimedAttemptSettings secondAttempt = algorithm.createNextAttempt(firstAttempt);
104 
105     // Checking only the most core values, to not make this test too implementation specific.
106     assertEquals(1, secondAttempt.getAttemptCount());
107     assertEquals(1, secondAttempt.getOverallAttemptCount());
108     assertEquals(Duration.ofMillis(1L), secondAttempt.getRetryDelay());
109     assertEquals(Duration.ofMillis(2L), secondAttempt.getRpcTimeout());
110 
111     TimedAttemptSettings thirdAttempt = algorithm.createNextAttempt(secondAttempt);
112     assertEquals(2, thirdAttempt.getAttemptCount());
113     assertEquals(Duration.ofMillis(2L), thirdAttempt.getRetryDelay());
114     assertEquals(Duration.ofMillis(4L), thirdAttempt.getRpcTimeout());
115   }
116 
117   @Test
testCreateNextAttemptOverride()118   public void testCreateNextAttemptOverride() {
119     TimedAttemptSettings firstAttempt = algorithm.createFirstAttempt(retryingContext);
120     TimedAttemptSettings secondAttempt = algorithm.createNextAttempt(firstAttempt);
121 
122     // Checking only the most core values, to not make this test too implementation specific.
123     assertEquals(1, secondAttempt.getAttemptCount());
124     assertEquals(1, secondAttempt.getOverallAttemptCount());
125     assertEquals(Duration.ofMillis(2L), secondAttempt.getRetryDelay());
126     assertEquals(Duration.ofMillis(6L), secondAttempt.getRpcTimeout());
127 
128     TimedAttemptSettings thirdAttempt = algorithm.createNextAttempt(secondAttempt);
129     assertEquals(2, thirdAttempt.getAttemptCount());
130     assertEquals(Duration.ofMillis(6L), thirdAttempt.getRetryDelay());
131     assertEquals(Duration.ofMillis(18L), thirdAttempt.getRpcTimeout());
132   }
133 
134   @Test
testTruncateToTotalTimeout()135   public void testTruncateToTotalTimeout() {
136     RetrySettings timeoutSettings =
137         retrySettings
138             .toBuilder()
139             .setInitialRpcTimeout(Duration.ofSeconds(4L))
140             .setMaxRpcTimeout(Duration.ofSeconds(4L))
141             .setTotalTimeout(Duration.ofSeconds(4L))
142             .build();
143     ExponentialRetryAlgorithm timeoutAlg = new ExponentialRetryAlgorithm(timeoutSettings, clock);
144 
145     TimedAttemptSettings firstAttempt = timeoutAlg.createFirstAttempt();
146     TimedAttemptSettings secondAttempt = timeoutAlg.createNextAttempt(firstAttempt);
147     assertThat(secondAttempt.getRpcTimeout()).isAtLeast(firstAttempt.getRpcTimeout());
148     assertThat(secondAttempt.getRpcTimeout()).isAtMost(Duration.ofSeconds(4L));
149 
150     TimedAttemptSettings thirdAttempt = timeoutAlg.createNextAttempt(secondAttempt);
151     assertThat(thirdAttempt.getRpcTimeout()).isAtMost(Duration.ofSeconds(4L));
152   }
153 
154   @Test
testShouldRetryTrue()155   public void testShouldRetryTrue() {
156     TimedAttemptSettings attempt = algorithm.createFirstAttempt();
157     for (int i = 0; i < 2; i++) {
158       attempt = algorithm.createNextAttempt(attempt);
159     }
160 
161     assertTrue(algorithm.shouldRetry(attempt));
162   }
163 
164   @Test
testShouldRetryFalseOnMaxAttempts()165   public void testShouldRetryFalseOnMaxAttempts() {
166     TimedAttemptSettings attempt = algorithm.createFirstAttempt();
167     for (int i = 0; i < 6; i++) {
168       assertTrue(algorithm.shouldRetry(attempt));
169       attempt = algorithm.createNextAttempt(attempt);
170     }
171 
172     assertFalse(algorithm.shouldRetry(attempt));
173   }
174 
175   // First attempt runs at 0ms
176   // Second attempt runs at 60ms if shouldRetry is true
177   // - RPC timeout is 2ms and Time Left is 140ms (shouldRetry == true)
178   // Third attempt runs at 60ms if shouldRetry is true
179   // - RPC timeout is 4ms and Time Left is 120ms (shouldRetry == true)
180   // Fourth attempt runs at 60ms if shouldRetry is true
181   // - RPC timeout is 8ms and Time Left is 20ms (shouldRetry == true)
182   // Fifth attempt runs at 60ms if shouldRetry is true
183   // - RPC timeout is 8ms and Time Left is -40ms (shouldRetry == false)
184   @Test
testShouldRetryFalseOnMaxTimeout()185   public void testShouldRetryFalseOnMaxTimeout() {
186     // Simulate each attempt with 60ms of clock time.
187     // "attempt" = RPC Timeout + createNextAttempt() and shouldRetry()
188     TimedAttemptSettings attempt = algorithm.createFirstAttempt();
189     clock.incrementNanoTime(Duration.ofMillis(60L).toNanos());
190     for (int i = 0; i < 3; i++) {
191       assertTrue(algorithm.shouldRetry(attempt));
192       attempt = algorithm.createNextAttempt(attempt);
193       clock.incrementNanoTime(Duration.ofMillis(60L).toNanos());
194     }
195     assertFalse(algorithm.shouldRetry(attempt));
196   }
197 }
198