Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
26 changes: 26 additions & 0 deletions CONTRIBUTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ We greatly value your feedback, feature requests, additions to the code, bug rep

- [Developer Guide](#developer-guide)
- [Repository structure](#repository-structure)
- [Implementing a module waiter](#implementing-a-module-waiter)
- [Waiter structure](#waiter-structure)
- [Notes](#notes)
- [Code Contributions](#code-contributions)
- [Bug Reports](#bug-reports)

Expand Down Expand Up @@ -39,6 +42,29 @@ The files located in `services/[service]` are automatically generated from the [

Inside the `core` submodule you can find several classes that are used by all service modules. Examples of usage of the SDK are located in the `examples` directory.

### Implementing a service waiter

Waiters are routines that wait for the completion of asynchronous operations. They are located in a folder named `wait` inside each service folder.

Let's suppose you want to implement the waiters for the `Create`, `Update` and `Delete` operations of a resource `bar` of service `foo`:

1. Start by creating a new folder `wait/` inside `services/foo/`, if it doesn't exist yet
2. Create a file `FooWait.java` inside your new Java package `cloud.stackit.sdk.resourcemanager.wait`, if it doesn't exist yet. The class should be named `FooWait`.
3. Refer to the [Waiter structure](./CONTRIBUTION.md/#waiter-structure) section for details on the structure of the file and the methods
4. Add unit tests to the wait functions

#### Waiter structure

You can find a typical waiter structure here: [Example](./services/resourcemanager/src/main/java/cloud/stackit/sdk/resourcemanager/wait/ResourcemanagerWait.java)

#### Notes

- The success condition may vary from service to service. In the example above we wait for the field `Status` to match a successful or failed message, but other services may have different fields and/or values to represent the state of the create, update or delete operations.
- The `id` and the `state` might not be present on the root level of the API response, this also varies from service to service. You must always match the resource `id` and the resource `state` to what is expected.
- The timeout values included above are just for reference, each resource takes different amounts of time to finish the create, update or delete operations. You should account for some buffer, e.g. 15 minutes, on top of normal execution times.
- For some resources, after a successful delete operation the resource can't be found anymore, so a call to the `Get` method would result in an error. In those cases, the waiter can be implemented by calling the `List` method and check that the resource is not present.
- The main objective of the waiter functions is to make sure that the operation was successful, which means any other special cases such as intermediate error states should also be handled.

## Code Contributions

To make your contribution, follow these steps:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package cloud.stackit.sdk.core.oapierror;

import cloud.stackit.sdk.core.exception.ApiException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;

public class GenericOpenAPIException extends ApiException {

// When a response has a bad status, this limits the number of characters that are shown from
// the response Body
public static int ApiErrorMaxCharacterLimit = 500;

private final int statusCode;
private byte[] body;
private final String errorMessage;
private Object model;

public GenericOpenAPIException(ApiException e) {
this.statusCode = e.getCode();
this.errorMessage = e.getMessage();
}

public GenericOpenAPIException(int statusCode, String errorMessage) {
this(statusCode, errorMessage, null, new HashMap<>());
}

public GenericOpenAPIException(int statusCode, String errorMessage, byte[] body, Object model) {
this.statusCode = statusCode;
this.errorMessage = errorMessage;
this.body = body;
this.model = model;
}

@Override
public String getMessage() {
// Prevent negative values
if (ApiErrorMaxCharacterLimit < 0) {
ApiErrorMaxCharacterLimit = 500;
}

if (body == null) {
return String.format("%s, status code %d", errorMessage, statusCode);
}

String bodyStr = new String(body, StandardCharsets.UTF_8);

if (bodyStr.length() <= ApiErrorMaxCharacterLimit) {
return String.format("%s, status code %d, Body: %s", errorMessage, statusCode, bodyStr);
}

int indexStart = ApiErrorMaxCharacterLimit / 2;
int indexEnd = bodyStr.length() - ApiErrorMaxCharacterLimit / 2;
int numberTruncatedCharacters = indexEnd - indexStart;

return String.format(
"%s, status code %d, Body: %s [...truncated %d characters...] %s",
errorMessage,
statusCode,
bodyStr.substring(0, indexStart),
numberTruncatedCharacters,
bodyStr.substring(indexEnd));
}

public int getStatusCode() {
return statusCode;
}

public byte[] getBody() {
return body;
}

public Object getModel() {
return model;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
package cloud.stackit.sdk.core.wait;

import cloud.stackit.sdk.core.exception.ApiException;
import cloud.stackit.sdk.core.oapierror.GenericOpenAPIException;
import java.net.HttpURLConnection;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;

public class AsyncActionHandler<T> {
public static final Set<Integer> RetryHttpErrorStatusCodes =
new HashSet<>(
Arrays.asList(
HttpURLConnection.HTTP_BAD_GATEWAY,
HttpURLConnection.HTTP_GATEWAY_TIMEOUT));

public final String TemporaryErrorMessage =
"Temporary error was found and the retry limit was reached.";
// public final String TimoutErrorMessage = "WaitWithContext() has timed out.";
public final String NonGenericAPIErrorMessage = "Found non-GenericOpenAPIError.";

private final Callable<AsyncActionResult<T>> checkFn;

private long sleepBeforeWaitMillis;
private long throttleMillis;
private long timeoutMillis;
private int tempErrRetryLimit;

private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

// private final WaitHandler waitHandler;

public AsyncActionHandler(Callable<AsyncActionResult<T>> checkFn) {
this.checkFn = checkFn;
this.sleepBeforeWaitMillis = 0;
this.throttleMillis = TimeUnit.SECONDS.toMillis(5);
this.timeoutMillis = TimeUnit.MINUTES.toMillis(30);
this.tempErrRetryLimit = 5;
}

/**
* SetThrottle sets the time interval between each check of the async action.
*
* @param duration
* @param unit
* @return
*/
public AsyncActionHandler<T> setThrottle(long duration, TimeUnit unit) {
this.throttleMillis = unit.toMillis(duration);
return this;
}

/**
* SetTimeout sets the duration for wait timeout.
*
* @param duration
* @param unit
* @return
*/
public AsyncActionHandler<T> setTimeout(long duration, TimeUnit unit) {
this.timeoutMillis = unit.toMillis(duration);
return this;
}

/**
* SetSleepBeforeWait sets the duration for sleep before wait.
*
* @param duration
* @param unit
* @return
*/
public AsyncActionHandler<T> setSleepBeforeWait(long duration, TimeUnit unit) {
this.sleepBeforeWaitMillis = unit.toMillis(duration);
return this;
}

/**
* SetTempErrRetryLimit sets the retry limit if a temporary error is found. The list of
* temporary errors is defined in the RetryHttpErrorStatusCodes variable.
*
* @param limit
* @return
*/
public AsyncActionHandler<T> setTempErrRetryLimit(int limit) {
this.tempErrRetryLimit = limit;
return this;
}

/**
* WaitWithContextAsync starts the wait until there's an error or wait is done
*
* @return
*/
public CompletableFuture<T> waitWithContextAsync() {
if (throttleMillis <= 0) {
throw new IllegalArgumentException("Throttle can't be 0 or less");
}

CompletableFuture<T> future = new CompletableFuture<>();
long startTime = System.currentTimeMillis();
AtomicInteger retryTempErrorCounter = new AtomicInteger(0);

// This runnable is called periodically.
Runnable checkTask =
new Runnable() {
@Override
public void run() {
if (System.currentTimeMillis() - startTime >= timeoutMillis) {
future.completeExceptionally(new TimeoutException("Timeout occurred."));
}

try {
AsyncActionResult<T> result = checkFn.call();
if (result.error != null) {
ErrorResult errorResult =
handleException(retryTempErrorCounter.get(), result.error);
retryTempErrorCounter.set(errorResult.retryTempErrorCounter);

if (retryTempErrorCounter.get() == tempErrRetryLimit) {
future.completeExceptionally(errorResult.getError());
}
}

if (result != null && result.isFinished()) {
future.complete(result.getResponse());
}
} catch (Exception e) {
future.completeExceptionally(e);
}
}
};

// start the periodic execution
ScheduledFuture<?> scheduledFuture =
scheduler.scheduleAtFixedRate(
checkTask, sleepBeforeWaitMillis, throttleMillis, TimeUnit.MILLISECONDS);

// stop task when future is completed
future.whenComplete(
(result, error) -> {
scheduledFuture.cancel(true);
});

return future;
}

private ErrorResult handleException(int retryTempErrorCounter, Exception exception) {
if (exception instanceof ApiException) {
ApiException apiException = (ApiException) exception;
GenericOpenAPIException oapiErr = new GenericOpenAPIException(apiException);
// Some APIs may return temporary errors and the request should be retried
if (!RetryHttpErrorStatusCodes.contains(oapiErr.getStatusCode())) {
return new ErrorResult(retryTempErrorCounter, oapiErr);
}
retryTempErrorCounter++;
if (retryTempErrorCounter == tempErrRetryLimit) {
return new ErrorResult(
retryTempErrorCounter, new Exception(TemporaryErrorMessage, oapiErr));
}
return new ErrorResult(retryTempErrorCounter, null);
} else {
retryTempErrorCounter++;
// If it's not a GenericOpenAPIError, handle it differently
return new ErrorResult(
retryTempErrorCounter, new Exception(NonGenericAPIErrorMessage, exception));
}
}

// Helper class to encapsulate the result of handleError
public static class ErrorResult {
private final int retryTempErrorCounter;
private final Exception error;

public ErrorResult(int retryTempErrorCounter, Exception error) {
this.retryTempErrorCounter = retryTempErrorCounter;
this.error = error;
}

public int getRetryErrorCounter() {
return retryTempErrorCounter;
}

public Exception getError() {
return error;
}
}

// Helper class to encapsulate the result of the checkFn
public static class AsyncActionResult<T> {
private final boolean finished;
private final T response;
private final Exception error;

public AsyncActionResult(boolean finished, T response, Exception error) {
this.finished = finished;
this.response = response;
this.error = error;
}

public boolean isFinished() {
return finished;
}

public T getResponse() {
return response;
}

public Exception getError() {
return error;
}
}
}
Loading