target_reader: Future me
objective: コードベース全体の外部サービス依存関係を、多態性を乗り越えて自動的に可視化する手法を確立する。
多数のマイクロサービスやバッチが、それぞれどのクラウドサービスリソース(SQSキュー、S3バケット等)にアクセスしているのか。この依存関係を網羅的かつ継続的に把握する必要が出てきた。
当初のアイデアは、ソースコードを静的解析することだった。Goであれば、AST1をパースして関数呼び出しのグラフを構築すれば、依存関係を抽出できると考えた。
// 静的解析で main -> process -> sendToSQS の呼び出しを追跡するイメージ
func main() {
process()
}
func process() {
// ...
sendToSQS("my-queue", "message")
}
func sendToSQS(queue, msg string) {
// ...
}
しかし、このアプローチはすぐに壁にぶつかった。Goのinterface
や、変数に格納された関数、引数で渡されるクロージャといった多態性2が絡むと、静的に呼び出し先を一つに特定することができない。
// 静的解析では、notifier.Notify() が具体的にどの実装を呼び出すか特定できない
type Notifier interface {
Notify(message string) error
}
func process(notifier Notifier) {
notifier.Notify("some event") // これはEmailNotifier? SlackNotifier?
}
grep
で呼び出し元を機械的に辿る方法も検討したが、これも同じ理由で破綻する。静的解析のみで完全な依存関係グラフを構築するのは、現実的ではないと判断した。
静的解析の複雑さを回避するため、アプローチを180度転換した。コードを実際に(ただし抽象的に)実行する「評価器」を作成する方針に切り替える。
この評価器の目的は、プログラムの振る舞いを完全にエミュレートすることではない。あくまで、特定の関数(例: SQSへの送信関数)が呼ばれるまでのコールスタックを取得することに特化させる。
- HTTPリクエストやDBアクセスといった副作用のあるI/O処理は、実際には実行しない。
- 変数のインクリメントのような状態変更も無視する。
重要なのは、制御フローだ。特に条件分岐は、実行パスを複数に派生させる。
// このif文で、評価器は両方の分岐を探索する必要がある
if featureFlag.IsEnabled() {
sendToNewSystem()
} else {
sendToOldSystem()
}
if
文に遭遇するたびに、評価器はそこから先の「未来」を分岐させて両方のパスを追跡する。これにより、解析すべき実行パスが指数関数的に増加する、いわゆる状態空間爆発3のリスクを内包することになる。一方で、ループ処理は新たな実行パスを生まないため、この文脈ではif
文ほど大きな問題にはならない。
この評価器のアイデアを、より具体的なユースケースに当てはめてみる。
-
Web APIサーバーの解析
- 解析の開始地点が単一の
main
関数ではなく、多数のAPIハンドラになる。 - ルーターの定義(例:
/users/{id}
)と、それに対応するハンドラ関数を正確に紐付ける機構が必要になる。 - さらに、ミドルウェアのような高階ハンドラが多用されている場合、多くのAPIで解析結果が画一的になり、本質的な処理の違いが見えなくなる可能性がある。引数として渡されたハンドラ本体を、共通処理から区別して追跡する必要がある。
- 解析の開始地点が単一の
-
設定値の追跡
- 「どのサービスを呼んでいるか」だけでなく、「どのキューやバケットを対象としているか」も可視化したい。
- そのためには、設定ファイルや環境変数から読み込まれた値が、最終的にどのように使われるかを追跡する必要がある。
// queueNameが設定ファイル由来の場合、この値を追跡したい
func handleRequest() {
queueName := config.Get("sqs.queue_name") // e.g. "production-event-queue"
client.Send(queueName, "message")
}
単に関数呼び出しを記録するだけでは、この要件は満たせない。評価器には、値そのものを追跡する能力も求められる。
値の追跡という新たな要件を満たすため、評価器にシンボリック実行の考え方を導入する。
値を具体的に計算するのではなく、その「出自」と「計算の構造」をシンボリックなデータとして保持し続ける。
config.Get("sqs.queue_name")
の戻り値は、"production-event-queue"
という文字列ではなく、Symbol<Source:ConfigFile, Key:"sqs.queue_name">
のようなオブジェクトとして扱う。"prefix-" + runtime_variable
のような文字列結合は、Concat<Literal<"prefix-">, Symbol<Source:Runtime>>
という構造で表現する。
このシンボリックな値の追跡は、無制限に行うと複雑になりすぎる。スコープを「監視対象の関数(sqs.send
など)の引数に渡されるまで」に限定する。それ以外の場所で値がどう使われようと、関心事ではない。
多態性の問題については、「注入されうる具象型をすべて列挙し、それぞれに対して評価器を実行する」という方針を採る。例えば、Notifier
インターフェイスの実装がEmailNotifier
とSlackNotifier
の2つ存在するなら、両方のパターンで解析を実行する。これは計算量を増大させるが、網羅性を担保するためのトレードオフとして許容する。
履歴
https://aistudio.google.com/app/prompts?state=%7B%22ids%22:%5B%221cvoeNPDpELIe05mZvHxkJxZx5eocCcOI%22%5D,%22action%22:%22open%22,%22userId%22:%22108405443477417806091%22,%22resourceKeys%22:%7B%7D%7D&usp=sharing