Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Introduce error modeling and retries #25

Merged
merged 4 commits into from
Oct 6, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .openapi-generator/FILES
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ src/main/java/dev/openfga/sdk/api/client/ClientListRelationsRequest.java
src/main/java/dev/openfga/sdk/api/client/ClientReadRequest.java
src/main/java/dev/openfga/sdk/api/client/ClientTupleKey.java
src/main/java/dev/openfga/sdk/api/client/ClientWriteRequest.java
src/main/java/dev/openfga/sdk/api/client/HttpRequestAttempt.java
src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java
src/main/java/dev/openfga/sdk/api/configuration/ApiToken.java
src/main/java/dev/openfga/sdk/api/configuration/BaseConfiguration.java
Expand Down Expand Up @@ -166,6 +167,11 @@ src/main/java/dev/openfga/sdk/api/model/WriteAuthorizationModelRequest.java
src/main/java/dev/openfga/sdk/api/model/WriteAuthorizationModelResponse.java
src/main/java/dev/openfga/sdk/api/model/WriteRequest.java
src/main/java/dev/openfga/sdk/errors/ApiException.java
src/main/java/dev/openfga/sdk/errors/FgaApiAuthenticationError.java
src/main/java/dev/openfga/sdk/errors/FgaApiInternalError.java
src/main/java/dev/openfga/sdk/errors/FgaApiNotFoundError.java
src/main/java/dev/openfga/sdk/errors/FgaApiRateLimitExceededError.java
src/main/java/dev/openfga/sdk/errors/FgaApiValidationError.java
src/main/java/dev/openfga/sdk/errors/FgaInvalidParameterException.java
src/main/java/dev/openfga/sdk/util/Pair.java
src/main/java/dev/openfga/sdk/util/StringUtil.java
Expand Down
640 changes: 247 additions & 393 deletions src/main/java/dev/openfga/sdk/api/OpenFgaApi.java

Large diffs are not rendered by default.

33 changes: 7 additions & 26 deletions src/main/java/dev/openfga/sdk/api/auth/OAuth2Client.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,19 @@

package dev.openfga.sdk.api.auth;

import com.fasterxml.jackson.databind.ObjectMapper;
import dev.openfga.sdk.api.client.ApiClient;
import dev.openfga.sdk.api.client.HttpRequestAttempt;
import dev.openfga.sdk.api.configuration.*;
import dev.openfga.sdk.errors.ApiException;
import dev.openfga.sdk.errors.FgaInvalidParameterException;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Instant;
import java.util.concurrent.CompletableFuture;

