Hola a todos, mi nombre es Dmitry, y hoy hablaré sobre cómo la necesidad de producción me convirtió en
colaborador del marco Micronaut. Seguramente muchos han oído hablar de él. En resumen, esta es una alternativa ligera a Spring Boot, donde el énfasis principal no está en la reflexión, sino en la compilación preliminar de todas las dependencias necesarias. Un conocido más detallado puede comenzar con la
documentación oficial.
El marco Micronaut se utiliza en varios proyectos internos de Yandex y se ha establecido bastante bien. Entonces, ¿qué nos estábamos perdiendo? Puedo decir de inmediato: fuera de la caja, el marco soporta, en principio, todas las características que un programador teóricamente podría necesitar para desarrollar backends. Sin embargo, hay casos raros que no son compatibles de fábrica. Uno de ellos es cuando necesita trabajar no a través de HTTP, sino con la extensión HTTP. Por ejemplo, con métodos adicionales. De hecho, tales casos son mucho más de lo que parece. Además, algunos de estos protocolos son estándares:
- Webdav es una extensión para acceder a los recursos. Además de los métodos estándar, HTTP requiere soporte para métodos adicionales como LOCK, PROPPATCH, etc.
- Caldav es una extensión de Webdav para trabajar con eventos de tipo calendario. Este protocolo con un alto grado de probabilidad se encuentra en las aplicaciones de su teléfono inteligente: para sincronizar calendarios, citas, etc.
Y la lista no se limita a esto. Si observa el
registro de métodos HTTP , verá que los métodos HTTP que solo se describen en los estándares RFC son actualmente 39. Y cuántos casos más hay un protocolo autoescrito sobre HTTP. Por lo tanto, la compatibilidad con métodos HTTP no estándar es bastante común. También sucede a menudo que el marco que utiliza no es compatible con dichos métodos. Aquí hay una discusión sobre Stack Overflow para
ExpressJS . Y aquí está la solicitud de extracción en el github para
Tornado . Bueno, dado que Micronaut a menudo se posiciona como una alternativa ligera a Spring, este es el mismo problema para
Spring .
No es sorprendente que cuando en uno de los proyectos necesitáramos soporte para un protocolo que extiende HTTP en términos de métodos, enfrentamos el mismo problema para Micronaut que hemos estado usando para este proyecto durante mucho tiempo. Resultó que hacer que Micronaut procese métodos no estándar es bastante difícil.
Por qué Porque si observa la definición de métodos HTTP en Micronaut en este momento,
encontrará que se configuran usando Enum, y no una clase, como se hace, por ejemplo, en Netty (no menciono accidentalmente Netty, más tarde aparecerá) más de una vez) Para empeorar las cosas, todas las coincidencias de llamadas al servidor se realizan mediante el filtrado por enumeración, no por el nombre de cadena del método. Esto significa que si necesita un método HTTP no estándar, debe escribirlo en Enum, y esta no es una solución tan buena para el problema. Primero, requerirá una confirmación en el repositorio cada vez que necesite un nuevo método. En segundo lugar, los métodos HTTP no están estandarizados por defecto y su lista no está fijada en ningún lado, por lo que no es realista prever todas las situaciones posibles. Es necesario forzar a Micronaut para que de alguna manera procese métodos que no fueron proporcionados previamente por los desarrolladores.
Solución uno: frente

