mirror of
https://github.com/frappe/gunicorn.git
synced 2026-07-01 18:21:30 +08:00
Celery Alternative Example
This example demonstrates how to replace Celery with Gunicorn's dirty arbiters for background task processing.
Why Replace Celery?
| Aspect | Celery | Dirty Arbiters |
|---|---|---|
| Dependencies | Redis/RabbitMQ + Celery | None (built into Gunicorn) |
| Deployment | Multiple processes/containers | Single process |
| State | Stateless workers | Stateful workers (keep models loaded) |
| Progress | Polling or WebSocket | Native streaming |
| Configuration | Separate config | Same gunicorn.conf.py |
Quick Start
Local Development
# Install dependencies
pip install flask requests pytest
pip install -e ../.. # Install gunicorn from source
# Run the application
gunicorn -c gunicorn_conf.py app:app
# In another terminal, test it
curl http://localhost:8000/health
curl -X POST http://localhost:8000/api/email/send \
-H "Content-Type: application/json" \
-d '{"to": "test@example.com", "subject": "Hello", "body": "World"}'
Docker
# Build and run
docker compose up --build
# Run with tests
docker compose --profile test up --build --abort-on-container-exit
Architecture
┌─────────────────────────────────────────────────────────────┐
│ Gunicorn Master │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ HTTP Worker │ │ HTTP Worker │ │ HTTP Worker │ ... │
│ │ (gthread) │ │ (gthread) │ │ (gthread) │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ └────────────────┼────────────────┘ │
│ │ │
│ Unix Socket IPC │
│ │ │
│ ┌────────────────┼────────────────┐ │
│ │ │ │ │
│ ┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐ │
│ │ EmailWorker │ │ ImageWorker │ │ DataWorker │ ... │
│ │ (2 procs) │ │ (2 procs) │ │ (4 procs) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ Dirty Arbiter │
└─────────────────────────────────────────────────────────────┘
Task Workers
EmailWorker
send_email(to, subject, body)- Send single emailsend_bulk_emails(recipients, subject, body)- Bulk send with progress streamingstats()- Worker statistics
ImageWorker
resize(image_data, width, height)- Resize imagegenerate_thumbnail(image_data, size)- Generate thumbnailprocess_batch(images, operation, **params)- Batch process with streaming
DataWorker
aggregate(data, group_by, agg_field, agg_func)- Aggregate dataetl_pipeline(source_data, transformations)- ETL with progress streamingcached_query(query_key, ttl)- Cached query execution
ScheduledWorker
cleanup_old_files(directory, max_age_days)- File cleanupgenerate_daily_report()- Daily report generationsync_external_data(source)- External data sync
API Endpoints
POST /api/email/send- Send single emailPOST /api/email/send-bulk- Bulk send (SSE streaming)GET /api/email/stats- Worker stats
Image
POST /api/image/resize- Resize imagePOST /api/image/thumbnail- Generate thumbnailPOST /api/image/process-batch- Batch process (SSE streaming)GET /api/image/stats- Worker stats
Data
POST /api/data/aggregate- Aggregate dataPOST /api/data/etl- ETL pipeline (SSE streaming)POST /api/data/query- Cached queryGET /api/data/stats- Worker stats
Scheduled
POST /api/scheduled/cleanup- Run cleanupPOST /api/scheduled/daily-report- Generate reportPOST /api/scheduled/sync- Sync dataGET /api/scheduled/stats- Worker stats
Streaming Progress Example
import requests
import json
# Start bulk email with streaming progress
resp = requests.post(
"http://localhost:8000/api/email/send-bulk",
json={
"recipients": ["a@x.com", "b@x.com", "c@x.com"],
"subject": "Newsletter",
"body": "Hello!",
},
stream=True,
)
for line in resp.iter_lines():
if line and line.startswith(b"data: "):
progress = json.loads(line[6:])
if progress["type"] == "progress":
print(f"Progress: {progress['percent']}%")
elif progress["type"] == "complete":
print(f"Done! Sent: {progress['sent']}, Failed: {progress['failed']}")
Celery Migration Guide
Before (Celery)
# tasks.py
from celery import Celery
app = Celery('tasks', broker='redis://localhost')
@app.task
def send_email(to, subject, body):
# Send email
return {"status": "sent"}
@app.task(bind=True)
def send_bulk(self, recipients, subject, body):
for i, to in enumerate(recipients):
send_email(to, subject, body)
self.update_state(state='PROGRESS', meta={'current': i})
# views.py
from tasks import send_email, send_bulk
def send_view(request):
send_email.delay(to, subject, body) # Async
return {"status": "queued"}
After (Dirty Arbiters)
# tasks.py
from gunicorn.dirty.app import DirtyApp
class EmailWorker(DirtyApp):
workers = 2 # Limit workers
def init(self):
self.smtp = connect_smtp() # Stateful!
def __call__(self, action, *args, **kwargs):
return getattr(self, action)(*args, **kwargs)
def send_email(self, to, subject, body):
return {"status": "sent"}
def send_bulk(self, recipients, subject, body):
for i, to in enumerate(recipients):
self.send_email(to, subject, body)
yield {"type": "progress", "current": i} # Native streaming!
# views.py
from gunicorn.dirty import get_dirty_client
def send_view(request):
client = get_dirty_client()
result = client.execute("tasks:EmailWorker", "send_email", to, subject, body)
return result # Sync result, no polling!
Configuration
# gunicorn_conf.py
# HTTP workers
workers = 4
worker_class = "gthread"
threads = 4
# Task workers (replace Celery)
dirty_apps = [
"tasks:EmailWorker",
"tasks:ImageWorker",
"tasks:DataWorker",
]
dirty_workers = 9
dirty_timeout = 300
Running Tests
# Unit tests (no server needed)
pytest tests/test_tasks.py -v
# Integration tests (server must be running)
APP_URL=http://localhost:8000 pytest tests/test_integration.py -v
# All tests via Docker
docker compose --profile test up --build --abort-on-container-exit