paint-brush
Comment déboguer une application Spring WebFluxpar@vladimirf
8,232 lectures
8,232 lectures

Comment déboguer une application Spring WebFlux

par Vladimir Filipchenko7m2023/05/09
Read on Terminal Reader

Trop long; Pour lire

Le débogage d'une application Spring WebFlux peut être une tâche difficile, en particulier lorsqu'il s'agit de flux réactifs complexes. Des problèmes tels que le blocage du code, les problèmes de concurrence et les conditions de concurrence peuvent tous provoquer des bogues subtils difficiles à diagnostiquer. La cause première pourrait être évidente à trouver pour ceux qui connaissent les applications réactives. Cependant, certaines pratiques ci-dessous pourraient être encore très utiles à réviser.
featured image - Comment déboguer une application Spring WebFlux
Vladimir Filipchenko HackerNoon profile picture
0-item
1-item

Le débogage d'une application Spring WebFlux peut être une tâche difficile, en particulier lorsqu'il s'agit de flux réactifs complexes. Contrairement aux applications de blocage traditionnelles, où la trace de la pile fournit une indication claire de la cause première d'un problème, les applications réactives peuvent être plus difficiles à déboguer. Des problèmes tels que le blocage du code, les problèmes de concurrence et les conditions de concurrence peuvent tous provoquer des bogues subtils difficiles à diagnostiquer.


Scénario

Lorsqu'il s'agit d'un bogue, ce n'est pas toujours un problème lié au code. Il peut s'agir d'un ensemble de facteurs, tels qu'une refactorisation récente, des changements d'équipe, des délais serrés, etc. Dans la vraie vie, il est courant de finir par dépanner des applications volumineuses créées par des personnes qui ont quitté l'entreprise il y a quelque temps et que vous venez de rejoindre.


Connaître un peu un domaine et des technologies ne va pas vous faciliter la vie.


Dans l'exemple de code ci-dessous, je voulais imaginer à quoi pourrait ressembler un code bogué pour une personne qui a récemment rejoint une équipe.


Considérez le débogage de ce code comme un voyage plutôt que comme un défi. La cause première pourrait être évidente à trouver pour ceux qui connaissent les applications réactives. Cependant, certaines pratiques ci-dessous pourraient être encore très utiles à réviser.


 @GetMapping("/greeting/{firstName}/{lastName}") public Mono<String> greeting(@PathVariable String firstName, @PathVariable String lastName) { return Flux.fromIterable(Arrays.asList(firstName, lastName)) .filter(this::wasWorkingNiceBeforeRefactoring) .transform(this::senselessTransformation) .collect(Collectors.joining()) .map(names -> "Hello, " + names); } private boolean wasWorkingNiceBeforeRefactoring(String aName) { // We don't want to greet with John, sorry return !aName.equals("John"); } private Flux<String> senselessTransformation(Flux<String> flux) { return flux .single() .flux() .subscribeOn(Schedulers.parallel()); }


Ainsi, ce que fait ce morceau de code est le suivant : il ajoute "Hello, " aux noms fournis en tant que paramètres.

Votre collègue John vous dit que tout fonctionne sur son ordinateur portable. C'est vrai:


 > curl localhost:8080/greeting/John/Doe > Hello, Doe


Mais lorsque vous l'exécutez comme curl localhost:8080/greeting/Mick/Jagger , vous voyez le stacktrace suivant :


 java.lang.IndexOutOfBoundsException: Source emitted more than one item at reactor.core.publisher.MonoSingle$SingleSubscriber.onNext(MonoSingle.java:134) ~[reactor-core-3.5.5.jar:3.5.5] Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: Error has been observed at the following site(s): *__checkpoint ⇢ Handler com.example.demo.controller.GreetingController#greeting(String, String) [DispatcherHandler] *__checkpoint ⇢ HTTP GET "/greeting/Mick/Jagger" [ExceptionHandlingWebHandler] Original Stack Trace: <18 internal lines> at java.base/java.util.concurrent.FutureTask.run(FutureTask.java) ~[na:na] (4 internal lines)


Bien, aucune des traces ne mène à un exemple de code ci-dessus.


Tout ce qu'il révèle, c'est que 1) cela s'est produit dans la méthode GreetingController#greeting , et 2) le client a effectué un `HTTP GET "/greeting/Mick/Jagger

