1. はじめに:「読み込み中...」で固まる画面

ユーザーから「アプリが固まって動かない」というクレームが殺到しました。確認すると、外部APIを呼び出す画面で30秒以上フリーズしていました。

原因は、ジェンスパーク(Genspark)が生成したAPI呼び出しコードにタイムアウト設定が完全に欠けていたことでした。外部APIが応答しない場合、プログラムは永遠に待ち続け、ユーザーは何もできない状態に陥っていたのです。

この記事で学べること

  • タイムアウト処理が欠けることによる実際の被害
  • AIが見落としやすい4つのタイムアウトポイント
  • 言語・ライブラリ別の正しいタイムアウト実装
  • 適切なタイムアウト値の決定方法
  • タイムアウト後の復旧戦略

2. 事件の詳細:外部API呼び出しのタイムアウト漏れ

ジェンスパーク(Genspark)が生成した問題のコードを見てみましょう。

2.1 問題のコード

# ⚠️ 危険:タイムアウト設定なし
import requests

def fetch_user_profile(user_id):
    """外部APIからユーザープロフィールを取得"""
    url = f"https://api.example.com/users/{user_id}"
    
    # タイムアウト設定なし!
    response = requests.get(url)
    
    if response.status_code == 200:
        return response.json()
    else:
        return None

2.2 何が問題だったか

このコードはrequests.get()にtimeoutパラメータを指定していません。その結果:

  • 外部APIが応答しない場合、無限に待ち続ける
  • ユーザーは画面がフリーズしたと感じる
  • サーバーリソースが無駄に消費される
  • 他の処理もブロックされる可能性

2.3 実際の被害

フリーズの影響

  • ユーザークレーム:47件「アプリが動かない」
  • 平均フリーズ時間:28.3秒
  • 離脱率:23%上昇
  • アプリストア評価:4.2 → 3.8に低下

2.4 なぜローカル環境で気づかなかったか

開発環境では外部APIが常に高速で応答していたため、タイムアウトの必要性に気づきませんでした。本番環境では、APIサーバーの一時的な障害や、ネットワークの遅延が発生し、問題が顕在化したのです。

3. タイムアウト処理が重要な理由

タイムアウト処理は防御的プログラミングの基本です。

3.1 外部依存の不確実性

外部システム(API、データベース、ファイルシステム)は常に正常に応答するとは限りません

  • サーバーの一時的な障害
  • ネットワークの遅延・切断
  • 過負荷によるレスポンス遅延
  • DDoS攻撃などのセキュリティ問題

3.2 リソースの有効活用

タイムアウトがないと、限られたリソースが無駄に消費されます:

  • スレッド/プロセス:待機状態で占有される
  • メモリ:接続情報が保持され続ける
  • ネットワーク接続:コネクションプールが枯渇

3.3 ユーザーエクスペリエンスの向上

適切なタイムアウトにより、迅速なエラーハンドリングが可能になります:

  • 数秒で「現在利用できません」と通知
  • 代替手段(キャッシュデータ)を提供
  • リトライオプションの提示

4. AIが見落とす4つのタイムアウトポイント

ジェンスパーク(Genspark)などのAIがタイムアウト設定を忘れやすい場所を解説します。

4.1 ポイント1:HTTP/APIリクエスト

最も一般的な見落としポイントです。

見落としパターン

  • requests.get() / requests.post() にtimeoutなし
  • urllib.request.urlopen() にtimeoutなし
  • axios / fetch にtimeoutなし

4.2 ポイント2:データベースクエリ

複雑なクエリや大量データの取得時に予想以上の時間がかかる場合があります。

4.3 ポイント3:ファイルI/O

ネットワークドライブや遅いストレージへのアクセスでフリーズすることがあります。

4.4 ポイント4:外部プロセス実行

subprocess.run() や os.system() で外部コマンドが終了しない場合があります。

5. 正しいタイムアウト実装:言語・ライブラリ別

各言語・ライブラリでの正しいタイムアウト実装方法をご紹介します。

5.1 Python - requests

# ✅ 正しいタイムアウト設定
import requests
from requests.exceptions import Timeout, RequestException

