Trong một ứng dụng bất kỳ, ở một số thời điểm nào đó, do nhiều nguyên nhân: network chậm, kết nối đến database hay đến các external services gặp vấn đề có thể khiến cho việc process request của user mất nhiều thời gian hơn so với bình thường. Việc này sẽ ảnh hưởng đến trải nghiệm người dùng đối với ứng dụng đó các bạn!
Việc cài đặt một timeout cho các operation của ứng dụng là một việc cần thiết để giải quyết vấn đề này. Sau khoảng thời gian timeout mà chúng ta cài đặt, nếu operation của ứng dụng vẫn chưa hoàn thành, chúng ta sẽ trả về cho người dùng để báo lỗi timeout… Và đây cũng chính là ý tưởng của Timeout Pattern đó các bạn!
Các bạn có thể hiện thực Timeout Pattern cho ứng dụng của mình với thư viện Resilience4j TimeLimiter. Trong bài viết này, mình sẽ hướng dẫn các bạn làm điều này, các bạn nhé!
Điều đầu tiên, mình cần nói với các bạn là nếu các bạn đã biết qua về interface Future và CompletableFuture của Java thì các bạn sẽ thấy, sau khi đọc xong bài viết của mình, thư viện Resilience4j TimeLimiter cũng giải quyết bài toán timeout cho các operation của ứng dụng theo cách tương tự của 2 interface trên. Và thực tế thì Resilience4j cũng sử dụng Future và CompletableFuture để hiện thực solution của mình.
Bây giờ, mình sẽ tạo mới một Maven project để làm ví dụ:
Resilience4j TimeLimiter dependency được khai báo như sau:
1 2 3 4 5 |
<dependency> <groupId>io.github.resilience4j</groupId> <artifactId>resilience4j-timelimiter</artifactId> <version>2.2.0</version> </dependency> |
Giả sử mình có một ứng dụng đơn giản với class Hello có phương thức hello() trả về chuỗi “Hello from Huong Dan Java” như sau:
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"; } } |
Để giả lập việc đôi lúc, khi gọi tới phương thức hello() của class Hello, thời gian trả về sẽ bị chậm, như các bạn thấy, mình đã 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ả kết quả về cho đối tượng gọi nó.
Ứng dụng của chúng ta đơn giản như sau:
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()); } } |
Chạy chương trình, các bạn hoặc là các bạn sẽ thấy kết quả liền hoặc sau 10s các bạn mới thấy kết quả, như sau:
Giả sử bây giờ, mình muốn chương trình của mình khi gọi tới phương thức hello() của class Hello phải trả về kết quả sau 3s, nếu không thì phải throw lỗi timeout để trải nghiệm của người dùng tốt hơn. Mình sẽ sử dụng thư viện Resilience4j TimeLimiter để giải quyết vấn đề này.
Thư viện Resilience4j TimeLimiter sử dụng class TimeLimiter để định nghĩa các cấu hình liên quan đến timeout của ứng dụng. Các vấn đề này bao gồm timeout duration, có cancel task đang run sử dụng interface Future hay không, … Chúng ta sử dụng class TimeLimiterRegistry để quản lý các đối tượng TimeLimiter này.
Các bạn khởi tạo đối tượng của class TimeLimiterRegistry như sau:
1 |
TimeLimiterRegistry timeLimiterRegistry = TimeLimiterRegistry.ofDefaults(); |
Với khai báo trên, các đối tượng TimeLimiter được lấy từ đối tượng TimeLimiterRegistry sẽ có timeout duration là 1s và sẽ cancel các task đang run sử dụng interface Future các bạn nhé!
Để thay đổi các cấu hình mặc định này, các bạn có thể định nghĩa một class TimeLimiterConfig, ví dụ như sau:
1 2 3 4 |
TimeLimiterConfig config = TimeLimiterConfig.custom() .cancelRunningFuture(true) .timeoutDuration(Duration.ofMillis(3000)) .build(); |
và khởi tạo đối tượng của class TimeLimiterRegistry sử dụng đối tượng TimeLimiterConfig này, như sau:
1 2 3 4 5 6 |
TimeLimiterConfig config = TimeLimiterConfig.custom() .cancelRunningFuture(true) .timeoutDuration(Duration.ofMillis(3000)) .build(); TimeLimiterRegistry timeLimiterRegistry = TimeLimiterRegistry.of(config); |
Bây giờ, thì các bạn có thể lấy đối tượng TimeLimiter từ đối tượng TimeLimiterRegistry như sau:
1 |
TimeLimiter timeLimiter = timeLimiterRegistry.timeLimiter("hello"); |
Chúng ta có thể đặt tên của đối tượng TimeLimiter theo ý chúng ta muốn để lấy đối tượng TimeLimiter.
Bây giờ thì mình sẽ sử dụng đối tượng TimeLimiter này để implement nhu cầu mà mình mong muốn.
Để hiện thực Timeout Pattern theo cơ chế blocking (tương tự như khi sử dụng phương thức Future.get()), các bạn có thể sử dụng phương thức executeFutureSupplier() của đối tượng TimeLimiter như sau:
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(); |
Viết gọn sử dụng Lambda Expression như sau:
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(); |
Chạy chương trình, các bạn sẽ thấy kết quả nếu có lỗi timeout xảy ra như sau:
Ở trên, mình đã cấu hình khi xảy ra lỗi timeout thì running task sử dụng interface Future sẽ bị cancel, nên nếu các bạn thêm dòng log vào phương thức hello() của class Hello như sau:
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"; } } |
Chạy lại chương trình, các bạn sẽ thấy dòng log này không được in ra.
Nhưng nếu mình thay đổi cấu hình cho đối tượng TimeLimiterConfig với cancelRunningFuture(false), thì khi xảy ra lỗi timeout, các bạn sẽ thấy sau 10s dòng log này sẽ được in ra như sau:
Đó là bởi vì, mặc dù xảy ra lỗi timeout nhưng tác vụ mà chúng ta submit với đối tượng của interface Future vẫn tiếp tục chạy đó các bạn!
Hiện tại thì cấu hình cancelRunningFuture() chỉ apply được với interface Future thôi nhé các bạn!
Nếu theo cơ chế non-blocking với CompletableFuture, các bạn có thể sử dụng phương thức executeCompletionStage() của đối tượng 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 |
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(); |
Khi chạy chương trình, nếu xảy ra lỗi timeout, các bạn sẽ thấy kết quả như sau:
Như các bạn thấy, trong cả 2 trường hợp blocking và non-blocking processing, ứng dụng của chúng ta đều throw ra lỗi TimeoutException nếu thời gian trả về kết quả của phương thức hello() của đối tượng Hello quá 3s.
Hiện tại thì với CompletableFuture, mặc dù xảy ra lỗi timeout, nhưng tác vụ của chúng ta vẫn tiếp tục chạy cho đến khi kết thúc nhé các bạn! Chưa có cách nào để stop tác vụ này nếu xảy ra lỗi!