§非同期の結果の処理
§コントローラを非同期にする
内部的には、Play Frameworkは下から上まで非同期です。Playはすべてのリクエストを非同期のノンブロッキングな方法で処理します。
デフォルトの設定は非同期コントローラに合わせて調整されています。言い換えれば、アプリケーションコードはコントローラ内でブロッキングを避ける必要があります。つまり、コントローラコードが操作を待機することを避ける必要があります。このようなブロッキング操作の一般的な例は、JDBC呼び出し、ストリーミングAPI、HTTPリクエスト、および長い計算です。
デフォルトの実行コンテキストのスレッド数を増やして、ブロッキングコントローラでより多くの同時リクエストを処理できるようにすることもできますが、コントローラを非同期に保つという推奨アプローチに従うことで、スケーリングが容易になり、負荷がかかった状態でもシステムが応答性を維持できます。
§ノンブロッキングアクションの作成
Playの動作方法のため、アクションコードは可能な限り高速である必要があります。つまり、ノンブロッキングである必要があります。したがって、まだ結果を生成できない場合は、結果として何を返す必要がありますか?答えは、将来の結果です!
Future[Result]
は、最終的にResult
型の値で償還されます。通常のResult
の代わりにFuture[Result]
を提供することで、ブロッキングせずに結果を迅速に生成できます。Playは、プロミスが償還されるとすぐに結果を提供します。
Webクライアントはレスポンスを待っている間ブロックされますが、サーバーでは何もブロックされず、サーバーリソースを他のクライアントにサービスを提供するために使用できます。
ただし、Future
の使用は全体像の半分にすぎません!JDBCなどのブロッキングAPIを呼び出している場合は、Playのレンダリングスレッドプールから移動するために、別のエグゼキューターで実行されるようにExecutionStageを構成する必要があります。これは、カスタムディスパッチャーへの参照を持つplay.api.libs.concurrent.CustomExecutionContext
のサブクラスを作成することで実現できます。
import play.api.libs.concurrent.CustomExecutionContext
// Make sure to bind the new context class to this trait using one of the custom
// binding techniques listed on the "Scala Dependency Injection" documentation page
trait MyExecutionContext extends ExecutionContext
class MyExecutionContextImpl @Inject() (system: ActorSystem)
extends CustomExecutionContext(system, "my.executor")
with MyExecutionContext
class HomeController @Inject() (myExecutionContext: MyExecutionContext, val controllerComponents: ControllerComponents)
extends BaseController {
def index = Action.async {
Future {
// Call some blocking API
Ok("result of blocking call")
}(myExecutionContext)
}
}
カスタム実行コンテキストの効果的な使用方法の詳細については、スレッドプールを参照してください。
§Future[Result]
の作成方法
Future[Result]
を作成するには、まず別のFutureが必要です。これは結果を計算するために必要な実際の値を提供するFutureです。
val futurePIValue: Future[Double] = computePIAsynchronously()
val futureResult: Future[Result] = futurePIValue.map { pi => Ok("PI value computed: " + pi) }
Playの非同期API呼び出しはすべてFuture
を提供します。これは、play.api.libs.WS
APIを使用して外部Webサービスを呼び出す場合、またはPekkoを使用して非同期タスクをスケジュールしたり、play.api.libs.Pekko
を使用してアクターと通信する場合に当てはまります。
以下は、コードブロックを非同期的に実行してFuture
を取得する簡単な方法です。
val futureInt: Future[Int] = scala.concurrent.Future {
intensiveComputation()
}
注意:Futureでどのスレッドがコードを実行するかを理解することが重要です。上記の2つのコードブロックでは、Playsのデフォルトの実行コンテキストに対してインポートがあります。これは、コールバックを受け入れるFuture APIのすべてのメソッドに渡される暗黙的なパラメーターです。実行コンテキストは、必ずしもそうではありませんが、多くの場合スレッドプールと同等になります。
Future
でラップしても、同期IOを魔法のように非同期にすることはできません。ブロッキング操作を回避するためにアプリケーションのアーキテクチャを変更できない場合は、ある時点でその操作を実行する必要があり、そのスレッドがブロックされます。したがって、操作をFuture
で囲むだけでなく、予想される同時実行を処理するのに十分なスレッドで構成された別の実行コンテキストで実行するように構成する必要があります。詳細については、Playのスレッドプールの理解を参照し、データベース統合を示すPlayのサンプルテンプレートをダウンロードしてください。ブロッキング操作にアクターを使用することも役立ちます。アクターは、タイムアウトと障害の処理、ブロッキング実行コンテキストの設定、およびサービスに関連付けられている可能性のある状態の管理のためのクリーンなモデルを提供します。また、アクターは、同時キャッシュリクエストとデータベースリクエストに対処し、バックエンドサーバーのクラスターでリモート実行を可能にする
ScatterGatherFirstCompletedRouter
のようなパターンも提供します。ただし、必要なものによっては、アクターが過剰な場合もあります。
§Futureの返却
これまでアクションを構築するためにAction.apply
ビルダーメソッドを使用していましたが、非同期の結果を送信するにはAction.async
ビルダーメソッドを使用する必要があります。
def index = Action.async {
val futureInt = scala.concurrent.Future { intensiveComputation() }
futureInt.map(i => Ok("Got result: " + i))
}
§アクションはデフォルトで非同期
Playのアクションはデフォルトで非同期です。たとえば、以下のコントローラーコードでは、コードの{ Ok(...) }
部分はコントローラーのメソッド本体ではありません。これはAction
オブジェクトのapply
メソッドに渡される匿名関数であり、Action
型のオブジェクトを作成します。内部的には、あなたが書いた匿名関数が呼び出され、その結果がFuture
で囲まれます。
def echo: Action[AnyContent] = Action { request => Ok("Got request [" + request + "]") }
注意:
Action.apply
とAction.async
の両方が内部的に同じ方法で処理されるAction
オブジェクトを作成します。非同期のAction
は1種類であり、2種類(同期のものと非同期のもの)ではありません。.async
ビルダーは、Future
を返すAPIに基づいてアクションの作成を簡素化するための機能であり、ノンブロッキングコードの記述を容易にします。
§タイムアウトの処理
Webブラウザーがブロックして、何かがうまくいかない場合に待機することを避けるために、タイムアウトを適切に処理すると便利なことがよくあります。play.api.libs.concurrent.Futures
を使用して、Futureをノンブロッキングのタイムアウトでラップできます。
import scala.concurrent.duration._
import play.api.libs.concurrent.Futures._
def index = Action.async {
// You will need an implicit Futures for withTimeout() -- you usually get
// that by injecting it into your controller's constructor
intensiveComputation()
.withTimeout(1.seconds)
.map { i => Ok("Got result: " + i) }
.recover {
case e: scala.concurrent.TimeoutException =>
InternalServerError("timeout")
}
}
注意:タイムアウトはキャンセルと同じではありません。タイムアウトの場合でも、指定されたFutureは完了しますが、その完了した値は返されません。
このドキュメントにエラーを見つけましたか?このページのソースコードはこちらにあります。ドキュメントガイドラインをお読みになったら、プルリクエストを自由に貢献してください。質問や共有できるアドバイスがありますか?コミュニティフォーラムにアクセスして、コミュニティとの会話を開始してください。