5454import org .junit .runners .JUnit4 ;
5555
5656/**
57- * Test for verifying correctness of CDC processing in {@link DataStreamToSpanner} DataStream to
58- * Spanner template.
57+ * Fault Tolerance (FT) test for verifying the robustness and correctness of Change Data Capture
58+ * (CDC) processing in the {@link DataStreamToSpanner} Dataflow template.
59+ *
60+ * <p>Objective: Verify that the pipeline can successfully process and replicate CDC events from a
61+ * MySQL source database to a Spanner target database, even when facing persistent network timeouts
62+ * and Spanner transaction aborts during peak load.
63+ *
64+ * <p>Edge cases covered in this test include:
65+ *
66+ * <ul>
67+ * <li>Handling simulated Spanner transaction timeouts using the {@code
68+ * TransactionTimeoutInjectionPolicy}.
69+ * <li>Replicating large bursts of CDC events (inserts/updates/deletes) simultaneously while
70+ * undergoing the injected failure condition.
71+ * <li>Ensuring the template successfully retries aborted commits and guarantees consistency in
72+ * CDC event processing without data loss.
73+ * </ul>
5974 */
6075@ Category ({TemplateIntegrationTest .class , SkipDirectRunnerTest .class })
6176@ TemplateIntegrationTest (DataStreamToSpanner .class )
@@ -84,30 +99,38 @@ public class DataStreamToSpannerCDCFT extends DataStreamToSpannerFTBase {
8499 */
85100 @ Before
86101 public void setUp () throws IOException , InterruptedException {
87- // create Spanner Resources
102+ // 1. Create Target Spanner Databases
103+ // The target database stores the replicated data. A separate Spanner database is explicitly
104+ // created and passed to the Dataflow job as the shadow database (used for internal state
105+ // management). Passing a distinct shadow database triggers the
106+ // "Separate Shadow Table" distributed transaction algorithm for processing CDC events.
88107 spannerResourceManager = createSpannerDatabase (SPANNER_DDL_RESOURCE );
89108 shadowTableSpannerResourceManager =
90109 SpannerResourceManager .builder ("shadow-" + testName , PROJECT , REGION )
91110 .maybeUseStaticInstance ()
92111 .build ();
93112 shadowTableSpannerResourceManager .ensureUsableAndCreateResources ();
94- // create Source Resources
113+
114+ // 2. Create Source MySQL Database
115+ // Set up the upstream MySQL database and create the Users table.
95116 sourceDBResourceManager = CloudMySQLResourceManager .builder (testName ).build ();
96117 sourceDBResourceManager .createTable (
97118 USERS_TABLE , new JDBCResourceManager .JDBCSchema (USERS_TABLE_MYSQL_DDL , "id" ));
98119 sourceConnectionProfile =
99120 createMySQLSourceConnectionProfile (sourceDBResourceManager , List .of (USERS_TABLE ));
100121
101- // create and upload GCS Resources
122+ // 3. Create and upload GCS Resources
123+ // Set up the GCS bucket where Datastream will write CDC files.
102124 gcsResourceManager =
103125 GcsResourceManager .builder (artifactBucketName , getClass ().getSimpleName (), credentials )
104126 .build ();
105127
106- // create pubsub manager
128+ // 4. Create Pub/Sub Resources
129+ // Set up Pub/Sub topics and subscriptions to notify Dataflow of new files in GCS.
130+ // Separate topics are created for the main data stream and Dead Letter Queue (DLQ).
107131 pubsubResourceManager = setUpPubSubResourceManager ();
108132
109133 String testRootDir = getClass ().getSimpleName ();
110- // create subscriptions
111134 String gcsPrefix =
112135 String .join ("/" , new String [] {testRootDir , gcsResourceManager .runId (), testName , "cdc" });
113136 SubscriptionName subscription =
@@ -124,7 +147,8 @@ public void setUp() throws IOException, InterruptedException {
124147 gcsResourceManager );
125148 String artifactBucket = TestProperties .artifactBucket ();
126149
127- // launch datastream
150+ // 5. Launch Datastream Stream
151+ // Create and start the Datastream stream to replicate data from MySQL to the GCS bucket.
128152 DatastreamResourceManager .Builder datastreamResourceManagerBuilder =
129153 DatastreamResourceManager .builder (testName , PROJECT , REGION )
130154 .setCredentialsProvider (credentialsProvider );
@@ -140,10 +164,34 @@ public void setUp() throws IOException, InterruptedException {
140164 int numRows = 100 ;
141165 int burstIterations = 10000 ;
142166
143- // generate Load
167+ // 6. Generate CDC Load
168+ // Initialize the source with 100 base rows, then simulate 10,000 random burst mutations
169+ // (inserts, updates, deletes) with a probability of 0.5 to generate CDC stream traffic.
144170 cdcLoadGenerator = new FuzzyCDCLoadGenerator ();
145171 cdcLoadGenerator .generateLoad (numRows , burstIterations , 0.5 , sourceDBResourceManager );
146172
173+ // 7. Configure Dataflow Failure Injection
174+ // This sets the TransactionTimeoutInjectionPolicy, which simulates 260-second long stalls
175+ // in Spanner transactions over a 30-minute window, causing standard Spanner RPCs to timeout and
176+ // abort.
177+ //
178+ // NOTE: This test is estimated to take around 45-50 minutes to complete (30-minute injection
179+ // window + setup + post-recovery processing buffer).
180+ //
181+ // Since the Datastream stream is started first and then the CDC load is generated before the
182+ // Dataflow job is launched, the pipeline will immediately see all the generated CDC events as a
183+ // massive burst upon startup.
184+ //
185+ // During the 30-minute window, the simulated 260-second delay exceeds standard Spanner Java
186+ // Client timeout settings (typically 60 seconds for commit RPCs). This results in
187+ // DEADLINE_EXCEEDED errors. This policy specifically tests the "Separate Shadow Table"
188+ // transaction algorithm, where the shadow database (tracking stream offsets) and the
189+ // main database are distinct. The algorithm utilizes nested transactions to ensure
190+ // consistency.
191+ //
192+ // After the 30-minute window expires, failures are no longer injected. The pipeline then
193+ // rapidly
194+ // processes the remaining backlog and successfully retries any previously failed events.
147195 FlexTemplateDataflowJobResourceManager .Builder flexTemplateBuilder =
148196 FlexTemplateDataflowJobResourceManager .builder (testName )
149197 .withAdditionalMavenProfile ("failureInjectionTest" )
@@ -153,7 +201,7 @@ public void setUp() throws IOException, InterruptedException {
153201 .addEnvironmentVariable ("numWorkers" , NUM_WORKERS )
154202 .addEnvironmentVariable ("maxWorkers" , MAX_WORKERS );
155203
156- // launch dataflow template
204+ // 8. Launch the Dataflow CDC template
157205 jobInfo =
158206 launchFwdDataflowJob (
159207 spannerResourceManager ,
@@ -185,11 +233,14 @@ public void cleanUp() throws IOException {
185233 public void liveMigrationCrossDbTxnCdcTest ()
186234 throws IOException , InterruptedException , SQLException , ExecutionException {
187235
188- // Wait for Forward migration job to be in running state
236+ // 1. Wait for the Forward migration job to be in a running state
189237 assertThatPipeline (jobInfo ).isRunning ();
190238
191239 long expectedRows = sourceDBResourceManager .getRowCount (USERS_TABLE );
192- // Wait for exact number of rows as source to appear in Spanner
240+
241+ // 2. Wait for the exact number of rows from the source to successfully appear in Spanner.
242+ // This checks that despite the induced transaction timeouts, the pipeline eventually
243+ // retries and converges to the correct row count.
193244 ConditionCheck spannerRowCountConditionCheck =
194245 SpannerRowsCheck .builder (spannerResourceManager , USERS_TABLE )
195246 .setMinRows ((int ) expectedRows )
@@ -202,11 +253,13 @@ public void liveMigrationCrossDbTxnCdcTest()
202253 createConfig (jobInfo , Duration .ofHours (1 )), spannerRowCountConditionCheck );
203254 assertThatResult (result ).meetsConditions ();
204255
205- // Usually the dataflow finishes processing the events within 10 minutes. Giving 10 more minutes
206- // buffer for the dataflow job to process the events before asserting the results.
256+ // 3. Usually the dataflow finishes processing the events within 10 minutes. Giving 10 more
257+ // minutes buffer for the dataflow job to process the events and ensure no late-arriving
258+ // events alter the state.
207259 Thread .sleep (600000 );
208260
209- // Read data from Spanner and assert that it exactly matches with SourceDb
261+ // 4. Read data from both Spanner and MySQL and assert that the exact row states match,
262+ // validating the content data integrity and consistency between SourceDb and Spanner.
210263 cdcLoadGenerator .assertRows (spannerResourceManager , sourceDBResourceManager );
211264 }
212265}
0 commit comments