Skip to content

Commit 060607e

Browse files
committed
feature(core): Add wait handler structure
Signed-off-by: Alexander Dahmen <[email protected]>
1 parent 6e169b7 commit 060607e

File tree

7 files changed

+865
-2
lines changed

7 files changed

+865
-2
lines changed

CONTRIBUTION.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ We greatly value your feedback, feature requests, additions to the code, bug rep
77

88
- [Developer Guide](#developer-guide)
99
- [Repository structure](#repository-structure)
10+
- [Implementing a module waiter](#implementing-a-module-waiter)
11+
- [Waiter structure](#waiter-structure)
12+
- [Notes](#notes)
1013
- [Code Contributions](#code-contributions)
1114
- [Bug Reports](#bug-reports)
1215

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

4043
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.
4144

45+
### Implementing a service waiter
46+
47+
For integration with other tools such as the [STACKIT Terraform Provider](https://github.com/stackitcloud/terraform-provider-stackit) and the [STACKIT CLI](https://github.com/stackitcloud/stackit-cli), there is the need to implement waiters for some SDK modules. Waiters are routines that wait for the completion of asynchronous operations. They are located in a folder named `wait` inside each service folder.
48+
49+
Let's suppose you want to implement the waiters for the `Create`, `Update` and `Delete` operations of a resource `bar` of service `foo`:
50+
51+
1. Start by creating a new folder `wait/` inside `services/foo/`, if it doesn't exist yet
52+
2. Create a file `FooWait.go` inside your new folder `services/foo/wait`, if it doesn't exist yet. The class should be named `FooWait`.
53+
3. Refer to the [Waiter structure](./CONTRIBUTION.md/#waiter-structure) section for details on the structure of the file and the methods
54+
4. Add unit tests to the wait functions
55+
56+
#### Waiter structure
57+
58+
You can find a typical waiter structure here: [Example](./services/resourcemanager/src/main/java/cloud/stackit/sdk/resourcemanager/wait/ResourcemanagerWait.java)
59+
60+
#### Notes
61+
62+
- 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.
63+
- 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.
64+
- 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.
65+
- 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.
66+
- 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.
67+
4268
## Code Contributions
4369

4470
To make your contribution, follow these steps:
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 negative values
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)