7 Eylül 2021 Salı

gRPC Error Handling

Giriş
Açıklaması şöyle
By default, gRPC relies heavily on status code for error handling. 
Sunucu tarafından fırlatılan exception gRPC tarafından StatusRuntimeException'a çevrilir.

Örnek
Şöyle yaparız. Burada ServiceException kendi sınıfımız
import io.grpc.StatusRuntimeException;

//Client call
public Product getProduct(String productId) {
  Product product = null;
  try {
    var request = GetProductRequest.newBuilder().setProductId(productId).build();
    var productApiServiceBlockingStub = ProductServiceGrpc.newBlockingStub(managedChannel);
    var response = productApiServiceBlockingStub.getProduct(request);
    // Map to domain object
    product = ProductMapper.MAPPER.map(response);
  } catch (StatusRuntimeException error) {
    log.error("Error while calling product service, cause {}", error.getMessage());
    throw new ServiceException(error.getCause());
  }
  return product;
}
Ancak bir problem var. O da hata mesajının kaybolması. Çıktı olarak şunu alırız
io.grpc.StatusRuntimeException: UNKNOWN
Açıklaması şöyle
gRPC wraps our custom exception in StatusRuntimeException and swallows the error message and assigns a default status code UNKNOWN.
Bunu düzeltmek için sunucu tarafında şöyle yaparız. Bu sefer onError() metodunu çağırıyoruz.
//Server Product Service API
public void getProduct(
    GetProductRequest request, StreamObserver<GetProductResponse> responseObserver) {
  try {
    ...
    responseObserver.onNext(response);
    responseObserver.onCompleted();
  } catch (ResourceNotFoundException error) {
    var status = Status.NOT_FOUND.withDescription(error.getMessage()).withCause(error);
    responseObserver.onError(status.asException());
  }
}
İstemci tarafında çıktı olarak şunu alırız
Error while calling product service, cause NOT_FOUND: Product ID not found
Hata mesajı düzgün ancak hala exception içindeki getCause() null döner. Sebebinin açıklaması şöyle. Yani io.grpc.Status.withCause() çağrısı orijinal exception'ı göndermiyor.
Create a derived instance of Status with the given cause. However, the cause is not transmitted from server to client.
Şöyle yaparız. Bu sefer io.grpc.Metadata kullanılıyor
public void getProduct(
    GetProductRequest request, StreamObserver<GetProductResponse> responseObserver) {
  try {
    ...
    responseObserver.onNext(response);
    responseObserver.onCompleted();
  } catch (ResourceNotFoundException error) {
    Map<String, String> errorMetaData = error.getErrorMetaData();
    var metadata = new Metadata();    
    errorMetaData.entrySet().stream() 
        .forEach(
            entry ->
                metadata.put(
                    Metadata.Key.of(entry.getKey(), Metadata.ASCII_STRING_MARSHALLER),
                    entry.getValue()));
    var statusRuntimeException =
        Status.NOT_FOUND.withDescription(error.getMessage()).asRuntimeException(metadata); 
    responseObserver.onError(statusRuntimeException);
  }
}
İstemci tarafında şöyle yaparız
} catch (StatusRuntimeException error) {

  Metadata trailers = error.getTrailers();
  Set<String> keys = trailers.keys();

  for (String key : keys) {
    Metadata.Key<String> k = Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER);
    log.info("Received key {}, with value {}", k, trailers.get(k));
  }
}
İstemci tarafında çıktı olarak şunu alırız
Received key Key{name='resource_id'}, with value 32c29935-da42-4801-825a-ac410584c281 
Received key Key{name='content-type'}, with value application/grpc 
Received key Key{name='message'}, with value Product ID not found
- Bütün bunlarlar uğraşmak yerine Google Richer Error Model kullanılabilir.
- Ayrıca sunucu tarafında bir sürü exceptıon yakalayıp io.grpc.Status dönmek yerine io.grpc.ServerInterceptor arayüzünden kalıtıp ortak bir kod yazılabilir.
- Eğer Spring kullanıyorsak aynı şeyi şöyle yaparız
@GrpcAdvice
public class ExceptionHandler {

  @GrpcExceptionHandler(ResourceNotFoundException.class)
  public StatusRuntimeException handleResourceNotFoundException(ResourceNotFoundException
cause) {
    var errorMetaData = cause.getErrorMetaData();
    var errorInfo =
        ErrorInfo.newBuilder()
            .setReason("Resource not found")
            .setDomain("Product")
            .putAllMetadata(errorMetaData)
            .build();
    var status =
        com.google.rpc.Status.newBuilder()
            .setCode(Code.NOT_FOUND.getNumber())
            .setMessage("Resource not found")
            .addDetails(Any.pack(errorInfo))
            .build();
    return StatusProto.toStatusRuntimeException(status);
  }
}


Hiç yorum yok:

Yorum Gönder