This is an automated email from the ASF dual-hosted git repository.

ningjiang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/servicecomb-pack.git

commit 7aa7e2c4c6b7bc69642edb1de98957665231d071
Author: Daniel Qian <chanjars...@gmail.com>
AuthorDate: Thu Dec 19 15:50:26 2019 +0800

    SCB-1670 Add an IT for explicit TransactionContext transfer
---
 integration-tests/coverage-aggregate/pom.xml       |   5 +
 .../explicit-transaction-context-tests/pom.xml     | 298 ++++++++++++++++++++
 .../explicitcontext/CommandEnvelopeRepository.java |  24 ++
 .../ExplicitTransactionContextIT.java              | 313 +++++++++++++++++++++
 .../tests/explicitcontext/GreetingApplication.java |  51 ++++
 .../tests/explicitcontext/GreetingController.java  | 119 ++++++++
 .../tests/explicitcontext/GreetingService.java     |  89 ++++++
 .../explicitcontext/TransactionContextDto.java     |  36 +++
 .../explicitcontext/TxEventEnvelopeRepository.java |  31 ++
 .../src/test/resources/application.yaml            |  21 ++
 .../src/test/resources/log4j2.xml                  |  30 ++
 integration-tests/pom.xml                          |   1 +
 12 files changed, 1018 insertions(+)

diff --git a/integration-tests/coverage-aggregate/pom.xml 
b/integration-tests/coverage-aggregate/pom.xml
index 83bee66..3ea5011 100644
--- a/integration-tests/coverage-aggregate/pom.xml
+++ b/integration-tests/coverage-aggregate/pom.xml
@@ -78,6 +78,11 @@
       <artifactId>pack-tests</artifactId>
       <version>0.6.0-SNAPSHOT</version>
     </dependency>
+    <dependency>
+      <groupId>org.apache.servicecomb.pack.tests</groupId>
+      <artifactId>explicit-transaction-context-tests</artifactId>
+      <version>0.6.0-SNAPSHOT</version>
+    </dependency>
   </dependencies>
 
   <profiles>
