Request Lifecycle¶
A Request is one user's extraction job. Its status drives the whole
pipeline — which background job looks at it, whether it is visible to the sweep,
and whether anything will ever pick it up again. Most operational questions
reduce to "what status is it in, and what moves it out".
Statuses¶
| Status | Meaning | What moves it on |
|---|---|---|
4 |
Submitted, awaiting materialization | materialize_request_tasks → -1 |
3 |
Awaiting custom boundary ingestion | ingest_custom_boundary_task |
-1 |
Queued, never yet swept | the sweep claims it → 2 |
0 |
Queued, previously swept and not ready | the sweep claims it → 2 |
2 |
A sweep is working on it | that sweep → 1, or back to 0 |
1 |
Complete, output downloadable | terminal |
-2 |
Error | terminal (see Recovery) |
Two of these are easy to misread:
0and-1are both "queued". The sweep selects exactly these two. The difference is only whether the "request received" email has been sent — which is tracked byprepare_timebeing set, not by the status.2is a claim, and it is committed. It is written in its own short transaction so other sweeps can see it and skip the request. Before that it was written inside the long work transaction and was invisible for exactly as long as it mattered. See Database §4.
The path through¶
submit ──► 4 ──► -1 ──► 2 ──► 1
│ ▲ │
│ └──────┘ not ready: back to 0, retried next sweep
└──► -2 nothing resolvable
- Submit.
create_requestwrites the row atstatus=4and returns immediately. Task creation is deferred so the HTTP request does not block. - Materialize. A
post_savesignal dispatchesmaterialize_request_tasks, which resolves the plan, createsExtractTaskandRequestMaprows, and moves the request to-1. It is idempotent — it deletes existingRequestMaprows before recreating them, so re-triggering a stuck request is safe. - Extract tasks run. A request's tasks are priority-bumped so they are claimed ahead of the general backlog. Tasks already computed for a previous request are reused rather than recomputed.
- Sweep.
process_user_requestsclaims the request (2), checks whether every task is done, and either builds the output or returns it to0. - Build and notify. Output is written to a temp directory and renamed into
place, the status goes to
1, and the completion email is sent.
The sweep runs on a 5-minute beat and is also triggered by a signal when materialization finishes, so a request does not usually wait a full interval.
Timings to expect¶
From a real 2,890-task request, with the fleet processing ~43k tasks/min:
| Stage | Duration |
|---|---|
| Submit → materialized | ~5 s |
| Materialized → first sweep claim | ~45 s |
| Extract tasks (2,890, priority-bumped) | ~3 min |
| Output build | ~2 s |
| Total | ~4 min |
Build time is dominated by task count only through the merge, which is batched — a 1,040-task build that once took 3 h 38 m now takes seconds. If a build is taking minutes, something is wrong; see Database §7.
Recovery¶
Every state that means "something is working on this" strands the request if the
worker dies, so each has a reaper. All three run hourly inside
reset_stale_requests:
| Stuck at | Recovered by | Returns to |
|---|---|---|
4 (materialization never ran) |
_redispatch_unmaterialized_requests |
re-dispatches, stays 4 until it works |
2 (sweep died mid-build) |
_reset_stale_requests |
0 — not -1, which would re-send the received email |
-2 (errored) |
reset_errored_requests, manually |
0 |
A long-running build is not stranded: the sweep heartbeats its claim, so
process_time stays fresh and the reaper leaves it alone.
Checking on a request¶
SELECT left(id::text,8) AS id, status,
submit_time, prepare_time, process_time, complete_time,
(SELECT count(*) FROM request_map rm WHERE rm.req_id = r.id) AS tasks
FROM requests r ORDER BY submit_time DESC LIMIT 5;
Read it as:
prepare_timeset → it has been claimed at least once (and the received email has gone out)process_timeadvancing whilestatus=2→ a build is running and heartbeating, not stuckstatus=2with aprocess_timeolder thanSTALE_TASK_MINUTES→ the sweep died; the reaper will return it to0status=4with tasks already mapped → materialization ran but the status was moved back by hand; nudge it to-1
Task progress for one request:
SELECT et.status, count(*)
FROM request_map rm
JOIN extract_tasks et ON et.dataset_id = rm.dataset_id AND et.id = rm.task_id
WHERE rm.req_id = '<uuid>'
GROUP BY 1 ORDER BY 1;
Note the join carries dataset_id — without it this scans every partition. That
rule applies everywhere; see Database §1.
Things worth knowing¶
- A bulk
UPDATEdoes not firepost_save. Setting a status withRequest.objects.filter(...).update(status=4)skips the signal, so nothing dispatches materialization. This is how one request sat at4for four days. Use.save()if you want the signal, or re-dispatch the task explicitly. status=4looks healthy. It is a normal transient state, and-2is the only status monitoring reads as an error, so a stranded request is invisible. The fingerprint worth alerting on is requests at4older than an hour.- Output directories starting with
.are transient..{id}.building.{hex}is an in-progress build (disposable);.{id}.replaced.{hex}is displaced output and may be the only copy of a completed request's results — never delete it blindly.