Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions backend/app/api/routes/dead_letter_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
)
from app.services.dead_letter_service import (
DeadLetterEventNotFoundError,
DeadLetterPublishError,
DeadLetterService,
UnsafeReprocessError,
)
Expand Down Expand Up @@ -81,11 +80,6 @@ def reprocess_dead_letter_event(
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Dead letter event not found.") from exc
except UnsafeReprocessError as exc:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc)) from exc
except DeadLetterPublishError as exc:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="Dead letter event was not republished.",
) from exc

event = dead_letter_event.event
routing_key = dead_letter_event.original_routing_key or event.routing_key or ""
Expand Down
4 changes: 0 additions & 4 deletions backend/app/services/dead_letter_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,6 @@ class UnsafeReprocessError(Exception):
pass


class DeadLetterPublishError(Exception):
pass


class DeadLetterService:
def __init__(self, db: Session) -> None:
self.db = db
Expand Down
9 changes: 6 additions & 3 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,9 @@ Possible errors:

### `POST /api/dead-letter-events/{id}/reprocess`

Republishes the original event to the main exchange with its original routing key. Relay preserves `correlation_id` and `trace_id`, moves the original event back to `queued`, and records the operational action.
Queues the original event for reprocessing through the Transactional Outbox. Relay reuses the existing `Event`, preserves `correlation_id` and `trace_id`, resets the processing state to `pending`, moves the event back to `queued`, creates or resets its `OutboxMessage` to `pending`, and records the operational action.

The request returns after the database transaction is persisted. RabbitMQ publication is asynchronous and performed later by the Outbox publisher, so `200` means the event was queued in the Outbox, not that broker delivery was already confirmed. Later publication failures remain retryable by the Outbox retry flow.

```json
{
Expand All @@ -228,13 +230,14 @@ Possible errors:

- `401`: missing, invalid, or expired token;
- `404`: DLQ event not found;
- `409`: reprocessing blocked by an operational safety rule;
- `503`: event could not be republished.
- `409`: reprocessing blocked by an operational safety rule.

## Reprocessing Semantics

- The original event is reused.
- No new `Event` is created.
- The routing key comes from `original_routing_key`, falling back to `event.routing_key`.
- `correlation_id` and `trace_id` are preserved.
- `EventProcessingState` is reset to `pending`.
- `OutboxMessage` is created or reset to `pending`.
- Reprocessing does not replace real handler idempotency.
8 changes: 5 additions & 3 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,9 @@ Shared retry queues return messages through `retry.*`. Workers inspect `x-origin

## Retry and Backoff

Workers avoid `basic_nack(requeue=True)`, preventing tight loops in the main queue. On failure, the original message is republished to the DLX with `x-retry-count` and the appropriate retry routing key, then acknowledged.
On normal handler failure, workers avoid `basic_nack(requeue=True)` to prevent tight loops in the main queue. The original message is published to the DLX with `x-retry-count` and the appropriate retry routing key. The original message is acknowledged only after the recovery message is published successfully and the corresponding state is persisted.

If publishing the retry or DLQ message fails, the worker does not persist the state as sent. It issues `basic_nack(requeue=True)`, calls `stop_consuming()`, and leaves the original message available for redelivery after the worker or messaging infrastructure recovers.

Failure progression:

Expand All @@ -162,9 +164,9 @@ The operational DLQ stores messages that exhausted automated retries. PostgreSQL

- `GET /api/dead-letter-events`: list DLQ events with failure and correlation data;
- `GET /api/dead-letter-events/{id}`: inspect payload, original event, attempts, and logs;
- `POST /api/dead-letter-events/{id}/reprocess`: republish the existing event.
- `POST /api/dead-letter-events/{id}/reprocess`: queue the existing event for Outbox-backed reprocessing.

Manual reprocessing preserves `correlation_id`, `trace_id`, and `original_routing_key`, moves the original event to `queued`, and creates an operational `EventLog`. It does not create a second `Event`.
Manual reprocessing preserves `correlation_id`, `trace_id`, and `original_routing_key`, resets the processing state, moves the original event to `queued`, creates or resets an `OutboxMessage` to `pending`, and creates an operational `EventLog`. It does not create a second `Event` or publish directly inside the HTTP request.

Recommended operation:

Expand Down
Loading