ca648ccf8b
Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> Agent-Logs-Url: https://github.com/christianlouis/pop_puller_to_gmail/sessions/dd261f53-4891-437e-bd71-5561ece62d7d
129 lines
5.0 KiB
Markdown
129 lines
5.0 KiB
Markdown
# ADR 005: Celery Task Retry Strategy
|
|
|
|
**Status:** Accepted
|
|
**Date:** 2026-02-01
|
|
**Deciders:** Development Team
|
|
|
|
## Context
|
|
|
|
Email processing tasks can fail for many transient reasons:
|
|
|
|
1. Mail server temporarily unavailable or overloaded
|
|
2. Network timeouts during POP3/IMAP connections
|
|
3. SMTP delivery failures (temporary, e.g. greylisting)
|
|
4. Gmail API rate limits or transient 5xx errors
|
|
5. Database connection errors
|
|
|
|
Without a retry strategy, transient failures result in permanently missed emails. However, aggressive retries can overload external services or cause duplicate deliveries.
|
|
|
|
## Decision
|
|
|
|
We will use **Celery's built-in autoretry mechanism** with **exponential backoff**, capped at **3 retries** per task invocation. Failed tasks after exhausting retries are recorded in the processing log and flagged on the mail account for user visibility.
|
|
|
|
## Alternatives Considered
|
|
|
|
### 1. No Retries (fail-fast)
|
|
- **Pros**: Simple, predictable
|
|
- **Cons**: Transient failures cause permanent data loss, poor user experience
|
|
|
|
### 2. Infinite Retries
|
|
- **Pros**: Guarantees eventual processing
|
|
- **Cons**: Fills task queue, may mask persistent failures, delays detection of real errors
|
|
|
|
### 3. Fixed-Interval Retries
|
|
- **Pros**: Simple to reason about
|
|
- **Cons**: Hammers failing services, does not give them time to recover
|
|
|
|
### 4. External Retry Orchestrator (e.g., Temporal, AWS Step Functions)
|
|
- **Pros**: Advanced workflow management, visual debugging
|
|
- **Cons**: Significant infrastructure overhead, unnecessary complexity at current scale
|
|
|
|
### 5. Manual Retry Queue
|
|
- **Pros**: Full control
|
|
- **Cons**: Re-implements what Celery already provides
|
|
|
|
## Rationale
|
|
|
|
Celery's `autoretry_for` with `retry_backoff=True` was chosen because:
|
|
|
|
1. **Native Integration**: No additional libraries required, built into Celery
|
|
2. **Exponential Backoff**: Gives external services time to recover between retries
|
|
3. **Jitter**: Prevents thundering herd when many accounts fail simultaneously
|
|
4. **Bounded Retries**: `max_retries=3` ensures tasks eventually fail rather than running forever
|
|
5. **Configurable**: Per-task retry configuration allows tuning per failure type
|
|
6. **Visibility**: Failed tasks appear in Flower dashboard and processing logs
|
|
|
|
## Implementation
|
|
|
|
```python
|
|
from celery import Task
|
|
from app.core.celery_app import celery_app
|
|
|
|
@celery_app.task(
|
|
bind=True,
|
|
autoretry_for=(ConnectionError, TimeoutError, OSError),
|
|
retry_kwargs={"max_retries": 3, "countdown": 60},
|
|
retry_backoff=True, # Exponential: 60s, 120s, 240s
|
|
retry_backoff_max=600, # Cap at 10 minutes
|
|
retry_jitter=True, # Add randomness to spread load
|
|
)
|
|
def process_mail_account(self: Task, account_id: int) -> dict:
|
|
"""Fetch and forward emails for a single mail account."""
|
|
try:
|
|
# ... email processing logic ...
|
|
pass
|
|
except Exception as exc:
|
|
# Log failure to processing_logs table before re-raising
|
|
_record_failure(account_id, str(exc))
|
|
raise
|
|
```
|
|
|
|
### Retry Schedule (default config)
|
|
|
|
| Attempt | Delay (approx.) | Total elapsed |
|
|
|---------|-----------------|---------------|
|
|
| 1st | immediate | 0s |
|
|
| 2nd | ~60s | 60s |
|
|
| 3rd | ~120s | 3m |
|
|
| 4th | ~240s | 7m |
|
|
| Final failure | — recorded to DB | ~7m |
|
|
|
|
### Failure Handling After All Retries
|
|
|
|
When all retries are exhausted, the task:
|
|
1. Marks the `ProcessingRun` as `failed` in the database
|
|
2. Increments the `consecutive_failures` counter on the `MailAccount`
|
|
3. If `consecutive_failures` exceeds threshold: marks account as `error` state
|
|
4. Triggers a user notification via Apprise (if configured)
|
|
|
|
## Consequences
|
|
|
|
### Positive
|
|
- Transient failures (network blips, greylisting) recover automatically
|
|
- Exponential backoff respects external service rate limits
|
|
- Bounded retry count prevents runaway task accumulation
|
|
- Failure visibility in Flower and processing logs
|
|
|
|
### Negative
|
|
- Up to ~7 minutes of additional delay for permanently failing accounts
|
|
- Retry state stored in Redis; Redis failure loses retry context
|
|
- Duplicate delivery possible if task succeeds after partial completion (must ensure idempotency)
|
|
|
|
### Idempotency Requirement
|
|
|
|
Tasks **must be idempotent**: re-running a task for the same mail account must not deliver duplicate emails. Implementations must:
|
|
- Track message UIDs already processed in `processing_logs`
|
|
- Use POP3 `UIDL` or IMAP `UID` to identify messages
|
|
- Check for existing `ProcessingLog` entries before delivery
|
|
|
|
## Related Decisions
|
|
|
|
- See ADR-001 for Celery architecture overview
|
|
- See ADR-009 for Gmail API delivery (idempotency considerations)
|
|
|
|
## References
|
|
|
|
- [Celery Retrying Tasks](https://docs.celeryq.dev/en/stable/userguide/tasks.html#retrying)
|
|
- [Celery autoretry_for](https://docs.celeryq.dev/en/stable/userguide/tasks.html#automatic-retry-for-known-exceptions)
|
|
- [Exponential Backoff and Jitter (AWS blog)](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/)
|