gRPC exception handling flow design

Posted by binto on Fri, 04 Mar 2022 16:55:35 +0100

stay My blog Read this article

1. Core demands

  • Service provider exceptions can be perceived by service consumers
  • Exception classification and handling:
    1. If the business is abnormal, you need to return the corresponding error code to facilitate the service consumer's prompt + log of international copywriting.
    2. Non business exceptions (such as NPE) need to return content to the service consumer for perception.
  • Expansibility & the process should be as simple as possible

2. Scheme selection

2.1. Directly call the OnError method and return after passing the Status wrapper exception

example:

try {

}catch (Throwable t) {
// Throwable t | StreamObserver<xxx> responseObserver
responseObserver.onError(Status.UNKNOWN
                  .withDescription(t.getMessage())
                  .withCause(t)
                  .asRuntimeException());
}

In this way, the client can perceive it, but the information that can be put in is limited. It can only be a string. It can only be in the parameter withDescription. If multiple parameters are required, it may be converted into a string with the help of some serialization frameworks.

2.2. With the help of protobuf's OneOf syntax

protobuf file:

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;
}

// The point here is that there are two kinds of callbacks, one success and the other failure
message Response {
  oneof response {
    SuccessResponse success_response = 1;
    ErrorResponse error_response = 2;
  }
}

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

Java code:

@Override
public void findSquare(Request request, StreamObserver<Response> responseObserver) {
    Response.Builder builder = Response.newBuilder();
		try {
			// Abnormal business
		}catch (Throwable t) {
				// If there is an exception, the wrong Message type will be returned
        ErrorResponse errorResponse = ErrorResponse.newBuilder()
								// Business exception
                .setInput(xxx)
								// Business error code
                .setErrorCode(errorCode)
                .build();
        builder.setErrorResponse(errorResponse);
				return;
		}
		
		// If successful, the correct Message type will be returned
    builder.setSuccessResponse(SuccessResponse.newBuilder()).build());
    responseObserver.onNext(builder.build());
    responseObserver.onCompleted();
}

This method can store multiple data (as long as more fields are defined in the success / failure Message), but it is troublesome to define two messages (success and failure) to complete the business.

2.3. Based on grpc metadata (actual scheme)

protobuf file:

This protobuf file suggests that the infrastructure department be fed back and integrated into the company's two-party package to avoid redefining each project.

syntax = "proto3";

package credit ;
option java_package = "com.maycur.grpc.credit";

	// General exception handling information
message ErrorInfo {

  // Wrong business code
  string errorCode  = 1;

  // Default prompt
  string defaultMsg = 2;
}

java code:

try{
	// Business code | streamobserver < XXX > responseobserver
} catch(Throwable t) {
	if (t instanceof ServiceException) {
            // Business exception, return the error code and default copy to the client
            ServiceException serviceException = (ServiceException) t;
            Metadata trailers = new Metadata();
            Error.ErrorInfo errorInfo = Error.ErrorInfo.newBuilder()
                    .setErrorCode(serviceException.getErrorCode())
                    .setDefaultMsg(serviceException.getMessage())
                    .build();
            Metadata.Key<Error.ErrorInfo> ERROR_INFO_TRAILER_KEY =
                    ProtoUtils.keyForProto(errorInfo);
            trailers.put(ERROR_INFO_TRAILER_KEY, errorInfo);
            responseObserver.onError(Status.UNKNOWN
                    .withCause(serviceException)
                    .asRuntimeException(trailers));
        } else {
            // For non business exceptions, return the exception details to the client.
            responseObserver.onError(Status.UNKNOWN
                    // Here is our custom exception information
                    .withDescription(t.getMessage())
                    .withCause(t)
                    .asRuntimeException());
        }
        // Throw exceptions to make the upper business aware (such as transaction rollback, which may be used)
        throw new RuntimeException(t);
}

In general, this is the picture:

2.4. Optimized scheme

The above scheme is not elegant enough, because all exceptions need to be manually try catch, which is very redundant. There are several options to optimize him.

2.4.1. Implement the ServerInterceptor interface provided by gRPC

class ExceptionInterceptor implements ServerInterceptor {
    public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
            ServerCall<ReqT, RespT> call, Metadata headers,
            ServerCallHandler<ReqT, RespT> next) {
        ServerCall.Listener<ReqT> reqTListener = next.startCall(call, headers);
        return new ExceptionListener(reqTListener, call);
    }
}

