Débogage des flux réactifs au printemps 5

Débogage des flux réactifs au printemps 5

1. Vue d'ensemble

Le débogage dereactive streams est probablement l’un des principaux défis auxquels nous devrons faire face une fois que nous commencerons à utiliser ces structures de données.

Et étant donné que les flux réactifs ont gagné en popularité au cours des dernières années, il est judicieux de savoir comment nous pouvons effectuer cette tâche efficacement.

Commençons par configurer un projet à l'aide d'une pile réactive pour voir pourquoi cela est souvent gênant.

2. Scénario avec des bugs

Nous voulons simuler un scénario réel, dans lequel plusieurs processus asynchrones sont en cours d’exécution, et dans lequel nous avons introduit des défauts dans le code qui finiront par déclencher des exceptions.

Pour comprendre la situation dans son ensemble, nous mentionnerons que notre application consommera et traitera des flux d'objets simplesFoo qui ne contiennent qu'unid, unformattedName et unquantitychamp de s. Pour plus de détails, veuillez consulteron the project here.

2.1. Analyser la sortie du journal

Examinons maintenant un extrait de code et la sortie qu'il génère lorsqu'une erreur non gérée apparaît:

public void processFoo(Flux flux) {
    flux = FooNameHelper.concatFooName(flux);
    flux = FooNameHelper.substringFooName(flux);
    flux = FooReporter.reportResult(flux);
    flux.subscribe();
}

public void processFooInAnotherScenario(Flux flux) {
    flux = FooNameHelper.substringFooName(flux);
    flux = FooQuantityHelper.divideFooQuantity(flux);
    flux.subscribe();
}

Après avoir exécuté notre application pendant quelques secondes, nous nous rendrons compte qu’elle enregistre les exceptions de temps en temps.

En examinant de près l'une des erreurs, nous trouverons quelque chose de similaire à ceci:

Caused by: java.lang.StringIndexOutOfBoundsException: String index out of range: 15
    at j.l.String.substring(String.java:1963)
    at com.example.debugging.consumer.service.FooNameHelper
      .lambda$1(FooNameHelper.java:38)
    at r.c.p.FluxMap$MapSubscriber.onNext(FluxMap.java:100)
    at r.c.p.FluxMap$MapSubscriber.onNext(FluxMap.java:114)
    at r.c.p.FluxConcatMap$ConcatMapImmediate.innerNext(FluxConcatMap.java:275)
    at r.c.p.FluxConcatMap$ConcatMapInner.onNext(FluxConcatMap.java:849)
    at r.c.p.Operators$MonoSubscriber.complete(Operators.java:1476)
    at r.c.p.MonoDelayUntil$DelayUntilCoordinator.signal(MonoDelayUntil.java:211)
    at r.c.p.MonoDelayUntil$DelayUntilTrigger.onComplete(MonoDelayUntil.java:290)
    at r.c.p.MonoDelay$MonoDelayRunnable.run(MonoDelay.java:118)
    at r.c.s.SchedulerTask.call(SchedulerTask.java:50)
    at r.c.s.SchedulerTask.call(SchedulerTask.java:27)
    at j.u.c.FutureTask.run(FutureTask.java:266)
    at j.u.c.ScheduledThreadPoolExecutor$ScheduledFutureTask
      .access$201(ScheduledThreadPoolExecutor.java:180)
    at j.u.c.ScheduledThreadPoolExecutor$ScheduledFutureTask
      .run(ScheduledThreadPoolExecutor.java:293)
    at j.u.c.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
    at j.u.c.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    at j.l.Thread.run(Thread.java:748)

En se basant sur la cause première, et en remarquant la classeFooNameHelper mentionnée dans la trace de pile, nous pouvons imaginer que dans certaines occasions, nos objetsFoo sont traités avec une valeurformattedName plus courte que attendu.

Bien entendu, il ne s'agit que d'un cas simplifié et la solution semble plutôt évidente.

Mais imaginons qu’il s’agisse d’un scénario réel où l’exception elle-même ne nous aide pas à résoudre le problème sans quelques informations de contexte.

L'exception a-t-elle été déclenchée dans le cadre de la méthodeprocessFoo, ou de la méthodeprocessFooInAnotherScenario?

D'autres étapes précédentes ont-elles affecté le champformattedName avant d'arriver à ce stade?

L'entrée de journal ne nous aiderait pas à résoudre ces questions.

Pour aggraver les choses, parfois l'exception n'est même pas lancée depuis notre fonctionnalité.

Par exemple, imaginons que nous comptions sur un référentiel réactif pour conserver nos objetsFoo. Si une erreur se produit à ce moment-là, il est possible que nous ne sachions même pas par où commencer pour déboguer notre code.

