Scalaの型パラメータと変位指定のメモ
変位指定がぜんぜん理解できてなかったので、いろいろ試してある程度理解したメモ。 型パラメータとは?とか変位指定とは?とかの詳細は別の記事をあたってください。 圏論がさっぱりな状態で書いているので、圏論的に変なこと言ってても気にしないでください。
trait Protocol sealed trait Req extends Protocol case class AddRequest() extends Req case class UpdateRequest() extends Req sealed trait Res
みたいな型定義があったときに、AddRequest, UpdateRequestを対象にした処理を書きたかった。
Req
から変位指定して型パラメータつけてあげればできるんじゃないかと思ってたけど全然コンパイルが通らず、理解できなかったのでいろいろ試してみた。
非変は自明なのでいいとして
- 共変(covariance)
class C[+T] val v1: C[Req] = new C[Req] val v2: C[Req] = new C[AddRequest] val v3: C[Req] = new C[Protocol] // NG
AddRequest
はReqを継承してるので共変ならば代入可能。
Protocol
はReqを継承していないので代入不可。
これはまぁ分かりやすい。
- 反変(contravariance)
class C[-T] val v1: C[Req] = new C[AddRequest] // NG val v2: C[AddRequest] = new C[Req] // OK
共変の逆。 関数を例にして挙動を説明される記事が多い。今のところまだ自分で指定したことは無いけど必要なケースはそのうちありそう。
- 上限型境界(upper type bounds)
class C[T <: Req] val v1: C[Req] = new C[Req] val v2: C[Req] = new C[AddRequest] // NG
これの
val v2: C[Req] = new C[AddRequest]
でコンパイルが通らない理由が全然わからなかったが
class C[+T <: Req] val v: C[Req] = new C[AddRequest]
上限型境界に共変性を持たせるとコンパイルが通ることが分かった。
これに気づいて、自分が何を理解して何を理解していなかったがはっきりした。
共変と上限型境界、反変と下限型境界を似たような概念のなんか少し違うもの、くらいでしか理解していなかった。 共変と反変は明確に型同士の関係性を示すものだった。 一方で上限型境界、下限型境界は型に指定できるものを制限する目的で使われるものだった。
class C[T <: Req]
と書くと、TにはReqのサブタイプなら入れられると思っていたが、例えばTに AddRequest
を入れると T
は AddRequest
で束縛されるので、 C[AddRequest]
型となる、ということが分かっていなかった。
もともと自分がやりたかったことは
def dispatch[T <: Req](request: T) = { request match { case _: AddRequest => case _: UpdateRequest => } }
のようなことだったが、 AddRequest
, UpdateRequest
に応じた処理を外から注入して合成しようとすると、どうにも型が不一致してコンパイルできなかった。
def dispatch[+T <: Req](request: T) = {
...
}
として、TをReqの共変として定義してあげればきっと通るだろう(未確認)
gRPC-Webのメモ
ただの使ってみた記事です。
ScalaでAkka gRPCを利用してgRPCサーバを動かし、Envoy Proxyを介してgRPC-Webを触れるようにしたので、gRPC-Webのクライアントを構築してみた。
gRPC-Web クライアント
grpc.io この辺りのチュートリアルを見ながらなぞっただけなのでクライアント側は特に変わったことはしていない。 2個前くらいの記事に置いといたToDoサービス用のprotoファイルを使ってコードを生成した。
とりあえずpackage.jsonに以下の記述を追加してnpm経由で叩けるようにはしたが
"scripts": { "gen-todo": "protoc -I /path/to/protobuf --js_out=import_style=commonjs:service --grpc-web_out=import_style=commonjs,mode=grpcwebtext:service todo.proto" }
いくつか設定値があるようなので、requireを使えるようにcommonjsにしたり、お試し用途なのでbinaryではなくplain textを扱う mode=grpcwebtext
を指定した。
生成されたファイルからの依存があるので、grpc-web
と google-protobuf
dependenciesに追加する必要がある。
あとは生成したファイルを素直に使い、Clientを初期化してRequestを渡してrpcメソッドを叩けば動く。 Repositoryとかインフラ層のレイヤーからここに依存させてメソッドを叩くだけで良いので、やれHTTP通信ライブラリ何にしようとか悩まなくて良くもなるのでかなり楽かもしれない。
ただ、ドキュメントで以下の記述になっているようにコールバックを渡す形式なのが困る。
echoService.echo(request, {}, function(err, response) { // ... });
ので、RepositoryとかでPromise化してあげる必要がありそう。 というか外部との通信レイヤーとのインターフェースはすでにPromiseなはずなので、インターフェースを保ってあげるだけで良いんだな。
という形でクライアントサイドは非常に楽だったのだが、CORSの設定周りでドハマリして時間がかかった。
CORS
結局はEnvoy Proxyの設定の話でしか無いのだけど。
- クライアントサイドからEnvoyにリクエストしたらCORSエラーが出た
- EnvoyにCORSを許可する設定を入れたがうまく動かなかった
- Akka gRPCのgRPC-Web対応のハンドラを試してみたがうまく動かなかった
- EnvoyのCORS設定をし直して動いた
という流れで解決したが、時間がかかった。
最終的にEnvoyのCORS周りの設定値は以下のようになった。
route_config: name: local_route virtual_hosts: - name: local_service domains: - "*" cors: allow_origin_string_match: - prefix: "*" allow_methods: "OPTIONS, GET, PUT, DELETE, POST" allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web expose_headers: grpc-status,grpc-message routes: - match: prefix: "/" route: cluster: todo_service http_filters: - name: envoy.filters.http.cors - name: envoy.filters.http.grpc_web - name: envoy.filters.http.router
もともと match
の箇所にgrpc
の設定値を入れていたのだが、あってはいけなかったらしい。
その設定値があるとOPTIONSメソッドでのリクエストがgrpcリクエストではないため正常にEnvoyでハンドリングされず、404だったり415だったりのエラーになってしまってgrpcリクエストが失敗してしまうという原因だった。
時間がかかる原因にもなったのだが、EvansのgRPC-Webオプション(--web
)をつけるとInputting Canceledなど原因不明なエラーに遭遇し、これがAkka gRPCのエラーなのかEnvoyのものなのかなど調べる必要がでてしまったこともある。
結局EvansのwebオプションをつけるとgRPC-Webのドキュメントからリンクされてるオフィシャルのプロジェクトですら動かなかったので、Evans側の問題なのだろうということにした… 追えればEvansのコード追ってみたい。
あと、gRPC-Web自体がどう動くものなのか分かっていなかったので少し調べたが、以下の記事が参考になった。
https://blog.envoyproxy.io/envoy-and-grpc-web-a-fresh-new-alternative-to-rest-6504ce7eb880
ブラウザからgRPCサーバに対してのgRPCリクエストは送れないので、間に中継役が必要になり、それがEnvoyだということ。
ブラウザからEnvoyに対してはHTTPリクエストを送信し、Envoyから裏側のgRPCサーバに対してHTTP/2の通信をしてくれるということ、などが解説されていて勉強になった。
gRPCサーバ、クライアントの構築が完了して、どちらもこれ以降の実装イメージが湧いたのでOKとする。
gRPCのためのEnvoy Proxy
Akka gRPCを使ってgRPCサーバを構築したが、実際の構成に近いようにということでEnvoyを経由してアクセスさせる。
Dockerで動かすべく見てみると現時点での最新Stableはv1.15.0だった。
この辺りを参考に動かそうとしたら設定内容が微妙に古いようなので、Envoyのv3 APIの情報を参考にしながら設定し直す。
v3 API reference — envoy 1.16.0-dev-7f4e35 documentation
最終的にはenvoy.yamlは以下のようになった
static_resources: listeners: - name: listener_0 address: socket_address: { protocol: TCP, address: 0.0.0.0, port_value: 8080 } filter_chains: - filters: - name: envoy.filters.network.http_connection_manager typed_config: "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager stat_prefix: ingress_http codec_type: auto route_config: name: local_route virtual_hosts: - name: local_service domains: - "*" routes: - match: prefix: "/" grpc: {} route: cluster: todo_service http_filters: - name: envoy.filters.http.router typed_config: {} clusters: - name: todo_service connect_timeout: 0.250s type: logical_dns http2_protocol_options: {} lb_policy: round_robin load_assignment: cluster_name: todo_service endpoints: - lb_endpoints: - endpoint: address: socket_address: address: scala-grpc port_value: 9090
関係ないけど、普段yamlはvimで触ることが多いもののインデントがよく崩れるので以下の設定を入れたら快適になった。
autocmd FileType yaml setlocal ts=2 sts=2 sw=2 expandtab
構築したgRPCサーバをsbt-native-packagerでdockerイメージを作り、 scala-grpc
という名前で動かすものの全然うまく動かなくてドハマリした。
エラーハンドリングをしていないせいで処理中に例外が発生すると INTERNAL
というエラーしか吐かれず、何が問題なのか分からなすぎて解決にやたらと時間がかかった。ちゃんとエラーをログに吐かないと辛いことになる。
ハマったエラーは、今回H2を使っているもののdockerイメージを作るタイミングでDDLファイルが同梱されず、テーブルが正常に生成されていないためにDBアクセスすると例外が吐かれている、というものだった。読み込むファイルパスを無理やり変えてマウントしてなんとか動かすことができた。
gRPCの疎通確認は
こちらのブログで紹介されてたEvansを使った。めっちゃ便利。
次こそgrpc-webの実装に入りたい。
Akka gRPCとSlickの学習記録
grpc-gateway, grpc-webをいい加減に触ろう、ということでScalaでgRPCサーバを構築することにした。 Akkaに多少馴染みが出てきたのでAkka gRPCを使うことにする。
Akka gRPC
https://developer.lightbend.com/guides/akka-grpc-quickstart-scala/
Akka gRPCのQuickstartのページがあり、基本的にこのままで動く。 gRPCはHttp/2ベースでSSL前提なので証明書が必要になる。リンクが貼られてるサンプルコードの中にそのへんのファイルとか記述があるのでコピペしたら動いた。
実際に動かすとなったら証明書周りどうするんだろ。 リリースまで行かなかったけど、以前導入することになりそうだったときはバックエンド間での通信だからInsecureでいいよね、ということで対象外だった気がする。
何にせよとりあえずScalaでのgRPCサーバの構築自体はいけた。 Greeterにクライアントアプリから接続してもつまらないので、こういうときの定番モノであるToDoアプリを作ることにする。
ということでprotoファイルをgistにあげた。まぁ単純なCRUD操作がある感じ。 example ToDo service proto · GitHub
Akka gRPCのコードに対しては特に書くところも無いな。 強いて言えば複数Serviceを動かすときはPartialFunctionとして、それを連結して動かす感じになる模様。 doc.akka.io
ToDoアプリを作る以上、何かしらデータベースが必要になる。 ちょっと前に作ったアプリでScalaからRedisを操作する辺りはやったので、いい加減にSlickを触ることにする。
Slick
スリックなのかシリックなのかスィリックなのかよく分からないやつ。スィリックって読んでる。 MySQLを用意するのもめんどっちいのでH2で済ませる。H2もちゃんと触ったことが無かった。
ドキュメントを読みながら一通り頭から流していく。 scala-slick.org scala-slick.org
コード生成しなくていいかな、と思ってたけど、サンプルコードを見てもわけが分からないのでおとなしくslick-codegenを入れて動かすべきだった。
個人的にドキュメントに載ってるコードはなかなか不親切で
( for( c <- coffees; if c.price < limit ) yield c.name ).result
の coffees
とかどこからどう出てきた何の型の値なの っていうところとか、多少分かれば空気読んでなんとかなるけど初めにこれ読んでも意味分からんのでは。
と思って見返したらすぐあとにCoffees
の型定義とval coffees
の初期化部分のコード載ってたわ。ちゃんと見ないとダメだな。。。
という感じで飛ばし飛ばしで見てたせいで全然Slickが動かせなくてまぁ時間がかかった。
TableQueryに対してもろもろ操作して生成したDBIOActionをdb.run
に突っ込んでクエリを実行する、っていう大雑把な理解をした。
H2
H2がよく分からなくて困ったところもあった。オンメモリで動作するっていうのは意識にあったものの、思ったよりすぐ消えるのでよく分からない。
DB_CLOSE_DELAY=-1
をつけろっていうのをStackoverflowで見かけたので従ってなんとか動くようになったりとか。
最終的に
url = "jdbc:h2:mem:todo;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false;INIT=runscript from 'app/src/main/sql/create.sql'"
というURL定義になった。
DATABASE_TO_UPPER=false
これをしないとテーブル名だったりカラム名が勝手にアッパーケースになるのか…
create.sqlにDDLを書いたり、初期値用のINSERT文を書いといたら、なぜかINSERT文が2回発行されてて重複したレコードができて謎だった。 この辺どこからどう実行されてるのか調べるのが骨が折れてもう… PHPならブレークポイント貼ってIDE上から該当プロセスを実行してしまえばある程度追えるのだけど、JavaやScalaはそういうことがしづらくて困るなー。
gRPCサーバ側の実装ができたので、次はクライアントサイドの実装をしていく。
PHPerによるScala入門その4 ログ
ずいぶんと間が空いて4だけど気にしない。
ログ
Scalaで吐くApplication log. PHPだと最近はもうmonlog一択な印象を受ける。
テキトーにAkka Httpで作ったアプリケーションを動かしてると
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder". SLF4J: Defaulting to no-operation (NOP) logger implementation SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details
みたいなログが出てくることがある。 何も設定してないけど、Akkaがslf4jに依存してるのかな?
何も知識が無いのでぐぐってみると、
というStack Overflowがあったり、入門者は困るんじゃないかなという空気を感じた。 というかまずPlayで入門していろいろ学んだ方が効率良いんじゃないのか…?
ともあれ、Stack Overflowの中でもslf4jがデファクトっぽいのでとりあえずこれの使い方を見ていく。
slf4sもあるようだけど GitHub - w11k/slf4s: Simple Scala facade for SLF4J
とりあえずすでにログも出てるし4jで進めていく。
SLF4J
SLFってなんぞと思っていたけど、 Simple Log Facade
なのね。
さて見ていくかと思ったのだけど、LightbendがScala Loggingというのを出していた。SLF4Jベースらしいし、Akkaを使ってるしLightbendに依存していこうということでこっちで見ていく。
Scala Logging
logbackとscala loggingをlibraryDependenciesに追加して
com.typesafe.scalalogging.Logger("name").info(message)
もうこれだけでログが出ますね。かんたーん。
Logback
とりあえず使う分にはこれでいいのだけど、実際に調べたかったことはLogback. logback.xmlでなにかしらを設定すると何か変わる、ということは知ってるけど実際に何をどう設定するのか全然知らないので見てみる。
logback-test.xml, logback.groovy, logback.xml の順で読まれるらしい。classpath内に直接置いとけば勝手に読んでくれるよ、ということらしいけど、classpathとはどれのことを指してるんだろう…
The class path is the path that the Java runtime environment searches for classes and other resource files.
ふむ、なるほど… とりあえずresrouceに置いとくか。
内容は
<configuration> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <!-- encoders are assigned the type ch.qos.logback.classic.encoder.PatternLayoutEncoder by default --> <encoder> <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <logger name="chapters.configuration" level="INFO" /> <logger name="chapters.configuration.Foo" level="DEBUG" /> <root level="debug"> <appender-ref ref="STDOUT" /> </root> </configuration>
大体こんな感じになるとのこと。
- appender
- logger
- root
がある。rootは特別なloggerって感じかな。 appenderは出力方式を決めてるところに見える。 FILEに吐くなりSTDOUTに吐くなり。
logger, rootはfilterかな。
変数を使いたければpropertyで指定すればいいらしい。
なるほど、Logback完全に理解した()
ISUCON9 本選で4位になれました
去年に引き続き今年もISUCONの本選に参加することができました。 今年の予選は全体の31番目でギリギリ本選は出れないところでしたが
この方々の誠実さのおかげで本選に出れることになりました。
繰り上がりで出ることになり、この方々の分も、とか、ワンチャン敗者復活からの優勝を果たしてサンドウィッチマンみたいに…!とか思ってました。
最終スコアは 13,016 で、まさかの4位!あと1000点で賞金圏内の3位だったって思うと少し悔しいですが、大健闘できました。ちなみに去年の本選は24位くらいで、得点が出たチームの中での下から2番目くらいでした。そのリベンジが果たせたことが個人的に本当に嬉しいです
チーム構成
言語はPHPで、メンバー構成は
- アプリ 2人
- インフラ 1人
サーバー構成は最終的に a: web c: db
bは使えなかった、という感じだった気がします。
何をしたかの記録
当日の記憶が怪しいですが。リポジトリはこちら
GitHub - shmurakami/isucon9_final
去年の本選の反省から、まずコードを読んで仕様を理解する ということから始めます。去年の本選はSNS告知(だったかな?)のフラグに最後まで気付かないという失態を犯していたので、とりあえずコード読まないと話にならんという学びがありました。
コードを読みながら、どうせ使うだろうということでRedisの導入をしたり、NewRelicの導入をしてもらったりしてました。
今回の問題はデフォルトで貼られているインデックスがほとんど無く、初期状態ではベンチマークが回らない前提だったのでベンチを回す前にクエリを見て最低限のインデックスを貼っていきます。
最低限のインデックスを貼ってベンチを回してもずっとFailしている状態が続き、途中で数値しか入らない train_name のカラムがvarcharなことに腹を立ててint に変えたりしましたが、ベンチが通らないので日和って戻したりしてた気がします。
ベンチマークのエラーメッセージが何を言ってるか分からずベンチのバグなのかこちらのバグなのかの切り分けのために運営に聞いてみたりしました。
仕様だということで理解。 何が不正なのかが分からずデバッグのしようも無いのでなかなか苦しかった。
後で分かりましたが、ベンチが通らなかった理由は
元々KEYが無かったテーブルにインデックスを追加したことで、取得する順番が変わってしまった
のようです。 「何もしてないのに壊れました」と言ってますがお前インデックス貼ってるやんけ という感じでちょっと何言ってるか分かんないですね。
このときはクエリにorder byをつけて解決したようです。どうやって気付いたか覚えてないけど、多分画面を見ておかしいと思ってorder byをつけたらベンチが通ったのでしょう。train_nameがvarcharなのでインデックスでおかしくなったので、intに変換してあげれば多分通ったんじゃないかな、という気がしてますが試してみたいですね。 結局ベンチマークが初めて通ったのは12:58:36でした。長かった… このときのスコアは 1,138
ベンチが通ってようやくパフォーマンス改善に取り組めるようになったので、N+1の改善とクエリの実行計画の改善、master系のテーブルの中身をメモリ上に乗せてRedisもMySQLも叩かなくていいようにしたりしてました。
全体をちょっとずつ触ったところで available_days を50にあげてみたところ、スコアは4,374まで上昇。
cancelのエンドポイントが遅かったのが気になりましたが、
- 外部APIに依存しておりそっちはどうしようもない
- DBのロックも範囲が狭いし多量のHTTPリクエストが直列発行されてるわけでもない
- PHPなので非同期リクエストは無理
- 処理できたところでキャンセルは得点が低いから別の方を優先したい
という理由で捨てました。
この辺りでボトルネックが大体 search, reserve 辺りまで絞れてきたのでこの辺の改善をしていくことに。
やることは特に変えずに、仕様を理解、クエリを改善(インデックス、不要なテーブルの参照を除去)、ということをしていって、
16:41:10 5,952
16:59:33 9,529
と順調にスコアを上げられていきました。
そろそろavailable_daysをもうちょっと上げて別の問題が出るかどうか見た方がいいんじゃないか、まだやれることはあるし後でいいんじゃないか、という葛藤を脳内でずっとしてましたけど、堅実にやれることから潰していったのは良い判断だったかな、と思います。
その後も運賃計算周りでDBにアクセスする必要をなくしたりと改善を続けて、 17:47:13 11,376
まで上げることができました。
この辺で、01_schema.sql に追加し忘れてるインデックスがあることに気付いたのはラッキーでした。このまま行ってたら再起動試験後にひどいことになってしまうところだった。
終了時間ギリギリまで available_days の変更とベンチマーク実行を繰り返してましたが、結局available_daysは70に落ち着きました。オリンピック時期は負荷が上がるということでしたが、7月末なので、240くらいまで上げないと効果が出ないのかな…?
手元での最終スコアは 11,415 でしたが、最終の再起動試験では 13,016 まで上がっててちょっと驚きました。メモリとかの問題かな。
締め
去年からISUCONに出始めて、2年連続で本選にまで出れてめっちゃ楽しかったです。 来年は今のところ出ないと思いますが、機会があれば是非また出たいですね。ありがとうございました🙏
PHP GuzzleでのAsyncリクエストの挙動
先月ISUCON9の予選に参加して予選突破しました。なんとかギリギリの繰り上がりでしたが、その話はまた別途記事を書くとして。ここまで書いて本戦前日まで公開を忘れてた。 外部のサーバーへのHTTPリクエストを非同期化したいところがあったんですが、競技時間内にどうしてもうまく実装できなかったのでGuzzleのAsyncリクエスト周りの挙動とコードを追ってみたので雑にまとめます。
guzzle
PHPのデファクトスタンダードなHTTPリクエストライブラリ。使ってみた記事とかいっぱいあるので詳細はgoogleで。
Asyncリクエスト
Guzzle6から?もうちょっと前から?Asyncリクエストがサポートされるようになった。curl_multi_execのwrapperだと思っている。 PHPでどうやって非同期処理してるの?と思ってcurl_multi_execの実装を読むと、どうやら非同期に処理してるというよりはcurlリクエストのまとまりを裏側で順番に実行して結果をまとめて返してるように見えた。 なので非同期というよりは並行リクエストだな、と思っている。けど、C力が低いので誤解してるかもしれない。理解が間違ってたら教えてください。
GuzzleではHTTPのリクエスト周りを担うパッケージ(guzzle)と、非同期処理のパッケージ(promise)が分割されている。PromiseはJSにあるものと同じ感覚で使えるようになってると思う。
guzzleには getAsync
, postAsync
のような非同期リクエスト行うためのメソッドと、 get
, post
のような同期リクエストを行うメソッドがある。
Clientを見れば分かるけど、get
, post
などの同期リクエストメソッドでもPromiseが使われており、内部でwaitして同期的に処理している。
Async処理のFulfilled, Rejected
guzzleからpromiseを使うときは以下のようなコードになる。
$fulfilled = function (ResponseInterface $response) {}; $rejected = function (ClientException $exception) {}; $promise = $client->getAsync('url', $options); $promise->then($fulfilled, $rejected); $promise->wait();
実際にリクエストが発行されるのは
$promise->wait();
が実行されたときになる。JSのようにPromiseが作られた時点でリクエストして欲しいけどそれは無理で、なぜかというと
となっているから。
これはブロッキングな処理なので、実際にリクエストを送信する前にどのリクエストを同時に送信したいかを定義する必要がある。
複数のリクエストを並行で送信するには Promiseの中で定義されてる all 関数を呼ぶのが楽。
$p1 = ... // Promise
$p2 = ... // Promise
$p3 = ... // Promise
GuzzleHttp\Promise\all([$p1, $p2, $p3])->wait();
という感じでpromiseの配列を渡すとすべての処理が完了されるまで待ってくれる。 内部でEachPromiseに変換して処理される。
Promise::then
は第1引数に完了時の処理(onFulfilled)、第2引数に失敗時の処理(onRejected)を渡すことができる。
promise->resolve()
, promise->reject()
を呼ぶと叩かれる。
だけど、guzzleの場合はHTTPリクエストが成功以外(status >= 400)を返したときにデフォルトでは onRejected
は呼ばれない。代わりに、waitの中から ClientException
の例外が投げられる。
onRejected
を叩かせるためには wait
の引数にfalseを渡してwrapped promiseとして実行する必要がある。
この辺りの挙動はREADMEを読むとよい。
When synchronously waiting on a promise, you are joining the state of the promise into the current state of execution (i.e., return the value of the promise if it was fulfilled or throw an exception if it was rejected). This is called "unwrapping" the promise. Waiting on a promise will by default unwrap the promise state. You can force a promise to resolve and not unwrap the state of the promise by passing
false
to the first argument of thewait
function: ...
JSだと >= 400 のステータスが返ってくるとcatchの方に入ってくれていてguzzleでもそのノリを期待してたんですが、この挙動を知らなかったのでちっともエラーハンドリングできなくて困りました。 複数のpromiseをまとめて処理したときに途中のリクエストがエラーを返すとそのタイミングで例外が投げられるのですべてのリクエストのレスポンスをハンドリングできません。 例えば、Httpリクエストを送信するpromiseを3つ実行してそれぞれが下のようなステータスのレスポンスだったとします。 promise1: 200 promise2: 400 promise3: 200
promise1は200なのでonFulfilledが実行されますが、promise2が400なので例外が投げられ、promise3のonFulfilledは実行されません。
なので、エラーがあった場合に例外を投げずに処理を継続したい場合は wait
にfalseを渡して wrapped promiseとして実行する必要があります。
そうしないで実行すると、wait メソッド内から例外が投げられるので、そちらで例外をキャッチする必要がある、ということでした。
なお、fulfilledに渡した関数からそれぞれのレスポンスを抽出しようと思うとuseに参照渡しなどして取得する必要がありそう。
いまいち理解できていないところがちょいちょいあるけど、とりあえずそんな感じで。