Skip to content

Commit b2215d1

Browse files
authored
ci: create workflow for Spanner failure injection tests (#3763)
* ci: create workflow for Spanner failure injection tests * Correcting error * Formatting changes * Removing duplicate cloudProxy parameter * Corrected step name
1 parent c21fb58 commit b2215d1

7 files changed

Lines changed: 319 additions & 30 deletions

File tree

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
name: Spanner Failure Injection tests
16+
17+
on:
18+
workflow_dispatch:
19+
inputs:
20+
test_name:
21+
description: 'Fully qualified test name or regex to run (e.g. DataStreamToSpannerCDCFT)'
22+
type: string
23+
required: true
24+
25+
permissions: write-all
26+
27+
jobs:
28+
spanner_failure_injection_tests:
29+
name: Spanner Failure Injection Tests
30+
timeout-minutes: 180
31+
runs-on: [ self-hosted, spanner-it ]
32+
steps:
33+
- name: Checkout Code
34+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
35+
with:
36+
fetch-depth: 0
37+
- name: Setup Environment
38+
id: setup-env
39+
uses: ./.github/actions/setup-env
40+
- name: Run Spanner failure injection tests
41+
run: |
42+
./cicd/run-failure-injection-tests \
43+
--modules-to-build="SPANNER" \
44+
--it-region="us-central1" \
45+
--it-project="span-cloud-teleport-testing" \
46+
--it-artifact-bucket="span-cloud-teleport-testing-it-gitactions" \
47+
--it-private-connectivity="datastream-connect-2" \
48+
--it-cloud-proxy-host="10.128.0.16" \
49+
--test="${{ github.event.inputs.test_name }}"
50+
- name: Upload Test Report
51+
uses: actions/upload-artifact@v7
52+
if: always() # always run even if the previous step fails
53+
with:
54+
name: surefire-test-results
55+
path: |
56+
**/surefire-reports/TEST-*.xml
57+
**/surefire-reports/*.html
58+
**/surefire-reports/html/**
59+
retention-days: 20
60+
- name: Failure Injection Test report on GitHub
61+
uses: dorny/test-reporter@v3
62+
if: always()
63+
with:
64+
name: Failure Injection Test report on GitHub
65+
path: '**/surefire-reports/TEST-*.xml'
66+
reporter: java-junit
67+
only-summary: 'true'
68+
token: ${{ secrets.GITHUB_TOKEN }}
69+
fail-on-error: 'false'
70+
list-suites: 'failed'
71+
list-tests: 'failed'
72+
- name: Cleanup Java Environment
73+
uses: ./.github/actions/cleanup-java-env
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
* Copyright (C) 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
17+
package main
18+
19+
import (
20+
"flag"
21+
"log"
22+
23+
"github.com/GoogleCloudPlatform/DataflowTemplates/cicd/internal/flags"
24+
"github.com/GoogleCloudPlatform/DataflowTemplates/cicd/internal/workflows"
25+
)
26+
27+
func main() {
28+
flags.RegisterCommonFlags()
29+
flags.RegisterItFlags()
30+
flag.Parse()
31+
32+
// Run mvn install before running integration tests
33+
mvnFlags := workflows.NewMavenFlags()
34+
err := workflows.MvnCleanInstall().Run(
35+
mvnFlags.IncludeDependencies(),
36+
mvnFlags.SkipDependencyAnalysis(),
37+
mvnFlags.SkipCheckstyle(),
38+
mvnFlags.SkipJib(),
39+
mvnFlags.SkipTests(),
40+
mvnFlags.SkipJacoco(),
41+
mvnFlags.SkipShade(),
42+
mvnFlags.ThreadCount(1),
43+
mvnFlags.InternalMaven())
44+
if err != nil {
45+
log.Fatalf("%v\n", err)
46+
}
47+
48+
// Run integration tests
49+
mvnFlags = workflows.NewMavenFlags()
50+
err = workflows.MvnVerify().Run(
51+
mvnFlags.DoNotIncludeDependencies(),
52+
mvnFlags.SkipDependencyAnalysis(),
53+
mvnFlags.SkipCheckstyle(),
54+
mvnFlags.SkipJib(),
55+
mvnFlags.SkipShade(),
56+
mvnFlags.RunFailureInjectionTests(),
57+
mvnFlags.ThreadCount(flags.ThreadCount()),
58+
mvnFlags.IntegrationTestParallelism(flags.IntegrationTestParallelism()),
59+
mvnFlags.StaticBigtableInstance("teleport"),
60+
mvnFlags.StaticSpannerInstance("teleport"),
61+
mvnFlags.InternalMaven(),
62+
flags.Region(),
63+
flags.Project(),
64+
flags.ArtifactBucket(),
65+
flags.StageBucket(),
66+
flags.HostIp(),
67+
flags.PrivateConnectivity(),
68+
flags.SpannerHost(),
69+
flags.FailureMode(),
70+
flags.RetryFailures(),
71+
flags.StaticOracleHost(),
72+
flags.StaticOracleSysPassword(),
73+
flags.CloudProxyHost(),
74+
flags.CloudProxyMySqlPort(),
75+
flags.CloudProxyPostgresPort(),
76+
flags.CloudProxyPassword(),
77+
flags.UnifiedWorkerHarnessContainerImage(),
78+
flags.CloudProxyPassword(),
79+
mvnFlags.SpecificTest(flags.TestToRun()),
80+
mvnFlags.FailIfNoTests(flags.TestToRun() != ""))
81+
if err != nil {
82+
log.Fatalf("%v\n", err)
83+
}
84+
log.Println("Build Successful!")
85+
}

cicd/cmd/run-spanner-staging-it-tests/main.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,7 @@ func main() {
7676
flags.CloudProxyMySqlPort(),
7777
flags.CloudProxyPostgresPort(),
7878
flags.CloudProxyPassword(),
79-
flags.UnifiedWorkerHarnessContainerImage(),
80-
flags.CloudProxyPassword())
79+
flags.UnifiedWorkerHarnessContainerImage())
8180
if err != nil {
8281
log.Fatalf("%v\n", err)
8382
}

