Compatibilidade

Nesta página, você encontra explicações mais detalhadas sobre a lista de alterações compatíveis e incompatíveis com versões anteriores apresentadas na seção Controle de versão.

Nem sempre fica claro o que conta como uma alteração incompatível com versões anteriores. A orientação deve ser tratada como indicativa, e não uma lista abrangente com todas as alterações possíveis.

As regras listadas neste documento dizem respeito apenas à compatibilidade do cliente. É esperado que os produtores de API tenham conhecimento dos seus próprios requisitos em relação à implantação, incluindo alterações nos detalhes de implementação.

O objetivo geral é que os clientes não fiquem sem compatibilidade por conta de uma atualização de serviço para um novo patch ou versão anterior. Veja a seguir os tipos de compatibilidade em consideração:

  • Compatibilidade de origem: código escrito em 1.0 com falha na compilação em 1.1.
  • Compatibilidade binária: código compilado em 1.0 com falha no link/execução em uma biblioteca de cliente 1.1 (os detalhes exatos dependem da plataforma do cliente, há variantes disso em diferentes situações).
  • Compatibilidade eletrônica: um aplicativo criado em 1.0 com falha na comunicação com um servidor 1.1.
  • Compatibilidade semântica: tudo é executado, mas produz resultados não planejados ou surpreendentes.

Em outras palavras, os clientes antigos poderão trabalhar nos servidores mais recentes dentro do mesmo número da versão principal e, quando quiserem atualizar para uma nova versão secundária (por exemplo, para se beneficiar de um recurso novo), poderão fazer isso com facilidade.

Observação: quando nos referimos a números de versão como v1.1 e v1.0, estamos nos referindo a números de versão lógica que nunca são concretizadas. Eles servem apenas para facilitar a descrição das alterações.

Além das considerações teóricas e baseadas em protocolos, há considerações práticas devido à existência de bibliotecas de cliente que envolvem código gerado e código gravado manualmente. Sempre que possível, teste as alterações que você está considerando por meio da geração de novas versões de bibliotecas de cliente e verificando se elas ainda passam nos testes.

A discussão abaixo divide mensagens do proto em três categorias:

  • Mensagens de solicitação (como GetBookRequest)
  • Mensagens de resposta (como ListBooksResponse)
  • Mensagens de recursos (como Book e inclusive mensagens usadas em outras mensagens de recursos)

Essas categorias têm regras diferentes, porque as mensagens de solicitação são enviadas somente do cliente ao servidor, as mensagens de resposta são enviadas apenas do servidor ao cliente, mas, normalmente, as mensagens de recursos são enviadas de ambas as maneiras. Em especial, os recursos que podem ser atualizados precisam ser considerados em termos de ciclo de leitura/modificação/gravação.

Alterações compatíveis com versões anteriores

Como adicionar uma interface de API a uma definição de serviço de API

Do ponto de vista do protocolo, essa operação é sempre segura. A única ressalva é que as bibliotecas de cliente já podem ter usado seu novo nome de interface da API dentro do código gravado manualmente. Se a interface nova for inteiramente ortogonal em relação às existentes, isso será improvável. Se for uma versão simplificada de uma interface existente, será mais provável que cause um conflito.

Como adicionar um método a uma interface de API

A menos que você adicione um método que esteja em conflito com outro que já está sendo gerado em bibliotecas de cliente, tudo ficará bem.

Exemplo em que pode haver quebra: se você tiver um método GetFoo, o gerador de código C# já criará métodos GetFoo e GetFooAsync. Portanto, adicionar um método GetFooAsync na interface da API seria uma alteração importante da perspectiva da biblioteca de cliente.

Como adicionar uma vinculação HTTP a um método

Supondo que a vinculação não apresente ambiguidades, fazer com que o servidor responda a um URL que anteriormente teria rejeitado é algo seguro. Isso pode ser executado ao fazer com que uma operação existente aplique a um padrão novo de nome de recurso.

Como adicionar um campo a uma mensagem de solicitação