Nous avons besoin d'outils pour déboguer efficacement les flux réactifs.

3. Utiliser une session de débogage

Une option pour comprendre ce qui se passe avec notre application consiste à démarrer une session de débogage à l'aide de notre IDE préféré.

Nous devrons configurer quelques points d'arrêt conditionnels et analyser le flux de données lorsque chaque étape du flux sera exécutée.

En effet, cela peut être une tâche fastidieuse, en particulier lorsque nous avons de nombreux processus réactifs en cours d’exécution et de partage de ressources.

De plus, il existe de nombreuses circonstances dans lesquelles nous ne pouvons pas démarrer une session de débogage pour des raisons de sécurité.

4. Journalisation des informations avec la méthodedoOnError  ou à l'aide du paramètre d'abonnement

Parfois,we can add useful context information, by providing a Consumer as a second parameter of the subscribe method:

public void processFoo(Flux flux) {

    // ...

    flux.subscribe(foo -> {
        logger.debug("Finished processing Foo with Id {}", foo.getId());
    }, error -> {
        logger.error(
          "The following error happened on processFoo method!",
           error);
    });
}

Remarque: il convient de mentionner que si nous n'avons pas besoin d'effectuer d'autres traitements sur la méthodesubscribe, nous pouvons chaîner la fonctiondoOnError sur notre éditeur:

flux.doOnError(error -> {
    logger.error("The following error happened on processFoo method!", error);
}).subscribe();

Nous allons maintenant avoir des indications sur l'origine de l'erreur, même si nous n'avons toujours pas beaucoup d'informations sur l'élément réel qui a généré l'exception.

5. Activation de la configuration de débogage globale de Reactor

La bibliothèqueReactor fournit une classeHooks qui nous permet de configurer le comportement des opérateursFlux etMono.

By just adding the following statement, our application will instrument the calls to the to the publishers' methods, wrap the construction of the operator, and capture a stack trace:

Hooks.onOperatorDebug();

Une fois le mode débogage activé, nos journaux d'exceptions contiendront des informations utiles:

16:06:35.334 [parallel-1] ERROR c.b.d.consumer.service.FooService
  - The following error happened on processFoo method!
java.lang.StringIndexOutOfBoundsException: String index out of range: 15
    at j.l.String.substring(String.java:1963)
    at c.d.b.c.s.FooNameHelper.lambda$1(FooNameHelper.java:38)
    ...
    at j.l.Thread.run(Thread.java:748)
    Suppressed: r.c.p.FluxOnAssembly$OnAssemblyException:
Assembly trace from producer [reactor.core.publisher.FluxMapFuseable] :
    reactor.core.publisher.Flux.map(Flux.java:5653)
    c.d.b.c.s.FooNameHelper.substringFooName(FooNameHelper.java:32)
    c.d.b.c.s.FooService.processFoo(FooService.java:24)
    c.d.b.c.c.ChronJobs.consumeInfiniteFlux(ChronJobs.java:46)
    o.s.s.s.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:84)
    o.s.s.s.DelegatingErrorHandlingRunnable
      .run(DelegatingErrorHandlingRunnable.java:54)
    o.u.c.Executors$RunnableAdapter.call(Executors.java:511)
    o.u.c.FutureTask.runAndReset(FutureTask.java:308)
Error has been observed by the following operator(s):
    |_    Flux.map ⇢ c.d.b.c.s.FooNameHelper
            .substringFooName(FooNameHelper.java:32)
    |_    Flux.map ⇢ c.d.b.c.s.FooReporter.reportResult(FooReporter.java:15)

Comme nous pouvons le constater, la première section reste relativement la même, mais les sections suivantes fournissent des informations sur:

  1. La trace d'assemblage de l'éditeur - ici, nous pouvons confirmer que l'erreur a d'abord été générée dans la méthodeprocessFoo.

  2. Les opérateurs qui ont observé l'erreur après son déclenchement initial, avec la classe d'utilisateurs où ils ont été chaînés.

Remarque: Dans cet exemple, principalement pour voir cela clairement, nous ajoutons les opérations sur différentes classes.

Nous pouvons activer ou désactiver le mode de débogage à tout moment, mais cela n’affectera pas les objetsFlux etMono qui ont déjà été instanciés.

5.1. Exécution d'opérateurs sur différents threads

Un autre aspect à garder à l'esprit est que la trace d'assembly est générée correctement, même si différents threads opèrent sur le flux.

Jetons un œil à l'exemple suivant:

public void processFoo(Flux flux) {
    flux = flux.publishOn(Schedulers.newSingle("foo-thread"));
    // ...

    flux = flux.publishOn(Schedulers.newSingle("bar-thread"));
    flux = FooReporter.reportResult(flux);
    flux.subscribeOn(Schedulers.newSingle("starter-thread"))
      .subscribe();
}

