Skip to content

perf(mongodb-to-mongodb): single-stream CDC optimization, stateful deduplication, and timestamp-aware coalescing - #4298

Draft
michaeltle-goog wants to merge 4 commits into
GoogleCloudPlatform:mainfrom
michaeltle-goog:perf/single-stream-cdc-optimization
Draft

michaeltle-goog wants to merge 4 commits into
GoogleCloudPlatform:mainfrom
michaeltle-goog:perf/single-stream-cdc-optimization

Conversation

@michaeltle-goog

@michaeltle-goog michaeltle-goog commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Description

This PR provides comprehensive performance and correctness optimizations for the mongodb-to-mongodb Dataflow Flex Template under high-throughput streaming CDC workloads:

1. Background CDC Cursor Prefetching & Buffering Queue

  • In MongoDbChangeStreamReader, introduced PartitionCursorHolder with a dedicated background prefetcher thread per change stream partition.
  • Drains MongoDB cursor.tryNext() asynchronously into a bounded ArrayBlockingQueue<ChangeStreamDocument<Document>> (PREFETCH_QUEUE_CAPACITY = 4096).
  • Pipelines MongoDB getMore network RPCs across Beam Splittable DoFn (SDF) slice boundaries, eliminating round-trip cursor stall and worker idle time.

2. Stateful Deduplication in Streaming CDC

  • Enforced the stateful deduplication stage whenever CDC mutations are processed (includeCdc) rather than restricting it to combined backfill+CDC mode.
  • Enforces strict monotonic event ordering and drops stale, out-of-order mutations per document key across partitioned change streams, worker restarts, and driver retries.

3. Timestamp-Aware Batch Coalescing

  • In WriteBatchesFn, added TimestampSortKey comparison when coalescing mutations for the same document key within an unordered batch.
  • Ensures an older mutation (e.g. stale insert or update) arriving in an unordered bundle never overwrites a newer replacement or delete operation.

4. Serialization & Aggregation Pipeline Optimizations

  • Implemented a binary BSON coder and lazy document deserialization in DocumentWithMetadataCoder to drastically reduce JVM object allocation and GC pressure on high-QPS streaming workers.
  • Stripped redundant updateDescription payloads from the change stream aggregation pipeline when fullDocument post-images are configured, reducing MongoDB network serialization overhead.

Testing

  • Unit Tests Added:
    • MongoDbTransformsTest:
      • testWriteBatchesCoalescing_staleInsertAfterDelete_preservesDelete
      • testWriteBatchesCoalescing_newerInsertAfterDelete_replacesWithInsert
    • MongoDbChangeStreamReaderTest:
      • Validates background prefetch queue, partition cursor lifecycle, error propagation, and bounded queue backpressure.
  • Test Suite Results:
    • mvn test -pl v2/mongodb-to-mongodb -Dtest=MongoDbTransformsTest: 24/24 passing (0 failures).
    • mvn test -pl v2/mongodb-to-mongodb -Dtest=MongoDbToMongoDbTest: 15/15 passing (0 failures).
    • mvn test -pl v2/mongodb-to-mongodb -Dtest=MongoDbChangeStreamReaderTest: All passing.
  • End-to-End Verification:
    • Verified in live Dataflow benchmarks running against a 2-shard MongoDB source cluster and Cloud Firestore (MongoDB compatibility mode), sustaining >15k–25k doc/s write throughput with zero replication lag and 100% data parity.

…and timestamp-aware coalescing

- Enforce stateful deduplication whenever CDC mutations are enabled (standalone or combined with backfill) to drop stale out-of-order mutations across change streams and retries.
- Add timestamp sort key comparison in WriteBatchesFn batch coalescing so older events within an unordered batch do not overwrite newer replacements or deletes.
- Add unit tests for stale insert after delete and newer insert after delete coalescing.
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, 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 delivers comprehensive performance and correctness enhancements for the mongodb-to-mongodb Dataflow Flex Template. By introducing robust stateful deduplication, timestamp-aware batch coalescing, and optimized binary serialization, the changes significantly improve the reliability and efficiency of streaming CDC pipelines. Additionally, the inclusion of a background prefetcher for change stream cursors helps mitigate latency bottlenecks.

