Quando o padrão HTTP não é suficiente. Confirmação de micronauta

Olá pessoal, meu nome é Dmitry, e hoje vou falar sobre como a produção precisa me tornar um colaborador da estrutura do Micronaut. Certamente muitos já ouviram falar dele. Em resumo, essa é uma alternativa leve ao Spring Boot, onde a ênfase principal não está na reflexão, mas na compilação preliminar de todas as dependências necessárias. Um conhecido mais detalhado pode começar com a documentação oficial.

A estrutura do Micronaut é usada em vários projetos internos do Yandex e se estabeleceu bastante bem. Então o que estávamos perdendo? Posso dizer imediatamente: pronto para uso, o framework suporta, em princípio, todos os recursos que um programador pode teoricamente precisar para desenvolver backends. No entanto, existem casos raros que não são suportados imediatamente. Uma delas é quando você precisa trabalhar não com HTTP, mas com a extensão HTTP. Por exemplo, com métodos adicionais. De fato, esses casos são muito mais do que parece. Além disso, alguns desses protocolos são padrões:

  • Webdav é uma extensão para acessar recursos. Além dos métodos padrão, o HTTP requer suporte para métodos adicionais, como LOCK, PROPPATCH, etc.
  • Caldav é uma extensão Webdav para trabalhar com eventos do tipo calendário. Este protocolo com um alto grau de probabilidade está nos aplicativos do seu smartphone: para sincronizar agendas, compromissos, etc.

E a lista não está limitada a isso. Se você observar o registro dos métodos HTTP , verá que os métodos HTTP descritos apenas pelos padrões RFC atualmente são 39. E quantos outros casos há um protocolo auto-escrito sobre HTTP. Portanto, o suporte a métodos HTTP não padrão é bastante comum. Também acontece frequentemente que a estrutura que você usa não suporta esses métodos. Aqui está uma discussão sobre Stack Overflow para ExpressJS . E aqui está a solicitação de recebimento no github do Tornado . Bem, como o Micronaut é frequentemente posicionado como uma alternativa leve ao Spring - esse é o mesmo problema para o Spring .

Não é de surpreender que, quando em um dos projetos, precisávamos de suporte para um protocolo que estenda o HTTP em termos de métodos, enfrentamos o mesmo problema para o Micronaut que estamos usando para esse projeto há muito tempo. Descobriu-se que fazer com que o Micronaut processasse métodos fora do padrão é bastante difícil.

Porque Como se você observar a definição de métodos HTTP no Micronaut no momento, descobrirá que eles são definidos usando Enum, e não uma classe, como é feito, por exemplo, no Netty (eu não mencionei acidentalmente o Netty, depois ele será exibido). mais de uma vez). Para piorar a situação, toda a correspondência de chamadas do servidor é feita filtrando por enumeração, não pelo nome da string do método. Isso significa que, se você precisar de um método HTTP não padrão, precisará escrevê-lo no Enum e, na verdade, essa não é uma solução tão boa para o problema. Primeiro, será necessário um commit no repositório toda vez que você precisar de um novo método. Em segundo lugar, os métodos HTTP não são padronizados por padrão e sua lista não é corrigida em nenhum lugar; portanto, não é realista prever todas as situações possíveis. É necessário forçar o Micronaut a processar de alguma forma métodos que não foram fornecidos anteriormente pelos desenvolvedores.

Solução 1: testa


imagem

A primeira e mais óbvia solução foi não tocar no Micronaut e não reescrever nada nele. Porque, porque você pode colocar o nronx na frente do Micronaut, como fizemos, partindo de um exemplo :

