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 を入れると TAddRequest で束縛されるので、 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-webgoogle-protobuf dependenciesに追加する必要がある。

grpc-web - npm

google-protobuf - npm

あとは生成したファイルを素直に使い、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だった。

Basics Tutorial – gRPC

この辺りを参考に動かそうとしたら設定内容が微妙に古いようなので、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

関係ないけど、普段yamlvimで触ることが多いもののインデントがよく崩れるので以下の設定を入れたら快適になった。

autocmd FileType yaml setlocal ts=2 sts=2 sw=2 expandtab

構築したgRPCサーバをsbt-native-packagerでdockerイメージを作り、 scala-grpcという名前で動かすものの全然うまく動かなくてドハマリした。

エラーハンドリングをしていないせいで処理中に例外が発生すると INTERNAL というエラーしか吐かれず、何が問題なのか分からなすぎて解決にやたらと時間がかかった。ちゃんとエラーをログに吐かないと辛いことになる。

ハマったエラーは、今回H2を使っているもののdockerイメージを作るタイミングでDDLファイルが同梱されず、テーブルが正常に生成されていないためにDBアクセスすると例外が吐かれている、というものだった。読み込むファイルパスを無理やり変えてマウントしてなんとか動かすことができた。

gRPCの疎通確認は

narinymous.hatenablog.com

こちらのブログで紹介されてた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.sqlDDLを書いたり、初期値用のINSERT文を書いといたら、なぜかINSERT文が2回発行されてて重複したレコードができて謎だった。 この辺どこからどう実行されてるのか調べるのが骨が折れてもう… PHPならブレークポイント貼ってIDE上から該当プロセスを実行してしまえばある程度追えるのだけど、JavaScalaはそういうことがしづらくて困るなー。

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に依存してるのかな?

何も知識が無いのでぐぐってみると、

stackoverflow.com

というStack Overflowがあったり、入門者は困るんじゃないかなという空気を感じた。 というかまずPlayで入門していろいろ学んだ方が効率良いんじゃないのか…?

ともあれ、Stack Overflowの中でもslf4jがデファクトっぽいのでとりあえずこれの使い方を見ていく。

slf4sもあるようだけど GitHub - w11k/slf4s: Simple Scala facade for SLF4J

とりあえずすでにログも出てるし4jで進めていく。

SLF4J

SLF4J Manual

SLFってなんぞと思っていたけど、 Simple Log Facade なのね。

さて見ていくかと思ったのだけど、LightbendがScala Loggingというのを出していた。SLF4Jベースらしいし、Akkaを使ってるしLightbendに依存していこうということでこっちで見ていく。

github.com

Scala Logging

logbackscala loggingをlibraryDependenciesに追加して

com.typesafe.scalalogging.Logger("name").info(message)

もうこれだけでログが出ますね。かんたーん。

Logback

とりあえず使う分にはこれでいいのだけど、実際に調べたかったことはLogback. logback.xmlでなにかしらを設定すると何か変わる、ということは知ってるけど実際に何をどう設定するのか全然知らないので見てみる。

logback-test.xml, logback.groovy, logback.xml の順で読まれるらしい。classpath内に直接置いとけば勝手に読んでくれるよ、ということらしいけど、classpathとはどれのことを指してるんだろう…

Setting the class path

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番目でギリギリ本選は出れないところでしたが

motemen.hatenablog.com

この方々の誠実さのおかげで本選に出れることになりました。

繰り上がりで出ることになり、この方々の分も、とか、ワンチャン敗者復活からの優勝を果たしてサンドウィッチマンみたいに…!とか思ってました。

最終スコアは 13,016 で、まさかの4位!あと1000点で賞金圏内の3位だったって思うと少し悔しいですが、大健闘できました。ちなみに去年の本選は24位くらいで、得点が出たチームの中での下から2番目くらいでした。そのリベンジが果たせたことが個人的に本当に嬉しいです

isucon.net

チーム構成

言語はPHPで、メンバー構成は

  • アプリ 2人
  • インフラ 1人

サーバー構成は最終的に a: web c: db

bは使えなかった、という感じだった気がします。

何をしたかの記録

当日の記憶が怪しいですが。リポジトリはこちら

GitHub - shmurakami/isucon9_final

去年の本選の反省から、まずコードを読んで仕様を理解する ということから始めます。去年の本選はSNS告知(だったかな?)のフラグに最後まで気付かないという失態を犯していたので、とりあえずコード読まないと話にならんという学びがありました。

コードを読みながら、どうせ使うだろうということでRedisの導入をしたり、NewRelicの導入をしてもらったりしてました。

今回の問題はデフォルトで貼られているインデックスがほとんど無く、初期状態ではベンチマークが回らない前提だったのでベンチを回す前にクエリを見て最低限のインデックスを貼っていきます。

最低限のインデックスを貼ってベンチを回してもずっとFailしている状態が続き、途中で数値しか入らない train_name のカラムがvarcharなことに腹を立ててint に変えたりしましたが、ベンチが通らないので日和って戻したりしてた気がします。

ベンチマークのエラーメッセージが何を言ってるか分からずベンチのバグなのかこちらのバグなのかの切り分けのために運営に聞いてみたりしました。

f:id:shmurakami:20191010133935p:plain

仕様だということで理解。 何が不正なのかが分からずデバッグのしようも無いのでなかなか苦しかった。

後で分かりましたが、ベンチが通らなかった理由は

元々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が作られた時点でリクエストして欲しいけどそれは無理で、なぜかというと

非同期に処理してるというよりはcurlリクエストのまとまりを裏側で順番に実行して結果をまとめて返してる

となっているから。

これはブロッキングな処理なので、実際にリクエストを送信する前にどのリクエストを同時に送信したいかを定義する必要がある。

複数のリクエストを並行で送信するには 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を読むとよい。

github.com

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 the wait 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に参照渡しなどして取得する必要がありそう。

いまいち理解できていないところがちょいちょいあるけど、とりあえずそんな感じで。