外部処理と内部処理を合わせたタイムアウトの判定

APIの処理において、外部サービスとの通信や内部処理の実行が遅延した場合に、どこでタイムアウトが発生したのかを明確に判別できる仕組みを構築することが重要です。本記事では、外部APIのリトライありタイムアウト、内部処理のリトライなしタイムアウトを統一的に管理し、エラーの発生箇所を特定できる方法を紹介します。


1. 設計方針

API全体のタイムアウトを統一し、外部APIアクセスと内部処理の制御を行います。

基本方針

  1. API全体のタイムアウトを統合
    • TOTAL_TIMEOUT = 10秒 で、外部APIと内部処理を含めた全体のタイムアウトを設定。
  2. 外部APIはリトライあり
    • EXTERNAL_API_MAX_RETRIES = 3(最大3回リトライ)。
    • 1回のAPI呼び出しのタイムアウトは min(残り時間, 5秒) で設定。
  3. 内部処理はリトライなし
    • PDO::ATTR_TIMEOUT を設定し、リクエストが全体の残り時間を超えないようにする。
  4. タイムアウト発生時に発生箇所を明確化
    • 外部APIでタイムアウトerror_source: "external_api"
    • 内部処理でタイムアウトerror_source: "internal_process"
    • API全体でタイムアウトerror_source: "api_timeout"

2. タイムアウトを統合したPHPコード

以下のコードでは、外部APIと内部処理の両方にタイムアウト制御を導入し、どこで問題が発生したのかを特定できるようにしています。

phpコピーする編集するheader("Content-Type: application/json");

define("TOTAL_TIMEOUT", 10); // API全体のタイムアウト(秒)
define("EXTERNAL_API_MAX_RETRIES", 3); // 外部APIの最大リトライ回数

$startTime = microtime(true); // APIの開始時間
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"
    ]);
}

/**
 * 外部サービスの呼び出し(リトライあり & 残り時間考慮)
 */
function callExternalApiWithRetry($url, $maxRetries)
{
    global $startTime;
    $attempt = 0;

    while ($attempt < $maxRetries) {
        $attempt++;
        try {
            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秒待機後に再試行
        }
    }
}

/**
 * 内部処理(リトライなし & 残り時間考慮)
 */
function executeInternalProcessing()
{
    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");
    }
}

/**
 * 残り時間を計算(API全体のタイムアウトを超えないように)
 */
function getRemainingTime()
{
    global $startTime;
    return max(0, TOTAL_TIMEOUT - (microtime(true) - $startTime));
}

/**
 * 正常なレスポンスを返す
 */
function response($statusCode, $data)
{
    http_response_code($statusCode);
    echo json_encode($data);
    exit;
}

/**
 * タイムアウト用のカスタム例外クラス
 */
class TimeoutException extends Exception
{
    private $source;

    public function __construct($message, $source)
    {
        parent::__construct($message);
        $this->source = $source;
    }

    public function getSource()
    {
        return $this->source;
    }
}

3. 仕組みのポイント

  1. API全体のタイムアウト管理
    • set_time_limit(TOTAL_TIMEOUT); でスクリプト全体の最大実行時間を設定。
    • getRemainingTime() を使用し、外部APIと内部処理の両方がAPI全体のタイムアウトを超えないよう制御。
  2. タイムアウトの発生場所を特定
    • 外部APIでタイムアウト → error_source: "external_api"
    • 内部処理でタイムアウト → error_source: "internal_process"
    • API全体のタイムアウト → error_source: "api_timeout"
  3. 外部APIのリトライ時にもAPI全体の残り時間を考慮
    • curl_setopt($ch, CURLOPT_TIMEOUT, min(getRemainingTime(), 5));
    • 最大3回リトライし、それでも失敗したら 504 Gateway Timeout を返す。
  4. 内部処理はリトライなし & タイムアウト即終了
    • PDO::ATTR_TIMEOUT => min(getRemainingTime(), 5);
    • getRemainingTime() をチェックし、時間切れなら即 504 Gateway Timeout を返す。

4. まとめ

  • API全体のタイムアウト管理を統一し、外部API・内部処理を統合。
  • タイムアウト発生時に、外部APIが原因か内部処理が原因かを明確に判別可能。
  • エラーレスポンスに error_source を追加し、クライアント側が適切な対応を取れるようにする。

この方法を適用することで、APIの安定性を高め、エラー発生時のトラブルシューティングを容易にすることができます。

Comments

コメントを残す

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