春のDeferredResultへのガイド

SpringのDeferredResultへのガイド

1. 概要

このチュートリアルでは、how we can use the DeferredResult class in Spring MVC to perform asynchronous request processingを見ていきます。

非同期サポートはServlet 3.0で導入され、簡単に言えば、リクエストレシーバースレッドとは別のスレッドでHTTPリクエストを処理できるようにします。

Spring 3.2以降で利用可能なDeferredResult,は、長時間実行される計算をhttp-workerスレッドから別のスレッドにオフロードするのに役立ちます。

他のスレッドは計算のためにいくつかのリソースを使用しますが、ワーカースレッドはその間ブロックされず、着信クライアント要求を処理できます。

非同期要求処理モデルは、特にIOを集中的に使用する操作の場合、高負荷時にアプリケーションを適切にスケーリングするのに役立つため、非常に便利です。

2. セットアップ

この例では、SpringBootアプリケーションを使用します。 アプリケーションをブートストラップする方法の詳細については、以前のarticleを参照してください。

次に、DeferredResult を使用した同期通信と非同期通信の両方を示し、高負荷でIOを集中的に使用するユースケースで非同期通信がどのように拡張されるかを比較します。

3. RESTサービスのブロック

標準のブロッキングRESTサービスの開発から始めましょう。

@GetMapping("/process-blocking")
public ResponseEntity handleReqSync(Model model) {
    // ...
    return ResponseEntity.ok("ok");
}

ここでの問題は、the request processing thread is blocked until the complete request is processedと結果が返されることです。 長時間実行される計算の場合、これは次善のソリューションです。

これに対処するために、次のセクションで説明するように、コンテナスレッドをより有効に活用してクライアントリクエストを処理できます。

4. DeferredResultを使用したノンブロッキングREST

ブロッキングを回避するために、コールバックベースのプログラミングモデルを使用します。実際の結果の代わりに、DeferredResultをサーブレットコンテナに返します。

@GetMapping("/async-deferredresult")
public DeferredResult> handleReqDefResult(Model model) {
    LOG.info("Received async-deferredresult request");
    DeferredResult> output = new DeferredResult<>();

    ForkJoinPool.commonPool().submit(() -> {
        LOG.info("Processing in separate thread");
        try {
            Thread.sleep(6000);
        } catch (InterruptedException e) {
        }
        output.setResult(ResponseEntity.ok("ok"));
    });

    LOG.info("servlet thread freed");
    return output;
}

リクエスト処理は別のスレッドで実行され、完了すると、DeferredResultオブジェクトに対してsetResult操作が呼び出されます。

ログ出力を見て、スレッドが期待どおりに動作することを確認しましょう。

[nio-8080-exec-6] com.example.controller.AsyncDeferredResultController:
Received async-deferredresult request
[nio-8080-exec-6] com.example.controller.AsyncDeferredResultController:
Servlet thread freed
[nio-8080-exec-6] java.lang.Thread : Processing in separate thread

内部的には、コンテナスレッドに通知され、HTTP応答がクライアントに配信されます。 接続は、応答が到着するかタイムアウトになるまで、コンテナ(サーブレット3.0以降)によって開かれたままになります。

5. DeferredResultコールバック

DeferredResultには、完了、タイムアウト、エラーの3種類のコールバックを登録できます。

onCompletion()メソッドを使用して、非同期リクエストが完了したときに実行されるコードのブロックを定義しましょう。

deferredResult.onCompletion(() -> LOG.info("Processing complete"));

同様に、onTimeout()を使用して、タイムアウトが発生したときに呼び出すカスタムコードを登録できます。 リクエストの処理時間を制限するために、DeferredResultオブジェクトの作成中にタイムアウト値を渡すことができます。

DeferredResult> deferredResult = new DeferredResult<>(500l);

deferredResult.onTimeout(() ->
  deferredResult.setErrorResult(
    ResponseEntity.status(HttpStatus.REQUEST_TIMEOUT)
      .body("Request timeout occurred.")));

タイムアウトの場合、DeferredResultに登録されたタイムアウトハンドラーを介して異なる応答ステータスを設定しています。

定義されたタイムアウト値である5秒を超えるリクエストを処理して、タイムアウトエラーをトリガーしましょう。

ForkJoinPool.commonPool().submit(() -> {
    LOG.info("Processing in separate thread");
    try {
        Thread.sleep(6000);
    } catch (InterruptedException e) {
        ...
    }
    deferredResult.setResult(ResponseEntity.ok("OK")));
});

ログを見てみましょう:

[nio-8080-exec-6] com.example.controller.DeferredResultController:
servlet thread freed
[nio-8080-exec-6] java.lang.Thread: Processing in separate thread
[nio-8080-exec-6] com.example.controller.DeferredResultController:
Request timeout occurred

何らかのエラーや例外が原因で長時間実行される計算が失敗するシナリオがあります。 この場合、onError()コールバックを登録することもできます。

deferredResult.onError((Throwable t) -> {
    deferredResult.setErrorResult(
      ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
        .body("An error occurred."));
});

エラーが発生した場合、応答の計算中に、このエラーハンドラーを介して別の応答ステータスとメッセージ本文を設定します。

6. 結論

この簡単な記事では、Spring MVCDeferredResultが非同期エンドポイントの作成をどのように容易にするかを見てきました。

いつものように、完全なソースコードはover on Githubで利用できます。