このマニュアルは現在作成中であり、未完成です。
改善にご協力いただければ幸いです。ご協力いただける場合は、READMEをご覧ください。

8 非同期&ノンブロッキング

Ratpackは、「非同期」かつ「ノンブロッキング」のリクエスト処理を目的として設計されています。内部IO(例:HTTPリクエストとレスポンスの転送)はすべてノンブロッキング方式で行われます(Nettyのおかげです)。このアプローチにより、スループットの向上、リソース使用率の低下、そして重要なことに、負荷下での動作の予測可能性が向上します。このプログラミングモデルは、Node.jsプラットフォームのおかげで、最近ますます人気が高まっています。Ratpackは、Node.jsと同じノンブロッキング、イベント駆動型のモデルに基づいて構築されています。

非同期プログラミングは、悪名高いほどトリッキーです。Ratpackの主要な価値提案の1つは、非同期の問題を解決するための構成要素と抽象化を提供することであり、パフォーマンスを向上させながら実装をシンプルに保つことです。

1.8 ブロッキングフレームワーク&コンテナとの比較

ほとんどのJVM Webフレームワークとコンテナ、そしてJDKの大部分を支えているJava Servlet APIは、基本的に同期プログラミングモデルに基づいています。ほとんどのJVMプログラマはこのプログラミングモデルに精通しており、快適に利用しています。このモデルでは、IOを実行する必要がある場合、呼び出しスレッドは操作が完了し、結果が利用可能になるまで単にスリープします。このモデルでは、比較的大きなスレッドプールが必要です。Webアプリケーションのコンテキストでは、これは通常、各リクエストが大きなプールからスレッドにバインドされ、アプリケーションが「X」個の並列リクエストを処理できることを意味します。「X」はスレッドプールのサイズです。

Servlet APIのバージョン3.0では、非同期リクエスト処理が容易になっています。しかし、オプトインオプションとして非同期サポートをレトロフィットすることは、完全に非同期のアプローチとは異なります。Ratpackは最初から非同期です。

このモデルの利点は、同期プログラミングが間違いなく「シンプル」であることです。このモデルの欠点は、ノンブロッキングモデルとは対照的に、より多くのリソース使用量を必要とし、スループットが低くなることです。並列にさらに多くのリクエストを処理するには、スレッドプールのサイズを増やす必要があります。これにより、計算リソースの競合が増加し、これらのスレッドのスケジューリングの管理に多くのサイクルが失われるだけでなく、メモリ消費量も増加します。最新のオペレーティングシステムとJVMは、この競合を非常にうまく管理できますが、それでもスケーリングのボトルネックです。さらに、より多くのリソース割り当てを必要とするため、最新の従量課金制のデプロイメント環境では深刻な考慮事項となります。

非同期、ノンブロッキングモデルは、大きなスレッドプールを必要としません。これは、IOを待機してスレッドがブロックされることがないためです。IOを実行する必要がある場合、呼び出しスレッドは、IOが完了したときに呼び出される何らかのコールバックを登録します。これにより、IOが発生している間、スレッドを他の処理に使用できます。このモデルでは、スレッドプールは利用可能な処理コアの数に合わせてサイズ変更されます。スレッドは常に計算でビジー状態であるため、スレッドを増やす意味はありません。

多くのJava API(InputStreamJDBCなど)は、ブロッキングIOモデルを前提としています。Ratpackは、ブロッキングコストを最小限に抑えながらそのようなAPIを使用するためのメカニズムを提供します(下記で説明します)。

Ratpackは、主に2つの点で根本的に非同期です…

  1. HTTP IOはイベント駆動型/ノンブロッキングです(Nettyのおかげです)
  2. リクエスト処理は、非同期関数のパイプラインとして編成されています

Ratpackを使用する場合、HTTP IOがイベント駆動型であることは、ほとんど透過的です。Nettyはただ動作するだけです。

