头图

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 asynchronously
FlightSearchService.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/


信码由缰
65 声望8 粉丝

“码”界老兵,分享程序人生。