PopNowAPI ドキュメント
API で作る
本文

冪等性

タイムアウト後も、二重生成や二重課金を心配せずに再試行する方法。

冪等キーは、「注文 8814 の動画をレンダリングする」のような 1 つの仕事に付けるラベルです。同じラベルのリクエストが PopNow に 2 回届いても、仕事は 1 回しか行われません。

POST /api/v1/jobs/createTask
Idempotency-Key: order-8814-render-1

なぜ必要か

タイムアウトすると判断がつきません。リクエストが届かなかったのかもしれないし、届いたのに応答だけが失われたのかもしれません。キーがなければ、諦める(ユーザーを無駄に待たせる)か、再試行する(二重に生成・課金されるかもしれない)しかありません。キーがあれば再試行は常に安全で、新しいタスクができるか、直前に作られたタスクが返るかのどちらかです。

動作

  • 同じ API キーから同じキーで送り直すと、元のタスクが現在の状態で返ります。タスク、確保、課金が増えることはありません。
  • キーは送った内容(model、input 全体、callBackUrl)と結び付きます。フィールドの順序や空白は関係ありませんが、実質的な変更は別物です。変更したリクエストを古いキーで送ると 409 になるので、新しいキーを付けてください。
  • キーは最初に使ってから 24 時間記憶されます。それを過ぎると同じキーでも新しい有料のタスクが作られるので、自動再試行はそれよりずっと前に止めてください。
  • キーは 128 文字までで、アカウント内で一意であれば十分です。アカウントの別の API キーからは再利用できず、元のタスク ID なしの 409 が返ります。
エンドポイント同じキーで送り直すと…
POST /api/v1/jobs/createTask元のタスクを返す
POST /api/v1/jobs/retryその再試行で作られたタスクを返す(キーの範囲は元のタスクごと)
POST /v1/videos元のタスクを返す
POST /api/v1/assetsすでに保存済みのアセットを返す(ここではヘッダーが必須)

読み取り系や、アップロード・ダウンロード用リンクを発行するエンドポイントはこのヘッダーを無視します。jobs/purge には不要で、繰り返し呼んでも害はありません。

キーの選び方

  • 仕事 1 つにつきキー 1 つです。HTTP の試行ごとではありません。再試行のたびに同じものを使います。
  • すでに保存している値から作りましょう。order-8814-render-1 ならクラッシュ後も作り直せますが、ランダムな UUID は先に保存しておかない限り取り戻せません。
  • UUID を使うなら、最初の試行より前に生成し、対応する仕事と一緒に保存してください。

再試行ループの中でキーを作らないでください

// Wrong: a new key per attempt defeats the whole mechanism
for (let i = 0; i < 3; i++) await createTask(body, crypto.randomUUID());

// Right: one key, created once
const key = crypto.randomUUID();
for (let i = 0; i < 3; i++) await createTask(body, key);

衝突

以下はいずれも code: 409 を返します。

何が起きたか再試行する?
同じキーの 2 つのリクエストが同時に届いたする。数百ミリ秒待って同じキーで送り直す
そのキーが別のリクエストに使われていたしない。リクエストが変わったので新しいキーが必要
アカウントの別の API キーがこのキーを使っていたしない。最初に試した API キーで再試行する

msg で分岐すべきではないので、409 の再試行は 1〜2 回にとどめ、その後は人が確認できるようエラーを表に出してください。

完全な例

JavaScript
const RETRYABLE = new Set([429, 500, 503, 50301]); // business codes from the body

async function createTaskOnce(body, idempotencyKey) {
  for (let attempt = 0; attempt < 4; attempt++) {
    try {
      const res = await fetch('https://api.popnow.ai/api/v1/jobs/createTask', {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${process.env.POPNOW_API_KEY}`,
          'Content-Type': 'application/json',
          'Idempotency-Key': idempotencyKey,
        },
        body: JSON.stringify(body),
      });
      const json = await res.json();
      if (json.code === 200) return json.data; // a new task, or the original one
      if (!RETRYABLE.has(json.code)) throw new Error(`${json.code} ${json.msg} (${json.request_id})`);
    } catch (err) {
      if (!(err instanceof TypeError)) throw err; // network error: safe to retry with the same key
    }
    await new Promise((r) => setTimeout(r, 2 ** attempt * 500 + Math.random() * 300));
  }
  throw new Error('createTask did not succeed after 4 attempts');
}

const task = await createTaskOnce(
  { model: 'alibaba/z-image-turbo/text-to-image', input: { prompt: 'A paper boat on a calm lake' } },
  'order-8814-render-1',
);

このページの内容