Skip to content

Commit 180f373

Browse files
committed
chore(wait): Rework wait handler to use CompletableFuture
Signed-off-by: Alexander Dahmen <alexander.dahmen@inovex.de>
1 parent 3a0eea7 commit 180f373

File tree

4 files changed

+104
-67
lines changed

4 files changed

+104
-67
lines changed

core/src/main/java/cloud/stackit/sdk/core/wait/AsyncActionHandler.java

Lines changed: 57 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,13 @@
77
import java.util.HashSet;
88
import java.util.Set;
99
import java.util.concurrent.Callable;
10+
import java.util.concurrent.CompletableFuture;
11+
import java.util.concurrent.Executors;
12+
import java.util.concurrent.ScheduledExecutorService;
13+
import java.util.concurrent.ScheduledFuture;
1014
import java.util.concurrent.TimeUnit;
1115
import java.util.concurrent.TimeoutException;
16+
import java.util.concurrent.atomic.AtomicInteger;
1217

1318
public class AsyncActionHandler<T> {
1419
public static final Set<Integer> RetryHttpErrorStatusCodes =
@@ -19,7 +24,7 @@ public class AsyncActionHandler<T> {
1924

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

2530
private final Callable<AsyncActionResult<T>> checkFn;
@@ -29,6 +34,10 @@ public class AsyncActionHandler<T> {
2934
private long timeoutMillis;
3035
private int tempErrRetryLimit;
3136

37+
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
38+
39+
// private final WaitHandler waitHandler;
40+
3241
public AsyncActionHandler(Callable<AsyncActionResult<T>> checkFn) {
3342
this.checkFn = checkFn;
3443
this.sleepBeforeWaitMillis = 0;
@@ -86,52 +95,61 @@ public AsyncActionHandler<T> setTempErrRetryLimit(int limit) {
8695
}
8796

8897
/**
89-
* WaitWithContext starts the wait until there's an error or wait is done
98+
* WaitWithContextAsync starts the wait until there's an error or wait is done
9099
*
91100
* @return
92-
* @throws Exception
93101
*/
94-
public T waitWithContext() throws Exception {
102+
public CompletableFuture<T> waitWithContextAsync() {
95103
if (throttleMillis <= 0) {
96104
throw new IllegalArgumentException("Throttle can't be 0 or less");
97105
}
98106

107+
CompletableFuture<T> future = new CompletableFuture<>();
99108
long startTime = System.currentTimeMillis();
100-
101-
// Wait some seconds for the API to process the request
102-
if (sleepBeforeWaitMillis > 0) {
103-
try {
104-
Thread.sleep(sleepBeforeWaitMillis);
105-
} catch (InterruptedException e) {
106-
Thread.currentThread().interrupt();
107-
throw new InterruptedException("Wait operation was interrupted before starting.");
108-
}
109-
}
110-
111-
int retryTempErrorCounter = 0;
112-
while (System.currentTimeMillis() - startTime < timeoutMillis) {
113-
AsyncActionResult<T> result = checkFn.call();
114-
if (result.error != null) { // error present
115-
ErrorResult errorResult = handleException(retryTempErrorCounter, result.error);
116-
retryTempErrorCounter = errorResult.retryTempErrorCounter;
117-
if (retryTempErrorCounter == tempErrRetryLimit) {
118-
throw errorResult.getError();
119-
}
120-
result = null;
121-
}
122-
123-
if (result != null && result.isFinished()) {
124-
return result.getResponse();
125-
}
126-
127-
try {
128-
Thread.sleep(throttleMillis);
129-
} catch (InterruptedException e) {
130-
Thread.currentThread().interrupt();
131-
throw new InterruptedException("Wait operation was interrupted.");
132-
}
133-
}
134-
throw new TimeoutException(TimoutErrorMessage);
109+
AtomicInteger retryTempErrorCounter = new AtomicInteger(0);
110+
111+
// This runnable is called periodically.
112+
Runnable checkTask =
113+
new Runnable() {
114+
@Override
115+
public void run() {
116+
if (System.currentTimeMillis() - startTime >= timeoutMillis) {
117+
future.completeExceptionally(new TimeoutException("Timeout occurred."));
118+
}
119+
120+
try {
121+
AsyncActionResult<T> result = checkFn.call();
122+
if (result.error != null) {
123+
ErrorResult errorResult =
124+
handleException(retryTempErrorCounter.get(), result.error);
125+
retryTempErrorCounter.set(errorResult.retryTempErrorCounter);
126+
127+
if (retryTempErrorCounter.get() == tempErrRetryLimit) {
128+
future.completeExceptionally(errorResult.getError());
129+
}
130+
}
131+
132+
if (result != null && result.isFinished()) {
133+
future.complete(result.getResponse());
134+
}
135+
} catch (Exception e) {
136+
future.completeExceptionally(e);
137+
}
138+
}
139+
};
140+
141+
// start the periodic execution
142+
ScheduledFuture<?> scheduledFuture =
143+
scheduler.scheduleAtFixedRate(
144+
checkTask, sleepBeforeWaitMillis, throttleMillis, TimeUnit.MILLISECONDS);
145+
146+
// stop task when future is completed
147+
future.whenComplete(
148+
(result, error) -> {
149+
scheduledFuture.cancel(true);
150+
});
151+
152+
return future;
135153
}
136154

137155
private ErrorResult handleException(int retryTempErrorCounter, Exception exception) {

core/src/test/java/cloud/stackit/sdk/core/wait/AsyncWaitHandlerTest.java

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package cloud.stackit.sdk.core.wait;
22

3-
import static org.junit.jupiter.api.Assertions.assertEquals;
3+
import static org.junit.Assert.assertTrue;
44
import static org.junit.jupiter.api.Assertions.assertThrows;
55
import static org.mockito.Mockito.when;
66

@@ -61,8 +61,9 @@ void testNonGenericOpenAPIError() throws Exception {
6161
handler.setTimeout(40, TimeUnit.MILLISECONDS);
6262
handler.setTempErrRetryLimit(2);
6363

64-
Exception thrown = assertThrows(Exception.class, handler::waitWithContext, "");
65-
assertEquals(thrown.getMessage(), handler.NonGenericAPIErrorMessage);
64+
Exception thrown =
65+
assertThrows(Exception.class, () -> handler.waitWithContextAsync().get(), "");
66+
assertTrue(thrown.getMessage().contains(handler.NonGenericAPIErrorMessage));
6667
}
6768

6869
// GenericOpenAPIError(ApiException) not in RetryHttpErrorStatusCodes
@@ -77,8 +78,9 @@ void testOpenAPIErrorNotInList() throws Exception {
7778
handler.setTimeout(40, TimeUnit.MILLISECONDS);
7879
handler.setTempErrRetryLimit(2);
7980

80-
Exception thrown = assertThrows(Exception.class, handler::waitWithContext, "");
81-
assertEquals(thrown.getMessage(), handler.TimoutErrorMessage);
81+
Exception thrown =
82+
assertThrows(Exception.class, () -> handler.waitWithContextAsync().get(), "");
83+
assertTrue(thrown.getMessage().contains("Timeout occurred"));
8284
}
8385

8486
// GenericOpenAPIError(ApiException) in RetryHttpErrorStatusCodes -> max retries reached
@@ -95,8 +97,11 @@ void testOpenAPIErrorTimeoutBadGateway() throws Exception {
9597
handler.setTempErrRetryLimit(2);
9698

9799
Exception thrown =
98-
assertThrows(Exception.class, handler::waitWithContext, apiException.getMessage());
99-
assertEquals(thrown.getMessage(), handler.TemporaryErrorMessage);
100+
assertThrows(
101+
Exception.class,
102+
() -> handler.waitWithContextAsync().get(),
103+
apiException.getMessage());
104+
assertTrue(thrown.getMessage().contains(handler.TemporaryErrorMessage));
100105
}
101106

102107
// GenericOpenAPIError(ApiException) in RetryHttpErrorStatusCodes -> max retries reached
@@ -113,7 +118,10 @@ void testOpenAPIErrorTimeoutGatewayTimeout() throws Exception {
113118
handler.setTempErrRetryLimit(2);
114119

115120
Exception thrown =
116-
assertThrows(Exception.class, handler::waitWithContext, apiException.getMessage());
117-
assertEquals(thrown.getMessage(), handler.TemporaryErrorMessage);
121+
assertThrows(
122+
Exception.class,
123+
() -> handler.waitWithContextAsync().get(),
124+
apiException.getMessage());
125+
assertTrue(thrown.getMessage().contains(handler.TemporaryErrorMessage));
118126
}
119127
}

examples/resourcemanager/src/main/java/cloud/stackit/sdk/resourcemanager/examples/ResourcemanagerExample.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ public static void main(String[] args) throws Exception {
7070

7171
ResourcemanagerWait.createProjectWaitHandler(
7272
resourceManagerApi, project.getContainerId())
73-
.waitWithContext();
73+
.waitWithContextAsync()
74+
.get();
7475

7576
/* list folders */
7677
ListFoldersResponse responseListFolders =
@@ -97,7 +98,8 @@ public static void main(String[] args) throws Exception {
9798

9899
ResourcemanagerWait.createProjectWaitHandler(
99100
resourceManagerApi, project.getContainerId())
100-
.waitWithContext();
101+
.waitWithContextAsync()
102+
.get();
101103

102104
/* get organization details */
103105
OrganizationResponse organizationResponse =
@@ -114,7 +116,8 @@ public static void main(String[] args) throws Exception {
114116

115117
ResourcemanagerWait.deleteProjectWaitHandler(
116118
resourceManagerApi, project.getContainerId())
117-
.waitWithContext();
119+
.waitWithContextAsync()
120+
.get();
118121

119122
/* delete folder */
120123
resourceManagerApi.deleteFolder(folder.getContainerId(), true);

services/resourcemanager/src/test/java/cloud/stackit/sdk/resourcemanager/ResourcemanagerWaitTestmanagerWaitTest.java

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package cloud.stackit.sdk.resourcemanager;
22

3-
import static org.junit.jupiter.api.Assertions.assertEquals;
43
import static org.junit.jupiter.api.Assertions.assertNotNull;
54
import static org.junit.jupiter.api.Assertions.assertThrows;
5+
import static org.junit.jupiter.api.Assertions.assertTrue;
66
import static org.mockito.Mockito.times;
77
import static org.mockito.Mockito.verify;
88
import static org.mockito.Mockito.when;
@@ -59,7 +59,7 @@ void testCreateProjectSuccess() throws Exception {
5959
handler.setThrottle(10, TimeUnit.MILLISECONDS);
6060
handler.setTimeout(2, TimeUnit.SECONDS);
6161

62-
GetProjectResponse result = handler.waitWithContext();
62+
GetProjectResponse result = handler.waitWithContextAsync().get();
6363

6464
assertNotNull(result);
6565
verify(apiClient, times(2)).getProject(containerId, false);
@@ -79,7 +79,9 @@ void testCreateProjectTimeout() throws Exception {
7979
handler.setThrottle(10, TimeUnit.MILLISECONDS);
8080
handler.setTimeout(500, TimeUnit.MILLISECONDS);
8181

82-
assertThrows(Exception.class, handler::waitWithContext, handler.TimoutErrorMessage);
82+
Exception thrown =
83+
assertThrows(Exception.class, () -> handler.waitWithContextAsync().get(), "");
84+
assertTrue(thrown.getMessage().contains("Timeout occurred"));
8385
}
8486

8587
// GenericOpenAPIError not in RetryHttpErrorStatusCodes
@@ -97,8 +99,11 @@ void testCreateProjectOpenAPIError() throws Exception {
9799
handler.setTempErrRetryLimit(2);
98100

99101
Exception thrown =
100-
assertThrows(Exception.class, handler::waitWithContext, apiException.getMessage());
101-
assertEquals(thrown.getMessage(), handler.TimoutErrorMessage);
102+
assertThrows(
103+
Exception.class,
104+
() -> handler.waitWithContextAsync().get(),
105+
apiException.getMessage());
106+
assertTrue(thrown.getMessage().contains("Timeout occurred"));
102107
}
103108

104109
// GenericOpenAPIError in RetryHttpErrorStatusCodes -> max retries reached
@@ -116,8 +121,11 @@ void testOpenAPIErrorTimeoutBadGateway() throws Exception {
116121
handler.setTempErrRetryLimit(2);
117122

118123
Exception thrown =
119-
assertThrows(Exception.class, handler::waitWithContext, apiException.getMessage());
120-
assertEquals(thrown.getMessage(), handler.TemporaryErrorMessage);
124+
assertThrows(
125+
Exception.class,
126+
() -> handler.waitWithContextAsync().get(),
127+
apiException.getMessage());
128+
assertTrue(thrown.getMessage().contains(handler.TemporaryErrorMessage));
121129
}
122130

123131
// GenericOpenAPIError in RetryHttpErrorStatusCodes -> max retries reached
@@ -137,9 +145,9 @@ void testOpenAPIErrorTimeoutGatewayTimeout() throws Exception {
137145
Exception thrown =
138146
assertThrows(
139147
Exception.class,
140-
() -> handler.waitWithContext(),
148+
() -> handler.waitWithContextAsync().get(),
141149
apiException.getMessage());
142-
assertEquals(thrown.getMessage(), handler.TemporaryErrorMessage);
150+
assertTrue(thrown.getMessage().contains(handler.TemporaryErrorMessage));
143151
}
144152

145153
@Test
@@ -169,7 +177,7 @@ void testDeleteProjectSuccessDeleting() throws Exception {
169177
handler.setThrottle(10, TimeUnit.MILLISECONDS);
170178
handler.setTimeout(2, TimeUnit.SECONDS);
171179

172-
handler.waitWithContext();
180+
handler.waitWithContextAsync().get();
173181
verify(apiClient, times(2)).getProject(containerId, false);
174182
}
175183

@@ -184,7 +192,7 @@ void testDeleteProjectSuccessNotFoundExc() throws Exception {
184192
handler.setSleepBeforeWait(0, TimeUnit.SECONDS);
185193
handler.setThrottle(10, TimeUnit.MILLISECONDS);
186194
handler.setTimeout(2, TimeUnit.SECONDS);
187-
handler.waitWithContext();
195+
handler.waitWithContextAsync().get();
188196
// Only one invocation since the project is gone (HTTP_NOT_FOUND)
189197
verify(apiClient, times(1)).getProject(containerId, false);
190198
}
@@ -200,7 +208,7 @@ void testDeleteProjectSuccessForbiddenExc() throws Exception {
200208
handler.setSleepBeforeWait(0, TimeUnit.SECONDS);
201209
handler.setThrottle(10, TimeUnit.MILLISECONDS);
202210
handler.setTimeout(2, TimeUnit.SECONDS);
203-
handler.waitWithContext();
211+
handler.waitWithContextAsync().get();
204212
// Only one invocation since the project is gone (HTTP_FORBIDDEN)
205213
verify(apiClient, times(1)).getProject(containerId, false);
206214
}
@@ -220,9 +228,9 @@ void testDeleteProjectDifferentErrorCode() throws Exception {
220228
Exception thrown =
221229
assertThrows(
222230
Exception.class,
223-
() -> handler.waitWithContext(),
231+
() -> handler.waitWithContextAsync().get(),
224232
apiException.getMessage());
225-
assertEquals(thrown.getMessage(), handler.TimoutErrorMessage);
233+
assertTrue(thrown.getMessage().contains("Timeout occurred"));
226234
}
227235

228236
@Test
@@ -241,8 +249,8 @@ void testOpenAPIErrorGatewayTimeout() throws Exception {
241249
Exception thrown =
242250
assertThrows(
243251
Exception.class,
244-
() -> handler.waitWithContext(),
252+
() -> handler.waitWithContextAsync().get(),
245253
apiException.getMessage());
246-
assertEquals(thrown.getMessage(), handler.TemporaryErrorMessage);
254+
assertTrue(thrown.getMessage().contains(handler.TemporaryErrorMessage));
247255
}
248256
}

0 commit comments

Comments
 (0)