Os campos de solicitação de adição podem ser compatíveis com versões anteriores, desde que os clientes que não especifiquem o campo sejam tratados da mesma forma que na versão antiga.

O exemplo mais óbvio de onde isso pode ser feito incorretamente é com paginação: se a v1.0 da API não incluir paginação para uma coleção, ela não poderá ser adicionada na v1.1, a menos que o page_size padrão seja tratado como infinito (o que geralmente é uma má ideia). Caso contrário, os clientes da v1.0 que esperam conseguir resultados completos de uma única solicitação podem receber resultados truncados, sem ter consciência de que o conjunto contém mais recursos.

Como adicionar um campo a uma mensagem de resposta

Uma mensagem de resposta que não é um recurso (por exemplo, um ListBooksResponse) pode ser expandida sem interromper clientes, desde que isso não altere o comportamento de outros campos de resposta. Qualquer campo preenchido anteriormente em uma resposta continuará a ser preenchido com a mesma semântica, mesmo que isso apresente redundância.

Por exemplo, uma resposta de consulta da 1.0 pode ter um campo booleano de contained_duplicates para indicar que alguns resultados foram omitidos devido à duplicação. Na 1.1, podemos fornecer informações mais detalhadas em um campo duplicate_count. Mesmo que ele seja redundante de uma perspectiva da 1.1, o campo contained_duplicates precisa ser preenchido.

Como adicionar um valor a um enum

Um enum que é usado apenas em uma mensagem de solicitação pode ser expandido livremente para incluir novos elementos. Por exemplo, ao usar o padrão de Visualização de recursos, uma visualização nova pode ser adicionada em uma versão secundária nova. Os clientes não precisam receber esse enum, então eles não têm que estar cientes de valores que não são relevantes.

Para mensagens de recursos e mensagens de resposta, a suposição padrão é que os clientes lidarão com valores de enum que não conhecem. No entanto, os produtores de API estarão cientes de que pode ser difícil gravar aplicativos para que lidem corretamente com novos elementos de enum. Os proprietários da API devem documentar o comportamento esperado do cliente ao encontrar um valor de enum desconhecido.

O Proto3 permite que os clientes recebam um valor que desconhecem e serializem novamente a mensagem mantendo o mesmo valor, portanto, isso não interrompe o ciclo de leitura/modificação/gravação. O formato JSON permite que um valor numérico seja enviado, em que o "nome" do valor é desconhecido, mas o servidor normalmente não reconhecerá se o cliente realmente sabe ou não sobre um valor específico. Os clientes JSON podem, portanto, estar cientes de que receberam um valor que era anteriormente desconhecido para eles, mas só verão o nome ou o número e não conhecerão os dois. Retornar o mesmo valor de volta ao servidor em um ciclo de leitura/modificação/gravação não alterará esse campo, porque o servidor reconhecerá ambos os formulários.

Como adicionar um campo de recurso somente para saída

Os campos de uma entidade de recurso que são fornecidos apenas pelo servidor podem ser adicionados. O servidor pode considerar que qualquer valor fornecido pelo cliente em uma solicitação é válido, mas não precisa apresentar falha se o valor for omitido.

Alterações incompatíveis com versões anteriores

Como remover ou renomear um valor de enum, método, campo ou serviço

