Lorsque la norme HTTP ne suffit pas. Micronaut s'engage

Bonjour à tous, mon nom est Dmitry, et aujourd'hui je vais parler de la façon dont le besoin de production m'a fait devenir un contributeur pour le framework Micronaut. Beaucoup ont sûrement entendu parler de lui. En bref, il s'agit d'une alternative légère à Spring Boot, où l'accent n'est pas mis sur la réflexion, mais sur la compilation préliminaire de toutes les dépendances nécessaires. Une connaissance plus détaillée peut commencer par la documentation officielle.

Le framework Micronaut est utilisé dans plusieurs projets internes Yandex et s'est assez bien établi. Alors qu'est-ce qui nous manquait? Je peux dire tout de suite: prêt à l'emploi, le framework prend en charge, en principe, toutes les fonctionnalités dont un programmeur pourrait théoriquement avoir besoin pour développer des backends. Cependant, il existe de rares cas qui ne sont pas pris en charge hors de la boîte. L'un d'eux est lorsque vous devez travailler non pas sur HTTP, mais avec l'extension HTTP. Par exemple, avec des méthodes supplémentaires. En fait, de tels cas sont bien plus qu'il n'y paraît. De plus, certains de ces protocoles sont des standards:

  • Webdav est une extension pour accéder aux ressources. En plus des méthodes standard, HTTP nécessite la prise en charge de méthodes supplémentaires telles que LOCK, PROPPATCH, etc.
  • Caldav est une extension Webdav pour travailler avec des événements de type calendrier. Ce protocole à forte probabilité se trouve dans les applications de votre smartphone: pour synchroniser calendriers, rendez-vous, etc.

Et la liste ne se limite pas à cela. Si vous regardez le registre des méthodes HTTP , vous verrez que les méthodes HTTP uniquement décrites par les normes RFC sont actuellement 39. Et combien de cas il existe un protocole auto-écrit sur HTTP. La prise en charge des méthodes HTTP non standard est donc assez courante. Il arrive également souvent que le cadre que vous utilisez ne prenne pas en charge ces méthodes. Voici une discussion sur Stack Overflow for ExpressJS . Et voici la demande de tirage sur le github pour Tornado . Eh bien, puisque Micronaut est souvent positionné comme une alternative légère au Spring - c'est le même problème pour Spring .

Il n'est pas surprenant que lorsque dans l'un des projets nous avions besoin d'un support pour un protocole qui étend HTTP en termes de méthodes, nous avons été confrontés au même problème pour Micronaut que nous utilisons depuis longtemps pour ce projet. Il s'est avéré qu'il est assez difficile d'amener Micronaut à traiter des méthodes non standard.

Pourquoi? Parce que si vous regardez la définition des méthodes HTTP dans Micronaut pour le moment, vous constaterez qu'elles sont définies en utilisant Enum, et non une classe, comme cela se fait, par exemple, dans Netty (je ne mentionne pas accidentellement Netty, plus tard elle apparaîtra plusieurs fois). Pour aggraver les choses, toutes les correspondances d'appels de serveur sont effectuées par filtrage par énumération et non par le nom de chaîne de la méthode. Cela signifie que si vous avez besoin d'une méthode HTTP non standard, vous devez l'écrire dans Enum, et ce n'est pas vraiment une bonne solution au problème. Tout d'abord, il faudra un commit dans le référentiel chaque fois que vous aurez besoin d'une nouvelle méthode. Deuxièmement, les méthodes HTTP ne sont pas standardisées par défaut et leur liste n'est fixe nulle part, il est donc irréaliste de prévoir toutes les situations possibles. Il est nécessaire de forcer Micronaut à traiter en quelque sorte des méthodes qui n'avaient pas été précédemment fournies par les développeurs.

Solution 1: front


image

La première et la plus évidente solution était de ne pas toucher du tout au Micronaut et de ne rien y réécrire. Pourquoi, parce que vous pouvez mettre nronx devant Micronaut, comme nous l'avons fait, à partir d'un exemple :

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