.doOnError()


La première et la plus simple chose à essayer est d'ajouter le rappel `.doOnError()` à la fin de la chaîne de salutation.


 @GetMapping("/greeting/{firstName}/{lastName}") public Mono<String> greeting(@PathVariable String firstName, @PathVariable String lastName) { return Flux.fromIterable(Arrays.asList(firstName, lastName)) // <...> .doOnError(e -> logger.error("Error while greeting", e)); }


Bien essayé, mais les journaux ne montrent aucune amélioration.


Néanmoins, la trace de la pile interne de Reactor :



Voici quelques façons dont doOnError peut/ne peut pas être utile pendant le débogage :

  1. Journalisation : vous pouvez utiliser doOnError pour consigner les messages d'erreur et fournir plus de contexte sur ce qui s'est mal passé dans votre flux réactif. Cela peut être particulièrement utile lors du débogage de problèmes dans un flux complexe avec de nombreux opérateurs.

  2. Récupération : doOnError peut également être utilisé pour récupérer des erreurs et continuer à traiter le flux. Par exemple, vous pouvez utiliser onErrorResume pour fournir une valeur ou un flux de secours en cas d'erreur.

  3. Débogage : très probablement doOnError ne fournira pas de meilleur stacktrace à l'exception de ce que vous avez déjà vu dans les journaux. Ne vous y fiez pas comme un bon dépanneur.


enregistrer()


Le prochain arrêt consiste à remplacer doOnError() précédemment ajouté par l'appel de méthode log() . Aussi simple que possible. log() observe tous les signaux Reactive Streams et les trace dans les journaux sous le niveau INFO par défaut.


Voyons maintenant quelles informations supplémentaires nous voyons dans les journaux :


Nous pouvons voir quelles méthodes réactives ont été appelées ( onSubscribe , request et onError ). De plus, savoir à partir de quels threads (pools) ces méthodes ont été appelées peut être une information très utile. Cependant, il n'est pas pertinent pour notre cas.


À propos des pools de threads


Le nom du fil ctor-http-nio-2 signifie reactor-http-nio-2 . Les méthodes réactives onSubscribe() et request() ont été exécutées sur le pool de threads IO (planificateur). Ces tâches ont été exécutées immédiatement sur un thread qui les a soumises.


En ayant .subscribeOn(Schedulers.parallel()) dans senselessTransformation , nous avons demandé à Reactor de souscrire d'autres éléments sur un autre pool de threads. C'est la raison pour laquelle onError a été exécuté sur le thread parallel-1 .


Vous pouvez en savoir plus sur le pool de threads dans cet article .


La méthode log() vous permet d'ajouter des instructions de journalisation à votre flux, ce qui facilite le suivi du flux de données et le diagnostic des problèmes. Si nous avions un flux de données plus complexe avec des éléments tels que flatMap, des sous-chaînes, des appels bloquants, etc., nous aurions beaucoup à gagner à ce que tout soit enregistré. C'est une chose très facile et agréable pour un usage quotidien. Cependant, nous ne connaissons toujours pas la cause première.


Hooks.onOperatorDebug()


L'instruction Hooks.onOperatorDebug() indique à Reactor d'activer le mode débogage pour tous les opérateurs dans les flux réactifs, permettant des messages d'erreur plus détaillés et des traces de pile.


Selon la documentation officielle :

Lorsque des erreurs sont observées ultérieurement, elles seront enrichies d'une exception supprimée détaillant la pile de la chaîne de montage d'origine. Doit être appelé avant que les producteurs (par exemple Flux.map, Mono.fromCallable) ne soient réellement appelés pour intercepter les bonnes informations de pile.


L'instruction doit être appelée une fois par exécution. L'un des meilleurs endroits serait Configuration ou Classes principales. Pour notre cas d'utilisation, ce serait:


 public Mono<String> greeting(@PathVariable String firstName, @PathVariable String lastName) { Hooks.onOperatorDebug(); return // <...> }


En ajoutant Hooks.onOperatorDebug() nous pouvons enfin progresser dans notre enquête. Stacktrace est bien plus utile :



Et à la ligne 42, nous avons un appel single() .


Ne faites pas défiler vers le haut, le senselessTransformation regarde ensuite :

 private Flux<String> senselessTransformation(Flux<String> flux) { return flux .single() // line 42 .flux() .subscribeOn(Schedulers.parallel()); }


C'est la cause profonde.


single() émet un élément de la source Flux ou signale IndexOutOfBoundsException pour une source avec plus d'un élément. Cela signifie que le flux dans la méthode émet plus d'un élément. En remontant dans la hiérarchie des appels on voit qu'à l'origine il y a un Flux à deux éléments Flux.fromIterable(Arrays.asList(firstName, lastName)) .


La méthode de filtrage wasWorkingNiceBeforeRefactoring supprime un élément d'un flux lorsqu'il est égal à John . C'est la raison pour laquelle le code fonctionne pour un collège nommé John. Hein.


Hooks.onOperatorDebug() peut être particulièrement utile lors du débogage de flux réactifs complexes, car il fournit des informations plus détaillées sur la manière dont le flux est traité. Cependant, l'activation du mode débogage peut avoir un impact sur les performances de votre application (en raison des traces de pile remplies), il ne doit donc être utilisé que pendant le développement et le débogage, et non en production.


Points de contrôle


Pour obtenir presque le même effet que Hooks.onOperatorDebug() donne avec un impact minimal sur les performances, il existe un opérateur spécial checkpoint() . Cela activera le mode débogage pour cette section du flux, tout en laissant le reste du flux inchangé.


Ajoutons deux points de contrôle après filtrage et après transformation :


 public Mono<String> greeting(@PathVariable String firstName, @PathVariable String lastName) { return Flux.fromIterable(Arrays.asList(firstName, lastName)) .filter(this::wasWorkingNiceBeforeRefactoring) /* new */ .checkpoint("After filtering") .transform(this::senselessTransformation) /* new */ .checkpoint("After transformation") .collect(Collectors.joining()) .map(names -> "Hello, " + names); }


Jetez un œil aux journaux :


Cette répartition des points de contrôle nous indique que l'erreur a été observée après notre deuxième point de contrôle décrit comme After transformation . Cela ne signifie pas que le premier point de contrôle n'a pas été atteint lors de l'exécution. C'était le cas, mais l'erreur n'a commencé à apparaître qu'après la deuxième. C'est pourquoi nous ne voyons pas After filtering .


Vous pouvez également voir deux autres points de contrôle mentionnés dans la ventilation, de DispatcherHandler et ExceptionHandlingWebHandler . Ils ont été atteints après celui que nous avons défini, jusqu'à la hiérarchie des appels.


Outre la description, vous pouvez forcer Reactor à générer un stacktrace pour votre point de contrôle en ajoutant true comme deuxième paramètre à la méthode checkpoint() . Il est important de noter que le stacktrace généré vous mènera à la ligne avec un point de contrôle. Il ne remplira pas de stacktrace pour l'exception d'origine. Cela n'a donc pas beaucoup de sens car vous pouvez facilement trouver un point de contrôle en fournissant une description.


Conclusion


En suivant ces meilleures pratiques, vous pouvez simplifier le processus de débogage et identifier et résoudre rapidement les problèmes dans votre application Spring WebFlux. Que vous soyez un développeur chevronné ou que vous débutiez dans la programmation réactive, ces conseils vous aideront à améliorer la qualité et la fiabilité de votre code et à offrir de meilleures expériences à vos utilisateurs.