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