diff --git a/integration-tests/explicit-transaction-context-tests/pom.xml 
b/integration-tests/explicit-transaction-context-tests/pom.xml
new file mode 100644
index 0000000..bb20f88
--- /dev/null
+++ b/integration-tests/explicit-transaction-context-tests/pom.xml
@@ -0,0 +1,298 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF licenses this file to You under the Apache License, Version 2.0
+  ~ (the "License"); you may not use this file except in compliance with
+  ~ the License.  You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0";
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
+  <parent>
+    <artifactId>integration-tests</artifactId>
+    <groupId>org.apache.servicecomb.pack.tests</groupId>
+    <version>0.6.0-SNAPSHOT</version>
+  </parent>
+  <modelVersion>4.0.0</modelVersion>
+
+  <artifactId>explicit-transaction-context-tests</artifactId>
+  <name>Pack::Integration Tests::Explict Transaction Context</name>
+
+
+  <dependencyManagement>
+    <dependencies>
+      <dependency>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-test</artifactId>
+        <version>${spring.boot.version}</version>
+        <exclusions>
+          <exclusion>
+            <groupId>org.json</groupId>
+            <artifactId>json</artifactId>
+          </exclusion>
+        </exclusions>
+        <scope>test</scope>
+      </dependency>
+      <dependency>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-dependencies</artifactId>
+        <version>${spring.boot.version}</version>
+        <type>pom</type>
+        <scope>import</scope>
+      </dependency>
+    </dependencies>
+  </dependencyManagement>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.servicecomb.pack</groupId>
+      <artifactId>pack-common</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.servicecomb.pack</groupId>
+      <artifactId>alpha-core</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.servicecomb.pack</groupId>
+      <artifactId>omega-spring-starter</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.servicecomb.pack</groupId>
+      <artifactId>omega-transport-resttemplate</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>commons-logging</groupId>
+      <artifactId>commons-logging</artifactId>
+      <version>1.2</version>
+    </dependency>
+    <dependency>
+      <groupId>io.rest-assured</groupId>
+      <artifactId>rest-assured</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.springframework.boot</groupId>
+      <artifactId>spring-boot-starter</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.springframework.boot</groupId>
+      <artifactId>spring-boot-starter-web</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>javax.persistence</groupId>
+      <artifactId>javax.persistence-api</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.postgresql</groupId>
+      <artifactId>postgresql</artifactId>
+      <scope>runtime</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.servicecomb.pack</groupId>
+      <artifactId>persistence-jpa</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.springframework.boot</groupId>
+      <artifactId>spring-boot-starter-test</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.codehaus.groovy</groupId>
+      <artifactId>groovy-all</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.awaitility</groupId>
+      <artifactId>awaitility</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <profiles>
+    <profile>
+      <id>docker</id>
+      <activation>
+        <file>
+          <exists>/var/run/docker.sock</exists>
+        </file>
+      </activation>
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>io.fabric8</groupId>
+            <artifactId>docker-maven-plugin</artifactId>
+            <configuration>
+              <images>
+                <image>
+                  <name>postgres</name>
+                  <alias>postgres</alias>
+                  <run>
+                    <env>
+                      <POSTGRES_DB>saga</POSTGRES_DB>
+                      <POSTGRES_USER>saga</POSTGRES_USER>
+                      <POSTGRES_PASSWORD>password</POSTGRES_PASSWORD>
+                    </env>
+                    <wait>
+                      <log>database system is ready to accept connections</log>
+                      <tcp>
+                        <ports>
+                          <port>5432</port>
+                        </ports>
+                      </tcp>
+                      <time>60000</time>
+                    </wait>
+                    <ports>
+                      <port>postgres.port:5432</port>
+                    </ports>
+                  </run>
+                </image>
+                <image>
+                  <name>alpha-server:${project.version}</name>
+                  <alias>alpha</alias>
+                  <run>
+                    <env>
+                      <JAVA_OPTS>
+                        -Dspring.profiles.active=prd 
-Dspring.datasource.initialization-mode=always
+                      </JAVA_OPTS>
+                    </env>
+                    <links>
+                      <link>postgres:postgresql.servicecomb.io</link>
+                    </links>
+                    <wait>
+                      <log>Started [a-zA-Z]+ in [0-9.]+ seconds</log>
+                      <tcp>
+                        <ports>
+                          <port>8080</port>
+                        </ports>
+                      </tcp>
+                      <time>120000</time>
+                    </wait>
+                    <ports>
+                      <port>alpha.port:8080</port>
+                    </ports>
+                    <dependsOn>
+                      <dependsOn>postgres</dependsOn>
+                    </dependsOn>
+                  </run>
+                </image>
+              </images>
+            </configuration>
+            <executions>
+              <execution>
+                <id>start</id>
+                <phase>pre-integration-test</phase>
+                <goals>
+                  <goal>start</goal>
+                </goals>
+              </execution>
+              <execution>
+                <id>stop</id>
+                <phase>post-integration-test</phase>
+                <goals>
+                  <goal>stop</goal>
+                </goals>
+              </execution>
+            </executions>
+          </plugin>
+          <plugin>
+            <groupId>org.codehaus.gmaven</groupId>
+            <artifactId>gmaven-plugin</artifactId>
+            <executions>
+              <execution>
+                <id>add-default-properties</id>
+                <phase>initialize</phase>
+                <goals>
+                  <goal>execute</goal>
+                </goals>
+                <configuration>
+                  <source>
+                    project.properties.setProperty('docker.hostname', 
'localhost')
+                    log.info("Docker hostname is " + 
project.properties['docker.hostname'])
+                  </source>
+                </configuration>
+              </execution>
+            </executions>
+          </plugin>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-failsafe-plugin</artifactId>
+            <version>${maven.failsafe.version}</version>
+            <configuration>
+              <systemPropertyVariables>
+                <alpha.cluster.address>
+                  ${docker.hostname}:${alpha.port}
+                </alpha.cluster.address>
+                <spring.datasource.url>
+                  
jdbc:postgresql://${docker.hostname}:${postgres.port}/saga?useSSL=false
+                </spring.datasource.url>
+              </systemPropertyVariables>
+              <argLine>${jacoco.failsafe.argLine}</argLine>
+            </configuration>
+            <executions>
+              <execution>
+                <goals>
+                  <goal>integration-test</goal>
+                  <goal>verify</goal>
+                </goals>
+              </execution>
+            </executions>
+          </plugin>
+          <plugin>
+            <groupId>com.ethlo.persistence.tools</groupId>
+            <artifactId>eclipselink-maven-plugin</artifactId>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+    <profile>
+      <id>docker-machine</id>
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>org.codehaus.gmaven</groupId>
+            <artifactId>gmaven-plugin</artifactId>
+            <executions>
+              <execution>
+                <id>add-dynamic-properties</id>
+                <phase>prepare-package</phase>
+                <goals>
+                  <goal>execute</goal>
+                </goals>
+                <configuration>
+                  <source>
+                    def process = "docker-machine ip default".execute()
+                    process.waitFor()
+                    project.properties.setProperty('docker.hostname', 
process.in.text.trim())
+
+                    log.info("Docker hostname is " + 
project.properties['docker.hostname'])
+                  </source>
+                </configuration>
+              </execution>
+            </executions>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+  </profiles>
+
+</project>
diff --git 
a/integration-tests/explicit-transaction-context-tests/src/test/java/org/apache/servicecomb/pack/integration/tests/explicitcontext/CommandEnvelopeRepository.java
 
