Uma chamada remota de procedimento (RPC) pode falhar devido a problemas na rede, no servidor ou no próprio cliente.
Mesmo quando a comunicação é possível entre cliente e servidor, este último pode detetar um problema nos argumentos recebidos ou ter um problema interno que precisa de reportar ao cliente.
O gRPC reporta todos os problemas através de um código de estado (status code) que é devolvido em situações de erro. Este mecanismo é básico e menos sofiscado, por exemplo, do que as exceções do Java. A opção de desenho deve-se ao facto do gRPC ser uma biblioteca agnóstica relativamente à linguagem de programação que se utiliza, pelo que o recurso a um conjunto limitado de códigos de erro é a forma base de tratar situações em que a invocação remota falha.
De acordo com a documentação oficial, existem três categorias de códigos de erro suportadas por todas as bibliotecas cliente/servidor gRPC e independentes do formato de dados:
Os código de base são suficientes em muitas situações, mas não permitem comunicar informações mais detalhadas acerca do erro em causa. Por este motivo, o gRPC tem também um conjunto mais alargado de códigos de erro, que já são definidos em protocol buffers.
A utilização de protocol buffers permite incluir detalhes que podem ser relevantes para o cliente conseguir recuperar do erro, como uma descrição textual do erro e/ou metadados. Nem todas as implementações de gRPC suportam este modelo, mas o Java, que usaremos, suporta. Felizmente, a união dos conjuntos de códigos de erro é apresentada ao programador Java através de uma classe apenas: io.grpc.Status.
Após a invocação de um procedimento remoto com gRPC, a chamada pode ter sucesso ou falhar, sendo enviado para o cliente um código de erro (ou error status code) neste último caso.
Tome como exemplo a implementação do método currentBoard do laboratório anterior:
public void currentBoard(CurrentBoardRequest request, StreamObserver<CurrentBoardResponse> responseObserver) { String board = ttt.currentBoard(); CurrentBoardResponse response = CurrentBoardResponse.newBuilder().setBoard(board).build(); responseObserver.onNext(response); responseObserver.onCompleted(); }
Em caso de erro, é importante notificar o cliente de que a execução remota falhou, para que consiga recuperar (por exemplo, através da repetição da invocação). Esse estado de erro pode ser induzido, por exemplo, por uma falha na conexão entre cliente e servidor (como descrito anteriormente) ou introduzido pelo próprio programador, para acautelar violações do domínio da aplicação. A validação de argumentos é um exemplo clássico.
No caso de Java, é possível explicitar que a chamada remota falhou invocando responseObserver.onError(...) quando necessário. Este método recebe um Throwable, sendo qualquer exceção em Java uma subclasse desta. Esta invocação altera o fluxo de execução do programa. Quer isto dizer que, num determinado fluxo, onCompleted e onError só podem ser invocadas uma vez e, se forem, devem ser as últimas (não podendo, por isso, ser executadas em conjunto). A título de exemplo, e considerando a guarda condition, deve ter-se algo como:
... if (condition) { ... responseObserver.onError(...); } else { ... responseObserver.onNext(...); responseObserver.onCompleted(); } ...
No entanto, há que ter em conta que as exceções passadas como argumento a onError são automaticamente encapsuladas dentro de StatusRuntimeException ou StatusException, perdendo informação relevante sobre a sua origem/causa (uma vez que esta informação pertence exclusivamente ao domínio do servidor e não deve ser enviada ao cliente). Assim sendo, as únicas exceções que o cliente poderá receber do seu lado são do tipo StatusRuntimeException (que herda de RuntimeException) ou StatusException (que herda de Exception).
No modelo que estamos a considerar, gRPC oferece uma estrutura que permite modelar o estado de uma invocação remota, Status (cuja implementação em Java pode ser consultada aqui). Em Java, esta classe define, para cada estado, um código e uma descrição do estado, modelando também os estados de erro de que temos vindo a falar. Um exemplo é o estado INVALID_ARGUMENT, utilizado para representar o estado de erro em que se incorre quando o cliente especifica um argumento inválido. A grande vantagem desta classe é que possui um método que encapsula automaticamente um objeto Status numa exceção, pronta a ser passada ao método onError.
Por exemplo, para que o cliente receba uma StatusRuntimeException (análogo para StatusException) ao especificar um argumento inválido, o servidor pode invocar:
... responseObserver.onError(Status.INVALID_ARGUMENT.asRuntimeException()); ...
Como discutido anteriormente, é ainda possível passar mais informação ao cliente acerca do erro em causa. Por exemplo, para passar uma descrição textual do erro, o servidor pode invocar:
... responseObserver.onError(Status.INVALID_ARGUMENT.withDescription("Invalid input!").asRuntimeException()); ...
Para captar o erro do lado do cliente, basta introduzir um bloco try-catch para uma exceção do tipo StatusRuntimeException, que oferece o método getStatus, que devolve a instância de Status previamente encapsulada. Supondo, então, que existe, do lado do cliente, um stub, imprimir o código é simples:
... try { CurrentBoardResponse response = stub.currentBoard(request); } catch (StatusRuntimeException e) { Status status = e.getStatus(); System.out.println(status.getDescription()); } ...
Atente-se que a descrição que se obtém ao invocar status.getDescription() é igual à descrição que foi passada no servidor em Status.INVALID_ARGUMENT.withDescription(desc).asRuntimeException().
© Docentes de Sistemas Distribuídos,
Dep. Eng. Informática,
Técnico Lisboa