🍃このブログは移転しました。
3秒後、自動的に移動します・・・。

miniflare@tre のコードを読む

`miniflare`のコードを読むシリーズもこれで3本目。

miniflare のコードを読む - console.lealog();
miniflare@next のコードを読む - console.lealog();

Overview

そもそも`@tre`ってのは、`miniflare`の`tre`ブランチにあるコードのこと。

これは現状の`wrangler`の最新バージョンであるv3系で内部的に使われてるバージョン。GitHubのデフォルトブランチは`master`のままで、こいつはv2系で使われてたもの。

v2からv3になった最大の差異は、`workerd`を内部的に使うようになって、`miniflare`のレイヤーでがんばってエミュレートしてた処理がごっそり消えたこと。(さよなら俺たちのコントリビューション)

というわけで、最新の`wrangler dev`では、`workerd`を使った`miniflare@tre`がデフォルトになったけど、

  • `--remote`をつけたときは`miniflare`を一切使ってないの?
  • そもそも`workerd`とはどういう役割分担になってる?
  • たとえばKVなんかをpersistしてるあの処理はどこで?

などなど気になったので、読んでみた。

`wrangler dev`のおさらい

まず、内部的に`workerd`を使うようになったとはいえ、`wrangler`自体が`workerd`を直接使っているわけではない。

ということで、まずは`wrangler dev`が何をやってるのかを調べておく。

リモート

`--remote`で起動したときの流れ。

この場合、ローカルではなくCloudflare WorkersのプレビューWorkerが実際にデプロイされて、そこに一時的にアクセスできるようになる。

内部的な処理としては、

  • `previewToken`(プレビューWorkerのURLや認証トークンなど)の取得
    • `wrangler.toml`をパースして、各バインディングやフラグを取得
    • Workerスクリプトのパース
    • REST APIのカタログには載ってない、`edge-preview`のエンドポイントに対してアップロード
      • Workerコードはすべて`FormData`に載せられる
    • `prewarm`なる処理のためにPOSTを1発
  • プレビューWorkerへとつながるProxyサーバーの起動
    • ローカルに立つのは`node:http(s)`のサーバー
    • このサーバーはHTTP/1だが、プレビューサーバーはHTTP/2なのでその変換
  • `http://127.0.0.1:8787`がready

って感じ。

コードはこのあたりから。

https://github.com/cloudflare/workers-sdk/blob/main/packages/wrangler/src/dev/remote.tsx#L64

実際にデプロイされるので、KVやらR2なんかも実際のCloudflareネットワークにある実際の値にアクセスできるってこと。

ローカル

`--remote`がないとき。aka `--experimental-local`だった挙動。

この場合、`workerd`が`miniflare`経由で利用される。

内部的な処理としては、

  • `miniflare`をラップしたサーバーを立てる
    • モジュールとしての`miniflare`を初期化
    • 以後、アップデートの度に`mf.setOptions(newOptions)`してリロード
  • `http://127.0.0.1:8787`がready

って感じ。

コードはこのあたりから。

https://github.com/cloudflare/workers-sdk/blob/main/packages/wrangler/src/dev/local.tsx#L131

さて、ここで立ててる`miniflare`のインスタンスが、内部的に`workerd`を抱えてるってことは予想できる。

けど、`workerd`自体にKVみたいなストレージの機能はないので、そこを`miniflare`が引き続きNode.jsでがんばって実装してるはず。
そしてその実装を`workerd`の設定がどうやってリンクしてるのか。

みたいなことを引き続き調べていく。

`miniflare@tre`

というわけで本題。

https://github.com/cloudflare/miniflare/tree/tre

根本のモジュールで`export`されてる`Miniflare`クラスから追っていく。

`Miniflare`

https://github.com/cloudflare/miniflare/blob/tre/packages/miniflare/src/index.ts#L393

コンストラクタでやってること。

  • `#initPlugins()`: KVなど各種バインディングの実装の初期化
  • `#init()`: ランタイムの初期化
    • ループバックサーバーの起動
    • ランタイムの起動
    • ランタイムに渡す設定のアップデート

ここでいうループバックサーバーは、ランタイムと同じホストに立っていて、`miniflare`側でやってるストレージの永続化まわりに一役買ってるらしい。

ランタイムは、`workerd`を抱えたクラスで、`workerd`に渡す設定は、`.capnp`フォーマットの文字列ではなく、それのバイナリ表現になってた。

`Runtime`

https://github.com/cloudflare/miniflare/blob/tre/packages/miniflare/src/runtime/index.ts#L71

ついに本丸である。

  • `#command`に、`workerd`のバイナリへのパスがある
  • `#args`がGetterになっていて、`workerd serve`を呼ぶための引数が並んでる
    • `serve --binary --experimental`
    • `--external-addr`でさっきのループバックサーバーを指定
  • `updateConfig(newConfig)`されると、新たな設定で`workerd`のプロセスが`spawn()`される

というわけで、仕組みとしてはとてもシンプルだった。

KVなど各種バインディングの指定なんかは、バイナリの`.capnp`の中に記述されていて、`updateConfig()`で反映されるってわけ。

少し横道にそれるけど、`workerd`のコードを読んでみた限り、内部的にKVのKVSやR2のBLOBストレージの実装を持っているわけではなさそうだった。
それらも結局は外部サービスのバインディングとして、`.capnp`で設定されてるだけ。

詳細はこっち。

cloudflare/workerd をセルフホスト目的で使う - console.lealog();

つまりは実装が別のどこかにあるわけで、それを`workerd`がホストしてるメインのWorkerスクリプトから呼び出してるだけ。
D1なんかにいたっては、JSで実装されたAPIを経由して、どこぞのSQLiteを呼び出しているっぽい。

https://github.com/cloudflare/workerd/blob/main/src/cloudflare/internal/d1-api.ts

というわけで、`miniflare`には引き続きがっっっっっっっつりKVSやストレージの実装が残ってるし、そのあたりの連携を設定バイナリにまとめてる処理は、それぞれのプラグイン側でやってる。

プラグイン

https://github.com/cloudflare/miniflare/blob/tre/packages/miniflare/src/plugins/index.ts

`Plugin`の実装は共通の型に沿うようになっていて、概ね以下の実装を持ってるイメージ。

  • `router`: 各プラグインネームスペースごとに、オペレーションするハンドラ
  • `gateway`: ハンドラが呼び出す実装の正体で、ストレージに直結してるやつ
  • `getBindings()`: `.capnp`で定義されたスキーマに基づくデータ構造
  • `getServices()`: `.capnp`で定義されたスキーマに基づくデータ構造

プラグインの実装も、`workerd`がホストするメインのスクリプトから見ると、それぞれ単なるWorkerとして登録されてるだけ。

  • cache
  • d1
  • do
  • kv
  • queues
  • r2

このあたり全部そう。その実態は、さっきのループバックサーバーから、各プラグインのハンドラを`fetch()`するようになってるだけ。

`workerd`がホストするメインのスクリプトはココにある。

https://github.com/cloudflare/miniflare/blob/tre/packages/miniflare/src/workers/core/entry.worker.ts#L167

さすが何度となくリライトされてるだけのことはあって、すごい正規化されてるし読みやすい・・・。勉強になる・・・。

ストレージ

https://github.com/cloudflare/miniflare/blob/tre/packages/miniflare/src/storage/storage.ts

オンメモリか、FS上で永続化するかの2通りの実装がある。

内部的には`node:fs`と、`better-sqlite3`で成り立っていて、それぞれのプラグインが設定に応じて使い分けるようになってる。

まとめ

`workerd`を使うことで実際のランタイムとのギャップは減った!という見方ができる一方で、実際にはグローバルオブジェクトの実装やらランタイムのAPIが`node:vm`から置き換わっただけって見方をすると、引き続きそれを取り巻くバインディング関連は依然としてオリジナルな実装のまま。

しかも残ってるやつらのほうが実装としてトリッキーになりやすい気がしていて、`miniflare`のコードもキレイではあるけどそれ相応に複雑なので、メンテするの大変そうやな・・・って。

ここまで一本化される日がくるとすれば、それはもうCloudflareのランタイムで実際に動いてるKVやR2やD1やDOやらの実装がローカルで動かせるようになる日ということで、「ポータブルなランタイムがついに全部入りになって、ローカルでも動くようになったぜ!」みたいな事態ってこと。

果たしてそんな日はくるのだろうか・・・。