開発book-knowledge-base

migration スクリプトを走らせたら conn.sync() で止まったきり戻ってこない。プロセス一覧を覗くと CommandLine が空になった Python の残骸が WAL ファイルを掴んだまま居座っていた。ロックを剥がして再実行しても 60 秒以上沈黙する。ファイル起因ではなく同期プロトコル側で詰まっていると気づいた瞬間、Turso から「Bytes Synced Usage が組織クォータの75%を超えた」というメールが飛んできて、点と点がつながった。

起きたこと

  • Kindle 取り込みの前段で migration を走らせたら、conn.sync() を呼んだ箇所で止まった
  • 前のセッションの Python プロセスが残骸として WAL ファイルを掴んでいた
  • 残骸を始末しても、新しいレプリカパスで試しても、sync() が60秒を超えて返ってこない
  • 別 DB(apple-health 等の小さめのもの)では 0.3 秒で完了 → 個別 DB 固有の問題と切り分け
  • .db-info を覗くと durable_frame_num: 0 のままで generation: 399 まで膨らんでいた。レプリカ状態が腐っていた
  • そこに Turso 運営から「Bytes Synced クォータ75%超」のアラートメールが届いた

アナリティクス画面を開いて確かめた

Chrome DevTools MCP 経由で app.turso.tech のアナリティクスを開いた。過去30日の Syncs 推移を眺めると、当日だけ突き抜けて 1.23 GB を吐き出している。Storage Transferred も 2.2 GB に達していた。migration ハング → レプリカ退避 → 再構築のループを回した結果が、そのまま帯域に乗っていた格好。

Free プランの上限は Syncs が月 3 GB。診断と再構築で 1 日に 1.23 GB を持っていかれた計算になる。

切り分けの順序

  1. WAL ファイルの巨大化と更新時刻でプロセス残骸を疑った
  2. プロセスを片付けても解消しないので、HTTP-only 接続で疎通を確認 → 0.04 秒で完了
  3. ネットワークと認証は健全なので、同期プロトコル自体に絞り込んだ
  4. 別 DB で再現しないことを確認し、特定 DB のレプリカ状態が壊れていると確定
  5. 壊れたレプリカを退避して作り直す → 1回目の sync は32秒、2回目は0.13秒で安定

これでハングは抜けたが、再構築のたびに数十 MB から100 MB 単位のバイトが Sync 枠を消費する事実が残った。

HTTPオンリー接続を検討した理由

書き込み中心の用途では、Embedded Replica の旨味(ローカル高速読み取り)を享受しない。にもかかわらず、書き込みのたびにレプリカ側へ反映するための Sync が走る。今日のように何かの拍子に再構築が起きると、月次クォータをまとめて持っていかれる。

接続コードはこういう違いになる。

# Embedded Replica(書き込みも sync が走る)
conn = libsql.connect(
    "book-knowledge-base.db",
    sync_url=os.environ["TURSO_DATABASE_URL"],
    auth_token=os.environ["TURSO_AUTH_TOKEN"],
)
conn.sync()  # ← ここで詰まるとクォータも食う

# HTTP-only(sync を呼ばない)
conn = libsql.connect(
    database=os.environ["TURSO_DATABASE_URL"],
    auth_token=os.environ["TURSO_AUTH_TOKEN"],
)

Kindle 取り込みのような書き込み主体のスクリプトは下段の形にしておけば、そもそも sync() を呼ばないので今回のハングにもクォータ浪費にも巻き込まれない。

結局どうしたか

完全な HTTPオンリー移行までは踏み込まず、月リセットまで様子見にした。判断材料は以下。

  • 今日のスパイクの原因(壊れたレプリカ)はすでに退避して作り直した
  • 残り 0.5 GB あれば月末まで通常運用では届かない
  • Sync を続ければローカル読み取りが速いというメリットも残しておきたい
  • 書き込み中心のスクリプトを順次 HTTPオンリーに寄せておけば、次にハングを踏んでもクォータが膨らまない

クォータ警告を「即座に全面移行するシグナル」と読まず、「Embedded Replica と HTTPオンリーを用途別に使い分ける契機」と読み替えた、というのが今日の落とし所。

学びメモ

  • conn.sync() の沈黙はファイルロックよりも先にレプリカ状態を疑う。.db-infodurable_frame_num が0で generation が3桁に膨らんでいたら腐っているサイン
  • ネットワーク疎通の切り分けには HTTPオンリー接続が一番速い。0.04 秒で返ってくれば認証もエンドポイントもシロ
  • Free プランの Syncs 3 GB は、診断のループを1日回しただけで簡単に半分以上消える。デバッグ時のレプリカ再構築は帯域を食う前提でやる
  • 書き込み中心のスクリプトは HTTPオンリーで書く方が安全。Embedded Replica は読み取り高速化が効くワークロードに限定する
  • クォータ警告メールが届いたら、まずアナリティクスで日次の内訳を見る。1日だけ突出しているなら異常値で、恒常的な使用増なら設計を見直す

次にやること

  • 書き込み中心の migration / import スクリプトを HTTPオンリー接続に書き換える
  • 月リセット後の Syncs 推移を1週間観察する
  • レプリカ再構築が必要になった場合のチェックリストを turso スキルに追記する