Repository navigation
Batch entry inserts so large category imports finish within the request timeout - #623
Merged
Merged
Conversation
Category imports inserted two rows per file one statement at a time (entries via ORM add + autoflush, then round_entries), so a ~21.5k-file category made ~43k round trips to the tools DB inside one request and was killed by gunicorn's 30 s worker timeout on montage-beta. add_entries now inserts each chunk's new entries with one multi-row INSERT and reads them back by name for their ids; add_round_entries inserts all round entries with one executemany. total_entries is counted in SQL because the core insert bypasses the session. For a 4,000-file import this drops the statement count from 8,048 to 89. Refs #618 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds montage/tests/test_import_entries.py: - parity: the same scenario (seeded entries, three chunks, non-ASCII, quotes, percent signs, emoji, reuploads, missing file_id, every autodisqualify reason) run through the previous per-row implementation and the batched one must leave identical rows and responses - chunk boundaries (1, 199, 200, 201, 401), empty category, exact and case-variant duplicates, re-import into the same round, entries shared between campaigns - a failure after the inserts rolls everything back - activation assigns quorum x eligible tasks for imported entries - total_entries is right when the round's entries were already loaded - pymysql turns both INSERTs into multi-row statements Also expires the round's round_entries after the batched insert: without it, a round whose entries were loaded earlier in the request reports a stale total_entries (caught by the new test). Refs #618 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
create_app() bootstraps the maintainers through get_mw_userid() before any responses mock is active, so every app a test created called the live Commons API. The new import tests create many apps; on PR #623 CI the Commons API started returning non-JSON partway through the run and every later fixture setup failed with JSONDecodeError. An autouse fixture now replaces montage.rdb.get_mw_userid with an offline lookup: Slaporte keeps their real id (the devtest session cookie carries it), everyone else gets a stable crc32-based id. With network access blocked, master gives 5 passed / 9 errors; this branch gives 29 passed, 1 xfailed. Refs #620 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 1, 2026
This was referenced Oct 2, 2026
Collaborator
Author
|
Tested on montage-dev in synchronous import mode (the way production runs), on 2026-10-02, on an integration branch with #623, #626, #628, #630 and #631:
Activating the 21.5k-file round afterwards got the webservice OOMKilled. That is activation, not changed by this PR; it is fixed in #632, with which the same round activated in 4.5 s. Written with Claude (Claude Code) on behalf of @lgelauff. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #620, the immediate cause of #618. Not a final fix for large imports: that is the background import job (#621 backend, #622 frontend).
Why
On the buildservice deploy, gunicorn's 30 s worker timeout kills large category imports. On montage-beta, a ~21.5k-file category was aborted at 30.5 s and left an empty round (details in #618).
The time goes into the database writes. Every imported file costs two single-row INSERTs, one into
entriesand one intoround_entries, so 21.5k files mean ~43k round trips inside one request. Lookups have been chunked since 2016, but inserts have been one row at a time since then. The old webservice had no 30 s limit, so this only started failing after the move to the buildservice.What changes
CoordinatorDAO.add_entries: new entries in each 200-entry chunk are inserted with one multi-row INSERT, then read back by name to get their ids.CoordinatorDAO.add_round_entries: all round entries are inserted with one executemany. The round'sround_entriescollection is then expired sototal_entriesis not stale.For a 4,000-file import (local SQLite), the number of statements drops from 8,048 to 89. On MariaDB, pymysql turns both executemany calls into multi-row INSERT statements: a test checks that the compiled statements match the pattern pymysql batches, but the suite does not run against MariaDB. The real-database evidence is the montage-dev run below.
Tests
montage/tests/test_import_entries.py, with synthetic data only:%, emoji and space-containing names, reuploads, missingfile_id, and every autodisqualify reason.total_entriesis right when the round's entries were already loaded.test_web_basic.py: a statement-count regression test (450 files must need at most 3 INSERTs per table).pytest montage/tests/→ 29 passed, 1 xfailed, also with network access blocked.conftest.py: Wikimedia user-id lookups are now mocked for the whole suite. App creation used to call the live Commons API, and the extra tests made CI hit Commons often enough to get throttled.Verified on montage-dev
Deployed
3bd7eedto montage-dev (default gunicorn 30 s timeout) and imported Images from Wiki Loves Monuments 2025 in Russia into a new round:POST /v1/admin/round/130/importsucceeded in 26.7 s, with noWORKER TIMEOUTor traceback in the webservice log.Known temporary limitation
The gunicorn timeout stays at the 30 s default; we are deliberately not raising it. The margin is small (26.7 s for ~21.5k files), and most of the remaining time is no longer the inserts. Categories somewhat larger than ~21.5k files, or a slow wikireplica, can still exceed 30 s and fail as in #618. This is accepted as temporary until imports run in the background (#621, #622).
Not in this PR
VoteINSERT per entry × quorum, seecreate_initial_rating_tasks), so activating a very large round can hit the same 30 s limit. Possibly related to Can not activate a round, because of 500 error #619; not yet verified.FrameLocalsProxy). Separate issue.Refs #618, #621, #622
Written with Claude (Claude Code) on behalf of @lgelauff.
🤖 Generated with Claude Code