Skip to content
This repository was archived by the owner on Jan 31, 2022. It is now read-only.

Commit 7aa2e32

Browse files
authored
AbstractClient: New retry logic (#192)
* AbstractClient: Extract method for timeout integrity checks * AbstractClient: New retry logic * IndexTest: Test host status retry logic * IndexTest: Fix obsolete or incomplete comments * IndexTest.hostStatus: Use randomUUID instead of String from random bytes * IndexTest.hostStatus: explicit method name * IndexTest.hostStatus: remove useless statuses * HostStatus: Simplify class * IndexTest: Update usage of searchAsync
1 parent 7796a90 commit 7aa2e32

File tree

2 files changed

+125
-14
lines changed

2 files changed

+125
-14
lines changed

algoliasearch/src/main/java/com/algolia/search/saas/AbstractClient.java

Lines changed: 80 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import java.net.URL;
4646
import java.util.ArrayList;
4747
import java.util.Arrays;
48+
import java.util.Date;
4849
import java.util.HashMap;
4950
import java.util.List;
5051
import java.util.Map;
@@ -87,6 +88,16 @@ public int hashCode() {
8788
}
8889
}
8990

91+
private static class HostStatus {
92+
boolean isUp = true;
93+
long lastTryTimestamp;
94+
95+
HostStatus(boolean isUp) {
96+
this.isUp = isUp;
97+
lastTryTimestamp = new Date().getTime();
98+
}
99+
}
100+
90101
// ----------------------------------------------------------------------
91102
// Constants
92103
// ----------------------------------------------------------------------
@@ -116,10 +127,14 @@ public int hashCode() {
116127
/** Read timeout for search requests (ms). */
117128
private int searchTimeout = 5000;
118129

130+
/** Delay to wait when a host is down before retrying it (ms). */
131+
private int hostDownDelay = 5000;
132+
119133
private final String applicationID;
120134
private final String apiKey;
121135
private List<String> readHosts;
122136
private List<String> writeHosts;
137+
private HashMap<String, HostStatus> hostStatuses = new HashMap<>();
123138

124139
/**
125140
* HTTP headers that will be sent with every request.
@@ -235,8 +250,7 @@ public int getConnectTimeout() {
235250
* @param connectTimeout The new connection timeout (ms).
236251
*/
237252
public void setConnectTimeout(int connectTimeout) {
238-
if (connectTimeout <= 0)
239-
throw new IllegalArgumentException();
253+
checkTimeout(connectTimeout);
240254
this.connectTimeout = connectTimeout;
241255
}
242256

@@ -255,8 +269,7 @@ public int getReadTimeout() {
255269
* @param readTimeout The default read timeout (ms).
256270
*/
257271
public void setReadTimeout(int readTimeout) {
258-
if (readTimeout <= 0)
259-
throw new IllegalArgumentException();
272+
checkTimeout(readTimeout);
260273
this.readTimeout = readTimeout;
261274
}
262275

@@ -275,11 +288,29 @@ public int getSearchTimeout() {
275288
* @param searchTimeout The read timeout for search requests (ms).
276289
*/
277290
public void setSearchTimeout(int searchTimeout) {
278-
if (searchTimeout <= 0)
279-
throw new IllegalArgumentException();
291+
checkTimeout(searchTimeout);
280292
this.searchTimeout = searchTimeout;
281293
}
282294

295+
/**
296+
* Get the timeout for retrying connection to a down host.
297+
*
298+
* @return The delay before connecting again to a down host (ms).
299+
*/
300+
public int getHostDownDelay() {
301+
return hostDownDelay;
302+
}
303+
304+
/**
305+
* Set the timeout for retrying connection to a down host.
306+
*
307+
* @param hostDownDelay The delay before connecting again to a down host (ms).
308+
*/
309+
public void setHostDownDelay(int hostDownDelay) {
310+
checkTimeout(hostDownDelay);
311+
this.hostDownDelay = hostDownDelay;
312+
}
313+
283314
/**
284315
* Add a software library to the list of user agents.
285316
*
@@ -333,6 +364,14 @@ private void updateUserAgents() {
333364
userAgentRaw = s.toString();
334365
}
335366

367+
private List<String> getReadHostsThatAreUp() {
368+
return hostsThatAreUp(readHosts);
369+
}
370+
371+
private List<String> getWriteHostsThatAreUp() {
372+
return hostsThatAreUp(writeHosts);
373+
}
374+
336375
// ----------------------------------------------------------------------
337376
// Utilities
338377
// ----------------------------------------------------------------------
@@ -345,27 +384,27 @@ private enum Method {
345384
}
346385

347386
protected byte[] getRequestRaw(String url, boolean search) throws AlgoliaException {
348-
return _requestRaw(Method.GET, url, null, readHosts, connectTimeout, search ? searchTimeout : readTimeout);
387+
return _requestRaw(Method.GET, url, null, getReadHostsThatAreUp(), connectTimeout, search ? searchTimeout : readTimeout);
349388
}
350389

351390
protected JSONObject getRequest(String url, boolean search) throws AlgoliaException {
352-
return _request(Method.GET, url, null, readHosts, connectTimeout, search ? searchTimeout : readTimeout);
391+
return _request(Method.GET, url, null, getReadHostsThatAreUp(), connectTimeout, search ? searchTimeout : readTimeout);
353392
}
354393

355394
protected JSONObject deleteRequest(String url) throws AlgoliaException {
356-
return _request(Method.DELETE, url, null, writeHosts, connectTimeout, readTimeout);
395+
return _request(Method.DELETE, url, null, getWriteHostsThatAreUp(), connectTimeout, readTimeout);
357396
}
358397

359398
protected JSONObject postRequest(String url, String obj, boolean readOperation) throws AlgoliaException {
360-
return _request(Method.POST, url, obj, (readOperation ? readHosts : writeHosts), connectTimeout, (readOperation ? searchTimeout : readTimeout));
399+
return _request(Method.POST, url, obj, (readOperation ? getReadHostsThatAreUp() : getWriteHostsThatAreUp()), connectTimeout, (readOperation ? searchTimeout : readTimeout));
361400
}
362401

363402
protected byte[] postRequestRaw(String url, String obj, boolean readOperation) throws AlgoliaException {
364-
return _requestRaw(Method.POST, url, obj, (readOperation ? readHosts : writeHosts), connectTimeout, (readOperation ? searchTimeout : readTimeout));
403+
return _requestRaw(Method.POST, url, obj, (readOperation ? getReadHostsThatAreUp() : getWriteHostsThatAreUp()), connectTimeout, (readOperation ? searchTimeout : readTimeout));
365404
}
366405

367406
protected JSONObject putRequest(String url, String obj) throws AlgoliaException {
368-
return _request(Method.PUT, url, obj, writeHosts, connectTimeout, readTimeout);
407+
return _request(Method.PUT, url, obj, getWriteHostsThatAreUp(), connectTimeout, readTimeout);
369408
}
370409

371410
/**
@@ -526,6 +565,7 @@ private byte[] _requestRaw(Method m, String url, String json, List<String> hosts
526565
if (stream == null) {
527566
throw new IOException(String.format("Null stream when reading connection (status %d)", code));
528567
}
568+
hostStatuses.put(host, new HostStatus(true));
529569

530570
final byte[] rawResponse;
531571
String encoding = hostConnection.getContentEncoding();
@@ -556,8 +596,8 @@ private byte[] _requestRaw(Method m, String url, String json, List<String> hosts
556596
catch (UnsupportedEncodingException e) { // fatal
557597
consumeQuietly(hostConnection);
558598
throw new AlgoliaException("Invalid encoding returned by server", e);
559-
}
560-
catch (IOException e) { // host error, continue on the next host
599+
} catch (IOException e) { // host error, continue on the next host
600+
hostStatuses.put(host, new HostStatus(false));
561601
consumeQuietly(hostConnection);
562602
errors.add(e);
563603
} finally {
@@ -599,6 +639,32 @@ private static void consumeQuietly(final HttpURLConnection connection) {
599639
}
600640
}
601641

642+
private void checkTimeout(int connectTimeout) {
643+
if (connectTimeout <= 0) {
644+
throw new IllegalArgumentException();
645+
}
646+
}
647+
648+
/**
649+
* Get the hosts that are not considered down in a given list.
650+
* @param hosts a list of hosts whose {@link HostStatus} will be checked.
651+
* @return the hosts considered up, or all hosts if none is known to be reachable.
652+
*/
653+
private List<String> hostsThatAreUp(List<String> hosts) {
654+
List<String> upHosts = new ArrayList<>();
655+
for (String host : hosts) {
656+
if (isUpOrCouldBeRetried(host)) {
657+
upHosts.add(host);
658+
}
659+
}
660+
return upHosts.isEmpty() ? hosts : upHosts;
661+
}
662+
663+
boolean isUpOrCouldBeRetried(String host) {
664+
HostStatus status = hostStatuses.get(host);
665+
return status == null || status.isUp || new Date().getTime() - status.lastTryTimestamp >= hostDownDelay;
666+
}
667+
602668
// ----------------------------------------------------------------------
603669
// Utils
604670
// ----------------------------------------------------------------------

algoliasearch/src/test/java/com/algolia/search/saas/IndexTest.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import java.util.HashMap;
4444
import java.util.List;
4545
import java.util.Map;
46+
import java.util.UUID;
4647

4748
import static org.junit.Assert.assertEquals;
4849
import static org.junit.Assert.assertFalse;
@@ -61,6 +62,7 @@
6162
* <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
6263
*/
6364

65+
@SuppressWarnings("unchecked") //Whitebox requires casts from Object
6466
@SuppressLint("DefaultLocale") //We use format for logging errors, locale issues are irrelevant
6567
public class IndexTest extends RobolectricTestCase {
6668
Client client;
@@ -1114,4 +1116,47 @@ public void doRequestCompleted(JSONObject content, AlgoliaException error) {
11141116
}
11151117
});
11161118
}
1119+
1120+
@Test
1121+
public void retryUsingHostStatus() throws Exception {
1122+
List<String> hostsArray = (List<String>) Whitebox.getInternalState(client, "readHosts");
1123+
String randomHostName = getRandomString() + "-dsn.algolia.biz";
1124+
final String nextHostName = hostsArray.get(1);
1125+
1126+
// Given a first host that timeouts, randomized to ensure no system caching
1127+
hostsArray.set(0, randomHostName);
1128+
Whitebox.setInternalState(client, "readHosts", hostsArray);
1129+
1130+
1131+
// Expect reachable hosts before any connection
1132+
assertTrue("Hosts should be considered up before first connection.", client.isUpOrCouldBeRetried(randomHostName));
1133+
assertTrue("Hosts should be considered up before first connection.", client.isUpOrCouldBeRetried(nextHostName));
1134+
1135+
// Expect success after a failing host
1136+
searchAsync();
1137+
1138+
// Expect down host after failed connection, up host after successful connection
1139+
assertFalse("A host that has failed recently should be considered down.", client.isUpOrCouldBeRetried(randomHostName));
1140+
assertTrue("A host that has succeeded recently should be considered up.", client.isUpOrCouldBeRetried(nextHostName));
1141+
1142+
hostsArray = (List<String>) Whitebox.getInternalState(client, "readHosts");
1143+
randomHostName = getRandomString() + "-dsn.algolia.biz";
1144+
1145+
// Given a short host delay and a first host that timeouts
1146+
final int delay = 100;
1147+
client.setHostDownDelay(delay);
1148+
hostsArray.set(0, randomHostName);
1149+
Whitebox.setInternalState(client, "readHosts", hostsArray);
1150+
1151+
// Expect success after a failing host
1152+
searchAsync();
1153+
1154+
// Expect host to be up again after the delay has passed
1155+
Thread.sleep(delay);
1156+
assertTrue("A host that has failed should be considered up once the delay is over.", client.isUpOrCouldBeRetried(randomHostName));
1157+
}
1158+
1159+
private String getRandomString() {
1160+
return UUID.randomUUID().toString();
1161+
}
11171162
}

0 commit comments

Comments
 (0)