-
Notifications
You must be signed in to change notification settings - Fork 0
feature(core): Add wait handler structure #19
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
base: main
Are you sure you want to change the base?
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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,202 @@ | ||
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.TimeUnit; | ||
import java.util.concurrent.TimeoutException; | ||
|
||
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; | ||
|
||
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; | ||
} | ||
|
||
/** | ||
* WaitWithContext starts the wait until there's an error or wait is done | ||
* | ||
* @return | ||
* @throws Exception | ||
*/ | ||
public T waitWithContext() throws Exception { | ||
if (throttleMillis <= 0) { | ||
throw new IllegalArgumentException("Throttle can't be 0 or less"); | ||
} | ||
|
||
long startTime = System.currentTimeMillis(); | ||
|
||
// Wait some seconds for the API to process the request | ||
if (sleepBeforeWaitMillis > 0) { | ||
try { | ||
Thread.sleep(sleepBeforeWaitMillis); | ||
|
||
} catch (InterruptedException e) { | ||
Thread.currentThread().interrupt(); | ||
throw new InterruptedException("Wait operation was interrupted before starting."); | ||
} | ||
} | ||
|
||
int retryTempErrorCounter = 0; | ||
while (System.currentTimeMillis() - startTime < timeoutMillis) { | ||
AsyncActionResult<T> result = checkFn.call(); | ||
if (result.error != null) { // error present | ||
ErrorResult errorResult = handleException(retryTempErrorCounter, result.error); | ||
retryTempErrorCounter = errorResult.retryTempErrorCounter; | ||
if (retryTempErrorCounter == tempErrRetryLimit) { | ||
throw errorResult.getError(); | ||
} | ||
result = null; | ||
} | ||
|
||
if (result != null && result.isFinished()) { | ||
return result.getResponse(); | ||
} | ||
|
||
try { | ||
Thread.sleep(throttleMillis); | ||
} catch (InterruptedException e) { | ||
Thread.currentThread().interrupt(); | ||
throw new InterruptedException("Wait operation was interrupted."); | ||
} | ||
} | ||
throw new TimeoutException(TimoutErrorMessage); | ||
} | ||
|
||
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; | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Throwing
Exception
is a little bit... to much here. Users of the SDK won't have the chance to handle this properly.