Controle de versão de serviços gRPC

Por James Newton-King

Novos recursos adicionados a um aplicativo podem exigir que os serviços gRPC fornecidos aos clientes sejam alterados, às vezes de maneiras inesperadas e interruptivas. Quando os serviços gRPC são alterados:

  • Deve-se considerar como as alterações afetam os clientes.
  • Uma estratégia de controle de versão para dar suporte a alterações deve ser implementada.

Compatibilidade com versões anteriores

O protocolo gRPC foi projetado para dar suporte a serviços que mudam ao longo do tempo. Em geral, as adições aos serviços e métodos gRPC não são interruptivas. As alterações não interruptivas permitem que os clientes existentes continuem funcionando sem alterações. Alterar ou excluir serviços gRPC são alterações interruptivas. Quando os serviços gRPC têm alterações interruptivas, os clientes que usam esse serviço precisam ser atualizados e reimplantados.

Fazer alterações não interruptivas em um serviço tem uma série de benefícios:

  • Os clientes existentes continuam a ser executados.
  • Evita o trabalho envolvido em notificar clientes sobre alterações interruptivas e atualizá-las.
  • Apenas uma versão do serviço precisa ser documentada e mantida.

Alterações não relacionadas à falha

Essas alterações não são interruptivas em um nível de protocolo gRPC e no nível binário do .NET.

  • Adicionando um novo serviço
  • Adicionando um novo método a um serviço
  • Adicionar um campo a uma mensagem de solicitação – os campos adicionados a uma mensagem de solicitação são desserializados com o valor padrão no servidor quando não definido. Para ser uma alteração não interruptiva, o serviço deve ter êxito quando o novo campo não é definido por clientes mais antigos.
  • Adicionar um campo a uma mensagem de resposta – os campos adicionados a uma mensagem de resposta são desserializados na coleção de campos desconhecidos da mensagem no cliente.
  • Adicionar um valor a uma enumeração – Enumerações são serializadas como um valor numérico. Novos valores de enumeração são desserializados no cliente para o valor de enumeração sem um nome de enumeração. Para ser uma alteração não interruptiva, os clientes mais antigos devem ser executados corretamente ao receber o novo valor de enumeração.

Alterações interruptivas binárias

As alterações a seguir não são interruptivas em um nível de protocolo gRPC, mas o cliente precisa ser atualizado se atualizar para o contrato mais recente .proto ou assembly .NET do cliente. A compatibilidade binária é importante se você planeja publicar uma biblioteca gRPC no NuGet.

  • Remover um campo – os valores de um campo removido são desserializados para os campos desconhecidos de uma mensagem. Essa não é uma alteração interruptiva do protocolo gRPC, mas o cliente precisa ser atualizado se atualizar para o contrato mais recente. É importante que um número de campo removido não seja reutilizado acidentalmente no futuro. Para garantir que isso não aconteça, especifique os números e nomes de campo excluídos na mensagem usando a palavra-chave reservada do Protobuf.
  • Renomear uma mensagem – os nomes de mensagem normalmente não são enviados na rede, portanto, essa não é uma alteração interruptiva do protocolo gRPC. O cliente precisará ser atualizado se atualizar para o contrato mais recente. Uma situação em que os nomes de mensagem são enviados na rede é com Qualquer campo, quando o nome da mensagem é usado para identificar o tipo de mensagem.
  • Aninhar ou desaninhar de uma mensagem – os tipos de mensagem podem ser aninhados. Aninhar ou desaninhar uma mensagem altera o nome da mensagem. Alterar como um tipo de mensagem é aninhado tem o mesmo impacto na compatibilidade que a renomeação.
  • Alterar csharp_namespace – Alterar csharp_namespace alterará o namespace de tipos .NET gerados. Essa não é uma alteração interruptiva do protocolo gRPC, mas o cliente precisa ser atualizado se atualizar para o contrato mais recente.

Alterações interruptivas no Protocolo