public class OAuth2Client {
private final HttpClient httpClient;
private final ApiClient apiClient;
private final Credentials credentials;
private final ObjectMapper mapper;
private final AccessToken token = new AccessToken();
private final CredentialsFlowRequest authRequest;
private final String apiTokenIssuer;
Expand All @@ -37,14 +33,11 @@ public class OAuth2Client {
* Initializes a new instance of the {@link OAuth2Client} class
*
* @param configuration Configuration, including credentials, that can be used to retrieve an access tokens
* @param httpClient Http client
*/
public OAuth2Client(Configuration configuration, HttpClient httpClient, ObjectMapper mapper)
throws FgaInvalidParameterException {
public OAuth2Client(Configuration configuration, ApiClient apiClient) throws FgaInvalidParameterException {
this.credentials = configuration.getCredentials();

this.httpClient = httpClient;
this.mapper = mapper;
this.apiClient = apiClient;
this.apiTokenIssuer = credentials.getClientCredentials().getApiTokenIssuer();
this.authRequest = new CredentialsFlowRequest();
this.authRequest.setClientId(credentials.getClientCredentials().getClientId());
Expand Down Expand Up @@ -78,27 +71,15 @@ public CompletableFuture<String> getAccessToken() throws FgaInvalidParameterExce
private CompletableFuture<CredentialsFlowResponse> exchangeToken()
throws ApiException, FgaInvalidParameterException {
try {
byte[] body = mapper.writeValueAsBytes(authRequest);
byte[] body = apiClient.getObjectMapper().writeValueAsBytes(authRequest);

Configuration config = new Configuration().apiUrl("https://" + apiTokenIssuer);

HttpRequest request = ApiClient.requestBuilder("POST", "/oauth/token", body, config)
.build();

return httpClient
.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenCompose(httpResponse -> {
if (httpResponse.statusCode() != HttpURLConnection.HTTP_OK) {
return CompletableFuture.failedFuture(new ApiException("exchangeToken", httpResponse));
}
try {
CredentialsFlowResponse response =
mapper.readValue(httpResponse.body(), CredentialsFlowResponse.class);
return CompletableFuture.completedFuture(response);
} catch (Exception e) {
return CompletableFuture.failedFuture(e);
}
});
return new HttpRequestAttempt<>(request, "exchangeToken", CredentialsFlowResponse.class, apiClient, config)
.attemptHttpRequest();
} catch (IOException e) {
throw new ApiException(e);
}
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/dev/openfga/sdk/api/client/ApiClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,17 @@ public HttpClient getHttpClient() {
return builder.build();
}

/**
* Get the current {@link HttpClient.Builder}.
*
* <p>The returned object is immutable and thread-safe.</p>
*
* @return The HTTP client.
*/
public HttpClient.Builder getHttpClientBuilder() {
return builder;
}

/**
* Set a custom {@link ObjectMapper} to serialize and deserialize the request
* and response bodies.
Expand Down
106 changes: 106 additions & 0 deletions src/main/java/dev/openfga/sdk/api/client/HttpRequestAttempt.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package dev.openfga.sdk.api.client;

import static dev.openfga.sdk.util.StringUtil.isNullOrWhitespace;

import dev.openfga.sdk.api.configuration.BaseConfiguration;
import dev.openfga.sdk.errors.*;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.concurrent.*;

public class HttpRequestAttempt<T> {
private final ApiClient apiClient;
private final BaseConfiguration configuration;
private final Class<T> clazz;
private final String name;
private final HttpRequest request;

public HttpRequestAttempt(
HttpRequest request, String name, Class<T> clazz, ApiClient apiClient, BaseConfiguration configuration)
throws FgaInvalidParameterException {
if (configuration.getMaxRetries() == null) {
throw new FgaInvalidParameterException("maxRetries", "Configuration");
}
this.apiClient = apiClient;
this.configuration = configuration;
this.name = name;
this.request = request;
this.clazz = clazz;
booniepepper marked this conversation as resolved.
Show resolved Hide resolved
}

public CompletableFuture<T> attemptHttpRequest() throws ApiException {
int retryNumber = 0;
return attemptHttpRequest(apiClient.getHttpClient(), retryNumber, null);
}

private CompletableFuture<T> attemptHttpRequest(HttpClient httpClient, int retryNumber, Throwable previousError) {
return httpClient
.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenCompose(response -> {
try {
checkStatus(name, response, previousError);
} catch (FgaApiRateLimitExceededError | FgaApiInternalError retryableError) {
if (retryNumber < configuration.getMaxRetries()) {
HttpClient delayingClient = getDelayedHttpClient();
return attemptHttpRequest(delayingClient, retryNumber + 1, retryableError);
}
return CompletableFuture.failedFuture(retryableError);
} catch (ApiException e) {
return CompletableFuture.failedFuture(e);
}

return deserializeResponse(response);
});
}

private CompletableFuture<T> deserializeResponse(HttpResponse<String> response) {
if (clazz == Void.class && isNullOrWhitespace(response.body())) {
return CompletableFuture.completedFuture(null);
}

try {
T deserialized = apiClient.getObjectMapper().readValue(response.body(), clazz);
return CompletableFuture.completedFuture(deserialized);
} catch (IOException e) {
// Malformed response.
return CompletableFuture.failedFuture(new ApiException(e));
}
}

private HttpClient getDelayedHttpClient() {
Duration retryDelay = configuration.getMinimumRetryDelay();
return apiClient
.getHttpClientBuilder()
.executor(CompletableFuture.delayedExecutor(retryDelay.toNanos(), TimeUnit.NANOSECONDS))
.build();
}

private static void checkStatus(String name, HttpResponse<String> response, Throwable previousError)
throws ApiException {
int status = response.statusCode();
String body = response.body();

switch (status) {
booniepepper marked this conversation as resolved.
Show resolved Hide resolved
case HttpURLConnection.HTTP_BAD_REQUEST:
case 422: // HTTP 422 Unprocessable Entity
throw new FgaApiValidationError(name, previousError, status, response.headers(), body);

case HttpURLConnection.HTTP_UNAUTHORIZED:
case HttpURLConnection.HTTP_FORBIDDEN:
throw new FgaApiAuthenticationError(name, previousError, status, response.headers(), body);

case HttpURLConnection.HTTP_NOT_FOUND:
throw new FgaApiNotFoundError(name, previousError, status, response.headers(), body);

case 429: // HTTP 429 Too Many Requests
throw new FgaApiRateLimitExceededError(name, previousError, status, response.headers(), body);

case HttpURLConnection.HTTP_INTERNAL_ERROR:
throw new FgaApiInternalError(name, previousError, status, response.headers(), body);
}
booniepepper marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,8 @@ public interface BaseConfiguration {
Duration getReadTimeout();

Duration getConnectTimeout();

Integer getMaxRetries();

Duration getMinimumRetryDelay();
}
Original file line number Diff line number Diff line change
Expand Up @@ -132,4 +132,16 @@ public ClientConfiguration connectTimeout(Duration connectTimeout) {
super.connectTimeout(connectTimeout);
return this;
}

@Override
public ClientConfiguration maxRetries(int maxRetries) {
super.maxRetries(maxRetries);
return this;
}

@Override
public ClientConfiguration minimumRetryDelay(Duration minimumRetryDelay) {
super.minimumRetryDelay(minimumRetryDelay);
return this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ public class Configuration implements BaseConfiguration {
private String userAgent;
private Duration readTimeout;
private Duration connectTimeout;
private int maxRetries;
private Duration minimumRetryDelay;

public Configuration() {
this.apiUrl = DEFAULT_API_URL;
Expand Down Expand Up @@ -101,6 +103,12 @@ public Configuration override(ConfigurationOverride configurationOverride) {
Duration overrideConnectTimeout = configurationOverride.getConnectTimeout();
result.connectTimeout(overrideConnectTimeout != null ? overrideConnectTimeout : connectTimeout);

Integer overrideMaxRetries = configurationOverride.getMaxRetries();
result.maxRetries(overrideMaxRetries != null ? overrideMaxRetries : maxRetries);

Duration overrideMinimumRetryDelay = configurationOverride.getMinimumRetryDelay();
result.minimumRetryDelay(overrideMinimumRetryDelay != null ? overrideMinimumRetryDelay : minimumRetryDelay);

return result;
}

Expand Down Expand Up @@ -231,4 +239,24 @@ public Configuration connectTimeout(Duration connectTimeout) {
public Duration getConnectTimeout() {
return connectTimeout;
}

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

@Override
public Integer getMaxRetries() {
return maxRetries;
}

public Configuration minimumRetryDelay(Duration minimumRetryDelay) {
this.minimumRetryDelay = minimumRetryDelay;
return this;
}

@Override
public Duration getMinimumRetryDelay() {
return minimumRetryDelay;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ public class ConfigurationOverride implements BaseConfiguration {
private String userAgent;
private Duration readTimeout;
private Duration connectTimeout;
private Integer maxRetries;
private Duration minimumRetryDelay;

public ConfigurationOverride() {
this.apiUrl = null;
Expand Down Expand Up @@ -157,4 +159,24 @@ public ConfigurationOverride connectTimeout(Duration connectTimeout) {
public Duration getConnectTimeout() {
return connectTimeout;
}

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

@Override
public Integer getMaxRetries() {
return maxRetries;
}

public ConfigurationOverride minimumRetryDelay(Duration minimumRetryDelay) {
this.minimumRetryDelay = minimumRetryDelay;
return this;
}

@Override
public Duration getMinimumRetryDelay() {
return minimumRetryDelay;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package dev.openfga.sdk.errors;

import java.net.http.HttpHeaders;

public class FgaApiAuthenticationError extends ApiException {
public FgaApiAuthenticationError(
String message, Throwable throwable, int code, HttpHeaders responseHeaders, String responseBody) {
super(message, code, responseHeaders, responseBody);
}

public FgaApiAuthenticationError(String message, int code, HttpHeaders responseHeaders, String responseBody) {
this(message, (Throwable) null, code, responseHeaders, responseBody);
}
}
14 changes: 14 additions & 0 deletions src/main/java/dev/openfga/sdk/errors/FgaApiInternalError.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package dev.openfga.sdk.errors;

import java.net.http.HttpHeaders;

public class FgaApiInternalError extends ApiException {
public FgaApiInternalError(
String message, Throwable throwable, int code, HttpHeaders responseHeaders, String responseBody) {
super(message, code, responseHeaders, responseBody);
}

public FgaApiInternalError(String message, int code, HttpHeaders responseHeaders, String responseBody) {
this(message, (Throwable) null, code, responseHeaders, responseBody);
}
}
14 changes: 14 additions & 0 deletions src/main/java/dev/openfga/sdk/errors/FgaApiNotFoundError.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package dev.openfga.sdk.errors;

import java.net.http.HttpHeaders;

public class FgaApiNotFoundError extends ApiException {
public FgaApiNotFoundError(
String message, Throwable throwable, int code, HttpHeaders responseHeaders, String responseBody) {
super(message, code, responseHeaders, responseBody);
}

public FgaApiNotFoundError(String message, int code, HttpHeaders responseHeaders, String responseBody) {
this(message, (Throwable) null, code, responseHeaders, responseBody);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package dev.openfga.sdk.errors;

import java.net.http.HttpHeaders;

public class FgaApiRateLimitExceededError extends ApiException {
public FgaApiRateLimitExceededError(
String message, Throwable throwable, int code, HttpHeaders responseHeaders, String responseBody) {
super(message, code, responseHeaders, responseBody);
}

public FgaApiRateLimitExceededError(String message, int code, HttpHeaders responseHeaders, String responseBody) {
this(message, (Throwable) null, code, responseHeaders, responseBody);
}
}
Loading