Importing a campaign is a background job read by the worker (#1054) #1104
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/1054-import-in-the-worker"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #1054 (D281, the owner's answer to Q153). Not to be merged before the independent review reports.
Summary
media/private/.imports/<job id>.zip. That directory is on the volume the backend and worker share, and nginx already answers 404 under/media/private/.GET /api/campaigns/import/jobs/{id}and…/jobs/latest. A job is visible to the person who started it and to an instance admin; anyone else gets the 404 a missing id gets.What changes in meaning
data.jsonmay be 24 MB and 1,200,000 values (it was 8 MB and 400,000).Two mechanisms beyond the plan
No cap alone made a worker survive every file inside the caps, because the caps bound what is parsed and not what the import then builds.
worker_max_memory_per_child = 300 MB, a Celery setting for every task. Without it an idle worker ratcheted from 208 to 365 MB after two stopped imports. A child over the mark is replaced after its task ends.The row-heavy finding (already true on main)
A file inside the old caps could build far more than it parsed:
Any of these would have killed a 512 MB backend. They are now stopped by the memory bound, in words. Counting rows up front is the better answer and is not built here.
Measured
docker run -m 1g, final code, peak above a 99 MB start:data.jsonThrough nginx with a real worker, against main:
data.json: backenddata.json: workeroom_kill 0, no restarts, and the worker back to 166 MB after each.Migration
d1054a2b3c4eaddsimport_jobs(status as TEXT with a CHECK; no enum). Run by hand against a throwaway Postgres: up, down one, up again, all clean.Bounds on a job
Changelog
changelog.d/1054-import-in-the-worker.changed.mdTest plan
1 failed, 8057 passed. The failure was the "one migration head" pin, which moved to the new migration's test. Not re-run at the head commit; CI is the run that counts.tests/export, the watchdog, session hygiene, enqueue, Celery app, orphan sweep and backup tests:687 passed.338 passed, 1 skipped; eslint 0 errors.🤖 Generated with Claude Code
An import's data.json was parsed inside the backend (512 MB, 340 to 425 MB once warm). The backend now receives the upload under the limits and answers it always had, checks what needs no reading (it is a zip an export could have made, with a data.json), stores it on the media volume the worker shares (media/private/.imports/<job id>.zip, which nothing serves and the media backup leaves out), and queues `read_import`. The task does what the route did from "read data.json" onward: - one transaction: the campaign and the job's "done" are one commit, so a job that reads "working" has no campaign, and a later delivery or the hourly sweep can remove what it stored without asking; - tried once: a delivery that finds its job "working" knows a worker died on it, fails it in words and removes its files and upload, and does not open the file again; - one import at a time, and never beside a map's picture: it takes the map lock, and gives its slot back rather than wait for it; - the walker for the import's maps is queued after the commit, as before. GET /api/campaigns/import/jobs/{id} follows a job (its starter and an administrator; a 404 to anyone else) and /import/jobs/latest finds a person's own again. The limits (two on a server, one a person) are counted from the table under an advisory lock, so they hold across backend processes and for a file that is waiting, not only one arriving. `unstick_lost_tasks` ends a job whose upload stopped arriving, that waited 8 hours, or that has been "working" for an hour, queues again one that has waited a quarter of an hour, and removes uploads nothing will read, including a file with no job. data.json may now be 24 MB and 1,200,000 values (from 8 MB and 400,000), measured in a 1 GB container: the dearest parse inside both grows the process by 309 MB. What an import builds from a file of nothing but rows is not bounded by either cap (at the old caps, 80,000 two-field sessions took a 1 GB container to 946 MB), so the reading process is watched from a thread and stopped by a signal when it has grown by 400 MB, with a sentence. A worker process whose peak passed 300 MB is replaced after its task (worker_max_memory_per_child): what a large import leaves behind is not handed back. Tests that import through the route read the job's end as the import's answer (tests/import_answer.py); the worker's task runs in the request. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>Independent review of
de6d8e1b: nothing serious, seven robustness gaps. Not to be merged until they are fixed.Found sound
campaigns/.detailis one of the app's own sentences, never an exception's text.Findings, being fixed on this branch
exceptreports the wrong sentence;Not checked by the review, to be shown on a real worker: what Celery does with a task queued behind one whose child is being recycled for memory, and what the hard time limit does to a task's acknowledgement.
Not run by the review: the test suites, a real worker, Postgres or Celery.
The review's findings are fixed in
38bfac15, each with a test. A run on a real worker found one more defect, also fixed.On a real worker
One worker process, so "queued behind" is literal.
acks_on_failure_or_timeout=Falseon the import task.Still open, and stated
except Exceptionclauses that return null). An import interrupted there carries on, and the hard limit ends it two minutes later.Co-Authored-Byline.Ran
38bfac15:8416 passed, 14 skipped, 107 deselected.368 passed, 1 skipped; eslint 0 errors.Not run