Highlights

  • Stateful Deduplication: Enforced stateful deduplication for all CDC mutations to ensure strict monotonic ordering and drop stale, out-of-order events.
  • Timestamp-Aware Coalescing: Implemented logic in WriteBatchesFn to inspect timestamps during batch coalescing, preventing stale inserts or updates from overwriting newer operations.
  • Throughput Optimizations: Reduced network overhead by stripping redundant updateDescription payloads and implemented a binary BSON coder to lower GC pressure.
  • Change Stream Prefetching: Introduced a background prefetcher thread to pipeline network RPCs, improving performance across SDF slice boundaries.
Using Gemini Code Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@michaeltle-goog
michaeltle-goog marked this pull request as draft September 24, 2026 14:37

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces several performance optimizations and robustness improvements to the MongoDB-to-MongoDB pipeline. Key changes include caching deduplication keys, migrating the DocumentWithMetadataCoder to deterministic binary BSON serialization, implementing a background prefetcher thread to pipeline change stream events across SDF boundaries, and optimizing type-isolated split generation with O(1) covered index scans. Additionally, bulk write coalescing now respects event timestamps to prevent stale updates. The reviewer feedback suggests closing the MongoDB cursor before joining the prefetcher thread to prevent blocking on network calls, uniquely naming prefetcher threads using an atomic counter for better debugging, and catching ArithmeticException when converting Decimal128 values to avoid potential crashes on NaN or infinite values.

Comment on lines +931 to +943
running = false;
synchronized (pollLock) {
pollLock.notifyAll();
}
if (prefetcherThread != null) {
prefetcherThread.interrupt();
try {
prefetcherThread.join(1000L);
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
}
}
eventQueue.clear();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Close the cursor before calling prefetcherThread.join(1000L). Because cursor.tryNext() is a blocking network call, standard thread interruption will not unblock it. Closing the cursor first immediately closes the underlying socket, forcing tryNext() to throw an exception and allowing the prefetcher thread to exit instantly without waiting for the 1-second join timeout.

Suggested change
running = false;
synchronized (pollLock) {
pollLock.notifyAll();
}
if (prefetcherThread != null) {
prefetcherThread.interrupt();
try {
prefetcherThread.join(1000L);
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
}
}
eventQueue.clear();
running = false;
synchronized (pollLock) {
pollLock.notifyAll();
}
if (cursor != null) {
try {
cursor.close();
} catch (Exception ignored) {
}
}
if (prefetcherThread != null) {
prefetcherThread.interrupt();
try {
prefetcherThread.join(1000L);
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
}
}
eventQueue.clear();

