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

1.概要

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

また、Reactive Streamsがここ数年で人気を集めていることを念頭に置いて、このタスクを効率的に実行する方法を知っておくことをお勧めします。

リアクティブスタックを使用してプロジェクトを設定することから始めましょう。

2.バグのあるシナリオ

  • いくつかの非同期プロセスが実行されていて、最終的に例外を引き起こす可能性があるコード内のいくつかの欠陥を導入した、実際のシナリオをシミュレートしたいです。

全体像を理解するために、私たちのアプリケーションは id formattedName 、および quantity フィールドのみを含む単純な Foo オブジェクトのストリームを消費および処理することになります。詳細についてはhttps://github.com/eugenp/tutorials/tree/master/spring-5-reactive[ここのプロジェクト]をご覧ください。

2.1. ログ出力の分析

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

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

public void processFooInAnotherScenario(Flux<Foo> 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.baeldung.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.デバッグセッションを使う

私たちのアプリケーションで何が起こっているのかを理解するための一つの選択肢は、私たちのお気に入りのIDEを使ってデバッグセッションを開始することです。

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

実際、これは面倒な作業になる可能性があります。特に、多数のリアクティブプロセスが実行され、リソースを共有している場合は特にそうです。

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

4. __doOnError __MethodまたはSubscribeパラメータを使用した情報のロギング

時々、 subscribe メソッドの2番目のパラメータとして Consumer を提供することで、有用なコンテキスト情報を追加することができます :

public void processFoo(Flux<Foo> 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 演算子の動作を設定できるhttps://projectreactor.io/docs/core/release/reference/#hooks[ Hooks ]クラスが用意されています。

  • 次の文を追加するだけで、アプリケーションはパブリッシャのメソッドへの呼び出しを計測し、演算子の構成をラップし、スタックトレースをキャプチャします** 。

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 メソッドで最初に生成されました。

  1. エラーが最初に発生した後にエラーを観察したオペレータ

それらが連鎖していたユーザクラスで。

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

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

5.1. 異なるスレッドでの演算子の実行

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

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

public void processFoo(Flux<Foo> 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.単一プロセスでデバッグ出力を有効にする

反応プロセスごとにスタックトレースを計測して生成するのはコストがかかります。

したがって、 前者のアプローチは、重大な場合にのみ実装する必要があります

とにかく、 Reactorは単一の重要なプロセスでデバッグモードを有効にする方法を提供します。それはより少ないメモリ消費 です。

私たちは checkpoint 演算子を参照しています:

public void processFoo(Flux<Foo> 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 メソッドを実装する必要があります。

そうでなければ、オペレータは下流で発生しているエラーを観察することができません。

また、ライブラリにはオーバーロードメソッドが用意されています。我々は避けることができます:

  • 引数なしを使用した場合に観察されたエラーの説明を指定する

オプション ** 塗りつぶされたスタックトレースを生成する(これは最もコストのかかる操作です)

カスタムの説明だけを提供する

7.一連の要素を記録する

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

  • リアクティブチェーン内の log メソッドを呼び出すことによって、アプリケーションはフロー内の各要素をその段階での状態でログに記録します。

例で試してみましょう。

public void processFoo(Flux<Foo> 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.まとめ

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

これは、リアクティブデータ構造や非同期データ構造の処理に慣れていない場合に特に当てはまります。また、物事の仕組みを理解するために特別な支援が必要な場合もあります。

いつものように、完全な例はhttps://github.com/eugenp/tutorials/tree/master/spring-5-reactive[Githubリポジトリ]にあります。