1. 問題の概要

Django 環境で transaction.on_commit() を活用して Celery タスクを呼び出す構造において,
ManyToMany フィールドのデータが Celery タスク内では空である現象が発生。


2. 現象

  • post.categories.add(...), post.tags.add(...)をすべて完了した後
  • transaction.on_commit() コールバックで translate_post.delay() を呼び出し
  • Celery タスク内で post.categories.all(), post.tags.all()を照会したところ 空のリスト([])が返される

3. 環境構成

  • WRITE: GCP VMの PostgreSQL マスター
  • READ: Raspberry Piの PostgreSQL レプリカ (streaming replication)
  • Django DB ルーターを介して読み取りリクエストをレプリカに送信するよう設定
  • ManyToMany 関係フィールドを使用する Post モデル

4. ログ分析

✅ 順序の要約 (例示時間基準)

  1. 13:13:56.728 — Post 作成
  2. 13:14:00.922 — 最後の TaggedItem (ManyToMany) クエリ
  3. 13:14:01.688on_commit() 実行 → Celery 呼び出し
  4. 13:14:01.772translate_post() 内の照会結果:
    • categories: []
    • tags: []

つまり、順序は正しいが内容が反映されていない


5. 原因分析

✅ PostgreSQL レプリカ遅延

  • PostgreSQL の基本的なレプリケーションは 非同期(async) で動作
  • マスターデータベースの変更がレプリカに 数 ms ~ 数百 ms 遅れて反映される
  • add() で接続される ManyToMany の中間テーブル記録が まだレプリカに反映されていなかったこと

✅ Django の on_commit() 動作方式

  • on_commit() は Django トランザクションがコミットされた直後に実行
  • しかし、Celery は別プロセスであり、読み取りはレプリカ DB を使用
  • 結果として on_commit 時点ではレプリカがまだ更新前の状態

6. 検証ポイント

  • Post 作成ログは存在
  • add() クエリログも正常に実行
  • 実際のクエリの順序とタスク呼び出しの順序は正しい
  • 問題は 照会時点の DB がレプリカであったこと

7. 解決策

1. Celery タスクで マスターデータベースから強制読み取り

# ORM の方法
post = Post.objects.using('default').get(id=post_id)
tags = post.tags.using('default').all()
categories = post.categories.using('default').all()

2. Celery 専用 DB ルーター構成

settings.pyに Celery タスクの場合は必ずマスターを使用するよう設定可能


3. レプリカが必ず最新の状態である必要がある場合:

  • PostgreSQLの synchronous_commit = on 設定が必要 (パフォーマンス低下の可能性あり)

8. 結論

この問題は Django の問題ではなく、非同期レプリケーション環境での遅延と ORM の読み取り優先ポリシーが衝突した結果だ。
解決の鍵は Celery タスクで強制的にマスターデータベースを使用することを保障することである。


9. Jesse のコメント

"on_commit()は明確に正しいタイミングで実行されたが、読んだDBがレプリカであったという事実が問題の本質だった。
DjangoとCeleryは何の問題もない。結局、システムアーキテクチャを理解し調整するのは開発者の責任である。"