検索/いくつものサービスに分かれたアプリに、バックエンドのコードと、それに合ったテストを足す

いくつものサービスに分かれたアプリに、バックエンドのコードと、それに合ったテストを足す

バックグラウンドワーカーを持ち、いくつものサービスに分かれたアプリをAIに直させる時に使う。環境と動いているサービスを確かめ、データベースとタスクのコードを決まった場所に置き、ジョブとエラーをプロジェクトの決まりどおりに作り、合う種類のテストを1つ選び、コードを書く前に計画を立てる。

このAIルールを使うには
## 作業の前に、環境を整えてサービスが動いているか確かめる - バックエンドの依存関係は、リポジトリ直下にあるロックファイル管理の仮想環境に入っている。まだ存在しない場合は、固定されたロックファイルから作成し、何かを実行する前に有効化する。 - テストに必要な資格情報や鍵は、リポジトリのローカル環境ファイルに入っている。値をでっち上げたり新しく発行を依頼したりせず、そこから読み取る。 - アプリケーションのサービスはすべてすでに動いている前提で進めるが、信用せずに確認する。ログディレクトリを見て、対象のサービスが今もエントリを出し続けているかを確かめる。 - データベースへは、自分で即席に組み立てた接続ではなく、プロジェクトに文書化されたクライアント呼び出し方法で、動作中のコンテナ経由で到達する。 - ブラウザ自動化でWebアプリケーションを動かすときは、エンドツーエンドのセットアップが作成するアカウントでサインインする。そのアカウントが存在しない場合は、サインアップページから登録する。最初に登録されたアカウントが管理者になるためである。 ## データベース処理を1か所に置き、テストの呼び出しはフロントエンド経由にする - データベース操作はすべて、指定されたデータアクセス用ディレクトリの中に置く。それ以外の場所からクエリを実行しない。そうすればスキーマに関する知識が1つのレイヤーに留まり、1か所でレビューできる。 - 手動テストやスクリプトによるテストでバックエンドを呼び出すときは、バックエンドのポートを直接叩くのではなく、常にフロントエンドのオリジンを経由する。フロントエンドを飛ばしたリクエストは、実際のトラフィックが通過するプロキシ・セッション・認証の挙動も飛ばしてしまうため、結果が実態を表さない。 ## 新しい処理を、合ったバックグラウンドワーカープールに置く 非同期処理は、1つの汎用キューではなく、専用化された複数のワーカープールに分かれている。新しい処理は、その性格に合うプールへ置く。 - 主プールはシステム全体にまたがる操作を統括する。リソースの破棄、同期、不要データの刈り込み、定期チェックなどである。 - フェッチプールは外部ソースからデータを取得し、バッチごとに処理タスクを起動する。ウォッチドッグによる監視が付いており、停止したソースを検出できる。 - 処理プールは取り込みパイプラインを実行する。レコードを永続化し、チャンクに分割し、情報を付加し、埋め込みを作り、検索インデックスへ書き込み、メタデータを更新する。 - 軽量プールは短い操作を高い並行度で扱う。重量プールはリソースを多く消費する操作を低い並行度で扱う。 - 監視プールはキューの深さ、メモリ、ヘルスの指標を収集する。監視に並列性は不要なのでシングルスレッドである。 - スケジューラープールは定期的なトリガーを受け持つ。秒単位で計られる間隔チェックから1時間ごとのクリーンアップまでを担い、スケジュールされた各タスクについてテナントを解決する。 プール間で共通する性質は次のとおり。安定性のためにプロセスではなくスレッドプールを使うこと、テナントごとの分離、優先度付きキュー、ハートビートと死活チェック、自動リトライと障害復旧、プロセス間の協調のための高速な共有ストア、そして永続的なタスク状態のためのリレーショナルデータベースである。 ## バックグラウンドタスクに、有効期限とタイムアウトを必ず付ける - タスクは、具体的なアプリケーションインスタンスに束縛するのではなく、フレームワークのアプリケーション非依存のデコレーターで宣言する。そうすればタスクをインポートしただけでアプリケーションの構築まで引きずり込むことがなくなる。 - タスクのモジュールは、そのエディションまたはティアに対応する所定のタスクディレクトリの下に置く。 - 有効期限を付けずにタスクをキューへ入れてはならない。スケジュールから送る場合でも、別のタスクから送る場合でも、必ず有効期限を指定する。有効期限なしでキューへ入れるコードを提出することは容認されない。キューが際限なく膨らむのを許してしまうからである。 - タスクはスレッドプール内で実行されるため、フレームワーク自身の時間制限機能は何の通知もなく無効化される。タイムアウトの処理はタスクの内部に自分で実装する。 - ワーカーはコード変更で自動リロードしない。ワーカーのコードを変更したら、テストの前にメンテナーへワーカーの再起動を依頼し、新しいコードが動いていると決めてかからない。 ## エンドポイントを足し、エラーはプロジェクト独自のエラー型で投げる - エンドポイントを追加するときは、レスポンスモデルのフィールドを別途宣言するのではなく、ハンドラ関数に直接型を付ける。 - フレームワークの汎用的なHTTP例外ではなく、プロジェクト独自のエラー型を送出する。ステータスコードをハードコードしたり、フレームワークからステータス定数をインポートしたりしてはならない。 - グローバルなハンドラーが、プロジェクトのエラーを、エラーコードと詳細を含む1つの標準的なレスポンスボディへ変換する。これにより定型コードがなくなり、バックエンド全体でエラー処理が一貫する。 - エラーコードは中央のレジストリから取る。新しいカテゴリーが必要なら、まずレジストリへ追加する。呼び出し箇所でコードをでっち上げてはならない。 - 上流の応答から得たステータスを持つ上流の失敗を転送するときは、エラー型を投げ捨てるのではなく、そのエラー型のステータス上書き引数でステータスを渡す。 ## 厳しい型付けを守り、後まで役立つコメントだけを書く - 変更を提案する前に、リポジトリのpre-commitフックを導入し、全ファイルに対して実行する。 - バックエンドでもフロントエンドでも、厳格に型を付けた状態を保つ。型を付けない抜け道は、容認できる近道ではない。 - コメントは簡潔にし、長期的に意味を持ち続ける情報だけに限る。今まさに進行中の変更を語るコメント、たとえば今何を追加した、何を取り除いた、何をリファクタリングしたといった記述は書かない。それらは変更が入った瞬間にノイズになる。 ## マイグレーションを手で書き、プライベートスキーマにも適用する - リビジョンファイルはマイグレーションツールで生成し、その後、生成されたファイルの中にマイグレーション本体を手で書く。意図の表現を自動生成に頼らない。 - マルチテナント構成では、既定の設定に加えてプライベートスキーマ設定に対してもマイグレーションを適用する必要があり、リビジョンを作成するときも同じ区別が当てはまる。 - マイグレーションは、後から適用する付け足しではなく、レビュー対象の変更の一部として扱う。 ## モックを減らし、変更を確かめられる種類のテストを1つ選ぶ レベルは4つある。重厚なモックよりも忠実度の高いレベルを優先し、その挙動を実際に証明できるものを選ぶ。 - ユニットテストは、どのサービスにも到達できない前提で、外界とのやり取りをすべてモックする。単体でロジックを固定しておく価値のある、複雑で独立したモジュールに対してのみ書く。 - 依存関係を伴うユニットテストは、外部の依存関係(データベース、キャッシュ、オブジェクトストレージ、検索インデックス、外向きのネットワーク)が動いている一方で、アプリケーションのコンテナは動いていない前提を取る。関数は直接呼び出し、選んだコンポーネントは引き続きモックしてもよい。モックを最小限に抑えつつ、不安定な挙動を制御する能力や、内部の呼び出しが特定の引数で行われたことを検証する能力を保ちたいときに使う。これは純粋な統合テストにはできないことである。 - 統合テストは実際のデプロイに対して実行し、何もモックしない。これを優先し、モックや内部検証がどうしても必要な場合にだけ、依存関係を伴うユニットテストへ後退する。 - ブラウザ駆動のテストは、完全なデプロイの上にWebサーバーを加える。フロントエンドとバックエンドの間で相当な連携を要する挙動に使う。 テストをやりすぎない。ある1つの変更に必要なのはたいてい1種類のテストであって、4種類すべてではない。 ## 統合テストで、今あるフィクスチャとヘルパーを使い回す - 統合テストを書く前に、利用できるフィクスチャを知るためにルートのテスト設定を読み、ヘルパーを知るために共有ユーティリティのディレクトリを読む。 - 生のHTTPライブラリでエンドポイントを直接呼ぶよりも、それらのユーティリティにあるマネージャー系ヘルパーを優先し、ヘルパーを手で呼ぶよりもフィクスチャを優先する。管理者用のフィクスチャがあるのに管理者を手作業で作ると、セットアップが重複し、テストスイートの他の部分から乖離していく。 - テストはディレクトリ単位で並列化されるため、同じディレクトリ内のファイルは、共有された可変状態に依存してはならない。 - テストが実際にモデルを呼び出すときは、フラッグシップモデルではなく、各プロバイダーの安価で高速なティアを使う。テストのコストはスイートの規模に比例して増えるうえ、検証したい内容はモデルの性能の高さに依存しない。 - 新しい構造を編み出すのではなく、同じカテゴリーにある既存の模範的なテストの形をなぞる。 ## 失敗の原因を考える前に、サービスごとのログを読む - 統合テストを書くときや、動作中のシステムを手で操作するときは、ログディレクトリにあるサービスごとのデバッグログファイルを読む。各サービスは自分の出力をそこへ流し続けている。 - 失敗についての仮説を立てる前にログを読む。サービスが起動していないといった無関係な原因は、ログを読むまで本物の欠陥と見分けがつかない。 ## APIキーをリポジトリに入れず、新しいエンドポイントを守る - APIキーやシークレットをリポジトリにコミットしてはならない。 - 連携用の資格情報は、平文の設定ではなく、暗号化された資格情報ストレージ経由で保管する。 - 機能を追加するときは、確立されたロールベースのアクセス制御のパターンに従う。新しいエンドポイントは、既定では何の保護も引き継がない。 - 入力は境界でスキーマモデルによって検証する。 - 構造上インジェクションが成立しないよう、パラメータ化クエリを使う。文字列連結でクエリを組み立ててはならない。 ## AIモデルの呼び出しすべてに、登録済みの操作名のタグを付けて追跡する 言語モデル、埋め込みモデル、リランカー、画像生成、音声コンポーネント、意図分類器へのすべての呼び出しは、中央のフローレジストリの値をタグに付けた生成スパンを開始しなければならない。呼び出し経路に合ったラッパーを使う。モデル抽象を通した呼び出し用のものと、それを迂回する直接のプロバイダー呼び出しやHTTP呼び出し用のものがある。 - 新しい操作を計装する前に、レジストリへ新しい値を追加する。生の文字列をタグとして渡してはならない。 - タグはプロバイダーではなく、リランキングや画像編集といった操作の名前を表す。プロバイダーはモデル設定のメタデータに属する。 - 明示的なスパンなしで抽象に到達した呼び出しに対しては、自動フォールバックがタグなしのセンチネル値を送出する。それらのセンチネルはダッシュボード上で見え、計装が欠けていることを意味する。フォールバックに頼らず、呼び出し箇所を直す。 ## コードを調べてから、日程もコードも入れない実装計画を書く 書く前に調べる。まずコードベースの関連部分を探索する。思い込みで書かれた計画は、計画がないよりも悪い。 計画には少なくとも次の要素を含める。 - 対処すべき課題: その変更が何を実現しようとしているか。 - 重要なメモ: 調査中に見つかった、実装にとって意味のある事柄。 - 実装方針: 変更を成立させるための大まかなアプローチ。 - テスト: 正しい挙動を検証するのはどのテストか。意図を持って選び、最小限に保つ。 タイムラインの節は入れない。ロールバック手順の扱いも入れない。計画の一部としてコードを書かない。大まかなレベルに留めるが、具体的なファイルや関数に言及するのは有用である。これは最低限の一覧なので、役に立つならさらに追加する。 ## サブプロジェクトで作業する前に、そこ専用の決まりを読む - ルートの指針は共通の関心事を扱う。各サブプロジェクトは自分のコードの隣に独自の基準文書を持ち、その文書がそのツリーを統べる。 - その領域で作業する前に、範囲を限定した文書を読む。あるクライアントのルールが別のクライアントにも当てはまると決めてかからない。ドキュメントオブジェクトモデルを持たず、スペーシングの値が相対的な段階スケールではなく絶対値であるスタイリングプリミティブを持ち、独自のルーティングとビューのプリミティブを持つモバイルクライアントは、Webクライアントとは異なるルールに従う。 - より広い工学的な実践は、リポジトリのコントリビューションガイドにある。この文書に加えて、そちらも読んで従う。