Asynchronous timeout Processing Using CompletableFuture in Java
One day, I was stuck by Future. get () When I improved multi-threaded code.
public void serve() throws InterruptedException, ExecutionException, TimeoutException { final Future<Response> responseFuture = asyncCode(); final Response response = responseFuture.get(1, SECONDS); send(response);}private void send(Response response) { //...}
This is an Akka application written in Java and uses a thread pool containing 1000 threads !) -- All threads are blocked in this get. The processing speed of the system cannot keep up with the number of concurrent requests. After reconstruction, we have killed all of these threads and kept only one, greatly reducing the memory usage. Here is a simple example of Java 8. The first step is to use CompletableFuture to replace the simple Future. See Tip 9 ).
- To submit a task to ExecutorService, you only need to use CompletableFuture. supplyAsync (..., ExecutorService) to replace executorService. submit (...) You can.
- Process callback-Based APIS: Use promises
Otherwise, if you have used a blocked API or Future <T>), many threads will be blocked. That's why so many asynchronous APIs are annoying. So, let's rewrite the previous code to receive CompletableFuture:
public void serve() throws InterruptedException, ExecutionException, TimeoutException { final CompletableFuture<Response> responseFuture = asyncCode(); final Response response = responseFuture.get(1, SECONDS); send(response);}
Obviously, this cannot solve any problem. We must also use the new style for programming:
public void serve() { final CompletableFuture<Response> responseFuture = asyncCode(); responseFuture.thenAccept(this::send);}
This function is equivalent, but serve () will only run for a short period of time without blocking or waiting ). Remember: this: send will be executed in the same thread that completes responseFuture. If you don't want to spend too much time reload an existing thread pool or the send () method, you can consider using thenAcceptAsync (this: send, sendPool, however, we lose two important attributes: Exception Propagation and timeout. Exception Propagation is hard to implement because we have changed the API. When serve () exists, asynchronous operations may not be completed yet. If you are concerned about exceptions, you can consider returning responseFutureor or other optional mechanisms. At least, there should be abnormal logs, otherwise the exception will be swallowed up.
final CompletableFuture<Response> responseFuture = asyncCode();responseFuture.exceptionally(throwable -> { log.error("Unrecoverable error", throwable); return null;});
Please be careful when the above Code: predictionally () tries to recover from failure and returns an optional result. Although this part can work normally, if you use chained call to predictionally () and withthenAccept (), the send () method will still be called even if the call fails, and a null parameter will be returned, or any other value returned from the predictionally () method.
responseFuture .exceptionally(throwable -> { log.error("Unrecoverable error", throwable); return null; }) .thenAccept(this::send); //probably not what you think
The problem of one-second timeout loss is very clever. The original code waits for a maximum of one second before the Future is finished.) Otherwise, A TimeoutException will be thrown. We have lost this function. Even worse, unit test timeout is not very convenient and we often skip this step. To maintain the timeout mechanism without disrupting the event-driven principle, we need to establish an additional module: a Future that will inevitably fail after a given time.
public static <T> CompletableFuture<T> failAfter(Duration duration) { final CompletableFuture<T> promise = new CompletableFuture<>(); scheduler.schedule(() -> { final TimeoutException ex = new TimeoutException("Timeout after " + duration); return promise.completeExceptionally(ex); }, duration.toMillis(), MILLISECONDS); return promise;}private static final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool( 1, new ThreadFactoryBuilder() .setDaemon(true) .setNameFormat("failAfter-%d") .build());
This is simple: we create a promise without a background task or a Future of the thread pool), and then throw a TimeoutException exception after the given java. time. Duration. If get () is called somewhere to obtain the Future, TimeoutException will be thrown when the blocking time reaches the specified time.
In fact, it is an ExecutionException that encapsulates TimeoutException. Note that I use a thread pool with a fixed thread. This is not just for the purpose of teaching: This is a scenario where "one thread should be able to meet the needs of anyone. FailAfter () is not very useful, but if it is used with ourresponseFuture, we can solve this problem.
final CompletableFuture<Response> responseFuture = asyncCode();final CompletableFuture<Response> oneSecondTimeout = failAfter(Duration.ofSeconds(1));responseFuture .acceptEither(oneSecondTimeout, this::send) .exceptionally(throwable -> { log.error("Problem", throwable); return null; });
Many other things have been done here. When a background task receives responseFuture, we also create a "merged" oneSecondTimeout future. This will never be executed when the task is successful, but it will fail in 1 second. Now we join these two called acceptEither. This operation will first complete the code block of the Future, and simply ignore the slow running of responseFuture or oneSecondTimeout. If the asyncCode () code is executed within one second, this: send will be called, and the oneSecondTimeout exception will not be thrown. However, if the execution of asyncCode () is really slow, the oneSecondTimeout exception will be thrown first. If a task fails due to an exception, the exceptionallyerror processor is called instead of the this: send method. You can choose to execute send () or predictionally, but not both. For example, if two "normal" Future operations are completed normally, the first response will call the send () method, and the subsequent ones will be discarded.
This is not the clearest solution. A clearer solution is to wrap the original Future and ensure that it can be executed within a given period of time. This operation applies to com. twitter. util. future is a viable Scala called within (), but scala. concurrent. the Future does not have this function, presumably to encourage the use of the previous method ). We will not discuss how to execute Scala at the moment. We will first implement a CompletableFuture-like operation. It accepts a Future as the input and returns a Future, which is completed when the background task is completed. However, if the underlying Future execution takes too long, an exception is thrown:
public static <T> CompletableFuture<T> within(CompletableFuture<T> future, Duration duration) { final CompletableFuture<T> timeout = failAfter(duration); return future.applyToEither(timeout, Function.identity());}
This leads us to achieve the ultimate, clear, and flexible method:
final CompletableFuture<Response> responseFuture = within( asyncCode(), Duration.ofSeconds(1));responseFuture .thenAccept(this::send) .exceptionally(throwable -> { log.error("Unrecoverable error", throwable); return null; });
I hope you like this article because you already know that implementing responsive programming in Java is no longer a problem.