§非同期の結果の処理
§コントローラーを非同期にする
Play Frameworkは内部的に、ボトムアップで非同期です。Playはすべてのリクエストを非同期かつノンブロッキングな方法で処理します。
デフォルトの設定は、非同期コントローラーに合わせて調整されています。言い換えれば、アプリケーションコードはコントローラー内でブロッキングを避ける必要があります。つまり、コントローラーコードが処理を待機しないようにする必要があります。このようなブロッキング操作の一般的な例としては、JDBC呼び出し、ストリーミングAPI、HTTPリクエスト、および長時間計算などがあります。
デフォルトの実行コンテキストのスレッド数を増やして、ブロッキングコントローラーでより多くの同時リクエストを処理できるようにすることは可能ですが、コントローラーを非同期に保つという推奨アプローチに従うと、負荷がかかった状態でもスケーリングしやすく、システムを応答性の高い状態に保つことができます。
§ノンブロッキングアクションの作成
Playの動作方法により、アクションコードは可能な限り高速、つまりノンブロッキングである必要があります。それでは、結果を計算できない場合、アクションから何を返すべきでしょうか?結果のプロミスを返す必要があります!
Java 8以降では、CompletionStage
と呼ばれる汎用的なプロミスAPIが提供されています。CompletionStage<Result>
は、最終的にResult
型の値で償還されます。通常のResult
の代わりにCompletionStage<Result>
を使用することで、何もブロックすることなくアクションからすぐに戻ることができます。Playは、プロミスが償還されるとすぐに結果を提供します。
§CompletionStage<Result>
を作成する方法
CompletionStage<Result>
を作成するには、まず別のプロミスが必要です。それは、結果を計算するために必要な実際の値を提供するプロミスです。
CompletionStage<Double> promiseOfPIValue = computePIAsynchronously();
// Runs in same thread
CompletionStage<Result> promiseOfResult =
promiseOfPIValue.thenApply(pi -> ok("PI value computed: " + pi));
Playの非同期APIメソッドはCompletionStage
を提供します。これは、play.libs.WS
APIを使用して外部Webサービスを呼び出す場合や、Pekkoを使用して非同期タスクをスケジュールしたり、play.libs.Pekko
を使用してアクターと通信したりする場合です。
この場合、CompletionStage.thenApply
を使用すると、前のタスクと同じ呼び出しスレッドで完了ステージが実行されます。これは、ブロッキングのない少量のCPUバウンドロジックがある場合には問題ありません。
コードブロックを非同期で実行し、CompletionStage
を取得する簡単な方法は、CompletableFuture.supplyAsync()
メソッドを使用することです。
// creates new task
CompletionStage<Integer> promiseOfInt =
CompletableFuture.supplyAsync(() -> intensiveComputation());
supplyAsync
を使用すると、フォークジョインプールに配置される新しいタスクが作成され、別のスレッドから呼び出される可能性があります。ただし、ここではデフォルトのエグゼキューターを使用しており、実際にはエグゼキューターを明示的に指定します。
CompletionStage
の「*Async」メソッドのみが非同期実行を提供します。
§ClassLoaderExecutionContextの使用
アクション内でJavaのCompletionStage
を使用する場合は、クラスローダーがスコープ内にあることを確認するために、クラスローダーの実行コンテキストをエグゼキューターとして明示的に指定する必要があります。
play.libs.concurrent.ClassLoaderExecutionContext
インスタンスは、依存性注入を介して提供できます。
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import javax.inject.Inject;
import play.libs.concurrent.ClassLoaderExecutionContext;
import play.mvc.*;
public class MyController extends Controller {
private ClassLoaderExecutionContext clExecutionContext;
@Inject
public MyController(ClassLoaderExecutionContext ec) {
this.clExecutionContext = ec;
}
public CompletionStage<Result> index() {
// Use a different task with explicit EC
return calculateResponse()
.thenApplyAsync(
answer -> {
return ok("answer was " + answer).flashing("info", "Response updated!");
},
clExecutionContext.current());
}
private static CompletionStage<String> calculateResponse() {
return CompletableFuture.completedFuture("42");
}
}
ClassLoaderExecutionContext
の使用方法の詳細については、クラスローダーを参照してください。
§CustomExecutionContextとClassLoaderExecutionの使用
ただし、CompletionStage
またはClassLoaderExecutionContext
を使用することは、状況の一面に過ぎません!この時点では、PlayのデフォルトのExecutionContextを使用しています。JDBCなどのブロッキングAPIを呼び出している場合は、Playのレンダリングスレッドプールから移動するために、異なるエグゼキューターでExecutionStageを実行する必要があります。これは、play.libs.concurrent.CustomExecutionContext
のサブクラスを、カスタムディスパッチャーへの参照とともに作成することで実行できます。
次のインポートを追加します。
import play.libs.concurrent.ClassLoaderExecution;
import javax.inject.Inject;
import java.util.concurrent.Executor;
import java.util.concurrent.CompletionStage;
import static java.util.concurrent.CompletableFuture.supplyAsync;
カスタム実行コンテキストを定義します。
public class MyExecutionContext extends CustomExecutionContext {
@Inject
public MyExecutionContext(ActorSystem actorSystem) {
// uses a custom thread pool defined in application.conf
super(actorSystem, "my.dispatcher");
}
}
application.conf
でカスタムディスパッチャーを定義する必要があります。これは、Pekkoディスパッチャー構成を通じて行われます。
カスタムディスパッチャーを取得したら、明示的なエグゼキューターを追加し、ClassLoaderExecution.fromThread
でラップします。
public class Application extends Controller {
private MyExecutionContext myExecutionContext;
@Inject
public Application(MyExecutionContext myExecutionContext) {
this.myExecutionContext = myExecutionContext;
}
public CompletionStage<Result> index() {
// Wrap an existing thread pool, using the context from the current thread
Executor myEc = ClassLoaderExecution.fromThread(myExecutionContext);
return supplyAsync(() -> intensiveComputation(), myEc)
.thenApplyAsync(i -> ok("Got result: " + i), myEc);
}
public int intensiveComputation() {
return 2;
}
}
CompletionStage
でラップするだけで、同期IOを非同期に魔法のように変換することはできません。ブロッキング操作を回避するためにアプリケーションのアーキテクチャを変更できない場合は、ある時点でその操作を実行する必要があり、そのスレッドはブロックされます。したがって、操作をCompletionStage
で囲むことに加えて、予想される同時実行数を処理するのに十分なスレッド数で構成された別の実行コンテキストで実行するように構成する必要があります。詳細については、Playスレッドプールの理解を参照し、データベース統合を示すPlayのサンプルテンプレートをダウンロードしてください。
§アクションはデフォルトで非同期です
Playのアクションはデフォルトで非同期です。たとえば、以下のコントローラーコードでは、返されたResult
は内部的にプロミスに囲まれています。
public Result index(Http.Request request) {
return ok("Got request " + request + "!");
}
注:アクションコードが
Result
またはCompletionStage<Result>
のどちらを返すかに関わらず、どちらの種類も内部的には同じように処理されます。Action
の種類は1つだけであり、それは非同期であり、2種類(同期と非同期)ではありません。CompletionStage
を返すことは、ノンブロッキングコードを記述するための手法です。
§タイムアウトの処理
何か問題が発生した場合にWebブラウザがブロックして待機することを避けるために、タイムアウトを適切に処理することが役立つことがよくあります。play.libs.concurrent.Futures.timeout
メソッドを使用して、CompletionStage
をノンブロッキングタイムアウトでラップできます。
class MyClass {
private final Futures futures;
private final Executor customExecutor = ForkJoinPool.commonPool();
@Inject
public MyClass(Futures futures) {
this.futures = futures;
}
CompletionStage<Double> callWithOneSecondTimeout() {
return futures.timeout(computePIAsynchronously(), Duration.ofSeconds(1));
}
public CompletionStage<String> delayedResult() {
long start = System.currentTimeMillis();
return futures.delayed(
() ->
CompletableFuture.supplyAsync(
() -> {
long end = System.currentTimeMillis();
long seconds = end - start;
return "rendered after " + seconds + " seconds";
},
customExecutor),
Duration.of(3, SECONDS));
}
}
注:タイムアウトはキャンセルと同じではありません。タイムアウトの場合でも、指定されたフューチャーは完了しますが、完了した値は返されません。
このドキュメントにエラーを見つけましたか?このページのソースコードはこちらにあります。ドキュメントのガイドラインを読んだ後、プルリクエストを自由に送信してください。質問や共有するアドバイスがありますか?コミュニティフォーラムに参加して、コミュニティとの会話を始めてください。