Background Job: Celery
Email, notification, report পেছনে চালানো
User order দিলো। এখন যা যা করতে হবে: order save, restaurant কে notification, customer কে SMS আর email, invoice এর PDF বানানো, analytics এ event পাঠানো। সব যদি API request এর ভেতরেই করো, user কে ৫-১০ সেকেন্ড "loading" দেখতে হবে, আর SMS gateway ধীর হলে পুরো order ই ব্যর্থ হতে পারে। সমাধান: শুধু জরুরি কাজটা (order save) এখনই করো, বাকিগুলো একটা queue তে রেখে দাও, আলাদা worker পেছনে সেগুলো করবে। এই অধ্যায়ে Python এর সবচেয়ে প্রচলিত background job system Celery শিখবো।
#কেন background job
Background job এর ভালো প্রার্থী:
- ধীর কাজ: PDF/report বানানো, ছবি resize, video process
- বাইরের service: SMS, email, push notification, payment gateway এর যাচাই, webhook পাঠানো
- Retry দরকার: ব্যর্থ হলে কিছুক্ষণ পর আবার চেষ্টা
- নির্দিষ্ট সময়ে: রাত ২টায় দৈনিক report, প্রতি ঘণ্টায় পুরনো OTP মোছা
- চাপ সমান করা: হঠাৎ ১০,০০০ email পাঠাতে হলে queue তে রেখে worker নিজের গতিতে পাঠায়
#Celery setup
uv add "celery[redis]"# workers/celery_app.py
from celery import Celery
from app.core.config import get_settings
settings = get_settings()
celery = Celery(
"notes",
broker=settings.redis_url, # job কোথায় রাখা হবে
backend=settings.redis_url, # ফলাফল কোথায় (দরকার হলে)
include=["app.workers.tasks"],
)
celery.conf.update(
task_acks_late=True, # কাজ শেষ হলে তবেই "নিলাম" বলবে
task_reject_on_worker_lost=True, # worker মরে গেলে job আবার queue তে
worker_prefetch_multiplier=1, # একবারে একটার বেশি job ধরে রাখবে না
task_time_limit=300, # ৫ মিনিটের বেশি চললে মেরে ফেলো
task_soft_time_limit=270,
timezone="Asia/Dhaka",
)#Task লেখা
# workers/tasks.py
import logging
import httpx
from app.workers.celery_app import celery
logger = logging.getLogger(__name__)
@celery.task(
bind=True,
autoretry_for=(httpx.HTTPError,), # এই error হলে নিজে থেকে retry
retry_backoff=True, # 1s, 2s, 4s, 8s ...
retry_backoff_max=600,
retry_jitter=True,
max_retries=6,
)
def send_sms(self, phone: str, message: str, dedup_key: str) -> None:
if already_sent(dedup_key): # idempotent: দুইবার চললেও একবারই SMS
return
with httpx.Client(timeout=10) as client:
r = client.post(SMS_URL, json={"to": phone, "text": message}, headers=SMS_HEADERS)
r.raise_for_status()
mark_sent(dedup_key)
logger.info("SMS পাঠানো হলো %s", mask(phone))
@celery.task
def build_invoice(order_id: int) -> str:
pdf = render_invoice_pdf(order_id)
key = upload_to_storage(pdf, f"invoices/{order_id}.pdf")
return keyAPI থেকে job পাঠানো:
@router.post("/orders", status_code=201)
async def create_order(...):
order = await svc.create(...) # database এ save + commit
send_sms.delay(order.phone, f"Order #{order.id} নেওয়া হয়েছে", dedup_key=f"order-sms-{order.id}")
build_invoice.delay(order.id)
return orderWorker চালানো (আলাদা process, আলাদা container):
uv run celery -A app.workers.celery_app worker --loglevel=info --concurrency=4#নির্ভরযোগ্যতার নিয়ম
#১. Job কে idempotent রাখো
Celery (আর প্রায় সব queue) "at least once" delivery দেয়: একটা job অন্তত একবার চলবে, কিন্তু কখনো কখনো দুইবারও চলতে পারে (worker কাজ শেষ করে "শেষ" বলার আগেই মরে গেলে)। তাই job এমনভাবে লেখো যাতে দুইবার চললেও ক্ষতি না হয়:
- SMS:
dedup_keyদিয়ে আগে পাঠানো হয়েছে কিনা দেখো - টাকা যোগ: "balance এ ৫০০ যোগ করো" না, "transaction #TX91 প্রয়োগ করো" (যেটা আগে প্রয়োগ হয়ে থাকলে কিছু করবে না)
- File বানানো: একই নামে বানাও, আবার বানালে শুধু overwrite
#২. Job এ id পাঠাও, পুরো object না
build_invoice.delay(order.id) # ✓ ছোট, worker নিজে সর্বশেষ data আনবে
build_invoice.delay(order.dict()) # ✗ বড়, আর queue তে থাকার সময় data পুরনো হয়ে যেতে পারে#৩. Transaction commit এর পরে job পাঠাও
#৪. সময়ের সীমা আর ব্যর্থতার জায়গা
- প্রতিটা task এ time limit, যাতে আটকে থাকা job worker দখল করে না রাখে
- বারবার ব্যর্থ হওয়া job কে আলাদা করে রাখো (dead letter queue) আর alert দাও, চুপচাপ হারিয়ে যেতে দিও না
#নির্দিষ্ট সময়ের কাজ: Celery Beat
from celery.schedules import crontab
celery.conf.beat_schedule = {
"daily-sales-report": {
"task": "app.workers.tasks.daily_report",
"schedule": crontab(hour=2, minute=0), # প্রতিদিন রাত ২টা (ঢাকা)
},
"cleanup-expired-tokens": {
"task": "app.workers.tasks.cleanup_tokens",
"schedule": crontab(minute=0), # প্রতি ঘণ্টায়
},
"reconcile-payments": {
"task": "app.workers.tasks.reconcile_payments",
"schedule": 300.0, # প্রতি ৫ মিনিটে
},
}uv run celery -A app.workers.celery_app beat --loglevel=infoBeat শুধু একটাই চালাবে। দুইটা চালালে প্রতিটা কাজ দুইবার হবে।
#Docker compose এ
services:
api:
build: .
command: uvicorn app.main:app --host 0.0.0.0 --port 8000
worker:
build: . # একই image
command: celery -A app.workers.celery_app worker --concurrency=4 --loglevel=info
beat:
build: .
command: celery -A app.workers.celery_app beat --loglevel=info
redis:
image: redis:7-alpineএকই image থেকে তিন রকম process, শুধু command আলাদা। চাপ বাড়লে শুধু worker এর সংখ্যা বাড়াও।
#বিকল্প
| Tool | কখন |
|---|---|
| Celery | পরিণত, অনেক feature, সবচেয়ে প্রচলিত; একটু ভারী |
| ARQ, Dramatiq, Taskiq | হালকা; ARQ/Taskiq async-native, FastAPI এর সাথে মানানসই |
FastAPI BackgroundTasks |
খুব ছোট কাজ, response এর পর একই process এ। Retry নেই, process restart হলে হারায়। শুধু log বা ছোট cleanup এর জন্য। |
| RabbitMQ/Kafka + নিজের consumer | বড় system, অনেক service (Queue অধ্যায়) |
#অনুশীলন
#নিজেকে যাচাই করো
কোন ধরনের কাজ API request এর ভেতরে না করে background job এ পাঠাবে?
ধীর কাজ (PDF, ছবি process), বাইরের service নির্ভর কাজ (SMS, email, push), retry দরকার এমন কাজ, নির্দিষ্ট সময়ের কাজ, আর অনেকগুলো একসাথে করতে হবে এমন কাজ।
"At least once" delivery মানে কী, আর এর জন্য task কেমন হওয়া উচিত?
একটা job অন্তত একবার চলবে, কিন্তু কখনো কখনো একাধিকবার চলতে পারে। তাই task idempotent হতে হবে: দুইবার চললেও ফল একবার চলার মতোই।
Order এর transaction commit এর আগে SMS job পাঠালে কী সমস্যা হতে পারে?
Worker দ্রুত চালু হয়ে database এ order খুঁজে না পেতে পারে। আর transaction rollback হলে এমন order এর SMS চলে যাবে যেটা আসলে তৈরি হয়নি।
Celery Beat দুইটা চালালে কী হবে?
প্রতিটা নির্দিষ্ট সময়ের কাজ দুইবার করে চলবে, যেমন দৈনিক report দুইবার, payment reconcile দুইবার।
চিহ্ন দিলে sidebar আর রোডম্যাপে এই অধ্যায়ের station ভরে যাবে।