裁断スキャンした投資の参考書(214ページ)を /yomitoku でOCRしてMarkdownに起こし、Turso(libSQL/SQLite互換のクラウドDB)へ取り込んだ。ページ単位の61チャンクをセクション単位の47チャンクに統合するスクリプトを走らせたら、出力を一行も吐かないまま固まった。240秒待っても画面は無言。原因はEmbedded ReplicaのWALロックだった。ハングしたプロセスをkillし、HTTP直接接続版に書き換えたら一発で通った。しかも調べ直したら、統合自体は最初のスクリプトで既に終わっていた。
やったこと(時系列)
ある著名投資家の財務分析書を裁断してスキャンした、約39MB・214ページのPDFがある。これを蔵書ナレッジベースに取り込むのが今日の作業だった。
/yomitokuコマンドで214ページ全てをMarkdown化。本文中の図を60ファイル抽出し、連番でリネームした- OCRしたテキストをTursoに61チャンクとして格納。既存の
amazon_metadataテーブルと紐付けて、書誌情報とコンテンツが1冊ぶん揃った - 続けて
/restructure-bookでページ単位のチャンクをセクション単位に統合(61→47チャンク)。OCRが拾った見出しはノイズだらけで、"35" や "38" は章番号ではなく装飾記号の誤読だった。章名を一つずつ正規化した
ここまでは順調だった。詰まったのはセクション統合のスクリプトを実行した瞬間だ。
出力ゼロのままハングした
統合スクリプトを走らせると、ターミナルが沈黙したまま動かなくなった。sleep で待とうとしたらブロックされたので、バックグラウンドの待機ループで監視に切り替えた。それでも240秒、出力は一行も出てこない。プログレスバーもエラーもない。ただ無言で固まっている。
「なかなか遅いですね。なんでだろう」と首をひねった。OCRもDB格納もここまでサクサク終わっていたのに、最後のセクション統合だけが完全に止まっている。これは遅いのではなく、何かを待ち続けている挙動だ。
原因はEmbedded ReplicaのWALロックだった
心当たりがあった。CLAUDE.mdに既知問題として書いてあるやつだ。Embedded ReplicaのWALロックである。
Embedded Replicaはローカルにレプリカファイルを持ち、そこへ書いてからクラウドへ同期する方式だ。先行プロセスが開いた接続のWALロックがレプリカファイルに残っていると、後から起動したプロセスの BEGIN がロック取得待ちで固まる。タイムアウトもエラーも出ず、ただ待ち続ける。240秒の沈黙はこれだった。
ハングしていたのはuvが起動したPythonプロセス群で、親子合わせて9個ぶら下がっていた。これを全部killした。Claude Codeのnodeプロセスには指一本触れないよう、Pythonのプロセスツリーだけを狙って落とした。
HTTP直接接続版に書き換えて救出した
ロックの巣窟であるレプリカファイルを使うのをやめて、Tursoクラウドへ直接書くHTTP直接接続版にスクリプトを書き換えてもらった。レプリカファイルを経由しないので、WALロックの待ち合わせが発生しない。
# Embedded Replica(ローカルレプリカ経由 → WALロックでハング)
conn = libsql.connect("local.db", sync_url=TURSO_URL, auth_token=TOKEN)
# HTTP直接接続(クラウドへ直接 → ロック待ちなし)
conn = libsql.connect(database=TURSO_URL, auth_token=TOKEN)
書き換えた版を走らせる前に、現状を確認しておこうとHTTP接続でチャンク数を数えた。ここで予想外の事実にぶつかった。
kill した時点で、統合は既に終わっていた
数えたら、チャンクは既に47になっていた。統合は完全に成功していたのだ。
最初のEmbedded Replica版スクリプトは、ハングして何もできていなかったわけではなかった。UPDATE/DELETEを実行し、トランザクションをcommitまで完了させていた。固まっていたのはその後、最後に呼ぶ sync()(レプリカの変更をクラウドへ同期する処理)だった。commitした時点でクラウド側のデータはもう書き換わっていて、killしても確定済みの変更は消えなかった。
つまり「ハングして失敗した」と思い込んでいたが、実際には「処理は終わっていて、後始末の sync() だけが固まっていた」が正解だった。FTSのリビルドと検索テストもHTTP接続で流して、47チャンクが正しく引けることを確認した。
再発防止:sync() を全部抜いた
同じ罠を踏まないよう、restructure-book のスラッシュコマンド定義テンプレート(.claude/commands/restructure-book.md)のStep 4を直した。
- DB書き込みパートをHTTP直接接続に統一
- ハングの元凶だった
sync()の呼び出しを、テンプレート内の7箇所すべて削除
これで次に /restructure-book を実行したときは、最初からレプリカを経由せずクラウドへ直接書く。sync() 待ちで固まる経路自体が消えた。
WALファイルが18.9MBに肥大化していた
ハングの巻き添えで、ローカルのWALファイルが18.9MBまで膨らんでいた。CLAUDE.mdの手順どおり、レプリカファイルを退避した。データの本体はクラウドにあるので、ローカルのレプリカを退かすのは非破壊的な操作だ。次に接続したとき、レプリカは自動で再構築される。
レプリカ再接続で動作確認した
「レプリカに接続してちゃんと動いているか見て」と念押しされた。退避したあとEmbedded Replicaで繋ぎ直すと、レプリカがゼロから組み上がり、9.2秒で全テストが完走した。蔵書58冊ぶんがフル再構築され、検索も問題なく返ってくる。HTTP直接接続で逃げただけで終わらせず、本来のEmbedded Replica経路でも健全に動くところまで見届けられた。
学びメモ
- ハング=失敗とは限らない。 commitが通っていれば、その後の
sync()で固まってkillしても、確定済みの変更はクラウドに残る。まず「どこまで終わっているか」をHTTP接続で数えてから、やり直すか判断する - Embedded Replicaの便利さには、WALロックという裏面がある。 先行接続のロックが残ると、後続の
BEGINが無言で待ち続ける。書き込みバッチを連続で回す処理は、HTTP直接接続のほうが事故らない - プロセスをkillするときは木を見て狙う。 uvが起こすPythonの親子9プロセスだけを落とし、Claude Codeのnodeには触れない。名前で一括killすると自分の足を撃つ
- 既知問題はCLAUDE.mdに書いておくと、240秒固まった瞬間に「あれだ」と当たりがつく。今日それに救われた