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>