競合は観測では閉じない。順序づけでしか閉じない
peithoのPDF書き出しは、Chromeの --print-to-pdf でページを印刷する。ところがページ側には非同期の後処理(グラデーションとbox-shadowをラスタライズする「平坦化」)が走っていて、Chromeはそれの完了を待たない。Chromeが「読み込み終わった」と判断した時点で印刷する。平坦化が間に合わなければ、壊れたPDFが黙って出る。
修正の試みが二段階あったのが要点になっている。
一段目 — 観測
「平坦化が走らなかった」ことをstderrの合図で検出する。
しかしこれでは「走ったが競合に負けた」を捕まえられない。合図が届くころにはPDFのバイト列はもうディスク上にある。
二段目 — 観測
出力されたPDFを検査して、平坦化の痕跡が無ければ失敗とする。
これも駄目だった。平坦化器は意図的に処理を飛ばす図形があり(痕跡が残らないのが正常)、SVGの内側は見えない。痕跡の有無から「平坦化が済んだか」を判定することは原理的にできない。
三段目 — 順序づけ
印刷を自分で駆動し、平坦化の完了を確認してから印刷を指示する。
競合そのものが存在しなくなる。
The only shape that closes the gap is one that orders rather than observes.
—docs/plans/2026-08-05-cdp-ordered-pdf-print.md
採った形
--print-to-pdf を捨て、CDP(Chrome DevTools Protocol)で印刷を自分で駆動する。
--remote-debugging-port=0でChromeを起動する(印刷の指示はまだ出さない)Runtime.evaluateでdata-peitho-pdf-flattened属性が現れるまで待つ- 現れてから
Page.printToPDFを呼ぶ
属性は平坦化器の成功経路と最上位catchの両方で立つので、スクリプトが動きさえすれば必ず現れる。現れなければ期限切れで大きな音を立てて失敗する。そして--print-to-pdf への退避経路は用意しない — 静かな退避は競合を呼び戻すからである。
一般化
「AがBより先に終わっていること」を保証したいとき、取れる形は二つしかない。
| 形 | 保証の強さ | |
|---|---|---|
| 観測 | Bを走らせ、Aの完了の痕跡を後から探す | 痕跡とAの完了が同値のときだけ成立 |
| 順序づけ | Aの完了を確認してからBを起動する | 構成上、競合が存在しない |
観測が成り立つ条件は思ったより厳しい。痕跡の不在が「やっていない」と同値でなければならないが、ここでは「やったが対象が無かった」も痕跡ゼロを生む。この二つが区別できない時点で、観測は判定器になりえない。
そしてこれは、typestateで壊れた状態を書けなくするが型について言っていることの、時間についての版である。あちらは「壊れた状態を表す値を構成できなくする」。こちらは「壊れた順序を実行できなくする」。どちらも、後から検査するのではなく、そもそも到達不能にしている。
静かな失敗であることも同じ根
この不具合が二度も対処を要したのは、失敗が静かだったからでもある。壊れたPDFは開けるし、見た目も一見おかしくない。黙って捨てるのは、落とすことより悪いと同じ構図で、損失の小ささと発覚の遅さが釣り合っていない。
同じリポジトリに、これと双子の実測がある。Linuxのheadless Chromeで image.decode() が解決も拒否もせず永遠に止まるという事象で、拒否されないので .catch() が発火せず、止まったこと自体が静かだった。ここでも手当ては同じ方向 — 期限を切って大きな音で失敗させる、退避経路は作らない。