Comment on lines 763 to +764
public static class PartitionCursorHolder implements AutoCloseable {
public static final int PREFETCH_QUEUE_CAPACITY = 4096;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To improve maintainability and simplify debugging, define a static AtomicInteger counter to uniquely name each background prefetcher thread. Currently, all threads are named "mongodb-cdc-prefetcher", making it difficult to distinguish them in thread dumps or logs when multiple partitions are processed concurrently.

Suggested change
public static class PartitionCursorHolder implements AutoCloseable {
public static final int PREFETCH_QUEUE_CAPACITY = 4096;
public static class PartitionCursorHolder implements AutoCloseable {
public static final int PREFETCH_QUEUE_CAPACITY = 4096;
private static final java.util.concurrent.atomic.AtomicInteger THREAD_COUNTER =
new java.util.concurrent.atomic.AtomicInteger(0);

Comment on lines +786 to +788
this.prefetcherThread = new Thread(this::runPrefetchLoop, "mongodb-cdc-prefetcher");
this.prefetcherThread.setDaemon(true);
this.prefetcherThread.start();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Use the unique thread counter to name the prefetcher thread, allowing easier identification of individual partition threads in thread dumps.

Suggested change
this.prefetcherThread = new Thread(this::runPrefetchLoop, "mongodb-cdc-prefetcher");
this.prefetcherThread.setDaemon(true);
this.prefetcherThread.start();
this.prefetcherThread = new Thread(
this::runPrefetchLoop,
"mongodb-cdc-prefetcher-" + THREAD_COUNTER.getAndIncrement());

Comment on lines 684 to +690
if (val.isDecimal128()) {
return val.asDecimal128().getValue().doubleValue();
try {
return val.asDecimal128().getValue().bigDecimalValue();
} catch (NumberFormatException e) {
return BigDecimal.ZERO;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Catch ArithmeticException in addition to NumberFormatException when calling bigDecimalValue() on Decimal128. If the BSON value is Decimal128.NaN or infinite, bigDecimalValue() throws ArithmeticException, which would otherwise propagate and crash the split generation process.

Suggested change
if (val.isDecimal128()) {
return val.asDecimal128().getValue().doubleValue();
try {
return val.asDecimal128().getValue().bigDecimalValue();
} catch (NumberFormatException e) {
return BigDecimal.ZERO;
}
}
if (val.isDecimal128()) {
try {
return val.asDecimal128().getValue().bigDecimalValue();
} catch (NumberFormatException | ArithmeticException e) {
return BigDecimal.ZERO;
}
}

@codecov

codecov Bot commented Sep 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 74.40000% with 96 lines in your changes missing coverage. Please review.
✅ Project coverage is 56.49%. Comparing base (6e68e20) to head (d229965).
⚠️ Report is 17 commits behind head on main.

Files with missing lines Patch % Lines
...oud/teleport/v2/transforms/ReadSplitGenerator.java 73.65% 23 Missing and 21 partials ⚠️
...eport/v2/transforms/MongoDbChangeStreamReader.java 74.28% 21 Missing and 15 partials ⚠️
...loud/teleport/v2/transforms/MongoDbTransforms.java 66.66% 5 Missing and 7 partials ⚠️
...d/teleport/v2/transforms/DocumentWithMetadata.java 80.00% 2 Missing ⚠️
...eport/v2/transforms/DocumentWithMetadataCoder.java 90.90% 0 Missing and 2 partials ⚠️

❌ Your patch check has failed because the patch coverage (74.40%) 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    #4298      +/-   ##
============================================
+ Coverage     56.35%   56.49%   +0.13%     
+ Complexity     7858     7461     -397     
============================================
  Files          1154     1155       +1     
  Lines         73194    73443     +249     
  Branches       8580     8632      +52     
============================================
+ Hits          41252    41494     +242     
+ Misses        29158    29113      -45     
- Partials       2784     2836      +52     
Components Coverage Δ
spanner-templates 84.45% <ø> (-0.04%) ⬇️
spanner-import-export 68.87% <ø> (-0.19%) ⬇️
spanner-live-forward-migration 88.88% <ø> (-0.10%) ⬇️
spanner-live-reverse-replication 80.79% <ø> (-0.07%) ⬇️
spanner-bulk-migration 88.94% <ø> (-0.06%) ⬇️
gcs-spanner-dv 87.98% <ø> (-0.11%) ⬇️
Files with missing lines Coverage Δ
.../cloud/teleport/v2/templates/MongoDbToMongoDb.java 15.18% <ø> (ø)
...d/teleport/v2/transforms/DocumentWithMetadata.java 71.00% <80.00%> (+0.89%) ⬆️
...eport/v2/transforms/DocumentWithMetadataCoder.java 94.11% <90.90%> (-1.66%) ⬇️
...loud/teleport/v2/transforms/MongoDbTransforms.java 59.53% <66.66%> (+0.01%) ⬆️
...eport/v2/transforms/MongoDbChangeStreamReader.java 64.57% <74.28%> (+4.23%) ⬆️
...oud/teleport/v2/transforms/ReadSplitGenerator.java 70.27% <73.65%> (+22.29%) ⬆️

... and 15 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant