【Poteto】自作Webフレームワークにホットリロードを実装した話
お正月はどうお過ごしでしたでしょうか?私は自作Webフレームワークの開発をしながらGo正月を過ごしました。
これまでの開発の記録として、前回の記事を貼っておきます。ぜひ興味があればご覧ください。
ホットリロード
ホットリロードとは
ホットリロードは、ファイルシステムに変更があると、アプリケーションサーバーのプロセスをkillしてもう一度立ち上げなおす仕組みです。開発中に、「ファイル変更→プロセスkill→立ち上げ」のステップを踏むことなく、スムーズに開発できる点が魅力です。
なぜホットリロード
- 単純に仕組みに興味があった
- 参考にしているgolangのフレームワークであるEchoには実装されていない(はず?)で差別化できると思った
ホットリロードの仕組み
開発のプロセスとしてはまず仕組みを調べることから始めました。こちらの記事を参考にしました。
以下は、私がつたない絵で図に表したものです。
以下の順序でホットリロードが達成されます。
- FileWatcherがFileSystemを監視
- FileWatcherがファイルの変更を検知したシグナルを送る
- シグナルによってBuildRunnerがAppServerのプロセスをkillして再スタート
- アプリサーバーからのログをLogTransporterがAgentServerに転送
ホットリロードの実装
cliツール上に実装していきました(整備があんま整ってなくて穴だらけ、、、)。
こっからは結構長いです。
FileWatcher
FileSystemの監視には以下のライブラリを使っています。
簡単に言えば、ファイル変更を検知した際に、FileChangeStream
チャネルに入力を行います。
工夫した点でいうと、最初はファイル変更の検知を無限ループで回していましたが、1回のファイル変更を2-3回検知してしまうという問題があったので、タイマーを設けて1ms毎にイベントを検知するようにしました。
BuildRunner
BuildRunner
はまず最初に一度AppServerを起動し、FileChangeStream
の入力に対して、アプリの再ビルドを行います
AppSeverプロセスのKill
AppServer
のプロセスIDを保存しておいて、それ自身や、子プロセスに対してSIGTERM
を送信します。
ゾンビプロセスが残ってしまったりと、意外とここがHotReloadの実装で一番苦労しました。以下を参考にしたものが、上手く動作しました。恐らく子プロセスが残ると、ゾンビ化されたプロセスが回収されきらないのだと理解しました。詳しい方捕捉ください。
またWindowsではsyscall.Kill
が使えないので、現状これが使えるOSでないと動作しません
AppServerのBuild
以前のプロセスが残っていれば、killし、設定ファイルからbuildScript(Goのファイル)パスを取得して、go run <script.go>
をします。立ち上げたプロセスのPIDは控えておきます。また、ログをAgentServerに流すために、logStreamを保存しておきます。
プロセスkillもするので、ReBuild
とかの方が良かったかもなあ、と少し後悔しています。。。
LogTransporter
上記までで、基本的なhot-reloadの仕組みは完成しました。ここからはAppServerで流したログをAgentServer(ターミナル)で流す実装になります。実際にはこちらを最初に実装しました。上手くいっているか分からないので。。。
AppServerをBuildした際に保存したLogStreamからログを一行ずつ取得して、AgentServerに流します。
詰まったポイントは、AppServerの再ビルドの際に、LogStreamが更新されても一度立ち上げたLogTranporter
内からはそれを取得できず、再ビルドしたアプリのログが流れなかった点です。
// re-watch log stream watcher
case <-fileChangeStream:
return nil
そのため、再ビルド時に、LogTransporter
を再実行するように、一旦処理を終了しています。これが何故上手くいかなかったのか、まったく分かっていないです。もし分かる方いましたら、教えていただけるとありがたいです。
AgentServer
RunRun
とかいうクソダサい関数名なのは、触れないでください笑。
長いですが、処理自体は単純です。これまでに紹介したFileWatcher
とBuildRunner
、LogTranporter
を並列に立ち上げているだけです。工夫した点としては、AgentServerを停止した際にもAppServerを閉じるようにした点です。
// Ctrl+Cで子プロセスをkillする
quit := make(chan os.Signal)
signal.Notify(quit, os.Interrupt)
case <-quit:
return runnerClient.Close()
作成したCLIツールの使い方
まずはインストール
go install github.com/poteto-go/poteto-cli/cmd/poteto-cli@latest
例えば以下みたいな感じでpoteto.yaml
を作ります。
app
┣ poteto.yaml
┣ main.go
┗ ...
ちなみに一応おいてるだけでversionは意味ないです。文字列ならなんでも動く(1つしかversionないので、、、)。その辺整備不足で、poteto-cli new
は存在しないversionを出力します、、、
version: "1.0"
build_script_path: "main.go"
debug_mode: true
この状態で、以下のコマンドを入力します。
poteto-cli run
これでホットリロードでアプリが立ち上がります。
終わりと今後
ひとまずWebフレームワークを作り始めたときに掲げていた目標は達成できたので、これからはしばらくドキュメント整備とバグ修正を行っていこうと思っています。また、徐々にPoteto
を使ったアプリも作っているので、そっちもゆっくり進めたいですね。
記事活動においては、JSONRPCAdapter
の差分についてまだ書いていないので、時間あるときにその話をまとめるかもしれません。
ここまでお付き合いありがとうございました。読んでくれた方含め自分も2025年良い年にしましょう、!!
Discussion