La primera y más obvia solución fue no tocar Micronaut en absoluto y no reescribir nada en él. Por qué, porque puedes poner nronx delante de Micronaut, como lo hicimos nosotros, a partir de un
ejemplo :
http { upstream other_PROPPATCH { server ...; } upstream other_REPORT { server ...; } server { location /service { proxy_method POST; proxy_pass http://other_$request_method; } } }
Cual es el punto? Podemos forzar a nginx para que métodos no estándar accedan al proxy que necesitamos, mientras usamos la capacidad de nginx para cambiar el método: es decir, accederemos a través del método POST y Micronaut puede manejarlo.
Que es malo Para empezar, en realidad hacemos que todas las solicitudes desde el punto de vista de Micronaut no sean idempotentes. No olvide que para los métodos no estándar también existe tal separación. Por ejemplo, REPORT es idempotente, mientras que PROPPATCH no lo es. Como resultado, el marco no conoce el tipo de solicitud, y el programador que está mirando el código de estos controladores tampoco podrá determinarlo. Sin embargo, este ni siquiera es el caso. Ya tenemos un conjunto de pruebas que verifican automáticamente el cumplimiento de la aplicación con el protocolo deseado. Para que estas pruebas funcionen con una solución de este tipo en un proyecto, debe elegir una de dos opciones:
- Aumente la imagen nginx con la configuración necesaria, además de la aplicación en sí, para que las pruebas accedan a nginx y no a Micronaut. Aunque la infraestructura de Yandex ciertamente le permite elevar componentes adicionales, en este caso parece que la sobre ingeniería es puramente para pruebas.
- Vuelva a escribir las pruebas para que no prueben el protocolo deseado, sino que se refieran a las rutas a las que redirige nginx. Es decir, de hecho, no estamos probando el protocolo, sino las agallas de su implementación específica de muletas.
Ambas opciones no son muy hermosas, por lo que surgió la idea: ¿por qué no arreglar Micronaut para el propósito correcto? Además, seguro que dicha edición será útil no solo para nosotros. Es decir, quería algo como esto:
@CustomMethod("PROPFIND") public String process( // Provide here HttpRequest or something else, as standard micronaut methods ) { }
Y alegremente asumí esta tarea, pero ¿qué pasó al final?
Solución dos: ¡reescribamos todo!

De hecho, es mucho más fácil de lo que parece a primera vista.
El commit simplemente cambia HttpMethod de enum a class. Luego, creamos métodos estáticos (principalmente valueOf) dentro de la clase que fueron llamados para enum. E IDEA junto con Gradle se aseguraron de que nada se rompiera.
Lo más difícil aquí fue con DefaultUriRouter, ya que asumió que el conjunto estaba arreglado y creó una matriz de listas de rutas para posibles métodos. Esto tuvo que ser abandonado para una nueva implementación. Pero en general, todo resultó ser bastante simple. Tenga en cuenta que tenía que agregar 240 líneas y eliminar 116.
El problema es que este es un cambio importante. Sí, en la práctica, en un proyecto regular que usa Micronaut, lo más probable es que no use HttpMethod directamente en el código, y si lo usa, es poco probable que use el método ordinal y otros métodos de enumeración específicos allí. Sin embargo, esto todavía no hace que tal cambio en la versión 1.x sea permisible, especialmente teniendo en cuenta el hecho de que todo esto se inició para apoyar un caso bastante raro. Pero para 2.x esta es una edición normal, pero aún tiene que vivir hasta 2.x. Por lo tanto, tuve que escribir más código ...
Solución tres: actuar evolutivamente

En realidad, puede ver la
solicitud de extracción correspondiente para la versión 1.3. Como puede ver, tuve que escribir aproximadamente cinco veces más código que para un cambio importante, y esto no es accidental. Aquí quiero elogiar los métodos predeterminados en las interfaces introducidas en el octavo Java. Para una refactorización que no rompa la compatibilidad con versiones anteriores, esto es irremplazable, y no puedo imaginar cómo haría estas ediciones para Java hasta la octava versión (aunque, curiosamente, podría hacerse un cambio importante antes de la octava).
Las ediciones básicas se basaron en el hecho de que la interfaz HttpRequest tenía un método getMethod, que se usaba para el filtrado. Regresó, como se puede suponer, enum. Por lo tanto, el método predeterminado getHttpMethodName se agregó a la interfaz, que de forma predeterminada devuelve el nombre del valor de enumeración. Luego encontraron dónde se usó el método original en la coincidencia de rutas, y allí fue reemplazado por llamadas al nuevo método. Y luego, en las implementaciones de la interfaz para el servidor Netty, el método de la interfaz se redefinió para usar el valor real del método HTTP.
Contenía una trampa que se puede ver
en la discusión , y se refiere a los clientes declarativos de Micronaut. Utilizan la conversión del nombre del valor enum a una instancia de la clase HttpMethod para Netty. Si mira la documentación del método
valueOf en esta clase, notará que el valor almacenado en caché se devolverá para los métodos estándar, y para los métodos no estándar, se devolverá una nueva instancia de la clase cada vez. Es decir, si tiene alta carga y recurre al servidor con un método HTTP no estándar un millón de veces, creará simultáneamente un millón de objetos nuevos. Por supuesto, los GC modernos deberían hacer frente a esto, pero aún así no quiero crear objetos adicionales así como así. Luego surgió la idea de usar
ConcurrentHashMap.computeIfAbsent para el almacenamiento en caché, pero aquí tampoco es tan simple: el problema está en el defecto de
Java 8 , lo que conducirá a bloquear secuencias incluso para el caso cuando no se realiza ninguna grabación. Como resultado, tomamos una decisión provisional:
- Para los métodos estándar, utilizamos el almacenamiento en caché de instancias, que Netty proporciona (de hecho, como era antes).
- Para métodos no estándar, deje que se creen nuevas instancias. Cualquiera que elija métodos no estándar debe asegurarse de que el recolector de basura pueda digerir la creación de objetos (nosotros, por ejemplo, usamos Shenandoah).
Conclusiones

¿Qué se puede decir al final?
- La conocida curva de costo de corrección de errores en diferentes etapas del desarrollo de software se manifestó aquí muy claramente. Específicamente, estamos hablando de un error de cálculo en la etapa inicial del desarrollo de Micronaut, cuando se decidió utilizar enum para los métodos HTTP. Es difícil decir cómo se justifica esta decisión, dado que Micronaut está girando sobre Netty, donde la clase se usa para lo mismo. Esencialmente, mantener una clase en lugar de una enumeración no valdría la pena el trabajo extra. Es por eso que resultó más fácil hacer un cambio importante en este plan que arreglarlo con soporte para compatibilidad con versiones anteriores.
- El conocido talón de Aquiles de los proyectos de código abierto (sin embargo, esto también se puede observar en proyectos industriales con código cerrado): no tienen documentación del proyecto. Al mismo tiempo, Micronaut tiene muy buena documentación: cuáles son las opciones para su uso y similares. Sin embargo, aquí estamos hablando de documentar cómo se tomaron las decisiones de diseño. Como resultado, es bastante difícil para el programador externo involucrarse en el desarrollo del proyecto, incluso si se requiere una ligera mejora.
- No olvide considerar el hecho de que uno u otro proyecto de código abierto se utiliza en entornos de alta carga y subprocesos múltiples. Aquí era necesario tener esto en cuenta incluso para una pequeña mejora.
PS
Mientras este artículo se estaba preparando para su publicación, la solicitud de extracción se aceptó en la rama del asistente de Micronaut y se lanzará en la versión 1.3.