Challenge 2: Add Retry Logic — Possible Solution ==================================================================== # catalog/tasks.py from celery import shared_task @shared_task( bind=True, autoretry_for=(ConnectionError,), retry_backoff=True, max_retries=3, ) def generate_book_report(self, book_id): from .models import Book book = Book.objects.get(pk=book_id) # Simulated external call that might raise ConnectionError, # e.g. calling a third-party pricing API to enrich the report: # external_data = fetch_market_price(book.title) print(f"Report: '{book.title}' — ${book.price}, in_stock={book.in_stock}") WHY THIS WORKS -------------- - bind=True gives the task access to `self` (the task instance itself), which is required for Celery's retry machinery to correctly track attempt counts, backoff timing, and task metadata across retries. - autoretry_for=(ConnectionError,) tells Celery to automatically retry this specific task, WITHOUT any manual try/except or self.retry() call needed in the task body, whenever it raises a ConnectionError — other exception types are NOT automatically retried, so a genuine bug (like a Book.DoesNotExist from a bad book_id) still fails immediately rather than being retried pointlessly. - retry_backoff=True spaces out retry attempts with increasing delays (roughly 1s, 2s, 4s by default) rather than retrying immediately three times in a row — useful when the failure is a temporary network blip that might need a few seconds to resolve, rather than hammering a struggling external service immediately again. - max_retries=3 caps the total number of retry attempts — after three failed retries, Celery gives up and marks the task as permanently failed, rather than retrying forever on a genuinely broken dependency.