Quản lý ngoại lệ (error/exception handling) trong Reactive

Quản lý ngoại lệ (error/exception handling) trong Reactive

Khi làm việc với reactive trong WebFlux việc ném ra ngoại lệ (throw exception) và bắt ngoại lệ (catching exception) có khác biệt so với Spring MVC, mình ghi lại một số ý chính khi làm việc anh em lưu ý

Mã nguồn của phần này mọi người có thể tham khảo tại đây

Xem lại bài này để nắm căn bản về Reactive

Ném ngoại lệ (throw exception)

Giả sử khi thực hiện validator ta cần kiểm tra độ dài của một chuỗi có thỏa mãn điều kiện không, ta thực hiện như sau, khi điều kiện không thỏa mãn ta sẽ trả về môt Mono.error() với đầu vào là một Throwable.

Phần này có sự khác biệt so với Spring MVC khi ta sẽ trả về throw new [an exception]

private Mono<Void> validateLength(String value, int minLength, int maxLength) {
        if (value == null) {
            return Mono.error(new NullPointerException("Value cannot be null")); // #1
        }

        if (value.length() < minLength || value.length() > maxLength) {
            return Mono.error(new IllegalArgumentException(
                    String.format("Value length must be between %d and %d characters", minLength, maxLength)));  // #2
        }

        return Mono.empty();
    }

Bắt ngoại lệ (catching exception)

Reactor cung cấp 6 toán tử (operators) cho việc điều khiển lỗi và ngoại lệ (error-handling)

.doOnError()

doOnError sử dụng toán tử này để in log, gửi message đến message bus, v.v… toán tử này là một ‘side effect’ tức nó không thay đổi luồng (stream chính).

.doOnError(error -> log.error("Error saving product: {}", error.getMessage()))

.onErrorMap()

.onErrorMap sử sụng toán từ này khi muốn biến một exception dự kiến sang một custom exception, như trên nếu exception là TimeoutException chúng ta sẽ chuyển sang kiểu custom là DatabaseOperationException

.onErrorMap(ex -> {
                    if (ex instanceof java.util.concurrent.TimeoutException) {
                        log.error("Operation {} timed out after {} seconds", operation, GLOBAL_TIMEOUT.toSeconds());
                        return new DatabaseOperationException("Operation timed out", ex);
                    }
                    return ex;
})

Trong bài trước về reactive mình có giới thiệu về cách xây dựng RESTful API với WebFlux mọi người

.onErrorResume()

Sử dụng toán tử này khi muốn trả về một Mono hoặc Flux khác (fallback value), một cách linh động. Với đầu vào là một Throwable được nhận từ stream, ta có thể dựa vào từng loại lỗi để trả về một.

Như đoạn code mô phỏng ta có thể thấy, khi một stream có ngoại lệ sảy ra là TimeoutException nó sẽ trả về một fallback response, các ngoại lệ khác sẽ trả về kiểu khác.

onErrorResume có thể trả về một ngoại lệ khác qua Mono.error()

.onErrorResume(error -> {
                    log.info("Fallback logic triggered");

                    if (error instanceof TimeoutException) {
                        return Mono.just(new ProductResponse(
                                "fallback-id-for-timeout",
                                "fallback-name-for-timeout",
                                "fallback-description-for-timeout",
                                BigDecimal.ZERO,
                                0,
                                Instant.now(),
                                Instant.now()
                        ));
                    } 
                    
                    return Mono.just(new ProductResponse(
                            "fallback-id",
                            "fallback-name",
                            "fallback-description",
                            BigDecimal.ZERO,
                            0,
                            Instant.now(),
                            Instant.now()
                    ));
                })

.onErrorReturn()

Nó cũng trả về một Mono hoặc Flux khác khi ngoại lệ sảy ra, nhưng onErrorReturn luôn trả về một fallback cố định và không thể trả về một Mono.error()

.onErrorReturn(new ProductResponse(
                        "default-id",
                        "default-name",
                        "default-description",
                        BigDecimal.ZERO,
                        0,
                        Instant.now(),
                        Instant.now()
                ))

.onErrorContinue()

Khi một stream có ngoại lệ sảy ra nếu sử dụng toán tử này tiếp tục xử lý các phần tử tiếp theo thay vì kết thúc xử lý với lỗi. Cách dùng như sau

.onErrorContinue((throwable, value) -> {
                    log.warn("Error processing value {}: {}", value, throwable.getMessage());
                })

.onErrorComplete()

Khi các toán tử sử dụng trước toán tử này sảy stream kết thúc, nhưng nó sẽ nuốt lỗi đó, không thông báo cho client biết là có lỗi sảy ra.

.onErrorComplete()

