So far in this series, we have learned about Resilience4j and its [Retry](
https://icodewalker.com/blog/261/) and [RateLimiter](
https://icodewalker.com/blog/288/) module. In this article, we will continue to explore Resilience4j through TimeLimiter. We will understand what problems it solves, when and how to use it, and look at some examples.
Code example
This article is attached [GitHub on](
https://github.com/thombergs/code-examples/tree/master/resilience4j/timelimiter) working code example.
What is Resilience4j?
Please refer to the description in the previous article for a quick understanding of [General Working Principle of Resilience4j](
https://icodewalker.com/blog/261/#what-is-resilience4j)。
What is a time limit?
Setting a limit on the time we are willing to wait for an operation to complete is called a time limit. If the operation is not completed within the time specified by us, we would like to be notified by a timeout error.
Sometimes this is also called "setting a deadline."
One of the main reasons we do this is to ensure that we do not let users or customers wait indefinitely. A slow service that does not provide any feedback can frustrate users.
Another reason we set time limits on operations is to ensure that we do not occupy server resources indefinitely. timeout
value specified when we use Spring's @Transactional
annotation is an example-in this case, we don't want to occupy database resources for a long time.
When to use Resilience4j TimeLimiter?
Resilience4j of TimeLimiter may be provided for use CompleteableFutures
time limit asynchronous operation implemented (time-out).
CompletableFuture
class introduced in Java 8 makes asynchronous, non-blocking programming easier. You can execute slow methods on different threads and release the current thread to handle other tasks. We can provide a callback to be executed slowMethod()
int slowMethod() {
// time-consuming computation or remote operation
return 42;
}
CompletableFuture.supplyAsync(this::slowMethod)
.thenAccept(System.out::println);
slowMethod()
here can be some calculation or remote operation. Usually, we want to set a time limit when making such asynchronous calls. We don't want to wait indefinitely for slowMethod()
to return. For example, if slowMethod()
takes more than one second, we may want to return the previously calculated, cached value, or it may even go wrong.
CompletableFuture
of Java 8, there is no easy way to set the time limit for asynchronous operations. CompletableFuture
implements the Future
interface, and Future
has an overloaded get()
method to specify how long we can wait:
CompletableFuture<Integer> completableFuture = CompletableFuture
.supplyAsync(this::slowMethod);
Integer result = completableFuture.get(3000, TimeUnit.MILLISECONDS);
System.out.println(result);
But there is a problem here-the get()
method is a blocking call. So it first defeats the purpose of using CompletableFuture, which is to release the current thread.
This is TimeLimiter
-it allows us to set time limits on asynchronous operations, while retaining the non-blocking benefits of CompletableFuture
CompletableFuture
has been resolved in Java 9. We can use in Java and later 9 CompletableFuture
on orTimeout()
or completeOnTimeout()
direct method to set a time limit. However, with Resilience4J
's indicators and events , it still provides added value compared to ordinary Java 9 solutions.
Resilience4j TimeLimiter concept
TimeLimiter
supports Future
and CompletableFuture
. But using it with Future
equivalent to Future.get(long timeout, TimeUnit unit)
. Therefore, we will focus on CompletableFuture
in the rest of this article.
Like other Resilience4j modules, TimeLimiter
works is to decorate our code with the required functions-if the operation is not timeoutDuration
in this case, it returns TimeoutException
.
We TimeLimiter
provide timeoutDuration
, ScheduledExecutorService
and asynchronous operation itself, expressed as CompletionStage
of Supplier
. It returns a CompletionStage
decorated Supplier
.
Internally, it uses the scheduler to schedule a timeout task-by throwing a TimeoutException
to complete the CompletableFuture
task. If the operation is completed first, TimeLimiter
cancels the internal timeout task.
In addition timeoutDuration
addition, there is another with TimeLimiter
associated configuration cancelRunningFuture
. This configuration is only applies to Future
not apply to CompletableFuture. When the timeout occurs, it will cancel the running TimeoutException
Future
.
Use Resilience4j TimeLimiter module
TimeLimiterRegistry
, TimeLimiterConfig
and TimeLimiter
are the main abstractions resilience4j-timelimiter
TimeLimiterRegistry
is a factory used to create and manage TimeLimiter
TimeLimiterConfig
encapsulates the timeoutDuration
and cancelRunningFuture
configurations. Each TimeLimiter
object is associated with a TimeLimiterConfig
.
TimeLimiter
provides auxiliary methods to create or execute decorators Future
and CompletableFuture
Suppliers
Let's see how to use the various functions available in the TimeLimiter
We will use the same examples as in the previous articles in this series. Suppose we are building a website for an airline to allow its customers to search and book flights. Our service talks to the remote service encapsulated in the FlightSearchService
The first step is to create a TimeLimiterConfig
:
TimeLimiterConfig config = TimeLimiterConfig.ofDefaults();
This will create a TimeLimiterConfig with default values of timeoutDuration
(1000ms) and cancelRunningFuture (true).
Suppose we want to set the timeout value to 2s instead of the default value:
TimeLimiterConfig config = TimeLimiterConfig.custom()
.timeoutDuration(Duration.ofSeconds(2))
.build();
Then we create a TimeLimiter
:
TimeLimiterRegistry registry = TimeLimiterRegistry.of(config);
TimeLimiter limiter = registry.timeLimiter("flightSearch");
We want to call asynchronouslyFlightSearchService.searchFlights()
, which returns a List<Flight>
. Let us express it as Supplier<CompletionStage<List<Flight>>>
:
Supplier<List<Flight>> flightSupplier = () -> service.searchFlights(request);
Supplier<CompletionStage<List<Flight>>> origCompletionStageSupplier =
() -> CompletableFuture.supplyAsync(flightSupplier);
Then we can use TimeLimiter
decorate Supplier
:
ScheduledExecutorService scheduler =
Executors.newSingleThreadScheduledExecutor();
Supplier<CompletionStage<List<Flight>>> decoratedCompletionStageSupplier =
limiter.decorateCompletionStage(scheduler, origCompletionStageSupplier);
Finally, let's call the decorated asynchronous operation:
decoratedCompletionStageSupplier.get().whenComplete((result, ex) -> {
if (ex != null) {
System.out.println(ex.getMessage());
}
if (result != null) {
System.out.println(result);
}
});
The following is a sample output of a successful flight search, which took less than the 2 seconds we specified timeoutDuration
:
Searching for flights; current time = 19:25:09 783; current thread = ForkJoinPool.commonPool-worker-3
Flight search successful
[Flight{flightNumber='XY 765', flightDate='08/30/2020', from='NYC', to='LAX'}, Flight{flightNumber='XY 746', flightDate='08/30/2020', from='NYC', to='LAX'}] on thread ForkJoinPool.commonPool-worker-3
This is a sample output of a timed flight search:
Exception java.util.concurrent.TimeoutException: TimeLimiter 'flightSearch' recorded a timeout exception on thread pool-1-thread-1 at 19:38:16 963
Searching for flights; current time = 19:38:18 448; current thread = ForkJoinPool.commonPool-worker-3
Flight search successful at 19:38:18 461
The timestamp and thread name above indicate that even if the asynchronous operation is completed later on another thread, the calling thread will receive a TimeoutException.
If we want to create a decorator and reuse it in different places in the code base, we will use decorateCompletionStage()
. If we want to create it and execute Supplier<CompletionStage>
immediately, we can use the executeCompletionStage()
instance method instead:
CompletionStage<List<Flight>> decoratedCompletionStage =
limiter.executeCompletionStage(scheduler, origCompletionStageSupplier);
TimeLimiter event
TimeLimiter
has a EventPublisher
which generates events of type TimeLimiterOnSuccessEvent
, TimeLimiterOnErrorEvent
and TimeLimiterOnTimeoutEvent
We can monitor these events and record them, for example:
TimeLimiter limiter = registry.timeLimiter("flightSearch");
limiter.getEventPublisher().onSuccess(e -> System.out.println(e.toString()));
limiter.getEventPublisher().onError(e -> System.out.println(e.toString()));
limiter.getEventPublisher().onTimeout(e -> System.out.println(e.toString()));
The sample output shows what was logged:
2020-08-07T11:31:48.181944: TimeLimiter 'flightSearch' recorded a successful call.
... other lines omitted ...
2020-08-07T11:31:48.582263: TimeLimiter 'flightSearch' recorded a timeout exception.
TimeLimiter indicator
TimeLimiter
tracks the number of successful, failed, and timed out calls.
First, we create TimeLimiterConfig
, TimeLimiterRegistry
and TimeLimiter
as usual. Then, we create a MeterRegistry
and TimeLimiterRegistry
to it:
MeterRegistry meterRegistry = new SimpleMeterRegistry();
TaggedTimeLimiterMetrics.ofTimeLimiterRegistry(registry)
.bindTo(meterRegistry);
After running several time-limited operations, we display the captured metrics:
Consumer<Meter> meterConsumer = meter -> {
String desc = meter.getId().getDescription();
String metricName = meter.getId().getName();
String metricKind = meter.getId().getTag("kind");
Double metricValue =
StreamSupport.stream(meter.measure().spliterator(), false)
.filter(m -> m.getStatistic().name().equals("COUNT"))
.findFirst()
.map(Measurement::getValue)
.orElse(0.0);
System.out.println(desc + " - " +
metricName +
"(" + metricKind + ")" +
": " + metricValue);
};
meterRegistry.forEachMeter(meterConsumer);
This is some sample output:
The number of timed out calls - resilience4j.timelimiter.calls(timeout): 6.0
The number of successful calls - resilience4j.timelimiter.calls(successful): 4.0
The number of failed calls - resilience4j.timelimiter.calls(failed): 0.0
In practical applications, we regularly export data to the monitoring system and analyze it on the dashboard.
Pitfalls and good practices when implementing time limits
Generally, we deal with two operations-query (or read) and command (or write). Time limits on queries are safe because we know they will not change the state of the system. searchFlights()
operation we saw is an example of a query operation.
Commands usually change the state of the system. bookFlights()
operation will be an example of the command. When imposing a time limit on a command, we must remember that when we time out, the command is likely to be still running. For example, bookFlights()
on call TimeoutException
does not necessarily mean that the command failed.
In this case, we need to manage the user experience-maybe at the time of the timeout, we can notify the user that the operation takes longer than we expected. Then we can query upstream to check the status of the operation and notify the user later.
in conclusion
In this article, we learned how to use Resilience4j's TimeLimiter
module to set time limits for asynchronous, non-blocking operations. We learned when to use it and how to configure it through some practical examples.
You can use [GitHub on](
https://github.com/thombergs/code-examples/tree/master/resilience4j/timelimiter) demonstrates a complete application to illustrate these ideas.
This article is translated from:
https://reflectoring.io/time-limiting-with-resilience4j/
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。