Django + Celery 非同期タスクパターン
Redis または RabbitMQ を使用し、Django で Celery を用いた本番環境向けバックグラウンドタスク処理のパターン。
いつアクティブにするか
- Django アプリにバックグラウンドジョブや非同期処理を追加するとき
- 定期的・スケジュールされたタスクを実装するとき
- リクエストサイクルから低速な処理(メール、PDF 生成、API 呼び出し)をオフロードするとき
- Celery Beat で cron 風のスケジューリングを設定するとき
- タスクの失敗、リトライ、キューのバックログをデバッグするとき
- Celery タスクのテストを書くとき
プロジェクトセットアップ
インストール
pip install 'celery[redis]' django-celery-results django-celery-beat
celery.py — アプリエントリーポイント
# config/celery.py
import os
from celery import Celery
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.development')
app = Celery('myproject')
app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks() # INSTALLED_APP 内の tasks.py を自動検出
@app.task(bind=True, ignore_result=True)
def debug_task(self):
print(f'Request: {self.request!r}')
# config/__init__.py
from .celery import app as celery_app
__all__ = ('celery_app',)
Django 設定
# config/settings/base.py
# ブローカー(本番では Redis 推奨)
CELERY_BROKER_URL = env('CELERY_BROKER_URL', default='redis://localhost:6379/0')
CELERY_RESULT_BACKEND = env('CELERY_RESULT_BACKEND', default='django-db')
# シリアライゼーション
CELERY_ACCEPT_CONTENT = ['json']
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'
# タスクの動作
CELERY_TASK_TRACK_STARTED = True
CELERY_TASK_TIME_LIMIT = 30 * 60 # ハードリミット: 30分
CELERY_TASK_SOFT_TIME_LIMIT = 25 * 60 # ソフトリミット: SoftTimeLimitExceeded を送出
CELERY_WORKER_PREFETCH_MULTIPLIER = 1 # ワーカーが長時間タスクを占有するのを防ぐ
CELERY_TASK_ACKS_LATE = True # ワーカークラッシュ時にキューに再投入
# 結果の保持
CELERY_RESULT_EXPIRES = 60 * 60 * 24 # 結果を24時間保持
# Beat スケジューラー(定期タスク用)
CELERY_BEAT_SCHEDULER = 'django_celery_beat.schedulers:DatabaseScheduler'
# インストール済みアプリ
INSTALLED_APPS += [
'django_celery_results',
'django_celery_beat',
]
ワーカーの起動
# ワーカー起動(開発)
celery -A config worker --loglevel=info
# Beat スケジューラー起動(定期タスク)
celery -A config beat --loglevel=info --scheduler django_celery_beat.schedulers:DatabaseScheduler
# ワーカー + Beat 統合(開発のみ、本番では非推奨)
celery -A config worker --beat --loglevel=info
# 本番: マルチワーカー、並行実行
celery -A config worker --loglevel=warning --concurrency=4 -Q default,high_priority
タスク設計パターン
基本タスク
# apps/notifications/tasks.py
from celery import shared_task
import logging
logger = logging.getLogger(__name__)
@shared_task(name='notifications.send_welcome_email')
def send_welcome_email(user_id: int) -> None:
"""新規登録ユーザーにウェルカムメールを送信する。"""
from apps.users.models import User
from apps.notifications.services import EmailService
try:
user = User.objects.get(pk=user_id)
except User.DoesNotExist:
logger.warning('send_welcome_email: ユーザー %s が見つかりません', user_id)
return # べき等 — タスクは既に完了不可能なので送出しない
EmailService.send_welcome(user)
logger.info('ウェルカムメールをユーザー %s に送信しました', user_id)
リトライ可能なタスク
@shared_task(
bind=True,
name='integrations.sync_to_crm',
max_retries=5,
default_retry_delay=60, # 初回リトライまでの秒数
autoretry_for=(ConnectionError, TimeoutError),
retry_backoff=True, # 指数バックオフ
retry_backoff_max=600, # 最大10分
retry_jitter=True, # ランダム化して輻輳を防止
)
def sync_contact_to_crm(self, contact_id: int) -> dict:
"""一時的な障害でリトライ可能な、外部CRMへの連絡先同期。"""
from apps.crm.services import CRMClient
try:
result = CRMClient().sync(contact_id)
return result
except CRMClient.RateLimitError as exc:
# レスポンスヘッダーから具体的なリトライ遅延を設定
raise self.retry(exc=exc, countdown=int(exc.retry_after))
べき等タスクパターン
同じ入力で安全に複数回実行できるようにタスクを設計する:
@shared_task(name='orders.mark_shipped')
def mark_order_shipped(order_id: int, tracking_number: str) -> None:
"""注文を発送済みにマーク — 複数回実行しても安全。"""
from apps.orders.models import Order
updated = Order.objects.filter(
pk=order_id,
status=Order.Status.PROCESSING, # ガード: まだ発送済みでない場合のみ更新
).update(
status=Order.Status.SHIPPED,
tracking_number=tracking_number,
)
if not updated:
logger.info('mark_order_shipped: 注文 %s は既に発送済みまたは見つかりません', order_id)
ソフトタイムリミット付きタスク
from celery.exceptions import SoftTimeLimitExceeded
@shared_task(
bind=True,
name='reports.generate_pdf',
soft_time_limit=120,
time_limit=150,
)
def generate_pdf_report(self, report_id: int) -> str:
"""グレースフルなタイムアウト処理付きPDFレポート生成。"""
from apps.reports.services import PDFGenerator
try:
path = PDFGenerator.build(report_id)
return path
except SoftTimeLimitExceeded:
# ハードキル前に部分ファイルをクリーンアップ
PDFGenerator.cleanup(report_id)
raise
タスクの呼び出し
from datetime import timedelta
from django.utils import timezone
# ファイアアンドフォーゲット(非同期)
send_welcome_email.delay(user.pk)
# 将来のスケジュール
send_reminder.apply_async(args=[user.pk], countdown=3600) # 1時間後
send_reminder.apply_async(args=[user.pk], eta=timezone.now() + timedelta(days=1))
# キュールーティング付きで適用
sync_contact_to_crm.apply_async(args=[contact.pk], queue='high_priority')
# 同期的に実行(テスト/デバッグのみ)
result = generate_pdf_report.apply(args=[report.pk])
Beat スケジューリング(定期タスク)
コード定義のスケジュール
# config/settings/base.py
from celery.schedules import crontab
CELERY_BEAT_SCHEDULE = {
'cleanup-expired-sessions': {
'task': 'users.cleanup_expired_sessions',
'schedule': crontab(hour=2, minute=0), # 毎日午前2時
},
'sync-inventory': {
'task': 'products.sync_inventory',
'schedule': 60.0, # 60秒ごと
},
'weekly-digest': {
'task': 'notifications.send_weekly_digest',
'schedule': crontab(day_of_week='monday', hour=8, minute=0),
},
}
データベース定義のスケジュール(django-celery-beat 経由)
# Django 管理画面またはコードから定期タスクを管理
from django_celery_beat.models import PeriodicTask, CrontabSchedule
import json
schedule, _ = CrontabSchedule.objects.get_or_create(
hour='*/6', minute='0',
timezone='UTC',
)
PeriodicTask.objects.update_or_create(
name='6時間ごとに在庫を同期',
defaults={
'crontab': schedule,
'task': 'products.sync_inventory',
'args': json.dumps([]),
'enabled': True,
}
)
Canvas: タスクのチェーンとグループ化
from celery import chain, group, chord
# チェーン: タスクを順次実行し、結果を渡す
pipeline = chain(
fetch_data.s(source_id),
transform_data.s(), # fetch_data の結果を最初の引数として受け取る
load_to_warehouse.s(),
)
pipeline.delay()
# グループ: タスクを並列実行
parallel = group(
send_welcome_email.s(user_id)
for user_id in new_user_ids
)
parallel.delay()
# Chord: 並列タスク + 全完了時のコールバック
result = chord(
group(process_chunk.s(chunk) for chunk in data_chunks),
aggregate_results.s(), # チャンク結果のリストで呼び出される
)
result.delay()
エラーハンドリングとデッドレターキュー
# apps/core/tasks.py
from celery.signals import task_failure
@task_failure.connect
def on_task_failure(sender, task_id, exception, args, kwargs, traceback, einfo, **kw):
"""すべてのタスク失敗を Sentry / アラートに記録する。"""
import sentry_sdk
with sentry_sdk.new_scope() as scope:
scope.set_context('celery', {
'task': sender.name,
'task_id': task_id,
'args': args,
'kwargs': kwargs,
})
sentry_sdk.capture_exception(exception)
# 最大リトライ後に失敗タスクをデッドレターキューにルーティング
@shared_task(
bind=True,
max_retries=3,
name='payments.charge_card',
)
def charge_card(self, order_id: int) -> None:
from apps.payments.models import Order, FailedCharge
try:
_do_charge(order_id)
except Exception as exc:
if self.request.retries >= self.max_retries:
# 手動再確認用にデッドレターテーブルに永続化
FailedCharge.objects.create(
order_id=order_id,
error=str(exc),
task_id=self.request.id,
)
return # 送出しない — タスクは永続的に失敗
raise self.retry(exc=exc)
Celery タスクのテスト
ユニットテスト(ブローカーなし)
# tests/test_tasks.py
import pytest
from unittest.mock import patch, MagicMock
from apps.notifications.tasks import send_welcome_email
class TestSendWelcomeEmail:
@pytest.mark.django_db
def test_sends_email_to_existing_user(self, user):
with patch('apps.notifications.services.EmailService') as mock_email:
send_welcome_email(user.pk)
mock_email.send_welcome.assert_called_once_with(user)
@pytest.mark.django_db
def test_skips_missing_user_gracefully(self):
"""エンキューと実行の間にユーザーが削除されても送出しない。"""
send_welcome_email(99999) # 存在しないユーザー — 送出してはならない
CELERY_TASK_ALWAYS_EAGER を使った統合テスト
# config/settings/test.py
CELERY_TASK_ALWAYS_EAGER = True # テストではタスクを同期的に実行
CELERY_TASK_EAGER_PROPAGATES = True # タスクの例外を再送出
# tests/test_integration.py
@pytest.mark.django_db
def test_registration_triggers_welcome_email(client):
with patch('apps.notifications.services.EmailService') as mock_email:
response = client.post('/api/users/', {
'email': '[email protected]',
'password': 'strongpass123',
})
assert response.status_code == 201
mock_email.send_welcome.assert_called_once()
リトライのテスト
@pytest.mark.django_db
def test_task_retries_on_connection_error():
with patch('apps.crm.services.CRMClient.sync') as mock_sync:
mock_sync.side_effect = ConnectionError('timeout')
with pytest.raises(ConnectionError):
sync_contact_to_crm.apply(args=[1], throw=True)
assert mock_sync.call_count == 1 # eager の場合は最初の試行のみ
監視
# アクティブなワーカーとキューを検査
celery -A config inspect active
celery -A config inspect stats
celery -A config inspect reserved
# キューの長さを確認(Redis)
redis-cli llen celery
# Flower: Webベースのリアルタイムモニター
pip install flower
celery -A config flower --port=5555
アンチパターン
# BAD: モデルインスタンスを渡す — 実行時には古くなっている可能性がある
send_welcome_email.delay(user) # ORM オブジェクトは渡さない
send_welcome_email.delay(user.pk) # 常に PK を渡す
# BAD: 本番ビューでタスクを同期的に呼び出す
result = generate_report.apply() # リクエストスレッドをブロックする
# BAD: ガードなしの非べき等タスク
@shared_task
def charge_and_fulfill(order_id):
order.charge() # タスクがリトライされると二重請求される可能性がある!
order.fulfill()
# GOOD: ステータスガード付きべき等
@shared_task
def charge_and_fulfill(order_id):
order = Order.objects.select_for_update().get(pk=order_id)
if order.status != Order.Status.PENDING:
return # 既に処理済み
order.charge()
order.fulfill()
本番チェックリスト
| チェック項目 | 設定 |
|---|---|
| ワーカーのクラッシュ時再起動 | supervisord または systemd ユニット |
CELERY_TASK_ACKS_LATE = True | ワーカークラッシュ時にタスクをキューに再投入 |
CELERY_WORKER_PREFETCH_MULTIPLIER = 1 | 長時間タスクの公平な分散 |
| 優先度別の個別キュー | -Q default,high_priority,low_priority |
CELERY_TASK_SOFT_TIME_LIMIT を設定 | ハードキル前のグレースフルなタイムアウト |
| Sentry 統合 | すべての task_failure シグナルをキャプチャ |
| Flower または他のモニター | キューの深さを可視化 |
| Beat は単一ノードでのみ実行 | スケジュールされたタスクの重複実行を防止 |
関連スキル
django-patterns— ORM、サービスレイヤー、プロジェクト構造django-tdd— Django モデル、ビュー、サービスのテストpython-testing— pytest の設定とフィクスチャ