Skip to main content

Retry, failure, and recovery

PR #23 replaces unlimited queue redelivery for handler failures with an explicit durable failure ledger in the application database. Pub/Sub remains the fallback only when that ledger cannot be written.

Failure handoff

The saved envelope includes extracted Shopify headers and identifiers, so replay does not depend on the original Pub/Sub message remaining available.

Failure state machine

Only pending rows are selected by retry_failed_events. Terminal failed rows are not retried automatically, and resolved rows are retained for audit.

Retry schedule

The retry command increments retry_count only after a failed replay. It then schedules 2 ** retry_count minutes, capped at 60 minutes.

Failed replay countNext delay
12 minutes
24 minutes
38 minutes
416 minutes
5Terminal with the default max_retries = 5
Higher custom limits32 minutes, then 60-minute maximum

A successful first replay goes directly from pending to resolved and does not increment retry_count.

Running retries

Retry up to 50 due events:

python manage.py retry_failed_events

Use a smaller batch:

python manage.py retry_failed_events --limit 10

Target one due, pending failure row:

python manage.py retry_failed_events --event-id 42

The command prints Retried=<n> resolved=<n> failed=<n>.

Scheduling is external

The PR adds the command but does not schedule it. A production deployment must invoke it periodically. Ensure only one scheduler instance runs at a time because the query does not claim rows with SELECT FOR UPDATE or a lease.

Error handling matrix

FailureCaptured fieldsTypical corrective action
Invalid JSON or non-object messageRaw decoded data where possible, exception, tracebackCorrect publisher format; replay only after envelope is usable
Unsupported or malformed payloadEnriched envelope, ValueError, tracebackCorrect data or parser and replay
Missing target configurationOrganization/shop context, StorageConfigurationErrorCreate Database and DatabaseCredential, provision, replay
Bad credentials or network outageFull envelope and psycopg exceptionRestore access, replay
Missing/incompatible columnFull envelope and PostgreSQL exceptionApply schema correction, replay
Deadlock or lock timeoutFull envelope and database exceptionLet contention clear, replay
Code logic errorFull envelope and Python exceptionDeploy fix, replay
Application failure table unavailableFailure cannot be storedRestore application DB; Pub/Sub redelivery is requested with nack

Worker restart backoff versus event retry

Two independent mechanisms exist:

MechanismHandlesTiming
Worker restart loopStreaming pull crashes or connection-level worker failure5, 10, 20, 40, then 60 seconds
SubscriberFailedEvent replayIndividual decode, parser, handler, and target database errors2, 4, 8, 16, 32, then 60 minutes depending on retry count

The restart loop keeps the subscriber process alive. It does not replay an event already acknowledged after durable failure capture.

Operational queries

Pending and due workload:

SELECT id, source, event_type, shop_domain, retry_count, next_retry_at, error_type
FROM subscriber_subscriberfailedevent
WHERE status = 'pending'
AND (next_retry_at IS NULL OR next_retry_at <= NOW())
ORDER BY created_at;

Terminal failures requiring intervention:

SELECT id, event_id, shop_domain, retry_count, error_type, error_message, updated_at
FROM subscriber_subscriberfailedevent
WHERE status = 'failed'
ORDER BY updated_at DESC;

Recent recovery rate:

SELECT status, COUNT(*)
FROM subscriber_subscriberfailedevent
WHERE created_at >= NOW() - INTERVAL '24 hours'
GROUP BY status;

Current reliability gaps

  • Missing or duplicate Shopify connections and unknown handlers are logged and acknowledged without creating a failure row.
  • Replay rows are not leased; overlapping retry processes can dispatch the same row concurrently.
  • There is no uniqueness constraint preventing duplicate failure rows for the same event.
  • There is no automatic operator alert for terminal failed status.
  • There is no retention or redaction policy for payloads and tracebacks.
  • Snapshot ordering is processing-time last-write-wins; out-of-order Shopify events are not compared using a source sequence or update timestamp.
  • Schema creation handles absent tables, not incompatible schema evolution.

Target-table upserts limit the impact of duplicate replay, but these gaps should be addressed before high-volume multi-worker operation.