Retries

Khi làm việc với Reactive, nó hỗ trợ cơ chế retry, đây là một cơ chế rất hữu ích, giả sử khi kế nối đến database bị gián đoạn tạm thời, service database trả về một exception ta có thể dựa trên exception đó để thực hiện retry. Khi thực hiện retry trong một stream nó sẽ không phát ra error cho subcription, nó sẽ phát ra nếu số lần retry đạt giới hạn.

trong đoạn code trên ta thấy, mono là một stream, và cách implement retry như bên dưới, các tham số cần chú ý

  • số lần retries (maxBackoff)
  • chỉ retry khi gặp các exception được chỉ định filter(this::isTransientDatabaseException)
  • khi đạt số lần cấu hình retry ta đưa ra ngoại lệ onRetryExhaustedThrow
  • map exception nếu cần thiết onErrorMap

Mọi người có thể xem chi tiết phần này trong class RetryUtil

mono.retryWhen(Retry.backoff(maxRetries, initialBackoff)
                .maxBackoff(maxBackoff)
                .filter(this::isTransientDatabaseException)
                .doBeforeRetry(signal -> log.warn("Retrying {} (Attempt: {}). Error: {}",
                        operation, signal.totalRetries() + 1, signal.failure().getMessage()))
                .onRetryExhaustedThrow((spec, signal) -> {
                    log.error("Failed to {} after {} retries", operation, signal.totalRetries());
                    return new DatabaseOperationException(
                            "Database operation failed after multiple retries", signal.failure());
                }))
                .onErrorMap(ex -> {
                    if (ex instanceof java.util.concurrent.TimeoutException) {
                        log.error("Operation {} timed out after {} seconds", operation, GLOBAL_TIMEOUT.toSeconds());
                        return new DatabaseOperationException("Operation timed out", ex);
                    }
                    return ex;
                });

toán tử as()

Trong ProductService ta thấy có sử dụng toán tử as, toán tử này nó sẽ đẩy Mono/Flux của luồng stream vào hàm khác để xử lý, ở đây là retryUtil, khi retryUtil thấy Mono/Flux này có sảy ra ngoại lệ nó sẽ tiến hành retry.

.as(mono -> retryUtil.applyRetry(mono, "save product", log))

ResponseEntityExceptionHandler

Trong Spring MVC khi các ngoại lệ ném ra không được bắt và xử lý tại các lớp khác, ta sẽ khai báo một lớp Advice để bắt ngoại lệ này và đưa ra response tương ứng. ResponseEntityExceptionHandler là một class như vậy trong reactive.

Nó thuộc package org.springframework.web.reactive.result.method.annotation.ResponseEntityExceptionHandler và đây là cách implement

@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
    
    @ExceptionHandler(ProductNotFoundException.class)
    public ResponseEntity<ProblemDetail> handleProductNotFoundException(ProductNotFoundException ex) {
        ProblemDetail problemDetail = ProblemDetail.forStatus(HttpStatus.NOT_FOUND);
        problemDetail.setTitle("ProductNotFoundException");
        problemDetail.setDetail(ex.getMessage());
        problemDetail.setStatus(HttpStatus.NOT_FOUND);
        problemDetail.setProperty("errorCode", "PRODUCT_NOT_FOUND");
        problemDetail.setProperty("timestamp", System.currentTimeMillis());
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(problemDetail);
    }

    @ExceptionHandler(TooManyRequestsException.class)
    public ResponseEntity<ProblemDetail> handleTooManyRequestsException(TooManyRequestsException ex) {
        ProblemDetail problemDetail = ProblemDetail.forStatus(HttpStatus.TOO_MANY_REQUESTS);
        problemDetail.setTitle("TOO_MANY_REQUESTS");
        problemDetail.setDetail(ex.getMessage());
        problemDetail.setStatus(HttpStatus.TOO_MANY_REQUESTS);
        problemDetail.setProperty("errorCode", "TOO_MANY_REQUESTS");
        problemDetail.setProperty("timestamp", System.currentTimeMillis());
        return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body(problemDetail);
    }
}

Tổng kết

Nắm rõ được cách điều khiển và quản lý lỗi trong Reactive giúp chúng ta kết hợp xử lý logic nghiệp vụ và xử lý lỗi một cách hợp lý.

Retry là một cơ chế quan trọng có thể coi là một ưu điểm của reactive so với lập trình truyền thống, giúp ta dễ dàng xử lý được khi gặp các tình huống như có ngoại lệ khi kết nối giữa các service, điều này là rất hay sảy ra trong mô hình microservice.

Written by :

user

Java Developer, System Architect, Learner and becoming Youtuber

View All Posts

Để lại một bình luận

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *