In any application, at some point, due to many reasons: a slow network, connection to a database or external services having problems can make the user’s request process take longer than usual. This will affect the user experience for that application!
Setting a timeout for the application’s operations is necessary to solve this problem. After the timeout period that we set, if the application’s operation is still not completed, we will return to the user to report a timeout error… And this is also the idea of the Timeout Pattern!
You can implement the Timeout Pattern for your application with the Resilience4j TimeLimiter library. In this tutorial, I will guide you to do this, guys!
First of all, I need to tell you that if you know about Java’s Future and CompletableFuture interfaces, you will see that after reading my article, the Resilience4j TimeLimiter library also solves the timeout problem for application operations in a similar way to the above two interfaces. Resilience4j also uses Future and CompletableFuture to implement its solution.
Now, I will create a new Maven project as an example:
Resilience4j TimeLimiter dependency is declared as follows:
1 2 3 4 5 |
<dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-timelimiter</artifactId> <version>2.2.0</version> </dependency> |
Suppose I have a simple application with a Hello class whose hello() method returns the string “Hello from Huong Dan Java” as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
package com.huongdanjava.resilience4j; import java.util.random.RandomGenerator; public class Hello { public String hello() throws InterruptedException { if (RandomGenerator.getDefault().nextBoolean()) { Thread.sleep(10000); } return "Hello from Huong Dan Java"; } } |
To simulate the fact that sometimes, when calling the hello() method of the Hello class, the return time will be slow, as you can see, I used the RandomGenerator class to randomly generate a boolean value of true or false. If this random value is true, this method will sleep for 10s before returning the result to the object that called it.
Our application is simple as follows:
1 2 3 4 5 6 7 8 9 |
package com.huongdanjava.resilience4j; public class Main { public static void main(String[] args) throws InterruptedException { Hello hello = new Hello(); System.out.println(hello.hello()); } } |
Run the program, you will either see the result immediately or after 10 seconds you will see the result, as follows:
Suppose now, I want my program when calling the hello() method of the Hello class to return the result after 3 seconds, otherwise, it must throw a timeout error for a better user experience. I will use the Resilience4j TimeLimiter library to solve this problem.
The Resilience4j TimeLimiter library uses the TimeLimiter class to define the configurations related to the application’s timeout. These configurations include timeout duration, whether to cancel the running task which is using the Future interface or not, … We use the TimeLimiterRegistry object to manage these TimeLimiter objects.
You initialize the object of the TimeLimiterRegistry class as follows:
1 |
TimeLimiterRegistry timeLimiterRegistry = TimeLimiterRegistry.ofDefaults(); |
With the above declaration, TimeLimiter objects taken from the TimeLimiterRegistry object will have a timeout duration of 1s and will cancel running tasks which is using the Future interface!
To change these default configurations, you can define a TimeLimiterConfig class, for example as follows:
1 2 3 4 |
TimeLimiterConfig config = TimeLimiterConfig.custom() .cancelRunningFuture(true) .timeoutDuration(Duration.ofMillis(3000)) .build(); |
and initialize an object of the TimeLimiterRegistry class using this TimeLimiterConfig object, as follows:
1 2 3 4 5 6 |
TimeLimiterConfig config = TimeLimiterConfig.custom() .cancelRunningFuture(true) .timeoutDuration(Duration.ofMillis(3000)) .build(); TimeLimiterRegistry timeLimiterRegistry = TimeLimiterRegistry.of(config); |
Now, you can get the TimeLimiter object from the TimeLimiterRegistry object like this:
1 |
TimeLimiter timeLimiter = timeLimiterRegistry.timeLimiter("hello"); |
We can name the TimeLimiter object as we want to get the TimeLimiter object.
Now we will use this TimeLimiter object to implement the need we want.
To implement the Timeout Pattern by blocking mechanism (similar to when using the Future.get() method), you can use the executeFutureSupplier() method of the TimeLimiter object as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
Hello hello = new Hello(); ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); String result = timeLimiter.executeFutureSupplier(new Supplier<>() { @Override public Future<String> get() { return scheduler.submit(new Callable<String>() { @Override public String call() throws Exception { return hello.hello(); } }); } }); System.out.println(result); scheduler.shutdown(); |
Re-write with Lambda Expression as follows:
1 2 3 4 5 6 7 8 9 |
Hello hello = new Hello(); ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); String result = timeLimiter.executeFutureSupplier(() -> scheduler.submit(() -> hello.hello())); System.out.println(result); scheduler.shutdown(); |
Run the program, you will see the result if a timeout error occurs as follows:
Above, I have configured that when a timeout error occurs, the running task using the Future interface will be canceled, so if you add a log line to the hello() method of the Hello class as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
package com.huongdanjava.resilience4j; import java.util.random.RandomGenerator; public class Hello { public String hello() throws InterruptedException { if (RandomGenerator.getDefault().nextBoolean()) { Thread.sleep(10000); System.out.println("Resumed"); } return "Hello from Huong Dan Java"; } } |
Run the program again, you will see that this log line is not printed.
But if I change the configuration for the TimeLimiterConfig object with cancelRunningFuture(false), then when a timeout error occurs, you will see that after 10s this log line will be printed as follows:
That’s because, even though the timeout error occurs, the task we submit with the Future interface object continues to run!
Currently, the cancelRunningFuture() configuration can only be applied to the Future interface!
If you follow the non-blocking mechanism with CompletableFuture, you can use the executeCompletionStage() method of the TimeLimiter object as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
Hello hello = new Hello(); ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); CompletableFuture<String> completableFuture = timeLimiter.executeCompletionStage(scheduler, () -> CompletableFuture.supplyAsync(() -> { try { return hello.hello(); } catch (InterruptedException e) { throw new RuntimeException(e); } })).toCompletableFuture(); completableFuture.whenComplete((r, ex) -> { if (ex != null) { ex.printStackTrace(); } completableFuture.thenAccept(System.out::println); }); System.out.println("End"); Thread.sleep(11000); scheduler.shutdown(); |
When running the program, if a timeout error occurs, you will see the following result:
As you can see, in both blocking and non-blocking processing cases, our application throws a TimeoutException if the time it takes for the hello() method of the Hello object to return the result is over 3 seconds.
Currently, with CompletableFuture, even though the timeout error occurs, our task will continue to run until it finishes! There is no way to stop this task if an error occurs!