Skip to content

feat(ingest): accept uploads and queue them as jobs - #437

Merged
lewismc merged 8 commits into
mainfrom
feat/async-ingest
Oct 18, 2026
Merged

lewismc merged 8 commits into
mainfrom
feat/async-ingest

Conversation

@lewismc

@lewismc lewismc commented Oct 4, 2026

Copy link
Copy Markdown
Member

POST/GET /ingest return 202 with a job id instead of holding the request open while every archive member is processed, which caused nginx 504s on large zips. Poll GET /ingest/jobs/{job_id} for status. One worker, started by docker-entrypoint.sh beside Gunicorn, runs queued jobs from the new ingest_job table. Clients that read the old 200 Ingest200 body must poll the job instead.

POST/GET /ingest return 202 with a job id instead of holding the
request open while every archive member is processed, which caused
nginx 504s on large zips. Poll GET /ingest/jobs/{job_id} for status.
One worker, started by docker-entrypoint.sh beside Gunicorn, runs
queued jobs from the new ingest_job table. Clients that read the old
200 Ingest200 body must poll the job instead.
renato2099
renato2099 previously approved these changes Oct 4, 2026

@renato2099 renato2099 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a very nice improvement @lewismc , thank you!

There are a few minor things that we should polish before shipping this, but overall I think it is good. There are two main points I would like to make:

  • The one process for processing files might have be revisited in the future once we need more parallelism.
  • This also doesn't really fix our core issue with data processing
    But we should follow those up separately and those shouldn't be a blocker for this PR

Comment thread tagbase_server/tagbase_server/controllers/ingest_controller.py
Comment thread services/fswatch/post.sh Outdated
Comment thread tagbase_server/tagbase_server/ingest_jobs.py Outdated
Comment thread tagbase_server/tagbase_server/ingest_jobs.py Outdated
Comment thread tagbase_server/tagbase_server/utils/processing_utils.py
Comment thread tagbase_server/tagbase_server/ingest_jobs.py Outdated
Comment thread tagbase_server/tagbase_server/ingest_jobs.py Outdated
Comment thread openapi.yaml Outdated
Comment thread tagbase_server/tagbase_server/ingest_worker.py
Comment thread tagbase_server/tagbase_server/ingest_worker.py Outdated
Comment thread services/fswatch/post.sh Outdated
@lewismc

lewismc commented Oct 9, 2026

Copy link
Copy Markdown
Member Author

keep /ingest as synchronous
add /asyncingest as asynchronous
fswatch uses /ingest
python ingest worker uses /asyncingest

@lewismc lewismc self-assigned this Oct 9, 2026
@lewismc lewismc added this to the 0.17.0 milestone Oct 9, 2026
POST /ingest/jobs streams a .txt, .zip, .tar.gz, or .tgz up to 2 GiB and
returns 202. One worker drains the ingest_job queue. Synchronous /ingest
still finishes inside the request and rejects bodies over 100 MiB.

The eTUFF migration matches histogram rows without joining every
time-series row, and resolves position times with one indexed lookup.
submission.filename stores the eTUFF name. Finished jobs are purged by
cron. fswatch and the upload page use the job API. Archive extraction
uses the Python standard library.
@lewismc

lewismc commented Oct 12, 2026

Copy link
Copy Markdown
Member Author

Follow-up on the review summary: one worker is still the limit for this PR, and the migration cost of a large eTUFF file is a separate problem. Neither blocks this change. Inline replies are on the original threads.

lewismc and others added 4 commits October 12, 2026 13:59
The job upload opened its staging file inside an async function, and
repeated error strings were duplicated literals.
bash 5 treated a zero-second read as another copy of the previous
path, so later drops never reached the job API. The watcher also
exited on startup because its state volume was not writable.
The contract test read the pending list as soon as the second POST
returned, before the watcher finished writing that job.

@renato2099 renato2099 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for the changes @lewismc ! there are still a few things that we will have to circle back to though, but we can do that in the next release

Comment thread services/nginx/proxy/upload.html
);
fileInput.addEventListener("change", updateFileListDisplay);

function loadJobs() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cool! you made the improvements on this file as well

one thing to note is that we will have to split this file shortly, it is already quite large+complex (it's not going to be maintainable by humans 😉 at this rate)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I'm going to create a follow-on issue for this.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filed as #463.

FROM data_profile
WHERE position_date_time IS NULL
) AS m
CROSS JOIN LATERAL (

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this makes sense, but ideally this should have been it's own separate PR such that we can
really validate that the results we are getting are consistent, I am also ok letting this part go in and
coming back to it later on

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I'll create a new issue for this change. We should have a single SQL source now as well. I'll create the issue for that.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filed as #464 for checking that the new position-date match matches the old updates, and #465 for generating the tables-only schema from one SQL source.

send,
413,
_TITLE_TOO_LARGE,
"Ingest jobs accept at most 2 GiB.",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this string should ideally use variable we defined, otherwise it is easy that they go out of sync

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes this was a placeholder. I'll remove the objectivity... however we need to figure out how to ingest files lager than 2GB as well...

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filed as #466 so the 413 text follows the byte-limit constants, and #467 for ingest above 2 GiB.

from tagbase_server.utils.io_utils import TEMP_DIR

SYNC_MAX_BYTES = 100 * 1024 * 1024
JOB_MAX_BYTES = 2 * 1024 * 1024 * 1024

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we want to enforce an upper limit, we should consider it post decompression no? 🤔 I guess it doesn't matter too much right now ... maybe something to think about for later as well

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm... if the file decompresses > available container disk space then we have a candidate "zip bomb". Lets keep this ceiling for now and inspect use cases. This may be big enough... who knows?

Comment thread openapi.yaml
- ingest
x-codegen-request-body-name: body
x-openapi-router-controller: tagbase_server.controllers.ingest_controller
/ingest/jobs:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we will have to change this name in the future as well 😅 as per classic REST API design we should use nouns for resources , so we would have something jobs with its own POST/GET/LIST and if we wanted to make it async, we could have a parameter for it ?sync=true or having /ingestions/sync + /ingestions/async, but we can also do that in a follow up

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes we are going back to my previous comments. I agree with you. You and I know there are a lot of implicit implications with this design decision. I am +1. Lets do it later.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filed as #469.

@sonarqubecloud

Copy link
Copy Markdown

@lewismc

lewismc commented Oct 18, 2026

Copy link
Copy Markdown
Member Author

Follow-ups from the review, for the next release:

#446, #447, and #448 already cover the earlier review notes.

lewismc added a commit that referenced this pull request Oct 18, 2026
Merge origin/main, including queued-upload ingest from #437.
Keep the deferred time-series date index rebuild in both schema files.
lewismc added a commit that referenced this pull request Oct 18, 2026
Merge origin/main, including queued-upload ingest from #437.
Keep the deferred time-series date index rebuild in both schema files.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants