diff --git a/sdk/pom.xml b/sdk/pom.xml
index b146fe72f42..b1231173d11 100644
--- a/sdk/pom.xml
+++ b/sdk/pom.xml
@@ -372,6 +372,11 @@
${junit-version}
test
+
+ com.squareup.okhttp3
+ mockwebserver
+ ${okhttp-mockwebserver-version}
+
org.hamcrest
hamcrest-library
@@ -395,6 +400,7 @@
1.0.0
2.11.1
4.9.1
+ 4.11.0
2.10.0
2.8.5
1.8.3
diff --git a/sdk/src/main/java/com/finbourne/lusid/utilities/ApiClientBuilder.java b/sdk/src/main/java/com/finbourne/lusid/utilities/ApiClientBuilder.java
index 6cd094e56e8..fdae1138d7d 100644
--- a/sdk/src/main/java/com/finbourne/lusid/utilities/ApiClientBuilder.java
+++ b/sdk/src/main/java/com/finbourne/lusid/utilities/ApiClientBuilder.java
@@ -2,9 +2,9 @@
import com.finbourne.lusid.ApiClient;
import com.finbourne.lusid.utilities.auth.HttpLusidTokenProvider;
+import com.finbourne.lusid.utilities.auth.RefreshingTokenProvider;
import com.finbourne.lusid.utilities.auth.LusidToken;
import com.finbourne.lusid.utilities.auth.LusidTokenException;
-import com.finbourne.lusid.utilities.auth.RefreshingTokenProvider;
import okhttp3.OkHttpClient;
/**
@@ -15,73 +15,88 @@ public class ApiClientBuilder {
private static final int DEFAULT_TIMEOUT_SECONDS = 10;
/**
- * Builds an ApiClient implementation configured against a secrets file. Typically used
- * for communicating with LUSID via the APIs (e.g. {@link com.finbourne.lusid.api.TransactionPortfoliosApi}, {@link com.finbourne.lusid.api.QuotesApi}.
+ * Builds an ApiClient implementation configured against a secrets file.
+ * Typically used
+ * for communicating with LUSID via the APIs (e.g.
+ * {@link com.finbourne.lusid.api.TransactionPortfoliosApi},
+ * {@link com.finbourne.lusid.api.QuotesApi}.
*
- * ApiClient implementation enables use of REFRESH tokens (see https://support.finbourne.com/using-a-refresh-token)
+ * ApiClient implementation enables use of REFRESH tokens (see
+ * https://support.finbourne.com/using-a-refresh-token)
* and automatically handles token refreshing on expiry.
*
* @param apiConfiguration configuration to connect to LUSID API
* @return
*
- * @throws LusidTokenException on failing to authenticate and retrieve an initial {@link LusidToken}
+ * @throws LusidTokenException on failing to authenticate and retrieve an
+ * initial {@link LusidToken}
*/
public ApiClient build(ApiConfiguration apiConfiguration) throws LusidTokenException {
- return this.build(apiConfiguration, DEFAULT_TIMEOUT_SECONDS, DEFAULT_TIMEOUT_SECONDS, DEFAULT_TIMEOUT_SECONDS);
+ return this.build(apiConfiguration, DEFAULT_TIMEOUT_SECONDS, DEFAULT_TIMEOUT_SECONDS, DEFAULT_TIMEOUT_SECONDS,
+ 3);
}
/**
- * Builds an ApiClient implementation configured against a secrets file. Typically used
+ * Builds an ApiClient implementation configured against a secrets file.
+ * Typically used
* for communicating with LUSID via the APIs
*
- * ApiClient implementation enables use of REFRESH tokens (see https://support.finbourne.com/using-a-refresh-token)
+ * ApiClient implementation enables use of REFRESH tokens (see
+ * https://support.finbourne.com/using-a-refresh-token)
* and automatically handles token refreshing on expiry.
*
* @param apiConfiguration configuration to connect to {{application_camel}} API
- * @param readTimeout read timeout in seconds
- * @param writeTimeout write timeout in seconds
+ * @param readTimeout read timeout in seconds
+ * @param writeTimeout write timeout in seconds
* @return
*
- * @throws LusidTokenException on failing to authenticate and retrieve an initial {@link LusidTokenException}
+ * @throws LusidTokenException on failing to authenticate and retrieve an
+ * initial {@link LusidTokenException}
*/
- public ApiClient build(ApiConfiguration apiConfiguration, int readTimeout, int writeTimeout) throws LusidTokenException {
- return this.build(apiConfiguration, readTimeout, writeTimeout, DEFAULT_TIMEOUT_SECONDS);
+ public ApiClient build(ApiConfiguration apiConfiguration, int readTimeout, int writeTimeout)
+ throws LusidTokenException {
+ return build(apiConfiguration, readTimeout, writeTimeout, DEFAULT_TIMEOUT_SECONDS, 3);
}
/**
- * Builds an ApiClient implementation configured against a secrets file. Typically used
+ * Builds an ApiClient implementation configured against a secrets file.
+ * Typically used
* for communicating with LUSID via the APIs
*
- * ApiClient implementation enables use of REFRESH tokens (see https://support.finbourne.com/using-a-refresh-token)
+ * ApiClient implementation enables use of REFRESH tokens (see
+ * https://support.finbourne.com/using-a-refresh-token)
* and automatically handles token refreshing on expiry.
*
* @param apiConfiguration configuration to connect to {{application_camel}} API
- * @param readTimeout read timeout in seconds
- * @param writeTimeout write timeout in seconds
- * @param connectTimeout connection timeout in seconds
+ * @param readTimeout read timeout in seconds
+ * @param writeTimeout write timeout in seconds
+ * @param connectTimeout connection timeout in seconds
* @return
*
- * @throws LusidTokenException on failing to authenticate and retrieve an initial {@link LusidTokenException}
+ * @throws LusidTokenException on failing to authenticate and retrieve an
+ * initial {@link LusidTokenException}
*/
- public ApiClient build(ApiConfiguration apiConfiguration, int readTimeout, int writeTimeout, int connectTimeout) throws LusidTokenException {
-
+ public ApiClient build(ApiConfiguration apiConfiguration, int readTimeout, int writeTimeout, int connectTimeout,
+ int retryMaxAttempts)
+ throws LusidTokenException {
// http client to use for api and auth calls
OkHttpClient httpClient = createHttpClient(apiConfiguration, readTimeout, writeTimeout, connectTimeout);
if (apiConfiguration.getPersonalAccessToken() != null && apiConfiguration.getApiUrl() != null) {
- // use Personal Access Token
+ // use Personal Access Token
LusidToken lusidToken = new LusidToken(apiConfiguration.getPersonalAccessToken(), null, null);
- ApiClient defaultApiClient = createDefaultApiClient(apiConfiguration, httpClient, lusidToken);
+ ApiClient defaultApiClient = createDefaultApiClient(apiConfiguration, httpClient, lusidToken,
+ retryMaxAttempts);
return defaultApiClient;
- }
- else {
+ } else {
// token provider to keep client authenticated with automated token refreshing
- RefreshingTokenProvider refreshingTokenProvider = new RefreshingTokenProvider(new HttpLusidTokenProvider(apiConfiguration, httpClient));
+ RefreshingTokenProvider refreshingTokenProvider = new RefreshingTokenProvider(
+ new HttpLusidTokenProvider(apiConfiguration, httpClient));
LusidToken lusidToken = refreshingTokenProvider.get();
// setup api client that managed submissions with the latest token
@@ -90,8 +105,14 @@ public ApiClient build(ApiConfiguration apiConfiguration, int readTimeout, int w
}
}
- ApiClient createDefaultApiClient(ApiConfiguration apiConfiguration, OkHttpClient httpClient, LusidToken lusidToken) throws LusidTokenException {
- ApiClient apiClient = createApiClient();
+ ApiClient createDefaultApiClient(ApiConfiguration apiConfiguration, OkHttpClient httpClient,
+ LusidToken lusidToken) throws LusidTokenException {
+ return createDefaultApiClient(apiConfiguration, httpClient, lusidToken, 3);
+ }
+
+ ApiClient createDefaultApiClient(ApiConfiguration apiConfiguration, OkHttpClient httpClient,
+ LusidToken lusidToken, int retryMaxAttempts) throws LusidTokenException {
+ ApiClient apiClient = createApiClient(retryMaxAttempts);
apiClient.setHttpClient(httpClient);
@@ -107,15 +128,20 @@ ApiClient createDefaultApiClient(ApiConfiguration apiConfiguration, OkHttpClient
}
apiClient.setBasePath(apiConfiguration.getApiUrl());
- return apiClient;
+ return apiClient;
}
- private OkHttpClient createHttpClient(ApiConfiguration apiConfiguration, int readTimeout, int writeTimeout, int connectTimeout){
+ private OkHttpClient createHttpClient(ApiConfiguration apiConfiguration, int readTimeout, int writeTimeout,
+ int connectTimeout) {
return new HttpClientFactory().build(apiConfiguration, readTimeout, writeTimeout, connectTimeout);
}
// allows us to mock out api client for testing purposes
- ApiClient createApiClient(){
- return new ApiClient();
+ ApiClient createApiClient() {
+ return new RetryingApiClient(3);
+ }
+
+ ApiClient createApiClient(int maxAttempts) {
+ return new RetryingApiClient(maxAttempts);
}
}
diff --git a/sdk/src/main/java/com/finbourne/lusid/utilities/LusidApiFactoryBuilder.java b/sdk/src/main/java/com/finbourne/lusid/utilities/LusidApiFactoryBuilder.java
index 751c5b1cc1b..654bafc91de 100644
--- a/sdk/src/main/java/com/finbourne/lusid/utilities/LusidApiFactoryBuilder.java
+++ b/sdk/src/main/java/com/finbourne/lusid/utilities/LusidApiFactoryBuilder.java
@@ -8,40 +8,50 @@ public class LusidApiFactoryBuilder {
private static final int DEFAULT_TIMEOUT_SECONDS = 10;
/**
- * Build a {@link LusidApiFactory} defining configuration using environment variables. For details on the environment arguments see https://support.lusid.com/getting-started-with-apis-sdks.
+ * Build a {@link LusidApiFactory} defining configuration using environment
+ * variables. For details on the environment arguments see
+ * https://support.lusid.com/getting-started-with-apis-sdks.
*
* @return
*/
public static LusidApiFactory build() throws ApiConfigurationException, LusidTokenException {
if (!areRequiredEnvironmentVariablesSet()) {
- throw new ApiConfigurationException("Environment variables to configure LUSID API client have not been set. See " +
- " see https://support.lusid.com/getting-started-with-apis-sdks for details.");
+ throw new ApiConfigurationException(
+ "Environment variables to configure LUSID API client have not been set. See " +
+ " see https://support.lusid.com/getting-started-with-apis-sdks for details.");
}
return createApiFactory(null, DEFAULT_TIMEOUT_SECONDS, DEFAULT_TIMEOUT_SECONDS, DEFAULT_TIMEOUT_SECONDS);
}
/**
- * Build a {@link LusidApiFactory} using the specified configuration file. For details on the format of the configuration file see https://support.lusid.com/getting-started-with-apis-sdks.
+ * Build a {@link LusidApiFactory} using the specified configuration file. For
+ * details on the format of the configuration file see
+ * https://support.lusid.com/getting-started-with-apis-sdks.
*/
- public static LusidApiFactory build(String configurationFile) throws ApiConfigurationException, LusidTokenException {
+ public static LusidApiFactory build(String configurationFile)
+ throws ApiConfigurationException, LusidTokenException {
return build(configurationFile, 10, 10);
}
- public static LusidApiFactory build(String configurationFile, int readTimeout, int writeTimeout) throws ApiConfigurationException, LusidTokenException {
+ public static LusidApiFactory build(String configurationFile, int readTimeout, int writeTimeout)
+ throws ApiConfigurationException, LusidTokenException {
return createApiFactory(configurationFile, readTimeout, writeTimeout, DEFAULT_TIMEOUT_SECONDS);
}
- public static LusidApiFactory build(String configurationFile, int readTimeout, int writeTimeout, int connectTimeout) throws ApiConfigurationException, LusidTokenException {
+ public static LusidApiFactory build(String configurationFile, int readTimeout, int writeTimeout, int connectTimeout)
+ throws ApiConfigurationException, LusidTokenException {
return createApiFactory(configurationFile, readTimeout, writeTimeout, connectTimeout);
}
- private static LusidApiFactory createApiFactory(String configurationFile, int readTimeout, int writeTimeout, int connectTimeout) throws ApiConfigurationException, LusidTokenException {
+ private static LusidApiFactory createApiFactory(String configurationFile, int readTimeout, int writeTimeout,
+ int connectTimeout) throws ApiConfigurationException, LusidTokenException {
ApiConfiguration apiConfiguration = new ApiConfigurationBuilder().build(configurationFile);
- ApiClient apiClient = new ApiClientBuilder().build(apiConfiguration, readTimeout, writeTimeout, connectTimeout);
- return new LusidApiFactory(apiClient);
+ ApiClient apiClient = new ApiClientBuilder().build(apiConfiguration, readTimeout, writeTimeout, connectTimeout,
+ 3);
+ return new LusidApiFactory(apiClient);
}
- private static boolean areRequiredEnvironmentVariablesSet(){
+ private static boolean areRequiredEnvironmentVariablesSet() {
return ((System.getenv("FBN_TOKEN_URL") != null &&
System.getenv("FBN_USERNAME") != null &&
System.getenv("FBN_PASSWORD") != null &&
@@ -49,6 +59,6 @@ private static boolean areRequiredEnvironmentVariablesSet(){
System.getenv("FBN_CLIENT_SECRET") != null &&
System.getenv("FBN_LUSID_API_URL") != null) ||
(System.getenv("FBN_TOKEN_URL") != null &&
- System.getenv("FBN_ACCESS_TOKEN") != null));
+ System.getenv("FBN_ACCESS_TOKEN") != null));
}
}
diff --git a/sdk/src/main/java/com/finbourne/lusid/utilities/RetryingApiClient.java b/sdk/src/main/java/com/finbourne/lusid/utilities/RetryingApiClient.java
new file mode 100644
index 00000000000..0e7c356e861
--- /dev/null
+++ b/sdk/src/main/java/com/finbourne/lusid/utilities/RetryingApiClient.java
@@ -0,0 +1,152 @@
+package com.finbourne.lusid.utilities;
+
+import java.lang.reflect.Type;
+
+import java.util.List;
+import java.util.Map;
+
+import com.finbourne.lusid.ApiCallback;
+import com.finbourne.lusid.ApiClient;
+import com.finbourne.lusid.ApiException;
+import com.finbourne.lusid.ApiResponse;
+
+import okhttp3.Call;
+
+public class RetryingApiClient extends ApiClient {
+ private int maxAttempts;
+
+ private void setMaxAttempts(int maxAttempts) throws IllegalArgumentException {
+ if (maxAttempts < 1) {
+ throw new IllegalArgumentException("maxAttempts must be greater than 0");
+ }
+ this.maxAttempts = maxAttempts;
+
+ }
+
+ /*
+ * Constructor for RetringApiClient
+ * specifying max number of attempts to retry on a 429 response
+ */
+ public RetryingApiClient(int maxAttempts) throws IllegalArgumentException {
+ super();
+ setMaxAttempts(maxAttempts);
+ }
+
+ /**
+ * Execute HTTP call and deserialize the HTTP response body into the given
+ * return type.
+ *
+ * @param returnType The return type used to deserialize HTTP response body
+ * @param The return type corresponding to (same with) returnType
+ * @param call Call
+ * @return ApiResponse object containing response status, headers and
+ * data, which is a Java object deserialized from response body and
+ * would be null
+ * when returnType is null.
+ * @throws ApiException If fail to execute the call
+ */
+ public ApiResponse execute(Call call, Type returnType) throws ApiException {
+ for (int attempt = 1; attempt < maxAttempts; attempt++) {
+ try {
+ return super.execute(call, returnType);
+ } catch (ApiException e) {
+ if (e.getCode() != 429 || maxAttempts == attempt) {
+ throw e;
+ } else {
+ call = call.clone();
+ Map> responseHeaders = e.getResponseHeaders();
+ try {
+ String retryAfterHeader = responseHeaders.get("retry-after").get(0);
+ long retryAfter = Long.parseLong(retryAfterHeader);
+ Thread.sleep(retryAfter);
+ } catch (InterruptedException exc) {
+ throw new ApiException("Failed to retry, thread interrupted", e, 429, responseHeaders,
+ e.getResponseBody());
+
+ } catch (NullPointerException exc) {
+ throw new ApiException("Retry-After header unavailable", e,
+ 429, responseHeaders,
+ e.getResponseBody());
+ } catch (NumberFormatException exc) {
+ throw new ApiException("Failed to parse Retry-After header", e,
+ 429, responseHeaders,
+ e.getResponseBody());
+ }
+ }
+ }
+ }
+ return super.execute(call, returnType);
+ }
+
+ /*
+ * Wrap original callback in callback that retries on 429
+ */
+ private ApiCallback buildRetryingCallback(Call call, final Type returnType, final ApiCallback callback,
+ int attempt) {
+
+ ApiCallback retryingCallback = new ApiCallback() {
+ @Override
+ public void onFailure(ApiException e, int statusCode, Map> responseHeaders) {
+ if (statusCode != 429 || attempt >= maxAttempts) {
+ callback.onFailure(e, statusCode, responseHeaders);
+ } else {
+ String retryAfterHeader = responseHeaders.get("retry-after").get(0);
+ long retryAfter = Long.parseLong(retryAfterHeader);
+ try {
+ Thread.sleep(retryAfter);
+ } catch (InterruptedException exc) {
+ callback.onFailure(
+ new ApiException("Failed to retry, thread interrupted", e, 429, responseHeaders,
+ e.getResponseBody()),
+ statusCode, responseHeaders);
+ } catch (NullPointerException exc) {
+ callback.onFailure(
+ new ApiException("Retry-After header unavailable", e,
+ 429, responseHeaders,
+ e.getResponseBody()),
+ statusCode, responseHeaders);
+ } catch (NumberFormatException exc) {
+ callback.onFailure(
+ new ApiException("Failed to parse Retry-After header", e,
+ 429, responseHeaders,
+ e.getResponseBody()),
+ statusCode, responseHeaders);
+ }
+ // Retry with new callback - maintain count of attempts
+ RetryingApiClient.super.executeAsync(call.clone(), returnType,
+ buildRetryingCallback(call, returnType, callback, attempt + 1));
+ }
+ }
+
+ @Override
+ public void onSuccess(T result, int statusCode, Map> responseHeaders) {
+ callback.onSuccess(result, statusCode, responseHeaders);
+ }
+
+ @Override
+ public void onUploadProgress(long bytesWritten, long contentLength, boolean done) {
+ callback.onUploadProgress(bytesWritten, contentLength, done);
+ }
+
+ @Override
+ public void onDownloadProgress(long bytesRead, long contentLength, boolean done) {
+ callback.onDownloadProgress(bytesRead, contentLength, done);
+ }
+ };
+ return retryingCallback;
+ }
+
+ /**
+ * Execute HTTP call asynchronously. Retry on 429s
+ *
+ * @param Type
+ * @param call The callback to be executed when the API call finishes
+ * @param returnType Return type
+ * @param callback ApiCallback
+ * @see #execute(Call, Type)
+ */
+ public void executeAsync(Call call, final Type returnType, final ApiCallback callback) {
+ super.executeAsync(call, returnType, buildRetryingCallback(call, returnType, callback, 1));
+ }
+
+}
diff --git a/sdk/src/test/integration/java/com/finbourne/lusid/utilities/ApiClientBuilderTests.java b/sdk/src/test/integration/java/com/finbourne/lusid/utilities/ApiClientBuilderTests.java
index 25ac3c29c2b..552ecff289c 100644
--- a/sdk/src/test/integration/java/com/finbourne/lusid/utilities/ApiClientBuilderTests.java
+++ b/sdk/src/test/integration/java/com/finbourne/lusid/utilities/ApiClientBuilderTests.java
@@ -26,27 +26,31 @@ public class ApiClientBuilderTests {
public ExpectedException thrown = ExpectedException.none();
@Before
- public void setUp(){
+ public void setUp() {
apiClientBuilder = new ApiClientBuilder();
}
@Test
- public void build_OnValidConfigurationFile_ShouldBuildKeepLiveApiClient() throws ApiConfigurationException, LusidTokenException {
- // This test assumes default secrets file is valid without a PAT. Same assertion as all other integration tests.
+ public void build_OnValidConfigurationFile_ShouldBuildKeepLiveApiClient()
+ throws ApiConfigurationException, LusidTokenException {
+ // This test assumes default secrets file is valid without a PAT. Same assertion
+ // as all other integration tests.
ApiConfiguration apiConfiguration = new ApiConfigurationBuilder().build(CredentialsSource.credentialsFile);
ApiClient apiClient = new ApiClientBuilder().build(apiConfiguration);
- // running with no exceptions ensures client built correctly with no configuration or token creation exceptions
- assertThat("Unexpected extended implementation of ApiClient for default build." ,
+ // running with no exceptions ensures client built correctly with no
+ // configuration or token creation exceptions
+ assertThat("Unexpected extended implementation of ApiClient for default build.",
apiClient, instanceOf(RefreshingTokenApiClient.class));
}
@Test
- public void build_WithValidPAT_ShouldBuildKeepLiveApiClient() throws ApiConfigurationException, LusidTokenException {
+ public void build_WithValidPAT_ShouldBuildKeepLiveApiClient()
+ throws ApiConfigurationException, LusidTokenException {
ApiConfiguration apiConfiguration = new ApiConfigurationBuilder().build("secrets-pat.json");
ApiClient apiClient = new ApiClientBuilder().build(apiConfiguration);
- OAuth auth = (OAuth)apiClient.getAuthentication("oauth2");
+ OAuth auth = (OAuth) apiClient.getAuthentication("oauth2");
assertThat(auth.getAccessToken(), equalTo(apiConfiguration.getPersonalAccessToken()));
}
@@ -58,7 +62,7 @@ public void build_BadTokenConfigurationFile_ShouldThrowException() throws LusidT
ApiClient apiClient = new ApiClientBuilder().build(apiConfiguration);
}
- private ApiConfiguration getBadTokenConfiguration(){
+ private ApiConfiguration getBadTokenConfiguration() {
return new ApiConfiguration(
"https://some-non-existing-test-instance.doesnotexist.com/oauth2/doesnotexist/v1/token",
"test.testing@finbourne.com",
@@ -68,8 +72,7 @@ private ApiConfiguration getBadTokenConfiguration(){
"https://some-non-existing-test-instance.lusid.com/api",
"non-existent",
// proxy strs
- "",8888,"",""
- );
+ "", 8888, "", "");
}
@Test
@@ -78,7 +81,8 @@ public void call_Api_With_Timeout() throws Exception {
int timeoutInSeconds = 20;
int defaultTimeout = 10;
ApiConfiguration apiConfiguration = new ApiConfigurationBuilder().build(CredentialsSource.credentialsFile);
- ApiClient apiClient = new ApiClientBuilder().build(apiConfiguration, timeoutInSeconds, timeoutInSeconds, timeoutInSeconds);
+ ApiClient apiClient = new ApiClientBuilder().build(apiConfiguration, timeoutInSeconds, timeoutInSeconds,
+ timeoutInSeconds, 3);
ScopesApi scopesApi = new ScopesApi(apiClient);
Instant start = Instant.now();
@@ -87,13 +91,12 @@ public void call_Api_With_Timeout() throws Exception {
start = Instant.now();
ResourceListOfScopeDefinition scopes = scopesApi.listScopes(null);
- // successful call within timeout
- }
- catch (Exception ex) {
+ // successful call within timeout
+ } catch (Exception ex) {
Instant finish = Instant.now();
long elapsed = Duration.between(start, finish).toMillis() / 1000;
- assertThat(elapsed, greaterThanOrEqualTo((long)defaultTimeout));
+ assertThat(elapsed, greaterThanOrEqualTo((long) defaultTimeout));
}
}
diff --git a/sdk/src/test/unit/java/com/finbourne/lusid/utilities/ApiClientBuilderTest.java b/sdk/src/test/unit/java/com/finbourne/lusid/utilities/ApiClientBuilderTest.java
index 2a5f3abe7d5..0566f87f52a 100644
--- a/sdk/src/test/unit/java/com/finbourne/lusid/utilities/ApiClientBuilderTest.java
+++ b/sdk/src/test/unit/java/com/finbourne/lusid/utilities/ApiClientBuilderTest.java
@@ -15,7 +15,7 @@ public class ApiClientBuilderTest {
private ApiClientBuilder apiClientBuilder;
- //mock dependencies
+ // mock dependencies
private ApiClient apiClient;
private OkHttpClient httpClient;
private ApiConfiguration apiConfiguration;
@@ -26,7 +26,7 @@ public class ApiClientBuilderTest {
public ExpectedException thrown = ExpectedException.none();
@Before
- public void setUp(){
+ public void setUp() {
httpClient = mock(OkHttpClient.class);
apiConfiguration = mock(ApiConfiguration.class);
lusidToken = mock(LusidToken.class);
@@ -34,7 +34,7 @@ public void setUp(){
apiClientBuilder = spy(new ApiClientBuilder());
// mock creation of default api client
- doReturn(apiClient).when(apiClientBuilder).createApiClient();
+ doReturn(apiClient).when(apiClientBuilder).createApiClient(any(Integer.class));
// mock default well formed lusid token
doReturn("access_token_01").when(lusidToken).getAccessToken();
}
@@ -50,7 +50,7 @@ public void createApiClient_OnProxyAddress_ShouldSetHttpClient() throws LusidTok
public void createApiClient_OnNoApplicationName_ShouldNotSetApplicationHeader() throws LusidTokenException {
doReturn(null).when(apiConfiguration).getApplicationName();
apiClientBuilder.createDefaultApiClient(apiConfiguration, httpClient, lusidToken);
- verify(apiClient,times(0)).addDefaultHeader(eq("X-LUSID-Application"), any());
+ verify(apiClient, times(0)).addDefaultHeader(eq("X-LUSID-Application"), any());
}
@Test
diff --git a/sdk/src/test/unit/java/com/finbourne/lusid/utilities/RetryingApiClientTest.java b/sdk/src/test/unit/java/com/finbourne/lusid/utilities/RetryingApiClientTest.java
new file mode 100644
index 00000000000..c5dab7215ee
--- /dev/null
+++ b/sdk/src/test/unit/java/com/finbourne/lusid/utilities/RetryingApiClientTest.java
@@ -0,0 +1,194 @@
+package com.finbourne.lusid.utilities;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+
+import java.io.IOException;
+
+import org.junit.Test;
+
+import com.finbourne.lusid.ApiCallback;
+import com.finbourne.lusid.ApiClient;
+import com.finbourne.lusid.ApiException;
+import com.finbourne.lusid.ApiResponse;
+
+import okhttp3.Call;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+
+public class RetryingApiClientTest {
+ @Test
+ public void constructApiClientWithLessThanOneAttemptsThrowsException() {
+ assertThrows(IllegalArgumentException.class, () -> new RetryingApiClient(0));
+ }
+
+ @Test
+ public void executeAttemptsNmkinus1TimesThenReturnsResult() throws IOException, ApiException {
+ final int MAX_ATTEMPTS = 2;
+
+ MockWebServer server = new MockWebServer();
+ MockResponse mockTooManyRequestsResponse = new MockResponse()
+ .setResponseCode(429)
+ .addHeader("Retry-After", 10);
+ MockResponse mockSuccessfulResponse = new MockResponse()
+ .setResponseCode(200);
+
+ server.enqueue(mockTooManyRequestsResponse);
+ server.enqueue(mockSuccessfulResponse);
+ server.start();
+
+ OkHttpClient client = new OkHttpClient();
+ Request request = new Request.Builder()
+ .url(server.url(""))
+ .build();
+ Call call = client.newCall(request);
+ ApiClient retryingApiClient = new RetryingApiClient(MAX_ATTEMPTS);
+ ApiResponse