Skip to content

Commit

Permalink
feat: add exponential backoff to api core (#102)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
dmbrooke authored Sep 12, 2023
1 parent 84dca2c commit 172c936
Show file tree
Hide file tree
Showing 13 changed files with 298 additions and 31 deletions.
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,11 @@
<artifactId>commons-codec</artifactId>
<version>1.16.0</version>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-retry</artifactId>
<version>1.7.0</version>
</dependency>


</dependencies>
Expand Down
6 changes: 3 additions & 3 deletions samples/DeleteOneDocument.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import com.coveo.pushapiclient.Source;
import com.coveo.pushapiclient.PushSource;

import java.io.IOException;
import java.net.http.HttpResponse;

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

try {
HttpResponse<String> response = source.deleteDocument("my_source_id", documentId, deleteChildren);
HttpResponse<String> response = source.deleteDocument(documentId, deleteChildren);
System.out.println(String.format("Delete document status: %s", response.statusCode()));
System.out.println(String.format("Delete document response: %s", response.body()));
} catch (IOException e) {
Expand Down
5 changes: 3 additions & 2 deletions samples/PushOneDocument.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import com.coveo.pushapiclient.BackoffOptionsBuilder;
import com.coveo.pushapiclient.DocumentBuilder;
import com.coveo.pushapiclient.Source;
import com.coveo.pushapiclient.PushSource;

import java.io.IOException;
import java.net.http.HttpResponse;

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

Expand Down
6 changes: 4 additions & 2 deletions samples/PushOneDocumentWithMetadata.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import com.coveo.pushapiclient.BackoffOptions;
import com.coveo.pushapiclient.BackoffOptionsBuilder;
import com.coveo.pushapiclient.DocumentBuilder;
import com.coveo.pushapiclient.Source;
import com.coveo.pushapiclient.PushSource;

import java.io.IOException;
import java.net.http.HttpResponse;
import java.util.HashMap;

public class PushOneDocumentWithMetadata {
public static void main(String[] args) {
Source source = new Source("my_api_key", "my_org_id");
PushSource source = new PushSource("my_api_key", "my_org_id", new BackoffOptionsBuilder().withTimeMultiple(1).build());
DocumentBuilder documentBuilder = new DocumentBuilder("https://my.document.uri", "My document title")
.withData("these words will be searchable")
.withAuthor("bob")
Expand Down
69 changes: 50 additions & 19 deletions src/main/java/com/coveo/pushapiclient/ApiCore.java
Original file line number Diff line number Diff line change
@@ -1,27 +1,70 @@
package com.coveo.pushapiclient;

import io.github.resilience4j.core.IntervalFunction;
import io.github.resilience4j.retry.Retry;
import io.github.resilience4j.retry.RetryConfig;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublisher;
import java.net.http.HttpResponse;
import java.util.function.Function;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

// TODO: LENS-934 - Support throttling
class ApiCore {
private final HttpClient httpClient;
private final Logger logger;
private final BackoffOptions options;

public ApiCore() {
this.httpClient = HttpClient.newHttpClient();
this.logger = LogManager.getLogger(ApiCore.class);
this(HttpClient.newHttpClient(), LogManager.getLogger(ApiCore.class));
}

public ApiCore(HttpClient httpClient, Logger logger) {
this(httpClient, logger, new BackoffOptionsBuilder().build());
}

public ApiCore(HttpClient httpClient, Logger logger, BackoffOptions options) {
this.httpClient = httpClient;
this.logger = logger;
this.options = options;
}

public HttpResponse<String> callApiWithRetries(HttpRequest request)
throws IOException, InterruptedException {
IntervalFunction intervalFn =
IntervalFunction.ofExponentialRandomBackoff(
this.options.getRetryAfter(), this.options.getTimeMultiple());

RetryConfig retryConfig =
RetryConfig.<HttpResponse<String>>custom()
.maxAttempts(this.options.getMaxRetries())
.intervalFunction(intervalFn)
.retryOnResult(response -> response != null && response.statusCode() == 429)
.build();

Retry retry = Retry.of("platformRequest", retryConfig);

Function<HttpRequest, HttpResponse<String>> retryRequestFn =
Retry.decorateFunction(retry, req -> sendRequest(req));

return retryRequestFn.apply(request);
}

public HttpResponse<String> sendRequest(HttpRequest request) {
String uri = request.uri().toString();
String reqMethod = request.method();
this.logger.debug(reqMethod + " " + uri);
try {
HttpResponse<String> response =
this.httpClient.send(request, HttpResponse.BodyHandlers.ofString());
this.logResponse(response);
return response;
} catch (IOException | InterruptedException e) {
throw new Error(e.getMessage());
}
}

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

public HttpResponse<String> post(URI uri, String[] headers, BodyPublisher body)
throws IOException, InterruptedException {
this.logger.debug("POST " + uri);
HttpRequest request = HttpRequest.newBuilder().headers(headers).uri(uri).POST(body).build();
HttpResponse<String> response =
this.httpClient.send(request, HttpResponse.BodyHandlers.ofString());
this.logResponse(response);
HttpResponse<String> response = this.callApiWithRetries(request);
return response;
}

public HttpResponse<String> put(URI uri, String[] headers, BodyPublisher body)
throws IOException, InterruptedException {
this.logger.debug("PUT " + uri);
HttpRequest request = HttpRequest.newBuilder().headers(headers).uri(uri).PUT(body).build();
HttpResponse<String> response =
this.httpClient.send(request, HttpResponse.BodyHandlers.ofString());
this.logResponse(response);
HttpResponse<String> response = this.callApiWithRetries(request);
return response;
}

public HttpResponse<String> delete(URI uri, String[] headers)
throws IOException, InterruptedException {
this.logger.debug("DELETE " + uri);
HttpRequest request = HttpRequest.newBuilder().headers(headers).uri(uri).DELETE().build();
HttpResponse<String> response =
this.httpClient.send(request, HttpResponse.BodyHandlers.ofString());
this.logResponse(response);
HttpResponse<String> response = this.callApiWithRetries(request);
return response;
}

public HttpResponse<String> delete(URI uri, String[] headers, BodyPublisher body)
throws IOException, InterruptedException {
this.logger.debug("DELETE " + uri);
HttpRequest request =
HttpRequest.newBuilder().headers(headers).uri(uri).method("DELETE", body).build();
HttpResponse<String> response =
this.httpClient.send(request, HttpResponse.BodyHandlers.ofString());
this.logResponse(response);
HttpResponse<String> response = this.callApiWithRetries(request);
return response;
}

Expand Down
25 changes: 25 additions & 0 deletions src/main/java/com/coveo/pushapiclient/BackoffOptions.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.coveo.pushapiclient;

public class BackoffOptions {
private final int retryAfter;
private final int maxRetries;
private final int timeMultiple;

public BackoffOptions(int retryAfter, int maxRetries, int timeMultiple) {
this.retryAfter = retryAfter;
this.maxRetries = maxRetries;
this.timeMultiple = timeMultiple;
}

public int getRetryAfter() {
return this.retryAfter;
}

public int getMaxRetries() {
return this.maxRetries;
}

public int getTimeMultiple() {
return this.timeMultiple;
}
}
30 changes: 30 additions & 0 deletions src/main/java/com/coveo/pushapiclient/BackoffOptionsBuilder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.coveo.pushapiclient;

public class BackoffOptionsBuilder {
public static final int DEFAULT_RETRY_AFTER = 5000;
public static final int DEFAULT_MAX_RETRIES = 50;
public static final int DEFAULT_TIME_MULTIPLE = 2;

private int retryAfter = DEFAULT_RETRY_AFTER;
private int maxRetries = DEFAULT_MAX_RETRIES;
private int timeMultiple = DEFAULT_TIME_MULTIPLE;

public BackoffOptionsBuilder withRetryAfter(int retryAfter) {
this.retryAfter = retryAfter;
return this;
}

public BackoffOptionsBuilder withMaxRetries(int maxRetries) {
this.maxRetries = maxRetries;
return this;
}

public BackoffOptionsBuilder withTimeMultiple(int timeMultiple) {
this.timeMultiple = timeMultiple;
return this;
}

public BackoffOptions build() {
return new BackoffOptions(this.retryAfter, this.maxRetries, this.timeMultiple);
}
}
52 changes: 49 additions & 3 deletions src/main/java/com/coveo/pushapiclient/PlatformClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ public class PlatformClient {
* @param organizationId The Coveo Organization identifier.
*/
public PlatformClient(String apiKey, String organizationId) {
this(apiKey, organizationId, new PlatformUrlBuilder().build());
this(
apiKey,
organizationId,
new PlatformUrlBuilder().build(),
new BackoffOptionsBuilder().build());
}

/**
Expand All @@ -38,9 +42,36 @@ public PlatformClient(String apiKey, String organizationId) {
* organization.
* @see <a href="https://docs.coveo.com/en/1718">Manage API Keys</a>
* @param organizationId The Coveo Organization identifier.
* @param platformUrl The PlatformUrl.
*/
public PlatformClient(String apiKey, String organizationId, PlatformUrl platformUrl) {
this(apiKey, organizationId, platformUrl, new BackoffOptionsBuilder().build());
}

/**
* Construct a PlatformClient
*
* @param apiKey An apiKey capable of pushing documents and managing sources in a Coveo
* organization.
* @see <a href="https://docs.coveo.com/en/1718">Manage API Keys</a>
* @param organizationId The Coveo Organization identifier.
* @param options The configuration options for exponential backoff.
*/
public PlatformClient(String apiKey, String organizationId, BackoffOptions options) {
this(apiKey, organizationId, new PlatformUrlBuilder().build(), options);
}

/**
* Construct a PlatformClient
*
* @param apiKey An apiKey capable of pushing documents and managing sources in a Coveo
* organization.
* @see <a href="https://docs.coveo.com/en/1718">Manage API Keys</a>
* @param organizationId The Coveo Organization identifier.
* @param platformUrl The PlatformUrl.
* @param options The configuration options for exponential backoff.
*/
public PlatformClient(
String apiKey, String organizationId, PlatformUrl platformUrl, BackoffOptions options) {
this.apiKey = apiKey;
this.organizationId = organizationId;
this.api = new ApiCore();
Expand All @@ -57,9 +88,24 @@ public PlatformClient(String apiKey, String organizationId, PlatformUrl platform
* @param httpClient The HttpClient.
*/
public PlatformClient(String apiKey, String organizationId, HttpClient httpClient) {
this(apiKey, organizationId, httpClient, new BackoffOptionsBuilder().build());
}

/**
* Construct a PlatformClient
*
* @param apiKey An apiKey capable of pushing documents and managing sources in a Coveo
* organization.
* @see <a href="https://docs.coveo.com/en/1718">Manage API Keys</a>
* @param organizationId The Coveo Organization identifier.
* @param httpClient The HttpClient.
* @param options The configuration options for exponential backoff.
*/
public PlatformClient(
String apiKey, String organizationId, HttpClient httpClient, BackoffOptions options) {
this.apiKey = apiKey;
this.organizationId = organizationId;
this.api = new ApiCore(httpClient, LogManager.getLogger(ApiCore.class));
this.api = new ApiCore(httpClient, LogManager.getLogger(ApiCore.class), options);
this.platformUrl = new PlatformUrlBuilder().build();
}

Expand Down
6 changes: 5 additions & 1 deletion src/main/java/com/coveo/pushapiclient/PushService.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,17 @@ public class PushService {
private PushServiceInternal service;

public PushService(PushEnabledSource source) {
this(source, new BackoffOptionsBuilder().build());
}

public PushService(PushEnabledSource source, BackoffOptions options) {
String apiKey = source.getApiKey();
String organizationId = source.getOrganizationId();
PlatformUrl platformUrl = source.getPlatformUrl();
UploadStrategy uploader = this.getUploadStrategy();
DocumentUploadQueue queue = new DocumentUploadQueue(uploader);

this.platformClient = new PlatformClient(apiKey, organizationId, platformUrl);
this.platformClient = new PlatformClient(apiKey, organizationId, platformUrl, options);
this.service = new PushServiceInternal(queue);
this.source = source;
}
Expand Down
40 changes: 40 additions & 0 deletions src/main/java/com/coveo/pushapiclient/PushSource.java
Original file line number Diff line number Diff line change
Expand Up @@ -125,13 +125,53 @@ public static PushSource fromPlatformUrl(
return new PushSource(apiKey, organizationId, sourceId, platformUrl);
}

/**
* Create a Push source instance
*
* @param apiKey The API key used for all operations regarding your source.
* <p>Ensure your API key has the required privileges for the operation you will be performing
* *
* <p>For more information about which privileges are required, see <a href=
* "https://docs.coveo.com/en/1707#sources-domain">Privilege Reference.</a>
* @param organizationId The unique identifier of your organization.
* <p>The Organization Id can be retrieved in the URL of your Coveo organization.
* @param sourceId The unique identifier of the target Push source.
* <p>The Source Id can be retrieved when you edit your source in the <a href=
* "https://docs.coveo.com/en/183/glossary/coveo-administration-console">Coveo Administration
* Console</a>
* @param platformUrl The object containing additional information on the URL endpoint. You can
* use the {@link PlatformUrl} when your organization is located in a non-default Coveo
* environement and/or region. When not specified, the default platform URL values will be
* used: {@link PlatformUrl#DEFAULT_ENVIRONMENT} and {@link PlatformUrl#DEFAULT_REGION}
* @param options The configuration options for exponential backoff.
*/
public static PushSource fromPlatformUrl(
String apiKey,
String organizationId,
String sourceId,
PlatformUrl platformUrl,
BackoffOptions options) {
return new PushSource(apiKey, organizationId, sourceId, platformUrl, options);
}

private PushSource(
String apiKey, String organizationId, String sourceId, PlatformUrl platformUrl) {
this.apiKey = apiKey;
this.urlExtractor = new ApiUrl(organizationId, sourceId, platformUrl);
this.platformClient = new PlatformClient(apiKey, organizationId, platformUrl);
}

private PushSource(
String apiKey,
String organizationId,
String sourceId,
PlatformUrl platformUrl,
BackoffOptions options) {
this.apiKey = apiKey;
this.urlExtractor = new ApiUrl(organizationId, sourceId, platformUrl);
this.platformClient = new PlatformClient(apiKey, organizationId, platformUrl, options);
}

/**
* Create or update a security identity.
*
Expand Down
Loading

0 comments on commit 172c936

Please sign in to comment.