Skip to content

Commit 172c936

Browse files
authored
feat: add exponential backoff to api core (#102)
* First set of changes * Use retry mechanism on all requests * More constructors, some docs comments * Actually leverage OOP 🤦 * Further implement options config * Backoff options fix + tests * Improved logic, APICore tests * Better docs strings, linting * Replace custom logic with Resilience4j Retry * Move to older version of resilience4j * Add samples, undo Source constructor changes * Linting
1 parent 84dca2c commit 172c936

13 files changed

+298
-31
lines changed

pom.xml

+5
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,11 @@
167167
<artifactId>commons-codec</artifactId>
168168
<version>1.16.0</version>
169169
</dependency>
170+
<dependency>
171+
<groupId>io.github.resilience4j</groupId>
172+
<artifactId>resilience4j-retry</artifactId>
173+
<version>1.7.0</version>
174+
</dependency>
170175

171176

172177
</dependencies>

samples/DeleteOneDocument.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1-
import com.coveo.pushapiclient.Source;
1+
import com.coveo.pushapiclient.PushSource;
22

33
import java.io.IOException;
44
import java.net.http.HttpResponse;
55

66
public class DeleteOneDocument {
77
public static void main(String[] args) {
8-
Source source = new Source("my_api_key", "my_org_id");
8+
PushSource source = new PushSource("my_api_key", "my_org_id");
99
String documentId = "https://my.document.uri";
1010
Boolean deleteChildren = true;
1111

1212
try {
13-
HttpResponse<String> response = source.deleteDocument("my_source_id", documentId, deleteChildren);
13+
HttpResponse<String> response = source.deleteDocument(documentId, deleteChildren);
1414
System.out.println(String.format("Delete document status: %s", response.statusCode()));
1515
System.out.println(String.format("Delete document response: %s", response.body()));
1616
} catch (IOException e) {

samples/PushOneDocument.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1+
import com.coveo.pushapiclient.BackoffOptionsBuilder;
12
import com.coveo.pushapiclient.DocumentBuilder;
2-
import com.coveo.pushapiclient.Source;
3+
import com.coveo.pushapiclient.PushSource;
34

45
import java.io.IOException;
56
import java.net.http.HttpResponse;
67

78
public class PushOneDocument {
89
public static void main(String[] args) {
9-
Source source = new Source("my_api_key", "my_org_id");
10+
PushSource source = new PushSource("my_api_key", "my_org_id", new BackoffOptionsBuilder().withMaxRetries(5).withRetryAfter(10000).build());
1011
DocumentBuilder documentBuilder = new DocumentBuilder("https://my.document.uri", "My document title")
1112
.withData("these words will be searchable");
1213

samples/PushOneDocumentWithMetadata.java

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1+
import com.coveo.pushapiclient.BackoffOptions;
2+
import com.coveo.pushapiclient.BackoffOptionsBuilder;
13
import com.coveo.pushapiclient.DocumentBuilder;
2-
import com.coveo.pushapiclient.Source;
4+
import com.coveo.pushapiclient.PushSource;
35

46
import java.io.IOException;
57
import java.net.http.HttpResponse;
68
import java.util.HashMap;
79

810
public class PushOneDocumentWithMetadata {
911
public static void main(String[] args) {
10-
Source source = new Source("my_api_key", "my_org_id");
12+
PushSource source = new PushSource("my_api_key", "my_org_id", new BackoffOptionsBuilder().withTimeMultiple(1).build());
1113
DocumentBuilder documentBuilder = new DocumentBuilder("https://my.document.uri", "My document title")
1214
.withData("these words will be searchable")
1315
.withAuthor("bob")

src/main/java/com/coveo/pushapiclient/ApiCore.java

+50-19
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,70 @@
11
package com.coveo.pushapiclient;
22

3+
import io.github.resilience4j.core.IntervalFunction;
4+
import io.github.resilience4j.retry.Retry;
5+
import io.github.resilience4j.retry.RetryConfig;
36
import java.io.IOException;
47
import java.net.URI;
58
import java.net.http.HttpClient;
69
import java.net.http.HttpRequest;
710
import java.net.http.HttpRequest.BodyPublisher;
811
import java.net.http.HttpResponse;
12+
import java.util.function.Function;
913
import org.apache.logging.log4j.LogManager;
1014
import org.apache.logging.log4j.Logger;
1115

12-
// TODO: LENS-934 - Support throttling
1316
class ApiCore {
1417
private final HttpClient httpClient;
1518
private final Logger logger;
19+
private final BackoffOptions options;
1620

1721
public ApiCore() {
18-
this.httpClient = HttpClient.newHttpClient();
19-
this.logger = LogManager.getLogger(ApiCore.class);
22+
this(HttpClient.newHttpClient(), LogManager.getLogger(ApiCore.class));
2023
}
2124

2225
public ApiCore(HttpClient httpClient, Logger logger) {
26+
this(httpClient, logger, new BackoffOptionsBuilder().build());
27+
}
28+
29+
public ApiCore(HttpClient httpClient, Logger logger, BackoffOptions options) {
2330
this.httpClient = httpClient;
2431
this.logger = logger;
32+
this.options = options;
33+
}
34+
35+
public HttpResponse<String> callApiWithRetries(HttpRequest request)
36+
throws IOException, InterruptedException {
37+
IntervalFunction intervalFn =
38+
IntervalFunction.ofExponentialRandomBackoff(
39+
this.options.getRetryAfter(), this.options.getTimeMultiple());
40+
41+
RetryConfig retryConfig =
42+
RetryConfig.<HttpResponse<String>>custom()
43+
.maxAttempts(this.options.getMaxRetries())
44+
.intervalFunction(intervalFn)
45+
.retryOnResult(response -> response != null && response.statusCode() == 429)
46+
.build();
47+
48+
Retry retry = Retry.of("platformRequest", retryConfig);
49+
50+
Function<HttpRequest, HttpResponse<String>> retryRequestFn =
51+
Retry.decorateFunction(retry, req -> sendRequest(req));
52+
53+
return retryRequestFn.apply(request);
54+
}
55+
56+
public HttpResponse<String> sendRequest(HttpRequest request) {
57+
String uri = request.uri().toString();
58+
String reqMethod = request.method();
59+
this.logger.debug(reqMethod + " " + uri);
60+
try {
61+
HttpResponse<String> response =
62+
this.httpClient.send(request, HttpResponse.BodyHandlers.ofString());
63+
this.logResponse(response);
64+
return response;
65+
} catch (IOException | InterruptedException e) {
66+
throw new Error(e.getMessage());
67+
}
2568
}
2669

2770
public HttpResponse<String> post(URI uri, String[] headers)
@@ -31,42 +74,30 @@ public HttpResponse<String> post(URI uri, String[] headers)
3174

3275
public HttpResponse<String> post(URI uri, String[] headers, BodyPublisher body)
3376
throws IOException, InterruptedException {
34-
this.logger.debug("POST " + uri);
3577
HttpRequest request = HttpRequest.newBuilder().headers(headers).uri(uri).POST(body).build();
36-
HttpResponse<String> response =
37-
this.httpClient.send(request, HttpResponse.BodyHandlers.ofString());
38-
this.logResponse(response);
78+
HttpResponse<String> response = this.callApiWithRetries(request);
3979
return response;
4080
}
4181

4282
public HttpResponse<String> put(URI uri, String[] headers, BodyPublisher body)
4383
throws IOException, InterruptedException {
44-
this.logger.debug("PUT " + uri);
4584
HttpRequest request = HttpRequest.newBuilder().headers(headers).uri(uri).PUT(body).build();
46-
HttpResponse<String> response =
47-
this.httpClient.send(request, HttpResponse.BodyHandlers.ofString());
48-
this.logResponse(response);
85+
HttpResponse<String> response = this.callApiWithRetries(request);
4986
return response;
5087
}
5188

5289
public HttpResponse<String> delete(URI uri, String[] headers)
5390
throws IOException, InterruptedException {
54-
this.logger.debug("DELETE " + uri);
5591
HttpRequest request = HttpRequest.newBuilder().headers(headers).uri(uri).DELETE().build();
56-
HttpResponse<String> response =
57-
this.httpClient.send(request, HttpResponse.BodyHandlers.ofString());
58-
this.logResponse(response);
92+
HttpResponse<String> response = this.callApiWithRetries(request);
5993
return response;
6094
}
6195

6296
public HttpResponse<String> delete(URI uri, String[] headers, BodyPublisher body)
6397
throws IOException, InterruptedException {
64-
this.logger.debug("DELETE " + uri);
6598
HttpRequest request =
6699
HttpRequest.newBuilder().headers(headers).uri(uri).method("DELETE", body).build();
67-
HttpResponse<String> response =
68-
this.httpClient.send(request, HttpResponse.BodyHandlers.ofString());
69-
this.logResponse(response);
100+
HttpResponse<String> response = this.callApiWithRetries(request);
70101
return response;
71102
}
72103

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.coveo.pushapiclient;
2+
3+
public class BackoffOptions {
4+
private final int retryAfter;
5+
private final int maxRetries;
6+
private final int timeMultiple;
7+
8+
public BackoffOptions(int retryAfter, int maxRetries, int timeMultiple) {
9+
this.retryAfter = retryAfter;
10+
this.maxRetries = maxRetries;
11+
this.timeMultiple = timeMultiple;
12+
}
13+
14+
public int getRetryAfter() {
15+
return this.retryAfter;
16+
}
17+
18+
public int getMaxRetries() {
19+
return this.maxRetries;
20+
}
21+
22+
public int getTimeMultiple() {
23+
return this.timeMultiple;
24+
}
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.coveo.pushapiclient;
2+
3+
public class BackoffOptionsBuilder {
4+
public static final int DEFAULT_RETRY_AFTER = 5000;
5+
public static final int DEFAULT_MAX_RETRIES = 50;
6+
public static final int DEFAULT_TIME_MULTIPLE = 2;
7+
8+
private int retryAfter = DEFAULT_RETRY_AFTER;
9+
private int maxRetries = DEFAULT_MAX_RETRIES;
10+
private int timeMultiple = DEFAULT_TIME_MULTIPLE;
11+
12+
public BackoffOptionsBuilder withRetryAfter(int retryAfter) {
13+
this.retryAfter = retryAfter;
14+
return this;
15+
}
16+
17+
public BackoffOptionsBuilder withMaxRetries(int maxRetries) {
18+
this.maxRetries = maxRetries;
19+
return this;
20+
}
21+
22+
public BackoffOptionsBuilder withTimeMultiple(int timeMultiple) {
23+
this.timeMultiple = timeMultiple;
24+
return this;
25+
}
26+
27+
public BackoffOptions build() {
28+
return new BackoffOptions(this.retryAfter, this.maxRetries, this.timeMultiple);
29+
}
30+
}

src/main/java/com/coveo/pushapiclient/PlatformClient.java

+49-3
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@ public class PlatformClient {
2828
* @param organizationId The Coveo Organization identifier.
2929
*/
3030
public PlatformClient(String apiKey, String organizationId) {
31-
this(apiKey, organizationId, new PlatformUrlBuilder().build());
31+
this(
32+
apiKey,
33+
organizationId,
34+
new PlatformUrlBuilder().build(),
35+
new BackoffOptionsBuilder().build());
3236
}
3337

3438
/**
@@ -38,9 +42,36 @@ public PlatformClient(String apiKey, String organizationId) {
3842
* organization.
3943
* @see <a href="https://docs.coveo.com/en/1718">Manage API Keys</a>
4044
* @param organizationId The Coveo Organization identifier.
41-
* @param platformUrl The PlatformUrl.
4245
*/
4346
public PlatformClient(String apiKey, String organizationId, PlatformUrl platformUrl) {
47+
this(apiKey, organizationId, platformUrl, new BackoffOptionsBuilder().build());
48+
}
49+
50+
/**
51+
* Construct a PlatformClient
52+
*
53+
* @param apiKey An apiKey capable of pushing documents and managing sources in a Coveo
54+
* organization.
55+
* @see <a href="https://docs.coveo.com/en/1718">Manage API Keys</a>
56+
* @param organizationId The Coveo Organization identifier.
57+
* @param options The configuration options for exponential backoff.
58+
*/
59+
public PlatformClient(String apiKey, String organizationId, BackoffOptions options) {
60+
this(apiKey, organizationId, new PlatformUrlBuilder().build(), options);
61+
}
62+
63+
/**
64+
* Construct a PlatformClient
65+
*
66+
* @param apiKey An apiKey capable of pushing documents and managing sources in a Coveo
67+
* organization.
68+
* @see <a href="https://docs.coveo.com/en/1718">Manage API Keys</a>
69+
* @param organizationId The Coveo Organization identifier.
70+
* @param platformUrl The PlatformUrl.
71+
* @param options The configuration options for exponential backoff.
72+
*/
73+
public PlatformClient(
74+
String apiKey, String organizationId, PlatformUrl platformUrl, BackoffOptions options) {
4475
this.apiKey = apiKey;
4576
this.organizationId = organizationId;
4677
this.api = new ApiCore();
@@ -57,9 +88,24 @@ public PlatformClient(String apiKey, String organizationId, PlatformUrl platform
5788
* @param httpClient The HttpClient.
5889
*/
5990
public PlatformClient(String apiKey, String organizationId, HttpClient httpClient) {
91+
this(apiKey, organizationId, httpClient, new BackoffOptionsBuilder().build());
92+
}
93+
94+
/**
95+
* Construct a PlatformClient
96+
*
97+
* @param apiKey An apiKey capable of pushing documents and managing sources in a Coveo
98+
* organization.
99+
* @see <a href="https://docs.coveo.com/en/1718">Manage API Keys</a>
100+
* @param organizationId The Coveo Organization identifier.
101+
* @param httpClient The HttpClient.
102+
* @param options The configuration options for exponential backoff.
103+
*/
104+
public PlatformClient(
105+
String apiKey, String organizationId, HttpClient httpClient, BackoffOptions options) {
60106
this.apiKey = apiKey;
61107
this.organizationId = organizationId;
62-
this.api = new ApiCore(httpClient, LogManager.getLogger(ApiCore.class));
108+
this.api = new ApiCore(httpClient, LogManager.getLogger(ApiCore.class), options);
63109
this.platformUrl = new PlatformUrlBuilder().build();
64110
}
65111

src/main/java/com/coveo/pushapiclient/PushService.java

+5-1
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,17 @@ public class PushService {
1010
private PushServiceInternal service;
1111

1212
public PushService(PushEnabledSource source) {
13+
this(source, new BackoffOptionsBuilder().build());
14+
}
15+
16+
public PushService(PushEnabledSource source, BackoffOptions options) {
1317
String apiKey = source.getApiKey();
1418
String organizationId = source.getOrganizationId();
1519
PlatformUrl platformUrl = source.getPlatformUrl();
1620
UploadStrategy uploader = this.getUploadStrategy();
1721
DocumentUploadQueue queue = new DocumentUploadQueue(uploader);
1822

19-
this.platformClient = new PlatformClient(apiKey, organizationId, platformUrl);
23+
this.platformClient = new PlatformClient(apiKey, organizationId, platformUrl, options);
2024
this.service = new PushServiceInternal(queue);
2125
this.source = source;
2226
}

src/main/java/com/coveo/pushapiclient/PushSource.java

+40
Original file line numberDiff line numberDiff line change
@@ -125,13 +125,53 @@ public static PushSource fromPlatformUrl(
125125
return new PushSource(apiKey, organizationId, sourceId, platformUrl);
126126
}
127127

128+
/**
129+
* Create a Push source instance
130+
*
131+
* @param apiKey The API key used for all operations regarding your source.
132+
* <p>Ensure your API key has the required privileges for the operation you will be performing
133+
* *
134+
* <p>For more information about which privileges are required, see <a href=
135+
* "https://docs.coveo.com/en/1707#sources-domain">Privilege Reference.</a>
136+
* @param organizationId The unique identifier of your organization.
137+
* <p>The Organization Id can be retrieved in the URL of your Coveo organization.
138+
* @param sourceId The unique identifier of the target Push source.
139+
* <p>The Source Id can be retrieved when you edit your source in the <a href=
140+
* "https://docs.coveo.com/en/183/glossary/coveo-administration-console">Coveo Administration
141+
* Console</a>
142+
* @param platformUrl The object containing additional information on the URL endpoint. You can
143+
* use the {@link PlatformUrl} when your organization is located in a non-default Coveo
144+
* environement and/or region. When not specified, the default platform URL values will be
145+
* used: {@link PlatformUrl#DEFAULT_ENVIRONMENT} and {@link PlatformUrl#DEFAULT_REGION}
146+
* @param options The configuration options for exponential backoff.
147+
*/
148+
public static PushSource fromPlatformUrl(
149+
String apiKey,
150+
String organizationId,
151+
String sourceId,
152+
PlatformUrl platformUrl,
153+
BackoffOptions options) {
154+
return new PushSource(apiKey, organizationId, sourceId, platformUrl, options);
155+
}
156+
128157
private PushSource(
129158
String apiKey, String organizationId, String sourceId, PlatformUrl platformUrl) {
130159
this.apiKey = apiKey;
131160
this.urlExtractor = new ApiUrl(organizationId, sourceId, platformUrl);
132161
this.platformClient = new PlatformClient(apiKey, organizationId, platformUrl);
133162
}
134163

164+
private PushSource(
165+
String apiKey,
166+
String organizationId,
167+
String sourceId,
168+
PlatformUrl platformUrl,
169+
BackoffOptions options) {
170+
this.apiKey = apiKey;
171+
this.urlExtractor = new ApiUrl(organizationId, sourceId, platformUrl);
172+
this.platformClient = new PlatformClient(apiKey, organizationId, platformUrl, options);
173+
}
174+
135175
/**
136176
* Create or update a security identity.
137177
*

0 commit comments

Comments
 (0)