Skip to content

Commit e859880

Browse files
committed
feature(core): Add wait handler structure
Signed-off-by: Alexander Dahmen <alexander.dahmen@inovex.de>
1 parent 11cd42e commit e859880

File tree

6 files changed

+805
-2
lines changed

6 files changed

+805
-2
lines changed
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package cloud.stackit.sdk.core.oapierror;
2+
3+
import cloud.stackit.sdk.core.exception.ApiException;
4+
import java.nio.charset.StandardCharsets;
5+
import java.util.HashMap;
6+
7+
public class GenericOpenAPIError extends ApiException {
8+
9+
// When a response has a bad status, this limits the number of characters that are shown from
10+
// the response Body
11+
public static int ApiErrorMaxCharacterLimit = 500;
12+
13+
private int statusCode;
14+
private byte[] body;
15+
private String errorMessage;
16+
private Object model;
17+
18+
public GenericOpenAPIError(ApiException e) {
19+
this.statusCode = e.getCode();
20+
this.errorMessage = e.getMessage();
21+
}
22+
23+
public GenericOpenAPIError(int statusCode, String errorMessage) {
24+
this(statusCode, errorMessage, null, new HashMap<>());
25+
}
26+
27+
public GenericOpenAPIError(int statusCode, String errorMessage, byte[] body, Object model) {
28+
this.statusCode = statusCode;
29+
this.errorMessage = errorMessage;
30+
this.body = body;
31+
this.model = model;
32+
}
33+
34+
@Override
35+
public String getMessage() {
36+
// Prevent panic in case of negative value
37+
if (ApiErrorMaxCharacterLimit < 0) {
38+
ApiErrorMaxCharacterLimit = 500;
39+
}
40+
41+
if (body == null) {
42+
return String.format("%s, status code %d", errorMessage, statusCode);
43+
}
44+
45+
String bodyStr = new String(body, StandardCharsets.UTF_8);
46+
47+
if (bodyStr.length() <= ApiErrorMaxCharacterLimit) {
48+
return String.format("%s, status code %d, Body: %s", errorMessage, statusCode, bodyStr);
49+
}
50+
51+
int indexStart = ApiErrorMaxCharacterLimit / 2;
52+
int indexEnd = bodyStr.length() - ApiErrorMaxCharacterLimit / 2;
53+
int numberTruncatedCharacters = indexEnd - indexStart;
54+
55+
return String.format(
56+
"%s, status code %d, Body: %s [...truncated %d characters...] %s",
57+
errorMessage,
58+
statusCode,
59+
bodyStr.substring(0, indexStart),
60+
numberTruncatedCharacters,
61+
bodyStr.substring(indexEnd));
62+
}
63+
64+
public int getStatusCode() {
65+
return statusCode;
66+
}
67+
68+
public byte[] getBody() {
69+
return body;
70+
}
71+
72+
public Object getModel() {
73+
return model;
74+
}
75+
}
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
package cloud.stackit.sdk.core.wait;
2+
3+
import cloud.stackit.sdk.core.exception.ApiException;
4+
import cloud.stackit.sdk.core.oapierror.GenericOpenAPIError;
5+
import java.net.HttpURLConnection;
6+
import java.util.Arrays;
7+
import java.util.HashSet;
8+
import java.util.Set;
9+
import java.util.concurrent.Callable;
10+
import java.util.concurrent.TimeUnit;
11+
12+
public class AsyncActionHandler<T> {
13+
public static final Set<Integer> RetryHttpErrorStatusCodes =
14+
new HashSet<>(
15+
Arrays.asList(
16+
HttpURLConnection.HTTP_BAD_GATEWAY,
17+
HttpURLConnection.HTTP_GATEWAY_TIMEOUT));
18+
19+
public final String TemporaryErrorMessage =
20+
"Temporary error was found and the retry limit was reached.";
21+
public final String TimoutErrorMessage = "WaitWithContext() has timed out.";
22+
public final String NonGenericAPIErrorMessage = "Found non-GenericOpenAPIError.";
23+
24+
private final Callable<AsyncActionResult<T>> checkFn;
25+
26+
private long sleepBeforeWaitMillis;
27+
private long throttleMillis;
28+
private long timeoutMillis;
29+
private int tempErrRetryLimit;
30+
31+
public AsyncActionHandler(Callable<AsyncActionResult<T>> checkFn) {
32+
this.checkFn = checkFn;
33+
this.sleepBeforeWaitMillis = 0;
34+
this.throttleMillis = TimeUnit.SECONDS.toMillis(5);
35+
this.timeoutMillis = TimeUnit.MINUTES.toMillis(30);
36+
this.tempErrRetryLimit = 5;
37+
}
38+
39+
/**
40+
* SetThrottle sets the time interval between each check of the async action.
41+
*
42+
* @param duration
43+
* @param unit
44+
* @return
45+
*/
46+
public AsyncActionHandler<T> setThrottle(long duration, TimeUnit unit) {
47+
this.throttleMillis = unit.toMillis(duration);
48+
return this;
49+
}
50+
51+
/**
52+
* SetTimeout sets the duration for wait timeout.
53+
*
54+
* @param duration
55+
* @param unit
56+
* @return
57+
*/
58+
public AsyncActionHandler<T> setTimeout(long duration, TimeUnit unit) {
59+
this.timeoutMillis = unit.toMillis(duration);
60+
return this;
61+
}
62+
63+
/**
64+
* SetSleepBeforeWait sets the duration for sleep before wait.
65+
*
66+
* @param duration
67+
* @param unit
68+
* @return
69+
*/
70+
public AsyncActionHandler<T> setSleepBeforeWait(long duration, TimeUnit unit) {
71+
this.sleepBeforeWaitMillis = unit.toMillis(duration);
72+
return this;
73+
}
74+
75+
/**
76+
* SetTempErrRetryLimit sets the retry limit if a temporary error is found. The list of
77+
* temporary errors is defined in the RetryHttpErrorStatusCodes variable.
78+
*
79+
* @param limit
80+
* @return
81+
*/
82+
public AsyncActionHandler<T> setTempErrRetryLimit(int limit) {
83+
this.tempErrRetryLimit = limit;
84+
return this;
85+
}
86+
87+
/**
88+
* WaitWithContext starts the wait until there's an error or wait is done
89+
*
90+
* @return
91+
* @throws Exception
92+
*/
93+
public T waitWithContext() throws Exception {
94+
if (throttleMillis <= 0) {
95+
throw new IllegalArgumentException("Throttle can't be 0 or less");
96+
}
97+
98+
long startTime = System.currentTimeMillis();
99+
100+
// Wait some seconds for the API to process the request
101+
if (sleepBeforeWaitMillis > 0) {
102+
try {
103+
Thread.sleep(sleepBeforeWaitMillis);
104+
} catch (InterruptedException e) {
105+
Thread.currentThread().interrupt();
106+
throw new InterruptedException("Wait operation was interrupted before starting.");
107+
}
108+
}
109+
110+
int retryTempErrorCounter = 0;
111+
while (System.currentTimeMillis() - startTime < timeoutMillis) {
112+
AsyncActionResult<T> result = checkFn.call();
113+
if (result.error != null) { // error present
114+
ErrorResult errorResult = handleError(retryTempErrorCounter, result.error);
115+
retryTempErrorCounter = errorResult.retryTempErrorCounter;
116+
if (retryTempErrorCounter == tempErrRetryLimit) {
117+
throw errorResult.getError();
118+
}
119+
result = null;
120+
}
121+
122+
if (result != null && result.isFinished()) {
123+
return result.getResponse();
124+
}
125+
126+
try {
127+
Thread.sleep(throttleMillis);
128+
} catch (InterruptedException e) {
129+
Thread.currentThread().interrupt();
130+
throw new InterruptedException("Wait operation was interrupted.");
131+
}
132+
}
133+
throw new Exception(TimoutErrorMessage);
134+
}
135+
136+
private ErrorResult handleError(int retryTempErrorCounter, Exception err) {
137+
if (err instanceof ApiException) {
138+
ApiException apiException = (ApiException) err;
139+
GenericOpenAPIError oapiErr = new GenericOpenAPIError(apiException);
140+
// Some APIs may return temporary errors and the request should be retried
141+
if (!RetryHttpErrorStatusCodes.contains(oapiErr.getStatusCode())) {
142+
return new ErrorResult(retryTempErrorCounter, oapiErr);
143+
}
144+
retryTempErrorCounter++;
145+
if (retryTempErrorCounter == tempErrRetryLimit) {
146+
return new ErrorResult(
147+
retryTempErrorCounter, new Exception(TemporaryErrorMessage, oapiErr));
148+
}
149+
return new ErrorResult(retryTempErrorCounter, null);
150+
} else {
151+
retryTempErrorCounter++;
152+
// If it's not a GenericOpenAPIError, handle it differently
153+
return new ErrorResult(
154+
retryTempErrorCounter, new Exception(NonGenericAPIErrorMessage, err));
155+
}
156+
}
157+
158+
// Helper class to encapsulate the result of handleError
159+
public static class ErrorResult {
160+
private final int retryTempErrorCounter;
161+
private final Exception error;
162+
163+
public ErrorResult(int retryTempErrorCounter, Exception error) {
164+
this.retryTempErrorCounter = retryTempErrorCounter;
165+
this.error = error;
166+
}
167+
168+
public int getRetryErrorCounter() {
169+
return retryTempErrorCounter;
170+
}
171+
172+
public Exception getError() {
173+
return error;
174+
}
175+
}
176+
177+
// Helper class to encapsulate the result of the checkFn
178+
public static class AsyncActionResult<T> {
179+
private final boolean finished;
180+
private final T response;
181+
private final Exception error;
182+
183+
public AsyncActionResult(boolean finished, T response, Exception error) {
184+
this.finished = finished;
185+
this.response = response;
186+
this.error = error;
187+
}
188+
189+
public boolean isFinished() {
190+
return finished;
191+
}
192+
193+
public T getResponse() {
194+
return response;
195+
}
196+
197+
public Exception getError() {
198+
return error;
199+
}
200+
}
201+
}

0 commit comments

Comments
 (0)