Message Passing #12 で OkHttp がどうやって HTTP/2 の上にブロッキング API を作っているのか、という疑問が書いてあった。興味が出たので Android も Kotlin も分からないけれど少し調べてみた。
最初はコードを読んでいたのだけど途中でしっかりした説明があることに気づいた。ここに知りたいことは書いてあった。
基本はもりたさんが想像した通りのようだ。ソケットからの読み込みには専用のスレッドを使う。この Read 用のスレッドはアプリケーションのコードを実行しない。アプリケーションのコードはどこでブロックするか分からないから。フレームを Read した後は、アプリケーションレイヤのコードを走らせたり、Ping に対する応答などをしなきゃいけない。Read 用の専用スレッドではこれらの処理を扱えないので Runnable をキューに突っ込んで executor threads に実行してもらうようにする。
ブロッキング API は Http2Stream と Http2Writer によって実装されている。Http2Stream は wait/notify を使ってソケットから対応する HTTP/2 stream のデータが届くまでブロックする。 Http2Writer は単純にソケットに書けるまでブロックする。
凝ったことはしていなくて素直な実装であった。
最近の趣味プロジェクトとして HTTP/2 サーバを自作している。
自作 HTTP/2 サーバも OkHttp と似たアプローチを取っている: ソケットからの読み込みは専用のタスク (tokio を使っているのでスレッドではなくタスク)を使う。このタスクは HTTP/2 フレーム単位での読み込み処理のみを行う。読み込んだフレームは他のタスクへ channel 経由で渡され、ファイル IO やソケットへの書き込みは別のタスクが行う。
この自作 HTTP/2 サーバ、単純なリクエスト (navigation + subresources) をさばけるぐらいまでは動いているのだけど、Flow Control にバグがあるらしく、たまに Stall してしまう。この週末はそのバグを直そうとしていた。いくつか問題を見つけて修正したがまだ Stall するケースが残っている。
OkHttp のドキュメントには「Write が Read をブロックしてはいけない、さもなくばデッドロックが生じるかもしれない」との注意書きがある。自作 HTTP/2 サーバがデッドロックするのはこの状況に陥っているからかもしれない、と思い始めた。今の実装は TCP バッファがあふれてて Write がブロックする状況を考慮していない。
こんな風に息抜きのつもりのちょっとした寄り道がヒントをくれたりするのは楽しい。