fix(mongodb-to-mongodb): optimize type discovery using canonical BSON order and O(1) covered seeks - #4270
Conversation
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request optimizes the MongoDB-to-MongoDB migration process by replacing inefficient, parallelized type discovery with a high-performance, O(1) covered index seek strategy. By leveraging the canonical BSON order of the _id index, the system can now determine collection boundaries with minimal overhead, particularly for homogeneous collections. Additionally, the change improves data completeness by removing restrictive upper bounds on splits, ensuring that newly inserted documents are included in the migration. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request refactors the BSON type boundary probing in ReadSplitGenerator to optimize performance. It replaces the parallel executor-based probing with a sequential global min/max check, allowing immediate resolution for homogeneous collections and targeted probing for mixed collections. Additionally, it adds support for boolean types, removes upper-bound constraints on tail slices, and updates unit tests. The feedback suggests further optimizing the intermediate candidate buckets loop by merging the existence check and the minimum bound query into a single sorted query to eliminate a redundant roundtrip.
Codecov Report❌ Patch coverage is
❌ Your patch check has failed because the patch coverage (47.76%) is below the target coverage (80.00%). You can increase the patch coverage or adjust the target coverage. Additional details and impacted files@@ Coverage Diff @@
## main #4270 +/- ##
============================================
- Coverage 56.35% 56.34% -0.02%
+ Complexity 7858 7408 -450
============================================
Files 1154 1154
Lines 73194 73209 +15
Branches 8580 8585 +5
============================================
- Hits 41252 41251 -1
- Misses 29158 29160 +2
- Partials 2784 2798 +14
🚀 New features to boost your workflow:
|
… order and O(1) covered seeks
06c1145 to
5078af2
Compare
Why This Change Was Needed
When running Dataflow backfill migrations from a sharded MongoDB cluster to Ignite,
ReadSplitGeneratortimed out during_idBSON type discovery (probeActiveTypeBounds).1. Type Discovery (
probeActiveTypeBounds)Before (
legacyProbeBuckets)Executors.newFixedThreadPool(6)) that fired 6 concurrent$typequeries across all candidate type buckets (number,string,objectId,binData,object,date), each with.sort({_id: 1}).limit(1)and a 3-secondmaxTimeMS(followed by a second.sort({_id: -1}).limit(1)query for matching buckets).MongoClientis initialized withminSize(0)(0 warm connections). Firing 6 threads concurrently forced 6 simultaneous TCP + TLS + auth handshakes tomongos.{_id: {$type: ...}}is not targeted to a single shard key value,mongosfanned out6 × N_shardsconcurrent cursor requests across every shard.probeActiveTypeBoundsreturned empty[], causinggenerateTypeIsolatedSplitsto abort before running$sampleorObjectIdinterpolation and instead fall back togenerateIndexSliceFilters(which wraps 4 types in a$orquery with$modand a hardcoded 5-yearObjectIdwindow).After (Canonical BSON Order + O(1) Covered Index Seeks)
KNOWN_TYPE_BUCKETSwith MongoDB's canonical BSON comparison order in the{_id: 1}B-tree index (number<string<object<binData<objectId<bool<date).col.find().projection({_id: 1}).sort({_id: 1}).limit(1)->minValcol.find().projection({_id: 1}).sort({_id: -1}).limit(1)->maxValgetBucketForValue(minVal)andgetBucketForValue(maxVal)belong to the same BSON type bucket, by B-tree ordering no other BSON type can exist between them. The method immediately returnsProbedTypeBounds(minBucket, minVal, maxVal)in 2 queries (~2ms) with zero$typequeries sent to the cluster.minIdxandmaxIdxin canonical BSON order.2. Split Generation Hierarchy in the New System
The new system uses a multi-tiered fallback hierarchy across both Type Discovery (Phase 1) and Split Generation (Phase 2) so that even if a downstream step fails or times out, it degrades gracefully using information already gathered:
Phase 0: Small Collection Short-Circuit
generateTypeIsolatedSplitscheckscol.estimatedDocumentCount().estimatedDocs <= 5,000(oreffectiveSplits <= 1), it immediately returns a single unfiltered split[{}]with zero index probes and zero sampling.Phase 1 Fallbacks: Type & Boundary Discovery (
probeActiveTypeBounds)sort({_id: 1}).limit(1)andsort({_id: -1}).limit(1)).minBucket == maxBucket, returns[ProbedTypeBounds(minBucket, minVal, maxVal)]immediately.minValandmaxValbelong to different known BSON buckets (minIdx < maxIdx).minIdxandmaxIdxin canonical BSON order (for (i = minIdx + 1; i < maxIdx; i++)), using an unsortedlimit(1)existence check first and only runningsort({_id: ±1}).limit(1)if that intermediate type actually exists.legacyProbeBuckets):minValormaxValis an unrecognized/exotic BSON type (e.g.,RegEx,Timestamp) or if an unexpected exception occurs during the global min/max seek.$typeprobes.Phase 2 Fallbacks: Split Generation (
generateSplitsForTypeBounds)Once Phase 1 yields
ProbedTypeBounds(bucket, minKey, maxKey), each active type is partitioned using the following cascade:$sampleQuantile Boundaries):[$sample, $project: {_id: 1}, $sort: {_id: 1}](30s timeout) to compute data-skew-aware quantile boundaries ($lte: b_0,$gt: b_{i-1}, $lte: b_i, ...,$gt: b_{last}).$sampletimes out on a large sharded cluster or returns fewer samples thansplits:objectId: CallsgenerateProbedObjectIdSplits(minHex, maxHex, splits)using the exactminKeyandmaxKeycaptured in Phase 1 to linearly interpolatesplitscontiguous 12-byte hex ranges (step = (maxBig - minBig) / splits), leaving the final slice open ($gte: lowHex).number: CallsgenerateNumberFilters(splits)($mod: [splits, r]).string: CallsgenerateStringFilters(splits)(alphanumeric prefix ranges0-9A-Za-z).object/binData/bool/date: Returns a single type-isolated filter[{"_id": {"$type": "<bucket>"}}].$orSplits):legacyProbeBuckets) return zero active types (e.g., cluster unreachable). CallsgenerateIndexSliceFilters(effectiveSplits).3. Final Split Upper Bound (
maxKey)Before
i == boundaryCountori == numSplits - 1ingenerateProbedObjectIdSplits, and single-split filters) appended"$lte": maxKeyto_id._idindex afterprobeActiveTypeBoundsran were excluded from the backfill.After
maxKeyis still used to compute evenly spaced interpolation step sizes (step = (maxBig - minBig) / numSplits), but"$lte": maxKeyis omitted from the final split and single-split filters ({"_id": {"$type": "objectId", "$gte": {"$oid": lowHex}}}).