cicd/internal/workflows/maven-workflows.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ type MavenFlags interface {
5555
RunSpannerStagingIntegrationTests() string
5656
RunLoadTests() string
5757
RunLoadTestObserver() string
58+
RunFailureInjectionTests() string
5859
ThreadCount(int) string
5960
IntegrationTestParallelism(int) string
6061
StaticBigtableInstance(string) string
@@ -138,6 +139,10 @@ func (*mvnFlags) RunLoadTestObserver() string {
138139
return "-PtemplatesLoadTestObserve"
139140
}
140141

142+
func (*mvnFlags) RunFailureInjectionTests() string {
143+
return "-PfailureInjectionTest"
144+
}
145+
141146
// The number of modules Maven is going to build in parallel in a multi-module project.
142147
func (*mvnFlags) ThreadCount(count int) string {
143148
return "-T" + strconv.Itoa(count)

pom.xml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -904,6 +904,34 @@
904904
</plugins>
905905
</build>
906906
</profile>
907+
<profile>
908+
<id>failureInjectionTest</id>
909+
<activation>
910+
<activeByDefault>false</activeByDefault>
911+
</activation>
912+
<properties>
913+
<!-- Skip coverage checks, unit tests are skipped -->
914+
<jacoco.skip>true</jacoco.skip>
915+
<!-- Some modules may yield no tests -->
916+
<failIfNoTests>false</failIfNoTests>
917+
</properties>
918+
<build>
919+
<plugins>
920+
<plugin>
921+
<groupId>org.apache.maven.plugins</groupId>
922+
<artifactId>maven-surefire-plugin</artifactId>
923+
<version>${surefire.version}</version>
924+
<configuration combine.self="override">
925+
<includes>
926+
<include>**/*FT.java</include>
927+
</includes>
928+
<excludedGroups></excludedGroups>
929+
<trimStackTrace>false</trimStackTrace>
930+
</configuration>
931+
</plugin>
932+
</plugins>
933+
</build>
934+
</profile>
907935
<profile>
908936
<id>pluginOutputDir</id>
909937
<activation>

v2/datastream-to-spanner/src/test/java/com/google/cloud/teleport/v2/templates/failureinjectiontesting/DataStreamToSpannerCDCFT.java

Lines changed: 68 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,23 @@
5454
import 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

Comments
 (0)