[gRPC] – gRPC Error Handling

Trong các ứng dụng microservices ngày nay việc giao tiếp giữa các service thì gRPC là một lựa chọn tốt. Trong một ứng dụng Client-Server đơn giản, Client gửi yêu cầu đến Server vá Server xử lý yêu cầu và gửi lại phản hồi. Nhưng không phải khi nào Client cũng gửi một yêu cầu

Trong các ứng dụng microservices ngày nay việc giao tiếp giữa các service thì gRPC là một lựa chọn tốt. Trong một ứng dụng Client-Server đơn giản, Client gửi yêu cầu đến Server vá Server xử lý yêu cầu và gửi lại phản hồi. Nhưng không phải khi nào Client cũng gửi một yêu cầu hợp lệ. Client có thể gửi yêu cầu mà không kèm thông tin xác thực (access_token) hoặc các giá trị tham số có thể nằm ngoài phạm vi mà Server không thể xử lý, v.v. Trong những trường hợp đó, Server phải gửi lại thông báo/mã lỗi đến Client.

Sample Application

Chúng ta hãy xem xét một ứng dụng để tính bình phương cho một số. Client gửi một số mà Server phản hồi bình phương của số đó.

Giả sử Server chỉ có khả năng tính bình phương cho các số từ 2 đến 20. Bất kỳ số nào nằm ngoài phạm vi này sẽ bị từ chối với thông báo lỗi thích hợp.

Protobuf – Service Definition

Chúng ta sẽ tiến hành định nghĩa dịch vụ cho kịch bản trên.

syntax = "proto3";

package calculator;

option java_package = "example.calculator";
option java_multiple_files = true;

message Request {
  int32 number = 1;
}

message Response {
  int32 result = 1;
}

service CalculatorService {
  rpc findSquare(Request) returns (Response) {};
}

Khi chúng ta chạy lệnh maven dưới đây, maven sẽ tự động tạo code cho client application và server application bằng công cụ protoc.

mvn clean compile

Một file định dạng “.proto” định nghĩa dịch vụ thực hiện hầu hết các công việc tạo ra các class (gen code) đối với việc giao tiếp giữa Client và Server.

Class CalculatorServiceImplBase là abstract class được tạo tự động khi gen code cần được phía Server implements. Tương tự CalculatorServiceStub là class mà phía client application sử dụng để gửi yêu cầu đến server.

Server Side

Đầu tiên chúng ta tạo ra class GrpcSquareService cho phép thực hiện phép tính bình phương, class này là implementation của class base CalculatorServiceImplBase để implements phương thức findSquare (phương thức triển khai nghiệp vụ tính bình phương).

public class GrpcSquareService extends CalculatorServiceGrpc.CalculatorServiceImplBase {

    @Override
    public void findSquare(Request request, StreamObserver<Response> responseObserver) {
        int number = request.getNumber();
        Response response = Response.newBuilder()
                                    .setResult(number * number)
                                    .build();
        responseObserver.onNext(response);
        responseObserver.onCompleted();
    }
}

Tiếp theo, chúng ta cần start gRPC server để cung cấp dịch vụ cho Client.

public class CalculatorServer {

    public static void main(String[] args) throws IOException, InterruptedException {

        // build gRPC server
        Server server = ServerBuilder.forPort(6565)
                .addService(new GrpcSquareService())
                .build();

        // start
        server.start();

        // shutdown hook
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println("gRPC server is shutting down!");
            server.shutdown();
        }));

        server.awaitTermination();

    }

}

Success Response

Trước tiên, chúng ta cùng xem happy case, trường hợp Client gửi request và nhận về response thành công.

public class SquareServiceTest {

    private ManagedChannel channel;
    private CalculatorServiceGrpc.CalculatorServiceBlockingStub clientStub;

    @Before
    public void setup(){
        this.channel = ManagedChannelBuilder.forAddress("localhost", 6565)
                .usePlaintext()
                .build();
        this.clientStub = CalculatorServiceGrpc.newBlockingStub(channel);
    }

    @Test
    public void squareServiceHappyPath(){
        // build the request object
        Request request = Request.newBuilder()
                .setNumber(50)
                .build();
        Response response = this.clientStub.findSquare(request);
        System.out.println("Success Response : " + response.getResult());
    }

    @After
    public void teardown(){
        this.channel.shutdown();
    }

}

