Lambdaカクテル

京都在住Webエンジニアの日記です

Invite link for Scalaわいわいランド

http4s + sbt-revolverでリロードが遅いときはデフォルトタイムアウトを設定してください

苦しんでいる人をちらほら見たので解決策を示しておく。

tl;dr

このように、http4sでサーバを書くとき、バックエンド実装がEmber(デフォルト)の場合はwithDefaultTimeout()を使って短いタイムアウトを指定するようにしてください。

import scala.concurrent.duration._

// (略)

  def run(args: List[String]): IO[ExitCode] =
    EmberServerBuilder
      .default[IO]
      .withHost(ipv4"0.0.0.0")
      .withPort(port"8080")
      .withHttpApp(helloWorldService)
      // デフォルトだと30秒待ってしまうので速攻で終了させる。
      // 開発環境では即座に、本番環境では長めに待つといった構成をすると丁寧
      .withShutdownTimeout(Duration(1, "second"))
      // ↑↑↑↑↑↑↑↑↑
      .build
      .use(_ => IO.never)
      .as(ExitCode.Success)

サンプルコードはこちらです。

github.com

あらすじ

このような記事を読んだ。

nzzzz27.hatenablog.com

念のため説明しておくとhttp4sとはScalaのHTTPサーバ/クライアントライブラリであり、sbt-revolverはこうしたサーバの再起動・リロードを行ってくれる、開発時に便利なsbtのプラグインだ。

http4s.org

github.com

たとえば、~reStartというコマンドをsbt上で実行すると、ソースコードの変更を検知して自動的に修正部分だけコンパイルしてサーバを再起動してくれる。サーバはバックグラウンドで動作するので、その間は好きにsbtのコマンドを入力できる。かなり便利だ。

sbt:http4s-revolver-fast> ~reStart                                                                                                                                                                                                             
[info] Application root not yet started                                                                                
[info] Starting application root in the background ...
root Starting Main.main()
[success] Total time: 4 s, completed 2023/06/17 0:23:51
[info] 1. Monitoring source files for root/reStart...
[info]    Press <enter> to interrupt or '?' for more options.
[info] Build triggered by /home/windymelt/src/github.com/windymelt/http4s-revolver-fast/src/main/scala/Main.scala. Running 'reStart'.
[info] compiling 1 Scala source to /home/windymelt/src/github.com/windymelt/http4s-revolver-fast/target/scala-3.3.0/classes ...
[info] Stopping application root (by killing the forked JVM) ...
[info] Starting application root in the background ...

ちなみに~はソースコードの監視を行うためのsbt標準のコマンドであり、reStartはサーバを再起動させるだけのコマンドである。これらを組み合わせることで、ソースコードを監視して適宜再起動するという動作になるのだ。すごいね。

そんな中、追試をしたところ同様に再起動が遅いという報告を発見した。

簡単なプログラムなのでそんなに時間がかかるのはおかしいし、複数人で同じような状況になっているのは奇妙だと考えたため、自分で原因を探ってみることにした。

サーバの再起動が遅い

結論から言うとサーバの再起動が遅い。以前自分はサーバがなかなかシャットダウンしなくて困る、というトラブルに遭遇しており、その時にはサーバのデフォルト設定とブラウザの挙動の問題が噛み合ってシャットダウンが遅くなっていたことを突き止めていた。

blog.3qe.us

自分も簡単なサーバを書いてsbt-revolver~reStartを試してみたが、確かに30秒ほど待たされる結果となった。

そこで、デフォルトタイムアウトが長くてシャットダウンが遅れているのではないかと仮説を立て、タイムアウトを設定してみたところ、すぐに再起動するようになった:

  def run(args: List[String]): IO[ExitCode] =
    EmberServerBuilder
      .default[IO]
      .withHost(ipv4"0.0.0.0")
      .withPort(port"8080")
      .withHttpApp(helloWorldService)
      .withShutdownTimeout(Duration(1, "second")) // ここ!!!!!!!!!!!!!!!!!!!!!!!!!
      .build
      .use(_ => IO.never)
      .as(ExitCode.Success)

処理に時間がかかるリクエストを実行中ではないはずなのに、なぜシャットダウンしないのか?その理由はKeep-aliveにある。

多くのブラウザは、あるページに接続した際にTCP接続をKeep-aliveして使い回そうとする。最近のサイトはAPIやリソースの取得のために同一のサーバに何度もリクエストを行うことが多く、リクエストのたびにTCP 3-way handshakeHTTPSのhandshakeを行うのではオーバーヘッドが大きい*1からだ。

そういうわけでブラウザはタブを開いている間中コネクションを開けっぱなしにしているから、この場合サーバからは接続が切れていないように見えてしまい(見えてしまいというか、切れていないのだ)、そのタブが閉じられるまでずっとタイムアウト待ちになるのだ。

developer.mozilla.org

この証左として、すぐに接続を閉じるcurlでリクエストを行ったり、一度もブラウザでそのページを開かずにリロードさせると瞬時に切り替わる。

対応

以下のような対応をすることで、http4sで快適に開発できるようになるだろう:

  • 本番環境では長めの、開発環境ではとても短めのshutdownTimeoutを指定する
  • 他のサーババックエンドを利用する(シャットダウンタイムアウトが速いかどうかはちゃんと確認する必要がある)
  • Keep-Aliveしないようにブラウザに指示する
    • ヘッダーで調整可能?

*1:サーバが遠距離にある場合、往復にとんでもない時間が積み重なる

★記事をRTしてもらえると喜びます
Webアプリケーション開発関連の記事を投稿しています.読者になってみませんか?