[{"data":1,"prerenderedAt":360},["ShallowReactive",2],{"content-/book-ocr-turso-wal-lock-recovery":3,"all-pages-for-dir":358,"og-image-/book-ocr-turso-wal-lock-recovery":359},{"id":4,"title":5,"body":6,"category":342,"description":343,"extension":344,"meta":345,"navigation":184,"ogImage":346,"path":347,"project_name":348,"published":349,"publishedAt":350,"seo":351,"stem":352,"tags":353,"todo":346,"unpublished":349,"updatedAt":346,"__hash__":357},"pages/2026-05/2026-05-25/book-ocr-turso-wal-lock-recovery.md","裁断スキャンした参考書をOCRしてTursoに取り込む：WALロックでハングしたスクリプトをHTTP直接接続で救出するまで",{"type":7,"value":8,"toc":331},"minimark",[9,18,23,26,49,52,55,62,65,69,77,84,91,95,102,224,227,231,234,241,247,251,262,273,282,286,289,292,295,298,327],[10,11,12,13,17],"p",{},"裁断スキャンした投資の参考書（214ページ）を ",[14,15,16],"code",{},"/yomitoku"," でOCRしてMarkdownに起こし、Turso（libSQL/SQLite互換のクラウドDB）へ取り込んだ。ページ単位の61チャンクをセクション単位の47チャンクに統合するスクリプトを走らせたら、出力を一行も吐かないまま固まった。240秒待っても画面は無言。原因はEmbedded ReplicaのWALロックだった。ハングしたプロセスをkillし、HTTP直接接続版に書き換えたら一発で通った。しかも調べ直したら、統合自体は最初のスクリプトで既に終わっていた。",[19,20,22],"h2",{"id":21},"やったこと時系列","やったこと（時系列）",[10,24,25],{},"ある著名投資家の財務分析書を裁断してスキャンした、約39MB・214ページのPDFがある。これを蔵書ナレッジベースに取り込むのが今日の作業だった。",[27,28,29,35,42],"ul",{},[30,31,32,34],"li",{},[14,33,16],{}," コマンドで214ページ全てをMarkdown化。本文中の図を60ファイル抽出し、連番でリネームした",[30,36,37,38,41],{},"OCRしたテキストをTursoに61チャンクとして格納。既存の ",[14,39,40],{},"amazon_metadata"," テーブルと紐付けて、書誌情報とコンテンツが1冊ぶん揃った",[30,43,44,45,48],{},"続けて ",[14,46,47],{},"/restructure-book"," でページ単位のチャンクをセクション単位に統合（61→47チャンク）。OCRが拾った見出しはノイズだらけで、\"35\" や \"38\" は章番号ではなく装飾記号の誤読だった。章名を一つずつ正規化した",[10,50,51],{},"ここまでは順調だった。詰まったのはセクション統合のスクリプトを実行した瞬間だ。",[19,53,54],{"id":54},"出力ゼロのままハングした",[10,56,57,58,61],{},"統合スクリプトを走らせると、ターミナルが沈黙したまま動かなくなった。",[14,59,60],{},"sleep"," で待とうとしたらブロックされたので、バックグラウンドの待機ループで監視に切り替えた。それでも240秒、出力は一行も出てこない。プログレスバーもエラーもない。ただ無言で固まっている。",[10,63,64],{},"「なかなか遅いですね。なんでだろう」と首をひねった。OCRもDB格納もここまでサクサク終わっていたのに、最後のセクション統合だけが完全に止まっている。これは遅いのではなく、何かを待ち続けている挙動だ。",[19,66,68],{"id":67},"原因はembedded-replicaのwalロックだった","原因はEmbedded ReplicaのWALロックだった",[10,70,71,72,76],{},"心当たりがあった。CLAUDE.mdに既知問題として書いてあるやつだ。",[73,74,75],"strong",{},"Embedded ReplicaのWALロック","である。",[10,78,79,80,83],{},"Embedded Replicaはローカルにレプリカファイルを持ち、そこへ書いてからクラウドへ同期する方式だ。先行プロセスが開いた接続のWALロックがレプリカファイルに残っていると、後から起動したプロセスの ",[14,81,82],{},"BEGIN"," がロック取得待ちで固まる。タイムアウトもエラーも出ず、ただ待ち続ける。240秒の沈黙はこれだった。",[10,85,86,87,90],{},"ハングしていたのはuvが起動したPythonプロセス群で、親子合わせて9個ぶら下がっていた。これを全部killした。",[73,88,89],{},"Claude Codeのnodeプロセスには指一本触れない","よう、Pythonのプロセスツリーだけを狙って落とした。",[19,92,94],{"id":93},"http直接接続版に書き換えて救出した","HTTP直接接続版に書き換えて救出した",[10,96,97,98,101],{},"ロックの巣窟であるレプリカファイルを使うのをやめて、Tursoクラウドへ直接書く",[73,99,100],{},"HTTP直接接続版","にスクリプトを書き換えてもらった。レプリカファイルを経由しないので、WALロックの待ち合わせが発生しない。",[103,104,109],"pre",{"className":105,"code":106,"language":107,"meta":108,"style":108},"language-python shiki shiki-themes vitesse-light vitesse-light","# Embedded Replica（ローカルレプリカ経由 → WALロックでハング）\nconn = libsql.connect(\"local.db\", sync_url=TURSO_URL, auth_token=TOKEN)\n\n# HTTP直接接続（クラウドへ直接 → ロック待ちなし）\nconn = libsql.connect(database=TURSO_URL, auth_token=TOKEN)\n","python","",[14,110,111,120,179,186,192],{"__ignoreMap":108},[112,113,116],"span",{"class":114,"line":115},"line",1,[112,117,119],{"class":118},"sxvE3","# Embedded Replica（ローカルレプリカ経由 → WALロックでハング）\n",[112,121,123,127,131,134,137,140,143,147,151,153,156,160,162,166,168,171,173,176],{"class":114,"line":122},2,[112,124,126],{"class":125},"sG7-3","conn ",[112,128,130],{"class":129},"shFtX","=",[112,132,133],{"class":125}," libsql",[112,135,136],{"class":129},".",[112,138,139],{"class":125},"connect",[112,141,142],{"class":129},"(",[112,144,146],{"class":145},"sMJiu","\"",[112,148,150],{"class":149},"sdGka","local.db",[112,152,146],{"class":145},[112,154,155],{"class":129},",",[112,157,159],{"class":158},"s4oTP"," sync_url",[112,161,130],{"class":129},[112,163,165],{"class":164},"snbK4","TURSO_URL",[112,167,155],{"class":129},[112,169,170],{"class":158}," auth_token",[112,172,130],{"class":129},[112,174,175],{"class":164},"TOKEN",[112,177,178],{"class":129},")\n",[112,180,182],{"class":114,"line":181},3,[112,183,185],{"emptyLinePlaceholder":184},true,"\n",[112,187,189],{"class":114,"line":188},4,[112,190,191],{"class":118},"# HTTP直接接続（クラウドへ直接 → ロック待ちなし）\n",[112,193,195,197,199,201,203,205,207,210,212,214,216,218,220,222],{"class":114,"line":194},5,[112,196,126],{"class":125},[112,198,130],{"class":129},[112,200,133],{"class":125},[112,202,136],{"class":129},[112,204,139],{"class":125},[112,206,142],{"class":129},[112,208,209],{"class":158},"database",[112,211,130],{"class":129},[112,213,165],{"class":164},[112,215,155],{"class":129},[112,217,170],{"class":158},[112,219,130],{"class":129},[112,221,175],{"class":164},[112,223,178],{"class":129},[10,225,226],{},"書き換えた版を走らせる前に、現状を確認しておこうとHTTP接続でチャンク数を数えた。ここで予想外の事実にぶつかった。",[19,228,230],{"id":229},"kill-した時点で統合は既に終わっていた","kill した時点で、統合は既に終わっていた",[10,232,233],{},"数えたら、チャンクは既に47になっていた。統合は完全に成功していたのだ。",[10,235,236,237,240],{},"最初のEmbedded Replica版スクリプトは、ハングして何もできていなかったわけではなかった。UPDATE/DELETEを実行し、トランザクションをcommitまで完了させていた。固まっていたのはその後、最後に呼ぶ ",[14,238,239],{},"sync()","（レプリカの変更をクラウドへ同期する処理）だった。commitした時点でクラウド側のデータはもう書き換わっていて、killしても確定済みの変更は消えなかった。",[10,242,243,244,246],{},"つまり「ハングして失敗した」と思い込んでいたが、実際には「処理は終わっていて、後始末の ",[14,245,239],{}," だけが固まっていた」が正解だった。FTSのリビルドと検索テストもHTTP接続で流して、47チャンクが正しく引けることを確認した。",[19,248,250],{"id":249},"再発防止sync-を全部抜いた","再発防止：sync() を全部抜いた",[10,252,253,254,257,258,261],{},"同じ罠を踏まないよう、",[14,255,256],{},"restructure-book"," のスラッシュコマンド定義テンプレート（",[14,259,260],{},".claude/commands/restructure-book.md","）のStep 4を直した。",[27,263,264,267],{},[30,265,266],{},"DB書き込みパートをHTTP直接接続に統一",[30,268,269,270,272],{},"ハングの元凶だった ",[14,271,239],{}," の呼び出しを、テンプレート内の7箇所すべて削除",[10,274,275,276,278,279,281],{},"これで次に ",[14,277,47],{}," を実行したときは、最初からレプリカを経由せずクラウドへ直接書く。",[14,280,239],{}," 待ちで固まる経路自体が消えた。",[19,283,285],{"id":284},"walファイルが189mbに肥大化していた","WALファイルが18.9MBに肥大化していた",[10,287,288],{},"ハングの巻き添えで、ローカルのWALファイルが18.9MBまで膨らんでいた。CLAUDE.mdの手順どおり、レプリカファイルを退避した。データの本体はクラウドにあるので、ローカルのレプリカを退かすのは非破壊的な操作だ。次に接続したとき、レプリカは自動で再構築される。",[19,290,291],{"id":291},"レプリカ再接続で動作確認した",[10,293,294],{},"「レプリカに接続してちゃんと動いているか見て」と念押しされた。退避したあとEmbedded Replicaで繋ぎ直すと、レプリカがゼロから組み上がり、9.2秒で全テストが完走した。蔵書58冊ぶんがフル再構築され、検索も問題なく返ってくる。HTTP直接接続で逃げただけで終わらせず、本来のEmbedded Replica経路でも健全に動くところまで見届けられた。",[19,296,297],{"id":297},"学びメモ",[27,299,300,309,318,324],{},[30,301,302,305,306,308],{},[73,303,304],{},"ハング＝失敗とは限らない。"," commitが通っていれば、その後の ",[14,307,239],{}," で固まってkillしても、確定済みの変更はクラウドに残る。まず「どこまで終わっているか」をHTTP接続で数えてから、やり直すか判断する",[30,310,311,314,315,317],{},[73,312,313],{},"Embedded Replicaの便利さには、WALロックという裏面がある。"," 先行接続のロックが残ると、後続の ",[14,316,82],{}," が無言で待ち続ける。書き込みバッチを連続で回す処理は、HTTP直接接続のほうが事故らない",[30,319,320,323],{},[73,321,322],{},"プロセスをkillするときは木を見て狙う。"," uvが起こすPythonの親子9プロセスだけを落とし、Claude Codeのnodeには触れない。名前で一括killすると自分の足を撃つ",[30,325,326],{},"既知問題はCLAUDE.mdに書いておくと、240秒固まった瞬間に「あれだ」と当たりがつく。今日それに救われた",[328,329,330],"style",{},"html pre.shiki code .sxvE3, html code.shiki .sxvE3{--shiki-default:#A0ADA0;--shiki-dark:#A0ADA0}html pre.shiki code .sG7-3, html code.shiki .sG7-3{--shiki-default:#393A34;--shiki-dark:#393A34}html pre.shiki code .shFtX, html code.shiki .shFtX{--shiki-default:#999999;--shiki-dark:#999999}html pre.shiki code .sMJiu, html code.shiki .sMJiu{--shiki-default:#B5695977;--shiki-dark:#B5695977}html pre.shiki code .sdGka, html code.shiki .sdGka{--shiki-default:#B56959;--shiki-dark:#B56959}html pre.shiki code .s4oTP, html code.shiki .s4oTP{--shiki-default:#B07D48;--shiki-dark:#B07D48}html pre.shiki code .snbK4, html code.shiki .snbK4{--shiki-default:#A65E2B;--shiki-dark:#A65E2B}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":108,"searchDepth":122,"depth":122,"links":332},[333,334,335,336,337,338,339,340,341],{"id":21,"depth":122,"text":22},{"id":54,"depth":122,"text":54},{"id":67,"depth":122,"text":68},{"id":93,"depth":122,"text":94},{"id":229,"depth":122,"text":230},{"id":249,"depth":122,"text":250},{"id":284,"depth":122,"text":285},{"id":291,"depth":122,"text":291},{"id":297,"depth":122,"text":297},"dev","214ページの裁断スキャンPDFをyomitokuでOCRし、Turso（libSQL/SQLite互換）に格納。セクション統合スクリプトがEmbedded ReplicaのWALロックでハングした原因を突き止め、HTTP直接接続版に書き換えて救出するまでの試行錯誤を記録する。","md",{},null,"/book-ocr-turso-wal-lock-recovery","book-knowledge-base",false,"2026-05-25T00:00:00.000Z",{"title":5,"description":343},"2026-05/2026-05-25/book-ocr-turso-wal-lock-recovery",[354,355,356],"OCR","Turso","SQLite","XUAPAFoUMocKjS2VFaj5MKQaeifhZ_6fWXrnzDPyvw8",[],"https://log.eurekapu.com/og/blog/book-ocr-turso-wal-lock-recovery.png?v=2026-05-25T00%3A00%3A00.000Z&title=%E8%A3%81%E6%96%AD%E3%82%B9%E3%82%AD%E3%83%A3%E3%83%B3%E3%81%97%E3%81%9F%E5%8F%82%E8%80%83%E6%9B%B8%E3%82%92OCR%E3%81%97%E3%81%A6Turso%E3%81%AB%E5%8F%96%E3%82%8A%E8%BE%BC%E3%82%80%EF%BC%9AWAL%E3%83%AD%E3%83%83%E3%82%AF%E3%81%A7%E3%83%8F%E3%83%B3%E3%82%B0%E3%81%97%E3%81%9F%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88%E3%82%92HTTP%E7%9B%B4%E6%8E%A5%E6%8E%A5%E7%B6%9A%E3%81%A7%E6%95%91%E5%87%BA%E3%81%99%E3%82%8B%E3%81%BE%E3%81%A7&author=Kei%20Komatsu&sig=a5298867b1851d72",1782528841988]