0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

失敗したジョブを自動でリトライ&復旧させる「不死身のバックエンド」の作り方

0
Posted at

1. はじめに

別記事では、非同期ジョブキューを使ってタイムアウトを回避する方法を紹介しました。
しかし、バックグラウンド処理には新たな敵が待ち受けています。
「予期せぬエラー」 です。

外部APIの瞬断、一時的なネットワーク不良、リソース不足…。
長い処理を実行していると、コードが正しくても失敗することは避けられません。

今回は、そんなエラーが発生しても自動で立ち上がり、ユーザーに気づかれないように復旧する 「不死身のバックエンド」 の作り方を紹介します。

2. 直面した課題:エラー即死

当初の実装は単純でした。
「エラーが出たら status = 'FAILED' に更新して終了」。

しかし、これには問題がありました。
YouTube APIなどは時々 500 Server Error を返すことがあります。
たった1回のエラーで「失敗しました」と表示されると、ユーザーは「このアプリは壊れている」と感じてしまいます。
さらに、失敗したジョブがDBに残り続け、いつの間にかゴミだらけになっていました。

3. 解決策:3つの不死身ロジック

そこで、以下の3つの機能を持った「リカバリーシステム」を実装しました。

  1. 自動リトライ (Auto Retry): 3回までは無言でやり直す。
  2. エラー隠蔽 (Error Masking): リトライ中はユーザーに「失敗」を見せない。
  3. 自動お掃除 (Auto Cleanup): 古いジョブや完了したジョブを自動で消す。

4. 実装の詳細

4.1 データベース設計(ステータス管理)

まず、ジョブを管理するテーブルに retry_count カラムを追加します。

ALTER TABLE job_queue ADD COLUMN retry_count INT DEFAULT 0;

ステータスの定義も少し見直しました。

  • PENDING: 待ち行列に入っている。
  • PROCESSING: 現在実行中。
  • FAILED: エラーで止まった(リトライ候補)。
  • COMPLETED: 完了。

4.2 リカバリーサービスのロジック

バックグラウンドで常に動く「リカバリー担当(Recovery Service)」を作ります。
このサービスは、定期的に(例えば1分おきに)DBをパトロールし、倒れているジョブを見つけて助け起こします。

具体的なPython風疑似コードは以下のようになります。

async def recovery_patrol():
    # 1. 失敗したジョブを探す
    failed_jobs = await db.select("*").table("job_queue").eq("status", "FAILED").execute()

    for job in failed_jobs:
        if job['retry_count'] < 3:
            # リトライ上限未満なら処理を実行
            print(f"Rescuing job {job['id']}...")
            
            # ステータスをPROCESSINGに戻し、リトライ回数を増やす
            await db.table("job_queue").update({
                "status": "PROCESSING",
                "retry_count": job['retry_count'] + 1,
                "error_message": None # エラーログは消して再挑戦
            }).eq("id", job['id']).execute()
            
        else:
            # もう助からない(リトライ上限到達)
            print(f"Giving up on job {job['id']}...")
            # ここで初めてユーザーに「失敗」として扱う(あるいは削除する)
            await db.table("job_queue").delete().eq("id", job['id']).execute()

4.3 エラー隠蔽(Error Masking)

ここがUX上の重要なポイントです。
フロントエンドへ状態を返すAPIでは、以下のような嘘をつきます。

if job['status'] == 'FAILED' and job['retry_count'] < 3:
    # 実は失敗しているが、リトライ中なので「処理中」と伝える
    return {"status": "PROCESSING", "message": "システム調整中..."}
else:
    # 本当のステータスを返す
    return {"status": job['status']}

これにより、ユーザーは裏でエラーが起きてリトライしていることに気づかず、「ちょっと時間がかかっているな」程度にしか感じません。

4.4 結果の同期(State Sync)

さらに、「同じ動画を分析したい人が複数いた場合」 の最適化も行いました。
Aさんが動画Xの分析をリクエストし、その処理中にBさんが同じ動画Xをリクエストした場合、Bさんに新しいジョブを作る必要はありません。

  1. 新規リクエスト時に、同じ video_id で処理中のジョブがないか確認。
  2. あれば、そのジョブIDをBさんにも返す。
  3. AさんとBさんは同じジョブの完了を待つことになる(SSEで同時に通知を受け取る)。

これでサーバーのリソースを大幅に節約できます。

5. 運用効果

この「不死身システム」を入れてから、以下の効果がありました。

  • エラーの予防: 一時的なAPIエラーで止まることを防止でき、再度リトライすることで一時的なエラーを回収する。
  • DBがクリーン: 自動クリーンアップも入れているため(作成から20分以上経過したジョブは強制削除)、メンテナンスフリーで運用できています。

6. おわりに

個人開発では、完璧なコードを書くことよりも 「失敗しても勝手に直る仕組み」 を作ることの方が重要だとどこかで見たことがあったので、堅牢性というものを意識して作ってみました。

皆さんもぜひ、自分のアプリに「自動リトライ」を組み込んで、不死身のバックエンドを作ってみてください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?