Skip to content

xano-community/job-retry

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 

Repository files navigation

job-retry

A durable job queue with exponential-backoff retries and a dead-letter tier — built entirely out of one Xano table and a handful of endpoints. Push a job, have a worker claim it, report success or failure. Failed jobs get re-queued with backoff; if they exhaust their retries they land in the DLQ where a human can inspect, requeue, or delete.

This is a module, not a full app: no frontend, no workers included. You bring the workers (another Xano task, an external cron, a Lambda, whatever) and call the claim / complete / fail endpoints.

What you get

  • 1 table jr_job — one row per job; state machine enforced by the endpoints
  • HTTP API job-retry — enqueue, claim, complete, fail, list, DLQ, requeue

Install

npm install -g @xano/cli
xano profile:wizard

cd backend
xano workspace push

Creates 1 table, 1 api group, and 8 endpoints.

State machine

              +---------+
 enqueue -->  | queued  | <---+
              +----+----+     |  fail (attempts < max)
                   |          |
           claim   v          |  with run_after = now + backoff * 2^(attempts-1)
              +---------+     |
              | running |-----+
              +----+----+
                   |
       complete    |    fail (attempts == max)
          v        |         v
    +-----------+  |    +--------+
    | completed |  +--->|  dead  | --- requeue ---> queued (attempts reset)
    +-----------+       +--------+

API surface

POST   /api:job-retry/jobs                        { job_type, queue?, payload?, max_attempts?, backoff_seconds?, run_after? }
GET    /api:job-retry/jobs                        ?status&queue&job_type&page&per_page
GET    /api:job-retry/jobs/{job_id}                   single job

POST   /api:job-retry/jobs/claim                  { queue?, worker_id? }       # atomic queued -> running
POST   /api:job-retry/jobs/{job_id}/complete          { result? }
POST   /api:job-retry/jobs/{job_id}/fail              { error }

GET    /api:job-retry/dlq                         ?queue&page&per_page         # dead-letter list
POST   /api:job-retry/dlq/{job_id}/requeue                                     # reset & re-queue

Usage

1. Enqueue a job

B=https://YOUR-INSTANCE.n7d.xano.io

curl -XPOST $B/api:job-retry/jobs \
  -H 'Content-Type: application/json' \
  -d '{
    "job_type": "send_welcome_email",
    "payload": {"user_id": 42},
    "max_attempts": 5,
    "backoff_seconds": 30
  }'

2. Worker loop (pseudocode)

while true; do
  job=$(curl -sXPOST $B/api:job-retry/jobs/claim -H 'Content-Type: application/json' -d '{"queue":"default","worker_id":"worker-1"}')
  if [[ $(echo "$job" | jq .claimed) == "false" ]]; then sleep 5; continue; fi

  id=$(echo "$job" | jq .job.id)
  if do_work_based_on "$job"; then
    curl -sXPOST $B/api:job-retry/jobs/$id/complete -H 'Content-Type: application/json' -d '{"result":{"ok":true}}'
  else
    curl -sXPOST $B/api:job-retry/jobs/$id/fail     -H 'Content-Type: application/json' -d '{"error":"provider 5xx"}'
  fi
done

3. Inspect dead-letter

curl -s $B/api:job-retry/dlq
curl -sXPOST $B/api:job-retry/dlq/7/requeue    # give it another shot

Backoff formula

When a job fails and retries remain, run_after is set to:

now + backoff_seconds * 2^(attempts - 1)

With default backoff_seconds=30 and max_attempts=5:

attempt delay before retry
1 30 s
2 1 min
3 2 min
4 4 min
(fails a 5th time → dead-letter)

Tune backoff_seconds and max_attempts per enqueue.

Schema

jr_job

field type notes
id int PK
created_at / updated_at timestamp
queue text default "default"
job_type text app-defined discriminator
payload json whatever the worker needs
status enum queued / running / completed / dead (no endpoint writes failed; the fail endpoint transitions to queued on retry or dead when exhausted)
attempts / max_attempts / backoff_seconds int
run_after timestamp jobs aren't claimable until this time
claimed_at / claimed_by timestamp / text who has it
last_attempted_at / last_error / completed_at / result audit trail

Indexed on status, queue, run_after, created_at desc.

Design notes

  • claim is the atomic primitive. Flip queuedrunning and return the row in one endpoint. Workers never see jobs that are dead, completed, or scheduled for the future.
  • run_after gates everything. Schedule a job for later by setting it at enqueue. Retries set it automatically via the backoff formula.
  • Requeue resets attempts. DLQ requeue is "give up on the retry history and try again" — so use it after you've fixed the bug, not as a way to keep the same retry clock running.
  • Delete vs dead-letter. There is no delete endpoint. If you want to drop dead jobs, do it at the DB level. Keeping them around is usually the right default for post-mortems.
  • At-least-once, not exactly-once. If a worker dies mid-job the row stays in running until you build reaping (e.g., a task that re-queues running jobs whose last_attempted_at is too old). Intentionally out of scope for this module.

License

MIT.

About

Durable job queue for Xano with exponential-backoff retries and a dead-letter tier. Enqueue, claim, complete, fail.

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages