| 
 | 1 | +package cloud.stackit.sdk.core.wait;  | 
 | 2 | + | 
 | 3 | +import cloud.stackit.sdk.core.exception.ApiException;  | 
 | 4 | +import cloud.stackit.sdk.core.exception.GenericOpenAPIException;  | 
 | 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.CompletableFuture;  | 
 | 10 | +import java.util.concurrent.ScheduledFuture;  | 
 | 11 | +import java.util.concurrent.TimeUnit;  | 
 | 12 | +import java.util.concurrent.TimeoutException;  | 
 | 13 | +import java.util.concurrent.atomic.AtomicInteger;  | 
 | 14 | + | 
 | 15 | +public class AsyncActionHandler<T> {  | 
 | 16 | +	public static final Set<Integer> RETRY_HTTP_ERROR_STATUS_CODES =  | 
 | 17 | +			new HashSet<>(  | 
 | 18 | +					Arrays.asList(  | 
 | 19 | +							HttpURLConnection.HTTP_BAD_GATEWAY,  | 
 | 20 | +							HttpURLConnection.HTTP_GATEWAY_TIMEOUT));  | 
 | 21 | + | 
 | 22 | +	public static final String TEMPORARY_ERROR_MESSAGE =  | 
 | 23 | +			"Temporary error was found and the retry limit was reached.";  | 
 | 24 | +	public static final String TIMEOUT_ERROR_MESSAGE = "WaitWithContextAsync() has timed out.";  | 
 | 25 | +	public static final String NON_GENERIC_API_ERROR_MESSAGE = "Found non-GenericOpenAPIError.";  | 
 | 26 | + | 
 | 27 | +	private final CheckFunction<AsyncActionResult<T>> checkFn;  | 
 | 28 | + | 
 | 29 | +	private long sleepBeforeWaitMillis;  | 
 | 30 | +	private long throttleMillis;  | 
 | 31 | +	private long timeoutMillis;  | 
 | 32 | +	private int tempErrRetryLimit;  | 
 | 33 | + | 
 | 34 | +	// The linter is complaining about this but since we are using Java 8 the  | 
 | 35 | +	// possibilities are restricted.  | 
 | 36 | +	// @SuppressWarnings("PMD.DoNotUseThreads")  | 
 | 37 | +	// private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);  | 
 | 38 | + | 
 | 39 | +	public AsyncActionHandler(CheckFunction<AsyncActionResult<T>> checkFn) {  | 
 | 40 | +		this.checkFn = checkFn;  | 
 | 41 | +		this.sleepBeforeWaitMillis = 0;  | 
 | 42 | +		this.throttleMillis = TimeUnit.SECONDS.toMillis(5);  | 
 | 43 | +		this.timeoutMillis = TimeUnit.MINUTES.toMillis(30);  | 
 | 44 | +		this.tempErrRetryLimit = 5;  | 
 | 45 | +	}  | 
 | 46 | + | 
 | 47 | +	/**  | 
 | 48 | +	 * SetThrottle sets the time interval between each check of the async action.  | 
 | 49 | +	 *  | 
 | 50 | +	 * @param duration  | 
 | 51 | +	 * @param unit  | 
 | 52 | +	 */  | 
 | 53 | +	public void setThrottle(long duration, TimeUnit unit) {  | 
 | 54 | +		this.throttleMillis = unit.toMillis(duration);  | 
 | 55 | +	}  | 
 | 56 | + | 
 | 57 | +	/**  | 
 | 58 | +	 * SetTimeout sets the duration for wait timeout.  | 
 | 59 | +	 *  | 
 | 60 | +	 * @param duration  | 
 | 61 | +	 * @param unit  | 
 | 62 | +	 */  | 
 | 63 | +	public void setTimeout(long duration, TimeUnit unit) {  | 
 | 64 | +		this.timeoutMillis = unit.toMillis(duration);  | 
 | 65 | +	}  | 
 | 66 | + | 
 | 67 | +	/**  | 
 | 68 | +	 * SetSleepBeforeWait sets the duration for sleep before wait.  | 
 | 69 | +	 *  | 
 | 70 | +	 * @param duration  | 
 | 71 | +	 * @param unit  | 
 | 72 | +	 */  | 
 | 73 | +	public void setSleepBeforeWait(long duration, TimeUnit unit) {  | 
 | 74 | +		this.sleepBeforeWaitMillis = unit.toMillis(duration);  | 
 | 75 | +	}  | 
 | 76 | + | 
 | 77 | +	/**  | 
 | 78 | +	 * SetTempErrRetryLimit sets the retry limit if a temporary error is found. The list of  | 
 | 79 | +	 * temporary errors is defined in the RetryHttpErrorStatusCodes variable.  | 
 | 80 | +	 *  | 
 | 81 | +	 * @param limit  | 
 | 82 | +	 */  | 
 | 83 | +	public void setTempErrRetryLimit(int limit) {  | 
 | 84 | +		this.tempErrRetryLimit = limit;  | 
 | 85 | +	}  | 
 | 86 | + | 
 | 87 | +	/**  | 
 | 88 | +	 * Runnable task which is executed periodically.  | 
 | 89 | +	 *  | 
 | 90 | +	 * @param future  | 
 | 91 | +	 * @param startTime  | 
 | 92 | +	 * @param retryTempErrorCounter  | 
 | 93 | +	 */  | 
 | 94 | +	private void executeCheckTask(  | 
 | 95 | +			CompletableFuture<T> future, long startTime, AtomicInteger retryTempErrorCounter) {  | 
 | 96 | +		if (future.isDone()) {  | 
 | 97 | +			return;  | 
 | 98 | +		}  | 
 | 99 | +		if (System.currentTimeMillis() - startTime >= timeoutMillis) {  | 
 | 100 | +			future.completeExceptionally(new TimeoutException(TIMEOUT_ERROR_MESSAGE));  | 
 | 101 | +		}  | 
 | 102 | +		try {  | 
 | 103 | +			AsyncActionResult<T> result = checkFn.execute();  | 
 | 104 | +			if (result != null && result.isFinished()) {  | 
 | 105 | +				future.complete(result.getResponse());  | 
 | 106 | +			}  | 
 | 107 | +		} catch (ApiException e) {  | 
 | 108 | +			GenericOpenAPIException oapiErr = new GenericOpenAPIException(e);  | 
 | 109 | +			// Some APIs may return temporary errors and the request should be retried  | 
 | 110 | +			if (!RETRY_HTTP_ERROR_STATUS_CODES.contains(oapiErr.getStatusCode())) {  | 
 | 111 | +				return;  | 
 | 112 | +			}  | 
 | 113 | +			if (retryTempErrorCounter.incrementAndGet() == tempErrRetryLimit) {  | 
 | 114 | +				// complete the future with corresponding exception  | 
 | 115 | +				future.completeExceptionally(new Exception(TEMPORARY_ERROR_MESSAGE, oapiErr));  | 
 | 116 | +			}  | 
 | 117 | +		} catch (IllegalStateException e) {  | 
 | 118 | +			future.completeExceptionally(e);  | 
 | 119 | +		}  | 
 | 120 | +	}  | 
 | 121 | + | 
 | 122 | +	/**  | 
 | 123 | +	 * WaitWithContextAsync starts the wait until there's an error or wait is done  | 
 | 124 | +	 *  | 
 | 125 | +	 * @return  | 
 | 126 | +	 */  | 
 | 127 | +	public CompletableFuture<T> waitWithContextAsync() {  | 
 | 128 | +		if (throttleMillis <= 0) {  | 
 | 129 | +			throw new IllegalArgumentException("Throttle can't be 0 or less");  | 
 | 130 | +		}  | 
 | 131 | + | 
 | 132 | +		CompletableFuture<T> future = new CompletableFuture<>();  | 
 | 133 | +		long startTime = System.currentTimeMillis();  | 
 | 134 | +		AtomicInteger retryTempErrorCounter = new AtomicInteger(0);  | 
 | 135 | + | 
 | 136 | +		// This runnable is called periodically.  | 
 | 137 | +		Runnable checkTask = () -> executeCheckTask(future, startTime, retryTempErrorCounter);  | 
 | 138 | + | 
 | 139 | +		// start the periodic execution  | 
 | 140 | +		ScheduledFuture<?> scheduledFuture =  | 
 | 141 | +				ScheduleExecutorSingleton.getInstance()  | 
 | 142 | +						.getScheduler()  | 
 | 143 | +						.scheduleAtFixedRate(  | 
 | 144 | +								checkTask,  | 
 | 145 | +								sleepBeforeWaitMillis,  | 
 | 146 | +								throttleMillis,  | 
 | 147 | +								TimeUnit.MILLISECONDS);  | 
 | 148 | + | 
 | 149 | +		// stop task when future is completed  | 
 | 150 | +		future.whenComplete(  | 
 | 151 | +				(result, error) -> {  | 
 | 152 | +					scheduledFuture.cancel(true);  | 
 | 153 | +					// scheduler.shutdown();  | 
 | 154 | +				});  | 
 | 155 | + | 
 | 156 | +		return future;  | 
 | 157 | +	}  | 
 | 158 | + | 
 | 159 | +	// Helper class to encapsulate the result of the checkFn  | 
 | 160 | +	public static class AsyncActionResult<T> {  | 
 | 161 | +		private final boolean finished;  | 
 | 162 | +		private final T response;  | 
 | 163 | + | 
 | 164 | +		public AsyncActionResult(boolean finished, T response) {  | 
 | 165 | +			this.finished = finished;  | 
 | 166 | +			this.response = response;  | 
 | 167 | +		}  | 
 | 168 | + | 
 | 169 | +		public boolean isFinished() {  | 
 | 170 | +			return finished;  | 
 | 171 | +		}  | 
 | 172 | + | 
 | 173 | +		public T getResponse() {  | 
 | 174 | +			return response;  | 
 | 175 | +		}  | 
 | 176 | +	}  | 
 | 177 | + | 
 | 178 | +	/**  | 
 | 179 | +	 * Helper function to check http status codes during deletion of a resource.  | 
 | 180 | +	 *  | 
 | 181 | +	 * @param e ApiException to check  | 
 | 182 | +	 * @return true if resource is gone otherwise false  | 
 | 183 | +	 */  | 
 | 184 | +	public static boolean checkResourceGoneStatusCodes(ApiException apiException) {  | 
 | 185 | +		GenericOpenAPIException oapiErr = new GenericOpenAPIException(apiException);  | 
 | 186 | +		return oapiErr.getStatusCode() == HttpURLConnection.HTTP_NOT_FOUND  | 
 | 187 | +				|| oapiErr.getStatusCode() == HttpURLConnection.HTTP_FORBIDDEN;  | 
 | 188 | +	}  | 
 | 189 | +}  | 
0 commit comments