I often meet with the task of transferring state between various successive calls, which are solved by a simple iteration in synchronous execution and which make it very painful in asynchronous. For example, in the case of writing an API client, it may be necessary to pass on what kind of account is an attempt to execute a request, in order to break off vain attempts and simply return an exception if the limit is exceeded. The synchronous version may look like this (the example is simplified):
HttpResponse execute(HttpRequest request) throws TimeoutException { Stopwatch timer = Stopwatch.createStarted(); for (int i = 0; i < MAX_RETRIES; i++) { if (timer.elapsed(TimeUnit.MILLISECONDS) > REQUEST_TIMEOUT) { throw new TimeoutException(); } try { return tryExecute(request); } catch (ConnectionException e) { // cycle one more time } } throw new UnreachableHostException(); } In the case of asynchronous pain begins: one way or another, the attempt number and the execution timer must fall into the method body. Here I see two solutions (and they are both so-so ):
Make a separate private state class in which to transfer everything you need, and update / re-create it for each call:
CompletableFuture<HttpResponse> execute(HttpRequest request, State state) { if (state.getTimer().elapsed(TimeUnit.MILLISECONDS) > REQUEST_TIMEOUT) { CompletableFuture<HttpResponse> synchronizer = new CompletableFuture<>(); synchronizer.completeExceptionally(new TimeoutException()); return synchronizer; } if (state.getAttempt() >= MAX_ATTEMPTS) { CompletableFuture<HttpResponse> synchronizer = new CompletableFuture<>(); synchronizer.completeExceptionally(new UnreachableHostException()); return synchronizer; } return tryExecute(request) // о да, мы идем прямо в ад. прошу не обращать // внимание на саму сложность конструкции .handle((result, throwable) -> { if (result != null) { return CompletableFuture.completedFuture(result); } if (throwable instanceof ConnectionException) { state.setAttempt(state.getAttempt() + 1); return execute(request, state); } CompletableFuture synchronizer = new CompletableFuture<>(); synchronizer.completeExceptionally(throwable); return synchronizer; }) .thenCompose(f -> f); }Make a separate private one-time request class in which to place the method itself and the required state in the form of fields. It will look about the same (only calls instead of the
statewill go tothis), so I will not give the code itself.
Both approaches look awful, require an insane amount of code, and don't impress me. The fact that the iterative version looks so simple tells me that I just didn’t get very well into the programming paradigm and misunderstand something. How to correctly convey the state in such calls?