2番目の点は、Ratpackの重要な特徴です。コードが同期していることを期待していません。オプトインの非同期サポートを持つ多くのWebフレームワークには、複雑な(つまり、現実世界の)非同期操作を実行しようとすると明らかになる深刻な制約と落とし穴があります。Ratpackは最初から非同期です。さらに、複雑な非同期処理を容易にする構成要素と抽象化を提供します。

2.8 ブロッキング操作の実行(例:IO)

ほとんどのアプリケーションでは、何らかのブロッキングIOを実行する必要があります。多くのJava APIは非同期オプションを提供していません(例:JDBC)。Ratpackは、別々のスレッドプールでブロッキング操作を実行するための簡単なメカニズムを提供します。これにより、リクエスト処理(つまり、計算)スレッドのブロッキングを回避できます(これは良いことです)が、スレッドの競合のためにいくらかのオーバーヘッドが発生します。ブロッキングIO APIを使用する必要がある場合は、残念ながら他に選択肢がありません。

でっち上げのデータストアAPIを考えてみましょう。実際のデータストアとの通信にはIOが必要になる可能性があります(または、メモリ内にある場合は、同じブロッキング効果を持つ1つ以上のロックを待機する必要があります)。APIメソッドは、リクエスト処理スレッドで呼び出すことができません。ブロックされるためです。代わりに、「ブロッキング」APIを使用する必要があります…

import ratpack.core.handling.InjectionHandler;
import ratpack.core.handling.Context;
import ratpack.exec.Blocking;

import ratpack.test.handling.RequestFixture;
import ratpack.test.handling.HandlingResult;

import java.util.Collections;
import java.io.IOException;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Example {

  // Some API that performs blocking operations
  public static interface Datastore {
    int deleteOlderThan(int days) throws IOException;
  }

  // A handler that uses the API
  public static class DeletingHandler extends InjectionHandler {
    void handle(final Context context, final Datastore datastore) {
      final int days = context.getPathTokens().asInt("days");
      Blocking.get(() -> datastore.deleteOlderThan(days))
        .then(i -> context.render(i + " records deleted"));
    }
  }

  // Unit test
  public static void main(String... args) throws Exception {
    HandlingResult result = RequestFixture.handle(new DeletingHandler(), fixture -> fixture
        .pathBinding(Collections.singletonMap("days", "10"))
        .registry(r -> r.add(Datastore.class, days -> days))
    );

    assertEquals("10 records deleted", result.rendered(String.class));
  }
}

ブロッキング操作として送信された関数は、非同期的に(つまり、Blocking.get()メソッドはプロミスをすぐに返します)、別々のスレッドプールで実行されます。返される結果は、リクエスト処理(つまり、計算)スレッドで処理されます。

Blocking#get()メソッドの詳細については、を参照してください。

3.8 非同期操作の実行

非同期APIとの統合には、Promise#async(Upstream<t>)を使用します。これは本質的に、サードパーティのAPIをRatpackのプロミスタイプに適合させるメカニズムです。

import ratpack.test.embed.EmbeddedApp;
import ratpack.exec.Promise;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class Example {
  public static void main(String... args) throws Exception {
    EmbeddedApp.fromHandler(ctx ->
        Promise.async((f) ->
            new Thread(() -> f.success("hello world")).start()
        ).then(ctx::render)
    ).test(httpClient -> {
      assertEquals("hello world", httpClient.getText());
    });
  }
}

4.8 非同期合成とコールバックヘルからの回避

非同期プログラミングの課題の1つは、合成にあります。些細ではない非同期プログラミングは、すぐに「コールバックヘル」として知られる現象に陥る可能性があります。これは、多くの層のネストされたコールバックの理解不能さを説明するために使用される用語です。

非同期操作を複雑なワークフローにエレガントかつクリーンに合成することは、現在急速に進化している分野です。Ratpackは、非同期合成のフレームワークを提供しようとはしていません。代わりに、このタスクのための特殊なツールを統合し、アダプタを提供することを目指しています。このアプローチの例として、RatpackとRxJavaの統合があります。

一般的に、統合は、RatpackのPromiseタイプをターゲットフレームワークの合成プリミティブに適合させることです。