http { upstream other_PROPPATCH { server ...; } upstream other_REPORT { server ...; } server { location /service { proxy_method POST; proxy_pass http://other_$request_method; } } } 

Qual é o objetivo? Podemos forçar o nginx para métodos não padrão a acessar o proxy de que precisamos, enquanto usamos a capacidade do nginx para alterar o método: ou seja, acessaremos através do método POST e o Micronaut pode processá-lo.

O que é ruim? Para começar, na verdade, fazemos todas as solicitações do ponto de vista do Micronaut sem idempotência. Não esqueça que, para métodos não padronizados, também existe essa separação. Por exemplo, REPORT é idempotente, enquanto PROPPATCH não. Como resultado, a estrutura não sabe sobre o tipo de solicitação, e o programador que está visualizando o código desses manipuladores também não poderá determinar isso. No entanto, este nem é o caso. Já temos um conjunto de testes que verificam automaticamente o aplicativo quanto à conformidade com o protocolo desejado. Para que esses testes funcionem com essa solução em um projeto, você precisa escolher uma das duas opções:

  • Aumente a imagem nginx com as configurações necessárias, além do próprio aplicativo, para que os testes acessem o nginx e não o próprio Micronaut. Embora a infraestrutura Yandex certamente permita que você crie componentes adicionais, nesse caso, parece que a engenharia excessiva é apenas para testes.
  • Reescreva os testes para que eles não testem o protocolo desejado, mas consulte os caminhos para os quais o nginx redireciona. Na verdade, estamos testando não o protocolo, mas a coragem de sua implementação específica de muletas.

Ambas as opções não são muito bonitas, então surgiu a idéia: por que não consertar o Micronaut para a finalidade correta, tanto mais que essa edição será útil não apenas para nós. Ou seja, eu queria algo assim:

 @CustomMethod("PROPFIND") public String process( // Provide here HttpRequest or something else, as standard micronaut methods ) { } 

E assumi alegremente essa tarefa, mas o que aconteceu no final?

Solução dois: vamos reescrever tudo!




De fato, é muito mais fácil do que parece à primeira vista. A confirmação simplesmente altera HttpMethod de enum para classe. Em seguida, criamos métodos estáticos (principalmente valueOf) dentro da classe que foi chamada para enum. E a IDEA, juntamente com Gradle, garantiu que nada quebrasse.

A coisa mais difícil aqui foi com o DefaultUriRouter, pois assumiu que o conjunto foi corrigido e criou uma matriz de listas de caminhos para possíveis métodos. Isso teve que ser abandonado para uma nova implementação. Mas, em geral, tudo acabou sendo bastante simples. Observe que você tinha que adicionar 240 linhas e excluir 116.

O problema é que essa é uma grande mudança. Sim, na prática, em um projeto regular usando o Micronaut, você - provavelmente - não usa o HttpMethod diretamente no código e, se o usar, é improvável que use o método ordinal e outros métodos enum específicos. No entanto, isso ainda não permite tal alteração na versão 1.x, especialmente considerando o fato de que tudo isso foi iniciado para dar suporte a um caso bastante raro. Mas para a versão 2.x, essa é uma edição normal, mas você ainda precisa viver até a versão 2.x. Portanto, eu tive que escrever mais código ...

Solução três: agir evolutivamente


imagem

Na verdade, você pode ver a solicitação pull correspondente para a versão 1.3. Como você pode ver, eu tive que escrever cerca de cinco vezes mais código do que para uma grande mudança, e isso não é por acaso. Aqui, quero elogiar os métodos padrão nas interfaces introduzidas no oitavo Java. Para uma refatoração que não quebra a compatibilidade com versões anteriores, isso é insubstituível e não consigo imaginar como faria essas edições para Java até a oitava versão (embora, por incrível que pareça, uma grande mudança possa ser feita antes da oitava).

As edições básicas foram baseadas no fato de que a interface HttpRequest tinha um método getMethod, usado para filtrar. Ele retornou, como você pode imaginar, enum. Portanto, o método padrão getHttpMethodName foi adicionado à interface, que, por padrão, retorna o nome do valor da enumeração. Em seguida, descobriram onde o método original era usado na correspondência de caminhos e lá foi substituído pelas chamadas para o novo método. E então, nas implementações da interface para o servidor Netty, o método da interface foi redefinido para usar o valor real do método HTTP.

Continha uma armadilha que pode ser vista na discussão e diz respeito aos clientes declarativos da Micronaut. Eles usam a conversão do nome do valor enum em uma instância da classe HttpMethod para Netty. Se você olhar para a documentação do método valueOf nesta classe, notará que o valor em cache será retornado para métodos padrão e, para métodos não padrão, uma nova instância da classe será retornada a cada vez. Ou seja, se você tiver uma carga alta e acessar o servidor com um método HTTP não padrão um milhão de vezes, criará simultaneamente um milhão de novos objetos. Obviamente, os GCs modernos devem lidar com isso, mas ainda não quero criar objetos adicionais assim. Então surgiu a ideia de usar ConcurrentHashMap.computeIfAbsent para armazenamento em cache, mas aqui também não é tão simples: o problema está no defeito do Java 8 , que levará ao bloqueio de fluxos mesmo para o caso em que nenhuma gravação é executada. Como resultado, tomamos uma decisão provisória:

  • Para métodos padrão, usamos o cache de instância, que o Netty fornece (na verdade, como era antes).
  • Para métodos não padronizados, permita que novas instâncias sejam criadas. Aqueles que escolherem métodos fora do padrão devem garantir que o coletor de lixo possa digerir a criação de objetos (por exemplo, usamos Shenandoah).

Conclusões


imagem

O que pode ser dito no final?

  • A conhecida curva de custos de correção de erros em diferentes estágios do desenvolvimento de software se manifestou aqui com muita clareza. Especificamente, estamos falando sobre erro de cálculo no estágio inicial do desenvolvimento do Micronaut, quando foi decidido usar enum para métodos HTTP. É difícil dizer como essa decisão se justifica, dado que o Micronaut está girando no Netty, onde a classe é usada para o mesmo. Essencialmente, manter uma classe em vez de enum não valeria o trabalho extra. Por isso, foi mais fácil fazer uma grande mudança nesse plano do que corrigi-lo com suporte à compatibilidade com versões anteriores.
  • O conhecido calcanhar de Aquiles dos projetos de código aberto (no entanto, isso também pode ser observado em projetos industriais com código fechado) - eles não possuem documentação do projeto. Ao mesmo tempo, o Micronaut possui uma documentação muito boa: quais são as opções para o seu uso e afins. No entanto, aqui estamos falando de documentar como as decisões de design foram tomadas. Como resultado, é bastante difícil para o programador de fora se envolver no desenvolvimento do projeto, mesmo que seja necessária uma pequena melhoria.
  • Não se esqueça de considerar o fato de que um ou outro projeto de código aberto é usado em ambientes de carga alta e multithread. Aqui era necessário levar isso em conta, mesmo para uma pequena melhoria.

PS


Enquanto este artigo estava sendo preparado para publicação, a solicitação de recebimento foi aceita na ramificação do assistente do Micronaut e será lançada na versão 1.3.

Source: https://habr.com/ru/post/pt466917/


All Articles