À quoi ça sert? Nous pouvons forcer nginx pour les méthodes non standard à accéder au proxy dont nous avons besoin, tout en utilisant la capacité de nginx à changer la méthode: c'est-à-dire que nous accéderons via la méthode POST, et Micronaut peut le gérer.

Qu'est-ce qui est mauvais? Pour commencer, nous rendons toutes les demandes du point de vue du Micronaut non idempotentes. N'oubliez pas que pour les méthodes non standard, il existe également une telle séparation. Par exemple, REPORT est idempotent, alors que PROPPATCH ne l'est pas. Par conséquent, le framework ne connaît pas le type de demande et le programmeur qui examine le code de ces gestionnaires ne pourra pas non plus le déterminer. Mais ce n'est même pas le cas. Nous avons déjà un ensemble de tests qui vérifient automatiquement la conformité de l'application avec le protocole souhaité. Pour que ces tests fonctionnent avec une telle solution dans un projet, vous devez choisir l'une des deux options:

  • Augmentez l'image nginx avec les paramètres nécessaires, en plus de l'application elle-même, afin que les tests accèdent à nginx, et non à Micronaut lui-même. Bien que l'infrastructure Yandex vous permette certainement d'augmenter des composants supplémentaires, dans ce cas, il semble que la suringénierie soit purement pour les tests.
  • Réécrivez les tests afin qu'ils ne testent pas le protocole souhaité, mais se réfèrent aux chemins vers lesquels nginx redirige. Autrement dit, nous testons non pas le protocole, mais les tripes de sa mise en œuvre spécifique de béquille.

Les deux options ne sont pas très belles, donc l'idée est venue: pourquoi ne pas fixer Micronaut dans le bon but, d'autant plus qu'une telle modification sera utile non seulement pour nous. Autrement dit, je voulais quelque chose comme ça:

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

Et j'ai joyeusement repris cette tâche, mais que s'est-il finalement passé?

Deuxième solution: réécrivons tout!




En fait, c'est beaucoup plus facile qu'il n'y paraît à première vue. La validation change simplement HttpMethod d'énumération en classe. Ensuite, nous avons créé des méthodes statiques (principalement valueOf) à l'intérieur de la classe qui ont été appelées pour enum. Et IDEA couplé à Gradle s'est assuré que rien ne se cassait.

La chose la plus difficile ici était avec DefaultUriRouter, car il supposait que l'ensemble était fixe et créait un tableau de listes de chemins pour les méthodes possibles. Cela a dû être abandonné pour une nouvelle mise en œuvre. Mais en général, tout s'est avéré assez simple. Notez que vous avez dû ajouter 240 lignes et en supprimer 116.

Le problème est qu'il s'agit d'un changement majeur. Oui, dans la pratique, dans un projet régulier utilisant Micronaut, vous - très probablement - n'utilisez pas HttpMethod directement dans le code, et si vous l'utilisez, il est peu probable que vous y utilisiez la méthode ordinale et d'autres méthodes d'énumération spécifiques. Cependant, cela ne permet toujours pas un tel changement dans la version 1.x, d'autant plus que tout cela a été démarré afin de prendre en charge un cas assez rare. Mais pour 2.x, c'est un montage normal, mais vous devez toujours vivre jusqu'à 2.x. Par conséquent, j'ai dû écrire plus de code ...

Troisième solution: agir de manière évolutive


image

En fait, vous pouvez voir la demande d'extraction correspondante pour la version 1.3. Comme vous pouvez le voir, j'ai dû écrire environ cinq fois plus de code que pour un changement majeur, et ce n'est pas un hasard. Ici, je veux faire l'éloge des méthodes par défaut dans les interfaces introduites dans le huitième Java. Pour un tel refactoring qui ne rompt pas la compatibilité descendante, cette chose est irremplaçable, et je ne peux pas imaginer comment je ferais ces modifications pour Java jusqu'à la huitième version (bien que, curieusement, un changement majeur pourrait bien être effectué avant le huitième).

