Skip to content

Create round tasks with batched inserts, so activating a large round fits in memory - #632

Merged
lgelauff merged 1 commit into
masterfrom
fix/batch-vote-inserts-619
Oct 4, 2026
Merged

lgelauff merged 1 commit into
masterfrom
fix/batch-vote-inserts-619

Conversation

@lgelauff

@lgelauff lgelauff commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator

Fixes #619 (very likely: see below).

What goes wrong

Activating a round builds one Vote object per task, and loads one RoundEntry object per entry, all held in
the request until it commits, and inserts the tasks one row at a time. For large rounds that needs more memory
than the webservice has.

On montage-dev (512 MiB container, 4 gunicorn workers), activating a round of 21,516 tasks got the container
OOMKilled after 6.6 s: the browser got a 503, the round stayed paused with no tasks (the transaction rolled
back), and nothing was logged, because the process was killed rather than raising an error. That matches #619,
an activation failure on production without a clear cause.

The fix

create_initial_rating_tasks and create_ranking_tasks now read only the shuffled round-entry ids and insert
the tasks with executemany in chunks of 5,000; pymysql turns each chunk into multi-row INSERTs. Which juror
gets which entry is unchanged. The session is flushed before the inserts and expired after them, so nothing
loaded earlier in the request is stale.

Measured locally (SQLite, 21,500 tasks): peak Python memory 219 MiB → 12 MiB, time 15.6 s → 1.0 s.

Tests

Not in this PR

  • Reassigning tasks (after changing jurors or the quorum) still loads and updates them one ORM object at a
    time, so it can hit the same limit on very large rounds.
  • The webservice's memory limit is unchanged.

Written with Claude (Claude Code) on behalf of @lgelauff.

🤖 Generated with Claude Code

Activating a round built one Vote object per task, plus one RoundEntry
object per entry, and inserted the tasks one row at a time. On
montage-dev, activating a round of 21,516 tasks ran the 512 MiB
webservice container out of memory (OOMKilled after 6.6 s, the round
rolled back), which is very likely the production activation failure
in #619.

create_initial_rating_tasks and create_ranking_tasks now read only the
shuffled round-entry ids and insert the tasks with executemany in
chunks of 5,000 (pymysql turns each chunk into multi-row INSERTs). The
assignment is unchanged. Measured locally on SQLite for 21,500 tasks:
peak Python memory 219 MiB -> 12 MiB, time 15.6 s -> 1.0 s.

The new test checks one INSERT for a 40-task round (master: 40), quorum
distinct jurors per entry, and the same row contents (status active,
empty flags, a create_date).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@lgelauff
lgelauff merged commit ed9b763 into master Oct 4, 2026
4 checks passed
@lgelauff
lgelauff deleted the fix/batch-vote-inserts-619 branch October 4, 2026 09:37
lgelauff added a commit that referenced this pull request Oct 4, 2026
Resolves the conflict with #632 (#619) in
montage/tests/test_web_basic.py: both PRs added a test at the same
spot; both are kept unchanged (the concurrent-activation test for
#334, then the batched task insert test).
rdb.py merged cleanly: activate_round takes the round lock before
create_initial_tasks, which now inserts in batches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Can not activate a round, because of 500 error Performance: bulk_save_objects should replace iterative session setup for Vote generation

1 participant