Maintenant, si nous vérifions les journaux, nous apprécierons que dans ce cas, la première section puisse changer un peu, mais les deux dernières restent sensiblement les mêmes.

La première partie est la trace de la pile de threads, donc elle affichera uniquement les opérations effectuées par un thread particulier.

Comme nous l'avons vu, ce n'est pas la section la plus importante lors du débogage de l'application. Cette modification est donc acceptable.

6. Activation de la sortie de débogage sur un processus unique

Instrumenter et générer une trace de pile dans chaque processus réactif est coûteux.

Ainsi,we should implement the former approach only in critical cases.

Quoi qu'il en soit,Reactor provides a way to enable the debug mode on single crucial processes, which is less memory-consuming.

Nous faisons référence à l'opérateurcheckpoint:

public void processFoo(Flux flux) {

    // ...

    flux = flux.checkpoint("Observed error on processFoo", true);
    flux.subscribe();
}

Notez que de cette manière, la trace de l’assemblage sera consignée à l’étape du point de contrôle:

Caused by: java.lang.StringIndexOutOfBoundsException: String index out of range: 15
    ...
Assembly trace from producer [reactor.core.publisher.FluxMap],
  described as [Observed error on processFoo] :
    r.c.p.Flux.checkpoint(Flux.java:3096)
    c.b.d.c.s.FooService.processFoo(FooService.java:26)
    c.b.d.c.c.ChronJobs.consumeInfiniteFlux(ChronJobs.java:46)
    o.s.s.s.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:84)
    o.s.s.s.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54)
    j.u.c.Executors$RunnableAdapter.call(Executors.java:511)
    j.u.c.FutureTask.runAndReset(FutureTask.java:308)
Error has been observed by the following operator(s):
    |_    Flux.checkpoint ⇢ c.b.d.c.s.FooService.processFoo(FooService.java:26)

Nous devrions implémenter la méthodecheckpoint vers la fin de la chaîne réactive.

Sinon, l'opérateur ne pourra pas observer les erreurs survenant en aval.

Notons également que la bibliothèque propose une méthode surchargée. Nous pouvons éviter:

  • spécifiant une description de l'erreur observée si nous utilisons l'option no-args

  • générer une trace de pile remplie (l'opération la plus coûteuse), en fournissant uniquement la description personnalisée

7. Enregistrer une séquence d'éléments

Enfin, les éditeurs Reactor proposent une méthode supplémentaire qui pourrait être utile dans certains cas.

By calling the log method in our reactive chain, the application will log each element in the flow with the state that it has at that stage.

Essayons-le dans notre exemple:

public void processFoo(Flux flux) {
    flux = FooNameHelper.concatFooName(flux);
    flux = FooNameHelper.substringFooName(flux);
    flux = flux.log();
    flux = FooReporter.reportResult(flux);
    flux = flux.doOnError(error -> {
        logger.error("The following error happened on processFoo method!", error);
    });
    flux.subscribe();
}

Et vérifiez les journaux:

INFO  reactor.Flux.Map.1 - onSubscribe(FluxMap.MapSubscriber)
INFO  reactor.Flux.Map.1 - request(unbounded)
INFO  reactor.Flux.Map.1 - onNext(Foo(id=0, formattedName=theFo, quantity=8))
INFO  reactor.Flux.Map.1 - onNext(Foo(id=1, formattedName=theFo, quantity=3))
INFO  reactor.Flux.Map.1 - onNext(Foo(id=2, formattedName=theFo, quantity=5))
INFO  reactor.Flux.Map.1 - onNext(Foo(id=3, formattedName=theFo, quantity=6))
INFO  reactor.Flux.Map.1 - onNext(Foo(id=4, formattedName=theFo, quantity=6))
INFO  reactor.Flux.Map.1 - cancel()
ERROR c.b.d.consumer.service.FooService
  - The following error happened on processFoo method!
...

Nous pouvons facilement voir l'état de chaque objetFoo à ce stade, et comment le framework annule le flux lorsqu'une exception se produit.

Bien entendu, cette approche est également coûteuse et nous devrons l’utiliser avec modération.

8. Conclusion

Nous pouvons consommer beaucoup de temps et d’efforts pour résoudre les problèmes si nous ne connaissons pas les outils et mécanismes pour déboguer correctement notre application.

Cela est particulièrement vrai si nous ne sommes pas habitués à gérer des structures de données réactives et asynchrones, et nous avons besoin d'une aide supplémentaire pour comprendre comment les choses fonctionnent.

Comme toujours, l'exemple complet est disponible sur lesGitHub repo.