はじめに
hkob の雑記録の第495回目(連続68日目)は、昨日に引き続きテンプレートの中身を確認していきます。次は skills の中にある sync-validate の SKILL.md を読んでみます。
sync-validate/SKILL.md
name: sync-validate description: sync 機能にありがちなバグをレビューする — カーソル進行、ページネーション終了条件、state 永続化、二相(bi-modal)の正当性、整合性バッファ、削除処理 user-invocable: true disable-model-invocation: true allowed-tools: ["Read", "Glob", "Grep", "Bash"]
昨日と同じように SKILL の概要が書かれています。こちらは sync の動作が正しく行われているかを検証する際の指示書です。
Instructions(手順)
src/index.ts(および import されているモジュール)内の sync capability を読む。見つかった sync ごとに、以下のチェックリストを適用する。結果は重要度(severity)ごとにグルーピングして報告する。開始前に、sync 概念の完全版リファレンスとして
.agents/skills/sync-guide/SKILL.mdを読むこと。Critical Issues(sync を壊す / データ損失につながる)
- ページネーションの終了条件がない:
hasMoreは最終的にfalseになるか?確認観点:nextStateが進まず無限ループする、base case がない、成立し得ない条件を終了条件にしている。- カーソルが進まない: 反復間で
nextStateは変化しているか?カーソルが前回 state と同一だと永久ループする。各execute呼び出しが前進していることを確認する。- 初回実行(first-run)の扱いがない:
stateがundefined(初回)でも安全に動作するか?確認観点:state?.cursorではなくstate.cursorを参照している、undefinedの可能性がある state に対するプロパティアクセスがある。- バッチが大きすぎる: 1 回の実行で changes を数千件返していないか?推奨は ~100 件程度のバッチ。大きすぎると失敗する。
- API が変更追跡できるのに replace を使っている: mode が
replace(または未指定でデフォルトがreplace)の場合、ソース API はupdated_atフィルタ、event feed 等の変更追跡を提供しているか?提供しているなら、毎回全件再取得を避けるためincrementalを推奨する。- state 永続化の誤解: incremental モードでは、カーソルはサイクル間でリセットされない。次サイクルは前回の続きから始まる。毎サイクル「最初から」始まる前提のコードがないか確認する(再取得や永久スキップの原因になる)。
sync が失敗する例を最初に説明しています。このようにならないように検証を実施することになるようです。
Structural Issues(二相 sync の正当性)
- incremental sync に単一モードのカーソルを使っている: backfill と delta に同一のカーソル戦略を使っていないか?API が
updated_atソート + 不透明カーソル(1 つのカーソルが自然に両方を満たす)でない限り、backfill / delta フェーズを分けた判別可能な state union が必要。- backfill→delta の遷移が欠けている: 二相 sync で、delta カーソルはどう seed されるか?backfill の最後のレコードから作ってはならない。backfill 開始前に取ったマーカー(event anchor、重なりを持つ timestamp 等)から seed する必要がある。そうでないと backfill 中に起きた変更が永久に失われる。
- 遷移時の重なり(overlap)がない: backfill から delta に切り替える際に、重なりウィンドウ(例:
backfillStartedAt - 5 minutes)があるか?重なりがないと、backfill 実行中に更新されたレコードが永久に欠落する。
二相 sync のトラブルの原因はやはりページネーションのカーソルの扱いによるもののようです。よくありそうな 3 つの原因が示されています。これらが正しく処理されているかを確認します。
Warnings(微妙なデータ品質バグを引き起こす可能性)
- 整合性バッファがない: 最終整合性 API を叩く incremental sync では、カーソルは「今」より 10〜60 秒遅らせるべき。ないと、未インデックスのレコードを飛び越えてしまい、カーソルが戻らないため永久欠損になる。
- タイムスタンプカーソルにタイブレークがない:
updated_atをカーソルにしている場合、同一 timestamp を持つ複数レコードがあり得るか?(バッチ投入、一括更新、低解像度 timestamp 等)あり得るならキーセット方式を推奨:(timestamp, id)を用いWHERE ts > X OR (ts = X AND id > Y)のようにする。- 削除処理がない: incremental モードで削除は扱われているか?確認:
- ソース API に delete シグナルがあるか(監査ログ、archived フィルタ、events 等)
- あるなら
{ type: "delete", key }を emit しているか- delete シグナルが別エンドポイントなら flip-flop パターン(サイクル境界でストリームを交互に回す)を使っているか
- delete シグナルがないなら、replace モードにすべきか(または backfill を削除掃除に使うべきか)
- secret のハードコード: API キー/トークン/資格情報が
process.envではなくコードに直書きされていないか。secret らしき文字列を検出したら指摘する。- fetch のエラーハンドリング不足: エラーハンドリングなしのネットワーク呼び出しは、一時障害で sync をクラッシュさせる。API エラーを catch して処理すべきか、あるいは runtime のリトライに委ねるべきかを検討する。
通常時は問題にならないが、同時実行が走る場合などの稀な場合にエラーが出ることがあります。この辺りはマルチスレッドのプログラミングをやっていた人なら思い当たることがたくさんありそうです。
出力形式
見つかった各 issue について:
- What: issue 名と、該当するコード位置を示す
- Why it matters: 放置した場合、本番で何が起きるか
- Fix: 修正案の具体的なコードスニペットを示す
問題が見つからなければ、その旨を述べる。問題を捏造しない。
何か問題があった時に検証するために使われる SKILL でした。あまり参照されることはないのかもしれませんが、問題があった場合にはちゃんと報告するように行っていますね。問題がないのに捏造するなというのは面白い表現ですね。
おわりに
ここまでの Sync のドキュメントでも、かなり詳しく書かれていたので、あまりこの指示書が運用されることは多くなさそうですが、キーセットあたりの話は重要になる場面もありそうだとは思いました。
https://hkob.notion.site/hkob-16dd8e4e98ab807cbe3cf3cc94cdfe0f?pvs=4hkob.notion.site