Exemplos de aplicação da Hyrum's Law em Golang
(abenezer.org)- Na base de código Go, em
net/http, há um comentário dizendo que a string de erro"http: request body too large", retornada porMaxBytesError.Error(), não pode ser alterada por causa da Hyrum's Law - Hyrum's Law é o princípio de que, quando uma API tem usuários suficientes, alguém passa a depender até de comportamentos observáveis que não fazem parte do contrato oficial
- Mesmo strings que parecem triviais, como mensagens de erro, podem quebrar código existente no momento em que forem alteradas, se código externo se comportar com base no texto exato
- Dentro do Go, também há comentários semelhantes em
crypto/rsaeinternal/weak, tratando do risco de comportamentos de fluxo aleatório ou semânticas ainda não definidas ficarem congelados - Como o problema não se limita ao Go, APIs públicas e bibliotecas devem ser projetadas para evitar que comportamentos não intencionais acabem se cristalizando como um padrão de fato
Hyrum's Law identificada no código Go
MaxBytesError.Error(), emnet/http/request.go, retorna a seguinte string"http: request body too large"- O comentário correspondente diz: “Due to Hyrum's law, this text cannot be changed.”
- Hyrum's Law é um princípio batizado em referência a Hyrum Wright, e a definição em hyrumslaw.com é a seguinte
- Com um número suficiente de usuários de uma API, alguém passará a depender de todos os comportamentos observáveis do sistema, independentemente do que foi prometido no contrato
- O ponto central do caso de
MaxBytesErroré que o texto exato de uma mensagem de erro pode ser usado por código externo- Mesmo uma pequena alteração no texto pode quebrar código existente
- Os resultados da busca por
http: request body too largemostram código open source em Go que usa essa string
Casos em outros pacotes Go e bases de código externas
-
Dependência de fluxo aleatório em
crypto/rsaEncryptOAEP, emcrypto/rsa/rsa.go, tem um comentário relacionado à Hyrum's Law- Essa função não promete execução determinística em relação ao fluxo aleatório, mas, por não aplicar
MaybeReadByte, existe a possibilidade de alguém depender do comportamento atual SignPSS, emcrypto/rsa/pss.go, também inclui um comentário no mesmo contexto- Em ambos os casos, um número bem definido de bytes aleatórios é incluído de uma maneira bem definida no texto cifrado ou na assinatura, então isso é tratado como uma promessa tolerável
-
Risco de congelamento da semântica em
internal/weakinternal/weakafirma que a toolchain proíbe explicitamente o acesso a esse pacote e às funções de referência por meio dego:linkname- A semântica desse pacote não passou pelo processo de proposta, e expor a funcionalidade poderia congelar a semântica existente por causa da Hyrum's Law
-
Um padrão que se repete também fora do Go
- Menções à Hyrum's Law não se limitam ao Go
- Nos resultados de busca multilíngue do grep.app, é possível ver casos em várias linguagens
urllib.parse, do Python, earray.h, do Pixar OpenUSD, também são exemplos de bases de código relacionadas- A evolução do JavaScript também se conecta a casos em que dependências amplas de vários comportamentos estranhos e não intencionais acabaram se tornando padrões de fato
O que verificar antes de fazer uma alteração
- Ao alterar código, é preciso considerar não só APIs documentadas, mas também comportamentos observáveis dos quais código externo possa depender
- É necessário projetar sistemas desde o início de modo a reduzir a possibilidade de dependência de comportamentos não intencionais
1 comentários
Opiniões no Hacker News
A Lei de Hyrum é uma observação útil, mas não se deve ficar obcecado por ela e chegar a conclusões equivocadas
O tempo total de execução de uma função também é uma propriedade observável; portanto, até otimizar uma função para torná-la mais rápida poderia ser visto como uma alteração incompatível. Afinal, a fila poderia de repente esvaziar rápido demais e causar um deadlock. Mesmo assim, 99,99999999% dos usuários provavelmente ficariam felizes em ver o código ficar mais rápido sem nenhum esforço
No fim, o que é uma alteração incompatível só pode ser um contrato social, não um contrato técnico. Caso contrário, literalmente nada poderia ser mudado. Autores de bibliotecas devem documentar as partes da API que não vão mudar, agir de forma razoável e ter empatia pelos usuários; usuários de bibliotecas devem entender que depender de interfaces não documentadas como dependências essenciais é por sua conta e risco, e também ter empatia pelos autores
Mas, por outro ponto de vista, a Lei de Hyrum não é nem um contrato técnico nem um contrato social, e sim uma propriedade técnica emergente que aparece em sistemas usados em escala suficiente
Como responder a essa propriedade depende do contexto social. Se você é mantenedor de FOSS, publica a otimização quando 99,99% ficam mais rápidos e só 0,01% precisam corrigir o código ou migrar para uma nova API. Se você está em uma grande empresa de tecnologia, precisa otimizar e, ao mesmo tempo, nada dentro da empresa pode quebrar, nem 0%; então colabora com várias equipes para encontrar um meio-termo. Se você é uma empresa de software corporativo, não publica se até 0,1% quebrar, caso esse usuário esteja entre os 5 maiores contratos
O autor original chamava várias funções assíncronas e presumia que, quando a rotina antiga e lenta terminasse, todas elas já teriam terminado. Demorou muito tempo para descobrir exatamente o que estava acontecendo
Por isso os PCs tinham um botão turbo que reduzia a velocidade, e computadores de 8 bits não aumentaram a velocidade durante uma década inteira, apesar de terem CPUs mais rápidas disponíveis. Hoje, quase tudo roda em duas ou mais CPUs; por isso, salvo a exigência de ser rápido o bastante, quase ninguém depende do tempo de execução de uma função. Mesmo em embarcados, depois de passar pela descontinuação de CPUs específicas, as pessoas tentam evitar esse tipo de dependência
Seria uma história sobre por que transformamos o HTTP Status 418 em uma dependência essencial em uma API interna e por que, dadas as restrições, essa era a escolha menos ruim
O ambiente de execução, a carga do sistema naquele momento, a execução do GC etc. podem todos influenciar
Em resumo, não vejo comportamento emergente gerado pela máquina como uma interface intencional ou qualquer tipo de contrato. Portanto, mesmo que alguém tenha dependido de um comportamento não intencional, assim como corrigir um bug sutil não é considerado uma alteração incompatível, eu também não consideraria isso uma alteração incompatível
Neste caso, acima de tudo, parece mais uma prova do fortíssimo compromisso do Go com compatibilidade retroativa
Haha, fui eu que escrevi o comentário em
crypto/rsa. Em Go, levamos a Lei de Hyrum e a compatibilidade retroativa https://go.dev/doc/go1compat muito a sérioPor exemplo, várias funções
GenerateKeyleem um byte extra do fluxo aleatório comMaybeReadBytehttps://pkg.go.dev/crypto/internal/randutil#MaybeReadByte para que o algoritmo não fique fixo. Ainda ontem chegou um relato de que uma chave privada ECDSA com chave pública nil funcionava antes e agora não funciona, então provavelmente teremos que fazê-la voltar a funcionar https://go.dev/issue/70468A iteração de mapas usa uma ordem aleatória para que a implementação interna não fique exposta. A saída de
rand.Randé considerada parte da promessa de compatibilidade, então foi preciso um esforço bem grande para melhorá-la https://go.dev/blog/randv2 https://go.dev/blog/chacha8randEstamos sempre discutindo quais promessas colocar na documentação e quais comportamentos declarar como “podem mudar”. Isso porque sabemos que o que está documentado nunca pode ser alterado, e mesmo o que não foi explicitamente marcado como “pode mudar” provavelmente será difícil de mudar https://go-review.googlesource.com/c/go/+/598336/comment/5d6...
Ainda assim, acho que foi uma troca valiosa. Uso bastante Go e gosto da forte compatibilidade retroativa, mas aceitaria de bom grado uma taxa um pouco maior de mudanças incompatíveis se isso desse aos desenvolvedores de Go mais liberdade para melhorar desempenho e adicionar recursos
Vendo o inferno que usuários de outros ecossistemas enfrentam, como Python, por exemplo, acho que não sou o único a pensar assim
MaybeReadByteé usado em várias funçõesGenerateKey, mas parece que isso não acontece em ed25519Antes de existir
ed25519.NewKeyFromSeed(), essa era a única forma de derivar uma chave pública Ed25519 a partir de uma chave privada, e tenho quase certeza de que escrevi código que dependia disso. Eu não gostava muito, mas era a única coisa possível, então é fácil de lembrarDito isso, é bom que a documentação de
ed25519.GenerateKeydeclare que a saída é determinística. Acho que a API de criptografia de Go tem feito um trabalho realmente bom em investigar e manter comportamentos que se cristalizaram, além de evitar novas cristalizaçõesComo a infame linha A20 (https://en.wikipedia.org/wiki/A20_line), acaba sendo necessário carregar esse comportamento quebrado para sempre
A solução para o problema mencionado especificamente é não usar erros baseados em strings, e sim erros sentinela https://thomas-guettler.de/go/wrapping-and-sentinel-errors
De forma mais geral, não se deve criar código que faça o consumidor da API sequer ter vontade de depender de strings não técnicas. Se forem usados elementos de primeira classe da linguagem, como valores de erro predefinidos, tipos ou constantes contendo strings não técnicas, o consumidor da API pode comparar o valor retornado com a constante em vez de hardcodar a string diretamente
A Lei de Hyrum claramente existe, mas é possível reduzir seu impacto
A Grafana, que parece ser a causa principal na busca vinculada, deveria ter usado
errors.As(&http.MaxBytesError{})em vez de comparação de stringO ponto central da Lei de Hyrum é que não importa o quão bem você projete a API. As pessoas passam a depender do comportamento, não do contrato
Ainda é possível escrever código que verifica
err.String() == "no more tea available.". Concordo que não se deve fazer isso, mas não há nada que impeçaAlém disso,
errors.Isfoi adicionado ao Go relativamente recentemente, então, na época em que as pessoas verificavam erros desse jeito, checar a string literal era mais fácil. Em Go, o provedor da API não consegue impedir o consumidor de verificar o valor retornado por.String()Especialmente na biblioteca padrão, quase não há desculpa
É isso que acontece quando se ignora deliberadamente a história das linguagens de programação e se adota uma abordagem de “vamos projetar enquanto construímos”
As formas de enfrentar a Lei de Hyrum também são um tema interessante
Uma possibilidade é inserir aleatoriedade nas partes das quais se espera que as pessoas não dependam
Se minha memória não falha, o protocolo QUIC faz isso. Na versão atual há um campo que não é usado, mas, para impedir que roteadores comecem a identificar pacotes por esse campo, a especificação exige que ele seja definido com um valor aleatório, não com um byte nulo
A fonte provavelmente é esta: https://www.rfc-editor.org/rfc/rfc9000#section-17.2.1
“O valor do campo Unused é definido pelo servidor como um valor arbitrário. O cliente deve ignorar esse valor de campo. [...] Observe que outras versões do QUIC podem não fazer recomendação semelhante”
Pelo que sei, isso é chamado de greasing e serve para evitar ossification
https://www.rfc-editor.org/rfc/rfc8701.html
O rascunho mais antigo dessa RFC remonta a meados de 2016, e é bem possível que tenha sido a primeira aparição pública do termo: https://datatracker.ietf.org/doc/html/draft-davidben-tls-gre...
Nada seria tão horrível quanto acordar daqui a 10 anos, descobrir que esses bits realmente passaram a ser necessários, e então ver que 20 modelos de roteadores de 10 marcas decidiram que esses bits precisam estar de uma forma específica
Pontos extras se houver checksum ou criptografia do outro lado, fazendo tudo quebrar caso os bits sejam alterados. Os “hacks espertos” das middleboxes são uma verdadeira dor de cabeça
Este é um bom exemplo de software stringly typed
Os projetistas de Go não queriam exceções, mas com
panic/recoverainda há algo parecido, e erros sem tipo são prejudiciais. Por outro lado, como tratar erros tipados sem pattern matching? Afinal, ocatchda maioria das linguagens é uma forma rudimentar de pattern matchinghttps://learn.microsoft.com/en-us/dotnet/csharp/language-ref...
Em um emprego antigo, encontrei um erro de digitação em uma mensagem de erro e o corrigi, só para descobrir que a rede de dependências daquele texto com erro era tão profunda que, na prática, não dava para consertar; no fim, tivemos de voltar ao texto com erro
Isso ainda me incomoda
https://en.wikipedia.org/wiki/HTTP_referer
Isto é uma espécie de Lei de Hyrum, mas na prática é apenas Go sendo Go
Se o erro fosse um tipo enum, o consumidor poderia mudá-lo apenas com substituição de strings. Em vez disso, como strings estão sendo usadas como se fossem tipos, torna-se impossível saber do que o consumidor pode estar dependendo. Ele pode estar verificando apenas 6 caracteres no meio da string de erro, e uma alteração poderia quebrar tudo
É mais uma decisão de design terrível e anacrônica, apesar de alternativas melhores já existirem em outras linguagens há décadas. Quando erros iniciais se combinam com impossibilidade de mudança, você fica preso a eles para sempre
É interessante que esta lei seja exatamente o oposto do princípio da robustez, ou Lei de Postel
“Seja conservador ao enviar, seja liberal ao receber”
Se você recebe entradas de forma liberal, precisa entender de que maneiras foi liberal e, no mínimo, documentá-las internamente. Por causa da Lei de Hyrum, mesmo depois de grandes mudanças na base de código, você acaba tendo de dar suporte a todas essas formas para sempre
É justamente por isso que tento não criar APIs “liberais no que recebem”
Se você define critérios frouxos para os dados recebidos pela API, no fim precisa decidir como vai massagear esses dados para alguma forma canônica. E essa decisão quase sempre parece resultar, de algum modo, em um comportamento surpreendente para o usuário
Parece que cada autor de pacote aceita esse problema em graus diferentes. Alguns dias atrás, vi este comentário no pacote
jsonisValidNumberinforma sesé um literal numérico JSON válidoisValidNumberdeveria ser um detalhe interno de implementação, mas pacotes amplamente usados o acessam vialinknameEntre os principais integrantes do hall of shame está
github.com/bytedance/sonicCoisas que aprendi ao publicar APIs
Clientes fazem tudo o que for necessário para concluir seu trabalho, mesmo que não seja da forma pretendida pelo publicador. Clientes não leem a documentação. Quando um número suficiente de clientes passa a depender de um comportamento, até um bug vira parte da API. A quantidade de chamadas de API não corresponde necessariamente à importância
Por isso, ao desenvolver APIs, tento lançar APIs beta o mais cedo possível e observar como elas são usadas, para reduzir surpresas. Na maioria dos casos, faço incremento da versão principal mantendo suporte à versão anterior. Para isso, é preciso definir o SLA da API