タグ: リトライ設計

  • WordPressへの下書き登録がタイムアウトしたら:再送せずに登録済みかを照合して再開する

    WordPressへの下書き登録がタイムアウトしたら:再送せずに登録済みかを照合して再開する

    記事をスクリプトやAIエージェントでWordPressへ下書き登録していると、登録の途中でタイムアウトのエラーが出ることがあります。同じコマンドをもう一度流したくなりますが、WordPress側では下書きができていて、返事だけが届かなかったのかもしれません。そのまま送り直すと、同じ記事の下書きが2つできることがあります。

    このサイトの下書き登録処理を、WordPressの代わりに応答する疑似のサイトにつないで確かめました。同じスラッグの再実行は、記事も画像も増やさずにスキップしました。ただし、このスキップは「前回の下書きが検索で見つかる」ことを前提にしています。登録は済んだのにまだ検索に出ない状態で、送る前に残した記録を消して再実行すると、2件目の作成要求が送られました。記録が残っていれば、処理は送り直さずに止まりました。

    そこでこの記事では、タイムアウトを「失敗」ではなく「結果不明」として残し、再送する前に登録済みかを照合して再開する手順と、それを支える記録の持ち方、AIに投稿処理を作らせるときのテスト条件をまとめます。読み終えたら、自分の投稿処理が結果不明のときに止まるかを確かめ、タイムアウトのあとに送り直してよいかを判断できます。

    タイムアウトは「失敗」ではなく「結果不明」

    作成の要求を送ってタイムアウトしたとき、WordPress側で起きている可能性は2つあります。要求が届かず何も作られていないか、下書きは作られて応答だけが失われたかです。手元のエラーからは、どちらかを区別できません。

    そのため、投稿の結果は「作成」「スキップ」の2つではなく、「結果不明」を加えた3つで記録します。結果不明の記事は、WordPress側で見つかるかを照合するまで、次の行動を決めません。

    結果 意味 次にすること
    作成 応答で投稿IDが返った 投稿IDを記録し、管理画面で表示を確認する
    スキップ 同じ記事が既にある 上書きせずに次の記事へ進む
    結果不明 要求は送ったが応答がない 送り直さず、WordPress側と記録を照合する

    このサイトの下書き登録の流れ

    このサイトでは、記事のHTMLと画像をスクリプトでWordPressへ下書き登録しています。CodexやClaude Codeに作業を任せるときの共通指示にも、「通信の結果が分からない操作は、記録を残して盲目的に再送しない」と書いています。登録の流れは次のとおりです。

    1. 事前確認:通信せずに、記事のタイトル・スラッグ・抜粋・タグ・カテゴリ・画像の参照を検査する。

    2. 権限確認:投稿と画像アップロードの権限がある利用者かを確かめる。

    3. 同時実行の防止:登録処理が2つ同時に動かないよう、作業用の目印(ロック)を置く。目印が残っていたら、前回の処理が途中で止まった可能性があるので、自動では消さずに止まる。

    4. 記録とWordPressの照合:記事ごとに、前回の記録とWordPress側の投稿を照らし合わせる(下の表)。

    5. 送る前の記録:作成の要求を送る直前に「送信中」と記録し、要求を1回だけ送る。応答で投稿IDが返ったら「完了」と投稿IDを記録する。画像も、ファイルごとに同じ記録を持つ。

    6. 管理画面での確認:下書きのレイアウトと画像の表示を人が目で確かめてから公開する。

    再実行したときの判断は、記録とWordPress側の検索結果の組み合わせで決まります。

    前回の記録 WordPress側 処理
    なし 見つかる スキップ:上書きしない
    なし 見つからない 作成:「送信中」を記録してから送る
    送信中 見つかる 照合して完了:送り直さずに記録を直す
    送信中 見つからない 止める:管理画面で確認する
    完了 記録の投稿IDで照会 照合:投稿IDとスラッグが合わなければ止める

    4行目が、タイムアウトの直後に起きうる状態です。下書きが作られたのに、まだ検索に出ていない、あるいは検索の条件から外れている。このとき「見つからないから作る」と判断すると、2件目ができます。

    疑似のサイトで、応答を失ったあとの再実行を確かめる

    このサイトの登録処理を、通信しない疑似のWordPressにつなぎ、5つの場面を1回ずつ動かしました(2026年10月21日)。疑似のWordPressは、作成の要求を受けて下書きを作ったあとで応答だけを失う動きと、作った下書きを検索に出さない動きを切り替えられるようにしています。

    場面 再実行の結果 作成要求の回数
    通常どおり登録したあとに再実行 スキップ。下書き1件、画像のアップロード1回のまま 1回
    応答を失い、再実行のときは検索で見つかる 照合して完了。送り直さない 1回
    応答を失い、再実行のときもまだ検索に出ない 「前回の投稿結果が不明…管理画面で確認してください」で止まる 1回
    3つ目と同じ状態で、送る前の記録を消して再実行 見つからないので作成し、2件目の要求を送る 2回
    画像のアップロードで応答を失い、再実行 「前回の画像アップロード結果が不明です」で止まる 0回
    送る前の記録がない再実行は、まだ検索に出ない下書きを見落として2件目を作り、記録が送信中のまま残っている再実行は止まることを並べた図
    同じ状態からの再実行でも、送る前の記録があるかどうかで、2件目を作るか止まるかが分かれました。

    同じスラッグのスキップは、1つ目と2つ目の場面では二重投稿を防いでいます。けれども4つ目の場面のように、前回の下書きが検索で見つからなければ、スキップは働きません。3つ目と4つ目の違いは、送る前に残した「送信中」の記録だけです。記録が残っていたので、処理は「見つからない」を「まだ作られていない」と取り違えずに止まりました。

    運用で守ることは2つです。止まったら、WordPressの管理画面で下書きの一覧とメディアライブラリを開き、記録と照らしてから再開すること。そして、止まった記録を消して再実行しないことです。記録を消すのは、4つ目の場面を自分で作ることになります。

    手元で確かめるコード

    自分の投稿処理で同じ判断ができているかを考えるときは、次のPythonで状況を作ってみてください。WordPressの代わりにメモリ上の登録先を使い、通信もファイルの書き込みもしません。Python 3.10以降の標準機能だけで動きます。

    # WordPressの代わりに、メモリ上の登録先と作業記録で「応答だけを失う」状況を作る。
    # 通信もファイルの書き込みもしない。
    class Site:
        def __init__(self):
            self.posts = []          # 作られた下書き(同じスラッグでも別の投稿になる)
            self.hidden = set()      # 登録直後で、まだ検索に出ないスラッグ
    
        def create(self, slug, body, lose_response=False):
            self.posts.append((slug, body))
            if lose_response:
                raise TimeoutError("登録は済んだが応答が届かない")
    
        def find(self, slug, can_query=True):
            if not can_query:
                raise ConnectionError("照会できない")
            if slug in self.hidden:
                return None
            return next((p for p in self.posts if p[0] == slug), None)
    
    
    # 1. 無条件の再実行:タイムアウトしたら、もう一度送る
    site = Site()
    for _ in range(2):
        try:
            site.create("first-article", "本文", lose_response=True)
            break
        except TimeoutError:
            pass
    print(f"無条件の再実行: 下書き{len(site.posts)}件")
    assert len(site.posts) == 2
    
    
    # 2. 送る前に記録を残し、再実行では先に照合する
    def post_draft(site, journal, slug, body, lose_response=False, can_query=True):
        if journal.get(slug) == "送信中":
            try:
                found = site.find(slug, can_query)
            except ConnectionError:
                return "照会できないので停止"
            if found is None:
                return "見つからないので停止(管理画面で確認)"
            if found[1] != body:
                return "内容が違うので停止(管理画面で確認)"
            journal[slug] = "完了"
            return "登録済みを照合して完了"
        if site.find(slug, can_query) is not None:
            return "既にあるのでスキップ"
        journal[slug] = "送信中"
        try:
            site.create(slug, body, lose_response)
        except TimeoutError:
            return "結果不明(記録は送信中のまま)"
        journal[slug] = "完了"
        return "作成"
    
    
    site, journal = Site(), {}
    print("1回目: " + post_draft(site, journal, "first-article", "本文", lose_response=True))
    print("照会できない: " + post_draft(site, journal, "first-article", "本文", can_query=False))
    site.hidden.add("first-article")
    print("まだ検索に出ない: " + post_draft(site, journal, "first-article", "本文"))
    site.hidden.clear()
    print("検索に出た: " + post_draft(site, journal, "first-article", "本文"))
    print("もう一度実行: " + post_draft(site, journal, "first-article", "本文"))
    print(f"照合する方式: 下書き{len(site.posts)}件")
    assert len(site.posts) == 1 and journal["first-article"] == "完了"
    
    # 送信中のまま本文を直して再実行すると、照合で止まる
    site, journal = Site(), {}
    post_draft(site, journal, "second-article", "直す前の本文", lose_response=True)
    print("本文を直して再実行: " + post_draft(site, journal, "second-article", "直した本文"))
    assert len(site.posts) == 1
    
    
    # 3. 記録を持たずに、検索だけで判断すると
    site = Site()
    try:
        site.create("first-article", "本文", lose_response=True)
    except TimeoutError:
        pass
    site.hidden.add("first-article")
    if site.find("first-article") is None:
        site.create("first-article", "本文")
    print(f"検索だけで判断: 下書き{len(site.posts)}件")
    assert len(site.posts) == 2
    無条件の再実行: 下書き2件
    1回目: 結果不明(記録は送信中のまま)
    照会できない: 照会できないので停止
    まだ検索に出ない: 見つからないので停止(管理画面で確認)
    検索に出た: 登録済みを照合して完了
    もう一度実行: 既にあるのでスキップ
    照合する方式: 下書き1件
    本文を直して再実行: 内容が違うので停止(管理画面で確認)
    検索だけで判断: 下書き2件

    無条件に送り直す方式と、検索だけで判断する方式は、どちらも下書きが2件になりました。送る前に記録を残して照合する方式は、照会できないとき、まだ検索に出ないとき、送信中のまま本文を直したときに止まり、検索に出てから完了しました。止まる分岐では作成の要求を送らないので、結果不明から2件目へは進みません。

    記録で守れる範囲と、あわせて決めること

    送る前の記録は、照合の手がかりを再実行まで持ち越すためのものです。次の条件もあわせて決めておきます。

    • 記録の置き場所:処理が終わっても、PCを再起動しても残る場所に置きます。メモリ上の記録は、処理が終われば消えます。このサイトでは、記事ごとの記録を作業フォルダ内の復旧用の記録として保存しています。

    • 記事を見分ける識別子:通信が失敗したという理由で、スラッグや記事のIDを作り直さないことです。要求のたびに新しいIDを作ると、照合の手がかりがなくなります。内容を直して別の記事として出し直すときだけ、識別子を変えます。

    • 同時実行:照合と作成のあいだに別の処理が同じ記事を登録すると、照合は意味を失います。登録処理は1つずつ動かします。

    • 照会の遅れと検索の条件:作った直後の下書きが検索に出るまでの時間や、検索が対象にする投稿の状態(下書き・予約・非公開など)は、登録先の仕様と設定で変わります。見つからないことだけを理由に送り直しません。

    • 登録先の保証:登録先が「同じ要求を一度だけ処理する」仕組み(重複防止のキー)と照会を保証していれば、送る側の照合を軽くできます。保証がなければ、結果不明の記事は人の確認へ回します。

    監視や通知には、失敗の件数だけでなく、結果不明で止まった件数と、確認が必要な記事の名前を出しておくと、止まったことに気づけます。

    AIに投稿処理を作らせるときのテスト条件と依頼文

    CodexやClaude Codeに投稿処理を作らせるときは、次の3つをテストに入れるよう頼みます。

    1. 登録は済んだが応答だけを失う(再実行で、送り直さずに照合して完了する)。

    2. 再実行のときに照会できない、またはまだ検索に出ない(作成せずに止まる)。

    3. 送信中のまま、同じスラッグで内容が違う記事を再実行する(上書きせずに止まる)。

    依頼文には、たとえば次の一文を添えます。

    最初はWordPressを模擬した登録先で、通信せずに状態の移り変わりをテストしてください。作成の要求を送る前に「送信中」を記録し、結果が分からない記事を、照合せずに送り直す処理は作らないでください。上の3条件をテストに入れ、エラーが消えるまで繰り返す実装にはしないでください。

    AIの生成の要求を再試行することと、生成した記事をWordPressへ登録し直すことは、別の処理です。前者は失敗しても記事が増えませんが、後者は増えます。再試行の仕組みは、登録の側で個別に決めます。

    参考にした公式情報