rust-job-queue-api-worker-system 0.1.0

A production-shaped Rust job queue: Axum API + async workers + Postgres SKIP LOCKED dequeue, retries with decorrelated jitter, idempotency, cooperative cancellation, OpenAPI, Prometheus metrics.
# API Reference

All endpoints return JSON. Error bodies have the shape `{"error": "<slug>", "message": "<human readable>"}`.

The full OpenAPI 3.0 spec is served at `GET /api-docs/openapi.json` and rendered at `GET /docs` (Swagger UI).

---

## POST /jobs

Enqueue a job. Returns the created (or, on idempotent retry, existing) job.

**Headers**

| Header | Required | Notes |
|---|---|---|
| `Content-Type: application/json` | yes | |
| `Idempotency-Key: <string>` | no | If supplied, repeated POSTs with the same key return the same job. Header takes precedence over `idempotency_key` in the body. |

**Body**

```json
{
  "kind": "send_email",
  "payload": {"to": "a@b.c", "subject": "hi", "body": "hello"},
  "max_attempts": 3,
  "idempotency_key": "user-42-welcome"
}
```

`kind` is one of: `send_email`, `resize_image`, `summarize_text`, `webhook_delivery`. The shape of `payload` must match the kind — see [src/payload.rs](../src/payload.rs).

**Responses**

| Code | When |
|---|---|
| 201 Created | A new row was inserted. Body is the new job. |
| 200 OK | The idempotency key matched an existing row. Body is the existing job (same shape). |
| 422 Unprocessable Entity | `payload` is missing required fields for the kind. |
| 500 Internal Server Error | DB or serialization failure. |

**curl**

```bash
curl -i -X POST http://localhost:8080/jobs \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: user-42-welcome' \
  -d '{
    "kind": "send_email",
    "payload": {"to": "a@b.c", "subject": "hi", "body": "hello"}
  }'
```

---

## GET /jobs/{id}

Fetch a single job by id.

**Responses**

| Code | When |
|---|---|
| 200 OK | Body is the job. |
| 404 Not Found | No row with that id. |

**curl**

```bash
curl -i http://localhost:8080/jobs/0190f9a0-1234-7000-8000-000000000001
```

---

## GET /jobs

List jobs in reverse-chronological order.

**Query**

| Param | Type | Default | Notes |
|---|---|---|---|
| `status` | `queued \| running \| succeeded \| retrying \| failed_permanent \| cancelled` | _(all)_ | Exact match |
| `kind` | `send_email \| resize_image \| summarize_text \| webhook_delivery` | _(all)_ | Exact match |
| `limit` | int | 50 | Clamped to `[1, 200]` |
| `offset` | int | 0 | |

**Responses**

| Code | Body |
|---|---|
| 200 OK | JSON array of jobs |

**curl**

```bash
curl 'http://localhost:8080/jobs?status=succeeded&limit=10'
```

---

## POST /jobs/{id}/cancel

Request cancellation.

**Responses**

| Code | When | Body |
|---|---|---|
| 200 OK | The job was `queued` / `retrying`. The status is now `cancelled` (atomic transition). | `{"status": "cancelled", "job_id": "..."}` |
| 202 Accepted | The job is `running`. The `cancel_requested` flag is set; the worker will observe it at the next sub-step. | `{"status": "pending", "job_id": "..."}` |
| 404 Not Found | No such job. | error body |
| 409 Conflict | The job is already in a terminal state (`succeeded` / `failed_permanent` / `cancelled`). | error body |

**curl**

```bash
curl -i -X POST http://localhost:8080/jobs/0190f9a0-1234-7000-8000-000000000001/cancel
```

---

## GET /health

DB-level liveness check (`SELECT 1`).

**Responses**

| Code | Body |
|---|---|
| 200 OK | `{"status": "ok"}` |
| 500 | DB unreachable |

```bash
curl http://localhost:8080/health
```

---

## GET /metrics

Prometheus text-format metrics, rendered from the `metrics-exporter-prometheus` recorder installed at startup.

The API binary serves `/metrics` on the API port, usually `8080`. The worker binary serves its own `/metrics` endpoint on `WORKER_METRICS_BIND_ADDR`, usually `0.0.0.0:9091`, and emits worker counters/histograms such as `worker_jobs_started_total`, `worker_jobs_completed_total`, and `worker_job_duration_seconds`.

```bash
curl http://localhost:8080/metrics
curl http://localhost:9091/metrics
```

---

## GET /docs

Swagger UI rendered from `ApiDoc` (utoipa-generated). Open in a browser.

## GET /api-docs/openapi.json

OpenAPI 3.0 JSON spec.

```bash
curl http://localhost:8080/api-docs/openapi.json | jq '.paths | keys'
```

---

## Error shape

Any 4xx/5xx response uses this body:

```json
{
  "error": "validation",
  "message": "payload validation failed for send_email: missing field `body`"
}
```

The `error` slug is one of: `not_found`, `bad_request`, `conflict`, `validation`, `internal`.

For 500 responses the `message` is redacted to `"internal server error"`; full detail is in the structured tracing logs.