ワイヤの向こう側になる
言語が走らせてきたネットワークプログラムは全て client だった —— 外へ dial し、話し、切る。5 本目の能力ツールはその鏡像: Redis wire protocol を話す key-value サーバ、接続を作る側でなく待つ側。欠けていた primitive はいつもどおり一午後で済んだ。サーバが本当に届けたのは、言語の並行性への最初の正直な負荷試験 —— そして可変 Map の thread 間共有が綺麗にコンパイルされ、静かに書き込みを失うという発見だった。
この Part は、プログラムが終わることを拒むと何が起きるか、についてだ。これまで建てた全て —— コンパイラも、ゲームも、inspector も、filter も —— は走り、終わり、プロセスと共にメモリを OS へ 返す。サーバにその贅沢はない。待ち、応じ、待ち続ける。そして言語が静かに浪費し、静かに壊している ものは、不可視であることをやめて請求書になり始める。この後の数話でその請求書は何度か届き、その 一枚一枚が、言語がこれまで見逃されてきた何かを暴く。最初の配達は予想外の方向から来た: メモリでは なく、thread についての型システムの約束からだ。
サーバまで primitive 一つ
ツールは Redis wire protocol を話す小さな key-value サーバで、市販の client がそのまま話しかけ られる。実測した gap は今や見慣れた形をしていた: runtime には TCP client の語彙が完備 —— connect・read・write・close、全て素の file descriptor 上、database client 群のためにずっと前に 育ったもの —— に加え、request を読み response を書いて切る HTTP 専用のサーバ helper が一つ。 生の接続を accept して開いたままにできるものは何もない。修正は履き慣れたパターンに従った: 新しい primitive を二つ、listen と accept、connect と同じ descriptor + buffer の FFI に対して 鏡映しに、各々数十行。これまでの全ての能力回と同じく、名指しされた gap は浅い方だった。 単一接続の echo サーバは同じ午後に動き、プロトコル処理 —— コマンドフレームを parse し、適用し、 reply を encode し、切らずに繰り返す —— は、平凡なコードにとうに慣れた言語での平凡なコード だった。
失敗するべきだったコンパイル
次が並行性だった。多くの client に応じるとは多くの thread が一つの store に手を伸ばすことで、 自明な初稿は自明なことをした: 接続ごとに thread を spawn し、全員に共有の可変 Map を触らせる。 コンパイラはそれを受理した。言語が Send/Sync 流の規律を持つのは、まさにこの種のプログラムが 安全であるか拒否されるかのどちらかであるためだ —— region に束ねられた値は thread-local である はずで、設計文書もそう言っていた。だが負荷試験 —— 8 client が各々 key を書き、即座に読み返す —— の下で、読みの約 2 パーセントが空で返ってきた。SET は acknowledge され、GET は無を返す。store の runtime は、競走する thread たちに伸ばされ index されるロックなしの配列だ。Map は検査器の祝福を 受けて thread 境界を越え、そしてまさにデータレースとして振る舞った。なぜは一つの楽観的な default に行き着いた: 分類器は container を型引数で判定し、container の region marker は未解決の 型変数で、未解決は「大丈夫とみなす」を意味していた。このバグの修正 —— そして追跡が暴いたもう 一つの、より大きな穴 —— はこの Part の後の回に譲る。正直な順序が大事だからだ: まずサーバが回避し、 それから言語が修理された。
共有ではなく通信で
回避策はロックではなかった。言語の channel は唯一の本当に thread-safe な primitive なので、
サーバは actor に再構成された: owner thread が一人で Map を持ち、それに触る唯一の thread になる。
各接続 thread はコマンドを parse し、私有の reply channel を添えて、共有の request channel へ送る。
store は thread 境界を一切越えず、だから競走するものも、ロックすべきものも無い。同じ負荷試験の
下で actor 版は数千の書き込みを一つも失わなかった。これは channel 機構に置かれた最初の実負荷でも
あった —— これまでテストとおもちゃにしか現れなかったものが、飛行中の全コマンドで channel を二跳び
する —— そして数字は持ちこたえた。このパターンは codebase の語彙に名前を得た: share by
communicating —— 可変構造を spawn 越しに手渡すのではなく。後に型システムが naive 版を拒否する
ことを学んだとき、このサーバは —— 無変更のまま —— 祝福されたパターンの回帰テストになった。
請求書が届く
actor サーバは動き、再利用バッファプールのおかげで数百の逐次接続を生き延び、実物の Redis client に
正しく応えた。それから負荷試験を少しだけ長く回すと、およそ 8 千コマンドでプロセスが死んだ ——
サーバのロジックのバグではなく、region OOM で。言語の program-lifetime のメモリ region、全ての
reply 文字列と parse 済みコマンドが静かに着地する場所には、固定の容量があり、何かを返すという
概念が無かった。短命のツールはこれに決して気づかない。サーバは長い走行そのものだ。この失敗 ——
それが成長と回収について暴いたもの、そして同じこのサーバが数十万コマンドの下でフラットなメモリを
保つに至る二段階の修理 —— が、この Part の残りが語る物語だ。