Skip to content

Faster import lookups: lookup by file_id in batches of 199 (#513) - #654

Merged
lgelauff merged 2 commits into
masterfrom
perf/import-lookup-file-id
Oct 5, 2026
Merged

lgelauff merged 2 commits into
masterfrom
perf/import-lookup-file-id

Conversation

@lgelauff

@lgelauff lgelauff commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator

Speeds up the import's lookup of existing entries. Related: #513, #650, #652.

Change

  • For campaigns opened from 2026-06-01 where every file has a file_id: look existing entries up by file_id, in batches of 199 (ix_entries_file_id). Every file new to Montage since 2026-05-31 has one.
  • Files not found by file_id are looked up by name only where entries.name is indexed (beta; production after Category imports time out on production: every file-name lookup reads the whole entries table #650), so older files keep their row. Without that index they get a second row.
  • Which indexes exist is read on the request's own connection (reflection on the engine fails because the app's SET NAMES listener closes connectionless connections; found on montage-dev).
  • Other imports (older campaigns, CSV) are unchanged. No schema change; FORCE INDEX documented as fallback.

Tests

6 new tests in test_import_entries.py, each checked to fail without its part of the change. Full suite: 56 passed, 1 xfailed.

On montage-dev (copy of production, production's indexes, no name index): a category import of 17,898 new files took ≤ 13 s; on master a 6,000-file import times out.

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

lgelauff and others added 2 commits October 5, 2026 09:57
The import looks up existing entries by name in batches of 200, twice
per batch (lookup, and read-back after the multi-row insert). Two things
make that slow on production (#513, #650):

- Production has no index on entries.name (lost when the tables were
  converted to utf8mb4_unicode_ci / Compact on 2025-07-01), so every
  batch reads the whole 2M-row table. On a copy of production (#652) a
  ~6,000-file category import was killed after 31 s, nothing saved.
- MariaDB's eq_range_index_dive_limit is 200: from 200 values in an
  IN (...) list on, the optimizer estimates from index statistics
  instead of counting in the index. Measured on production (read-only,
  2026-10-05) for newest, oldest, random and new file_ids: 50, 150 and
  199 ids always use ix_entries_file_id (1-7 ms, also with 0 hits),
  200, 201 and 400 ids always read the whole table (0.56 s warm,
  9.4 s cold).

Change: for campaigns opened on or after 2026-06-01, when every file in
the import has a file_id (wikireplica imports; CSV imports have none),
add_entries finds existing rows, and reads new rows back, by file_id in
batches of 199 (FILE_ID_LOOKUP_CHUNK_SIZE). No index hint is needed;
FORCE INDEX is documented next to the constant as a fallback. On
production the last category-imported entry without a file_id dates
from 2026-05-30 13:21 and the first with one from 2026-05-31 20:52, so
every file new to Montage after the cutoff has its file_id.

Files not found by file_id are looked up by name only where
entries.name is indexed (beta, fresh installs, production once #650's
index is restored); a row found that way is reused if it has no
file_id, so older files keep their row and the import does not fail on
beta's unique name index. Without a name index (production today) that
lookup is skipped; such files get a second row. The indexes are read
from the database and cached for ten minutes. A renamed file (same
file_id) keeps its existing row. Other imports are unchanged; no schema
change.

Tests: no name lookup after the cutoff with production's indexes, and
re-imports keep row ids; name lookup before the cutoff and without a
file_id; a renamed file keeps its row; a file without file_id keeps its
row where name is indexed; every file_id lookup stays below 200 values.
Each fails without its part of the change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found on montage-dev: a 17,000-file import failed after 4 s with
"ResourceClosedError: This Connection is closed [SQL: 'SHOW CREATE
TABLE entries']" from _entries_indexes.

inspect(engine) reflects through connectionless execution, on a
connection that closes itself once its first result is exhausted. The
app's engine_connect listener (set_mysql_session_charset_and_collation)
runs SET NAMES on every new connection; that result has no rows, so it
is exhausted at once and the connection closes before the reflection
query runs. Reflecting on rdb_session.connection() uses the request's
own connection instead (no new connection, same transaction).

Reproduced and verified on MariaDB with the app's engine and listeners
(old: fails with the same error; new: indexes read, session still
usable). Test: an engine_connect listener that runs a statement without
rows (like SET NAMES) no longer breaks _entries_indexes; fails with the
old code with the same ResourceClosedError.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant