最終更新: 2026/06/22 18:56
MAS-178: 堅牢なエラーハンドリングとリカバリー機構
概要
| 項目 | 内容 |
|---|---|
| 案件ID | MAS-178 |
| カテゴリ | 信頼性 |
| Phase | P1 |
| 優先度 | ★★ |
| 所要時間 | 2-3時間 |
| 対象ファイル | 000_infra/004_utils.js(ログ基盤拡張)600_report/602_datamart_main.js(タイムアウト対策)500_import/502_receipt_reader.js(API制限対策) |
目的
データ不整合、API制限、GAS実行時間超過(6分)に対するエラー通知とリカバリー機構を強化し、システムの安定稼働を確保する。
現状の課題
課題一覧
| # | 課題 | 重大度 | 現状 |
|---|---|---|---|
| 1 | 永続ログなし | 高 | console.log のみ(7日で自動削除、検索不可) |
| 2 | 実行時間超過対策なし | 高 | 6分制限への監視・中断機構なし |
| 3 | API制限対応不十分 | 中 | 503リトライあり。429(Rate Limit)未対応、Exponential Backoff未実装 |
| 4 | ロールバック機構なし | 中 | writeDtosToSheet_ で clearContent → setValues 間に失敗するとデータ消失 |
| 5 | 処理中の不整合検出が throw のみ | 低 | 科目未登録等で即例外。警告スキップ不可 |
既存のエラーハンドリングパターン
// 全17ファイルで共通パターン
try {
// 処理
} catch (e) {
Utils.logError(FUNC, e); // console.error のみ
SpreadsheetApp.getUi().alert('🚨 ...', e.message, ...); // ダイアログ
}
修正方針(3ステップ段階実装)
大規模なアーキテクチャ変更ではなく、既存パターンを維持しつつ段階的に強化する。
Step 1: 永続ログ基盤(Utils拡張)
004_utils.js に永続ログ記録機能を追加。専用シート 99_error_log にエラー・警告を記録する。
/**
* エラーログをスプレッドシートに永続記録する
* @param {string} level - 'INFO' | 'WARN' | 'ERROR'
* @param {string} funcName
* @param {string} message
* @param {string} [detail] - スタックトレース等
*/
persistLog: function(level, funcName, message, detail) {
try {
var ss = getWebSpreadsheet_();
var sheet = ss.getSheetByName('99_error_log');
if (!sheet) {
sheet = ss.insertSheet('99_error_log');
sheet.getRange(1, 1, 1, 5).setValues([['日時', 'レベル', '関数名', 'メッセージ', '詳細']]);
sheet.getRange(1, 1, 1, 5).setBackground('#434343').setFontColor('#FFFFFF').setFontWeight('bold');
sheet.setFrozenRows(1);
}
sheet.appendRow([new Date(), level, funcName, message, detail || '']);
} catch (e) {
// ログ記録自体の失敗は握りつぶす(無限ループ防止)
console.error('[PERSIST_LOG_FAIL] ' + e.message);
}
}
既存の logError を拡張:
logError: function(funcName, error, context) {
var msg = context ? context + ' - ' + error.message : error.message;
console.error('[ERROR] ' + funcName + ': ' + msg);
if (error.stack) console.error(error.stack);
// S-02追加: 永続記録
Utils.persistLog('ERROR', funcName, msg, error.stack || '');
}
Step 2: 実行時間監視(タイムアウトガード)
602_datamart_main.js の buildBudgetTrendDataMart() に実行時間チェックを追加。
// 関数冒頭
var startTime = new Date().getTime();
var TIMEOUT_MS = 300000; // 5分(6分制限の50秒前に警告)
// 各ステップ間にチェック
function checkTimeout_(stepName) {
var elapsed = new Date().getTime() - startTime;
if (elapsed > TIMEOUT_MS) {
var msg = stepName + ' でタイムアウト警告(経過: ' + Math.round(elapsed/1000) + '秒)';
Utils.persistLog('WARN', FUNC, msg);
throw new Error('⏰ 実行時間が5分を超えました。処理を中断します。\n' + msg);
}
Utils.logInfo(FUNC, stepName + ' 完了 (' + Math.round(elapsed/1000) + '秒)');
}
// 使用例
dmIngestData_(ctx, sheetInv, sheetBank, sheetAcct);
checkTimeout_('ステップ1: データ取込');
dmProcessAllEvents_(ctx);
checkTimeout_('ステップ2: イベント処理');
Step 3: API制限対策(Exponential Backoff)
502_receipt_reader.js の callGeminiForReceipt_() にExponential Backoffと429対応を追加。
// 既存: 503のみ固定5秒待機
// 改善: 429/503に対応、待機時間を指数的に増加
var MAX_RETRIES = 4;
var baseWaitMs = 2000; // 2秒
for (var retry = 0; retry < MAX_RETRIES; retry++) {
response = UrlFetchApp.fetch(url, options);
status = response.getResponseCode();
if (status === 200) break;
if ((status === 429 || status === 503) && retry < MAX_RETRIES - 1) {
var waitMs = baseWaitMs * Math.pow(2, retry); // 2s, 4s, 8s, 16s
Utils.logInfo(FUNC, 'API ' + status + ' リトライ待機: ' + waitMs + 'ms');
Utilities.sleep(waitMs);
continue;
}
throw new Error('Gemini API エラー: HTTP ' + status);
}
ロールバック機構について
GASにはネイティブなトランザクションAPIがなく、完全なロールバックは困難。以下の軽量な保護策を推奨:
| 対策 | 適用先 | 内容 |
|---|---|---|
| 書き込み順序の最適化 | writeDtosToSheet_ | clearContent と setValues を可能な限り近接させ、間に重い処理を入れない(現状で対応済み) |
| 冪等性の維持 | 全RPA関数 | isDuplicate_ による重複防止(MAS-076で修正済み) |
| エラーログによる手動復旧 | Step 1 | 99_error_log でどこまで処理が進んだか追跡可能にする |
本格的なロールバック(処理前スナップショットの保存+復元)は GCP 移行(Phase 3-4)で対応。
影響範囲
| Step | ファイル | 変更量 | 既存動作への影響 |
|---|---|---|---|
| 1 | 004_utils.js | 約25行追加 | logError に永続記録を追加。既存の console 出力はそのまま |
| 2 | 602_datamart_main.js | 約15行追加 | 5分超過時に例外で中断。通常は影響なし |
| 3 | 502_receipt_reader.js | 約10行変更 | リトライ回数増(3→4)、待機時間変更(固定5s→指数2-16s) |
注意事項
- 99_error_log シートの肥大化: appendRow は高速だが、数千行を超えると遅延の可能性。月次で古いログを手動削除、またはMAS-212(物理削除アーカイブ)と連携
- persistLog の失敗: ログ記録自体が例外を投げないよう try-catch で保護。console.error にフォールバック
- checkTimeout_ の精度:
new Date().getTime()ベースのため、GASのサーバー時計に依存。±数秒の誤差あり。50秒のマージンで対応 - Exponential Backoff の最大待機: 4回目のリトライで16秒待機。合計30秒のAPI待機が加わる可能性
関連ドキュメント
| 仕様書 | 関連箇所 |
|---|---|
| CLAUDE.md | clasp認証切れ時のエラー対応 |
| MAS-179 監査証跡の強化 | 99_error_log はMAS-179の簡易版として機能 |
人間が検討すべき事項
- エラー通知先(メール or Slack)の選定: 本案件ではスプレッドシート内ログのみ。外部通知はMAS-184(通知連携)で対応
- 99_error_log の保持期間とクリーンアップ方針
実装プロンプト(Claude Code 用)
Step 1: 永続ログ基盤
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 N-02 Step 1「永続ログ基盤」を実装してください。
## 実行前タスク
以下のファイルを読み込んでください:
1. `000_infra/004_utils.js` — Utils.logInfo, Utils.logError, Utils.toastResult の現在の実装(L232-257)
2. `CLAUDE.md`
3. `docs/dev/dev_mas-178_error_handling.md`
## 実装内容
### A: Utils.persistLog() の追加(004_utils.js)
Utils オブジェクトに `persistLog` メソッドを追加。99_error_log シートに [日時, レベル, 関数名, メッセージ, 詳細] を appendRow。シートが存在しなければ自動作成。
### B: Utils.logError() の拡張
既存の console.error 出力の後に `Utils.persistLog('ERROR', ...)` を呼び出し追加。
## 制約
- Utils.logInfo は変更しない(INFO レベルはログ量が多すぎるため永続記録しない)
- persistLog 内の例外は握りつぶす(無限ループ防止)
Step 2: 実行時間監視
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 N-02 Step 2「実行時間監視」を実装してください。
## 実行前タスク
以下のファイルを読み込んでください:
1. `600_report/602_datamart_main.js` — buildBudgetTrendDataMart() の全体構造(L157-348)。各ステップのログ出力箇所(L209-227)
2. `docs/dev/dev_mas-178_error_handling.md`
## 実装内容
buildBudgetTrendDataMart() の冒頭に startTime を記録。各ステップ間に checkTimeout_ を挿入。5分超過時は Utils.persistLog で記録後に throw。
## 制約
- 既存のステップログ(Utils.logInfo)はそのまま維持(checkTimeout_ 内に統合してもよい)
- タイムアウト閾値は 300000ms (5分) をハードコード。03_sys_params 化は後日
Step 3: API制限対策
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 N-02 Step 3「API制限対策」を実装してください。
## 実行前タスク
以下のファイルを読み込んでください:
1. `500_import/502_receipt_reader.js` — callGeminiForReceipt_() のリトライ処理(L213-226付近)
2. `docs/dev/dev_mas-178_error_handling.md`
## 実装内容
既存のリトライループを Exponential Backoff に置換:
- 最大リトライ回数: 4回
- 対応ステータス: 429, 503
- 待機時間: 2s → 4s → 8s → 16s
- Utils.logInfo でリトライ状況をログ出力
## 制約
- callGeminiForReceipt_ の関数シグネチャは変更しない
- 200以外のレスポンスの最終的な throw は維持
### 拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---------|:--------:|------|
| Step 1: 永続ログ | なし | appendRow + 自動シート作成のシンプルなパターン |
| Step 2: タイムアウト | なし | startTime + checkTimeout_ の単純な時間比較 |
| Step 3: Exponential Backoff | なし | Math.pow(2, retry) の定型パターン |
推奨実行モデル
| 工程 | 推奨モデル | 理由 |
|---|---|---|
| 仕様書作成(本ドキュメント) | Claude Opus 4.6 | 5つの課題の重大度評価、ロールバック可否の技術判断、段階的実装設計 |
| Step 1-3 実装 | Claude Haiku 4.5 | 各ステップとも定型パターンの実装。判断要素が少ない |
変更履歴
| 日付 | 変更内容 |
|---|---|
| 2026-04-14 | 初版作成 |