Trong bài viết trước, mình đã hướng dẫn các bạn cách hiện thực Timeout Pattern với thư viện Resilience4j TimeLimiter. Đối với các ứng dụng Spring Boot thì hiện tại chưa có official starter của Spring Boot support cho thư viện Resilience4j nhưng cũng có 1 starter của thư viện Resilience4j support cho việc này, tên là resilience4j-spring-boot3. Trong bài viết này, mình sẽ hướng dẫn các bạn cách hiện thực Timeout Pattern với thư viện Resilience4j TimeLimiter trong ứng dụng Spring Boot sử dụng resilience4j-spring-boot3 starter các bạn nhé!
Đầu tiên, mình sẽ tạo mới một Spring Boot project với Web starter dependency như sau:
Kết quả:
Để sử dụng resilience4j-spring-boot3 starter, các bạn cần khai báo sử dụng thêm AOP starter dependency của Spring Boot như sau:
1 2 3 4 5 6 7 8 9 10 |
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-spring-boot3</artifactId> <version>2.2.0</version> </dependency> |
Bây giờ, mình sẽ hiện thực một ứng dụng RESTful API đơn giản với request “/hello” trả về dòng chữ “Hello from Huong Dan Java” như sau:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
package com.huongdanjava.springboot.resilience4j; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class HelloController { @GetMapping("/hello") public String hello() { return "Hello from Huong Dan Java"; } } |
Kết quả khi mình chạy ứng dụng và request tới địa chỉ “http://localhost:8080/hello” như sau:
Để giả lập việc đôi lúc, khi gọi tới request này, thời gian trả về sẽ bị chậm, mình sẽ refactor code một xí. Mình sẽ move đoạn code trả về dòng chữ “Hello from Huong Dan Java” ra một phương thức riêng và sử dụng class RandomGenerator để generate random giá trị boolean true hoặc false. Nếu giá trị random này là true, phương thức này sẽ sleep 10s trước khi trả dòng chữ này về cho đối tượng gọi nó, như sau:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
package com.huongdanjava.springboot.resilience4j; import java.util.random.RandomGenerator; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class HelloController { @GetMapping("/hello") public String hello() throws InterruptedException { return response(); } private String response() throws InterruptedException { if (RandomGenerator.getDefault().nextBoolean()) { Thread.sleep(10000); } return "Hello from Huong Dan Java"; } } |
Bây giờ chúng ta sẽ sử dụng thư viện Resilience4j TimeLimiter để hiện thực Timeout Pattern cho ứng dụng này. Nếu thời gian handle request quá 3s, chúng ta sẽ return lại lỗi timeout cho người dùng các bạn nhé!
Với resilience4j-spring-boot3 starter, các bạn không cần phải khai báo đối tượng TimeLimiterRegistry manually nữa, starter này sẽ làm cho chúng ta. Việc chúng ta cần làm là sử dụng một số properties mà starter này cung cấp để cấu hình cho các đối tượng TimeLimiter.
Để cấu hình cho tất cả các đối tượng TimeLimiter, các bạn có thể sử dụng các properties bắt đầu với “resilience4j.timelimiter.configs.default” như sau:
1 2 |
resilience4j.timelimiter.configs.default.timeout-duration=3s resilience4j.timelimiter.configs.default.cancel-running-future=true |
Như các bạn thấy, nó cũng tương tự như khi chúng ta sử dụng class TimeLimiterConfig đó các bạn!
Để cấu hình cho từng đối tượng TimeLimiter, các bạn sử dụng các properties bắt đầu với “resilience4j.timelimiter.instances” ví dụ như sau:
1 |
resilience4j.timelimiter.instances.hello.base-config=default |
Hoặc:
1 2 |
resilience4j.timelimiter.instances.hello.timeout-duration=3s resilience4j.timelimiter.instances.hello.cancel-running-future=false |
Trong đó, “hello” là tên của đối tượng TimeLimiter.
Chúng ta apply các cấu hình này cho endpoint “/hello” bằng cách khai báo sử dụng annotation @TimeLimiter trong class HelloController như sau:
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 26 27 28 29 30 31 |
package com.huongdanjava.springboot.resilience4j; import io.github.resilience4j.timelimiter.annotation.TimeLimiter; import java.util.concurrent.CompletableFuture; import java.util.random.RandomGenerator; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class HelloController { @GetMapping("/hello") @TimeLimiter(name = "hello") public CompletableFuture<String> hello() { return CompletableFuture.supplyAsync(() -> { try { return response(); } catch (InterruptedException e) { throw new RuntimeException(e); } }); } private String response() throws InterruptedException { if (RandomGenerator.getDefault().nextBoolean()) { Thread.sleep(10000); } return "Hello from Huong Dan Java"; } } |
Như các bạn thấy, mình cũng sửa lại nội dung của phương thức hello() để sử dụng interface CompletableFuture.
Lúc này, nếu chạy ứng dụng rồi request tới địa chỉ “http://localhost:8080/hello”, nếu thời gian xử lý request quá 3s, các bạn sẽ thấy lỗi timeout như sau:
Để handle lỗi này, các bạn hoặc có thể định nghĩa một Global Exception Handler, ví dụ như sau:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
package com.huongdanjava.springboot.resilience4j; import java.util.concurrent.TimeoutException; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; @ControllerAdvice public class GlobalErrorHandler { @ExceptionHandler(TimeoutException.class) @ResponseStatus(HttpStatus.REQUEST_TIMEOUT) public void handleTimeoutException() { System.out.println("Request timed out"); } } |
để trả về cho consumer HTTP status code 408 và response message lỗi theo ý của các bạn!
Hoặc các bạn cũng có thể định nghĩa một fallback method:
1 2 3 |
private CompletableFuture<String> fallback(Throwable throwable) { return CompletableFuture.supplyAsync(() -> "Hello World"); } |
rồi khai báo fallback method trong annotation @TimeLimiter như sau:
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 26 27 28 29 30 31 32 33 34 35 |
package com.huongdanjava.springboot.resilience4j; import io.github.resilience4j.timelimiter.annotation.TimeLimiter; import java.util.concurrent.CompletableFuture; import java.util.random.RandomGenerator; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class HelloController { @GetMapping("/hello") @TimeLimiter(name = "hello", fallbackMethod = "fallback") public CompletableFuture<String> hello() { return CompletableFuture.supplyAsync(() -> { try { return response(); } catch (InterruptedException e) { throw new RuntimeException(e); } }); } private String response() throws InterruptedException { if (RandomGenerator.getDefault().nextBoolean()) { Thread.sleep(10000); } return "Hello from Huong Dan Java"; } private CompletableFuture<String> fallback(Throwable throwable) { return CompletableFuture.supplyAsync(() -> "Hello World"); } } |
để trong trường hợp xảy ra lỗi, fallback method sẽ được gọi để trả về kết quả cho consumer.
Các bạn nhớ là phương thức fallback cần khai báo tham số Throwable nhé các bạn!
Cho ví dụ của mình, các bạn sẽ thấy kết quả trong trường hợp xảy ra lỗi timeout như sau:
Trong trường hợp các bạn vừa định nghĩa Global Exception Handler cho TimeoutException vừa khai báo fallback method thì fallback vẫn sẽ được sử dụng các bạn nhé!
Và như mình đã nói trong bài viết Hiện thực Timeout Pattern với thư viện Resilience4j TimeLimiter, tác vụ sử dụng với CompletableFuture sẽ không được cancel khi TimeoutException xảy ra các bạn nhé!