Se o código do cliente se referir a algo, removê-lo ou renomeá-lo é uma alteração incompatível com versões anteriores e precisa resultar em um aumento na versão principal. O código que se refere ao nome antigo causará falhas na hora da compilação para algumas linguagens (como C# e Java) e poderá causar falhas no momento da execução ou perda de dados em outras linguagens. A compatibilidade do formato de transmissão é irrelevante.

Como alterar uma vinculação HTTP

"Alterar" é efetivamente "apagar e adicionar". Por exemplo, se você decidir que realmente quer ser compatível com PATCH, mas sua versão publicada é compatível com PUT, ou você usou o nome do verbo personalizado incorreto, você poderá adicionar a nova vinculação, mas não precisará remover a antiga pelas mesmas razões de a remoção de um método de serviço ser uma alteração incompatível com versões anteriores.

Como alterar o tipo de campo

Mesmo quando o tipo novo é compatível com o formato de transmissão, isso pode alterar o código gerado para bibliotecas de cliente e, portanto, precisa resultar em um aumento na versão principal. Para linguagens compiladas e estaticamente tipadas, isso pode introduzir erros de tempo de compilação.

Como alterar o formato de um nome de recurso

O nome de um recurso não precisa ser alterado, o que significa que os nomes dos conjuntos não podem ser alterados.

Ao contrário da maioria das alterações incompatíveis com versões anteriores, isso também afeta as versões principais. Se um cliente que espera usar a v2.0 acessa um recurso que foi criado em v1.0 ou vice-versa, o mesmo nome de recurso será usado em ambas as versões.

De maneira mais sutil, o conjunto de nomes de recursos válidos também não deve mudar, pelas seguintes razões:

  • Se ele for mais restritivo, uma solicitação que anteriormente teria funcionado apresentará uma falha.
  • Se ele for menos restritivo do que o anteriormente documentado, os clientes que fazem suposições com base na documentação anterior podem ficar com incompatibilidade. É provável que os clientes armazenem nomes de recursos em outros lugares, de maneira que podem ser afetados pelo conjunto de caracteres permitidos e pelo comprimento do nome. Como alternativa, os clientes podem realizar sua própria validação de nomes de recursos para seguir a documentação (por exemplo, a Amazon avisou muitas vezes os clientes e teve um período de migração quando começou a permitir códigos de recursos do EC2 mais longos).

Observe que essa alteração só pode ser visível na documentação de um proto. Portanto, ao revisar um CL para procurar por incompatibilidades com versões anteriores, não é suficiente analisar as alterações que não têm comentários.

Como alterar o comportamento visível das solicitações existentes

Os clientes geralmente dependem do comportamento e da semântica da API, mesmo quando esse comportamento não é explicitamente compatível ou documentado. Portanto, na maioria dos casos, alterar o comportamento ou semântica de dados da API será visto como uma incompatibilidade pelos consumidores. Se o comportamento não estiver oculto por criptografia, você precisa supor que os usuários descobriram e dependerão disso.

Também é uma boa ideia criptografar tokens de paginação por esse motivo (mesmo quando os dados não são interessantes), para evitar que os usuários criem seus próprios tokens e possam ficar sem compatibilidade quando o comportamento do token for alterado.

Como alterar o formato do URL na definição de HTTP

Há dois tipos de alterações a serem consideradas aqui, além das mudanças de nome de recurso listadas acima:

  • Nomes de métodos personalizados: embora não faça parte do nome do recurso, um nome de método personalizado faz parte do URL que está sendo postado pelos clientes da REST. A alteração de um nome de método personalizado não deixará os clientes do gRPC incompatíveis com versões anteriores, mas as APIs públicas precisam supor que têm clientes da REST.
  • Nomes de parâmetros de recursos: uma alteração de v1/shelves/{shelf}/books/{book} para v1/shelves/{shelf_id}/books/{book_id} não afeta o nome do recurso substituído, mas pode afetar a geração de código.

Como adicionar um campo de leitura/gravação a uma mensagem de recurso

Os clientes geralmente executam operações de leitura/modificação/gravação. A maioria dos clientes não fornecerá valores para os campos que não conhecem, e o proto3, em particular, não é compatível com isso. Você pode especificar que qualquer campo em que tipos de mensagem estão ausentes (em vez de tipos primitivos) significa que uma atualização não é aplicada a ele, mas isso dificulta a remoção explícita desse valor de campo de uma entidade. Tipos primitivos (incluindo string e bytes) simplesmente não podem ser manipulados dessa maneira, pois não há diferença em proto3 entre especificar explicitamente um campo int32 como 0 e não especificá-lo.

Se todas as atualizações forem executadas usando uma máscara de campo, isso não será um problema, porque o cliente não substituirá os campos que não conhece de maneira implícita. No entanto, essa seria uma decisão incomum para a API. A maioria das APIs permite atualizações de "recursos inteiros".