def fetch_user_profile(user_id, timeout=5):
    """外部APIからユーザープロフィールを取得"""
    url = f"https://api.example.com/users/{user_id}"
    
    try:
        # 接続タイムアウト3秒、読み取りタイムアウト5秒
        response = requests.get(url, timeout=(3, 5))
        response.raise_for_status()
        return response.json()
    except Timeout:
        logger.error(f"Timeout fetching user {user_id}")
        return None
    except RequestException as e:
        logger.error(f"Error: {e}")
        return None

5.2 Python - データベース

import pymysql

# 接続時にタイムアウト設定
conn = pymysql.connect(
    host='localhost',
    user='user',
    password='pass',
    database='db',
    connect_timeout=5, # 接続タイムアウト
    read_timeout=10, # 読み取りタイムアウト
    write_timeout=10 # 書き込みタイムアウト
)

5.3 JavaScript - fetch API

// ✅ AbortControllerでタイムアウト実装
async function fetchUserProfile(userId, timeout = 5000) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeout);

  try {
    const response = await fetch(
      `https://api.example.com/users/${userId}`,
      { signal: controller.signal }
    );
    clearTimeout(timeoutId);
    return await response.json();
  } catch (error) {
    if (error.name === 'AbortError') {
      console.error('Fetch timeout');
    }
    return null;
  }
}

5.4 Python - subprocess

import subprocess

try:
    # 外部コマンドに10秒のタイムアウト
    result = subprocess.run(
        ['external_command', 'arg1'],
        timeout=10,
        capture_output=True
    )
except subprocess.TimeoutExpired:
    logger.error("Command timeout")
    # 強制終了処理

6. タイムアウト設定のベストプラクティス

適切なタイムアウト値の決定方法と運用戦略をご紹介します。

6.1 タイムアウト値の決定方法

推奨タイムアウト値

  • API呼び出し:5-10秒(高速APIは3秒)
  • データベースクエリ:10-30秒(複雑なクエリは60秒)
  • ファイルダウンロード:60-300秒(サイズに応じて)
  • 外部プロセス:30-120秒(処理内容に応じて)

6.2 段階的タイムアウト

複数の段階でタイムアウトを設定します:

  • 接続タイムアウト:短め(3-5秒)
  • 読み取りタイムアウト:中程度(5-10秒)
  • 全体タイムアウト:長め(10-30秒)

6.3 リトライ戦略

import time
from requests.exceptions import Timeout

def fetch_with_retry(url, max_retries=3, timeout=5):
    """タイムアウト時に自動リトライ"""
    for attempt in range(max_retries):
        try:
            response = requests.get(url, timeout=timeout)
            return response.json()
        except Timeout:
            if attempt < max_retries - 1:
                wait_time = 2 ** attempt # 指数バックオフ
                logger.warning(f"Retry {attempt + 1}, wait {wait_time}s")
                time.sleep(wait_time)
            else:
                raise
    return None

6.4 フォールバック戦略

タイムアウト発生時の代替手段を用意します:

  • キャッシュデータ:古いデータでも表示
  • デフォルト値:安全なデフォルト値を返す
  • ユーザー通知:状況を明確に伝える

6.5 監視とアラート

監視すべきメトリクス

  • タイムアウト発生率:全リクエストの何%がタイムアウトしたか
  • 平均レスポンスタイム:タイムアウト値に近づいていないか
  • リトライ回数:過度なリトライが発生していないか

7. まとめ:防御的プログラミングの重要性

タイムアウト処理の欠如は、ユーザーエクスペリエンスを著しく損なう重大な問題です。

5つの教訓

  1. 全ての外部呼び出しにタイムアウト:API、DB、ファイルI/O全て
  2. 適切な値設定:処理内容に応じた現実的な値
  3. 段階的タイムアウト:接続・読み取り・全体で設定
  4. リトライ戦略:指数バックオフで自動リトライ
  5. 監視とアラート:タイムアウト発生を常に監視

ジェンスパーク(Genspark)が生成するコードは「ハッピーパス」のみを想定しがちです。エラーケース、タイムアウト、ネットワーク障害など、現実世界の不確実性を考慮した防御的プログラミングが不可欠です。

関連記事

参考リンク