1313 * License for the specific language governing permissions and limitations under
1414 * the License.
1515 */
16- package com .google .cloud .teleport .v2 .templates . failureinjectiontesting ;
16+ package com .google .cloud .teleport .v2 .templates ;
1717
18+ import static com .google .cloud .teleport .v2 .spanner .testutils .failureinjectiontesting .MySQLSrcDataProvider .AUTHORS_TABLE ;
19+ import static com .google .cloud .teleport .v2 .spanner .testutils .failureinjectiontesting .MySQLSrcDataProvider .BOOKS_TABLE ;
1820import static com .google .cloud .teleport .v2 .templates .MySQLDataTypesIT .repeatString ;
1921import static org .apache .beam .it .truthmatchers .PipelineAsserts .assertThatPipeline ;
2022import static org .apache .beam .it .truthmatchers .PipelineAsserts .assertThatResult ;
2325import com .google .cloud .teleport .metadata .SkipDirectRunnerTest ;
2426import com .google .cloud .teleport .metadata .TemplateIntegrationTest ;
2527import com .google .cloud .teleport .v2 .spanner .migrations .transformation .CustomTransformation ;
26- import com .google .cloud .teleport .v2 .templates .SourceDbToSpanner ;
28+ import com .google .cloud .teleport .v2 .spanner .testutils .failureinjectiontesting .MySQLSrcDataProvider ;
29+ import com .google .cloud .teleport .v2 .templates .failureinjectiontesting .SourceDbToSpannerFTBase ;
2730import java .io .IOException ;
2831import java .time .Duration ;
2932import java .util .ArrayList ;
5154import org .slf4j .LoggerFactory ;
5255
5356/**
54- * An integration test for {@link SourceDbToSpanner} Flex template which tests all data types
55- * migration with custom transformations, bulk failure injection, and live retry.
57+ * A failure injection test for Bulk + retry DLQ Live migration i.e., SourceDbToSpanner and
58+ * DataStreamToSpanner templates. The bulk migration template does not retry transient failures.
59+ * Live migration template is used to retry the failures which happened during the Bulk migration.
60+ * This test injects Spanner errors to simulate transient errors during Bulk migration and checks
61+ * the template behaviour. This test tests all data types migration with custom transformations,
62+ * bulk failure injection, and live retry. The Bulk failures are injected both at Custom
63+ * transformation phase and Spanner write phase. The test case also includes an interleaved table.
5664 */
5765@ Category ({TemplateIntegrationTest .class , SkipDirectRunnerTest .class })
5866@ TemplateIntegrationTest (SourceDbToSpanner .class )
5967@ RunWith (JUnit4 .class )
60- public class MySQLAllDataTypesCustomTransformationsBulkAndLiveFT extends SourceDbToSpannerFTBase {
68+ public class MySQLAllDataTypesBulkAndLiveIT extends SourceDbToSpannerFTBase {
6169
62- private static final Logger LOG =
63- LoggerFactory .getLogger (MySQLAllDataTypesCustomTransformationsBulkAndLiveFT .class );
70+ private static final Logger LOG = LoggerFactory .getLogger (MySQLAllDataTypesBulkAndLiveIT .class );
6471
6572 private static final String MYSQL_DDL_RESOURCE =
66- "MySQLAllDataTypesCustomTransformationsBulkAndLiveFT /mysql-schema.sql" ;
73+ "MySQLAllDataTypesBulkAndLiveIT /mysql-schema.sql" ;
6774 private static final String SPANNER_DDL_RESOURCE =
68- "MySQLAllDataTypesCustomTransformationsBulkAndLiveFT/spanner-schema.sql" ;
69- private static final String TABLE_NAME = "AllDataTypes" ;
75+ "MySQLAllDataTypesBulkAndLiveIT/spanner-schema.sql" ;
76+
77+ private static final String TABLE_CT = "AllDataTypes_CT" ; // Custom transformation failures
78+ private static final String TABLE_SWF = "AllDataTypes_SWF" ; // Spanner write failures
7079
7180 private static PipelineLauncher .LaunchInfo bulkJobInfo ;
72- private static PipelineLauncher .LaunchInfo retryLiveJobInfo ;
81+ private static PipelineLauncher .LaunchInfo liveJobInfo ;
7382
7483 public static CloudMySQLResourceManager mySQLResourceManager ;
7584 public static SpannerResourceManager spannerResourceManager ;
7685 private static GcsResourceManager gcsResourceManager ;
77- private static String bulkErrorFolderFullPath ;
7886
7987 @ Before
8088 public void setUp () throws Exception {
@@ -93,100 +101,142 @@ public void setUp() throws Exception {
93101 // create MySQL Resources
94102 mySQLResourceManager = CloudMySQLResourceManager .builder (testName ).build ();
95103
104+ // Load DDL for AllDataTypes tables and Authors/Books
96105 loadSQLFileResource (mySQLResourceManager , MYSQL_DDL_RESOURCE );
97106
98- bulkErrorFolderFullPath = getGcsPath ("output" , gcsResourceManager );
107+ // Insert data for Authors and Books
108+ MySQLSrcDataProvider .writeRowsInSourceDB (1 , 200 , mySQLResourceManager );
109+ }
110+
111+ @ After
112+ public void cleanUp () {
113+ ResourceManagerUtils .cleanResources (
114+ spannerResourceManager , mySQLResourceManager , gcsResourceManager );
115+ }
116+
117+ @ Test
118+ public void testAllScenarios () throws IOException {
119+ // --------------------------------------------------------------------------------------------
120+ // Phase 1: Bulk Migration
121+ // --------------------------------------------------------------------------------------------
122+
123+ // Launch Bulk Job for All Scenarios
124+ // We use CustomTransformationAllTypesWithException which is selective:
125+ // - AllDataTypes_CT table: Fails (Simulated) - Custom transformation class throws exception
126+ // - AllDataTypes_SWF table: Passes transformation, Fails at Spanner Write (Schema mismatch) -
127+ // length of bit_col is too small
128+ // - Authors/Books table: Passes transformation, Fails at Spanner Write (Schema mismatch for
129+ // Authors) - length of name column in authors table is too small. It is such that 9 rows
130+ // `author_1` to `author_9` gets inserted and the rest 191 rows fail. Similarly, 9 books rows
131+ // corresponding to authors would get inserted and rest 191 would fail.
99132
100- // Define Custom Transformation with Exception (Bad)
101133 CustomTransformation customTransformationBad =
102134 CustomTransformation .builder (
103135 "customTransformation.jar" , "com.custom.CustomTransformationAllTypesWithException" )
104136 .build ();
105137
106- // launch bulk migration
138+ Map <String , String > params = new HashMap <>();
139+ // No 'tables' parameter needed, migrate everything
140+ params .put ("outputDirectory" , getGcsPath ("output" , gcsResourceManager ));
141+
107142 bulkJobInfo =
108143 launchBulkDataflowJob (
109- getClass ().getSimpleName (),
110- null ,
144+ getClass ().getSimpleName () + "-Bulk" ,
111145 null ,
146+ params ,
112147 spannerResourceManager ,
113148 gcsResourceManager ,
114149 mySQLResourceManager ,
115150 customTransformationBad );
116- }
117-
118- @ After
119- public void cleanUp () {
120- ResourceManagerUtils .cleanResources (
121- spannerResourceManager , mySQLResourceManager , gcsResourceManager );
122- }
123151
124- @ Test
125- public void testAllDataTypesCustomTransformationsBulkAndLive () throws IOException {
126- // Wait for Bulk migration job to be in running state
152+ // Wait for bulk job
127153 assertThatPipeline (bulkJobInfo ).isRunning ();
128154
129155 PipelineOperator .Result result =
130156 pipelineOperator ().waitUntilDone (createConfig (bulkJobInfo , Duration .ofMinutes (30 )));
131157 assertThatResult (result ).isLaunchFinished ();
132158
133- // Verify DLQ has 3 events
134- ConditionCheck conditionCheck =
135- // Check that there is at least 3 errors in DLQ
159+ // Verify DLQ Events
160+ // Total events expected:
161+ // - CT: 4 events (3 rows + 1 null row)
162+ // - SWF: 3 events (3 rows, bit_col too small)
163+ // - Authors: 191 events (name column too small)
164+ // - Books: 191 events (parent Authors failed)
165+ // Total: 4 + 3 + 191 + 191 = 389
166+ assertTrue (
136167 DlqEventsCountCheck .builder (gcsResourceManager , "output/dlq/severe/" )
137- .setMinEvents (4 )
138- .build ();
139- assertTrue (conditionCheck .get ());
168+ .setMinEvents (389 )
169+ .build ()
170+ .get ());
171+
172+ // --------------------------------------------------------------------------------------------
173+ // Phase 2: Live Migration (Retry)
174+ // --------------------------------------------------------------------------------------------
140175
141- // Define Custom Transformation without Exception (Good)
176+ // Fix schemas before retry
177+ spannerResourceManager .executeDdlStatement (
178+ "ALTER TABLE `" + TABLE_SWF + "` ALTER COLUMN `bit_col` BYTES(MAX)" );
179+ spannerResourceManager .executeDdlStatement (
180+ "ALTER TABLE `Authors` ALTER COLUMN `name` STRING(200)" );
181+
182+ // Retry with Good Custom Transformation class
142183 CustomTransformation customTransformationGood =
143184 CustomTransformation .builder (
144185 "customTransformation.jar" , "com.custom.CustomTransformationAllTypes" )
145186 .build ();
146187
147- // launch forward migration template in retryDLQ mode
148- retryLiveJobInfo =
188+ liveJobInfo =
149189 launchFwdDataflowJobInRetryDlqMode (
150190 spannerResourceManager ,
151- bulkErrorFolderFullPath ,
152- bulkErrorFolderFullPath + " /dlq" ,
191+ getGcsPath ( "output" , gcsResourceManager ) ,
192+ getGcsPath ( "output /dlq", gcsResourceManager ) ,
153193 gcsResourceManager ,
154194 customTransformationGood );
155195
156- // Wait for Spanner to have all 3 rows
157- conditionCheck =
196+ // Wait and Verify All Tables
197+ ConditionCheck conditionCheck =
158198 ChainedConditionCheck .builder (
159199 List .of (
160- SpannerRowsCheck .builder (spannerResourceManager , TABLE_NAME )
200+ SpannerRowsCheck .builder (spannerResourceManager , TABLE_CT )
161201 .setMinRows (4 )
162202 .setMaxRows (4 )
203+ .build (),
204+ SpannerRowsCheck .builder (spannerResourceManager , TABLE_SWF )
205+ .setMinRows (3 )
206+ .setMaxRows (3 )
207+ .build (),
208+ SpannerRowsCheck .builder (spannerResourceManager , AUTHORS_TABLE )
209+ .setMinRows (200 )
210+ .setMaxRows (200 )
211+ .build (),
212+ SpannerRowsCheck .builder (spannerResourceManager , BOOKS_TABLE )
213+ .setMinRows (200 )
214+ .setMaxRows (200 )
163215 .build ()))
164216 .build ();
165217
166- result =
167- pipelineOperator ()
168- .waitForConditionAndCancel (
169- createConfig (retryLiveJobInfo , Duration .ofMinutes (15 )), conditionCheck );
170- assertThatResult ( result ) .meetsConditions ();
218+ assertThatResult (
219+ pipelineOperator ()
220+ .waitForConditionAndCancel (
221+ createConfig (liveJobInfo , Duration .ofMinutes (15 )), conditionCheck ))
222+ .meetsConditions ();
171223
172- // Verify Non null Data Content
224+ // Verify CT Data
173225 List <Map <String , Object >> expectedDataNonNull = getExpectedData ();
174-
175- List <com .google .cloud .spanner .Struct > allRecords =
176- spannerResourceManager .runQuery ("SELECT * FROM " + TABLE_NAME );
177-
178- SpannerAsserts .assertThatStructs (allRecords )
226+ List <com .google .cloud .spanner .Struct > allRecordsCT =
227+ spannerResourceManager .runQuery ("SELECT * FROM " + TABLE_CT );
228+ SpannerAsserts .assertThatStructs (allRecordsCT )
179229 .hasRecordsUnorderedCaseInsensitiveColumns (expectedDataNonNull );
230+ verifyNullRow (allRecordsCT );
180231
181- // Manual assertion for the null row
182- verifyNullRow (allRecords );
232+ // Verify SWF Data
233+ SpannerAsserts .assertThatStructs (spannerResourceManager .runQuery ("SELECT * FROM " + TABLE_SWF ))
234+ .hasRecordsUnorderedCaseInsensitiveColumns (expectedDataNonNull );
183235 }
184236
185237 private void verifyNullRow (List <com .google .cloud .spanner .Struct > structs ) {
186- // Iterate over all structs and verify the struct with id=4
187238 for (com .google .cloud .spanner .Struct struct : structs ) {
188239 if (struct .getLong ("id" ) == 4 ) {
189- // Verify all columns except id are null
190240 for (com .google .cloud .spanner .Type .StructField field : struct .getType ().getStructFields ()) {
191241 if (field .getName ().equalsIgnoreCase ("id" )) {
192242 continue ;
0 commit comments