b/integration-tests/explicit-transaction-context-tests/src/test/java/org/apache/servicecomb/pack/integration/tests/explicitcontext/CommandEnvelopeRepository.java
new file mode 100644
index 0000000..f80b922
--- /dev/null
+++ 
b/integration-tests/explicit-transaction-context-tests/src/test/java/org/apache/servicecomb/pack/integration/tests/explicitcontext/CommandEnvelopeRepository.java
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.pack.integration.tests.explicitcontext;
+
+import org.apache.servicecomb.pack.alpha.core.Command;
+import org.springframework.data.repository.CrudRepository;
+
+public interface CommandEnvelopeRepository extends CrudRepository<Command, 
Long> {
+}
diff --git 
a/integration-tests/explicit-transaction-context-tests/src/test/java/org/apache/servicecomb/pack/integration/tests/explicitcontext/ExplicitTransactionContextIT.java
 
b/integration-tests/explicit-transaction-context-tests/src/test/java/org/apache/servicecomb/pack/integration/tests/explicitcontext/ExplicitTransactionContextIT.java
new file mode 100644
index 0000000..4b752b9
--- /dev/null
+++ 
b/integration-tests/explicit-transaction-context-tests/src/test/java/org/apache/servicecomb/pack/integration/tests/explicitcontext/ExplicitTransactionContextIT.java
@@ -0,0 +1,313 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.pack.integration.tests.explicitcontext;
+
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.awaitility.Awaitility.await;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.Matchers.greaterThanOrEqualTo;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;
+import static org.springframework.http.HttpStatus.OK;
+
+import java.util.List;
+import java.util.Queue;
+
+import org.apache.servicecomb.pack.alpha.core.TxEvent;
+import org.hamcrest.Matchers;
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
+import org.springframework.boot.test.web.client.TestRestTemplate;
+import org.springframework.http.ResponseEntity;
+import org.springframework.test.context.junit4.SpringRunner;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest(classes = GreetingApplication.class, webEnvironment = 
WebEnvironment.DEFINED_PORT,
+    properties = {"server.port=8080", 
"spring.application.name=greeting-service"})
+public class ExplicitTransactionContextIT {
+  private static final String serviceName = "greeting-service";
+
+  @Autowired
+  private TestRestTemplate restTemplate;
+
+  @Autowired
+  private TxEventEnvelopeRepository eventRepo;
+
+  @Autowired
+  private CommandEnvelopeRepository commandRepo;
+
+  @Autowired
+  private Queue<String> compensatedMessages;
+
+  @Autowired
+  private GreetingService greetingService;
+
+  @After
+  public void tearDown() throws Exception {
+    eventRepo.deleteAll();
+    commandRepo.deleteAll();
+    compensatedMessages.clear();
+    greetingService.resetCount();
+  }
+
+  @Test(timeout = 5000)
+  public void updatesTxStateToAlpha() throws Exception {
+    ResponseEntity<String> entity = 
restTemplate.getForEntity("/greet?name={name}",
+        String.class,
+        "mike");
+
+    assertThat(entity.getStatusCode(), is(OK));
+    assertThat(entity.getBody(), is("Greetings, mike; Bonjour, mike"));
+
+    List<String> distinctGlobalTxIds = eventRepo.findDistinctGlobalTxId();
+    assertThat(distinctGlobalTxIds.size(), greaterThanOrEqualTo(1));
+
+    String globalTxId = distinctGlobalTxIds.get(0);
+    List<TxEvent> events = 
eventRepo.findByGlobalTxIdOrderByCreationTime(globalTxId);
+
+    assertThat(events.size(), is(6));
+
+    TxEvent sagaStartedEvent = events.get(0);
+    assertThat(sagaStartedEvent.type(), is("SagaStartedEvent"));
+    assertThat(sagaStartedEvent.localTxId(), is(globalTxId));
+    assertThat(sagaStartedEvent.parentTxId(), is(nullValue()));
+    assertThat(sagaStartedEvent.serviceName(), is(serviceName));
+    assertThat(sagaStartedEvent.instanceId(), is(notNullValue()));
+
+    TxEvent txStartedEvent1 = events.get(1);
+    assertThat(txStartedEvent1.type(), is("TxStartedEvent"));
+    assertThat(txStartedEvent1.localTxId(), is(notNullValue()));
+    assertThat(txStartedEvent1.parentTxId(), is(globalTxId));
+    assertThat(txStartedEvent1.serviceName(), is(serviceName));
+    assertThat(txStartedEvent1.instanceId(), 
is(sagaStartedEvent.instanceId()));
+
+    TxEvent txEndedEvent1 = events.get(2);
+    assertThat(txEndedEvent1.type(), is("TxEndedEvent"));
+    assertThat(txEndedEvent1.localTxId(), is(txStartedEvent1.localTxId()));
+    assertThat(txEndedEvent1.parentTxId(), is(globalTxId));
+    assertThat(txEndedEvent1.serviceName(), is(serviceName));
+    assertThat(txEndedEvent1.instanceId(), is(txStartedEvent1.instanceId()));
+
+    TxEvent txStartedEvent2 = events.get(3);
+    assertThat(txStartedEvent2.type(), is("TxStartedEvent"));
+    assertThat(txStartedEvent2.localTxId(), is(notNullValue()));
+    assertThat(txStartedEvent2.parentTxId(), is(globalTxId));
+    assertThat(txStartedEvent2.serviceName(), is(serviceName));
+    assertThat(txStartedEvent2.instanceId(), is(notNullValue()));
+
+    TxEvent txEndedEvent2 = events.get(4);
+    assertThat(txEndedEvent2.type(), is("TxEndedEvent"));
+    assertThat(txEndedEvent2.localTxId(), is(txStartedEvent2.localTxId()));
+    assertThat(txEndedEvent2.parentTxId(), is(globalTxId));
+    assertThat(txEndedEvent2.serviceName(), is(serviceName));
+    assertThat(txEndedEvent2.instanceId(), is(txStartedEvent2.instanceId()));
+
+    TxEvent sagaEndedEvent = events.get(5);
+    assertThat(sagaEndedEvent.type(), is("SagaEndedEvent"));
+    assertThat(sagaEndedEvent.localTxId(), is(globalTxId));
+    assertThat(sagaEndedEvent.parentTxId(), is(nullValue()));
+    assertThat(sagaEndedEvent.serviceName(), is(serviceName));
+    assertThat(sagaEndedEvent.instanceId(), is(notNullValue()));
+
+    assertThat(compensatedMessages.isEmpty(), is(true));
+  }
+
+  @Test(timeout = 10000)
+  public void compensatesFailedGlobalTransaction() throws Exception {
+    ResponseEntity<String> entity = 
restTemplate.getForEntity("/greet?name={name}",
+        String.class,
+        GreetingController.TRESPASSER);
+
+    assertThat(entity.getStatusCode(), is(INTERNAL_SERVER_ERROR));
+
+    await().atMost(4, SECONDS).until(() -> eventRepo.count() == 8);
+
+    List<String> distinctGlobalTxIds = eventRepo.findDistinctGlobalTxId();
+    assertThat(distinctGlobalTxIds.size(), greaterThanOrEqualTo(1));
+
+    String globalTxId = distinctGlobalTxIds.get(0);
+    List<TxEvent> events = 
eventRepo.findByGlobalTxIdOrderByCreationTime(globalTxId);
+    assertThat(events.size(), is(8));
+
+    TxEvent sagaStartedEvent = events.get(0);
+    assertThat(sagaStartedEvent.type(), is("SagaStartedEvent"));
+
+    TxEvent txStartedEvent1 = events.get(1);
+    assertThat(txStartedEvent1.type(), is("TxStartedEvent"));
+    assertThat(events.get(2).type(), is("TxEndedEvent"));
+
+    TxEvent txStartedEvent2 = events.get(3);
+    assertThat(txStartedEvent2.type(), is("TxStartedEvent"));
+
+    TxEvent txAbortedEvent = events.get(4);
+    assertThat(txAbortedEvent.type(), is("TxAbortedEvent"));
+    assertThat(txAbortedEvent.localTxId(), is(txStartedEvent2.localTxId()));
+    assertThat(txAbortedEvent.parentTxId(), is(globalTxId));
+    assertThat(txAbortedEvent.serviceName(), is(serviceName));
+    assertThat(txAbortedEvent.instanceId(), is(txStartedEvent2.instanceId()));
+
+    // The TxAbortedEvent and TxCompensatedEvent could arrive in different 
order
+    TxEvent event = events.get(5);
+    checkedLastTwoEvents(globalTxId, txStartedEvent1, event);
+
+    event = events.get(6);
+    checkedLastTwoEvents(globalTxId, txStartedEvent1, event);
+
+    assertThat(compensatedMessages, Matchers.contains("Goodbye, " + 
GreetingController.TRESPASSER));
+  }
+
+  private void checkedLastTwoEvents(String globalTxId, TxEvent 
txStartedEvent1, TxEvent event) {
+    if ("TxAbortedEvent".equals(event.type())) {
+      // check the globalTx
+      checkGloableTransactionEvent(event, globalTxId);
+    } else {
+      checkCompensatedTransactionEvent(event, txStartedEvent1, globalTxId);
+    }
+  }
+
+  private void checkCompensatedTransactionEvent(TxEvent txCompensatedEvent, 
TxEvent txStartedEvent, String globalTxId) {
+    assertThat(txCompensatedEvent.localTxId(), is(txStartedEvent.localTxId()));
+    assertThat(txCompensatedEvent.parentTxId(), is(globalTxId));
+    assertThat(txCompensatedEvent.serviceName(), is(serviceName));
+    assertThat(txCompensatedEvent.instanceId(), 
is(txStartedEvent.instanceId()));
+  }
+
+  private void checkGloableTransactionEvent(TxEvent txAbortedEvent, String 
globalTxId) {
+    assertThat(txAbortedEvent.localTxId(), is(globalTxId));
+    assertThat(txAbortedEvent.globalTxId(), is(globalTxId));
+    assertThat(txAbortedEvent.parentTxId(), is(nullValue()));
+  }
+
+  @Test(timeout = 5000)
+  public void updatesEmbeddedTxStateToAlpha() throws Exception {
+    ResponseEntity<String> entity = 
restTemplate.getForEntity("/goodMorning?name={name}",
+        String.class,
+        "mike");
+
+    assertThat(entity.getStatusCode(), is(OK));
+    assertThat(entity.getBody(), is("Good morning, Bonjour, mike"));
+
+    List<String> distinctGlobalTxIds = eventRepo.findDistinctGlobalTxId();
+    assertThat(distinctGlobalTxIds.size(), greaterThanOrEqualTo(1));
+
+    String globalTxId = distinctGlobalTxIds.get(0);
+    List<TxEvent> events = 
eventRepo.findByGlobalTxIdOrderByCreationTime(globalTxId);
+
+    assertThat(events.size(), is(6));
+
+    TxEvent sagaStartedEvent = events.get(0);
+    assertThat(sagaStartedEvent.type(), is("SagaStartedEvent"));
+
+    TxEvent txStartedEvent1 = events.get(1);
+    assertThat(txStartedEvent1.type(), is("TxStartedEvent"));
+    assertThat(txStartedEvent1.localTxId(), is(notNullValue()));
+    assertThat(txStartedEvent1.parentTxId(), is(globalTxId));
+
+    TxEvent txStartedEvent2 = events.get(2);
+    assertThat(txStartedEvent2.type(), is("TxStartedEvent"));
+    assertThat(txStartedEvent2.localTxId(), is(notNullValue()));
+    assertThat(txStartedEvent2.parentTxId(), is(txStartedEvent1.localTxId()));
+
+    TxEvent txEndedEvent2 = events.get(3);
+    assertThat(txEndedEvent2.type(), is("TxEndedEvent"));
+    assertThat(txEndedEvent2.localTxId(), is(txStartedEvent2.localTxId()));
+    assertThat(txEndedEvent2.parentTxId(), is(txStartedEvent1.localTxId()));
+
+    TxEvent txEndedEvent1 = events.get(4);
+    assertThat(txEndedEvent1.type(), is("TxEndedEvent"));
+    assertThat(txEndedEvent1.localTxId(), is(txStartedEvent1.localTxId()));
+    assertThat(txEndedEvent1.parentTxId(), is(globalTxId));
+
+    TxEvent sagaEndedEvent = events.get(5);
+    assertThat(sagaEndedEvent.type(), is("SagaEndedEvent"));
+
+    assertThat(compensatedMessages.isEmpty(), is(true));
+  }
+
+  @Test(timeout = 15000)
+  public void retrySubTransactionSuccess() {
+    ResponseEntity<String> entity = 
restTemplate.getForEntity("/open?name={name}&retries={retries}",
+        String.class,
+        "eric",
+        2);
+
+    assertThat(entity.getStatusCode(), is(OK));
+    assertThat(entity.getBody(), is("Greetings, eric; Welcome to visit the 
zoo, eric"));
+
+    await().atMost(10, SECONDS).until(() -> eventRepo.count() == 8);
+
+    List<String> distinctGlobalTxIds = eventRepo.findDistinctGlobalTxId();
+    assertThat(distinctGlobalTxIds.size(), greaterThanOrEqualTo(1));
+
+    String globalTxId = distinctGlobalTxIds.get(0);
+    List<TxEvent> events = 
eventRepo.findByGlobalTxIdOrderByCreationTime(globalTxId);
+    assertThat(events.size(), is(8));
+
+    assertThat(events.get(0).type(), is("SagaStartedEvent"));
+    assertThat(events.get(1).type(), is("TxStartedEvent"));
+    assertThat(events.get(2).type(), is("TxEndedEvent"));
+    assertThat(events.get(3).type(), is("TxStartedEvent"));
+    assertThat(events.get(4).type(), is("TxAbortedEvent"));
+    assertThat(events.get(5).type(), is("TxStartedEvent"));
+    assertThat(events.get(6).type(), is("TxEndedEvent"));
+    assertThat(events.get(7).type(), is("SagaEndedEvent"));
+
+    assertThat(compensatedMessages.isEmpty(), is(true));
+  }
+
+  @Test(timeout = 15000)
+  public void compensateWhenRetryReachesMaximum() throws InterruptedException {
+    // retries 3 times and then compensate
+    ResponseEntity<String> entity = 
restTemplate.getForEntity("/open?name={name}&retries={retries}",
+        String.class,
+        GreetingController.TRESPASSER,
+        5);
+
+    assertThat(entity.getStatusCode(), is(INTERNAL_SERVER_ERROR));
+
+    await().atMost(10, SECONDS).until(() -> eventRepo.count() == 12);
+
+    List<String> distinctGlobalTxIds = eventRepo.findDistinctGlobalTxId();
+    assertThat(distinctGlobalTxIds.size(), greaterThanOrEqualTo(1));
+
+    String globalTxId = distinctGlobalTxIds.get(0);
+    List<TxEvent> events = 
eventRepo.findByGlobalTxIdOrderByCreationTime(globalTxId);
+    assertThat(events.size(), is(12));
+
+    assertThat(events.get(0).type(), is("SagaStartedEvent"));
+    assertThat(events.get(1).type(), is("TxStartedEvent"));
+    assertThat(events.get(2).type(), is("TxEndedEvent"));
+    assertThat(events.get(3).type(), is("TxStartedEvent"));
+    assertThat(events.get(4).type(), is("TxAbortedEvent"));
+    assertThat(events.get(5).type(), is("TxStartedEvent"));
+    assertThat(events.get(6).type(), is("TxAbortedEvent"));
+    assertThat(events.get(7).type(), is("TxStartedEvent"));
+    assertThat(events.get(8).type(), is("TxAbortedEvent"));
+    // This event is for the whole saga event
+    assertThat(events.get(9).type(), is("TxAbortedEvent"));
+    assertThat(events.get(10).type(), is("TxCompensatedEvent"));
+
+    assertThat(compensatedMessages, Matchers.contains("Goodbye, " + 
GreetingController.TRESPASSER));
+  }
+}
diff --git 
a/integration-tests/explicit-transaction-context-tests/src/test/java/org/apache/servicecomb/pack/integration/tests/explicitcontext/GreetingApplication.java
 
b/integration-tests/explicit-transaction-context-tests/src/test/java/org/apache/servicecomb/pack/integration/tests/explicitcontext/GreetingApplication.java
new file mode 100644
index 0000000..080fdfa
--- /dev/null
+++ 
b/integration-tests/explicit-transaction-context-tests/src/test/java/org/apache/servicecomb/pack/integration/tests/explicitcontext/GreetingApplication.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.pack.integration.tests.explicitcontext;
+
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+
+import org.apache.servicecomb.pack.omega.spring.EnableOmega;
+import 
org.apache.servicecomb.pack.omega.transport.resttemplate.RestTemplateConfig;
+import org.apache.servicecomb.pack.omega.transport.resttemplate.WebConfig;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.domain.EntityScan;
+import org.springframework.boot.web.client.RestTemplateBuilder;
+import org.springframework.context.annotation.Bean;
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
+import org.springframework.web.client.RestTemplate;
+
+@EnableOmega
+@SpringBootApplication(exclude = {WebConfig.class, RestTemplateConfig.class})
+@EntityScan(basePackages = "org.apache.servicecomb.pack.alpha")
+public class GreetingApplication {
+  public static void main(String[] args) {
+    SpringApplication.run(GreetingApplication.class, args);
+  }
+
+  @Bean
+  Queue<String> compensated() {
+    return new ConcurrentLinkedQueue<>();
+  }
+
+  @Bean
+  RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
+    return restTemplateBuilder.rootUri("http://localhost:8080";).build();
+  }
+}
diff --git 
a/integration-tests/explicit-transaction-context-tests/src/test/java/org/apache/servicecomb/pack/integration/tests/explicitcontext/GreetingController.java
 
b/integration-tests/explicit-transaction-context-tests/src/test/java/org/apache/servicecomb/pack/integration/tests/explicitcontext/GreetingController.java
new file mode 100644
index 0000000..739f8f0
--- /dev/null
+++ 
b/integration-tests/explicit-transaction-context-tests/src/test/java/org/apache/servicecomb/pack/integration/tests/explicitcontext/GreetingController.java
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.pack.integration.tests.explicitcontext;
+
+import org.apache.servicecomb.pack.omega.context.OmegaContext;
+import org.apache.servicecomb.pack.omega.context.annotations.SagaStart;
+import org.apache.servicecomb.pack.omega.transaction.annotations.Compensable;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.client.RestTemplate;
+
+@Controller
+@RequestMapping("/")
+public class GreetingController {
+
+  static final String TRESPASSER = "trespasser";
+
+  private final GreetingService greetingService;
+
+  private final RestTemplate restTemplate;
+
+  private final OmegaContext omegaContext;
+
+
+  @Autowired
+  public GreetingController(GreetingService greetingService, RestTemplate 
restTemplate, OmegaContext omegaContext) {
+    this.greetingService = greetingService;
+    this.restTemplate = restTemplate;
+    this.omegaContext = omegaContext;
+  }
+
+  @SagaStart
+  @GetMapping("/greet")
+  ResponseEntity<String> greet(@RequestParam String name) {
+
+    HttpEntity transactionContext = transactionContextRequestBody();
+    String greetings = greetingService.greet(name);
+
+    if (!TRESPASSER.equals(name)) {
+      String bonjour = restTemplate
+          .postForObject("/bonjour?name={name}", transactionContext, 
String.class, name);
+      return ResponseEntity.ok(greetings + "; " + bonjour);
+    }
+
+    String rude = restTemplate
+        .postForObject("/rude?name={name}", transactionContext, String.class, 
name);
+
+    return ResponseEntity.ok(greetings + "; " + rude);
+  }
+
+  @PostMapping(value = "/bonjour", consumes = 
MediaType.APPLICATION_JSON_UTF8_VALUE)
+  ResponseEntity<String> bonjour(@RequestParam String name, @RequestBody 
TransactionContextDto transactionContext) {
+    return ResponseEntity.ok(greetingService.bonjour(name, 
transactionContext.convertBack()));
+  }
+
+  @PostMapping(value = "/rude", consumes = 
MediaType.APPLICATION_JSON_UTF8_VALUE)
+  ResponseEntity<String> rude(@RequestParam String name, @RequestBody 
TransactionContextDto transactionContext) {
+    return ResponseEntity.ok(greetingService.beingRude(name, 
transactionContext.convertBack()));
+  }
+
+  @SagaStart
+  @Compensable(compensationMethod = "goodNight")
+  @GetMapping("/goodMorning")
+  ResponseEntity<String> goodMorning(@RequestParam String name) {
+    String bonjour = restTemplate
+        .postForObject("/bonjour?name={name}", 
transactionContextRequestBody(), String.class,
+            name);
+    return ResponseEntity.ok("Good morning, " + bonjour);
+  }
+
+  ResponseEntity<String> goodNight(@RequestParam String name) {
+    return ResponseEntity.ok("Good night, " + name);
+  }
+
+  @SagaStart
+  @GetMapping("/open")
+  ResponseEntity<String> open(@RequestParam String name, @RequestParam int 
retries) {
+
+    String greetings = greetingService.greet(name);
+    String status = greetingService.open(name, retries, 
omegaContext.getTransactionContext());
+    return ResponseEntity.ok(greetings + "; " + status);
+  }
+
+  private HttpEntity transactionContextRequestBody() {
+    HttpHeaders headers = new HttpHeaders();
+    headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
+
+    HttpEntity transactionContext = new HttpEntity<>(
+        TransactionContextDto.convert(omegaContext.getTransactionContext()),
+        headers
+    );
+
+    return transactionContext;
+  }
+}
diff --git 
a/integration-tests/explicit-transaction-context-tests/src/test/java/org/apache/servicecomb/pack/integration/tests/explicitcontext/GreetingService.java
 
b/integration-tests/explicit-transaction-context-tests/src/test/java/org/apache/servicecomb/pack/integration/tests/explicitcontext/GreetingService.java
new file mode 100644
index 0000000..c2dd6c5
--- /dev/null
+++ 
b/integration-tests/explicit-transaction-context-tests/src/test/java/org/apache/servicecomb/pack/integration/tests/explicitcontext/GreetingService.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.pack.integration.tests.explicitcontext;
+
+import java.util.Queue;
+
+import org.apache.servicecomb.pack.omega.context.TransactionContext;
+import org.apache.servicecomb.pack.omega.transaction.annotations.Compensable;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+@Service
+class GreetingService  {
+  private final Queue<String> compensated;
+
+  private final int MAX_COUNT = 3;
+  private int failedCount = 1;
+
+  @Autowired
+  GreetingService(Queue<String> compensated) {
+    this.compensated = compensated;
+  }
+
+  @Compensable(compensationMethod = "goodbye")
+  String greet(String name) {
+    return "Greetings, " + name;
+  }
+
+  String goodbye(String name) {
+    return appendMessage("Goodbye, " + name);
+  }
+
+  @Compensable(compensationMethod = "auRevoir")
+  String bonjour(String name, TransactionContext transactionContext) {
+    return "Bonjour, " + name;
+  }
+
+  String auRevoir(String name, TransactionContext transactionContext) {
+    return appendMessage("Au revoir, " + name);
+  }
+
+  @Compensable(compensationMethod = "apologize")
+  String beingRude(String name, TransactionContext transactionContext) {
+    throw new IllegalStateException("You know where the door is, " + name);
+  }
+
+  String apologize(String name, TransactionContext transactionContext) {
+    return appendMessage("My bad, please take the window instead, " + name);
+  }
+
+  @Compensable(retries = MAX_COUNT, compensationMethod = "close")
+  String open(String name, int retries, TransactionContext transactionContext) 
{
+    if (failedCount < retries) {
+      failedCount += 1;
+      throw new IllegalStateException("You know when the zoo opens, " + name);
+    }
+    resetCount();
+    return "Welcome to visit the zoo, " + name;
+  }
+
+  String close(String name, int retries, TransactionContext 
transactionContext) {
+    resetCount();
+    return appendMessage("Sorry, the zoo has already closed, " + name);
+  }
+
+  private String appendMessage(String message) {
+    compensated.add(message);
+    return message;
+  }
+
+  public void resetCount() {
+    this.failedCount = 1;
+  }
+}
diff --git 
a/integration-tests/explicit-transaction-context-tests/src/test/java/org/apache/servicecomb/pack/integration/tests/explicitcontext/TransactionContextDto.java
 
b/integration-tests/explicit-transaction-context-tests/src/test/java/org/apache/servicecomb/pack/integration/tests/explicitcontext/TransactionContextDto.java
new file mode 100644
index 0000000..18f1a71
--- /dev/null
+++ 
b/integration-tests/explicit-transaction-context-tests/src/test/java/org/apache/servicecomb/pack/integration/tests/explicitcontext/TransactionContextDto.java
@@ -0,0 +1,36 @@
+package org.apache.servicecomb.pack.integration.tests.explicitcontext;
+
+import org.apache.servicecomb.pack.omega.context.TransactionContext;
+
+public class TransactionContextDto {
+
+  private String globalTxId;
+  private String localTxId;
+
+  public String getGlobalTxId() {
+    return globalTxId;
+  }
+
+  public void setGlobalTxId(String globalTxId) {
+    this.globalTxId = globalTxId;
+  }
+
+  public String getLocalTxId() {
+    return localTxId;
+  }
+
+  public void setLocalTxId(String localTxId) {
+    this.localTxId = localTxId;
+  }
+
+  public TransactionContext convertBack() {
+    return new TransactionContext(globalTxId, localTxId);
+  }
+
+  public static TransactionContextDto convert(TransactionContext 
transactionContext) {
+    TransactionContextDto transactionContextDto = new TransactionContextDto();
+    transactionContextDto.setGlobalTxId(transactionContext.globalTxId());
+    transactionContextDto.setLocalTxId(transactionContext.localTxId());
+    return transactionContextDto;
+  }
+}
diff --git 
a/integration-tests/explicit-transaction-context-tests/src/test/java/org/apache/servicecomb/pack/integration/tests/explicitcontext/TxEventEnvelopeRepository.java
 
b/integration-tests/explicit-transaction-context-tests/src/test/java/org/apache/servicecomb/pack/integration/tests/explicitcontext/TxEventEnvelopeRepository.java
new file mode 100644
index 0000000..3806396
--- /dev/null
+++ 
b/integration-tests/explicit-transaction-context-tests/src/test/java/org/apache/servicecomb/pack/integration/tests/explicitcontext/TxEventEnvelopeRepository.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.servicecomb.pack.integration.tests.explicitcontext;
+
+import java.util.List;
+
+import org.apache.servicecomb.pack.alpha.core.TxEvent;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.CrudRepository;
+
+public interface TxEventEnvelopeRepository extends CrudRepository<TxEvent, 
Long> {
+  List<TxEvent> findByGlobalTxIdOrderByCreationTime(String globalTxId);
+
+  @Query("SELECT DISTINCT(e.globalTxId) from TxEvent e order by e.creationTime 
desc")
+  List<String> findDistinctGlobalTxId();
+}
diff --git 
a/integration-tests/explicit-transaction-context-tests/src/test/resources/application.yaml
 
b/integration-tests/explicit-transaction-context-tests/src/test/resources/application.yaml
new file mode 100644
index 0000000..34b72c3
--- /dev/null
+++ 
b/integration-tests/explicit-transaction-context-tests/src/test/resources/application.yaml
@@ -0,0 +1,21 @@
+## ---------------------------------------------------------------------------
+## Licensed to the Apache Software Foundation (ASF) under one or more
+## contributor license agreements.  See the NOTICE file distributed with
+## this work for additional information regarding copyright ownership.
+## The ASF licenses this file to You under the Apache License, Version 2.0
+## (the "License"); you may not use this file except in compliance with
+## the License.  You may obtain a copy of the License at
+##
+##      http://www.apache.org/licenses/LICENSE-2.0
+##
+## Unless required by applicable law or agreed to in writing, software
+## distributed under the License is distributed on an "AS IS" BASIS,
+## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+## See the License for the specific language governing permissions and
+## limitations under the License.
+## ---------------------------------------------------------------------------
+spring:
+  datasource:
+    username: saga
+    password: password
+    driver-class-name: org.postgresql.Driver
diff --git 
a/integration-tests/explicit-transaction-context-tests/src/test/resources/log4j2.xml
 
b/integration-tests/explicit-transaction-context-tests/src/test/resources/log4j2.xml
new file mode 100644
index 0000000..cae04cb
--- /dev/null
+++ 
b/integration-tests/explicit-transaction-context-tests/src/test/resources/log4j2.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF licenses this file to You under the Apache License, Version 2.0
+  ~ (the "License"); you may not use this file except in compliance with
+  ~ the License.  You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<Configuration status="WARN">
+  <Appenders>
+    <Console name="Console" target="SYSTEM_OUT">
+      <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - 
%msg%n"/>
+    </Console>
+  </Appenders>
+  <Loggers>
+    <AsyncRoot level="info">
+      <AppenderRef ref="Console"/>
+    </AsyncRoot>
+  </Loggers>
+</Configuration>
diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml
index 3651e69..c268c43 100644
--- a/integration-tests/pom.xml
+++ b/integration-tests/pom.xml
@@ -32,6 +32,7 @@
   <modules>
     <module>coverage-aggregate</module>
     <module>pack-tests</module>
+    <module>explicit-transaction-context-tests</module>
   </modules>
 
   <build>

Reply via email to