Os seguintes itens são alterações de falha de protocolo e binárias:

  • Renomear um campo – com o conteúdo do Protobuf, os nomes de campo são usados apenas no código gerado. O número do campo é usado para identificar campos na rede. Renomear um campo não é uma alteração de falha de protocolo para Protobuf. No entanto, se um servidor estiver usando JSo conteúdo ON, renomear um campo será uma alteração interruptiva.
  • Alterar um tipo de dados de campo – alterar o tipo de dados de um campo para um tipo incompatível causará erros ao desserializar a mensagem. Mesmo que o novo tipo de dados seja compatível, é provável que o cliente precise ser atualizado para dar suporte ao novo tipo se atualizar para o contrato mais recente.
  • Alterar um número de campo – com cargas protobuf, o número do campo é usado para identificar campos na rede.
  • Renomear um pacote, serviço ou método – o gRPC usa o nome do pacote, o nome do serviço e o nome do método para compilar a URL. O cliente obtém um status UNIMPLEMENTED do servidor.
  • Remover um serviço ou método – o cliente obtém um status UNIMPLEMENTED do servidor ao chamar o método removido.

Alterações interruptivas de comportamento

Ao fazer alterações não interruptivas, você também deve considerar se os clientes mais antigos podem continuar trabalhando com o novo comportamento de serviço. Por exemplo, adicionar um novo campo a uma mensagem de solicitação:

  • Não é uma alteração interruptiva de protocolo.
  • Retornar um erro status no servidor se o novo campo não estiver definido o torna uma alteração interruptiva para clientes antigos.

A compatibilidade de comportamento é determinada pelo código específico do aplicativo.

Serviços de número de versão

Os serviços devem se esforçar para permanecer compatíveis com versões anteriores com clientes antigos. Eventualmente, as alterações em seu aplicativo podem exigir alterações interruptivas. Quebrar clientes antigos e forçá-los a serem atualizados junto com seu serviço não é uma boa experiência do usuário. Uma maneira de manter a compatibilidade com versões anteriores ao fazer alterações interruptivas é publicar várias versões de um serviço.

O gRPC dá suporte a um especificador de pacote opcional, que funciona muito parecido com um namespace do .NET. Na verdade, o package será usado como o namespace do .NET para tipos .NET gerados se option csharp_namespace não estiver definido no .proto arquivo. O pacote pode ser usado para especificar um número de versão para seu serviço e suas mensagens:

syntax = "proto3";

package greet.v1;

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply);
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

O nome do pacote é combinado com o nome do serviço para identificar um endereço de serviço. Um endereço de serviço permite que várias versões de um serviço sejam hospedadas lado a lado:

  • greet.v1.Greeter
  • greet.v2.Greeter

As implementações do serviço com versão são registradas em Startup.cs:

app.UseEndpoints(endpoints =>
{
    // Implements greet.v1.Greeter
    endpoints.MapGrpcService<GreeterServiceV1>();

    // Implements greet.v2.Greeter
    endpoints.MapGrpcService<GreeterServiceV2>();
});

Incluir um número de versão no nome do pacote oferece a oportunidade de publicar uma versão v2 do serviço com alterações interruptivas, enquanto continua a dar suporte a clientes mais antigos que chamam a versão v1 . Depois que os clientes tiverem sido atualizados para usar o serviço v2 , você poderá optar por remover a versão antiga. Ao planejar a publicação de várias versões de um serviço:

  • Evite alterações interruptivas se for razoável.
  • Não atualize o número de versão, a menos que faça alterações interruptivas.
  • Atualize o número de versão quando você fizer alterações interruptivas.

A publicação de várias versões de um serviço o duplica. Para reduzir a duplicação, considere mover a lógica de negócios das implementações de serviço para um local centralizado que possa ser reutilizado pelas implementações antigas e novas:

using Greet.V1;
using Grpc.Core;
using System.Threading.Tasks;

namespace Services
{
    public class GreeterServiceV1 : Greeter.GreeterBase
    {
        private readonly IGreeter _greeter;
        public GreeterServiceV1(IGreeter greeter)
        {
            _greeter = greeter;
        }

        public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
        {
            return Task.FromResult(new HelloReply
            {
                Message = _greeter.GetHelloMessage(request.Name)
            });
        }
    }
}

Serviços e mensagens gerados com nomes de pacote diferentes são tipos diferentes do .NET. Mover a lógica de negócios para um local centralizado requer o mapeamento de mensagens para tipos comuns.

Recursos adicionais