Les modifications de base étaient basées sur le fait que l'interface HttpRequest avait une méthode getMethod, qui était utilisée pour le filtrage. Il est revenu, comme vous pouvez le deviner, enum. Par conséquent, la méthode par défaut getHttpMethodName a été ajoutée à l'interface, qui renvoie par défaut le nom de la valeur d'énumération. Ensuite, ils ont trouvé où la méthode d'origine était utilisée dans la correspondance des chemins, et là, elle a été remplacée par des appels à la nouvelle méthode. Et puis, dans les implémentations de l'interface pour le serveur Netty, la méthode d'interface a été redéfinie pour utiliser la valeur réelle de la méthode HTTP.

Il contenait un écueil visible dans la discussion , et il concernait les clients déclaratifs de Micronaut. Ils utilisent la conversion du nom de la valeur enum en une instance de la classe HttpMethod pour Netty. Si vous consultez la documentation de la méthode valueOf dans cette classe, vous remarquerez que la valeur mise en cache sera renvoyée pour les méthodes standard et pour les méthodes non standard, une nouvelle instance de la classe sera retournée à chaque fois. Autrement dit, si vous avez une charge élevée et que vous vous tournez vers le serveur avec une méthode HTTP non standard un million de fois, vous créerez simultanément un million de nouveaux objets. Bien sûr, les GC modernes devraient faire face à cela, mais je ne veux toujours pas créer des objets supplémentaires comme ça. Ensuite, l'idée est venue d'utiliser ConcurrentHashMap.computeIfAbsent pour la mise en cache, mais ici ce n'est pas si simple non plus: le problème est dans le défaut de Java 8 , ce qui entraînera le blocage des flux même pour le cas où aucun enregistrement n'est effectué. En conséquence, nous avons pris une décision provisoire:

  • Pour les méthodes standard, nous utilisons la mise en cache d'instance, que Netty fournit (en fait, comme avant).
  • Pour les méthodes non standard, laissez de nouvelles instances être créées. Ceux qui choisissent des méthodes non standard doivent s'assurer que le ramasse-miettes peut digérer la création d'objets (nous, par exemple, utilisons Shenandoah).

Conclusions


image

Que peut-on dire finalement?

  • La courbe de coût de correction d'erreur bien connue à différents stades de développement logiciel s'est manifestée ici très clairement. Plus précisément, nous parlons d'une erreur de calcul au tout début du développement de Micronaut, lorsqu'il a été décidé d'utiliser enum pour les méthodes HTTP. Il est difficile de dire comment cette décision est justifiée, étant donné que Micronaut tourne sur Netty, où la classe est utilisée pour la même chose. Essentiellement, le maintien d'une classe au lieu d'enum ne vaudrait pas la peine de faire le travail supplémentaire. C'est pourquoi il s'est avéré plus facile d'apporter un changement majeur à ce plan que de le corriger avec le support de la compatibilité descendante.
  • Le talon d'Achille bien connu des projets open source (cependant, cela peut également être observé dans les projets industriels avec code fermé) - ils n'ont pas de documentation de projet. Dans le même temps, Micronaut dispose en effet d'une très bonne documentation: quelles sont les options pour son utilisation, etc. Cependant, nous parlons ici de documenter comment les décisions de conception ont été prises. Par conséquent, il est assez difficile pour le programmeur de l'extérieur de s'impliquer dans le développement du projet, même si une légère amélioration est nécessaire.
  • N'oubliez pas de tenir compte du fait que l'un ou l'autre projet open source est utilisé dans des environnements multi-threads et à haute charge. Ici, il fallait en tenir compte même pour une petite amélioration.

PS


Pendant la préparation de cet article en vue de sa publication, la demande d'extraction a été acceptée dans la branche de l'assistant Micronaut et sera publiée dans la version 1.3.

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


All Articles