class ExceptionListener extends ServerCall.Listener {
    ......
    public void onHalfClose() {
        try {
            this.delegate.onHalfClose();
        } catch (Throwable t) {
            // Unified exception handling
            ExtendedStatusRuntimeException exception = fromThrowable(t);
            // Call Close() sends Status and metadata
            // This method is essentially the same as onError()
            call.close(exception.getStatus(), exception.getTrailers());
        }
    }
}

In fact, the core of this scheme is to manually execute the contents of the onError method without calling the onError() method. However, I personally think that doing so violates the programming contract. If the subsequent gRPC code changes, there are potential risks.

2.4.2. Wrap the StreamObserver class to enhance its functionality. (scheme adopted)

/**
 * gRPC The callback delegation (decoration) class is responsible for enhancing the original {@ link StreamObserver}, adding and capturing gRPC exceptions and performing corresponding processing
 * <p>
 * {@link GrpcService} This class should be combined and implemented through delegation
 * <p>
 * Thread-unSafe implementation
 *
 * @author masaiqi
 * @date 2021/4/12 18:11
 */
public class StreamObserverDelegate<ReqT extends Message, RespT extends Message> implements StreamObserver<RespT> {

    private static final Logger logger = LoggerFactory.getLogger(StreamObserverDelegate.class);

    private StreamObserver<RespT> originResponseObserver;

    public StreamObserverDelegate(StreamObserver<RespT> originResponseObserver) {
        Assert.notNull(originResponseObserver, "originResponseObserver must not null!");
        this.originResponseObserver = originResponseObserver;
    }

    @Override
    public void onNext(RespT value) {
        this.originResponseObserver.onNext(value);
    }

    @Override
    public void onError(Throwable t) {
        if (t instanceof ServiceException) {
            // Business exception, return the error code and default copy to the client
            ServiceException serviceException = (ServiceException) t;
            Metadata trailers = new Metadata();
            Error.ErrorInfo errorInfo = Error.ErrorInfo.newBuilder()
                    .setErrorCode(serviceException.getErrorCode())
                    .setDefaultMsg(serviceException.getMessage())
                    .build();
            Metadata.Key<Error.ErrorInfo> ERROR_INFO_TRAILER_KEY =
                    ProtoUtils.keyForProto(errorInfo);
            trailers.put(ERROR_INFO_TRAILER_KEY, errorInfo);
            this.originResponseObserver.onError(Status.UNKNOWN
                    .withCause(serviceException)
                    .asRuntimeException(trailers));
        } else {
            // For non business exceptions, return the exception details to the client.
            this.originResponseObserver.onError(Status.UNKNOWN
                    // Here is our custom exception information
                    .withDescription(t.getMessage())
                    .withCause(t)
                    .asRuntimeException());
        }
        // Throw exceptions to make the upper business aware (such as transaction rollback, which may be used)
        throw new RuntimeException(t);
    }

    @Override
    public void onCompleted() {
        if (originResponseObserver != null) {
            originResponseObserver.onCompleted();
        }
    }

    /**
     * Execute business (automatically handle exceptions)
     *
     * @author masaiqi
     * @date 2021/4/12 18:11
     */
    public RespT executeWithException(Function<ReqT, RespT> function, ReqT request) {
        RespT response = null;
        try {
            response = function.apply(request);
        } catch (Throwable e) {
            this.onError(e);
        }
        return response;
    }

    /**
     * Execute business (automatically handle exceptions)
     *
     * @author masaiqi
     * @date 2021/4/12 18:11
     */
    public RespT executeWithException(Supplier<RespT> supplier) {
        RespT response = null;
        try {
            response = supplier.get();
        } catch (Throwable e) {
            this.onError(e);
        }
        return response;
    }
}

With the above delegation class (wrapper class), you can easily implement the gRPC method. Take the call method of an Unary RPC as an example:

public class xxxGrpc extends xxxImplBase {

    @Override
    public void saveXXX(XXX request, StreamObserver<XXX> responseObserver) {
        StreamObserverDelegate streamObserverDelegate = new StreamObserverDelegate(responseObserver);
        streamObserverDelegate.executeWithException(() -> {
						// Business code
            return xxx;
        });
    }
}

The service provider obtains the exception information in the following ways:

3. Reference documents

Topics: Java grpc