APIにおけるタイムアウト判定の実装と方法

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 を追加し、どこで発生したかを明確にする

この実装により、クライアント側から見ても統一されたレスポンスになりつつ、内部でのタイムアウト発生場所も特定できるようになります。

Comments

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です