Spring 5でのリアクティブストリームのデバッグ

Spring 5でのリアクティブストリームのデバッグ

1. 概要

reactive streamsのデバッグは、これらのデータ構造の使用を開始したときに直面しなければならない主な課題の1つです。

また、リアクティブストリームはここ数年人気が高まっていることを念頭に置いて、このタスクを効率的に実行する方法を知っておくことをお勧めします。

リアクティブスタックを使用してプロジェクトを設定することから始めて、これがしばしば厄介な理由を確認しましょう。

2. バグのあるシナリオ

複数の非同期プロセスが実行されており、最終的に例外をトリガーするコードにいくつかの欠陥が導入された実際のシナリオをシミュレートしたいと思います。

全体像を理解するために、アプリケーションがidformattedName、およびquantityのみを含む単純なFooオブジェクトのストリームを消費および処理することに言及します。 sフィールド。 詳細については、on the project hereをご覧ください。

2.1. ログ出力の分析

それでは、スニペットと、未処理のエラーが表示されたときに生成される出力を調べてみましょう。

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();
}

アプリケーションを数秒間実行すると、例外がログに記録されることがあります。

エラーの1つを詳しく見ると、次のようなものが見つかります。

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)

根本原因に基づいて、スタックトレースで言及されているFooNameHelperクラスに気付くと、場合によっては、Fooオブジェクトがより短いformattedName valueで処理されていることが想像できます。期待されます。

もちろん、これは単なる単純化されたケースであり、解決策はかなり明白です。

しかし、これが実際のシナリオであり、例外自体がコンテキスト情報なしでは問題の解決に役立たないと想像してみましょう。

例外は、processFoo,またはprocessFooInAnotherScenarioメソッドの一部としてトリガーされましたか?

他の前のステップは、この段階に到達する前にformattedNameフィールドに影響を与えましたか?

ログエントリは、これらの質問を理解するのに役立ちません。

さらに悪いことに、機能内から例外がスローされない場合もあります。

たとえば、Fooオブジェクトを永続化するためにリアクティブリポジトリに依存していると想像してください。 その時点でエラーが発生した場合、コードのデバッグを開始する場所がわからない場合もあります。

リアクティブストリームを効率的にデバッグするツールが必要です。

3. デバッグセッションの使用

アプリケーションで何が起こっているかを把握するための1つのオプションは、お気に入りのIDEを使用してデバッグセッションを開始することです。

いくつかの条件付きブレークポイントを設定し、ストリームの各ステップが実行されたときのデータの流れを分析する必要があります。

実際、これは面倒な作業になる可能性があります。特に、多くの事後対応プロセスを実行してリソースを共有している場合はなおさらです。

さらに、セキュリティ上の理由からデバッグセッションを開始できない状況は数多くあります。

4. doOnError Methodを使用するか、Subscribeパラメータを使用して情報をログに記録する

時々、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);
    });
}

注:subscribeメソッドでさらに処理を実行する必要がない場合は、パブリッシャーでdoOnError関数をチェーンできることに注意してください。

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

これで、例外を生成した実際の要素に関する情報がまだ多くない場合でも、エラーの原因についてのガイダンスが得られます。

5. Reactorのグローバルデバッグ構成のアクティブ化

Reactorライブラリは、FluxおよびMono演算子の動作を構成できるHooksクラスを提供します。

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();

デバッグモードがアクティブになると、例外ログに役立つ情報が含まれます。

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)

ご覧のとおり、最初のセクションは比較的同じままですが、次のセクションでは以下に関する情報を提供します。

  1. サイト運営者のアセンブリトレース-ここでは、エラーが最初にprocessFooメソッドで生成されたことを確認できます。

  2. エラーが最初にトリガーされた後にエラーを観察したオペレーターと、チェーンされたユーザークラス。

注:この例では、主にこれを明確にするために、さまざまなクラスで操作を追加しています。

デバッグモードのオンとオフはいつでも切り替えることができますが、すでにインスタンス化されているFluxおよびMonoオブジェクトには影響しません。

5.1. 異なるスレッドでのオペレーターの実行

留意すべきもう1つの側面は、ストリームで異なるスレッドが動作している場合でも、アセンブリトレースが適切に生成されることです。

次の例を見てみましょう。

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();
}

ログを確認すると、この場合、最初のセクションが少し変わる可能性がありますが、最後の2つはほとんど同じです。

最初の部分はスレッドスタックトレースであるため、特定のスレッドによって実行された操作のみが表示されます。

これまで見てきたように、これはアプリケーションをデバッグするときに最も重要なセクションではないため、この変更は許容されます。

6. 単一プロセスでのデバッグ出力のアクティブ化

すべてのリアクティブプロセスでスタックトレースをインスツルメントおよび生成するには、コストがかかります。

したがって、we should implement the former approach only in critical cases

とにかく、Reactor provides a way to enable the debug mode on single crucial processes, which is less memory-consuming

checkpoint演算子を参照しています。

public void processFoo(Flux flux) {

    // ...

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

この方法では、アセンブリトレースがチェックポイントステージでログに記録されることに注意してください。

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)

反応性チェーンの終わりに向かってcheckpointメソッドを実装する必要があります。

そうしないと、オペレーターはダウンストリームで発生するエラーを監視できません。

また、ライブラリがオーバーロードされたメソッドを提供していることに注意してください。 回避できます:

  • no-argsオプションを使用する場合、観察されたエラーの説明を指定する

  • カスタム記述のみを提供することにより、充填されたスタックトレースを生成します(最もコストのかかる操作です)。

7. 一連の要素のロギング

最後に、Reactorパブリッシャーは、場合によっては便利になる可能性のあるもう1つの方法を提供します。

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

この例で試してみましょう。

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();
}

そしてログを確認してください:

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!
...

この段階での各Fooオブジェクトの状態と、例外が発生したときにフレームワークがフローをキャンセルする方法を簡単に確認できます。

もちろん、このアプローチにもコストがかかるため、適度に使用する必要があります。

8. 結論

アプリケーションを適切にデバッグするためのツールとメカニズムがわからない場合、問題のトラブルシューティングに多くの時間と労力を費やす可能性があります。

これは、リアクティブおよび非同期のデータ構造の処理に慣れておらず、物事がどのように機能するかを理解するために追加の支援が必要な場合に特に当てはまります。

いつものように、完全な例はGitHub repoで利用できます。