Skip to content

Import messages and users concurrently to speed up large migrations - #67

Open
tgvashworth wants to merge 2 commits into
google:masterfrom
tgvashworth:concurrent-imports
Open

Import messages and users concurrently to speed up large migrations#67
tgvashworth wants to merge 2 commits into
google:masterfrom
tgvashworth:concurrent-imports

Conversation

@tgvashworth

Copy link
Copy Markdown

Summary

  • The script imports every message strictly one-at-a-time, even though Gmail's API quota allows far more throughput. On a real test migration this measured ~0.65 messages/sec against a per-user quota ceiling of ~4/sec — under 20% utilization.
  • Adds --message_concurrency (default 4) to import multiple messages from one mbox file in parallel, and --user_concurrency (default 4) to import multiple users in parallel. Each user has an independent Gmail API quota bucket, so user-level concurrency is the main lever for migrations covering many mailboxes.
  • httplib2.Http isn't safe to share across threads (per the google-api-python-client thread safety guide), so each worker thread gets its own Gmail service built from the same (thread-safe) credentials. In-flight requests are bounded via a sliding window so memory use doesn't grow with the size of a large mbox file.
  • Both flags default to the old fully-sequential behavior when set to 1, so existing invocations are unaffected unless you opt in.

Why

Gmail's documented quota for messages.import is 25 units per call, with a default of 6,000 units/minute per user (~4 imports/sec sustained) and 1,200,000 units/minute per project. The script's one-message-at-a-time loop meant a migration was bottlenecked by its own blocking I/O long before it ever got close to that quota — large migrations (many users, multi-gigabyte mailboxes) could take far longer than necessary.

Test plan

  • python -m unittest tests.test_import — all 11 tests pass (8 existing + 3 new covering concurrent message import, --from_message correctness under concurrency, and multi-user aggregation)
  • Benchmarked against a live test Gmail account: 200 real messages imported in 54s (3.7/sec) at --message_concurrency 8, and in 49s (4.1/sec) at --message_concurrency 16 — confirming throughput caps out at Gmail's documented per-user quota ceiling rather than being limited by the script, with zero errors or retries in either run
  • Verified the same per-thread-service approach works correctly when resuming a real multi-thousand-message import via --from_message

Refs #19

The script imported one message at a time, fully sequentially, which left
most of Gmail's per-user API quota unused (observed ~0.65 messages/sec vs.
a ~4/sec quota ceiling). For migrations covering many users and gigabytes
of mail this made large imports impractically slow.

- Add --message_concurrency (default 4) to import multiple messages from
  one mbox file in parallel, using a per-thread Gmail service since
  httplib2 isn't safe to share across threads.
- Add --user_concurrency (default 4) to import multiple users in parallel,
  since each user has an independent Gmail API quota bucket.
- Bound in-flight requests via a sliding window so memory use doesn't grow
  with the size of a (potentially huge) mbox file.
- Both default to the old sequential behavior when set to 1.
- Document the quota math and new flags in the README.

Benchmarked against a real test mailbox: ~6x speedup (0.65 -> ~4
messages/sec), which lines up with Gmail's documented per-user quota
ceiling (6,000 units/minute / 25 units per messages.import call).
Self-review caught a serious bug: get_service_for_thread cached "first
call wins" per thread, but threads in the --user_concurrency pool are
reused across different users. With --message_concurrency 1 (the
documented fallback to one-at-a-time behavior) and more users than
--user_concurrency, a reused thread would silently keep importing
messages using a stale, previously-cached user's credentials instead of
the current user's - i.e. mail could land in the wrong mailbox.

- Key the per-thread service cache on credentials identity, so a reused
  thread rebuilds its service when it sees different credentials.
- Make --message_concurrency 1 actually skip the per-thread cache and use
  the passed-in service directly, matching what the docstring already
  claimed.
- Wrap per-thread service construction in a try/except so a transient
  failure counts as one failed message instead of raising out of
  process_mbox_file and aborting the rest of that user's import.
- Validate --message_concurrency/--user_concurrency >= 1 with a clear
  argparse error instead of a raw ValueError from ThreadPoolExecutor.
- Simplify imap_bounded to track a set of futures instead of a dict with
  unused values.

Reproduced the original bug mechanism in isolation (7/8 mismatches under
thread reuse) and confirmed the fix resolves it (0/8), in addition to the
new regression tests.
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