Output:

Success Response : 2500

gRPC Error Handling – OnError

Server sẽ xác thực đầu vào và nếu nó không nằm trong phạm vi đã cho, nó có thể sử dụng phương thức onError của StreamObserver để cho phản hồi lại cho Client biết rằng request gửi đến không hợp lệ.

public class GrpcSquareService extends CalculatorServiceGrpc.CalculatorServiceImplBase {

    @Override
    public void findSquare(Request request, StreamObserver<Response> responseObserver) {
        int number = request.getNumber();

        if(number < 2 || number > 20){
            Status status = Status.FAILED_PRECONDITION.withDescription("Not between 2 and 20");
            responseObserver.onError(status.asRuntimeException());
            return;
        }
        
        // only valid ranges
        Response response = Response.newBuilder()
                                    .setResult(number * number).
                                    build();
        responseObserver.onNext(response);
        responseObserver.onCompleted();
    }
    
}

Nếu Client gửi một request không hợp lệ, Client sẽ nhận lại thông báo lỗi như dưới đây.

io.grpc.StatusRuntimeException: FAILED_PRECONDITION: Not between 2 and 20

    at io.grpc.stub.ClientCalls.toStatusRuntimeException(ClientCalls.java:244)
    at io.grpc.stub.ClientCalls.getUnchecked(ClientCalls.java:225)
    at io.grpc.stub.ClientCalls.blockingUnaryCall(ClientCalls.java:142)

Chúng ta có thể bắt ngoại lệ trong khối try-catch như bình thường và truy cập đối tượng Status từ ngoại lệ.

try{
    Response response = this.clientStub.findSquare(request);
    System.out.println("Success Response : " + response.getResult());
}catch (Exception e){
    Status status = Status.fromThrowable(e);
    System.out.println(status.getCode() + " : " + status.getDescription());
}

gRPC Error Handling – Metadata

Cách tiếp cận trên hoạt động tốt. Tuy nhiên, chúng ta chỉ có thể gửi một trong các mã lỗi được xác định trước của gRPC. Nếu chúng ta cần gửi một số mã lỗi/thông điệp/đối tượng mà chúng ta định nghĩa thì trong trường hợp này, trước tiên chúng ta phải xác định cách phản hồi lỗi của chúng ta bằng cách định nghĩa chúng trên file “.proto” mà chúng ta định nghĩa dịch vụ.

  • Ví dụ chúng ta sử dụng một số mã lỗi bằng cách sử dụng enum
  • Chúng ta cũng định nghĩa một đối tượng là ErrorResponse với các tham số tùy chỉnh.
syntax = "proto3";

package calculator;

option java_package = "example.calculator";
option java_multiple_files = true;

message Request {
  int32 number = 1;
}

message Response {
  int32 result = 1;
}

enum ErrorCode {
  ABOVE_20 = 0;
  BELOW_2 = 1;
}

message ErrorResponse {
  int32 input = 1;
  ErrorCode error_code = 2;
}

service CalculatorService {
  rpc findSquare(Request) returns (Response) {};
}
  • Ở phía Server, chúng ta buid đối tượng ErrorResponse và gửi nó đến Client thông qua Metadata khi Client gửi request không hợp lệ.
@Override
public void findSquare(Request request, StreamObserver<Response> responseObserver) {
    int number = request.getNumber();

    if(number < 2 || number > 20){
        Metadata metadata = new Metadata();
        Metadata.Key<ErrorResponse> responseKey = ProtoUtils.keyForProto(ErrorResponse.getDefaultInstance());
        ErrorCode errorCode = number > 20 ? ErrorCode.ABOVE_20 : ErrorCode.BELOW_2;
        ErrorResponse errorResponse = ErrorResponse.newBuilder()
                .setErrorCode(errorCode)
                .setInput(number)
                .build();
        // pass the error object via metadata
        metadata.put(responseKey, errorResponse);
        responseObserver.onError(Status.FAILED_PRECONDITION.asRuntimeException(metadata));
        return;
    }

    // only valid ranges
    Response response = Response.newBuilder()
                                .setResult(number * number).
                                build();
    responseObserver.onNext(response);
    responseObserver.onCompleted();
}
  • Ở phía Client, chúng ta thực hiện try/catch để bắt lỗi. Nhưng chúng ta có thể truy cập metadata và đối tượng ErrorResponse từ ngoại lệ xảy ra.
try{
    Response response = this.clientStub.findSquare(request);
    System.out.println("Success Response : " + response.getResult());
}catch (Exception e){
    Metadata metadata = Status.trailersFromThrowable(e);
    ErrorResponse errorResponse = metadata.get(ProtoUtils.keyForProto(ErrorResponse.getDefaultInstance()));
    System.out.println(errorResponse.getInput() + " : " + errorResponse.getErrorCode());
}

Output:

50 : ABOVE_20

gRPC Error Handling – OneOf

Trong một số trường hợp chúng ta không muốn coi số nằm ngoài phạm vi cho phép là lỗi là ngoại lệ, chúng ta cũng có thể gửi lại response thích hợp thay vì ngoại lệ. Server có thể gửi lại một trong hai message phản hồi bằng cách sử dụng oneof

File định nghĩa dịch vụ protobuf khi đó như sau:

message Request {
  int32 number = 1;
}

message SuccessResponse {
  int32 result = 1;
}

enum ErrorCode {
  ABOVE_20 = 0;
  BELOW_2 = 1;
}

message ErrorResponse {
  int32 input = 1;
  ErrorCode error_code = 2;
}

message Response {
  oneof response {
    SuccessResponse success_response = 1;
    ErrorResponse error_response = 2;
  }
}

service CalculatorService {
  rpc findSquare(Request) returns (Response) {};
}
  • Server side
@Override
public void findSquare(Request request, StreamObserver<Response> responseObserver) {
    int number = request.getNumber();

    Response.Builder builder = Response.newBuilder();
    if(number < 2 || number > 20){
        ErrorCode errorCode = number > 20 ? ErrorCode.ABOVE_20 : ErrorCode.BELOW_2;
        ErrorResponse errorResponse = ErrorResponse.newBuilder()
                .setInput(number)
                .setErrorCode(errorCode)
                .build();
        builder.setErrorResponse(errorResponse);
    }else{
        // only valid ranges
        builder.setSuccessResponse(SuccessResponse.newBuilder().setResult(number * number).build());
    }
    responseObserver.onNext(builder.build());
    responseObserver.onCompleted();
}
  • Bây giờ Client sẽ không nhận được bất kỳ ngoại lệ nào. Thay vào đó, nó sẽ nhận được 2 loại phản hồi có thể là SUCCESS_RESPONSE hoặc ERROR_RESPONSE. Tùy thuộc vào loại đối tượng mà chúng ta nhận được, chúng ta ta sẽ đưa ra những xử lý thích hợp.
Response response = this.clientStub.findSquare(request);

switch (response.getResponseCase()){
    case SUCCESS_RESPONSE:
        System.out.println("Success Response : " + response.getSuccessResponse().getResult());
        break;
    case ERROR_RESPONSE:
        System.out.println("Error Response : " + response.getErrorResponse().getErrorCode());
        break;
}

Ví dụ, gửi 2 request với 2 số là 10 và 50, chúng ta nhận được kết quả như sau:

Success Response : 100
Error Response : ABOVE_20

Tổng kết

Trên đây là một số các cách xử lý lỗi vởi gRPC. Mọi người có thể sử dụng bất kỳ tùy chọn nào trong số các cách này tùy vào trường hợp sử dụng. Hi vọng bài viết hữu ích với mọi người.

Nguồn:https://thenewstack.wordpress.com/2021/11/24/grpc-grpc-error-handling/

Follow me: thenewstack.wordpress.com

Nguồn: viblo.asia

Bài viết liên quan

WebP là gì? Hướng dẫn cách để chuyển hình ảnh jpg, png qua webp

WebP là gì? WebP là một định dạng ảnh hiện đại, được phát triển bởi Google

Điểm khác biệt giữa IPv4 và IPv6 là gì?

IPv4 và IPv6 là hai phiên bản của hệ thống địa chỉ Giao thức Internet (IP). IP l

Check nameservers của tên miền xem website trỏ đúng chưa

Tìm hiểu cách check nameservers của tên miền để xác định tên miền đó đang dùn

Mình đang dùng Google Domains để check tên miền hàng ngày

Từ khi thông báo dịch vụ Google Domains bỏ mác Beta, mình mới để ý và bắt đầ