APIのサーバー側でタイムアウトを適切に判定するには、API全体のタイムアウト管理と個別の処理(外部API・内部処理)のタイムアウト管理を適切に設計する必要があります。本記事では、タイムアウト判定を行う場所と方法について詳しく解説します。
1. タイムアウト判定を行う場所
① API全体のタイムアウト管理
判定するタイミング:
- 処理の開始時に 開始時間を記録($startTime = microtime(true);)
- 各処理の前に 残り時間を計算(getRemainingTime()で確認)
- 残り時間が 0 以下 になったら 即エラーを返す
② 外部API呼び出し時(リトライあり)
判定するタイミング:
- 外部APIのレスポンスが 指定時間を超えた場合
- curl_setopt($ch, CURLOPT_TIMEOUT, min(getRemainingTime(), 5));を設定し、API全体のタイムアウトを考慮
- リトライ時に getRemainingTime()を確認
phpコピーする編集するif (getRemainingTime() <= 0) { 
    throw new TimeoutException("External API timeout.", "external_api"); 
}
③ 内部処理(データベース処理やロジック処理)(リトライなし)
判定するタイミング:
- 処理開始前に残り時間を確認
phpコピーする編集するif (getRemainingTime() <= 0) { 
    throw new TimeoutException("Internal processing timeout.", "internal_process"); 
}
- データベース接続時に PDO::ATTR_TIMEOUTを設定
phpコピーする編集するPDO::ATTR_TIMEOUT => min(getRemainingTime(), 5);
2. タイムアウト判定の実装
以下のコードでは、各処理の前に getRemainingTime() を呼び出し、残り時間が 0 以下なら即エラーを返します。
API全体の処理
phpコピーする編集するheader("Content-Type: application/json");
define("TOTAL_TIMEOUT", 10); // API全体のタイムアウト(秒)
define("EXTERNAL_API_MAX_RETRIES", 3); // 外部APIの最大リトライ回数
$startTime = microtime(true); // 開始時間
set_time_limit(TOTAL_TIMEOUT); // PHPスクリプトの最大実行時間を設定
try {
    // 外部APIの呼び出し(リトライあり & タイムアウト監視)
    $externalData = callExternalApiWithRetry("https://api.example.com/data", EXTERNAL_API_MAX_RETRIES);
    // 内部処理の実行(リトライなし & タイムアウト監視)
    $internalData = executeInternalProcessing();
    // 全体のレスポンスを返す
    response(200, ["external" => $externalData, "internal" => $internalData]);
} catch (TimeoutException $e) {
    response(504, [
        "status" => "error",
        "message" => "Gateway Timeout: " . $e->getMessage(),
        "error_source" => $e->getSource()
    ]);
} catch (Exception $e) {
    response(500, [
        "status" => "error",
        "message" => "Internal Server Error: " . $e->getMessage(),
        "error_source" => "unknown"
    ]);
}
外部API呼び出し時(リトライあり & タイムアウト監視)
phpコピーする編集するfunction callExternalApiWithRetry($url, $maxRetries)
{
    global $startTime;
    $attempt = 0;
    while ($attempt < $maxRetries) {
        $attempt++;
        try {
            // API全体の残り時間をチェック
            if (getRemainingTime() <= 0) {
                throw new TimeoutException("API request exceeded total timeout.", "external_api");
            }
            $ch = curl_init($url);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($ch, CURLOPT_TIMEOUT, min(getRemainingTime(), 5)); // 外部APIのタイムアウト
            curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 3);
            $response = curl_exec($ch);
            $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
            curl_close($ch);
            if ($httpCode === 200 && $response) {
                return json_decode($response, true);
            }
            throw new Exception("HTTP $httpCode - API response error");
        } catch (Exception $e) {
            if ($attempt >= $maxRetries) {
                throw new TimeoutException("External API failed after retries.", "external_api");
            }
            sleep(1); // 1秒待機後に再試行
        }
    }
}
内部処理(リトライなし & タイムアウト監視)
phpコピーする編集するfunction executeInternalProcessing()
{
    // API全体の残り時間をチェック
    if (getRemainingTime() <= 0) {
        throw new TimeoutException("Internal processing exceeded total timeout.", "internal_process");
    }
    try {
        $pdo = new PDO("mysql:host=localhost;dbname=testdb", "user", "password", [
            PDO::ATTR_TIMEOUT => min(getRemainingTime(), 5),
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
        ]);
        $stmt = $pdo->prepare("SELECT * FROM users WHERE active = 1");
        $stmt->execute();
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    } catch (PDOException $e) {
        throw new TimeoutException("Database Error: " . $e->getMessage(), "internal_process");
    }
}
タイムアウト計測
phpコピーする編集するfunction getRemainingTime()
{
    global $startTime;
    return max(0, TOTAL_TIMEOUT - (microtime(true) - $startTime));
}
タイムアウト発生時のエラーハンドリング
phpコピーする編集するclass TimeoutException extends Exception
{
    private $source;
    public function __construct($message, $source)
    {
        parent::__construct($message);
        $this->source = $source;
    }
    public function getSource()
    {
        return $this->source;
    }
}
3. タイムアウト判定のまとめ
| 判定する処理 | どこで判定する? | どのように判定する? | エラーメッセージ | 
|---|---|---|---|
| API全体のタイムアウト | 各処理の開始前 | getRemainingTime() <= 0 | "API request exceeded total timeout." | 
| 外部APIのレスポンスが遅い | cURLオプション | CURLOPT_TIMEOUT = min(getRemainingTime(), 5) | "External API failed after retries." | 
| 内部処理(DB処理含む)が遅い | 処理開始前 & DB接続 | PDO::ATTR_TIMEOUT = min(getRemainingTime(), 5) | "Internal processing exceeded total timeout." | 
4. まとめ
- API全体のタイムアウト (TOTAL_TIMEOUT) を統一し、各処理がAPI全体のタイムアウトを超えないようにする
- getRemainingTime()を使い、各処理の前に残り時間を確認し、超えていたら即エラーを返す
- 外部APIのタイムアウトは CURLOPT_TIMEOUTで設定し、最大リトライ回数 (EXTERNAL_API_MAX_RETRIES) を制御
- 内部処理は PDO::ATTR_TIMEOUTを使い、リトライなしで処理
- タイムアウト発生時のエラーメッセージに error_sourceを追加し、どこで発生したかを明確にする
この実装により、クライアント側から見ても統一されたレスポンスになりつつ、内部でのタイムアウト発生場所も特定できるようになります。
コメントを残す